data layer core & types updates

This commit is contained in:
Sidney Alcantara
2022-05-12 20:28:15 +10:00
parent 5211d1d013
commit 0d3672b538
22 changed files with 400 additions and 207 deletions

View File

@@ -6,7 +6,7 @@ import { userRolesAtom } from "./auth";
import { UserSettings } from "./user";
import {
UpdateDocFunction,
UpdateCollectionFunction,
UpdateCollectionDocFunction,
TableSettings,
TableSchema,
} from "@src/types/table";
@@ -54,7 +54,15 @@ export type ProjectSettings = Partial<{
}>;
/** Project settings are visible to authenticated users */
export const projectSettingsAtom = atom<ProjectSettings>({});
/** Stores a function that updates project settings */
/**
* Stores a function that updates project settings
*
* @example Basic usage:
* ```
* const [updateProjectSettings] = useAtom(updateProjectSettingsAtom, globalScope);
* if (updateProjectSettings) updateProjectSettings({ ... });
* ```
*/
export const updateProjectSettingsAtom = atom<
UpdateDocFunction<ProjectSettings> | undefined
>(undefined);
@@ -143,5 +151,5 @@ export const rolesAtom = atom((get) =>
export const allUsersAtom = atom<UserSettings[]>([]);
/** Stores a function that updates a user document */
export const updateUserAtom = atom<
UpdateCollectionFunction<UserSettings> | undefined
UpdateCollectionDocFunction<UserSettings> | undefined
>(undefined);

View File

@@ -5,11 +5,11 @@ import { ThemeOptions } from "@mui/material";
import themes from "@src/theme";
import { publicSettingsAtom } from "./project";
import { UpdateDocFunction, TableFilter } from "@src/types/table";
import { UpdateDocFunction, TableFilter, TableRowRef } from "@src/types/table";
/** User info and settings */
export type UserSettings = Partial<{
_rowy_id: string;
_rowy_ref: TableRowRef;
/** Synced from user auth info */
user: {
email: string;

View File

@@ -1,5 +1,6 @@
import { renderHook, act } from "@testing-library/react";
import { useAtomValue, useSetAtom } from "jotai";
import {
tableScope,
tableSchemaAtom,
@@ -9,7 +10,6 @@ import {
deleteColumnAtom,
} from "@src/atoms/tableScope";
import { TableSchema } from "@src/types/table";
import { FieldType } from "@src/constants/fields";
const initUpdateTableSchemaAtom = (initialTableSchema?: TableSchema) =>
@@ -49,10 +49,10 @@ describe("addColumn", () => {
initUpdateTableSchemaAtom();
const {
result: { current: addColumn },
} = renderHook(() => useAtomValue(addColumnAtom, tableScope));
} = renderHook(() => useSetAtom(addColumnAtom, tableScope));
expect(addColumn).toBeDefined();
await act(() => addColumn(columnToAdd));
await act(() => addColumn({ config: columnToAdd }));
const {
result: { current: tableSchema },
@@ -65,10 +65,10 @@ describe("addColumn", () => {
initUpdateTableSchemaAtom({ columns: generatedColumns });
const {
result: { current: addColumn },
} = renderHook(() => useAtomValue(addColumnAtom, tableScope));
} = renderHook(() => useSetAtom(addColumnAtom, tableScope));
expect(addColumn).toBeDefined();
await act(() => addColumn(columnToAdd));
await act(() => addColumn({ config: columnToAdd }));
const {
result: { current: tableSchema },
@@ -83,10 +83,10 @@ describe("addColumn", () => {
initUpdateTableSchemaAtom({ columns: generatedColumns });
const {
result: { current: addColumn },
} = renderHook(() => useAtomValue(addColumnAtom, tableScope));
} = renderHook(() => useSetAtom(addColumnAtom, tableScope));
expect(addColumn).toBeDefined();
await act(() => addColumn(columnToAdd, 7));
await act(() => addColumn({ config: columnToAdd, index: 7 }));
const {
result: { current: tableSchema },
@@ -104,10 +104,12 @@ describe("updateColumn", () => {
initUpdateTableSchemaAtom({ columns: generatedColumns });
const {
result: { current: updateColumn },
} = renderHook(() => useAtomValue(updateColumnAtom, tableScope));
} = renderHook(() => useSetAtom(updateColumnAtom, tableScope));
expect(updateColumn).toBeDefined();
await act(() => updateColumn("column7", { name: "Updated column" }));
await act(() =>
updateColumn({ key: "column7", config: { name: "Updated column" } })
);
const {
result: { current: tableSchema },
@@ -126,17 +128,17 @@ describe("updateColumn", () => {
initUpdateTableSchemaAtom({ columns: generatedColumns });
const {
result: { current: updateColumn },
} = renderHook(() => useAtomValue(updateColumnAtom, tableScope));
} = renderHook(() => useSetAtom(updateColumnAtom, tableScope));
expect(updateColumn).toBeDefined();
const SOURCE_INDEX = 2;
const TARGET_INDEX = 4;
await act(() =>
updateColumn(
`column${SOURCE_INDEX}`,
{ name: "Updated column" },
TARGET_INDEX
)
updateColumn({
key: `column${SOURCE_INDEX}`,
config: { name: "Updated column" },
index: TARGET_INDEX,
})
);
const {
@@ -162,17 +164,17 @@ describe("updateColumn", () => {
initUpdateTableSchemaAtom({ columns: generatedColumns });
const {
result: { current: updateColumn },
} = renderHook(() => useAtomValue(updateColumnAtom, tableScope));
} = renderHook(() => useSetAtom(updateColumnAtom, tableScope));
expect(updateColumn).toBeDefined();
const SOURCE_INDEX = 9;
const TARGET_INDEX = 3;
await act(() =>
updateColumn(
`column${SOURCE_INDEX}`,
{ name: "Updated column" },
TARGET_INDEX
)
updateColumn({
key: `column${SOURCE_INDEX}`,
config: { name: "Updated column" },
index: TARGET_INDEX,
})
);
const {
@@ -198,12 +200,16 @@ describe("updateColumn", () => {
initUpdateTableSchemaAtom({ columns: generatedColumns });
const {
result: { current: updateColumn },
} = renderHook(() => useAtomValue(updateColumnAtom, tableScope));
} = renderHook(() => useSetAtom(updateColumnAtom, tableScope));
expect(updateColumn).toBeDefined();
expect(() => {
act(() => updateColumn("nonExistentColumn", {}));
}).toThrow(/Column with key .* not found/);
let error = new Error();
try {
await updateColumn({ key: "nonExistentColumn", config: {} });
} catch (e: any) {
error = e;
}
expect(error?.message).toMatch(/Column with key .* not found/);
const {
result: { current: tableSchema },
@@ -215,12 +221,16 @@ describe("updateColumn", () => {
initUpdateTableSchemaAtom();
const {
result: { current: updateColumn },
} = renderHook(() => useAtomValue(updateColumnAtom, tableScope));
} = renderHook(() => useSetAtom(updateColumnAtom, tableScope));
expect(updateColumn).toBeDefined();
expect(() => {
act(() => updateColumn("nonExistentColumn", {}));
}).toThrow(/Column with key .* not found/);
let error = new Error();
try {
await updateColumn({ key: "nonExistentColumn", config: {} });
} catch (e: any) {
error = e;
}
expect(error?.message).toMatch(/Column with key .* not found/);
const {
result: { current: tableSchema },
@@ -234,7 +244,7 @@ describe("deleteColumn", () => {
initUpdateTableSchemaAtom({ columns: generatedColumns });
const {
result: { current: deleteColumn },
} = renderHook(() => useAtomValue(deleteColumnAtom, tableScope));
} = renderHook(() => useSetAtom(deleteColumnAtom, tableScope));
expect(deleteColumn).toBeDefined();
await act(() => deleteColumn("column7"));
@@ -251,7 +261,7 @@ describe("deleteColumn", () => {
initUpdateTableSchemaAtom({ columns: generatedColumns });
const {
result: { current: deleteColumn },
} = renderHook(() => useAtomValue(deleteColumnAtom, tableScope));
} = renderHook(() => useSetAtom(deleteColumnAtom, tableScope));
expect(deleteColumn).toBeDefined();
await act(() => deleteColumn("column72"));
@@ -269,7 +279,7 @@ describe("deleteColumn", () => {
initUpdateTableSchemaAtom();
const {
result: { current: deleteColumn },
} = renderHook(() => useAtomValue(deleteColumnAtom, tableScope));
} = renderHook(() => useSetAtom(deleteColumnAtom, tableScope));
expect(deleteColumn).toBeDefined();
await act(() => deleteColumn("column7"));

View File

@@ -0,0 +1,134 @@
import { atom } from "jotai";
import { orderBy, findIndex } from "lodash-es";
import { tableSchemaAtom, updateTableSchemaAtom } from "./table";
import { ColumnConfig } from "@src/types/table";
/** Store the table columns as an ordered array */
export const tableColumnsOrderedAtom = atom<ColumnConfig[]>((get) => {
const tableSchema = get(tableSchemaAtom);
if (!tableSchema || !tableSchema.columns) return [];
return orderBy(Object.values(tableSchema?.columns ?? {}), "index");
});
/** Reducer function to convert from array of columns to columns object */
export const tableColumnsReducer = (
a: Record<string, ColumnConfig>,
c: ColumnConfig,
index: number
) => {
a[c.key] = { ...c, index };
return a;
};
export interface IAddColumnOptions {
/** Column config to add. `config.index` is ignored */
config: Omit<ColumnConfig, "index">;
/** Index to add column at. If undefined, adds to end */
index?: number;
}
/**
* Store function to add a column to tableSchema, to the end or by index.
* Also fixes any issues with column indexes, so they go from 0 to length - 1
* @param options - {@link IAddColumnOptions}
*
* @example Basic usage:
* ```
* const addColumn = useSetAtom(addColumnAtom, tableScope);
* addColumn({ config: {...}, index?: 0 });
* ```
*/
export const addColumnAtom = atom(
null,
async (get, _set, { config, index }: IAddColumnOptions) => {
const tableColumnsOrdered = [...get(tableColumnsOrderedAtom)];
const updateTableSchema = get(updateTableSchemaAtom);
if (!updateTableSchema) throw new Error("Cannot update table schema");
// If index is provided, insert at index. Otherwise, append to end
tableColumnsOrdered.splice(index ?? tableColumnsOrdered.length, 0, {
...config,
index: index ?? tableColumnsOrdered.length,
} as ColumnConfig);
// Reduce array into single object with updated indexes
const updatedColumns = tableColumnsOrdered.reduce(tableColumnsReducer, {});
await updateTableSchema({ columns: updatedColumns });
}
);
export interface IUpdateColumnOptions {
/** Unique key of column to update */
key: string;
/** Partial column config to add. `config.index` is ignored */
config: Partial<ColumnConfig>;
/** If passed, reorders the column to the index */
index?: number;
}
/**
* Store function to update a column in tableSchema
* @throws Error if column not found
* @param options - {@link IUpdateColumnOptions}
*
* @example Basic usage:
* ```
* const updateColumn = useSetAtom(updateColumnAtom, tableScope);
* updateColumn({ key: "", config: {...}, index?: 0 });
* ```
*/
export const updateColumnAtom = atom(
null,
async (get, _set, { key, config, index }: IUpdateColumnOptions) => {
const tableColumnsOrdered = [...get(tableColumnsOrderedAtom)];
const updateTableSchema = get(updateTableSchemaAtom);
if (!updateTableSchema) throw new Error("Cannot update table schema");
const currentIndex = findIndex(tableColumnsOrdered, ["key", key]);
if (currentIndex === -1)
throw new Error(`Column with key "${key}" not found`);
// If column is not being reordered, just update the config
if (!index) {
tableColumnsOrdered[currentIndex] = {
...tableColumnsOrdered[currentIndex],
...config,
index: currentIndex,
};
}
// Otherwise, remove the column from the current position
// Then insert it at the new position
else {
const currentColumn = tableColumnsOrdered.splice(currentIndex, 1)[0];
tableColumnsOrdered.splice(index, 0, {
...currentColumn,
...config,
index,
});
}
// Reduce array into single object with updated indexes
const updatedColumns = tableColumnsOrdered.reduce(tableColumnsReducer, {});
await updateTableSchema({ columns: updatedColumns });
}
);
/**
* Store function to delete a column in tableSchema
* @param key - Unique key of column to delete
*
* @example Basic usage:
* ```
* const deleteColumn = useSetAtom(deleteColumnAtom, tableScope);
* deleteColumn(" ... ");
* ```
*/
export const deleteColumnAtom = atom(null, async (get, _set, key: string) => {
const tableColumnsOrdered = [...get(tableColumnsOrderedAtom)];
const updateTableSchema = get(updateTableSchemaAtom);
if (!updateTableSchema) throw new Error("Cannot update table schema");
const updatedColumns = tableColumnsOrdered
.filter((c) => c.key !== key)
.reduce(tableColumnsReducer, {});
await updateTableSchema({ columns: updatedColumns });
});

View File

@@ -2,3 +2,4 @@
export const tableScope = Symbol("tableScope");
export * from "./table";
export * from "./columns";

View File

@@ -1,5 +1,5 @@
import { atom } from "jotai";
import { uniqBy, orderBy, findIndex } from "lodash-es";
import { uniqBy } from "lodash-es";
import {
TableSettings,
@@ -7,7 +7,8 @@ import {
TableFilter,
TableOrder,
TableRow,
ColumnConfig,
UpdateCollectionDocFunction,
DeleteCollectionDocFunction,
} from "@src/types/table";
/** Root atom from which others are derived */
@@ -20,116 +21,6 @@ export const tableSchemaAtom = atom<TableSchema | undefined>(undefined);
export const updateTableSchemaAtom = atom<
((update: Partial<TableSchema>) => Promise<void>) | undefined
>(undefined);
/** Store the table columns as an ordered array */
export const tableColumnsOrderedAtom = atom<ColumnConfig[]>((get) => {
const tableSchema = get(tableSchemaAtom);
if (!tableSchema || !tableSchema.columns) return [];
return orderBy(Object.values(tableSchema?.columns ?? {}), "index");
});
/** Reducer function to convert from array of columns to columns object */
export const tableColumnsReducer = (
a: Record<string, ColumnConfig>,
c: ColumnConfig,
index: number
) => {
a[c.key] = { ...c, index };
return a;
};
/**
* Store function to add a column to tableSchema, to the end or by index.
* Also fixes any issues with column indexes, so they go from 0 to length - 1
* @param config - Column config to add. `config.index` is ignored
* @param index - Index to add column at. If undefined, adds to end
*/
export const addColumnAtom = atom((get) => {
const tableColumnsOrdered = [...get(tableColumnsOrderedAtom)];
const updateTableSchema = get(updateTableSchemaAtom);
if (!updateTableSchema) {
return async (config: ColumnConfig, index?: number) => {
throw new Error("Cannot update table schema");
};
}
return (config: Omit<ColumnConfig, "index">, index?: number) => {
// If index is provided, insert at index. Otherwise, append to end
tableColumnsOrdered.splice(index ?? tableColumnsOrdered.length, 0, {
...config,
index: index ?? tableColumnsOrdered.length,
} as ColumnConfig);
// Reduce array into single object with updated indexes
const updatedColumns = tableColumnsOrdered.reduce(tableColumnsReducer, {});
return updateTableSchema({ columns: updatedColumns });
};
});
/**
* Store function to update a column in tableSchema. If not found, throws error.
* @param key - Unique key of column to update
* @param config - Partial column config to add. `config.index` is ignored
* @param index - If passed, reorders the column to the index
*/
export const updateColumnAtom = atom((get) => {
const tableColumnsOrdered = [...get(tableColumnsOrderedAtom)];
const updateTableSchema = get(updateTableSchemaAtom);
if (!updateTableSchema) {
return async (key: string, config: Partial<ColumnConfig>) => {
throw new Error("Cannot update table schema");
};
}
return (key: string, config: Partial<ColumnConfig>, index?: number) => {
const currentIndex = findIndex(tableColumnsOrdered, ["key", key]);
if (currentIndex === -1)
throw new Error(`Column with key "${key}" not found`);
// If column is not being reordered, just update the config
if (!index) {
tableColumnsOrdered[currentIndex] = {
...tableColumnsOrdered[currentIndex],
...config,
index: currentIndex,
};
}
// Otherwise, remove the column from the current position
// Then insert it at the new position
else {
const currentColumn = tableColumnsOrdered.splice(currentIndex, 1)[0];
tableColumnsOrdered.splice(index, 0, {
...currentColumn,
...config,
index,
});
}
// Reduce array into single object with updated indexes
const updatedColumns = tableColumnsOrdered.reduce(tableColumnsReducer, {});
return updateTableSchema({ columns: updatedColumns });
};
});
/**
* Store function to delete a column in tableSchema
* @param key - Unique key of column to delete
*/
export const deleteColumnAtom = atom((get) => {
const tableColumnsOrdered = [...get(tableColumnsOrderedAtom)];
const updateTableSchema = get(updateTableSchemaAtom);
if (!updateTableSchema) {
return async (key: string) => {
throw new Error("Cannot update table schema");
};
}
return (key: string) => {
const updatedColumns = tableColumnsOrdered
.filter((c) => c.key !== key)
.reduce(tableColumnsReducer, {});
return updateTableSchema({ columns: updatedColumns });
};
});
/** Filters applied to the local view */
export const tableFiltersAtom = atom<TableFilter[]>([]);
@@ -151,3 +42,40 @@ export const tableRowsAtom = atom<TableRow[]>((get) =>
);
/** Store loading more state for infinite scroll */
export const tableLoadingMoreAtom = atom(false);
/**
* Store function to add or update row in db directly.
* Has same behaviour as Firestore setDoc with merge.
* See https://stackoverflow.com/a/47554197/3572007
* @internal Use {@link addRowAtom} or {@link updateRowAtom} instead
*/
export const _updateRowDbAtom = atom<UpdateCollectionDocFunction | undefined>(
undefined
);
/**
* Store function to delete row in db directly
* @internal Use {@link deleteRowAtom} instead
*/
export const _deleteRowDbAtom = atom<DeleteCollectionDocFunction | undefined>(
undefined
);
export type AuditChangeFunction = (
type: "ADD_ROW" | "UPDATE_CELL" | "DELETE_ROW",
rowId: string,
data?:
| {
updatedField?: string | undefined;
}
| undefined
) => Promise<any>;
/**
* Store function to write auditing logs when user makes changes to the table.
* Silently fails if auditing is disabled for the table or Rowy Run version
* not compatible.
*
* @param type - Action type: "ADD_ROW" | "UPDATE_CELL" | "DELETE_ROW"
* @param rowId - ID of row updated
* @param data - Optional additional data to log
*/
export const auditChangeAtom = atom<AuditChangeFunction | undefined>(undefined);

View File

@@ -16,7 +16,7 @@ import { globalScope, confirmDialogAtom } from "@src/atoms/globalScope";
/**
* Display a confirm dialog using `confirmDialogAtom` in `globalState`
* {@link confirmDialogAtom | See usage example}
* @see {@link confirmDialogAtom | Usage example}
*/
export default function ConfirmDialog() {
const [

View File

@@ -24,7 +24,7 @@ import { WIKI_LINKS } from "@src/constants/externalLinks";
/**
* Display a modal asking the user to deploy or upgrade Rowy Run
* using `rowyRunModalAtom` in `globalState`
* {@link rowyRunModalAtom | See usage example}
* @see {@link rowyRunModalAtom | Usage example}
*/
export default function RowyRunModal() {
const [userRoles] = useAtom(userRolesAtom, globalScope);

View File

@@ -30,7 +30,7 @@ import { runRoutes } from "@src/constants/runRoutes";
import { USERS } from "@src/config/dbPaths";
export default function UserItem({
_rowy_id,
_rowy_ref,
user,
roles: rolesProp,
}: UserSettings) {
@@ -59,7 +59,7 @@ export default function UserItem({
});
if (res.success) {
if (!updateUser) throw new Error("Could not update user document");
await updateUser(_rowy_id!, { roles: value });
await updateUser(_rowy_ref!.id, { roles: value });
closeSnackbar(loadingSnackbarId);
enqueueSnackbar(`Set roles for ${user!.email}: ${value.join(", ")}`);
}
@@ -179,9 +179,11 @@ export default function UserItem({
<IconButton
aria-label="Copy UID"
onClick={async () => {
if (!_rowy_id) return;
await navigator.clipboard.writeText(_rowy_id);
enqueueSnackbar(`Copied UID for ${user?.email}: ${_rowy_id}`);
if (!_rowy_ref?.id) return;
await navigator.clipboard.writeText(_rowy_ref.id);
enqueueSnackbar(
`Copied UID for ${user?.email}: ${_rowy_ref.id}`
);
}}
>
<CopyIcon />

View File

@@ -12,6 +12,7 @@ import {
FirestoreError,
setDoc,
doc,
deleteDoc,
CollectionReference,
Query,
} from "firebase/firestore";
@@ -19,7 +20,8 @@ import { useErrorHandler } from "react-error-boundary";
import { globalScope } from "@src/atoms/globalScope";
import {
UpdateCollectionFunction,
UpdateCollectionDocFunction,
DeleteCollectionDocFunction,
TableFilter,
TableOrder,
TableRow,
@@ -44,8 +46,10 @@ interface IUseFirestoreCollectionWithAtomOptions<T> {
onError?: (error: FirestoreError) => void;
/** Optionally disable Suspense */
disableSuspense?: boolean;
/** Set this atoms value to a function that updates a document in the collection. If `collectionGroup` is true, you must pass the full path. Uses same scope as `dataScope`. */
updateDataAtom?: PrimitiveAtom<UpdateCollectionFunction<T> | undefined>;
/** Set this atoms value to a function that updates a document in the collection. Must pass the full path. Uses same scope as `dataScope`. */
updateDocAtom?: PrimitiveAtom<UpdateCollectionDocFunction<T> | undefined>;
/** Set this atoms value to a function that deletes a document in the collection. Must pass the full path. Uses same scope as `dataScope`. */
deleteDocAtom?: PrimitiveAtom<DeleteCollectionDocFunction | undefined>;
}
/**
@@ -66,9 +70,13 @@ export function useFirestoreCollectionWithAtom<T = TableRow>(
) {
const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
const setDataAtom = useSetAtom(dataAtom, dataScope);
const setUpdateDataAtom = useSetAtom(
options?.updateDataAtom || (dataAtom as any),
globalScope
const setUpdateDocAtom = useSetAtom(
options?.updateDocAtom || (dataAtom as any),
dataScope
);
const setDeleteDocAtom = useSetAtom(
options?.deleteDocAtom || (dataAtom as any),
dataScope
);
const handleError = useErrorHandler();
@@ -81,7 +89,8 @@ export function useFirestoreCollectionWithAtom<T = TableRow>(
limit = DEFAULT_COLLECTION_QUERY_LIMIT,
onError,
disableSuspense,
updateDataAtom,
updateDocAtom,
deleteDocAtom,
} = options || {};
useEffect(() => {
@@ -96,8 +105,7 @@ export function useFirestoreCollectionWithAtom<T = TableRow>(
suspended = true;
}
// Create a collection or collection group reference
// to query data and to use in `updateDataAtom`
// Create a collection or collection group reference to query data
const collectionRef = collectionGroup
? (queryCollectionGroup(
firebaseDb,
@@ -108,6 +116,7 @@ export function useFirestoreCollectionWithAtom<T = TableRow>(
path,
...((pathSegments as string[]) || [])
) as CollectionReference<T>);
// Create the query with filters and orders
const _query = query<T>(
collectionRef,
@@ -122,11 +131,9 @@ export function useFirestoreCollectionWithAtom<T = TableRow>(
_query,
(querySnapshot) => {
try {
// Extract doc data from query
// and add `_rowy_id` and `_rowy_ref` fields
// Extract doc data from query and add `_rowy_ref` fields
const docs = querySnapshot.docs.map((doc) => ({
...doc.data(),
_rowy_id: doc.id,
_rowy_ref: doc.ref,
}));
setDataAtom(docs);
@@ -143,26 +150,31 @@ export function useFirestoreCollectionWithAtom<T = TableRow>(
}
);
// If `options?.updateDataAtom` was passed,
// If `options?.updateDocAtom` was passed,
// set the atoms value to a function that updates a doc in the collection
if (updateDataAtom) {
setUpdateDataAtom(
if (updateDocAtom) {
setUpdateDocAtom(
() => (path: string, update: T) =>
setDoc(
collectionGroup
? doc(firebaseDb, path)
: doc(collectionRef as CollectionReference<T>, path),
update,
{ merge: true }
)
setDoc(doc(firebaseDb, path), update, { merge: true })
);
}
// If `options?.deleteDocAtom` was passed,
// set the atoms value to a function that deletes a doc in the collection
if (deleteDocAtom) {
setDeleteDocAtom(
() => (path: string) => deleteDoc(doc(firebaseDb, path))
);
}
return () => {
unsubscribe();
// If `options?.updateDataAtom` was passed,
// If `options?.updateDocAtom` was passed,
// reset the atoms value to prevent writes
if (updateDataAtom) setUpdateDataAtom(undefined);
if (updateDocAtom) setUpdateDocAtom(undefined);
// If `options?.deleteDoc` was passed,
// reset the atoms value to prevent deletes
if (deleteDocAtom) setDeleteDocAtom(undefined);
};
}, [
firebaseDb,
@@ -176,8 +188,10 @@ export function useFirestoreCollectionWithAtom<T = TableRow>(
setDataAtom,
disableSuspense,
handleError,
updateDataAtom,
setUpdateDataAtom,
updateDocAtom,
setUpdateDocAtom,
deleteDocAtom,
setDeleteDocAtom,
]);
}

View File

@@ -85,9 +85,12 @@ export function useFirestoreDocWithAtom<T = TableRow>(
try {
if (!docSnapshot.exists() && !!createIfNonExistent) {
setDoc(docSnapshot.ref, createIfNonExistent);
setDataAtom(createIfNonExistent);
setDataAtom({ ...createIfNonExistent, _rowy_ref: docSnapshot.ref });
} else {
setDataAtom(docSnapshot.data() || ({} as T));
setDataAtom({
...(docSnapshot.data() as T),
_rowy_ref: docSnapshot.ref,
});
}
} catch (error) {
if (onError) onError(error as FirestoreError);

View File

@@ -59,7 +59,7 @@ function UserManagementPage() {
<List sx={{ py: { xs: 0, sm: 1.5 }, px: { xs: 0, sm: 1 } }}>
<TransitionGroup>
{results.map((user) => (
<Collapse key={user._rowy_id}>
<Collapse key={user._rowy_ref!.id}>
<UserItem {...user} />
</Collapse>
))}

View File

@@ -10,12 +10,18 @@ import {
tableFiltersAtom,
tableOrdersAtom,
tableRowsAtom,
auditChangeAtom,
} from "@src/atoms/tableScope";
import TableSourceFirestore from "@src/sources/TableSourceFirestore";
import TableHeaderSkeleton from "@src/components/Table/Skeleton/TableHeaderSkeleton";
import HeaderRowSkeleton from "@src/components/Table/Skeleton/HeaderRowSkeleton";
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
import { globalScope } from "@src/atoms/globalScope";
import { doc, setDoc, updateDoc } from "firebase/firestore";
import { TABLE_SCHEMAS } from "@src/config/dbPaths";
function TableTestPage() {
const [tableId] = useAtom(tableIdAtom, tableScope);
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
@@ -25,22 +31,51 @@ function TableTestPage() {
const setTableOrders = useSetAtom(tableOrdersAtom, tableScope);
const [tableRows] = useAtom(tableRowsAtom, tableScope);
const [auditChange] = useAtom(auditChangeAtom, tableScope);
console.log(tableId, tableSchema);
const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
return (
<div>
<p>
Table ID: <code>{tableId}</code>
</p>
<pre style={{ height: "4em", overflow: "auto" }}>
<pre style={{ height: "4em", overflow: "auto", resize: "vertical" }}>
tableSettings: {JSON.stringify(tableSettings, undefined, 2)}
</pre>
<pre style={{ height: "4em", overflow: "auto" }}>
<pre style={{ height: "4em", overflow: "auto", resize: "vertical" }}>
tableSchema: {JSON.stringify(tableSchema, undefined, 2)}
</pre>
<button
onClick={() => {
setDoc(
doc(firebaseDb, TABLE_SCHEMAS, tableId!),
{
_test: { [Date.now()]: "write" },
_testArray: [{ [Date.now()]: "writeArray" }],
},
{ merge: true }
);
}}
>
Firestore set + merge
</button>
<button
onClick={() => {
updateDoc(doc(firebaseDb, TABLE_SCHEMAS, tableId!), {
_test: { [Date.now()]: "write" },
_testArray: [{ [Date.now()]: "writeArray" }],
});
}}
>
Firestore update
</button>
<br />
<button
onClick={() =>
setTableFilters([{ key: "signedUp", operator: "==", value: true }])
@@ -58,9 +93,9 @@ function TableTestPage() {
<button onClick={() => setTableFilters([])}>Clear table orders</button>
<ol>
{tableRows.map(({ _rowy_id, ...data }) => (
<li key={_rowy_id}>
{_rowy_id}: {data.firstName} {data.signedUp.toString()}
{tableRows.map(({ _rowy_ref, ...data }) => (
<li key={_rowy_ref.id}>
{_rowy_ref.id}: {data.firstName} {data.signedUp.toString()}
</li>
))}
</ol>

View File

@@ -30,7 +30,7 @@ export function useTableFunctions() {
// Create a function to get the latest tables from project settings,
// so we dont create new functions when tables change
const getTables = useAtomCallback(
const readTables = useAtomCallback(
useCallback((get) => get(projectSettingsAtom).tables, []),
globalScope
);
@@ -51,7 +51,7 @@ export function useTableFunctions() {
} = additionalSettings || {};
// Get latest tables
const tables = (await getTables()) || [];
const tables = (await readTables()) || [];
// Get columns from imported table settings or _schemaSource if provided
let columns: NonNullable<TableSchema["columns"]> =
@@ -120,7 +120,7 @@ export function useTableFunctions() {
await Promise.all([promiseUpdateSettings, promiseAddSchema]);
}
);
}, [firebaseDb, getTables, setCreateTable]);
}, [firebaseDb, readTables, setCreateTable]);
// Set the createTable function
const setUpdateTable = useSetAtom(updateTableAtom, globalScope);
@@ -134,7 +134,7 @@ export function useTableFunctions() {
const { _schema } = additionalSettings || {};
// Get latest tables
const tables = [...((await getTables()) || [])];
const tables = [...((await readTables()) || [])];
const foundIndex = findIndex(tables, ["id", settings.id]);
const tableIndex = foundIndex > -1 ? foundIndex : tables.length;
@@ -166,14 +166,14 @@ export function useTableFunctions() {
await Promise.all([promiseUpdateSettings, promiseUpdateSchema]);
}
);
}, [firebaseDb, getTables, setUpdateTable]);
}, [firebaseDb, readTables, setUpdateTable]);
// Set the deleteTable function
const setDeleteTable = useSetAtom(deleteTableAtom, globalScope);
useEffect(() => {
setDeleteTable(() => async (id: string) => {
// Get latest tables
const tables = (await getTables()) || [];
const tables = (await readTables()) || [];
const table = find(tables, ["id", id]);
// Removes table from settings doc array
@@ -196,14 +196,14 @@ export function useTableFunctions() {
// Wait for both to complete
await Promise.all([promiseUpdateSettings, promiseDeleteSchema]);
});
}, [firebaseDb, getTables, setDeleteTable]);
}, [firebaseDb, readTables, setDeleteTable]);
// Set the getTableSchema function
const setGetTableSchema = useSetAtom(getTableSchemaAtom, globalScope);
useEffect(() => {
setGetTableSchema(() => async (id: string) => {
// Get latest tables
const tables = (await getTables()) || [];
const tables = (await readTables()) || [];
const table = find(tables, ["id", id]);
const tableSchemaDocRef = doc(
@@ -217,5 +217,5 @@ export function useTableFunctions() {
(doc) => (doc.data() || {}) as TableSchema
);
});
}, [firebaseDb, getTables, setGetTableSchema]);
}, [firebaseDb, readTables, setGetTableSchema]);
}

View File

@@ -2,7 +2,12 @@ import { memo, useMemo, useEffect } from "react";
import { useAtom, useSetAtom } from "jotai";
import { find } from "lodash-es";
import { globalScope, tablesAtom } from "@src/atoms/globalScope";
import {
globalScope,
tablesAtom,
rowyRunAtom,
compatibleRowyRunVersionAtom,
} from "@src/atoms/globalScope";
import {
tableScope,
tableIdAtom,
@@ -12,6 +17,9 @@ import {
tableOrdersAtom,
tablePageAtom,
tableRowsDbAtom,
_updateRowDbAtom,
_deleteRowDbAtom,
auditChangeAtom,
} from "@src/atoms/tableScope";
import useFirestoreDocWithAtom from "@src/hooks/useFirestoreDocWithAtom";
@@ -23,6 +31,7 @@ import { TABLE_SCHEMAS, TABLE_GROUP_SCHEMAS } from "@src/config/dbPaths";
import type { FirestoreError } from "firebase/firestore";
import { useSnackbar } from "notistack";
import { useErrorHandler } from "react-error-boundary";
import { runRoutes } from "@src/constants/runRoutes";
import { Button } from "@mui/material";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
@@ -30,7 +39,6 @@ export const ERROR_TABLE_NOT_FOUND = "Table not found";
const TableSourceFirestore = memo(function TableSourceFirestore() {
const [tables] = useAtom(tablesAtom, globalScope);
// const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
// Get tableSettings from tableId and tables in globalScope
const [tableId] = useAtom(tableIdAtom, tableScope);
@@ -77,9 +85,54 @@ const TableSourceFirestore = memo(function TableSourceFirestore() {
collectionGroup: isCollectionGroup,
onError: (error) =>
handleFirestoreError(error, enqueueSnackbar, elevateError),
updateDocAtom: _updateRowDbAtom,
deleteDocAtom: _deleteRowDbAtom,
}
);
// Set auditChange function
const setAuditChange = useSetAtom(auditChangeAtom, tableScope);
const [rowyRun] = useAtom(rowyRunAtom, globalScope);
const [compatibleRowyRunVersion] = useAtom(
compatibleRowyRunVersionAtom,
globalScope
);
useEffect(() => {
if (
!tableSettings?.id ||
!tableSettings?.collection ||
!tableSettings.audit ||
!compatibleRowyRunVersion({ minVersion: "1.1.1" })
) {
setAuditChange(undefined);
return;
}
setAuditChange(
() =>
(
type: "ADD_ROW" | "UPDATE_CELL" | "DELETE_ROW",
rowId: string,
data?: { updatedField?: string }
) =>
rowyRun({
route: runRoutes.auditChange,
body: {
type,
ref: {
rowPath: tableSettings.collection,
rowId,
tableId: tableSettings.id,
collectionPath: tableSettings.collection,
},
data,
},
})
);
return () => setAuditChange(undefined);
}, [setAuditChange, rowyRun, compatibleRowyRunVersion, tableSettings]);
return null;
});

View File

@@ -11,7 +11,7 @@ import { USERS } from "@src/config/dbPaths";
const UserManagementSourceFirebase = memo(
function UserManagementSourceFirebase() {
useFirestoreCollectionWithAtom(allUsersAtom, globalScope, USERS, {
updateDataAtom: updateUserAtom,
updateDocAtom: updateUserAtom,
});
return null;

13
src/types/table.d.ts vendored
View File

@@ -9,11 +9,13 @@ export type UpdateDocFunction<T = TableRow> = (
update: Partial<T>
) => Promise<void>;
export type UpdateCollectionFunction<T = TableRow> = (
export type UpdateCollectionDocFunction<T = TableRow> = (
path: string,
update: Partial<T>
) => Promise<void>;
export type DeleteCollectionDocFunction = (path: string) => Promise<void>;
/** Table settings stored in project settings */
export type TableSettings = {
id: string;
@@ -78,10 +80,13 @@ export type TableOrder = {
direction: Parameters<typeof orderBy>[1];
};
export type TableRow = DocumentData & {
_rowy_id: string;
_rowy_ref: { id: string; path: string } & Partial<DocumentReference>;
export type TableRowRef = {
id: string;
path: string;
} & Partial<DocumentReference>;
export type TableRow = DocumentData & {
_rowy_ref: TableRowRef;
_rowy_missingRequiredFields?: string[];
_rowy_outOfOrder?: boolean;
};