From c907212e0582ffee325c91f00deca9a3ac47854d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Dec 2021 14:53:39 +0100 Subject: [PATCH 01/12] Rrndering deferred array --- .../DocumentBuilder.stories.tsx | 75 ++++++++++++++++++- .../documentBuilder/DocumentList.tsx | 73 ++++++++++++++++++ .../documentBuilder/ElementEdit.tsx | 34 ++++++++- .../documentBuilder/allowedElementTypes.ts | 7 +- .../documentBuilder/createNewElement.ts | 3 + .../documentBuilder/documentBuilderTypes.ts | 1 + .../documentBuilder/documentTree.tsx | 31 +++++++- .../documentBuilder/elementEditSchemas.tsx | 10 +++ src/runtime/mapArgs.ts | 2 +- 9 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 src/components/documentBuilder/DocumentList.tsx diff --git a/src/components/documentBuilder/DocumentBuilder.stories.tsx b/src/components/documentBuilder/DocumentBuilder.stories.tsx index 414676297d..a57de2d5ea 100644 --- a/src/components/documentBuilder/DocumentBuilder.stories.tsx +++ b/src/components/documentBuilder/DocumentBuilder.stories.tsx @@ -40,7 +40,80 @@ const DocumentBuilder: React.FC = () => { return ( {({ handleSubmit }) => ( diff --git a/src/components/documentBuilder/DocumentList.tsx b/src/components/documentBuilder/DocumentList.tsx new file mode 100644 index 0000000000..77b6c61150 --- /dev/null +++ b/src/components/documentBuilder/DocumentList.tsx @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2021 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React, { useContext } from "react"; +import DocumentContext from "./DocumentContext"; +import { UnknownObject } from "@/types"; +import { Args, mapArgs } from "@/runtime/mapArgs"; +import { useAsyncState } from "@/hooks/common"; +import { GridLoader } from "react-spinners"; +import { getComponentDefinition } from "./documentTree"; +import { DocumentElement } from "./documentBuilderTypes"; + +type DocumentListProps = { + array: UnknownObject[]; + elementKey?: string; + config: Args; +}; + +const DocumentList: React.FC = ({ + array, + elementKey = "element", + config, +}) => { + const ctxt = useContext(DocumentContext); + console.log("DocumentList", { + array, + elementKey, + config, + }); + const [componentDefinitions, isLoading] = useAsyncState( + async () => + Promise.all( + array.map(async (data) => { + const elementContext = { + ...ctxt, + [`@${elementKey}`]: data, + }; + return (mapArgs(config, elementContext, { + implicitRender: null, + }) as Promise).then((documentElement) => + getComponentDefinition(documentElement) + ); + }) + ), + [array, elementKey, config, ctxt] + ); + + return isLoading ? ( + + ) : ( + <> + {componentDefinitions.map(({ Component, props }, index) => ( + + ))} + + ); +}; + +export default DocumentList; diff --git a/src/components/documentBuilder/ElementEdit.tsx b/src/components/documentBuilder/ElementEdit.tsx index 45531063a6..329c2551e2 100644 --- a/src/components/documentBuilder/ElementEdit.tsx +++ b/src/components/documentBuilder/ElementEdit.tsx @@ -16,7 +16,7 @@ */ import { useField } from "formik"; -import React from "react"; +import React, { useState } from "react"; import { DocumentElement, DocumentElementType } from "./documentBuilderTypes"; import SchemaField from "@/components/fields/schemaFields/SchemaField"; import { getElementEditSchemas } from "./elementEditSchemas"; @@ -25,6 +25,7 @@ import { Row, Col } from "react-bootstrap"; import styles from "./DocumentEditor.module.scss"; import RemoveElementAction from "./RemoveElementAction"; import MoveElementAction from "./MoveElementAction"; +import FieldTemplate from "@/components/form/FieldTemplate"; type ElementEditProps = { elementName: string; @@ -42,6 +43,7 @@ const elementTypeLabels: Record = { text: "Text", button: "Button", block: "Block", + list: "List", }; const ElementEdit: React.FC = ({ @@ -49,9 +51,36 @@ const ElementEdit: React.FC = ({ setActiveElement, }) => { const [{ value: documentElement }] = useField(elementName); + const [{ value: element }, , { setValue: setElement }] = useField( + `${elementName}.config.element` + ); const editSchemas = getElementEditSchemas(documentElement, elementName); + const [val, setVal] = useState(element?.config?.text); + const onElChange: React.ChangeEventHandler = (event) => { + const { value } = event.target; + + const element = { + __type__: "defer", + __value__: { + type: "text", + config: { + text: { + __type__: "var", + __value__: value, + }, + }, + } as DocumentElement, + }; + + setVal(value); + setElement(element); + setTimeout(() => { + console.log("ElementEdit", { documentElement }); + }, 100); + }; + return ( <> @@ -79,6 +108,9 @@ const ElementEdit: React.FC = ({ {editSchemas.map((editSchema) => ( ))} + {documentElement.type === "list" && ( + + )} diff --git a/src/components/documentBuilder/allowedElementTypes.ts b/src/components/documentBuilder/allowedElementTypes.ts index 022cd9ce87..ec39be8730 100644 --- a/src/components/documentBuilder/allowedElementTypes.ts +++ b/src/components/documentBuilder/allowedElementTypes.ts @@ -26,11 +26,12 @@ export const ROOT_ELEMENT_TYPES: DocumentElementType[] = [ "card", "block", "button", + "list", ]; const allowedChildTypes: Record = { - container: ["row"], - row: ["column"], + container: ["row", "list"], + row: ["column", "list"], column: [ "header_1", "header_2", @@ -39,6 +40,7 @@ const allowedChildTypes: Record = { "card", "block", "button", + "list", ], card: [ "header_1", @@ -48,6 +50,7 @@ const allowedChildTypes: Record = { "container", "block", "button", + "list", ], }; diff --git a/src/components/documentBuilder/createNewElement.ts b/src/components/documentBuilder/createNewElement.ts index 790ed6f6bd..5a8b823b1c 100644 --- a/src/components/documentBuilder/createNewElement.ts +++ b/src/components/documentBuilder/createNewElement.ts @@ -59,6 +59,9 @@ export function createNewElement(elementType: DocumentElementType) { element.config.title = "Click me"; break; + case "list": + break; + default: throw new Error( `Can't create new element. Type "${elementType} is not supported.` diff --git a/src/components/documentBuilder/documentBuilderTypes.ts b/src/components/documentBuilder/documentBuilderTypes.ts index a7076d1d74..03a7d00a54 100644 --- a/src/components/documentBuilder/documentBuilderTypes.ts +++ b/src/components/documentBuilder/documentBuilderTypes.ts @@ -28,6 +28,7 @@ export const DOCUMENT_ELEMENT_TYPES = [ "card", "block", "button", + "list", ] as const; export type DocumentElementType = typeof DOCUMENT_ELEMENT_TYPES[number]; diff --git a/src/components/documentBuilder/documentTree.tsx b/src/components/documentBuilder/documentTree.tsx index 4c7cea8ab0..dba0dea777 100644 --- a/src/components/documentBuilder/documentTree.tsx +++ b/src/components/documentBuilder/documentTree.tsx @@ -26,6 +26,7 @@ import DocumentButton from "@/components/documentBuilder/DocumentButton"; import useNotifications from "@/hooks/useNotifications"; import documentTreeStyles from "./documentTree.module.scss"; import cx from "classnames"; +import DocumentList from "@/components/documentBuilder/DocumentList"; const headerComponents = { header_1: "h1", @@ -46,7 +47,7 @@ const UnknownType: React.FC<{ componentType: string }> = ({ export function getComponentDefinition( element: DocumentElement ): DocumentComponent { - const componentType = String(element.type); + const componentType = element.type; const config = get(element, "config", {} as UnknownObject); switch (componentType) { @@ -125,6 +126,21 @@ export function getComponentDefinition( }; } + case "list": { + const props = { + array: config.array, + elementKey: config.elementKey, + config: config.element, + }; + + console.log("list", { element, props }); + + return { + Component: DocumentList, + props, + }; + } + default: { return { Component: UnknownType, @@ -265,6 +281,19 @@ export function getPreviewComponentDefinition( return { Component: PreviewComponent }; } + case "list": { + const PreviewComponent: React.FC = ({ + className, + ...restPreviewProps + }) => ( +
+

List

+
+ ); + + return { Component: PreviewComponent }; + } + default: return getComponentDefinition(element); } diff --git a/src/components/documentBuilder/elementEditSchemas.tsx b/src/components/documentBuilder/elementEditSchemas.tsx index d0f9dbba3b..2663e8c5f5 100644 --- a/src/components/documentBuilder/elementEditSchemas.tsx +++ b/src/components/documentBuilder/elementEditSchemas.tsx @@ -95,6 +95,16 @@ export function getElementEditSchemas( return [titleEdit, variantEdit, sizeEdit, getClassNameEdit(elementName)]; } + case "list": { + const arraySourceEdit: SchemaFieldProps = { + name: joinName(elementName, "config", "array"), + schema: { type: "array" }, + label: "Array", + }; + + return [arraySourceEdit]; + } + default: return [getClassNameEdit(elementName)]; } diff --git a/src/runtime/mapArgs.ts b/src/runtime/mapArgs.ts index 4132666cb2..58dedecabc 100644 --- a/src/runtime/mapArgs.ts +++ b/src/runtime/mapArgs.ts @@ -37,7 +37,7 @@ const expressionTypes: ExpressionType[] = [ "defer", ]; -type Args = string | UnknownObject | UnknownObject[]; +export type Args = string | UnknownObject | UnknownObject[]; /** * Returns true if value represents an explicit expression From 4cb8ceaf74ebd8f5f1acb36c625d2b9c4bee1645 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 6 Dec 2021 14:56:46 +0100 Subject: [PATCH 02/12] better editing --- .../documentBuilder/ElementEdit.tsx | 60 +++++++++---------- .../documentBuilder/createNewElement.ts | 5 ++ .../documentBuilder/elementEditSchemas.tsx | 7 ++- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/components/documentBuilder/ElementEdit.tsx b/src/components/documentBuilder/ElementEdit.tsx index 329c2551e2..97b6db67c7 100644 --- a/src/components/documentBuilder/ElementEdit.tsx +++ b/src/components/documentBuilder/ElementEdit.tsx @@ -16,8 +16,12 @@ */ import { useField } from "formik"; -import React, { useState } from "react"; -import { DocumentElement, DocumentElementType } from "./documentBuilderTypes"; +import React from "react"; +import { + DOCUMENT_ELEMENT_TYPES, + DocumentElement, + DocumentElementType, +} from "./documentBuilderTypes"; import SchemaField from "@/components/fields/schemaFields/SchemaField"; import { getElementEditSchemas } from "./elementEditSchemas"; import { getProperty } from "@/utils"; @@ -25,7 +29,8 @@ import { Row, Col } from "react-bootstrap"; import styles from "./DocumentEditor.module.scss"; import RemoveElementAction from "./RemoveElementAction"; import MoveElementAction from "./MoveElementAction"; -import FieldTemplate from "@/components/form/FieldTemplate"; +import ConnectedFieldTemplate from "@/components/form/ConnectedFieldTemplate"; +import SelectWidget from "@/components/form/widgets/SelectWidget"; type ElementEditProps = { elementName: string; @@ -51,35 +56,17 @@ const ElementEdit: React.FC = ({ setActiveElement, }) => { const [{ value: documentElement }] = useField(elementName); - const [{ value: element }, , { setValue: setElement }] = useField( - `${elementName}.config.element` - ); const editSchemas = getElementEditSchemas(documentElement, elementName); - - const [val, setVal] = useState(element?.config?.text); - const onElChange: React.ChangeEventHandler = (event) => { - const { value } = event.target; - - const element = { - __type__: "defer", - __value__: { - type: "text", - config: { - text: { - __type__: "var", - __value__: value, - }, - }, - } as DocumentElement, - }; - - setVal(value); - setElement(element); - setTimeout(() => { - console.log("ElementEdit", { documentElement }); - }, 100); - }; + if (documentElement.type === "list") { + const elementEditSchemas = getElementEditSchemas( + documentElement.config.element.__value__, + `${elementName}.config.element.__value__` + ); + for (const x of elementEditSchemas) { + editSchemas.push(x); + } + } return ( <> @@ -105,12 +92,19 @@ const ElementEdit: React.FC = ({ + {documentElement.type === "list" && ( + ({ + label: elementTypeLabels[x], + value: x, + }))} + /> + )} {editSchemas.map((editSchema) => ( ))} - {documentElement.type === "list" && ( - - )} diff --git a/src/components/documentBuilder/createNewElement.ts b/src/components/documentBuilder/createNewElement.ts index 5a8b823b1c..507e38e532 100644 --- a/src/components/documentBuilder/createNewElement.ts +++ b/src/components/documentBuilder/createNewElement.ts @@ -16,6 +16,7 @@ */ import { DocumentElement, DocumentElementType } from "./documentBuilderTypes"; +import { Expression } from "@/core"; export function createNewElement(elementType: DocumentElementType) { const element: DocumentElement = { @@ -60,6 +61,10 @@ export function createNewElement(elementType: DocumentElementType) { break; case "list": + element.config.element = { + __type__: "defer", + __value__: createNewElement("text"), + } as Expression; break; default: diff --git a/src/components/documentBuilder/elementEditSchemas.tsx b/src/components/documentBuilder/elementEditSchemas.tsx index 2663e8c5f5..441757a7c0 100644 --- a/src/components/documentBuilder/elementEditSchemas.tsx +++ b/src/components/documentBuilder/elementEditSchemas.tsx @@ -101,8 +101,13 @@ export function getElementEditSchemas( schema: { type: "array" }, label: "Array", }; + const elementKeyEdit: SchemaFieldProps = { + name: joinName(elementName, "config", "elementKey"), + schema: { type: "string" }, + label: "Element Key", + }; - return [arraySourceEdit]; + return [arraySourceEdit, elementKeyEdit]; } default: From 0ed4353800e521fa034389eb7c91cb110fbff182 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 6 Dec 2021 22:59:34 +0100 Subject: [PATCH 03/12] Edit and Preview list --- .../documentBuilder/ElementEdit.tsx | 58 +++++++---- .../documentBuilder/ElementPreview.tsx | 16 ++- .../documentBuilder/RemoveElementAction.tsx | 3 + .../documentBuilder/documentBuilderTypes.ts | 23 ++++- .../documentBuilder/documentTree.tsx | 10 +- .../getElementCollectionName.test.ts | 12 +++ .../tabs/editTab/dataPanel/DataPanel.tsx | 11 ++- src/runtime/brickYaml.test.ts | 97 +++++++++++++++++++ 8 files changed, 203 insertions(+), 27 deletions(-) diff --git a/src/components/documentBuilder/ElementEdit.tsx b/src/components/documentBuilder/ElementEdit.tsx index 97b6db67c7..3bf62e4c78 100644 --- a/src/components/documentBuilder/ElementEdit.tsx +++ b/src/components/documentBuilder/ElementEdit.tsx @@ -16,11 +16,12 @@ */ import { useField } from "formik"; -import React from "react"; +import React, { ChangeEventHandler } from "react"; import { - DOCUMENT_ELEMENT_TYPES, DocumentElement, DocumentElementType, + isListDocument, + ListDocumentElement, } from "./documentBuilderTypes"; import SchemaField from "@/components/fields/schemaFields/SchemaField"; import { getElementEditSchemas } from "./elementEditSchemas"; @@ -29,8 +30,11 @@ import { Row, Col } from "react-bootstrap"; import styles from "./DocumentEditor.module.scss"; import RemoveElementAction from "./RemoveElementAction"; import MoveElementAction from "./MoveElementAction"; -import ConnectedFieldTemplate from "@/components/form/ConnectedFieldTemplate"; import SelectWidget from "@/components/form/widgets/SelectWidget"; +import FieldTemplate from "@/components/form/FieldTemplate"; +import { ROOT_ELEMENT_TYPES } from "@/components/documentBuilder/allowedElementTypes"; +import { produce } from "immer"; +import { createNewElement } from "@/components/documentBuilder/createNewElement"; type ElementEditProps = { elementName: string; @@ -55,18 +59,30 @@ const ElementEdit: React.FC = ({ elementName, setActiveElement, }) => { - const [{ value: documentElement }] = useField(elementName); + const [ + { value: documentElement }, + , + { setValue: setDocumentElement }, + ] = useField(elementName); const editSchemas = getElementEditSchemas(documentElement, elementName); - if (documentElement.type === "list") { - const elementEditSchemas = getElementEditSchemas( - documentElement.config.element.__value__, - `${elementName}.config.element.__value__` + + const isList = isListDocument(documentElement); + + const onElementTypeChange: ChangeEventHandler = (event) => { + const nextType = event.target.value as DocumentElementType; + + const nextDocumentElement = produce( + documentElement, + (draft: ListDocumentElement) => { + const nextElement = createNewElement(nextType); + draft.config.element.__value__ = nextElement; + return draft; + } ); - for (const x of elementEditSchemas) { - editSchemas.push(x); - } - } + + setDocumentElement(nextDocumentElement); + }; return ( <> @@ -92,19 +108,23 @@ const ElementEdit: React.FC = ({ - {documentElement.type === "list" && ( - ( + + ))} + + {isList && ( + ({ + options={ROOT_ELEMENT_TYPES.map((x) => ({ label: elementTypeLabels[x], value: x, }))} /> )} - {editSchemas.map((editSchema) => ( - - ))} diff --git a/src/components/documentBuilder/ElementPreview.tsx b/src/components/documentBuilder/ElementPreview.tsx index ee40e5a122..4c38bee4d0 100644 --- a/src/components/documentBuilder/ElementPreview.tsx +++ b/src/components/documentBuilder/ElementPreview.tsx @@ -18,7 +18,7 @@ import React, { MouseEventHandler } from "react"; import styles from "./ElementPreview.module.scss"; import cx from "classnames"; -import { DocumentElement } from "./documentBuilderTypes"; +import { DocumentElement, isListDocument } from "./documentBuilderTypes"; import { getPreviewComponentDefinition } from "./documentTree"; import AddElementAction from "./AddElementAction"; import { useField } from "formik"; @@ -63,8 +63,12 @@ const ElementPreview: React.FC = ({ } }; + // Render children and Add Menu for the container element const isContainer = Array.isArray(documentElement.children); + // Render the item template and the Item Type Selector for the list element + const isList = isListDocument(documentElement); + const { Component: PreviewComponent, props } = getPreviewComponentDefinition( documentElement ); @@ -101,6 +105,16 @@ const ElementPreview: React.FC = ({ menuBoundary={menuBoundary} /> )} + {isList && ( + + )} ); }; diff --git a/src/components/documentBuilder/RemoveElementAction.tsx b/src/components/documentBuilder/RemoveElementAction.tsx index 62ec880d76..9ea047bdc6 100644 --- a/src/components/documentBuilder/RemoveElementAction.tsx +++ b/src/components/documentBuilder/RemoveElementAction.tsx @@ -32,6 +32,9 @@ const RemoveElementAction: React.FC = ({ elementName, setActiveElement, }) => { + // Gives the name of the elements's collection + // In case of a list item element point to the collection of the list element, + // i.e. removing the item of the list will actually remove the list itself. const { collectionName, elementIndex } = getElementCollectionName( elementName ); diff --git a/src/components/documentBuilder/documentBuilderTypes.ts b/src/components/documentBuilder/documentBuilderTypes.ts index 03a7d00a54..2a6dd59e75 100644 --- a/src/components/documentBuilder/documentBuilderTypes.ts +++ b/src/components/documentBuilder/documentBuilderTypes.ts @@ -16,6 +16,7 @@ */ import { UnknownObject } from "@/types"; +import { Expression } from "@/core"; export const DOCUMENT_ELEMENT_TYPES = [ "header_1", @@ -33,12 +34,28 @@ export const DOCUMENT_ELEMENT_TYPES = [ export type DocumentElementType = typeof DOCUMENT_ELEMENT_TYPES[number]; -export type DocumentElement = { - type: DocumentElementType; - config: UnknownObject; +export type DocumentElement< + TType extends DocumentElementType = DocumentElementType, + TConfig = UnknownObject +> = { + type: TType; + config: TConfig; children?: DocumentElement[]; }; +type ListConfig = { + array: Expression; + elementKey?: string; + element: Expression; +}; +export type ListDocumentElement = DocumentElement<"list", ListConfig>; + +export function isListDocument( + element: DocumentElement +): element is ListDocumentElement { + return element.type === "list"; +} + export type DocumentComponent = { Component: React.ElementType; props?: UnknownObject | undefined; diff --git a/src/components/documentBuilder/documentTree.tsx b/src/components/documentBuilder/documentTree.tsx index dba0dea777..0dd6e454a4 100644 --- a/src/components/documentBuilder/documentTree.tsx +++ b/src/components/documentBuilder/documentTree.tsx @@ -282,12 +282,16 @@ export function getPreviewComponentDefinition( } case "list": { + const arrayValue = isExpression(config.array) + ? config.array.__value__ + : String(config.array); const PreviewComponent: React.FC = ({ - className, + children, ...restPreviewProps }) => ( -
-

List

+
+
List: {arrayValue}
+ {children}
); diff --git a/src/components/documentBuilder/getElementCollectionName.test.ts b/src/components/documentBuilder/getElementCollectionName.test.ts index 9fc4f429af..7fa57d6f85 100644 --- a/src/components/documentBuilder/getElementCollectionName.test.ts +++ b/src/components/documentBuilder/getElementCollectionName.test.ts @@ -25,6 +25,7 @@ test("returns collection name for an element", () => { expect(collectionName).toBe("body.0.children"); expect(elementIndex).toBe(3); }); + test("works for root element", () => { const elementName = "body.5"; const { collectionName, elementIndex } = getElementCollectionName( @@ -33,3 +34,14 @@ test("works for root element", () => { expect(collectionName).toBe("body"); expect(elementIndex).toBe(5); }); + +test("works for list element", () => { + const elementName = "body.5.children.3.config.element.__value__"; + const { collectionName, elementIndex } = getElementCollectionName( + elementName + ); + + // Name of list element collection points to the collection of the list itself + expect(collectionName).toBe("body.5.children"); + expect(elementIndex).toBe(3); +}); diff --git a/src/devTools/editor/tabs/editTab/dataPanel/DataPanel.tsx b/src/devTools/editor/tabs/editTab/dataPanel/DataPanel.tsx index 48e05b6c27..ee5d41139f 100644 --- a/src/devTools/editor/tabs/editTab/dataPanel/DataPanel.tsx +++ b/src/devTools/editor/tabs/editTab/dataPanel/DataPanel.tsx @@ -22,7 +22,7 @@ import { isEmpty, isEqual, pickBy, startsWith } from "lodash"; import { useFormikContext } from "formik"; import formBuilderSelectors from "@/devTools/editor/slices/formBuilderSelectors"; import { actions } from "@/devTools/editor/slices/formBuilderSlice"; -import { Alert, Nav, Tab } from "react-bootstrap"; +import { Alert, Button, Nav, Tab } from "react-bootstrap"; import JsonTree from "@/components/jsonTree/JsonTree"; import styles from "./DataPanel.module.scss"; import FormPreview from "@/components/formBuilder/FormPreview"; @@ -48,6 +48,7 @@ import useDataPanelTabSearchQuery from "@/devTools/editor/tabs/editTab/dataPanel import DocumentPreview from "@/components/documentBuilder/DocumentPreview"; import documentBuilderSelectors from "@/devTools/editor/slices/documentBuilderSelectors"; import { actions as documentBuilderActions } from "@/devTools/editor/slices/documentBuilderSlice"; +import copy from "copy-to-clipboard"; /** * Exclude irrelevant top-level keys. @@ -229,6 +230,14 @@ const DataPanel: React.FC<{ visible to developers
+ )} diff --git a/src/runtime/brickYaml.test.ts b/src/runtime/brickYaml.test.ts index ea97a4ded5..da4d4fc8b6 100644 --- a/src/runtime/brickYaml.test.ts +++ b/src/runtime/brickYaml.test.ts @@ -125,3 +125,100 @@ describe("dumpYaml", () => { expect(dumped).toBe("metadata:\n id: test/brick\n"); }); }); + +describe("parse/dump document", () => { + const brickYaml = ` +id: '@test/document' +config: + body: + - type: header_1 + config: + title: !var '@data.header' + - type: list + config: + element: !defer + type: text + config: + text: List item text. + array: !var '@data.items'`; + /* + - type: card + config: + heading: !var '@card.name' + children: + - type: block + config: + pipeline: !pipeline + - id: '@pixiebrix/markdown' + config: + markdown: 'Markdown content.' +*/ + + const brickJson = { + id: "@test/document", + config: { + body: [ + { + type: "header_1", + config: { + title: { + __type__: "var", + __value__: "@data.header", + }, + }, + }, + { + type: "list", + config: { + element: { + __type__: "defer", + __value__: { + type: "text", + config: { + text: "List item text.", + }, + }, + }, + array: { + __type__: "var", + __value__: "@data.items", + }, + }, + }, + // { + // type: "card", + // config: { + // heading: { + // __type__: "var", + // __value__: "@card.name", + // }, + // }, + // children: [ + // { + // type: "block", + // config: { + // pipeline: { + // __type__: "pipeline", + // __values__: { + // id: "@pixiebrix/markdown", + // config: { + // markdown: "Markdown content.", + // }, + // }, + // }, + // }, + // }, + // ], + // }, + ], + }, + }; + + test("serialize document", () => { + expect(dumpBrickYaml(brickJson)).toBe(brickYaml); + }); + + test("deserealize document", async () => { + expect(loadBrickYaml(brickYaml)).toEqual(brickJson); + }); +}); From 943688d1bbaebf8aecb5fb47bc7ee2f2e015edaa Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 7 Dec 2021 12:38:57 +0100 Subject: [PATCH 04/12] Brick serialization test --- src/blocks/renderers/document.tsx | 1 + .../documentBuilder/documentTree.tsx | 9 +- src/runtime/brickYaml.test.ts | 291 ++++++++++++++---- src/services/api.ts | 5 + 4 files changed, 249 insertions(+), 57 deletions(-) diff --git a/src/blocks/renderers/document.tsx b/src/blocks/renderers/document.tsx index c22ab55126..71c3b321a0 100644 --- a/src/blocks/renderers/document.tsx +++ b/src/blocks/renderers/document.tsx @@ -43,6 +43,7 @@ export class DocumentRenderer extends Renderer { { body }: BlockArg, options: BlockOptions ): Promise { + console.log("document", { options }); return { Component: DocumentViewLazy, props: { diff --git a/src/components/documentBuilder/documentTree.tsx b/src/components/documentBuilder/documentTree.tsx index 0dd6e454a4..ff327242e2 100644 --- a/src/components/documentBuilder/documentTree.tsx +++ b/src/components/documentBuilder/documentTree.tsx @@ -287,10 +287,17 @@ export function getPreviewComponentDefinition( : String(config.array); const PreviewComponent: React.FC = ({ children, + className, ...restPreviewProps }) => ( -
+
List: {arrayValue}
+
+ Element key: {config.elementKey ?? "@element"} +
{children}
); diff --git a/src/runtime/brickYaml.test.ts b/src/runtime/brickYaml.test.ts index da4d4fc8b6..001e0e8721 100644 --- a/src/runtime/brickYaml.test.ts +++ b/src/runtime/brickYaml.test.ts @@ -127,7 +127,8 @@ describe("dumpYaml", () => { }); describe("parse/dump document", () => { - const brickYaml = ` + const brick1 = [ + ` id: '@test/document' config: body: @@ -140,8 +141,8 @@ config: type: text config: text: List item text. - array: !var '@data.items'`; - /* + array: !var '@data.items'`, + /* - type: card config: heading: !var '@card.name' @@ -154,71 +155,249 @@ config: markdown: 'Markdown content.' */ - const brickJson = { - id: "@test/document", - config: { - body: [ - { - type: "header_1", - config: { - title: { - __type__: "var", - __value__: "@data.header", + { + id: "@test/document", + config: { + body: [ + { + type: "header_1", + config: { + title: { + __type__: "var", + __value__: "@data.header", + }, + }, + }, + { + type: "list", + config: { + element: { + __type__: "defer", + __value__: { + type: "text", + config: { + text: "List item text.", + }, + }, + }, + array: { + __type__: "var", + __value__: "@data.items", + }, + }, + }, + // { + // type: "card", + // config: { + // heading: { + // __type__: "var", + // __value__: "@card.name", + // }, + // }, + // children: [ + // { + // type: "block", + // config: { + // pipeline: { + // __type__: "pipeline", + // __values__: { + // id: "@pixiebrix/markdown", + // config: { + // markdown: "Markdown content.", + // }, + // }, + // }, + // }, + // }, + // ], + // }, + ], + }, + }, + ]; + + const brick2 = [ + ` +kind: recipe +metadata: + id: '@balehok/side-panel-test' + version: 1.0.0 + name: My habr.com side panel WS + description: Custom stuff available in WS only. +apiVersion: v3 +definitions: + extensionPoint: + kind: extensionPoint + definition: + type: actionPanel + reader: + - '@pixiebrix/document-metadata' + isAvailable: + matchPatterns: + - https://habr.com/* + urlPatterns: [] + selectors: [] +extensionPoints: + - id: extensionPoint + label: My habr.com side panel + config: + heading: Workshop + body: + - id: '@pixiebrix/jquery-reader' + config: + selectors: + header: h1 + titles: + multi: true + selector: '[data-article-link] span' + className: .tm-tabs__tab-link[href='\\/en\\/companies\\/'] + firstArticle: >- + [data-article-link][href='\\/en\\/company\\/pvs-studio\\/blog\\/592213\\/'] + span + outputKey: data + - id: '@pixiebrix/get' + config: + url: https://api.publicapis.org/categories + outputKey: response + - id: '@pixiebrix/document' + config: + body: + - type: card + config: + heading: !nunjucks jQuery ({{@data.titles | length}}) + children: + - type: list + config: + element: !defer + type: text + config: + text: Paragraph text. + array: !var '@data.titles' + - type: card + config: + heading: !nunjucks API ({{@response | length}}) + children: [] + services: {} +`, + { + kind: "recipe", + metadata: { + id: "@balehok/side-panel-test", + version: "1.0.0", + name: "My habr.com side panel WS", + description: "Custom stuff available in WS only.", + }, + apiVersion: "v3", + definitions: { + extensionPoint: { + kind: "extensionPoint", + definition: { + type: "actionPanel", + reader: ["@pixiebrix/document-metadata"], + isAvailable: { + matchPatterns: ["https://habr.com/*"], + urlPatterns: [] as any[], + selectors: [] as any[], }, }, }, + }, + extensionPoints: [ { - type: "list", + id: "extensionPoint", + label: "My habr.com side panel", config: { - element: { - __type__: "defer", - __value__: { - type: "text", + heading: "Workshop", + body: [ + { + id: "@pixiebrix/jquery-reader", config: { - text: "List item text.", + selectors: { + header: "h1", + titles: { + multi: true, + selector: "[data-article-link] span", + }, + className: + ".tm-tabs__tab-link[href='\\/en\\/companies\\/']", + firstArticle: + "[data-article-link][href='\\/en\\/company\\/pvs-studio\\/blog\\/592213\\/'] span", + }, }, + outputKey: "data", }, - }, - array: { - __type__: "var", - __value__: "@data.items", - }, + { + id: "@pixiebrix/get", + config: { + url: "https://api.publicapis.org/categories", + }, + outputKey: "response", + }, + { + id: "@pixiebrix/document", + config: { + body: [ + { + type: "card", + config: { + heading: { + __type__: "nunjucks", + __value__: "jQuery ({{@data.titles | length}})", + }, + }, + children: [ + { + type: "list", + config: { + element: { + __type__: "defer", + __value__: { + type: "text", + config: { + text: "Paragraph text.", + }, + }, + }, + array: { + __type__: "var", + __value__: "@data.titles", + }, + }, + }, + ], + }, + { + type: "card", + config: { + heading: { + __type__: "nunjucks", + __value__: "API ({{@response | length}})", + }, + }, + children: [], + }, + ], + }, + }, + ], }, + services: {}, }, - // { - // type: "card", - // config: { - // heading: { - // __type__: "var", - // __value__: "@card.name", - // }, - // }, - // children: [ - // { - // type: "block", - // config: { - // pipeline: { - // __type__: "pipeline", - // __values__: { - // id: "@pixiebrix/markdown", - // config: { - // markdown: "Markdown content.", - // }, - // }, - // }, - // }, - // }, - // ], - // }, ], }, - }; + ]; - test("serialize document", () => { - expect(dumpBrickYaml(brickJson)).toBe(brickYaml); - }); + test.each([brick1, brick2])( + "serialize document", + (brickYaml: string, brickJson: any) => { + expect(dumpBrickYaml(brickJson)).toBe(brickYaml); + } + ); - test("deserealize document", async () => { - expect(loadBrickYaml(brickYaml)).toEqual(brickJson); - }); + test.each([brick1, brick2])( + "deserealize document", + async (brickYaml: string, brickJson: any) => { + expect(loadBrickYaml(brickYaml)).toEqual(brickJson); + } + ); }); diff --git a/src/services/api.ts b/src/services/api.ts index 9451593a0f..4681c33c4f 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -217,6 +217,11 @@ export const appApi = createApi({ query: ({ packageId, recipe }) => { const recipeConfig = dumpBrickYaml(recipe); + console.log("updateRecipe", { + json: recipe, + yaml: recipeConfig, + }); + return { url: `api/bricks/${packageId}/`, method: "put", From 4a062e2e1f4af0bcf2ca5967265b92a8a1263e87 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 7 Dec 2021 16:59:21 +0100 Subject: [PATCH 05/12] Rendering the nested list items --- .../documentBuilder/DocumentList.tsx | 48 ++++++++++--------- .../documentBuilder/documentBuilderTypes.ts | 2 + .../documentBuilder/documentTree.tsx | 13 +++-- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/components/documentBuilder/DocumentList.tsx b/src/components/documentBuilder/DocumentList.tsx index 77b6c61150..362d721d0b 100644 --- a/src/components/documentBuilder/DocumentList.tsx +++ b/src/components/documentBuilder/DocumentList.tsx @@ -21,51 +21,55 @@ import { UnknownObject } from "@/types"; import { Args, mapArgs } from "@/runtime/mapArgs"; import { useAsyncState } from "@/hooks/common"; import { GridLoader } from "react-spinners"; -import { getComponentDefinition } from "./documentTree"; -import { DocumentElement } from "./documentBuilderTypes"; +import { BuildDocumentBranch, DocumentElement } from "./documentBuilderTypes"; +import { produce } from "immer"; type DocumentListProps = { array: UnknownObject[]; elementKey?: string; config: Args; + buildDocumentBranch: BuildDocumentBranch; }; const DocumentList: React.FC = ({ array, elementKey = "element", config, + buildDocumentBranch, }) => { - const ctxt = useContext(DocumentContext); - console.log("DocumentList", { - array, - elementKey, - config, - }); - const [componentDefinitions, isLoading] = useAsyncState( + const documentContext = useContext(DocumentContext); + + const [rootDefinitions, isLoading] = useAsyncState( async () => Promise.all( - array.map(async (data) => { - const elementContext = { - ...ctxt, - [`@${elementKey}`]: data, - }; - return (mapArgs(config, elementContext, { + array.map(async (itemData) => { + const elementContext = produce(documentContext, (draft) => { + draft.options.ctxt[`@${elementKey}`] = itemData; + }); + + return (mapArgs(config, elementContext.options.ctxt, { implicitRender: null, - }) as Promise).then((documentElement) => - getComponentDefinition(documentElement) - ); + }) as Promise).then((documentElement) => ({ + documentElement, + elementContext, + })); }) ), - [array, elementKey, config, ctxt] + [array, elementKey, config, documentContext] ); return isLoading ? ( ) : ( <> - {componentDefinitions.map(({ Component, props }, index) => ( - - ))} + {rootDefinitions.map(({ documentElement, elementContext }, i) => { + const { Component, props } = buildDocumentBranch(documentElement); + return ( + + + + ); + })} ); }; diff --git a/src/components/documentBuilder/documentBuilderTypes.ts b/src/components/documentBuilder/documentBuilderTypes.ts index 2a6dd59e75..29d6eb280d 100644 --- a/src/components/documentBuilder/documentBuilderTypes.ts +++ b/src/components/documentBuilder/documentBuilderTypes.ts @@ -60,3 +60,5 @@ export type DocumentComponent = { Component: React.ElementType; props?: UnknownObject | undefined; }; + +export type BuildDocumentBranch = (root: DocumentElement) => DocumentComponent; diff --git a/src/components/documentBuilder/documentTree.tsx b/src/components/documentBuilder/documentTree.tsx index ff327242e2..e92ac22353 100644 --- a/src/components/documentBuilder/documentTree.tsx +++ b/src/components/documentBuilder/documentTree.tsx @@ -21,7 +21,11 @@ import { isExpression, isPipelineExpression } from "@/runtime/mapArgs"; import { UnknownObject } from "@/types"; import { get } from "lodash"; import { Card, Col, Container, Row } from "react-bootstrap"; -import { DocumentComponent, DocumentElement } from "./documentBuilderTypes"; +import { + BuildDocumentBranch, + DocumentComponent, + DocumentElement, +} from "./documentBuilderTypes"; import DocumentButton from "@/components/documentBuilder/DocumentButton"; import useNotifications from "@/hooks/useNotifications"; import documentTreeStyles from "./documentTree.module.scss"; @@ -131,10 +135,9 @@ export function getComponentDefinition( array: config.array, elementKey: config.elementKey, config: config.element, + buildDocumentBranch, }; - console.log("list", { element, props }); - return { Component: DocumentList, props, @@ -310,7 +313,7 @@ export function getPreviewComponentDefinition( } } -export function buildDocumentBranch(root: DocumentElement): DocumentComponent { +export const buildDocumentBranch: BuildDocumentBranch = (root) => { const componentDefinition = getComponentDefinition(root); if (root.children?.length > 0) { componentDefinition.props.children = root.children.map((child, i) => { @@ -320,4 +323,4 @@ export function buildDocumentBranch(root: DocumentElement): DocumentComponent { } return componentDefinition; -} +}; From 7e0989c7e634552713069cde0864d1cd69bff8c8 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 7 Dec 2021 18:18:58 +0100 Subject: [PATCH 06/12] Code cleanup --- src/blocks/renderers/document.tsx | 1 - .../DocumentBuilder.stories.tsx | 75 +---- src/runtime/brickYaml.test.ts | 276 ------------------ src/services/api.ts | 5 - 4 files changed, 1 insertion(+), 356 deletions(-) diff --git a/src/blocks/renderers/document.tsx b/src/blocks/renderers/document.tsx index 71c3b321a0..c22ab55126 100644 --- a/src/blocks/renderers/document.tsx +++ b/src/blocks/renderers/document.tsx @@ -43,7 +43,6 @@ export class DocumentRenderer extends Renderer { { body }: BlockArg, options: BlockOptions ): Promise { - console.log("document", { options }); return { Component: DocumentViewLazy, props: { diff --git a/src/components/documentBuilder/DocumentBuilder.stories.tsx b/src/components/documentBuilder/DocumentBuilder.stories.tsx index a57de2d5ea..414676297d 100644 --- a/src/components/documentBuilder/DocumentBuilder.stories.tsx +++ b/src/components/documentBuilder/DocumentBuilder.stories.tsx @@ -40,80 +40,7 @@ const DocumentBuilder: React.FC = () => { return ( {({ handleSubmit }) => ( diff --git a/src/runtime/brickYaml.test.ts b/src/runtime/brickYaml.test.ts index 001e0e8721..ea97a4ded5 100644 --- a/src/runtime/brickYaml.test.ts +++ b/src/runtime/brickYaml.test.ts @@ -125,279 +125,3 @@ describe("dumpYaml", () => { expect(dumped).toBe("metadata:\n id: test/brick\n"); }); }); - -describe("parse/dump document", () => { - const brick1 = [ - ` -id: '@test/document' -config: - body: - - type: header_1 - config: - title: !var '@data.header' - - type: list - config: - element: !defer - type: text - config: - text: List item text. - array: !var '@data.items'`, - /* - - type: card - config: - heading: !var '@card.name' - children: - - type: block - config: - pipeline: !pipeline - - id: '@pixiebrix/markdown' - config: - markdown: 'Markdown content.' -*/ - - { - id: "@test/document", - config: { - body: [ - { - type: "header_1", - config: { - title: { - __type__: "var", - __value__: "@data.header", - }, - }, - }, - { - type: "list", - config: { - element: { - __type__: "defer", - __value__: { - type: "text", - config: { - text: "List item text.", - }, - }, - }, - array: { - __type__: "var", - __value__: "@data.items", - }, - }, - }, - // { - // type: "card", - // config: { - // heading: { - // __type__: "var", - // __value__: "@card.name", - // }, - // }, - // children: [ - // { - // type: "block", - // config: { - // pipeline: { - // __type__: "pipeline", - // __values__: { - // id: "@pixiebrix/markdown", - // config: { - // markdown: "Markdown content.", - // }, - // }, - // }, - // }, - // }, - // ], - // }, - ], - }, - }, - ]; - - const brick2 = [ - ` -kind: recipe -metadata: - id: '@balehok/side-panel-test' - version: 1.0.0 - name: My habr.com side panel WS - description: Custom stuff available in WS only. -apiVersion: v3 -definitions: - extensionPoint: - kind: extensionPoint - definition: - type: actionPanel - reader: - - '@pixiebrix/document-metadata' - isAvailable: - matchPatterns: - - https://habr.com/* - urlPatterns: [] - selectors: [] -extensionPoints: - - id: extensionPoint - label: My habr.com side panel - config: - heading: Workshop - body: - - id: '@pixiebrix/jquery-reader' - config: - selectors: - header: h1 - titles: - multi: true - selector: '[data-article-link] span' - className: .tm-tabs__tab-link[href='\\/en\\/companies\\/'] - firstArticle: >- - [data-article-link][href='\\/en\\/company\\/pvs-studio\\/blog\\/592213\\/'] - span - outputKey: data - - id: '@pixiebrix/get' - config: - url: https://api.publicapis.org/categories - outputKey: response - - id: '@pixiebrix/document' - config: - body: - - type: card - config: - heading: !nunjucks jQuery ({{@data.titles | length}}) - children: - - type: list - config: - element: !defer - type: text - config: - text: Paragraph text. - array: !var '@data.titles' - - type: card - config: - heading: !nunjucks API ({{@response | length}}) - children: [] - services: {} -`, - { - kind: "recipe", - metadata: { - id: "@balehok/side-panel-test", - version: "1.0.0", - name: "My habr.com side panel WS", - description: "Custom stuff available in WS only.", - }, - apiVersion: "v3", - definitions: { - extensionPoint: { - kind: "extensionPoint", - definition: { - type: "actionPanel", - reader: ["@pixiebrix/document-metadata"], - isAvailable: { - matchPatterns: ["https://habr.com/*"], - urlPatterns: [] as any[], - selectors: [] as any[], - }, - }, - }, - }, - extensionPoints: [ - { - id: "extensionPoint", - label: "My habr.com side panel", - config: { - heading: "Workshop", - body: [ - { - id: "@pixiebrix/jquery-reader", - config: { - selectors: { - header: "h1", - titles: { - multi: true, - selector: "[data-article-link] span", - }, - className: - ".tm-tabs__tab-link[href='\\/en\\/companies\\/']", - firstArticle: - "[data-article-link][href='\\/en\\/company\\/pvs-studio\\/blog\\/592213\\/'] span", - }, - }, - outputKey: "data", - }, - { - id: "@pixiebrix/get", - config: { - url: "https://api.publicapis.org/categories", - }, - outputKey: "response", - }, - { - id: "@pixiebrix/document", - config: { - body: [ - { - type: "card", - config: { - heading: { - __type__: "nunjucks", - __value__: "jQuery ({{@data.titles | length}})", - }, - }, - children: [ - { - type: "list", - config: { - element: { - __type__: "defer", - __value__: { - type: "text", - config: { - text: "Paragraph text.", - }, - }, - }, - array: { - __type__: "var", - __value__: "@data.titles", - }, - }, - }, - ], - }, - { - type: "card", - config: { - heading: { - __type__: "nunjucks", - __value__: "API ({{@response | length}})", - }, - }, - children: [], - }, - ], - }, - }, - ], - }, - services: {}, - }, - ], - }, - ]; - - test.each([brick1, brick2])( - "serialize document", - (brickYaml: string, brickJson: any) => { - expect(dumpBrickYaml(brickJson)).toBe(brickYaml); - } - ); - - test.each([brick1, brick2])( - "deserealize document", - async (brickYaml: string, brickJson: any) => { - expect(loadBrickYaml(brickYaml)).toEqual(brickJson); - } - ); -}); diff --git a/src/services/api.ts b/src/services/api.ts index 4681c33c4f..9451593a0f 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -217,11 +217,6 @@ export const appApi = createApi({ query: ({ packageId, recipe }) => { const recipeConfig = dumpBrickYaml(recipe); - console.log("updateRecipe", { - json: recipe, - yaml: recipeConfig, - }); - return { url: `api/bricks/${packageId}/`, method: "put", From c44d76dd4b90208b488760ac89776dcf239758bf Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 8 Dec 2021 15:38:41 +0100 Subject: [PATCH 07/12] Some improvements --- src/components/documentBuilder/ElementEdit.tsx | 11 +++++------ src/components/documentBuilder/allowedElementTypes.ts | 8 +++++++- .../documentBuilder/documentTree.module.scss | 5 +++++ src/components/documentBuilder/documentTree.tsx | 6 +++++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/components/documentBuilder/ElementEdit.tsx b/src/components/documentBuilder/ElementEdit.tsx index 3bf62e4c78..f4baa0008a 100644 --- a/src/components/documentBuilder/ElementEdit.tsx +++ b/src/components/documentBuilder/ElementEdit.tsx @@ -26,13 +26,13 @@ import { import SchemaField from "@/components/fields/schemaFields/SchemaField"; import { getElementEditSchemas } from "./elementEditSchemas"; import { getProperty } from "@/utils"; -import { Row, Col } from "react-bootstrap"; +import { Col, Row } from "react-bootstrap"; import styles from "./DocumentEditor.module.scss"; import RemoveElementAction from "./RemoveElementAction"; import MoveElementAction from "./MoveElementAction"; import SelectWidget from "@/components/form/widgets/SelectWidget"; import FieldTemplate from "@/components/form/FieldTemplate"; -import { ROOT_ELEMENT_TYPES } from "@/components/documentBuilder/allowedElementTypes"; +import { getAllowedChildTypes } from "@/components/documentBuilder/allowedElementTypes"; import { produce } from "immer"; import { createNewElement } from "@/components/documentBuilder/createNewElement"; @@ -75,9 +75,7 @@ const ElementEdit: React.FC = ({ const nextDocumentElement = produce( documentElement, (draft: ListDocumentElement) => { - const nextElement = createNewElement(nextType); - draft.config.element.__value__ = nextElement; - return draft; + draft.config.element.__value__ = createNewElement(nextType); } ); @@ -119,7 +117,8 @@ const ElementEdit: React.FC = ({ value={documentElement.config.element.__value__.type} onChange={onElementTypeChange} as={SelectWidget} - options={ROOT_ELEMENT_TYPES.map((x) => ({ + options={getAllowedChildTypes(documentElement).map((x) => ({ + // eslint-disable-next-line security/detect-object-injection -- x is a know string label: elementTypeLabels[x], value: x, }))} diff --git a/src/components/documentBuilder/allowedElementTypes.ts b/src/components/documentBuilder/allowedElementTypes.ts index ec39be8730..16df896b05 100644 --- a/src/components/documentBuilder/allowedElementTypes.ts +++ b/src/components/documentBuilder/allowedElementTypes.ts @@ -15,7 +15,11 @@ * along with this program. If not, see . */ -import { DocumentElement, DocumentElementType } from "./documentBuilderTypes"; +import { + DOCUMENT_ELEMENT_TYPES, + DocumentElement, + DocumentElementType, +} from "./documentBuilderTypes"; export const ROOT_ELEMENT_TYPES: DocumentElementType[] = [ "header_1", @@ -52,6 +56,8 @@ const allowedChildTypes: Record = { "button", "list", ], + // Any element we can add to the list + list: (DOCUMENT_ELEMENT_TYPES as unknown) as DocumentElementType[], }; export function getAllowedChildTypes( diff --git a/src/components/documentBuilder/documentTree.module.scss b/src/components/documentBuilder/documentTree.module.scss index 2e86ee5a32..bc84191e75 100644 --- a/src/components/documentBuilder/documentTree.module.scss +++ b/src/components/documentBuilder/documentTree.module.scss @@ -32,6 +32,11 @@ } } +:global(.container) > .listContainer { + margin-left: -12px; + padding-left: 15px; +} + .inlineWrapper { display: inline-block; } diff --git a/src/components/documentBuilder/documentTree.tsx b/src/components/documentBuilder/documentTree.tsx index e92ac22353..f3c566758e 100644 --- a/src/components/documentBuilder/documentTree.tsx +++ b/src/components/documentBuilder/documentTree.tsx @@ -294,7 +294,11 @@ export function getPreviewComponentDefinition( ...restPreviewProps }) => (
List: {arrayValue}
From 83351a10b0279240ab2827e78e9b478af48fa426 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 8 Dec 2021 17:23:29 +0100 Subject: [PATCH 08/12] Element key input --- .../documentBuilder/DocumentList.tsx | 5 ++- .../documentBuilder/ElementEdit.tsx | 35 ++++++++++++------- .../documentBuilder/documentTree.tsx | 2 +- .../documentBuilder/elementEditSchemas.tsx | 7 +--- src/components/form/widgets/KeyNameWidget.tsx | 31 ++++++++++++++++ .../EditorNodeConfigPanel.tsx | 15 ++------ 6 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 src/components/form/widgets/KeyNameWidget.tsx diff --git a/src/components/documentBuilder/DocumentList.tsx b/src/components/documentBuilder/DocumentList.tsx index 362d721d0b..2e79776f40 100644 --- a/src/components/documentBuilder/DocumentList.tsx +++ b/src/components/documentBuilder/DocumentList.tsx @@ -33,10 +33,13 @@ type DocumentListProps = { const DocumentList: React.FC = ({ array, - elementKey = "element", + elementKey, config, buildDocumentBranch, }) => { + // Should be 'element' for any falsy value including empty strings. + elementKey = elementKey || "element"; + const documentContext = useContext(DocumentContext); const [rootDefinitions, isLoading] = useAsyncState( diff --git a/src/components/documentBuilder/ElementEdit.tsx b/src/components/documentBuilder/ElementEdit.tsx index f4baa0008a..a7dfc727be 100644 --- a/src/components/documentBuilder/ElementEdit.tsx +++ b/src/components/documentBuilder/ElementEdit.tsx @@ -25,7 +25,7 @@ import { } from "./documentBuilderTypes"; import SchemaField from "@/components/fields/schemaFields/SchemaField"; import { getElementEditSchemas } from "./elementEditSchemas"; -import { getProperty } from "@/utils"; +import { getProperty, joinName } from "@/utils"; import { Col, Row } from "react-bootstrap"; import styles from "./DocumentEditor.module.scss"; import RemoveElementAction from "./RemoveElementAction"; @@ -35,6 +35,8 @@ import FieldTemplate from "@/components/form/FieldTemplate"; import { getAllowedChildTypes } from "@/components/documentBuilder/allowedElementTypes"; import { produce } from "immer"; import { createNewElement } from "@/components/documentBuilder/createNewElement"; +import ConnectedFieldTemplate from "@/components/form/ConnectedFieldTemplate"; +import KeyNameWidget from "@/components/form/widgets/KeyNameWidget"; type ElementEditProps = { elementName: string; @@ -111,18 +113,25 @@ const ElementEdit: React.FC = ({ ))} {isList && ( - ({ - // eslint-disable-next-line security/detect-object-injection -- x is a know string - label: elementTypeLabels[x], - value: x, - }))} - /> + <> + + ({ + // eslint-disable-next-line security/detect-object-injection -- x is a know string + label: elementTypeLabels[x], + value: x, + }))} + /> + )} diff --git a/src/components/documentBuilder/documentTree.tsx b/src/components/documentBuilder/documentTree.tsx index f3c566758e..ff55fd8df0 100644 --- a/src/components/documentBuilder/documentTree.tsx +++ b/src/components/documentBuilder/documentTree.tsx @@ -303,7 +303,7 @@ export function getPreviewComponentDefinition( >
List: {arrayValue}
- Element key: {config.elementKey ?? "@element"} + Element key: @{config.elementKey || "element"}
{children}
diff --git a/src/components/documentBuilder/elementEditSchemas.tsx b/src/components/documentBuilder/elementEditSchemas.tsx index 441757a7c0..2663e8c5f5 100644 --- a/src/components/documentBuilder/elementEditSchemas.tsx +++ b/src/components/documentBuilder/elementEditSchemas.tsx @@ -101,13 +101,8 @@ export function getElementEditSchemas( schema: { type: "array" }, label: "Array", }; - const elementKeyEdit: SchemaFieldProps = { - name: joinName(elementName, "config", "elementKey"), - schema: { type: "string" }, - label: "Element Key", - }; - return [arraySourceEdit, elementKeyEdit]; + return [arraySourceEdit]; } default: diff --git a/src/components/form/widgets/KeyNameWidget.tsx b/src/components/form/widgets/KeyNameWidget.tsx new file mode 100644 index 0000000000..87b23f673e --- /dev/null +++ b/src/components/form/widgets/KeyNameWidget.tsx @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Form, InputGroup } from "react-bootstrap"; +import React from "react"; +import { FormControlProps } from "react-bootstrap/FormControl"; + +const KeyNameWidget: React.FC = (props) => ( + + + @ + + + +); + +export default KeyNameWidget; diff --git a/src/devTools/editor/tabs/editTab/editorNodeConfigPanel/EditorNodeConfigPanel.tsx b/src/devTools/editor/tabs/editTab/editorNodeConfigPanel/EditorNodeConfigPanel.tsx index 128c45cdcc..21879823b6 100644 --- a/src/devTools/editor/tabs/editTab/editorNodeConfigPanel/EditorNodeConfigPanel.tsx +++ b/src/devTools/editor/tabs/editTab/editorNodeConfigPanel/EditorNodeConfigPanel.tsx @@ -16,10 +16,9 @@ */ import React, { useMemo } from "react"; -import { Button, Col, Form, InputGroup, Row } from "react-bootstrap"; +import { Button, Col, Row } from "react-bootstrap"; import { RegistryId } from "@/core"; import ConnectedFieldTemplate from "@/components/form/ConnectedFieldTemplate"; -import { CustomFieldWidget, FieldProps } from "@/components/form/FieldTemplate"; import BlockConfigurationV1 from "@/devTools/editor/tabs/effect/v1/BlockConfiguration"; import BlockConfigurationV3 from "@/devTools/editor/tabs/effect/v3/BlockConfiguration"; import { useAsyncState } from "@/hooks/common"; @@ -31,15 +30,7 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons"; import styles from "./EditorNodeConfigPanel.module.scss"; import PopoverInfoLabel from "@/components/form/popoverInfoLabel/PopoverInfoLabel"; import useApiVersionAtLeast from "@/devTools/editor/hooks/useApiVersionAtLeast"; - -const OutputKeyWidget: CustomFieldWidget = (props: FieldProps) => ( - - - @ - - - -); +import KeyNameWidget from "@/components/form/widgets/KeyNameWidget"; const PopoverOutputLabel: React.FC<{ description: string; @@ -106,7 +97,7 @@ const EditorNodeConfigPanel: React.FC<{ name={`${blockFieldName}.outputKey`} label={outputKeyLabel} disabled={isOutputDisabled} - as={OutputKeyWidget} + as={KeyNameWidget} /> From aa113317ded1191ae2a2e35825a4ff1976a678f2 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 8 Dec 2021 17:40:52 +0100 Subject: [PATCH 09/12] ErrorBoundary around List --- src/components/documentBuilder/DocumentList.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/documentBuilder/DocumentList.tsx b/src/components/documentBuilder/DocumentList.tsx index 2e79776f40..e9c5a530db 100644 --- a/src/components/documentBuilder/DocumentList.tsx +++ b/src/components/documentBuilder/DocumentList.tsx @@ -23,6 +23,7 @@ import { useAsyncState } from "@/hooks/common"; import { GridLoader } from "react-spinners"; import { BuildDocumentBranch, DocumentElement } from "./documentBuilderTypes"; import { produce } from "immer"; +import ErrorBoundary from "@/components/ErrorBoundary"; type DocumentListProps = { array: UnknownObject[]; @@ -31,13 +32,13 @@ type DocumentListProps = { buildDocumentBranch: BuildDocumentBranch; }; -const DocumentList: React.FC = ({ +const DocumentListInternal: React.FC = ({ array, elementKey, config, buildDocumentBranch, }) => { - // Should be 'element' for any falsy value including empty strings. + // Should be 'element' for any falsy value including empty string. elementKey = elementKey || "element"; const documentContext = useContext(DocumentContext); @@ -77,4 +78,10 @@ const DocumentList: React.FC = ({ ); }; +const DocumentList: React.FC = (props) => ( + + + +); + export default DocumentList; From a35f7a2734e7cc2f0cd5693cfbe4b71ec7b58721 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 8 Dec 2021 17:50:20 +0100 Subject: [PATCH 10/12] Linting --- src/components/form/widgets/KeyNameWidget.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/form/widgets/KeyNameWidget.tsx b/src/components/form/widgets/KeyNameWidget.tsx index 87b23f673e..70c4f2f595 100644 --- a/src/components/form/widgets/KeyNameWidget.tsx +++ b/src/components/form/widgets/KeyNameWidget.tsx @@ -15,9 +15,8 @@ * along with this program. If not, see . */ -import { Form, InputGroup } from "react-bootstrap"; +import { Form, InputGroup, FormControlProps } from "react-bootstrap"; import React from "react"; -import { FormControlProps } from "react-bootstrap/FormControl"; const KeyNameWidget: React.FC = (props) => ( From 7e61bafbe5e152bd2c79ba2f72e612e209c3e4d8 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 9 Dec 2021 18:23:19 +0100 Subject: [PATCH 11/12] Linting --- src/components/documentBuilder/DocumentList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/documentBuilder/DocumentList.tsx b/src/components/documentBuilder/DocumentList.tsx index e9c5a530db..d81ed341a3 100644 --- a/src/components/documentBuilder/DocumentList.tsx +++ b/src/components/documentBuilder/DocumentList.tsx @@ -53,6 +53,7 @@ const DocumentListInternal: React.FC = ({ return (mapArgs(config, elementContext.options.ctxt, { implicitRender: null, + autoescape: true, }) as Promise).then((documentElement) => ({ documentElement, elementContext, From 829777c0e389f2dfd81a75b496d5d1349d3f9ff0 Mon Sep 17 00:00:00 2001 From: Todd Schiller Date: Thu, 9 Dec 2021 12:51:37 -0500 Subject: [PATCH 12/12] #1976: show list error --- .../documentBuilder/DocumentList.tsx | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/components/documentBuilder/DocumentList.tsx b/src/components/documentBuilder/DocumentList.tsx index d81ed341a3..abd8ae482a 100644 --- a/src/components/documentBuilder/DocumentList.tsx +++ b/src/components/documentBuilder/DocumentList.tsx @@ -24,6 +24,7 @@ import { GridLoader } from "react-spinners"; import { BuildDocumentBranch, DocumentElement } from "./documentBuilderTypes"; import { produce } from "immer"; import ErrorBoundary from "@/components/ErrorBoundary"; +import { getErrorMessage } from "@/errors"; type DocumentListProps = { array: UnknownObject[]; @@ -43,7 +44,7 @@ const DocumentListInternal: React.FC = ({ const documentContext = useContext(DocumentContext); - const [rootDefinitions, isLoading] = useAsyncState( + const [rootDefinitions, isLoading, error] = useAsyncState( async () => Promise.all( array.map(async (itemData) => { @@ -63,9 +64,26 @@ const DocumentListInternal: React.FC = ({ [array, elementKey, config, documentContext] ); - return isLoading ? ( - - ) : ( + if (isLoading) { + return ; + } + + if (error) { + return ( +
+ {getErrorMessage(error)} + +
+          {((error as Error).stack ?? "").replaceAll(
+            `chrome-extension://${process.env.CHROME_EXTENSION_ID}/`,
+            ""
+          )}
+        
+
+ ); + } + + return ( <> {rootDefinitions.map(({ documentElement, elementContext }, i) => { const { Component, props } = buildDocumentBranch(documentElement);