From 6d7f54620a3597a1ccf46e25b0aeb2c366b1105c Mon Sep 17 00:00:00 2001 From: Miguel Garcia Garcia Date: Thu, 23 May 2024 17:37:33 +0200 Subject: [PATCH] feat(codeeditor): add diff viewer, types, schema viewer to demo --- README.md | 14 +- formule-demo/src/App.tsx | 193 +++++++++++++------ src/exposed.tsx | 14 +- src/index.ts | 17 +- src/store/schemaWizard.ts | 2 + src/utils/CodeDiffViewer.tsx | 72 +++++++ src/utils/{CodeEditor.jsx => CodeEditor.tsx} | 23 ++- src/utils/{CodeViewer.jsx => CodeViewer.tsx} | 33 ++-- src/utils/index.js | 15 ++ 9 files changed, 289 insertions(+), 94 deletions(-) create mode 100644 src/utils/CodeDiffViewer.tsx rename src/utils/{CodeEditor.jsx => CodeEditor.tsx} (67%) rename src/utils/{CodeViewer.jsx => CodeViewer.tsx} (72%) diff --git a/README.md b/README.md index 6bd1a13..425d39e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

-

🕹️ DEMO

+

🕹️ DEMO 🕹️

[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) @@ -32,9 +32,15 @@ Formule consists of the following main components: It also exports the following functions: -- **`initFormuleSchema`**: Inits the JSONSchema, _needs_ to be run on startup. +- **`initFormuleSchema`**: Inits the JSONSchema, **_needs_** to be run on startup. - **`getFormuleState`**: Formule has its own internal redux state. You can retrieve it at any moment if you so require for more advanced use cases. If you want to continuosly synchronize the Formule state in your app, you can pass a callback function to FormuleContext instead (see below), which will be called every time the form state changes. +And the following utilities: + +- **`CodeEditor`**: Useful if you want to edit the JSON schemas (or any other code) manually. +- **`CodeViewer`**: Useful if you want to visualize the JSON schemas that are being generated (as you can see in the demo). +- **`CodeDiffViewer`**: Useful if you want to compare two different JSON schemas, for example to see the changes since the last save. + ### Field types Formule includes a variety of predefined field types, grouped in three categories: @@ -46,7 +52,7 @@ Formule includes a variety of predefined field types, grouped in three categorie - `Accordion`: When containing a `List`, it works as a `List` with collapsible entries. - `Layer`: When containing a `List`, it works as a `List` whose entries will open in a dialog window. - `Tab`: It's commonly supposed to be used as a wrapper around the rest of the elements. You will normally want to add an `Object` inside and you can use it to separate the form in different pages or sections. -- **Advanced fields**: More complex or situational fields such as `URI`, `Rich/Latex editor`, `Tags` and `ID Fetcher`. +- **Advanced fields**: More complex or situational fields such as `URI`, `Rich/Latex editor`, `Tags`, `ID Fetcher` and `Code Editor`. You can freely remove some of these predefined fields and add your own custom fields and widgets following the JSON Schema specifications. More details below. @@ -126,4 +132,4 @@ Alternatively, you can pull the current state on demand by calling `getFormuleSt ## :space_invader: Local demo & how to contribute -You can also clone the repo and run `formule-demo` to play around. Follow the instructions in its [README](./formule-demo/README.md): it will explain how to install `react-formule` as a local dependency (with either `yarn link` or, better, `yalc`) so that you can modify Formule and test the changes live in your host app, which will be ideal if you want to troubleshoot or contribute to the project. +Apart from trying the online [demo](https://cern-sis.github.io/react-formule/) you can clone the repo and run `formule-demo` to play around. Follow the instructions in its [README](./formule-demo/README.md): it will explain how to install `react-formule` as a local dependency (with either `yarn link` or, better, `yalc`) so that you can modify Formule and test the changes live in your host app, which will be ideal if you want to troubleshoot or contribute to the project. diff --git a/formule-demo/src/App.tsx b/formule-demo/src/App.tsx index 755d8f7..1cf5d1c 100644 --- a/formule-demo/src/App.tsx +++ b/formule-demo/src/App.tsx @@ -1,9 +1,15 @@ -import { FormuleContext, SelectOrEdit } from "react-formule"; -import { SchemaPreview } from "react-formule"; -import { FormPreview } from "react-formule"; -import { initFormuleSchema } from "react-formule"; -import { useEffect } from "react"; -import { Row, Col, Layout, Space, Typography } from "antd"; +import { FileTextOutlined } from "@ant-design/icons"; +import { Col, FloatButton, Layout, Modal, Row, Space, Typography } from "antd"; +import { useEffect, useState } from "react"; +import { + CodeViewer, + FormPreview, + FormuleContext, + SchemaPreview, + SchemaWizardState, + SelectOrEdit, + initFormuleSchema, +} from "react-formule"; import { theme } from "./theme"; import "./style.css"; @@ -15,64 +21,127 @@ function App() { initFormuleSchema(); }, []); + const [formuleState, setFormuleState] = useState(); + const [modalOpen, setModalOpen] = useState(false); + + const handleFormuleStateChange = (newState: SchemaWizardState) => { + setFormuleState(newState); + }; + return ( - - - - - - - - - - - - - - - - -
- - - - Running react-formule v{import.meta.env.REACT_FORMULE_VERSION} - - + <> + setModalOpen(false)} + width={1000} + footer={null} + > + + + Schema + + + + UI Schema + + -
-
+ + + + + + + + + + + + + + + + + +
+ + + + Running react-formule v{import.meta.env.REACT_FORMULE_VERSION} + + + +
+
+ setModalOpen(true)} + shape="square" + description={ +
+ View generated schemas +
+ } + style={{ width: "200px" }} + type="primary" + /> + ); } diff --git a/src/exposed.tsx b/src/exposed.tsx index a9ad7be..015d378 100644 --- a/src/exposed.tsx +++ b/src/exposed.tsx @@ -6,9 +6,9 @@ import { ConfigProvider, ThemeConfig } from "antd"; import { Provider } from "react-redux"; import store from "./store/configureStore"; import fieldTypes from "./admin/utils/fieldTypes"; -import { FC, ReactNode } from "react"; +import { ReactNode } from "react"; import { RJSFSchema } from "@rjsf/utils"; -import { schemaInit } from "./store/schemaWizard"; +import { SchemaWizardState, schemaInit } from "./store/schemaWizard"; import StateSynchronizer from "./StateSynchronizer"; type FormuleContextProps = { @@ -17,11 +17,11 @@ type FormuleContextProps = { customFields?: object; customWidgets?: object; theme?: ThemeConfig; - synchronizeState?: (state: string) => void; + synchronizeState?: (state: SchemaWizardState) => void; transformSchema?: (schema: object) => object; }; -export const FormuleContext: FC = ({ +export const FormuleContext = ({ children, customFieldTypes, customFields, @@ -29,7 +29,7 @@ export const FormuleContext: FC = ({ theme, synchronizeState, transformSchema = (schema) => schema, -}) => { +}: FormuleContextProps) => { const content = synchronizeState ? ( {children} @@ -63,7 +63,7 @@ export const FormuleContext: FC = ({ export const initFormuleSchema = ( data?: RJSFSchema, name?: string, - description?: string + description?: string, ) => { const { deposit_schema, deposit_options, ...configs } = data || {}; store.dispatch( @@ -73,7 +73,7 @@ export const initFormuleSchema = ( ? { schema: deposit_schema, uiSchema: deposit_options } : initSchemaStructure(name, description), configs: configs || { fullname: name }, - }) + }), ); }; diff --git a/src/index.ts b/src/index.ts index b580a04..517db1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ -export { initFormuleSchema } from "./exposed" -export { getFormuleState } from "./exposed" -export { FormuleContext } from "./exposed" +export { initFormuleSchema } from "./exposed"; +export { getFormuleState } from "./exposed"; +export { FormuleContext } from "./exposed"; export { default as PropertyEditor } from "./admin/components/PropertyEditor"; export { default as SelectFieldType } from "./admin/components/SelectFieldType"; @@ -10,4 +10,13 @@ export { default as SelectOrEdit } from "./admin/components/SelectOrEdit"; export { default as FormuleForm } from "./forms/Form"; -export { common as commonFields, extra as extraFields } from "./admin/utils/fieldTypes" \ No newline at end of file +export { + common as commonFields, + extra as extraFields, +} from "./admin/utils/fieldTypes"; + +export { default as CodeEditor } from "./utils/CodeEditor"; +export { default as CodeViewer } from "./utils/CodeViewer"; +export { default as CodeDiffViewer } from "./utils/CodeDiffViewer"; + +export type { SchemaWizardState } from "./store/schemaWizard"; diff --git a/src/store/schemaWizard.ts b/src/store/schemaWizard.ts index f742860..1501251 100644 --- a/src/store/schemaWizard.ts +++ b/src/store/schemaWizard.ts @@ -23,6 +23,8 @@ const initialState = { version: null, }; +export type SchemaWizardState = typeof initialState; + const schemaWizard = createSlice({ name: "schemaWizard", initialState, diff --git a/src/utils/CodeDiffViewer.tsx b/src/utils/CodeDiffViewer.tsx new file mode 100644 index 0000000..eea4c55 --- /dev/null +++ b/src/utils/CodeDiffViewer.tsx @@ -0,0 +1,72 @@ +import { MutableRefObject, useEffect, useRef } from "react"; +import { basicSetup } from "codemirror"; +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { MergeView } from "@codemirror/merge"; +import { CODEMIRROR_LANGUAGES } from "."; + +type CodeDiffViewerProps = { + left: string; + right: string; + lang?: string; + height?: string; +}; + +const CodeDiffViewer = ({ left, right, lang, height }: CodeDiffViewerProps) => { + const editorRef = useRef() as MutableRefObject; + + useEffect(() => { + editorRef.current.innerHTML = ""; + + const extensions = [ + basicSetup, + EditorState.readOnly.of(true), + EditorView.theme({ + "&": { + width: "100%", + height: "100%", + }, + }), + lang && lang in CODEMIRROR_LANGUAGES ? CODEMIRROR_LANGUAGES[lang] : [], + ]; + + const leftExtensions = [ + EditorView.theme({ + ".cm-changedLine": { + "background-color": "#ffeded !important", + }, + ".cm-changedText": { + "background-color": "#ffbaba !important", + }, + }), + ]; + + const rightExtensions = [ + EditorView.theme({ + ".cm-changedLine": { + "background-color": "#e8fbe8 !important", + }, + ".cm-changedText": { + "background-color": "#a6f1a6 !important", + }, + }), + ]; + + new MergeView({ + a: { + doc: left, + extensions: [...extensions, ...leftExtensions], + }, + b: { + doc: right, + extensions: [...extensions, ...rightExtensions], + }, + parent: editorRef.current, + collapseUnchanged: {}, + }); + }, [lang, left, right]); + + return
; +}; + +export default CodeDiffViewer; diff --git a/src/utils/CodeEditor.jsx b/src/utils/CodeEditor.tsx similarity index 67% rename from src/utils/CodeEditor.jsx rename to src/utils/CodeEditor.tsx index fb100fa..d529cc3 100644 --- a/src/utils/CodeEditor.jsx +++ b/src/utils/CodeEditor.tsx @@ -2,6 +2,22 @@ import { EditorView, keymap } from "@codemirror/view"; import { linter, lintGutter } from "@codemirror/lint"; import { indentWithTab } from "@codemirror/commands"; import CodeViewer from "./CodeViewer"; +import { CODEMIRROR_LINTERS } from "."; + +type CodeEditorProps = { + initialValue: string; + lang?: string; + lint?: string; + isEditable?: boolean; + isReadOnly?: boolean; + handleEdit: (value: string) => void; + schema?: object; + height?: string; + extraExtensions?: []; + reset?: boolean; + minimal?: boolean; + validationSchema?: object; +}; const CodeEditor = ({ initialValue, @@ -16,10 +32,12 @@ const CodeEditor = ({ reset, minimal, validationSchema, -}) => { +}: CodeEditorProps) => { const editorExtensions = [ keymap.of([indentWithTab]), - lint ? [linter(lint()), lintGutter()] : [], + lint && lint in CODEMIRROR_LINTERS + ? [linter(CODEMIRROR_LINTERS[lint]), lintGutter()] + : [], EditorView.updateListener.of((update) => { if (update.docChanged) { handleEdit(update.state.doc.toString()); @@ -37,7 +55,6 @@ const CodeEditor = ({ extraExtensions={editorExtensions} schema={schema} height={height} - listener={handleEdit} reset={reset} minimal={minimal} validationSchema={validationSchema} diff --git a/src/utils/CodeViewer.jsx b/src/utils/CodeViewer.tsx similarity index 72% rename from src/utils/CodeViewer.jsx rename to src/utils/CodeViewer.tsx index b7c4705..ede5fb0 100644 --- a/src/utils/CodeViewer.jsx +++ b/src/utils/CodeViewer.tsx @@ -1,24 +1,28 @@ -import { useEffect, useMemo, useRef } from "react"; +import { MutableRefObject, useEffect, useMemo, useRef } from "react"; import { basicSetup, minimalSetup } from "codemirror"; -import { EditorState } from "@codemirror/state"; +import { EditorState, Extension } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; import { theme } from "antd"; import { updateSchema, jsonSchema } from "codemirror-json-schema"; -import { StreamLanguage } from "@codemirror/language"; -import { jinja2 } from "@codemirror/legacy-modes/mode/jinja2"; -import { json } from "@codemirror/lang-json"; -import { stex } from "@codemirror/legacy-modes/mode/stex"; +import { CODEMIRROR_LANGUAGES } from "."; -const LANGUAGES = { - json: json(), - jinja: StreamLanguage.define(jinja2), - stex: StreamLanguage.define(stex), +type CodeViewerProps = { + value: string; + lang?: string; + isEditable?: boolean; + isReadOnly?: boolean; + extraExtensions?: Extension[]; + height?: string; + schema?: object; + reset?: boolean; + minimal?: boolean; + validationSchema?: object; }; const CodeViewer = ({ value, lang, - isEditable = true, + isEditable = false, isReadOnly, extraExtensions = [], height, @@ -26,8 +30,8 @@ const CodeViewer = ({ reset, minimal, validationSchema, -}) => { - const editorRef = useRef(null); +}: CodeViewerProps) => { + const editorRef = useRef() as MutableRefObject; const { token } = theme.useToken(); @@ -49,7 +53,7 @@ const CodeViewer = ({ borderColor: token.colorPrimary, }, }), - lang && lang in LANGUAGES ? LANGUAGES[lang] : [], + lang && lang in CODEMIRROR_LANGUAGES ? CODEMIRROR_LANGUAGES[lang] : [], ...extraExtensions, validationSchema ? jsonSchema() : [], ], @@ -76,6 +80,7 @@ const CodeViewer = ({ extensions: extensions, }), parent: editorRef.current, + scrollTo: EditorView.scrollIntoView(0), }); updateSchema(editor, validationSchema); } diff --git a/src/utils/index.js b/src/utils/index.js index e8827fc..13f16a7 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,6 +1,21 @@ +import { json, jsonParseLinter } from "@codemirror/lang-json"; +import { StreamLanguage } from "@codemirror/language"; +import { jinja2 } from "@codemirror/legacy-modes/mode/jinja2"; +import { stex } from "@codemirror/legacy-modes/mode/stex"; + export const URL_REGEX = "https?://(www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)"; +export const CODEMIRROR_LANGUAGES = { + json: json(), + jinja: StreamLanguage.define(jinja2), + stex: StreamLanguage.define(stex), +}; + +export const CODEMIRROR_LINTERS = { + json: jsonParseLinter(), +}; + export const stringToHslColor = (str, s, l) => { let hash = 0; for (let i = 0; i < str.length; i++) {