UNPKG

@knapsack/app

Version:

Build Design Systems on top of knapsack, by Basalt

525 lines (495 loc) • 16.8 kB
/** * Copyright (C) 2018 Basalt This file is part of Knapsack. Knapsack is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Knapsack is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Knapsack; if not, see <https://www.gnu.org/licenses>. */ import React, { Component } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import arrayMove from 'array-move'; import shortid from 'shortid'; import { Spinner, StatusMessage } from '@knapsack/design-system'; // @todo determine if this is still functioning; upgraded `react-dnd` from v5 => v9 and just updated this import without checking to see if it works since it's been disabled as part of Knapsack v1 => v2 import { DndContext } from 'react-dnd'; import HTML5Backend from 'react-dnd-html5-backend'; import { Query, Mutation } from 'react-apollo'; import gql from 'graphql-tag'; import PageWithSidebar from '../../layouts/page-with-sidebar'; import PlaygroundSlice from './page-builder-slice'; import PageBuilderStartSliceInsert from './page-builder-start-slice-insert'; import PageBuilderSidebar, { SIDEBAR_DEFAULT, SIDEBAR_FORM, SIDEBAR_PATTERNS, } from './page-builder-sidebar'; import { PageBuilderContext } from './page-builder-context'; import './index.scss'; const query = gql` query PageBuilerPages($id: ID) { pageBuilderPage(id: $id) { id title path description slices { id patternId templateId data } } patterns { id templates { id path schema title demoDatas uiSchema } meta { description title uses type hasIcon } } } `; const mutation = gql` mutation setPageBuilderPage($id: ID, $data: JSON) { setPageBuilderPage(id: $id, data: $data) { id title path slices { id patternId data } } } `; class Playground extends Component { static contextType = PageBuilderContext; constructor(props) { super(props); this.handleAddSlice = this.handleAddSlice.bind(this); this.moveSlice = this.moveSlice.bind(this); this.moveSliceUp = this.moveSliceUp.bind(this); this.moveSliceDown = this.moveSliceDown.bind(this); this.deleteSlice = this.deleteSlice.bind(this); this.handleHideEditForm = this.handleHideEditForm.bind(this); this.save = this.save.bind(this); this.getTemplateFromPatternId = this.getTemplateFromPatternId.bind(this); this.handleEditFormChange = this.handleEditFormChange.bind(this); this.handleClearData = this.handleClearData.bind(this); this.handleCancelAddSlice = this.handleCancelAddSlice.bind(this); this.handleFilterChange = this.handleFilterChange.bind(this); this.handleFilterReset = this.handleFilterReset.bind(this); this.handleMetaFormChange = this.handleMetaFormChange.bind(this); this.handleStartInsertSlice = this.handleStartInsertSlice.bind(this); this.briefHighlight = this.briefHighlight.bind(this); // All state is passed to PageBuilderContext so all children can use it /* eslint-disable react/no-unused-state, react/prop-types */ this.state = { isUserAbleToSave: props.isUserAbleToSave, patterns: [], example: null, slices: null, sidebarContent: SIDEBAR_DEFAULT, editFormInsertionIndex: null, editFormSchema: {}, editFormUiSchema: {}, editFormSliceId: null, filterTerm: '', statusMessage: '', statusType: 'info', hasVisibleControls: true, changeId: null, handleAddSlice: this.handleAddSlice, }; /* eslint-enable react/no-unused-state, react/prop-types */ } /** * @param {string} patternId - ID of Pattern, i.e. `media-block` * @param {string} [templateId] - ID of Template * @return {import('../../../schemas/patterns').KnapsackPatternTemplate} - Pattern template */ getTemplateFromPatternId(patternId, templateId) { const pattern = this.state.patterns.find(p => p.id === patternId); if (templateId) { return pattern.templates.find(t => t.id === templateId); } return pattern.templates[0]; } handleFilterReset() { this.setState({ filterTerm: '', }); } /** * Save Whole Example page to server via GraphQL mutation * @param {Function} setPageBuilderPage - The function provided by the GraphQL <Mutation> component * @return {Promise<object>|null} - Returns the structued object defined by the mutation. */ async save(setPageBuilderPage) { if (!this.props.isUserAbleToSave) { this.setState({ statusMessage: 'Updating and saving data has been disabled through feature flags. This page builder example cannot be saved at this time.', statusType: 'error', }); setTimeout(() => { this.setState({ statusMessage: '', statusType: 'info', }); }, 6000); return null; } const results = await setPageBuilderPage({ variables: { id: this.props.id, data: { ...this.state.example, slices: this.state.slices }, }, }); if (results.data) { this.setState({ statusMessage: 'Page Builder example saved', }); setTimeout(() => { this.setState({ statusMessage: '', }); }, 3000); } return results; } handleHideEditForm() { this.setState({ editFormSliceId: null, sidebarContent: SIDEBAR_DEFAULT, changeId: null, }); } /** * Move Slice * @param {number} fromIndex - Move this item * @param {number} toIndex - To this index * @param {string} id - Slice Id * @return {void} - sets state */ moveSlice(fromIndex, toIndex, id) { this.setState(prevState => ({ slices: arrayMove(prevState.slices, fromIndex, toIndex), editFormInsertionIndex: null, })); this.briefHighlight(id); } /** * @param {number} index - Index of item in `this.state.slices` to move up * @param {number} id - ID of item in `this.state.slices` to move up * @return {void} - sets state */ moveSliceUp(index, id) { this.setState(prevState => ({ slices: arrayMove(prevState.slices, index, index - 1), editFormInsertionIndex: null, sidebarContent: SIDEBAR_DEFAULT, editFormSliceId: null, })); this.briefHighlight(id); } /** * @param {number} index - Index of item in `this.state.slices` to move down * @param {number} id - ID of item in `this.state.slices` to move down * @return {void} - sets state */ moveSliceDown(index, id) { this.setState(prevState => ({ slices: arrayMove(prevState.slices, index, index + 1), editFormInsertionIndex: null, sidebarContent: SIDEBAR_DEFAULT, editFormSliceId: null, })); this.briefHighlight(id); } deleteSlice(sliceId) { this.setState(prevState => ({ slices: prevState.slices.filter(slice => slice.id !== sliceId), sidebarContent: SIDEBAR_DEFAULT, editFormInsertionIndex: null, })); this.setState({ statusMessage: 'Slice Deleted', }); setTimeout(() => { this.setState({ statusMessage: '', }); }, 3000); } briefHighlight(sliceId) { this.setState({ changeId: sliceId, }); setTimeout(() => { this.setState({ changeId: null, }); }, 1000); } /** * @param {string} patternId - unique id * @param {string} templateId - unique id * @returns {void} - sets state */ handleAddSlice(patternId, templateId) { const { schema, uiSchema, demoDatas = [{}], } = this.getTemplateFromPatternId(patternId, templateId); const id = shortid.generate(); this.setState(prevState => { prevState.slices.splice(prevState.editFormInsertionIndex, 0, { id, patternId, templateId, data: demoDatas[0], }); return { slices: prevState.slices, editFormSliceId: id, editFormSchema: schema, editFormUiSchema: uiSchema, sidebarContent: SIDEBAR_FORM, editFormInsertionIndex: null, }; }); this.briefHighlight(id); this.handleFilterReset(); } handleStartInsertSlice(index) { this.setState({ sidebarContent: SIDEBAR_PATTERNS, editFormInsertionIndex: index, editFormSliceId: null, }); } handleEditFormChange(data) { this.setState(prevState => ({ slices: prevState.slices.map(slice => { if (slice.id === prevState.editFormSliceId) { slice.data = data.formData; } return slice; }), })); } handleClearData() { this.setState(prevState => ({ slices: prevState.slices.map(slice => { if (slice.id === prevState.editFormSliceId) { slice.data = {}; } return slice; }), })); } handleCancelAddSlice() { this.setState({ editFormSliceId: null, editFormInsertionIndex: null, sidebarContent: SIDEBAR_DEFAULT, }); } handleFilterChange(event) { this.setState({ filterTerm: event.target.value }); } handleMetaFormChange(formData) { this.setState(prevState => ({ example: { ...prevState.example, ...formData }, hasVisibleControls: formData.hasVisibleControls, })); } render() { if (!this.state.example) { return ( <Query query={query} variables={{ id: this.props.id }}> {({ loading, error, data }) => { if (loading) return <Spinner />; if (error) return <StatusMessage message={error.message} type="error" />; this.setState({ example: data.pageBuilderPage, slices: data.pageBuilderPage.slices, patterns: data.patterns, }); return null; }} </Query> ); } const { props } = this; const SideBarContent = ( <> <Mutation mutation={mutation} variables={{ id: this.props.id, data: this.state.example, }} > {(setPageBuilderPage, { error }) => ( <> {error && <StatusMessage message={error.message} type="error" />} <PageBuilderSidebar editFormSchema={this.state.editFormSchema} editFormUiSchema={this.state.editFormUiSchema} editFormSliceId={this.state.editFormSliceId} filterTerm={this.state.filterTerm} handleEditFormChange={this.handleEditFormChange} handleClearData={this.handleClearData} handleCancelAddSlice={this.handleCancelAddSlice} handleHideEditForm={this.handleHideEditForm} handleFilterChange={this.handleFilterChange} handleFilterReset={this.handleFilterReset} handleMetaFormChange={this.handleMetaFormChange} handleSave={() => this.save(setPageBuilderPage)} metaFormData={this.state.example} patterns={this.props.patterns} sidebarContent={this.state.sidebarContent} slices={this.state.slices} /> </> )} </Mutation> </> ); return ( <PageBuilderContext.Provider value={this.state}> <PageWithSidebar {...props} sidebar={SideBarContent}> <div className="ks-page-builder-index"> {this.state.hasVisibleControls && ( <> <h4 className="ks-eyebrow">Prototyping Sandbox</h4> <h2>{this.state.example.title}</h2> {this.state.statusMessage && ( <StatusMessage message={this.state.statusMessage} // @ts-ignore type={this.state.statusType} /> )} </> )} <PageBuilderStartSliceInsert onClick={() => this.handleStartInsertSlice(0)} onKeyPress={() => this.handleStartInsertSlice(0)} hasVisibleControls={this.state.hasVisibleControls} isActive={this.state.editFormInsertionIndex === 0} /> {this.state.slices.map((slice, sliceIndex) => { const { templateId, patternId } = slice; const template = this.getTemplateFromPatternId( slice.patternId, templateId, ); if (!slice.patternId && slice.templateId) { return ( <div key={`${slice.id}--fragment`} onKeyPress={() => this.deleteSlice(slice.id)} onClick={() => this.deleteSlice(slice.id)} role="button" aria-label="delete component" tabIndex={0} > <StatusMessage message={`Template for "${slice.patternId}" not found. Click to delete.`} type="warning" /> </div> ); } return ( <React.Fragment key={`${slice.id}--fragment`}> {template && ( <PlaygroundSlice key={slice.id} id={slice.id} index={sliceIndex} templateId={template.id} patternId={patternId} data={slice.data} showEditForm={() => { this.setState({ editFormSliceId: slice.id, editFormSchema: template.schema, editFormUiSchema: template.uiSchema, sidebarContent: SIDEBAR_FORM, editFormInsertionIndex: null, }); this.briefHighlight(slice.id); }} deleteMe={() => this.deleteSlice(slice.id)} moveSlice={(dragIndex, hoverIndex) => { console.log('moving...', { dragIndex, hoverIndex }); this.moveSlice(dragIndex, hoverIndex, slice.id); }} moveUp={() => this.moveSliceUp(sliceIndex, slice.id)} moveDown={() => this.moveSliceDown(sliceIndex, slice.id)} hasVisibleControls={this.state.hasVisibleControls} isBeingEdited={this.state.editFormSliceId === slice.id} isFirst={sliceIndex === 0} isLast={this.state.slices.length - 1 === sliceIndex} isChanged={this.state.changeId === slice.id} /> )} <PageBuilderStartSliceInsert key={`${slice.id}--handleAddSlice`} onClick={() => this.handleStartInsertSlice(sliceIndex + 1)} onKeyPress={() => this.handleStartInsertSlice(sliceIndex + 1) } isActive={ this.state.editFormInsertionIndex === sliceIndex + 1 } hasVisibleControls={this.state.hasVisibleControls} /> </React.Fragment> ); })} </div> </PageWithSidebar> </PageBuilderContext.Provider> ); } } function mapStateToProps({ patternsState, userState }) { return { patterns: patternsState.patterns, isUserAbleToSave: userState.role.permissions.includes('write'), }; } Playground.propTypes = { isUserAbleToSave: PropTypes.bool.isRequired, // context: contextPropTypes.isRequired, patterns: PropTypes.array.isRequired, // eslint-disable-line example: PropTypes.object.isRequired, // eslint-disable-line id: PropTypes.string.isRequired, // @todo save/show playgrounds based on `id` }; export default DndContext(HTML5Backend)(connect(mapStateToProps)(Playground));