From 3ba5fef72f1e8d4aae4c6110f69eb0d25d946930 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 9 May 2022 16:01:56 +1000 Subject: [PATCH] enable TableSettingsDialog --- src/atoms/globalScope/project.ts | 50 ++++ src/atoms/globalScope/rowyRun.ts | 31 ++- src/atoms/globalScope/ui.ts | 11 +- src/components/SteppedAccordion.tsx | 2 +- .../ActionsMenu/ActionsMenu.tsx | 54 ++++- .../ActionsMenu/ExportSettings.tsx | 34 ++- .../ActionsMenu/ImportSettings.tsx | 28 ++- .../TableSettingsDialog/DeleteMenu.tsx | 42 ++-- .../TableSettingsDialog.tsx | 64 ++--- src/components/TableSettingsDialog/form.tsx | 2 +- src/components/fields/index.tsx | 133 +++++++++++ src/components/fields/types.ts | 86 +++++++ src/sources/ProjectSourceFirebase.tsx | 168 ------------- .../ProjectSourceFirebase.tsx | 35 +++ src/sources/ProjectSourceFirebase/index.ts | 4 + src/sources/ProjectSourceFirebase/init.ts | 65 +++++ .../ProjectSourceFirebase/useAuthUser.ts | 54 +++++ .../ProjectSourceFirebase/useSettingsDocs.ts | 53 +++++ .../useTableFunctions.ts | 223 ++++++++++++++++++ src/types/table.d.ts | 3 +- 20 files changed, 872 insertions(+), 270 deletions(-) create mode 100644 src/components/fields/index.tsx create mode 100644 src/components/fields/types.ts delete mode 100644 src/sources/ProjectSourceFirebase.tsx create mode 100644 src/sources/ProjectSourceFirebase/ProjectSourceFirebase.tsx create mode 100644 src/sources/ProjectSourceFirebase/index.ts create mode 100644 src/sources/ProjectSourceFirebase/init.ts create mode 100644 src/sources/ProjectSourceFirebase/useAuthUser.ts create mode 100644 src/sources/ProjectSourceFirebase/useSettingsDocs.ts create mode 100644 src/sources/ProjectSourceFirebase/useTableFunctions.ts diff --git a/src/atoms/globalScope/project.ts b/src/atoms/globalScope/project.ts index df8ecd86..f19a027e 100644 --- a/src/atoms/globalScope/project.ts +++ b/src/atoms/globalScope/project.ts @@ -8,7 +8,9 @@ import { UpdateDocFunction, UpdateCollectionFunction, TableSettings, + TableSchema, } from "@src/types/table"; +import { FieldType } from "@src/constants/fields"; export const projectIdAtom = atom(""); @@ -75,6 +77,54 @@ export const tablesAtom = atom((get) => { })); }); +/** + * Additional table settings that can be passed to write functions + * but are not written to the settings document + */ +export type AdditionalTableSettings = Partial<{ + _schemaSource: string; + _initialColumns: Record; + _schema: TableSchema; + _suggestedRules: string; +}>; + +/** Stores a function to create a table with schema doc */ +export const createTableAtom = atom< + | (( + settings: TableSettings, + additionalSettings?: AdditionalTableSettings + ) => Promise) + | null +>(null); + +/** + * Minimum amount of table settings required to be passed to updateTable to + * idetify the table and schema doc + */ +export type MinimumTableSettings = { + id: TableSettings["id"]; + tableType: TableSettings["tableType"]; +} & Partial; + +/** Stores a function to update a table and its schema doc */ +export const updateTableAtom = atom< + | (( + settings: MinimumTableSettings, + additionalSettings?: AdditionalTableSettings + ) => Promise) + | null +>(null); + +/** Stores a function to delete a table and its schema doc */ +export const deleteTableAtom = atom<((id: string) => Promise) | null>( + null +); + +/** Stores a function to get a table’s schema doc (without listener) */ +export const getTableSchemaAtom = atom< + ((id: string) => Promise) | null +>(null); + /** Roles used in the project based on table settings */ export const rolesAtom = atom((get) => Array.from( diff --git a/src/atoms/globalScope/rowyRun.ts b/src/atoms/globalScope/rowyRun.ts index e53d102e..f9815f7b 100644 --- a/src/atoms/globalScope/rowyRun.ts +++ b/src/atoms/globalScope/rowyRun.ts @@ -2,10 +2,11 @@ import { atom } from "jotai"; import { selectAtom, atomWithStorage } from "jotai/utils"; import { isEqual } from "lodash-es"; import { getIdTokenResult } from "firebase/auth"; +import { compare } from "compare-versions"; import { projectSettingsAtom } from "./project"; import { currentUserAtom } from "./auth"; -import { RunRoute } from "@src/constants/runRoutes"; +import { RunRoute, runRoutes } from "@src/constants/runRoutes"; import meta from "@root/package.json"; /** @@ -114,6 +115,34 @@ export const rowyRunAtom = atom((get) => { }; }); +/** Store deployed Rowy Run version */ +export const rowyRunVersionAtom = atom(async (get) => { + const rowyRun = get(rowyRunAtom); + const response = await rowyRun({ route: runRoutes.version }); + return response.version as string | false; +}); + +/** + * Helper function to check if deployed Rowy Run version + * is compatible with a feature + */ +export const compatibleRowyRunVersionAtom = atom((get) => { + const deployedVersion = get(rowyRunVersionAtom); + + return ({ + minVersion, + maxVersion, + }: { + minVersion?: string; + maxVersion?: string; + }) => { + if (!deployedVersion) return false; + if (minVersion && compare(deployedVersion, minVersion, "<")) return false; + if (maxVersion && compare(deployedVersion, maxVersion, ">")) return false; + return true; + }; +}); + type RowyRunLatestUpdate = { lastChecked: string; rowy: null | Record; diff --git a/src/atoms/globalScope/ui.ts b/src/atoms/globalScope/ui.ts index 1725a696..8092bcf7 100644 --- a/src/atoms/globalScope/ui.ts +++ b/src/atoms/globalScope/ui.ts @@ -2,7 +2,8 @@ import { atom } from "jotai"; import { atomWithStorage } from "jotai/utils"; import { DialogProps, ButtonProps } from "@mui/material"; -import { TableSettings } from "@src/types/table"; +import { TableSettings, TableSchema } from "@src/types/table"; +import { getTableSchemaAtom } from "./project"; /** Nav open state stored in local storage. */ export const navOpenAtom = atomWithStorage("__ROWY__NAV_OPEN", false); @@ -124,3 +125,11 @@ export const tableSettingsDialogAtom = atom( }); } ); + +export const tableSettingsDialogIdAtom = atom(""); +export const tableSettingsDialogSchemaAtom = atom(async (get) => { + const tableId = get(tableSettingsDialogIdAtom); + const getTableSchema = get(getTableSchemaAtom); + if (!tableId || !getTableSchema) return {} as TableSchema; + return getTableSchema(tableId); +}); diff --git a/src/components/SteppedAccordion.tsx b/src/components/SteppedAccordion.tsx index 3b3a85f7..8e691790 100644 --- a/src/components/SteppedAccordion.tsx +++ b/src/components/SteppedAccordion.tsx @@ -94,7 +94,7 @@ export default function SteppedAccordion({ > {title} - + diff --git a/src/components/TableSettingsDialog/ActionsMenu/ActionsMenu.tsx b/src/components/TableSettingsDialog/ActionsMenu/ActionsMenu.tsx index 591bd984..f95b4ffb 100644 --- a/src/components/TableSettingsDialog/ActionsMenu/ActionsMenu.tsx +++ b/src/components/TableSettingsDialog/ActionsMenu/ActionsMenu.tsx @@ -1,15 +1,20 @@ -import { useState } from "react"; +import { useState, Suspense } from "react"; import { Control } from "react-hook-form"; +import { useSetAtom } from "jotai"; import type { UseFormReturn, FieldValues } from "react-hook-form"; -import { IconButton, Menu } from "@mui/material"; +import { IconButton, Menu, MenuItem } from "@mui/material"; import ExportIcon from "assets/icons/Export"; import ImportIcon from "assets/icons/Import"; import ImportSettings from "./ImportSettings"; import ExportSettings from "./ExportSettings"; -import { TableSettingsDialogState } from "@src/atoms/globalScope"; +import { + globalScope, + tableSettingsDialogIdAtom, + TableSettingsDialogState, +} from "@src/atoms/globalScope"; export interface IActionsMenuProps { mode: TableSettingsDialogState["mode"]; @@ -24,7 +29,25 @@ export default function ActionsMenu({ }: IActionsMenuProps) { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); - const handleClose = () => setAnchorEl(null); + + const setTableSettingsDialogId = useSetAtom( + tableSettingsDialogIdAtom, + globalScope + ); + + // On open, set tableSettingsDialogIdAtom so the derived + // tableSettingsDialogSchemaAtom can fetch the schema doc + const handleOpen: React.MouseEventHandler = (e) => { + setAnchorEl(e.currentTarget); + const tableId = useFormMethods.getValues("id") as string; + setTableSettingsDialogId(tableId); + }; + // Reset the tableSettingsDialogIdAtom so we fetch fresh data every time + // the menu is opened + const handleClose = () => { + setAnchorEl(null); + setTableSettingsDialogId(""); + }; return ( <> @@ -34,7 +57,7 @@ export default function ActionsMenu({ aria-controls="table-settings-actions-menu" aria-haspopup="true" aria-expanded={open ? "true" : undefined} - onClick={(e) => setAnchorEl(e.currentTarget)} + onClick={handleOpen} > {mode === "create" ? : } @@ -49,12 +72,21 @@ export default function ActionsMenu({ anchorOrigin={{ vertical: "bottom", horizontal: "right" }} transformOrigin={{ vertical: "top", horizontal: "right" }} > - - + + Loading table settings… + + + } + > + + + ); diff --git a/src/components/TableSettingsDialog/ActionsMenu/ExportSettings.tsx b/src/components/TableSettingsDialog/ActionsMenu/ExportSettings.tsx index 92ef49e9..225d3f78 100644 --- a/src/components/TableSettingsDialog/ActionsMenu/ExportSettings.tsx +++ b/src/components/TableSettingsDialog/ActionsMenu/ExportSettings.tsx @@ -1,15 +1,21 @@ import { useState } from "react"; +import { useAtom } from "jotai"; import { Control, useWatch } from "react-hook-form"; import stringify from "json-stable-stringify-without-jsonify"; -import { isEmpty } from "lodash-es"; +import { merge } from "lodash-es"; import { useSnackbar } from "notistack"; -import { MenuItem, DialogContentText, LinearProgress } from "@mui/material"; +import { MenuItem, DialogContentText } from "@mui/material"; -import { analytics, logEvent } from "@src/analytics"; import Modal from "@src/components/Modal"; import CodeEditor from "@src/components/CodeEditor"; +import { + globalScope, + tableSettingsDialogSchemaAtom, +} from "@src/atoms/globalScope"; +import { analytics, logEvent } from "@src/analytics"; + export interface IExportSettingsProps { closeMenu: () => void; control: Control; @@ -22,17 +28,12 @@ export default function ExportSettings({ const [open, setOpen] = useState(false); const { _suggestedRules, ...values } = useWatch({ control }); - // TODO: - const tableConfigState = {} as any; - // const [tableConfigState] = useTableConfig(values.id); - const { id, ref, ..._schema } = tableConfigState.doc ?? {}; + const [tableSchema] = useAtom(tableSettingsDialogSchemaAtom, globalScope); const formattedJson = stringify( - // Allow values._schema to take priority if user imported _schema before - "_schema" in values || isEmpty(_schema) ? values : { ...values, _schema }, + { ...values, _schema: merge(tableSchema, values._schema) }, { space: 2, - // TODO: types cmp: (a: any, b: any) => // Sort _schema at the end a.key.startsWith("_") @@ -66,16 +67,9 @@ export default function ExportSettings({ onClose={handleClose} title="Export table settings" header={ - <> - {tableConfigState.loading && values.id && ( - - )} - - Export table settings and columns in JSON format - - + + Export table settings and columns in JSON format + } body={
diff --git a/src/components/TableSettingsDialog/ActionsMenu/ImportSettings.tsx b/src/components/TableSettingsDialog/ActionsMenu/ImportSettings.tsx index 69415ff2..eb40cf3c 100644 --- a/src/components/TableSettingsDialog/ActionsMenu/ImportSettings.tsx +++ b/src/components/TableSettingsDialog/ActionsMenu/ImportSettings.tsx @@ -1,9 +1,9 @@ import { useState } from "react"; -import { useSetAtom } from "jotai"; +import { useAtom, useSetAtom } from "jotai"; import { Control, useWatch } from "react-hook-form"; import type { UseFormReturn, FieldValues } from "react-hook-form"; import stringify from "json-stable-stringify-without-jsonify"; -import { isEmpty, get } from "lodash-es"; +import { merge, get } from "lodash-es"; import { useSnackbar } from "notistack"; import { MenuItem, DialogContentText, FormHelperText } from "@mui/material"; @@ -11,8 +11,12 @@ import { MenuItem, DialogContentText, FormHelperText } from "@mui/material"; import Modal from "@src/components/Modal"; import DiffEditor from "@src/components/CodeEditor/DiffEditor"; -// import useTableConfig from "@src/hooks/useTable/useTableConfig"; -import { globalScope, confirmDialogAtom } from "@src/atoms/globalScope"; +import { + globalScope, + confirmDialogAtom, + tableSettingsDialogSchemaAtom, + tableSettingsDialogAtom, +} from "@src/atoms/globalScope"; import { analytics, logEvent } from "@src/analytics"; export interface IImportSettingsProps { @@ -32,17 +36,12 @@ export default function ImportSettings({ const [valid, setValid] = useState(true); const { _suggestedRules, ...values } = useWatch({ control }); - // TODO: - const tableConfigState = {} as any; - // const [tableConfigState] = useTableConfig(values.id); - const { id, ref, ..._schema } = tableConfigState.doc ?? {}; + const [tableSchema] = useAtom(tableSettingsDialogSchemaAtom, globalScope); const formattedJson = stringify( - // Allow values._schema to take priority if user imported _schema before - "_schema" in values || isEmpty(_schema) ? values : { ...values, _schema }, + { ...values, _schema: merge(tableSchema, values._schema) }, { space: 2, - // TODO: types cmp: (a: any, b: any) => // Sort _schema at the end a.key.startsWith("_") @@ -62,6 +61,7 @@ export default function ImportSettings({ const confirm = useSetAtom(confirmDialogAtom, globalScope); const { enqueueSnackbar } = useSnackbar(); const { setValue } = useFormMethods; + const [tableSettingsDialog] = useAtom(tableSettingsDialogAtom, globalScope); const handleImport = () => { logEvent(analytics, "import_tableSettings"); @@ -73,7 +73,11 @@ export default function ImportSettings({ }); } - enqueueSnackbar("Imported settings"); + enqueueSnackbar( + `Imported settings. Click ${ + tableSettingsDialog.mode === "create" ? "Create" : "Update" + } to save to the table.` + ); handleClose(); }; diff --git a/src/components/TableSettingsDialog/DeleteMenu.tsx b/src/components/TableSettingsDialog/DeleteMenu.tsx index 1b509fef..b1d90c04 100644 --- a/src/components/TableSettingsDialog/DeleteMenu.tsx +++ b/src/components/TableSettingsDialog/DeleteMenu.tsx @@ -1,12 +1,17 @@ import { useState } from "react"; -import { useSetAtom } from "jotai"; +import { useAtom, useSetAtom } from "jotai"; import { useNavigate } from "react-router-dom"; import { useSnackbar } from "notistack"; import { IconButton, Menu, MenuItem, DialogContentText } from "@mui/material"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; -import { globalScope, confirmDialogAtom } from "@src/atoms/globalScope"; +import { + globalScope, + confirmDialogAtom, + updateTableAtom, + deleteTableAtom, +} from "@src/atoms/globalScope"; import { TableSettings } from "@src/types/table"; import { ROUTES } from "@src/constants/routes"; import { analytics, logEvent } from "@src/analytics"; @@ -25,40 +30,27 @@ export default function DeleteMenu({ clearDialog, data }: IDeleteMenuProps) { const confirm = useSetAtom(confirmDialogAtom, globalScope); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const [updateTable] = useAtom(updateTableAtom, globalScope); const handleResetStructure = async () => { const snack = enqueueSnackbar("Resetting columns…", { persist: true }); - - // TODO: - // const schemaDocRef = db.doc(`${TABLE_SCHEMAS}/${data!.id}`); - // await schemaDocRef.update({ columns: {} }); - + await updateTable!( + { id: data!.id, tableType: data!.tableType }, + { _schema: { columns: {} } } + ); clearDialog(); closeSnackbar(snack); + enqueueSnackbar("Columns reset"); }; + const [deleteTable] = useAtom(deleteTableAtom, globalScope); const handleDelete = async () => { const snack = enqueueSnackbar("Deleting table…", { persist: true }); - - // TODO: - // const tablesDocRef = db.doc(SETTINGS); - // const tableData = (await tablesDocRef.get()).data(); - // const updatedTables = tableData?.tables.filter( - // (table) => table.id !== data?.id || table.tableType !== data?.tableType - // ); - // tablesDocRef.update({ tables: updatedTables }); - // await db - // .collection( - // data?.tableType === "primaryCollection" - // ? TABLE_SCHEMAS - // : TABLE_GROUP_SCHEMAS - // ) - // .doc(data?.id) - // .delete(); - + await deleteTable!(data!.id); logEvent(analytics, "delete_table"); clearDialog(); closeSnackbar(snack); navigate(ROUTES.home); + enqueueSnackbar("Deleted table"); }; return ( @@ -106,6 +98,7 @@ export default function DeleteMenu({ clearDialog, data }: IDeleteMenuProps) { handleConfirm: handleResetStructure, }) } + disabled={!updateTable} > Reset columns… @@ -131,6 +124,7 @@ export default function DeleteMenu({ clearDialog, data }: IDeleteMenuProps) { handleConfirm: handleDelete, }) } + disabled={!deleteTable} > Delete table… diff --git a/src/components/TableSettingsDialog/TableSettingsDialog.tsx b/src/components/TableSettingsDialog/TableSettingsDialog.tsx index 70614dec..23996015 100644 --- a/src/components/TableSettingsDialog/TableSettingsDialog.tsx +++ b/src/components/TableSettingsDialog/TableSettingsDialog.tsx @@ -4,6 +4,7 @@ import { useSnackbar } from "notistack"; import { useLocation, useNavigate } from "react-router-dom"; import { find, sortBy, get, isEmpty } from "lodash-es"; import { FieldValues } from "react-hook-form"; +import { Controller } from "react-hook-form"; import { DialogContentText, Stack, Typography } from "@mui/material"; @@ -23,6 +24,9 @@ import { rolesAtom, rowyRunAtom, confirmDialogAtom, + createTableAtom, + updateTableAtom, + AdditionalTableSettings, } from "@src/atoms/globalScope"; import { TableSettings } from "@src/types/table"; import { analytics, logEvent } from "@src/analytics"; @@ -35,7 +39,6 @@ import { TABLE_GROUP_SCHEMAS, TABLE_SCHEMAS, } from "@src/config/dbPaths"; -import { Controller } from "react-hook-form"; import { ROUTES } from "@src/constants/routes"; const customComponents = { @@ -88,16 +91,21 @@ export default function TableSettingsDialog() { } ); + const [createTable] = useAtom(createTableAtom, globalScope); + const [updateTable] = useAtom(updateTableAtom, globalScope); + if (!open) return null; - // TODO: types - const handleSubmit = async (v: FieldValues) => { - const { _suggestedRules, ...values } = v; + const handleSubmit = async (v: TableSettings & AdditionalTableSettings) => { + const { + _schemaSource, + _initialColumns, + _schema, + _suggestedRules, + ...values + } = v; const data = { ...values }; - if (values.schemaSource) - data.schemaSource = find(tables, { id: values.schemaSource }); - const hasExtensions = !isEmpty(get(data, "_schema.extensionObjects")); const hasWebhooks = !isEmpty(get(data, "_schema.webhooks")); const deployExtensionsWebhooks = (onComplete?: () => void) => { @@ -178,12 +186,10 @@ export default function TableSettingsDialog() { ); } - // TODO: - // await settingsActions?.updateTable({ - // id: data.id, - // tableType: data.tableType, - // _schema, - // }); + await updateTable!( + { id: data.id, tableType: data.tableType }, + { _schema } + ); if (onComplete) onComplete(); }, }); @@ -193,8 +199,7 @@ export default function TableSettingsDialog() { }; if (mode === "update") { - // TODO: - // await settingsActions?.updateTable(data); + await updateTable!(data, { _schema }); deployExtensionsWebhooks(); clearDialog(); logEvent(analytics, "update_table", { type: values.tableType }); @@ -203,15 +208,16 @@ export default function TableSettingsDialog() { const creatingSnackbar = enqueueSnackbar("Creating table…", { persist: true, }); - // TODO: - // await settingsActions?.createTable(data); - await logEvent(analytics, "create_table", { type: values.tableType }); + + await createTable!(data, { + _schemaSource, + _initialColumns, + _schema, + _suggestedRules, + }); + logEvent(analytics, "create_table", { type: values.tableType }); deployExtensionsWebhooks(() => { - if (location.pathname === ROUTES.tables) { - navigate(`${ROUTES.table}/${values.id}`); - } else { - navigate(values.id); - } + navigate(`${ROUTES.table}/${values.id}`); clearDialog(); closeSnackbar(creatingSnackbar); }); @@ -237,10 +243,7 @@ export default function TableSettingsDialog() { return ( { const { errors } = formFieldsProps.useFormMethods.formState; @@ -444,11 +447,12 @@ export default function TableSettingsDialog() { }} customComponents={customComponents} values={{ ...data }} - onSubmit={handleSubmit} + onSubmit={handleSubmit as any} SubmitButtonProps={{ children: mode === "create" ? "Create" : "Update", - // TODO: - disabled: true, + disabled: + (mode === "create" && !createTable) || + (mode === "update" && !updateTable), }} /> ); diff --git a/src/components/TableSettingsDialog/form.tsx b/src/components/TableSettingsDialog/form.tsx index 989d9e0a..47e28c81 100644 --- a/src/components/TableSettingsDialog/form.tsx +++ b/src/components/TableSettingsDialog/form.tsx @@ -428,7 +428,7 @@ export const tableSettings = ( ? { step: "columns", type: FieldType.singleSelect, - name: "schemaSource", + name: "_schemaSource", label: "Copy columns from existing table (optional)", labelPlural: "tables", options: tables, diff --git a/src/components/fields/index.tsx b/src/components/fields/index.tsx new file mode 100644 index 00000000..ab31d466 --- /dev/null +++ b/src/components/fields/index.tsx @@ -0,0 +1,133 @@ +import { find, get } from "lodash-es"; + +import { FieldType } from "@src/constants/fields"; +import { IFieldConfig } from "./types"; + +// // Import field configs +// import ShortText from "./ShortText"; +// import LongText from "./LongText"; +// import RichText from "./RichText"; +// import Email from "./Email"; +// import Phone from "./Phone"; +// import Url from "./Url"; +// import Number_ from "./Number"; +// import Checkbox from "./Checkbox"; +// import Percentage from "./Percentage"; +// import Rating from "./Rating"; +// import Slider from "./Slider"; +// import Color from "./Color"; +// import Date_ from "./Date"; +// import DateTime from "./DateTime"; +// import Duration from "./Duration"; +// import Image_ from "./Image"; +// import File_ from "./File"; +// import SingleSelect from "./SingleSelect"; +// import MultiSelect from "./MultiSelect"; +// import SubTable from "./SubTable"; +// import ConnectTable from "./ConnectTable"; +// import ConnectService from "./ConnectService"; +// import Json from "./Json"; +// import Code from "./Code"; +// import Action from "./Action"; +// import Derivative from "./Derivative"; +// // import Aggregate from "./Aggregate"; +// import CreatedBy from "./CreatedBy"; +// import UpdatedBy from "./UpdatedBy"; +// import CreatedAt from "./CreatedAt"; +// import UpdatedAt from "./UpdatedAt"; +// import User from "./User"; +// import Id from "./Id"; +// import Status from "./Status"; +// import Connector from "./Connector"; +// import { TableColumn } from "../Table"; + +// Export field configs in order for FieldsDropdown +export const FIELDS: IFieldConfig[] = [ + // // TEXT + // ShortText, + // LongText, + // RichText, + // Email, + // Phone, + // Url, + // // SELECT + // SingleSelect, + // MultiSelect, + // // NUMERIC + // Number_, + // Checkbox, + // Percentage, + // Rating, + // Slider, + // Color, + // // DATE & TIME + // Date_, + // DateTime, + // Duration, + // // FILE + // Image_, + // File_, + // // CONNECTION + // Connector, + // SubTable, + // ConnectTable, + // ConnectService, + // // CODE + // Json, + // Code, + // // CLOUD FUNCTION + // Action, + // Derivative, + // // Aggregate, + // Status, + // // AUDITING + // CreatedBy, + // UpdatedBy, + // CreatedAt, + // UpdatedAt, + // // METADATA + // User, + // Id, +]; + +/** + * Returns specific property of field config + * @param fieldType + */ +export const getFieldProp = ( + prop: keyof IFieldConfig, + fieldType: FieldType +) => { + const field = find(FIELDS, { type: fieldType }); + return get(field, prop); +}; + +/** + * Returns `true` if it receives an existing fieldType + * @param fieldType + */ +export const isFieldType = (fieldType: any) => { + const fieldTypes = FIELDS.map((field) => field.type); + return fieldTypes.includes(fieldType); +}; + +/** + * Returns array of fieldTypes with dataType included dataTypes array + * @param dataTypes + */ +export const hasDataTypes = (dataTypes: string[]) => { + const fieldTypes = FIELDS.map((field) => field.type); + return fieldTypes.filter((fieldType) => + dataTypes.includes(getFieldProp("dataType", fieldType)) + ); +}; + +export const getColumnType = (column: { + type: FieldType; + config: { + renderFieldType: FieldType; + }; +}) => + column.type === FieldType.derivative + ? column.config.renderFieldType + : column.type; diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts new file mode 100644 index 00000000..41716eb0 --- /dev/null +++ b/src/components/fields/types.ts @@ -0,0 +1,86 @@ +import { FieldType } from "@src/constants/fields"; +import { FormatterProps, EditorProps } from "react-data-grid"; +import { Control, UseFormReturn } from "react-hook-form"; +import { PopoverProps } from "@mui/material"; +import { DocumentReference, WhereFilterOp } from "firebase/firestore"; +// import { SelectedCell } from "@src/atoms/ContextMenu"; +// import { IContextMenuActions } from "./_BasicCell/BasicCellContextMenuActions"; + +export { FieldType }; + +export interface IFieldConfig { + type: FieldType; + name: string; + group: string; + dataType: string; + initializable?: boolean; + requireConfiguration?: boolean; + initialValue: any; + icon?: React.ReactNode; + description?: string; + setupGuideLink?: string; + // contextMenuActions?: ( + // selectedCell: SelectedCell, + // reset: () => Promise + // ) => IContextMenuActions[]; + TableCell: React.ComponentType>; + TableEditor: React.ComponentType>; + SideDrawerField: React.ComponentType; + settings?: React.ComponentType; + settingsValidator?: (config: Record) => Record; + filter?: { + operators: IFilterOperator[]; + customInput?: React.ComponentType; + defaultValue?: any; + valueFormatter?: (value: any) => string; + }; + sortKey?: string; + csvExportFormatter?: (value: any, config?: any) => string; + csvImportParser?: (value: string, config?: any) => any; +} + +export interface IBasicCellProps { + value: any; + type: FieldType; + name: string; +} +export interface IHeavyCellProps extends IBasicCellProps, FormatterProps { + column: FormatterProps["column"] & { config?: Record }; + onSubmit: (value: any) => void; + docRef: DocumentReference; + disabled: boolean; +} + +export interface IPopoverInlineCellProps extends IHeavyCellProps { + showPopoverCell: React.Dispatch>; +} +export interface IPopoverCellProps extends IPopoverInlineCellProps { + parentRef: PopoverProps["anchorEl"]; +} + +export interface ISideDrawerFieldProps { + column: FormatterProps["column"] & { config?: Record }; + control: Control; + docRef: DocumentReference; + disabled: boolean; + useFormMethods: UseFormReturn; +} + +export interface ISettingsProps { + onChange: (key: string) => (value: any) => void; + config: Record; + fieldName: string; + onBlur: React.FocusEventHandler; + errors: Record; +} + +// TODO: WRITE TYPES +export interface IFiltersProps { + onChange: (key: string) => (value: any) => void; + [key: string]: any; +} + +export interface IFilterOperator { + value: WhereFilterOp; + label: string; +} diff --git a/src/sources/ProjectSourceFirebase.tsx b/src/sources/ProjectSourceFirebase.tsx deleted file mode 100644 index 18aea444..00000000 --- a/src/sources/ProjectSourceFirebase.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { memo, useEffect, useCallback } from "react"; -import { atom, useAtom, useSetAtom } from "jotai"; -import { useAtomCallback } from "jotai/utils"; - -import { FirebaseOptions, initializeApp } from "firebase/app"; -import { getAuth, connectAuthEmulator, getIdTokenResult } from "firebase/auth"; -import { - initializeFirestore, - connectFirestoreEmulator, - enableMultiTabIndexedDbPersistence, -} from "firebase/firestore"; - -import useFirestoreDocWithAtom from "@src/hooks/useFirestoreDocWithAtom"; -import { - globalScope, - projectIdAtom, - projectSettingsAtom, - updateProjectSettingsAtom, - publicSettingsAtom, - updatePublicSettingsAtom, - currentUserAtom, - userRolesAtom, - userSettingsAtom, - updateUserSettingsAtom, -} from "@src/atoms/globalScope"; -import { SETTINGS, PUBLIC_SETTINGS, USERS } from "@src/config/dbPaths"; - -export const envConfig = { - apiKey: process.env.REACT_APP_FIREBASE_PROJECT_WEB_API_KEY, - authDomain: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseapp.com`, - databaseURL: `https://${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseio.com`, - projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, - storageBucket: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.appspot.com`, -}; - -// Connect emulators based on env vars -const envConnectEmulators = - process.env.NODE_ENV === "test" || - process.env.REACT_APP_FIREBASE_EMULATORS === "true"; - -/** - * Store Firebase config here so it can be set programmatically. - * This lets us switch between Firebase projects. - * Root atom from which app, auth, db, storage are derived. - */ -export const firebaseConfigAtom = atom(envConfig); - -/** Store Firebase app instance */ -export const firebaseAppAtom = atom((get) => { - const firebaseConfig = get(firebaseConfigAtom); - return initializeApp(firebaseConfig, firebaseConfig.projectId); -}); - -/** - * Store Firebase Auth instance for current app. - * Connects to emulators based on env vars. - */ -export const firebaseAuthAtom = atom((get) => { - const auth = getAuth(get(firebaseAppAtom)); - if (envConnectEmulators && !(window as any).firebaseAuthEmulatorStarted) { - connectAuthEmulator(auth, "http://localhost:9099", { - disableWarnings: true, - }); - (window as any).firebaseAuthEmulatorStarted = true; - } - return auth; -}); - -/** - * Store Firestore instance for current app. - * Connects to emulators based on env vars, or enables multi-tab indexed db persistence. - */ -export const firebaseDbAtom = atom((get) => { - const db = initializeFirestore(get(firebaseAppAtom), { - ignoreUndefinedProperties: true, - }); - if (!(window as any).firebaseDbStarted) { - if (envConnectEmulators) connectFirestoreEmulator(db, "localhost", 9299); - else enableMultiTabIndexedDbPersistence(db); - (window as any).firebaseDbStarted = true; - } - return db; -}); - -/** - * When rendered, connects to a Firebase project. - * - * Sets project ID, project settings, public settings, current user, user roles, and user settings. - */ -export const ProjectSourceFirebase = memo(function ProjectSourceFirebase() { - // Set projectId from Firebase project - const [firebaseConfig] = useAtom(firebaseConfigAtom, globalScope); - const setProjectId = useSetAtom(projectIdAtom, globalScope); - useEffect(() => { - setProjectId(firebaseConfig.projectId || ""); - }, [firebaseConfig.projectId, setProjectId]); - - // Get current user and store in atoms - const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope); - const [currentUser, setCurrentUser] = useAtom(currentUserAtom, globalScope); - const setUserRoles = useSetAtom(userRolesAtom, globalScope); - // Must use `useAtomCallback`, otherwise `useAtom(updateUserSettingsAtom)` - // will cause infinite re-render - const updateUserSettings = useAtomCallback( - useCallback((get) => get(updateUserSettingsAtom), []), - globalScope - ); - - useEffect(() => { - // Suspend when currentUser has not been read yet - (setCurrentUser as any)(new Promise(() => {})); - - const unsubscribe = firebaseAuth.onAuthStateChanged(async (user) => { - setCurrentUser(user); - - if (user) { - // Get user roles - const tokenResult = await getIdTokenResult(user); - const roles = (tokenResult.claims.roles as string[]) ?? []; - setUserRoles(roles); - - // Update user settings doc with roles for User Management page - const _updateUserSettings = await updateUserSettings(); - if (_updateUserSettings) _updateUserSettings({ roles }); - } else { - setUserRoles([]); - } - }); - - return () => { - unsubscribe(); - }; - }, [firebaseAuth, setCurrentUser, setUserRoles, updateUserSettings]); - - // Store public settings in atom - useFirestoreDocWithAtom(publicSettingsAtom, globalScope, PUBLIC_SETTINGS, { - updateDataAtom: updatePublicSettingsAtom, - }); - - // Store project settings in atom when a user is signed in. - // If they have no access, display AccessDenied screen via ErrorBoundary. - useFirestoreDocWithAtom( - projectSettingsAtom, - globalScope, - currentUser ? SETTINGS : undefined, - { updateDataAtom: updateProjectSettingsAtom } - ); - - // Store user settings in atom when a user is signed in - useFirestoreDocWithAtom(userSettingsAtom, globalScope, USERS, { - pathSegments: [currentUser?.uid], - createIfNonExistent: currentUser - ? { - user: { - email: currentUser.email || "", - displayName: currentUser.displayName || undefined, - photoURL: currentUser.photoURL || undefined, - phoneNumber: currentUser.phoneNumber || undefined, - }, - } - : undefined, - updateDataAtom: updateUserSettingsAtom, - }); - - return null; -}); - -export default ProjectSourceFirebase; diff --git a/src/sources/ProjectSourceFirebase/ProjectSourceFirebase.tsx b/src/sources/ProjectSourceFirebase/ProjectSourceFirebase.tsx new file mode 100644 index 00000000..c3051e7b --- /dev/null +++ b/src/sources/ProjectSourceFirebase/ProjectSourceFirebase.tsx @@ -0,0 +1,35 @@ +import { memo, useEffect } from "react"; +import { useAtom, useSetAtom } from "jotai"; + +import { globalScope, projectIdAtom } from "@src/atoms/globalScope"; +import { firebaseConfigAtom } from "./init"; + +import { useAuthUser } from "./useAuthUser"; +import { useSettingsDocs } from "./useSettingsDocs"; +import { useTableFunctions } from "./useTableFunctions"; + +/** + * When rendered, connects to a Firebase project and populates + * all atoms in src/atoms/globalScope/project. + */ +export const ProjectSourceFirebase = memo(function ProjectSourceFirebase() { + // Set projectId from Firebase project + const [firebaseConfig] = useAtom(firebaseConfigAtom, globalScope); + const setProjectId = useSetAtom(projectIdAtom, globalScope); + useEffect(() => { + setProjectId(firebaseConfig.projectId || ""); + }, [firebaseConfig.projectId, setProjectId]); + + // Sets currentUser and userRoles based on Firebase Auth user. + useAuthUser(); + + // Sets listeners to public settings, project settings, and user settings. + // Also sets functions to update those documents. + useSettingsDocs(); + useTableFunctions(); + console.log("rerender"); + + return null; +}); + +export default ProjectSourceFirebase; diff --git a/src/sources/ProjectSourceFirebase/index.ts b/src/sources/ProjectSourceFirebase/index.ts new file mode 100644 index 00000000..2594617b --- /dev/null +++ b/src/sources/ProjectSourceFirebase/index.ts @@ -0,0 +1,4 @@ +export * from "./ProjectSourceFirebase"; +export { default } from "./ProjectSourceFirebase"; + +export * from "./init"; diff --git a/src/sources/ProjectSourceFirebase/init.ts b/src/sources/ProjectSourceFirebase/init.ts new file mode 100644 index 00000000..5b46692e --- /dev/null +++ b/src/sources/ProjectSourceFirebase/init.ts @@ -0,0 +1,65 @@ +import { atom } from "jotai"; +import { FirebaseOptions, initializeApp } from "firebase/app"; +import { getAuth, connectAuthEmulator } from "firebase/auth"; +import { + initializeFirestore, + connectFirestoreEmulator, + enableMultiTabIndexedDbPersistence, +} from "firebase/firestore"; + +export const envConfig = { + apiKey: process.env.REACT_APP_FIREBASE_PROJECT_WEB_API_KEY, + authDomain: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseapp.com`, + databaseURL: `https://${process.env.REACT_APP_FIREBASE_PROJECT_ID}.firebaseio.com`, + projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, + storageBucket: `${process.env.REACT_APP_FIREBASE_PROJECT_ID}.appspot.com`, +}; + +// Connect emulators based on env vars +const envConnectEmulators = + process.env.NODE_ENV === "test" || + process.env.REACT_APP_FIREBASE_EMULATORS === "true"; + +/** + * Store Firebase config here so it can be set programmatically. + * This lets us switch between Firebase projects. + * Root atom from which app, auth, db, storage are derived. + */ +export const firebaseConfigAtom = atom(envConfig); + +/** Store Firebase app instance */ +export const firebaseAppAtom = atom((get) => { + const firebaseConfig = get(firebaseConfigAtom); + return initializeApp(firebaseConfig, firebaseConfig.projectId); +}); + +/** + * Store Firebase Auth instance for current app. + * Connects to emulators based on env vars. + */ +export const firebaseAuthAtom = atom((get) => { + const auth = getAuth(get(firebaseAppAtom)); + if (envConnectEmulators && !(window as any).firebaseAuthEmulatorStarted) { + connectAuthEmulator(auth, "http://localhost:9099", { + disableWarnings: true, + }); + (window as any).firebaseAuthEmulatorStarted = true; + } + return auth; +}); + +/** + * Store Firestore instance for current app. + * Connects to emulators based on env vars, or enables multi-tab indexed db persistence. + */ +export const firebaseDbAtom = atom((get) => { + const db = initializeFirestore(get(firebaseAppAtom), { + ignoreUndefinedProperties: true, + }); + if (!(window as any).firebaseDbStarted) { + if (envConnectEmulators) connectFirestoreEmulator(db, "localhost", 9299); + else enableMultiTabIndexedDbPersistence(db); + (window as any).firebaseDbStarted = true; + } + return db; +}); diff --git a/src/sources/ProjectSourceFirebase/useAuthUser.ts b/src/sources/ProjectSourceFirebase/useAuthUser.ts new file mode 100644 index 00000000..cce0edb4 --- /dev/null +++ b/src/sources/ProjectSourceFirebase/useAuthUser.ts @@ -0,0 +1,54 @@ +import { useEffect, useCallback } from "react"; +import { useAtom, useSetAtom } from "jotai"; +import { useAtomCallback } from "jotai/utils"; +import { getIdTokenResult } from "firebase/auth"; + +import { + globalScope, + currentUserAtom, + userRolesAtom, + updateUserSettingsAtom, +} from "@src/atoms/globalScope"; +import { firebaseAuthAtom } from "./init"; + +/** + * Sets currentUser and userRoles based on Firebase Auth user + */ +export function useAuthUser() { + // Get current user and store in atoms + const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope); + const setCurrentUser = useSetAtom(currentUserAtom, globalScope); + const setUserRoles = useSetAtom(userRolesAtom, globalScope); + // Must use `useAtomCallback`, otherwise `useAtom(updateUserSettingsAtom)` + // will cause infinite re-render + const updateUserSettings = useAtomCallback( + useCallback((get) => get(updateUserSettingsAtom), []), + globalScope + ); + + useEffect(() => { + // Suspend when currentUser has not been read yet + (setCurrentUser as any)(new Promise(() => {})); + + const unsubscribe = firebaseAuth.onAuthStateChanged(async (user) => { + setCurrentUser(user); + + if (user) { + // Get user roles + const tokenResult = await getIdTokenResult(user); + const roles = (tokenResult.claims.roles as string[]) ?? []; + setUserRoles(roles); + + // Update user settings doc with roles for User Management page + const _updateUserSettings = await updateUserSettings(); + if (_updateUserSettings) _updateUserSettings({ roles }); + } else { + setUserRoles([]); + } + }); + + return () => { + unsubscribe(); + }; + }, [firebaseAuth, setCurrentUser, setUserRoles, updateUserSettings]); +} diff --git a/src/sources/ProjectSourceFirebase/useSettingsDocs.ts b/src/sources/ProjectSourceFirebase/useSettingsDocs.ts new file mode 100644 index 00000000..a200557b --- /dev/null +++ b/src/sources/ProjectSourceFirebase/useSettingsDocs.ts @@ -0,0 +1,53 @@ +import { useAtom } from "jotai"; + +import { + globalScope, + projectSettingsAtom, + updateProjectSettingsAtom, + publicSettingsAtom, + updatePublicSettingsAtom, + currentUserAtom, + userSettingsAtom, + updateUserSettingsAtom, +} from "@src/atoms/globalScope"; + +import useFirestoreDocWithAtom from "@src/hooks/useFirestoreDocWithAtom"; +import { SETTINGS, PUBLIC_SETTINGS, USERS } from "@src/config/dbPaths"; + +/** + * Sets listeners to public settings, project settings, and user settings. + * Also sets functions to update those documents. + */ +export function useSettingsDocs() { + const [currentUser] = useAtom(currentUserAtom, globalScope); + + // Store public settings in atom + useFirestoreDocWithAtom(publicSettingsAtom, globalScope, PUBLIC_SETTINGS, { + updateDataAtom: updatePublicSettingsAtom, + }); + + // Store project settings in atom when a user is signed in. + // If they have no access, display AccessDenied screen via ErrorBoundary. + useFirestoreDocWithAtom( + projectSettingsAtom, + globalScope, + currentUser ? SETTINGS : undefined, + { updateDataAtom: updateProjectSettingsAtom } + ); + + // Store user settings in atom when a user is signed in + useFirestoreDocWithAtom(userSettingsAtom, globalScope, USERS, { + pathSegments: [currentUser?.uid], + createIfNonExistent: currentUser + ? { + user: { + email: currentUser.email || "", + displayName: currentUser.displayName || undefined, + photoURL: currentUser.photoURL || undefined, + phoneNumber: currentUser.phoneNumber || undefined, + }, + } + : undefined, + updateDataAtom: updateUserSettingsAtom, + }); +} diff --git a/src/sources/ProjectSourceFirebase/useTableFunctions.ts b/src/sources/ProjectSourceFirebase/useTableFunctions.ts new file mode 100644 index 00000000..7d950c0b --- /dev/null +++ b/src/sources/ProjectSourceFirebase/useTableFunctions.ts @@ -0,0 +1,223 @@ +import { useEffect, useCallback } from "react"; +import { useAtom, useSetAtom } from "jotai"; +import { useAtomCallback } from "jotai/utils"; +import { doc, getDoc, setDoc, deleteDoc } from "firebase/firestore"; +import { camelCase, find, findIndex, isEmpty } from "lodash-es"; + +import { + globalScope, + projectSettingsAtom, + createTableAtom, + updateTableAtom, + deleteTableAtom, + getTableSchemaAtom, + AdditionalTableSettings, + MinimumTableSettings, +} from "@src/atoms/globalScope"; +import { firebaseDbAtom } from "./init"; + +import { + SETTINGS, + TABLE_SCHEMAS, + TABLE_GROUP_SCHEMAS, +} from "@src/config/dbPaths"; +import { TableSettings, TableSchema } from "@src/types/table"; +import { FieldType } from "@src/constants/fields"; +import { getFieldProp } from "@src/components/fields"; + +export function useTableFunctions() { + const [firebaseDb] = useAtom(firebaseDbAtom, globalScope); + + // Create a function to get the latest tables from project settings, + // so we don’t create new functions when tables change + const getTables = useAtomCallback( + useCallback((get) => get(projectSettingsAtom).tables, []), + globalScope + ); + + // Set the createTable function + const setCreateTable = useSetAtom(createTableAtom, globalScope); + useEffect(() => { + console.log("effect firebaseDb"); + setCreateTable( + () => + async ( + settings: TableSettings, + additionalSettings?: AdditionalTableSettings + ) => { + const { + _schemaSource, + _initialColumns = {}, + _schema, + } = additionalSettings || {}; + + // Get latest tables + const tables = (await getTables()) || []; + console.log("createTable", tables); + + // Get columns from imported table settings or _schemaSource if provided + let columns: NonNullable = + Array.isArray(_schema?.columns) || !_schema?.columns + ? {} + : _schema?.columns; + + // If _schemaSource is provided, get the schema doc for that table + if (_schemaSource) { + const sourceTable = find(tables, ["id", _schemaSource]); + + if (sourceTable) { + const sourceDocRef = doc( + firebaseDb, + sourceTable.tableType !== "collectionGroup" + ? TABLE_SCHEMAS + : TABLE_GROUP_SCHEMAS, + _schemaSource + ); + const sourceDoc = await getDoc(sourceDocRef); + columns = sourceDoc.get("columns") || {}; + } + } + + // Add columns from `_initialColumns` + for (const [type, checked] of Object.entries(_initialColumns)) { + if ( + checked && + // Make sure we don’t have + !Object.values(columns).some((column) => column.type === type) + ) + columns["_" + camelCase(type)] = { + type, + name: getFieldProp("name", type as FieldType), + key: "_" + camelCase(type), + fieldName: "_" + camelCase(type), + config: {}, + index: Object.values(columns).length, + }; + } + + // Appends table to settings doc + const promiseUpdateSettings = setDoc( + doc(firebaseDb, SETTINGS), + { tables: [...tables, settings] }, + { merge: true } + ); + + // Creates schema doc with columns + const { functionConfigPath, functionBuilderRef, ...schemaToWrite } = + _schema ?? {}; + const tableSchemaDocRef = doc( + firebaseDb, + settings.tableType !== "collectionGroup" + ? TABLE_SCHEMAS + : TABLE_GROUP_SCHEMAS, + settings.id + ); + const promiseAddSchema = await setDoc( + tableSchemaDocRef, + { ...schemaToWrite, columns }, + { merge: true } + ); + + // Wait for both to complete + await Promise.all([promiseUpdateSettings, promiseAddSchema]); + } + ); + }, [firebaseDb, getTables, setCreateTable]); + + // Set the createTable function + const setUpdateTable = useSetAtom(updateTableAtom, globalScope); + useEffect(() => { + setUpdateTable( + () => + async ( + settings: MinimumTableSettings, + additionalSettings?: AdditionalTableSettings + ) => { + const { _schema } = additionalSettings || {}; + + // Get latest tables + const tables = [...((await getTables()) || [])]; + const foundIndex = findIndex(tables, ["id", settings.id]); + const tableIndex = foundIndex > -1 ? foundIndex : tables.length; + + // Shallow merge new settings with old + tables[tableIndex] = { ...tables[tableIndex], ...settings }; + + // Updates settings doc with new tables array + const promiseUpdateSettings = setDoc( + doc(firebaseDb, SETTINGS), + { tables }, + { merge: true } + ); + + // Updates schema doc if param is provided + const { functionConfigPath, functionBuilderRef, ...schemaToWrite } = + _schema ?? {}; + const tableSchemaDocRef = doc( + firebaseDb, + settings.tableType !== "collectionGroup" + ? TABLE_SCHEMAS + : TABLE_GROUP_SCHEMAS, + settings.id + ); + const promiseUpdateSchema = isEmpty(schemaToWrite) + ? Promise.resolve() + : await setDoc(tableSchemaDocRef, schemaToWrite, { merge: true }); + + // Wait for both to complete + await Promise.all([promiseUpdateSettings, promiseUpdateSchema]); + } + ); + }, [firebaseDb, getTables, setUpdateTable]); + + // Set the deleteTable function + const setDeleteTable = useSetAtom(deleteTableAtom, globalScope); + useEffect(() => { + setDeleteTable(() => async (id: string) => { + // Get latest tables + const tables = (await getTables()) || []; + const table = find(tables, ["id", id]); + + // Removes table from settings doc array + const promiseUpdateSettings = setDoc( + doc(firebaseDb, SETTINGS), + { tables: tables.filter((table) => table.id !== id) }, + { merge: true } + ); + + // Deletes table schema doc + const tableSchemaDocRef = doc( + firebaseDb, + table?.tableType === "collectionGroup" + ? TABLE_GROUP_SCHEMAS + : TABLE_SCHEMAS, + id + ); + const promiseDeleteSchema = deleteDoc(tableSchemaDocRef); + + // Wait for both to complete + await Promise.all([promiseUpdateSettings, promiseDeleteSchema]); + }); + }, [firebaseDb, getTables, setDeleteTable]); + + // Set the getTableSchema function + const setGetTableSchema = useSetAtom(getTableSchemaAtom, globalScope); + useEffect(() => { + setGetTableSchema(() => async (id: string) => { + // Get latest tables + const tables = (await getTables()) || []; + const table = find(tables, ["id", id]); + + const tableSchemaDocRef = doc( + firebaseDb, + table?.tableType === "collectionGroup" + ? TABLE_GROUP_SCHEMAS + : TABLE_SCHEMAS, + id + ); + return getDoc(tableSchemaDocRef).then( + (doc) => (doc.data() || {}) as TableSchema + ); + }); + }, [firebaseDb, getTables, setGetTableSchema]); +} diff --git a/src/types/table.d.ts b/src/types/table.d.ts index 32d90feb..34a056cb 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -21,8 +21,8 @@ export type TableSettings = { name: string; roles: string[]; - description: string; section: string; + description?: string; tableType: "primaryCollection" | "collectionGroup"; @@ -39,6 +39,7 @@ export type TableSchema = { filters?: TableFilter[]; functionConfigPath?: string; + functionBuilderRef?: any; extensionObjects?: any[]; webhooks?: any[];