├── .DS_Store ├── .eslintcache ├── .gitignore ├── README.md ├── now.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html └── src ├── components ├── AddSettingForm.js ├── App.js ├── Block.js ├── Blocks.js ├── EditOptions.js ├── EditSettingForm.js ├── NotFound.js ├── OptionSetList.js ├── Options.js ├── PageLayout.js ├── RenderSchemaModal.js ├── SettingItem.js ├── SettingTextField.js ├── SettingsModal.js └── SettingsSection.js ├── css └── styles.css ├── data ├── sections.json └── types.json ├── index.js ├── store.js └── utils └── helpers.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkdallman/shopify-schema-builder/b47be86350aa9e3b258b1b74c388ebc28343a366/.DS_Store -------------------------------------------------------------------------------- /.eslintcache: -------------------------------------------------------------------------------- 1 | [{"/Users/tkdallman/shopify-schema-builder/src/index.js":"1","/Users/tkdallman/shopify-schema-builder/src/components/App.js":"2","/Users/tkdallman/shopify-schema-builder/src/components/PageLayout.js":"3","/Users/tkdallman/shopify-schema-builder/src/store.js":"4","/Users/tkdallman/shopify-schema-builder/src/components/RenderSchemaModal.js":"5","/Users/tkdallman/shopify-schema-builder/src/components/Blocks.js":"6","/Users/tkdallman/shopify-schema-builder/src/components/SettingsModal.js":"7","/Users/tkdallman/shopify-schema-builder/src/components/SettingsSection.js":"8","/Users/tkdallman/shopify-schema-builder/src/components/EditSettingForm.js":"9","/Users/tkdallman/shopify-schema-builder/src/components/Block.js":"10","/Users/tkdallman/shopify-schema-builder/src/components/AddSettingForm.js":"11","/Users/tkdallman/shopify-schema-builder/src/components/SettingItem.js":"12","/Users/tkdallman/shopify-schema-builder/src/components/OptionSetList.js":"13","/Users/tkdallman/shopify-schema-builder/src/components/EditOptions.js":"14","/Users/tkdallman/shopify-schema-builder/src/components/Options.js":"15","/Users/tkdallman/shopify-schema-builder/src/utils/helpers.js":"16"},{"size":150,"mtime":1613212835364,"results":"17","hashOfConfig":"18"},{"size":409,"mtime":1613212970698,"results":"19","hashOfConfig":"18"},{"size":4898,"mtime":1613212970701,"results":"20","hashOfConfig":"18"},{"size":2600,"mtime":1613212970703,"results":"21","hashOfConfig":"18"},{"size":2060,"mtime":1613212970701,"results":"22","hashOfConfig":"18"},{"size":3438,"mtime":1613212970699,"results":"23","hashOfConfig":"18"},{"size":6130,"mtime":1613212970702,"results":"24","hashOfConfig":"18"},{"size":2723,"mtime":1613212970702,"results":"25","hashOfConfig":"18"},{"size":3388,"mtime":1613212970700,"results":"26","hashOfConfig":"18"},{"size":1798,"mtime":1613212970699,"results":"27","hashOfConfig":"18"},{"size":3349,"mtime":1613212970698,"results":"28","hashOfConfig":"18"},{"size":705,"mtime":1613212835362,"results":"29","hashOfConfig":"18"},{"size":1097,"mtime":1613212835361,"results":"30","hashOfConfig":"18"},{"size":1785,"mtime":1613212835360,"results":"31","hashOfConfig":"18"},{"size":2280,"mtime":1613212970700,"results":"32","hashOfConfig":"18"},{"size":329,"mtime":1613212970704,"results":"33","hashOfConfig":"18"},{"filePath":"34","messages":"35","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},"jvkq20",{"filePath":"37","messages":"38","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"39","messages":"40","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"41","messages":"42","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"43","messages":"44","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"45","messages":"46","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"47","messages":"48","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"49","messages":"50","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"51","messages":"52","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"53","messages":"54","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"55","messages":"56","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"57","messages":"58","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"59","messages":"60","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"61","messages":"62","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"63"},{"filePath":"64","messages":"65","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},{"filePath":"66","messages":"67","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"36"},"/Users/tkdallman/shopify-schema-builder/src/index.js",[],["68","69"],"/Users/tkdallman/shopify-schema-builder/src/components/App.js",[],"/Users/tkdallman/shopify-schema-builder/src/components/PageLayout.js",[],"/Users/tkdallman/shopify-schema-builder/src/store.js",[],"/Users/tkdallman/shopify-schema-builder/src/components/RenderSchemaModal.js",[],"/Users/tkdallman/shopify-schema-builder/src/components/Blocks.js",[],"/Users/tkdallman/shopify-schema-builder/src/components/SettingsModal.js",[],"/Users/tkdallman/shopify-schema-builder/src/components/SettingsSection.js",[],"/Users/tkdallman/shopify-schema-builder/src/components/EditSettingForm.js",[],"/Users/tkdallman/shopify-schema-builder/src/components/Block.js",[],"/Users/tkdallman/shopify-schema-builder/src/components/AddSettingForm.js",[],"/Users/tkdallman/shopify-schema-builder/src/components/SettingItem.js",[],"/Users/tkdallman/shopify-schema-builder/src/components/OptionSetList.js",[],"/Users/tkdallman/shopify-schema-builder/src/components/EditOptions.js",[],["70","71"],"/Users/tkdallman/shopify-schema-builder/src/components/Options.js",[],"/Users/tkdallman/shopify-schema-builder/src/utils/helpers.js",[],{"ruleId":"72","replacedBy":"73"},{"ruleId":"74","replacedBy":"75"},{"ruleId":"72","replacedBy":"76"},{"ruleId":"74","replacedBy":"77"},"no-native-reassign",["78"],"no-negated-in-lhs",["79"],["78"],["79"],"no-global-assign","no-unsafe-negation"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Shopify Schema Builder 2 | 3 | This project was created with React, Redux, and Shopify Polaris. Mostly it was built as a learning tool to familiarize myself with these technologies however it may have some practical use for Shopify theme developers! 4 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "cotd", 4 | "builds": [{ "src": "package.json", "use": "@now/static-build" }], 5 | "routes": [ 6 | { "src": "^/static/(.*)", "dest": "/static/$1" }, 7 | { "src": "^/favicon.ico", "dest": "/favicon.ico" }, 8 | { "src": "^/asset-manifest.json", "dest": "/asset-manifest.json" }, 9 | { "src": "^/manifest.json", "dest": "/manifest.json" }, 10 | { 11 | "src": "^/service-worker.js", 12 | "headers": { "cache-control": "s-maxage=0" }, 13 | "dest": "/service-worker.js" 14 | }, 15 | { "src": "^/precache-manifest.(.*)", "dest": "/precache-manifest.$1" }, 16 | { "src": "^/(.*)", "dest": "/index.html" } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopify-schema-builder", 3 | "author": "TK Dallman", 4 | "homepage": "https://tkdallman.github.io/shopify-schema-builder/", 5 | "description": "Dev tool for Shopify theme devs", 6 | "version": "0.1.0", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "json-loader": "^0.5.7", 10 | "react-scripts": "4.0.1", 11 | "redux-devtools": "^3.7.0" 12 | }, 13 | "dependencies": { 14 | "@reduxjs/toolkit": "^1.4.0", 15 | "@shopify/polaris": "^3.15.0", 16 | "@testing-library/jest-dom": "^4.2.4", 17 | "@testing-library/react": "^9.3.2", 18 | "@testing-library/user-event": "^7.1.2", 19 | "array-move": "^2.1.0", 20 | "browserslist": "^4.15.0", 21 | "eslint-utils": "^1.4.2", 22 | "gh-pages": "^3.1.0", 23 | "immer": "^8.0.1", 24 | "ini": "^1.3.8", 25 | "lodash": "^4.17.20", 26 | "node-notifier": "^8.0.1", 27 | "react": "^17.0.1", 28 | "react-dom": "^17.0.1", 29 | "react-redux": "^7.1.3", 30 | "react-scripts": "4.0.1" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "predeploy": "npm run build", 36 | "deploy": "gh-pages -d build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject" 39 | }, 40 | "eslintConfig": { 41 | "extends": "react-app" 42 | }, 43 | "browserslist": { 44 | "production": [ 45 | ">0.2%", 46 | "not dead", 47 | "not op_mini all" 48 | ], 49 | "development": [ 50 | "last 1 chrome version", 51 | "last 1 firefox version", 52 | "last 1 safari version" 53 | ] 54 | }, 55 | "engines": { 56 | "node": "12.x" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkdallman/shopify-schema-builder/b47be86350aa9e3b258b1b74c388ebc28343a366/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Shopify Schema Maker 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/AddSettingForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Stack, Form, Select, FormLayout, TextField, InlineError } from "@shopify/polaris"; 4 | import Options from './Options'; 5 | import PropTypes from "prop-types"; 6 | 7 | const sections = require("../data/sections.json"); 8 | 9 | class AddSettingForm extends Component { 10 | 11 | static propTypes = { 12 | preloadData: PropTypes.func, 13 | settings: PropTypes.object, 14 | updateAndClose: PropTypes.func, 15 | handleSettingChange: PropTypes.func, 16 | idError: PropTypes.bool, 17 | } 18 | 19 | componentDidMount() { 20 | if (this.props.modal.modalType === 'duplicate') { 21 | this.props.preloadData(this.props.modal.item) 22 | } 23 | } 24 | 25 | render() { 26 | const { settings, handleSettingChange } = this.props; 27 | const allOptions = [ 'Pick an Option', ...Object.keys(sections)]; 28 | const options = allOptions.map(option => { return { value: option, label: option }}); 29 | let additionalInputs = []; 30 | 31 | if (settings.type !== 'Pick an Option') { 32 | additionalInputs = (Object.keys(sections[settings.type]) || []); 33 | } 34 | const numberInputs = ['min', 'max', 'step']; 35 | 36 | return ( 37 |
38 | 39 | 40 | handleSettingChange( 46 | { 47 | changeType: "editInput", 48 | input: "type" 49 | } 50 | , value)} 51 | value={settings.type} 52 | /> 53 | 54 | {inputs.map(input => { 55 | if (input === "options") { 56 | if (!settings.options) return false; 57 | return ( 58 |
59 |

Options

60 | {settings.options.map((item, index) => { 61 | const isLastItem = settings.options.length - 1 === index; 62 | 63 | return ( 64 | 72 | ); 73 | })} 74 |
75 | ) 76 | } 77 | 78 | return ( 79 |
80 | handleSettingChange({ 86 | changeType: 'editInput', 87 | input 88 | }, value)} 89 | /> 90 | {input === 'id' && this.props.errorState && ( 91 | 92 | )} 93 |
94 | ); 95 | })} 96 |
97 |
98 |
99 | ); 100 | } 101 | } 102 | 103 | const mapStateToProps = state => ({ 104 | settingItems: state.settings, 105 | modalType: state.modal.modalType, 106 | modal: state.modal, 107 | error: state.error 108 | }) 109 | 110 | export default connect(mapStateToProps)(EditSettingForm); 111 | -------------------------------------------------------------------------------- /src/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NotFound = () => ( 4 |
5 |

Not Found

6 |
7 | ); 8 | 9 | export default NotFound; 10 | -------------------------------------------------------------------------------- /src/components/OptionSetList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { TextStyle, Badge, Stack } from '@shopify/polaris'; 3 | 4 | class OptionSetList extends Component { 5 | 6 | render() { 7 | const { options } = this.props; 8 | 9 | if (!options) return false; 10 | 11 | return ( 12 |
13 | {'Options: '} 14 | {options.map((optionSet, index) => { 15 | const hasGroupProperty = optionSet.hasOwnProperty('group'); 16 | if (optionSet.value === undefined) return false; 17 | return ( 18 | 19 | 20 | 21 | { hasGroupProperty && <>Group: { optionSet.group + ' '} } 22 | Value: {optionSet.value + ' '} 23 | Label: {optionSet.label} 24 | 25 | 26 | 27 | ) 28 | })} 29 |
30 | ); 31 | } 32 | } 33 | 34 | export default OptionSetList; -------------------------------------------------------------------------------- /src/components/Options.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { TextField, Button, FormLayout } from "@shopify/polaris"; 3 | import PropTypes from "prop-types"; 4 | 5 | class Options extends Component { 6 | static propTypes = { 7 | index: PropTypes.number, 8 | type: PropTypes.string, 9 | settings: PropTypes.object, 10 | handleSettingChange: PropTypes.func, 11 | }; 12 | 13 | changeOption = (input, index, value, options) => { 14 | options[index][input] = value; 15 | this.props.handleSettingChange("options", options); 16 | }; 17 | 18 | render() { 19 | const { 20 | index, 21 | settings: { options }, 22 | type, 23 | handleSettingChange, 24 | } = this.props; 25 | const currentOptionSet = options[index]; 26 | const isLastItem = options.length - 1 === index; 27 | const sharedInputs = ["value", "label"]; 28 | let group; 29 | 30 | if (type === "select") { 31 | group = ( 32 | 37 | handleSettingChange( 38 | { 39 | changeType: "editOption", 40 | index, 41 | attribute: "group", 42 | }, 43 | value 44 | ) 45 | } 46 | /> 47 | ); 48 | } 49 | 50 | return ( 51 | 52 | {group} 53 | {sharedInputs.map((input) => { 54 | return ( 55 | 60 | handleSettingChange( 61 | { 62 | changeType: "editOption", 63 | index, 64 | attribute: input, 65 | }, 66 | value 67 | ) 68 | } 69 | /> 70 | ); 71 | })} 72 | {isLastItem ? ( 73 | 79 | ) : ( 80 | 88 | )} 89 | 90 | ); 91 | } 92 | } 93 | 94 | export default Options; 95 | -------------------------------------------------------------------------------- /src/components/PageLayout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { 4 | Layout, 5 | Page, 6 | Card, 7 | TextField, 8 | FormLayout, 9 | Form, 10 | Stack, 11 | TextContainer, 12 | } from "@shopify/polaris"; 13 | import RenderSchemaModal from "./RenderSchemaModal"; 14 | import Blocks from "./Blocks"; 15 | import SettingsSection from "./SettingsSection"; 16 | import SettingsModal from "./SettingsModal"; 17 | import { uppercaseFirst } from "../utils/helpers"; 18 | 19 | const types = require("../data/types.json"); 20 | 21 | class PageLayout extends Component { 22 | state = { 23 | modalActive: false, 24 | modalType: "", 25 | settingItemTriggered: {}, 26 | settings: { type: "Pick an Option" }, 27 | blockTriggeredIndex: undefined, 28 | idError: false, 29 | }; 30 | 31 | handleModalChange = (modalChangeType, id, index) => { 32 | let itemDetails, selectedItem; 33 | if (["edit", "duplicate"].includes(modalChangeType)) { 34 | selectedItem = this.props.storeSettings[id][index]; 35 | } 36 | 37 | itemDetails = selectedItem 38 | ? JSON.parse(JSON.stringify(selectedItem)) 39 | : null; 40 | 41 | switch (modalChangeType) { 42 | case "edit": 43 | this.props.modalEdit(itemDetails, index, id); 44 | break; 45 | case "duplicate": 46 | this.props.modalDuplicate(itemDetails, id); 47 | break; 48 | 49 | case "add": 50 | this.setState(({ idError }) => ({ idError: true })); 51 | this.props.setErrorState(false); 52 | this.props.modalAdd(id); 53 | break; 54 | default: 55 | return false; 56 | } 57 | }; 58 | 59 | render() { 60 | // TODO: integrate other 'types' of schema objects 61 | const activeFields = Object.keys(types["section"]); 62 | 63 | const textFields = ["name", "class", "tag"]; 64 | const { mainFields, updateValue } = this.props; 65 | 66 | return ( 67 | 68 | 69 | 73 | 74 | 75 |
76 | 77 | {activeFields.map((field) => { 78 | if (textFields.includes(field)) 79 | return ( 80 | updateValue(field, value)} 85 | /> 86 | ); 87 | return

; 88 | })} 89 |
90 |
91 |
92 |
93 |
94 | 95 | 99 | 100 | 105 | 106 | 107 | 111 | {activeFields.includes("blocks") && ( 112 | 113 | )} 114 | 115 | 116 | 117 | 118 | 121 | 122 | 123 | 124 |
125 | 126 | 127 |
128 | ); 129 | } 130 | } 131 | 132 | const mapStateToProps = (state) => ({ 133 | storeSettings: state.settings, 134 | modal: state.modal, 135 | error: state.error, 136 | fields: state.fields, 137 | mainFields: state.fields.store, 138 | }); 139 | 140 | const mapDispatchToProps = (dispatch) => { 141 | return { 142 | setErrorState: (errorState) => dispatch({ 143 | type: "SET_ERROR_STATE", 144 | errorState 145 | }), 146 | modalEdit: (itemDetails, index, id) => dispatch({ 147 | type: "MODAL_ACTIVE", 148 | modalActive: true, 149 | modalType: "edit", 150 | item: itemDetails, 151 | index, 152 | id, 153 | }), 154 | modalAdd: (id) => dispatch({ 155 | type: "MODAL_ACTIVE", 156 | modalActive: true, 157 | modalType: "add", 158 | item: null, 159 | index: null, 160 | id, 161 | }), 162 | modalDuplicate: (itemDetails, id) => dispatch({ 163 | type: "MODAL_ACTIVE", 164 | modalActive: true, 165 | modalType: "duplicate", 166 | item: itemDetails, 167 | index: null, 168 | id, 169 | }), 170 | updateValue: (field, value) => dispatch({ 171 | type: "UPDATE_FIELD", 172 | id: "store", 173 | field, 174 | value, 175 | }) 176 | } 177 | } 178 | 179 | export default connect(mapStateToProps, mapDispatchToProps)(PageLayout); 180 | -------------------------------------------------------------------------------- /src/components/RenderSchemaModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { Modal, Button } from "@shopify/polaris"; 4 | import "../css/styles.css"; 5 | import PropTypes from "prop-types"; 6 | 7 | class RenderFieldModal extends Component { 8 | state = { 9 | modalActive: false, 10 | }; 11 | 12 | static propTypes = { 13 | activeFields: PropTypes.array 14 | } 15 | 16 | handleModalChange = () => { 17 | this.setState(({ modalActive }) => ({ modalActive: !modalActive })); 18 | }; 19 | 20 | getFieldJSON = () => { 21 | const removeQuotesRegex = new RegExp(/"(min|max|step)": "(\d*)"/gi); 22 | const { fields, settings, blocks } = this.props; 23 | 24 | const reorderedBlocks = 25 | blocks.length === 0 26 | ? null 27 | : blocks.map(({ id }) => { 28 | return { ...fields[id], settings: settings[id] }; 29 | }); 30 | 31 | const reorderedObject = { 32 | ...fields.store, 33 | settings: settings.store, 34 | ...(blocks.length > 0 && { blocks: reorderedBlocks }), 35 | }; 36 | 37 | let stringifiedFieldItems = JSON.stringify( 38 | reorderedObject, 39 | null, 40 | 2 41 | ).replace(removeQuotesRegex, '"$1": $2'); 42 | 43 | stringifiedFieldItems = 44 | `{% schema %}\n` + stringifiedFieldItems + `\n{% endschema %}`; 45 | 46 | return stringifiedFieldItems; 47 | }; 48 | 49 | render() { 50 | const { modalActive } = this.state; 51 | const fieldItemsJSON = this.getFieldJSON(); 52 | 53 | return ( 54 |
55 | 56 | 65 | 66 | 67 | 68 | 69 |
70 | ); 71 | } 72 | } 73 | 74 | const mapStateToProps = (state) => ({ 75 | settings: state.settings, 76 | modal: state.modal, 77 | error: state.error, 78 | blocks: state.blocks, 79 | fields: state.fields, 80 | }); 81 | 82 | export default connect(mapStateToProps)(RenderFieldModal); 83 | -------------------------------------------------------------------------------- /src/components/SettingItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { TextStyle } from '@shopify/polaris'; 3 | import OptionSetList from './OptionSetList.js'; 4 | 5 | class SettingItem extends Component { 6 | render() { 7 | const { item } = this.props; 8 | return ( 9 |
10 | {Object.keys(item).map((keyName, keyIndex) => { 11 | if (keyName === 'options') { return ( 12 |

13 | 14 |

15 | )}; 16 | 17 | return ( 18 |

19 | {keyName + ': '}{item[keyName]} 20 |

21 | ) 22 | }) } 23 |
24 | ); 25 | } 26 | } 27 | 28 | export default SettingItem; -------------------------------------------------------------------------------- /src/components/SettingTextField.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { TextField } from "@shopify/polaris"; 3 | 4 | class SettingTextField extends Component { 5 | state = { 6 | input: '', 7 | }; 8 | 9 | render() { 10 | return ( 11 | this.setState({ 'input': v })} 15 | /> 16 | ); 17 | } 18 | } 19 | 20 | export default SettingTextField; -------------------------------------------------------------------------------- /src/components/SettingsModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { Modal } from "@shopify/polaris"; 4 | import AddSettingForm from "./AddSettingForm"; 5 | import EditSettingForm from "./EditSettingForm"; 6 | import { removeExtraneous, uppercaseFirst } from "../utils/helpers"; 7 | 8 | const sections = require("../data/sections.json"); 9 | 10 | class SettingsModal extends Component { 11 | state = { 12 | settings: { type: "Pick an Option" }, 13 | }; 14 | 15 | deleteItem = () => { 16 | const { 17 | modal: { id, index }, 18 | } = this.props; 19 | 20 | this.props.deleteSetting(id, index); 21 | 22 | this.handleClose(); 23 | }; 24 | 25 | handleSettingChange = (change, value) => { 26 | const newSettings = { ...this.state.settings }; 27 | const changeType = change.changeType; 28 | 29 | switch (changeType) { 30 | case "editOption": 31 | newSettings.options[change.index][change.attribute] = value; 32 | break; 33 | 34 | case "removeOption": 35 | newSettings.options.splice(change.index, 1); 36 | break; 37 | 38 | case "addOption": 39 | newSettings.options.push({}); 40 | break; 41 | 42 | case "editInput": 43 | const itemsWithOptions = ["radio", "select"]; 44 | 45 | if (itemsWithOptions.includes(value)) { 46 | newSettings.options = [{}]; 47 | } 48 | 49 | newSettings[change.input] = value; 50 | break; 51 | 52 | default: 53 | return false; 54 | } 55 | 56 | this.setState(({ settings }) => ({ settings: newSettings })); 57 | }; 58 | 59 | updateAndClose() { 60 | const { 61 | error, 62 | savedSettings, 63 | modal: { modalType, index, id }, 64 | } = this.props; 65 | const { settings } = this.state; 66 | 67 | let errorState = false; 68 | const settingIds = savedSettings[id] ? savedSettings[id].map((setting) => setting.id) : []; 69 | 70 | // add error checking for block IDs 71 | if (modalType !== "edit") { 72 | if ( 73 | settingIds.includes(settings.id) || 74 | !settings.id || 75 | settings.id === "" 76 | ) 77 | errorState = true; 78 | if (error.errorState !== errorState) { 79 | this.props.setErrorState(errorState); 80 | } 81 | } 82 | 83 | if (errorState) return; 84 | 85 | if (modalType === "edit") { 86 | const updatedSettingItem = settings; 87 | const settingItemProperties = [ 88 | "type", 89 | ...Object.keys(sections[updatedSettingItem.type]), 90 | ]; 91 | const settingItemWithoutExtraneous = removeExtraneous( 92 | updatedSettingItem, 93 | settingItemProperties 94 | ); 95 | 96 | if ( 97 | settingItemWithoutExtraneous.type === "radio" && 98 | settingItemWithoutExtraneous.options 99 | ) { 100 | settingItemWithoutExtraneous.options = settingItemWithoutExtraneous.options.map( 101 | (option) => { 102 | return removeExtraneous(option, ["value", "label"]); 103 | } 104 | ); 105 | } 106 | 107 | this.props.updateSetting(settingItemWithoutExtraneous, index, id) 108 | } else { 109 | const settingItem = {}; 110 | const settingItemProperties = Object.keys(sections[settings.type]); 111 | 112 | Object.keys(settings) 113 | .filter( 114 | (item) => settingItemProperties.includes(item) || item === "type" 115 | ) 116 | .forEach((item) => { 117 | settingItem[item] = settings[item]; 118 | }); 119 | 120 | this.props.addSetting(settingItem, id); 121 | } 122 | 123 | this.handleClose(); 124 | } 125 | 126 | handleClose = () => { 127 | this.props.setErrorState(false); 128 | 129 | this.props.closeModal(); 130 | this.setState(({ settings }) => ({ settings: { type: "Pick an Option" } })); 131 | }; 132 | 133 | render() { 134 | const { 135 | modal: { modalActive, modalType }, 136 | } = this.props; 137 | 138 | if (!modalActive || !modalType) return false; 139 | 140 | 141 | return ( 142 | this.updateAndClose(), 149 | }} 150 | secondaryActions={modalType === 'edit' && [ 151 | { 152 | content: "Delete item", 153 | onAction: this.deleteItem, 154 | }, 155 | ]} 156 | > 157 | 158 | {modalType === "edit" && ( 159 | 161 | this.setState(({ settings }) => ({ settings: newSettings })) 162 | } 163 | settingItemTriggered={this.props.settingItemTriggered} 164 | updateAndClose={this.updateAndClose} 165 | settings={this.state.settings} 166 | handleSettingChange={this.handleSettingChange} 167 | idError={this.props.idError} 168 | /> 169 | )} 170 | {modalType === "duplicate" && ( 171 | 173 | this.setState(({ settings }) => ({ settings: newSettings })) 174 | } 175 | updateAndClose={this.updateAndClose} 176 | handleSettingChange={this.handleSettingChange} 177 | idError={this.props.idError} 178 | settings={this.state.settings} 179 | /> 180 | )} 181 | {modalType === "add" && ( 182 | 188 | )} 189 | 190 | 191 | ); 192 | } 193 | } 194 | 195 | const mapStateToProps = (state) => ({ 196 | modal: state.modal, 197 | error: state.error, 198 | savedSettings: state.settings, 199 | }); 200 | 201 | const mapDispatchToProps = (dispatch) => { 202 | return { 203 | deleteSetting: (id, index) => dispatch({ type: 'DELETE_SETTING', id, index}), 204 | setErrorState: (errorState) => dispatch({ type: 'SET_ERROR_STATE', errorState }), 205 | updateSetting: (updatedSetting, index, id) => dispatch({ 206 | type: "UPDATE_SETTING", 207 | setting: updatedSetting, 208 | index, 209 | id, 210 | }), 211 | addSetting: (setting, id) => dispatch({ 212 | type: "ADD_SETTING", 213 | setting, 214 | id, 215 | }), 216 | closeModal: () => dispatch({ 217 | type: "MODAL_ACTIVE", 218 | modalActive: false, 219 | modalType: null, 220 | item: null, 221 | index: null, 222 | blockIndex: null, 223 | id: null 224 | }) 225 | } 226 | } 227 | 228 | export default connect(mapStateToProps, mapDispatchToProps)(SettingsModal); 229 | -------------------------------------------------------------------------------- /src/components/SettingsSection.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import { Button, ResourceList, Stack } from "@shopify/polaris"; 5 | import SettingItem from "./SettingItem"; 6 | 7 | class SettingsSection extends Component { 8 | static propTypes = { 9 | id: PropTypes.string, 10 | handleModalChange: PropTypes.func, 11 | showSettingsButton: PropTypes.bool, 12 | }; 13 | 14 | getSettings = (index) => { 15 | const settings = []; 16 | const { handleModalChange, id, moveSetting } = this.props; 17 | const numSettings = this.props.storeSettings[this.props.id] 18 | ? this.props.storeSettings[this.props.id].length 19 | : 0; 20 | 21 | if (numSettings === 0) return false; 22 | 23 | if (index > 0) { 24 | settings.push({ 25 | content: "↑", 26 | onClick: () => moveSetting(index, index - 1), 27 | }); 28 | } 29 | if (index !== numSettings - 1) { 30 | settings.push({ 31 | content: "↓", 32 | onClick: () => moveSetting(index, index + 1), 33 | }); 34 | } 35 | settings.push({ 36 | content: "Duplicate", 37 | onClick: () => handleModalChange("duplicate", id, index), 38 | }); 39 | 40 | return settings; 41 | }; 42 | 43 | render() { 44 | const { storeSettings, handleModalChange, id } = this.props; 45 | let settings = storeSettings[id]; 46 | 47 | return ( 48 | <> 49 | {settings && settings.length > 0 && ( 50 | { 54 | const index = settings.indexOf(item); 55 | if (item) 56 | return ( 57 | handleModalChange("edit", id, index)} 61 | shortcutActions={this.getSettings(index)} 62 | > 63 | 64 | 65 | ); 66 | }} 67 | /> 68 | )} 69 | 70 | {this.props.showSettingsButton && ( 71 | 72 | 75 | 76 | )} 77 | 78 | ); 79 | } 80 | } 81 | 82 | const mapStateToProps = (state) => ({ 83 | storeSettings: state.settings, 84 | modal: state.modal, 85 | error: state.error, 86 | }); 87 | 88 | const mapDispatchToProps = (dispatch, ownProps) => { 89 | return { 90 | moveSetting: (index, destination) => dispatch({ 91 | type: "MOVE_SETTING", 92 | index, 93 | destination, 94 | id: ownProps.id 95 | }) 96 | } 97 | } 98 | 99 | export default connect(mapStateToProps, mapDispatchToProps)(SettingsSection); 100 | -------------------------------------------------------------------------------- /src/css/styles.css: -------------------------------------------------------------------------------- 1 | textarea { 2 | width: 100%; 3 | margin-bottom: 1rem; 4 | min-height: auto; 5 | height: 230px; 6 | resize: none; 7 | font-family: monospace; 8 | font-size: 11px; 9 | line-height: 15px; 10 | } -------------------------------------------------------------------------------- /src/data/sections.json: -------------------------------------------------------------------------------- 1 | { 2 | "text": { 3 | "id": true, 4 | "placeholder": false, 5 | "default": false, 6 | "info": false 7 | }, 8 | "textarea": { 9 | "id": true, 10 | "placeholder": false, 11 | "default": false, 12 | "info": false 13 | }, 14 | "image_picker": { 15 | "id": true 16 | }, 17 | "radio": { 18 | "id": true, 19 | "options": false, 20 | "default": false, 21 | "info": false 22 | }, 23 | "select": { 24 | "id": true, 25 | "options": false, 26 | "default": false, 27 | "info": false 28 | }, 29 | "checkbox": { 30 | "id": true, 31 | "default": false, 32 | "info": false 33 | }, 34 | "range": { 35 | "id": true, 36 | "min": true, 37 | "max": true, 38 | "step": true, 39 | "unit": true, 40 | "default": false 41 | }, 42 | "color": { 43 | "id": true, 44 | "default": false, 45 | "info": false 46 | }, 47 | "font_picker": { 48 | "id": true, 49 | "default": false, 50 | "info": false 51 | }, 52 | "collection": { 53 | "id": true, 54 | "info": false 55 | }, 56 | "product": { 57 | "id": true, 58 | "info": false 59 | }, 60 | "blog": { 61 | "id": true, 62 | "info": false 63 | }, 64 | "page": { 65 | "id": true, 66 | "info": false 67 | }, 68 | "link_list": { 69 | "id": true, 70 | "info": false 71 | }, 72 | "url": { 73 | "id": true, 74 | "label": false 75 | }, 76 | "video_url": { 77 | "id": true, 78 | "accept": true, 79 | "default": false, 80 | "placeholder": false 81 | }, 82 | "richtext": { 83 | "id": true, 84 | "default": false 85 | }, 86 | "html": { 87 | "id": true, 88 | "default": false 89 | }, 90 | "article": { 91 | "id": true 92 | }, 93 | "header": { 94 | "content": true, 95 | "info": false 96 | }, 97 | "paragraph": { 98 | "content": true 99 | } 100 | } 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/data/types.json: -------------------------------------------------------------------------------- 1 | { 2 | "section": { 3 | "name": true, 4 | "class": false, 5 | "tag": false, 6 | "settings": true, 7 | "blocks": false, 8 | "max_blocks": false, 9 | "presets": false, 10 | "default": false, 11 | "locales": false 12 | }, 13 | "schemaSetting": { 14 | "name": true, 15 | "settings": true 16 | } 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import App from "./components/App"; 4 | 5 | render(, document.querySelector("#main")); 6 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux' 2 | const arrayMove = require("array-move"); 3 | 4 | function fields (state = { store: {} }, { type, id = 'store', field, value }) { 5 | const defaultState = { ...state }; 6 | 7 | switch (type) { 8 | case 'ADD_FIELDS': 9 | defaultState[id] = {} 10 | return defaultState 11 | case 'DELETE_ALL_FIELDS': 12 | delete defaultState[id] 13 | return defaultState 14 | case 'UPDATE_FIELD': 15 | if (!defaultState[id]) defaultState[id] = {} 16 | defaultState[id][field] = value 17 | return defaultState 18 | default: 19 | return state 20 | } 21 | } 22 | 23 | function settings (state = { store: [] }, { type, setting, id = 'store', index, destination }) { 24 | const defaultState = { ...state }; 25 | 26 | switch (type) { 27 | case 'ADD_SETTING': 28 | setting && defaultState[id] 29 | ? defaultState[id].push(setting) 30 | : defaultState[id] = [] 31 | return defaultState 32 | case 'UPDATE_SETTING': 33 | defaultState[id][index] = setting 34 | return defaultState 35 | case 'DELETE_SETTING': 36 | defaultState[id].splice(index, 1) 37 | return defaultState 38 | case 'DELETE_ALL_SETTINGS': 39 | delete defaultState[id] 40 | return defaultState 41 | case 'MOVE_SETTING': 42 | const movedSettings = arrayMove(defaultState[id], index, destination) 43 | defaultState[id] = movedSettings 44 | return defaultState 45 | 46 | default: return state 47 | } 48 | } 49 | 50 | function modal (state = { modalActive: false }, action) { 51 | switch (action.type) { 52 | case 'MODAL_ACTIVE': 53 | return { 54 | modalActive: action.modalActive, 55 | modalType: action.modalType, 56 | item: action.item, 57 | index: action.index, 58 | id: action.id, 59 | blockIndex: action.blockIndex 60 | } 61 | default: return state 62 | } 63 | } 64 | 65 | function blocks (state = [], action) { 66 | const updatedState = [ ...state ] 67 | switch (action.type) { 68 | case 'ADD_BLOCK': 69 | updatedState.push({ id: `block_${Date.now()}`, isOpen: true }) 70 | return updatedState 71 | case 'TOGGLE_BLOCK': 72 | updatedState[action.index].isOpen = action.setting 73 | return updatedState 74 | case 'DELETE_BLOCK': 75 | updatedState.splice(action.index, 1) 76 | return updatedState 77 | default: return state 78 | } 79 | } 80 | 81 | function error (state = { errorState: false }, action) { 82 | switch (action.type) { 83 | case 'SET_ERROR_STATE': 84 | return { 85 | errorState: action.errorState 86 | } 87 | default: return state 88 | } 89 | } 90 | 91 | const reducer = combineReducers({ settings, modal, error, blocks, fields }) 92 | const store = createStore( 93 | reducer, {}, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) 94 | 95 | export default store -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | export function removeExtraneous(object, desiredKeys) { 2 | const newObject = desiredKeys.reduce((obj, property) => { 3 | if (object[property]) obj[property] = object[property]; 4 | return obj 5 | }, {}); 6 | return newObject; 7 | } 8 | 9 | export function uppercaseFirst(string) { 10 | return string.charAt(0).toUpperCase() + string.slice(1); 11 | } --------------------------------------------------------------------------------