mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
data layer core & types updates
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
134
src/atoms/tableScope/columns.ts
Normal file
134
src/atoms/tableScope/columns.ts
Normal 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 });
|
||||
});
|
||||
@@ -2,3 +2,4 @@
|
||||
export const tableScope = Symbol("tableScope");
|
||||
|
||||
export * from "./table";
|
||||
export * from "./columns";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 atom’s 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 atom’s 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 atom’s 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 atom’s 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 atom’s 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 atom’s value to prevent writes
|
||||
if (updateDataAtom) setUpdateDataAtom(undefined);
|
||||
if (updateDocAtom) setUpdateDocAtom(undefined);
|
||||
// If `options?.deleteDoc` was passed,
|
||||
// reset the atom’s 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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function useTableFunctions() {
|
||||
|
||||
// Create a function to get the latest tables from project settings,
|
||||
// so we don’t 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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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
13
src/types/table.d.ts
vendored
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user