From bdd7d2aaa2a61012f3abb18b810a07c80b3aa594 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 29 Oct 2021 18:09:37 +1100 Subject: [PATCH 01/15] add DiffEditor --- src/components/CodeEditor/DiffEditor.tsx | 93 +++++++ .../CodeEditor/github-light-default.json | 4 +- src/components/CodeEditor/index.tsx | 231 ++---------------- .../CodeEditor/useMonacoCustomizations.ts | 218 +++++++++++++++++ 4 files changed, 339 insertions(+), 207 deletions(-) create mode 100644 src/components/CodeEditor/DiffEditor.tsx create mode 100644 src/components/CodeEditor/useMonacoCustomizations.ts diff --git a/src/components/CodeEditor/DiffEditor.tsx b/src/components/CodeEditor/DiffEditor.tsx new file mode 100644 index 00000000..ad128708 --- /dev/null +++ b/src/components/CodeEditor/DiffEditor.tsx @@ -0,0 +1,93 @@ +import { + DiffEditor as MonacoDiffEditor, + DiffEditorProps, + EditorProps, +} from "@monaco-editor/react"; + +import { useTheme, Box, BoxProps } from "@mui/material"; +import CircularProgressOptical from "@src/components/CircularProgressOptical"; +import ResizeBottomRightIcon from "@src/assets/icons/ResizeBottomRight"; + +import useMonacoCustomizations, { + IUseMonacoCustomizationsProps, +} from "./useMonacoCustomizations"; + +export interface IDiffEditorProps + extends Partial, + IUseMonacoCustomizationsProps { + onChange?: EditorProps["onChange"]; + containerProps?: Partial; +} + +export default function DiffEditor({ + onChange, + minHeight = 100, + disabled, + error, + containerProps, + + extraLibs, + diagnosticsOptions, + onUnmount, + + ...props +}: IDiffEditorProps) { + const theme = useTheme(); + + const { boxSx } = useMonacoCustomizations({ + minHeight, + disabled, + error, + extraLibs, + diagnosticsOptions, + onUnmount, + }); + + // Needs manual patch since `onMount` prop is not available in `DiffEditor` + // https://github.com/suren-atoyan/monaco-react/issues/281 + const handleEditorMount: DiffEditorProps["onMount"] = (editor, monaco) => { + const modifiedEditor = editor.getModifiedEditor(); + modifiedEditor.onDidChangeModelContent((ev) => { + onChange?.(modifiedEditor.getValue(), ev); + }); + + props.onMount?.(editor, monaco); + }; + + return ( + + } + className="editor" + {...props} + onMount={handleEditorMount} + options={ + { + readOnly: disabled, + fontFamily: theme.typography.fontFamilyMono, + rulers: [80], + minimap: { enabled: false }, + lineNumbersMinChars: 4, + lineDecorationsWidth: "18", + automaticLayout: true, + fixedOverflowWidgets: true, + tabSize: 2, + ...props.options, + } as any + } + /> + + + + ); +} diff --git a/src/components/CodeEditor/github-light-default.json b/src/components/CodeEditor/github-light-default.json index da31839d..e27c1a01 100644 --- a/src/components/CodeEditor/github-light-default.json +++ b/src/components/CodeEditor/github-light-default.json @@ -126,8 +126,8 @@ "editorGutter.modifiedBackground": "#d4a72c66", "editorGutter.addedBackground": "#4ac26b66", "editorGutter.deletedBackground": "#ff818266", - "diffEditor.insertedTextBackground": "#dafbe1", - "diffEditor.removedTextBackground": "#ffebe9", + "diffEditor.insertedTextBackground": "#85e89d33", + "diffEditor.removedTextBackground": "#f9758326", "scrollbar.shadow": "#6a737d33", "scrollbarSlider.background": "#959da533", "scrollbarSlider.hoverBackground": "#959da544", diff --git a/src/components/CodeEditor/index.tsx b/src/components/CodeEditor/index.tsx index 81c4abc5..31745502 100644 --- a/src/components/CodeEditor/index.tsx +++ b/src/components/CodeEditor/index.tsx @@ -1,39 +1,26 @@ -import React, { useState, useEffect } from "react"; - -import Editor, { EditorProps, useMonaco } from "@monaco-editor/react"; -import type { editor, languages } from "monaco-editor/esm/vs/editor/editor.api"; -import githubLightTheme from "./github-light-default.json"; -import githubDarkTheme from "./github-dark-default.json"; +import { useState } from "react"; +import Editor, { EditorProps } from "@monaco-editor/react"; +import type { editor } from "monaco-editor/esm/vs/editor/editor.api"; import { useTheme, Box, BoxProps } from "@mui/material"; import CircularProgressOptical from "@src/components/CircularProgressOptical"; import ResizeBottomRightIcon from "@src/assets/icons/ResizeBottomRight"; -import { useProjectContext } from "@src/contexts/ProjectContext"; -import { getFieldProp } from "@src/components/fields"; +import useMonacoCustomizations, { + IUseMonacoCustomizationsProps, +} from "./useMonacoCustomizations"; -/* eslint-disable import/no-webpack-loader-syntax */ -import firestoreDefs from "!!raw-loader!./firestore.d.ts"; -import firebaseAuthDefs from "!!raw-loader!./firebaseAuth.d.ts"; -import firebaseStorageDefs from "!!raw-loader!./firebaseStorage.d.ts"; -import utilsDefs from "!!raw-loader!./utils.d.ts"; -import extensionsDefs from "!!raw-loader!./extensions.d.ts"; - -export interface ICodeEditorProps extends Partial { +export interface ICodeEditorProps + extends Partial, + IUseMonacoCustomizationsProps { value: string; - minHeight?: number; - disabled?: boolean; - error?: boolean; containerProps?: Partial; - extraLibs?: string[]; onValidate?: EditorProps["onValidate"]; onValidStatusUpdate?: (result: { isValid: boolean; markers: editor.IMarker[]; }) => void; - diagnosticsOptions?: languages.typescript.DiagnosticsOptions; - onUnmount?: () => void; } export default function CodeEditor({ @@ -43,210 +30,44 @@ export default function CodeEditor({ error, containerProps, - extraLibs, onValidate, onValidStatusUpdate, + + extraLibs, diagnosticsOptions, onUnmount, ...props }: ICodeEditorProps) { const theme = useTheme(); - const { tableState } = useProjectContext(); + // Store editor value to prevent code editor values not being saved when + // Side Drawer is in the middle of a refresh const [initialEditorValue] = useState(value ?? ""); - const monaco = useMonaco(); - useEffect(() => { - return () => { - onUnmount?.(); - }; - }, []); + const { boxSx } = useMonacoCustomizations({ + minHeight, + disabled, + error, + extraLibs, + diagnosticsOptions, + onUnmount, + }); const onValidate_: EditorProps["onValidate"] = (markers) => { - if (onValidStatusUpdate) - onValidStatusUpdate({ isValid: markers.length <= 0, markers }); - else if (onValidate) onValidate(markers); + onValidStatusUpdate?.({ isValid: markers.length <= 0, markers }); + onValidate?.(markers); }; - useEffect(() => { - if (!monaco) { - // useMonaco returns a monaco instance but initialisation is done asynchronously - // dont execute the logic until the instance is initialised - return; - } - - setTimeout(() => { - try { - monaco.editor.defineTheme("github-light", githubLightTheme as any); - monaco.editor.defineTheme("github-dark", githubDarkTheme as any); - monaco.editor.setTheme("github-" + theme.palette.mode); - } catch (error) { - console.error("Could not set Monaco theme: ", error); - } - }); - }, [monaco, theme.palette.mode]); - - useEffect(() => { - if (!monaco) { - // useMonaco returns a monaco instance but initialisation is done asynchronously - // dont execute the logic until the instance is initialised - return; - } - - try { - monaco.editor.defineTheme("github-light", githubLightTheme as any); - monaco.editor.defineTheme("github-dark", githubDarkTheme as any); - - monaco.languages.typescript.javascriptDefaults.addExtraLib(firestoreDefs); - monaco.languages.typescript.javascriptDefaults.addExtraLib( - firebaseAuthDefs - ); - monaco.languages.typescript.javascriptDefaults.addExtraLib( - firebaseStorageDefs - ); - monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions( - diagnosticsOptions ?? { - noSemanticValidation: true, - noSyntaxValidation: false, - } - ); - // compiler options - monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ - target: monaco.languages.typescript.ScriptTarget.ES2020, - allowNonTsExtensions: true, - }); - if (extraLibs) { - monaco.languages.typescript.javascriptDefaults.addExtraLib( - extraLibs.join("\n"), - "ts:filename/extraLibs.d.ts" - ); - } - monaco.languages.typescript.javascriptDefaults.addExtraLib( - utilsDefs, - "ts:filename/utils.d.ts" - ); - - const rowDefinition = - Object.keys(tableState?.columns!) - .map((columnKey: string) => { - const column = tableState?.columns[columnKey]; - return `static ${columnKey}: ${getFieldProp("type", column.type)}`; - }) - .join(";\n") + ";"; - - const availableFields = Object.keys(tableState?.columns!) - .map((columnKey: string) => `"${columnKey}"`) - .join("|\n"); - - monaco.languages.typescript.javascriptDefaults.addExtraLib( - [ - "/**", - " * extensions type configuration", - " */", - "// basic types that are used in all places", - `type Row = {${rowDefinition}};`, - `type Field = ${availableFields} | string | object;`, - `type Fields = Field[];`, - extensionsDefs, - ].join("\n"), - "ts:filename/extensions.d.ts" - ); - - monaco.languages.typescript.javascriptDefaults.addExtraLib( - [ - "declare var require: any;", - "declare var Buffer: any;", - "const ref: FirebaseFirestore.DocumentReference;", - "const storage: firebasestorage.Storage;", - "const db: FirebaseFirestore.Firestore;", - "const auth: adminauth.BaseAuth;", - "declare class row {", - " /**", - " * Returns the row fields", - " */", - rowDefinition, - "}", - ].join("\n"), - "ts:filename/rowFields.d.ts" - ); - } catch (error) { - console.error( - "An error occurred during initialization of Monaco: ", - error - ); - } - }, [tableState?.columns, monaco, diagnosticsOptions, extraLibs]); - return ( - + } className="editor" {...props} + onValidate={onValidate_} options={{ readOnly: disabled, fontFamily: theme.typography.fontFamilyMono, @@ -262,7 +83,7 @@ export default function CodeEditor({ /> void; +} + +export default function useMonacoCustomizations({ + minHeight, + disabled, + error, + + extraLibs, + diagnosticsOptions, + onUnmount, +}: IUseMonacoCustomizationsProps) { + const theme = useTheme(); + const { tableState } = useProjectContext(); + + const monaco = useMonaco(); + + useEffect(() => { + return () => { + onUnmount?.(); + }; + }, []); + + useEffect(() => { + if (!monaco) { + // useMonaco returns a monaco instance but initialisation is done asynchronously + // dont execute the logic until the instance is initialised + return; + } + + setTimeout(() => { + try { + monaco.editor.defineTheme("github-light", githubLightTheme as any); + monaco.editor.defineTheme("github-dark", githubDarkTheme as any); + monaco.editor.setTheme("github-" + theme.palette.mode); + } catch (error) { + console.error("Could not set Monaco theme: ", error); + } + }); + }, [monaco, theme.palette.mode]); + + useEffect(() => { + if (!monaco) { + // useMonaco returns a monaco instance but initialisation is done asynchronously + // dont execute the logic until the instance is initialised + return; + } + + try { + monaco.editor.defineTheme("github-light", githubLightTheme as any); + monaco.editor.defineTheme("github-dark", githubDarkTheme as any); + + monaco.languages.typescript.javascriptDefaults.addExtraLib(firestoreDefs); + monaco.languages.typescript.javascriptDefaults.addExtraLib( + firebaseAuthDefs + ); + monaco.languages.typescript.javascriptDefaults.addExtraLib( + firebaseStorageDefs + ); + monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions( + diagnosticsOptions ?? { + noSemanticValidation: true, + noSyntaxValidation: false, + } + ); + // compiler options + monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + target: monaco.languages.typescript.ScriptTarget.ES2020, + allowNonTsExtensions: true, + }); + if (extraLibs) { + monaco.languages.typescript.javascriptDefaults.addExtraLib( + extraLibs.join("\n"), + "ts:filename/extraLibs.d.ts" + ); + } + monaco.languages.typescript.javascriptDefaults.addExtraLib( + utilsDefs, + "ts:filename/utils.d.ts" + ); + + const rowDefinition = + Object.keys(tableState?.columns!) + .map((columnKey: string) => { + const column = tableState?.columns[columnKey]; + return `static ${columnKey}: ${getFieldProp("type", column.type)}`; + }) + .join(";\n") + ";"; + + const availableFields = Object.keys(tableState?.columns!) + .map((columnKey: string) => `"${columnKey}"`) + .join("|\n"); + + monaco.languages.typescript.javascriptDefaults.addExtraLib( + [ + "/**", + " * extensions type configuration", + " */", + "// basic types that are used in all places", + `type Row = {${rowDefinition}};`, + `type Field = ${availableFields} | string | object;`, + `type Fields = Field[];`, + extensionsDefs, + ].join("\n"), + "ts:filename/extensions.d.ts" + ); + + monaco.languages.typescript.javascriptDefaults.addExtraLib( + [ + "declare var require: any;", + "declare var Buffer: any;", + "const ref: FirebaseFirestore.DocumentReference;", + "const storage: firebasestorage.Storage;", + "const db: FirebaseFirestore.Firestore;", + "const auth: adminauth.BaseAuth;", + "declare class row {", + " /**", + " * Returns the row fields", + " */", + rowDefinition, + "}", + ].join("\n"), + "ts:filename/rowFields.d.ts" + ); + } catch (error) { + console.error( + "An error occurred during initialization of Monaco: ", + error + ); + } + }, [tableState?.columns, monaco, diagnosticsOptions, extraLibs]); + + const boxSx: SxProps = { + minWidth: 400, + minHeight, + height: minHeight, + borderRadius: 1, + resize: "vertical", + overflow: "hidden", + position: "relative", + backgroundColor: disabled ? "transparent" : theme.palette.action.input, + + "&::after": { + content: '""', + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + pointerEvents: "none", + borderRadius: "inherit", + + boxShadow: `0 -1px 0 0 ${theme.palette.text.disabled} inset, + 0 0 0 1px ${theme.palette.action.inputOutline} inset`, + transition: theme.transitions.create("box-shadow", { + duration: theme.transitions.duration.short, + }), + }, + + "&:hover::after": { + boxShadow: `0 -1px 0 0 ${theme.palette.text.primary} inset, + 0 0 0 1px ${theme.palette.action.inputOutline} inset`, + }, + "&:focus-within::after": { + boxShadow: `0 -2px 0 0 ${theme.palette.primary.main} inset, + 0 0 0 1px ${theme.palette.action.inputOutline} inset`, + }, + + ...(error + ? { + "&::after, &:hover::after, &:focus-within::after": { + boxShadow: `0 -2px 0 0 ${theme.palette.error.main} inset, + 0 0 0 1px ${theme.palette.action.inputOutline} inset`, + }, + } + : {}), + + "& .editor": { + // Overwrite user-select: none that causes editor + // to not be focusable in Safari + userSelect: "auto", + height: "100%", + }, + + "& .monaco-editor, & .monaco-editor .margin, & .monaco-editor-background": { + backgroundColor: "transparent", + }, + }; + + return { boxSx }; +} From 97e3f85f66abb133048f34905a15724ed667f944 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 29 Oct 2021 18:09:49 +1100 Subject: [PATCH 02/15] add DiffEditor to setup rules step --- src/components/Setup/SetupItem.tsx | 2 +- src/components/Setup/Step4Rules.tsx | 199 ++++++++++++++-------------- src/pages/Setup.tsx | 6 +- 3 files changed, 102 insertions(+), 105 deletions(-) diff --git a/src/components/Setup/SetupItem.tsx b/src/components/Setup/SetupItem.tsx index 6125b07e..ea099950 100644 --- a/src/components/Setup/SetupItem.tsx +++ b/src/components/Setup/SetupItem.tsx @@ -31,7 +31,7 @@ export default function SetupItem({ )} - + {title} {children} diff --git a/src/components/Setup/Step4Rules.tsx b/src/components/Setup/Step4Rules.tsx index 53fb9144..ce4cd124 100644 --- a/src/components/Setup/Step4Rules.tsx +++ b/src/components/Setup/Step4Rules.tsx @@ -7,13 +7,14 @@ import { Checkbox, Button, Link, - TextField, + Grid, } from "@mui/material"; import LoadingButton from "@mui/lab/LoadingButton"; import CopyIcon from "@src/assets/icons/Copy"; import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; import SetupItem from "./SetupItem"; +import DiffEditor from "@src/components/CodeEditor/DiffEditor"; import { name } from "@root/package.json"; import { useAppContext } from "@src/contexts/AppContext"; @@ -21,7 +22,7 @@ import { CONFIG } from "@src/config/dbPaths"; import { requiredRules, adminRules, utilFns } from "@src/config/firestoreRules"; import { rowyRun } from "@src/utils/rowyRun"; import { runRoutes } from "@src/constants/runRoutes"; -import { useConfirmation } from "@src/components/ConfirmationDialog"; +// import { useConfirmation } from "@src/components/ConfirmationDialog"; export default function Step4Rules({ rowyRunUrl, @@ -29,7 +30,7 @@ export default function Step4Rules({ setCompletion, }: ISetupStepBodyProps) { const { projectId, getAuthToken } = useAppContext(); - const { requestConfirmation } = useConfirmation(); + // const { requestConfirmation } = useConfirmation(); const [hasRules, setHasRules] = useState(completion.rules); const [adminRule, setAdminRule] = useState(true); @@ -97,18 +98,20 @@ export default function Step4Rules({ } }; - const handleSkip = () => { - requestConfirmation({ - title: "Skip rules", - body: "This might prevent you or other users in your project from accessing firestore data on Rowy", - confirm: "Skip", - cancel: "cancel", - handleConfirm: async () => { - setCompletion((c) => ({ ...c, rules: true })); - setHasRules(true); - }, - }); - }; + const [showManualMode, setShowManualMode] = useState(false); + + // const handleSkip = () => { + // requestConfirmation({ + // title: "Skip rules", + // body: "This might prevent you or other users in your project from accessing firestore data on Rowy", + // confirm: "Skip", + // cancel: "cancel", + // handleConfirm: async () => { + // setCompletion((c) => ({ ...c, rules: true })); + // setHasRules(true); + // }, + // }); + // }; return ( <> @@ -138,89 +141,21 @@ export default function Step4Rules({ label="Allow admins to read and write all documents" sx={{ "&&": { ml: -11 / 8, mb: -11 / 8 }, width: "100%" }} /> - - $1` - ), - }} + - - - - )} - - - {!hasRules && ( - - You can add these rules{" "} - - in the Firebase Console - - {" "} - or directly below: - - } - > - setNewRules(e.target.value)} - multiline - rows={5} - fullWidth - sx={{ - "& .MuiInputBase-input": { - fontFamily: "mono", - letterSpacing: 0, - resize: "vertical", - }, - }} - /> - - - Please check the generated rules first. - - -
- {" "} + Please verify the new rules first. + Set Firestore Rules - + {rulesStatus !== "LOADING" && typeof rulesStatus === "string" && ( + + {rulesStatus} + + )} + {!showManualMode && ( + setShowManualMode(true)} + > + Alternatively, add these rules in the Firebase Console + + )} + + )} + + + {!hasRules && showManualMode && ( + + $1` + ), + }} + /> + +
+ + + + + + + + +
- {rulesStatus !== "LOADING" && typeof rulesStatus === "string" && ( - - {rulesStatus} - - )}
)} diff --git a/src/pages/Setup.tsx b/src/pages/Setup.tsx index 882e5e21..f9789223 100644 --- a/src/pages/Setup.tsx +++ b/src/pages/Setup.tsx @@ -60,6 +60,8 @@ export interface ISetupStepBodyProps { rowyRunUrl: string; } +const BASE_WIDTH = 1024; + const checkAllSteps = async ( rowyRunUrl: string, currentUser: firebase.default.User | null | undefined, @@ -256,7 +258,7 @@ export default function SetupPage() { alpha(theme.palette.background.paper, 0.5), backdropFilter: "blur(20px) saturate(150%)", - maxWidth: 840, + maxWidth: BASE_WIDTH, width: (theme) => `calc(100vw - ${theme.spacing(2)})`, maxHeight: (theme) => `calc(${ @@ -264,7 +266,7 @@ export default function SetupPage() { } - ${theme.spacing( 2 )} - env(safe-area-inset-top) - env(safe-area-inset-bottom))`, - height: 840 * 0.75, + height: BASE_WIDTH * 0.75, resize: "both", p: 0, From 01c94b5b39e2d8ae88cdf315aa2e041d2ba00f98 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 29 Oct 2021 18:10:02 +1100 Subject: [PATCH 03/15] NavDrawer: use material icon for closing drawer --- src/components/Navigation/NavDrawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Navigation/NavDrawer.tsx b/src/components/Navigation/NavDrawer.tsx index 23e48e91..a223d857 100644 --- a/src/components/Navigation/NavDrawer.tsx +++ b/src/components/Navigation/NavDrawer.tsx @@ -15,7 +15,7 @@ import HomeIcon from "@mui/icons-material/HomeOutlined"; import SettingsIcon from "@mui/icons-material/SettingsOutlined"; import ProjectSettingsIcon from "@mui/icons-material/BuildCircleOutlined"; import UserManagementIcon from "@mui/icons-material/AccountCircleOutlined"; -import CloseIcon from "@src/assets/icons/Backburger"; +import CloseIcon from "@mui/icons-material/MenuOpen"; import PinIcon from "@mui/icons-material/PushPinOutlined"; import UnpinIcon from "@mui/icons-material/PushPin"; From f2d630ea1e6c5fb2dead6bfc658c6231208f2fff Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Sun, 31 Oct 2021 15:07:30 +1100 Subject: [PATCH 04/15] fix action SchemaPath ref --- src/components/fields/Action/ActionFab.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/fields/Action/ActionFab.tsx b/src/components/fields/Action/ActionFab.tsx index 30b5eeee..962f647e 100644 --- a/src/components/fields/Action/ActionFab.tsx +++ b/src/components/fields/Action/ActionFab.tsx @@ -54,7 +54,6 @@ export default function ActionFab({ const { tableState, rowyRun } = useProjectContext(); const { ref } = row; const { config } = column as any; - const action = !value ? "run" : value.undo @@ -71,7 +70,7 @@ export default function ActionFab({ ref: { path: ref.path }, column: { ...column, editor: undefined }, action, - schemaDocPath: formatPath(tableState?.tablePath ?? ""), + schemaDocPath: tableState?.config.tableConfig.path, actionParams, }); From 58ecfedad2ac6bc35f6aa650da8105ef05945054 Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Sun, 31 Oct 2021 16:27:05 +1100 Subject: [PATCH 05/15] connect service config --- .../fields/ConnectService/Settings.tsx | 44 +++++++++++-------- .../fields/ConnectService/index.tsx | 1 + 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/components/fields/ConnectService/Settings.tsx b/src/components/fields/ConnectService/Settings.tsx index fd4205e1..78f409ef 100644 --- a/src/components/fields/ConnectService/Settings.tsx +++ b/src/components/fields/ConnectService/Settings.tsx @@ -1,4 +1,4 @@ -import { TextField, FormControlLabel, Switch } from "@mui/material"; +import { TextField, FormControlLabel, Switch, Grid } from "@mui/material"; export default function Settings({ config, handleChange }) { return ( @@ -32,24 +32,30 @@ export default function Settings({ config, handleChange }) { handleChange("primaryKey")(e.target.value); }} /> - { - handleChange("titleKey")(e.target.value); - }} - /> - { - handleChange("subtitleKey")(e.target.value); - }} - /> + + + { + handleChange("titleKey")(e.target.value); + }} + /> + + + { + handleChange("subtitleKey")(e.target.value); + }} + />{" "} + {" "} + Date: Mon, 1 Nov 2021 12:24:48 +1100 Subject: [PATCH 06/15] remove monaco diff editor validation for firestore rules --- src/components/Setup/Step4Rules.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Setup/Step4Rules.tsx b/src/components/Setup/Step4Rules.tsx index ce4cd124..73626860 100644 --- a/src/components/Setup/Step4Rules.tsx +++ b/src/components/Setup/Step4Rules.tsx @@ -146,7 +146,7 @@ export default function Step4Rules({ modified={newRules} containerProps={{ sx: { width: "100%" } }} minHeight={400} - // options={{ renderValidationDecorations: "off" }} + options={{ renderValidationDecorations: "off" }} /> Date: Mon, 1 Nov 2021 13:18:21 +1100 Subject: [PATCH 07/15] ConfirmationDialog: add hideCancel prop --- src/components/ConfirmationDialog/Dialog.tsx | 5 ++++- src/components/ConfirmationDialog/props.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/ConfirmationDialog/Dialog.tsx b/src/components/ConfirmationDialog/Dialog.tsx index 57cf77d4..d32a6088 100644 --- a/src/components/ConfirmationDialog/Dialog.tsx +++ b/src/components/ConfirmationDialog/Dialog.tsx @@ -17,6 +17,7 @@ export default function Confirmation({ customBody, body, cancel, + hideCancel, confirm, confirmationCommand, handleConfirm, @@ -55,7 +56,9 @@ export default function Confirmation({ - + {!hideCancel && ( + + )}
); } + +export const settingsValidator = (config) => { + const errors: Record = {}; + if (!config.listenerFields) errors.listenerFields = "Required"; + if (!config.renderFieldType) errors.renderFieldType = "Required"; + return errors; +}; diff --git a/src/components/fields/Derivative/index.tsx b/src/components/fields/Derivative/index.tsx index 21b4c57f..ecde4190 100644 --- a/src/components/fields/Derivative/index.tsx +++ b/src/components/fields/Derivative/index.tsx @@ -4,7 +4,7 @@ import withBasicCell from "../_withTableCell/withBasicCell"; import DerivativeIcon from "@src/assets/icons/Derivative"; import BasicCell from "../_BasicCell/BasicCellNull"; import NullEditor from "@src/components/Table/editors/NullEditor"; -import Settings from "./Settings"; +import Settings, { settingsValidator } from "./Settings"; export const config: IFieldConfig = { type: FieldType.derivative, @@ -21,5 +21,6 @@ export const config: IFieldConfig = { TableEditor: NullEditor as any, SideDrawerField: BasicCell as any, settings: Settings, + settingsValidator, }; export default config; diff --git a/src/components/fields/Json/Settings.tsx b/src/components/fields/Json/Settings.tsx index d424c707..c5c95283 100644 --- a/src/components/fields/Json/Settings.tsx +++ b/src/components/fields/Json/Settings.tsx @@ -1,13 +1,13 @@ import { Checkbox, FormControlLabel, FormHelperText } from "@mui/material"; -const Settings = ({ config, handleChange }) => { +const Settings = ({ config, onChange }) => { return ( <> handleChange("isArray")(!Boolean(config.isArray))} + onChange={() => onChange("isArray")(!Boolean(config.isArray))} name="isArray" /> } diff --git a/src/components/fields/Rating/Settings.tsx b/src/components/fields/Rating/Settings.tsx index 22dfdc6a..abf9565a 100644 --- a/src/components/fields/Rating/Settings.tsx +++ b/src/components/fields/Rating/Settings.tsx @@ -3,7 +3,7 @@ import { ISettingsProps } from "../types"; import { Slider } from "@mui/material"; import Subheading from "@src/components/Table/ColumnMenu/Subheading"; -export default function Settings({ handleChange, config }: ISettingsProps) { +export default function Settings({ onChange, config }: ISettingsProps) { return ( <> Maximum number of stars @@ -14,7 +14,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) { aria-labelledby="max-slider" valueLabelDisplay="auto" onChange={(_, v) => { - handleChange("max")(v); + onChange("max")(v); }} step={1} marks @@ -29,7 +29,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) { aria-labelledby="precision-slider" valueLabelDisplay="auto" onChange={(_, v) => { - handleChange("precision")(v); + onChange("precision")(v); }} step={0.25} marks diff --git a/src/components/fields/ShortText/Settings.tsx b/src/components/fields/ShortText/Settings.tsx index 90efca64..345c0894 100644 --- a/src/components/fields/ShortText/Settings.tsx +++ b/src/components/fields/ShortText/Settings.tsx @@ -1,6 +1,6 @@ import { TextField } from "@mui/material"; -export default function Settings({ handleChange, config }) { +export default function Settings({ onChange, config }) { return ( <> { - if (e.target.value === "0") handleChange("maxLength")(null); - else handleChange("maxLength")(e.target.value); + if (e.target.value === "0") onChange("maxLength")(null); + else onChange("maxLength")(e.target.value); }} /> { - if (e.target.value === "") handleChange("validationRegex")(null); - else handleChange("validationRegex")(e.target.value); + if (e.target.value === "") onChange("validationRegex")(null); + else onChange("validationRegex")(e.target.value); }} /> diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index d744bcf2..dd2fd69d 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -29,14 +29,14 @@ const useStyles = makeStyles(() => }) ); -export default function Settings({ handleChange, config }) { +export default function Settings({ onChange, config }) { const listEndRef: any = useRef(null); const options = config.options ?? []; const classes = useStyles(); const [newOption, setNewOption] = useState(""); const handleAdd = () => { if (newOption.trim() !== "") { - handleChange("options")([...options, newOption.trim()]); + onChange("options")([...options, newOption.trim()]); setNewOption(""); listEndRef.current.scrollIntoView({ behavior: "smooth", block: "end" }); } @@ -62,7 +62,7 @@ export default function Settings({ handleChange, config }) { - handleChange("options")( + onChange("options")( options.filter((o: string) => o !== option) ) } @@ -112,7 +112,7 @@ export default function Settings({ handleChange, config }) { control={ handleChange("freeText")(e.target.checked)} + onChange={(e) => onChange("freeText")(e.target.checked)} /> } label={ diff --git a/src/components/fields/Slider/Settings.tsx b/src/components/fields/Slider/Settings.tsx index b1b6e12a..e333788b 100644 --- a/src/components/fields/Slider/Settings.tsx +++ b/src/components/fields/Slider/Settings.tsx @@ -1,7 +1,7 @@ import { TextField, FormControlLabel, Switch } from "@mui/material"; import Subheading from "@src/components/Table/ColumnMenu/Subheading"; -export default function Settings({ handleChange, config }) { +export default function Settings({ onChange, config }) { return ( <> Slider config @@ -10,7 +10,7 @@ export default function Settings({ handleChange, config }) { variant="filled" fullWidth margin="none" - onChange={(e) => handleChange("min")(parseFloat(e.target.value))} + onChange={(e) => onChange("min")(parseFloat(e.target.value))} value={config["min"]} id={`settings-field-min`} label="Minimum value" @@ -21,7 +21,7 @@ export default function Settings({ handleChange, config }) { variant="filled" fullWidth margin="none" - onChange={(e) => handleChange("max")(parseFloat(e.target.value))} + onChange={(e) => onChange("max")(parseFloat(e.target.value))} value={config["max"]} id={`settings-field-max`} label="Maximum value" @@ -32,7 +32,7 @@ export default function Settings({ handleChange, config }) { variant="filled" fullWidth margin="none" - onChange={(e) => handleChange("step")(parseFloat(e.target.value))} + onChange={(e) => onChange("step")(parseFloat(e.target.value))} value={config["step"]} id={`settings-field-step`} label="Step value" @@ -43,7 +43,7 @@ export default function Settings({ handleChange, config }) { control={ handleChange("marks")(!Boolean(config.marks))} + onChange={() => onChange("marks")(!Boolean(config.marks))} name="marks" /> } diff --git a/src/components/fields/Status/Settings.tsx b/src/components/fields/Status/Settings.tsx index 9b9a5e51..27f20bc9 100644 --- a/src/components/fields/Status/Settings.tsx +++ b/src/components/fields/Status/Settings.tsx @@ -172,7 +172,7 @@ const ConditionModal = ({ modal, setModal, conditions, setConditions }) => { ); }; -export default function Settings({ handleChange, config }: ISettingsProps) { +export default function Settings({ onChange, config }: ISettingsProps) { const [modal, setModal] = useState(EMPTY_STATE); const { conditions } = config; return ( @@ -228,7 +228,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) { modal={modal} setModal={setModal} conditions={config.conditions} - setConditions={handleChange("conditions")} + setConditions={onChange("conditions")} /> ); diff --git a/src/components/fields/SubTable/Settings.tsx b/src/components/fields/SubTable/Settings.tsx index 1fe6d9a3..43b74ad5 100644 --- a/src/components/fields/SubTable/Settings.tsx +++ b/src/components/fields/SubTable/Settings.tsx @@ -2,7 +2,7 @@ import MultiSelect from "@rowy/multiselect"; import { FieldType } from "@src/constants/fields"; import { useProjectContext } from "@src/contexts/ProjectContext"; -const Settings = ({ config, handleChange }) => { +const Settings = ({ config, onChange }) => { const { tableState } = useProjectContext(); if (!tableState?.columns) return <>; const columnOptions = Object.values(tableState.columns) @@ -21,7 +21,7 @@ const Settings = ({ config, handleChange }) => { label="Parent label" options={columnOptions} value={config.parentLabel ?? []} - onChange={handleChange("parentLabel")} + onChange={onChange("parentLabel")} /> ); diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index a62a6199..0a2b3a8e 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -21,6 +21,7 @@ export interface IFieldConfig { TableEditor: React.ComponentType>; SideDrawerField: React.ComponentType; settings?: React.ComponentType; + settingsValidator?: (config: Record) => Record; filter?: { operators: IFilterOperator[]; customInput?: React.ComponentType; @@ -59,14 +60,16 @@ export interface ISideDrawerFieldProps { } export interface ISettingsProps { - handleChange: (key: string) => (value: any) => void; + onChange: (key: string) => (value: any) => void; config: Record; fieldName: string; + onBlur: React.FocusEventHandler; + errors: Record; } // TODO: WRITE TYPES export interface IFiltersProps { - handleChange: (key: string) => (value: any) => void; + onChange: (key: string) => (value: any) => void; [key: string]: any; } From cfea4fde327346a39223cc736600c537fb7d0bf6 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 1 Nov 2021 15:50:27 +1100 Subject: [PATCH 09/15] fix hidden fields & filters not working with multiple table views --- src/components/SideDrawer/index.tsx | 2 +- src/components/Table/BulkActions/index.tsx | 2 +- src/components/Table/Filters/index.tsx | 10 +++++----- src/components/Table/HiddenFields.tsx | 4 ++-- src/components/Table/TableHeader/Export/Download.tsx | 2 +- src/components/Table/TableHeader/Export/Export.tsx | 3 ++- src/components/Table/index.tsx | 4 ++-- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/components/SideDrawer/index.tsx b/src/components/SideDrawer/index.tsx index 2a5b5156..a7ebedad 100644 --- a/src/components/SideDrawer/index.tsx +++ b/src/components/SideDrawer/index.tsx @@ -70,7 +70,7 @@ export default function SideDrawer() { if (cell && tableState?.rows[cell.row]) { window.history.pushState( "", - `${tableState?.tablePath}`, + `${tableState?.config.id}`, `${window.location.pathname}?rowRef=${encodeURIComponent( tableState?.rows[cell.row].ref.path )}` diff --git a/src/components/Table/BulkActions/index.tsx b/src/components/Table/BulkActions/index.tsx index 54cd3725..5253f143 100644 --- a/src/components/Table/BulkActions/index.tsx +++ b/src/components/Table/BulkActions/index.tsx @@ -147,7 +147,7 @@ export default function BulkActions({ selectedRows, columns, clearSelection }) { }, column: actionColumn, action: actionType, - schemaDocPath: formatPath(tableState?.tablePath ?? ""), + schemaDocPath: formatPath(tableState?.config.id ?? ""), actionParams: {}, }; return true; diff --git a/src/components/Table/Filters/index.tsx b/src/components/Table/Filters/index.tsx index 02324083..15e5cc94 100644 --- a/src/components/Table/Filters/index.tsx +++ b/src/components/Table/Filters/index.tsx @@ -39,15 +39,15 @@ export default function Filters() { const [anchorEl, setAnchorEl] = useState(null); useEffect(() => { - if (userDoc.state.doc && tableState?.tablePath) { - if (userDoc.state.doc.tables?.[tableState?.tablePath]?.filters) { + if (userDoc.state.doc && tableState?.config.id) { + if (userDoc.state.doc.tables?.[tableState?.config.id]?.filters) { tableActions?.table.filter( - userDoc.state.doc.tables[tableState?.tablePath].filters + userDoc.state.doc.tables[tableState?.config.id].filters ); tableActions?.table.orderBy(); } } - }, [userDoc.state, tableState?.tablePath]); + }, [userDoc.state, tableState?.config.id]); const filterColumns = _sortBy(Object.values(tableState!.columns), "index") .filter((c) => getFieldProp("filter", c.type)) @@ -100,7 +100,7 @@ export default function Filters() { userDoc.dispatch({ action: DocActions.update, data: { - tables: { [`${tableState?.tablePath}`]: { filters } }, + tables: { [`${tableState?.config.id}`]: { filters } }, }, }); }; diff --git a/src/components/Table/HiddenFields.tsx b/src/components/Table/HiddenFields.tsx index edac302c..8a02db85 100644 --- a/src/components/Table/HiddenFields.tsx +++ b/src/components/Table/HiddenFields.tsx @@ -69,7 +69,7 @@ export default function HiddenFields() { // Initialise hiddenFields from user doc const userDocHiddenFields = - userDoc.state.doc?.tables?.[formatSubTableName(tableState?.tablePath!)] + userDoc.state.doc?.tables?.[formatSubTableName(tableState?.config.id!)] ?.hiddenFields; useEffect(() => { if (userDocHiddenFields) setHiddenFields(userDocHiddenFields); @@ -94,7 +94,7 @@ export default function HiddenFields() { action: DocActions.update, data: { tables: { - [formatSubTableName(tableState?.tablePath)]: { hiddenFields }, + [formatSubTableName(tableState?.config.id)]: { hiddenFields }, }, }, }); diff --git a/src/components/Table/TableHeader/Export/Download.tsx b/src/components/Table/TableHeader/Export/Download.tsx index 72b84168..374eacb9 100644 --- a/src/components/Table/TableHeader/Export/Download.tsx +++ b/src/components/Table/TableHeader/Export/Download.tsx @@ -64,7 +64,7 @@ export default function Export({ query, closeModal }) { const [columns, setColumns] = useState([]); const [labelColumnsEnabled, setLabelColumnsEnabled] = useState(false); const [labelColumns, setLabelColumns] = useState([]); - const [packageName, setPackageName] = useState(tableState?.tablePath); + const [packageName, setPackageName] = useState(tableState?.config.id); const handleClose = () => { closeModal(); diff --git a/src/components/Table/TableHeader/Export/Export.tsx b/src/components/Table/TableHeader/Export/Export.tsx index 8c99a9a5..a2b527e6 100644 --- a/src/components/Table/TableHeader/Export/Export.tsx +++ b/src/components/Table/TableHeader/Export/Export.tsx @@ -126,7 +126,8 @@ export default function Export({ query, closeModal }) { ...doc.data(), })); - const fileName = `${tableState?.tablePath!}-${new Date().toISOString()}.${exportType}`; + const fileName = `${tableState?.config + .id!}-${new Date().toISOString()}.${exportType}`; switch (exportType) { case "csv": const csvData = docs.map((doc: any) => diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 8a69e9ec..01ac6d8b 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -38,7 +38,7 @@ export type TableColumn = Column & { }; const rowKeyGetter = (row: any) => row.id; -const SelectColumn = { ..._SelectColumn, width: 42, maxWidth: 42 }; +// const SelectColumn = { ..._SelectColumn, width: 42, maxWidth: 42 }; export default function Table() { const classes = useStyles(); @@ -48,7 +48,7 @@ export default function Table() { const { userDoc } = useAppContext(); const userDocHiddenFields = - userDoc.state.doc?.tables?.[formatSubTableName(tableState?.tablePath)] + userDoc.state.doc?.tables?.[formatSubTableName(tableState?.config.id)] ?.hiddenFields ?? []; const [columns, setColumns] = useState([]); From 7840b416ad64a9f1c567308f73265ed1dbea1902 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 1 Nov 2021 16:09:07 +1100 Subject: [PATCH 10/15] fix Switch with label spacing --- src/components/fields/Action/Settings.tsx | 12 ------------ src/components/fields/Slider/Settings.tsx | 4 ---- src/theme/components.tsx | 8 +++++++- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/components/fields/Action/Settings.tsx b/src/components/fields/Action/Settings.tsx index a81295cd..e8de7b34 100644 --- a/src/components/fields/Action/Settings.tsx +++ b/src/components/fields/Action/Settings.tsx @@ -74,10 +74,6 @@ const Settings = ({ config, onChange }) => { /> } label="Set as an action script" - sx={{ - alignItems: "center", - "& .MuiFormControlLabel-label": { mt: 0 }, - }} /> {!Boolean(config.isActionScript) ? ( { /> } label="User can redo (re-runs the same script)" - sx={{ - alignItems: "center", - "& .MuiFormControlLabel-label": { mt: 0 }, - }} /> { /> } label="User can undo" - sx={{ - alignItems: "center", - "& .MuiFormControlLabel-label": { mt: 0 }, - }} /> {config["undo.enabled"] && ( <> diff --git a/src/components/fields/Slider/Settings.tsx b/src/components/fields/Slider/Settings.tsx index e333788b..8e6e3bbb 100644 --- a/src/components/fields/Slider/Settings.tsx +++ b/src/components/fields/Slider/Settings.tsx @@ -48,10 +48,6 @@ export default function Settings({ onChange, config }) { /> } label="Show slider steps" - sx={{ - alignItems: "center", - "& .MuiFormControlLabel-label": { mt: 0 }, - }} /> ); diff --git a/src/theme/components.tsx b/src/theme/components.tsx index 52676c5a..ab09d407 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -792,7 +792,13 @@ export const components = (theme: Theme): ThemeOptions => { root: { display: "flex", alignItems: "flex-start", - "& .MuiSwitch-root": { marginRight: theme.spacing(1) }, + "& .MuiSwitch-root": { + marginRight: theme.spacing(1), + + "&.MuiSwitch-sizeSmall + .MuiFormControlLabel-label": { + marginTop: 4, + }, + }, }, label: { marginTop: 10, From 32572b6b541f5f2c49a95fcfccada24daa5630b8 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 1 Nov 2021 16:19:27 +1100 Subject: [PATCH 11/15] SideDrawer: add option to show hidden fields --- src/components/SideDrawer/Form/index.tsx | 41 ++++++++++++++++++++---- src/components/Table/HiddenFields.tsx | 1 + 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/components/SideDrawer/Form/index.tsx b/src/components/SideDrawer/Form/index.tsx index 8565efec..a64facfc 100644 --- a/src/components/SideDrawer/Form/index.tsx +++ b/src/components/SideDrawer/Form/index.tsx @@ -2,8 +2,9 @@ import { createElement, useEffect } from "react"; import { useForm } from "react-hook-form"; import _sortBy from "lodash/sortBy"; import _isEmpty from "lodash/isEmpty"; +import createPersistedState from "use-persisted-state"; -import { Stack } from "@mui/material"; +import { Stack, FormControlLabel, Switch } from "@mui/material"; import { Values } from "./utils"; import { getFieldProp } from "@src/components/fields"; @@ -16,6 +17,10 @@ import { useAppContext } from "@src/contexts/AppContext"; import { useProjectContext } from "@src/contexts/ProjectContext"; import { sanitizeFirestoreRefs } from "@src/utils/fns"; +const useSideDrawerShowHiddenFieldsState = createPersistedState( + "__ROWY__SIDE_DRAWER_SHOW_HIDDEN_FIELDS" +); + export interface IFormProps { values: Values; } @@ -23,12 +28,18 @@ export interface IFormProps { export default function Form({ values }: IFormProps) { const { tableState, sideDrawerRef } = useProjectContext(); const { userDoc } = useAppContext(); - const userDocHiddenFields = - userDoc.state.doc?.tables?.[`${tableState!.tablePath}`]?.hiddenFields ?? []; - const fields = _sortBy(Object.values(tableState!.columns), "index").filter( - (f) => !userDocHiddenFields.includes(f.name) - ); + const userDocHiddenFields = + userDoc.state.doc?.tables?.[`${tableState!.config.id}`]?.hiddenFields ?? []; + + const [showHiddenFields, setShowHiddenFields] = + useSideDrawerShowHiddenFieldsState(false); + + const fields = showHiddenFields + ? _sortBy(Object.values(tableState!.columns), "index") + : _sortBy(Object.values(tableState!.columns), "index").filter( + (f) => !userDocHiddenFields.includes(f.key) + ); // Get initial values from fields config. This won’t be written to the db // when the SideDrawer is opened. Only dirty fields will be written @@ -121,6 +132,24 @@ export default function Form({ values }: IFormProps) { label="Document path" debugText={values.ref?.path ?? values.id ?? "No ref"} /> + + {userDocHiddenFields.length > 0 && ( + setShowHiddenFields(e.target.checked)} + /> + } + sx={{ + borderTop: 1, + borderColor: "divider", + pt: 3, + "& .MuiSwitch-root": { ml: -0.5 }, + }} + /> + )}
); diff --git a/src/components/Table/HiddenFields.tsx b/src/components/Table/HiddenFields.tsx index 8a02db85..7676591f 100644 --- a/src/components/Table/HiddenFields.tsx +++ b/src/components/Table/HiddenFields.tsx @@ -13,6 +13,7 @@ import { useProjectContext } from "@src/contexts/ProjectContext"; import { useAppContext } from "@src/contexts/AppContext"; import { DocActions } from "@src/hooks/useDoc"; import { formatSubTableName } from "../../utils/fns"; + const useStyles = makeStyles((theme) => createStyles({ listbox: {}, From de4d69859158abe026f25168ee0e9408dfd32c75 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 1 Nov 2021 16:20:33 +1100 Subject: [PATCH 12/15] Test page: fix small switch spacing --- src/pages/Test.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/pages/Test.tsx b/src/pages/Test.tsx index 021608df..08830b58 100644 --- a/src/pages/Test.tsx +++ b/src/pages/Test.tsx @@ -768,14 +768,7 @@ export default function TestView() {
- } - label="Label" - sx={{ - alignItems: "center", - "& .MuiFormControlLabel-label": { mt: 0 }, - }} - /> + } label="Label" /> } label="Label" From b0d403478f955ad1e7e53784c4cffcb1140e3e9c Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 1 Nov 2021 16:51:31 +1100 Subject: [PATCH 13/15] setup: remove insecure rule insecure rule that allows anyone to access any part of your database --- src/components/Setup/Step4Rules.tsx | 35 ++++++++++++++++++++++++--- src/components/TableSettings/form.tsx | 1 + src/config/firestoreRules.ts | 6 +++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/components/Setup/Step4Rules.tsx b/src/components/Setup/Step4Rules.tsx index 73626860..b2ae87f2 100644 --- a/src/components/Setup/Step4Rules.tsx +++ b/src/components/Setup/Step4Rules.tsx @@ -10,6 +10,7 @@ import { Grid, } from "@mui/material"; import LoadingButton from "@mui/lab/LoadingButton"; +import InfoIcon from "@mui/icons-material/InfoOutlined"; import CopyIcon from "@src/assets/icons/Copy"; import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; @@ -19,7 +20,12 @@ import DiffEditor from "@src/components/CodeEditor/DiffEditor"; import { name } from "@root/package.json"; import { useAppContext } from "@src/contexts/AppContext"; import { CONFIG } from "@src/config/dbPaths"; -import { requiredRules, adminRules, utilFns } from "@src/config/firestoreRules"; +import { + requiredRules, + adminRules, + utilFns, + insecureRule, +} from "@src/config/firestoreRules"; import { rowyRun } from "@src/utils/rowyRun"; import { runRoutes } from "@src/constants/runRoutes"; // import { useConfirmation } from "@src/components/ConfirmationDialog"; @@ -53,6 +59,17 @@ export default function Step4Rules({ .then((data) => setCurrentRules(data?.source?.[0]?.content ?? "")); }, [rowyRunUrl, hasRules, currentRules, getAuthToken]); + const insecureRuleRegExp = new RegExp( + insecureRule + .replace(/\//g, "\\/") + .replace(/\*/g, "\\*") + .replace(/\s{2,}/g, "\\s+") + .replace(/\s/g, "\\s*") + .replace(/\n/g, "\\s+") + .replace(/;/g, ";?") + ); + const hasInsecureRule = insecureRuleRegExp.test(currentRules); + const [newRules, setNewRules] = useState(""); useEffect(() => { let rulesToInsert = rules; @@ -64,13 +81,15 @@ export default function Step4Rules({ rulesToInsert = rulesToInsert.replace(/function hasAnyRole[^}]*}/s, ""); } - const inserted = currentRules.replace( + let inserted = currentRules.replace( /match\s*\/databases\/\{database\}\/documents\s*\{/, `match /databases/{database}/documents {\n` + rulesToInsert ); + if (hasInsecureRule) inserted = inserted.replace(insecureRuleRegExp, ""); + setNewRules(inserted); - }, [currentRules, rules]); + }, [currentRules, rules, hasInsecureRule, insecureRuleRegExp]); const [rulesStatus, setRulesStatus] = useState<"LOADING" | string>(""); const setRules = async () => { @@ -141,6 +160,16 @@ export default function Step4Rules({ label="Allow admins to read and write all documents" sx={{ "&&": { ml: -11 / 8, mb: -11 / 8 }, width: "100%" }} /> + + + + We removed an insecure rule that allows anyone to access any part + of your database + + You change which Firestore collection to display. Data in the new diff --git a/src/config/firestoreRules.ts b/src/config/firestoreRules.ts index 0f674357..e23047bd 100644 --- a/src/config/firestoreRules.ts +++ b/src/config/firestoreRules.ts @@ -37,3 +37,9 @@ export const utilFns = ` return request.auth != null && request.auth.token.roles.hasAny(roles); } ` as const; + +export const insecureRule = ` + match /{document=**} { + allow read, write: if true; + } +` as const; From b4c154e6c0f1d9bf178b8eea573d8bad30159665 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 1 Nov 2021 17:00:13 +1100 Subject: [PATCH 14/15] remove duplicate ConnectServiceSelect code --- .../ConnectServiceSelect/PopupContents.tsx | 204 ------------------ src/components/ConnectServiceSelect/index.tsx | 73 ------- src/components/ConnectServiceSelect/styles.ts | 83 ------- 3 files changed, 360 deletions(-) delete mode 100644 src/components/ConnectServiceSelect/PopupContents.tsx delete mode 100644 src/components/ConnectServiceSelect/index.tsx delete mode 100644 src/components/ConnectServiceSelect/styles.ts diff --git a/src/components/ConnectServiceSelect/PopupContents.tsx b/src/components/ConnectServiceSelect/PopupContents.tsx deleted file mode 100644 index 3381ba3c..00000000 --- a/src/components/ConnectServiceSelect/PopupContents.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import React, { useEffect, useState } from "react"; -import clsx from "clsx"; -import { useDebouncedCallback } from "use-debounce"; -import _get from "lodash/get"; - -import { - Button, - Checkbox, - Divider, - Grid, - InputAdornment, - List, - ListItemIcon, - ListItemText, - MenuItem, - TextField, - Typography, - Radio, -} from "@mui/material"; -import SearchIcon from "@mui/icons-material/Search"; - -import { IConnectServiceSelectProps } from "."; -import useStyles from "./styles"; -import Loading from "@src/components/Loading"; - -export interface IPopupContentsProps - extends Omit {} - -// TODO: Implement infinite scroll here -export default function PopupContents({ - value = [], - onChange, - config, - - docRef, -}: IPopupContentsProps) { - const url = config.url; - const titleKey = config.titleKey ?? config.primaryKey; - const subtitleKey = config.subtitleKey; - const resultsKey = config.resultsKey; - const primaryKey = config.primaryKey; - const multiple = Boolean(config.multiple); - - const classes = useStyles(); - - // Webservice search query - const [query, setQuery] = useState(""); - // Webservice response - const [response, setResponse] = useState(null); - - const [docData, setDocData] = useState(null); - useEffect(() => { - docRef.get().then((d) => setDocData(d.data())); - }, []); - - const hits: any["hits"] = _get(response, resultsKey) ?? []; - const [search] = useDebouncedCallback( - async (query: string) => { - if (!docData) return; - if (!url) return; - const uri = new URL(url), - params = { q: query }; - Object.keys(params).forEach((key) => - uri.searchParams.append(key, params[key]) - ); - - const resp = await fetch(uri.toString(), { - method: "POST", - body: JSON.stringify(docData), - headers: { "content-type": "application/json" }, - }); - - const jsonBody = await resp.json(); - setResponse(jsonBody); - }, - 1000, - { leading: true } - ); - - useEffect(() => { - search(query); - }, [query, docData]); - - if (!response) return ; - - const select = (hit: any) => () => { - if (multiple) onChange([...value, hit]); - else onChange([hit]); - }; - const deselect = (hit: any) => () => { - if (multiple) - onChange(value.filter((v) => v[primaryKey] !== hit[primaryKey])); - else onChange([]); - }; - - const selectedValues = value?.map((item) => _get(item, primaryKey)); - - const clearSelection = () => onChange([]); - - return ( - - - setQuery(e.target.value)} - fullWidth - variant="filled" - margin="dense" - label="Search items" - className={classes.noMargins} - InputProps={{ - endAdornment: ( - - - - ), - }} - onClick={(e) => e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - /> - - - - - {hits.map((hit) => { - const isSelected = - selectedValues.indexOf(_get(hit, primaryKey)) !== -1; - console.log(`Selected Values: ${selectedValues}`); - return ( - - - - {multiple ? ( - - ) : ( - - )} - - - - - - ); - })} - - - - {multiple && ( - - - - {value?.length} of {hits?.length} - - - - - - )} - - ); -} diff --git a/src/components/ConnectServiceSelect/index.tsx b/src/components/ConnectServiceSelect/index.tsx deleted file mode 100644 index 1805ae2f..00000000 --- a/src/components/ConnectServiceSelect/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { lazy, Suspense } from "react"; -import clsx from "clsx"; - -import { TextField, TextFieldProps } from "@mui/material"; -import useStyles from "./styles"; -import Loading from "@src/components/Loading"; -import ErrorBoundary from "@src/components/ErrorBoundary"; - -const PopupContents = lazy( - () => import("./PopupContents" /* webpackChunkName: "PopupContents" */) -); - -export type ServiceValue = { value: string; [prop: string]: any }; - -export interface IConnectServiceSelectProps { - value: ServiceValue[]; - onChange: (value: ServiceValue[]) => void; - row: any; - config: { - displayKey: string; - [key: string]: any; - }; - editable?: boolean; - /** Optional style overrides for root MUI `TextField` component */ - className?: string; - /** Override any props of the root MUI `TextField` component */ - TextFieldProps?: Partial; - docRef: firebase.default.firestore.DocumentReference; -} - -export default function ConnectServiceSelect({ - value = [], - className, - TextFieldProps = {}, - ...props -}: IConnectServiceSelectProps) { - const classes = useStyles(); - - const sanitisedValue = Array.isArray(value) ? value : []; - - return ( - `${(value as any[]).length} selected`, - displayEmpty: true, - classes: { root: classes.selectRoot }, - ...TextFieldProps.SelectProps, - // Must have this set to prevent MUI transforming `value` - // prop for this component to a comma-separated string - MenuProps: { - classes: { paper: classes.paper, list: classes.menuChild }, - MenuListProps: { disablePadding: true }, - anchorOrigin: { vertical: "bottom", horizontal: "center" }, - transformOrigin: { vertical: "top", horizontal: "center" }, - ...TextFieldProps.SelectProps?.MenuProps, - }, - }} - > - - }> - - - - - ); -} diff --git a/src/components/ConnectServiceSelect/styles.ts b/src/components/ConnectServiceSelect/styles.ts deleted file mode 100644 index 72b9b4cd..00000000 --- a/src/components/ConnectServiceSelect/styles.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { makeStyles, createStyles } from "@mui/styles"; - -export const useStyles = makeStyles((theme) => - createStyles({ - root: { minWidth: 200 }, - selectRoot: { paddingRight: theme.spacing(4) }, - - paper: { overflow: "hidden", maxHeight: "calc(100% - 48px)" }, - menuChild: { - padding: `0 ${theme.spacing(2)}`, - minWidth: 340, - // Need to set fixed height here so popup is positioned correctly - height: 340, - }, - - grid: { outline: 0 }, - - noMargins: { margin: 0 }, - - searchRow: { marginTop: theme.spacing(2) }, - - listRow: { - background: `${theme.palette.background.paper} no-repeat`, - position: "relative", - margin: theme.spacing(0, -2), - maxWidth: `calc(100% + ${theme.spacing(4)})`, - - "&::before, &::after": { - content: '""', - position: "absolute", - top: 0, - left: 0, - right: 0, - zIndex: 9, - - display: "block", - height: 16, - - background: `linear-gradient(to bottom, #fff, rgba(255, 255, 255, 0))`, - }, - - "&::after": { - top: "auto", - bottom: 0, - background: `linear-gradient(to top, #fff, rgba(255, 255, 255, 0))`, - }, - }, - list: () => { - let maxHeightDeductions = 0; - maxHeightDeductions -= 64; // search box - maxHeightDeductions -= 48; // multiple - maxHeightDeductions += 8; // footer padding - - return { - padding: theme.spacing(2, 0), - overflowY: "auto" as "auto", - // height: `calc(340px - ${-maxHeightDeductions}px)`, - height: 340 + maxHeightDeductions, - }; - }, - - checkboxContainer: { minWidth: theme.spacing(36 / 8) }, - checkbox: { - padding: theme.spacing(6 / 8, 9 / 8), - "&:hover": { background: "transparent" }, - }, - - divider: { margin: theme.spacing(0, 2, 0, 6.5) }, - - footerRow: { marginBottom: theme.spacing(2) }, - selectedRow: { - "$listRow + &": { marginTop: -theme.spacing(1) }, - "$footerRow + &": { marginTop: -theme.spacing(2) }, - - marginBottom: 0, - "& > div": { height: 48 }, - }, - selectAllButton: { marginRight: -theme.spacing(1) }, - selectedNum: { fontFeatureSettings: '"tnum"' }, - }) -); - -export default useStyles; From 7cc74dba9b4ae2e5158286409a0760a708420ff9 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 1 Nov 2021 17:00:31 +1100 Subject: [PATCH 15/15] ConnectServiceSelect: fix list edge styles on dark theme --- .../fields/ConnectService/ConnectServiceSelect/styles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/fields/ConnectService/ConnectServiceSelect/styles.ts b/src/components/fields/ConnectService/ConnectServiceSelect/styles.ts index 72b9b4cd..8398b468 100644 --- a/src/components/fields/ConnectService/ConnectServiceSelect/styles.ts +++ b/src/components/fields/ConnectService/ConnectServiceSelect/styles.ts @@ -36,13 +36,13 @@ export const useStyles = makeStyles((theme) => display: "block", height: 16, - background: `linear-gradient(to bottom, #fff, rgba(255, 255, 255, 0))`, + background: `linear-gradient(to bottom, ${theme.palette.background.paper}, rgba(255, 255, 255, 0))`, }, "&::after": { top: "auto", bottom: 0, - background: `linear-gradient(to top, #fff, rgba(255, 255, 255, 0))`, + background: `linear-gradient(to top, ${theme.palette.background.paper}, rgba(255, 255, 255, 0))`, }, }, list: () => {