diff --git a/ashes/src/components/content-types/content-type-form.jsx b/ashes/src/components/content-types/content-type-form.jsx index 111ac3ab27..4fd0d3aa7b 100644 --- a/ashes/src/components/content-types/content-type-form.jsx +++ b/ashes/src/components/content-types/content-type-form.jsx @@ -5,26 +5,41 @@ import _ from 'lodash'; import React, { Element } from 'react'; import { autobind } from 'core-decorators'; import { assoc } from 'sprout-data'; +import classNames from 'classnames'; import styles from '../object-page/object-details.css'; import ObjectDetails from '../object-page/object-details'; +import Modal from './modal'; +import Form from './form'; import { FormField } from '../forms'; import RadioButton from 'components/core/radio-button'; -import SelectCustomerGroups from '../customers-groups/select-groups'; import DiscountAttrs from './discount-attrs'; import offers from './offers'; import qualifiers from './qualifiers'; -import { setDiscountAttr } from 'paragons/promotion'; +import ContentBox from 'components/content-box/content-box'; +import { Button } from 'components/core/button'; + +import { + addContentTypeObject, + updateContentTypeObject, + removeContentTypeObject +} from 'paragons/content-type'; import { setObjectAttr, omitObjectAttr } from 'paragons/object'; import { customerGroups } from 'paragons/object-types'; const layout = require('./layout.json'); -export default class PromotionForm extends ObjectDetails { +export default class ContentTypeForm extends ObjectDetails { layout = layout; + state = { + tabs: {}, + sections: {}, + properties: {}, + } + renderApplyType() { const promotion = this.props.object; return ( @@ -171,18 +186,289 @@ export default class PromotionForm extends ObjectDetails { this.props.onUpdateObject(newPromotion); } - renderCustomers(): Element<*> { - const promotion = this.props.object; + @autobind + setIsVisible(key, value) { + return id => { + this.setState({ + [key]: { + ...this.state[key], + showModal: value, + id, + } + }); + }; + } + + @autobind + onSave(key, id) { + return attributes => { + const { object: contentType } = this.props; + if (id > 0) { + this.props.onUpdateObject(updateContentTypeObject(contentType, key, id, attributes)); + return id; + } else { + const object = addContentTypeObject(contentType, key, attributes); + this.props.onUpdateObject(object); + return _.last(object[key].allIds); + } + }; + } + + @autobind + onDelete(key, id) { + return () => { + const { object: contentType } = this.props; + this.props.onUpdateObject(removeContentTypeObject(contentType, key, id)); + }; + } + + @autobind + onCancel(key) { + return this.setIsVisible(key, false); + } + + formData(key: string) { + const schemes = { + tabs: { + fieldsToRender: ['title'], + schema: { + "type": "object", + "required": [ + "title" + ], + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string", + "minLength": 1 + }, + "custom-properties": { + "title": "Custom Properties can be added to this section", + "type": "boolean" + } + } + } + }, + sections: { + fieldsToRender: ['title', 'slug', 'custom-properties'], + schema: { + "type": "object", + "required": [ + "title" + ], + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string", + "minLength": 1 + }, + "custom-properties": { + "title": "Custom Properties can be added to this section", + "type": "boolean" + } + } + } + }, + properties: { + fieldsToRender: ['title', 'slug'], + schema: { + "type": "object", + "required": [ + "title" + ], + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string", + "minLength": 1 + }, + "custom-properties": { + "title": "Custom Properties can be added to this section", + "type": "boolean" + } + } + } + } + }; + + return _.get(schemes, key, {}); + } + + modal({ key, title }): Element<*> { + const formData = this.formData(key); + const { object: contentType } = this.props; + const { id, showModal } = this.state[key]; + + return ( + + ); + } + + form({ key }): Element<*> { + const formData = this.formData(key); + const { object: contentType } = this.props; + const { id, showModal } = this.state[key]; + + if (!showModal) return null; + + return ( +
+ ); + } + + + column({ key, title, children, footer, emptyBody }): Element<*> { + const isEmpty = _.isEmpty(children); + + const bodyClassName = classNames( + styles['column-body'], + {[styles['column-body-empty']]: isEmpty} + ); + + return ( + + {footer} + + )} + indentContent={false} + > + {children} + {isEmpty && emptyBody} + {key === 'properties' ? null : this.modal({ key, title })} + + ); + } + + renderColumns(): Element<*> { + const { object: contentType } = this.props; return ( -
-
Customers
- +
+ {this.column( + { + key: 'tabs', + title: 'Tab', + emptyBody: ( + + Add a tab! + + ), + footer: ( + + ), + children: _.map(contentType.tabs.byId, (tab) => ) + } + )} + {this.column( + { + key: 'sections', + title: 'Section', + emptyBody: ( + + Add a section! + + ), + footer: ( + + ), + children: _.map(contentType.sections.byId, (section, id) => ( +
+ {section.attributes.title.v} + +
+ )) + } + )} + {this.column( + { + key: 'properties', + title: 'Properties', + emptyBody: ( + + Add a property! + + ), + footer: ( + + ), + children: _.map(this.props.object.properties.byId, (property, id) => ( + + )) + } + )} + {this.column( + { + key: 'properties', + title: 'Property Settings', + footer: this.state.properties.showModal ? ( + + ) : null, + children: this.form( + { + key: 'properties', + } + ) + } + )}
); } diff --git a/ashes/src/components/content-types/content-type-page.jsx b/ashes/src/components/content-types/content-type-page.jsx index b9f9367b81..673f0a1e14 100644 --- a/ashes/src/components/content-types/content-type-page.jsx +++ b/ashes/src/components/content-types/content-type-page.jsx @@ -13,6 +13,102 @@ import { transitionTo } from 'browserHistory'; import * as ContentTypeActions from 'modules/content-types/details'; class ContentTypePage extends ObjectPage { + componentDidMount() { + this.props.actions.clearFetchErrors(); + // this.props.actions.fetchSchema(this.props.namespace, true); + this.props.actions.fetchSchema('json', + [ + { + "name": "content-type", + "kind": "contentType", + "schema": { + "type": "object", + "title": "Content Type", + "$schema": "http:\/\/json-schema.org\/draft-04\/schema#", + "properties": { + "discounts": { + "type": "array", + "items": { + "$ref": "#\/definitions\/discount" + } + }, + "attributes": { + "type": "object", + "required": [ + "title" + ], + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": [ + "string", + "null" + ], + }, + "slug": { + "type": "string", + "minLength": 1 + } + } + } + }, + "definitions": { + "discount": { + "type": "object", + "title": "Discount", + "$schema": "http:\/\/json-schema.org\/draft-04\/schema#", + "properties": { + "id": { + "type": "number" + }, + "attributes": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "offer": { + "type": "object" + }, + "title": { + "type": "string" + }, + "qualifier": { + "type": "object" + }, + "description": { + "type": "string", + "widget": "richText" + } + } + } + } + } + } + } + } + ] + ); + + if (this.isNew) { + this.props.actions.newEntity(); + } else { + this.fetchEntity() + .then(({ payload }) => { + if (isArchived(payload)) this.transitionToList(); + }); + } + + this.props.actions.fetchAmazonStatus() + .catch(() => {}); // pass + } + save(): ?Promise<*> { let isNew = this.isNew; let willBePromo = super.save(); diff --git a/ashes/src/components/content-types/form.js b/ashes/src/components/content-types/form.js new file mode 100644 index 0000000000..fd31748806 --- /dev/null +++ b/ashes/src/components/content-types/form.js @@ -0,0 +1,70 @@ +/* @flow */ + +// libs +import React, { Component, Element } from 'react'; +import { autobind } from 'core-decorators'; + +// components +import ObjectFormInner from 'components/object-form/object-form-inner'; +import SaveCancel from 'components/core/save-cancel'; + +type Props = { + title: string, + isVisible: boolean, + schema: object, + object: object, + fieldsToRender: Array, + onCancel: Function, + onUpdateObject: Function, +}; + +type State = { + object: object, +}; + +export default class MyForm extends Component { + props: Props; + + state: State = { + object: {}, + }; + + @autobind + handleChange(object: object) { + this.setState({ object }); + } + + @autobind + handleSave() { + this.props.onSave(this.state.object); + this.props.onCancel(); + this.setState({ object: {} }); + } + + get footer() { + const saveDisabled = false; + + return ; + } + + render() { + const props = this.props; + + const attributes = { + ...props.object, + ...this.state.object, + }; + + return ( +
+ + {this.footer} +
+ ); + } +} diff --git a/ashes/src/components/content-types/layout.json b/ashes/src/components/content-types/layout.json index cfe43deb50..4605ff4710 100644 --- a/ashes/src/components/content-types/layout.json +++ b/ashes/src/components/content-types/layout.json @@ -5,11 +5,13 @@ "title": "General", "content": [ { - "canAddProperty": true, + "canAddProperty": false, "includeRest": true, "type": "fields", "value": [ - "name" + "title", + "description", + "slug" ], "omit": [ "storefrontName", @@ -25,46 +27,7 @@ ] }, { - "type": "group", - "title": "Discounts", - "content": [ - { - "type": "discounts" - }, - { - "type": "customers" - } - ] - }, - { - "type": "group", - "title": "Usage Rules", - "content": [ - { - "type": "usage-rules" - } - ] - }, - { - "type": "group", - "showIfNew": true, - "title": "Apply Type", - "content": [ - { - "type": "apply-type" - } - ] - } - ], - "aside": [ - { - "type": "state" - }, - { - "type": "tags" - }, - { - "type": "watchers" + "type": "columns" } ] } diff --git a/ashes/src/components/content-types/modal.js b/ashes/src/components/content-types/modal.js new file mode 100644 index 0000000000..5b88ddf184 --- /dev/null +++ b/ashes/src/components/content-types/modal.js @@ -0,0 +1,70 @@ +/* @flow */ + +// libs +import React, { Component, Element } from 'react'; +import { autobind } from 'core-decorators'; + +// components +import Modal from 'components/core/modal'; +import ObjectFormInner from 'components/object-form/object-form-inner'; +import SaveCancel from 'components/core/save-cancel'; + +type Props = { + title: string, + isVisible: boolean, + schema: object, + object: object, + fieldsToRender: Array, + onCancel: Function, + onUpdateObject: Function, +}; + +type State = { + object: object, +}; + +export default class MyModal extends Component { + props: Props; + + state: State = { + object: {}, + }; + + @autobind + handleChange(object: object) { + this.setState({ object }); + } + + @autobind + handleSave() { + this.props.onSave(this.state.object); + this.props.onCancel(); + this.setState({ object: {} }); + } + + get footer() { + const saveDisabled = false; + + return ; + } + + render() { + const props = this.props; + + const attributes = { + ...props.object, + ...this.state.object, + }; + + return ( + + + + ); + } +} diff --git a/ashes/src/components/object-page/object-details.css b/ashes/src/components/object-page/object-details.css index cde4149f47..e813f42ca9 100644 --- a/ashes/src/components/object-page/object-details.css +++ b/ashes/src/components/object-page/object-details.css @@ -6,6 +6,15 @@ flex-wrap: wrap; } +.full-page { + width: 100%; + min-width: 400px; + + & > div { + margin-bottom: 20px; + } +} + .main { width: calc(67% - 1.85%); margin-right: 1.85%; @@ -39,3 +48,45 @@ .customer-groups { margin-top: 20px; } + +.columns { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; +} + +.column { + flex-grow: 1; + margin-top: 20px; + margin-right: 0; + border-left: 0; + + &:first-child { + border-left: 1px solid #d9d9d9; + } +} + +.column-body { + min-height: 600px; +} + +.column-body > * { + width: 100%; +} + +.column-body-empty { + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.column-footer { + padding: 10px; + border-top: 1px solid #d9d9d9; +} + +.column-footer > button { + width: 100%; +} diff --git a/ashes/src/components/object-page/object-details.jsx b/ashes/src/components/object-page/object-details.jsx index a459e5c2e3..0edf3dcd8c 100644 --- a/ashes/src/components/object-page/object-details.jsx +++ b/ashes/src/components/object-page/object-details.jsx @@ -186,6 +186,16 @@ export default class ObjectDetails extends Component { return addKeys(name, section.map(desc => this.renderNode(desc, section))); } + get main() { + if (this.layout.main != null) { + return ( +
+ {this.renderSection('main')} +
+ ); + } + } + get aside() { if (this.layout.aside != null) { return ( @@ -202,7 +212,7 @@ export default class ObjectDetails extends Component { return (
- {this.renderSection('main')} + {this.main}
{this.aside} diff --git a/ashes/src/modules/object-schema.js b/ashes/src/modules/object-schema.js index b94bab28f3..7e6b948a5d 100644 --- a/ashes/src/modules/object-schema.js +++ b/ashes/src/modules/object-schema.js @@ -6,7 +6,12 @@ import Api from 'lib/api'; const _fetchSchema = createAsyncActions( 'fetchSchema', - (kind, id = void 0) => Api.get(`/object/schemas/byKind/${kind}`) + (kind, id = void 0) => { + if (kind === 'json') { + return Promise.resolve(id); + } + return Api.get(`/object/schemas/byKind/${kind}`); + } ); export const saveSchema = createAction('SCHEMA_SAVE', (kind, schema) => [kind, schema]); diff --git a/ashes/src/paragons/content-type.js b/ashes/src/paragons/content-type.js index 8e4d585b8d..e8f47158e8 100644 --- a/ashes/src/paragons/content-type.js +++ b/ashes/src/paragons/content-type.js @@ -1,50 +1,103 @@ import { assoc } from 'sprout-data'; -function addEmptyDiscount(contentType) { - const discount = { - id: null, +let id = 0; +function uniqId() { + return id++; +} + +export function addContentTypeObject(contentType, key, attributes) { + const object = { + id: uniqId(), createdAt: null, - attributes: { - qualifier: { - t: 'qualifier', - v: { - orderAny: {} - } + attributes, + }; + + return { + ...contentType, + [key]: { + ...contentType[key], + byId: { + ...contentType[key].byId, + [object.id]: object, }, - offer: { - t: 'offer', - v: { - orderPercentOff: {} - } - } + allIds: [ + ...contentType[key].allIds, + object.id, + ], }, }; +} - contentType.discounts.push(discount); - return contentType; +export function updateContentTypeObject(contentType, key, id, attributes) { + const object = { + id: id, + createdAt: null, + attributes, + }; + + return { + ...contentType, + [key]: { + ...contentType[key], + byId: { + ...contentType[key].byId, + [object.id]: object, + }, + allIds: [ + ...contentType[key].allIds, + object.id, + ], + }, + }; +} + +export function removeContentTypeObject(contentType, key, id) { + const byId = contentType[key].byId; + delete byId[id]; + return { + ...contentType, + [key]: { + ...contentType[key], + byId, + allIds: contentType[key].allIds.filter(itemId => itemId !== id), + }, + }; +} + +function addEmptyTab(contentType) { + return addContentTypeObject(contentType, 'tabs', { + title: { + t: 'string', + v: 'Details', + }, + }); } export function createEmptyContentType() { const contentType = { id: null, - applyType: 'auto', - isExclusive: true, createdAt: null, attributes: { - storefrontName: { - t: 'richText', - v: 'Storefront name' - }, - customerGroupIds: { - t: 'tock673sjgmqbi5zlfx43o4px6jnxi7absotzjvxwir7jo2v', - v: null, - }, + title: null, + description: null, + slug: null, + }, + tabs: { + byId: {}, + allIds: [], + }, + sections: { + byId: {}, + allIds: [], + }, + properties: { + byId: {}, + allIds: [], }, - discounts: [], }; - return addEmptyDiscount(contentType); + return addEmptyTab(contentType); } export function setDiscountAttr(contentType, label, value) {