From ccff7500bc67e0215f3ce15238d87a8ed1514a0c Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 26 Nov 2021 11:35:17 +1100 Subject: [PATCH] add collapsible SteppedAccordion --- src/components/SteppedAccordion.tsx | 92 ++ .../TableHeader/Extensions/ExtensionModal.tsx | 135 +-- .../TableHeader/Extensions/Step1Triggers.tsx | 79 +- .../Extensions/Step2RequiredFields.tsx | 89 +- .../Extensions/Step3Conditions.tsx | 7 + .../TableHeader/Webhooks/Step2Conditions.tsx | 7 + .../TableHeader/Webhooks/Step3Parser.tsx | 19 + .../TableHeader/Webhooks/WebhookModal.tsx | 113 +-- src/components/fields/Action/Settings.tsx | 914 ++++++++---------- 9 files changed, 689 insertions(+), 766 deletions(-) create mode 100644 src/components/SteppedAccordion.tsx diff --git a/src/components/SteppedAccordion.tsx b/src/components/SteppedAccordion.tsx new file mode 100644 index 00000000..932bc10e --- /dev/null +++ b/src/components/SteppedAccordion.tsx @@ -0,0 +1,92 @@ +import { useState } from "react"; + +import { + Stepper, + StepperProps, + Step, + StepProps, + StepButton, + StepButtonProps, + Typography, + StepContent, + StepContentProps, +} from "@mui/material"; +import ExpandIcon from "@mui/icons-material/KeyboardArrowDown"; + +export interface ISteppedAccordionProps extends Partial { + steps: { + id: string; + title: React.ReactNode; + optional?: boolean; + content: React.ReactNode; + + stepProps?: Partial; + titleProps?: Partial; + contentProps?: Partial; + }[]; +} + +export default function SteppedAccordion({ + steps, + ...props +}: ISteppedAccordionProps) { + const [activeStep, setActiveStep] = useState(steps[0].id); + + return ( + x.id === activeStep)} + orientation="vertical" + {...props} + sx={{ + mt: 0, + + "& .MuiStepLabel-root": { width: "100%" }, + "& .MuiStepLabel-label": { + display: "flex", + width: "100%", + typography: "subtitle2", + "&.Mui-active": { typography: "subtitle2" }, + }, + "& .MuiStepLabel-label svg": { + display: "block", + marginLeft: "auto", + my: ((24 - 18) / 2 / 8) * -1, + transition: (theme) => theme.transitions.create("transform"), + }, + "& .Mui-active svg": { + transform: "rotate(180deg)", + }, + + ...props.sx, + }} + > + {steps.map( + ({ + id, + title, + optional, + content, + stepProps, + titleProps, + contentProps, + }) => ( + + setActiveStep((s) => (s === id ? "" : id))} + optional={ + optional && Optional + } + {...titleProps} + > + {title} + + + + {content} + + ) + )} + + ); +} diff --git a/src/components/TableHeader/Extensions/ExtensionModal.tsx b/src/components/TableHeader/Extensions/ExtensionModal.tsx index 7e7cbb74..b112c339 100644 --- a/src/components/TableHeader/Extensions/ExtensionModal.tsx +++ b/src/components/TableHeader/Extensions/ExtensionModal.tsx @@ -1,33 +1,18 @@ import { useState } from "react"; import _isEqual from "lodash/isEqual"; -import _upperFirst from "lodash/upperFirst"; import useStateRef from "react-usestateref"; -import { - Grid, - TextField, - FormControlLabel, - Switch, - Stepper, - Step, - StepButton, - StepContent, - Typography, - Link, -} from "@mui/material"; -import ExpandIcon from "@mui/icons-material/KeyboardArrowDown"; -import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; +import { Grid, TextField, FormControlLabel, Switch } from "@mui/material"; import Modal, { IModalProps } from "@src/components/Modal"; +import SteppedAccordion from "@src/components/SteppedAccordion"; import Step1Triggers from "./Step1Triggers"; import Step2RequiredFields from "./Step2RequiredFields"; import Step3Conditions from "./Step3Conditions"; import Step4Body from "./Step4Body"; import { useConfirmation } from "@src/components/ConfirmationDialog"; - import { extensionNames, IExtension } from "./utils"; -import { WIKI_LINKS } from "@src/constants/externalLinks"; type StepValidation = Record<"condition" | "extensionBody", boolean>; export interface IExtensionModalStepProps { @@ -58,8 +43,6 @@ export default function ExtensionModal({ const [extensionObject, setExtensionObject] = useState(initialObject); - const [activeStep, setActiveStep] = useState(0); - const [validation, setValidation, validationRef] = useStateRef({ condition: true, extensionBody: true }); @@ -144,100 +127,32 @@ export default function ExtensionModal({ - , }, - "& .MuiStepLabel-label svg": { - display: "block", - marginLeft: "auto", - my: ((24 - 18) / 2 / 8) * -1, - transition: (theme) => theme.transitions.create("transform"), + { + id: "requiredFields", + title: "Required fields", + optional: true, + content: , }, - "& .Mui-active svg": { - transform: "rotate(180deg)", + { + id: "conditions", + title: "Trigger conditions", + optional: true, + content: , }, - }} - > - - setActiveStep(0)}> - Trigger events - - - - - Select which events trigger this extension - - - - - - - setActiveStep(1)}> - Required fields (optional) - - - - - Optionally, select fields that must have a value set for the - extension to be triggered for that row - - - - - - - setActiveStep(2)}> - Trigger conditions (optional) - - - - - Optionally, write a function that determines if the extension - should be triggered for a given row. Leave the function to - always return true if you do not want to write - additional logic. - - - - - - - setActiveStep(3)}> - Extension body - - - - - Write the extension body function. Make sure you have set all - the required parameters.{" "} - - Docs - - - - - - - + { + id: "body", + title: "Extension body", + content: , + }, + ]} + /> } actions={{ diff --git a/src/components/TableHeader/Extensions/Step1Triggers.tsx b/src/components/TableHeader/Extensions/Step1Triggers.tsx index 7e630712..0fd0ca48 100644 --- a/src/components/TableHeader/Extensions/Step1Triggers.tsx +++ b/src/components/TableHeader/Extensions/Step1Triggers.tsx @@ -1,6 +1,7 @@ import { IExtensionModalStepProps } from "./ExtensionModal"; import { + Typography, FormControl, FormLabel, FormGroup, @@ -15,42 +16,48 @@ export default function Step1Triggers({ setExtensionObject, }: IExtensionModalStepProps) { return ( - - - Triggers - + <> + + Select which events trigger this extension + - - {triggerTypes.map((trigger) => ( - { - setExtensionObject((extensionObject) => { - if (extensionObject.triggers.includes(trigger)) { - return { - ...extensionObject, - triggers: extensionObject.triggers.filter( - (t) => t !== trigger - ), - }; - } else { - return { - ...extensionObject, - triggers: [...extensionObject.triggers, trigger], - }; - } - }); - }} - /> - } - /> - ))} - - + + + Triggers + + + + {triggerTypes.map((trigger) => ( + { + setExtensionObject((extensionObject) => { + if (extensionObject.triggers.includes(trigger)) { + return { + ...extensionObject, + triggers: extensionObject.triggers.filter( + (t) => t !== trigger + ), + }; + } else { + return { + ...extensionObject, + triggers: [...extensionObject.triggers, trigger], + }; + } + }); + }} + /> + } + /> + ))} + + + ); } diff --git a/src/components/TableHeader/Extensions/Step2RequiredFields.tsx b/src/components/TableHeader/Extensions/Step2RequiredFields.tsx index 00281eef..2f5d646d 100644 --- a/src/components/TableHeader/Extensions/Step2RequiredFields.tsx +++ b/src/components/TableHeader/Extensions/Step2RequiredFields.tsx @@ -1,8 +1,8 @@ import { IExtensionModalStepProps } from "./ExtensionModal"; import _sortBy from "lodash/sortBy"; +import { Typography, ListItemIcon } from "@mui/material"; import MultiSelect from "@rowy/multiselect"; -import { ListItemIcon } from "@mui/material"; import { useProjectContext } from "@src/contexts/ProjectContext"; import { FieldType } from "@src/constants/fields"; @@ -15,45 +15,52 @@ export default function Step2RequiredFields({ const { tableState } = useProjectContext(); return ( - c.type !== FieldType.id) - .map((c) => ({ - value: c.key, - label: c.name, - type: c.type, - })) - : [] - } - onChange={(requiredFields) => - setExtensionObject((e) => ({ ...e, requiredFields })) - } - TextFieldProps={{ autoFocus: true }} - freeText - AddButtonProps={{ children: "Add other field…" }} - AddDialogProps={{ - title: "Add other field", - textFieldLabel: "Field key", - }} - itemRenderer={(option: { - value: string; - label: string; - type?: FieldType; - }) => ( - <> - - {option.type && getFieldProp("icon", option.type)} - - {option.label} - {option.value} - - )} - /> + <> + + Optionally, select fields that must have a value set for the extension + to be triggered for that row + + + c.type !== FieldType.id) + .map((c) => ({ + value: c.key, + label: c.name, + type: c.type, + })) + : [] + } + onChange={(requiredFields) => + setExtensionObject((e) => ({ ...e, requiredFields })) + } + TextFieldProps={{ autoFocus: true }} + freeText + AddButtonProps={{ children: "Add other field…" }} + AddDialogProps={{ + title: "Add other field", + textFieldLabel: "Field key", + }} + itemRenderer={(option: { + value: string; + label: string; + type?: FieldType; + }) => ( + <> + + {option.type && getFieldProp("icon", option.type)} + + {option.label} + {option.value} + + )} + /> + ); } diff --git a/src/components/TableHeader/Extensions/Step3Conditions.tsx b/src/components/TableHeader/Extensions/Step3Conditions.tsx index 06ec8acf..c3c6284d 100644 --- a/src/components/TableHeader/Extensions/Step3Conditions.tsx +++ b/src/components/TableHeader/Extensions/Step3Conditions.tsx @@ -2,6 +2,7 @@ import { lazy, Suspense } from "react"; import { IExtensionModalStepProps } from "./ExtensionModal"; import useStateRef from "react-usestateref"; +import { Typography } from "@mui/material"; import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton"; import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper"; import { WIKI_LINKS } from "@src/constants/externalLinks"; @@ -49,6 +50,12 @@ export default function Step3Conditions({ return ( <> + + Optionally, write a function that determines if the extension should be + triggered for a given row. Leave the function to always return{" "} + true if you do not want to write additional logic. + + }> + + Optionally, write a function that determines if the webhook call should + be processed. Leave the function to always return true if + you do not want to write additional logic. + +
+ + Write the webhook parsed function. The returned object of the parser + will be added as new row{" "} + + Docs + + + +
; export interface IWebhookModalStepProps { @@ -56,8 +41,6 @@ export default function WebhookModal({ const [webhookObject, setWebhookObject] = useState(initialObject); - const [activeStep, setActiveStep] = useState(0); - const [validation, setValidation, validationRef] = useStateRef({ condition: true, parser: true }); @@ -141,83 +124,27 @@ export default function WebhookModal({ - , }, - "& .MuiStepLabel-label svg": { - display: "block", - marginLeft: "auto", - my: ((24 - 18) / 2 / 8) * -1, - transition: (theme) => theme.transitions.create("transform"), + { + id: "conditions", + title: "Conditions", + optional: true, + content: , }, - "& .Mui-active svg": { - transform: "rotate(180deg)", + { + id: "parser", + title: "Parser", + content: , }, - }} - > - - setActiveStep(0)}> - Verification - - - - - - - - - setActiveStep(1)}> - Conditions (optional) - - - - - Optionally, write a function that determines if the webhook - call should be processed. Leave the function to always return{" "} - true if you do not want to write additional - logic. - - - - - - - setActiveStep(2)}> - Parser - - - - - Write the webhook parsed function. The returned object of the - parser will be added as new row{" "} - - Docs - - - - - - - + ]} + /> } actions={{ diff --git a/src/components/fields/Action/Settings.tsx b/src/components/fields/Action/Settings.tsx index 8db0ba76..da29613f 100644 --- a/src/components/fields/Action/Settings.tsx +++ b/src/components/fields/Action/Settings.tsx @@ -27,6 +27,7 @@ import RunIcon from "@mui/icons-material/PlayArrow"; import RedoIcon from "@mui/icons-material/Refresh"; import UndoIcon from "@mui/icons-material/Undo"; +import SteppedAccordion from "@src/components/SteppedAccordion"; import MultiSelect from "@rowy/multiselect"; import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton"; import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper"; @@ -101,287 +102,368 @@ const Settings = ({ config, onChange }) => { config.confirmation !== ""); return ( - theme.transitions.create("transform"), - }, - "& .Mui-active svg": { - transform: "rotate(180deg)", - }, - }} - > - - setActiveStep("requirements")}> - Requirements - - - - - - - - - - - - - - - - - setActiveStep("friction")}> - Confirmation - - - - - - - - Clicking the action button will: - - - onChange("friction")(e.target.value)} - > - } - label="Run the action immediately" + + + - } - label="Ask the user for confirmation" + + + - } - label={ - <> - - Ask the user for input in a form (Alpha) - + + + ), + }, + { + id: "confirmation", + title: "Confirmation", + content: ( + + + + Clicking the action button will: + - - This feature is currently undocumented and is subject to - change in future minor versions - - + - - - - {showConfirmationField && ( - onChange("confirmation")(e.target.value)} - fullWidth - helperText="The action button will not ask for confirmation if this is left empty" - /> - )} - - {config.friction === "params" && ( - - - - Form fields - - - - - - - }> - { - try { - if (v) { - const parsed = JSON.parse(v); - onChange("params")(parsed); - } - } catch (e) { - console.log(`Failed to parse JSON: ${e}`); - setCodeValid(false); - } - }} - onValidStatusUpdate={({ isValid }) => setCodeValid(isValid)} - error={!codeValid} + value={config.friction} + onChange={(e) => onChange("friction")(e.target.value)} + > + } + label="Run the action immediately" /> - + } + label="Ask the user for confirmation" + /> + } + label={ + <> + + Ask the user for input in a form (Alpha) + - {!codeValid && ( - - Invalid JSON - - )} + + This feature is currently undocumented and is subject + to change in future minor versions + + + } + /> + - )} - - - - - setActiveStep("action")}> - Action - - - - - - - - Clicking the action button will run a: - - - onChange("isActionScript")(e.target.value === "actionScript") - } - > - } - label={ - <> - Script - - Write JavaScript code below that will be executed by - Rowy Run.{" "} - - Requires Rowy Run setup - - - - - } + {showConfirmationField && ( + onChange("confirmation")(e.target.value)} + fullWidth + helperText="The action button will not ask for confirmation if this is left empty" /> - } - label={ - <> - Callable - - A{" "} - - callable function - - {" "} - you’ve deployed on your Firestore or Google Cloud - project - - - } - /> - - + )} - {!config.isActionScript ? ( - onChange("callableName")(e.target.value)} - helperText={ - <> - Write the name of the callable function you’ve deployed to - your project.{" "} - - View your callable functions - - -
- Your callable function must be compatible with Rowy Action - columns.{" "} - - View requirements - - - - } - /> - ) : ( - <> + {config.friction === "params" && ( - Action script + + + Form fields + + + + + + }> { + try { + if (v) { + const parsed = JSON.parse(v); + onChange("params")(parsed); + } + } catch (e) { + console.log(`Failed to parse JSON: ${e}`); + setCodeValid(false); + } + }} + onValidStatusUpdate={({ isValid }) => + setCodeValid(isValid) + } + error={!codeValid} + /> + + + {!codeValid && ( + + Invalid JSON + + )} + + )} +
+ ), + }, + { + id: "action", + title: "Action", + content: ( + + + + Clicking the action button will run a: + + + onChange("isActionScript")( + e.target.value === "actionScript" + ) + } + > + } + label={ + <> + Script + + Write JavaScript code below that will be executed by + Rowy Run.{" "} + + Requires Rowy Run setup + + + + + } + /> + } + label={ + <> + Callable + + A{" "} + + callable function + + {" "} + you’ve deployed on your Firestore or Google Cloud + project + + + } + /> + + + + {!config.isActionScript ? ( + onChange("callableName")(e.target.value)} + helperText={ + <> + Write the name of the callable function you’ve deployed to + your project.{" "} + + View your callable functions + + +
+ Your callable function must be compatible with Rowy Action + columns.{" "} + + View requirements + + + + } + /> + ) : ( + <> + + Action script + }> + + + + + + + + + onChange("redo.enabled")( + !Boolean(config.redo?.enabled) + ) + } + name="redo" + /> + } + label={ + <> + + User can redo + + + Re-runs the script above + + + } + style={{ marginLeft: -11 }} + /> + + + + onChange("undo.enabled")( + !Boolean(config.undo?.enabled) + ) + } + name="undo" + /> + } + label={ + <> + + User can undo + + + Runs a new script + + + } + style={{ marginLeft: -11 }} + /> + + + + )} +
+ ), + }, + config.isActionScript && + _get(config, "undo.enabled") && { + id: "undo", + title: "Undo action", + content: ( + + {(showConfirmationField || + !config.friction || + config.friction === "none") && ( + { + onChange("undo.confirmation")(e.target.value); + }} + fullWidth + helperText={ + <> + {showConfirmationField && + "Override the confirmation message above. "} + The action button will not ask for confirmation if this + is left empty{showConfirmationField && "."} + + } + /> + )} + + + Undo script + }> + @@ -390,230 +472,90 @@ const Settings = ({ config, onChange }) => { additionalVariables={[]} /> + + ), + }, + { + id: "customization", + title: "Customization", + content: ( + <> + + onChange("customIcons.enabled")(e.target.checked) + } + name="customIcons.enabled" + /> + } + label="Customize button icons with emoji" + style={{ marginLeft: -11 }} + /> - - - - onChange("redo.enabled")( - !Boolean(config.redo?.enabled) - ) - } - name="redo" - /> - } - label={ - <> - - User can redo - - - Re-runs the script above - - - } - style={{ marginLeft: -11 }} - /> + {config.customIcons?.enabled && ( + + + + + onChange("customIcons.run")(e.target.value) + } + label="Run:" + className="labelHorizontal" + inputProps={{ style: { width: "3ch" } }} + /> + + {_get(config, "customIcons.run") || } + + - - - onChange("undo.enabled")( - !Boolean(config.undo?.enabled) - ) - } - name="undo" - /> - } - label={ - <> - - User can undo - - - Runs a new script - - - } - style={{ marginLeft: -11 }} - /> + + + + + onChange("customIcons.redo")(e.target.value) + } + label="Redo:" + className="labelHorizontal" + inputProps={{ style: { width: "3ch" } }} + /> + + {_get(config, "customIcons.redo") || } + + + + + + + + onChange("customIcons.undo")(e.target.value) + } + label="Undo:" + className="labelHorizontal" + inputProps={{ style: { width: "3ch" } }} + /> + + {_get(config, "customIcons.undo") || } + + - - )} - -
-
- - {config.isActionScript && _get(config, "undo.enabled") && ( - - setActiveStep("undo")}> - Undo action - - - - - - {(showConfirmationField || - !config.friction || - config.friction === "none") && ( - { - onChange("undo.confirmation")(e.target.value); - }} - fullWidth - helperText={ - <> - {showConfirmationField && - "Override the confirmation message above. "} - The action button will not ask for confirmation if this is - left empty{showConfirmationField && "."} - - } - /> )} - - - Undo script - }> - - - - - - - - )} - - - setActiveStep("customization")}> - Customization - - - - - {/* */} - - onChange("customIcons.enabled")(e.target.checked) - } - name="customIcons.enabled" - /> - } - label="Customize button icons with emoji" - style={{ marginLeft: -11 }} - /> - - {config.customIcons?.enabled && ( - - - - - onChange("customIcons.run")(e.target.value) - } - label="Run:" - className="labelHorizontal" - inputProps={{ style: { width: "3ch" } }} - /> - - {_get(config, "customIcons.run") || } - - - - - - - - onChange("customIcons.redo")(e.target.value) - } - label="Redo:" - className="labelHorizontal" - inputProps={{ style: { width: "3ch" } }} - /> - - {_get(config, "customIcons.redo") || } - - - - - - - - onChange("customIcons.undo")(e.target.value) - } - label="Undo:" - className="labelHorizontal" - inputProps={{ style: { width: "3ch" } }} - /> - - {_get(config, "customIcons.undo") || } - - - - - )} - {/* */} - - {/* - - - - - - - */} - - -
+ + ), + }, + ].filter(Boolean)} + /> ); }; export default Settings;