@knapsack/app
Version:
Build Design Systems on top of knapsack, by Basalt
525 lines (495 loc) • 16.8 kB
JSX
/**
* 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));