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/90] 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 d669d494d..5038e845a 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/90] 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 000000000..cee3a29b5 --- /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 2fc908b9a..26362f5d7 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/90] 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 cee3a29b5..104f6a908 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/90] 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 000000000..a9d7e6a81 --- /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 104f6a908..83b591924 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 26362f5d7..d0fe7fdb6 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/90] 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 a9d7e6a81..49462fc41 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 83b591924..cb0a37310 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/90] 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 89ebdb212..b1cc1fc7b 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/90] 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 cb0a37310..4adbc67bc 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 d0fe7fdb6..c2c242a06 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/90] 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 6e145d50e..be3216e7d 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 fe7d6b873..38c546814 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 ece750d17..2cb307c34 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 c2c242a06..15563ce70 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/90] 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 38c546814..8310acef3 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 2cb307c34..967633b09 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 15563ce70..908249d1a 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/90] 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 99c260bee..4482350f7 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 288c425f6..42ef2a4d0 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/90] 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 42ef2a4d0..074116fc8 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/90] 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 4482350f7..28ef41e42 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 074116fc8..b1d33b56b 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/90] 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 2f3926e8b..1caf94b95 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 276656068..39ce7b1d5 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 b1d33b56b..bc87d0885 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/90] 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 75b496f58..abf15be00 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 959765a0c..cffc2c521 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 000000000..c606527c6 --- /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 000000000..fd2fa5027 --- /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 000000000..ffab1b0f1 --- /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 b40e0e8c9..551b69ec1 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 000000000..333141371 --- /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 000000000..b5fb7511a --- /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 c5d832996..dc3396877 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 d855da032..ec6ad97d9 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/90] 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 | 79 +++++++++++++------ .../TableToolbar/ImportData/ImportFromCsv.tsx | 2 +- 5 files changed, 93 insertions(+), 58 deletions(-) diff --git a/src/components/Table/Mock/mockValue/file.ts b/src/components/Table/Mock/mockValue/file.ts index c606527c6..990db3409 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 551b69ec1..421fbc30e 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 333141371..dba43aaae 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 b5fb7511a..d6727a0ac 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,34 +92,66 @@ 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: ( + + ), } - } + ); + }, + [enqueueSnackbar] + ); - if (setProgress) setProgress((p: number) => p + currentJobs.length); - await batchUpload(currentJobs); + const runBatchUpload = useCallback(async () => { + if (!snackbarProgressId.current) { + showProgress(jobs.current.length); + } + let currentJobs: UploadParamTypes[] = []; - if (jobs.current.length > 0) { - runBatchUpload(); + while ( + currentJobs.length < MAX_CONCURRENT_TASKS && + jobs.current.length > 0 + ) { + const job = jobs.current.shift(); + if (job) { + currentJobs.push(job); } - }, - [batchUpload] - ); + } + + 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 f164f90af..269f306d0 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/90] 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 cffc2c521..7d6f501b7 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 fd2fa5027..000000000 --- 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 ffab1b0f1..000000000 --- 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 dba43aaae..34a9d4f0a 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/90] 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 7d6f501b7..0944d3a9a 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 34a9d4f0a..34593f97b 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/90] 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 4adbc67bc..1c225901b 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/90] 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 876a52156..c79804279 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/90] 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 c79804279..97946dff7 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 28ef41e42..8cdaf330e 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/90] 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 6c95922f7..6b5e5911d 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/90] 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 a86146518..457e1e281 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 6b5e5911d..498a79268 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 45985da2c..417665984 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/90] 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 417665984..b8ee6b5f2 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/90] 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 3b9d27011..b4a53d5d4 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/90] 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 7af0f6b9a..398debc97 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 0f3b57615..0e762841a 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/90] 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 7a6cee3f3..193743c22 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 421fbc30e..4f3f9a9e2 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 489a31279..58f597be8 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 0fae28fd7..00d99b693 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/90] 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 000000000..21d2d3fd9 --- /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 fff6341fd..889f31abb 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 cefc6c2f7..e43c747ba 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/90] 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 272816251..4a2cbbf18 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/90] 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 6f9d66d8c..48af64b02 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 88a973c29..6030b89a7 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 28b584e67..e6023e4d5 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 c6372865a..0f27da9e1 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 a05705422..8de722846 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/90] =?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 ea283a012..d169bb1ee 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/90] 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 a8989d48d..8a126c228 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 38e0e4129..65c77ff57 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/90] 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 65c77ff57..ea0305c8c 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/90] 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 a2b64de4b..e501be4ba 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/90] 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 989f6716b..66a5c9d59 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/90] 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 0bd770cbe..45955cf88 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/90] 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 ad6ba46a8..e15b91bc9 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/90] 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 921997ad0..ecbe3f52e 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/90] 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 ecbe3f52e..88a90ed2c 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/90] update the action and the redo code templates --- src/components/fields/Action/templates.ts | 88 +++++++++++------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/components/fields/Action/templates.ts b/src/components/fields/Action/templates.ts index 46b0a0b58..294eab3e5 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: +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"); + /* - 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: 'PUT', + 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 updated successfully on example service', - status: "upto date" - } + message: "User updated successfully on example service", + status: "upto date", + }; } catch (error) { return { success: false, - message: 'User update 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"); -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: "DELETE", 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 deleted successfully on example service", + status: null, + }; } catch (error) { return { success: false, - message: 'User delete failed on example service', - } + message: "User delete failed on example service", + }; } */ - // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY -}`; +}; +`; From e841c994e5a174965a154613c20afd2e44393b15 Mon Sep 17 00:00:00 2001 From: Yaman Katby Date: Thu, 6 Apr 2023 17:30:21 +0300 Subject: [PATCH 48/90] 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 1c2486d29..0ca5dd512 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/90] 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 294eab3e5..3a9baf7a4 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/90] 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 5146c387b..b5e3dc057 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/90] 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 222ee9427..0cd213fbf 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 d145d72e6..012bf4c95 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 dfb3e90f3..1ee073db3 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 21d2d3fd9..217eb0959 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 ea24dabe1..e23bef183 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 bbf8c5a5f..9c34383f6 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 bcc1b0281..848bad075 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 59bafba9b..94f4f2eb2 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 e43c747ba..f8f85ff44 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 cceaca983..a1af11699 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/90] 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 000000000..d7f5a2ada --- /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 fe35d6540..5ff521a7b 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 75b496f58..8a885d80d 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 7182a41d1..b170d2502 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 529c3b840..2b2497e8b 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 c6a352cd0..eb76afa8e 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 ef676bd19..faeb15ad6 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 7b6a65784..e2abdfa4e 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 88a973c29..5154ba8b6 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 f07839b38..1f14e2dbf 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 28b584e67..041d3f0fe 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 8dd53d739..edaed4911 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 c80380d40..cacc89468 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 4ea514338..5c664b352 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 3353d23fe..81141a77b 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 516124d3c..49cdc2d87 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 c6372865a..13e24cd50 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 7af0f6b9a..d7d21fbe4 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 7ab899e2d..53b3629de 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 000000000..3796d5530 --- /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 000000000..d586d46c0 --- /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 000000000..0f03a45b6 --- /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 000000000..9e062b65f --- /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 000000000..c00f7d7c3 --- /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 cffa0ea88..a6dab1748 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 98fe8cb6e..257da8716 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 4c18a0f31..a7ea0600f 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 a520c594d..c175cfc81 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 00287c23d..38be7a84f 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 d99ccf674..a5305cab3 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 ceec43070..d79d1b0f3 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 70c58b885..a21af2c2e 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 7e153771e..574c0a2ec 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 d6e5eb92d..1375347be 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 4b1c3a420..c4f733d15 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 0d54b0a57..97726ea09 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 a11e1e10f..ffc552f20 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 900b88db6..d91111a4d 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 af23e2cd1..70e8d9eb3 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 000000000..58affa180 --- /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 000000000..1e6e256c6 --- /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 04fc0e618..d8e85c85c 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 000000000..e7920b011 --- /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 46a5a939e..7b8787fd7 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 691dd3850..7a143a8b3 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/90] 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 516124d3c..10b2bf76c 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/90] 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 89130dcba..2659e2add 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/90] 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 8f7b77660..0fb62f62a 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 faeb15ad6..49df73391 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 e2abdfa4e..2f1e0b8a2 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 5154ba8b6..5437c2dd6 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 041d3f0fe..16f6ee689 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 b99c11295..d930b794c 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 edaed4911..3292ceab6 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 cacc89468..217c40eb1 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 5c664b352..f7e501fbc 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 81141a77b..7c18a5b51 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 49cdc2d87..e79823134 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 c175cfc81..487e9feeb 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 38be7a84f..00287c23d 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 a5305cab3..40f4c394a 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 d79d1b0f3..1516d96f5 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 a21af2c2e..70c58b885 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 ffc552f20..1c60aa92e 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 58affa180..76fb1a09d 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 7b8787fd7..5a0281dc4 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 7a143a8b3..691dd3850 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/90] 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 3292ceab6..b50fcdf69 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/90] 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 00d99b693..bc957dd33 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/90] 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 13e24cd50..ebeb51c49 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 76fb1a09d..d4a809c6b 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/90] 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 40f4c394a..161e44c0e 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 d4a809c6b..b9f2650c7 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/90] 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 8a885d80d..33da9dba5 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/90] 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 2b2497e8b..18c9425e5 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 1f14e2dbf..2301f43eb 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 d7d21fbe4..8224c2369 100644 --- a/src/components/TableToolbar/TableToolbar.tsx +++ b/src/components/TableToolbar/TableToolbar.tsx @@ -95,10 +95,14 @@ export default function TableToolbar({ }, }} > - {tableSettings.isNotACollection ? : } + {tableSettings.isCollection === false ? ( + + ) : ( + + )}
{/* Spacer */} - {tableSettings.isNotACollection ? ( + {tableSettings.isCollection === false ? ( From 60d4213b43dfb6292480c6cc6412a17a0bf19f44 Mon Sep 17 00:00:00 2001 From: alceil Date: Wed, 3 May 2023 20:09:26 +0530 Subject: [PATCH 77/90] Fix label not showing in status type --- src/components/fields/Status/utils/getLabelHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fields/Status/utils/getLabelHelper.ts b/src/components/fields/Status/utils/getLabelHelper.ts index 7cb8b33e5..fceabe276 100644 --- a/src/components/fields/Status/utils/getLabelHelper.ts +++ b/src/components/fields/Status/utils/getLabelHelper.ts @@ -63,7 +63,7 @@ export default function getLabel(value: any, conditions: any) { let _label: any = undefined; const isBoolean = Boolean(typeof value === "boolean"); const notBoolean = Boolean(typeof value !== "boolean"); - const isNullOrUndefined = Boolean(!value && notBoolean); + const isNullOrUndefined = Boolean((value === null || value === undefined) && notBoolean); const isNumeric = Boolean(typeof value === "number"); if (isNullOrUndefined) _label = getFalseyLabelFrom(conditions, value); From 20f54ed3b096580790211688d3ed8d9a73ebb902 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 5 May 2023 15:44:41 +0530 Subject: [PATCH 78/90] fix: JSON column configuration overlapping fields --- src/components/fields/Json/SideDrawerField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fields/Json/SideDrawerField.tsx b/src/components/fields/Json/SideDrawerField.tsx index 4c0fbc961..03a451c60 100644 --- a/src/components/fields/Json/SideDrawerField.tsx +++ b/src/components/fields/Json/SideDrawerField.tsx @@ -83,7 +83,7 @@ export default function Json({ sx={{ minHeight: 32, mt: -32 / 8, - ".MuiPopover-root &": { mt: 0 }, // Don’t have margins in popover cell + ".MuiPopover-root & , .MuiDialog-root &": { mt: 0 }, // Don’t have margins in popover cell and dialog "& .MuiTabs-flexContainer": { justifyContent: "flex-end", From 3a3f4656bc269dc22c76d54d1f8ca3a3cad0ef33 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 5 May 2023 15:50:24 +0530 Subject: [PATCH 79/90] fix: Action button on table with frozen column, floats with high z index --- src/components/fields/Action/ActionFab.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/fields/Action/ActionFab.tsx b/src/components/fields/Action/ActionFab.tsx index c09c29030..7896fae8b 100644 --- a/src/components/fields/Action/ActionFab.tsx +++ b/src/components/fields/Action/ActionFab.tsx @@ -204,6 +204,7 @@ export default function ActionFab({ } size="small" sx={{ + zIndex: 1, "&:not(.MuiFab-primary):not(.MuiFab-secondary):not(.Mui-disabled)": { bgcolor: (theme) => theme.palette.mode === "dark" From de84161b5c328623a873d7290babe86b428c85d5 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 5 May 2023 16:31:09 +0530 Subject: [PATCH 80/90] fix: context right click menu breaks when cell being selected is in editor mode --- src/components/Table/TableCell/TableCell.tsx | 24 ++++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/Table/TableCell/TableCell.tsx b/src/components/Table/TableCell/TableCell.tsx index 4ea514338..76d92c81d 100644 --- a/src/components/Table/TableCell/TableCell.tsx +++ b/src/components/Table/TableCell/TableCell.tsx @@ -182,13 +182,27 @@ export const TableCell = memo(function TableCell({ }} onContextMenu={(e) => { e.preventDefault(); - setSelectedCell({ - path: row.original._rowy_ref.path, - columnKey: cell.column.id, - focusInside: false, + + let isEditorCell = false; + + setSelectedCell((prev) => { + isEditorCell = prev?.focusInside === true; + return { + path: row.original._rowy_ref.path, + columnKey: cell.column.id, + focusInside: false, + // focusInside: !!!prev + // ? false + // : prev?.columnKey === cell.column.id && + // prev.path === row.original._rowy_ref.path + // ? prev?.focusInside + // : false, + }; }); (e.target as HTMLDivElement).focus(); - setContextMenuTarget(e.target as HTMLElement); + if (!isEditorCell) { + setContextMenuTarget(e.target as HTMLElement); + } }} > {renderedValidationTooltip} From c688be388190a01fe70e4f06f892e0a6f9e30fb0 Mon Sep 17 00:00:00 2001 From: Anish Roy <6275anishroy@gmail.com> Date: Fri, 5 May 2023 16:44:22 +0530 Subject: [PATCH 81/90] feat: Array value formatted on display cells --- src/components/fields/Array/DisplayCell.tsx | 10 ++++++++++ .../fields/Array/SideDrawerField/SupportedTypes.ts | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/components/fields/Array/DisplayCell.tsx b/src/components/fields/Array/DisplayCell.tsx index e934ea5f9..22f8c0a12 100644 --- a/src/components/fields/Array/DisplayCell.tsx +++ b/src/components/fields/Array/DisplayCell.tsx @@ -1,5 +1,7 @@ import { useTheme } from "@mui/material"; import { IDisplayCellProps } from "@src/components/fields/types"; +import { isArray } from "lodash-es"; +import { SupportedTypes, detectType } from "./SideDrawerField/SupportedTypes"; export default function Array({ value }: IDisplayCellProps) { const theme = useTheme(); @@ -7,6 +9,14 @@ export default function Array({ value }: IDisplayCellProps) { if (!value) { return null; } + if (isArray(value)) { + value = value.map((item: any) => { + let itemType = detectType(item); + let converter = SupportedTypes[itemType].humanize; + if (!converter) return item; + return converter(item); + }); + } return (
{ + return `${value.latitude}, ${value.longitude}`; + }, }, [FieldType.dateTime]: { Sidebar: DateTimeValueSidebar, initialValue: Timestamp.now(), dataType: "firestore-type", instance: Timestamp, + humanize: (value: Timestamp) => { + return value.toDate().toLocaleString(); + }, }, [FieldType.reference]: { Sidebar: ReferenceValueSidebar, initialValue: null, dataType: "firestore-type", instance: DocumentReference, + humanize: (value: DocumentReference) => { + return value.path; + }, }, }; From a292667c92b2f94590327df95fe0b7c27299c5e9 Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Wed, 10 May 2023 15:24:25 +0100 Subject: [PATCH 82/90] Fix Safari turning decimal number inputs to 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chrome has some slightly unique input[type="number"] handling behaviour, where it restricts the characters that can be typed to numeric characters. Under-the-hood, Chrome withholds certain invalid input states from being emitted as change events – so we only receive change events for valid numbers. Safari behaves slightly differently. It allows any characters to be entered and emits a change event on each character press, but its internal "value" is only set if the typed input is a valid number. The change events then either come through with a numeric value represented, or as an empty string. For example, when typing "12.34" we receive onChange events with "1", "12", "", "12.3", and "12.34". On that third onChange event when we pass "" up to React, React happily ignores the change (I _think_ bceause the incoming value "" already matches the element's value of ""; nonetheless, React have solved this issue for us). When we parse the input via `Number(v)` we encounter problems. `Number("")` resolves to `0`, React sets the input's value to `0`, and the user is therefore unable to type decimal values successfully. The solution for this is not to cast to a number at all. We'll rely on default browser behaviour to manage the input for us (allowing users to enter invalid characters), but we're still safe because the element's internal state will always be numeric. In the scenario that a customer does try enter an invalid input "abc123", the input field simply blanks itself when they leave it. --- src/components/fields/Number/EditorCell.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/fields/Number/EditorCell.tsx b/src/components/fields/Number/EditorCell.tsx index e3594eac0..68bd7d6e6 100644 --- a/src/components/fields/Number/EditorCell.tsx +++ b/src/components/fields/Number/EditorCell.tsx @@ -3,10 +3,6 @@ import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextF export default function Number_(props: IEditorCellProps) { return ( - props.onChange(Number(v))} - /> + ); } From df577cefd76ac1ad7217e3834214ccdfd2c6c4e2 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Fri, 12 May 2023 01:20:49 +0300 Subject: [PATCH 83/90] fix: derivative sidedrawer --- src/components/SideDrawer/MemoizedField.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/SideDrawer/MemoizedField.tsx b/src/components/SideDrawer/MemoizedField.tsx index 0c43c93bd..ebf97b1c1 100644 --- a/src/components/SideDrawer/MemoizedField.tsx +++ b/src/components/SideDrawer/MemoizedField.tsx @@ -3,7 +3,7 @@ import useStateRef from "react-usestateref"; import { isEqual, isEmpty } from "lodash-es"; import FieldWrapper from "./FieldWrapper"; -import { IFieldConfig } from "@src/components/fields/types"; +import { FieldType, IFieldConfig } from "@src/components/fields/types"; import { getFieldProp } from "@src/components/fields"; import { ColumnConfig, TableRowRef } from "@src/types/table"; import { TableRow } from "@src/types/table"; @@ -44,6 +44,9 @@ export const MemoizedField = memo( }, [field.fieldName, localValueRef, onSubmit]); let type = field.type; + if (field.type !== FieldType.formula && field.config?.renderFieldType) { + type = field.config.renderFieldType; + } const fieldComponent: IFieldConfig["SideDrawerField"] = getFieldProp( "SideDrawerField", From 65686d80e0b4201312a0ecf611204a7eefc440ef Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Sun, 14 May 2023 13:30:44 +0100 Subject: [PATCH 84/90] Fix number inputs on SideDrawerField --- src/components/fields/Number/EditorCell.tsx | 14 ++++++++++++-- src/components/fields/Number/SideDrawerField.tsx | 11 +++++++++-- src/components/fields/types.ts | 2 +- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/fields/Number/EditorCell.tsx b/src/components/fields/Number/EditorCell.tsx index 68bd7d6e6..0fae32914 100644 --- a/src/components/fields/Number/EditorCell.tsx +++ b/src/components/fields/Number/EditorCell.tsx @@ -1,8 +1,18 @@ import type { IEditorCellProps } from "@src/components/fields/types"; import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; -export default function Number_(props: IEditorCellProps) { +export default function Number_(props: IEditorCellProps) { return ( - + { + // Safari/Firefox gives us an empty string for invalid inputs, which includes inputs like "12." on the way to + // typing "12.34". Number would cast these to 0 and replace the user's input to 0 whilst they're mid-way through + // typing. We want to avoid that. + const parsedValue = v === "" ? v : Number(v); + props.onChange(parsedValue); + }} + /> ); } diff --git a/src/components/fields/Number/SideDrawerField.tsx b/src/components/fields/Number/SideDrawerField.tsx index 0b27556ea..cd3f41d02 100644 --- a/src/components/fields/Number/SideDrawerField.tsx +++ b/src/components/fields/Number/SideDrawerField.tsx @@ -9,13 +9,20 @@ export default function Number_({ onChange, onSubmit, disabled, -}: ISideDrawerFieldProps) { +}: ISideDrawerFieldProps) { return ( onChange(Number(e.target.value))} + onChange={(e) => { + // Safari/Firefox gives us an empty string for invalid inputs, which includes inputs like "12." on the way to + // typing "12.34". Number would cast these to 0 and replace the user's input to 0 whilst they're mid-way through + // typing. We want to avoid that. + const parsedValue = + e.target.value === "" ? e.target.value : Number(e.target.value); + onChange(parsedValue); + }} onBlur={onSubmit} value={value} id={getFieldId(column.key)} diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index a11e1e10f..2d0cdb666 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -86,7 +86,7 @@ export interface ISideDrawerFieldProps { /** Call when the user has input but changes have not been saved */ onDirty: (dirty?: boolean) => void; /** Update the local value. Also calls onDirty */ - onChange: (T: any) => void; + onChange: (value: T) => void; /** Call when user input is ready to be saved (e.g. onBlur) */ onSubmit: () => void; From 40477f7a36a62040741609b4297b8996a8496271 Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Sun, 14 May 2023 13:46:08 +0100 Subject: [PATCH 85/90] Cast number inputs before save --- .../Table/TableCell/EditorCellTextField.tsx | 14 +++++++++++++- src/components/fields/Number/EditorCell.tsx | 5 +++++ src/components/fields/Number/SideDrawerField.tsx | 6 +++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/Table/TableCell/EditorCellTextField.tsx b/src/components/Table/TableCell/EditorCellTextField.tsx index 1f3cb1554..048b17d77 100644 --- a/src/components/Table/TableCell/EditorCellTextField.tsx +++ b/src/components/Table/TableCell/EditorCellTextField.tsx @@ -4,6 +4,7 @@ import { spreadSx } from "@src/utils/ui"; export interface IEditorCellTextFieldProps extends IEditorCellProps { InputProps?: Partial; + onBlur?: () => void; } export default function EditorCellTextField({ @@ -11,6 +12,7 @@ export default function EditorCellTextField({ value, onDirty, onChange, + onBlur, setFocusInsideCell, InputProps = {}, }: IEditorCellTextFieldProps) { @@ -19,7 +21,12 @@ export default function EditorCellTextField({ return ( onDirty()} + onBlur={() => { + if (onBlur) { + onBlur(); + } + onDirty(); + }} onChange={(e) => onChange(e.target.value)} fullWidth autoFocus @@ -42,6 +49,11 @@ export default function EditorCellTextField({ setTimeout(() => setFocusInsideCell(false)); } if (e.key === "Enter" && !e.shiftKey) { + // Trigger an onBlur in case we have any final mutations + if (onBlur) { + onBlur(); + } + // Removes focus from inside cell, triggering save on unmount setFocusInsideCell(false); } diff --git a/src/components/fields/Number/EditorCell.tsx b/src/components/fields/Number/EditorCell.tsx index 0fae32914..7a707dad5 100644 --- a/src/components/fields/Number/EditorCell.tsx +++ b/src/components/fields/Number/EditorCell.tsx @@ -13,6 +13,11 @@ export default function Number_(props: IEditorCellProps) { const parsedValue = v === "" ? v : Number(v); props.onChange(parsedValue); }} + onBlur={() => { + // Cast to number when the user has finished editing + props.onChange(Number(props.value)); + props.onDirty(); + }} /> ); } diff --git a/src/components/fields/Number/SideDrawerField.tsx b/src/components/fields/Number/SideDrawerField.tsx index cd3f41d02..76212c8cf 100644 --- a/src/components/fields/Number/SideDrawerField.tsx +++ b/src/components/fields/Number/SideDrawerField.tsx @@ -23,7 +23,11 @@ export default function Number_({ e.target.value === "" ? e.target.value : Number(e.target.value); onChange(parsedValue); }} - onBlur={onSubmit} + onBlur={() => { + // Cast to number when the user has finished editing + onChange(Number(value)); + onSubmit(); + }} value={value} id={getFieldId(column.key)} label="" From a57848432e8718e0f490985fb35415256ebb4d7d Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Sun, 14 May 2023 13:49:12 +0100 Subject: [PATCH 86/90] Apply Safari number fixes to Percentage --- src/components/fields/Percentage/EditorCell.tsx | 13 +++++++++++-- .../fields/Percentage/SideDrawerField.tsx | 17 ++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/components/fields/Percentage/EditorCell.tsx b/src/components/fields/Percentage/EditorCell.tsx index 54247f558..c10ee35e8 100644 --- a/src/components/fields/Percentage/EditorCell.tsx +++ b/src/components/fields/Percentage/EditorCell.tsx @@ -2,7 +2,7 @@ import type { IEditorCellProps } from "@src/components/fields/types"; import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; import { multiply100WithPrecision, divide100WithPrecision } from "./utils"; -export default function Percentage(props: IEditorCellProps) { +export default function Percentage(props: IEditorCellProps) { return ( ) { : props.value } onChange={(v) => { - props.onChange(divide100WithPrecision(Number(v))); + // Safari/Firefox gives us an empty string for invalid inputs, which includes inputs like "12." on the way to + // typing "12.34". Number would cast these to 0 and replace the user's input to 0 whilst they're mid-way through + // typing. We want to avoid that. + const parsedValue = v === "" ? v : divide100WithPrecision(Number(v)); + props.onChange(parsedValue); + }} + onBlur={() => { + // Cast to number when the user has finished editing + props.onChange(Number(props.value)); + props.onDirty(); }} /> ); diff --git a/src/components/fields/Percentage/SideDrawerField.tsx b/src/components/fields/Percentage/SideDrawerField.tsx index feaf5673c..9262b9af1 100644 --- a/src/components/fields/Percentage/SideDrawerField.tsx +++ b/src/components/fields/Percentage/SideDrawerField.tsx @@ -11,7 +11,7 @@ export default function Percentage({ onChange, onSubmit, disabled, -}: ISideDrawerFieldProps) { +}: ISideDrawerFieldProps) { const { colors } = (column as any).config; const theme = useTheme(); return ( @@ -19,8 +19,19 @@ export default function Percentage({ variant="filled" fullWidth margin="none" - onChange={(e) => onChange(Number(e.target.value) / 100)} - onBlur={onSubmit} + onChange={(e) => { + // Safari/Firefox gives us an empty string for invalid inputs, which includes inputs like "12." on the way to + // typing "12.34". Number would cast these to 0 and replace the user's input to 0 whilst they're mid-way through + // typing. We want to avoid that. + const parsedValue = + e.target.value === "" ? e.target.value : Number(e.target.value) / 100; + onChange(parsedValue); + }} + onBlur={() => { + // Cast to number when the user has finished editing + onChange(Number(value)); + onSubmit(); + }} value={ typeof value === "number" ? multiply100WithPrecision(value) : value } From 316807a0ed37d65e23e9912dfc24d198b4cd1006 Mon Sep 17 00:00:00 2001 From: Manmeet Date: Tue, 23 May 2023 13:10:50 +0530 Subject: [PATCH 87/90] add sort button --- src/components/TableToolbar/Sort/Sort.tsx | 133 ++++++++++++++++++ .../TableToolbar/Sort/SortPopover.tsx | 67 +++++++++ src/components/TableToolbar/Sort/index.tsx | 2 + src/components/TableToolbar/TableToolbar.tsx | 26 ++-- 4 files changed, 219 insertions(+), 9 deletions(-) create mode 100644 src/components/TableToolbar/Sort/Sort.tsx create mode 100644 src/components/TableToolbar/Sort/SortPopover.tsx create mode 100644 src/components/TableToolbar/Sort/index.tsx diff --git a/src/components/TableToolbar/Sort/Sort.tsx b/src/components/TableToolbar/Sort/Sort.tsx new file mode 100644 index 000000000..1ab76a427 --- /dev/null +++ b/src/components/TableToolbar/Sort/Sort.tsx @@ -0,0 +1,133 @@ +import { useAtom } from "jotai"; + +import { + Grid, + IconButton, + MenuItem, + Stack, + TextField, + Typography, + alpha, +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/DeleteOutlined"; + +import { + tableColumnsOrderedAtom, + tableScope, + tableSettingsAtom, + tableSortsAtom, +} from "@src/atoms/tableScope"; +import SortPopover from "./SortPopover"; +import ColumnSelect from "@src/components/Table/ColumnSelect"; + +import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; +import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; +import { projectScope, userRolesAtom } from "@src/atoms/projectScope"; +import useSaveTableSorts from "@src/components/Table/ColumnHeader/useSaveTableSorts"; + +export default function Sort() { + const [userRoles] = useAtom(userRolesAtom, projectScope); + const [tableSettings] = useAtom(tableSettingsAtom, tableScope); + + const canEditColumns = Boolean( + userRoles.includes("ADMIN") || + tableSettings.modifiableBy?.some((r) => userRoles.includes(r)) + ); + + const [tableSorts, setTableSorts] = useAtom(tableSortsAtom, tableScope); + const triggerSaveTableSorts = useSaveTableSorts(canEditColumns); + + const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + + const sortColumns = tableColumnsOrdered.map(({ key, name, type, index }) => ({ + value: key, + label: name, + type, + index, + })); + + return ( + + {({ handleClose }) => ( + + + { + if (value) { + setTableSorts([ + { key: value, direction: tableSorts[0].direction }, + ]); + + triggerSaveTableSorts([ + { key: value, direction: tableSorts[0].direction }, + ]); + } else { + setTableSorts([]); + } + }} + /> + + + + { + setTableSorts([ + { + key: tableSorts[0].key, + direction: e.target.value === "asc" ? "asc" : "desc", + }, + ]); + triggerSaveTableSorts([ + { + key: tableSorts[0].key, + direction: e.target.value === "asc" ? "asc" : "desc", + }, + ]); + }} + > + + + + Sort ascending + + + + + + Sort descending + + + + + + setTableSorts([])} + sx={{ + "&:hover, &:focus": { + color: "error.main", + backgroundColor: (theme) => + alpha( + theme.palette.error.main, + theme.palette.action.hoverOpacity * 2 + ), + }, + }} + > + + + + + )} + + ); +} diff --git a/src/components/TableToolbar/Sort/SortPopover.tsx b/src/components/TableToolbar/Sort/SortPopover.tsx new file mode 100644 index 000000000..11d86b442 --- /dev/null +++ b/src/components/TableToolbar/Sort/SortPopover.tsx @@ -0,0 +1,67 @@ +import { useRef, useState } from "react"; +import { useAtom } from "jotai"; + +import { Popover } from "@mui/material"; + +import ButtonWithStatus from "@src/components/ButtonWithStatus"; + +import { tableScope, tableSortsAtom } from "@src/atoms/tableScope"; + +import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; + +export interface ISortPopoverProps { + children: (props: { handleClose: () => void }) => React.ReactNode; +} + +export default function SortPopover({ children }: ISortPopoverProps) { + const [tableSortPopoverState, setTableSortPopoverState] = useState(false); + + const anchorEl = useRef(null); + const popoverId = tableSortPopoverState ? "sort-popover" : undefined; + const handleClose = () => setTableSortPopoverState(false); + + const [tableSorts] = useAtom(tableSortsAtom, tableScope); + + return ( + <> + setTableSortPopoverState(true)} + active={true} + startIcon={ + + theme.transitions.create("transform", { + duration: theme.transitions.duration.short, + }), + + transform: + tableSorts[0].direction === "asc" ? "rotate(180deg)" : "none", + }} + /> + } + aria-describedby={popoverId} + > + Sorted: {tableSorts[0].key} + + + + {children({ handleClose })} + + + ); +} diff --git a/src/components/TableToolbar/Sort/index.tsx b/src/components/TableToolbar/Sort/index.tsx new file mode 100644 index 000000000..a67b91b27 --- /dev/null +++ b/src/components/TableToolbar/Sort/index.tsx @@ -0,0 +1,2 @@ +export * from "./Sort"; +export { default } from "./Sort"; diff --git a/src/components/TableToolbar/TableToolbar.tsx b/src/components/TableToolbar/TableToolbar.tsx index abd448bde..5845e1854 100644 --- a/src/components/TableToolbar/TableToolbar.tsx +++ b/src/components/TableToolbar/TableToolbar.tsx @@ -32,11 +32,15 @@ import { tableSettingsAtom, tableSchemaAtom, tableModalAtom, + tableSortsAtom, } 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 Sort = lazy(() => import("./Sort" /* webpackChunkName: "Filters" */)); + // prettier-ignore const Filters = lazy(() => import("./Filters" /* webpackChunkName: "Filters" */)); // prettier-ignore @@ -62,6 +66,7 @@ export default function TableToolbar({ const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const openTableModal = useSetAtom(tableModalAtom, tableScope); + const [tableSorts] = useAtom(tableSortsAtom, tableScope); const hasDerivatives = Object.values(tableSchema.columns ?? {}).filter( (column) => column.type === FieldType.derivative @@ -116,6 +121,11 @@ export default function TableToolbar({ )} + {tableSorts.length > 0 && tableSettings.isCollection !== false && ( + }> + + + )}
{/* Spacer */}
@@ -134,22 +144,20 @@ export default function TableToolbar({ ) )} - {(!projectSettings.exporterRoles || projectSettings.exporterRoles.length === 0 || userRoles.some((role) => projectSettings.exporterRoles?.includes(role) )) && ( - }> - openTableModal("export")} - icon={} - disabled={disabledTools.includes("export")} - /> + }> + openTableModal("export")} + icon={} + disabled={disabledTools.includes("export")} + /> )} - {userRoles.includes("ADMIN") && ( <>
{/* Spacer */} From b263c922d84cb4b9807b0c98b668b4817f01eb80 Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Wed, 10 May 2023 20:38:35 +0100 Subject: [PATCH 88/90] Add colour change customisation to Slider --- src/components/fields/Slider/DisplayCell.tsx | 13 +- src/components/fields/Slider/Settings.tsx | 167 ++++++++++++++++++- 2 files changed, 177 insertions(+), 3 deletions(-) diff --git a/src/components/fields/Slider/DisplayCell.tsx b/src/components/fields/Slider/DisplayCell.tsx index ae8cc7362..de72a45a6 100644 --- a/src/components/fields/Slider/DisplayCell.tsx +++ b/src/components/fields/Slider/DisplayCell.tsx @@ -1,18 +1,22 @@ import { IDisplayCellProps } from "@src/components/fields/types"; -import { Grid, Box } from "@mui/material"; +import { Grid, Box, useTheme } from "@mui/material"; import { resultColorsScale } from "@src/utils/color"; export default function Slider({ column, value }: IDisplayCellProps) { + const theme = useTheme(); + const { max, min, unit, + colors, }: { max: number; min: number; unit?: string; + colors: any; } = { max: 10, min: 0, @@ -24,6 +28,7 @@ export default function Slider({ column, value }: IDisplayCellProps) { ? 0 : ((value - min) / (max - min)) * 100; + const percentage = progress / 100; return ( @@ -48,7 +53,11 @@ export default function Slider({ column, value }: IDisplayCellProps) { maxWidth: "100%", width: `${progress}%`, - backgroundColor: resultColorsScale(progress / 100).toHex(), + backgroundColor: resultColorsScale( + percentage, + colors, + theme.palette.background.paper + ).toHex(), }} /> diff --git a/src/components/fields/Slider/Settings.tsx b/src/components/fields/Slider/Settings.tsx index c4bb268e3..96009d040 100644 --- a/src/components/fields/Slider/Settings.tsx +++ b/src/components/fields/Slider/Settings.tsx @@ -1,7 +1,54 @@ +import { useState } from "react"; + +import { + Box, + TextField, + FormControlLabel, + Switch, + MenuItem, + Checkbox, + Grid, + InputLabel, + Typography, + useTheme, +} from "@mui/material"; +import ColorPickerInput from "@src/components/ColorPickerInput"; import { ISettingsProps } from "@src/components/fields/types"; -import { TextField, FormControlLabel, Switch } from "@mui/material"; + +import { Color, toColor } from "react-color-palette"; +import { fieldSx } from "@src/components/SideDrawer/utils"; +import { resultColorsScale, defaultColors } from "@src/utils/color"; + +const colorLabels: { [key: string]: string } = { + 0: "Start", + 1: "Middle", + 2: "End", +}; export default function Settings({ onChange, config }: ISettingsProps) { + const colors: string[] = config.colors ?? defaultColors; + + const [checkStates, setCheckStates] = useState( + colors.map(Boolean) + ); + + const onCheckboxChange = (index: number, checked: boolean) => { + onChange("colors")( + colors.map((value: any, idx: number) => + index === idx ? (checked ? value || defaultColors[idx] : null) : value + ) + ); + setCheckStates( + checkStates.map((value, idx) => (index === idx ? checked : value)) + ); + }; + + const handleColorChange = (index: number, color: Color): void => { + onChange("colors")( + colors.map((value, idx) => (index === idx ? color.hex : value)) + ); + }; + return ( <> + + + {checkStates.map((checked: boolean, index: number) => { + const colorHex = colors[index]; + return ( + + onCheckboxChange(index, !checked)} + /> + + + {checked && ( + + + `0 0 0 1px ${theme.palette.divider} inset`, + borderRadius: 0.5, + opacity: 0.5, + }} + /> + {colorHex} + + )} + + {colorHex && ( +
+ + handleColorChange(index, color) + } + disabled={!checkStates[index]} + /> +
+ )} +
+
+ ); + })} +
+ ); } + +const Preview = ({ colors }: { colors: any }) => { + const theme = useTheme(); + return ( + + Preview: + + {[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1].map((value) => { + return ( + + + + {value * 100}% + + + ); + })} + + + ); +}; From 9b0ed580518c103dc2560fa4843ed94e24aaa41b Mon Sep 17 00:00:00 2001 From: Manmeet Date: Thu, 1 Jun 2023 10:38:08 +0530 Subject: [PATCH 89/90] save the sort in firebase --- src/components/ColumnMenu/ColumnMenu.tsx | 13 +++++----- .../Table/ColumnHeader/ColumnHeaderSort.tsx | 14 +++++----- src/components/TableToolbar/Sort/Sort.tsx | 26 ++++++++++--------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index dec3f6fe4..d84bd16d2 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -192,9 +192,10 @@ export default function ColumnMenu({ setTableSorts( isSorted && !isAsc ? [] : [{ key: sortKey, direction: "desc" }] ); - if (!isSorted || isAsc) { - triggerSaveTableSorts([{ key: sortKey, direction: "desc" }]); - } + + triggerSaveTableSorts( + isSorted && !isAsc ? [] : [{ key: sortKey, direction: "desc" }] + ); handleClose(); }, active: isSorted && !isAsc, @@ -209,9 +210,9 @@ export default function ColumnMenu({ setTableSorts( isSorted && isAsc ? [] : [{ key: sortKey, direction: "asc" }] ); - if (!isSorted || !isAsc) { - triggerSaveTableSorts([{ key: sortKey, direction: "asc" }]); - } + triggerSaveTableSorts( + isSorted && isAsc ? [] : [{ key: sortKey, direction: "asc" }] + ); handleClose(); }, active: isSorted && isAsc, diff --git a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx index 45e924f0d..2478ccbd0 100644 --- a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx @@ -38,14 +38,12 @@ export const ColumnHeaderSort = memo(function ColumnHeaderSort({ const triggerSaveTableSorts = useSaveTableSorts(canEditColumns); const handleSortClick = () => { - if (nextSort === "none") setTableSorts([]); - else setTableSorts([{ key: sortKey, direction: nextSort }]); - triggerSaveTableSorts([ - { - key: sortKey, - direction: nextSort === "none" ? "asc" : nextSort, - }, - ]); + setTableSorts( + nextSort === "none" ? [] : [{ key: sortKey, direction: nextSort }] + ); + triggerSaveTableSorts( + nextSort === "none" ? [] : [{ key: sortKey, direction: nextSort }] + ); }; return ( diff --git a/src/components/TableToolbar/Sort/Sort.tsx b/src/components/TableToolbar/Sort/Sort.tsx index 1ab76a427..9d6d362e6 100644 --- a/src/components/TableToolbar/Sort/Sort.tsx +++ b/src/components/TableToolbar/Sort/Sort.tsx @@ -57,17 +57,16 @@ export default function Sort() { options={sortColumns} value={tableSorts[0].key} onChange={(value: string | null) => { - if (value) { - setTableSorts([ - { key: value, direction: tableSorts[0].direction }, - ]); - - triggerSaveTableSorts([ - { key: value, direction: tableSorts[0].direction }, - ]); - } else { - setTableSorts([]); - } + setTableSorts( + value === null + ? [] + : [{ key: value, direction: tableSorts[0].direction }] + ); + triggerSaveTableSorts( + value === null + ? [] + : [{ key: value, direction: tableSorts[0].direction }] + ); }} />
@@ -111,7 +110,10 @@ export default function Sort() { setTableSorts([])} + onClick={() => { + setTableSorts([]); + triggerSaveTableSorts([]); + }} sx={{ "&:hover, &:focus": { color: "error.main", From 23e29ca9f7cf6aa8234abeac4a1c63be876fe113 Mon Sep 17 00:00:00 2001 From: mnmt7 <107614965+mnmt7@users.noreply.github.com> Date: Fri, 2 Jun 2023 13:17:01 +0530 Subject: [PATCH 90/90] "Filter by" option to filter by the value of the cell (#1251) * add filter-by option in right-click menu * enable filterby option on fields which are spread out versions of a JSON * remove "Filter value" option --- .../Table/ContextMenu/MenuContents.tsx | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/src/components/Table/ContextMenu/MenuContents.tsx b/src/components/Table/ContextMenu/MenuContents.tsx index 1e4cf4253..6f934c26a 100644 --- a/src/components/Table/ContextMenu/MenuContents.tsx +++ b/src/components/Table/ContextMenu/MenuContents.tsx @@ -22,6 +22,7 @@ import { userRolesAtom, altPressAtom, confirmDialogAtom, + updateUserSettingsAtom, } from "@src/atoms/projectScope"; import { tableScope, @@ -34,8 +35,10 @@ import { updateFieldAtom, tableFiltersPopoverAtom, _updateRowDbAtom, + tableIdAtom, } from "@src/atoms/tableScope"; import { FieldType } from "@src/constants/fields"; +import { TableRow } from "@src/types/table"; interface IMenuContentsProps { onClose: () => void; @@ -58,6 +61,8 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { tableFiltersPopoverAtom, tableScope ); + const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope); + const [tableId] = useAtom(tableIdAtom, tableScope); const addRowIdType = tableSchema.idType || "decrement"; @@ -241,7 +246,28 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { // Cell actions // TODO: Add copy and paste here - const cellValue = row?.[selectedCell.columnKey]; + + const selectedColumnKey = selectedCell.columnKey; + const selectedColumnKeySplit = selectedColumnKey.split("."); + + const getNestedFieldValue = (object: TableRow, keys: string[]) => { + let value = object; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + + if (value && typeof value === "object" && key in value) { + value = value[key]; + } else { + // Handle cases where the key does not exist in the nested structure + return undefined; + } + } + + return value; + }; + + const cellValue = getNestedFieldValue(row, selectedColumnKeySplit); const columnFilters = getFieldProp( "filter", @@ -249,14 +275,18 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { ? selectedColumn.config?.renderFieldType : selectedColumn?.type ); - const handleFilterValue = () => { - openTableFiltersPopover({ - defaultQuery: { + const handleFilterBy = () => { + const filters = [ + { key: selectedColumn.fieldName, operator: columnFilters!.operators[0]?.value || "==", value: cellValue, }, - }); + ]; + + if (updateUserSettings) { + updateUserSettings({ tables: { [`${tableId}`]: { filters } } }); + } onClose(); }; const cellActions = [ @@ -272,10 +302,10 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { onClick: handleClearValue, }, { - label: "Filter value", + label: "Filter by", icon: , disabled: !columnFilters || cellValue === undefined, - onClick: handleFilterValue, + onClick: handleFilterBy, }, ]; actionGroups.push(cellActions);