diff --git a/formule-demo/src/App.tsx b/formule-demo/src/App.tsx index 29d7c8d..7d94f05 100644 --- a/formule-demo/src/App.tsx +++ b/formule-demo/src/App.tsx @@ -1,5 +1,32 @@ -import { FileTextOutlined } from "@ant-design/icons"; -import { Col, FloatButton, Layout, Modal, Row, Space, Typography } from "antd"; +import { + FileTextOutlined, + FolderOpenOutlined, + DeleteOutlined, + SaveOutlined, + FileAddOutlined, + CheckOutlined, + InfoCircleOutlined, + DownloadOutlined, + UploadOutlined, + RollbackOutlined, + ToolOutlined, +} from "@ant-design/icons"; +import { + Button, + Col, + Drawer, + FloatButton, + Layout, + List, + message, + Modal, + Popconfirm, + Row, + Space, + Tooltip, + Typography, + Upload, +} from "antd"; import { useEffect, useState } from "react"; import { CodeViewer, @@ -8,7 +35,12 @@ import { SchemaPreview, SchemaWizardState, SelectOrEdit, + deleteFromLocalStorage, + getAllFromLocalStorage, initFormuleSchema, + isUnsaved, + saveToLocalStorage, + loadFromLocalStorage, } from "react-formule"; import { theme } from "./theme"; @@ -17,23 +49,70 @@ import "./style.css"; const { Content, Footer } = Layout; function App() { + const [formuleState, setFormuleState] = useState(); + const [localSchemas, setLocalSchemas] = useState(getAllFromLocalStorage()); + const [viewerOpen, setViewerOpen] = useState(false); + const [listOpen, setListOpen] = useState(false); + const [helpOpen, setHelpOpen] = useState(false); + const [justSaved, setJustSaved] = useState(false); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [openFloatButtons, setOpenFloatButtons] = useState(true); + useEffect(() => { - initFormuleSchema(); - }, []); + setHasUnsavedChanges(isUnsaved()); + }, [formuleState]); - const [formuleState, setFormuleState] = useState(); - const [modalOpen, setModalOpen] = useState(false); + useEffect(() => { + const handleKeyDowEvent = (e: KeyboardEvent) => { + if (e.ctrlKey || (e.metaKey && e.key === "s")) { + e.preventDefault(); + saveLocalSchema(); + } + }; + window.addEventListener("keydown", handleKeyDowEvent); + return () => { + window.removeEventListener("keydown", handleKeyDowEvent); + }; + }, []); const handleFormuleStateChange = (newState: SchemaWizardState) => { setFormuleState(newState); }; + const handleDownload = (key: string, schema: object) => { + const a = document.createElement("a"); + const file = new Blob([JSON.stringify(schema, null, 4)], { + type: "text/json", + }); + a.href = URL.createObjectURL(file); + a.download = `${key}.json`; + a.click(); + }; + + const deleteLocalSchema = (key: string) => { + deleteFromLocalStorage(key).then((list) => setLocalSchemas(list)); + if (key === `formuleForm_${formuleState?.config?.name}`) { + initFormuleSchema(); + } + }; + + const saveLocalSchema = () => { + saveToLocalStorage().then((list) => { + setHasUnsavedChanges(isUnsaved()); + setLocalSchemas(list); + setJustSaved(true); + setTimeout(() => { + setJustSaved(false); + }, 500); + }); + }; + return ( <> setModalOpen(false)} + open={viewerOpen} + onCancel={() => setViewerOpen(false)} width={1000} footer={null} > @@ -85,6 +164,159 @@ function App() { + setHelpOpen(false)} + width={500} + footer={null} + > + + The state of the current form is saved automatically on + change. However, this should simply be taken as a security measure to + prevent your from accidentally losing data, as changes are not + autosaved into your local schemas repository. + + + If you want to persist the local changes to your schema in your local + repository (local storage) you can{" "} + click on the save button or press [ctrl+s] (your saved + schema won't be updated with the latest changes until you do this). + You can switch between schemas and load them later on. You can tell + when you have unsaved changes by looking at the blue dot{" "} + on the save button. You can also revert unsaved changes by clicking on + the corresponding button. + + + You are encouraged to download any important data as a + JSON file for more safety in case your browser's storage gets wiped. + You can do so from the "Saved Schemas" drawer. + + + Additionally, you can also upload schemas from a JSON + file in the same drawer. + + + setListOpen(false)} + width={600} + footer={ + + The schemas displayed here are stored in your browser's local + storage. They are preserved between sessions unless you wipe your + browser's storage. Please download your schemas if you don't + want to risk losing them. + + } + placement="left" + extra={ + + + { + if (file.type != "application/json") { + message.error("The file format should be json"); + } + const reader = new FileReader(); + reader.onload = (event) => { + const newSchema = JSON.parse( + event?.target?.result as string, + ); + const { schema, uiSchema } = newSchema; + if (schema && uiSchema) { + initFormuleSchema(newSchema); + saveLocalSchema(); + message.success("Uploaded and loaded successfully"); + } else { + message.error( + "Your json should include a schema and a uiSchema key", + ); + } + }; + reader.readAsText(file); + // Prevent POST upload + return false; + }} + > + + + )} + + } + > +
+ + Make sure you have saved your progress in the current schema + before switching to another one! + +
+ ( + + Delete schema permanently? + + } + okType="danger" + placement="right" + onConfirm={() => deleteLocalSchema(item.key)} + > + , + ]} + > + +
+ + {item.value.config.name} + +
+
+ )} + /> +
- + - setModalOpen(true)} - shape="square" - description={ -
- View generated schemas -
- } - style={{ width: "200px" }} - type="primary" - /> + + setOpenFloatButtons(!openFloatButtons)} + icon={} + type="primary" + badge={ + !openFloatButtons && hasUnsavedChanges + ? { + dot: true, + color: "firebrick", + } + : undefined + } + > + {hasUnsavedChanges && + localSchemas.some( + (s) => s.key === `formuleForm_${formuleState?.config?.name}`, + ) && ( + + Revert and discard the current changes? + + } + okType="danger" + placement="right" + onConfirm={() => { + loadFromLocalStorage(formuleState?.config?.name ?? ""); + }} + > + } + tooltip="Revert changes (reload save)" + /> + + )} + + You will lose the current changes + + } + okType="danger" + placement="right" + onConfirm={() => initFormuleSchema()} + > + } tooltip="New schema" /> + + saveLocalSchema()} + icon={justSaved ? : } + style={ + justSaved + ? { backgroundColor: theme.token.colorPrimary } + : undefined + } + badge={ + hasUnsavedChanges + ? { + dot: true, + color: theme.token.colorPrimary, + } + : undefined + } + tooltip="Save schema to local repository [ctrl+s]" + /> + setListOpen(true)} + icon={} + badge={{ + count: localSchemas.length, + style: { borderRadius: theme.token.borderRadius }, + color: theme.token.colorPrimary, + size: "small", + }} + tooltip="Load schema from local repository" + /> + setViewerOpen(true)} + icon={} + tooltip="View generated schemas" + /> + setHelpOpen(true)} + icon={} + tooltip="Information" + /> + + ); } diff --git a/src/admin/components/SchemaPreview.jsx b/src/admin/components/SchemaPreview.jsx index 929652f..1f04f6e 100644 --- a/src/admin/components/SchemaPreview.jsx +++ b/src/admin/components/SchemaPreview.jsx @@ -5,11 +5,11 @@ 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 (
@@ -17,11 +17,17 @@ const SchemaPreview = () => { Schema tree + {!hideSchemaKey && ( + {name} + )} { align="middle" style={{ padding: "0 10px" }} > - + {(schema && schema.title) || "root"} -