mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
add updateFieldAtom & id decrement
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { cloneDeep, unset } from "lodash-es";
|
||||
|
||||
import {
|
||||
tableScope,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
} from "@src/atoms/tableScope";
|
||||
import { TableSchema } from "@src/types/table";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { updateRowData } from "@src/utils/table";
|
||||
|
||||
const initUpdateTableSchemaAtom = (initialTableSchema?: TableSchema) =>
|
||||
renderHook(() => {
|
||||
@@ -18,9 +20,19 @@ const initUpdateTableSchemaAtom = (initialTableSchema?: TableSchema) =>
|
||||
setTableSchema(initialTableSchema ?? {});
|
||||
|
||||
const setUpdateTableSchema = useSetAtom(updateTableSchemaAtom, tableScope);
|
||||
setUpdateTableSchema(() => async (update: Partial<TableSchema>) => {
|
||||
setTableSchema(update);
|
||||
});
|
||||
setUpdateTableSchema(
|
||||
() => async (update: Partial<TableSchema>, deleteFields?: string[]) => {
|
||||
setTableSchema((current) => {
|
||||
const withFieldsDeleted = cloneDeep(current);
|
||||
if (Array.isArray(deleteFields)) {
|
||||
for (const field of deleteFields) {
|
||||
unset(withFieldsDeleted, field);
|
||||
}
|
||||
}
|
||||
return updateRowData(withFieldsDeleted || {}, update);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const GENERATED_COLUMNS_LENGTH = 10;
|
||||
|
||||
@@ -130,5 +130,5 @@ export const deleteColumnAtom = atom(null, async (get, _set, key: string) => {
|
||||
.filter((c) => c.key !== key)
|
||||
.reduce(tableColumnsReducer, {});
|
||||
|
||||
await updateTableSchema({ columns: updatedColumns });
|
||||
await updateTableSchema({ columns: updatedColumns }, [`columns.${key}`]);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { useAtomCallback } from "jotai/utils";
|
||||
import { find, findIndex, mergeWith, isArray } from "lodash-es";
|
||||
|
||||
import { currentUserAtom } from "@src/atoms/globalScope";
|
||||
import {
|
||||
tableScope,
|
||||
tableSettingsAtom,
|
||||
@@ -11,15 +12,24 @@ import {
|
||||
tableRowsLocalAtom,
|
||||
_updateRowDbAtom,
|
||||
_deleteRowDbAtom,
|
||||
addRowAtom,
|
||||
deleteRowAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { TableRow } from "@src/types/table";
|
||||
import { tableRowsAtom } from "./table";
|
||||
import { updateRowData, decrementId } from "@src/utils/table";
|
||||
|
||||
const TEST_COLLECTION = "_testing";
|
||||
|
||||
const initRows = (initialRowsDb?: TableRow[], initialRowsLocal?: TableRow[]) =>
|
||||
renderHook(async () => {
|
||||
const setCurrentUser = useSetAtom(currentUserAtom, tableScope);
|
||||
setCurrentUser({
|
||||
uid: "TEST_USER",
|
||||
displayName: "Test User",
|
||||
email: "test@example.com",
|
||||
} as any);
|
||||
|
||||
const setTableSettings = useSetAtom(tableSettingsAtom, tableScope);
|
||||
setTableSettings({
|
||||
id: TEST_COLLECTION,
|
||||
@@ -43,23 +53,17 @@ const initRows = (initialRowsDb?: TableRow[], initialRowsLocal?: TableRow[]) =>
|
||||
const setUpdateRowDb = useSetAtom(_updateRowDbAtom, tableScope);
|
||||
setUpdateRowDb(() => async (path: string, update: Partial<TableRow>) => {
|
||||
const rows = [...(await readRowsDb())];
|
||||
const index = findIndex(rows, ["_rowy_ref.id", path]);
|
||||
const index = findIndex(rows, ["_rowy_ref.path", path]);
|
||||
// Append if not found
|
||||
if (index === -1) {
|
||||
setRowsDb((rows) => [
|
||||
setRowsDb([
|
||||
...rows,
|
||||
{ ...update, _rowy_ref: { id: path, path: "TEST_COLLECTION" } },
|
||||
{ ...update, _rowy_ref: { id: path.split("/").pop()!, path } },
|
||||
]);
|
||||
} else {
|
||||
rows[index] = mergeWith(
|
||||
rows[index],
|
||||
update,
|
||||
// If proeprty to be merged is array, overwrite the array entirely.
|
||||
// This matches Firestore set with merge behaviour
|
||||
(objValue, srcValue) => (isArray(objValue) ? srcValue : undefined)
|
||||
);
|
||||
rows[index] = updateRowData(rows[index], update);
|
||||
setRowsDb(rows);
|
||||
}
|
||||
setRowsDb(rows);
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
@@ -89,6 +93,136 @@ const generatedRowsLocal = new Array(GENERATED_ROWS_LENGTH)
|
||||
index: i,
|
||||
}));
|
||||
|
||||
describe("addRow", () => {
|
||||
test("adds a single row with pre-defined id", async () => {
|
||||
initRows(generatedRows);
|
||||
const {
|
||||
result: { current: addRow },
|
||||
} = renderHook(() => useSetAtom(addRowAtom, tableScope));
|
||||
expect(addRow).toBeDefined();
|
||||
|
||||
await act(() =>
|
||||
addRow({
|
||||
row: {
|
||||
_rowy_ref: { id: "addedRow", path: TEST_COLLECTION + "/addedRow" },
|
||||
added: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current: tableRows },
|
||||
} = renderHook(() => useAtomValue(tableRowsAtom, tableScope));
|
||||
expect(tableRows).toHaveLength(GENERATED_ROWS_LENGTH + 1);
|
||||
expect(find(tableRows, ["_rowy_ref.id", "addedRow"])).toBeDefined();
|
||||
expect(find(tableRows, ["_rowy_ref.id", "addedRow"])?.added).toBe(true);
|
||||
});
|
||||
|
||||
test("adds a single row with pre-defined id to an empty table", async () => {
|
||||
initRows();
|
||||
const {
|
||||
result: { current: addRow },
|
||||
} = renderHook(() => useSetAtom(addRowAtom, tableScope));
|
||||
expect(addRow).toBeDefined();
|
||||
|
||||
await act(() =>
|
||||
addRow({
|
||||
row: {
|
||||
_rowy_ref: { id: "addedRow", path: TEST_COLLECTION + "/addedRow" },
|
||||
added: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current: tableRows },
|
||||
} = renderHook(() => useAtomValue(tableRowsAtom, tableScope));
|
||||
expect(tableRows).toHaveLength(1);
|
||||
expect(find(tableRows, ["_rowy_ref.id", "addedRow"])).toBeDefined();
|
||||
expect(find(tableRows, ["_rowy_ref.id", "addedRow"])?.added).toBe(true);
|
||||
});
|
||||
|
||||
test("adds a single row and generate random id", async () => {
|
||||
initRows(generatedRows);
|
||||
const {
|
||||
result: { current: addRow },
|
||||
} = renderHook(() => useSetAtom(addRowAtom, tableScope));
|
||||
expect(addRow).toBeDefined();
|
||||
|
||||
await act(() =>
|
||||
addRow({
|
||||
row: {
|
||||
_rowy_ref: { id: "addedRow", path: TEST_COLLECTION + "/addedRow" },
|
||||
added: true,
|
||||
},
|
||||
setId: "random",
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current: tableRows },
|
||||
} = renderHook(() => useAtomValue(tableRowsAtom, tableScope));
|
||||
expect(tableRows).toHaveLength(GENERATED_ROWS_LENGTH + 1);
|
||||
expect(find(tableRows, ["_rowy_ref.id", "addedRow"])).toBeUndefined();
|
||||
expect(find(tableRows, ["added", true])).toBeDefined();
|
||||
expect(find(tableRows, ["added", true])?._rowy_ref.id).toHaveLength(20);
|
||||
});
|
||||
|
||||
test("adds a single row and decrement id", async () => {
|
||||
initRows(generatedRows);
|
||||
const {
|
||||
result: { current: addRow },
|
||||
} = renderHook(() => useSetAtom(addRowAtom, tableScope));
|
||||
expect(addRow).toBeDefined();
|
||||
|
||||
await act(() =>
|
||||
addRow({
|
||||
row: {
|
||||
_rowy_ref: { id: "addedRow", path: TEST_COLLECTION + "/addedRow" },
|
||||
added: true,
|
||||
},
|
||||
setId: "decrement",
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current: tableRows },
|
||||
} = renderHook(() => useAtomValue(tableRowsAtom, tableScope));
|
||||
expect(tableRows).toHaveLength(GENERATED_ROWS_LENGTH + 1);
|
||||
expect(find(tableRows, ["_rowy_ref.id", "addedRow"])).toBeUndefined();
|
||||
expect(find(tableRows, ["added", true])).toBeDefined();
|
||||
expect(find(tableRows, ["added", true])?._rowy_ref.id).toBe(
|
||||
decrementId("row0")
|
||||
);
|
||||
});
|
||||
|
||||
test("adds a single row with decrement id to an empty table", async () => {
|
||||
initRows([]);
|
||||
const {
|
||||
result: { current: addRow },
|
||||
} = renderHook(() => useSetAtom(addRowAtom, tableScope));
|
||||
expect(addRow).toBeDefined();
|
||||
|
||||
await act(() =>
|
||||
addRow({
|
||||
row: {
|
||||
_rowy_ref: { id: "addedRow", path: TEST_COLLECTION + "/addedRow" },
|
||||
added: true,
|
||||
},
|
||||
setId: "decrement",
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current: tableRows },
|
||||
} = renderHook(() => useAtomValue(tableRowsAtom, tableScope));
|
||||
expect(tableRows).toHaveLength(1);
|
||||
expect(find(tableRows, ["_rowy_ref.id", "addedRow"])).toBeUndefined();
|
||||
expect(find(tableRows, ["added", true])).toBeDefined();
|
||||
expect(find(tableRows, ["added", true])?._rowy_ref.id).toBe(decrementId());
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteRow", () => {
|
||||
test("deletes a single row", async () => {
|
||||
initRows(generatedRows);
|
||||
@@ -173,4 +307,19 @@ describe("deleteRow", () => {
|
||||
} = renderHook(() => useAtomValue(tableRowsAtom, tableScope));
|
||||
expect(tableRows).toHaveLength(GENERATED_ROWS_LENGTH);
|
||||
});
|
||||
|
||||
test("doesn't delete from empty rows", async () => {
|
||||
initRows();
|
||||
const {
|
||||
result: { current: deleteRow },
|
||||
} = renderHook(() => useSetAtom(deleteRowAtom, tableScope));
|
||||
expect(deleteRow).toBeDefined();
|
||||
|
||||
await act(() => deleteRow("nonExistent"));
|
||||
|
||||
const {
|
||||
result: { current: tableRows },
|
||||
} = renderHook(() => useAtomValue(tableRowsAtom, tableScope));
|
||||
expect(tableRows).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { atom } from "jotai";
|
||||
import { find } from "lodash-es";
|
||||
import { cloneDeep, find, set as _set, unset } from "lodash-es";
|
||||
|
||||
import { currentUserAtom } from "@src/atoms/globalScope";
|
||||
import {
|
||||
@@ -7,22 +7,30 @@ import {
|
||||
tableSettingsAtom,
|
||||
tableFiltersAtom,
|
||||
tableRowsLocalAtom,
|
||||
tableRowsAtom,
|
||||
_updateRowDbAtom,
|
||||
_deleteRowDbAtom,
|
||||
} from "./table";
|
||||
import { tableColumnsOrderedAtom } from "./columnActions";
|
||||
import { TableRow } from "@src/types/table";
|
||||
import { rowyUser } from "@src/utils/table";
|
||||
import {
|
||||
rowyUser,
|
||||
generateId,
|
||||
decrementId,
|
||||
updateRowData,
|
||||
} from "@src/utils/table";
|
||||
|
||||
export interface IAddRowOptions {
|
||||
/** The row or array of rows to add */
|
||||
row: TableRow | TableRow[];
|
||||
/** If true, ignores checking required fields have values */
|
||||
ignoreRequiredFields?: boolean;
|
||||
/** Optionally overwite the IDs in the provided rows */
|
||||
setId?: "random" | "decrement";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a row or an array of rows.
|
||||
* Adds to rowsDb if it has no missing required fields,
|
||||
* otherwise to or rowsLocal.
|
||||
* Adds to rowsDb if it has no missing required fields, otherwise to rowsLocal.
|
||||
* @param options - {@link IAddRowOptions}
|
||||
*
|
||||
* @example Basic usage:
|
||||
@@ -33,7 +41,7 @@ export interface IAddRowOptions {
|
||||
*/
|
||||
export const addRowAtom = atom(
|
||||
null,
|
||||
async (get, set, { row, ignoreRequiredFields }: IAddRowOptions) => {
|
||||
async (get, set, { row, ignoreRequiredFields, setId }: IAddRowOptions) => {
|
||||
const updateRowDb = get(_updateRowDbAtom);
|
||||
if (!updateRowDb) throw new Error("Cannot write to database");
|
||||
const tableSettings = get(tableSettingsAtom);
|
||||
@@ -43,6 +51,7 @@ export const addRowAtom = atom(
|
||||
const auditChange = get(auditChangeAtom);
|
||||
const tableFilters = get(tableFiltersAtom);
|
||||
const tableColumnsOrdered = get(tableColumnsOrderedAtom);
|
||||
const tableRows = get(tableRowsAtom);
|
||||
|
||||
const _addSingleRowAndAudit = async (row: TableRow) => {
|
||||
// Store initial values to be written
|
||||
@@ -113,10 +122,43 @@ export const addRowAtom = atom(
|
||||
};
|
||||
|
||||
if (Array.isArray(row)) {
|
||||
const promises = row.map(_addSingleRowAndAudit);
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
let lastId = tableRows[0]?._rowy_ref.id;
|
||||
for (const r of row) {
|
||||
const id =
|
||||
setId === "random"
|
||||
? generateId()
|
||||
: setId === "decrement"
|
||||
? decrementId(lastId)
|
||||
: r._rowy_ref.id;
|
||||
lastId = id;
|
||||
|
||||
const path = setId
|
||||
? `${r._rowy_ref.path.split("/").slice(0, -1).join("/")}/${id}`
|
||||
: r._rowy_ref.path;
|
||||
|
||||
promises.push(
|
||||
_addSingleRowAndAudit(setId ? { ...r, _rowy_ref: { id, path } } : r)
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
} else {
|
||||
await _addSingleRowAndAudit(row);
|
||||
const id =
|
||||
setId === "random"
|
||||
? generateId()
|
||||
: setId === "decrement"
|
||||
? decrementId(tableRows[0]?._rowy_ref.id)
|
||||
: row._rowy_ref.id;
|
||||
|
||||
const path = setId
|
||||
? `${row._rowy_ref.path.split("/").slice(0, -1).join("/")}/${id}`
|
||||
: row._rowy_ref.path;
|
||||
|
||||
await _addSingleRowAndAudit(
|
||||
setId ? { ...row, _rowy_ref: { id, path } } : row
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -138,10 +180,12 @@ export const deleteRowAtom = atom(
|
||||
if (!deleteRowDb) throw new Error("Cannot write to database");
|
||||
|
||||
const auditChange = get(auditChangeAtom);
|
||||
const rowsLocal = get(tableRowsLocalAtom);
|
||||
const tableRowsLocal = get(tableRowsLocalAtom);
|
||||
|
||||
const _deleteSingleRowAndAudit = async (path: string) => {
|
||||
const isLocalRow = Boolean(find(rowsLocal, ["_rowy_ref.path", path]));
|
||||
const isLocalRow = Boolean(
|
||||
find(tableRowsLocal, ["_rowy_ref.path", path])
|
||||
);
|
||||
if (isLocalRow) set(tableRowsLocalAtom, { type: "delete", path });
|
||||
else await deleteRowDb(path);
|
||||
if (auditChange) auditChange("DELETE_ROW", path);
|
||||
@@ -155,3 +199,107 @@ export const deleteRowAtom = atom(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export interface IUpdateFieldOptions {
|
||||
/** The path to the row to update */
|
||||
path: string;
|
||||
/** The field name to update. Use dot notation to access nested fields. */
|
||||
fieldName: string;
|
||||
/** The value to write */
|
||||
value: any;
|
||||
/** Optionally, delete the field with fieldName. Use dot notation to access nested fields. */
|
||||
deleteField?: boolean;
|
||||
/** If true, ignores checking required fields have values */
|
||||
ignoreRequiredFields?: boolean;
|
||||
}
|
||||
/**
|
||||
* Updates or deletes a field in a row.
|
||||
* Adds to rowsDb if it has no missing required fields,
|
||||
* otherwise keeps in rowsLocal.
|
||||
* @param options - {@link IAddRowOptions}
|
||||
*
|
||||
* @example Basic usage:
|
||||
* ```
|
||||
* const updateField = useSetAtom(updateFieldAtom, tableScope);
|
||||
* updateField({ path, fieldName: "", value: null, deleteField: true });
|
||||
* ```
|
||||
*/
|
||||
export const updateFieldAtom = atom(
|
||||
null,
|
||||
async (
|
||||
get,
|
||||
set,
|
||||
{
|
||||
path,
|
||||
fieldName,
|
||||
value,
|
||||
deleteField,
|
||||
ignoreRequiredFields,
|
||||
}: IUpdateFieldOptions
|
||||
) => {
|
||||
const updateRowDb = get(_updateRowDbAtom);
|
||||
if (!updateRowDb) throw new Error("Cannot write to database");
|
||||
const tableSettings = get(tableSettingsAtom);
|
||||
if (!tableSettings) throw new Error("Cannot read table settings");
|
||||
const currentUser = get(currentUserAtom);
|
||||
if (!currentUser) throw new Error("Cannot read current user");
|
||||
const auditChange = get(auditChangeAtom);
|
||||
const tableColumnsOrdered = get(tableColumnsOrderedAtom);
|
||||
const tableRows = get(tableRowsAtom);
|
||||
const tableRowsLocal = get(tableRowsLocalAtom);
|
||||
|
||||
const row = find(tableRows, ["_rowy_ref.path", path]);
|
||||
if (!row) throw new Error("Could not find row");
|
||||
const isLocalRow = Boolean(find(tableRowsLocal, ["_rowy_ref.path", path]));
|
||||
|
||||
const update: Partial<TableRow> = {};
|
||||
|
||||
// Write audit fields if not explicitly disabled
|
||||
if (tableSettings.audit !== false) {
|
||||
const auditValue = rowyUser(currentUser);
|
||||
update[tableSettings.auditFieldUpdatedBy || "_updatedBy"] = auditValue;
|
||||
}
|
||||
|
||||
// Check for required fields
|
||||
const requiredFields = ignoreRequiredFields
|
||||
? []
|
||||
: tableColumnsOrdered
|
||||
.filter((column) => column.config?.required)
|
||||
.map((column) => column.key);
|
||||
const missingRequiredFields = ignoreRequiredFields
|
||||
? []
|
||||
: requiredFields.filter((field) => row[field] === undefined);
|
||||
|
||||
// Apply field update
|
||||
if (!deleteField) _set(update, fieldName, value);
|
||||
|
||||
// Update rowsLocal if any required fields are missing
|
||||
if (missingRequiredFields.length > 0) {
|
||||
set(tableRowsLocalAtom, {
|
||||
type: "update",
|
||||
path,
|
||||
row: update,
|
||||
deleteFields: deleteField ? [fieldName] : [],
|
||||
});
|
||||
}
|
||||
// If no required fields are missing and the row is only local,
|
||||
// write the entire row to the database
|
||||
else if (isLocalRow) {
|
||||
const rowValues = updateRowData(cloneDeep(row), update);
|
||||
if (deleteField) unset(rowValues, fieldName);
|
||||
|
||||
await updateRowDb(
|
||||
row._rowy_ref.path,
|
||||
rowValues,
|
||||
deleteField ? [fieldName] : []
|
||||
);
|
||||
}
|
||||
// Otherwise, update single field in database
|
||||
else {
|
||||
await updateRowDb(path, update, deleteField ? [fieldName] : []);
|
||||
}
|
||||
|
||||
if (auditChange)
|
||||
auditChange("UPDATE_CELL", path, { updatedField: fieldName });
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { atom } from "jotai";
|
||||
import { atomWithReducer } from "jotai/utils";
|
||||
import { uniqBy, findIndex } from "lodash-es";
|
||||
import { uniqBy, findIndex, cloneDeep, unset } from "lodash-es";
|
||||
|
||||
import {
|
||||
TableSettings,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TableFilter,
|
||||
TableOrder,
|
||||
TableRow,
|
||||
UpdateDocFunction,
|
||||
UpdateCollectionDocFunction,
|
||||
DeleteCollectionDocFunction,
|
||||
} from "@src/types/table";
|
||||
@@ -18,10 +19,10 @@ export const tableIdAtom = atom<string | undefined>(undefined);
|
||||
/** Store tableSettings from project settings document */
|
||||
export const tableSettingsAtom = atom<TableSettings | undefined>(undefined);
|
||||
/** Store tableSchema from schema document */
|
||||
export const tableSchemaAtom = atom<TableSchema | undefined>(undefined);
|
||||
export const tableSchemaAtom = atom<TableSchema>({});
|
||||
/** Store function to update tableSchema */
|
||||
export const updateTableSchemaAtom = atom<
|
||||
((update: Partial<TableSchema>) => Promise<void>) | undefined
|
||||
UpdateDocFunction<TableSchema> | undefined
|
||||
>(undefined);
|
||||
|
||||
/** Filters applied to the local view */
|
||||
@@ -37,7 +38,12 @@ type TableRowsLocalAction =
|
||||
/** Add a row or multiple rows */
|
||||
| { type: "add"; row: TableRow | TableRow[] }
|
||||
/** Update a row */
|
||||
| { type: "update"; path: string; row: Partial<TableRow> }
|
||||
| {
|
||||
type: "update";
|
||||
path: string;
|
||||
row: Partial<TableRow>;
|
||||
deleteFields?: string[];
|
||||
}
|
||||
/** Delete a row or multiple rows */
|
||||
| { type: "delete"; path: string | string[] };
|
||||
const tableRowsLocalReducer = (
|
||||
@@ -55,7 +61,13 @@ const tableRowsLocalReducer = (
|
||||
const index = findIndex(prev, ["_rowy_ref.path", action.path]);
|
||||
if (index > -1) {
|
||||
const updatedRows = [...prev];
|
||||
updatedRows[index] = updateRowData(prev[index], action.row);
|
||||
if (Array.isArray(action.deleteFields)) {
|
||||
updatedRows[index] = cloneDeep(prev[index]);
|
||||
for (const field of action.deleteFields) {
|
||||
unset(updatedRows[index], field);
|
||||
}
|
||||
}
|
||||
updatedRows[index] = updateRowData(updatedRows[index], action.row);
|
||||
return updatedRows;
|
||||
}
|
||||
// If not found, add to start
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { useAtom, PrimitiveAtom, useSetAtom } from "jotai";
|
||||
import { Scope } from "jotai/core/atom";
|
||||
import { set } from "lodash-es";
|
||||
import {
|
||||
query,
|
||||
collection,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
deleteDoc,
|
||||
CollectionReference,
|
||||
Query,
|
||||
deleteField,
|
||||
} from "firebase/firestore";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
|
||||
@@ -154,8 +156,17 @@ export function useFirestoreCollectionWithAtom<T = TableRow>(
|
||||
// set the atom’s value to a function that updates a doc in the collection
|
||||
if (updateDocAtom) {
|
||||
setUpdateDocAtom(
|
||||
() => (path: string, update: T) =>
|
||||
setDoc(doc(firebaseDb, path), update, { merge: true })
|
||||
() => (path: string, update: T, deleteFields?: string[]) => {
|
||||
const updateToDb = { ...update };
|
||||
|
||||
if (Array.isArray(deleteFields)) {
|
||||
for (const field of deleteFields) {
|
||||
set(updateToDb as any, field, deleteField());
|
||||
}
|
||||
}
|
||||
|
||||
return setDoc(doc(firebaseDb, path), updateToDb, { merge: true });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useEffect } from "react";
|
||||
import { useAtom, PrimitiveAtom, useSetAtom } from "jotai";
|
||||
import { Scope } from "jotai/core/atom";
|
||||
import { set } from "lodash-es";
|
||||
import {
|
||||
doc,
|
||||
onSnapshot,
|
||||
FirestoreError,
|
||||
setDoc,
|
||||
DocumentReference,
|
||||
deleteField,
|
||||
} from "firebase/firestore";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
|
||||
@@ -108,9 +110,17 @@ export function useFirestoreDocWithAtom<T = TableRow>(
|
||||
// If `options?.updateDataAtom` was passed,
|
||||
// set the atom’s value to a function that updates the document
|
||||
if (updateDataAtom) {
|
||||
setUpdateDataAtom(
|
||||
() => (update: T) => setDoc(ref, update, { merge: true })
|
||||
);
|
||||
setUpdateDataAtom(() => (update: T, deleteFields?: string[]) => {
|
||||
const updateToDb = { ...update };
|
||||
|
||||
if (Array.isArray(deleteFields)) {
|
||||
for (const field of deleteFields) {
|
||||
set(updateToDb as any, field, deleteField());
|
||||
}
|
||||
}
|
||||
|
||||
return setDoc(ref, updateToDb, { merge: true });
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -33,7 +33,7 @@ function TableTestPage() {
|
||||
const [tableRows] = useAtom(tableRowsAtom, tableScope);
|
||||
const [auditChange] = useAtom(auditChangeAtom, tableScope);
|
||||
|
||||
console.log(tableId, tableSchema);
|
||||
console.log(tableId, tableSettings, tableSchema);
|
||||
|
||||
const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
tableIdAtom,
|
||||
tableSettingsAtom,
|
||||
tableSchemaAtom,
|
||||
updateTableSchemaAtom,
|
||||
tableFiltersAtom,
|
||||
tableOrdersAtom,
|
||||
tablePageAtom,
|
||||
@@ -63,7 +64,11 @@ const TableSourceFirestore = memo(function TableSourceFirestore() {
|
||||
tableSchemaAtom,
|
||||
tableScope,
|
||||
isCollectionGroup ? TABLE_GROUP_SCHEMAS : TABLE_SCHEMAS,
|
||||
{ pathSegments: [tableId], createIfNonExistent: { columns: {} } }
|
||||
{
|
||||
pathSegments: [tableId],
|
||||
createIfNonExistent: { columns: {} },
|
||||
updateDataAtom: updateTableSchemaAtom,
|
||||
}
|
||||
);
|
||||
|
||||
// Get table filters and orders
|
||||
|
||||
24
src/types/table.d.ts
vendored
24
src/types/table.d.ts
vendored
@@ -5,15 +5,35 @@ import type {
|
||||
DocumentReference,
|
||||
} from "firebase/firestore";
|
||||
|
||||
/**
|
||||
* A standard function to update a doc in the database
|
||||
* @param update - The updates to be deeply merged with the existing doc. Note arrays should be ovewritten to match Firestore set with merge behavior
|
||||
* @param deleteFields - Optionally, fields to be deleted from the doc. Access nested fields with dot notation
|
||||
* @returns Promise when complete
|
||||
*/
|
||||
export type UpdateDocFunction<T = TableRow> = (
|
||||
update: Partial<T>
|
||||
update: Partial<T>,
|
||||
deleteFields?: string[]
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* A standard function to update a doc in a specific collection in the database
|
||||
* @param path - The full path to the doc
|
||||
* @param update - The updates to be deeply merged with the existing doc. Note arrays should be ovewritten to match Firestore set with merge behavior
|
||||
* @param deleteFields - Optionally, fields to be deleted from the doc. Access nested fields with dot notation
|
||||
* @returns Promise when complete
|
||||
*/
|
||||
export type UpdateCollectionDocFunction<T = TableRow> = (
|
||||
path: string,
|
||||
update: Partial<T>
|
||||
update: Partial<T>,
|
||||
deleteFields?: string[]
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* A standard function to delete a doc in a specific collection in the database
|
||||
* @param path - The full path to the doc
|
||||
* @returns Promise when complete
|
||||
*/
|
||||
export type DeleteCollectionDocFunction = (path: string) => Promise<void>;
|
||||
|
||||
/** Table settings stored in project settings */
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { mergeWith, isArray } from "lodash-es";
|
||||
import type { User } from "firebase/auth";
|
||||
import { TableRow } from "@src/types/table";
|
||||
|
||||
/**
|
||||
* Creates a standard user object to write to table rows
|
||||
@@ -32,10 +31,65 @@ export const rowyUser = (currentUser: User, data?: Record<string, any>) => {
|
||||
* @param update - The partial update to apply
|
||||
* @returns The row with updated values
|
||||
*/
|
||||
export const updateRowData = (row: TableRow, update: Partial<TableRow>) =>
|
||||
export const updateRowData = <T = Record<string, any>>(
|
||||
row: T,
|
||||
update: Partial<T>
|
||||
): T =>
|
||||
mergeWith(
|
||||
row,
|
||||
update,
|
||||
// If the proeprty to be merged is array, overwrite the array entirely
|
||||
(objValue, srcValue) => (isArray(objValue) ? srcValue : undefined)
|
||||
);
|
||||
|
||||
const ID_CHARACTERS =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
/**
|
||||
* Generate an ID compatible with Firestore
|
||||
* @param length - The length of the ID to generate
|
||||
* @returns - Generated ID
|
||||
*/
|
||||
export const generateId = (length: number = 20) => {
|
||||
let result = "";
|
||||
const charactersLength = ID_CHARACTERS.length;
|
||||
for (var i = 0; i < length; i++)
|
||||
result += ID_CHARACTERS.charAt(
|
||||
Math.floor(Math.random() * charactersLength)
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lexicographically decrement a given ID
|
||||
* @param id - The ID to decrement. If not provided, set to 20 `z` characters
|
||||
* @returns - The decremented ID
|
||||
*/
|
||||
export const decrementId = (id: string = "zzzzzzzzzzzzzzzzzzzz") => {
|
||||
const newId = id.split("");
|
||||
|
||||
// Loop through ID characters from the end
|
||||
let i = newId.length - 1;
|
||||
while (i > -1) {
|
||||
const newCharacterIndex = ID_CHARACTERS.indexOf(newId[i]) - 1;
|
||||
|
||||
newId[i] =
|
||||
ID_CHARACTERS[
|
||||
newCharacterIndex > -1 ? newCharacterIndex : ID_CHARACTERS.length - 1
|
||||
];
|
||||
|
||||
// If we don’t hit 0, we’re done
|
||||
if (newCharacterIndex > -1) break;
|
||||
|
||||
// Otherwise, if we hit 0, we need to decrement the next character
|
||||
i--;
|
||||
}
|
||||
|
||||
// Ensure we don't get 00...0, then the next ID would be 00...0z,
|
||||
// which would appear as the second row
|
||||
if (newId.every((x) => x === ID_CHARACTERS[0]))
|
||||
newId.push(ID_CHARACTERS[ID_CHARACTERS.length - 1]);
|
||||
|
||||
return newId.join("");
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user