Skip to content

Commit

Permalink
feat(form): add (auto)save and local schema repo
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrc committed Sep 25, 2024
1 parent 490c2d9 commit 1d36a83
Show file tree
Hide file tree
Showing 9 changed files with 505 additions and 65 deletions.
358 changes: 338 additions & 20 deletions formule-demo/src/App.tsx

Large diffs are not rendered by default.

41 changes: 27 additions & 14 deletions src/admin/components/SchemaPreview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,55 @@ import { SettingOutlined } from "@ant-design/icons";
import { useDispatch, useSelector } from "react-redux";
import { selectProperty } from "../../store/schemaWizard";

const SchemaPreview = () => {
const SchemaPreview = ({ hideSchemaKey }) => {
const schema = useSelector((state) => state.schemaWizard.current.schema);
const name = useSelector((state) => state.schemaWizard.config.name);

const schema = useSelector((state) => state.schemaWizard.current.schema)

const dispatch = useDispatch()
const dispatch = useDispatch();

return (
<div style={{ height: "80%" }} data-cy="schemaTree">
<Row justify="center">
<Col span={24}>
<Typography.Title
level={4}
style={{ textAlign: "center", margin: "15px 0" }}
style={{
textAlign: "center",
margin: hideSchemaKey ? "15px 0" : "15px 0 0 0",
}}
>
Schema tree
</Typography.Title>
</Col>
{!hideSchemaKey && (
<Typography.Text type="secondary">{name}</Typography.Text>
)}
</Row>
<Row
wrap={false}
justify="space-between"
align="middle"
style={{ padding: "0 10px" }}
>
<Typography.Title level={5} style={{ margin: 0 }} ellipsis data-cy="rootTitle">
<Typography.Title
level={5}
style={{ margin: 0 }}
ellipsis
data-cy="rootTitle"
>
{(schema && schema.title) || "root"}
</Typography.Title>
<Tooltip title="Edit root settings">
<Button
type="link"
shape="circle"
icon={<SettingOutlined />}
onClick={() => dispatch(selectProperty({ path: { schema: [], uiSchema: [] }}))}
className="tour-root-settings"
data-cy="rootSettings"
/>
<Button
type="link"
shape="circle"
icon={<SettingOutlined />}
onClick={() =>
dispatch(selectProperty({ path: { schema: [], uiSchema: [] } }))
}
className="tour-root-settings"
data-cy="rootSettings"
/>
</Tooltip>
</Row>
<Row style={{ padding: "0 10px" }}>
Expand Down
5 changes: 4 additions & 1 deletion src/admin/formComponents/ObjectFieldTemplate.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import RenderSortable from "./RenderSortable";
import update from "immutability-helper";
import { useDispatch } from "react-redux";
import { updateUiSchemaByPath } from "../../store/schemaWizard";
import { isEqual } from "lodash-es";

const ObjectFieldTemplate = ({
properties,
Expand Down Expand Up @@ -73,7 +74,9 @@ const ObjectFieldTemplate = ({
card.prop = properties[index];
});
}
setCards(newCards);
if (!isEqual(newCards, cards)) {
setCards(newCards);
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
10 changes: 0 additions & 10 deletions src/admin/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,6 @@ export const SIZE_OPTIONS = {
xlarge: 24,
};

export const initSchemaStructure = (name = "New schema", description = "") => ({
schema: {
title: name,
description: description,
type: "object",
properties: {},
},
uiSchema: {},
});

let _addErrors = (errors, path) => {
errors.addError({ schema: path.schema, uiSchema: path.uiSchema });

Expand Down
102 changes: 91 additions & 11 deletions src/exposed.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { DndProvider } from "react-dnd";
import { MultiBackend } from "react-dnd-multi-backend";
import { HTML5toTouch } from "rdndmb-html5-to-touch";
import { initSchemaStructure, combineFieldTypes } from "./admin/utils";
import { combineFieldTypes } from "./admin/utils";
import CustomizationContext from "./contexts/CustomizationContext";
import { ConfigProvider, ThemeConfig } from "antd";
import { Provider } from "react-redux";
import store from "./store/configureStore";
import fieldTypes from "./admin/utils/fieldTypes";
import { ReactNode } from "react";
import { ReactNode, SetStateAction } from "react";
import { RJSFSchema } from "@rjsf/utils";
import { SchemaWizardState, schemaInit } from "./store/schemaWizard";
import {
SchemaWizardState,
initialState,
schemaInit,
} from "./store/schemaWizard";
import StateSynchronizer from "./StateSynchronizer";
import { isEqual, pick } from "lodash-es";
import { itemKeyGenerator } from "./utils";

type FormuleContextProps = {
children: ReactNode;
Expand Down Expand Up @@ -73,24 +79,98 @@ export const FormuleContext = ({
);
};

// TODO: Review typing (here and in the actions file)
export const initFormuleSchema = (
data?: RJSFSchema,
name?: string,
title?: string,
description?: string,
) => {
const { deposit_schema, deposit_options, ...configs } = data || {};
// eslint-disable-next-line prefer-const
let { schema, uiSchema, ...configs } = data || {};
if (!configs || !configs.config || !configs.config.name) {
configs = {
...configs,
config: {
name: itemKeyGenerator(),
},
};
}
store.dispatch(
schemaInit({
data:
deposit_schema && deposit_options
? { schema: deposit_schema, uiSchema: deposit_options }
: initSchemaStructure(name, description),
configs: configs || { fullname: name },
data: {
schema: schema || {
...initialState.current.schema,
...(title && { title }),
...(description && { description }),
},
uiSchema: uiSchema || initialState.current.uiSchema,
},
configs,
}),
);
};

export const getFormuleState = () => {
return store.getState().schemaWizard;
};

export const getAllFromLocalStorage = () => {
return Object.entries(localStorage)
.filter(([k, v]) => {
if (k.startsWith("formuleForm_")) {
try {
JSON.parse(v);
return true;
} catch (error) {
console.error("Error parsing formuleForm JSON: ", error);
return false;
}
}
return false;
})
.map(([k, v]) => ({ key: k, value: JSON.parse(v) }));
};

const handleStorageEvent = (resolve) => {
resolve(getAllFromLocalStorage());
window.removeEventListener("storage", handleStorageEvent);
};

const storagePromise = (func) => {
return new Promise<SetStateAction<{ key: string; value: object }[]>>(
(resolve) => {
window.addEventListener("storage", () => handleStorageEvent(resolve));
func();
window.dispatchEvent(new Event("storage"));
},
);
};

export const saveToLocalStorage = () => {
return storagePromise(() => {
const formuleState = store.getState().schemaWizard;
const localStorageKey = `formuleForm_${formuleState.config.name}`;
localStorage.setItem(
localStorageKey,
JSON.stringify({ ...formuleState.current, config: formuleState.config }),
);
});
};

export const deleteFromLocalStorage = (key: string) => {
return storagePromise(() => localStorage.removeItem(key));
};

export const loadFromLocalStorage = (key: string) => {
const localState = JSON.parse(
localStorage.getItem(`formuleForm_${key}`) || "{}",
);
initFormuleSchema(localState);
};

export const isUnsaved = () => {
const state = store.getState().schemaWizard;
const localState = JSON.parse(
localStorage.getItem(`formuleForm_${state.config.name}`) ?? "{}",
);
return !isEqual(state.current, pick(localState, ["schema", "uiSchema"]));
};
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export { initFormuleSchema } from "./exposed";
export { getFormuleState } from "./exposed";
export { FormuleContext } from "./exposed";
export { getAllFromLocalStorage } from "./exposed";
export { saveToLocalStorage } from "./exposed";
export { deleteFromLocalStorage } from "./exposed";
export { loadFromLocalStorage } from "./exposed";
export { isUnsaved } from "./exposed";

export { default as PropertyEditor } from "./admin/components/PropertyEditor";
export { default as SelectFieldType } from "./admin/components/SelectFieldType";
Expand Down
23 changes: 22 additions & 1 deletion src/store/configureStore.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import schemaWizard from "./schemaWizard";
import schemaWizard, { initialState } from "./schemaWizard";
import { configureStore } from "@reduxjs/toolkit";

const preloadedState = () => {
let parsedData;
try {
parsedData = JSON.parse(localStorage.getItem("formuleCurrent"));
} catch (error) {
console.error("Error parsing formuleCurrent from localStorage: ", error);
}
return parsedData || { schemaWizard: initialState };
};

export const persistMiddleware = ({ getState }) => {
return (next) => (action) => {
const result = next(action);
localStorage.setItem("formuleCurrent", JSON.stringify(getState()));
return result;
};
};

const store = configureStore({
reducer: {
schemaWizard,
},
preloadedState: preloadedState(),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(persistMiddleware),
});

export default store;
23 changes: 15 additions & 8 deletions src/store/schemaWizard.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { createSlice } from "@reduxjs/toolkit";
import { notification } from "antd";
import { set, get } from "lodash-es";
import { findParentPath } from "../utils";
import { findParentPath, itemKeyGenerator } from "../utils";
import type { PayloadAction } from "@reduxjs/toolkit";

const initialState = {
export const initialState = {
current: {
schema: {},
schema: {
title: "New schema",
description: "",
type: "object",
properties: {},
},
uiSchema: {},
},
initial: {
schema: {},
uiSchema: {},
},
initialConfig: {},
config: {},
config: {
name: itemKeyGenerator(),
},
field: {},
formData: {},
propKeyEditor: null,
Expand All @@ -31,12 +38,12 @@ const schemaWizard = createSlice({
reducers: {
schemaInit(state, action: PayloadAction<{ data; configs }>) {
const { data, configs } = action.payload;
Object.assign(state, initialState);
state["current"] = data;
state["initial"] = data;
state["config"] = configs;
state["config"] = configs.config;
state["version"] = configs.version;
state["initialConfig"] = configs;
state["loader"] = false;
state["initialConfig"] = configs.config;
},
enableCreateMode(state) {
state["field"] = {};
Expand Down Expand Up @@ -69,7 +76,7 @@ const schemaWizard = createSlice({
let _path = schemaPath;
let _uiPath = uiSchemaPath;

const random_name = `item_${Math.random().toString(36).substring(2, 8)}`;
const random_name = `item_${itemKeyGenerator()}`;

if (schema.type) {
if (schema.type == "object") {
Expand Down
3 changes: 3 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ export const findParentPath = (schemaPath) => {
}
return [];
};

export const itemKeyGenerator = () =>
Math.random().toString(36).substring(2, 8);

0 comments on commit 1d36a83

Please sign in to comment.