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 01/73] 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 189c86a16ee6c6b0ebb7b20d16143ce592169655 Mon Sep 17 00:00:00 2001 From: Emmanuel Watila Date: Mon, 27 Feb 2023 18:10:12 +0100 Subject: [PATCH 02/73] 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 03/73] 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 04/73] 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 05/73] 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 06/73] 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 07/73] 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 08/73] 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 09/73] 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 10/73] 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 11/73] 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 12/73] 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 13/73] 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 14/73] 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 15/73] 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 16/73] 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 17/73] 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 18/73] 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 27/73] 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 28/73] 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 29/73] 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 30/73] 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 31/73] 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 32/73] 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 33/73] 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 34/73] 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 35/73] 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 36/73] 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 37/73] 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 38/73] =?UTF-8?q?=E2=9C=A8=20feat(App.tsx):=20add=20route?= =?UTF-8?q?=20for=20ProvidedArraySubTablePage=20at=20/array-sub-table/:doc?= =?UTF-8?q?Path/: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 39/73] 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 40/73] 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 41/73] 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 42/73] 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 43/73] 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 44/73] 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 45/73] 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 46/73] 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 47/73] 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 48/73] 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 49/73] 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 50/73] 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 51/73] 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 52/73] 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 53/73] 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 54/73] 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 55/73] 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 56/73] 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 57/73] 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 58/73] 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 59/73] 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 60/73] 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 61/73] 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 ? (