-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
EZP-28841: Content On the Fly v2 #55
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import React, { Component } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import { loadLocation } from '../../services/universal.discovery.service'; | ||
|
||
import './css/content.creator.component.css'; | ||
|
||
export default class ContentCreatorComponent extends Component { | ||
constructor(props) { | ||
super(props); | ||
|
||
this.handleIframeLoad = this.handleIframeLoad.bind(this); | ||
this.handlePublish = this.handlePublish.bind(this); | ||
|
||
this.state = { | ||
iframeLoading: true | ||
}; | ||
} | ||
|
||
handlePublish() { | ||
this.iframe.contentWindow.onbeforeunload = () => {}; | ||
this.iframe.contentWindow.document.body.querySelector('#ezrepoforms_content_edit_publish').click(); | ||
} | ||
|
||
handleIframeLoad() { | ||
const locationId = this.iframe.contentWindow.document.querySelector('meta[name="LocationID"]'); | ||
const iframeUrl = this.generateIframeUrl(); | ||
|
||
if (this.iframe.contentWindow.location.pathname !== iframeUrl && !locationId) { | ||
this.iframe.setAttribute('src', iframeUrl); | ||
|
||
return; | ||
} | ||
|
||
if (locationId) { | ||
this.loadLocationInfo(locationId.content); | ||
} else { | ||
this.setState(state => Object.assign({}, state, { iframeLoading: false })); | ||
|
||
this.iframe.contentWindow.onbeforeunload = () => { | ||
return ''; | ||
}; | ||
this.iframe.contentWindow.onunload = () => { | ||
this.setState(state => Object.assign({}, state, { iframeLoading: true })); | ||
}; | ||
} | ||
} | ||
|
||
loadLocationInfo(locationId) { | ||
const {loadLocation, handlePublish, restInfo} = this.props; | ||
const promise = new Promise(resolve => loadLocation( | ||
Object.assign({}, restInfo, { locationId }), | ||
resolve | ||
)); | ||
|
||
promise | ||
.then((response) => { | ||
handlePublish(response.View.Result.searchHits.searchHit[0].value.Location); | ||
}); | ||
} | ||
|
||
generateIframeUrl() { | ||
const {selectedLocationId, selectedLanguage, selectedContentType} = this.props; | ||
|
||
return window.Routing.generate('ezplatform.content_on_the_fly.create', { | ||
locationId: selectedLocationId, | ||
languageCode: selectedLanguage.languageCode, | ||
contentTypeIdentifier: selectedContentType.identifier | ||
}); | ||
} | ||
|
||
/** | ||
* Renders a loading state spinner | ||
* | ||
* @method renderLoadingSpinner | ||
* @returns {Element} | ||
* @memberof FinderTreeLeafComponent | ||
*/ | ||
renderLoadingSpinner() { | ||
if (!this.state.iframeLoading) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<svg className="ez-icon ez-spin ez-icon-x2 ez-icon-spinner"> | ||
<use xlinkHref="/bundles/ezplatformadminui/img/ez-icons.svg#spinner"></use> | ||
</svg> | ||
); | ||
} | ||
|
||
render() { | ||
const {labels, selectedContentType, selectedLanguage, maxHeight, onCancel} = this.props; | ||
const title = labels.contentOnTheFly.creatingContent | ||
.replace('{contentType}', selectedContentType.name) | ||
.replace('{language}', selectedLanguage.name); | ||
const iframeUrl = this.generateIframeUrl(); | ||
const contentClass = this.state.iframeLoading ? 'm-ud__content is-loading' : 'm-ud__content'; | ||
|
||
return ( | ||
<div className="m-ud__wrapper"> | ||
<div className="m-ud c-content-creator"> | ||
<h1 className="m-ud__title">{title}</h1> | ||
<div className="m-ud__content-wrapper"> | ||
<div className={contentClass} ref={ref => this._refContentContainer = ref}> | ||
{this.renderLoadingSpinner()} | ||
<iframe | ||
src={iframeUrl} | ||
ref={ref => this.iframe = ref} | ||
className="c-content-creator__iframe" | ||
onLoad={this.handleIframeLoad} | ||
style={{height:`${maxHeight + 32}px`}} /> | ||
</div> | ||
<div className="m-ud__actions"> | ||
<div className="m-ud__btns"> | ||
<button className="m-ud__action--cancel" onClick={onCancel}>{labels.udw.cancel}</button> | ||
<button className="m-ud__action--publish" onClick={this.handlePublish}>{labels.contentOnTheFly.publish}</button> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
ContentCreatorComponent.propTypes = { | ||
maxHeight: PropTypes.number.isRequired, | ||
labels: PropTypes.object.isRequired, | ||
selectedLanguage: PropTypes.object.isRequired, | ||
selectedContentType: PropTypes.object.isRequired, | ||
selectedLocationId: PropTypes.number.isRequired, | ||
onCancel: PropTypes.func.isRequired, | ||
handlePublish: PropTypes.func.isRequired, | ||
loadLocation: PropTypes.func, | ||
restInfo: PropTypes.object.isRequired | ||
}; | ||
|
||
ContentCreatorComponent.defaultProps = { | ||
loadLocation | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
.c-content-creator .m-ud__content-wrapper { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we agreed to move the CSS code directly into SASS files of a bundle. We are not importing CSS into the components anymore. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought this was applied only to Page Builder. If we move CSS to admin-ui sass we would always need to make two PR when changing something in modules |
||
padding: 0 0 16px 0; | ||
grid-template-rows: 1fr 64px; | ||
grid-template-areas: 'main' 'footer'; | ||
} | ||
|
||
.c-content-creator .m-ud__content { | ||
padding: 0 | ||
} | ||
|
||
.c-content-creator .m-ud__actions { | ||
padding-right: 16px; | ||
} | ||
|
||
.c-content-creator .ez-icon-spinner { | ||
position: absolute; | ||
top: 50%; | ||
left: 50%; | ||
transform: translate(-50%, -50%); | ||
} | ||
|
||
.c-content-creator__iframe { | ||
width: 100%; | ||
border: none; | ||
} | ||
|
||
.m-ud__action--publish { | ||
font-weight: 700; | ||
background: #f15a10; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recently, I've found out that we're exposing the CSS variables in the |
||
} | ||
|
||
.m-ud__action--publish:hover, | ||
.m-ud__action--publish:focus { | ||
background: #cb3400; | ||
} | ||
|
||
.m-ud__content { | ||
position: relative; | ||
} | ||
|
||
.is-loading:before { | ||
content: ""; | ||
position: absolute; | ||
top: 0; | ||
left: 0; | ||
right: 0; | ||
bottom: 0; | ||
background-color: #e3e3e3; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import React, { Component } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import './css/create.choose.content.type.component.css'; | ||
|
||
export default class ChooseContentTypeComponent extends Component { | ||
constructor(props) { | ||
super(props); | ||
|
||
this._filterTimeout = null; | ||
|
||
this.updateFilterQuery = this.updateFilterQuery.bind(this); | ||
this.renderGroup = this.renderGroup.bind(this); | ||
this.renderItem = this.renderItem.bind(this); | ||
|
||
this.state = { | ||
selected: {}, | ||
filterQuery: '' | ||
}; | ||
} | ||
|
||
updateSelectedItem(item) { | ||
this.props.onContentTypeSelected(item); | ||
|
||
this.setState(state => Object.assign({}, state, {selected: item})); | ||
} | ||
|
||
updateFilterQuery(event) { | ||
const filterQuery = event.target.value.toLowerCase(); | ||
|
||
window.clearTimeout(this._filterTimeout); | ||
|
||
this._filterTimeout = window.setTimeout(() => { | ||
this.setState(state => Object.assign({}, state, { filterQuery })); | ||
}, 200); | ||
} | ||
|
||
renderItem(item, index) { | ||
const attrs = { | ||
className: 'c-choose-content-type__group-item', | ||
onClick: this.updateSelectedItem.bind(this, item), | ||
key: index | ||
}; | ||
|
||
if (this.state.selected.identifier === item.identifier) { | ||
attrs.className = `${attrs.className} is-selected`; | ||
} | ||
|
||
if (this.state.filterQuery && !item.name.toLowerCase().includes(this.state.filterQuery)) { | ||
attrs.hidden = true; | ||
} | ||
|
||
return ( | ||
<div {...attrs}> | ||
{item.name} | ||
</div> | ||
); | ||
} | ||
|
||
renderGroup(groupName, index) { | ||
const items = this.props.contentTypes[groupName]; | ||
const groupAttrs = {}; | ||
|
||
if (this.state.filterQuery && items.every(item => !item.name.toLowerCase().includes(this.state.filterQuery))) { | ||
groupAttrs.hidden = true; | ||
} | ||
|
||
return ( | ||
<div className="c-choose-content-type__group" key={index}> | ||
<div className="c-choose-content-type__group-name" {...groupAttrs}> | ||
{groupName} | ||
</div> | ||
{items.map(this.renderItem)} | ||
</div> | ||
); | ||
} | ||
|
||
render() { | ||
const {labels, maxHeight, contentTypes} = this.props; | ||
|
||
return ( | ||
<div className="c-choose-content-type"> | ||
<p className="c-choose-content-type__title">{labels.contentOnTheFly.selectContentType}</p> | ||
<div className="c-choose-content-type__list-wrapper"> | ||
<input className="form-control" type="text" placeholder={labels.contentOnTheFly.typeToRefine} onChange={this.updateFilterQuery} /> | ||
<div className="c-choose-content-type__list" style={{maxHeight:`${maxHeight - 232}px`}}> | ||
{Object.keys(contentTypes).map(this.renderGroup)} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
ChooseContentTypeComponent.propTypes = { | ||
maxHeight: PropTypes.number.isRequired, | ||
labels: PropTypes.object.isRequired, | ||
contentTypes: PropTypes.object.isRequired, | ||
onContentTypeSelected: PropTypes.func.isRequired | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import React, { Component } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import './css/create.choose.language.component.css'; | ||
|
||
export default class ChooseLanguageComponent extends Component { | ||
constructor(props) { | ||
super(props); | ||
|
||
this.updateSelection = this.updateSelection.bind(this); | ||
this.renderOption = this.renderOption.bind(this); | ||
|
||
this.state = { | ||
selectedLanguage: props.languages[0] | ||
}; | ||
} | ||
|
||
updateSelection(event) { | ||
const languageCode = event.target.value; | ||
const selectedLanguage = this.props.languages.find(language => language.languageCode === languageCode); | ||
|
||
this.props.onLanguageSelected(selectedLanguage); | ||
|
||
this.setState(state => Object.assign({}, state, { selectedLanguage })); | ||
} | ||
|
||
renderOption(item, index) { | ||
const attrs = { | ||
key: index, | ||
value: item.languageCode | ||
}; | ||
|
||
if (item.languageCode === this.props.forcedLanguage) { | ||
attrs.selected = true; | ||
} | ||
|
||
return ( | ||
<option {...attrs}>{item.name}</option> | ||
); | ||
} | ||
|
||
render() { | ||
const selectAttrs = { | ||
className: 'form-control', | ||
onChange: this.updateSelection | ||
}; | ||
|
||
if (this.props.forcedLanguage) { | ||
selectAttrs.disabled = true; | ||
} | ||
|
||
return ( | ||
<div className="c-choose-language"> | ||
<p className="c-choose-language__title">{this.props.labels.contentOnTheFly.selectLanguage}</p> | ||
<div className="c-choose-lagauge__select-wrapper"> | ||
<select {...selectAttrs}> | ||
{this.props.languages.map(this.renderOption)} | ||
</select> | ||
</div> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
ChooseLanguageComponent.propTypes = { | ||
maxHeight: PropTypes.number.isRequired, | ||
labels: PropTypes.object.isRequired, | ||
languages: PropTypes.array.isRequired, | ||
onLanguageSelected: PropTypes.func.isRequired, | ||
forcedLanguage: PropTypes.string.isRequired | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're not using props to initialize state, then you can omit the
props
variable in theconstructor
method.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can but I don't think you should.
Class components should always call the base constructor with props.
from React doc: https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-classThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is recommendation, right. We can follow this rule. It's especially useful when you're extending your own component classes. In this case, when you extend base component class it's not that relevant. Because any kind of props won't affect the way the base class works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it will not cause any problems when extending
React.Component
for now, but if they have this in their doc they may require this at some point in the future.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, let's keep it then. From the currect perspective, it brings no value now, but it's a recommended by React doc.