mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
enable TableSettingsDialog
This commit is contained in:
@@ -8,7 +8,9 @@ import {
|
||||
UpdateDocFunction,
|
||||
UpdateCollectionFunction,
|
||||
TableSettings,
|
||||
TableSchema,
|
||||
} from "@src/types/table";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
|
||||
export const projectIdAtom = atom<string>("");
|
||||
|
||||
@@ -75,6 +77,54 @@ export const tablesAtom = atom<TableSettings[]>((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<FieldType, boolean>;
|
||||
_schema: TableSchema;
|
||||
_suggestedRules: string;
|
||||
}>;
|
||||
|
||||
/** Stores a function to create a table with schema doc */
|
||||
export const createTableAtom = atom<
|
||||
| ((
|
||||
settings: TableSettings,
|
||||
additionalSettings?: AdditionalTableSettings
|
||||
) => Promise<void>)
|
||||
| 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<TableSettings>;
|
||||
|
||||
/** Stores a function to update a table and its schema doc */
|
||||
export const updateTableAtom = atom<
|
||||
| ((
|
||||
settings: MinimumTableSettings,
|
||||
additionalSettings?: AdditionalTableSettings
|
||||
) => Promise<void>)
|
||||
| null
|
||||
>(null);
|
||||
|
||||
/** Stores a function to delete a table and its schema doc */
|
||||
export const deleteTableAtom = atom<((id: string) => Promise<void>) | null>(
|
||||
null
|
||||
);
|
||||
|
||||
/** Stores a function to get a table’s schema doc (without listener) */
|
||||
export const getTableSchemaAtom = atom<
|
||||
((id: string) => Promise<TableSchema>) | null
|
||||
>(null);
|
||||
|
||||
/** Roles used in the project based on table settings */
|
||||
export const rolesAtom = atom((get) =>
|
||||
Array.from(
|
||||
|
||||
@@ -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<string, any>;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function SteppedAccordion({
|
||||
>
|
||||
<StepLabel error={error} {...labelProps}>
|
||||
{title}
|
||||
<ExpandIcon />
|
||||
<ExpandIcon sx={{ mr: -0.5 }} />
|
||||
</StepLabel>
|
||||
</StepButton>
|
||||
|
||||
|
||||
@@ -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 | HTMLElement>(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<HTMLButtonElement> = (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" ? <ImportIcon /> : <ExportIcon />}
|
||||
</IconButton>
|
||||
@@ -49,12 +72,21 @@ export default function ActionsMenu({
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
>
|
||||
<ImportSettings
|
||||
closeMenu={handleClose}
|
||||
control={control}
|
||||
useFormMethods={useFormMethods}
|
||||
/>
|
||||
<ExportSettings closeMenu={handleClose} control={control} />
|
||||
<Suspense
|
||||
fallback={
|
||||
<>
|
||||
<MenuItem disabled>Loading table settings…</MenuItem>
|
||||
<MenuItem disabled />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ImportSettings
|
||||
closeMenu={handleClose}
|
||||
control={control}
|
||||
useFormMethods={useFormMethods}
|
||||
/>
|
||||
<ExportSettings closeMenu={handleClose} control={control} />
|
||||
</Suspense>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 && (
|
||||
<LinearProgress
|
||||
style={{ position: "absolute", top: 0, left: 0, right: 0 }}
|
||||
/>
|
||||
)}
|
||||
<DialogContentText style={{ margin: "0 var(--dialog-spacing)" }}>
|
||||
Export table settings and columns in JSON format
|
||||
</DialogContentText>
|
||||
</>
|
||||
<DialogContentText style={{ margin: "0 var(--dialog-spacing)" }}>
|
||||
Export table settings and columns in JSON format
|
||||
</DialogContentText>
|
||||
}
|
||||
body={
|
||||
<div style={{ marginTop: "var(--dialog-contents-spacing)" }}>
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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…
|
||||
</MenuItem>
|
||||
@@ -131,6 +124,7 @@ export default function DeleteMenu({ clearDialog, data }: IDeleteMenuProps) {
|
||||
handleConfirm: handleDelete,
|
||||
})
|
||||
}
|
||||
disabled={!deleteTable}
|
||||
>
|
||||
Delete table…
|
||||
</MenuItem>
|
||||
|
||||
@@ -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 (
|
||||
<FormDialog
|
||||
onClose={clearDialog}
|
||||
title={
|
||||
(mode === "create" ? "Create table" : "Table settings") +
|
||||
" (INCOMPLETE)"
|
||||
}
|
||||
title={mode === "create" ? "Create table" : "Table settings"}
|
||||
fields={fields}
|
||||
customBody={(formFieldsProps) => {
|
||||
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),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
133
src/components/fields/index.tsx
Normal file
133
src/components/fields/index.tsx
Normal file
@@ -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;
|
||||
86
src/components/fields/types.ts
Normal file
86
src/components/fields/types.ts
Normal file
@@ -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<void>
|
||||
// ) => IContextMenuActions[];
|
||||
TableCell: React.ComponentType<FormatterProps<any>>;
|
||||
TableEditor: React.ComponentType<EditorProps<any, any>>;
|
||||
SideDrawerField: React.ComponentType<ISideDrawerFieldProps>;
|
||||
settings?: React.ComponentType<ISettingsProps>;
|
||||
settingsValidator?: (config: Record<string, any>) => Record<string, string>;
|
||||
filter?: {
|
||||
operators: IFilterOperator[];
|
||||
customInput?: React.ComponentType<IFiltersProps>;
|
||||
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<any> {
|
||||
column: FormatterProps<any>["column"] & { config?: Record<string, any> };
|
||||
onSubmit: (value: any) => void;
|
||||
docRef: DocumentReference;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export interface IPopoverInlineCellProps extends IHeavyCellProps {
|
||||
showPopoverCell: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
export interface IPopoverCellProps extends IPopoverInlineCellProps {
|
||||
parentRef: PopoverProps["anchorEl"];
|
||||
}
|
||||
|
||||
export interface ISideDrawerFieldProps {
|
||||
column: FormatterProps<any>["column"] & { config?: Record<string, any> };
|
||||
control: Control;
|
||||
docRef: DocumentReference;
|
||||
disabled: boolean;
|
||||
useFormMethods: UseFormReturn;
|
||||
}
|
||||
|
||||
export interface ISettingsProps {
|
||||
onChange: (key: string) => (value: any) => void;
|
||||
config: Record<string, any>;
|
||||
fieldName: string;
|
||||
onBlur: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||
errors: Record<string, any>;
|
||||
}
|
||||
|
||||
// TODO: WRITE TYPES
|
||||
export interface IFiltersProps {
|
||||
onChange: (key: string) => (value: any) => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface IFilterOperator {
|
||||
value: WhereFilterOp;
|
||||
label: string;
|
||||
}
|
||||
@@ -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<FirebaseOptions>(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;
|
||||
35
src/sources/ProjectSourceFirebase/ProjectSourceFirebase.tsx
Normal file
35
src/sources/ProjectSourceFirebase/ProjectSourceFirebase.tsx
Normal file
@@ -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;
|
||||
4
src/sources/ProjectSourceFirebase/index.ts
Normal file
4
src/sources/ProjectSourceFirebase/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./ProjectSourceFirebase";
|
||||
export { default } from "./ProjectSourceFirebase";
|
||||
|
||||
export * from "./init";
|
||||
65
src/sources/ProjectSourceFirebase/init.ts
Normal file
65
src/sources/ProjectSourceFirebase/init.ts
Normal file
@@ -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<FirebaseOptions>(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;
|
||||
});
|
||||
54
src/sources/ProjectSourceFirebase/useAuthUser.ts
Normal file
54
src/sources/ProjectSourceFirebase/useAuthUser.ts
Normal file
@@ -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]);
|
||||
}
|
||||
53
src/sources/ProjectSourceFirebase/useSettingsDocs.ts
Normal file
53
src/sources/ProjectSourceFirebase/useSettingsDocs.ts
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
223
src/sources/ProjectSourceFirebase/useTableFunctions.ts
Normal file
223
src/sources/ProjectSourceFirebase/useTableFunctions.ts
Normal file
@@ -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<TableSchema["columns"]> =
|
||||
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]);
|
||||
}
|
||||
3
src/types/table.d.ts
vendored
3
src/types/table.d.ts
vendored
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user