From 3941ae548ecdc79330db3c6c500645e9d2d704f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Jan 2023 18:17:26 +0000 Subject: [PATCH 001/183] Bump ua-parser-js from 0.7.32 to 0.7.33 Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.32 to 0.7.33. - [Release notes](https://github.com/faisalman/ua-parser-js/releases) - [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md) - [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.32...0.7.33) --- updated-dependencies: - dependency-name: ua-parser-js dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d669d494..5038e845 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12264,9 +12264,9 @@ typescript@^4.9.3: integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== ua-parser-js@^0.7.30: - version "0.7.32" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.32.tgz#cd8c639cdca949e30fa68c44b7813ef13e36d211" - integrity sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw== + version "0.7.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" + integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== unbox-primitive@^1.0.2: version "1.0.2" From 645d36630d970040f9dbfed318da92ec6015a8af Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sun, 29 Jan 2023 00:14:51 +0700 Subject: [PATCH 002/183] implement auto typing for await import --- package.json | 2 +- src/components/CodeEditor/CodeEditor.tsx | 19 ++++++++-- .../CodeEditor/useMonacoCustomizations.ts | 37 ++++++++----------- yarn.lock | 2 +- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 4fac6672..e31ea62e 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "match-sorter": "^6.3.1", "material-ui-popup-state": "^4.0.1", "mdi-material-ui": "^7.3.0", - "monaco-editor-auto-typings": "^0.4.0", + "monaco-editor-auto-typings": "^0.4.3", "notistack": "^2.0.4", "path-browserify": "^1.0.1", "pb-util": "^1.0.3", diff --git a/src/components/CodeEditor/CodeEditor.tsx b/src/components/CodeEditor/CodeEditor.tsx index 2abfd45b..638759ed 100644 --- a/src/components/CodeEditor/CodeEditor.tsx +++ b/src/components/CodeEditor/CodeEditor.tsx @@ -14,6 +14,7 @@ import FullScreenButton from "@src/components/FullScreenButton"; import { spreadSx } from "@src/utils/ui"; import githubLightTheme from "@src/components/CodeEditor/github-light-default.json"; import githubDarkTheme from "@src/components/CodeEditor/github-dark-default.json"; +import { AutoTypings, LocalStorageCache } from "monaco-editor-auto-typings"; export interface ICodeEditorProps extends Partial, @@ -47,7 +48,7 @@ export default function CodeEditor({ extraLibs, diagnosticsOptions, onUnmount, - defaultLanguage = "javascript", + defaultLanguage = "typescript", ...props }: ICodeEditorProps) { const theme = useTheme(); @@ -95,11 +96,22 @@ export default function CodeEditor({ monaco.editor.defineTheme("github-light", githubLightTheme as any); monaco.editor.defineTheme("github-dark", githubDarkTheme as any); }} - onMount={(editor) => { + {...props} + onMount={async (editor, monaco) => { + if (props.onMount) { + props.onMount(editor, monaco); + } if (onFocus) editor.onDidFocusEditorWidget(onFocus); if (onBlur) editor.onDidBlurEditorWidget(onBlur); + const autoTypings = await AutoTypings.create(editor, { + monaco: monaco, + sourceCache: new LocalStorageCache(), + debounceDuration: 500, // ms + onError: (e) => { + console.log("Auto typing error", e); + }, + }); }} - {...props} onValidate={onValidate_} theme={`github-${theme.palette.mode}`} options={{ @@ -113,6 +125,7 @@ export default function CodeEditor({ fixedOverflowWidgets: true, tabSize: 2, ...props.options, + language: "typescript", }} /> diff --git a/src/components/CodeEditor/useMonacoCustomizations.ts b/src/components/CodeEditor/useMonacoCustomizations.ts index e6d55533..c73fba31 100644 --- a/src/components/CodeEditor/useMonacoCustomizations.ts +++ b/src/components/CodeEditor/useMonacoCustomizations.ts @@ -13,15 +13,9 @@ import { } from "@src/atoms/tableScope"; import { useMonaco } from "@monaco-editor/react"; import type { languages } from "monaco-editor/esm/vs/editor/editor.api"; -import githubLightTheme from "./github-light-default.json"; -import githubDarkTheme from "./github-dark-default.json"; - import { useTheme } from "@mui/material"; import type { SystemStyleObject, Theme } from "@mui/system"; -// TODO: -// import { getFieldType, getFieldProp } from "@src/components/fields"; - /* eslint-disable import/no-webpack-loader-syntax */ import firestoreDefs from "!!raw-loader!./firestore.d.ts"; import firebaseAuthDefs from "!!raw-loader!./firebaseAuth.d.ts"; @@ -77,24 +71,26 @@ export default function useMonacoCustomizations({ if (!monaco) return; try { - monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + module: monaco.languages.typescript.ModuleKind.CommonJS, target: monaco.languages.typescript.ScriptTarget.ES2020, allowNonTsExtensions: true, + typeRoots: ["node_modules/@types"], }); - monaco.languages.typescript.javascriptDefaults.addExtraLib(firestoreDefs); - monaco.languages.typescript.javascriptDefaults.addExtraLib( + monaco.languages.typescript.typescriptDefaults.addExtraLib(firestoreDefs); + monaco.languages.typescript.typescriptDefaults.addExtraLib( firebaseAuthDefs ); - monaco.languages.typescript.javascriptDefaults.addExtraLib( + monaco.languages.typescript.typescriptDefaults.addExtraLib( firebaseStorageDefs ); - monaco.languages.typescript.javascriptDefaults.addExtraLib( + monaco.languages.typescript.typescriptDefaults.addExtraLib( utilsDefs, "ts:filename/utils.d.ts" ); - monaco.languages.typescript.javascriptDefaults.addExtraLib(rowyUtilsDefs); + monaco.languages.typescript.typescriptDefaults.addExtraLib(rowyUtilsDefs); } catch (error) { console.error( "An error occurred during initialization of Monaco: ", @@ -108,7 +104,7 @@ export default function useMonacoCustomizations({ if (!monaco) return; if (!extraLibs) return; try { - monaco.languages.typescript.javascriptDefaults.addExtraLib( + monaco.languages.typescript.typescriptDefaults.addExtraLib( extraLibs.join("\n"), "ts:filename/extraLibs.d.ts" ); @@ -123,7 +119,7 @@ export default function useMonacoCustomizations({ if (!monaco) return; try { - monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ + monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ ...JSON.parse(stringifiedDiagnosticsOptions), diagnosticCodesToIgnore: [ 1323, // remove dynamic import error @@ -143,11 +139,11 @@ export default function useMonacoCustomizations({ .map((row) => row[columnKey]) .filter((entry) => entry !== undefined) .map((entry) => JSON.stringify(entry)); - monaco?.languages.typescript.javascriptDefaults.addExtraLib( + monaco?.languages.typescript.typescriptDefaults.addExtraLib( `type ${interfaceName} = any;` ); // if (!samples || samples.length === 0) { - // monaco?.languages.typescript.javascriptDefaults.addExtraLib( + // monaco?.languages.typescript.typescriptDefaults.addExtraLib( // `type ${interfaceName} = any;` // ); // return; @@ -165,7 +161,7 @@ export default function useMonacoCustomizations({ // rendererOptions: { "just-types": "true" }, // }); // const newLib = result.lines.join("\n").replaceAll("export ", ""); - // monaco?.languages.typescript.javascriptDefaults.addExtraLib(newLib); + // monaco?.languages.typescript.typescriptDefaults.addExtraLib(newLib); //} }; @@ -184,12 +180,11 @@ export default function useMonacoCustomizations({ .join("\n")} } `; - monaco?.languages.typescript.javascriptDefaults.addExtraLib(secretsDef); + monaco?.languages.typescript.typescriptDefaults.addExtraLib(secretsDef); } catch (error) { console.error("Could not set secret definitions: ", error); } }; - //TODO: types const setBaseDefinitions = () => { const rowDefinition = tableColumnsOrdered @@ -208,13 +203,13 @@ export default function useMonacoCustomizations({ .map((key) => `"${key}"`) .join("|\n"); - monaco?.languages.typescript.javascriptDefaults.addExtraLib( + monaco?.languages.typescript.typescriptDefaults.addExtraLib( ["/**", " * extensions type configuration", " */", extensionsDefs].join( "\n" ), "ts:filename/extensions.d.ts" ); - monaco?.languages.typescript.javascriptDefaults.addExtraLib( + monaco?.languages.typescript.typescriptDefaults.addExtraLib( [ "// basic types that are used in all places", "declare var require: any;", diff --git a/yarn.lock b/yarn.lock index 8ff8ab68..2fd3cb4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8961,7 +8961,7 @@ mkdirp@^0.5.1, mkdirp@~0.5.1: dependencies: minimist "^1.2.6" -monaco-editor-auto-typings@^0.4.0: +monaco-editor-auto-typings@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/monaco-editor-auto-typings/-/monaco-editor-auto-typings-0.4.3.tgz#24498064ff876c641467815e65d54ae75f31cd7f" integrity sha512-7lpiWHkg8eX2DRJGApaFq1wGn95Ute3Xv4PLz36qFyTytzz86irjxwMl00JnOOGX/R0eUbfuudbEtkQh2VK9Gg== From 189c86a16ee6c6b0ebb7b20d16143ce592169655 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Mon, 27 Feb 2023 18:10:12 +0100 Subject: [PATCH 003/183] Created Color Select and Implemented it on MultiSelect --- src/components/ColorSelect.tsx | 150 ++++++++++++++++++ .../fields/SingleSelect/Settings.tsx | 8 +- 2 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 src/components/ColorSelect.tsx diff --git a/src/components/ColorSelect.tsx b/src/components/ColorSelect.tsx new file mode 100644 index 00000000..cee3a29b --- /dev/null +++ b/src/components/ColorSelect.tsx @@ -0,0 +1,150 @@ +import { useState } from "react"; +import Button from "@mui/material/Button"; +import Box from "@mui/material/Box"; +import Menu from "@mui/material/Menu"; +import Grid from "@mui/material/Grid"; +import { Chip, Divider, Typography } from "@mui/material"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import FormatColorResetIcon from "@mui/icons-material/FormatColorReset"; + +const MUIColorsArray = [ + "primary", + "secondary", + "info", + "success", + "error", + "warning", +]; + +const ColorSelect = () => { + /* Hold the current state of a given option defaults to `default` */ + const [color, setColor] = useState("primary"); + + /* MUI Specific state */ + const [colorSelectAnchor, setColorSelectAnchor] = + useState(null); + const open = Boolean(colorSelectAnchor); + + /* MUI Menu event handlers */ + const handleClick = (event: React.MouseEvent) => { + setColorSelectAnchor(event.currentTarget); + }; + const handleClose = () => { + setColorSelectAnchor(null); + }; + + return ( +
+ + + {/* Menu */} + + + COLOURS + + + + {MUIColorsArray.map((color_string: string, index: number) => ( + + + + + + + + + + Preview + + + + + + + +
+ ); +}; + +export default ColorSelect; diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index 2fc908b9..26362f5d 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -14,6 +14,7 @@ import { } from "@mui/material"; import AddIcon from "@mui/icons-material/AddCircle"; import RemoveIcon from "@mui/icons-material/CancelRounded"; +import ColorSelect from "@src/components/ColorSelect"; export default function Settings({ onChange, config }: ISettingsProps) { const listEndRef: any = useRef(null); @@ -52,9 +53,12 @@ export default function Settings({ onChange, config }: ISettingsProps) { alignItems="center" > - {option} + + + {option} + - + From d81b203cfd0cd977ac97e9dc4f813471896a3534 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Thu, 2 Mar 2023 22:53:49 +0100 Subject: [PATCH 004/183] Added predefined colors from palette & added the customizable color modal --- src/components/ColorSelect.tsx | 171 ++++++++++++++++++++++++++++----- 1 file changed, 145 insertions(+), 26 deletions(-) diff --git a/src/components/ColorSelect.tsx b/src/components/ColorSelect.tsx index cee3a29b..104f6a90 100644 --- a/src/components/ColorSelect.tsx +++ b/src/components/ColorSelect.tsx @@ -1,24 +1,46 @@ -import { useState } from "react"; +import { FC, useEffect, useState } from "react"; import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; import Menu from "@mui/material/Menu"; import Grid from "@mui/material/Grid"; -import { Chip, Divider, Typography } from "@mui/material"; +import { Chip, Divider, Typography, useTheme } from "@mui/material"; +import Modal from "./Modal"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import FormatColorResetIcon from "@mui/icons-material/FormatColorReset"; +import { paletteToMui, palette } from "@src/theme/palette"; +import ColorPickerInput from "./ColorPickerInput"; +import { toColor } from "react-color-palette"; -const MUIColorsArray = [ - "primary", - "secondary", - "info", - "success", - "error", - "warning", -]; +interface SelectColorThemeOptions { + light: string; + dark: string; +} const ColorSelect = () => { + /* Get current */ + const theme = useTheme(); + const mode = theme.palette.mode; + + /* Palette - reset paletter to object */ + const palettes = Object({ + gray: palette.aGray, + blue: palette.blue, + red: palette.aRed, + green: palette.green, + yellow: palette.yellow, + pink: palette.pink, + teal: palette.teal, + tangerine: palette.tangerine, + orange: palette.orange, + cyan: palette.cyan, + amber: palette.amber, + lightGreen: palette.lightGreen, + }); + /* Hold the current state of a given option defaults to `default` */ - const [color, setColor] = useState("primary"); + const [color, setColor] = useState( + paletteToMui(palette["gray"]) + ); /* MUI Specific state */ const [colorSelectAnchor, setColorSelectAnchor] = @@ -53,7 +75,7 @@ const ColorSelect = () => { width: 20, height: 20, borderRadius: 100, - backgroundColor: `${color}.main`, + backgroundColor: color[mode], }} /> @@ -87,39 +109,41 @@ const ColorSelect = () => { COLOURS - - {MUIColorsArray.map((color_string: string, index: number) => ( + + {Object.keys(palettes).map((key: string, index: number) => ( @@ -138,7 +162,7 @@ const ColorSelect = () => { component="small" size="small" label="Option 1" - color={color as any} + sx={{ backgroundColor: color[mode] }} /> @@ -147,4 +171,99 @@ const ColorSelect = () => { ); }; +interface CustomizeColor { + currentColor: SelectColorThemeOptions; + onChange: (value: SelectColorThemeOptions) => void; +} + +const CustomSelectColor: FC = ({ currentColor, onChange }) => { + const [color, setColor] = useState(currentColor); + + /* Update color value onFocus */ + useEffect(() => { + setColor(currentColor); + }, [currentColor]); + + /* Pass value to the onChange function */ + const handleChange = (color: SelectColorThemeOptions) => { + setColor(color); + onChange(color); + }; + + /* MUI Specific state */ + const [open, setOpen] = useState(false); + + /* MUI Menu event handlers */ + const handleClick = () => setOpen(true); + const handleClose = () => setOpen(false); + + return ( +
+ + + + {/* Light Theme Customize Color */} + + + handleChange({ ...color, ...{ light: value.hex } }) + } + /> + + + + Light Theme + + + + + + + + + {/* Dark Theme Customize Color */} + + + handleChange({ ...color, ...{ dark: value.hex } }) + } + /> + + + + Dark Theme + + + + + + + + + +
+ ); +}; + export default ColorSelect; From 9bcab13f53cd8b95b97fdc5d0fb9ef453782d4a8 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Thu, 2 Mar 2023 23:25:20 +0100 Subject: [PATCH 005/183] Seperated components and moved files into a folder --- .../SelectColors/CustomizeColorModal.tsx | 113 ++++++++++++++++++ .../index.tsx} | 103 +--------------- .../fields/SingleSelect/Settings.tsx | 2 +- 3 files changed, 117 insertions(+), 101 deletions(-) create mode 100644 src/components/SelectColors/CustomizeColorModal.tsx rename src/components/{ColorSelect.tsx => SelectColors/index.tsx} (59%) diff --git a/src/components/SelectColors/CustomizeColorModal.tsx b/src/components/SelectColors/CustomizeColorModal.tsx new file mode 100644 index 00000000..a9d7e6a8 --- /dev/null +++ b/src/components/SelectColors/CustomizeColorModal.tsx @@ -0,0 +1,113 @@ +import { FC, useEffect, useState } from "react"; +import Button from "@mui/material/Button"; +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid"; +import { Chip, Typography } from "@mui/material"; +import Modal from "@src/components/Modal"; +import ColorPickerInput from "@src/components/ColorPickerInput"; +import { toColor } from "react-color-palette"; + +interface SelectColorThemeOptions { + light: string; + dark: string; +} + +interface CustomizeColor { + currentColor: SelectColorThemeOptions; + onChange: (value: SelectColorThemeOptions) => void; +} + +const CustomizeColorModal: FC = ({ + currentColor, + onChange, +}) => { + const [color, setColor] = useState(currentColor); + + /* Update color value onFocus */ + useEffect(() => { + setColor(currentColor); + }, [currentColor]); + + /* Pass value to the onChange function */ + const handleChange = (color: SelectColorThemeOptions) => { + setColor(color); + onChange(color); + }; + + /* MUI Specific state */ + const [open, setOpen] = useState(false); + + /* MUI Menu event handlers */ + const handleClick = () => setOpen(true); + const handleClose = () => setOpen(false); + + return ( +
+ + + + {/* Light Theme Customize Color */} + + + handleChange({ ...color, ...{ light: value.hex } }) + } + /> + + + + Light Theme + + + + + + + + + {/* Dark Theme Customize Color */} + + + handleChange({ ...color, ...{ dark: value.hex } }) + } + /> + + + + Dark Theme + + + + + + + + + +
+ ); +}; + +export default CustomizeColorModal; diff --git a/src/components/ColorSelect.tsx b/src/components/SelectColors/index.tsx similarity index 59% rename from src/components/ColorSelect.tsx rename to src/components/SelectColors/index.tsx index 104f6a90..83b59192 100644 --- a/src/components/ColorSelect.tsx +++ b/src/components/SelectColors/index.tsx @@ -1,15 +1,13 @@ -import { FC, useEffect, useState } from "react"; +import { useState } from "react"; import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; import Menu from "@mui/material/Menu"; import Grid from "@mui/material/Grid"; import { Chip, Divider, Typography, useTheme } from "@mui/material"; -import Modal from "./Modal"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import FormatColorResetIcon from "@mui/icons-material/FormatColorReset"; import { paletteToMui, palette } from "@src/theme/palette"; -import ColorPickerInput from "./ColorPickerInput"; -import { toColor } from "react-color-palette"; +import CustomizeColorModal from "./CustomizeColorModal"; interface SelectColorThemeOptions { light: string; @@ -131,7 +129,7 @@ const ColorSelect = () => {
- setColor(color)} /> @@ -171,99 +169,4 @@ const ColorSelect = () => { ); }; -interface CustomizeColor { - currentColor: SelectColorThemeOptions; - onChange: (value: SelectColorThemeOptions) => void; -} - -const CustomSelectColor: FC = ({ currentColor, onChange }) => { - const [color, setColor] = useState(currentColor); - - /* Update color value onFocus */ - useEffect(() => { - setColor(currentColor); - }, [currentColor]); - - /* Pass value to the onChange function */ - const handleChange = (color: SelectColorThemeOptions) => { - setColor(color); - onChange(color); - }; - - /* MUI Specific state */ - const [open, setOpen] = useState(false); - - /* MUI Menu event handlers */ - const handleClick = () => setOpen(true); - const handleClose = () => setOpen(false); - - return ( -
- - - - {/* Light Theme Customize Color */} - - - handleChange({ ...color, ...{ light: value.hex } }) - } - /> - - - - Light Theme - - - - - - - - - {/* Dark Theme Customize Color */} - - - handleChange({ ...color, ...{ dark: value.hex } }) - } - /> - - - - Dark Theme - - - - - - - - - -
- ); -}; - export default ColorSelect; diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index 26362f5d..d0fe7fdb 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -14,7 +14,7 @@ import { } from "@mui/material"; import AddIcon from "@mui/icons-material/AddCircle"; import RemoveIcon from "@mui/icons-material/CancelRounded"; -import ColorSelect from "@src/components/ColorSelect"; +import ColorSelect from "@src/components/SelectColors"; export default function Settings({ onChange, config }: ISettingsProps) { const listEndRef: any = useRef(null); From 3866f4d15bd22f34098d0cd12e4ff0f04dfa32f4 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Fri, 3 Mar 2023 00:31:22 +0100 Subject: [PATCH 006/183] Removed duplicate interface for ColorSelect --- src/components/SelectColors/CustomizeColorModal.tsx | 6 +----- src/components/SelectColors/index.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/SelectColors/CustomizeColorModal.tsx b/src/components/SelectColors/CustomizeColorModal.tsx index a9d7e6a8..49462fc4 100644 --- a/src/components/SelectColors/CustomizeColorModal.tsx +++ b/src/components/SelectColors/CustomizeColorModal.tsx @@ -6,11 +6,7 @@ import { Chip, Typography } from "@mui/material"; import Modal from "@src/components/Modal"; import ColorPickerInput from "@src/components/ColorPickerInput"; import { toColor } from "react-color-palette"; - -interface SelectColorThemeOptions { - light: string; - dark: string; -} +import { SelectColorThemeOptions } from "."; interface CustomizeColor { currentColor: SelectColorThemeOptions; diff --git a/src/components/SelectColors/index.tsx b/src/components/SelectColors/index.tsx index 83b59192..cb0a3731 100644 --- a/src/components/SelectColors/index.tsx +++ b/src/components/SelectColors/index.tsx @@ -9,7 +9,7 @@ import FormatColorResetIcon from "@mui/icons-material/FormatColorReset"; import { paletteToMui, palette } from "@src/theme/palette"; import CustomizeColorModal from "./CustomizeColorModal"; -interface SelectColorThemeOptions { +export interface SelectColorThemeOptions { light: string; dark: string; } @@ -35,17 +35,17 @@ const ColorSelect = () => { lightGreen: palette.lightGreen, }); - /* Hold the current state of a given option defaults to `default` */ + /* Hold the current state of a given option defaults to `gray` from the color palette */ const [color, setColor] = useState( paletteToMui(palette["gray"]) ); - /* MUI Specific state */ + /* MUI Specific state for color context menu */ const [colorSelectAnchor, setColorSelectAnchor] = useState(null); const open = Boolean(colorSelectAnchor); - /* MUI Menu event handlers */ + /* MUI Menu event handlers for color context menu */ const handleClick = (event: React.MouseEvent) => { setColorSelectAnchor(event.currentTarget); }; From 0ff87300b46cfc74d7a794b8d78bae8abb4d178a Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Fri, 3 Mar 2023 15:38:18 +0100 Subject: [PATCH 007/183] fixed react-color-palette issue --- src/components/ColorPickerInput.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/ColorPickerInput.tsx b/src/components/ColorPickerInput.tsx index 89ebdb21..b1cc1fc7 100644 --- a/src/components/ColorPickerInput.tsx +++ b/src/components/ColorPickerInput.tsx @@ -43,6 +43,7 @@ export default function ColorPickerInput({ const [localValue, setLocalValue] = useState(value); const [width, setRef] = useResponsiveWidth(); const theme = useTheme(); + const isDark = theme.palette.mode === "dark" ? true : false; return ( @@ -70,6 +74,7 @@ export default function ColorPickerInput({ color={localValue} onChange={(color) => setLocalValue(color)} onChangeComplete={onChangeComplete} + dark={isDark} /> ); From 16c32b0fcec82f3ef674e51882ff2d4b0c8f2729 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Sat, 4 Mar 2023 23:41:22 +0100 Subject: [PATCH 008/183] Chip colors now being added to config and being returned and consumed in app - last task; showing colors on chips --- src/components/SelectColors/index.tsx | 20 +++++++++---- .../fields/SingleSelect/Settings.tsx | 28 +++++++++++++++++-- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/components/SelectColors/index.tsx b/src/components/SelectColors/index.tsx index cb0a3731..4adbc67b 100644 --- a/src/components/SelectColors/index.tsx +++ b/src/components/SelectColors/index.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { FC, useState } from "react"; import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; import Menu from "@mui/material/Menu"; @@ -14,7 +14,12 @@ export interface SelectColorThemeOptions { dark: string; } -const ColorSelect = () => { +interface IColorSelect { + handleChange: (value: SelectColorThemeOptions) => void; + initialValue: SelectColorThemeOptions; +} + +const ColorSelect: FC = ({ handleChange, initialValue }) => { /* Get current */ const theme = useTheme(); const mode = theme.palette.mode; @@ -37,9 +42,14 @@ const ColorSelect = () => { /* Hold the current state of a given option defaults to `gray` from the color palette */ const [color, setColor] = useState( - paletteToMui(palette["gray"]) + initialValue || paletteToMui(palette["gray"]) ); + const onChange = (color: SelectColorThemeOptions) => { + setColor(color); + handleChange(color); + }; + /* MUI Specific state for color context menu */ const [colorSelectAnchor, setColorSelectAnchor] = useState(null); @@ -121,7 +131,7 @@ const ColorSelect = () => { }, }} size="small" - onClick={() => setColor(paletteToMui(palettes[key]))} + onClick={() => onChange(paletteToMui(palettes[key]))} key={index} />
@@ -131,7 +141,7 @@ const ColorSelect = () => { setColor(color)} + onChange={(color) => onChange(color)} /> diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index d0fe7fdb..c2c242a0 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -14,12 +14,21 @@ import { } from "@mui/material"; import AddIcon from "@mui/icons-material/AddCircle"; import RemoveIcon from "@mui/icons-material/CancelRounded"; -import ColorSelect from "@src/components/SelectColors"; +import ColorSelect, { + SelectColorThemeOptions, +} from "@src/components/SelectColors"; export default function Settings({ onChange, config }: ISettingsProps) { const listEndRef: any = useRef(null); const options = config.options ?? []; const [newOption, setNewOption] = useState(""); + + /* State for holding Chip Colors for Select and MultiSelect */ + const colors = config.colors ?? []; + const [chipColors, setChipColors] = useState<{}>( + Object.assign({}, colors) || {} + ); + const handleAdd = () => { if (newOption.trim() !== "") { if (options.includes(newOption)) { @@ -32,6 +41,14 @@ export default function Settings({ onChange, config }: ISettingsProps) { } }; + const handleChipColorChange = ( + index: number, + color: SelectColorThemeOptions + ) => { + setChipColors((current) => ({ ...current, [index]: color })); + onChange("colors")(Object.values(chipColors)); + }; + return (
Options @@ -43,7 +60,7 @@ export default function Settings({ onChange, config }: ISettingsProps) { marginBottom: 5, }} > - {options?.map((option: string) => ( + {options?.map((option: string, index: number) => ( <> - + + handleChipColorChange(index, color) + } + /> {option} From 4a3867b8b0ebac58c5be820959bf7637b22f5f90 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Mon, 6 Mar 2023 02:25:02 +0100 Subject: [PATCH 009/183] Select and MultiSelect chips are now colorized; working on fixing and implementing some logic --- src/components/FormattedChip.tsx | 7 ++++- .../fields/MultiSelect/DisplayCell.tsx | 19 ++++++++++-- .../fields/SingleSelect/DisplayCell.tsx | 25 ++++++++++++++-- .../fields/SingleSelect/Settings.tsx | 29 +++++++++++-------- 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/components/FormattedChip.tsx b/src/components/FormattedChip.tsx index 6e145d50..be3216e7 100644 --- a/src/components/FormattedChip.tsx +++ b/src/components/FormattedChip.tsx @@ -16,7 +16,12 @@ export default function FormattedChip(props: ChipProps) { return ( ); diff --git a/src/components/fields/MultiSelect/DisplayCell.tsx b/src/components/fields/MultiSelect/DisplayCell.tsx index fe7d6b87..38c54681 100644 --- a/src/components/fields/MultiSelect/DisplayCell.tsx +++ b/src/components/fields/MultiSelect/DisplayCell.tsx @@ -1,12 +1,13 @@ import { IDisplayCellProps } from "@src/components/fields/types"; -import { ButtonBase, Grid, Tooltip } from "@mui/material"; +import { ButtonBase, Grid, Tooltip, useTheme } from "@mui/material"; import WarningIcon from "@mui/icons-material/WarningAmber"; import { ChevronDown } from "@src/assets/icons"; import { sanitiseValue } from "./utils"; import ChipList from "@src/components/Table/TableCell/ChipList"; import FormattedChip from "@src/components/FormattedChip"; +import palette, { paletteToMui } from "@src/theme/palette"; export default function MultiSelect({ value, @@ -14,7 +15,12 @@ export default function MultiSelect({ disabled, tabIndex, rowHeight, + column, }: IDisplayCellProps) { + const defaultColor = paletteToMui(palette.aGray); + const colors = column?.config?.colors ?? {}; + const { mode } = useTheme().palette; + const rendered = typeof value === "string" && value !== "" ? (
@@ -30,7 +36,16 @@ export default function MultiSelect({ (item) => typeof item === "string" && ( - + 0 && + colors[item.toLocaleLowerCase()] + ? colors[item.toLocaleLowerCase()][mode] + : defaultColor[mode], + }} + /> ) )} diff --git a/src/components/fields/SingleSelect/DisplayCell.tsx b/src/components/fields/SingleSelect/DisplayCell.tsx index ece750d1..2cb307c3 100644 --- a/src/components/fields/SingleSelect/DisplayCell.tsx +++ b/src/components/fields/SingleSelect/DisplayCell.tsx @@ -1,16 +1,25 @@ import { IDisplayCellProps } from "@src/components/fields/types"; -import { ButtonBase } from "@mui/material"; +import { ButtonBase, Chip } from "@mui/material"; import { ChevronDown } from "@src/assets/icons"; +import { useTheme } from "@mui/material"; import { sanitiseValue } from "./utils"; +import palette, { paletteToMui } from "@src/theme/palette"; +import ChipList from "@src/components/Table/TableCell/ChipList"; export default function SingleSelect({ value, showPopoverCell, disabled, tabIndex, + column, + rowHeight, }: IDisplayCellProps) { + const defaultColor = paletteToMui(palette.aGray); + const colors = column?.config?.colors ?? {}; + const { mode } = useTheme().palette; + const rendered = (
- {sanitiseValue(value)} + + 0 && + colors[value.toLocaleLowerCase()] + ? colors[value.toLocaleLowerCase()][mode] + : defaultColor[mode], + }} + /> +
); diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index c2c242a0..15563ce7 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -24,10 +24,7 @@ export default function Settings({ onChange, config }: ISettingsProps) { const [newOption, setNewOption] = useState(""); /* State for holding Chip Colors for Select and MultiSelect */ - const colors = config.colors ?? []; - const [chipColors, setChipColors] = useState<{}>( - Object.assign({}, colors) || {} - ); + const colors = config.colors ?? {}; const handleAdd = () => { if (newOption.trim() !== "") { @@ -42,11 +39,18 @@ export default function Settings({ onChange, config }: ISettingsProps) { }; const handleChipColorChange = ( - index: number, + key: string, color: SelectColorThemeOptions ) => { - setChipColors((current) => ({ ...current, [index]: color })); - onChange("colors")(Object.values(chipColors)); + const _key = key.toLocaleLowerCase(); + colors[_key] = color; + onChange("colors")(Object(colors)); + }; + + const handleChipColorDelete = (key: string) => { + const _key = key.toLocaleLowerCase(); + delete colors[_key]; + onChange("colors")(Object(colors)); }; return ( @@ -72,9 +76,9 @@ export default function Settings({ onChange, config }: ISettingsProps) { - handleChipColorChange(index, color) + handleChipColorChange(option, color) } /> {option} @@ -83,11 +87,12 @@ export default function Settings({ onChange, config }: ISettingsProps) { + onClick={() => { onChange("options")( options.filter((o: string) => o !== option) - ) - } + ); + handleChipColorDelete(option); + }} > {} From 5c6a343161fb49cd09d6d0f161422707eca9e622 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Mon, 6 Mar 2023 03:05:03 +0100 Subject: [PATCH 010/183] Change check logic for values in SingleSelect and MultiSelect DisplayCells --- .../fields/MultiSelect/DisplayCell.tsx | 8 +++----- .../fields/SingleSelect/DisplayCell.tsx | 18 +++++++++--------- .../fields/SingleSelect/Settings.tsx | 7 +++---- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/components/fields/MultiSelect/DisplayCell.tsx b/src/components/fields/MultiSelect/DisplayCell.tsx index 38c54681..8310acef 100644 --- a/src/components/fields/MultiSelect/DisplayCell.tsx +++ b/src/components/fields/MultiSelect/DisplayCell.tsx @@ -39,11 +39,9 @@ export default function MultiSelect({ 0 && - colors[item.toLocaleLowerCase()] - ? colors[item.toLocaleLowerCase()][mode] - : defaultColor[mode], + backgroundColor: colors[item.toLocaleLowerCase()] + ? colors[item.toLocaleLowerCase()][mode] + : defaultColor[mode], }} /> diff --git a/src/components/fields/SingleSelect/DisplayCell.tsx b/src/components/fields/SingleSelect/DisplayCell.tsx index 2cb307c3..967633b0 100644 --- a/src/components/fields/SingleSelect/DisplayCell.tsx +++ b/src/components/fields/SingleSelect/DisplayCell.tsx @@ -29,17 +29,17 @@ export default function SingleSelect({ }} > - 0 && - colors[value.toLocaleLowerCase()] + {value && ( + + }} + /> + )}
); diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index 15563ce7..908249d1 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -87,12 +87,11 @@ export default function Settings({ onChange, config }: ISettingsProps) { { + onClick={() => onChange("options")( options.filter((o: string) => o !== option) - ); - handleChipColorDelete(option); - }} + ) + } > {} From d283a4721d21659e959f40e95c1b28f777ffae25 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Tue, 7 Mar 2023 00:11:18 +0100 Subject: [PATCH 011/183] Fixed color on multiselect chip in side drawer field and start of logic changes --- .../fields/MultiSelect/SideDrawerField.tsx | 5 +- .../fields/SingleSelect/Settings.tsx | 52 ++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/components/fields/MultiSelect/SideDrawerField.tsx b/src/components/fields/MultiSelect/SideDrawerField.tsx index 99c260be..4482350f 100644 --- a/src/components/fields/MultiSelect/SideDrawerField.tsx +++ b/src/components/fields/MultiSelect/SideDrawerField.tsx @@ -1,6 +1,6 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types"; -import { Grid, Button, Tooltip } from "@mui/material"; +import { Grid, Button, Tooltip, useTheme } from "@mui/material"; import WarningIcon from "@mui/icons-material/WarningAmber"; import MultiSelectComponent from "@rowy/multiselect"; import FormattedChip from "@src/components/FormattedChip"; @@ -16,6 +16,8 @@ export default function MultiSelect({ disabled, }: ISideDrawerFieldProps) { const config = column.config ?? {}; + const colors = column.config?.colors ?? {}; + const { mode } = useTheme().palette; const handleDelete = (index: number) => () => { const newValues = [...value]; @@ -75,6 +77,7 @@ export default function MultiSelect({ ) diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index 288c425f..42ef2a4d 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -42,7 +42,7 @@ export default function Settings({ onChange, config }: ISettingsProps) { const [newOption, setNewOption] = useState(""); /* State for holding Chip Colors for Select and MultiSelect */ - const colors = config.colors ?? {}; + let colors = config.colors ?? {}; const handleAdd = () => { if (newOption.trim() !== "") { @@ -65,8 +65,12 @@ export default function Settings({ onChange, config }: ISettingsProps) { onChange("colors")(Object(colors)); }; - const handleChipColorDelete = (key: string) => { - const _key = key.toLocaleLowerCase(); + const handleItemDelete = (option: string) => { + onChange("options")(options.filter((o: string) => o !== option)); + }; + + const handleItemColorDelete = (option: string) => { + const _key = option.toLocaleLowerCase(); delete colors[_key]; onChange("colors")(Object(colors)); }; @@ -114,6 +118,7 @@ export default function Settings({ onChange, config }: ISettingsProps) { {...provided.dragHandleProps} item sx={{ display: "flex" }} + alignItems="center" > - - - - handleChipColorChange(option, color) - } - /> - {option} - + + + handleChipColorChange(option, color) + } + /> + {option} - onChange("options")( - options.filter((o: string) => o !== option) - ) - } + onClick={() => { + handleItemDelete(option); + //handleItemColorDelete(option); + }} > {} From 78cd3deabf25a725f743d25cd029283ff559903c Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Tue, 7 Mar 2023 23:37:37 +0100 Subject: [PATCH 012/183] Adding logic - but have a blocker on deleting colors alongside deleting options --- .../fields/SingleSelect/Settings.tsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index 42ef2a4d..074116fc 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -42,7 +42,7 @@ export default function Settings({ onChange, config }: ISettingsProps) { const [newOption, setNewOption] = useState(""); /* State for holding Chip Colors for Select and MultiSelect */ - let colors = config.colors ?? {}; + const colors = config.colors ?? {}; const handleAdd = () => { if (newOption.trim() !== "") { @@ -57,22 +57,20 @@ export default function Settings({ onChange, config }: ISettingsProps) { }; const handleChipColorChange = ( + type: "save" | "delete", key: string, - color: SelectColorThemeOptions + color?: SelectColorThemeOptions ) => { const _key = key.toLocaleLowerCase(); - colors[_key] = color; - onChange("colors")(Object(colors)); + const { [_key]: _, ...newColors } = colors; + if (type === "save") colors[_key] = color; + else if (type === "delete") return newColors; }; const handleItemDelete = (option: string) => { onChange("options")(options.filter((o: string) => o !== option)); - }; - - const handleItemColorDelete = (option: string) => { - const _key = option.toLocaleLowerCase(); - delete colors[_key]; - onChange("colors")(Object(colors)); + onChange("colors")(handleChipColorChange("delete", option)); + console.log(options, colors); // Here for debugging reasons }; const handleOnDragEnd = (result: any) => { @@ -139,7 +137,7 @@ export default function Settings({ onChange, config }: ISettingsProps) { colors[option.toLocaleLowerCase()] } handleChange={(color) => - handleChipColorChange(option, color) + handleChipColorChange("save", option, color) } /> {option} @@ -148,10 +146,7 @@ export default function Settings({ onChange, config }: ISettingsProps) { { - handleItemDelete(option); - //handleItemColorDelete(option); - }} + onClick={() => handleItemDelete(option)} > {} From 5e9b211ff99b62fdc3ccfcff548e0f9b072fe709 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Wed, 8 Mar 2023 01:23:43 +0100 Subject: [PATCH 013/183] Fixed error on MultiSelect SideDrawerField --- .../fields/MultiSelect/SideDrawerField.tsx | 6 +++++- src/components/fields/SingleSelect/Settings.tsx | 17 ++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/fields/MultiSelect/SideDrawerField.tsx b/src/components/fields/MultiSelect/SideDrawerField.tsx index 4482350f..28ef41e4 100644 --- a/src/components/fields/MultiSelect/SideDrawerField.tsx +++ b/src/components/fields/MultiSelect/SideDrawerField.tsx @@ -77,7 +77,11 @@ export default function MultiSelect({ ) diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index 074116fc..b1d33b56 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -38,7 +38,7 @@ const getItemStyle = ( export default function Settings({ onChange, config }: ISettingsProps) { const listEndRef: any = useRef(null); - const options = config.options ?? []; + let options = config.options ?? []; const [newOption, setNewOption] = useState(""); /* State for holding Chip Colors for Select and MultiSelect */ @@ -62,15 +62,13 @@ export default function Settings({ onChange, config }: ISettingsProps) { color?: SelectColorThemeOptions ) => { const _key = key.toLocaleLowerCase(); - const { [_key]: _, ...newColors } = colors; if (type === "save") colors[_key] = color; - else if (type === "delete") return newColors; + else if (type === "delete") delete colors[_key]; + onChange("colors")(colors); }; - const handleItemDelete = (option: string) => { + const handleItemDelete = async (option: string) => { onChange("options")(options.filter((o: string) => o !== option)); - onChange("colors")(handleChipColorChange("delete", option)); - console.log(options, colors); // Here for debugging reasons }; const handleOnDragEnd = (result: any) => { @@ -146,7 +144,12 @@ export default function Settings({ onChange, config }: ISettingsProps) { handleItemDelete(option)} + onClick={() => + handleItemDelete(option).then(() => { + handleChipColorChange("delete", option); + console.log(options, colors); // Here for debugging purposes + }) + } > {} From b0b897efd7a155b8ca080e545622547d081102a5 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Fri, 10 Mar 2023 00:30:30 +0100 Subject: [PATCH 014/183] Deleting options and colors at the same time --- src/atoms/tableScope/columnActions.ts | 1 + .../ColumnModals/ColumnConfigModal/ColumnConfig.tsx | 3 ++- src/components/fields/SingleSelect/Settings.tsx | 11 +++++------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/atoms/tableScope/columnActions.ts b/src/atoms/tableScope/columnActions.ts index 2f3926e8..1caf94b9 100644 --- a/src/atoms/tableScope/columnActions.ts +++ b/src/atoms/tableScope/columnActions.ts @@ -100,6 +100,7 @@ export const updateColumnAtom = atom( // Reduce array into single object with updated indexes const updatedColumns = tableColumnsOrdered.reduce(tableColumnsReducer, {}); await updateTableSchema({ columns: updatedColumns }); + console.log(updatedColumns); // Testing Purpose Only @devsgnr } ); diff --git a/src/components/ColumnModals/ColumnConfigModal/ColumnConfig.tsx b/src/components/ColumnModals/ColumnConfigModal/ColumnConfig.tsx index 27665606..39ce7b1d 100644 --- a/src/components/ColumnModals/ColumnConfigModal/ColumnConfig.tsx +++ b/src/components/ColumnModals/ColumnConfigModal/ColumnConfig.tsx @@ -78,8 +78,9 @@ export default function ColumnConfigModal({ ) { setShowRebuildPrompt(true); } - const updatedConfig = set({ ...newConfig }, key, update); + const updatedConfig = set(newConfig, key, update); // Modified by @devsgnr, spread operator `{...newConfig}` instead of just `newConfig` was preventing multiple calls from running properly setNewConfig(updatedConfig); + console.log(updatedConfig); // Testing Purpose Only @devsgnr validateSettings(); }; diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index b1d33b56..bc87d088 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -38,11 +38,11 @@ const getItemStyle = ( export default function Settings({ onChange, config }: ISettingsProps) { const listEndRef: any = useRef(null); - let options = config.options ?? []; + const options = config.options ?? []; const [newOption, setNewOption] = useState(""); /* State for holding Chip Colors for Select and MultiSelect */ - const colors = config.colors ?? {}; + let colors = config.colors ?? {}; const handleAdd = () => { if (newOption.trim() !== "") { @@ -145,10 +145,9 @@ export default function Settings({ onChange, config }: ISettingsProps) { - handleItemDelete(option).then(() => { - handleChipColorChange("delete", option); - console.log(options, colors); // Here for debugging purposes - }) + handleItemDelete(option).then( + () => handleChipColorChange("delete", option) //@devsgnr + ) } > {} From 7e8b0ed96cdacec7afdd46e8bf16fcfcd1648a93 Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Fri, 10 Mar 2023 13:43:36 +0530 Subject: [PATCH 015/183] import files from URL --- src/atoms/tableScope/rowActions.ts | 13 +- src/components/Table/Mock/Cell.tsx | 2 + src/components/Table/Mock/mockValue/file.ts | 11 ++ src/components/Table/Mock/mockValue/index.ts | 20 +++ .../Table/Mock/mockValue/reference.ts | 12 ++ .../ImportCsvWizard/ImportCsvWizard.tsx | 101 ++++++++++++- .../ImportCsvWizard/useConverter.ts | 49 ++++++ .../ImportCsvWizard/useUploadFileFromURL.tsx | 141 ++++++++++++++++++ .../ImportExistingWizard/Step1Columns.tsx | 6 +- .../TableModals/ImportExistingWizard/utils.ts | 5 + 10 files changed, 354 insertions(+), 6 deletions(-) create mode 100644 src/components/Table/Mock/mockValue/file.ts create mode 100644 src/components/Table/Mock/mockValue/index.ts create mode 100644 src/components/Table/Mock/mockValue/reference.ts create mode 100644 src/components/TableModals/ImportCsvWizard/useConverter.ts create mode 100644 src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx diff --git a/src/atoms/tableScope/rowActions.ts b/src/atoms/tableScope/rowActions.ts index 75b496f5..abf15be0 100644 --- a/src/atoms/tableScope/rowActions.ts +++ b/src/atoms/tableScope/rowActions.ts @@ -242,10 +242,15 @@ export interface IBulkAddRowsOptions { rows: Partial; collection: string; onBatchCommit?: Parameters[1]; + type?: "add"; } export const bulkAddRowsAtom = atom( null, - async (get, _, { rows, collection, onBatchCommit }: IBulkAddRowsOptions) => { + async ( + get, + _, + { rows, collection, onBatchCommit, type }: IBulkAddRowsOptions + ) => { const bulkWriteDb = get(_bulkWriteDbAtom); if (!bulkWriteDb) throw new Error("Cannot write to database"); const tableSettings = get(tableSettingsAtom); @@ -277,7 +282,11 @@ export const bulkAddRowsAtom = atom( // Assign a random ID to each row const operations = rows.map((row) => ({ - type: row?._rowy_ref?.id ? ("update" as "update") : ("add" as "add"), + type: type + ? type + : row?._rowy_ref?.id + ? ("update" as "update") + : ("add" as "add"), path: `${collection}/${row?._rowy_ref?.id ?? generateId()}`, data: { ...initialValues, ...omitRowyFields(row) }, })); diff --git a/src/components/Table/Mock/Cell.tsx b/src/components/Table/Mock/Cell.tsx index 959765a0..cffc2c52 100644 --- a/src/components/Table/Mock/Cell.tsx +++ b/src/components/Table/Mock/Cell.tsx @@ -7,6 +7,7 @@ import EmptyState from "@src/components/EmptyState"; import { FieldType } from "@src/constants/fields"; import { getFieldProp } from "@src/components/fields"; import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; +import mockValue from "./mockValue"; export interface ICellProps extends Partial< @@ -31,6 +32,7 @@ export default function Cell({ ...props }: ICellProps) { const tableCell = type ? getFieldProp("TableCell", type) : null; + value = mockValue(value, type); return ( diff --git a/src/components/Table/Mock/mockValue/file.ts b/src/components/Table/Mock/mockValue/file.ts new file mode 100644 index 00000000..c606527c --- /dev/null +++ b/src/components/Table/Mock/mockValue/file.ts @@ -0,0 +1,11 @@ +export const fileValueConverter = (value: any) => { + if (!value) return []; + if (Array.isArray(value)) return value; + if (typeof value === "string") { + return value.split(",").map((url) => ({ + downloadURL: url.trim(), + name: +new Date() + "-" + Math.round(Math.random() * 1000), + })); + } + return []; +}; diff --git a/src/components/Table/Mock/mockValue/index.ts b/src/components/Table/Mock/mockValue/index.ts new file mode 100644 index 00000000..fd2fa502 --- /dev/null +++ b/src/components/Table/Mock/mockValue/index.ts @@ -0,0 +1,20 @@ +import { FieldType } from "@src/constants/fields"; +import { fileValueConverter } from "./file"; +import { referenceValueConverter } from "./reference"; + +export const VALUE_CONVERTERS: Partial<{ + [key in FieldType]: (value: any) => any; +}> = { + [FieldType.image]: fileValueConverter, + [FieldType.reference]: referenceValueConverter, + [FieldType.file]: fileValueConverter, +}; + +export default function convert(value: any, type: FieldType) { + const converter = VALUE_CONVERTERS[type]; + if (converter) { + return converter(value); + } + + return value; +} diff --git a/src/components/Table/Mock/mockValue/reference.ts b/src/components/Table/Mock/mockValue/reference.ts new file mode 100644 index 00000000..ffab1b0f --- /dev/null +++ b/src/components/Table/Mock/mockValue/reference.ts @@ -0,0 +1,12 @@ +export const referenceValueConverter = (value: any) => { + if (typeof value === "string") { + if ( + value !== "" && + value.split("/").length > 0 && + value.split("/").length % 2 === 0 + ) { + return { path: value }; + } + } + return value; +}; diff --git a/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx b/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx index b40e0e8c..551b69ec 100644 --- a/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx +++ b/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx @@ -37,7 +37,11 @@ import { import { ColumnConfig } from "@src/types/table"; import { getFieldProp } from "@src/components/fields"; import { analytics, logEvent } from "@src/analytics"; +import { FieldType } from "@src/constants/fields"; +import { generateId } from "@src/utils/table"; import { isValidDocId } from "./utils"; +import useUploadFileFromURL from "./useUploadFileFromURL"; +import useConverter from "./useConverter"; export type CsvConfig = { pairs: { csvKey: string; columnKey: string }[]; @@ -46,6 +50,8 @@ export type CsvConfig = { documentIdCsvKey: string | null; }; +const needsUploadTypes = [FieldType.image, FieldType.file]; + export interface IStepProps { csvData: NonNullable; config: CsvConfig; @@ -66,6 +72,10 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { const isXs = useMediaQuery(theme.breakpoints.down("sm")); const snackbarProgressRef = useRef(); + const snackbarUploadProgressRef = useRef(); + const { addTask, runBatchUpload, askPermission } = useUploadFileFromURL(); + const { needsConverter, getConverter } = useConverter(); + const columns = useMemoValue(tableSchema.columns ?? {}, isEqual); const [config, setConfig] = useState({ @@ -74,6 +84,7 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { documentId: "auto", documentIdCsvKey: null, }); + const updateConfig: IStepProps["updateConfig"] = useCallback((value) => { setConfig((prev) => { const pairs = uniqBy([...prev.pairs, ...(value.pairs ?? [])], "csvKey"); @@ -123,6 +134,35 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { ) : { validRows: parsedRows, invalidRows: [] }; + const { requiredConverts, requiredUploads } = useMemo(() => { + const columns = config.pairs.map(({ csvKey, columnKey }) => ({ + csvKey, + columnKey, + ...(tableSchema.columns?.[columnKey] ?? + find(config.newColumns, { key: columnKey }) ?? + {}), + })); + + let requiredConverts: any = {}; + let requiredUploads: any = {}; + columns.forEach((column, index) => { + if (needsConverter(column.type)) { + requiredConverts[index] = getConverter(column.type); + console.log({ needsUploadTypes }, column.type); + if (needsUploadTypes.includes(column.type)) { + requiredUploads[column.fieldName + ""] = true; + } + } + }); + return { requiredConverts, requiredUploads }; + }, [ + config.newColumns, + config.pairs, + getConverter, + needsConverter, + tableSchema.columns, + ]); + const handleFinish = async () => { if (!parsedRows) return; console.time("importCsv"); @@ -176,12 +216,48 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { { variant: "warning" } ); } + const newValidRows = validRows.map((row) => { + // Convert required values + Object.keys(row).forEach((key, i) => { + if (requiredConverts[i]) { + row[key] = requiredConverts[i](row[key]); + } + }); + + const id = generateId(); + const newRow = { + _rowy_ref: { + path: `${tableSettings.collection}/${row?._rowy_ref?.id ?? id}`, + id, + }, + ...row, + }; + return newRow; + }); + promises.push( bulkAddRows({ - rows: validRows, + type: "add", + rows: newValidRows, collection: tableSettings.collection, - onBatchCommit: (batchNumber: number) => - snackbarProgressRef.current?.setProgress(batchNumber), + onBatchCommit: async (batchNumber: number) => { + if (Object.keys(requiredUploads).length > 0) { + newValidRows + .slice((batchNumber - 1) * 500, batchNumber * 500 - 1) + .forEach((row) => { + Object.keys(requiredUploads).forEach((key) => { + if (requiredUploads[key]) { + addTask({ + docRef: row._rowy_ref, + fieldName: key, + files: row[key], + }); + } + }); + }); + } + snackbarProgressRef.current?.setProgress(batchNumber); + }, }) ); @@ -192,6 +268,25 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { `Imported ${Number(validRows.length).toLocaleString()} rows`, { variant: "success" } ); + if (await askPermission()) { + const uploadingSnackbar = enqueueSnackbar( + `Importing ${Number( + validRows.length + ).toLocaleString()} rows. This might take a while.`, + { + persist: true, + action: ( + + ), + } + ); + await runBatchUpload(snackbarUploadProgressRef.current?.setProgress); + closeSnackbar(uploadingSnackbar); + } } catch (e) { enqueueSnackbar((e as Error).message, { variant: "error" }); } finally { diff --git a/src/components/TableModals/ImportCsvWizard/useConverter.ts b/src/components/TableModals/ImportCsvWizard/useConverter.ts new file mode 100644 index 00000000..33314137 --- /dev/null +++ b/src/components/TableModals/ImportCsvWizard/useConverter.ts @@ -0,0 +1,49 @@ +import { projectScope } from "@src/atoms/projectScope"; +import { FieldType } from "@src/constants/fields"; +import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase"; +import { doc, DocumentReference as Reference } from "firebase/firestore"; +import { useAtom } from "jotai"; + +const needsConverter = (type: FieldType) => + [FieldType.image, FieldType.reference, FieldType.file].includes(type); + +export default function useConverter() { + const [firebaseDb] = useAtom(firebaseDbAtom, projectScope); + + const referenceConverter = (value: string): Reference | null => { + if (!value) return null; + if (value.split("/").length % 2 !== 0) return null; + return doc(firebaseDb, value); + }; + + const imageOrFileConverter = (urls: string): RowyFile[] => { + return urls.split(",").map((url) => { + url = url.trim(); + return { + downloadURL: url, + name: url.split("/").pop() || "", + lastModifiedTS: +new Date(), + type: "", + }; + }); + }; + + const getConverter = (type: FieldType) => { + switch (type) { + case FieldType.image: + case FieldType.file: + return imageOrFileConverter; + case FieldType.reference: + return referenceConverter; + default: + return null; + } + }; + + return { + needsConverter, + referenceConverter, + imageOrFileConverter, + getConverter, + }; +} diff --git a/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx b/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx new file mode 100644 index 00000000..b5fb7511 --- /dev/null +++ b/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx @@ -0,0 +1,141 @@ +import { useCallback, useRef } from "react"; +import { useSetAtom } from "jotai"; +import { useSnackbar } from "notistack"; +import Button from "@mui/material/Button"; + +import useUploader from "@src/hooks/useFirebaseStorageUploader"; +import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; +import { TableRowRef } from "@src/types/table"; + +const MAX_PARALLEL_TASKS = 30; + +type UploadParamTypes = { + docRef: TableRowRef; + fieldName: string; + files: RowyFile[]; +}; + +export default function useUploadFileFromURL() { + const { upload } = useUploader(); + const updateField = useSetAtom(updateFieldAtom, tableScope); + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const jobs = useRef([]); + + const askPermission = useCallback(async (): Promise => { + return new Promise((resolve) => { + enqueueSnackbar("Upload files to firebase storage?", { + persist: true, + preventDuplicate: true, + action: ( + <> + + + + ), + }); + }); + }, [enqueueSnackbar, closeSnackbar]); + + const handleUpload = useCallback( + async ({ + docRef, + fieldName, + files, + }: UploadParamTypes): Promise => { + try { + const files_ = await getFileFromURL( + files.map((file) => file.downloadURL) + ); + const { uploads, failures } = await upload({ + docRef, + fieldName, + files: files_, + }); + if (failures.length > 0) { + return false; + } + updateField({ + path: docRef.path, + fieldName, + value: uploads, + useArrayUnion: false, + }); + return true; + } catch (error) { + return false; + } + }, + [upload, updateField] + ); + + const batchUpload = useCallback( + async (batch: UploadParamTypes[]) => { + await Promise.all(batch.map((job) => handleUpload(job))); + }, + [handleUpload] + ); + + const runBatchUpload = useCallback( + async (setProgress?: any) => { + let currentJobs: UploadParamTypes[] = []; + + while ( + currentJobs.length < MAX_PARALLEL_TASKS && + jobs.current.length > 0 + ) { + const job = jobs.current.shift(); + if (job) { + currentJobs.push(job); + } + } + + if (setProgress) setProgress((p: number) => p + currentJobs.length); + await batchUpload(currentJobs); + + if (jobs.current.length > 0) { + runBatchUpload(); + } + }, + [batchUpload] + ); + + const addTask = useCallback((job: UploadParamTypes) => { + jobs.current.push(job); + }, []); + + return { + addTask, + runBatchUpload, + askPermission, + }; +} + +function getFileFromURL(urls: string[]): Promise { + const promises = urls.map((url) => { + return fetch(url) + .then((response) => response.blob()) + .then((blob) => new File([blob], +new Date() + url, { type: blob.type })); + }); + return Promise.all(promises); +} diff --git a/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx b/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx index c5d83299..dc339687 100644 --- a/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx +++ b/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx @@ -117,7 +117,11 @@ export default function Step1Columns({ config, setConfig }: IStepProps) { color="default" /> } - label={selectedFields.length == allFields.length ? "Clear all" : "Select all"} + label={ + selectedFields.length === allFields.length + ? "Clear all" + : "Select all" + } sx={{ height: 42, mr: 0, diff --git a/src/components/TableModals/ImportExistingWizard/utils.ts b/src/components/TableModals/ImportExistingWizard/utils.ts index d855da03..ec6ad97d 100644 --- a/src/components/TableModals/ImportExistingWizard/utils.ts +++ b/src/components/TableModals/ImportExistingWizard/utils.ts @@ -18,6 +18,9 @@ export const SELECTABLE_TYPES = [ FieldType.url, FieldType.rating, + FieldType.image, + FieldType.file, + FieldType.singleSelect, FieldType.multiSelect, @@ -26,6 +29,8 @@ export const SELECTABLE_TYPES = [ FieldType.color, FieldType.slider, + + FieldType.reference, ]; export const REGEX_EMAIL = From 2a04ee1bd2a33d8bf3a409afbbc1757a5db8dbfa Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Fri, 10 Mar 2023 14:06:09 +0530 Subject: [PATCH 016/183] bug fixes and added progress on snackbar --- src/components/Table/Mock/mockValue/file.ts | 17 +++- .../ImportCsvWizard/ImportCsvWizard.tsx | 24 +----- .../ImportCsvWizard/useConverter.ts | 29 ++++--- .../ImportCsvWizard/useUploadFileFromURL.tsx | 81 +++++++++++++------ .../TableToolbar/ImportData/ImportFromCsv.tsx | 2 +- 5 files changed, 94 insertions(+), 59 deletions(-) diff --git a/src/components/Table/Mock/mockValue/file.ts b/src/components/Table/Mock/mockValue/file.ts index c606527c..990db340 100644 --- a/src/components/Table/Mock/mockValue/file.ts +++ b/src/components/Table/Mock/mockValue/file.ts @@ -2,10 +2,19 @@ export const fileValueConverter = (value: any) => { if (!value) return []; if (Array.isArray(value)) return value; if (typeof value === "string") { - return value.split(",").map((url) => ({ - downloadURL: url.trim(), - name: +new Date() + "-" + Math.round(Math.random() * 1000), - })); + return value + .split(",") + .map((url) => { + url = url.trim(); + if (url !== "") { + return { + downloadURL: url, + name: +new Date() + "-" + Math.round(Math.random() * 1000), + }; + } + return null; + }) + .filter((mockValue) => mockValue !== null); } return []; }; diff --git a/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx b/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx index 551b69ec..421fbc30 100644 --- a/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx +++ b/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx @@ -71,8 +71,6 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { const theme = useTheme(); const isXs = useMediaQuery(theme.breakpoints.down("sm")); const snackbarProgressRef = useRef(); - - const snackbarUploadProgressRef = useRef(); const { addTask, runBatchUpload, askPermission } = useUploadFileFromURL(); const { needsConverter, getConverter } = useConverter(); @@ -148,7 +146,7 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { columns.forEach((column, index) => { if (needsConverter(column.type)) { requiredConverts[index] = getConverter(column.type); - console.log({ needsUploadTypes }, column.type); + // console.log({ needsUploadTypes }, column.type); if (needsUploadTypes.includes(column.type)) { requiredUploads[column.fieldName + ""] = true; } @@ -268,24 +266,8 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { `Imported ${Number(validRows.length).toLocaleString()} rows`, { variant: "success" } ); - if (await askPermission()) { - const uploadingSnackbar = enqueueSnackbar( - `Importing ${Number( - validRows.length - ).toLocaleString()} rows. This might take a while.`, - { - persist: true, - action: ( - - ), - } - ); - await runBatchUpload(snackbarUploadProgressRef.current?.setProgress); - closeSnackbar(uploadingSnackbar); + if (Object.keys(requiredUploads).length && (await askPermission())) { + await runBatchUpload(); } } catch (e) { enqueueSnackbar((e as Error).message, { variant: "error" }); diff --git a/src/components/TableModals/ImportCsvWizard/useConverter.ts b/src/components/TableModals/ImportCsvWizard/useConverter.ts index 33314137..dba43aaa 100644 --- a/src/components/TableModals/ImportCsvWizard/useConverter.ts +++ b/src/components/TableModals/ImportCsvWizard/useConverter.ts @@ -17,15 +17,26 @@ export default function useConverter() { }; const imageOrFileConverter = (urls: string): RowyFile[] => { - return urls.split(",").map((url) => { - url = url.trim(); - return { - downloadURL: url, - name: url.split("/").pop() || "", - lastModifiedTS: +new Date(), - type: "", - }; - }); + if (!urls) return []; + if (typeof urls === "string") { + return urls + .split(",") + .map((url) => { + url = url.trim(); + if (url !== "") { + return { + downloadURL: url, + name: url.split("/").pop() || "", + lastModifiedTS: +new Date(), + type: "", + }; + } + + return null; + }) + .filter((val) => val !== null) as RowyFile[]; + } + return []; }; const getConverter = (type: FieldType) => { diff --git a/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx b/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx index b5fb7511..d6727a0a 100644 --- a/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx +++ b/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx @@ -1,13 +1,14 @@ import { useCallback, useRef } from "react"; import { useSetAtom } from "jotai"; -import { useSnackbar } from "notistack"; +import { SnackbarKey, useSnackbar } from "notistack"; import Button from "@mui/material/Button"; import useUploader from "@src/hooks/useFirebaseStorageUploader"; import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; import { TableRowRef } from "@src/types/table"; +import SnackbarProgress from "@src/components/SnackbarProgress"; -const MAX_PARALLEL_TASKS = 30; +const MAX_CONCURRENT_TASKS = 10; type UploadParamTypes = { docRef: TableRowRef; @@ -75,7 +76,7 @@ export default function useUploadFileFromURL() { if (failures.length > 0) { return false; } - updateField({ + await updateField({ path: docRef.path, fieldName, value: uploads, @@ -91,35 +92,67 @@ export default function useUploadFileFromURL() { const batchUpload = useCallback( async (batch: UploadParamTypes[]) => { - await Promise.all(batch.map((job) => handleUpload(job))); + await Promise.all( + batch.map((job) => + handleUpload(job).then(() => { + snackbarProgressRef.current?.setProgress((p: number) => p + 1); + }) + ) + ); }, [handleUpload] ); - const runBatchUpload = useCallback( - async (setProgress?: any) => { - let currentJobs: UploadParamTypes[] = []; - - while ( - currentJobs.length < MAX_PARALLEL_TASKS && - jobs.current.length > 0 - ) { - const job = jobs.current.shift(); - if (job) { - currentJobs.push(job); + const snackbarProgressRef = useRef(null); + const snackbarProgressId = useRef(null); + const showProgress = useCallback( + (totalJobs: number) => { + snackbarProgressId.current = enqueueSnackbar( + `Uploading ${Number( + totalJobs + ).toLocaleString()} files/images. This might take a while.`, + { + persist: true, + action: ( + + ), } - } - - if (setProgress) setProgress((p: number) => p + currentJobs.length); - await batchUpload(currentJobs); - - if (jobs.current.length > 0) { - runBatchUpload(); - } + ); }, - [batchUpload] + [enqueueSnackbar] ); + const runBatchUpload = useCallback(async () => { + if (!snackbarProgressId.current) { + showProgress(jobs.current.length); + } + let currentJobs: UploadParamTypes[] = []; + + while ( + currentJobs.length < MAX_CONCURRENT_TASKS && + jobs.current.length > 0 + ) { + const job = jobs.current.shift(); + if (job) { + currentJobs.push(job); + } + } + + await batchUpload(currentJobs); + + if (jobs.current.length > 0) { + await runBatchUpload(); + } + + if (snackbarProgressId.current) { + closeSnackbar(snackbarProgressId.current); + } + }, [batchUpload, closeSnackbar, showProgress, snackbarProgressId]); + const addTask = useCallback((job: UploadParamTypes) => { jobs.current.push(job); }, []); diff --git a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx index f164f90a..269f306d 100644 --- a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx +++ b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx @@ -162,7 +162,7 @@ export default function ImportFromFile() { {} ) ); - console.log(mappedRows); + // console.log(mappedRows); setImportCsv({ importType: importTypeRef.current, csvData: { columns, rows: mappedRows }, From 4e92dd39b005a18490161ddef160b2cea05f9dc6 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 10 Mar 2023 14:38:26 +0530 Subject: [PATCH 017/183] made one function for converter --- src/components/Table/Mock/Cell.tsx | 8 ++++---- src/components/Table/Mock/mockValue/file.ts | 20 ------------------- src/components/Table/Mock/mockValue/index.ts | 20 ------------------- .../Table/Mock/mockValue/reference.ts | 12 ----------- .../ImportCsvWizard/useConverter.ts | 9 +++++++++ 5 files changed, 13 insertions(+), 56 deletions(-) delete mode 100644 src/components/Table/Mock/mockValue/file.ts delete mode 100644 src/components/Table/Mock/mockValue/index.ts delete mode 100644 src/components/Table/Mock/mockValue/reference.ts diff --git a/src/components/Table/Mock/Cell.tsx b/src/components/Table/Mock/Cell.tsx index cffc2c52..7d6f501b 100644 --- a/src/components/Table/Mock/Cell.tsx +++ b/src/components/Table/Mock/Cell.tsx @@ -7,7 +7,7 @@ import EmptyState from "@src/components/EmptyState"; import { FieldType } from "@src/constants/fields"; import { getFieldProp } from "@src/components/fields"; import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; -import mockValue from "./mockValue"; +import useConverter from "@src/components/TableModals/ImportCsvWizard/useConverter"; export interface ICellProps extends Partial< @@ -26,14 +26,14 @@ export interface ICellProps export default function Cell({ field, type, - value, + value: value_, name, rowHeight = DEFAULT_ROW_HEIGHT, ...props }: ICellProps) { const tableCell = type ? getFieldProp("TableCell", type) : null; - value = mockValue(value, type); - + const { checkAndConvert } = useConverter(); + const value = checkAndConvert(value_, type); return ( { - if (!value) return []; - if (Array.isArray(value)) return value; - if (typeof value === "string") { - return value - .split(",") - .map((url) => { - url = url.trim(); - if (url !== "") { - return { - downloadURL: url, - name: +new Date() + "-" + Math.round(Math.random() * 1000), - }; - } - return null; - }) - .filter((mockValue) => mockValue !== null); - } - return []; -}; diff --git a/src/components/Table/Mock/mockValue/index.ts b/src/components/Table/Mock/mockValue/index.ts deleted file mode 100644 index fd2fa502..00000000 --- a/src/components/Table/Mock/mockValue/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { FieldType } from "@src/constants/fields"; -import { fileValueConverter } from "./file"; -import { referenceValueConverter } from "./reference"; - -export const VALUE_CONVERTERS: Partial<{ - [key in FieldType]: (value: any) => any; -}> = { - [FieldType.image]: fileValueConverter, - [FieldType.reference]: referenceValueConverter, - [FieldType.file]: fileValueConverter, -}; - -export default function convert(value: any, type: FieldType) { - const converter = VALUE_CONVERTERS[type]; - if (converter) { - return converter(value); - } - - return value; -} diff --git a/src/components/Table/Mock/mockValue/reference.ts b/src/components/Table/Mock/mockValue/reference.ts deleted file mode 100644 index ffab1b0f..00000000 --- a/src/components/Table/Mock/mockValue/reference.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const referenceValueConverter = (value: any) => { - if (typeof value === "string") { - if ( - value !== "" && - value.split("/").length > 0 && - value.split("/").length % 2 === 0 - ) { - return { path: value }; - } - } - return value; -}; diff --git a/src/components/TableModals/ImportCsvWizard/useConverter.ts b/src/components/TableModals/ImportCsvWizard/useConverter.ts index dba43aaa..34a9d4f0 100644 --- a/src/components/TableModals/ImportCsvWizard/useConverter.ts +++ b/src/components/TableModals/ImportCsvWizard/useConverter.ts @@ -51,10 +51,19 @@ export default function useConverter() { } }; + const checkAndConvert = (value: any, type: FieldType) => { + if (needsConverter(type)) { + const converter = getConverter(type); + if (converter) return converter(value); + } + return value; + }; + return { needsConverter, referenceConverter, imageOrFileConverter, getConverter, + checkAndConvert, }; } From dd02ea1811f13b57547870008d89788b84af56eb Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 10 Mar 2023 15:46:09 +0530 Subject: [PATCH 018/183] export image/ file with JSON --- src/components/Table/Mock/Cell.tsx | 1 + .../ExportModal/ModalContentsExport.tsx | 15 +++++++++++++++ .../TableModals/ImportCsvWizard/useConverter.ts | 3 ++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/Table/Mock/Cell.tsx b/src/components/Table/Mock/Cell.tsx index 7d6f501b..0944d3a9 100644 --- a/src/components/Table/Mock/Cell.tsx +++ b/src/components/Table/Mock/Cell.tsx @@ -34,6 +34,7 @@ export default function Cell({ const tableCell = type ? getFieldProp("TableCell", type) : null; const { checkAndConvert } = useConverter(); const value = checkAndConvert(value_, type); + return ( (accumulator: Record, currentColumn: ColumnConfig) => { const value = get(doc, currentColumn.key); + + if ( + currentColumn.type === FieldType.file || + currentColumn.type === FieldType.image + ) { + return { + ...accumulator, + [currentColumn.key]: value + ? value + .map((item: { downloadURL: string }) => item.downloadURL) + .join() + : "", + }; + } + return { ...accumulator, [currentColumn.key]: value, diff --git a/src/components/TableModals/ImportCsvWizard/useConverter.ts b/src/components/TableModals/ImportCsvWizard/useConverter.ts index 34a9d4f0..34593f97 100644 --- a/src/components/TableModals/ImportCsvWizard/useConverter.ts +++ b/src/components/TableModals/ImportCsvWizard/useConverter.ts @@ -16,8 +16,9 @@ export default function useConverter() { return doc(firebaseDb, value); }; - const imageOrFileConverter = (urls: string): RowyFile[] => { + const imageOrFileConverter = (urls: any): RowyFile[] => { if (!urls) return []; + if (Array.isArray(urls)) return urls; if (typeof urls === "string") { return urls .split(",") From aa5e4c00296ebe6dc41746c8fe5eeb22c27238b6 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Mon, 13 Mar 2023 02:41:40 +0100 Subject: [PATCH 019/183] Made UI fixes on the color select context menu; Grid issue --- src/components/SelectColors/index.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/SelectColors/index.tsx b/src/components/SelectColors/index.tsx index 4adbc67b..1c225901 100644 --- a/src/components/SelectColors/index.tsx +++ b/src/components/SelectColors/index.tsx @@ -20,7 +20,7 @@ interface IColorSelect { } const ColorSelect: FC = ({ handleChange, initialValue }) => { - /* Get current */ + /* Get current theme */ const theme = useTheme(); const mode = theme.palette.mode; @@ -38,6 +38,8 @@ const ColorSelect: FC = ({ handleChange, initialValue }) => { cyan: palette.cyan, amber: palette.amber, lightGreen: palette.lightGreen, + lightBlue: palette.lightBlue, + violet: palette.violet, }); /* Hold the current state of a given option defaults to `gray` from the color palette */ @@ -117,9 +119,17 @@ const ColorSelect: FC = ({ handleChange, initialValue }) => { COLOURS - + {Object.keys(palettes).map((key: string, index: number) => ( - + From 60dd1874939d9445c7aedbda152a62fbe9f34420 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Wed, 15 Mar 2023 11:01:22 +0100 Subject: [PATCH 028/183] Implemented the generalized solution with backwards compatibility --- src/components/FormattedChip.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/FormattedChip.tsx b/src/components/FormattedChip.tsx index 876a5215..c7980427 100644 --- a/src/components/FormattedChip.tsx +++ b/src/components/FormattedChip.tsx @@ -10,7 +10,8 @@ const paletteColor = { no: paletteToMui(palette.aRed), } as const; -// TODO: Create a more generalised solution for this +// Switched to a more generalized solution - adding backwards compatibility to maintain [Yes, No, Maybe] colors even if no color is selected +// Modified by @devsgnr export default function FormattedChip(props: ChipProps) { const defaultColor = paletteToMui(palette.aGray); const { mode } = useTheme().palette; From cf25f7c4b65b0cc3a336cb5734a235fac193d437 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Wed, 15 Mar 2023 11:19:45 +0100 Subject: [PATCH 029/183] Fixed SideDrawerChips --- src/components/FormattedChip.tsx | 3 ++- src/components/fields/MultiSelect/SideDrawerField.tsx | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/FormattedChip.tsx b/src/components/FormattedChip.tsx index c7980427..97946dff 100644 --- a/src/components/FormattedChip.tsx +++ b/src/components/FormattedChip.tsx @@ -16,6 +16,7 @@ export default function FormattedChip(props: ChipProps) { const defaultColor = paletteToMui(palette.aGray); const { mode } = useTheme().palette; const fallback = { backgroundColor: defaultColor[mode] }; + const { sx, ...newProps } = props; const label = typeof props.label === "string" ? props.label.toLowerCase() : ""; @@ -24,7 +25,6 @@ export default function FormattedChip(props: ChipProps) { return ( ); } diff --git a/src/components/fields/MultiSelect/SideDrawerField.tsx b/src/components/fields/MultiSelect/SideDrawerField.tsx index 28ef41e4..8cdaf330 100644 --- a/src/components/fields/MultiSelect/SideDrawerField.tsx +++ b/src/components/fields/MultiSelect/SideDrawerField.tsx @@ -7,6 +7,8 @@ import FormattedChip from "@src/components/FormattedChip"; import { fieldSx } from "@src/components/SideDrawer/utils"; import { sanitiseValue } from "./utils"; +import { getColors } from "@src/components/fields/SingleSelect/Settings"; +import palette, { paletteToMui } from "@src/theme/palette"; export default function MultiSelect({ column, @@ -15,8 +17,9 @@ export default function MultiSelect({ onSubmit, disabled, }: ISideDrawerFieldProps) { + const defaultColor = paletteToMui(palette.aGray); const config = column.config ?? {}; - const colors = column.config?.colors ?? {}; + const colors = column.config?.colors ?? []; const { mode } = useTheme().palette; const handleDelete = (index: number) => () => { @@ -79,8 +82,7 @@ export default function MultiSelect({ onDelete={disabled ? undefined : handleDelete(i)} sx={{ backgroundColor: - colors[item.toLowerCase()] && - colors[item.toLowerCase()][mode], + getColors(colors, item)[mode] || defaultColor[mode], }} /> From 8ecae1060e93de7d23d9cfd8a432fddd9ff253b8 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Thu, 16 Mar 2023 17:34:51 +0100 Subject: [PATCH 030/183] Changed the short-circuit to a tenary on SingleSelect Chip --- src/components/fields/SingleSelect/DisplayCell.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/fields/SingleSelect/DisplayCell.tsx b/src/components/fields/SingleSelect/DisplayCell.tsx index 6c95922f..6b5e5911 100644 --- a/src/components/fields/SingleSelect/DisplayCell.tsx +++ b/src/components/fields/SingleSelect/DisplayCell.tsx @@ -35,8 +35,9 @@ export default function SingleSelect({ size="small" label={sanitiseValue(value)} sx={{ - backgroundColor: - (value && getColors(colors, value)[mode]) || defaultColor[mode], + backgroundColor: sanitiseValue(value) + ? getColors(colors, value)[mode] + : defaultColor[mode], }} /> )} From a35e5d652473f2cc02cf093ab9270d65b6a25fd3 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Thu, 16 Mar 2023 19:22:20 +0100 Subject: [PATCH 031/183] Stripped away null check and move them into getColors function - attempting to fix and error --- src/components/fields/MultiSelect/DisplayCell.tsx | 5 +---- src/components/fields/SingleSelect/DisplayCell.tsx | 6 +----- src/components/fields/SingleSelect/Settings.tsx | 5 ++++- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/components/fields/MultiSelect/DisplayCell.tsx b/src/components/fields/MultiSelect/DisplayCell.tsx index a8614651..457e1e28 100644 --- a/src/components/fields/MultiSelect/DisplayCell.tsx +++ b/src/components/fields/MultiSelect/DisplayCell.tsx @@ -7,7 +7,6 @@ import { ChevronDown } from "@src/assets/icons"; import { sanitiseValue } from "./utils"; import ChipList from "@src/components/Table/TableCell/ChipList"; import FormattedChip from "@src/components/FormattedChip"; -import palette, { paletteToMui } from "@src/theme/palette"; import { getColors, IColors, @@ -21,7 +20,6 @@ export default function MultiSelect({ rowHeight, column, }: IDisplayCellProps) { - const defaultColor = paletteToMui(palette.aGray); const colors: IColors[] = column?.config?.colors ?? []; const { mode } = useTheme().palette; @@ -43,8 +41,7 @@ export default function MultiSelect({ diff --git a/src/components/fields/SingleSelect/DisplayCell.tsx b/src/components/fields/SingleSelect/DisplayCell.tsx index 6b5e5911..498a7926 100644 --- a/src/components/fields/SingleSelect/DisplayCell.tsx +++ b/src/components/fields/SingleSelect/DisplayCell.tsx @@ -5,7 +5,6 @@ import { ChevronDown } from "@src/assets/icons"; import { useTheme } from "@mui/material"; import { sanitiseValue } from "./utils"; -import palette, { paletteToMui } from "@src/theme/palette"; import ChipList from "@src/components/Table/TableCell/ChipList"; import { getColors, IColors } from "./Settings"; @@ -17,7 +16,6 @@ export default function SingleSelect({ column, rowHeight, }: IDisplayCellProps) { - const defaultColor = paletteToMui(palette.aGray); const colors: IColors[] = column?.config?.colors ?? []; const { mode } = useTheme().palette; @@ -35,9 +33,7 @@ export default function SingleSelect({ size="small" label={sanitiseValue(value)} sx={{ - backgroundColor: sanitiseValue(value) - ? getColors(colors, value)[mode] - : defaultColor[mode], + backgroundColor: getColors(colors, value)[mode], }} /> )} diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index 45985da2..41766598 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -41,7 +41,10 @@ export interface IColors extends SelectColorThemeOptions { name: string; } -export const getColors = (list: IColors[], option: string) => { +export const getColors = ( + list: IColors[], + option: string +): SelectColorThemeOptions => { const defaultColor = paletteToMui(palette.aGray); const key = option.toLocaleLowerCase().replace(" ", "_").trim(); const color = list.find((opt: IColors) => opt.name === key); From d03e62fd565bb09f3e07c2fc786db9f45fc0bc9b Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Fri, 17 Mar 2023 10:36:16 +0100 Subject: [PATCH 032/183] Verified null checker --- src/components/fields/SingleSelect/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index 41766598..b8ee6b5f 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -48,7 +48,7 @@ export const getColors = ( const defaultColor = paletteToMui(palette.aGray); const key = option.toLocaleLowerCase().replace(" ", "_").trim(); const color = list.find((opt: IColors) => opt.name === key); - + // Null check in return return color || defaultColor; }; From aded1f795994547f68aebed94888c1034903ddf0 Mon Sep 17 00:00:00 2001 From: Han Tuerker <46192266+htuerker@users.noreply.github.com> Date: Mon, 20 Mar 2023 21:20:39 +0100 Subject: [PATCH 033/183] add set rowy run guard when update roles --- src/components/Settings/UserManagement/UserItem.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/Settings/UserManagement/UserItem.tsx b/src/components/Settings/UserManagement/UserItem.tsx index 3b9d2701..b4a53d5d 100644 --- a/src/components/Settings/UserManagement/UserItem.tsx +++ b/src/components/Settings/UserManagement/UserItem.tsx @@ -46,15 +46,18 @@ export default function UserItem({ const [value, setValue] = useState(Array.isArray(rolesProp) ? rolesProp : []); const allRoles = new Set(["ADMIN", ...(projectRoles ?? []), ...value]); + const hasRowyRun = !!projectSettings.rowyRunUrl; const handleSave = async () => { + if (!hasRowyRun) { + openRowyRunModal({ feature: "User Management" }); + return; + } try { if (!user) throw new Error("User is not defined"); if (JSON.stringify(value) === JSON.stringify(rolesProp)) return; - const loadingSnackbarId = enqueueSnackbar("Setting roles…"); - - const res = await rowyRun?.({ + const res = await rowyRun({ route: runRoutes.setUserRoles, body: { email: user!.email, roles: value }, }); @@ -91,7 +94,7 @@ export default function UserItem({ ); const handleDelete = async () => { - if (!projectSettings.rowyRunUrl) { + if (!hasRowyRun) { openRowyRunModal({ feature: "User Management" }); return; } From e7d9b291965db4e12a5517c2a0d78382e9c37bff Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Thu, 23 Mar 2023 16:35:15 +0100 Subject: [PATCH 034/183] exporterRoles table --- src/components/TableToolbar/TableToolbar.tsx | 20 +++++++++++++------- src/types/settings.d.ts | 1 + 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/TableToolbar/TableToolbar.tsx b/src/components/TableToolbar/TableToolbar.tsx index 7af0f6b9..398debc9 100644 --- a/src/components/TableToolbar/TableToolbar.tsx +++ b/src/components/TableToolbar/TableToolbar.tsx @@ -103,13 +103,19 @@ export default function TableToolbar() { )} - }> - openTableModal("export")} - icon={} - /> - + {(!projectSettings.exporterRoles || + projectSettings.exporterRoles.length === 0 || + userRoles.some((role) => + projectSettings.exporterRoles?.includes(role) + )) && ( + }> + openTableModal("export")} + icon={} + /> + + )} {userRoles.includes("ADMIN") && ( <>
{/* Spacer */} diff --git a/src/types/settings.d.ts b/src/types/settings.d.ts index 0f3b5761..0e762841 100644 --- a/src/types/settings.d.ts +++ b/src/types/settings.d.ts @@ -32,6 +32,7 @@ export type ProjectSettings = Partial<{ builder: string; terminal: string; }>; + exporterRoles?: string[]; }>; /** User info and settings */ From bb2fcf3b64cc2f2f287c7942376e44a4e4c3ab9b Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Tue, 28 Mar 2023 10:21:41 +0530 Subject: [PATCH 035/183] added Airtable import/upload files --- .../ImportAirtableWizard.tsx | 24 +++++- .../ImportCsvWizard/ImportCsvWizard.tsx | 14 ++-- .../ImportCsvWizard/useConverter.ts | 74 ++++++++++++++----- .../ImportCsvWizard/useUploadFileFromURL.tsx | 8 +- 4 files changed, 89 insertions(+), 31 deletions(-) diff --git a/src/components/TableModals/ImportAirtableWizard/ImportAirtableWizard.tsx b/src/components/TableModals/ImportAirtableWizard/ImportAirtableWizard.tsx index 7a6cee3f..193743c2 100644 --- a/src/components/TableModals/ImportAirtableWizard/ImportAirtableWizard.tsx +++ b/src/components/TableModals/ImportAirtableWizard/ImportAirtableWizard.tsx @@ -28,6 +28,8 @@ import { fieldParser } from "@src/components/TableModals/ImportAirtableWizard/ut import Step1Columns from "./Step1Columns"; import Step2NewColumns from "./Step2NewColumns"; import Step3Preview from "./Step3Preview"; +import useConverter from "@src/components/TableModals/ImportCsvWizard/useConverter"; +import useUploadFileFromURL from "@src/components/TableModals/ImportCsvWizard/useUploadFileFromURL"; export type AirtableConfig = { pairs: { fieldKey: string; columnKey: string }[]; @@ -65,6 +67,8 @@ export default function ImportAirtableWizard({ onClose }: ITableModalProps) { newColumns: [], documentId: "recordId", }); + const { needsUploadTypes, getConverter } = useConverter(); + const { addTask, runBatchedUpload, hasUploadJobs } = useUploadFileFromURL(); const updateConfig: IStepProps["updateConfig"] = useCallback((value) => { setConfig((prev) => { @@ -99,10 +103,24 @@ export default function ImportAirtableWizard({ onClose }: ITableModalProps) { const matchingColumn = columns[pair.columnKey] ?? find(config.newColumns, { key: pair.columnKey }); - const parser = fieldParser(matchingColumn.type); + const parser = + getConverter(matchingColumn.type) || fieldParser(matchingColumn.type); const value = parser ? parser(record.fields[pair.fieldKey]) : record.fields[pair.fieldKey]; + + if (needsUploadTypes(matchingColumn.type)) { + if (value && value.length > 0) { + addTask({ + docRef: { + path: `${tableSettings.collection}/${record.id}`, + id: record.id, + }, + fieldName: pair.columnKey, + files: value, + }); + } + } return config.documentId === "recordId" ? { ...a, [pair.columnKey]: value, _rowy_ref: { id: record.id } } : { ...a, [pair.columnKey]: value }; @@ -196,6 +214,10 @@ export default function ImportAirtableWizard({ onClose }: ITableModalProps) { `Imported ${Number(countRef.current).toLocaleString()} rows`, { variant: "success" } ); + + if (hasUploadJobs()) { + await runBatchedUpload(); + } } catch (e) { enqueueSnackbar((e as Error).message, { variant: "error" }); } finally { diff --git a/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx b/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx index 421fbc30..4f3f9a9e 100644 --- a/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx +++ b/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx @@ -37,7 +37,6 @@ import { import { ColumnConfig } from "@src/types/table"; import { getFieldProp } from "@src/components/fields"; import { analytics, logEvent } from "@src/analytics"; -import { FieldType } from "@src/constants/fields"; import { generateId } from "@src/utils/table"; import { isValidDocId } from "./utils"; import useUploadFileFromURL from "./useUploadFileFromURL"; @@ -50,8 +49,6 @@ export type CsvConfig = { documentIdCsvKey: string | null; }; -const needsUploadTypes = [FieldType.image, FieldType.file]; - export interface IStepProps { csvData: NonNullable; config: CsvConfig; @@ -71,8 +68,8 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { const theme = useTheme(); const isXs = useMediaQuery(theme.breakpoints.down("sm")); const snackbarProgressRef = useRef(); - const { addTask, runBatchUpload, askPermission } = useUploadFileFromURL(); - const { needsConverter, getConverter } = useConverter(); + const { addTask, runBatchedUpload } = useUploadFileFromURL(); + const { needsUploadTypes, needsConverter, getConverter } = useConverter(); const columns = useMemoValue(tableSchema.columns ?? {}, isEqual); @@ -147,7 +144,7 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { if (needsConverter(column.type)) { requiredConverts[index] = getConverter(column.type); // console.log({ needsUploadTypes }, column.type); - if (needsUploadTypes.includes(column.type)) { + if (needsUploadTypes(column.type)) { requiredUploads[column.fieldName + ""] = true; } } @@ -158,6 +155,7 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { config.pairs, getConverter, needsConverter, + needsUploadTypes, tableSchema.columns, ]); @@ -266,8 +264,8 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { `Imported ${Number(validRows.length).toLocaleString()} rows`, { variant: "success" } ); - if (Object.keys(requiredUploads).length && (await askPermission())) { - await runBatchUpload(); + if (Object.keys(requiredUploads).length > 0) { + await runBatchedUpload(); } } catch (e) { enqueueSnackbar((e as Error).message, { variant: "error" }); diff --git a/src/components/TableModals/ImportCsvWizard/useConverter.ts b/src/components/TableModals/ImportCsvWizard/useConverter.ts index 489a3127..58f597be 100644 --- a/src/components/TableModals/ImportCsvWizard/useConverter.ts +++ b/src/components/TableModals/ImportCsvWizard/useConverter.ts @@ -16,6 +16,9 @@ const needsConverter = (type: FieldType) => FieldType.geoPoint, ].includes(type); +const needsUploadTypes = (type: FieldType) => + [FieldType.image, FieldType.file].includes(type); + export default function useConverter() { const [firebaseDb] = useAtom(firebaseDbAtom, projectScope); @@ -35,27 +38,59 @@ export default function useConverter() { }; const imageOrFileConverter = (urls: any): RowyFile[] => { - if (!urls) return []; - if (Array.isArray(urls)) return urls; - if (typeof urls === "string") { - return urls - .split(",") - .map((url) => { - url = url.trim(); - if (url !== "") { - return { - downloadURL: url, - name: url.split("/").pop() || "", - lastModifiedTS: +new Date(), - type: "", - }; - } + try { + if (!urls) return []; + if (Array.isArray(urls)) { + return urls + .map((url) => { + if (typeof url === "string") { + url = url.trim(); + if (url !== "") { + return { + downloadURL: url, + name: url.split("/").pop() || "", + lastModifiedTS: +new Date(), + type: "", + }; + } + } else if (url && typeof url === "object" && url.downloadURL) { + return url; + } else { + if (url.url) { + return { + downloadURL: url.url, + name: url.filename || url.url.split("/").pop() || "", + lastModifiedTS: +new Date(), + type: "", + }; + } + } + return null; + }) + .filter((val) => val !== null) as RowyFile[]; + } + if (typeof urls === "string") { + return urls + .split(",") + .map((url) => { + url = url.trim(); + if (url !== "") { + return { + downloadURL: url, + name: url.split("/").pop() || "", + lastModifiedTS: +new Date(), + type: "", + }; + } - return null; - }) - .filter((val) => val !== null) as RowyFile[]; + return null; + }) + .filter((val) => val !== null) as RowyFile[]; + } + return []; + } catch (e) { + return []; } - return []; }; const geoPointConverter = (value: any) => { @@ -120,5 +155,6 @@ export default function useConverter() { imageOrFileConverter, getConverter, checkAndConvert, + needsUploadTypes, }; } diff --git a/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx b/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx index 0fae28fd..00d99b69 100644 --- a/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx +++ b/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx @@ -126,7 +126,7 @@ export default function useUploadFileFromURL() { [enqueueSnackbar] ); - const runBatchUpload = useCallback(async () => { + const runBatchedUpload = useCallback(async () => { if (!snackbarProgressId.current) { showProgress(jobs.current.length); } @@ -145,7 +145,7 @@ export default function useUploadFileFromURL() { await batchUpload(currentJobs); if (jobs.current.length > 0) { - await runBatchUpload(); + await runBatchedUpload(); } if (snackbarProgressId.current) { @@ -157,10 +157,12 @@ export default function useUploadFileFromURL() { jobs.current.push(job); }, []); + const hasUploadJobs = () => jobs.current.length > 0; return { addTask, - runBatchUpload, + runBatchedUpload, askPermission, + hasUploadJobs, }; } From 6ffa689fa6da9eb69c34d559bcc5bb7844688b21 Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Tue, 28 Mar 2023 12:08:57 +0200 Subject: [PATCH 036/183] firebase auth webhook --- .../WebhooksModal/Schemas/firebaseAuth.tsx | 67 +++++++++++++++++++ .../WebhooksModal/Schemas/index.ts | 3 +- .../TableModals/WebhooksModal/utils.tsx | 24 ++++++- 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/components/TableModals/WebhooksModal/Schemas/firebaseAuth.tsx diff --git a/src/components/TableModals/WebhooksModal/Schemas/firebaseAuth.tsx b/src/components/TableModals/WebhooksModal/Schemas/firebaseAuth.tsx new file mode 100644 index 00000000..21d2d3fd --- /dev/null +++ b/src/components/TableModals/WebhooksModal/Schemas/firebaseAuth.tsx @@ -0,0 +1,67 @@ +import { Typography, Link, TextField } from "@mui/material"; +import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; +import { TableSettings } from "@src/types/table"; +import { + IWebhook, + ISecret, +} from "@src/components/TableModals/WebhooksModal/utils"; + +export const webhookFirebaseAuth = { + name: "firebaseAuth", + parser: { + additionalVariables: null, + extraLibs: null, + template: ( + table: TableSettings + ) => `const firebaseAuthParser: Parser = async({req, db, ref, logging}) =>{ + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("firebaseAuthParser started") + /** + * This is a sample parser for firebase authentication + * creates a user document in the collection if it doesn't exist + // check if document exists, + const userDoc = await ref.doc(user.uid).get() + if(!userDoc.exists){ + await ref.doc(user.uid).set({email:user.email}) + } + */ + return; +};`, + }, + condition: { + additionalVariables: null, + extraLibs: null, + template: ( + table: TableSettings + ) => `const condition: Condition = async({ref, req, db, logging}) => { + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("condition started") + + return true; + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`, + }, + auth: ( + webhookObject: IWebhook, + setWebhookObject: (w: IWebhook) => void, + secrets: ISecret + ) => { + return ( + <> + + For Firebase authentication, you need to include the following header + in your request: +
+ Authorization: Bear ACCESS_TOKEN +
+ + + Once enabled requests without a valid token will return{" "} + 401 response. + + + ); + }, +}; + +export default webhookFirebaseAuth; diff --git a/src/components/TableModals/WebhooksModal/Schemas/index.ts b/src/components/TableModals/WebhooksModal/Schemas/index.ts index fff6341f..889f31ab 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/index.ts +++ b/src/components/TableModals/WebhooksModal/Schemas/index.ts @@ -1,7 +1,8 @@ import basic from "./basic"; +import firebaseAuth from "./firebaseAuth"; import typeform from "./typeform"; import sendgrid from "./sendgrid"; import webform from "./webform"; import stripe from "./stripe"; -export { basic, typeform, sendgrid, webform, stripe }; +export { basic, typeform, sendgrid, webform, stripe, firebaseAuth }; diff --git a/src/components/TableModals/WebhooksModal/utils.tsx b/src/components/TableModals/WebhooksModal/utils.tsx index cefc6c2f..e43c747b 100644 --- a/src/components/TableModals/WebhooksModal/utils.tsx +++ b/src/components/TableModals/WebhooksModal/utils.tsx @@ -1,12 +1,20 @@ import { TableSettings } from "@src/types/table"; import { generateId } from "@src/utils/table"; -import { typeform, basic, sendgrid, webform, stripe } from "./Schemas"; +import { + typeform, + basic, + sendgrid, + webform, + stripe, + firebaseAuth, +} from "./Schemas"; export const webhookTypes = [ "basic", "typeform", "sendgrid", "webform", + "firebaseAuth", //"shopify", //"twitter", "stripe", @@ -35,6 +43,18 @@ export const parserExtraLibs = [ send: (v:any)=>void; sendStatus: (status:number)=>void }; + user: { + uid: string; + email: string; + email_verified: boolean; + exp: number; + iat: number; + iss: string; + aud: string; + auth_time: number; + phone_number: string; + picture: string; + } | undefined; logging: RowyLogging; auth:firebaseauth.BaseAuth; storage:firebasestorage.Storage; @@ -71,6 +91,7 @@ export type WebhookType = typeof webhookTypes[number]; export const webhookNames: Record = { sendgrid: "SendGrid", typeform: "Typeform", + firebaseAuth: "Firebase Auth", //github:"GitHub", // shopify: "Shopify", // twitter: "Twitter", @@ -110,6 +131,7 @@ export const webhookSchemas = { sendgrid, webform, stripe, + firebaseAuth, }; export function emptyWebhookObject( From 23fd0c77bcaff041fbced8d40de01a88084c2bb4 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Wed, 29 Mar 2023 11:10:45 +1300 Subject: [PATCH 037/183] fix code template typo --- src/components/TableModals/ExtensionsModal/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/TableModals/ExtensionsModal/utils.ts b/src/components/TableModals/ExtensionsModal/utils.ts index 27281625..4a2cbbf1 100644 --- a/src/components/TableModals/ExtensionsModal/utils.ts +++ b/src/components/TableModals/ExtensionsModal/utils.ts @@ -132,8 +132,8 @@ const extensionBodyTemplate = { return({ fieldsToSync: [], // a list of string of column names row: row, // object of data to sync, usually the row itself - index: "", // algolia index to sync to - objectID: ref.id, // algolia object ID, ref.id is one possible choice + index: "", // meili search index to sync to + objectID: ref.id, // meili search object ID, ref.id is one possible choice }) // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, @@ -144,8 +144,8 @@ const extensionBodyTemplate = { return ({ fieldsToSync: [], // a list of string of column names row: row, // object of data to sync, usually the row itself - index: "", // algolia index to sync to - objectID: ref.id, // algolia object ID, ref.id is one possible choice + index: "", // bigquery dataset to sync to + objectID: ref.id, // bigquery object ID, ref.id is one possible choice }) // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, From 02eec0050b31b67376b59069e8cdc729fe6f610e Mon Sep 17 00:00:00 2001 From: Han Tuerker <46192266+htuerker@users.noreply.github.com> Date: Wed, 29 Mar 2023 12:34:25 +0300 Subject: [PATCH 038/183] update table schema with id customization --- src/atoms/projectScope/ui.ts | 4 ---- .../Table/ContextMenu/MenuContents.tsx | 4 ++-- .../Table/FinalColumn/FinalColumn.tsx | 11 +++++---- src/components/TableToolbar/AddRow.tsx | 23 ++++++++++++------- src/types/table.d.ts | 3 +++ 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/atoms/projectScope/ui.ts b/src/atoms/projectScope/ui.ts index 6f9d66d8..48af64b0 100644 --- a/src/atoms/projectScope/ui.ts +++ b/src/atoms/projectScope/ui.ts @@ -147,10 +147,6 @@ export const tableSettingsDialogSchemaAtom = atom(async (get) => { /** Open the Get Started checklist from anywhere */ export const getStartedChecklistAtom = atom(false); -/** Persist the state of the add row ID type */ -export const tableAddRowIdTypeAtom = atomWithStorage< - "decrement" | "random" | "custom" ->("__ROWY__ADD_ROW_ID_TYPE", "decrement"); /** Persist when the user dismissed the row out of order warning */ export const tableOutOfOrderDismissedAtom = atomWithStorage( "__ROWY__OUT_OF_ORDER_TOOLTIP_DISMISSED", diff --git a/src/components/Table/ContextMenu/MenuContents.tsx b/src/components/Table/ContextMenu/MenuContents.tsx index 88a973c2..6030b89a 100644 --- a/src/components/Table/ContextMenu/MenuContents.tsx +++ b/src/components/Table/ContextMenu/MenuContents.tsx @@ -21,7 +21,6 @@ import { projectIdAtom, userRolesAtom, altPressAtom, - tableAddRowIdTypeAtom, confirmDialogAtom, } from "@src/atoms/projectScope"; import { @@ -45,7 +44,6 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { const [projectId] = useAtom(projectIdAtom, projectScope); const [userRoles] = useAtom(userRolesAtom, projectScope); const [altPress] = useAtom(altPressAtom, projectScope); - const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, projectScope); const confirm = useSetAtom(confirmDialogAtom, projectScope); const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); @@ -59,6 +57,8 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { tableScope ); + const addRowIdType = tableSchema.idType || "decrement"; + if (!tableSchema.columns || !selectedCell) return null; const selectedColumn = tableSchema.columns[selectedCell.columnKey]; diff --git a/src/components/Table/FinalColumn/FinalColumn.tsx b/src/components/Table/FinalColumn/FinalColumn.tsx index 28b584e6..e6023e4d 100644 --- a/src/components/Table/FinalColumn/FinalColumn.tsx +++ b/src/components/Table/FinalColumn/FinalColumn.tsx @@ -10,7 +10,6 @@ import MenuIcon from "@mui/icons-material/MoreHoriz"; import { projectScope, userRolesAtom, - tableAddRowIdTypeAtom, altPressAtom, confirmDialogAtom, } from "@src/atoms/projectScope"; @@ -20,6 +19,7 @@ import { addRowAtom, deleteRowAtom, contextMenuTargetAtom, + tableSchemaAtom, } from "@src/atoms/tableScope"; export const FinalColumn = memo(function FinalColumn({ @@ -27,16 +27,19 @@ export const FinalColumn = memo(function FinalColumn({ focusInsideCell, }: IRenderedTableCellProps) { const [userRoles] = useAtom(userRolesAtom, projectScope); - const [addRowIdType] = useAtom(tableAddRowIdTypeAtom, projectScope); - const confirm = useSetAtom(confirmDialogAtom, projectScope); - const [tableSettings] = useAtom(tableSettingsAtom, tableScope); + const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const addRow = useSetAtom(addRowAtom, tableScope); const deleteRow = useSetAtom(deleteRowAtom, tableScope); const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope); + const confirm = useSetAtom(confirmDialogAtom, projectScope); const [altPress] = useAtom(altPressAtom, projectScope); + + const addRowIdType = tableSchema.idType || "decrement"; + const handleDelete = () => deleteRow(row.original._rowy_ref.path); + const handleDuplicate = () => { addRow({ row: row.original, diff --git a/src/components/TableToolbar/AddRow.tsx b/src/components/TableToolbar/AddRow.tsx index c6372865..0f27da9e 100644 --- a/src/components/TableToolbar/AddRow.tsx +++ b/src/components/TableToolbar/AddRow.tsx @@ -16,33 +16,40 @@ import { ChevronDown as ArrowDropDownIcon, } from "@src/assets/icons"; -import { - projectScope, - userRolesAtom, - tableAddRowIdTypeAtom, -} from "@src/atoms/projectScope"; +import { projectScope, userRolesAtom } from "@src/atoms/projectScope"; import { tableScope, tableSettingsAtom, tableFiltersAtom, tableSortsAtom, addRowAtom, + tableSchemaAtom, + updateTableSchemaAtom, } from "@src/atoms/tableScope"; +import { TableIdType } from "@src/types/table"; export default function AddRow() { const [userRoles] = useAtom(userRolesAtom, projectScope); const [tableSettings] = useAtom(tableSettingsAtom, tableScope); + const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const [tableFilters] = useAtom(tableFiltersAtom, tableScope); const [tableSorts] = useAtom(tableSortsAtom, tableScope); + const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope); const addRow = useSetAtom(addRowAtom, tableScope); - const [idType, setIdType] = useAtom(tableAddRowIdTypeAtom, projectScope); - const anchorEl = useRef(null); const [open, setOpen] = useState(false); const [openIdModal, setOpenIdModal] = useState(false); + const idType = tableSchema.idType || "decrement"; const forceRandomId = tableFilters.length > 0 || tableSorts.length > 0; + const handleSetIdType = async (idType: TableIdType) => { + // TODO(han): refactor atom - error handler + await updateTableSchema!({ + idType, + }); + }; + const handleClick = () => { if (idType === "random" || (forceRandomId && idType === "decrement")) { addRow({ @@ -118,7 +125,7 @@ export default function AddRow() { label="Row add position" style={{ display: "none" }} value={forceRandomId && idType === "decrement" ? "random" : idType} - onChange={(e) => setIdType(e.target.value as typeof idType)} + onChange={(e) => handleSetIdType(e.target.value as typeof idType)} MenuProps={{ anchorEl: anchorEl.current, MenuListProps: { "aria-labelledby": "add-row-menu-button" }, diff --git a/src/types/table.d.ts b/src/types/table.d.ts index a0570542..8de72284 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -95,9 +95,12 @@ export type TableSettings = { readOnly?: boolean; }; +export type TableIdType = "decrement" | "random" | "custom"; + /** Table schema document loaded when table or table settings dialog is open */ export type TableSchema = { columns?: Record; + idType?: TableIdType; rowHeight?: number; filters?: TableFilter[]; filtersOverridable?: boolean; From 41d8feb84b0243db15d731b95d9b8e42bc7a598e Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Thu, 30 Mar 2023 15:03:08 +0530 Subject: [PATCH 039/183] =?UTF-8?q?=E2=9C=A8=20feat(App.tsx):=20add=20rout?= =?UTF-8?q?e=20for=20ProvidedArraySubTablePage=20at=20/array-sub-table/:do?= =?UTF-8?q?cPath/:subTableKey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/App.tsx b/src/App.tsx index ea283a01..d169bb1e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,6 +23,7 @@ import useKeyPressWithAtom from "@src/hooks/useKeyPressWithAtom"; import TableGroupRedirectPage from "./pages/TableGroupRedirectPage"; import SignOutPage from "@src/pages/Auth/SignOutPage"; +import ProvidedArraySubTablePage from "./pages/Table/ProvidedArraySubTablePage"; // prettier-ignore const AuthPage = lazy(() => import("@src/pages/Auth/AuthPage" /* webpackChunkName: "AuthPage" */)); @@ -134,6 +135,27 @@ export default function App() { } /> + + } /> + + + + } + > + + + } + /> + From a91b75199550b0bf446cde5d6a6d76e798aeecf0 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sat, 1 Apr 2023 02:37:14 +1300 Subject: [PATCH 040/183] fix export crash due to toDate issue --- src/components/fields/Date/index.tsx | 9 +++++++-- src/components/fields/DateTime/index.tsx | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/fields/Date/index.tsx b/src/components/fields/Date/index.tsx index a8989d48..8a126c22 100644 --- a/src/components/fields/Date/index.tsx +++ b/src/components/fields/Date/index.tsx @@ -35,8 +35,13 @@ export const config: IFieldConfig = { filter: { operators: filterOperators, valueFormatter }, settings: Settings, csvImportParser: (value, config) => parse(value, DATE_FORMAT, new Date()), - csvExportFormatter: (value: any, config?: any) => - format(value.toDate(), DATE_FORMAT), + csvExportFormatter: (value: any, config?: any) => { + if (typeof value === "number") { + return format(new Date(value), DATE_FORMAT); + } else { + return format(value.toDate(), DATE_FORMAT); + } + }, }; export default config; diff --git a/src/components/fields/DateTime/index.tsx b/src/components/fields/DateTime/index.tsx index 38e0e412..65c77ff5 100644 --- a/src/components/fields/DateTime/index.tsx +++ b/src/components/fields/DateTime/index.tsx @@ -47,8 +47,13 @@ export const config: IFieldConfig = { }, settings: Settings, csvImportParser: (value) => new Date(value), - csvExportFormatter: (value: any, config?: any) => - format(value.toDate(), DATE_TIME_FORMAT), + csvExportFormatter: (value: any, config?: any) => { + if (typeof value === "number") { + return format(new Date(value), config?.format || DATE_TIME_FORMAT); + } else { + return format(value.toDate(), config?.format || DATE_TIME_FORMAT); + } + }, }; export default config; From f66e8b988072c139b52ce9300fc2307a7ba4033f Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sat, 1 Apr 2023 02:52:00 +1300 Subject: [PATCH 041/183] fix commit history --- src/components/fields/DateTime/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/fields/DateTime/index.tsx b/src/components/fields/DateTime/index.tsx index 65c77ff5..ea0305c8 100644 --- a/src/components/fields/DateTime/index.tsx +++ b/src/components/fields/DateTime/index.tsx @@ -49,9 +49,9 @@ export const config: IFieldConfig = { csvImportParser: (value) => new Date(value), csvExportFormatter: (value: any, config?: any) => { if (typeof value === "number") { - return format(new Date(value), config?.format || DATE_TIME_FORMAT); + return format(new Date(value), DATE_TIME_FORMAT); } else { - return format(value.toDate(), config?.format || DATE_TIME_FORMAT); + return format(value.toDate(), DATE_TIME_FORMAT); } }, }; From cfde5886c43c519ada8a3730d86ec2f01446d845 Mon Sep 17 00:00:00 2001 From: Harini Janakiraman Date: Wed, 5 Apr 2023 19:08:08 +1000 Subject: [PATCH 042/183] Update externalLinks.ts --- src/constants/externalLinks.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/constants/externalLinks.ts b/src/constants/externalLinks.ts index a2b64de4..e501be4b 100644 --- a/src/constants/externalLinks.ts +++ b/src/constants/externalLinks.ts @@ -65,8 +65,10 @@ const WIKI_PATHS = { webhooks: "/webhooks", importAirtable: "/import-export-data/import-airtable", - importAirtableApiKey: "/import-export-data/import-airtable#api-key", - importAirtableTableUrl: "/import-export-data/import-airtable#table-url", + importAirtableApiKey: + "/import-export-data/import-airtable#retrieving-the-airtable-api-key", + importAirtableTableUrl: + "/import-export-data/import-airtable#obtaining-the-airtable-table-url", cloudLogs: "/cloud-logs", }; export const WIKI_LINKS = mapValues( From 60c42d099bef7d7a8851057cc19014c6e3bd0592 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Thu, 6 Apr 2023 11:17:47 +0530 Subject: [PATCH 043/183] fixed - BUG: While changing table name/description --- src/components/TableSettingsDialog/TableName.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/TableSettingsDialog/TableName.tsx b/src/components/TableSettingsDialog/TableName.tsx index 989f6716..66a5c9d5 100644 --- a/src/components/TableSettingsDialog/TableName.tsx +++ b/src/components/TableSettingsDialog/TableName.tsx @@ -12,16 +12,21 @@ export interface ITableNameProps extends IShortTextComponentProps { export default function TableName({ watchedField, ...props }: ITableNameProps) { const { - field: { onChange }, + field: { onChange, value }, useFormMethods: { control }, disabled, } = props; const watchedValue = useWatch({ control, name: watchedField } as any); useEffect(() => { - if (!disabled && typeof watchedValue === "string" && !!watchedValue) - onChange(startCase(watchedValue)); - }, [watchedValue, disabled]); + if (!disabled) { + if (typeof value === "string" && value.trim() !== "") { + onChange(value); + } else if (typeof watchedValue === "string" && !!watchedValue) { + onChange(startCase(watchedValue)); + } + } + }, [watchedValue, disabled, onChange, value]); return ; } From 4f7fbb791910f12470f196bfbdb3d4a74f27f9bb Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Thu, 6 Apr 2023 11:44:32 +0530 Subject: [PATCH 044/183] fixed - Visual bug: in dropdown menu(Connector field) --- src/components/fields/Connector/Select/PopupContents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fields/Connector/Select/PopupContents.tsx b/src/components/fields/Connector/Select/PopupContents.tsx index 0bd770cb..45955cf8 100644 --- a/src/components/fields/Connector/Select/PopupContents.tsx +++ b/src/components/fields/Connector/Select/PopupContents.tsx @@ -108,7 +108,7 @@ export default function PopupContents({ onChange={(e) => setQuery(e.target.value)} fullWidth variant="filled" - label="Search items" + // label="Search items" hiddenLabel placeholder="Search items" InputProps={{ From bd7ef2eb3b11f0b6a613687cc4bed1825ba8831e Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Thu, 6 Apr 2023 12:51:31 +0530 Subject: [PATCH 045/183] fixed checked icon getting inverted --- src/components/GetStartedChecklist/GetStartedChecklist.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/GetStartedChecklist/GetStartedChecklist.tsx b/src/components/GetStartedChecklist/GetStartedChecklist.tsx index ad6ba46a..e15b91bc 100644 --- a/src/components/GetStartedChecklist/GetStartedChecklist.tsx +++ b/src/components/GetStartedChecklist/GetStartedChecklist.tsx @@ -80,6 +80,9 @@ export default function GetStartedChecklist({ marginRight: `max(env(safe-area-inset-right), 8px)`, width: 360, }, + ".MuiStepLabel-iconContainer.Mui-active svg": { + transform: "rotate(0deg) !important", + }, }, ]} > From bc815afe74dc543c58a5f970406701b539128597 Mon Sep 17 00:00:00 2001 From: Yaman Katby Date: Thu, 6 Apr 2023 17:16:26 +0300 Subject: [PATCH 046/183] update the connector field default value --- src/components/fields/Connector/utils.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/fields/Connector/utils.ts b/src/components/fields/Connector/utils.ts index 921997ad..ecbe3f52 100644 --- a/src/components/fields/Connector/utils.ts +++ b/src/components/fields/Connector/utils.ts @@ -11,16 +11,19 @@ export const replacer = (data: any) => (m: string, key: string) => { return get(data, objKey, defaultValue); }; -export const baseFunction = `const connectorFn: Connector = async ({query, row, user, logging}) => { - // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY - logging.log("connectorFn started") - - // Import any NPM package needed - // const lodash = require('lodash'); - - return []; - // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY -};`; +export const baseFunction = `// Import any NPM package needed +import lodash from "lodash"; + +const connector: Connector = async ({ query, row, user, logging }) => { + logging.log("connector started"); + // return [ + // { id: "a", name: "Apple" }, + // { id: "b", name: "Banana" }, + // ]; +}; + +export default connector; +`; export const getLabel = (config: any, row: TableRow) => { if (!config.labelFormatter) { From 0bc104d07e8c72404f169c88ed83923b82aebe98 Mon Sep 17 00:00:00 2001 From: Yaman Katby Date: Thu, 6 Apr 2023 17:18:59 +0300 Subject: [PATCH 047/183] convert the import into comment --- src/components/fields/Connector/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fields/Connector/utils.ts b/src/components/fields/Connector/utils.ts index ecbe3f52..88a90ed2 100644 --- a/src/components/fields/Connector/utils.ts +++ b/src/components/fields/Connector/utils.ts @@ -12,7 +12,7 @@ export const replacer = (data: any) => (m: string, key: string) => { }; export const baseFunction = `// Import any NPM package needed -import lodash from "lodash"; +// import _ from "lodash"; const connector: Connector = async ({ query, row, user, logging }) => { logging.log("connector started"); From 13c1b693864d419998d9ccfc7b0b778078bdff55 Mon Sep 17 00:00:00 2001 From: Yaman Katby Date: Thu, 6 Apr 2023 17:26:03 +0300 Subject: [PATCH 048/183] update the action and the redo code templates --- src/components/fields/Action/templates.ts | 110 +++++++++++----------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/src/components/fields/Action/templates.ts b/src/components/fields/Action/templates.ts index 46b0a0b5..294eab3e 100644 --- a/src/components/fields/Action/templates.ts +++ b/src/components/fields/Action/templates.ts @@ -1,67 +1,67 @@ -export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { - // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY - logging.log("action started") - - // Import any NPM package needed - // const lodash = require('lodash'); - - // Example: - /* - const authToken = await rowy.secrets.get("service") - try { - const resp = await fetch('https://example.com/api/v1/users/'+ref.id,{ - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Authorization': authToken - }, - body: JSON.stringify(row) - }) - return { - success: true, - message: 'User updated successfully on example service', - status: "upto date" - } - } catch (error) { - return { - success: false, - message: 'User update failed on example service', - } - } - */ - // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY -}`; +export const RUN_ACTION_TEMPLATE = `// Import any NPM package needed +// import _ from "lodash"; + +const action: Action = async ({ row, ref, db, storage, auth, actionParams, user, logging }) => { + logging.log("action started"); -export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { - // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY - logging.log("action started") - - // Import any NPM package needed - // const lodash = require('lodash'); - - // Example: /* - const authToken = await rowy.secrets.get("service") + // Example: + const authToken = await rowy.secrets.get("service"); try { - const resp = await fetch('https://example.com/api/v1/users/'+ref.id,{ - method: 'DELETE', + const resp = await fetch("https://example.com/api/v1/users/" + ref.id, { + method: "PUT", headers: { - 'Content-Type': 'application/json', - 'Authorization': authToken + "Content-Type": "application/json", + Authorization: authToken, }, - body: JSON.stringify(row) - }) + body: JSON.stringify(row), + }); return { success: true, - message: 'User deleted successfully on example service', - status: null - } + message: "User updated successfully on example service", + status: "upto date", + }; } catch (error) { return { success: false, - message: 'User delete failed on example service', - } + message: "User update failed on example service", + }; } */ - // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY -}`; +}; + +export default action; +`; + +export const UNDO_ACTION_TEMPLATE = `// Import any NPM package needed +// import _ from "lodash"; + +const action: Action = async ({ row, ref, db, storage, auth, actionParams, user, logging }) => { + logging.log("action started"); + + /* + // Example: + const authToken = await rowy.secrets.get("service"); + try { + const resp = await fetch("https://example.com/api/v1/users/" + ref.id, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: authToken, + }, + body: JSON.stringify(row), + }); + return { + success: true, + message: "User deleted successfully on example service", + status: null, + }; + } catch (error) { + return { + success: false, + message: "User delete failed on example service", + }; + } + */ +}; +`; From e841c994e5a174965a154613c20afd2e44393b15 Mon Sep 17 00:00:00 2001 From: Yaman Katby Date: Thu, 6 Apr 2023 17:30:21 +0300 Subject: [PATCH 049/183] Update the derivative default template --- src/components/fields/Derivative/Settings.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/fields/Derivative/Settings.tsx b/src/components/fields/Derivative/Settings.tsx index 1c2486d2..0ca5dd51 100644 --- a/src/components/fields/Derivative/Settings.tsx +++ b/src/components/fields/Derivative/Settings.tsx @@ -75,18 +75,19 @@ export default function Settings({ ${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")} // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }` - : `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{ - // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY - logging.log("derivative started") - - // Import any NPM package needed - // const lodash = require('lodash'); - + : `// Import any NPM package needed +// import _ from "lodash"; + +const derivative: Derivative = async ({ row, ref, db, storage, auth, logging }) => { + logging.log("derivative started"); + // Example: // const sum = row.a + row.b; // return sum; - // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY -}`; +}; + +export default derivative; +`; return ( <> From 8254623c2cbafa8fb7e56927c25554cf1d5c2479 Mon Sep 17 00:00:00 2001 From: Yaman Katby Date: Thu, 6 Apr 2023 17:38:40 +0300 Subject: [PATCH 050/183] fix redo action template --- src/components/fields/Action/templates.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/fields/Action/templates.ts b/src/components/fields/Action/templates.ts index 294eab3e..3a9baf7a 100644 --- a/src/components/fields/Action/templates.ts +++ b/src/components/fields/Action/templates.ts @@ -64,4 +64,6 @@ const action: Action = async ({ row, ref, db, storage, auth, actionParams, user, } */ }; + +export default action; `; From d0dd89b00b177102c3951cf7040289907a6f0498 Mon Sep 17 00:00:00 2001 From: Yaman Katby Date: Thu, 6 Apr 2023 17:38:53 +0300 Subject: [PATCH 051/183] update default value function template --- .../ColumnConfigModal/DefaultValueInput.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx index 5146c387..b5e3dc05 100644 --- a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx +++ b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx @@ -61,15 +61,19 @@ function CodeEditor({ type, column, handleChange }: ICodeEditorProps) { // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`; } else { - dynamicValueFn = `const dynamicValueFn: DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ - // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY - logging.log("dynamicValueFn started") - + dynamicValueFn = `// Import any NPM package needed +// import _ from "lodash"; + +const defaultValue: DefaultValue = async ({ row, ref, db, storage, auth, logging }) => { + logging.log("dynamicValueFn started"); + // Example: generate random hex color // const color = "#" + Math.floor(Math.random() * 16777215).toString(16); // return color; - // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY -}`; +}; + +export default defaultValue; +`; } return ( From 5bf33f097c8f4998ca80e8eab2ec5e9f4d0da688 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Mon, 10 Apr 2023 15:18:34 +0800 Subject: [PATCH 052/183] optimise endpoint call for listSecrets --- src/atoms/projectScope/project.ts | 13 +++ .../CodeEditor/useMonacoCustomizations.ts | 47 ++++----- .../WebhooksModal/Schemas/basic.tsx | 11 +-- .../WebhooksModal/Schemas/firebaseAuth.tsx | 11 +-- .../WebhooksModal/Schemas/sendgrid.tsx | 11 +-- .../WebhooksModal/Schemas/stripe.tsx | 99 ++++++++++++------- .../WebhooksModal/Schemas/typeform.tsx | 11 +-- .../WebhooksModal/Schemas/webform.tsx | 11 +-- .../TableModals/WebhooksModal/Step1Auth.tsx | 35 +------ .../TableModals/WebhooksModal/utils.tsx | 6 -- .../useTableFunctions.ts | 41 ++++++++ 11 files changed, 147 insertions(+), 149 deletions(-) diff --git a/src/atoms/projectScope/project.ts b/src/atoms/projectScope/project.ts index 222ee942..0cd213fb 100644 --- a/src/atoms/projectScope/project.ts +++ b/src/atoms/projectScope/project.ts @@ -133,3 +133,16 @@ export const FunctionsIndexAtom = atom([]); export const updateFunctionAtom = atom< UpdateCollectionDocFunction | undefined >(undefined); + +export interface ISecretNames { + loading: boolean; + secretNames: null | string[]; +} + +export const secretNamesAtom = atom({ + loading: true, + secretNames: null, +}); +export const updateSecretNamesAtom = atom< + ((clearSecretNames?: boolean) => Promise) | undefined +>(undefined); diff --git a/src/components/CodeEditor/useMonacoCustomizations.ts b/src/components/CodeEditor/useMonacoCustomizations.ts index d145d72e..012bf4c9 100644 --- a/src/components/CodeEditor/useMonacoCustomizations.ts +++ b/src/components/CodeEditor/useMonacoCustomizations.ts @@ -19,8 +19,7 @@ import firebaseStorageDefs from "!!raw-loader!./firebaseStorage.d.ts"; import utilsDefs from "!!raw-loader!./utils.d.ts"; import rowyUtilsDefs from "!!raw-loader!./rowy.d.ts"; import extensionsDefs from "!!raw-loader!./extensions.d.ts"; -import { runRoutes } from "@src/constants/runRoutes"; -import { rowyRunAtom, projectScope } from "@src/atoms/projectScope"; +import { projectScope, secretNamesAtom } from "@src/atoms/projectScope"; import { getFieldProp } from "@src/components/fields"; export interface IUseMonacoCustomizationsProps { @@ -53,8 +52,8 @@ export default function useMonacoCustomizations({ const theme = useTheme(); const monaco = useMonaco(); const [tableRows] = useAtom(tableRowsAtom, tableScope); - const [rowyRun] = useAtom(rowyRunAtom, projectScope); const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + const [secretNames] = useAtom(secretNamesAtom, projectScope); useEffect(() => { return () => { @@ -206,26 +205,6 @@ export default function useMonacoCustomizations({ //} }; - const setSecrets = async () => { - // set secret options - try { - const listSecrets = await rowyRun({ - route: runRoutes.listSecrets, - }); - const secretsDef = `type SecretNames = ${listSecrets - .map((secret: string) => `"${secret}"`) - .join(" | ")} - enum secrets { - ${listSecrets - .map((secret: string) => `${secret} = "${secret}"`) - .join("\n")} - } - `; - monaco?.languages.typescript.javascriptDefaults.addExtraLib(secretsDef); - } catch (error) { - console.error("Could not set secret definitions: ", error); - } - }; //TODO: types const setBaseDefinitions = () => { const rowDefinition = @@ -275,14 +254,24 @@ export default function useMonacoCustomizations({ } catch (error) { console.error("Could not set basic", error); } - // set available secrets from secretManager - try { - setSecrets(); - } catch (error) { - console.error("Could not set secrets: ", error); - } }, [monaco, tableColumnsOrdered]); + useEffect(() => { + if (!monaco) return; + if (secretNames.loading) return; + if (!secretNames.secretNames) return; + const secretsDef = `type SecretNames = ${secretNames.secretNames + .map((secret: string) => `"${secret}"`) + .join(" | ")} + enum secrets { + ${secretNames.secretNames + .map((secret: string) => `${secret} = "${secret}"`) + .join("\n")} + } + `; + monaco?.languages.typescript.javascriptDefaults.addExtraLib(secretsDef); + }, [monaco, secretNames]); + let boxSx: SystemStyleObject = { minWidth: 400, minHeight, diff --git a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx index dfb3e90f..1ee073db 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx @@ -1,10 +1,7 @@ import { Typography } from "@mui/material"; import WarningIcon from "@mui/icons-material/WarningAmber"; import { TableSettings } from "@src/types/table"; -import { - IWebhook, - ISecret, -} from "@src/components/TableModals/WebhooksModal/utils"; +import { IWebhook } from "@src/components/TableModals/WebhooksModal/utils"; const requestType = [ "declare type WebHookRequest {", @@ -101,11 +98,7 @@ export const webhookBasic = { // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, }, - auth: ( - webhookObject: IWebhook, - setWebhookObject: (w: IWebhook) => void, - secrets: ISecret - ) => { + Auth: (webhookObject: IWebhook, setWebhookObject: (w: IWebhook) => void) => { return ( diff --git a/src/components/TableModals/WebhooksModal/Schemas/firebaseAuth.tsx b/src/components/TableModals/WebhooksModal/Schemas/firebaseAuth.tsx index 21d2d3fd..217eb095 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/firebaseAuth.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/firebaseAuth.tsx @@ -1,10 +1,7 @@ import { Typography, Link, TextField } from "@mui/material"; import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; import { TableSettings } from "@src/types/table"; -import { - IWebhook, - ISecret, -} from "@src/components/TableModals/WebhooksModal/utils"; +import { IWebhook } from "@src/components/TableModals/WebhooksModal/utils"; export const webhookFirebaseAuth = { name: "firebaseAuth", @@ -41,11 +38,7 @@ export const webhookFirebaseAuth = { // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, }, - auth: ( - webhookObject: IWebhook, - setWebhookObject: (w: IWebhook) => void, - secrets: ISecret - ) => { + Auth: (webhookObject: IWebhook, setWebhookObject: (w: IWebhook) => void) => { return ( <> diff --git a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx index ea24dabe..e23bef18 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx @@ -1,10 +1,7 @@ import { Typography, Link, TextField } from "@mui/material"; import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; import { TableSettings } from "@src/types/table"; -import { - IWebhook, - ISecret, -} from "@src/components/TableModals/WebhooksModal/utils"; +import { IWebhook } from "@src/components/TableModals/WebhooksModal/utils"; export const webhookSendgrid = { name: "SendGrid", @@ -51,11 +48,7 @@ export const webhookSendgrid = { // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, }, - auth: ( - webhookObject: IWebhook, - setWebhookObject: (w: IWebhook) => void, - secrets: ISecret - ) => { + Auth: (webhookObject: IWebhook, setWebhookObject: (w: IWebhook) => void) => { return ( <> diff --git a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx index bbf8c5a5..9c34383f 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx @@ -1,14 +1,18 @@ -import { Typography, Link, TextField, Alert } from "@mui/material"; +import { useAtom } from "jotai"; +import { Typography, Link, TextField, Alert, Box } from "@mui/material"; import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; import { TableSettings } from "@src/types/table"; +import { IWebhook } from "@src/components/TableModals/WebhooksModal/utils"; import { - IWebhook, - ISecret, -} from "@src/components/TableModals/WebhooksModal/utils"; + projectScope, + secretNamesAtom, + updateSecretNamesAtom, +} from "@src/atoms/projectScope"; import InputLabel from "@mui/material/InputLabel"; import MenuItem from "@mui/material/MenuItem"; import FormControl from "@mui/material/FormControl"; import Select from "@mui/material/Select"; +import LoadingButton from "@mui/lab/LoadingButton"; export const webhookStripe = { name: "Stripe", @@ -49,11 +53,10 @@ export const webhookStripe = { // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, }, - auth: ( - webhookObject: IWebhook, - setWebhookObject: (w: IWebhook) => void, - secrets: ISecret - ) => { + Auth: (webhookObject: IWebhook, setWebhookObject: (w: IWebhook) => void) => { + const [secretNames] = useAtom(secretNamesAtom, projectScope); + const [updateSecretNames] = useAtom(updateSecretNamesAtom, projectScope); + return ( <> @@ -77,8 +80,9 @@ export const webhookStripe = { {webhookObject.auth.secretKey && - !secrets.loading && - !secrets.keys.includes(webhookObject.auth.secretKey) && ( + !secretNames.loading && + secretNames.secretNames && + !secretNames.secretNames.includes(webhookObject.auth.secretKey) && ( Your previously selected key{" "} {webhookObject.auth.secretKey} does not exist in @@ -86,34 +90,55 @@ export const webhookStripe = { )} - - Secret key - { + setWebhookObject({ + ...webhookObject, + auth: { ...webhookObject.auth, secretKey: e.target.value }, + }); }} > - Add a key in Secret Manager - - - + {secretNames.secretNames?.map((secret) => { + return {secret}; + })} + { + const secretManagerLink = `https://console.cloud.google.com/security/secret-manager/create`; + window?.open?.(secretManagerLink, "_blank")?.focus(); + }} + > + Add a key in Secret Manager + + + + { + updateSecretNames?.(); + }} + > + Refresh + + void, - secrets: ISecret - ) => { + Auth: (webhookObject: IWebhook, setWebhookObject: (w: IWebhook) => void) => { return ( <> diff --git a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx index bcc1b028..848bad07 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx @@ -1,10 +1,7 @@ import { Typography, Link, TextField } from "@mui/material"; import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; import { TableSettings } from "@src/types/table"; -import { - IWebhook, - ISecret, -} from "@src/components/TableModals/WebhooksModal/utils"; +import { IWebhook } from "@src/components/TableModals/WebhooksModal/utils"; export const webhook = { name: "Web Form", @@ -51,11 +48,7 @@ export const webhook = { // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, }, - auth: ( - webhookObject: IWebhook, - setWebhookObject: (w: IWebhook) => void, - secrets: ISecret - ) => { + Auth: (webhookObject: IWebhook, setWebhookObject: (w: IWebhook) => void) => { return ( <> diff --git a/src/components/TableModals/WebhooksModal/Step1Auth.tsx b/src/components/TableModals/WebhooksModal/Step1Auth.tsx index 59bafba9..94f4f2eb 100644 --- a/src/components/TableModals/WebhooksModal/Step1Auth.tsx +++ b/src/components/TableModals/WebhooksModal/Step1Auth.tsx @@ -1,41 +1,13 @@ -import { useAtom } from "jotai"; -import { useEffect, useState } from "react"; import { IWebhookModalStepProps } from "./WebhookModal"; import { FormControlLabel, Checkbox, Typography } from "@mui/material"; -import { - projectIdAtom, - projectScope, - rowyRunAtom, -} from "@src/atoms/projectScope"; -import { runRoutes } from "@src/constants/runRoutes"; -import { webhookSchemas, ISecret } from "./utils"; +import { webhookSchemas } from "./utils"; export default function Step1Endpoint({ webhookObject, setWebhookObject, }: IWebhookModalStepProps) { - const [rowyRun] = useAtom(rowyRunAtom, projectScope); - const [projectId] = useAtom(projectIdAtom, projectScope); - const [secrets, setSecrets] = useState({ - loading: true, - keys: [], - projectId, - }); - - useEffect(() => { - rowyRun({ - route: runRoutes.listSecrets, - }).then((secrets) => { - setSecrets({ - loading: false, - keys: secrets as string[], - projectId, - }); - }); - }, []); - return ( <> @@ -63,10 +35,9 @@ export default function Step1Endpoint({ /> {webhookObject.auth?.enabled && - webhookSchemas[webhookObject.type].auth( + webhookSchemas[webhookObject.type].Auth( webhookObject, - setWebhookObject, - secrets + setWebhookObject )} {} diff --git a/src/components/TableModals/WebhooksModal/utils.tsx b/src/components/TableModals/WebhooksModal/utils.tsx index e43c747b..f8f85ff4 100644 --- a/src/components/TableModals/WebhooksModal/utils.tsx +++ b/src/components/TableModals/WebhooksModal/utils.tsx @@ -119,12 +119,6 @@ export interface IWebhook { auth?: any; } -export interface ISecret { - loading: boolean; - keys: string[]; - projectId: string; -} - export const webhookSchemas = { basic, typeform, diff --git a/src/sources/ProjectSourceFirebase/useTableFunctions.ts b/src/sources/ProjectSourceFirebase/useTableFunctions.ts index cceaca98..a1af1169 100644 --- a/src/sources/ProjectSourceFirebase/useTableFunctions.ts +++ b/src/sources/ProjectSourceFirebase/useTableFunctions.ts @@ -22,6 +22,10 @@ import { AdditionalTableSettings, MinimumTableSettings, currentUserAtom, + updateSecretNamesAtom, + projectIdAtom, + rowyRunAtom, + secretNamesAtom, } from "@src/atoms/projectScope"; import { firebaseDbAtom } from "./init"; @@ -34,10 +38,15 @@ import { rowyUser } from "@src/utils/table"; import { TableSettings, TableSchema, SubTablesSchema } from "@src/types/table"; import { FieldType } from "@src/constants/fields"; import { getFieldProp } from "@src/components/fields"; +import { runRoutes } from "@src/constants/runRoutes"; export function useTableFunctions() { const [firebaseDb] = useAtom(firebaseDbAtom, projectScope); const [currentUser] = useAtom(currentUserAtom, projectScope); + const [projectId] = useAtom(projectIdAtom, projectScope); + const [rowyRun] = useAtom(rowyRunAtom, projectScope); + const [secretNames, setSecretNames] = useAtom(secretNamesAtom, projectScope); + const [updateSecretNames] = useAtom(updateSecretNamesAtom, projectScope); // Create a function to get the latest tables from project settings, // so we don’t create new functions when tables change @@ -330,4 +339,36 @@ export function useTableFunctions() { return tableSchema as TableSchema; }); }, [firebaseDb, readTables, setGetTableSchema]); + + // Set the deleteTable function + const setUpdateSecretNames = useSetAtom(updateSecretNamesAtom, projectScope); + useEffect(() => { + if (!projectId || !rowyRun || !secretNamesAtom) return; + setUpdateSecretNames(() => async (clearSecretNames?: boolean) => { + setSecretNames({ + loading: true, + secretNames: clearSecretNames ? null : secretNames.secretNames, + }); + rowyRun({ + route: runRoutes.listSecrets, + }) + .then((secrets: string[]) => { + setSecretNames({ + loading: false, + secretNames: secrets, + }); + }) + .catch((e) => { + setSecretNames({ + loading: false, + secretNames: clearSecretNames ? null : secretNames.secretNames, + }); + }); + }); + }, [projectId, rowyRun, setUpdateSecretNames]); + useEffect(() => { + if (updateSecretNames) { + updateSecretNames(true); + } + }, [updateSecretNames]); } From da0cf161dfb2c9dd63572ee7d5f06d1ee5b36fd0 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Wed, 12 Apr 2023 17:43:07 +0530 Subject: [PATCH 053/183] worked on array subtable --- src/assets/icons/ArraySubTable.tsx | 9 + src/atoms/tableScope/rowActions.test.ts | 36 +- src/atoms/tableScope/rowActions.ts | 47 ++- src/atoms/tableScope/ui.ts | 1 + .../ColumnModals/FieldsDropdown.tsx | 23 +- src/components/SideDrawer/FieldWrapper.tsx | 10 +- src/components/SideDrawer/MemoizedField.tsx | 5 +- src/components/SideDrawer/SideDrawer.tsx | 20 +- .../SideDrawer/SideDrawerFields.tsx | 24 +- .../Table/ContextMenu/MenuContents.tsx | 119 +++--- src/components/Table/EmptyTable.tsx | 81 ++-- .../Table/FinalColumn/FinalColumn.tsx | 119 +++--- src/components/Table/TableBody.tsx | 5 +- .../Table/TableCell/EditorCellController.tsx | 1 + src/components/Table/TableCell/TableCell.tsx | 4 + .../Table/useKeyboardNavigation.tsx | 5 + src/components/Table/useMenuAction.tsx | 17 +- src/components/TableToolbar/AddRow.tsx | 86 +++++ src/components/TableToolbar/TableToolbar.tsx | 55 ++- src/components/fields/Action/index.tsx | 1 + .../fields/ArraySubTable/DisplayCell.tsx | 46 +++ .../fields/ArraySubTable/Settings.tsx | 32 ++ .../fields/ArraySubTable/SideDrawerField.tsx | 56 +++ src/components/fields/ArraySubTable/index.tsx | 36 ++ src/components/fields/ArraySubTable/utils.ts | 34 ++ src/components/fields/CreatedAt/index.tsx | 1 + src/components/fields/CreatedBy/index.tsx | 1 + src/components/fields/Derivative/index.tsx | 1 + src/components/fields/File/EditorCell.tsx | 9 +- .../fields/File/SideDrawerField.tsx | 8 +- src/components/fields/File/useFileUpload.ts | 19 +- src/components/fields/Image/EditorCell.tsx | 18 +- .../fields/Image/SideDrawerField.tsx | 14 +- src/components/fields/SubTable/index.tsx | 1 + src/components/fields/UpdatedAt/index.tsx | 1 + src/components/fields/UpdatedBy/index.tsx | 1 + src/components/fields/index.ts | 2 + src/components/fields/types.ts | 5 +- src/constants/fields.ts | 1 + src/constants/routes.tsx | 2 + .../useFirestoreDocAsCollectionWithAtom.ts | 357 ++++++++++++++++++ src/pages/Table/ProvidedArraySubTablePage.tsx | 156 ++++++++ src/pages/Table/TablePage.tsx | 9 +- .../ArraySubTableSourceFirestore.tsx | 143 +++++++ src/types/table.d.ts | 29 +- src/utils/table.ts | 1 + 46 files changed, 1450 insertions(+), 201 deletions(-) create mode 100644 src/assets/icons/ArraySubTable.tsx create mode 100644 src/components/fields/ArraySubTable/DisplayCell.tsx create mode 100644 src/components/fields/ArraySubTable/Settings.tsx create mode 100644 src/components/fields/ArraySubTable/SideDrawerField.tsx create mode 100644 src/components/fields/ArraySubTable/index.tsx create mode 100644 src/components/fields/ArraySubTable/utils.ts create mode 100644 src/hooks/useFirestoreDocAsCollectionWithAtom.ts create mode 100644 src/pages/Table/ProvidedArraySubTablePage.tsx create mode 100644 src/sources/TableSourceFirestore/ArraySubTableSourceFirestore.tsx diff --git a/src/assets/icons/ArraySubTable.tsx b/src/assets/icons/ArraySubTable.tsx new file mode 100644 index 00000000..d7f5a2ad --- /dev/null +++ b/src/assets/icons/ArraySubTable.tsx @@ -0,0 +1,9 @@ +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; + +export function ArraySubTable(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/atoms/tableScope/rowActions.test.ts b/src/atoms/tableScope/rowActions.test.ts index fe35d654..5ff521a7 100644 --- a/src/atoms/tableScope/rowActions.test.ts +++ b/src/atoms/tableScope/rowActions.test.ts @@ -494,7 +494,11 @@ describe("deleteRow", () => { } = renderHook(() => useSetAtom(deleteRowAtom, tableScope)); expect(deleteRow).toBeDefined(); - await act(() => deleteRow(TEST_COLLECTION + "/row2")); + await act(() => + deleteRow({ + path: TEST_COLLECTION + "/row2", + }) + ); const { result: { current: tableRows }, @@ -510,7 +514,11 @@ describe("deleteRow", () => { } = renderHook(() => useSetAtom(deleteRowAtom, tableScope)); expect(deleteRow).toBeDefined(); - await act(() => deleteRow(TEST_COLLECTION + "/rowLocal2")); + await act(() => + deleteRow({ + path: TEST_COLLECTION + "/rowLocal2", + }) + ); const { result: { current: tableRows }, @@ -527,9 +535,9 @@ describe("deleteRow", () => { expect(deleteRow).toBeDefined(); await act(() => - deleteRow( - ["row1", "row2", "row8"].map((id) => TEST_COLLECTION + "/" + id) - ) + deleteRow({ + path: ["row1", "row2", "row8"].map((id) => TEST_COLLECTION + "/" + id), + }) ); const { @@ -548,7 +556,11 @@ describe("deleteRow", () => { } = renderHook(() => useSetAtom(deleteRowAtom, tableScope)); expect(deleteRow).toBeDefined(); - await act(() => deleteRow(generatedRows.map((row) => row._rowy_ref.path))); + await act(() => + deleteRow({ + path: generatedRows.map((row) => row._rowy_ref.path), + }) + ); const { result: { current: tableRows }, @@ -563,7 +575,11 @@ describe("deleteRow", () => { } = renderHook(() => useSetAtom(deleteRowAtom, tableScope)); expect(deleteRow).toBeDefined(); - await act(() => deleteRow("nonExistent")); + await act(() => + deleteRow({ + path: "nonExistent", + }) + ); const { result: { current: tableRows }, @@ -578,7 +594,11 @@ describe("deleteRow", () => { } = renderHook(() => useSetAtom(deleteRowAtom, tableScope)); expect(deleteRow).toBeDefined(); - await act(() => deleteRow("nonExistent")); + await act(() => + deleteRow({ + path: "nonExistent", + }) + ); const { result: { current: tableRows }, diff --git a/src/atoms/tableScope/rowActions.ts b/src/atoms/tableScope/rowActions.ts index 75b496f5..8a885d80 100644 --- a/src/atoms/tableScope/rowActions.ts +++ b/src/atoms/tableScope/rowActions.ts @@ -22,7 +22,11 @@ import { _bulkWriteDbAtom, } from "./table"; -import { TableRow, BulkWriteFunction } from "@src/types/table"; +import { + TableRow, + BulkWriteFunction, + ArrayTableRowData, +} from "@src/types/table"; import { rowyUser, generateId, @@ -211,7 +215,17 @@ export const addRowAtom = atom( */ export const deleteRowAtom = atom( null, - async (get, set, path: string | string[]) => { + async ( + get, + set, + { + path, + options, + }: { + path: string | string[]; + options?: ArrayTableRowData; + } + ) => { const deleteRowDb = get(_deleteRowDbAtom); if (!deleteRowDb) throw new Error("Cannot write to database"); @@ -223,9 +237,9 @@ export const deleteRowAtom = atom( find(tableRowsLocal, ["_rowy_ref.path", path]) ); if (isLocalRow) set(tableRowsLocalAtom, { type: "delete", path }); - // Always delete from db in case it exists - await deleteRowDb(path); + // *options* are passed in case of array table to target specific row + await deleteRowDb(path, options); if (auditChange) auditChange("DELETE_ROW", path); }; @@ -312,6 +326,8 @@ export interface IUpdateFieldOptions { useArrayUnion?: boolean; /** Optionally, uses firestore's arrayRemove with the given value. Removes given value items from the existing array */ useArrayRemove?: boolean; + /** Optionally, used to locate the row in ArraySubTable. */ + arrayTableData?: ArrayTableRowData; } /** * Set function updates or deletes a field in a row. @@ -339,6 +355,7 @@ export const updateFieldAtom = atom( disableCheckEquality, useArrayUnion, useArrayRemove, + arrayTableData, }: IUpdateFieldOptions ) => { const updateRowDb = get(_updateRowDbAtom); @@ -387,7 +404,12 @@ export const updateFieldAtom = atom( ...(row[fieldName] ?? []), ...localUpdate[fieldName], ]; - dbUpdate[fieldName] = arrayUnion(...dbUpdate[fieldName]); + // if we are updating a row of ArraySubTable + if (arrayTableData?.index !== undefined) { + dbUpdate[fieldName] = localUpdate[fieldName]; + } else { + dbUpdate[fieldName] = arrayUnion(...dbUpdate[fieldName]); + } } //apply arrayRemove @@ -400,8 +422,15 @@ export const updateFieldAtom = atom( row[fieldName] ?? [], (el) => !find(localUpdate[fieldName], el) ); - dbUpdate[fieldName] = arrayRemove(...dbUpdate[fieldName]); + + // if we are updating a row of ArraySubTable + if (arrayTableData?.index !== undefined) { + dbUpdate[fieldName] = localUpdate[fieldName]; + } else { + dbUpdate[fieldName] = arrayRemove(...dbUpdate[fieldName]); + } } + // need to pass the index of the row to updateRowDb // Check for required fields const newRowValues = updateRowData(cloneDeep(row), dbUpdate); @@ -431,7 +460,8 @@ export const updateFieldAtom = atom( await updateRowDb( row._rowy_ref.path, omitRowyFields(newRowValues), - deleteField ? [fieldName] : [] + deleteField ? [fieldName] : [], + arrayTableData ); } } @@ -440,7 +470,8 @@ export const updateFieldAtom = atom( await updateRowDb( row._rowy_ref.path, omitRowyFields(dbUpdate), - deleteField ? [fieldName] : [] + deleteField ? [fieldName] : [], + arrayTableData ); } diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 7182a41d..b170d250 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -134,6 +134,7 @@ export type SelectedCell = { path: string | "_rowy_header"; columnKey: string | "_rowy_row_actions"; focusInside: boolean; + arrayIndex?: number; // for array sub table }; /** Store selected cell in table. Used in side drawer and context menu */ export const selectedCellAtom = atom(null); diff --git a/src/components/ColumnModals/FieldsDropdown.tsx b/src/components/ColumnModals/FieldsDropdown.tsx index 529c3b84..2b2497e8 100644 --- a/src/components/ColumnModals/FieldsDropdown.tsx +++ b/src/components/ColumnModals/FieldsDropdown.tsx @@ -11,6 +11,7 @@ import { projectSettingsAtom, rowyRunModalAtom, } from "@src/atoms/projectScope"; +import { tableScope, tableSettingsAtom } from "@src/atoms/tableScope"; export interface IFieldsDropdownProps { value: FieldType | ""; @@ -35,17 +36,22 @@ export default function FieldsDropdown({ }: IFieldsDropdownProps) { const [projectSettings] = useAtom(projectSettingsAtom, projectScope); const openRowyRunModal = useSetAtom(rowyRunModalAtom, projectScope); + const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const fieldTypesToDisplay = optionsProp ? FIELDS.filter((fieldConfig) => optionsProp.indexOf(fieldConfig.type) > -1) : FIELDS; const options = fieldTypesToDisplay.map((fieldConfig) => { const requireCloudFunctionSetup = fieldConfig.requireCloudFunction && !projectSettings.rowyRunUrl; + const requireCollectionTable = + tableSettings.isNotACollection === true && + fieldConfig.requireCollectionTable === true; return { label: fieldConfig.name, value: fieldConfig.type, - disabled: requireCloudFunctionSetup, + disabled: requireCloudFunctionSetup || requireCollectionTable, requireCloudFunctionSetup, + requireCollectionTable, }; }); @@ -82,7 +88,18 @@ export default function FieldsDropdown({ {getFieldProp("icon", option.value as FieldType)} {option.label} - {option.requireCloudFunctionSetup && ( + {option.requireCollectionTable ? ( + + {" "} + Unavailable + + ) : option.requireCloudFunctionSetup ? ( - )} + ) : null} )} label={label || "Field type"} diff --git a/src/components/SideDrawer/FieldWrapper.tsx b/src/components/SideDrawer/FieldWrapper.tsx index c6a352cd..eb76afa8 100644 --- a/src/components/SideDrawer/FieldWrapper.tsx +++ b/src/components/SideDrawer/FieldWrapper.tsx @@ -35,6 +35,7 @@ export interface IFieldWrapperProps { fieldName?: string; label?: React.ReactNode; debugText?: React.ReactNode; + debugValue?: React.ReactNode; disabled?: boolean; hidden?: boolean; index?: number; @@ -46,6 +47,7 @@ export default function FieldWrapper({ fieldName, label, debugText, + debugValue, disabled, hidden, index, @@ -100,7 +102,7 @@ export default function FieldWrapper({ }> {children ?? - (!debugText && ( + (!debugValue && ( - {debugText && ( + {debugValue && ( { - copyToClipboard(debugText as string); + copyToClipboard(debugValue as string); enqueueSnackbar("Copied!"); }} > @@ -139,7 +141,7 @@ export default function FieldWrapper({ void; onSubmit: (fieldName: string, value: any) => void; @@ -25,6 +26,7 @@ export const MemoizedField = memo( hidden, value, _rowy_ref, + _rowy_arrayTableData, isDirty, onDirty, onSubmit, @@ -78,6 +80,7 @@ export const MemoizedField = memo( }, onSubmit: handleSubmit, disabled, + _rowy_arrayTableData, })} ); diff --git a/src/components/SideDrawer/SideDrawer.tsx b/src/components/SideDrawer/SideDrawer.tsx index ef676bd1..faeb15ad 100644 --- a/src/components/SideDrawer/SideDrawer.tsx +++ b/src/components/SideDrawer/SideDrawer.tsx @@ -30,11 +30,21 @@ export default function SideDrawer() { const [cell, setCell] = useAtom(selectedCellAtom, tableScope); const [open, setOpen] = useAtom(sideDrawerOpenAtom, tableScope); - const selectedRow = find(tableRows, ["_rowy_ref.path", cell?.path]); - const selectedCellRowIndex = findIndex(tableRows, [ - "_rowy_ref.path", - cell?.path, - ]); + const selectedRow = find( + tableRows, + cell?.arrayIndex === undefined + ? ["_rowy_ref.path", cell?.path] + : // if the table is an array table, we need to use the array index to find the row + ["_rowy_arrayTableData.index", cell?.arrayIndex] + ); + + const selectedCellRowIndex = findIndex( + tableRows, + cell?.arrayIndex === undefined + ? ["_rowy_ref.path", cell?.path] + : // if the table is an array table, we need to use the array index to find the row + ["_rowy_arrayTableData.index", cell?.arrayIndex] + ); const handleNavigate = (direction: "up" | "down") => () => { if (!tableRows || !cell) return; diff --git a/src/components/SideDrawer/SideDrawerFields.tsx b/src/components/SideDrawer/SideDrawerFields.tsx index 7b6a6578..e2abdfa4 100644 --- a/src/components/SideDrawer/SideDrawerFields.tsx +++ b/src/components/SideDrawer/SideDrawerFields.tsx @@ -66,7 +66,16 @@ export default function SideDrawerFields({ row }: ISideDrawerFieldsProps) { setSaveState("saving"); try { - await updateField({ path: selectedCell!.path, fieldName, value }); + await updateField({ + path: selectedCell!.path, + fieldName, + value, + deleteField: undefined, + arrayTableData: { + index: selectedCell.arrayIndex ?? 0, + }, + }); + setSaveState("saved"); } catch (e) { enqueueSnackbar((e as Error).message, { variant: "error" }); @@ -121,6 +130,7 @@ export default function SideDrawerFields({ row }: ISideDrawerFieldsProps) { onDirty={onDirty} onSubmit={onSubmit} isDirty={dirtyField === field.key} + _rowy_arrayTableData={row._rowy_arrayTableData} /> ))} @@ -128,7 +138,17 @@ export default function SideDrawerFields({ row }: ISideDrawerFieldsProps) { type="debug" fieldName="_rowy_ref.path" label="Document path" - debugText={row._rowy_ref.path ?? row._rowy_ref.id ?? "No ref"} + debugText={ + row._rowy_arrayTableData + ? row._rowy_ref.path + + " → " + + row._rowy_arrayTableData.parentField + + "[" + + row._rowy_arrayTableData.index + + "]" + : row._rowy_ref.path + } + debugValue={row._rowy_ref.path ?? row._rowy_ref.id ?? "No ref"} /> {userDocHiddenFields.length > 0 && ( diff --git a/src/components/Table/ContextMenu/MenuContents.tsx b/src/components/Table/ContextMenu/MenuContents.tsx index 88a973c2..5154ba8b 100644 --- a/src/components/Table/ContextMenu/MenuContents.tsx +++ b/src/components/Table/ContextMenu/MenuContents.tsx @@ -34,6 +34,7 @@ import { deleteRowAtom, updateFieldAtom, tableFiltersPopoverAtom, + _updateRowDbAtom, } from "@src/atoms/tableScope"; import { FieldType } from "@src/constants/fields"; @@ -54,6 +55,7 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { const addRow = useSetAtom(addRowAtom, tableScope); const deleteRow = useSetAtom(deleteRowAtom, tableScope); const updateField = useSetAtom(updateFieldAtom, tableScope); + const [updateRowDb] = useAtom(_updateRowDbAtom, tableScope); const openTableFiltersPopover = useSetAtom( tableFiltersPopoverAtom, tableScope @@ -62,19 +64,83 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { if (!tableSchema.columns || !selectedCell) return null; const selectedColumn = tableSchema.columns[selectedCell.columnKey]; - const row = find(tableRows, ["_rowy_ref.path", selectedCell.path]); + const row = find( + tableRows, + selectedCell?.arrayIndex === undefined + ? ["_rowy_ref.path", selectedCell.path] + : // if the table is an array table, we need to use the array index to find the row + ["_rowy_arrayTableData.index", selectedCell.arrayIndex] + ); if (!row) return null; const actionGroups: IContextMenuItem[][] = []; const handleDuplicate = () => { - addRow({ - row, - setId: addRowIdType === "custom" ? "decrement" : addRowIdType, - }); + const _duplicate = () => { + if (row._rowy_arrayTableData !== undefined) { + if (!updateRowDb) return; + + return updateRowDb("", {}, undefined, { + index: row._rowy_arrayTableData.index, + operation: { + addRow: "bottom", + base: row, + }, + }); + } + return addRow({ + row: row, + setId: addRowIdType === "custom" ? "decrement" : addRowIdType, + }); + }; + + if (altPress || row._rowy_arrayTableData !== undefined) { + _duplicate(); + } else { + confirm({ + title: "Duplicate row?", + body: ( + <> + Row path: +
+ + {row._rowy_ref.path} + + + ), + confirm: "Duplicate", + handleConfirm: _duplicate, + }); + } + }; + const handleDelete = () => { + const _delete = () => + deleteRow({ + path: row._rowy_ref.path, + options: row._rowy_arrayTableData, + }); + + if (altPress || row._rowy_arrayTableData !== undefined) { + _delete(); + } else { + confirm({ + title: "Delete row?", + body: ( + <> + Row path: +
+ + {row._rowy_ref.path} + + + ), + confirm: "Delete", + confirmColor: "error", + handleConfirm: _delete, + }); + } }; - const handleDelete = () => deleteRow(row._rowy_ref.path); const rowActions: IContextMenuItem[] = [ { label: "Copy ID", @@ -112,51 +178,14 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { disabled: tableSettings.tableType === "collectionGroup" || (!userRoles.includes("ADMIN") && tableSettings.readOnly), - onClick: altPress - ? handleDuplicate - : () => { - confirm({ - title: "Duplicate row?", - body: ( - <> - Row path: -
- - {row._rowy_ref.path} - - - ), - confirm: "Duplicate", - handleConfirm: handleDuplicate, - }); - onClose(); - }, + onClick: handleDuplicate, }, { label: altPress ? "Delete" : "Delete…", color: "error", icon: , disabled: !userRoles.includes("ADMIN") && tableSettings.readOnly, - onClick: altPress - ? handleDelete - : () => { - confirm({ - title: "Delete row?", - body: ( - <> - Row path: -
- - {row._rowy_ref.path} - - - ), - confirm: "Delete", - confirmColor: "error", - handleConfirm: handleDelete, - }); - onClose(); - }, + onClick: handleDelete, }, ]; diff --git a/src/components/Table/EmptyTable.tsx b/src/components/Table/EmptyTable.tsx index f07839b3..1f14e2db 100644 --- a/src/components/Table/EmptyTable.tsx +++ b/src/components/Table/EmptyTable.tsx @@ -34,7 +34,7 @@ export default function EmptyTable() { : false; let contents = <>; - if (hasData) { + if (!tableSettings.isNotACollection && hasData) { contents = ( <>
@@ -72,47 +72,56 @@ export default function EmptyTable() { Get started - There is no data in the Firestore collection: + {tableSettings.isNotACollection === true + ? "There is no data in this Array Sub Table:" + : "There is no data in the Firestore collection:"}
- {tableSettings.collection} + + {tableSettings.collection} + {tableSettings.subTableKey?.length && + `.${tableSettings.subTableKey}`} +
- - - - You can import data from an external source: - + {!tableSettings.isNotACollection && ( + <> + + + You can import data from an external source: + - ( - - )} - PopoverProps={{ - anchorOrigin: { - vertical: "bottom", - horizontal: "center", - }, - transformOrigin: { - vertical: "top", - horizontal: "center", - }, - }} - /> - + ( + + )} + PopoverProps={{ + anchorOrigin: { + vertical: "bottom", + horizontal: "center", + }, + transformOrigin: { + vertical: "top", + horizontal: "center", + }, + }} + /> + - - - or - - + + + or + + + + )} diff --git a/src/components/Table/FinalColumn/FinalColumn.tsx b/src/components/Table/FinalColumn/FinalColumn.tsx index 28b584e6..041d3f0f 100644 --- a/src/components/Table/FinalColumn/FinalColumn.tsx +++ b/src/components/Table/FinalColumn/FinalColumn.tsx @@ -20,8 +20,8 @@ import { addRowAtom, deleteRowAtom, contextMenuTargetAtom, + _updateRowDbAtom, } from "@src/atoms/tableScope"; - export const FinalColumn = memo(function FinalColumn({ row, focusInsideCell, @@ -31,17 +31,77 @@ export const FinalColumn = memo(function FinalColumn({ const confirm = useSetAtom(confirmDialogAtom, projectScope); const [tableSettings] = useAtom(tableSettingsAtom, tableScope); + const [updateRowDb] = useAtom(_updateRowDbAtom, tableScope); + const addRow = useSetAtom(addRowAtom, tableScope); const deleteRow = useSetAtom(deleteRowAtom, tableScope); const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope); const [altPress] = useAtom(altPressAtom, projectScope); - const handleDelete = () => deleteRow(row.original._rowy_ref.path); + + const handleDelete = () => { + const _delete = () => + deleteRow({ + path: row.original._rowy_ref.path, + options: row.original._rowy_arrayTableData, + }); + if (altPress || row.original._rowy_arrayTableData !== undefined) { + _delete(); + } else { + confirm({ + title: "Delete row?", + body: ( + <> + Row path: +
+ + {row.original._rowy_ref.path} + + + ), + confirm: "Delete", + confirmColor: "error", + handleConfirm: _delete, + }); + } + }; + const handleDuplicate = () => { - addRow({ - row: row.original, - setId: addRowIdType === "custom" ? "decrement" : addRowIdType, - }); + const _duplicate = () => { + if (row.original._rowy_arrayTableData !== undefined) { + if (!updateRowDb) return; + + return updateRowDb("", {}, undefined, { + index: row.original._rowy_arrayTableData.index, + operation: { + addRow: "bottom", + base: row.original, + }, + }); + } + return addRow({ + row: row.original, + setId: addRowIdType === "custom" ? "decrement" : addRowIdType, + }); + }; + if (altPress || row.original._rowy_arrayTableData !== undefined) { + _duplicate(); + } else { + confirm({ + title: "Duplicate row?", + body: ( + <> + Row path: +
+ + {row.original._rowy_ref.path} + + + ), + confirm: "Duplicate", + handleConfirm: _duplicate, + }); + } }; if (!userRoles.includes("ADMIN") && tableSettings.readOnly === true) @@ -73,28 +133,7 @@ export const FinalColumn = memo(function FinalColumn({ size="small" color="inherit" disabled={tableSettings.tableType === "collectionGroup"} - onClick={ - altPress - ? handleDuplicate - : () => { - confirm({ - title: "Duplicate row?", - body: ( - <> - Row path: -
- - {row.original._rowy_ref.path} - - - ), - confirm: "Duplicate", - handleConfirm: handleDuplicate, - }); - } - } + onClick={handleDuplicate} className="row-hover-iconButton" tabIndex={focusInsideCell ? 0 : -1} > @@ -106,29 +145,7 @@ export const FinalColumn = memo(function FinalColumn({ { - confirm({ - title: "Delete row?", - body: ( - <> - Row path: -
- - {row.original._rowy_ref.path} - - - ), - confirm: "Delete", - confirmColor: "error", - handleConfirm: handleDelete, - }); - } - } + onClick={handleDelete} className="row-hover-iconButton" tabIndex={focusInsideCell ? 0 : -1} sx={{ diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx index 8dd53d73..edaed491 100644 --- a/src/components/Table/TableBody.tsx +++ b/src/components/Table/TableBody.tsx @@ -102,7 +102,10 @@ export const TableBody = memo(function TableBody({ const isSelectedCell = selectedCell?.path === row.original._rowy_ref.path && - selectedCell?.columnKey === cell.column.id; + selectedCell?.columnKey === cell.column.id && + // if the table is an array sub table, we need to check the array index as well + selectedCell?.arrayIndex === + row.original._rowy_arrayTableData?.index; const fieldTypeGroup = getFieldProp( "group", diff --git a/src/components/Table/TableCell/EditorCellController.tsx b/src/components/Table/TableCell/EditorCellController.tsx index c80380d4..cacc8946 100644 --- a/src/components/Table/TableCell/EditorCellController.tsx +++ b/src/components/Table/TableCell/EditorCellController.tsx @@ -66,6 +66,7 @@ export default function EditorCellController({ fieldName: props.column.fieldName, value: localValueRef.current, deleteField: localValueRef.current === undefined, + arrayTableData: props.row?._rowy_arrayTableData, }); } catch (e) { enqueueSnackbar((e as Error).message, { variant: "error" }); diff --git a/src/components/Table/TableCell/TableCell.tsx b/src/components/Table/TableCell/TableCell.tsx index 4ea51433..5c664b35 100644 --- a/src/components/Table/TableCell/TableCell.tsx +++ b/src/components/Table/TableCell/TableCell.tsx @@ -123,6 +123,7 @@ export const TableCell = memo(function TableCell({ focusInsideCell, setFocusInsideCell: (focusInside: boolean) => setSelectedCell({ + arrayIndex: row.original._rowy_arrayTableData?.index, path: row.original._rowy_ref.path, columnKey: cell.column.id, focusInside, @@ -166,6 +167,7 @@ export const TableCell = memo(function TableCell({ }} onClick={(e) => { setSelectedCell({ + arrayIndex: row.original._rowy_arrayTableData?.index, path: row.original._rowy_ref.path, columnKey: cell.column.id, focusInside: false, @@ -174,6 +176,7 @@ export const TableCell = memo(function TableCell({ }} onDoubleClick={(e) => { setSelectedCell({ + arrayIndex: row.original._rowy_arrayTableData?.index, path: row.original._rowy_ref.path, columnKey: cell.column.id, focusInside: true, @@ -183,6 +186,7 @@ export const TableCell = memo(function TableCell({ onContextMenu={(e) => { e.preventDefault(); setSelectedCell({ + arrayIndex: row.original._rowy_arrayTableData?.index, path: row.original._rowy_ref.path, columnKey: cell.column.id, focusInside: false, diff --git a/src/components/Table/useKeyboardNavigation.tsx b/src/components/Table/useKeyboardNavigation.tsx index 3353d23f..81141a77 100644 --- a/src/components/Table/useKeyboardNavigation.tsx +++ b/src/components/Table/useKeyboardNavigation.tsx @@ -128,6 +128,11 @@ export function useKeyboardNavigation({ ? tableRows[newRowIndex]._rowy_ref.path : "_rowy_header", columnKey: leafColumns[newColIndex].id! || leafColumns[0].id!, + arrayIndex: + newRowIndex > -1 + ? tableRows[newRowIndex]._rowy_arrayTableData?.index + : undefined, + // When selected cell changes, exit current cell focusInside: false, }; diff --git a/src/components/Table/useMenuAction.tsx b/src/components/Table/useMenuAction.tsx index 516124d3..49cdc2d8 100644 --- a/src/components/Table/useMenuAction.tsx +++ b/src/components/Table/useMenuAction.tsx @@ -71,6 +71,9 @@ export function useMenuAction( fieldName: selectedCol.fieldName, value: undefined, deleteField: true, + arrayTableData: { + index: selectedCell.arrayIndex ?? 0, + }, }); } catch (error) { enqueueSnackbar(`Failed to cut: ${error}`, { variant: "error" }); @@ -115,6 +118,9 @@ export function useMenuAction( path: selectedCell.path, fieldName: selectedCol.fieldName, value: parsed, + arrayTableData: { + index: selectedCell.arrayIndex ?? 0, + }, }); } catch (error) { enqueueSnackbar( @@ -130,7 +136,14 @@ export function useMenuAction( const selectedCol = tableSchema.columns?.[selectedCell.columnKey]; if (!selectedCol) return setCellValue(""); setSelectedCol(selectedCol); - const selectedRow = find(tableRows, ["_rowy_ref.path", selectedCell.path]); + + const selectedRow = find( + tableRows, + selectedCell.arrayIndex === undefined + ? ["_rowy_ref.path", selectedCell.path] + : // if the table is an array table, we need to use the array index to find the row + ["_rowy_arrayTableData.index", selectedCell.arrayIndex] + ); setCellValue(get(selectedRow, selectedCol.fieldName)); }, [selectedCell, tableSchema, tableRows]); @@ -149,7 +162,7 @@ export function useMenuAction( } }; }, - [selectedCol] + [enqueueSnackbar, selectedCol?.type] ); return { diff --git a/src/components/TableToolbar/AddRow.tsx b/src/components/TableToolbar/AddRow.tsx index c6372865..13e24cd5 100644 --- a/src/components/TableToolbar/AddRow.tsx +++ b/src/components/TableToolbar/AddRow.tsx @@ -27,6 +27,7 @@ import { tableFiltersAtom, tableSortsAtom, addRowAtom, + _updateRowDbAtom, } from "@src/atoms/tableScope"; export default function AddRow() { @@ -207,3 +208,88 @@ export default function AddRow() { ); } + +export function AddRowArraySubTable() { + const [updateRowDb] = useAtom(_updateRowDbAtom, tableScope); + const [open, setOpen] = useState(false); + + const anchorEl = useRef(null); + const [addRowAt, setAddNewRowAt] = useState<"top" | "bottom">("bottom"); + if (!updateRowDb) return null; + + const handleClick = () => { + updateRowDb("", {}, undefined, { + index: 0, + operation: { + addRow: addRowAt, + }, + }); + }; + return ( + <> + + + + + + + + + ); +} diff --git a/src/components/TableToolbar/TableToolbar.tsx b/src/components/TableToolbar/TableToolbar.tsx index 7af0f6b9..d7d21fbe 100644 --- a/src/components/TableToolbar/TableToolbar.tsx +++ b/src/components/TableToolbar/TableToolbar.tsx @@ -1,17 +1,19 @@ import { lazy, Suspense } from "react"; import { useAtom, useSetAtom } from "jotai"; -import { Stack } from "@mui/material"; +import { Button, Stack } from "@mui/material"; import WebhookIcon from "@mui/icons-material/Webhook"; import { Export as ExportIcon, Extension as ExtensionIcon, CloudLogs as CloudLogsIcon, + Import as ImportIcon, } from "@src/assets/icons"; + import TableToolbarButton from "./TableToolbarButton"; import { ButtonSkeleton } from "./TableToolbarSkeleton"; -import AddRow from "./AddRow"; +import AddRow, { AddRowArraySubTable } from "./AddRow"; import LoadedRowsStatus from "./LoadedRowsStatus"; import TableSettings from "./TableSettings"; import HiddenFields from "./HiddenFields"; @@ -32,6 +34,8 @@ import { tableModalAtom, } from "@src/atoms/tableScope"; import { FieldType } from "@src/constants/fields"; +import { TableToolsType } from "@src/types/table"; +import FilterIcon from "@mui/icons-material/FilterList"; // prettier-ignore const Filters = lazy(() => import("./Filters" /* webpackChunkName: "Filters" */)); @@ -43,7 +47,11 @@ const ReExecute = lazy(() => import("./ReExecute" /* webpackChunkName: "ReExecut export const TABLE_TOOLBAR_HEIGHT = 44; -export default function TableToolbar() { +export default function TableToolbar({ + disabledTools, +}: { + disabledTools?: TableToolsType[]; +}) { const [projectSettings] = useAtom(projectSettingsAtom, projectScope); const [userRoles] = useAtom(userRolesAtom, projectScope); const [compatibleRowyRunVersion] = useAtom( @@ -54,7 +62,6 @@ export default function TableToolbar() { const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const openTableModal = useSetAtom(tableModalAtom, tableScope); - const hasDerivatives = Object.values(tableSchema.columns ?? {}).filter( (column) => column.type === FieldType.derivative @@ -64,6 +71,7 @@ export default function TableToolbar() { tableSchema.compiledExtension && tableSchema.compiledExtension.replace(/\W/g, "")?.length > 0; + disabledTools = disabledTools ?? []; return ( - + {tableSettings.isNotACollection ? : }
{/* Spacer */} - }> - - + {tableSettings.isNotACollection ? ( + + ) : ( + }> + + + )}
{/* Spacer */}
{/* Spacer */} - {tableSettings.tableType !== "collectionGroup" && ( - }> - - + {disabledTools.includes("import") ? ( + } + disabled={true} + /> + ) : ( + tableSettings.tableType !== "collectionGroup" && ( + }> + + + ) )} }> openTableModal("export")} icon={} + disabled={disabledTools.includes("export")} /> {userRoles.includes("ADMIN") && ( @@ -123,6 +151,7 @@ export default function TableToolbar() { } }} icon={} + disabled={disabledTools.includes("webhooks")} /> } + disabled={disabledTools.includes("extensions")} /> {(hasDerivatives || hasExtensions) && ( }> diff --git a/src/components/fields/Action/index.tsx b/src/components/fields/Action/index.tsx index 7ab899e2..53b3629d 100644 --- a/src/components/fields/Action/index.tsx +++ b/src/components/fields/Action/index.tsx @@ -31,6 +31,7 @@ export const config: IFieldConfig = { settings: Settings, requireConfiguration: true, requireCloudFunction: true, + requireCollectionTable: true, sortKey: "status", }; export default config; diff --git a/src/components/fields/ArraySubTable/DisplayCell.tsx b/src/components/fields/ArraySubTable/DisplayCell.tsx new file mode 100644 index 00000000..3796d553 --- /dev/null +++ b/src/components/fields/ArraySubTable/DisplayCell.tsx @@ -0,0 +1,46 @@ +import { IDisplayCellProps } from "@src/components/fields/types"; +import { Link } from "react-router-dom"; + +import { Stack, IconButton } from "@mui/material"; +import OpenIcon from "@mui/icons-material/OpenInBrowser"; + +import { useSubTableData } from "./utils"; + +export default function ArraySubTable({ + column, + row, + _rowy_ref, + tabIndex, +}: IDisplayCellProps) { + const { documentCount, label, subTablePath } = useSubTableData( + column as any, + row, + _rowy_ref + ); + + if (!_rowy_ref) return null; + + return ( + +
+ {documentCount} {column.name as string}: {label} +
+ + + + +
+ ); +} diff --git a/src/components/fields/ArraySubTable/Settings.tsx b/src/components/fields/ArraySubTable/Settings.tsx new file mode 100644 index 00000000..d586d46c --- /dev/null +++ b/src/components/fields/ArraySubTable/Settings.tsx @@ -0,0 +1,32 @@ +import { useAtom } from "jotai"; +import { ISettingsProps } from "@src/components/fields/types"; + +import MultiSelect from "@rowy/multiselect"; +import { FieldType } from "@src/constants/fields"; + +import { tableScope, tableColumnsOrderedAtom } from "@src/atoms/tableScope"; + +const Settings = ({ config, onChange }: ISettingsProps) => { + const [tableOrderedColumns] = useAtom(tableColumnsOrderedAtom, tableScope); + + const columnOptions = tableOrderedColumns + .filter((column) => + [ + FieldType.shortText, + FieldType.singleSelect, + FieldType.email, + FieldType.phone, + ].includes(column.type) + ) + .map((c) => ({ label: c.name, value: c.key })); + + return ( + + ); +}; +export default Settings; diff --git a/src/components/fields/ArraySubTable/SideDrawerField.tsx b/src/components/fields/ArraySubTable/SideDrawerField.tsx new file mode 100644 index 00000000..0f03a45b --- /dev/null +++ b/src/components/fields/ArraySubTable/SideDrawerField.tsx @@ -0,0 +1,56 @@ +import { useMemo } from "react"; +import { useAtom } from "jotai"; +import { selectAtom } from "jotai/utils"; +import { find, isEqual } from "lodash-es"; +import { ISideDrawerFieldProps } from "@src/components/fields/types"; +import { Link } from "react-router-dom"; + +import { Box, Stack, IconButton } from "@mui/material"; +import OpenIcon from "@mui/icons-material/OpenInBrowser"; + +import { tableScope, tableRowsAtom } from "@src/atoms/tableScope"; +import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils"; +import { useSubTableData } from "./utils"; + +export default function ArraySubTable({ + column, + _rowy_ref, +}: ISideDrawerFieldProps) { + const [row] = useAtom( + useMemo( + () => + selectAtom( + tableRowsAtom, + (tableRows) => find(tableRows, ["_rowy_ref.path", _rowy_ref.path]), + isEqual + ), + [_rowy_ref.path] + ), + tableScope + ); + + const { documentCount, label, subTablePath } = useSubTableData( + column as any, + row as any, + _rowy_ref + ); + + return ( + + + {documentCount} {column.name as string}: {label} + + + + + + + ); +} diff --git a/src/components/fields/ArraySubTable/index.tsx b/src/components/fields/ArraySubTable/index.tsx new file mode 100644 index 00000000..9e062b65 --- /dev/null +++ b/src/components/fields/ArraySubTable/index.tsx @@ -0,0 +1,36 @@ +import { lazy } from "react"; +import { IFieldConfig, FieldType } from "@src/components/fields/types"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; + +import { ArraySubTable as ArraySubTableIcon } from "@src/assets/icons/ArraySubTable"; +import DisplayCell from "./DisplayCell"; + +const SideDrawerField = lazy( + () => + import( + "./SideDrawerField" /* webpackChunkName: "SideDrawerField-ArraySubTable" */ + ) +); +const Settings = lazy( + () => import("./Settings" /* webpackChunkName: "Settings-ArraySubtable" */) +); +export const config: IFieldConfig = { + type: FieldType.arraySubTable, + name: "Array-Sub-Table", + group: "Connection", + dataType: "undefined", + initialValue: null, + icon: , + settings: Settings, + description: + "Connects to a sub-table in the current row. Also displays number of rows inside the sub-table. Max sub-table depth: 100.", + TableCell: withRenderTableCell(DisplayCell, null, "focus", { + usesRowData: true, + disablePadding: true, + }), + SideDrawerField, + initializable: false, + requireConfiguration: true, + requireCollectionTable: true, +}; +export default config; diff --git a/src/components/fields/ArraySubTable/utils.ts b/src/components/fields/ArraySubTable/utils.ts new file mode 100644 index 00000000..c00f7d7c --- /dev/null +++ b/src/components/fields/ArraySubTable/utils.ts @@ -0,0 +1,34 @@ +import { useLocation } from "react-router-dom"; + +import { ROUTES } from "@src/constants/routes"; +import { ColumnConfig, TableRow, TableRowRef } from "@src/types/table"; + +export const useSubTableData = ( + column: ColumnConfig, + row: TableRow, + _rowy_ref: TableRowRef +) => { + const label = (column.config?.parentLabel ?? []).reduce((acc, curr) => { + if (acc !== "") return `${acc} - ${row[curr]}`; + else return row[curr]; + }, ""); + + const documentCount: string = row[column.fieldName]?.count ?? ""; + + const location = useLocation(); + const rootTablePath = decodeURIComponent( + location.pathname.split("/" + ROUTES.subTable)[0] + ); + + // Get params from URL: /table/:tableId/arraySubTable/:docPath/:arraySubTableKey + let subTablePath = [ + rootTablePath, + ROUTES.arraySubTable, + encodeURIComponent(_rowy_ref.path), + column.key, + ].join("/"); + + subTablePath += "?parentLabel=" + encodeURIComponent(label ?? ""); + + return { documentCount, label, subTablePath }; +}; diff --git a/src/components/fields/CreatedAt/index.tsx b/src/components/fields/CreatedAt/index.tsx index cffa0ea8..a6dab174 100644 --- a/src/components/fields/CreatedAt/index.tsx +++ b/src/components/fields/CreatedAt/index.tsx @@ -27,5 +27,6 @@ export const config: IFieldConfig = { TableCell: withRenderTableCell(DisplayCell, null), SideDrawerField, settings: Settings, + requireCollectionTable: true, }; export default config; diff --git a/src/components/fields/CreatedBy/index.tsx b/src/components/fields/CreatedBy/index.tsx index 98fe8cb6..257da871 100644 --- a/src/components/fields/CreatedBy/index.tsx +++ b/src/components/fields/CreatedBy/index.tsx @@ -28,5 +28,6 @@ export const config: IFieldConfig = { TableCell: withRenderTableCell(DisplayCell, null), SideDrawerField, settings: Settings, + requireCollectionTable: true, }; export default config; diff --git a/src/components/fields/Derivative/index.tsx b/src/components/fields/Derivative/index.tsx index 4c18a0f3..a7ea0600 100644 --- a/src/components/fields/Derivative/index.tsx +++ b/src/components/fields/Derivative/index.tsx @@ -22,5 +22,6 @@ export const config: IFieldConfig = { settingsValidator, requireConfiguration: true, requireCloudFunction: true, + requireCollectionTable: true, }; export default config; diff --git a/src/components/fields/File/EditorCell.tsx b/src/components/fields/File/EditorCell.tsx index a520c594..c175cfc8 100644 --- a/src/components/fields/File/EditorCell.tsx +++ b/src/components/fields/File/EditorCell.tsx @@ -1,4 +1,3 @@ -import { useCallback } from "react"; import { IEditorCellProps } from "@src/components/fields/types"; import { useSetAtom } from "jotai"; @@ -22,11 +21,17 @@ export default function File_({ _rowy_ref, tabIndex, rowHeight, + row: { _rowy_arrayTableData }, }: IEditorCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); const { loading, progress, handleDelete, localFiles, dropzoneState } = - useFileUpload(_rowy_ref, column.key, { multiple: true }); + useFileUpload( + _rowy_ref, + column.key, + { multiple: true }, + _rowy_arrayTableData + ); const { isDragActive, getRootProps, getInputProps } = dropzoneState; const dropzoneProps = getRootProps(); diff --git a/src/components/fields/File/SideDrawerField.tsx b/src/components/fields/File/SideDrawerField.tsx index 00287c23..38be7a84 100644 --- a/src/components/fields/File/SideDrawerField.tsx +++ b/src/components/fields/File/SideDrawerField.tsx @@ -25,10 +25,16 @@ export default function File_({ _rowy_ref, value, disabled, + _rowy_arrayTableData, }: ISideDrawerFieldProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); const { loading, progress, handleDelete, localFiles, dropzoneState } = - useFileUpload(_rowy_ref, column.key, { multiple: true }); + useFileUpload( + _rowy_ref, + column.key, + { multiple: true }, + _rowy_arrayTableData + ); const { isDragActive, getRootProps, getInputProps } = dropzoneState; diff --git a/src/components/fields/File/useFileUpload.ts b/src/components/fields/File/useFileUpload.ts index d99ccf67..a5305cab 100644 --- a/src/components/fields/File/useFileUpload.ts +++ b/src/components/fields/File/useFileUpload.ts @@ -5,12 +5,17 @@ import { DropzoneOptions, useDropzone } from "react-dropzone"; import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; import useUploader from "@src/hooks/useFirebaseStorageUploader"; -import type { FileValue, TableRowRef } from "@src/types/table"; +import type { + ArrayTableRowData, + FileValue, + TableRowRef, +} from "@src/types/table"; export default function useFileUpload( docRef: TableRowRef, fieldName: string, - dropzoneOptions: DropzoneOptions = {} + dropzoneOptions: DropzoneOptions = {}, + arrayTableData?: ArrayTableRowData ) { const updateField = useSetAtom(updateFieldAtom, tableScope); const { uploaderState, upload, deleteUpload } = useUploader(); @@ -47,7 +52,9 @@ export default function useFileUpload( async (files: File[]) => { const { uploads, failures } = await upload({ docRef, - fieldName, + fieldName: arrayTableData + ? `${arrayTableData?.parentField}/${fieldName}` + : fieldName, files, }); updateField({ @@ -55,10 +62,11 @@ export default function useFileUpload( fieldName, value: uploads, useArrayUnion: true, + arrayTableData, }); return { uploads, failures }; }, - [docRef, fieldName, updateField, upload] + [arrayTableData, docRef, fieldName, updateField, upload] ); const handleDelete = useCallback( @@ -69,10 +77,11 @@ export default function useFileUpload( value: [file], useArrayRemove: true, disableCheckEquality: true, + arrayTableData, }); deleteUpload(file); }, - [deleteUpload, docRef, fieldName, updateField] + [arrayTableData, deleteUpload, docRef.path, fieldName, updateField] ); return { diff --git a/src/components/fields/Image/EditorCell.tsx b/src/components/fields/Image/EditorCell.tsx index ceec4307..d79d1b0f 100644 --- a/src/components/fields/Image/EditorCell.tsx +++ b/src/components/fields/Image/EditorCell.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { IEditorCellProps } from "@src/components/fields/types"; -import { useAtom, useSetAtom } from "jotai"; +import { useSetAtom } from "jotai"; import { assignIn } from "lodash-es"; import { alpha, Box, Stack, Grid, IconButton, ButtonBase } from "@mui/material"; @@ -11,8 +11,6 @@ import Thumbnail from "@src/components/Thumbnail"; import CircularProgressOptical from "@src/components/CircularProgressOptical"; import { projectScope, confirmDialogAtom } from "@src/atoms/projectScope"; -import { tableSchemaAtom, tableScope } from "@src/atoms/tableScope"; -import { DEFAULT_ROW_HEIGHT } from "@src/components/Table"; import { FileValue } from "@src/types/table"; import useFileUpload from "@src/components/fields/File/useFileUpload"; import { IMAGE_MIME_TYPES } from "./index"; @@ -25,14 +23,20 @@ export default function Image_({ _rowy_ref, tabIndex, rowHeight, + row: { _rowy_arrayTableData }, }: IEditorCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); const { loading, progress, handleDelete, localFiles, dropzoneState } = - useFileUpload(_rowy_ref, column.key, { - multiple: true, - accept: IMAGE_MIME_TYPES, - }); + useFileUpload( + _rowy_ref, + column.key, + { + multiple: true, + accept: IMAGE_MIME_TYPES, + }, + _rowy_arrayTableData + ); const localImages = useMemo( () => diff --git a/src/components/fields/Image/SideDrawerField.tsx b/src/components/fields/Image/SideDrawerField.tsx index 70c58b88..a21af2c2 100644 --- a/src/components/fields/Image/SideDrawerField.tsx +++ b/src/components/fields/Image/SideDrawerField.tsx @@ -84,6 +84,7 @@ export default function Image_({ _rowy_ref, value, disabled, + _rowy_arrayTableData, }: ISideDrawerFieldProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); @@ -94,10 +95,15 @@ export default function Image_({ uploaderState, localFiles, dropzoneState, - } = useFileUpload(_rowy_ref, column.key, { - multiple: true, - accept: IMAGE_MIME_TYPES, - }); + } = useFileUpload( + _rowy_ref, + column.key, + { + multiple: true, + accept: IMAGE_MIME_TYPES, + }, + _rowy_arrayTableData + ); const localImages = useMemo( () => diff --git a/src/components/fields/SubTable/index.tsx b/src/components/fields/SubTable/index.tsx index 7e153771..574c0a2e 100644 --- a/src/components/fields/SubTable/index.tsx +++ b/src/components/fields/SubTable/index.tsx @@ -31,5 +31,6 @@ export const config: IFieldConfig = { SideDrawerField, initializable: false, requireConfiguration: true, + requireCollectionTable: true, }; export default config; diff --git a/src/components/fields/UpdatedAt/index.tsx b/src/components/fields/UpdatedAt/index.tsx index d6e5eb92..1375347b 100644 --- a/src/components/fields/UpdatedAt/index.tsx +++ b/src/components/fields/UpdatedAt/index.tsx @@ -28,5 +28,6 @@ export const config: IFieldConfig = { TableCell: withRenderTableCell(DisplayCell, null), SideDrawerField, settings: Settings, + requireCollectionTable: true, }; export default config; diff --git a/src/components/fields/UpdatedBy/index.tsx b/src/components/fields/UpdatedBy/index.tsx index 4b1c3a42..c4f733d1 100644 --- a/src/components/fields/UpdatedBy/index.tsx +++ b/src/components/fields/UpdatedBy/index.tsx @@ -29,5 +29,6 @@ export const config: IFieldConfig = { TableCell: withRenderTableCell(DisplayCell, null), SideDrawerField, settings: Settings, + requireCollectionTable: true, }; export default config; diff --git a/src/components/fields/index.ts b/src/components/fields/index.ts index 0d54b0a5..97726ea0 100644 --- a/src/components/fields/index.ts +++ b/src/components/fields/index.ts @@ -26,6 +26,7 @@ import Image_ from "./Image"; import File_ from "./File"; import Connector from "./Connector"; import SubTable from "./SubTable"; +import ArraySubTable from "./ArraySubTable"; import Reference from "./Reference"; import ConnectTable from "./ConnectTable"; import ConnectService from "./ConnectService"; @@ -74,6 +75,7 @@ export const FIELDS: IFieldConfig[] = [ File_, /** CONNECTION */ Connector, + ArraySubTable, SubTable, Reference, ConnectTable, diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index a11e1e10..ffc552f2 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -6,6 +6,7 @@ import type { TableRow, TableRowRef, TableFilter, + ArrayTableRowData, } from "@src/types/table"; import type { SelectedCell } from "@src/atoms/tableScope"; import type { IContextMenuItem } from "@src/components/Table/ContextMenu/ContextMenuItem"; @@ -20,6 +21,7 @@ export interface IFieldConfig { initializable?: boolean; requireConfiguration?: boolean; requireCloudFunction?: boolean; + requireCollectionTable?: boolean; initialValue: any; icon?: React.ReactNode; description?: string; @@ -80,7 +82,8 @@ export interface ISideDrawerFieldProps { column: ColumnConfig; /** The row’s _rowy_ref object */ _rowy_ref: TableRowRef; - + /** The array table row’s data */ + _rowy_arrayTableData?: ArrayTableRowData; /** The field’s local value – synced with db when field is not dirty */ value: T; /** Call when the user has input but changes have not been saved */ diff --git a/src/constants/fields.ts b/src/constants/fields.ts index 900b88db..d91111a4 100644 --- a/src/constants/fields.ts +++ b/src/constants/fields.ts @@ -28,6 +28,7 @@ export enum FieldType { // CONNECTION connector = "CONNECTOR", subTable = "SUB_TABLE", + arraySubTable = "ARRAY_SUB_TABLE", reference = "REFERENCE", connectTable = "DOCUMENT_SELECT", connectService = "SERVICE_SELECT", diff --git a/src/constants/routes.tsx b/src/constants/routes.tsx index af23e2cd..70e8d9eb 100644 --- a/src/constants/routes.tsx +++ b/src/constants/routes.tsx @@ -25,8 +25,10 @@ export enum ROUTES { tableWithId = "/table/:id", /** Nested route: `/table/:id/subTable/...` */ subTable = "subTable", + arraySubTable = "arraySubTable", /** Nested route: `/table/:id/subTable/...` */ subTableWithId = "subTable/:docPath/:subTableKey", + arraySubTableWithId = "arraySubTable/:docPath/:subTableKey", /** @deprecated Redirects to /table */ tableGroup = "/tableGroup", /** @deprecated Redirects to /table */ diff --git a/src/hooks/useFirestoreDocAsCollectionWithAtom.ts b/src/hooks/useFirestoreDocAsCollectionWithAtom.ts new file mode 100644 index 00000000..58affa18 --- /dev/null +++ b/src/hooks/useFirestoreDocAsCollectionWithAtom.ts @@ -0,0 +1,357 @@ +import { useCallback, useEffect } from "react"; +import useMemoValue from "use-memo-value"; +import { useAtom, PrimitiveAtom, useSetAtom } from "jotai"; +import { orderBy } from "lodash-es"; +import { useSnackbar } from "notistack"; + +import { + Firestore, + doc, + refEqual, + onSnapshot, + FirestoreError, + setDoc, + DocumentReference, +} from "firebase/firestore"; +import { useErrorHandler } from "react-error-boundary"; + +import { projectScope } from "@src/atoms/projectScope"; +import { + ArrayTableRowData, + DeleteCollectionDocFunction, + TableRow, + TableSort, + UpdateCollectionDocFunction, +} from "@src/types/table"; +import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase"; +import { omitRowyFields } from "@src/utils/table"; + +/** Options for {@link useFirestoreDocWithAtom} */ +interface IUseFirestoreDocWithAtomOptions { + /** Called when an error occurs. Make sure to wrap in useCallback! If not provided, errors trigger the nearest ErrorBoundary. */ + onError?: (error: FirestoreError) => void; + /** Optionally disable Suspense */ + disableSuspense?: boolean; + /** Optionally create the document if it doesn’t exist with the following data */ + createIfNonExistent?: T; + /** Set this atom’s value to a function that updates the document. Uses same scope as `dataScope`. */ + // updateDataAtom?: PrimitiveAtom | undefined>; + updateDocAtom?: PrimitiveAtom | undefined>; + deleteDocAtom?: PrimitiveAtom; + sorts?: TableSort[]; +} + +/** + * Attaches a listener for a Firestore document and unsubscribes on unmount. + * Gets the Firestore instance initiated in projectScope. + * Updates an atom and Suspends that atom until the first snapshot is received. + * + * @param dataAtom - Atom to store data in + * @param dataScope - Atom scope + * @param path - Document path. If falsy, the listener isn’t created at all. + * @param fieldName - Parent field name + * @param options - {@link IUseFirestoreDocWithAtomOptions} + */ +export function useFirestoreDocAsCollectionWithAtom( + dataAtom: PrimitiveAtom, + dataScope: Parameters[1] | undefined, + path: string, + fieldName: string, + options: IUseFirestoreDocWithAtomOptions +) { + // Destructure options so they can be used as useEffect dependencies + const { + onError, + disableSuspense, + createIfNonExistent, + updateDocAtom, + deleteDocAtom, + sorts, + } = options || {}; + + const [firebaseDb] = useAtom(firebaseDbAtom, projectScope); + const setDataAtom = useSetAtom(dataAtom, dataScope); + + const handleError = useErrorHandler(); + const { enqueueSnackbar } = useSnackbar(); + const setUpdateDocAtom = useSetAtom( + updateDocAtom || (dataAtom as any), + dataScope + ); + const setDeleteRowAtom = useSetAtom( + deleteDocAtom || (dataAtom as any), + dataScope + ); + + // Create the doc ref and memoize using Firestore’s refEqual + const memoizedDocRef = useMemoValue( + getDocRef(firebaseDb, path), + (next, prev) => refEqual(next as any, prev as any) + ); + + useEffect(() => { + // If path is invalid and no memoizedDocRef was created, don’t continue + if (!memoizedDocRef) return; + + // Suspend data atom until we get the first snapshot + let suspended = false; + if (!disableSuspense) { + setDataAtom(new Promise(() => []) as unknown as T[]); + suspended = true; + } + + // Create a listener for the document + const unsubscribe = onSnapshot( + memoizedDocRef, + { includeMetadataChanges: true }, + (docSnapshot) => { + try { + if (docSnapshot.exists() && docSnapshot.data() !== undefined) { + const pseudoDoc = docSnapshot.get(fieldName) || []; + const pseudoRow = pseudoDoc.map((row: any, i: number) => { + return { + ...row, + _rowy_ref: docSnapshot.ref, + _rowy_arrayTableData: { + index: i, + parentField: fieldName, + }, + }; + }); + const sorted = sortRows(pseudoRow, sorts); + setDataAtom(sorted); + } else { + enqueueSnackbar(`Array table doesn't exist`, { + variant: "error", + }); + // console.log("docSnapshot", docSnapshot.data()); + // setDataAtom([] as T[]); + } + } catch (error) { + if (onError) onError(error as FirestoreError); + else handleError(error); + } + suspended = false; + }, + (error) => { + if (suspended) setDataAtom([] as T[]); + if (onError) onError(error); + else handleError(error); + } + ); + + // When the listener will change, unsubscribe + return () => { + unsubscribe(); + }; + }, [ + memoizedDocRef, + onError, + setDataAtom, + disableSuspense, + createIfNonExistent, + handleError, + fieldName, + sorts, + enqueueSnackbar, + ]); + + const setRows = useCallback( + (rows: T[]) => { + rows = rows.map((row: any, i: number) => omitRowyFields(row)); + if (!fieldName) return; + try { + return setDoc( + doc(firebaseDb, path), + { [fieldName]: rows }, + { merge: true } + ); + } catch (error) { + enqueueSnackbar(`Error updating array table`, { + variant: "error", + }); + return; + } + }, + [enqueueSnackbar, fieldName, firebaseDb, path] + ); + + useEffect(() => { + if (deleteDocAtom) { + setDeleteRowAtom(() => (_: string, options?: ArrayTableRowData) => { + if (!options) return; + + const deleteRow = () => { + let temp: T[] = []; + setDataAtom((prevData) => { + temp = unsortRows(prevData); + temp.splice(options.index, 1); + for (let i = options.index; i < temp.length; i++) { + // @ts-ignore + temp[i]._rowy_arrayTableData.index = i; + } + return sortRows(temp, sorts); + }); + return setRows(temp); + }; + deleteRow(); + }); + } + }, [ + deleteDocAtom, + firebaseDb, + path, + setDataAtom, + setDeleteRowAtom, + setRows, + sorts, + ]); + + useEffect(() => { + if (updateDocAtom) { + setUpdateDocAtom( + () => + ( + path_: string, + update: T, + deleteFields?: string[], + options?: ArrayTableRowData + ) => { + if (options === undefined) return; + + const deleteRowFields = () => { + let temp: T[] = []; + setDataAtom((prevData) => { + temp = unsortRows(prevData); + + if (deleteFields === undefined) return prevData; + + temp[options.index] = { + ...temp[options.index], + ...deleteFields?.reduce( + (acc, field) => ({ ...acc, [field]: undefined }), + {} + ), + }; + + return sortRows(temp, sorts); + }); + + return setRows(temp); + }; + + const updateRowValues = () => { + let temp: T[] = []; + setDataAtom((prevData) => { + temp = unsortRows(prevData); + + temp[options.index] = { + ...temp[options.index], + ...update, + }; + return sortRows(temp, sorts); + }); + return setRows(temp); + }; + + const addNewRow = (addTo: "top" | "bottom", base?: TableRow) => { + let temp: T[] = []; + + const newRow = (i: number) => + ({ + ...base, + _rowy_ref: { + id: doc(firebaseDb, path).id, + path: doc(firebaseDb, path).path, + }, + _rowy_arrayTableData: { + index: i, + parentField: fieldName, + }, + } as T); + + setDataAtom((prevData) => { + temp = unsortRows(prevData); + + if (addTo === "bottom") { + temp.push(newRow(prevData.length)); + } else { + const modifiedPrevData = temp.map((row: any, i: number) => { + return { + ...row, + _rowy_arrayTableData: { + index: i + 1, + }, + }; + }); + temp = [newRow(0), ...modifiedPrevData]; + } + return sortRows(temp, sorts); + }); + + return setRows(temp); + }; + + if (Array.isArray(deleteFields) && deleteFields.length > 0) { + return deleteRowFields(); + } else if (options.operation?.addRow) { + return addNewRow( + options.operation.addRow, + options?.operation.base + ); + } else { + return updateRowValues(); + } + } + ); + } + }, [ + fieldName, + firebaseDb, + path, + setDataAtom, + setRows, + setUpdateDocAtom, + sorts, + updateDocAtom, + ]); +} + +export default useFirestoreDocAsCollectionWithAtom; + +/** + * Create the Firestore document reference. + * Put code in a function so the results can be compared by useMemoValue. + */ +export const getDocRef = ( + firebaseDb: Firestore, + path: string | undefined, + pathSegments?: Array +) => { + if (!path || (Array.isArray(pathSegments) && pathSegments?.some((x) => !x))) + return null; + + return doc( + firebaseDb, + path, + ...((pathSegments as string[]) || []) + ) as DocumentReference; +}; + +function sortRows( + rows: T[], + sorts: TableSort[] | undefined +): T[] { + if (sorts === undefined || sorts.length < 1) { + return rows; + } + + const order: "asc" | "desc" = + sorts[0].direction === undefined ? "asc" : sorts[0].direction; + + return orderBy(rows, [sorts[0].key], [order]); +} + +function unsortRows(rows: T[]): T[] { + return orderBy(rows, ["_rowy_arrayTableData.index"], ["asc"]); +} diff --git a/src/pages/Table/ProvidedArraySubTablePage.tsx b/src/pages/Table/ProvidedArraySubTablePage.tsx new file mode 100644 index 00000000..1e6e256c --- /dev/null +++ b/src/pages/Table/ProvidedArraySubTablePage.tsx @@ -0,0 +1,156 @@ +import { lazy, Suspense, useMemo } from "react"; +import { useAtom, Provider } from "jotai"; +import { selectAtom } from "jotai/utils"; +import { DebugAtoms } from "@src/atoms/utils"; +import { ErrorBoundary } from "react-error-boundary"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { find, isEqual } from "lodash-es"; + +import Modal from "@src/components/Modal"; +import BreadcrumbsSubTable from "@src/components/Table/Breadcrumbs/BreadcrumbsSubTable"; +import ErrorFallback from "@src/components/ErrorFallback"; +import ArraySubTableSourceFirestore from "@src/sources/TableSourceFirestore/ArraySubTableSourceFirestore"; +import TableToolbarSkeleton from "@src/components/TableToolbar/TableToolbarSkeleton"; +import TableSkeleton from "@src/components/Table/TableSkeleton"; + +import { projectScope, currentUserAtom } from "@src/atoms/projectScope"; +import { + tableScope, + tableIdAtom, + tableSettingsAtom, + tableSchemaAtom, +} from "@src/atoms/tableScope"; +import { ROUTES } from "@src/constants/routes"; +import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar"; +import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar"; + +// prettier-ignore +const TablePage = lazy(() => import("./TablePage" /* webpackChunkName: "TablePage" */)); + +/** + * Wraps `TablePage` with the data for a array-sub-table. + * + * Differences to `ProvidedTablePage`: + * - Renders a `Modal` + * - When this is a child of `ProvidedTablePage`, the `TablePage` rendered for + * the root table has its modals disabled + */ +export default function ProvidedArraySubTablePage() { + const location = useLocation(); + const navigate = useNavigate(); + // Get params from URL: /arraySubTable/:docPath/:subTableKey + const { docPath, subTableKey } = useParams(); + + const [currentUser] = useAtom(currentUserAtom, projectScope); + + // Get table settings and the source column from root table + const [rootTableSettings] = useAtom(tableSettingsAtom, tableScope); + const [sourceColumn] = useAtom( + useMemo( + () => + selectAtom( + tableSchemaAtom, + (tableSchema) => find(tableSchema.columns, ["key", subTableKey]), + isEqual + ), + [subTableKey] + ), + tableScope + ); + + // Consumed by children as `tableSettings.collection` + const subTableCollection = docPath ?? ""; // + "/" + (sourceColumn?.fieldName || subTableKey); + + // Must be compatible with `getTableSchemaPath`: tableId/rowId/subTableKey + // This is why we can’t have a sub-table column fieldName !== key + const subTableId = + docPath?.replace(rootTableSettings.collection, rootTableSettings.id) + + "/" + + subTableKey; + + // Write fake tableSettings + const subTableSettings = { + ...rootTableSettings, + collection: subTableCollection, + id: subTableId, + subTableKey, + isNotACollection: true, + tableType: "primaryCollection" as "primaryCollection", + name: sourceColumn?.name || subTableKey || "", + }; + + const rootTableLink = location.pathname.split("/" + ROUTES.arraySubTable)[0]; + + return ( + + } + onClose={() => navigate(rootTableLink)} + disableBackdropClick + disableEscapeKeyDown + fullScreen + sx={{ + "& > .MuiDialog-container > .MuiPaper-root": { + bgcolor: "background.default", + backgroundImage: "none", + }, + "& .modal-title-row": { + height: TOP_BAR_HEIGHT, + "& .MuiDialogTitle-root": { + px: 2, + py: (TOP_BAR_HEIGHT - 28) / 2 / 8, + }, + "& .dialog-close": { m: (TOP_BAR_HEIGHT - 40) / 2 / 8, ml: -1 }, + }, + "& .table-container": { + height: `calc(100vh - ${TOP_BAR_HEIGHT}px - ${TABLE_TOOLBAR_HEIGHT}px - 16px)`, + }, + }} + ScrollableDialogContentProps={{ + disableTopDivider: true, + disableBottomDivider: true, + style: { "--dialog-spacing": 0, "--dialog-contents-spacing": 0 } as any, + }} + BackdropProps={{ key: "sub-table-modal-backdrop" }} + > + + + + + + } + > + + + + + + + + + ); +} diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 04fc0e61..d8e85c85 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -41,6 +41,7 @@ import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar"; import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar"; import { DRAWER_COLLAPSED_WIDTH } from "@src/components/SideDrawer"; import { formatSubTableName } from "@src/utils/table"; +import { TableToolsType } from "@src/types/table"; // prettier-ignore const BuildLogsSnack = lazy(() => import("@src/components/TableModals/CloudLogsModal/BuildLogs/BuildLogsSnack" /* webpackChunkName: "TableModals-BuildLogsSnack" */)); @@ -53,6 +54,10 @@ export interface ITablePageProps { disableModals?: boolean; /** Disable side drawer */ disableSideDrawer?: boolean; + /* Array table is not a collection */ + tableNotACollection?: boolean; + + disabledTools?: TableToolsType; } /** @@ -71,6 +76,8 @@ export interface ITablePageProps { export default function TablePage({ disableModals, disableSideDrawer, + tableNotACollection, + disabledTools, }: ITablePageProps) { const [userRoles] = useAtom(userRolesAtom, projectScope); const [userSettings] = useAtom(userSettingsAtom, projectScope); @@ -127,7 +134,7 @@ export default function TablePage({ }> - + diff --git a/src/sources/TableSourceFirestore/ArraySubTableSourceFirestore.tsx b/src/sources/TableSourceFirestore/ArraySubTableSourceFirestore.tsx new file mode 100644 index 00000000..e7920b01 --- /dev/null +++ b/src/sources/TableSourceFirestore/ArraySubTableSourceFirestore.tsx @@ -0,0 +1,143 @@ +import { memo, useCallback, useEffect } from "react"; +import { useAtom, useSetAtom } from "jotai"; +import useMemoValue from "use-memo-value"; +import { cloneDeep, set } from "lodash-es"; +import { + FirestoreError, + deleteField, + refEqual, + setDoc, +} from "firebase/firestore"; +import { useSnackbar } from "notistack"; +import { useErrorHandler } from "react-error-boundary"; + +import { + tableScope, + tableSettingsAtom, + tableSchemaAtom, + updateTableSchemaAtom, + tableSortsAtom, + tableRowsDbAtom, + _updateRowDbAtom, + _deleteRowDbAtom, + tableNextPageAtom, +} from "@src/atoms/tableScope"; + +import useFirestoreDocWithAtom, { + getDocRef, +} from "@src/hooks/useFirestoreDocWithAtom"; + +import useAuditChange from "./useAuditChange"; +import useBulkWriteDb from "./useBulkWriteDb"; +import { handleFirestoreError } from "./handleFirestoreError"; + +import { getTableSchemaPath } from "@src/utils/table"; +import { TableRow, TableSchema } from "@src/types/table"; +import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase"; +import { projectScope } from "@src/atoms/projectScope"; +import useFirestoreDocAsCollectionWithAtom from "@src/hooks/useFirestoreDocAsCollectionWithAtom"; + +/** + * When rendered, provides atom values for top-level tables and sub-tables + */ +export const TableSourceFirestore2 = memo(function TableSourceFirestore() { + const [firebaseDb] = useAtom(firebaseDbAtom, projectScope); + const [tableSettings] = useAtom(tableSettingsAtom, tableScope); + const setTableSchema = useSetAtom(tableSchemaAtom, tableScope); + const setUpdateTableSchema = useSetAtom(updateTableSchemaAtom, tableScope); + const setTableNextPage = useSetAtom(tableNextPageAtom, tableScope); + const { enqueueSnackbar } = useSnackbar(); + + if (!tableSettings) throw new Error("No table config"); + if (!tableSettings.collection) + throw new Error("Invalid table config: no collection"); + + const tableSchemaDocRef = useMemoValue( + getDocRef(firebaseDb, getTableSchemaPath(tableSettings)), + (next, prev) => refEqual(next as any, prev as any) + ); + + setTableNextPage({ + loading: false, + available: false, + }); + useEffect(() => { + if (!tableSchemaDocRef) return; + + setUpdateTableSchema( + () => (update: TableSchema, deleteFields?: string[]) => { + const updateToDb = cloneDeep(update); + + if (Array.isArray(deleteFields)) { + for (const field of deleteFields) { + // Use deterministic set firestore sentinel's on schema columns config + // Required for nested columns + // i.e field = "columns.base.nested.nested" + // key: columns, rest: base.nested.nested + // set columns["base.nested.nested"] instead columns.base.nested.nested + const [key, ...rest] = field.split("."); + if (key === "columns") { + (updateToDb as any).columns[rest.join(".")] = deleteField(); + } else { + set(updateToDb, field, deleteField()); + } + } + } + + // Update UI state to reflect changes immediately to prevent flickering effects + setTableSchema((tableSchema) => ({ ...tableSchema, ...update })); + + return setDoc(tableSchemaDocRef, updateToDb, { merge: true }).catch( + (e) => { + enqueueSnackbar((e as Error).message, { variant: "error" }); + } + ); + } + ); + + return () => { + setUpdateTableSchema(undefined); + }; + }, [tableSchemaDocRef, setTableSchema, setUpdateTableSchema, enqueueSnackbar]); + + // Get tableSchema and store in tableSchemaAtom. + // If it doesn’t exist, initialize columns + useFirestoreDocWithAtom( + tableSchemaAtom, + tableScope, + getTableSchemaPath(tableSettings), + { + createIfNonExistent: { columns: {} }, + disableSuspense: true, + } + ); + + // Get table sorts + const [sorts] = useAtom(tableSortsAtom, tableScope); + // Get documents from collection and store in tableRowsDbAtom + // and handle some errors with snackbars + const elevateError = useErrorHandler(); + const handleErrorCallback = useCallback( + (error: FirestoreError) => + handleFirestoreError(error, enqueueSnackbar, elevateError), + [enqueueSnackbar, elevateError] + ); + useFirestoreDocAsCollectionWithAtom( + tableRowsDbAtom, + tableScope, + tableSettings.collection, + tableSettings.subTableKey || "", + { + sorts, + onError: handleErrorCallback, + updateDocAtom: _updateRowDbAtom, + deleteDocAtom: _deleteRowDbAtom, + } + ); + useAuditChange(); + useBulkWriteDb(); + + return null; +}); + +export default TableSourceFirestore2; diff --git a/src/types/table.d.ts b/src/types/table.d.ts index 46a5a939..7b8787fd 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -31,7 +31,8 @@ export type UpdateDocFunction = ( export type UpdateCollectionDocFunction = ( path: string, update: Partial, - deleteFields?: string[] + deleteFields?: string[], + options?: ArrayTableRowData ) => Promise; /** @@ -39,7 +40,10 @@ export type UpdateCollectionDocFunction = ( * @param path - The full path to the doc * @returns Promise */ -export type DeleteCollectionDocFunction = (path: string) => Promise; +export type DeleteCollectionDocFunction = ( + path: string, + options?: ArrayTableRowData +) => Promise; export type BulkWriteOperation = | { type: "delete"; path: string } @@ -71,6 +75,8 @@ export type TableSettings = { /** Roles that can see this table in the UI and navigate. Firestore Rules need to be set to give access to the data */ roles: string[]; + isNotACollection?: boolean; + subTableKey?: string | undefined; section: string; description?: string; details?: string; @@ -187,6 +193,15 @@ export type TableFilter = { value: any; }; +export const TableTools = [ + "import", + "export", + "webhooks", + "extensions", + "cloud_logs", +] as const; +export type TableToolsType = typeof Tools[number]; + export type TableSort = { key: string; direction: Parameters[1]; @@ -197,10 +212,20 @@ export type TableRowRef = { path: string; } & Partial; +type ArrayTableOperations = { + addRow?: "top" | "bottom"; + base?: TableRow; +}; +export type ArrayTableRowData = { + index: number; + parentField?: string; + operation?: ArrayTableOperations; +}; export type TableRow = DocumentData & { _rowy_ref: TableRowRef; _rowy_missingRequiredFields?: string[]; _rowy_outOfOrder?: boolean; + _rowy_arrayTableData?: ArrayTableRowData; }; export type FileValue = { diff --git a/src/utils/table.ts b/src/utils/table.ts index 691dd385..7a143a8b 100644 --- a/src/utils/table.ts +++ b/src/utils/table.ts @@ -51,6 +51,7 @@ export const omitRowyFields = >(row: T) => { delete shallowClonedRow["_rowy_outOfOrder"]; delete shallowClonedRow["_rowy_missingRequiredFields"]; delete shallowClonedRow["_rowy_new"]; + delete shallowClonedRow["_rowy_arrayTableData"]; return shallowClonedRow as T; }; From 8b5d2a353d4b19de2be7f4f22569a264d6fac225 Mon Sep 17 00:00:00 2001 From: Prabhat Sachdeva Date: Thu, 13 Apr 2023 12:38:20 +0530 Subject: [PATCH 054/183] Fixed a typo, changed clilboard to clipboard --- src/components/Table/useMenuAction.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Table/useMenuAction.tsx b/src/components/Table/useMenuAction.tsx index 516124d3..10b2bf76 100644 --- a/src/components/Table/useMenuAction.tsx +++ b/src/components/Table/useMenuAction.tsx @@ -92,7 +92,7 @@ export function useMenuAction( try { text = await navigator.clipboard.readText(); } catch (e) { - enqueueSnackbar(`Read clilboard permission denied.`, { + enqueueSnackbar(`Read clipboard permission denied.`, { variant: "error", }); return; From af071d8f580afdf9032f539185c3bf7e24857db7 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Thu, 13 Apr 2023 13:30:33 +0530 Subject: [PATCH 055/183] airtable date parser fix --- src/components/TableModals/ImportAirtableWizard/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TableModals/ImportAirtableWizard/utils.ts b/src/components/TableModals/ImportAirtableWizard/utils.ts index 89130dcb..2659e2ad 100644 --- a/src/components/TableModals/ImportAirtableWizard/utils.ts +++ b/src/components/TableModals/ImportAirtableWizard/utils.ts @@ -67,7 +67,7 @@ export const fieldParser = (fieldType: FieldType) => { case FieldType.dateTime: return (v: string) => { const date = parseISO(v); - return isValidDate(date) ? date.getTime() : null; + return isValidDate(date) ? new Date(date) : null; }; default: return (v: any) => v; From dd481415fca4d782ebc4c611aca8b61c6ea6362a Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 14 Apr 2023 17:56:12 +0530 Subject: [PATCH 056/183] removed _rowy_arrayTableData and expanded _rowy_ref --- src/components/SideDrawer/MemoizedField.tsx | 5 +-- src/components/SideDrawer/SideDrawer.tsx | 7 +++-- .../SideDrawer/SideDrawerFields.tsx | 7 ++--- .../Table/ContextMenu/MenuContents.tsx | 12 +++---- .../Table/FinalColumn/FinalColumn.tsx | 10 +++--- src/components/Table/Table.tsx | 2 -- src/components/Table/TableBody.tsx | 2 +- .../Table/TableCell/EditorCellController.tsx | 2 +- src/components/Table/TableCell/TableCell.tsx | 8 ++--- .../Table/useKeyboardNavigation.tsx | 2 +- src/components/Table/useMenuAction.tsx | 2 +- src/components/fields/File/EditorCell.tsx | 8 +---- .../fields/File/SideDrawerField.tsx | 8 +---- src/components/fields/File/useFileUpload.ts | 21 +++++-------- src/components/fields/Image/EditorCell.tsx | 14 +++------ .../fields/Image/SideDrawerField.tsx | 14 +++------ src/components/fields/types.ts | 3 -- .../useFirestoreDocAsCollectionWithAtom.ts | 31 ++++++++++++------- src/types/table.d.ts | 14 +++++---- src/utils/table.ts | 1 - 20 files changed, 72 insertions(+), 101 deletions(-) diff --git a/src/components/SideDrawer/MemoizedField.tsx b/src/components/SideDrawer/MemoizedField.tsx index 8f7b7766..0fb62f62 100644 --- a/src/components/SideDrawer/MemoizedField.tsx +++ b/src/components/SideDrawer/MemoizedField.tsx @@ -5,7 +5,7 @@ import { isEqual, isEmpty } from "lodash-es"; import FieldWrapper from "./FieldWrapper"; import { IFieldConfig } from "@src/components/fields/types"; import { getFieldProp } from "@src/components/fields"; -import { ArrayTableRowData, ColumnConfig, TableRowRef } from "@src/types/table"; +import { ColumnConfig, TableRowRef } from "@src/types/table"; export interface IMemoizedFieldProps { field: ColumnConfig; @@ -13,7 +13,6 @@ export interface IMemoizedFieldProps { hidden: boolean; value: any; _rowy_ref: TableRowRef; - _rowy_arrayTableData?: ArrayTableRowData; isDirty: boolean; onDirty: (fieldName: string) => void; onSubmit: (fieldName: string, value: any) => void; @@ -26,7 +25,6 @@ export const MemoizedField = memo( hidden, value, _rowy_ref, - _rowy_arrayTableData, isDirty, onDirty, onSubmit, @@ -80,7 +78,6 @@ export const MemoizedField = memo( }, onSubmit: handleSubmit, disabled, - _rowy_arrayTableData, })} ); diff --git a/src/components/SideDrawer/SideDrawer.tsx b/src/components/SideDrawer/SideDrawer.tsx index faeb15ad..49df7339 100644 --- a/src/components/SideDrawer/SideDrawer.tsx +++ b/src/components/SideDrawer/SideDrawer.tsx @@ -35,7 +35,7 @@ export default function SideDrawer() { cell?.arrayIndex === undefined ? ["_rowy_ref.path", cell?.path] : // if the table is an array table, we need to use the array index to find the row - ["_rowy_arrayTableData.index", cell?.arrayIndex] + ["_rowy_ref.arrayTableData.index", cell?.arrayIndex] ); const selectedCellRowIndex = findIndex( @@ -43,7 +43,7 @@ export default function SideDrawer() { cell?.arrayIndex === undefined ? ["_rowy_ref.path", cell?.path] : // if the table is an array table, we need to use the array index to find the row - ["_rowy_arrayTableData.index", cell?.arrayIndex] + ["_rowy_ref.arrayTableData.index", cell?.arrayIndex] ); const handleNavigate = (direction: "up" | "down") => () => { @@ -55,8 +55,9 @@ export default function SideDrawer() { setCell((cell) => ({ columnKey: cell!.columnKey, - path: newPath, + path: cell?.arrayIndex !== undefined ? cell.path : newPath, focusInside: false, + arrayIndex: cell?.arrayIndex !== undefined ? rowIndex : undefined, })); }; diff --git a/src/components/SideDrawer/SideDrawerFields.tsx b/src/components/SideDrawer/SideDrawerFields.tsx index e2abdfa4..2f1e0b8a 100644 --- a/src/components/SideDrawer/SideDrawerFields.tsx +++ b/src/components/SideDrawer/SideDrawerFields.tsx @@ -130,7 +130,6 @@ export default function SideDrawerFields({ row }: ISideDrawerFieldsProps) { onDirty={onDirty} onSubmit={onSubmit} isDirty={dirtyField === field.key} - _rowy_arrayTableData={row._rowy_arrayTableData} /> ))} @@ -139,12 +138,12 @@ export default function SideDrawerFields({ row }: ISideDrawerFieldsProps) { fieldName="_rowy_ref.path" label="Document path" debugText={ - row._rowy_arrayTableData + row._rowy_ref.arrayTableData ? row._rowy_ref.path + " → " + - row._rowy_arrayTableData.parentField + + row._rowy_ref.arrayTableData.parentField + "[" + - row._rowy_arrayTableData.index + + row._rowy_ref.arrayTableData.index + "]" : row._rowy_ref.path } diff --git a/src/components/Table/ContextMenu/MenuContents.tsx b/src/components/Table/ContextMenu/MenuContents.tsx index 5154ba8b..5437c2dd 100644 --- a/src/components/Table/ContextMenu/MenuContents.tsx +++ b/src/components/Table/ContextMenu/MenuContents.tsx @@ -69,7 +69,7 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { selectedCell?.arrayIndex === undefined ? ["_rowy_ref.path", selectedCell.path] : // if the table is an array table, we need to use the array index to find the row - ["_rowy_arrayTableData.index", selectedCell.arrayIndex] + ["_rowy_ref.arrayTableData.index", selectedCell.arrayIndex] ); if (!row) return null; @@ -78,11 +78,11 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { const handleDuplicate = () => { const _duplicate = () => { - if (row._rowy_arrayTableData !== undefined) { + if (row._rowy_ref.arrayTableData !== undefined) { if (!updateRowDb) return; return updateRowDb("", {}, undefined, { - index: row._rowy_arrayTableData.index, + index: row._rowy_ref.arrayTableData.index, operation: { addRow: "bottom", base: row, @@ -95,7 +95,7 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { }); }; - if (altPress || row._rowy_arrayTableData !== undefined) { + if (altPress || row._rowy_ref.arrayTableData !== undefined) { _duplicate(); } else { confirm({ @@ -118,10 +118,10 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { const _delete = () => deleteRow({ path: row._rowy_ref.path, - options: row._rowy_arrayTableData, + options: row._rowy_ref.arrayTableData, }); - if (altPress || row._rowy_arrayTableData !== undefined) { + if (altPress || row._rowy_ref.arrayTableData !== undefined) { _delete(); } else { confirm({ diff --git a/src/components/Table/FinalColumn/FinalColumn.tsx b/src/components/Table/FinalColumn/FinalColumn.tsx index 041d3f0f..16f6ee68 100644 --- a/src/components/Table/FinalColumn/FinalColumn.tsx +++ b/src/components/Table/FinalColumn/FinalColumn.tsx @@ -43,9 +43,9 @@ export const FinalColumn = memo(function FinalColumn({ const _delete = () => deleteRow({ path: row.original._rowy_ref.path, - options: row.original._rowy_arrayTableData, + options: row.original._rowy_ref.arrayTableData, }); - if (altPress || row.original._rowy_arrayTableData !== undefined) { + if (altPress || row.original._rowy_ref.arrayTableData !== undefined) { _delete(); } else { confirm({ @@ -68,11 +68,11 @@ export const FinalColumn = memo(function FinalColumn({ const handleDuplicate = () => { const _duplicate = () => { - if (row.original._rowy_arrayTableData !== undefined) { + if (row.original._rowy_ref.arrayTableData !== undefined) { if (!updateRowDb) return; return updateRowDb("", {}, undefined, { - index: row.original._rowy_arrayTableData.index, + index: row.original._rowy_ref.arrayTableData.index, operation: { addRow: "bottom", base: row.original, @@ -84,7 +84,7 @@ export const FinalColumn = memo(function FinalColumn({ setId: addRowIdType === "custom" ? "decrement" : addRowIdType, }); }; - if (altPress || row.original._rowy_arrayTableData !== undefined) { + if (altPress || row.original._rowy_ref.arrayTableData !== undefined) { _duplicate(); } else { confirm({ diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index b99c1129..d930b794 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -200,8 +200,6 @@ export default function Table({ if (result.destination?.index === undefined || !result.draggableId) return; - console.log(result.draggableId, result.destination.index); - updateColumn({ key: result.draggableId, index: result.destination.index, diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx index edaed491..3292ceab 100644 --- a/src/components/Table/TableBody.tsx +++ b/src/components/Table/TableBody.tsx @@ -105,7 +105,7 @@ export const TableBody = memo(function TableBody({ selectedCell?.columnKey === cell.column.id && // if the table is an array sub table, we need to check the array index as well selectedCell?.arrayIndex === - row.original._rowy_arrayTableData?.index; + row.original._rowy_ref.arrayTableData?.index; const fieldTypeGroup = getFieldProp( "group", diff --git a/src/components/Table/TableCell/EditorCellController.tsx b/src/components/Table/TableCell/EditorCellController.tsx index cacc8946..217c40eb 100644 --- a/src/components/Table/TableCell/EditorCellController.tsx +++ b/src/components/Table/TableCell/EditorCellController.tsx @@ -66,7 +66,7 @@ export default function EditorCellController({ fieldName: props.column.fieldName, value: localValueRef.current, deleteField: localValueRef.current === undefined, - arrayTableData: props.row?._rowy_arrayTableData, + arrayTableData: props.row?._rowy_ref.arrayTableData, }); } catch (e) { enqueueSnackbar((e as Error).message, { variant: "error" }); diff --git a/src/components/Table/TableCell/TableCell.tsx b/src/components/Table/TableCell/TableCell.tsx index 5c664b35..f7e501fb 100644 --- a/src/components/Table/TableCell/TableCell.tsx +++ b/src/components/Table/TableCell/TableCell.tsx @@ -123,7 +123,7 @@ export const TableCell = memo(function TableCell({ focusInsideCell, setFocusInsideCell: (focusInside: boolean) => setSelectedCell({ - arrayIndex: row.original._rowy_arrayTableData?.index, + arrayIndex: row.original._rowy_ref.arrayTableData?.index, path: row.original._rowy_ref.path, columnKey: cell.column.id, focusInside, @@ -167,7 +167,7 @@ export const TableCell = memo(function TableCell({ }} onClick={(e) => { setSelectedCell({ - arrayIndex: row.original._rowy_arrayTableData?.index, + arrayIndex: row.original._rowy_ref.arrayTableData?.index, path: row.original._rowy_ref.path, columnKey: cell.column.id, focusInside: false, @@ -176,7 +176,7 @@ export const TableCell = memo(function TableCell({ }} onDoubleClick={(e) => { setSelectedCell({ - arrayIndex: row.original._rowy_arrayTableData?.index, + arrayIndex: row.original._rowy_ref.arrayTableData?.index, path: row.original._rowy_ref.path, columnKey: cell.column.id, focusInside: true, @@ -186,7 +186,7 @@ export const TableCell = memo(function TableCell({ onContextMenu={(e) => { e.preventDefault(); setSelectedCell({ - arrayIndex: row.original._rowy_arrayTableData?.index, + arrayIndex: row.original._rowy_ref.arrayTableData?.index, path: row.original._rowy_ref.path, columnKey: cell.column.id, focusInside: false, diff --git a/src/components/Table/useKeyboardNavigation.tsx b/src/components/Table/useKeyboardNavigation.tsx index 81141a77..7c18a5b5 100644 --- a/src/components/Table/useKeyboardNavigation.tsx +++ b/src/components/Table/useKeyboardNavigation.tsx @@ -130,7 +130,7 @@ export function useKeyboardNavigation({ columnKey: leafColumns[newColIndex].id! || leafColumns[0].id!, arrayIndex: newRowIndex > -1 - ? tableRows[newRowIndex]._rowy_arrayTableData?.index + ? tableRows[newRowIndex]._rowy_ref.arrayTableData?.index : undefined, // When selected cell changes, exit current cell diff --git a/src/components/Table/useMenuAction.tsx b/src/components/Table/useMenuAction.tsx index 49cdc2d8..e7982313 100644 --- a/src/components/Table/useMenuAction.tsx +++ b/src/components/Table/useMenuAction.tsx @@ -142,7 +142,7 @@ export function useMenuAction( selectedCell.arrayIndex === undefined ? ["_rowy_ref.path", selectedCell.path] : // if the table is an array table, we need to use the array index to find the row - ["_rowy_arrayTableData.index", selectedCell.arrayIndex] + ["_rowy_ref.arrayTableData.index", selectedCell.arrayIndex] ); setCellValue(get(selectedRow, selectedCol.fieldName)); }, [selectedCell, tableSchema, tableRows]); diff --git a/src/components/fields/File/EditorCell.tsx b/src/components/fields/File/EditorCell.tsx index c175cfc8..487e9fee 100644 --- a/src/components/fields/File/EditorCell.tsx +++ b/src/components/fields/File/EditorCell.tsx @@ -21,17 +21,11 @@ export default function File_({ _rowy_ref, tabIndex, rowHeight, - row: { _rowy_arrayTableData }, }: IEditorCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); const { loading, progress, handleDelete, localFiles, dropzoneState } = - useFileUpload( - _rowy_ref, - column.key, - { multiple: true }, - _rowy_arrayTableData - ); + useFileUpload(_rowy_ref, column.key, { multiple: true }); const { isDragActive, getRootProps, getInputProps } = dropzoneState; const dropzoneProps = getRootProps(); diff --git a/src/components/fields/File/SideDrawerField.tsx b/src/components/fields/File/SideDrawerField.tsx index 38be7a84..00287c23 100644 --- a/src/components/fields/File/SideDrawerField.tsx +++ b/src/components/fields/File/SideDrawerField.tsx @@ -25,16 +25,10 @@ export default function File_({ _rowy_ref, value, disabled, - _rowy_arrayTableData, }: ISideDrawerFieldProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); const { loading, progress, handleDelete, localFiles, dropzoneState } = - useFileUpload( - _rowy_ref, - column.key, - { multiple: true }, - _rowy_arrayTableData - ); + useFileUpload(_rowy_ref, column.key, { multiple: true }); const { isDragActive, getRootProps, getInputProps } = dropzoneState; diff --git a/src/components/fields/File/useFileUpload.ts b/src/components/fields/File/useFileUpload.ts index a5305cab..40f4c394 100644 --- a/src/components/fields/File/useFileUpload.ts +++ b/src/components/fields/File/useFileUpload.ts @@ -5,17 +5,12 @@ import { DropzoneOptions, useDropzone } from "react-dropzone"; import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; import useUploader from "@src/hooks/useFirebaseStorageUploader"; -import type { - ArrayTableRowData, - FileValue, - TableRowRef, -} from "@src/types/table"; +import type { FileValue, TableRowRef } from "@src/types/table"; export default function useFileUpload( docRef: TableRowRef, fieldName: string, - dropzoneOptions: DropzoneOptions = {}, - arrayTableData?: ArrayTableRowData + dropzoneOptions: DropzoneOptions = {} ) { const updateField = useSetAtom(updateFieldAtom, tableScope); const { uploaderState, upload, deleteUpload } = useUploader(); @@ -52,8 +47,8 @@ export default function useFileUpload( async (files: File[]) => { const { uploads, failures } = await upload({ docRef, - fieldName: arrayTableData - ? `${arrayTableData?.parentField}/${fieldName}` + fieldName: docRef.arrayTableData + ? `${docRef.arrayTableData?.parentField}/${fieldName}` : fieldName, files, }); @@ -62,11 +57,11 @@ export default function useFileUpload( fieldName, value: uploads, useArrayUnion: true, - arrayTableData, + arrayTableData: docRef.arrayTableData, }); return { uploads, failures }; }, - [arrayTableData, docRef, fieldName, updateField, upload] + [docRef, fieldName, updateField, upload] ); const handleDelete = useCallback( @@ -77,11 +72,11 @@ export default function useFileUpload( value: [file], useArrayRemove: true, disableCheckEquality: true, - arrayTableData, + arrayTableData: docRef.arrayTableData, }); deleteUpload(file); }, - [arrayTableData, deleteUpload, docRef.path, fieldName, updateField] + [deleteUpload, docRef.arrayTableData, docRef.path, fieldName, updateField] ); return { diff --git a/src/components/fields/Image/EditorCell.tsx b/src/components/fields/Image/EditorCell.tsx index d79d1b0f..1516d96f 100644 --- a/src/components/fields/Image/EditorCell.tsx +++ b/src/components/fields/Image/EditorCell.tsx @@ -23,20 +23,14 @@ export default function Image_({ _rowy_ref, tabIndex, rowHeight, - row: { _rowy_arrayTableData }, }: IEditorCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); const { loading, progress, handleDelete, localFiles, dropzoneState } = - useFileUpload( - _rowy_ref, - column.key, - { - multiple: true, - accept: IMAGE_MIME_TYPES, - }, - _rowy_arrayTableData - ); + useFileUpload(_rowy_ref, column.key, { + multiple: true, + accept: IMAGE_MIME_TYPES, + }); const localImages = useMemo( () => diff --git a/src/components/fields/Image/SideDrawerField.tsx b/src/components/fields/Image/SideDrawerField.tsx index a21af2c2..70c58b88 100644 --- a/src/components/fields/Image/SideDrawerField.tsx +++ b/src/components/fields/Image/SideDrawerField.tsx @@ -84,7 +84,6 @@ export default function Image_({ _rowy_ref, value, disabled, - _rowy_arrayTableData, }: ISideDrawerFieldProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); @@ -95,15 +94,10 @@ export default function Image_({ uploaderState, localFiles, dropzoneState, - } = useFileUpload( - _rowy_ref, - column.key, - { - multiple: true, - accept: IMAGE_MIME_TYPES, - }, - _rowy_arrayTableData - ); + } = useFileUpload(_rowy_ref, column.key, { + multiple: true, + accept: IMAGE_MIME_TYPES, + }); const localImages = useMemo( () => diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index ffc552f2..1c60aa92 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -6,7 +6,6 @@ import type { TableRow, TableRowRef, TableFilter, - ArrayTableRowData, } from "@src/types/table"; import type { SelectedCell } from "@src/atoms/tableScope"; import type { IContextMenuItem } from "@src/components/Table/ContextMenu/ContextMenuItem"; @@ -82,8 +81,6 @@ export interface ISideDrawerFieldProps { column: ColumnConfig; /** The row’s _rowy_ref object */ _rowy_ref: TableRowRef; - /** The array table row’s data */ - _rowy_arrayTableData?: ArrayTableRowData; /** The field’s local value – synced with db when field is not dirty */ value: T; /** Call when the user has input but changes have not been saved */ diff --git a/src/hooks/useFirestoreDocAsCollectionWithAtom.ts b/src/hooks/useFirestoreDocAsCollectionWithAtom.ts index 58affa18..76fb1a09 100644 --- a/src/hooks/useFirestoreDocAsCollectionWithAtom.ts +++ b/src/hooks/useFirestoreDocAsCollectionWithAtom.ts @@ -111,10 +111,13 @@ export function useFirestoreDocAsCollectionWithAtom( const pseudoRow = pseudoDoc.map((row: any, i: number) => { return { ...row, - _rowy_ref: docSnapshot.ref, - _rowy_arrayTableData: { - index: i, - parentField: fieldName, + _rowy_ref: { + path: docSnapshot.ref.path, + id: docSnapshot.ref.id, + arrayTableData: { + index: i, + parentField: fieldName, + }, }, }; }); @@ -188,7 +191,7 @@ export function useFirestoreDocAsCollectionWithAtom( temp.splice(options.index, 1); for (let i = options.index; i < temp.length; i++) { // @ts-ignore - temp[i]._rowy_arrayTableData.index = i; + temp[i]._rowy_ref.arrayTableData.index = i; } return sortRows(temp, sorts); }); @@ -263,10 +266,10 @@ export function useFirestoreDocAsCollectionWithAtom( _rowy_ref: { id: doc(firebaseDb, path).id, path: doc(firebaseDb, path).path, - }, - _rowy_arrayTableData: { - index: i, - parentField: fieldName, + arrayTableData: { + index: i, + parentField: fieldName, + }, }, } as T); @@ -279,8 +282,12 @@ export function useFirestoreDocAsCollectionWithAtom( const modifiedPrevData = temp.map((row: any, i: number) => { return { ...row, - _rowy_arrayTableData: { - index: i + 1, + _rowy_ref: { + ...row._rowy_ref, + arrayTableData: { + index: i + 1, + parentField: fieldName, + }, }, }; }); @@ -353,5 +360,5 @@ function sortRows( } function unsortRows(rows: T[]): T[] { - return orderBy(rows, ["_rowy_arrayTableData.index"], ["asc"]); + return orderBy(rows, ["_rowy_ref.arrayTableData.index"], ["asc"]); } diff --git a/src/types/table.d.ts b/src/types/table.d.ts index 7b8787fd..5a0281dc 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -207,25 +207,27 @@ export type TableSort = { direction: Parameters[1]; }; +export type ArrayTableRowData = { + index: number; + parentField?: string; + operation?: ArrayTableOperations; +}; + export type TableRowRef = { id: string; path: string; + arrayTableData?: ArrayTableRowData; } & Partial; type ArrayTableOperations = { addRow?: "top" | "bottom"; base?: TableRow; }; -export type ArrayTableRowData = { - index: number; - parentField?: string; - operation?: ArrayTableOperations; -}; + export type TableRow = DocumentData & { _rowy_ref: TableRowRef; _rowy_missingRequiredFields?: string[]; _rowy_outOfOrder?: boolean; - _rowy_arrayTableData?: ArrayTableRowData; }; export type FileValue = { diff --git a/src/utils/table.ts b/src/utils/table.ts index 7a143a8b..691dd385 100644 --- a/src/utils/table.ts +++ b/src/utils/table.ts @@ -51,7 +51,6 @@ export const omitRowyFields = >(row: T) => { delete shallowClonedRow["_rowy_outOfOrder"]; delete shallowClonedRow["_rowy_missingRequiredFields"]; delete shallowClonedRow["_rowy_new"]; - delete shallowClonedRow["_rowy_arrayTableData"]; return shallowClonedRow as T; }; From e802db5725900de8290f30dbe00990fb1ceb24b5 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 14 Apr 2023 18:42:43 +0530 Subject: [PATCH 057/183] removed key errors --- src/components/Table/TableBody.tsx | 2 +- src/components/Table/TableHeader.tsx | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx index 3292ceab..b50fcdf6 100644 --- a/src/components/Table/TableBody.tsx +++ b/src/components/Table/TableBody.tsx @@ -83,7 +83,7 @@ export const TableBody = memo(function TableBody({ return ( - {headerGroups.map((headerGroup) => ( - + {headerGroups.map((headerGroup, _i) => ( + {(provided) => ( Date: Mon, 17 Apr 2023 10:35:15 +0530 Subject: [PATCH 058/183] MAX_CONCURRENT_TASKS --- .../TableModals/ImportCsvWizard/useUploadFileFromURL.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx b/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx index 00d99b69..bc957dd3 100644 --- a/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx +++ b/src/components/TableModals/ImportCsvWizard/useUploadFileFromURL.tsx @@ -8,7 +8,7 @@ import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; import { TableRowRef } from "@src/types/table"; import SnackbarProgress from "@src/components/SnackbarProgress"; -const MAX_CONCURRENT_TASKS = 10; +const MAX_CONCURRENT_TASKS = 1000; type UploadParamTypes = { docRef: TableRowRef; From 83080d267ba115c6796b86da6ae7c959b7362d4d Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Mon, 17 Apr 2023 21:25:10 +0530 Subject: [PATCH 059/183] fixed default values --- src/components/TableToolbar/AddRow.tsx | 15 ++++++++++++++- src/hooks/useFirestoreDocAsCollectionWithAtom.ts | 10 +++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/components/TableToolbar/AddRow.tsx b/src/components/TableToolbar/AddRow.tsx index 13e24cd5..ebeb51c4 100644 --- a/src/components/TableToolbar/AddRow.tsx +++ b/src/components/TableToolbar/AddRow.tsx @@ -28,6 +28,7 @@ import { tableSortsAtom, addRowAtom, _updateRowDbAtom, + tableColumnsOrderedAtom, } from "@src/atoms/tableScope"; export default function AddRow() { @@ -215,10 +216,22 @@ export function AddRowArraySubTable() { const anchorEl = useRef(null); const [addRowAt, setAddNewRowAt] = useState<"top" | "bottom">("bottom"); + const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + if (!updateRowDb) return null; const handleClick = () => { - updateRowDb("", {}, undefined, { + const initialValues: Record = {}; + + // Set initial values based on default values + for (const column of tableColumnsOrdered) { + if (column.config?.defaultValue?.type === "static") + initialValues[column.key] = column.config.defaultValue.value!; + else if (column.config?.defaultValue?.type === "null") + initialValues[column.key] = null; + } + + updateRowDb("", initialValues, undefined, { index: 0, operation: { addRow: addRowAt, diff --git a/src/hooks/useFirestoreDocAsCollectionWithAtom.ts b/src/hooks/useFirestoreDocAsCollectionWithAtom.ts index 76fb1a09..d4a809c6 100644 --- a/src/hooks/useFirestoreDocAsCollectionWithAtom.ts +++ b/src/hooks/useFirestoreDocAsCollectionWithAtom.ts @@ -260,9 +260,9 @@ export function useFirestoreDocAsCollectionWithAtom( const addNewRow = (addTo: "top" | "bottom", base?: TableRow) => { let temp: T[] = []; - const newRow = (i: number) => - ({ - ...base, + const newRow = (i: number) => { + return { + ...(base ?? update), _rowy_ref: { id: doc(firebaseDb, path).id, path: doc(firebaseDb, path).path, @@ -271,8 +271,8 @@ export function useFirestoreDocAsCollectionWithAtom( parentField: fieldName, }, }, - } as T); - + } as T; + }; setDataAtom((prevData) => { temp = unsortRows(prevData); From a41bc5d256b8a76c178f9f0249a79b3e696fa400 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Tue, 18 Apr 2023 15:32:16 +0530 Subject: [PATCH 060/183] transaction [WIP] --- src/components/fields/File/useFileUpload.ts | 2 +- .../useFirestoreDocAsCollectionWithAtom.ts | 297 ++++++++++++------ 2 files changed, 202 insertions(+), 97 deletions(-) diff --git a/src/components/fields/File/useFileUpload.ts b/src/components/fields/File/useFileUpload.ts index 40f4c394..161e44c0 100644 --- a/src/components/fields/File/useFileUpload.ts +++ b/src/components/fields/File/useFileUpload.ts @@ -48,7 +48,7 @@ export default function useFileUpload( const { uploads, failures } = await upload({ docRef, fieldName: docRef.arrayTableData - ? `${docRef.arrayTableData?.parentField}/${fieldName}` + ? `${docRef.arrayTableData?.parentField}/${docRef.arrayTableData?.index}/${fieldName}` : fieldName, files, }); diff --git a/src/hooks/useFirestoreDocAsCollectionWithAtom.ts b/src/hooks/useFirestoreDocAsCollectionWithAtom.ts index d4a809c6..b9f2650c 100644 --- a/src/hooks/useFirestoreDocAsCollectionWithAtom.ts +++ b/src/hooks/useFirestoreDocAsCollectionWithAtom.ts @@ -10,8 +10,8 @@ import { refEqual, onSnapshot, FirestoreError, - setDoc, DocumentReference, + runTransaction, } from "firebase/firestore"; import { useErrorHandler } from "react-error-boundary"; @@ -24,7 +24,8 @@ import { UpdateCollectionDocFunction, } from "@src/types/table"; import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase"; -import { omitRowyFields } from "@src/utils/table"; + +type UpdateFunction = (rows: T[]) => T[]; /** Options for {@link useFirestoreDocWithAtom} */ interface IUseFirestoreDocWithAtomOptions { @@ -71,7 +72,16 @@ export function useFirestoreDocAsCollectionWithAtom( const [firebaseDb] = useAtom(firebaseDbAtom, projectScope); const setDataAtom = useSetAtom(dataAtom, dataScope); - + const { addRow, deleteRow, deleteField, updateTable } = useAlterArrayTable( + { + firebaseDb, + dataAtom, + dataScope, + sorts, + path, + fieldName, + } + ); const handleError = useErrorHandler(); const { enqueueSnackbar } = useSnackbar(); const setUpdateDocAtom = useSetAtom( @@ -160,15 +170,23 @@ export function useFirestoreDocAsCollectionWithAtom( ]); const setRows = useCallback( - (rows: T[]) => { - rows = rows.map((row: any, i: number) => omitRowyFields(row)); + (updateFunction: UpdateFunction) => { if (!fieldName) return; + try { - return setDoc( - doc(firebaseDb, path), - { [fieldName]: rows }, - { merge: true } - ); + return runTransaction(firebaseDb, async (transaction) => { + const docRef = doc(firebaseDb, path); + const docSnap = await transaction.get(docRef); + const rows = docSnap.data()?.[fieldName] || []; + + const updatedRows = updateFunction(rows); + + return await transaction.set( + docRef, + { [fieldName]: updatedRows }, + { merge: true } + ); + }); } catch (error) { enqueueSnackbar(`Error updating array table`, { variant: "error", @@ -183,25 +201,14 @@ export function useFirestoreDocAsCollectionWithAtom( if (deleteDocAtom) { setDeleteRowAtom(() => (_: string, options?: ArrayTableRowData) => { if (!options) return; - - const deleteRow = () => { - let temp: T[] = []; - setDataAtom((prevData) => { - temp = unsortRows(prevData); - temp.splice(options.index, 1); - for (let i = options.index; i < temp.length; i++) { - // @ts-ignore - temp[i]._rowy_ref.arrayTableData.index = i; - } - return sortRows(temp, sorts); - }); - return setRows(temp); - }; - deleteRow(); + const updateFunction = deleteRow(options.index); + return setRows(updateFunction); }); } }, [ deleteDocAtom, + deleteRow, + fieldName, firebaseDb, path, setDataAtom, @@ -215,7 +222,7 @@ export function useFirestoreDocAsCollectionWithAtom( setUpdateDocAtom( () => ( - path_: string, + _: string, update: T, deleteFields?: string[], options?: ArrayTableRowData @@ -223,80 +230,18 @@ export function useFirestoreDocAsCollectionWithAtom( if (options === undefined) return; const deleteRowFields = () => { - let temp: T[] = []; - setDataAtom((prevData) => { - temp = unsortRows(prevData); - - if (deleteFields === undefined) return prevData; - - temp[options.index] = { - ...temp[options.index], - ...deleteFields?.reduce( - (acc, field) => ({ ...acc, [field]: undefined }), - {} - ), - }; - - return sortRows(temp, sorts); - }); - - return setRows(temp); + const updateFunction = deleteField(options.index, deleteFields); + return setRows(updateFunction); }; const updateRowValues = () => { - let temp: T[] = []; - setDataAtom((prevData) => { - temp = unsortRows(prevData); - - temp[options.index] = { - ...temp[options.index], - ...update, - }; - return sortRows(temp, sorts); - }); - return setRows(temp); + const updateFunction = updateTable(options.index, update); + return setRows(updateFunction); }; - const addNewRow = (addTo: "top" | "bottom", base?: TableRow) => { - let temp: T[] = []; - - const newRow = (i: number) => { - return { - ...(base ?? update), - _rowy_ref: { - id: doc(firebaseDb, path).id, - path: doc(firebaseDb, path).path, - arrayTableData: { - index: i, - parentField: fieldName, - }, - }, - } as T; - }; - setDataAtom((prevData) => { - temp = unsortRows(prevData); - - if (addTo === "bottom") { - temp.push(newRow(prevData.length)); - } else { - const modifiedPrevData = temp.map((row: any, i: number) => { - return { - ...row, - _rowy_ref: { - ...row._rowy_ref, - arrayTableData: { - index: i + 1, - parentField: fieldName, - }, - }, - }; - }); - temp = [newRow(0), ...modifiedPrevData]; - } - return sortRows(temp, sorts); - }); - - return setRows(temp); + const addNewRow = (addTo: "top" | "bottom", base?: T) => { + const updateFunction = addRow(addTo, base ?? update); + return setRows(updateFunction); }; if (Array.isArray(deleteFields) && deleteFields.length > 0) { @@ -304,7 +249,7 @@ export function useFirestoreDocAsCollectionWithAtom( } else if (options.operation?.addRow) { return addNewRow( options.operation.addRow, - options?.operation.base + options?.operation.base as T ); } else { return updateRowValues(); @@ -313,6 +258,8 @@ export function useFirestoreDocAsCollectionWithAtom( ); } }, [ + addRow, + deleteField, fieldName, firebaseDb, path, @@ -321,11 +268,169 @@ export function useFirestoreDocAsCollectionWithAtom( setUpdateDocAtom, sorts, updateDocAtom, + updateTable, ]); } export default useFirestoreDocAsCollectionWithAtom; +function useAlterArrayTable({ + firebaseDb, + dataAtom, + dataScope, + sorts, + path, + fieldName, +}: { + firebaseDb: Firestore; + dataAtom: PrimitiveAtom; + dataScope: Parameters[1] | undefined; + sorts: TableSort[] | undefined; + path: string; + fieldName: string; +}) { + const setData = useSetAtom(dataAtom, dataScope); + + const add = useCallback( + (addTo: "top" | "bottom", base?: T): UpdateFunction => { + const newRow = (i: number, noMeta?: boolean) => { + const meta = noMeta + ? {} + : { + _rowy_ref: { + id: doc(firebaseDb, path).id, + path: doc(firebaseDb, path).path, + arrayTableData: { + index: i, + parentField: fieldName, + }, + }, + }; + return { + ...(base ?? {}), + ...meta, + } as T; + }; + + setData((prevData) => { + prevData = unsortRows(prevData); + + if (addTo === "bottom") { + prevData.push(newRow(prevData.length)); + } else { + const modifiedPrevData = prevData.map((row: any, i: number) => { + return { + ...row, + _rowy_ref: { + ...row._rowy_ref, + arrayTableData: { + index: i + 1, + parentField: fieldName, + }, + }, + }; + }); + prevData = [newRow(0), ...modifiedPrevData]; + } + return sortRows(prevData, sorts); + }); + + return (rows) => { + if (addTo === "bottom") { + rows.push(newRow(rows.length, true)); + } else { + rows = [newRow(0, true), ...rows]; + } + return rows; + }; + }, + [fieldName, firebaseDb, path, setData, sorts] + ); + + const _delete = useCallback( + (index: number): UpdateFunction => { + setData((prevData) => { + prevData = unsortRows(prevData); + prevData.splice(index, 1); + for (let i = index; i < prevData.length; i++) { + // @ts-ignore + prevData[i]._rowy_ref.arrayTableData.index = i; + } + return sortRows(prevData, sorts); + }); + return (rows) => { + rows.splice(index, 1); + return [...rows]; + }; + }, + [setData, sorts] + ); + + const deleteField = useCallback( + (index: number, deleteFields?: string[]): UpdateFunction => { + setData((prevData) => { + prevData = unsortRows(prevData); + + if (deleteFields === undefined) return prevData; + + prevData[index] = { + ...prevData[index], + ...deleteFields?.reduce( + (acc, field) => ({ ...acc, [field]: undefined }), + {} + ), + }; + + return sortRows(prevData, sorts); + }); + return (rows) => { + if (deleteFields === undefined) return rows; + + rows[index] = { + ...rows[index], + ...deleteFields?.reduce( + (acc, field) => ({ ...acc, [field]: undefined }), + {} + ), + }; + + return rows; + }; + }, + [setData, sorts] + ); + + const update = useCallback( + (index: number, update: Partial): UpdateFunction => { + setData((prevData) => { + prevData = unsortRows(prevData); + prevData[index] = { + ...prevData[index], + ...update, + }; + + return sortRows(prevData, sorts); + }); + + return (rows) => { + rows[index] = { + ...rows[index], + ...update, + }; + return rows; + }; + }, + [setData, sorts] + ); + + return { + addRow: add, + deleteRow: _delete, + deleteField: deleteField, + updateTable: update, + }; +} + /** * Create the Firestore document reference. * Put code in a function so the results can be compared by useMemoValue. From ee5de5e0b727db91e594d11d9421c93ac53e2704 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Tue, 18 Apr 2023 16:18:32 +0530 Subject: [PATCH 061/183] update bug fix -> transaction complete --- src/atoms/tableScope/rowActions.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/atoms/tableScope/rowActions.ts b/src/atoms/tableScope/rowActions.ts index 8a885d80..33da9dba 100644 --- a/src/atoms/tableScope/rowActions.ts +++ b/src/atoms/tableScope/rowActions.ts @@ -369,7 +369,13 @@ export const updateFieldAtom = atom( const tableRows = get(tableRowsAtom); const tableRowsLocal = get(tableRowsLocalAtom); - const row = find(tableRows, ["_rowy_ref.path", path]); + const row = find( + tableRows, + arrayTableData?.index !== undefined + ? ["_rowy_ref.arrayTableData.index", arrayTableData?.index] + : ["_rowy_ref.path", path] + ); + if (!row) throw new Error("Could not find row"); const isLocalRow = Boolean(find(tableRowsLocal, ["_rowy_ref.path", path])); From f5557f80726b057b4ca9ef818474f4b56f978d1d Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Wed, 19 Apr 2023 19:28:01 +0530 Subject: [PATCH 062/183] worked on requested changes --- src/components/ColumnModals/FieldsDropdown.tsx | 2 +- src/components/Table/EmptyTable.tsx | 6 +++--- src/components/TableToolbar/TableToolbar.tsx | 8 ++++++-- src/pages/Table/ProvidedArraySubTablePage.tsx | 3 +-- src/pages/Table/TablePage.tsx | 5 +---- src/types/table.d.ts | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/ColumnModals/FieldsDropdown.tsx b/src/components/ColumnModals/FieldsDropdown.tsx index 2b2497e8..18c9425e 100644 --- a/src/components/ColumnModals/FieldsDropdown.tsx +++ b/src/components/ColumnModals/FieldsDropdown.tsx @@ -44,7 +44,7 @@ export default function FieldsDropdown({ const requireCloudFunctionSetup = fieldConfig.requireCloudFunction && !projectSettings.rowyRunUrl; const requireCollectionTable = - tableSettings.isNotACollection === true && + tableSettings.isCollection === false && fieldConfig.requireCollectionTable === true; return { label: fieldConfig.name, diff --git a/src/components/Table/EmptyTable.tsx b/src/components/Table/EmptyTable.tsx index 1f14e2db..2301f43e 100644 --- a/src/components/Table/EmptyTable.tsx +++ b/src/components/Table/EmptyTable.tsx @@ -34,7 +34,7 @@ export default function EmptyTable() { : false; let contents = <>; - if (!tableSettings.isNotACollection && hasData) { + if (tableSettings.isCollection !== false && hasData) { contents = ( <>
@@ -72,7 +72,7 @@ export default function EmptyTable() { Get started - {tableSettings.isNotACollection === true + {tableSettings.isCollection === false ? "There is no data in this Array Sub Table:" : "There is no data in the Firestore collection:"}
@@ -84,7 +84,7 @@ export default function EmptyTable() {
- {!tableSettings.isNotACollection && ( + {tableSettings.isCollection !== false && ( <> diff --git a/src/components/TableToolbar/TableToolbar.tsx b/src/components/TableToolbar/TableToolbar.tsx index d7d21fbe..8224c236 100644 --- a/src/components/TableToolbar/TableToolbar.tsx +++ b/src/components/TableToolbar/TableToolbar.tsx @@ -95,10 +95,14 @@ export default function TableToolbar({ }, }} > - {tableSettings.isNotACollection ? : } + {tableSettings.isCollection === false ? ( + + ) : ( + + )}
{/* Spacer */} - {tableSettings.isNotACollection ? ( + {tableSettings.isCollection === false ? ( From 60d4213b43dfb6292480c6cc6412a17a0bf19f44 Mon Sep 17 00:00:00 2001 From: alceil Date: Wed, 3 May 2023 20:09:26 +0530 Subject: [PATCH 078/183] Fix label not showing in status type --- src/components/fields/Status/utils/getLabelHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fields/Status/utils/getLabelHelper.ts b/src/components/fields/Status/utils/getLabelHelper.ts index 7cb8b33e..fceabe27 100644 --- a/src/components/fields/Status/utils/getLabelHelper.ts +++ b/src/components/fields/Status/utils/getLabelHelper.ts @@ -63,7 +63,7 @@ export default function getLabel(value: any, conditions: any) { let _label: any = undefined; const isBoolean = Boolean(typeof value === "boolean"); const notBoolean = Boolean(typeof value !== "boolean"); - const isNullOrUndefined = Boolean(!value && notBoolean); + const isNullOrUndefined = Boolean((value === null || value === undefined) && notBoolean); const isNumeric = Boolean(typeof value === "number"); if (isNullOrUndefined) _label = getFalseyLabelFrom(conditions, value); From 20f54ed3b096580790211688d3ed8d9a73ebb902 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 5 May 2023 15:44:41 +0530 Subject: [PATCH 079/183] fix: JSON column configuration overlapping fields --- src/components/fields/Json/SideDrawerField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fields/Json/SideDrawerField.tsx b/src/components/fields/Json/SideDrawerField.tsx index 4c0fbc96..03a451c6 100644 --- a/src/components/fields/Json/SideDrawerField.tsx +++ b/src/components/fields/Json/SideDrawerField.tsx @@ -83,7 +83,7 @@ export default function Json({ sx={{ minHeight: 32, mt: -32 / 8, - ".MuiPopover-root &": { mt: 0 }, // Don’t have margins in popover cell + ".MuiPopover-root & , .MuiDialog-root &": { mt: 0 }, // Don’t have margins in popover cell and dialog "& .MuiTabs-flexContainer": { justifyContent: "flex-end", From 3a3f4656bc269dc22c76d54d1f8ca3a3cad0ef33 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 5 May 2023 15:50:24 +0530 Subject: [PATCH 080/183] fix: Action button on table with frozen column, floats with high z index --- src/components/fields/Action/ActionFab.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/fields/Action/ActionFab.tsx b/src/components/fields/Action/ActionFab.tsx index c09c2903..7896fae8 100644 --- a/src/components/fields/Action/ActionFab.tsx +++ b/src/components/fields/Action/ActionFab.tsx @@ -204,6 +204,7 @@ export default function ActionFab({ } size="small" sx={{ + zIndex: 1, "&:not(.MuiFab-primary):not(.MuiFab-secondary):not(.Mui-disabled)": { bgcolor: (theme) => theme.palette.mode === "dark" From de84161b5c328623a873d7290babe86b428c85d5 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 5 May 2023 16:31:09 +0530 Subject: [PATCH 081/183] fix: context right click menu breaks when cell being selected is in editor mode --- src/components/Table/TableCell/TableCell.tsx | 24 ++++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/Table/TableCell/TableCell.tsx b/src/components/Table/TableCell/TableCell.tsx index 4ea51433..76d92c81 100644 --- a/src/components/Table/TableCell/TableCell.tsx +++ b/src/components/Table/TableCell/TableCell.tsx @@ -182,13 +182,27 @@ export const TableCell = memo(function TableCell({ }} onContextMenu={(e) => { e.preventDefault(); - setSelectedCell({ - path: row.original._rowy_ref.path, - columnKey: cell.column.id, - focusInside: false, + + let isEditorCell = false; + + setSelectedCell((prev) => { + isEditorCell = prev?.focusInside === true; + return { + path: row.original._rowy_ref.path, + columnKey: cell.column.id, + focusInside: false, + // focusInside: !!!prev + // ? false + // : prev?.columnKey === cell.column.id && + // prev.path === row.original._rowy_ref.path + // ? prev?.focusInside + // : false, + }; }); (e.target as HTMLDivElement).focus(); - setContextMenuTarget(e.target as HTMLElement); + if (!isEditorCell) { + setContextMenuTarget(e.target as HTMLElement); + } }} > {renderedValidationTooltip} From c688be388190a01fe70e4f06f892e0a6f9e30fb0 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 5 May 2023 16:44:22 +0530 Subject: [PATCH 082/183] feat: Array value formatted on display cells --- src/components/fields/Array/DisplayCell.tsx | 10 ++++++++++ .../fields/Array/SideDrawerField/SupportedTypes.ts | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/components/fields/Array/DisplayCell.tsx b/src/components/fields/Array/DisplayCell.tsx index e934ea5f..22f8c0a1 100644 --- a/src/components/fields/Array/DisplayCell.tsx +++ b/src/components/fields/Array/DisplayCell.tsx @@ -1,5 +1,7 @@ import { useTheme } from "@mui/material"; import { IDisplayCellProps } from "@src/components/fields/types"; +import { isArray } from "lodash-es"; +import { SupportedTypes, detectType } from "./SideDrawerField/SupportedTypes"; export default function Array({ value }: IDisplayCellProps) { const theme = useTheme(); @@ -7,6 +9,14 @@ export default function Array({ value }: IDisplayCellProps) { if (!value) { return null; } + if (isArray(value)) { + value = value.map((item: any) => { + let itemType = detectType(item); + let converter = SupportedTypes[itemType].humanize; + if (!converter) return item; + return converter(item); + }); + } return (
{ + return `${value.latitude}, ${value.longitude}`; + }, }, [FieldType.dateTime]: { Sidebar: DateTimeValueSidebar, initialValue: Timestamp.now(), dataType: "firestore-type", instance: Timestamp, + humanize: (value: Timestamp) => { + return value.toDate().toLocaleString(); + }, }, [FieldType.reference]: { Sidebar: ReferenceValueSidebar, initialValue: null, dataType: "firestore-type", instance: DocumentReference, + humanize: (value: DocumentReference) => { + return value.path; + }, }, }; From 9f29defc8b6b5458b5a4d3efcfbd7b345868aa64 Mon Sep 17 00:00:00 2001 From: mnmt Date: Sun, 7 May 2023 11:10:30 +0530 Subject: [PATCH 083/183] add edit functionality for options of single and multi select fields --- .../fields/SingleSelect/Settings.tsx | 94 ++++++++++++++++++- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index b8ee6b5f..d9180b41 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -14,6 +14,7 @@ import { } from "@mui/material"; import AddIcon from "@mui/icons-material/AddCircle"; import RemoveIcon from "@mui/icons-material/CancelRounded"; +import CheckIcon from "@mui/icons-material/CheckCircleRounded"; import ColorSelect, { SelectColorThemeOptions, } from "@src/components/SelectColors"; @@ -56,6 +57,10 @@ export default function Settings({ onChange, config }: ISettingsProps) { const listEndRef: any = useRef(null); const options = config.options ?? []; const [newOption, setNewOption] = useState(""); + const [editOption, setEditOption] = useState({ + oldOption: "", + newOption: "", + }); /* State for holding Chip Colors for Select and MultiSelect */ let colors = config.colors ?? []; @@ -72,10 +77,41 @@ export default function Settings({ onChange, config }: ISettingsProps) { } }; + const handleEdit = () => { + const { oldOption, newOption: _newOption } = editOption; + const newOption = _newOption.trim(); + if (oldOption === newOption) { + setEditOption({ + oldOption: "", + newOption: "", + }); + return; + } + + if (newOption !== "") { + if (options.includes(newOption)) { + window.alert(`"${newOption}" is already an option`); + } else { + const newOptions = options.map((option: string) => + option === oldOption ? newOption : option + ); + onChange("options")(newOptions); + + handleChipColorChange("update", oldOption, undefined, newOption); + + setEditOption({ + oldOption: "", + newOption: "", + }); + } + } + }; + const handleChipColorChange = ( - type: "save" | "delete", + type: "save" | "delete" | "update", key: string, - color?: SelectColorThemeOptions + color?: SelectColorThemeOptions, + newKey?: string ) => { const _key = key.toLocaleLowerCase().replace(" ", "_").trim(); const exists = colors.findIndex((option: IColors) => option.name === _key); @@ -97,6 +133,14 @@ export default function Settings({ onChange, config }: ISettingsProps) { ); onChange("colors")(updatedColors); } + + if (type === "update" && newKey) { + const _newKey = newKey.toLocaleLowerCase().replace(" ", "_").trim(); + const updatedColors = colors.map((option: IColors) => + option.name === _key ? { ...option, name: _newKey } : option + ); + onChange("colors")(updatedColors); + } }; const handleItemDelete = (option: string) => { @@ -146,7 +190,7 @@ export default function Settings({ onChange, config }: ISettingsProps) { - {option} + + {editOption.oldOption === option ? ( + + + setEditOption({ + oldOption: option, + newOption: e.target.value, + }) + } + value={editOption.newOption} + sx={{ flexGrow: 1 }} + autoFocus + onKeyPress={(e) => { + if (e.key === "Enter") { + handleEdit(); + } + }} + /> + handleEdit()} + > + {} + + + ) : ( + { + setEditOption({ + oldOption: option, + newOption: option, + }); + }} + sx={{ "&:hover": { cursor: "pointer" } }} + > + {option} + + )} From efbb50fd903a2f30f8033efa3ddc22292ed0b69c Mon Sep 17 00:00:00 2001 From: Guruvignesh Date: Tue, 9 May 2023 13:17:44 +0530 Subject: [PATCH 084/183] feat(filter): update the url value based on filter change --- src/components/TableToolbar/Filters/Filters.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/TableToolbar/Filters/Filters.tsx b/src/components/TableToolbar/Filters/Filters.tsx index 8ebc32ff..f62c9803 100644 --- a/src/components/TableToolbar/Filters/Filters.tsx +++ b/src/components/TableToolbar/Filters/Filters.tsx @@ -109,7 +109,7 @@ export default function Filters() { } else if (hasUserFilters) { filtersToApply = userFilters; } - + if (filtersToApply.length) updatePageURL(filtersToApply); setLocalFilters(filtersToApply); // Reset order so we don’t have to make a new index if (filtersToApply.length) { @@ -173,7 +173,18 @@ export default function Filters() { if (updateUserSettings && filters) updateUserSettings({ tables: { [`${tableId}`]: { filters } } }); }; - + function updatePageURL(filters: TableFilter[]) { + const [filter] = filters; + const queryParams = `?filter=${filter.key}${filter.operator}${filter.value}`; + const newUrl = + window.location.protocol + + "//" + + window.location.host + + window.location.pathname + + queryParams; + window.history.pushState({ path: newUrl }, "", newUrl); + console.log("updatePageUrll", filters, newUrl); + } return ( Date: Tue, 9 May 2023 19:40:11 +0530 Subject: [PATCH 085/183] feat(filter): add the update filter value from url --- .../TableToolbar/Filters/Filters.tsx | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/components/TableToolbar/Filters/Filters.tsx b/src/components/TableToolbar/Filters/Filters.tsx index f62c9803..c2d67a1a 100644 --- a/src/components/TableToolbar/Filters/Filters.tsx +++ b/src/components/TableToolbar/Filters/Filters.tsx @@ -39,6 +39,7 @@ import { import { useFilterInputs, INITIAL_QUERY } from "./useFilterInputs"; import { analytics, logEvent } from "@src/analytics"; import type { TableFilter } from "@src/types/table"; +import { useSearchParams } from "react-router-dom"; const shouldDisableApplyButton = (value: any) => isEmpty(value) && @@ -62,12 +63,12 @@ export default function Filters() { const [, setTableSorts] = useAtom(tableSortsAtom, tableScope); const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope); const [{ defaultQuery }] = useAtom(tableFiltersPopoverAtom, tableScope); - const tableFilterInputs = useFilterInputs(tableColumnsOrdered); const setTableQuery = tableFilterInputs.setQuery; const userFilterInputs = useFilterInputs(tableColumnsOrdered, defaultQuery); const setUserQuery = userFilterInputs.setQuery; const { availableFilters } = userFilterInputs; + const [searchParams, setSearchParams] = useSearchParams(); // Get table filters & user filters from config documents const tableFilters = useMemoValue( @@ -82,7 +83,32 @@ export default function Filters() { const hasTableFilters = Array.isArray(tableFilters) && tableFilters.length > 0; const hasUserFilters = Array.isArray(userFilters) && userFilters.length > 0; - + useEffect(() => { + console.log("Initialcallll", hasUserFilters, searchParams.get("filter")); + let isFiltered = searchParams.get("filter"); + if (isFiltered) updateUserFilter(isFiltered); + }, []); + function updateUserFilter(str: string) { + let { operators = [], operands = [] } = separateOperands(str); + if (!operators.length) return; + // filtersToApply=[{key:"sId",operator:">=",value:12}] + // if(operators.length){ + // let appliedFilter: TableFilter[] = []; + // appliedFilter=[{key:operands[0],operator:operators[0],value:operands[1]}] + // } + } + function findOperators(str: string) { + const operators = [">=", "<=", ">", "<", "==", "!=", "="]; + const regex = new RegExp(operators.map((op) => `\\${op}`).join("|"), "g"); + return str.match(regex) || []; + } + function separateOperands(str: string) { + const operators = findOperators(str); + const operands = str.split( + new RegExp(operators.map((op) => `\\${op}`).join("|"), "g") + ); + return { operators, operands }; + } // Set the local table filter useEffect(() => { // Set local state for UI @@ -110,6 +136,8 @@ export default function Filters() { filtersToApply = userFilters; } if (filtersToApply.length) updatePageURL(filtersToApply); + console.log("fskdjflksdjf", filtersToApply); + // filtersToApply=[{key:"sId",operator:">=",value:12}] setLocalFilters(filtersToApply); // Reset order so we don’t have to make a new index if (filtersToApply.length) { @@ -183,7 +211,6 @@ export default function Filters() { window.location.pathname + queryParams; window.history.pushState({ path: newUrl }, "", newUrl); - console.log("updatePageUrll", filters, newUrl); } return ( Date: Wed, 10 May 2023 15:24:25 +0100 Subject: [PATCH 086/183] Fix Safari turning decimal number inputs to 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chrome has some slightly unique input[type="number"] handling behaviour, where it restricts the characters that can be typed to numeric characters. Under-the-hood, Chrome withholds certain invalid input states from being emitted as change events – so we only receive change events for valid numbers. Safari behaves slightly differently. It allows any characters to be entered and emits a change event on each character press, but its internal "value" is only set if the typed input is a valid number. The change events then either come through with a numeric value represented, or as an empty string. For example, when typing "12.34" we receive onChange events with "1", "12", "", "12.3", and "12.34". On that third onChange event when we pass "" up to React, React happily ignores the change (I _think_ bceause the incoming value "" already matches the element's value of ""; nonetheless, React have solved this issue for us). When we parse the input via `Number(v)` we encounter problems. `Number("")` resolves to `0`, React sets the input's value to `0`, and the user is therefore unable to type decimal values successfully. The solution for this is not to cast to a number at all. We'll rely on default browser behaviour to manage the input for us (allowing users to enter invalid characters), but we're still safe because the element's internal state will always be numeric. In the scenario that a customer does try enter an invalid input "abc123", the input field simply blanks itself when they leave it. --- src/components/fields/Number/EditorCell.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/fields/Number/EditorCell.tsx b/src/components/fields/Number/EditorCell.tsx index e3594eac..68bd7d6e 100644 --- a/src/components/fields/Number/EditorCell.tsx +++ b/src/components/fields/Number/EditorCell.tsx @@ -3,10 +3,6 @@ import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextF export default function Number_(props: IEditorCellProps) { return ( - props.onChange(Number(v))} - /> + ); } From 60c0ac2b5f321d76f8004cd22cf95b6bf7d5c0a8 Mon Sep 17 00:00:00 2001 From: staticGuru Date: Thu, 11 May 2023 16:31:50 +0530 Subject: [PATCH 087/183] fix(filter): add the operator type for filter url --- src/types/table.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/table.d.ts b/src/types/table.d.ts index a0570542..5aa8053b 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -184,7 +184,8 @@ export type TableFilter = { | "time-minute-equal" | "id-equal" | "color-equal" - | "color-not-equal"; + | "color-not-equal" + | "-is-"; value: any; }; From b7759a1dd13b155e3b09731ec8e21deb69e7a4ac Mon Sep 17 00:00:00 2001 From: staticGuru Date: Thu, 11 May 2023 16:40:36 +0530 Subject: [PATCH 088/183] chore(filter): Add filter url changes --- .../TableToolbar/Filters/Filters.tsx | 87 +++++++++++++------ 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/src/components/TableToolbar/Filters/Filters.tsx b/src/components/TableToolbar/Filters/Filters.tsx index c2d67a1a..0fda2759 100644 --- a/src/components/TableToolbar/Filters/Filters.tsx +++ b/src/components/TableToolbar/Filters/Filters.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { useState, useEffect } from "react"; import { useAtom } from "jotai"; import useMemoValue from "use-memo-value"; @@ -67,8 +68,12 @@ export default function Filters() { const setTableQuery = tableFilterInputs.setQuery; const userFilterInputs = useFilterInputs(tableColumnsOrdered, defaultQuery); const setUserQuery = userFilterInputs.setQuery; - const { availableFilters } = userFilterInputs; - const [searchParams, setSearchParams] = useSearchParams(); + const { availableFilters, filterColumns } = userFilterInputs; + const [searchParams] = useSearchParams(); + useEffect(() => { + let isFiltered = searchParams.get("filter"); + if (isFiltered) updateUserFilter(isFiltered); + }, [searchParams]); // Get table filters & user filters from config documents const tableFilters = useMemoValue( @@ -83,26 +88,51 @@ export default function Filters() { const hasTableFilters = Array.isArray(tableFilters) && tableFilters.length > 0; const hasUserFilters = Array.isArray(userFilters) && userFilters.length > 0; - useEffect(() => { - console.log("Initialcallll", hasUserFilters, searchParams.get("filter")); - let isFiltered = searchParams.get("filter"); - if (isFiltered) updateUserFilter(isFiltered); - }, []); function updateUserFilter(str: string) { - let { operators = [], operands = [] } = separateOperands(str); + let { operators, operands = [] } = separateOperands(str); if (!operators.length) return; - // filtersToApply=[{key:"sId",operator:">=",value:12}] - // if(operators.length){ - // let appliedFilter: TableFilter[] = []; - // appliedFilter=[{key:operands[0],operator:operators[0],value:operands[1]}] - // } + if (operators.length) { + let appliedFilter: TableFilter[] = []; + appliedFilter = [ + { + key: operands[0], + operator: operators[0], + value: Number(operands[1]), + }, + ]; + let isValidFilter = checkFilterValidation(appliedFilter[0]); + if (isValidFilter) { + setOverrideTableFilters(true); + setUserFilters(appliedFilter); + } else { + setUserFilters([]); + userFilterInputs.resetQuery(); + } + } + } + function checkFilterValidation(filter: TableFilter): boolean { + let isFilterableColumn = filterColumns?.filter( + (item) => + item.key === filter.key || + item.label === filter.key || + item.type === filter.key + ); + if (!isFilterableColumn?.length) return false; + filter.key = isFilterableColumn?.[0]?.value; + filter.operator = filter.operator === "-is-" ? "id-equal" : filter.operator; + filter.value = + filter.operator === "id-equal" ? filter.value.toString() : filter.value; + return true; } function findOperators(str: string) { - const operators = [">=", "<=", ">", "<", "==", "!=", "="]; + const operators = [">=", "<=", ">", "<", "==", "!=", "=", "-is-"]; const regex = new RegExp(operators.map((op) => `\\${op}`).join("|"), "g"); return str.match(regex) || []; } - function separateOperands(str: string) { + function separateOperands(str: string): { + operators: any[]; + operands: string[]; + } { const operators = findOperators(str); const operands = str.split( new RegExp(operators.map((op) => `\\${op}`).join("|"), "g") @@ -135,9 +165,7 @@ export default function Filters() { } else if (hasUserFilters) { filtersToApply = userFilters; } - if (filtersToApply.length) updatePageURL(filtersToApply); - console.log("fskdjflksdjf", filtersToApply); - // filtersToApply=[{key:"sId",operator:">=",value:12}] + updatePageURL(filtersToApply); setLocalFilters(filtersToApply); // Reset order so we don’t have to make a new index if (filtersToApply.length) { @@ -148,7 +176,6 @@ export default function Filters() { hasUserFilters, setLocalFilters, setTableSorts, - setTableQuery, tableFilters, tableFiltersOverridable, setUserQuery, @@ -202,15 +229,25 @@ export default function Filters() { updateUserSettings({ tables: { [`${tableId}`]: { filters } } }); }; function updatePageURL(filters: TableFilter[]) { - const [filter] = filters; - const queryParams = `?filter=${filter.key}${filter.operator}${filter.value}`; - const newUrl = + let newUrl = window.location.protocol + "//" + window.location.host + - window.location.pathname + - queryParams; - window.history.pushState({ path: newUrl }, "", newUrl); + window.location.pathname; + if (!filters.length) { + window.history.pushState({ path: newUrl }, "", newUrl); + } else { + const [filter] = filters; + const fieldName = filter.key === "_rowy_ref.id" ? "ID" : filter.key; + const operator = + filter.operator === "id-equal" ? "-is-" : filter.operator; + const formattedValue = availableFilters?.valueFormatter + ? availableFilters.valueFormatter(filter.value, filter.operator) + : filter.value.toString(); + const queryParams = `?filter=${fieldName}${operator}${formattedValue}`; + newUrl = newUrl + queryParams; + window.history.pushState({ path: newUrl }, "", newUrl); + } } return ( Date: Thu, 11 May 2023 17:31:04 +0530 Subject: [PATCH 089/183] feat(utils): add the utils functionalities --- src/components/TableToolbar/Filters/utils.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/components/TableToolbar/Filters/utils.tsx diff --git a/src/components/TableToolbar/Filters/utils.tsx b/src/components/TableToolbar/Filters/utils.tsx new file mode 100644 index 00000000..e675839e --- /dev/null +++ b/src/components/TableToolbar/Filters/utils.tsx @@ -0,0 +1,27 @@ +export const URL = + window.location.protocol + + "//" + + window.location.host + + window.location.pathname; +export function separateOperands(str: string): { + operators: any[]; + operands: string[]; +} { + const operators = findOperators(str); + const operands = str.split( + new RegExp(operators.map((op) => `\\${op}`).join("|"), "g") + ); + return { operators, operands }; +} +export function changePageUrl(newURL: string | undefined = URL) { + if (newURL !== URL) { + newURL = URL + newURL; + } + window.history.pushState({ path: newURL }, "", newURL); +} + +function findOperators(str: string) { + const operators = [">=", "<=", ">", "<", "==", "!=", "=", "-is-"]; + const regex = new RegExp(operators.map((op) => `\\${op}`).join("|"), "g"); + return str.match(regex) || []; +} From dc07e8ee6aa35c2e8762f82f84500653e8f1a857 Mon Sep 17 00:00:00 2001 From: staticGuru Date: Thu, 11 May 2023 17:31:39 +0530 Subject: [PATCH 090/183] feat(filter): refactor the filter code base --- .../TableToolbar/Filters/Filters.tsx | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/src/components/TableToolbar/Filters/Filters.tsx b/src/components/TableToolbar/Filters/Filters.tsx index 0fda2759..8b715376 100644 --- a/src/components/TableToolbar/Filters/Filters.tsx +++ b/src/components/TableToolbar/Filters/Filters.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import { useAtom } from "jotai"; import useMemoValue from "use-memo-value"; import { isEmpty, isDate } from "lodash-es"; +import { useSearchParams } from "react-router-dom"; import { Tab, @@ -20,6 +21,7 @@ import TabPanel from "@mui/lab/TabPanel"; import FiltersPopover from "./FiltersPopover"; import FilterInputs from "./FilterInputs"; +import { changePageUrl, separateOperands } from "./utils"; import { projectScope, @@ -40,7 +42,6 @@ import { import { useFilterInputs, INITIAL_QUERY } from "./useFilterInputs"; import { analytics, logEvent } from "@src/analytics"; import type { TableFilter } from "@src/types/table"; -import { useSearchParams } from "react-router-dom"; const shouldDisableApplyButton = (value: any) => isEmpty(value) && @@ -124,21 +125,7 @@ export default function Filters() { filter.operator === "id-equal" ? filter.value.toString() : filter.value; return true; } - function findOperators(str: string) { - const operators = [">=", "<=", ">", "<", "==", "!=", "=", "-is-"]; - const regex = new RegExp(operators.map((op) => `\\${op}`).join("|"), "g"); - return str.match(regex) || []; - } - function separateOperands(str: string): { - operators: any[]; - operands: string[]; - } { - const operators = findOperators(str); - const operands = str.split( - new RegExp(operators.map((op) => `\\${op}`).join("|"), "g") - ); - return { operators, operands }; - } + // Set the local table filter useEffect(() => { // Set local state for UI @@ -229,13 +216,8 @@ export default function Filters() { updateUserSettings({ tables: { [`${tableId}`]: { filters } } }); }; function updatePageURL(filters: TableFilter[]) { - let newUrl = - window.location.protocol + - "//" + - window.location.host + - window.location.pathname; if (!filters.length) { - window.history.pushState({ path: newUrl }, "", newUrl); + changePageUrl(); } else { const [filter] = filters; const fieldName = filter.key === "_rowy_ref.id" ? "ID" : filter.key; @@ -245,8 +227,7 @@ export default function Filters() { ? availableFilters.valueFormatter(filter.value, filter.operator) : filter.value.toString(); const queryParams = `?filter=${fieldName}${operator}${formattedValue}`; - newUrl = newUrl + queryParams; - window.history.pushState({ path: newUrl }, "", newUrl); + changePageUrl(queryParams); } } return ( From ffec232e7aa19d0786f30d28b0e85fbfdca0d71e Mon Sep 17 00:00:00 2001 From: staticGuru Date: Thu, 11 May 2023 17:39:43 +0530 Subject: [PATCH 091/183] feat(snackbar): add invalid filter snackbar --- src/components/TableToolbar/Filters/Filters.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/TableToolbar/Filters/Filters.tsx b/src/components/TableToolbar/Filters/Filters.tsx index 8b715376..868d6f86 100644 --- a/src/components/TableToolbar/Filters/Filters.tsx +++ b/src/components/TableToolbar/Filters/Filters.tsx @@ -4,6 +4,7 @@ import { useAtom } from "jotai"; import useMemoValue from "use-memo-value"; import { isEmpty, isDate } from "lodash-es"; import { useSearchParams } from "react-router-dom"; +import { useSnackbar } from "notistack"; import { Tab, @@ -71,6 +72,7 @@ export default function Filters() { const setUserQuery = userFilterInputs.setQuery; const { availableFilters, filterColumns } = userFilterInputs; const [searchParams] = useSearchParams(); + const { enqueueSnackbar } = useSnackbar(); useEffect(() => { let isFiltered = searchParams.get("filter"); if (isFiltered) updateUserFilter(isFiltered); @@ -106,7 +108,9 @@ export default function Filters() { setOverrideTableFilters(true); setUserFilters(appliedFilter); } else { + enqueueSnackbar("Oops, Invalid filter!!!", { variant: "error" }); setUserFilters([]); + setOverrideTableFilters(false); userFilterInputs.resetQuery(); } } From df577cefd76ac1ad7217e3834214ccdfd2c6c4e2 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Fri, 12 May 2023 01:20:49 +0300 Subject: [PATCH 092/183] fix: derivative sidedrawer --- src/components/SideDrawer/MemoizedField.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/SideDrawer/MemoizedField.tsx b/src/components/SideDrawer/MemoizedField.tsx index 0c43c93b..ebf97b1c 100644 --- a/src/components/SideDrawer/MemoizedField.tsx +++ b/src/components/SideDrawer/MemoizedField.tsx @@ -3,7 +3,7 @@ import useStateRef from "react-usestateref"; import { isEqual, isEmpty } from "lodash-es"; import FieldWrapper from "./FieldWrapper"; -import { IFieldConfig } from "@src/components/fields/types"; +import { FieldType, IFieldConfig } from "@src/components/fields/types"; import { getFieldProp } from "@src/components/fields"; import { ColumnConfig, TableRowRef } from "@src/types/table"; import { TableRow } from "@src/types/table"; @@ -44,6 +44,9 @@ export const MemoizedField = memo( }, [field.fieldName, localValueRef, onSubmit]); let type = field.type; + if (field.type !== FieldType.formula && field.config?.renderFieldType) { + type = field.config.renderFieldType; + } const fieldComponent: IFieldConfig["SideDrawerField"] = getFieldProp( "SideDrawerField", From 65686d80e0b4201312a0ecf611204a7eefc440ef Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Sun, 14 May 2023 13:30:44 +0100 Subject: [PATCH 093/183] Fix number inputs on SideDrawerField --- src/components/fields/Number/EditorCell.tsx | 14 ++++++++++++-- src/components/fields/Number/SideDrawerField.tsx | 11 +++++++++-- src/components/fields/types.ts | 2 +- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/fields/Number/EditorCell.tsx b/src/components/fields/Number/EditorCell.tsx index 68bd7d6e..0fae3291 100644 --- a/src/components/fields/Number/EditorCell.tsx +++ b/src/components/fields/Number/EditorCell.tsx @@ -1,8 +1,18 @@ import type { IEditorCellProps } from "@src/components/fields/types"; import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; -export default function Number_(props: IEditorCellProps) { +export default function Number_(props: IEditorCellProps) { return ( - + { + // Safari/Firefox gives us an empty string for invalid inputs, which includes inputs like "12." on the way to + // typing "12.34". Number would cast these to 0 and replace the user's input to 0 whilst they're mid-way through + // typing. We want to avoid that. + const parsedValue = v === "" ? v : Number(v); + props.onChange(parsedValue); + }} + /> ); } diff --git a/src/components/fields/Number/SideDrawerField.tsx b/src/components/fields/Number/SideDrawerField.tsx index 0b27556e..cd3f41d0 100644 --- a/src/components/fields/Number/SideDrawerField.tsx +++ b/src/components/fields/Number/SideDrawerField.tsx @@ -9,13 +9,20 @@ export default function Number_({ onChange, onSubmit, disabled, -}: ISideDrawerFieldProps) { +}: ISideDrawerFieldProps) { return ( onChange(Number(e.target.value))} + onChange={(e) => { + // Safari/Firefox gives us an empty string for invalid inputs, which includes inputs like "12." on the way to + // typing "12.34". Number would cast these to 0 and replace the user's input to 0 whilst they're mid-way through + // typing. We want to avoid that. + const parsedValue = + e.target.value === "" ? e.target.value : Number(e.target.value); + onChange(parsedValue); + }} onBlur={onSubmit} value={value} id={getFieldId(column.key)} diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index a11e1e10..2d0cdb66 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -86,7 +86,7 @@ export interface ISideDrawerFieldProps { /** Call when the user has input but changes have not been saved */ onDirty: (dirty?: boolean) => void; /** Update the local value. Also calls onDirty */ - onChange: (T: any) => void; + onChange: (value: T) => void; /** Call when user input is ready to be saved (e.g. onBlur) */ onSubmit: () => void; From 40477f7a36a62040741609b4297b8996a8496271 Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Sun, 14 May 2023 13:46:08 +0100 Subject: [PATCH 094/183] Cast number inputs before save --- .../Table/TableCell/EditorCellTextField.tsx | 14 +++++++++++++- src/components/fields/Number/EditorCell.tsx | 5 +++++ src/components/fields/Number/SideDrawerField.tsx | 6 +++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/Table/TableCell/EditorCellTextField.tsx b/src/components/Table/TableCell/EditorCellTextField.tsx index 1f3cb155..048b17d7 100644 --- a/src/components/Table/TableCell/EditorCellTextField.tsx +++ b/src/components/Table/TableCell/EditorCellTextField.tsx @@ -4,6 +4,7 @@ import { spreadSx } from "@src/utils/ui"; export interface IEditorCellTextFieldProps extends IEditorCellProps { InputProps?: Partial; + onBlur?: () => void; } export default function EditorCellTextField({ @@ -11,6 +12,7 @@ export default function EditorCellTextField({ value, onDirty, onChange, + onBlur, setFocusInsideCell, InputProps = {}, }: IEditorCellTextFieldProps) { @@ -19,7 +21,12 @@ export default function EditorCellTextField({ return ( onDirty()} + onBlur={() => { + if (onBlur) { + onBlur(); + } + onDirty(); + }} onChange={(e) => onChange(e.target.value)} fullWidth autoFocus @@ -42,6 +49,11 @@ export default function EditorCellTextField({ setTimeout(() => setFocusInsideCell(false)); } if (e.key === "Enter" && !e.shiftKey) { + // Trigger an onBlur in case we have any final mutations + if (onBlur) { + onBlur(); + } + // Removes focus from inside cell, triggering save on unmount setFocusInsideCell(false); } diff --git a/src/components/fields/Number/EditorCell.tsx b/src/components/fields/Number/EditorCell.tsx index 0fae3291..7a707dad 100644 --- a/src/components/fields/Number/EditorCell.tsx +++ b/src/components/fields/Number/EditorCell.tsx @@ -13,6 +13,11 @@ export default function Number_(props: IEditorCellProps) { const parsedValue = v === "" ? v : Number(v); props.onChange(parsedValue); }} + onBlur={() => { + // Cast to number when the user has finished editing + props.onChange(Number(props.value)); + props.onDirty(); + }} /> ); } diff --git a/src/components/fields/Number/SideDrawerField.tsx b/src/components/fields/Number/SideDrawerField.tsx index cd3f41d0..76212c8c 100644 --- a/src/components/fields/Number/SideDrawerField.tsx +++ b/src/components/fields/Number/SideDrawerField.tsx @@ -23,7 +23,11 @@ export default function Number_({ e.target.value === "" ? e.target.value : Number(e.target.value); onChange(parsedValue); }} - onBlur={onSubmit} + onBlur={() => { + // Cast to number when the user has finished editing + onChange(Number(value)); + onSubmit(); + }} value={value} id={getFieldId(column.key)} label="" From a57848432e8718e0f490985fb35415256ebb4d7d Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Sun, 14 May 2023 13:49:12 +0100 Subject: [PATCH 095/183] Apply Safari number fixes to Percentage --- src/components/fields/Percentage/EditorCell.tsx | 13 +++++++++++-- .../fields/Percentage/SideDrawerField.tsx | 17 ++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/components/fields/Percentage/EditorCell.tsx b/src/components/fields/Percentage/EditorCell.tsx index 54247f55..c10ee35e 100644 --- a/src/components/fields/Percentage/EditorCell.tsx +++ b/src/components/fields/Percentage/EditorCell.tsx @@ -2,7 +2,7 @@ import type { IEditorCellProps } from "@src/components/fields/types"; import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; import { multiply100WithPrecision, divide100WithPrecision } from "./utils"; -export default function Percentage(props: IEditorCellProps) { +export default function Percentage(props: IEditorCellProps) { return ( ) { : props.value } onChange={(v) => { - props.onChange(divide100WithPrecision(Number(v))); + // Safari/Firefox gives us an empty string for invalid inputs, which includes inputs like "12." on the way to + // typing "12.34". Number would cast these to 0 and replace the user's input to 0 whilst they're mid-way through + // typing. We want to avoid that. + const parsedValue = v === "" ? v : divide100WithPrecision(Number(v)); + props.onChange(parsedValue); + }} + onBlur={() => { + // Cast to number when the user has finished editing + props.onChange(Number(props.value)); + props.onDirty(); }} /> ); diff --git a/src/components/fields/Percentage/SideDrawerField.tsx b/src/components/fields/Percentage/SideDrawerField.tsx index feaf5673..9262b9af 100644 --- a/src/components/fields/Percentage/SideDrawerField.tsx +++ b/src/components/fields/Percentage/SideDrawerField.tsx @@ -11,7 +11,7 @@ export default function Percentage({ onChange, onSubmit, disabled, -}: ISideDrawerFieldProps) { +}: ISideDrawerFieldProps) { const { colors } = (column as any).config; const theme = useTheme(); return ( @@ -19,8 +19,19 @@ export default function Percentage({ variant="filled" fullWidth margin="none" - onChange={(e) => onChange(Number(e.target.value) / 100)} - onBlur={onSubmit} + onChange={(e) => { + // Safari/Firefox gives us an empty string for invalid inputs, which includes inputs like "12." on the way to + // typing "12.34". Number would cast these to 0 and replace the user's input to 0 whilst they're mid-way through + // typing. We want to avoid that. + const parsedValue = + e.target.value === "" ? e.target.value : Number(e.target.value) / 100; + onChange(parsedValue); + }} + onBlur={() => { + // Cast to number when the user has finished editing + onChange(Number(value)); + onSubmit(); + }} value={ typeof value === "number" ? multiply100WithPrecision(value) : value } From 30672b8a3add6d14497a20cbb3d5882ff63f7abc Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Tue, 16 May 2023 02:53:07 +0300 Subject: [PATCH 096/183] add esm support to formula --- package.json | 1 + src/components/fields/Formula/worker.ts | 39 ++++++++++---- yarn.lock | 67 ++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 4fac6672..77589f41 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "remark-gfm": "^3.0.1", "seedrandom": "^3.0.5", "stream-browserify": "^3.0.0", + "sucrase": "^3.32.0", "swr": "^1.3.0", "tinymce": "^5", "tss-react": "^4.4.4", diff --git a/src/components/fields/Formula/worker.ts b/src/components/fields/Formula/worker.ts index 2b0af366..12b80fde 100644 --- a/src/components/fields/Formula/worker.ts +++ b/src/components/fields/Formula/worker.ts @@ -1,15 +1,36 @@ +import { transform } from "sucrase"; + +const defaultExportName = "formula"; + +const transpile = (code: string | undefined) => { + if (code) { + let transpiledCode = transform(code, { + transforms: ["typescript", "imports"], + }).code; + const defaultExportRegex = /exports\s*?\.\s*?default\s*?=/; + if (!defaultExportRegex.test(transpiledCode)) { + transpiledCode += `\nexports.default = ${defaultExportName};`; + } + + return transpiledCode; + } else { + return ` + exports.default = async function ${defaultExportName}({ + row, + ref, + }) {};`; + } +}; + onmessage = async ({ data }) => { try { const { formulaFn, row, ref } = JSON.parse(data); - const AsyncFunction = async function () {}.constructor as any; - const [_, fnBody] = formulaFn.match(/=>\s*({?[\s\S]*}?)$/); - if (!fnBody) return; - const fn = new AsyncFunction( - "row", - "ref", - `const fn = async () => \n${fnBody}\n return fn();` - ); - const result = await fn(row, ref); + const codeToRun = transpile(formulaFn); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const exports = {}; + // eslint-disable-next-line no-eval + const formulaScript = eval(codeToRun); + const result = await formulaScript({ row, ref }); postMessage({ result }); } catch (error: any) { console.error("Error: ", error); diff --git a/yarn.lock b/yarn.lock index 2099ba0b..206d978c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3849,6 +3849,11 @@ ansi-to-react@^6.1.6: anser "^1.4.1" escape-carriage "^1.3.0" +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -4637,6 +4642,11 @@ commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + commander@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" @@ -6468,6 +6478,18 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -8999,6 +9021,15 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + nanoclone@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" @@ -9128,7 +9159,7 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== -object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -9502,7 +9533,7 @@ pify@^2.3.0: resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== -pirates@^4.0.4: +pirates@^4.0.1, pirates@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== @@ -11818,6 +11849,19 @@ stylis@4.1.3: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== +sucrase@^3.32.0: + version "3.32.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.32.0.tgz#c4a95e0f1e18b6847127258a75cf360bc568d4a7" + integrity sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + glob "7.1.6" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -12009,6 +12053,20 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + throat@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" @@ -12125,6 +12183,11 @@ tryer@^1.0.1: resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + ts-jest@^28.0.2: version "28.0.8" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-28.0.8.tgz#cd204b8e7a2f78da32cf6c95c9a6165c5b99cc73" From 76f31363001c215ddb12880a84d2dae47cdbc305 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Tue, 16 May 2023 02:53:16 +0300 Subject: [PATCH 097/183] update preview table after formulaFn has changed --- .../fields/Formula/PreviewTable.tsx | 105 ++++++--------- src/components/fields/Formula/Settings.tsx | 35 ++++- .../fields/Formula/TableSourcePreview.ts | 127 ++++++++++-------- src/components/fields/Formula/useFormula.tsx | 1 + src/components/fields/Formula/util.tsx | 2 +- 5 files changed, 142 insertions(+), 128 deletions(-) diff --git a/src/components/fields/Formula/PreviewTable.tsx b/src/components/fields/Formula/PreviewTable.tsx index 6cf85c22..a1c6f57e 100644 --- a/src/components/fields/Formula/PreviewTable.tsx +++ b/src/components/fields/Formula/PreviewTable.tsx @@ -1,73 +1,46 @@ -import { Provider, useAtom } from "jotai"; - -import { currentUserAtom } from "@src/atoms/projectScope"; -import { - tableRowsDbAtom, - tableScope, - tableSettingsAtom, -} from "@src/atoms/tableScope"; - import TablePage from "@src/pages/Table/TablePage"; -import { TableSchema } from "@src/types/table"; -import { Box, InputLabel } from "@mui/material"; -import TableSourcePreview from "./TableSourcePreview"; +import { Box } from "@mui/material"; -const PreviewTable = ({ tableSchema }: { tableSchema: TableSchema }) => { - const [currentUser] = useAtom(currentUserAtom, tableScope); - const [tableSettings] = useAtom(tableSettingsAtom, tableScope); +const PreviewTable = () => { return ( - - Preview table - - - div:first-child": { - display: "none", - }, - // table grid - "& > div:nth-of-type(2)": { - height: "unset", - }, - // emtpy state - "& .empty-state": { - display: "none", - }, - // column actions - add column - '& [data-col-id="_rowy_column_actions"]': { - display: "none", - }, - // row headers - sort by, column settings - '& [data-row-id="_rowy_header"] > button': { - display: "none", - }, - // row headers - drag handler - '& [data-row-id="_rowy_header"] > .column-drag-handle': { - display: "none !important", - }, - // row headers - resize handler - '& [data-row-id="_rowy_header"] >:last-child': { - display: "none !important", - }, - }} - > - - - + // table toolbar + "& > div:first-child": { + display: "none", + }, + // table grid + "& > div:nth-of-type(2)": { + height: "unset", + }, + // emtpy state + "& .empty-state": { + display: "none", + }, + // column actions - add column + '& [data-col-id="_rowy_column_actions"]': { + display: "none", + }, + // row headers - sort by, column settings + '& [data-row-id="_rowy_header"] > button': { + display: "none", + }, + // row headers - drag handler + '& [data-row-id="_rowy_header"] > .column-drag-handle': { + display: "none !important", + }, + // row headers - resize handler + '& [data-row-id="_rowy_header"] >:last-child': { + display: "none !important", + }, + }} + > + ); }; diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx index 666615fe..085b0b65 100644 --- a/src/components/fields/Formula/Settings.tsx +++ b/src/components/fields/Formula/Settings.tsx @@ -1,14 +1,17 @@ import { lazy, Suspense, useMemo } from "react"; import { useDebouncedCallback } from "use-debounce"; -import { useAtom } from "jotai"; +import { Provider, useAtom } from "jotai"; import MultiSelect from "@rowy/multiselect"; -import { Grid, InputLabel, Stack, FormHelperText } from "@mui/material"; +import { Grid, InputLabel, Stack, FormHelperText, Box } from "@mui/material"; import { tableColumnsOrderedAtom, + tableRowsDbAtom, tableSchemaAtom, tableScope, + tableSettingsAtom, + updateFieldAtom, } from "@src/atoms/tableScope"; import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; @@ -17,7 +20,12 @@ import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown"; import { DEFAULT_COL_WIDTH, DEFAULT_ROW_HEIGHT } from "@src/components/Table"; import { ColumnConfig } from "@src/types/table"; -import { defaultFn, listenerFieldTypes, outputFieldTypes } from "./util"; +import { + defaultFn, + listenerFieldTypes, + outputFieldTypes, + serializeRef, +} from "./util"; import PreviewTable from "./PreviewTable"; import { getFieldProp } from ".."; @@ -25,6 +33,8 @@ import { getFieldProp } from ".."; import formulaDefs from "!!raw-loader!./formula.d.ts"; import { WIKI_LINKS } from "@src/constants/externalLinks"; import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper"; +import { currentUserAtom } from "@src/atoms/projectScope"; +import TableSourcePreview from "./TableSourcePreview"; const CodeEditor = lazy( () => @@ -44,6 +54,8 @@ export default function Settings({ onBlur, errors, }: ISettingsProps) { + const [currentUser] = useAtom(currentUserAtom, tableScope); + const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); const returnType = getFieldProp("dataType", config.renderFieldType) ?? "any"; @@ -59,6 +71,7 @@ export default function Settings({ ...columns[key], fixed: false, width: DEFAULT_COL_WIDTH, + editable: true, }; } if (columns[key].fieldName === fieldName) { @@ -157,7 +170,21 @@ export default function Settings({ />
- + + Preview table + + + + + ); } diff --git a/src/components/fields/Formula/TableSourcePreview.ts b/src/components/fields/Formula/TableSourcePreview.ts index 07712f87..3cf762f7 100644 --- a/src/components/fields/Formula/TableSourcePreview.ts +++ b/src/components/fields/Formula/TableSourcePreview.ts @@ -1,81 +1,94 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { useAtom, useSetAtom } from "jotai"; -import { useAtomCallback } from "jotai/utils"; -import { cloneDeep, findIndex, sortBy } from "lodash-es"; +import { cloneDeep, findIndex, isEqual, sortBy } from "lodash-es"; import { _deleteRowDbAtom, _updateRowDbAtom, tableNextPageAtom, + tableRowsAtom, tableRowsDbAtom, - tableSchemaAtom, + tableRowsLocalAtom, tableScope, tableSettingsAtom, } from "@src/atoms/tableScope"; -import { TableRow, TableSchema } from "@src/types/table"; -import { updateRowData } from "@src/utils/table"; +import { TableRow } from "@src/types/table"; +import { generateId, updateRowData } from "@src/utils/table"; import { serializeRef } from "./util"; -const TableSourcePreview = ({ tableSchema }: { tableSchema: TableSchema }) => { +const TableSourcePreview = ({ formulaFn }: { formulaFn: string }) => { + const prevFn = useRef(formulaFn); + const isInitialMount = useRef(true); + const [tableSettings] = useAtom(tableSettingsAtom, tableScope); - const setTableSchemaAtom = useSetAtom(tableSchemaAtom, tableScope); - const setRows = useSetAtom(tableRowsDbAtom, tableScope); - useEffect(() => { - setRows( - ["preview-doc-1", "preview-doc-2", "preview-doc-3"].map((docId) => ({ - _rowy_ref: serializeRef(`${tableSettings.collection}/${docId}`), - })) - ); - }, [setRows, tableSettings.collection]); + const [rows, setRows] = useAtom(tableRowsDbAtom, tableScope); + const setUpdateRowDb = useSetAtom(_updateRowDbAtom, tableScope); + const setDeleteRowDb = useSetAtom(_deleteRowDbAtom, tableScope); + const setNextPageAtom = useSetAtom(tableNextPageAtom, tableScope); - useEffect(() => { - setTableSchemaAtom(() => ({ - ...tableSchema, - _rowy_ref: "preview", - })); - }, [tableSchema, setTableSchemaAtom]); - - const readRowsDb = useAtomCallback( - useCallback((get) => get(tableRowsDbAtom) || [], []), - tableScope + const generateRows = useCallback( + (rows: TableRow[]) => + rows.map((row) => ({ + ...row, + _rowy_ref: serializeRef(`${tableSettings.collection}/${generateId()}`), + })), + [tableSettings.collection] ); - const setUpdateRowDb = useSetAtom(_updateRowDbAtom, tableScope); - setUpdateRowDb(() => async (path: string, update: Partial) => { - const rows = await readRowsDb(); - const index = findIndex(rows, ["_rowy_ref.path", path]); - if (index === -1) { - setRows( - sortBy( - [ - ...rows, - { ...update, _rowy_ref: { id: path.split("/").pop()!, path } }, - ], - ["_rowy_ref.id"] - ) - ); - } else { - const updatedRows = [...rows]; - updatedRows[index] = cloneDeep(rows[index]); - updatedRows[index] = updateRowData(updatedRows[index], update); - setRows(updatedRows); + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + const emptyRow = {} as TableRow; + setRows(generateRows([emptyRow, emptyRow, emptyRow])); } - return Promise.resolve(); - }); + }, [setRows, generateRows, tableSettings.collection]); - const setDeleteRowDb = useSetAtom(_deleteRowDbAtom, tableScope); - setDeleteRowDb(() => async (path: string) => { - const rows = await readRowsDb(); - const index = findIndex(rows, ["_rowy_ref.path", path]); - if (index > -1) { - setRows(rows.filter((_, idx) => idx !== index)); + useEffect(() => { + if (!isEqual(prevFn.current, formulaFn)) { + prevFn.current = formulaFn; + setRows(rows.map((row) => ({ ...row, __mock_field__: Math.random() }))); } - return Promise.resolve(); - }); + }, [rows, setRows, generateRows, formulaFn]); - const setNextPageAtom = useSetAtom(tableNextPageAtom, tableScope); - setNextPageAtom({ loading: false, available: false }); + useEffect(() => { + setUpdateRowDb(() => (path: string, update: Partial) => { + const index = findIndex(rows, ["_rowy_ref.path", path]); + if (index === -1) { + setRows( + sortBy( + [ + ...rows, + { ...update, _rowy_ref: { id: path.split("/").pop()!, path } }, + ], + ["_rowy_ref.id"] + ) + ); + } else { + const updatedRows = [...rows]; + updatedRows[index] = cloneDeep(rows[index]); + updatedRows[index] = updateRowData(updatedRows[index], update); + setRows(updatedRows); + } + return Promise.resolve(); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setRows, setUpdateRowDb]); + + useEffect(() => { + setDeleteRowDb(() => (path: string) => { + const index = findIndex(rows, ["_rowy_ref.path", path]); + if (index > -1) { + setRows(rows.filter((_, idx) => idx !== index)); + } + return Promise.resolve(); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setRows, setDeleteRowDb]); + + useEffect(() => { + setNextPageAtom({ loading: false, available: false }); + }, [setNextPageAtom]); return null; }; diff --git a/src/components/fields/Formula/useFormula.tsx b/src/components/fields/Formula/useFormula.tsx index 7b60228f..6836e3dc 100644 --- a/src/components/fields/Formula/useFormula.tsx +++ b/src/components/fields/Formula/useFormula.tsx @@ -85,6 +85,7 @@ export const useFormula = ({ }, [useDeepCompareMemoize(listeners), formulaFn]); const updateField = useSetAtom(updateFieldAtom, tableScope); + useEffect(() => { updateField({ path: row._rowy_ref.path, diff --git a/src/components/fields/Formula/util.tsx b/src/components/fields/Formula/util.tsx index 71973a2e..9f4c9565 100644 --- a/src/components/fields/Formula/util.tsx +++ b/src/components/fields/Formula/util.tsx @@ -24,7 +24,7 @@ import JsonDisplayCell from "@src/components/fields/Json/DisplayCell"; import CodeDisplayCell from "@src/components/fields/Code/DisplayCell"; import MarkdownDisplayCell from "@src/components/fields/Markdown/DisplayCell"; import CreatedByDisplayCell from "@src/components/fields/CreatedBy/DisplayCell"; -import { TableRowRef } from "@src/types/table"; +import { TableRowRef, TableSettings } from "@src/types/table"; import { DocumentData, DocumentReference } from "firebase/firestore"; export function useDeepCompareMemoize(value: T) { From bf1bd98535ec5ead375108cf6de386cfe0850b7a Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Tue, 23 May 2023 12:19:34 +1000 Subject: [PATCH 098/183] add clarification tooltip to "add row" button when disabled for collection group --- src/components/TableToolbar/AddRow.tsx | 72 +++++++++++++++----------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/src/components/TableToolbar/AddRow.tsx b/src/components/TableToolbar/AddRow.tsx index 2fc5e108..55a85b64 100644 --- a/src/components/TableToolbar/AddRow.tsx +++ b/src/components/TableToolbar/AddRow.tsx @@ -9,6 +9,7 @@ import { MenuItem, ListItemText, Box, + Tooltip, } from "@mui/material"; import { AddRow as AddRowIcon, @@ -83,42 +84,51 @@ export default function AddRow() { return ( <> - - + - - + + +