├── .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 |
94 | );
95 | }
96 | }
97 |
98 | const mapStateToProps = state => ({
99 | settingItems: state.settings,
100 | settingItemTriggered: state.modal.item,
101 | modal: state.modal,
102 | modalType: state.modal.modalType,
103 | error: state.error.errorState
104 | })
105 |
106 | export default connect(mapStateToProps)(AddSettingForm);
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Provider } from "react-redux";
3 | import { AppProvider } from "@shopify/polaris";
4 | import PageLayout from "./PageLayout";
5 | import store from "../store";
6 |
7 | class App extends Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 | }
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/src/components/Block.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import PropTypes from "prop-types";
4 | import { TextField, FormLayout, Form } from "@shopify/polaris";
5 | import SettingsSection from "./SettingsSection";
6 | import { uppercaseFirst } from "../utils/helpers";
7 |
8 | class Block extends Component {
9 | static propTypes = {
10 | blockValues: PropTypes.object,
11 | id: PropTypes.string,
12 | handleModalChange: PropTypes.func,
13 | };
14 |
15 | handleFieldChange(field, value) {
16 | this.props.updateField(field, value, this.props.id);
17 | }
18 |
19 | render() {
20 | const activeFields = ["type", "name"];
21 | const { handleModalChange, fields, id } = this.props;
22 | const blockValues = fields[id] || {};
23 |
24 | return (
25 |
26 |
46 |
47 | );
48 | }
49 | }
50 |
51 | const mapStateToProps = (state) => ({
52 | modal: state.modal,
53 | error: state.error,
54 | settings: state.settings.blocks,
55 | blocks: state.blocks,
56 | fields: state.fields,
57 | });
58 |
59 | const mapDispatchToProps = (dispatch, ownProps) => {
60 | return {
61 | updateField: (field, value) => dispatch({
62 | type: "UPDATE_FIELD",
63 | field,
64 | value,
65 | id: ownProps.id,
66 | })
67 | }
68 | }
69 |
70 | export default connect(mapStateToProps, mapDispatchToProps)(Block);
71 |
--------------------------------------------------------------------------------
/src/components/Blocks.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | import PropTypes from "prop-types";
4 | import { Button, Card, Collapsible, Stack, TextField } from "@shopify/polaris";
5 | import Block from "./Block";
6 |
7 | class Blocks extends Component {
8 | static propTypes = {
9 | handleModalChange: PropTypes.func,
10 | };
11 |
12 | addBlock() {
13 | const id = `block_${Date.now()}`;
14 | this.props.addBlock(id);
15 | this.props.addFields(id);
16 | this.props.addSetting(id);
17 | }
18 |
19 | deleteBlock(index, blockId) {
20 | this.props.deleteBlock(index);
21 | this.props.deleteAllSettings(blockId);
22 | this.props.deleteAllFields(blockId);
23 | }
24 |
25 | updateMaxBlocks(value) {
26 | this.props.updateField("maxBlocks", parseInt(value));
27 | }
28 |
29 | handleToggleClick(index, isOpen) {
30 | this.props.toggleBlock(index, isOpen);
31 | }
32 |
33 | render() {
34 | const { blocks, fields, handleModalChange } = this.props;
35 |
36 | return (
37 | <>
38 | {blocks.map((block, index) => {
39 | return (
40 |
41 |
42 |
48 |
49 |
50 |
53 |
56 |
63 |
64 |
65 | );
66 | })}
67 |
68 |
69 |
70 |
71 | {blocks.length > 0 && (
72 | this.updateMaxBlocks(value)}
79 | />
80 | )}
81 |
82 |
83 | >
84 | );
85 | }
86 | }
87 |
88 | const mapStateToProps = (state) => ({
89 | storeSettings: state.settings.blocks,
90 | modal: state.modal,
91 | error: state.error,
92 | blocks: state.blocks,
93 | fields: state.fields,
94 | });
95 |
96 | const mapDispatchToProps = (dispatch) => {
97 | return {
98 | addBlock: (id ) => dispatch({ type: 'ADD_BLOCK', id }),
99 | addFields: (id ) => dispatch({ type: 'ADD_FIELDS', id }),
100 | addSetting: (id ) => dispatch({ type: 'ADD_SETTING', id }),
101 | updateField: (field, value, id) => dispatch({ type: "UPDATE_FIELD", field, value, id }),
102 | toggleBlock: (index, isOpen) => dispatch({ type: 'TOGGLE_BLOCK', index, setting: isOpen }),
103 | deleteBlock: (index ) => dispatch({ type: 'DELETE_BLOCK', index }),
104 | deleteAllSettings: (id) =>dispatch({ type:'DELETE_ALL_SETTINGS', id }),
105 | deleteAllFields: (id) => dispatch({ type: 'DELETE_ALL_FIELDS', id }),
106 | }
107 | }
108 |
109 | export default connect(mapStateToProps, mapDispatchToProps)(Blocks);
110 |
--------------------------------------------------------------------------------
/src/components/EditOptions.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { TextField, Stack, Button } from "@shopify/polaris";
3 | import PropTypes from "prop-types";
4 |
5 | class EditOptions extends Component {
6 | static propTypes = {
7 | index: PropTypes.number,
8 | inputType: PropTypes.string,
9 | options: PropTypes.object,
10 | handleSettingChange: PropTypes.func,
11 | isLastItem: PropTypes.bool,
12 | }
13 |
14 | render() {
15 | const { inputType, options, index, handleSettingChange, isLastItem } = this.props;
16 | const sharedInputs = ["value", "label"];
17 | let group;
18 |
19 | if (inputType === "select") {
20 | group = (
21 | handleSettingChange({
26 | changeType: 'editOption',
27 | index,
28 | attribute: "group"
29 | }, value)}
30 | />
31 | );
32 | }
33 |
34 | return (
35 |
36 |
37 | {group}
38 |
39 | {sharedInputs.map(input => {
40 | return (
41 | handleSettingChange({
46 | changeType: 'editOption',
47 | index,
48 | attribute: input
49 | },
50 | value)}
51 | />
52 | );
53 | })}
54 | {isLastItem ? (
55 |
58 | ) : (
59 |
62 | )}
63 |
64 |
65 | );
66 | }
67 | }
68 |
69 | export default EditOptions;
70 |
--------------------------------------------------------------------------------
/src/components/EditSettingForm.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 PropTypes from "prop-types";
5 | import EditOptions from "./EditOptions";
6 |
7 | const sections = require("../data/sections.json");
8 |
9 | class EditSettingForm extends Component {
10 |
11 | static propTypes = {
12 | preloadData: PropTypes.func,
13 | updateSettingItem: PropTypes.func,
14 | handleSettingChange: PropTypes.func,
15 | updateAndClose: PropTypes.func,
16 | idError: PropTypes.bool,
17 | }
18 |
19 | componentDidMount() {
20 | this.props.preloadData(this.props.modal.item)
21 | }
22 |
23 | render() {
24 | const { handleSettingChange, settings } = this.props;
25 |
26 | const allOptions = Object.keys(sections);
27 | if (!allOptions) return false;
28 | if (!this.props.modal.modalActive || !settings || settings.type === "Pick an Option") return false;
29 |
30 | const options = allOptions.map(option => {
31 | return { value: option, label: option };
32 | });
33 |
34 | const inputs = Object.keys(sections[settings.type]);
35 |
36 | const numberInputs = ["min", "max", "step"];
37 |
38 | return (
39 |
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 |
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 | }
--------------------------------------------------------------------------------