mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
Merge branch 'develop' into clipboard-typo-fix
This commit is contained in:
@@ -22,11 +22,21 @@ to start.
|
||||
|
||||
## Working on existing issues
|
||||
|
||||
Before you get started working on an
|
||||
[issue](https://github.com/rowyio/rowy/issues), please make sure to share that
|
||||
you are working on it by commenting on the issue and posting a message on
|
||||
#contributions channel in Rowy's
|
||||
[Discord](https://discord.com/invite/fjBugmvzZP). The maintainers will then
|
||||
assign the issue to you after making sure any relevant information or context in
|
||||
addition is provided before you can start on the task.
|
||||
|
||||
|
||||
Before you get started working on an [issue](https://github.com/rowyio/rowy/issues), please make sure to share that you are working on it by commenting on the issue and posting a message on #contributions channel in Rowy's [Discord](https://discord.com/invite/fjBugmvzZP). The maintainers will then assign the issue to you after making sure any relevant information or context in addition is provided before you can start on the task.
|
||||
|
||||
Once you are assigned a task, please provide periodic updates or share any questions or roadblocks on either discord or the Github issue, so that the commmunity or the project maintainers can provide you any feedback or guidance as needed. If you are inactive for more than 1-2 week on a issue that was assigned to you, then we will assume you have stopped working on it and we will unassign it from you - so that we can give a chance to others in the community to work on it.
|
||||
Once you are assigned a task, please provide periodic updates or share any
|
||||
questions or roadblocks on either discord or the Github issue, so that the
|
||||
commmunity or the project maintainers can provide you any feedback or guidance
|
||||
as needed. If you are inactive for more than 1-2 week on a issue that was
|
||||
assigned to you, then we will assume you have stopped working on it and we will
|
||||
unassign it from you - so that we can give a chance to others in the community
|
||||
to work on it.
|
||||
|
||||
## File a feature request
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ Low-code for Firebase and Google Cloud.
|
||||
|
||||
## Features ✨
|
||||
|
||||
|
||||
<!-- <table>
|
||||
<tr>
|
||||
<th>
|
||||
|
||||
@@ -133,3 +133,16 @@ export const FunctionsIndexAtom = atom<FunctionSettings[]>([]);
|
||||
export const updateFunctionAtom = atom<
|
||||
UpdateCollectionDocFunction<FunctionSettings> | undefined
|
||||
>(undefined);
|
||||
|
||||
export interface ISecretNames {
|
||||
loading: boolean;
|
||||
secretNames: null | string[];
|
||||
}
|
||||
|
||||
export const secretNamesAtom = atom<ISecretNames>({
|
||||
loading: true,
|
||||
secretNames: null,
|
||||
});
|
||||
export const updateSecretNamesAtom = atom<
|
||||
((clearSecretNames?: boolean) => Promise<void>) | undefined
|
||||
>(undefined);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -242,10 +242,15 @@ export interface IBulkAddRowsOptions {
|
||||
rows: Partial<TableRow[]>;
|
||||
collection: string;
|
||||
onBatchCommit?: Parameters<BulkWriteFunction>[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) },
|
||||
}));
|
||||
|
||||
@@ -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<Theme> = {
|
||||
minWidth: 400,
|
||||
minHeight,
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
@@ -61,6 +62,9 @@ export default function ColorPickerInput({
|
||||
boxSizing: "unset",
|
||||
},
|
||||
},
|
||||
".rcp-dark": {
|
||||
"--rcp-background": "transparent",
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -70,6 +74,7 @@ export default function ColorPickerInput({
|
||||
color={localValue}
|
||||
onChange={(color) => setLocalValue(color)}
|
||||
onChangeComplete={onChangeComplete}
|
||||
dark={isDark}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -78,7 +78,7 @@ 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);
|
||||
validateSettings();
|
||||
};
|
||||
|
||||
@@ -1,26 +1,39 @@
|
||||
import { Chip, ChipProps } from "@mui/material";
|
||||
import palette, { paletteToMui } from "@src/theme/palette";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { isEqual } from "lodash-es";
|
||||
|
||||
export const VARIANTS = ["yes", "no", "maybe"] as const;
|
||||
const paletteColor = {
|
||||
yes: "success",
|
||||
maybe: "warning",
|
||||
no: "error",
|
||||
yes: paletteToMui(palette.green),
|
||||
maybe: paletteToMui(palette.yellow),
|
||||
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;
|
||||
const fallback = { backgroundColor: defaultColor[mode] };
|
||||
const { sx, ...newProps } = props;
|
||||
|
||||
const label =
|
||||
typeof props.label === "string" ? props.label.toLowerCase() : "";
|
||||
const inVariant = VARIANTS.includes(label as any);
|
||||
|
||||
if (VARIANTS.includes(label as any)) {
|
||||
return (
|
||||
<Chip
|
||||
size="small"
|
||||
color={paletteColor[label as typeof VARIANTS[number]]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Chip size="small" {...props} />;
|
||||
return (
|
||||
<Chip
|
||||
size="small"
|
||||
sx={
|
||||
inVariant && isEqual(props.sx, fallback)
|
||||
? {
|
||||
backgroundColor:
|
||||
paletteColor[label as typeof VARIANTS[number]][mode],
|
||||
}
|
||||
: props.sx
|
||||
}
|
||||
{...newProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
109
src/components/SelectColors/CustomizeColorModal.tsx
Normal file
109
src/components/SelectColors/CustomizeColorModal.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
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";
|
||||
import { SelectColorThemeOptions } from ".";
|
||||
|
||||
interface CustomizeColor {
|
||||
currentColor: SelectColorThemeOptions;
|
||||
onChange: (value: SelectColorThemeOptions) => void;
|
||||
}
|
||||
|
||||
const CustomizeColorModal: FC<CustomizeColor> = ({
|
||||
currentColor,
|
||||
onChange,
|
||||
}) => {
|
||||
const [color, setColor] = useState<SelectColorThemeOptions>(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<boolean>(false);
|
||||
|
||||
/* MUI Menu event handlers */
|
||||
const handleClick = () => setOpen(true);
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button size="small" color="success" variant="text" onClick={handleClick}>
|
||||
Customise
|
||||
</Button>
|
||||
<Modal
|
||||
title="Customize Color"
|
||||
aria-labelledby="custom-color-picker-modal"
|
||||
aria-describedby="custom-color-picker-modal"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
disableBackdropClick
|
||||
>
|
||||
<Box display="grid" gridTemplateColumns="repeat(6, 1fr)" gap={1}>
|
||||
{/* Light Theme Customize Color */}
|
||||
<Box gridColumn="span 3">
|
||||
<ColorPickerInput
|
||||
value={toColor("hex", color.light)}
|
||||
onChangeComplete={(value) =>
|
||||
handleChange({ ...color, ...{ light: value.hex } })
|
||||
}
|
||||
/>
|
||||
<Grid container gap={1} py={1} px={2} alignItems="center">
|
||||
<Grid item>
|
||||
<Typography fontSize={13} fontWeight="light">
|
||||
Light Theme
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Chip
|
||||
component="small"
|
||||
size="small"
|
||||
label="Option 1"
|
||||
sx={{ backgroundColor: color.light, color: "black" }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* Dark Theme Customize Color */}
|
||||
<Box gridColumn="span 3">
|
||||
<ColorPickerInput
|
||||
value={toColor("hex", color.dark)}
|
||||
onChangeComplete={(value) =>
|
||||
handleChange({ ...color, ...{ dark: value.hex } })
|
||||
}
|
||||
/>
|
||||
<Grid container gap={1} py={1} px={2} alignItems="center">
|
||||
<Grid item>
|
||||
<Typography fontSize={13} fontWeight="light">
|
||||
Dark Theme
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Chip
|
||||
component="small"
|
||||
size="small"
|
||||
label="Option 1"
|
||||
sx={{ backgroundColor: color.dark, color: "white" }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomizeColorModal;
|
||||
192
src/components/SelectColors/index.tsx
Normal file
192
src/components/SelectColors/index.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { FC, 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 KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import FormatColorResetIcon from "@mui/icons-material/FormatColorReset";
|
||||
import { paletteToMui, palette } from "@src/theme/palette";
|
||||
import CustomizeColorModal from "./CustomizeColorModal";
|
||||
|
||||
export interface SelectColorThemeOptions {
|
||||
light: string;
|
||||
dark: string;
|
||||
}
|
||||
|
||||
interface IColorSelect {
|
||||
handleChange: (value: SelectColorThemeOptions) => void;
|
||||
initialValue: SelectColorThemeOptions;
|
||||
}
|
||||
|
||||
const ColorSelect: FC<IColorSelect> = ({ handleChange, initialValue }) => {
|
||||
/* Get current theme */
|
||||
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,
|
||||
lightBlue: palette.lightBlue,
|
||||
purple: palette.purple,
|
||||
});
|
||||
|
||||
/* Hold the current state of a given option defaults to `gray` from the color palette */
|
||||
const [color, setColor] = useState<SelectColorThemeOptions>(
|
||||
initialValue || paletteToMui(palette["gray"])
|
||||
);
|
||||
|
||||
const onChange = (color: SelectColorThemeOptions) => {
|
||||
setColor(color);
|
||||
handleChange(color);
|
||||
};
|
||||
|
||||
/* MUI Specific state for color context menu */
|
||||
const [colorSelectAnchor, setColorSelectAnchor] =
|
||||
useState<null | HTMLElement>(null);
|
||||
const open = Boolean(colorSelectAnchor);
|
||||
|
||||
/* MUI Menu event handlers for color context menu */
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setColorSelectAnchor(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setColorSelectAnchor(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
sx={{ margin: "7.5px 0", width: "auto" }}
|
||||
size="small"
|
||||
id="color-picker-btn"
|
||||
aria-controls={open ? "color-picker-menu" : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
variant="outlined"
|
||||
disableElevation
|
||||
onClick={handleClick}
|
||||
endIcon={<KeyboardArrowDownIcon />}
|
||||
>
|
||||
<Box
|
||||
m={0.5}
|
||||
sx={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 100,
|
||||
backgroundColor: color[mode],
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{/* Menu */}
|
||||
<Menu
|
||||
sx={{ marginTop: 0.5 }}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
id="color-picker-menu"
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "color-pick-btn",
|
||||
}}
|
||||
anchorEl={colorSelectAnchor}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<Typography
|
||||
fontSize={11}
|
||||
color="secondary"
|
||||
py={1}
|
||||
px={2}
|
||||
fontWeight="bold"
|
||||
>
|
||||
COLOURS
|
||||
</Typography>
|
||||
|
||||
<Grid
|
||||
container
|
||||
py={1}
|
||||
px={2}
|
||||
rowGap={2}
|
||||
columnGap={2}
|
||||
display="grid"
|
||||
gridTemplateColumns="repeat(7, auto)"
|
||||
>
|
||||
{Object.keys(palettes).map((key: string, index: number) => (
|
||||
<Grid item xs sx={{ maxWidth: "fit-content" }}>
|
||||
<Button
|
||||
sx={{
|
||||
minWidth: "25px",
|
||||
minHeight: "25px",
|
||||
backgroundColor: paletteToMui(palettes[key])[mode],
|
||||
borderRadius: 100,
|
||||
"&:hover": {
|
||||
backgroundColor: paletteToMui(palettes[key])[mode],
|
||||
},
|
||||
}}
|
||||
size="small"
|
||||
onClick={() => onChange(paletteToMui(palettes[key]))}
|
||||
key={index}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Box pt={1} px={2}>
|
||||
<CustomizeColorModal
|
||||
currentColor={color}
|
||||
onChange={(color) => onChange(color)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box px={2} py={2}>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ borderRadius: 100 }}
|
||||
fullWidth
|
||||
startIcon={<FormatColorResetIcon />}
|
||||
onClick={() => onChange(paletteToMui(palettes["gray"]))}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Grid container gap={1} py={1} px={2} alignItems="center">
|
||||
<Grid item>
|
||||
<Typography fontSize={13} fontWeight="light">
|
||||
Preview
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Chip
|
||||
component="small"
|
||||
size="small"
|
||||
label="Option 1"
|
||||
sx={{ backgroundColor: color[mode] }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorSelect;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 useConverter from "@src/components/TableModals/ImportCsvWizard/useConverter";
|
||||
|
||||
export interface ICellProps
|
||||
extends Partial<
|
||||
@@ -25,12 +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;
|
||||
const { checkAndConvert } = useConverter();
|
||||
const value = checkAndConvert(value_, type);
|
||||
|
||||
return (
|
||||
<StyledTable>
|
||||
|
||||
@@ -33,6 +33,27 @@ const selectedColumnsJsonReducer =
|
||||
(doc: TableRow) =>
|
||||
(accumulator: Record<string, any>, 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()
|
||||
: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (currentColumn.type === FieldType.reference) {
|
||||
return {
|
||||
...accumulator,
|
||||
[currentColumn.key]: value ? value.path : "",
|
||||
};
|
||||
}
|
||||
return {
|
||||
...accumulator,
|
||||
[currentColumn.key]: value,
|
||||
|
||||
@@ -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
|
||||
}`,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -57,7 +57,6 @@ export default function Step1Columns({
|
||||
config.pairs.map((pair) => pair.fieldKey)
|
||||
);
|
||||
|
||||
|
||||
const fieldKeys = Object.keys(airtableData.records[0].fields);
|
||||
|
||||
// When a field is selected to be imported
|
||||
@@ -128,8 +127,8 @@ export default function Step1Columns({
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedFields.length !== fieldKeys.length) {
|
||||
setSelectedFields(fieldKeys)
|
||||
fieldKeys.forEach(field => {
|
||||
setSelectedFields(fieldKeys);
|
||||
fieldKeys.forEach((field) => {
|
||||
// Try to match each field to a column in the table
|
||||
const match =
|
||||
find(tableColumns, (column) =>
|
||||
@@ -158,15 +157,13 @@ export default function Step1Columns({
|
||||
];
|
||||
}
|
||||
updateConfig(columnConfig);
|
||||
})
|
||||
});
|
||||
} else {
|
||||
setSelectedFields([])
|
||||
setConfig((config) => ({ ...config, newColumns: [], pairs: [] }))
|
||||
setSelectedFields([]);
|
||||
setConfig((config) => ({ ...config, newColumns: [], pairs: [] }));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
// When a field is mapped to a new column
|
||||
const handleChange = (fieldKey: string) => (value: string) => {
|
||||
if (!value) return;
|
||||
@@ -236,7 +233,13 @@ export default function Step1Columns({
|
||||
color="default"
|
||||
/>
|
||||
}
|
||||
label={selectedFields.length == fieldKeys.length ? "Clear all" : "Select all"}
|
||||
label={
|
||||
|
||||
selectedFields.length === fieldKeys.length
|
||||
|
||||
? "Clear all"
|
||||
: "Select all"
|
||||
}
|
||||
sx={{
|
||||
height: 42,
|
||||
mr: 0,
|
||||
@@ -251,8 +254,8 @@ export default function Step1Columns({
|
||||
find(config.pairs, { fieldKey: field })?.columnKey ?? null;
|
||||
const matchingColumn = columnKey
|
||||
? tableSchema.columns?.[columnKey] ??
|
||||
find(config.newColumns, { key: columnKey }) ??
|
||||
null
|
||||
find(config.newColumns, { key: columnKey }) ??
|
||||
null
|
||||
: null;
|
||||
const isNewColumn = !!find(config.newColumns, { key: columnKey });
|
||||
return (
|
||||
|
||||
@@ -37,7 +37,10 @@ import {
|
||||
import { ColumnConfig } from "@src/types/table";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
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 }[];
|
||||
@@ -65,6 +68,8 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) {
|
||||
const theme = useTheme();
|
||||
const isXs = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const snackbarProgressRef = useRef<ISnackbarProgressRef>();
|
||||
const { addTask, runBatchedUpload } = useUploadFileFromURL();
|
||||
const { needsUploadTypes, needsConverter, getConverter } = useConverter();
|
||||
|
||||
const columns = useMemoValue(tableSchema.columns ?? {}, isEqual);
|
||||
|
||||
@@ -74,6 +79,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 +129,36 @@ 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(column.type)) {
|
||||
requiredUploads[column.fieldName + ""] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return { requiredConverts, requiredUploads };
|
||||
}, [
|
||||
config.newColumns,
|
||||
config.pairs,
|
||||
getConverter,
|
||||
needsConverter,
|
||||
needsUploadTypes,
|
||||
tableSchema.columns,
|
||||
]);
|
||||
|
||||
const handleFinish = async () => {
|
||||
if (!parsedRows) return;
|
||||
console.time("importCsv");
|
||||
@@ -176,12 +212,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 +264,9 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) {
|
||||
`Imported ${Number(validRows.length).toLocaleString()} rows`,
|
||||
{ variant: "success" }
|
||||
);
|
||||
if (Object.keys(requiredUploads).length > 0) {
|
||||
await runBatchedUpload();
|
||||
}
|
||||
} catch (e) {
|
||||
enqueueSnackbar((e as Error).message, { variant: "error" });
|
||||
} finally {
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function Step1Columns({
|
||||
const handleSelectAll = () => {
|
||||
if (selectedFields.length !== csvData.columns.length) {
|
||||
setSelectedFields(csvData.columns);
|
||||
csvData.columns.forEach(field => {
|
||||
csvData.columns.forEach((field) => {
|
||||
// Try to match each field to a column in the table
|
||||
const match =
|
||||
find(tableColumns, (column) =>
|
||||
@@ -89,10 +89,10 @@ export default function Step1Columns({
|
||||
];
|
||||
}
|
||||
updateConfig(columnConfig);
|
||||
})
|
||||
});
|
||||
} else {
|
||||
setSelectedFields([])
|
||||
setConfig((config) => ({ ...config, newColumns: [], pairs: [] }))
|
||||
setSelectedFields([]);
|
||||
setConfig((config) => ({ ...config, newColumns: [], pairs: [] }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -232,7 +232,11 @@ export default function Step1Columns({
|
||||
color="default"
|
||||
/>
|
||||
}
|
||||
label={selectedFields.length == csvData.columns.length ? "Clear all" : "Select all"}
|
||||
label={
|
||||
selectedFields.length === csvData.columns.length
|
||||
? "Clear all"
|
||||
: "Select all"
|
||||
}
|
||||
sx={{
|
||||
height: 42,
|
||||
mr: 0,
|
||||
@@ -247,8 +251,8 @@ export default function Step1Columns({
|
||||
find(config.pairs, { csvKey: field })?.columnKey ?? null;
|
||||
const matchingColumn = columnKey
|
||||
? tableSchema.columns?.[columnKey] ??
|
||||
find(config.newColumns, { key: columnKey }) ??
|
||||
null
|
||||
find(config.newColumns, { key: columnKey }) ??
|
||||
null
|
||||
: null;
|
||||
const isNewColumn = !!find(config.newColumns, { key: columnKey });
|
||||
|
||||
|
||||
160
src/components/TableModals/ImportCsvWizard/useConverter.ts
Normal file
160
src/components/TableModals/ImportCsvWizard/useConverter.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { projectScope } from "@src/atoms/projectScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
|
||||
import {
|
||||
doc,
|
||||
DocumentReference as Reference,
|
||||
GeoPoint,
|
||||
} from "firebase/firestore";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
const needsConverter = (type: FieldType) =>
|
||||
[
|
||||
FieldType.image,
|
||||
FieldType.reference,
|
||||
FieldType.file,
|
||||
FieldType.geoPoint,
|
||||
].includes(type);
|
||||
|
||||
const needsUploadTypes = (type: FieldType) =>
|
||||
[FieldType.image, 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.charAt(value.length - 1) === "/") {
|
||||
value = value.slice(0, -1);
|
||||
}
|
||||
if (value.split("/").length % 2 === 0) {
|
||||
try {
|
||||
return doc(firebaseDb, value);
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const imageOrFileConverter = (urls: any): RowyFile[] => {
|
||||
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 [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const geoPointConverter = (value: any) => {
|
||||
if (!value) return null;
|
||||
if (typeof value === "string") {
|
||||
let latitude, longitude;
|
||||
// covered cases:
|
||||
// [3.2, 32.3]
|
||||
// {latitude: 3.2, longitude: 32.3}
|
||||
// "3.2, 32.3"
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (Array.isArray(parsed)) {
|
||||
[latitude, longitude] = parsed;
|
||||
} else {
|
||||
latitude = parsed.latitude;
|
||||
longitude = parsed.longitude;
|
||||
}
|
||||
|
||||
if (latitude && longitude) {
|
||||
latitude = parseFloat(latitude);
|
||||
longitude = parseFloat(longitude);
|
||||
}
|
||||
} catch (e) {
|
||||
[latitude, longitude] = value
|
||||
.split(",")
|
||||
.map((val) => parseFloat(val.trim()));
|
||||
}
|
||||
|
||||
if (latitude && longitude) {
|
||||
return new GeoPoint(latitude, longitude);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getConverter = (type: FieldType) => {
|
||||
switch (type) {
|
||||
case FieldType.image:
|
||||
case FieldType.file:
|
||||
return imageOrFileConverter;
|
||||
case FieldType.reference:
|
||||
return referenceConverter;
|
||||
case FieldType.geoPoint:
|
||||
return geoPointConverter;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
needsUploadTypes,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
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_CONCURRENT_TASKS = 10;
|
||||
|
||||
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<UploadParamTypes[]>([]);
|
||||
|
||||
const askPermission = useCallback(async (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
enqueueSnackbar("Upload files to firebase storage?", {
|
||||
persist: true,
|
||||
preventDuplicate: true,
|
||||
action: (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
closeSnackbar();
|
||||
resolve(true);
|
||||
}}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
closeSnackbar();
|
||||
resolve(false);
|
||||
}}
|
||||
>
|
||||
No
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
});
|
||||
});
|
||||
}, [enqueueSnackbar, closeSnackbar]);
|
||||
|
||||
const handleUpload = useCallback(
|
||||
async ({
|
||||
docRef,
|
||||
fieldName,
|
||||
files,
|
||||
}: UploadParamTypes): Promise<boolean> => {
|
||||
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;
|
||||
}
|
||||
await 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).then(() => {
|
||||
snackbarProgressRef.current?.setProgress((p: number) => p + 1);
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
[handleUpload]
|
||||
);
|
||||
|
||||
const snackbarProgressRef = useRef<any>(null);
|
||||
const snackbarProgressId = useRef<SnackbarKey | null>(null);
|
||||
const showProgress = useCallback(
|
||||
(totalJobs: number) => {
|
||||
snackbarProgressId.current = enqueueSnackbar(
|
||||
`Uploading files form ${Number(
|
||||
totalJobs
|
||||
).toLocaleString()} cells. This might take a while.`,
|
||||
{
|
||||
persist: true,
|
||||
action: (
|
||||
<SnackbarProgress
|
||||
stateRef={snackbarProgressRef}
|
||||
target={totalJobs}
|
||||
label=" completed"
|
||||
/>
|
||||
),
|
||||
}
|
||||
);
|
||||
},
|
||||
[enqueueSnackbar]
|
||||
);
|
||||
|
||||
const runBatchedUpload = useCallback(async () => {
|
||||
if (!snackbarProgressId.current) {
|
||||
showProgress(jobs.current.length);
|
||||
}
|
||||
let currentJobs: UploadParamTypes[] = [];
|
||||
|
||||
while (
|
||||
currentJobs.length < MAX_CONCURRENT_TASKS &&
|
||||
jobs.current.length > 0
|
||||
) {
|
||||
const job = jobs.current.shift();
|
||||
if (job) {
|
||||
currentJobs.push(job);
|
||||
}
|
||||
}
|
||||
|
||||
await batchUpload(currentJobs);
|
||||
|
||||
if (jobs.current.length > 0) {
|
||||
await runBatchedUpload();
|
||||
}
|
||||
|
||||
if (snackbarProgressId.current) {
|
||||
closeSnackbar(snackbarProgressId.current);
|
||||
}
|
||||
}, [batchUpload, closeSnackbar, showProgress, snackbarProgressId]);
|
||||
|
||||
const addTask = useCallback((job: UploadParamTypes) => {
|
||||
jobs.current.push(job);
|
||||
}, []);
|
||||
|
||||
const hasUploadJobs = () => jobs.current.length > 0;
|
||||
return {
|
||||
addTask,
|
||||
runBatchedUpload,
|
||||
askPermission,
|
||||
hasUploadJobs,
|
||||
};
|
||||
}
|
||||
|
||||
function getFileFromURL(urls: string[]): Promise<File[]> {
|
||||
const promises = urls.map((url) => {
|
||||
return fetch(url)
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => new File([blob], +new Date() + "", { type: blob.type }));
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -18,14 +18,21 @@ export const SELECTABLE_TYPES = [
|
||||
FieldType.url,
|
||||
FieldType.rating,
|
||||
|
||||
FieldType.image,
|
||||
FieldType.file,
|
||||
|
||||
FieldType.singleSelect,
|
||||
FieldType.multiSelect,
|
||||
|
||||
FieldType.json,
|
||||
FieldType.code,
|
||||
|
||||
FieldType.geoPoint,
|
||||
|
||||
FieldType.color,
|
||||
FieldType.slider,
|
||||
|
||||
FieldType.reference,
|
||||
];
|
||||
|
||||
export const REGEX_EMAIL =
|
||||
|
||||
@@ -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 (
|
||||
<Typography color="text.disabled">
|
||||
<WarningIcon aria-label="Warning" style={{ verticalAlign: "bottom" }} />
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Typography, Link, TextField } from "@mui/material";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
import { IWebhook } 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) => {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="inherit" paragraph>
|
||||
For Firebase authentication, you need to include the following header
|
||||
in your request:
|
||||
<br />
|
||||
<code>Authorization: Bear ACCESS_TOKEN</code>
|
||||
</Typography>
|
||||
|
||||
<Typography variant="inherit" paragraph>
|
||||
Once enabled requests without a valid token will return{" "}
|
||||
<code>401</code> response.
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default webhookFirebaseAuth;
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Typography gutterBottom>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Typography gutterBottom>
|
||||
@@ -77,8 +80,9 @@ export const webhookStripe = {
|
||||
</Typography>
|
||||
|
||||
{webhookObject.auth.secretKey &&
|
||||
!secrets.loading &&
|
||||
!secrets.keys.includes(webhookObject.auth.secretKey) && (
|
||||
!secretNames.loading &&
|
||||
secretNames.secretNames &&
|
||||
!secretNames.secretNames.includes(webhookObject.auth.secretKey) && (
|
||||
<Alert severity="error" sx={{ height: "auto!important" }}>
|
||||
Your previously selected key{" "}
|
||||
<code>{webhookObject.auth.secretKey}</code> does not exist in
|
||||
@@ -86,34 +90,55 @@ export const webhookStripe = {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<FormControl fullWidth margin={"normal"}>
|
||||
<InputLabel id="stripe-secret-key">Secret key</InputLabel>
|
||||
<Select
|
||||
labelId="stripe-secret-key"
|
||||
id="stripe-secret-key"
|
||||
label="Secret key"
|
||||
variant="filled"
|
||||
value={webhookObject.auth.secretKey}
|
||||
onChange={(e) => {
|
||||
setWebhookObject({
|
||||
...webhookObject,
|
||||
auth: { ...webhookObject.auth, secretKey: e.target.value },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{secrets.keys.map((secret) => {
|
||||
return <MenuItem value={secret}>{secret}</MenuItem>;
|
||||
})}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
const secretManagerLink = `https://console.cloud.google.com/security/secret-manager/create?project=${secrets.projectId}`;
|
||||
window?.open?.(secretManagerLink, "_blank")?.focus();
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginY: 1,
|
||||
}}
|
||||
>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="stripe-secret-key">Secret key</InputLabel>
|
||||
<Select
|
||||
labelId="stripe-secret-key"
|
||||
id="stripe-secret-key"
|
||||
label="Secret key"
|
||||
variant="filled"
|
||||
value={webhookObject.auth.secretKey}
|
||||
onChange={(e) => {
|
||||
setWebhookObject({
|
||||
...webhookObject,
|
||||
auth: { ...webhookObject.auth, secretKey: e.target.value },
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add a key in Secret Manager
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{secretNames.secretNames?.map((secret) => {
|
||||
return <MenuItem value={secret}>{secret}</MenuItem>;
|
||||
})}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
const secretManagerLink = `https://console.cloud.google.com/security/secret-manager/create`;
|
||||
window?.open?.(secretManagerLink, "_blank")?.focus();
|
||||
}}
|
||||
>
|
||||
Add a key in Secret Manager
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<LoadingButton
|
||||
sx={{
|
||||
height: "100%",
|
||||
marginLeft: 1,
|
||||
}}
|
||||
loading={secretNames.loading}
|
||||
onClick={() => {
|
||||
updateSecretNames?.();
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
<TextField
|
||||
id="stripe-signing-secret"
|
||||
label="Signing key"
|
||||
|
||||
@@ -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 webhookTypeform = {
|
||||
name: "Typeform",
|
||||
@@ -83,11 +80,7 @@ export const webhookTypeform = {
|
||||
// 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 (
|
||||
<>
|
||||
<Typography gutterBottom>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Typography gutterBottom>
|
||||
|
||||
@@ -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<ISecret>({
|
||||
loading: true,
|
||||
keys: [],
|
||||
projectId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
rowyRun({
|
||||
route: runRoutes.listSecrets,
|
||||
}).then((secrets) => {
|
||||
setSecrets({
|
||||
loading: false,
|
||||
keys: secrets as string[],
|
||||
projectId,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="inherit" paragraph>
|
||||
@@ -63,10 +35,9 @@ export default function Step1Endpoint({
|
||||
/>
|
||||
|
||||
{webhookObject.auth?.enabled &&
|
||||
webhookSchemas[webhookObject.type].auth(
|
||||
webhookSchemas[webhookObject.type].Auth(
|
||||
webhookObject,
|
||||
setWebhookObject,
|
||||
secrets
|
||||
setWebhookObject
|
||||
)}
|
||||
{}
|
||||
</>
|
||||
|
||||
@@ -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<WebhookType, string> = {
|
||||
sendgrid: "SendGrid",
|
||||
typeform: "Typeform",
|
||||
firebaseAuth: "Firebase Auth",
|
||||
//github:"GitHub",
|
||||
// shopify: "Shopify",
|
||||
// twitter: "Twitter",
|
||||
@@ -98,18 +119,13 @@ export interface IWebhook {
|
||||
auth?: any;
|
||||
}
|
||||
|
||||
export interface ISecret {
|
||||
loading: boolean;
|
||||
keys: string[];
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export const webhookSchemas = {
|
||||
basic,
|
||||
typeform,
|
||||
sendgrid,
|
||||
webform,
|
||||
stripe,
|
||||
firebaseAuth,
|
||||
};
|
||||
|
||||
export function emptyWebhookObject(
|
||||
|
||||
@@ -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 <ShortTextComponent {...props} />;
|
||||
}
|
||||
|
||||
@@ -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<HTMLDivElement>(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" },
|
||||
|
||||
@@ -132,20 +132,6 @@ export default function ImportFromFile() {
|
||||
};
|
||||
}, [setImportCsv]);
|
||||
|
||||
const parseFile = useCallback((rawData: string) => {
|
||||
if (importTypeRef.current === "json") {
|
||||
if (!hasProperJsonStructure(rawData)) {
|
||||
return setError("Invalid Structure! It must be an Array");
|
||||
}
|
||||
const converted = convertJSONToCSV(rawData);
|
||||
if (!converted) {
|
||||
return setError("No columns detected");
|
||||
}
|
||||
rawData = converted;
|
||||
}
|
||||
parseCsv(rawData);
|
||||
}, []);
|
||||
|
||||
const parseCsv = useCallback(
|
||||
(csvString: string) =>
|
||||
parse(csvString, { delimiter: [",", "\t"] }, (err, rows) => {
|
||||
@@ -162,7 +148,7 @@ export default function ImportFromFile() {
|
||||
{}
|
||||
)
|
||||
);
|
||||
console.log(mappedRows);
|
||||
// console.log(mappedRows);
|
||||
setImportCsv({
|
||||
importType: importTypeRef.current,
|
||||
csvData: { columns, rows: mappedRows },
|
||||
@@ -174,6 +160,23 @@ export default function ImportFromFile() {
|
||||
[setImportCsv]
|
||||
);
|
||||
|
||||
const parseFile = useCallback(
|
||||
(rawData: string) => {
|
||||
if (importTypeRef.current === "json") {
|
||||
if (!hasProperJsonStructure(rawData)) {
|
||||
return setError("Invalid Structure! It must be an Array");
|
||||
}
|
||||
const converted = convertJSONToCSV(rawData);
|
||||
if (!converted) {
|
||||
return setError("No columns detected");
|
||||
}
|
||||
rawData = converted;
|
||||
}
|
||||
parseCsv(rawData);
|
||||
},
|
||||
[parseCsv]
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
async (acceptedFiles: File[]) => {
|
||||
try {
|
||||
|
||||
@@ -103,13 +103,19 @@ export default function TableToolbar() {
|
||||
<ImportData />
|
||||
</Suspense>
|
||||
)}
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
<TableToolbarButton
|
||||
title="Export/Download"
|
||||
onClick={() => openTableModal("export")}
|
||||
icon={<ExportIcon />}
|
||||
/>
|
||||
</Suspense>
|
||||
{(!projectSettings.exporterRoles ||
|
||||
projectSettings.exporterRoles.length === 0 ||
|
||||
userRoles.some((role) =>
|
||||
projectSettings.exporterRoles?.includes(role)
|
||||
)) && (
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
<TableToolbarButton
|
||||
title="Export/Download"
|
||||
onClick={() => openTableModal("export")}
|
||||
icon={<ExportIcon />}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
{userRoles.includes("ADMIN") && (
|
||||
<>
|
||||
<div /> {/* Spacer */}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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), DATE_TIME_FORMAT);
|
||||
} else {
|
||||
return format(value.toDate(), DATE_TIME_FORMAT);
|
||||
}
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
||||
|
||||
@@ -25,17 +25,5 @@ export const config: IFieldConfig = {
|
||||
popoverProps: { PaperProps: { sx: { p: 1, pt: 0 } } },
|
||||
}),
|
||||
SideDrawerField,
|
||||
csvImportParser: (value: string) => {
|
||||
try {
|
||||
const { latitude, longitude } = JSON.parse(value);
|
||||
if (latitude && longitude) {
|
||||
return new GeoPoint(latitude, longitude);
|
||||
}
|
||||
throw new Error();
|
||||
} catch (e) {
|
||||
console.error("Invalid GeoPoint value");
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
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 {
|
||||
getColors,
|
||||
IColors,
|
||||
} from "@src/components/fields/SingleSelect/Settings";
|
||||
|
||||
export default function MultiSelect({
|
||||
value,
|
||||
@@ -14,7 +18,11 @@ export default function MultiSelect({
|
||||
disabled,
|
||||
tabIndex,
|
||||
rowHeight,
|
||||
column,
|
||||
}: IDisplayCellProps) {
|
||||
const colors: IColors[] = column?.config?.colors ?? [];
|
||||
const { mode } = useTheme().palette;
|
||||
|
||||
const rendered =
|
||||
typeof value === "string" && value !== "" ? (
|
||||
<div style={{ flexGrow: 1, paddingLeft: "var(--cell-padding)" }}>
|
||||
@@ -30,7 +38,12 @@ export default function MultiSelect({
|
||||
(item) =>
|
||||
typeof item === "string" && (
|
||||
<Grid item key={item}>
|
||||
<FormattedChip label={item} />
|
||||
<FormattedChip
|
||||
label={item}
|
||||
sx={{
|
||||
backgroundColor: getColors(colors, item)[mode],
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
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";
|
||||
|
||||
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,7 +17,10 @@ export default function MultiSelect({
|
||||
onSubmit,
|
||||
disabled,
|
||||
}: ISideDrawerFieldProps) {
|
||||
const defaultColor = paletteToMui(palette.aGray);
|
||||
const config = column.config ?? {};
|
||||
const colors = column.config?.colors ?? [];
|
||||
const { mode } = useTheme().palette;
|
||||
|
||||
const handleDelete = (index: number) => () => {
|
||||
const newValues = [...value];
|
||||
@@ -75,6 +80,10 @@ export default function MultiSelect({
|
||||
<FormattedChip
|
||||
label={item}
|
||||
onDelete={disabled ? undefined : handleDelete(i)}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
getColors(colors, item)[mode] || defaultColor[mode],
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)
|
||||
|
||||
@@ -29,5 +29,6 @@ export const config: IFieldConfig = {
|
||||
}),
|
||||
SideDrawerField,
|
||||
filter: { operators: filterOperators, valueFormatter: valueFormatter },
|
||||
csvExportFormatter: (value: any) => value?.path,
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
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 ChipList from "@src/components/Table/TableCell/ChipList";
|
||||
import { getColors, IColors } from "./Settings";
|
||||
|
||||
export default function SingleSelect({
|
||||
value,
|
||||
showPopoverCell,
|
||||
disabled,
|
||||
tabIndex,
|
||||
column,
|
||||
rowHeight,
|
||||
}: IDisplayCellProps) {
|
||||
const colors: IColors[] = column?.config?.colors ?? [];
|
||||
const { mode } = useTheme().palette;
|
||||
|
||||
const rendered = (
|
||||
<div
|
||||
style={{
|
||||
@@ -19,7 +27,17 @@ export default function SingleSelect({
|
||||
paddingLeft: "var(--cell-padding)",
|
||||
}}
|
||||
>
|
||||
{sanitiseValue(value)}
|
||||
<ChipList rowHeight={rowHeight}>
|
||||
{value && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={sanitiseValue(value)}
|
||||
sx={{
|
||||
backgroundColor: getColors(colors, value)[mode],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ChipList>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
} from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/AddCircle";
|
||||
import RemoveIcon from "@mui/icons-material/CancelRounded";
|
||||
import ColorSelect, {
|
||||
SelectColorThemeOptions,
|
||||
} from "@src/components/SelectColors";
|
||||
|
||||
import {
|
||||
DragDropContext,
|
||||
@@ -23,6 +26,7 @@ import {
|
||||
NotDraggingStyle,
|
||||
} from "react-beautiful-dnd";
|
||||
import DragIndicatorOutlinedIcon from "@mui/icons-material/DragIndicatorOutlined";
|
||||
import palette, { paletteToMui } from "@src/theme/palette";
|
||||
|
||||
const getItemStyle = (
|
||||
isDragging: boolean,
|
||||
@@ -33,10 +37,29 @@ const getItemStyle = (
|
||||
...draggableStyle,
|
||||
});
|
||||
|
||||
export interface IColors extends SelectColorThemeOptions {
|
||||
name: 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);
|
||||
// Null check in return
|
||||
return color || defaultColor;
|
||||
};
|
||||
|
||||
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 */
|
||||
let colors = config.colors ?? [];
|
||||
|
||||
const handleAdd = () => {
|
||||
if (newOption.trim() !== "") {
|
||||
if (options.includes(newOption)) {
|
||||
@@ -49,6 +72,38 @@ export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleChipColorChange = (
|
||||
type: "save" | "delete",
|
||||
key: string,
|
||||
color?: SelectColorThemeOptions
|
||||
) => {
|
||||
const _key = key.toLocaleLowerCase().replace(" ", "_").trim();
|
||||
const exists = colors.findIndex((option: IColors) => option.name === _key);
|
||||
|
||||
// If saving Check if object with the `color.name` is equal to `_key` and replace value at the index of `exists`
|
||||
// Else save new value with `_key` as `color.name`
|
||||
if (type === "save") {
|
||||
if (exists !== -1) {
|
||||
colors[exists] = { name: _key, ...{ ...color } };
|
||||
onChange("colors")(colors);
|
||||
} else {
|
||||
onChange("colors")([...colors, { name: _key, ...{ ...color } }]);
|
||||
}
|
||||
}
|
||||
// If deleting Filter out object that has `color.name` equals to `_key`
|
||||
if (type === "delete") {
|
||||
const updatedColors = colors.filter(
|
||||
(option: IColors) => option.name !== _key
|
||||
);
|
||||
onChange("colors")(updatedColors);
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemDelete = (option: string) => {
|
||||
onChange("options")(options.filter((o: string) => o !== option));
|
||||
handleChipColorChange("delete", option);
|
||||
};
|
||||
|
||||
const handleOnDragEnd = (result: any) => {
|
||||
if (!result.destination) return;
|
||||
const [removed] = options.splice(result.source.index, 1);
|
||||
@@ -92,6 +147,7 @@ export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
{...provided.dragHandleProps}
|
||||
item
|
||||
sx={{ display: "flex" }}
|
||||
alignItems="center"
|
||||
>
|
||||
<DragIndicatorOutlinedIcon
|
||||
color="disabled"
|
||||
@@ -101,16 +157,26 @@ export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Typography>{option}</Typography>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
>
|
||||
<ColorSelect
|
||||
key={option}
|
||||
initialValue={getColors(colors, option)}
|
||||
handleChange={(color) =>
|
||||
handleChipColorChange("save", option, color)
|
||||
}
|
||||
/>
|
||||
<Typography>{option}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
aria-label="Remove"
|
||||
onClick={() =>
|
||||
onChange("options")(
|
||||
options.filter((o: string) => o !== option)
|
||||
)
|
||||
}
|
||||
onClick={() => handleItemDelete(option)}
|
||||
>
|
||||
{<RemoveIcon />}
|
||||
</IconButton>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -113,7 +113,12 @@ export default function AuthLayout({
|
||||
}
|
||||
>
|
||||
{title && (
|
||||
<Typography component="h1" variant="h4" align="center" sx={{ mt: -1 }}>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h4"
|
||||
align="center"
|
||||
sx={{ mt: -1 }}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
1
src/types/settings.d.ts
vendored
1
src/types/settings.d.ts
vendored
@@ -32,6 +32,7 @@ export type ProjectSettings = Partial<{
|
||||
builder: string;
|
||||
terminal: string;
|
||||
}>;
|
||||
exporterRoles?: string[];
|
||||
}>;
|
||||
|
||||
/** User info and settings */
|
||||
|
||||
3
src/types/table.d.ts
vendored
3
src/types/table.d.ts
vendored
@@ -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<string, ColumnConfig>;
|
||||
idType?: TableIdType;
|
||||
rowHeight?: number;
|
||||
filters?: TableFilter[];
|
||||
filtersOverridable?: boolean;
|
||||
|
||||
12
yarn.lock
12
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"
|
||||
@@ -12732,9 +12732,9 @@ webpack-sources@^3.2.3:
|
||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||
|
||||
webpack@^5.64.4:
|
||||
version "5.75.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152"
|
||||
integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==
|
||||
version "5.76.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
|
||||
integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.3"
|
||||
"@types/estree" "^0.0.51"
|
||||
|
||||
Reference in New Issue
Block a user