From 6419ae1213534660a8d76fe9a89128f2fe4758b7 Mon Sep 17 00:00:00 2001 From: Miguel Garcia Garcia Date: Mon, 22 Jan 2024 13:44:56 +0100 Subject: [PATCH 1/2] run lint-staged with yarn --- .husky/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af219..d2ae35e 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx lint-staged +yarn lint-staged From 3bc643756e11af8135c40ec213b4bc4b1b1ba842 Mon Sep 17 00:00:00 2001 From: Miguel Garcia Garcia Date: Mon, 22 Jan 2024 13:47:53 +0100 Subject: [PATCH 2/2] make dnd optional --- README.md | 52 +++--- formule-demo/src/App.tsx | 26 +-- src/admin/components/SelectFieldModal.jsx | 22 +++ src/admin/components/SelectFieldType.jsx | 85 ++++++--- .../formComponents/ArrayFieldTemplate.jsx | 14 +- src/admin/formComponents/FieldTemplate.jsx | 63 +++++-- .../formComponents/ObjectFieldTemplate.jsx | 80 +++++---- .../formComponents/RenderFieldWithArrows.jsx | 38 ++++ src/admin/formComponents/SchemaTreeItem.jsx | 164 +++++++++++------- src/exposed.tsx | 7 +- 10 files changed, 369 insertions(+), 182 deletions(-) create mode 100644 src/admin/components/SelectFieldModal.jsx create mode 100644 src/admin/formComponents/RenderFieldWithArrows.jsx diff --git a/README.md b/README.md index b840cca..42f4c03 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Formule is a **powerful, user-friendly, extensible and mobile-friendly form buil It originated from the need of a flexible tool for physicists at CERN to create their custom forms in the [CERN Analysis Preservation](https://github.com/cernanalysispreservation/analysispreservation.cern.ch) application (a process that was originally done by the CAP team who had to manually define the JSON schemas for every member experiment) in a zero-code fashion. This tool proved to be very useful for us to more easily scalate and expand, reaching a wider audience here at CERN. So, we thought it could also be useful for other people and decided to decouple it from CAP and release it as an open source library. ->[!WARNING] ->react-formule has just come out and is undergoing active development, so please feel free to share any issue you find with us and/or to contribute! +> [!WARNING] +> react-formule has just come out and is undergoing active development, so please feel free to share any issue you find with us and/or to contribute! ## :carousel_horse: How it looks like @@ -21,19 +21,20 @@ Formule consists of the following main components: - **`FormuleContext`**: Formule components need to be wrapped by a FormuleContext. It also allows you to provide an antd theme and your own custom fields and widgets. - The form editor, which has been split into three different components that work together for more flexibility: - - **`SelectOrEdit`** (or, separately, **`SelectFieldType`** and **`PropertyEditor`**): You can select fields to add to the form and customize their properties. - - **`SchemaPreview`**: A tree view of the fields where you can rearrange or select fields to be edited. - - **`FormPreview`**: A live, iteractive preview of the form. + - **`SelectOrEdit`** (or, separately, **`SelectFieldType`** and **`PropertyEditor`**): You can select fields to add to the form and customize their properties. + - **`SchemaPreview`**: A tree view of the fields where you can rearrange or select fields to be edited. + - **`FormPreview`**: A live, iteractive preview of the form. - **`FormuleForm`**: You can use it to display a form (JSON Schema) generated by Formule. 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. ### Field types Formule includes a variety of predefined field types, grouped in three categories: + - **Simple fields**: `Text`, `Text area`, `Number`, `Checkbox`, `Switch`, `Radio`, `Select` and `Date` fields. - **Collections**: - `Object`: Use it of you want to group fields or to add several of them inside of a `List`. @@ -45,11 +46,12 @@ Formule includes a variety of predefined field types, grouped in three categorie 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. -All of these items contain different settings that you can tinker with, separated into **Schema Settings** (*generally* affecting how the field *works*) and **UI Schema Settings** (*generally* affecting how the field *looks* like). +All of these items contain different settings that you can tinker with, separated into **Schema Settings** (_generally_ affecting how the field _works_) and **UI Schema Settings** (_generally_ affecting how the field _looks_ like). ## :horse_racing: Setting it up ### Installation + ```sh npm install react-formule # or @@ -57,11 +59,12 @@ yarn add react-formule ``` ### Basic setup + ```jsx -import { - FormuleContext, - SelectOrEdit, - SchemaPreview, +import { + FormuleContext, + SelectOrEdit, + SchemaPreview, FormPreview, initFormuleSchema } from "react-formule"; @@ -75,7 +78,10 @@ const useEffect(() => initFormuleSchema(), []); ``` +If you want to disable the [DnD](https://github.com/react-dnd/react-dnd) functionality, you can pass `dnd={false}` to `FormuleContext`. This will enable an alternative, button-based method to add and move fields. + ### Customizing and adding new field types + ```jsx // ... @@ -86,26 +92,25 @@ If you use Formule to edit existing JSON schemas that include extra fields (e.g. ```jsx const transformSchema = (schema) => { - // Remove properties... - return transformedSchema -} + // Remove properties... + return transformedSchema; +}; - -// ... - +// ...; ``` ### Syncing Formule state + If you want to run some logic in your application every time the current Formule state changes in any way (e.g. to run some action every time a new field is added to the form) you can pass a function to be called back when that happens: ```jsx -const handleFormuleStateChange = newState => { - // Do something when the state changes -} +const handleFormuleStateChange = (newState) => { + // Do something when the state changes +}; -// ... - + // ... +; ``` Alternatively, you can pull the current state on demand by calling `getFormuleState` at any moment. @@ -114,4 +119,5 @@ Alternatively, you can pull the current state on demand by calling `getFormuleSt > For more examples, feel free to browse around the [CERN Analysis Preservation](https://github.com/cernanalysispreservation/analysispreservation.cern.ch) repository, where we use all the features mentioned above. ## :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. \ No newline at end of file + +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. diff --git a/formule-demo/src/App.tsx b/formule-demo/src/App.tsx index ab3d3e7..f252fcf 100644 --- a/formule-demo/src/App.tsx +++ b/formule-demo/src/App.tsx @@ -5,7 +5,7 @@ import { initFormuleSchema } from "react-formule"; import { useEffect } from "react"; import { Row, Col } from "antd"; -import "./style.css" +import "./style.css"; const PRIMARY_COLOR = "#006996"; @@ -15,16 +15,18 @@ function App() { }, []); return ( - + @@ -59,7 +62,6 @@ function App() { - ); } diff --git a/src/admin/components/SelectFieldModal.jsx b/src/admin/components/SelectFieldModal.jsx new file mode 100644 index 0000000..1f569fe --- /dev/null +++ b/src/admin/components/SelectFieldModal.jsx @@ -0,0 +1,22 @@ +import { Button, Modal } from "antd"; +import SelectFieldType from "./SelectFieldType"; + +const SelectFieldModal = ({ visible, setVisible, insertInPath }) => { + return ( + setVisible(false)} + footer={ + + } + width={450} + > + + + ); +}; + +export default SelectFieldModal; diff --git a/src/admin/components/SelectFieldType.jsx b/src/admin/components/SelectFieldType.jsx index 74c07ec..0f3e1aa 100644 --- a/src/admin/components/SelectFieldType.jsx +++ b/src/admin/components/SelectFieldType.jsx @@ -1,14 +1,49 @@ -import { Col, Collapse, Row, Space, Typography } from "antd"; +import { Button, Col, Collapse, Row, Space, Typography } from "antd"; import Draggable from "./Draggable"; import { useContext } from "react"; import CustomizationContext from "../../contexts/CustomizationContext"; +import PlusOutlined from "@ant-design/icons/PlusOutlined"; +import { useDispatch } from "react-redux"; +import { addByPath } from "../../store/schemaWizard"; -const SelectFieldType = () => { - - const customizationContext = useContext(CustomizationContext) +const SelectFieldType = ({ insertInPath }) => { + const dispatch = useDispatch(); + + const customizationContext = useContext(CustomizationContext); + + const DraggableOrNot = ({ index, type, objectKey, children }) => { + if (customizationContext.dnd && !insertInPath) { + return ( + + {children} + + ); + } else { + return ( + + {children} + + + ) + )} + + ); + // If it's the actual root if (formContext.schema.length == 0) { return ( dispatch(addByPath({path, value}))} + addProperty={(path, value) => dispatch(addByPath({ path, value }))} key={id} path={path} shouldHideChildren={shouldBoxHideChildren(uiSchema)} @@ -108,7 +137,7 @@ const FieldTemplate = props => { // The HoverBox wrapper here is needed to allow dropping items into objects // or arrays directly without having to expand them first dispatch(addByPath({path, value}))} + addProperty={(path, value) => dispatch(addByPath({ path, value }))} key={id} path={path} shouldHideChildren={shouldBoxHideChildren(uiSchema)} @@ -130,4 +159,4 @@ FieldTemplate.propTypes = { schema: PropTypes.object, }; -export default FieldTemplate +export default FieldTemplate; diff --git a/src/admin/formComponents/ObjectFieldTemplate.jsx b/src/admin/formComponents/ObjectFieldTemplate.jsx index 72b5dd2..c430fa6 100644 --- a/src/admin/formComponents/ObjectFieldTemplate.jsx +++ b/src/admin/formComponents/ObjectFieldTemplate.jsx @@ -1,14 +1,23 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useContext } from "react"; import PropTypes from "prop-types"; import RenderSortable from "./RenderSortable"; import update from "immutability-helper"; import { useDispatch } from "react-redux"; import { updateUiSchemaByPath } from "../../store/schemaWizard"; - -const ObjectFieldTemplate = ({properties, uiSchema, formContext, idSchema}) => { +import CustomizationContext from "../../contexts/CustomizationContext"; +import RenderFieldWithArrows from "./RenderFieldWithArrows"; + +const ObjectFieldTemplate = ({ + properties, + uiSchema, + formContext, + idSchema, +}) => { const [cards, setCards] = useState([]); - const dispatch = useDispatch() + const dispatch = useDispatch(); + + const customizationContext = useContext(CustomizationContext); useEffect( () => { @@ -34,16 +43,16 @@ const ObjectFieldTemplate = ({properties, uiSchema, formContext, idSchema}) => { // if there is no change with the number of the items it means that either there is a re ordering // or some update at each props data if (propertiesLength === cardsLength) { - let uiCards = cards.map(item => item.name); - let uiProperties = properties.map(item => item.name); + let uiCards = cards.map((item) => item.name); + let uiProperties = properties.map((item) => item.name); let different; - uiProperties.map(item => { + uiProperties.map((item) => { if (!uiCards.includes(item)) { different = item; } }); - const newCards = [...cards] + const newCards = [...cards]; // the different variable will define if there was a change in the prop keys or there is just a re ordering if (different) { @@ -53,7 +62,7 @@ const ObjectFieldTemplate = ({properties, uiSchema, formContext, idSchema}) => { }); let itemProps; - properties.map(item => { + properties.map((item) => { if (item.name === different) itemProps = item; }); @@ -62,39 +71,39 @@ const ObjectFieldTemplate = ({properties, uiSchema, formContext, idSchema}) => { name: different, prop: itemProps, }; - newCards[diffIndex] = item + newCards[diffIndex] = item; } else { newCards.map((card, index) => { card.prop = properties[index]; }); } - setCards(newCards) + setCards(newCards); } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [properties] + [properties], ); - // Updates the uiSchema after the cards update with the new ui:order + // Updates the uiSchema after the cards update with the new ui:order // (so that the form preview displays the correct order) - useEffect( - () => { - let uiCards = cards.map(item => item.name); - let uiProperties = properties.map(item => item.name); - let { ...rest } = uiSchema; + useEffect(() => { + let uiCards = cards.map((item) => item.name); + let uiProperties = properties.map((item) => item.name); + let { ...rest } = uiSchema; - uiCards = uiProperties.length < uiCards.length ? uiProperties : uiCards; + uiCards = uiProperties.length < uiCards.length ? uiProperties : uiCards; - dispatch(updateUiSchemaByPath({ + dispatch( + updateUiSchemaByPath({ path: formContext.uiSchema.length > 0 ? formContext.uiSchema : [], value: { ...rest, "ui:order": [...uiCards, "*"], - } - })); + }, + }), + ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [cards] - ); + }, [cards]); // create a new array to keep track of the changes in the order properties.map((prop, index) => { @@ -117,19 +126,23 @@ const ObjectFieldTemplate = ({properties, uiSchema, formContext, idSchema}) => { if (dragCard) { setCards( update(cards, { - $splice: [[dragIndex, 1], [hoverIndex, 0, dragCard]], - }) + $splice: [ + [dragIndex, 1], + [hoverIndex, 0, dragCard], + ], + }), ); } }, - [cards] + [cards], ); + if (idSchema.$id == "root") { - return ( -
- {cards.map((card, i) => RenderSortable(formContext.uiSchema, card, i, moveCard))} -
- ); + return customizationContext.dnd + ? cards.map((card, i) => + RenderSortable(formContext.uiSchema, card, i, moveCard), + ) + : cards.map((card, i) => RenderFieldWithArrows(card, cards, i, moveCard)); } }; @@ -141,5 +154,4 @@ ObjectFieldTemplate.propTypes = { uiSchema: PropTypes.object, }; - -export default ObjectFieldTemplate +export default ObjectFieldTemplate; diff --git a/src/admin/formComponents/RenderFieldWithArrows.jsx b/src/admin/formComponents/RenderFieldWithArrows.jsx new file mode 100644 index 0000000..16c3248 --- /dev/null +++ b/src/admin/formComponents/RenderFieldWithArrows.jsx @@ -0,0 +1,38 @@ +import { Button, Col, Row } from "antd"; +import ArrowUpOutlined from "@ant-design/icons/ArrowUpOutlined"; +import ArrowDownOutlined from "@ant-design/icons/ArrowDownOutlined"; + +const RenderFieldWithArrows = (card, cards, i, moveCard) => { + if (card === undefined || card.prop === undefined) { + return null; + } + return ( + + {card.prop.content} + + +