mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
worked on array subtable
This commit is contained in:
9
src/assets/icons/ArraySubTable.tsx
Normal file
9
src/assets/icons/ArraySubTable.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
|
||||
export function ArraySubTable(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon {...props}>
|
||||
<path d="M1 4C1 2.34315 2.34315 1 4 1H18C19.6569 1 21 2.34315 21 4V11H19H12V15V17H4C2.34315 17 1 15.6569 1 14V4ZM10 15V11H3V14C3 14.5523 3.44772 15 4 15H10ZM12 9H19V5H12V9ZM10 5H3V9H10V5ZM15 13H14V14V22V23H15H17V21H16V15H17V13H15ZM21 13H22V14V22V23H21H19V21H20V15H19V13H21Z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -494,7 +494,11 @@ describe("deleteRow", () => {
|
||||
} = renderHook(() => useSetAtom(deleteRowAtom, tableScope));
|
||||
expect(deleteRow).toBeDefined();
|
||||
|
||||
await act(() => deleteRow(TEST_COLLECTION + "/row2"));
|
||||
await act(() =>
|
||||
deleteRow({
|
||||
path: TEST_COLLECTION + "/row2",
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current: tableRows },
|
||||
@@ -510,7 +514,11 @@ describe("deleteRow", () => {
|
||||
} = renderHook(() => useSetAtom(deleteRowAtom, tableScope));
|
||||
expect(deleteRow).toBeDefined();
|
||||
|
||||
await act(() => deleteRow(TEST_COLLECTION + "/rowLocal2"));
|
||||
await act(() =>
|
||||
deleteRow({
|
||||
path: TEST_COLLECTION + "/rowLocal2",
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current: tableRows },
|
||||
@@ -527,9 +535,9 @@ describe("deleteRow", () => {
|
||||
expect(deleteRow).toBeDefined();
|
||||
|
||||
await act(() =>
|
||||
deleteRow(
|
||||
["row1", "row2", "row8"].map((id) => TEST_COLLECTION + "/" + id)
|
||||
)
|
||||
deleteRow({
|
||||
path: ["row1", "row2", "row8"].map((id) => TEST_COLLECTION + "/" + id),
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -548,7 +556,11 @@ describe("deleteRow", () => {
|
||||
} = renderHook(() => useSetAtom(deleteRowAtom, tableScope));
|
||||
expect(deleteRow).toBeDefined();
|
||||
|
||||
await act(() => deleteRow(generatedRows.map((row) => row._rowy_ref.path)));
|
||||
await act(() =>
|
||||
deleteRow({
|
||||
path: generatedRows.map((row) => row._rowy_ref.path),
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current: tableRows },
|
||||
@@ -563,7 +575,11 @@ describe("deleteRow", () => {
|
||||
} = renderHook(() => useSetAtom(deleteRowAtom, tableScope));
|
||||
expect(deleteRow).toBeDefined();
|
||||
|
||||
await act(() => deleteRow("nonExistent"));
|
||||
await act(() =>
|
||||
deleteRow({
|
||||
path: "nonExistent",
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current: tableRows },
|
||||
@@ -578,7 +594,11 @@ describe("deleteRow", () => {
|
||||
} = renderHook(() => useSetAtom(deleteRowAtom, tableScope));
|
||||
expect(deleteRow).toBeDefined();
|
||||
|
||||
await act(() => deleteRow("nonExistent"));
|
||||
await act(() =>
|
||||
deleteRow({
|
||||
path: "nonExistent",
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
result: { current: tableRows },
|
||||
|
||||
@@ -22,7 +22,11 @@ import {
|
||||
_bulkWriteDbAtom,
|
||||
} from "./table";
|
||||
|
||||
import { TableRow, BulkWriteFunction } from "@src/types/table";
|
||||
import {
|
||||
TableRow,
|
||||
BulkWriteFunction,
|
||||
ArrayTableRowData,
|
||||
} from "@src/types/table";
|
||||
import {
|
||||
rowyUser,
|
||||
generateId,
|
||||
@@ -211,7 +215,17 @@ export const addRowAtom = atom(
|
||||
*/
|
||||
export const deleteRowAtom = atom(
|
||||
null,
|
||||
async (get, set, path: string | string[]) => {
|
||||
async (
|
||||
get,
|
||||
set,
|
||||
{
|
||||
path,
|
||||
options,
|
||||
}: {
|
||||
path: string | string[];
|
||||
options?: ArrayTableRowData;
|
||||
}
|
||||
) => {
|
||||
const deleteRowDb = get(_deleteRowDbAtom);
|
||||
if (!deleteRowDb) throw new Error("Cannot write to database");
|
||||
|
||||
@@ -223,9 +237,9 @@ export const deleteRowAtom = atom(
|
||||
find(tableRowsLocal, ["_rowy_ref.path", path])
|
||||
);
|
||||
if (isLocalRow) set(tableRowsLocalAtom, { type: "delete", path });
|
||||
|
||||
// Always delete from db in case it exists
|
||||
await deleteRowDb(path);
|
||||
// *options* are passed in case of array table to target specific row
|
||||
await deleteRowDb(path, options);
|
||||
if (auditChange) auditChange("DELETE_ROW", path);
|
||||
};
|
||||
|
||||
@@ -312,6 +326,8 @@ export interface IUpdateFieldOptions {
|
||||
useArrayUnion?: boolean;
|
||||
/** Optionally, uses firestore's arrayRemove with the given value. Removes given value items from the existing array */
|
||||
useArrayRemove?: boolean;
|
||||
/** Optionally, used to locate the row in ArraySubTable. */
|
||||
arrayTableData?: ArrayTableRowData;
|
||||
}
|
||||
/**
|
||||
* Set function updates or deletes a field in a row.
|
||||
@@ -339,6 +355,7 @@ export const updateFieldAtom = atom(
|
||||
disableCheckEquality,
|
||||
useArrayUnion,
|
||||
useArrayRemove,
|
||||
arrayTableData,
|
||||
}: IUpdateFieldOptions
|
||||
) => {
|
||||
const updateRowDb = get(_updateRowDbAtom);
|
||||
@@ -387,7 +404,12 @@ export const updateFieldAtom = atom(
|
||||
...(row[fieldName] ?? []),
|
||||
...localUpdate[fieldName],
|
||||
];
|
||||
dbUpdate[fieldName] = arrayUnion(...dbUpdate[fieldName]);
|
||||
// if we are updating a row of ArraySubTable
|
||||
if (arrayTableData?.index !== undefined) {
|
||||
dbUpdate[fieldName] = localUpdate[fieldName];
|
||||
} else {
|
||||
dbUpdate[fieldName] = arrayUnion(...dbUpdate[fieldName]);
|
||||
}
|
||||
}
|
||||
|
||||
//apply arrayRemove
|
||||
@@ -400,8 +422,15 @@ export const updateFieldAtom = atom(
|
||||
row[fieldName] ?? [],
|
||||
(el) => !find(localUpdate[fieldName], el)
|
||||
);
|
||||
dbUpdate[fieldName] = arrayRemove(...dbUpdate[fieldName]);
|
||||
|
||||
// if we are updating a row of ArraySubTable
|
||||
if (arrayTableData?.index !== undefined) {
|
||||
dbUpdate[fieldName] = localUpdate[fieldName];
|
||||
} else {
|
||||
dbUpdate[fieldName] = arrayRemove(...dbUpdate[fieldName]);
|
||||
}
|
||||
}
|
||||
// need to pass the index of the row to updateRowDb
|
||||
|
||||
// Check for required fields
|
||||
const newRowValues = updateRowData(cloneDeep(row), dbUpdate);
|
||||
@@ -431,7 +460,8 @@ export const updateFieldAtom = atom(
|
||||
await updateRowDb(
|
||||
row._rowy_ref.path,
|
||||
omitRowyFields(newRowValues),
|
||||
deleteField ? [fieldName] : []
|
||||
deleteField ? [fieldName] : [],
|
||||
arrayTableData
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -440,7 +470,8 @@ export const updateFieldAtom = atom(
|
||||
await updateRowDb(
|
||||
row._rowy_ref.path,
|
||||
omitRowyFields(dbUpdate),
|
||||
deleteField ? [fieldName] : []
|
||||
deleteField ? [fieldName] : [],
|
||||
arrayTableData
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ export type SelectedCell = {
|
||||
path: string | "_rowy_header";
|
||||
columnKey: string | "_rowy_row_actions";
|
||||
focusInside: boolean;
|
||||
arrayIndex?: number; // for array sub table
|
||||
};
|
||||
/** Store selected cell in table. Used in side drawer and context menu */
|
||||
export const selectedCellAtom = atom<SelectedCell | null>(null);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
projectSettingsAtom,
|
||||
rowyRunModalAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
import { tableScope, tableSettingsAtom } from "@src/atoms/tableScope";
|
||||
|
||||
export interface IFieldsDropdownProps {
|
||||
value: FieldType | "";
|
||||
@@ -35,17 +36,22 @@ export default function FieldsDropdown({
|
||||
}: IFieldsDropdownProps) {
|
||||
const [projectSettings] = useAtom(projectSettingsAtom, projectScope);
|
||||
const openRowyRunModal = useSetAtom(rowyRunModalAtom, projectScope);
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const fieldTypesToDisplay = optionsProp
|
||||
? FIELDS.filter((fieldConfig) => optionsProp.indexOf(fieldConfig.type) > -1)
|
||||
: FIELDS;
|
||||
const options = fieldTypesToDisplay.map((fieldConfig) => {
|
||||
const requireCloudFunctionSetup =
|
||||
fieldConfig.requireCloudFunction && !projectSettings.rowyRunUrl;
|
||||
const requireCollectionTable =
|
||||
tableSettings.isNotACollection === true &&
|
||||
fieldConfig.requireCollectionTable === true;
|
||||
return {
|
||||
label: fieldConfig.name,
|
||||
value: fieldConfig.type,
|
||||
disabled: requireCloudFunctionSetup,
|
||||
disabled: requireCloudFunctionSetup || requireCollectionTable,
|
||||
requireCloudFunctionSetup,
|
||||
requireCollectionTable,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -82,7 +88,18 @@ export default function FieldsDropdown({
|
||||
{getFieldProp("icon", option.value as FieldType)}
|
||||
</ListItemIcon>
|
||||
<Typography>{option.label}</Typography>
|
||||
{option.requireCloudFunctionSetup && (
|
||||
{option.requireCollectionTable ? (
|
||||
<Typography
|
||||
color="error"
|
||||
variant="inherit"
|
||||
component="span"
|
||||
marginLeft={1}
|
||||
className={"require-cloud-function"}
|
||||
>
|
||||
{" "}
|
||||
Unavailable
|
||||
</Typography>
|
||||
) : option.requireCloudFunctionSetup ? (
|
||||
<Typography
|
||||
color="error"
|
||||
variant="inherit"
|
||||
@@ -107,7 +124,7 @@ export default function FieldsDropdown({
|
||||
Cloud Function
|
||||
</span>
|
||||
</Typography>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
label={label || "Field type"}
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface IFieldWrapperProps {
|
||||
fieldName?: string;
|
||||
label?: React.ReactNode;
|
||||
debugText?: React.ReactNode;
|
||||
debugValue?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
index?: number;
|
||||
@@ -46,6 +47,7 @@ export default function FieldWrapper({
|
||||
fieldName,
|
||||
label,
|
||||
debugText,
|
||||
debugValue,
|
||||
disabled,
|
||||
hidden,
|
||||
index,
|
||||
@@ -100,7 +102,7 @@ export default function FieldWrapper({
|
||||
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
|
||||
<Suspense fallback={<FieldSkeleton />}>
|
||||
{children ??
|
||||
(!debugText && (
|
||||
(!debugValue && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
@@ -112,7 +114,7 @@ export default function FieldWrapper({
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
|
||||
{debugText && (
|
||||
{debugValue && (
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Typography
|
||||
variant="body2"
|
||||
@@ -131,7 +133,7 @@ export default function FieldWrapper({
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
copyToClipboard(debugText as string);
|
||||
copyToClipboard(debugValue as string);
|
||||
enqueueSnackbar("Copied!");
|
||||
}}
|
||||
>
|
||||
@@ -139,7 +141,7 @@ export default function FieldWrapper({
|
||||
</IconButton>
|
||||
<IconButton
|
||||
href={`https://console.firebase.google.com/project/${projectId}/firestore/data/~2F${(
|
||||
debugText as string
|
||||
debugValue as string
|
||||
).replace(/\//g, "~2F")}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { isEqual, isEmpty } from "lodash-es";
|
||||
import FieldWrapper from "./FieldWrapper";
|
||||
import { IFieldConfig } from "@src/components/fields/types";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { ColumnConfig, TableRowRef } from "@src/types/table";
|
||||
import { ArrayTableRowData, ColumnConfig, TableRowRef } from "@src/types/table";
|
||||
|
||||
export interface IMemoizedFieldProps {
|
||||
field: ColumnConfig;
|
||||
@@ -13,6 +13,7 @@ export interface IMemoizedFieldProps {
|
||||
hidden: boolean;
|
||||
value: any;
|
||||
_rowy_ref: TableRowRef;
|
||||
_rowy_arrayTableData?: ArrayTableRowData;
|
||||
isDirty: boolean;
|
||||
onDirty: (fieldName: string) => void;
|
||||
onSubmit: (fieldName: string, value: any) => void;
|
||||
@@ -25,6 +26,7 @@ export const MemoizedField = memo(
|
||||
hidden,
|
||||
value,
|
||||
_rowy_ref,
|
||||
_rowy_arrayTableData,
|
||||
isDirty,
|
||||
onDirty,
|
||||
onSubmit,
|
||||
@@ -78,6 +80,7 @@ export const MemoizedField = memo(
|
||||
},
|
||||
onSubmit: handleSubmit,
|
||||
disabled,
|
||||
_rowy_arrayTableData,
|
||||
})}
|
||||
</FieldWrapper>
|
||||
);
|
||||
|
||||
@@ -30,11 +30,21 @@ export default function SideDrawer() {
|
||||
|
||||
const [cell, setCell] = useAtom(selectedCellAtom, tableScope);
|
||||
const [open, setOpen] = useAtom(sideDrawerOpenAtom, tableScope);
|
||||
const selectedRow = find(tableRows, ["_rowy_ref.path", cell?.path]);
|
||||
const selectedCellRowIndex = findIndex(tableRows, [
|
||||
"_rowy_ref.path",
|
||||
cell?.path,
|
||||
]);
|
||||
const selectedRow = find(
|
||||
tableRows,
|
||||
cell?.arrayIndex === undefined
|
||||
? ["_rowy_ref.path", cell?.path]
|
||||
: // if the table is an array table, we need to use the array index to find the row
|
||||
["_rowy_arrayTableData.index", cell?.arrayIndex]
|
||||
);
|
||||
|
||||
const selectedCellRowIndex = findIndex(
|
||||
tableRows,
|
||||
cell?.arrayIndex === undefined
|
||||
? ["_rowy_ref.path", cell?.path]
|
||||
: // if the table is an array table, we need to use the array index to find the row
|
||||
["_rowy_arrayTableData.index", cell?.arrayIndex]
|
||||
);
|
||||
|
||||
const handleNavigate = (direction: "up" | "down") => () => {
|
||||
if (!tableRows || !cell) return;
|
||||
|
||||
@@ -66,7 +66,16 @@ export default function SideDrawerFields({ row }: ISideDrawerFieldsProps) {
|
||||
|
||||
setSaveState("saving");
|
||||
try {
|
||||
await updateField({ path: selectedCell!.path, fieldName, value });
|
||||
await updateField({
|
||||
path: selectedCell!.path,
|
||||
fieldName,
|
||||
value,
|
||||
deleteField: undefined,
|
||||
arrayTableData: {
|
||||
index: selectedCell.arrayIndex ?? 0,
|
||||
},
|
||||
});
|
||||
|
||||
setSaveState("saved");
|
||||
} catch (e) {
|
||||
enqueueSnackbar((e as Error).message, { variant: "error" });
|
||||
@@ -121,6 +130,7 @@ export default function SideDrawerFields({ row }: ISideDrawerFieldsProps) {
|
||||
onDirty={onDirty}
|
||||
onSubmit={onSubmit}
|
||||
isDirty={dirtyField === field.key}
|
||||
_rowy_arrayTableData={row._rowy_arrayTableData}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -128,7 +138,17 @@ export default function SideDrawerFields({ row }: ISideDrawerFieldsProps) {
|
||||
type="debug"
|
||||
fieldName="_rowy_ref.path"
|
||||
label="Document path"
|
||||
debugText={row._rowy_ref.path ?? row._rowy_ref.id ?? "No ref"}
|
||||
debugText={
|
||||
row._rowy_arrayTableData
|
||||
? row._rowy_ref.path +
|
||||
" → " +
|
||||
row._rowy_arrayTableData.parentField +
|
||||
"[" +
|
||||
row._rowy_arrayTableData.index +
|
||||
"]"
|
||||
: row._rowy_ref.path
|
||||
}
|
||||
debugValue={row._rowy_ref.path ?? row._rowy_ref.id ?? "No ref"}
|
||||
/>
|
||||
|
||||
{userDocHiddenFields.length > 0 && (
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
deleteRowAtom,
|
||||
updateFieldAtom,
|
||||
tableFiltersPopoverAtom,
|
||||
_updateRowDbAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
|
||||
@@ -54,6 +55,7 @@ export default function MenuContents({ onClose }: IMenuContentsProps) {
|
||||
const addRow = useSetAtom(addRowAtom, tableScope);
|
||||
const deleteRow = useSetAtom(deleteRowAtom, tableScope);
|
||||
const updateField = useSetAtom(updateFieldAtom, tableScope);
|
||||
const [updateRowDb] = useAtom(_updateRowDbAtom, tableScope);
|
||||
const openTableFiltersPopover = useSetAtom(
|
||||
tableFiltersPopoverAtom,
|
||||
tableScope
|
||||
@@ -62,19 +64,83 @@ export default function MenuContents({ onClose }: IMenuContentsProps) {
|
||||
if (!tableSchema.columns || !selectedCell) return null;
|
||||
|
||||
const selectedColumn = tableSchema.columns[selectedCell.columnKey];
|
||||
const row = find(tableRows, ["_rowy_ref.path", selectedCell.path]);
|
||||
const row = find(
|
||||
tableRows,
|
||||
selectedCell?.arrayIndex === undefined
|
||||
? ["_rowy_ref.path", selectedCell.path]
|
||||
: // if the table is an array table, we need to use the array index to find the row
|
||||
["_rowy_arrayTableData.index", selectedCell.arrayIndex]
|
||||
);
|
||||
|
||||
if (!row) return null;
|
||||
|
||||
const actionGroups: IContextMenuItem[][] = [];
|
||||
|
||||
const handleDuplicate = () => {
|
||||
addRow({
|
||||
row,
|
||||
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
|
||||
});
|
||||
const _duplicate = () => {
|
||||
if (row._rowy_arrayTableData !== undefined) {
|
||||
if (!updateRowDb) return;
|
||||
|
||||
return updateRowDb("", {}, undefined, {
|
||||
index: row._rowy_arrayTableData.index,
|
||||
operation: {
|
||||
addRow: "bottom",
|
||||
base: row,
|
||||
},
|
||||
});
|
||||
}
|
||||
return addRow({
|
||||
row: row,
|
||||
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
|
||||
});
|
||||
};
|
||||
|
||||
if (altPress || row._rowy_arrayTableData !== undefined) {
|
||||
_duplicate();
|
||||
} else {
|
||||
confirm({
|
||||
title: "Duplicate row?",
|
||||
body: (
|
||||
<>
|
||||
Row path:
|
||||
<br />
|
||||
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
|
||||
{row._rowy_ref.path}
|
||||
</code>
|
||||
</>
|
||||
),
|
||||
confirm: "Duplicate",
|
||||
handleConfirm: _duplicate,
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleDelete = () => {
|
||||
const _delete = () =>
|
||||
deleteRow({
|
||||
path: row._rowy_ref.path,
|
||||
options: row._rowy_arrayTableData,
|
||||
});
|
||||
|
||||
if (altPress || row._rowy_arrayTableData !== undefined) {
|
||||
_delete();
|
||||
} else {
|
||||
confirm({
|
||||
title: "Delete row?",
|
||||
body: (
|
||||
<>
|
||||
Row path:
|
||||
<br />
|
||||
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
|
||||
{row._rowy_ref.path}
|
||||
</code>
|
||||
</>
|
||||
),
|
||||
confirm: "Delete",
|
||||
confirmColor: "error",
|
||||
handleConfirm: _delete,
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleDelete = () => deleteRow(row._rowy_ref.path);
|
||||
const rowActions: IContextMenuItem[] = [
|
||||
{
|
||||
label: "Copy ID",
|
||||
@@ -112,51 +178,14 @@ export default function MenuContents({ onClose }: IMenuContentsProps) {
|
||||
disabled:
|
||||
tableSettings.tableType === "collectionGroup" ||
|
||||
(!userRoles.includes("ADMIN") && tableSettings.readOnly),
|
||||
onClick: altPress
|
||||
? handleDuplicate
|
||||
: () => {
|
||||
confirm({
|
||||
title: "Duplicate row?",
|
||||
body: (
|
||||
<>
|
||||
Row path:
|
||||
<br />
|
||||
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
|
||||
{row._rowy_ref.path}
|
||||
</code>
|
||||
</>
|
||||
),
|
||||
confirm: "Duplicate",
|
||||
handleConfirm: handleDuplicate,
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onClick: handleDuplicate,
|
||||
},
|
||||
{
|
||||
label: altPress ? "Delete" : "Delete…",
|
||||
color: "error",
|
||||
icon: <DeleteIcon />,
|
||||
disabled: !userRoles.includes("ADMIN") && tableSettings.readOnly,
|
||||
onClick: altPress
|
||||
? handleDelete
|
||||
: () => {
|
||||
confirm({
|
||||
title: "Delete row?",
|
||||
body: (
|
||||
<>
|
||||
Row path:
|
||||
<br />
|
||||
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
|
||||
{row._rowy_ref.path}
|
||||
</code>
|
||||
</>
|
||||
),
|
||||
confirm: "Delete",
|
||||
confirmColor: "error",
|
||||
handleConfirm: handleDelete,
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onClick: handleDelete,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function EmptyTable() {
|
||||
: false;
|
||||
let contents = <></>;
|
||||
|
||||
if (hasData) {
|
||||
if (!tableSettings.isNotACollection && hasData) {
|
||||
contents = (
|
||||
<>
|
||||
<div>
|
||||
@@ -72,47 +72,56 @@ export default function EmptyTable() {
|
||||
Get started
|
||||
</Typography>
|
||||
<Typography>
|
||||
There is no data in the Firestore collection:
|
||||
{tableSettings.isNotACollection === true
|
||||
? "There is no data in this Array Sub Table:"
|
||||
: "There is no data in the Firestore collection:"}
|
||||
<br />
|
||||
<code>{tableSettings.collection}</code>
|
||||
<code>
|
||||
{tableSettings.collection}
|
||||
{tableSettings.subTableKey?.length &&
|
||||
`.${tableSettings.subTableKey}`}
|
||||
</code>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs>
|
||||
<Typography paragraph>
|
||||
You can import data from an external source:
|
||||
</Typography>
|
||||
{!tableSettings.isNotACollection && (
|
||||
<>
|
||||
<Grid item xs>
|
||||
<Typography paragraph>
|
||||
You can import data from an external source:
|
||||
</Typography>
|
||||
|
||||
<ImportData
|
||||
render={(onClick) => (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<ImportIcon />}
|
||||
onClick={onClick}
|
||||
>
|
||||
Import data
|
||||
</Button>
|
||||
)}
|
||||
PopoverProps={{
|
||||
anchorOrigin: {
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<ImportData
|
||||
render={(onClick) => (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<ImportIcon />}
|
||||
onClick={onClick}
|
||||
>
|
||||
Import data
|
||||
</Button>
|
||||
)}
|
||||
PopoverProps={{
|
||||
anchorOrigin: {
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Divider orientation="vertical">
|
||||
<Typography variant="overline">or</Typography>
|
||||
</Divider>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Divider orientation="vertical">
|
||||
<Typography variant="overline">or</Typography>
|
||||
</Divider>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Grid item xs>
|
||||
<Typography paragraph>
|
||||
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
addRowAtom,
|
||||
deleteRowAtom,
|
||||
contextMenuTargetAtom,
|
||||
_updateRowDbAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
|
||||
export const FinalColumn = memo(function FinalColumn({
|
||||
row,
|
||||
focusInsideCell,
|
||||
@@ -31,17 +31,77 @@ export const FinalColumn = memo(function FinalColumn({
|
||||
const confirm = useSetAtom(confirmDialogAtom, projectScope);
|
||||
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const [updateRowDb] = useAtom(_updateRowDbAtom, tableScope);
|
||||
|
||||
const addRow = useSetAtom(addRowAtom, tableScope);
|
||||
const deleteRow = useSetAtom(deleteRowAtom, tableScope);
|
||||
const setContextMenuTarget = useSetAtom(contextMenuTargetAtom, tableScope);
|
||||
|
||||
const [altPress] = useAtom(altPressAtom, projectScope);
|
||||
const handleDelete = () => deleteRow(row.original._rowy_ref.path);
|
||||
|
||||
const handleDelete = () => {
|
||||
const _delete = () =>
|
||||
deleteRow({
|
||||
path: row.original._rowy_ref.path,
|
||||
options: row.original._rowy_arrayTableData,
|
||||
});
|
||||
if (altPress || row.original._rowy_arrayTableData !== undefined) {
|
||||
_delete();
|
||||
} else {
|
||||
confirm({
|
||||
title: "Delete row?",
|
||||
body: (
|
||||
<>
|
||||
Row path:
|
||||
<br />
|
||||
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
|
||||
{row.original._rowy_ref.path}
|
||||
</code>
|
||||
</>
|
||||
),
|
||||
confirm: "Delete",
|
||||
confirmColor: "error",
|
||||
handleConfirm: _delete,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicate = () => {
|
||||
addRow({
|
||||
row: row.original,
|
||||
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
|
||||
});
|
||||
const _duplicate = () => {
|
||||
if (row.original._rowy_arrayTableData !== undefined) {
|
||||
if (!updateRowDb) return;
|
||||
|
||||
return updateRowDb("", {}, undefined, {
|
||||
index: row.original._rowy_arrayTableData.index,
|
||||
operation: {
|
||||
addRow: "bottom",
|
||||
base: row.original,
|
||||
},
|
||||
});
|
||||
}
|
||||
return addRow({
|
||||
row: row.original,
|
||||
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
|
||||
});
|
||||
};
|
||||
if (altPress || row.original._rowy_arrayTableData !== undefined) {
|
||||
_duplicate();
|
||||
} else {
|
||||
confirm({
|
||||
title: "Duplicate row?",
|
||||
body: (
|
||||
<>
|
||||
Row path:
|
||||
<br />
|
||||
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
|
||||
{row.original._rowy_ref.path}
|
||||
</code>
|
||||
</>
|
||||
),
|
||||
confirm: "Duplicate",
|
||||
handleConfirm: _duplicate,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!userRoles.includes("ADMIN") && tableSettings.readOnly === true)
|
||||
@@ -73,28 +133,7 @@ export const FinalColumn = memo(function FinalColumn({
|
||||
size="small"
|
||||
color="inherit"
|
||||
disabled={tableSettings.tableType === "collectionGroup"}
|
||||
onClick={
|
||||
altPress
|
||||
? handleDuplicate
|
||||
: () => {
|
||||
confirm({
|
||||
title: "Duplicate row?",
|
||||
body: (
|
||||
<>
|
||||
Row path:
|
||||
<br />
|
||||
<code
|
||||
style={{ userSelect: "all", wordBreak: "break-all" }}
|
||||
>
|
||||
{row.original._rowy_ref.path}
|
||||
</code>
|
||||
</>
|
||||
),
|
||||
confirm: "Duplicate",
|
||||
handleConfirm: handleDuplicate,
|
||||
});
|
||||
}
|
||||
}
|
||||
onClick={handleDuplicate}
|
||||
className="row-hover-iconButton"
|
||||
tabIndex={focusInsideCell ? 0 : -1}
|
||||
>
|
||||
@@ -106,29 +145,7 @@ export const FinalColumn = memo(function FinalColumn({
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
onClick={
|
||||
altPress
|
||||
? handleDelete
|
||||
: () => {
|
||||
confirm({
|
||||
title: "Delete row?",
|
||||
body: (
|
||||
<>
|
||||
Row path:
|
||||
<br />
|
||||
<code
|
||||
style={{ userSelect: "all", wordBreak: "break-all" }}
|
||||
>
|
||||
{row.original._rowy_ref.path}
|
||||
</code>
|
||||
</>
|
||||
),
|
||||
confirm: "Delete",
|
||||
confirmColor: "error",
|
||||
handleConfirm: handleDelete,
|
||||
});
|
||||
}
|
||||
}
|
||||
onClick={handleDelete}
|
||||
className="row-hover-iconButton"
|
||||
tabIndex={focusInsideCell ? 0 : -1}
|
||||
sx={{
|
||||
|
||||
@@ -102,7 +102,10 @@ export const TableBody = memo(function TableBody({
|
||||
|
||||
const isSelectedCell =
|
||||
selectedCell?.path === row.original._rowy_ref.path &&
|
||||
selectedCell?.columnKey === cell.column.id;
|
||||
selectedCell?.columnKey === cell.column.id &&
|
||||
// if the table is an array sub table, we need to check the array index as well
|
||||
selectedCell?.arrayIndex ===
|
||||
row.original._rowy_arrayTableData?.index;
|
||||
|
||||
const fieldTypeGroup = getFieldProp(
|
||||
"group",
|
||||
|
||||
@@ -66,6 +66,7 @@ export default function EditorCellController({
|
||||
fieldName: props.column.fieldName,
|
||||
value: localValueRef.current,
|
||||
deleteField: localValueRef.current === undefined,
|
||||
arrayTableData: props.row?._rowy_arrayTableData,
|
||||
});
|
||||
} catch (e) {
|
||||
enqueueSnackbar((e as Error).message, { variant: "error" });
|
||||
|
||||
@@ -123,6 +123,7 @@ export const TableCell = memo(function TableCell({
|
||||
focusInsideCell,
|
||||
setFocusInsideCell: (focusInside: boolean) =>
|
||||
setSelectedCell({
|
||||
arrayIndex: row.original._rowy_arrayTableData?.index,
|
||||
path: row.original._rowy_ref.path,
|
||||
columnKey: cell.column.id,
|
||||
focusInside,
|
||||
@@ -166,6 +167,7 @@ export const TableCell = memo(function TableCell({
|
||||
}}
|
||||
onClick={(e) => {
|
||||
setSelectedCell({
|
||||
arrayIndex: row.original._rowy_arrayTableData?.index,
|
||||
path: row.original._rowy_ref.path,
|
||||
columnKey: cell.column.id,
|
||||
focusInside: false,
|
||||
@@ -174,6 +176,7 @@ export const TableCell = memo(function TableCell({
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
setSelectedCell({
|
||||
arrayIndex: row.original._rowy_arrayTableData?.index,
|
||||
path: row.original._rowy_ref.path,
|
||||
columnKey: cell.column.id,
|
||||
focusInside: true,
|
||||
@@ -183,6 +186,7 @@ export const TableCell = memo(function TableCell({
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectedCell({
|
||||
arrayIndex: row.original._rowy_arrayTableData?.index,
|
||||
path: row.original._rowy_ref.path,
|
||||
columnKey: cell.column.id,
|
||||
focusInside: false,
|
||||
|
||||
@@ -128,6 +128,11 @@ export function useKeyboardNavigation({
|
||||
? tableRows[newRowIndex]._rowy_ref.path
|
||||
: "_rowy_header",
|
||||
columnKey: leafColumns[newColIndex].id! || leafColumns[0].id!,
|
||||
arrayIndex:
|
||||
newRowIndex > -1
|
||||
? tableRows[newRowIndex]._rowy_arrayTableData?.index
|
||||
: undefined,
|
||||
|
||||
// When selected cell changes, exit current cell
|
||||
focusInside: false,
|
||||
};
|
||||
|
||||
@@ -71,6 +71,9 @@ export function useMenuAction(
|
||||
fieldName: selectedCol.fieldName,
|
||||
value: undefined,
|
||||
deleteField: true,
|
||||
arrayTableData: {
|
||||
index: selectedCell.arrayIndex ?? 0,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueSnackbar(`Failed to cut: ${error}`, { variant: "error" });
|
||||
@@ -115,6 +118,9 @@ export function useMenuAction(
|
||||
path: selectedCell.path,
|
||||
fieldName: selectedCol.fieldName,
|
||||
value: parsed,
|
||||
arrayTableData: {
|
||||
index: selectedCell.arrayIndex ?? 0,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueSnackbar(
|
||||
@@ -130,7 +136,14 @@ export function useMenuAction(
|
||||
const selectedCol = tableSchema.columns?.[selectedCell.columnKey];
|
||||
if (!selectedCol) return setCellValue("");
|
||||
setSelectedCol(selectedCol);
|
||||
const selectedRow = find(tableRows, ["_rowy_ref.path", selectedCell.path]);
|
||||
|
||||
const selectedRow = find(
|
||||
tableRows,
|
||||
selectedCell.arrayIndex === undefined
|
||||
? ["_rowy_ref.path", selectedCell.path]
|
||||
: // if the table is an array table, we need to use the array index to find the row
|
||||
["_rowy_arrayTableData.index", selectedCell.arrayIndex]
|
||||
);
|
||||
setCellValue(get(selectedRow, selectedCol.fieldName));
|
||||
}, [selectedCell, tableSchema, tableRows]);
|
||||
|
||||
@@ -149,7 +162,7 @@ export function useMenuAction(
|
||||
}
|
||||
};
|
||||
},
|
||||
[selectedCol]
|
||||
[enqueueSnackbar, selectedCol?.type]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
tableFiltersAtom,
|
||||
tableSortsAtom,
|
||||
addRowAtom,
|
||||
_updateRowDbAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
|
||||
export default function AddRow() {
|
||||
@@ -207,3 +208,88 @@ export default function AddRow() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function AddRowArraySubTable() {
|
||||
const [updateRowDb] = useAtom(_updateRowDbAtom, tableScope);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const anchorEl = useRef<HTMLDivElement>(null);
|
||||
const [addRowAt, setAddNewRowAt] = useState<"top" | "bottom">("bottom");
|
||||
if (!updateRowDb) return null;
|
||||
|
||||
const handleClick = () => {
|
||||
updateRowDb("", {}, undefined, {
|
||||
index: 0,
|
||||
operation: {
|
||||
addRow: addRowAt,
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup
|
||||
variant="contained"
|
||||
color="primary"
|
||||
aria-label="Split button"
|
||||
ref={anchorEl}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleClick}
|
||||
startIcon={addRowAt === "top" ? <AddRowTopIcon /> : <AddRowIcon />}
|
||||
>
|
||||
Add row to {addRowAt}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
aria-label="Select row add position"
|
||||
aria-haspopup="menu"
|
||||
style={{ padding: 0 }}
|
||||
onClick={() => setOpen(true)}
|
||||
id="add-row-menu-button"
|
||||
aria-controls={open ? "add-row-menu" : undefined}
|
||||
aria-expanded={open ? "true" : "false"}
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<Select
|
||||
id="add-row-menu"
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
label="Row add position"
|
||||
style={{ display: "none" }}
|
||||
value={addRowAt}
|
||||
onChange={(e) => setAddNewRowAt(e.target.value as typeof addRowAt)}
|
||||
MenuProps={{
|
||||
anchorEl: anchorEl.current,
|
||||
MenuListProps: { "aria-labelledby": "add-row-menu-button" },
|
||||
anchorOrigin: { horizontal: "left", vertical: "bottom" },
|
||||
transformOrigin: { horizontal: "left", vertical: "top" },
|
||||
}}
|
||||
>
|
||||
<MenuItem value="top">
|
||||
<ListItemText
|
||||
primary="To top"
|
||||
secondary="Adds a new row to the top of this table"
|
||||
secondaryTypographyProps={{ variant: "caption" }}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem value="bottom">
|
||||
<ListItemText
|
||||
primary="To bottom"
|
||||
secondary={"Adds a new row to the bottom of this table"}
|
||||
secondaryTypographyProps={{
|
||||
variant: "caption",
|
||||
whiteSpace: "pre-line",
|
||||
}}
|
||||
/>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
|
||||
import { Stack } from "@mui/material";
|
||||
import { Button, Stack } from "@mui/material";
|
||||
import WebhookIcon from "@mui/icons-material/Webhook";
|
||||
import {
|
||||
Export as ExportIcon,
|
||||
Extension as ExtensionIcon,
|
||||
CloudLogs as CloudLogsIcon,
|
||||
Import as ImportIcon,
|
||||
} from "@src/assets/icons";
|
||||
|
||||
import TableToolbarButton from "./TableToolbarButton";
|
||||
import { ButtonSkeleton } from "./TableToolbarSkeleton";
|
||||
|
||||
import AddRow from "./AddRow";
|
||||
import AddRow, { AddRowArraySubTable } from "./AddRow";
|
||||
import LoadedRowsStatus from "./LoadedRowsStatus";
|
||||
import TableSettings from "./TableSettings";
|
||||
import HiddenFields from "./HiddenFields";
|
||||
@@ -32,6 +34,8 @@ import {
|
||||
tableModalAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { TableToolsType } from "@src/types/table";
|
||||
import FilterIcon from "@mui/icons-material/FilterList";
|
||||
|
||||
// prettier-ignore
|
||||
const Filters = lazy(() => import("./Filters" /* webpackChunkName: "Filters" */));
|
||||
@@ -43,7 +47,11 @@ const ReExecute = lazy(() => import("./ReExecute" /* webpackChunkName: "ReExecut
|
||||
|
||||
export const TABLE_TOOLBAR_HEIGHT = 44;
|
||||
|
||||
export default function TableToolbar() {
|
||||
export default function TableToolbar({
|
||||
disabledTools,
|
||||
}: {
|
||||
disabledTools?: TableToolsType[];
|
||||
}) {
|
||||
const [projectSettings] = useAtom(projectSettingsAtom, projectScope);
|
||||
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
||||
const [compatibleRowyRunVersion] = useAtom(
|
||||
@@ -54,7 +62,6 @@ export default function TableToolbar() {
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
||||
const openTableModal = useSetAtom(tableModalAtom, tableScope);
|
||||
|
||||
const hasDerivatives =
|
||||
Object.values(tableSchema.columns ?? {}).filter(
|
||||
(column) => column.type === FieldType.derivative
|
||||
@@ -64,6 +71,7 @@ export default function TableToolbar() {
|
||||
tableSchema.compiledExtension &&
|
||||
tableSchema.compiledExtension.replace(/\W/g, "")?.length > 0;
|
||||
|
||||
disabledTools = disabledTools ?? [];
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -87,27 +95,47 @@ export default function TableToolbar() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AddRow />
|
||||
{tableSettings.isNotACollection ? <AddRowArraySubTable /> : <AddRow />}
|
||||
<div /> {/* Spacer */}
|
||||
<HiddenFields />
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
<Filters />
|
||||
</Suspense>
|
||||
{tableSettings.isNotACollection ? (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<FilterIcon />}
|
||||
disabled={true}
|
||||
>
|
||||
Filter
|
||||
</Button>
|
||||
) : (
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
<Filters />
|
||||
</Suspense>
|
||||
)}
|
||||
<div /> {/* Spacer */}
|
||||
<LoadedRowsStatus />
|
||||
<div style={{ flexGrow: 1, minWidth: 64 }} />
|
||||
<RowHeight />
|
||||
<div /> {/* Spacer */}
|
||||
{tableSettings.tableType !== "collectionGroup" && (
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
<ImportData />
|
||||
</Suspense>
|
||||
{disabledTools.includes("import") ? (
|
||||
<TableToolbarButton
|
||||
title="Import data"
|
||||
icon={<ImportIcon />}
|
||||
disabled={true}
|
||||
/>
|
||||
) : (
|
||||
tableSettings.tableType !== "collectionGroup" && (
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
<ImportData />
|
||||
</Suspense>
|
||||
)
|
||||
)}
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
<TableToolbarButton
|
||||
title="Export/Download"
|
||||
onClick={() => openTableModal("export")}
|
||||
icon={<ExportIcon />}
|
||||
disabled={disabledTools.includes("export")}
|
||||
/>
|
||||
</Suspense>
|
||||
{userRoles.includes("ADMIN") && (
|
||||
@@ -123,6 +151,7 @@ export default function TableToolbar() {
|
||||
}
|
||||
}}
|
||||
icon={<WebhookIcon />}
|
||||
disabled={disabledTools.includes("webhooks")}
|
||||
/>
|
||||
<TableToolbarButton
|
||||
title="Extensions"
|
||||
@@ -131,6 +160,7 @@ export default function TableToolbar() {
|
||||
else openRowyRunModal({ feature: "Extensions" });
|
||||
}}
|
||||
icon={<ExtensionIcon />}
|
||||
disabled={disabledTools.includes("extensions")}
|
||||
/>
|
||||
<TableToolbarButton
|
||||
title="Cloud logs"
|
||||
@@ -139,6 +169,7 @@ export default function TableToolbar() {
|
||||
if (projectSettings.rowyRunUrl) openTableModal("cloudLogs");
|
||||
else openRowyRunModal({ feature: "Cloud logs" });
|
||||
}}
|
||||
disabled={disabledTools.includes("cloud_logs")}
|
||||
/>
|
||||
{(hasDerivatives || hasExtensions) && (
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
|
||||
@@ -31,6 +31,7 @@ export const config: IFieldConfig = {
|
||||
settings: Settings,
|
||||
requireConfiguration: true,
|
||||
requireCloudFunction: true,
|
||||
requireCollectionTable: true,
|
||||
sortKey: "status",
|
||||
};
|
||||
export default config;
|
||||
|
||||
46
src/components/fields/ArraySubTable/DisplayCell.tsx
Normal file
46
src/components/fields/ArraySubTable/DisplayCell.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { IDisplayCellProps } from "@src/components/fields/types";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Stack, IconButton } from "@mui/material";
|
||||
import OpenIcon from "@mui/icons-material/OpenInBrowser";
|
||||
|
||||
import { useSubTableData } from "./utils";
|
||||
|
||||
export default function ArraySubTable({
|
||||
column,
|
||||
row,
|
||||
_rowy_ref,
|
||||
tabIndex,
|
||||
}: IDisplayCellProps) {
|
||||
const { documentCount, label, subTablePath } = useSubTableData(
|
||||
column as any,
|
||||
row,
|
||||
_rowy_ref
|
||||
);
|
||||
|
||||
if (!_rowy_ref) return null;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
style={{ paddingLeft: "var(--cell-padding)", width: "100%" }}
|
||||
>
|
||||
<div style={{ flexGrow: 1, overflow: "hidden" }}>
|
||||
{documentCount} {column.name as string}: {label}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={subTablePath}
|
||||
className="row-hover-iconButton end"
|
||||
size="small"
|
||||
disabled={!subTablePath}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<OpenIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
32
src/components/fields/ArraySubTable/Settings.tsx
Normal file
32
src/components/fields/ArraySubTable/Settings.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { ISettingsProps } from "@src/components/fields/types";
|
||||
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
|
||||
import { tableScope, tableColumnsOrderedAtom } from "@src/atoms/tableScope";
|
||||
|
||||
const Settings = ({ config, onChange }: ISettingsProps) => {
|
||||
const [tableOrderedColumns] = useAtom(tableColumnsOrderedAtom, tableScope);
|
||||
|
||||
const columnOptions = tableOrderedColumns
|
||||
.filter((column) =>
|
||||
[
|
||||
FieldType.shortText,
|
||||
FieldType.singleSelect,
|
||||
FieldType.email,
|
||||
FieldType.phone,
|
||||
].includes(column.type)
|
||||
)
|
||||
.map((c) => ({ label: c.name, value: c.key }));
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
label="Parent label"
|
||||
options={columnOptions}
|
||||
value={config.parentLabel ?? []}
|
||||
onChange={onChange("parentLabel")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default Settings;
|
||||
56
src/components/fields/ArraySubTable/SideDrawerField.tsx
Normal file
56
src/components/fields/ArraySubTable/SideDrawerField.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useMemo } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import { find, isEqual } from "lodash-es";
|
||||
import { ISideDrawerFieldProps } from "@src/components/fields/types";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Box, Stack, IconButton } from "@mui/material";
|
||||
import OpenIcon from "@mui/icons-material/OpenInBrowser";
|
||||
|
||||
import { tableScope, tableRowsAtom } from "@src/atoms/tableScope";
|
||||
import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils";
|
||||
import { useSubTableData } from "./utils";
|
||||
|
||||
export default function ArraySubTable({
|
||||
column,
|
||||
_rowy_ref,
|
||||
}: ISideDrawerFieldProps) {
|
||||
const [row] = useAtom(
|
||||
useMemo(
|
||||
() =>
|
||||
selectAtom(
|
||||
tableRowsAtom,
|
||||
(tableRows) => find(tableRows, ["_rowy_ref.path", _rowy_ref.path]),
|
||||
isEqual
|
||||
),
|
||||
[_rowy_ref.path]
|
||||
),
|
||||
tableScope
|
||||
);
|
||||
|
||||
const { documentCount, label, subTablePath } = useSubTableData(
|
||||
column as any,
|
||||
row as any,
|
||||
_rowy_ref
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack direction="row" id={getFieldId(column.key)}>
|
||||
<Box sx={fieldSx}>
|
||||
{documentCount} {column.name as string}: {label}
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={subTablePath}
|
||||
edge="end"
|
||||
size="small"
|
||||
sx={{ ml: 1 }}
|
||||
disabled={!subTablePath}
|
||||
>
|
||||
<OpenIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
36
src/components/fields/ArraySubTable/index.tsx
Normal file
36
src/components/fields/ArraySubTable/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { lazy } from "react";
|
||||
import { IFieldConfig, FieldType } from "@src/components/fields/types";
|
||||
import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell";
|
||||
|
||||
import { ArraySubTable as ArraySubTableIcon } from "@src/assets/icons/ArraySubTable";
|
||||
import DisplayCell from "./DisplayCell";
|
||||
|
||||
const SideDrawerField = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./SideDrawerField" /* webpackChunkName: "SideDrawerField-ArraySubTable" */
|
||||
)
|
||||
);
|
||||
const Settings = lazy(
|
||||
() => import("./Settings" /* webpackChunkName: "Settings-ArraySubtable" */)
|
||||
);
|
||||
export const config: IFieldConfig = {
|
||||
type: FieldType.arraySubTable,
|
||||
name: "Array-Sub-Table",
|
||||
group: "Connection",
|
||||
dataType: "undefined",
|
||||
initialValue: null,
|
||||
icon: <ArraySubTableIcon />,
|
||||
settings: Settings,
|
||||
description:
|
||||
"Connects to a sub-table in the current row. Also displays number of rows inside the sub-table. Max sub-table depth: 100.",
|
||||
TableCell: withRenderTableCell(DisplayCell, null, "focus", {
|
||||
usesRowData: true,
|
||||
disablePadding: true,
|
||||
}),
|
||||
SideDrawerField,
|
||||
initializable: false,
|
||||
requireConfiguration: true,
|
||||
requireCollectionTable: true,
|
||||
};
|
||||
export default config;
|
||||
34
src/components/fields/ArraySubTable/utils.ts
Normal file
34
src/components/fields/ArraySubTable/utils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import { ColumnConfig, TableRow, TableRowRef } from "@src/types/table";
|
||||
|
||||
export const useSubTableData = (
|
||||
column: ColumnConfig,
|
||||
row: TableRow,
|
||||
_rowy_ref: TableRowRef
|
||||
) => {
|
||||
const label = (column.config?.parentLabel ?? []).reduce((acc, curr) => {
|
||||
if (acc !== "") return `${acc} - ${row[curr]}`;
|
||||
else return row[curr];
|
||||
}, "");
|
||||
|
||||
const documentCount: string = row[column.fieldName]?.count ?? "";
|
||||
|
||||
const location = useLocation();
|
||||
const rootTablePath = decodeURIComponent(
|
||||
location.pathname.split("/" + ROUTES.subTable)[0]
|
||||
);
|
||||
|
||||
// Get params from URL: /table/:tableId/arraySubTable/:docPath/:arraySubTableKey
|
||||
let subTablePath = [
|
||||
rootTablePath,
|
||||
ROUTES.arraySubTable,
|
||||
encodeURIComponent(_rowy_ref.path),
|
||||
column.key,
|
||||
].join("/");
|
||||
|
||||
subTablePath += "?parentLabel=" + encodeURIComponent(label ?? "");
|
||||
|
||||
return { documentCount, label, subTablePath };
|
||||
};
|
||||
@@ -27,5 +27,6 @@ export const config: IFieldConfig = {
|
||||
TableCell: withRenderTableCell(DisplayCell, null),
|
||||
SideDrawerField,
|
||||
settings: Settings,
|
||||
requireCollectionTable: true,
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -28,5 +28,6 @@ export const config: IFieldConfig = {
|
||||
TableCell: withRenderTableCell(DisplayCell, null),
|
||||
SideDrawerField,
|
||||
settings: Settings,
|
||||
requireCollectionTable: true,
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -22,5 +22,6 @@ export const config: IFieldConfig = {
|
||||
settingsValidator,
|
||||
requireConfiguration: true,
|
||||
requireCloudFunction: true,
|
||||
requireCollectionTable: true,
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useCallback } from "react";
|
||||
import { IEditorCellProps } from "@src/components/fields/types";
|
||||
import { useSetAtom } from "jotai";
|
||||
|
||||
@@ -22,11 +21,17 @@ export default function File_({
|
||||
_rowy_ref,
|
||||
tabIndex,
|
||||
rowHeight,
|
||||
row: { _rowy_arrayTableData },
|
||||
}: IEditorCellProps) {
|
||||
const confirm = useSetAtom(confirmDialogAtom, projectScope);
|
||||
|
||||
const { loading, progress, handleDelete, localFiles, dropzoneState } =
|
||||
useFileUpload(_rowy_ref, column.key, { multiple: true });
|
||||
useFileUpload(
|
||||
_rowy_ref,
|
||||
column.key,
|
||||
{ multiple: true },
|
||||
_rowy_arrayTableData
|
||||
);
|
||||
|
||||
const { isDragActive, getRootProps, getInputProps } = dropzoneState;
|
||||
const dropzoneProps = getRootProps();
|
||||
|
||||
@@ -25,10 +25,16 @@ export default function File_({
|
||||
_rowy_ref,
|
||||
value,
|
||||
disabled,
|
||||
_rowy_arrayTableData,
|
||||
}: ISideDrawerFieldProps) {
|
||||
const confirm = useSetAtom(confirmDialogAtom, projectScope);
|
||||
const { loading, progress, handleDelete, localFiles, dropzoneState } =
|
||||
useFileUpload(_rowy_ref, column.key, { multiple: true });
|
||||
useFileUpload(
|
||||
_rowy_ref,
|
||||
column.key,
|
||||
{ multiple: true },
|
||||
_rowy_arrayTableData
|
||||
);
|
||||
|
||||
const { isDragActive, getRootProps, getInputProps } = dropzoneState;
|
||||
|
||||
|
||||
@@ -5,12 +5,17 @@ import { DropzoneOptions, useDropzone } from "react-dropzone";
|
||||
|
||||
import { tableScope, updateFieldAtom } from "@src/atoms/tableScope";
|
||||
import useUploader from "@src/hooks/useFirebaseStorageUploader";
|
||||
import type { FileValue, TableRowRef } from "@src/types/table";
|
||||
import type {
|
||||
ArrayTableRowData,
|
||||
FileValue,
|
||||
TableRowRef,
|
||||
} from "@src/types/table";
|
||||
|
||||
export default function useFileUpload(
|
||||
docRef: TableRowRef,
|
||||
fieldName: string,
|
||||
dropzoneOptions: DropzoneOptions = {}
|
||||
dropzoneOptions: DropzoneOptions = {},
|
||||
arrayTableData?: ArrayTableRowData
|
||||
) {
|
||||
const updateField = useSetAtom(updateFieldAtom, tableScope);
|
||||
const { uploaderState, upload, deleteUpload } = useUploader();
|
||||
@@ -47,7 +52,9 @@ export default function useFileUpload(
|
||||
async (files: File[]) => {
|
||||
const { uploads, failures } = await upload({
|
||||
docRef,
|
||||
fieldName,
|
||||
fieldName: arrayTableData
|
||||
? `${arrayTableData?.parentField}/${fieldName}`
|
||||
: fieldName,
|
||||
files,
|
||||
});
|
||||
updateField({
|
||||
@@ -55,10 +62,11 @@ export default function useFileUpload(
|
||||
fieldName,
|
||||
value: uploads,
|
||||
useArrayUnion: true,
|
||||
arrayTableData,
|
||||
});
|
||||
return { uploads, failures };
|
||||
},
|
||||
[docRef, fieldName, updateField, upload]
|
||||
[arrayTableData, docRef, fieldName, updateField, upload]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
@@ -69,10 +77,11 @@ export default function useFileUpload(
|
||||
value: [file],
|
||||
useArrayRemove: true,
|
||||
disableCheckEquality: true,
|
||||
arrayTableData,
|
||||
});
|
||||
deleteUpload(file);
|
||||
},
|
||||
[deleteUpload, docRef, fieldName, updateField]
|
||||
[arrayTableData, deleteUpload, docRef.path, fieldName, updateField]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { IEditorCellProps } from "@src/components/fields/types";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { assignIn } from "lodash-es";
|
||||
|
||||
import { alpha, Box, Stack, Grid, IconButton, ButtonBase } from "@mui/material";
|
||||
@@ -11,8 +11,6 @@ import Thumbnail from "@src/components/Thumbnail";
|
||||
import CircularProgressOptical from "@src/components/CircularProgressOptical";
|
||||
|
||||
import { projectScope, confirmDialogAtom } from "@src/atoms/projectScope";
|
||||
import { tableSchemaAtom, tableScope } from "@src/atoms/tableScope";
|
||||
import { DEFAULT_ROW_HEIGHT } from "@src/components/Table";
|
||||
import { FileValue } from "@src/types/table";
|
||||
import useFileUpload from "@src/components/fields/File/useFileUpload";
|
||||
import { IMAGE_MIME_TYPES } from "./index";
|
||||
@@ -25,14 +23,20 @@ export default function Image_({
|
||||
_rowy_ref,
|
||||
tabIndex,
|
||||
rowHeight,
|
||||
row: { _rowy_arrayTableData },
|
||||
}: IEditorCellProps) {
|
||||
const confirm = useSetAtom(confirmDialogAtom, projectScope);
|
||||
|
||||
const { loading, progress, handleDelete, localFiles, dropzoneState } =
|
||||
useFileUpload(_rowy_ref, column.key, {
|
||||
multiple: true,
|
||||
accept: IMAGE_MIME_TYPES,
|
||||
});
|
||||
useFileUpload(
|
||||
_rowy_ref,
|
||||
column.key,
|
||||
{
|
||||
multiple: true,
|
||||
accept: IMAGE_MIME_TYPES,
|
||||
},
|
||||
_rowy_arrayTableData
|
||||
);
|
||||
|
||||
const localImages = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -84,6 +84,7 @@ export default function Image_({
|
||||
_rowy_ref,
|
||||
value,
|
||||
disabled,
|
||||
_rowy_arrayTableData,
|
||||
}: ISideDrawerFieldProps) {
|
||||
const confirm = useSetAtom(confirmDialogAtom, projectScope);
|
||||
|
||||
@@ -94,10 +95,15 @@ export default function Image_({
|
||||
uploaderState,
|
||||
localFiles,
|
||||
dropzoneState,
|
||||
} = useFileUpload(_rowy_ref, column.key, {
|
||||
multiple: true,
|
||||
accept: IMAGE_MIME_TYPES,
|
||||
});
|
||||
} = useFileUpload(
|
||||
_rowy_ref,
|
||||
column.key,
|
||||
{
|
||||
multiple: true,
|
||||
accept: IMAGE_MIME_TYPES,
|
||||
},
|
||||
_rowy_arrayTableData
|
||||
);
|
||||
|
||||
const localImages = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -31,5 +31,6 @@ export const config: IFieldConfig = {
|
||||
SideDrawerField,
|
||||
initializable: false,
|
||||
requireConfiguration: true,
|
||||
requireCollectionTable: true,
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -28,5 +28,6 @@ export const config: IFieldConfig = {
|
||||
TableCell: withRenderTableCell(DisplayCell, null),
|
||||
SideDrawerField,
|
||||
settings: Settings,
|
||||
requireCollectionTable: true,
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -29,5 +29,6 @@ export const config: IFieldConfig = {
|
||||
TableCell: withRenderTableCell(DisplayCell, null),
|
||||
SideDrawerField,
|
||||
settings: Settings,
|
||||
requireCollectionTable: true,
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -26,6 +26,7 @@ import Image_ from "./Image";
|
||||
import File_ from "./File";
|
||||
import Connector from "./Connector";
|
||||
import SubTable from "./SubTable";
|
||||
import ArraySubTable from "./ArraySubTable";
|
||||
import Reference from "./Reference";
|
||||
import ConnectTable from "./ConnectTable";
|
||||
import ConnectService from "./ConnectService";
|
||||
@@ -74,6 +75,7 @@ export const FIELDS: IFieldConfig[] = [
|
||||
File_,
|
||||
/** CONNECTION */
|
||||
Connector,
|
||||
ArraySubTable,
|
||||
SubTable,
|
||||
Reference,
|
||||
ConnectTable,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
TableRow,
|
||||
TableRowRef,
|
||||
TableFilter,
|
||||
ArrayTableRowData,
|
||||
} from "@src/types/table";
|
||||
import type { SelectedCell } from "@src/atoms/tableScope";
|
||||
import type { IContextMenuItem } from "@src/components/Table/ContextMenu/ContextMenuItem";
|
||||
@@ -20,6 +21,7 @@ export interface IFieldConfig {
|
||||
initializable?: boolean;
|
||||
requireConfiguration?: boolean;
|
||||
requireCloudFunction?: boolean;
|
||||
requireCollectionTable?: boolean;
|
||||
initialValue: any;
|
||||
icon?: React.ReactNode;
|
||||
description?: string;
|
||||
@@ -80,7 +82,8 @@ export interface ISideDrawerFieldProps<T = any> {
|
||||
column: ColumnConfig;
|
||||
/** The row’s _rowy_ref object */
|
||||
_rowy_ref: TableRowRef;
|
||||
|
||||
/** The array table row’s data */
|
||||
_rowy_arrayTableData?: ArrayTableRowData;
|
||||
/** The field’s local value – synced with db when field is not dirty */
|
||||
value: T;
|
||||
/** Call when the user has input but changes have not been saved */
|
||||
|
||||
@@ -28,6 +28,7 @@ export enum FieldType {
|
||||
// CONNECTION
|
||||
connector = "CONNECTOR",
|
||||
subTable = "SUB_TABLE",
|
||||
arraySubTable = "ARRAY_SUB_TABLE",
|
||||
reference = "REFERENCE",
|
||||
connectTable = "DOCUMENT_SELECT",
|
||||
connectService = "SERVICE_SELECT",
|
||||
|
||||
@@ -25,8 +25,10 @@ export enum ROUTES {
|
||||
tableWithId = "/table/:id",
|
||||
/** Nested route: `/table/:id/subTable/...` */
|
||||
subTable = "subTable",
|
||||
arraySubTable = "arraySubTable",
|
||||
/** Nested route: `/table/:id/subTable/...` */
|
||||
subTableWithId = "subTable/:docPath/:subTableKey",
|
||||
arraySubTableWithId = "arraySubTable/:docPath/:subTableKey",
|
||||
/** @deprecated Redirects to /table */
|
||||
tableGroup = "/tableGroup",
|
||||
/** @deprecated Redirects to /table */
|
||||
|
||||
357
src/hooks/useFirestoreDocAsCollectionWithAtom.ts
Normal file
357
src/hooks/useFirestoreDocAsCollectionWithAtom.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import useMemoValue from "use-memo-value";
|
||||
import { useAtom, PrimitiveAtom, useSetAtom } from "jotai";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import {
|
||||
Firestore,
|
||||
doc,
|
||||
refEqual,
|
||||
onSnapshot,
|
||||
FirestoreError,
|
||||
setDoc,
|
||||
DocumentReference,
|
||||
} from "firebase/firestore";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
|
||||
import { projectScope } from "@src/atoms/projectScope";
|
||||
import {
|
||||
ArrayTableRowData,
|
||||
DeleteCollectionDocFunction,
|
||||
TableRow,
|
||||
TableSort,
|
||||
UpdateCollectionDocFunction,
|
||||
} from "@src/types/table";
|
||||
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
|
||||
import { omitRowyFields } from "@src/utils/table";
|
||||
|
||||
/** Options for {@link useFirestoreDocWithAtom} */
|
||||
interface IUseFirestoreDocWithAtomOptions<T> {
|
||||
/** Called when an error occurs. Make sure to wrap in useCallback! If not provided, errors trigger the nearest ErrorBoundary. */
|
||||
onError?: (error: FirestoreError) => void;
|
||||
/** Optionally disable Suspense */
|
||||
disableSuspense?: boolean;
|
||||
/** Optionally create the document if it doesn’t exist with the following data */
|
||||
createIfNonExistent?: T;
|
||||
/** Set this atom’s value to a function that updates the document. Uses same scope as `dataScope`. */
|
||||
// updateDataAtom?: PrimitiveAtom<UpdateDocFunction<T> | undefined>;
|
||||
updateDocAtom?: PrimitiveAtom<UpdateCollectionDocFunction<T> | undefined>;
|
||||
deleteDocAtom?: PrimitiveAtom<DeleteCollectionDocFunction | undefined>;
|
||||
sorts?: TableSort[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a listener for a Firestore document and unsubscribes on unmount.
|
||||
* Gets the Firestore instance initiated in projectScope.
|
||||
* Updates an atom and Suspends that atom until the first snapshot is received.
|
||||
*
|
||||
* @param dataAtom - Atom to store data in
|
||||
* @param dataScope - Atom scope
|
||||
* @param path - Document path. If falsy, the listener isn’t created at all.
|
||||
* @param fieldName - Parent field name
|
||||
* @param options - {@link IUseFirestoreDocWithAtomOptions}
|
||||
*/
|
||||
export function useFirestoreDocAsCollectionWithAtom<T = TableRow>(
|
||||
dataAtom: PrimitiveAtom<T[]>,
|
||||
dataScope: Parameters<typeof useAtom>[1] | undefined,
|
||||
path: string,
|
||||
fieldName: string,
|
||||
options: IUseFirestoreDocWithAtomOptions<T>
|
||||
) {
|
||||
// Destructure options so they can be used as useEffect dependencies
|
||||
const {
|
||||
onError,
|
||||
disableSuspense,
|
||||
createIfNonExistent,
|
||||
updateDocAtom,
|
||||
deleteDocAtom,
|
||||
sorts,
|
||||
} = options || {};
|
||||
|
||||
const [firebaseDb] = useAtom(firebaseDbAtom, projectScope);
|
||||
const setDataAtom = useSetAtom(dataAtom, dataScope);
|
||||
|
||||
const handleError = useErrorHandler();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const setUpdateDocAtom = useSetAtom(
|
||||
updateDocAtom || (dataAtom as any),
|
||||
dataScope
|
||||
);
|
||||
const setDeleteRowAtom = useSetAtom(
|
||||
deleteDocAtom || (dataAtom as any),
|
||||
dataScope
|
||||
);
|
||||
|
||||
// Create the doc ref and memoize using Firestore’s refEqual
|
||||
const memoizedDocRef = useMemoValue(
|
||||
getDocRef<T>(firebaseDb, path),
|
||||
(next, prev) => refEqual(next as any, prev as any)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// If path is invalid and no memoizedDocRef was created, don’t continue
|
||||
if (!memoizedDocRef) return;
|
||||
|
||||
// Suspend data atom until we get the first snapshot
|
||||
let suspended = false;
|
||||
if (!disableSuspense) {
|
||||
setDataAtom(new Promise(() => []) as unknown as T[]);
|
||||
suspended = true;
|
||||
}
|
||||
|
||||
// Create a listener for the document
|
||||
const unsubscribe = onSnapshot(
|
||||
memoizedDocRef,
|
||||
{ includeMetadataChanges: true },
|
||||
(docSnapshot) => {
|
||||
try {
|
||||
if (docSnapshot.exists() && docSnapshot.data() !== undefined) {
|
||||
const pseudoDoc = docSnapshot.get(fieldName) || [];
|
||||
const pseudoRow = pseudoDoc.map((row: any, i: number) => {
|
||||
return {
|
||||
...row,
|
||||
_rowy_ref: docSnapshot.ref,
|
||||
_rowy_arrayTableData: {
|
||||
index: i,
|
||||
parentField: fieldName,
|
||||
},
|
||||
};
|
||||
});
|
||||
const sorted = sortRows<T>(pseudoRow, sorts);
|
||||
setDataAtom(sorted);
|
||||
} else {
|
||||
enqueueSnackbar(`Array table doesn't exist`, {
|
||||
variant: "error",
|
||||
});
|
||||
// console.log("docSnapshot", docSnapshot.data());
|
||||
// setDataAtom([] as T[]);
|
||||
}
|
||||
} catch (error) {
|
||||
if (onError) onError(error as FirestoreError);
|
||||
else handleError(error);
|
||||
}
|
||||
suspended = false;
|
||||
},
|
||||
(error) => {
|
||||
if (suspended) setDataAtom([] as T[]);
|
||||
if (onError) onError(error);
|
||||
else handleError(error);
|
||||
}
|
||||
);
|
||||
|
||||
// When the listener will change, unsubscribe
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [
|
||||
memoizedDocRef,
|
||||
onError,
|
||||
setDataAtom,
|
||||
disableSuspense,
|
||||
createIfNonExistent,
|
||||
handleError,
|
||||
fieldName,
|
||||
sorts,
|
||||
enqueueSnackbar,
|
||||
]);
|
||||
|
||||
const setRows = useCallback(
|
||||
(rows: T[]) => {
|
||||
rows = rows.map((row: any, i: number) => omitRowyFields(row));
|
||||
if (!fieldName) return;
|
||||
try {
|
||||
return setDoc(
|
||||
doc(firebaseDb, path),
|
||||
{ [fieldName]: rows },
|
||||
{ merge: true }
|
||||
);
|
||||
} catch (error) {
|
||||
enqueueSnackbar(`Error updating array table`, {
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
[enqueueSnackbar, fieldName, firebaseDb, path]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteDocAtom) {
|
||||
setDeleteRowAtom(() => (_: string, options?: ArrayTableRowData) => {
|
||||
if (!options) return;
|
||||
|
||||
const deleteRow = () => {
|
||||
let temp: T[] = [];
|
||||
setDataAtom((prevData) => {
|
||||
temp = unsortRows<T>(prevData);
|
||||
temp.splice(options.index, 1);
|
||||
for (let i = options.index; i < temp.length; i++) {
|
||||
// @ts-ignore
|
||||
temp[i]._rowy_arrayTableData.index = i;
|
||||
}
|
||||
return sortRows(temp, sorts);
|
||||
});
|
||||
return setRows(temp);
|
||||
};
|
||||
deleteRow();
|
||||
});
|
||||
}
|
||||
}, [
|
||||
deleteDocAtom,
|
||||
firebaseDb,
|
||||
path,
|
||||
setDataAtom,
|
||||
setDeleteRowAtom,
|
||||
setRows,
|
||||
sorts,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (updateDocAtom) {
|
||||
setUpdateDocAtom(
|
||||
() =>
|
||||
(
|
||||
path_: string,
|
||||
update: T,
|
||||
deleteFields?: string[],
|
||||
options?: ArrayTableRowData
|
||||
) => {
|
||||
if (options === undefined) return;
|
||||
|
||||
const deleteRowFields = () => {
|
||||
let temp: T[] = [];
|
||||
setDataAtom((prevData) => {
|
||||
temp = unsortRows(prevData);
|
||||
|
||||
if (deleteFields === undefined) return prevData;
|
||||
|
||||
temp[options.index] = {
|
||||
...temp[options.index],
|
||||
...deleteFields?.reduce(
|
||||
(acc, field) => ({ ...acc, [field]: undefined }),
|
||||
{}
|
||||
),
|
||||
};
|
||||
|
||||
return sortRows(temp, sorts);
|
||||
});
|
||||
|
||||
return setRows(temp);
|
||||
};
|
||||
|
||||
const updateRowValues = () => {
|
||||
let temp: T[] = [];
|
||||
setDataAtom((prevData) => {
|
||||
temp = unsortRows(prevData);
|
||||
|
||||
temp[options.index] = {
|
||||
...temp[options.index],
|
||||
...update,
|
||||
};
|
||||
return sortRows(temp, sorts);
|
||||
});
|
||||
return setRows(temp);
|
||||
};
|
||||
|
||||
const addNewRow = (addTo: "top" | "bottom", base?: TableRow) => {
|
||||
let temp: T[] = [];
|
||||
|
||||
const newRow = (i: number) =>
|
||||
({
|
||||
...base,
|
||||
_rowy_ref: {
|
||||
id: doc(firebaseDb, path).id,
|
||||
path: doc(firebaseDb, path).path,
|
||||
},
|
||||
_rowy_arrayTableData: {
|
||||
index: i,
|
||||
parentField: fieldName,
|
||||
},
|
||||
} as T);
|
||||
|
||||
setDataAtom((prevData) => {
|
||||
temp = unsortRows(prevData);
|
||||
|
||||
if (addTo === "bottom") {
|
||||
temp.push(newRow(prevData.length));
|
||||
} else {
|
||||
const modifiedPrevData = temp.map((row: any, i: number) => {
|
||||
return {
|
||||
...row,
|
||||
_rowy_arrayTableData: {
|
||||
index: i + 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
temp = [newRow(0), ...modifiedPrevData];
|
||||
}
|
||||
return sortRows(temp, sorts);
|
||||
});
|
||||
|
||||
return setRows(temp);
|
||||
};
|
||||
|
||||
if (Array.isArray(deleteFields) && deleteFields.length > 0) {
|
||||
return deleteRowFields();
|
||||
} else if (options.operation?.addRow) {
|
||||
return addNewRow(
|
||||
options.operation.addRow,
|
||||
options?.operation.base
|
||||
);
|
||||
} else {
|
||||
return updateRowValues();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [
|
||||
fieldName,
|
||||
firebaseDb,
|
||||
path,
|
||||
setDataAtom,
|
||||
setRows,
|
||||
setUpdateDocAtom,
|
||||
sorts,
|
||||
updateDocAtom,
|
||||
]);
|
||||
}
|
||||
|
||||
export default useFirestoreDocAsCollectionWithAtom;
|
||||
|
||||
/**
|
||||
* Create the Firestore document reference.
|
||||
* Put code in a function so the results can be compared by useMemoValue.
|
||||
*/
|
||||
export const getDocRef = <T>(
|
||||
firebaseDb: Firestore,
|
||||
path: string | undefined,
|
||||
pathSegments?: Array<string | undefined>
|
||||
) => {
|
||||
if (!path || (Array.isArray(pathSegments) && pathSegments?.some((x) => !x)))
|
||||
return null;
|
||||
|
||||
return doc(
|
||||
firebaseDb,
|
||||
path,
|
||||
...((pathSegments as string[]) || [])
|
||||
) as DocumentReference<T>;
|
||||
};
|
||||
|
||||
function sortRows<T = TableRow>(
|
||||
rows: T[],
|
||||
sorts: TableSort[] | undefined
|
||||
): T[] {
|
||||
if (sorts === undefined || sorts.length < 1) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const order: "asc" | "desc" =
|
||||
sorts[0].direction === undefined ? "asc" : sorts[0].direction;
|
||||
|
||||
return orderBy(rows, [sorts[0].key], [order]);
|
||||
}
|
||||
|
||||
function unsortRows<T = TableRow>(rows: T[]): T[] {
|
||||
return orderBy(rows, ["_rowy_arrayTableData.index"], ["asc"]);
|
||||
}
|
||||
156
src/pages/Table/ProvidedArraySubTablePage.tsx
Normal file
156
src/pages/Table/ProvidedArraySubTablePage.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { lazy, Suspense, useMemo } from "react";
|
||||
import { useAtom, Provider } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import { DebugAtoms } from "@src/atoms/utils";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { find, isEqual } from "lodash-es";
|
||||
|
||||
import Modal from "@src/components/Modal";
|
||||
import BreadcrumbsSubTable from "@src/components/Table/Breadcrumbs/BreadcrumbsSubTable";
|
||||
import ErrorFallback from "@src/components/ErrorFallback";
|
||||
import ArraySubTableSourceFirestore from "@src/sources/TableSourceFirestore/ArraySubTableSourceFirestore";
|
||||
import TableToolbarSkeleton from "@src/components/TableToolbar/TableToolbarSkeleton";
|
||||
import TableSkeleton from "@src/components/Table/TableSkeleton";
|
||||
|
||||
import { projectScope, currentUserAtom } from "@src/atoms/projectScope";
|
||||
import {
|
||||
tableScope,
|
||||
tableIdAtom,
|
||||
tableSettingsAtom,
|
||||
tableSchemaAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
|
||||
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
|
||||
|
||||
// prettier-ignore
|
||||
const TablePage = lazy(() => import("./TablePage" /* webpackChunkName: "TablePage" */));
|
||||
|
||||
/**
|
||||
* Wraps `TablePage` with the data for a array-sub-table.
|
||||
*
|
||||
* Differences to `ProvidedTablePage`:
|
||||
* - Renders a `Modal`
|
||||
* - When this is a child of `ProvidedTablePage`, the `TablePage` rendered for
|
||||
* the root table has its modals disabled
|
||||
*/
|
||||
export default function ProvidedArraySubTablePage() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
// Get params from URL: /arraySubTable/:docPath/:subTableKey
|
||||
const { docPath, subTableKey } = useParams();
|
||||
|
||||
const [currentUser] = useAtom(currentUserAtom, projectScope);
|
||||
|
||||
// Get table settings and the source column from root table
|
||||
const [rootTableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const [sourceColumn] = useAtom(
|
||||
useMemo(
|
||||
() =>
|
||||
selectAtom(
|
||||
tableSchemaAtom,
|
||||
(tableSchema) => find(tableSchema.columns, ["key", subTableKey]),
|
||||
isEqual
|
||||
),
|
||||
[subTableKey]
|
||||
),
|
||||
tableScope
|
||||
);
|
||||
|
||||
// Consumed by children as `tableSettings.collection`
|
||||
const subTableCollection = docPath ?? ""; // + "/" + (sourceColumn?.fieldName || subTableKey);
|
||||
|
||||
// Must be compatible with `getTableSchemaPath`: tableId/rowId/subTableKey
|
||||
// This is why we can’t have a sub-table column fieldName !== key
|
||||
const subTableId =
|
||||
docPath?.replace(rootTableSettings.collection, rootTableSettings.id) +
|
||||
"/" +
|
||||
subTableKey;
|
||||
|
||||
// Write fake tableSettings
|
||||
const subTableSettings = {
|
||||
...rootTableSettings,
|
||||
collection: subTableCollection,
|
||||
id: subTableId,
|
||||
subTableKey,
|
||||
isNotACollection: true,
|
||||
tableType: "primaryCollection" as "primaryCollection",
|
||||
name: sourceColumn?.name || subTableKey || "",
|
||||
};
|
||||
|
||||
const rootTableLink = location.pathname.split("/" + ROUTES.arraySubTable)[0];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<BreadcrumbsSubTable
|
||||
rootTableSettings={rootTableSettings}
|
||||
subTableSettings={subTableSettings}
|
||||
rootTableLink={rootTableLink}
|
||||
/>
|
||||
}
|
||||
onClose={() => navigate(rootTableLink)}
|
||||
disableBackdropClick
|
||||
disableEscapeKeyDown
|
||||
fullScreen
|
||||
sx={{
|
||||
"& > .MuiDialog-container > .MuiPaper-root": {
|
||||
bgcolor: "background.default",
|
||||
backgroundImage: "none",
|
||||
},
|
||||
"& .modal-title-row": {
|
||||
height: TOP_BAR_HEIGHT,
|
||||
"& .MuiDialogTitle-root": {
|
||||
px: 2,
|
||||
py: (TOP_BAR_HEIGHT - 28) / 2 / 8,
|
||||
},
|
||||
"& .dialog-close": { m: (TOP_BAR_HEIGHT - 40) / 2 / 8, ml: -1 },
|
||||
},
|
||||
"& .table-container": {
|
||||
height: `calc(100vh - ${TOP_BAR_HEIGHT}px - ${TABLE_TOOLBAR_HEIGHT}px - 16px)`,
|
||||
},
|
||||
}}
|
||||
ScrollableDialogContentProps={{
|
||||
disableTopDivider: true,
|
||||
disableBottomDivider: true,
|
||||
style: { "--dialog-spacing": 0, "--dialog-contents-spacing": 0 } as any,
|
||||
}}
|
||||
BackdropProps={{ key: "sub-table-modal-backdrop" }}
|
||||
>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<>
|
||||
<TableToolbarSkeleton />
|
||||
<TableSkeleton />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Provider
|
||||
key={tableScope.description + "/subTable/" + subTableSettings.id}
|
||||
scope={tableScope}
|
||||
initialValues={[
|
||||
[currentUserAtom, currentUser],
|
||||
[tableIdAtom, subTableSettings.id],
|
||||
[tableSettingsAtom, subTableSettings],
|
||||
]}
|
||||
>
|
||||
<DebugAtoms scope={tableScope} />
|
||||
<ArraySubTableSourceFirestore />
|
||||
<TablePage
|
||||
tableNotACollection={true}
|
||||
disabledTools={[
|
||||
"import",
|
||||
"export",
|
||||
"webhooks",
|
||||
"extensions",
|
||||
"cloud_logs",
|
||||
]}
|
||||
/>
|
||||
</Provider>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
|
||||
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
|
||||
import { DRAWER_COLLAPSED_WIDTH } from "@src/components/SideDrawer";
|
||||
import { formatSubTableName } from "@src/utils/table";
|
||||
import { TableToolsType } from "@src/types/table";
|
||||
|
||||
// prettier-ignore
|
||||
const BuildLogsSnack = lazy(() => import("@src/components/TableModals/CloudLogsModal/BuildLogs/BuildLogsSnack" /* webpackChunkName: "TableModals-BuildLogsSnack" */));
|
||||
@@ -53,6 +54,10 @@ export interface ITablePageProps {
|
||||
disableModals?: boolean;
|
||||
/** Disable side drawer */
|
||||
disableSideDrawer?: boolean;
|
||||
/* Array table is not a collection */
|
||||
tableNotACollection?: boolean;
|
||||
|
||||
disabledTools?: TableToolsType;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,6 +76,8 @@ export interface ITablePageProps {
|
||||
export default function TablePage({
|
||||
disableModals,
|
||||
disableSideDrawer,
|
||||
tableNotACollection,
|
||||
disabledTools,
|
||||
}: ITablePageProps) {
|
||||
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
||||
const [userSettings] = useAtom(userSettingsAtom, projectScope);
|
||||
@@ -127,7 +134,7 @@ export default function TablePage({
|
||||
<ActionParamsProvider>
|
||||
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
|
||||
<Suspense fallback={<TableToolbarSkeleton />}>
|
||||
<TableToolbar />
|
||||
<TableToolbar disabledTools={disabledTools} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { memo, useCallback, useEffect } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import useMemoValue from "use-memo-value";
|
||||
import { cloneDeep, set } from "lodash-es";
|
||||
import {
|
||||
FirestoreError,
|
||||
deleteField,
|
||||
refEqual,
|
||||
setDoc,
|
||||
} from "firebase/firestore";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
|
||||
import {
|
||||
tableScope,
|
||||
tableSettingsAtom,
|
||||
tableSchemaAtom,
|
||||
updateTableSchemaAtom,
|
||||
tableSortsAtom,
|
||||
tableRowsDbAtom,
|
||||
_updateRowDbAtom,
|
||||
_deleteRowDbAtom,
|
||||
tableNextPageAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
|
||||
import useFirestoreDocWithAtom, {
|
||||
getDocRef,
|
||||
} from "@src/hooks/useFirestoreDocWithAtom";
|
||||
|
||||
import useAuditChange from "./useAuditChange";
|
||||
import useBulkWriteDb from "./useBulkWriteDb";
|
||||
import { handleFirestoreError } from "./handleFirestoreError";
|
||||
|
||||
import { getTableSchemaPath } from "@src/utils/table";
|
||||
import { TableRow, TableSchema } from "@src/types/table";
|
||||
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
|
||||
import { projectScope } from "@src/atoms/projectScope";
|
||||
import useFirestoreDocAsCollectionWithAtom from "@src/hooks/useFirestoreDocAsCollectionWithAtom";
|
||||
|
||||
/**
|
||||
* When rendered, provides atom values for top-level tables and sub-tables
|
||||
*/
|
||||
export const TableSourceFirestore2 = memo(function TableSourceFirestore() {
|
||||
const [firebaseDb] = useAtom(firebaseDbAtom, projectScope);
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const setTableSchema = useSetAtom(tableSchemaAtom, tableScope);
|
||||
const setUpdateTableSchema = useSetAtom(updateTableSchemaAtom, tableScope);
|
||||
const setTableNextPage = useSetAtom(tableNextPageAtom, tableScope);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
if (!tableSettings) throw new Error("No table config");
|
||||
if (!tableSettings.collection)
|
||||
throw new Error("Invalid table config: no collection");
|
||||
|
||||
const tableSchemaDocRef = useMemoValue(
|
||||
getDocRef<TableSchema>(firebaseDb, getTableSchemaPath(tableSettings)),
|
||||
(next, prev) => refEqual(next as any, prev as any)
|
||||
);
|
||||
|
||||
setTableNextPage({
|
||||
loading: false,
|
||||
available: false,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!tableSchemaDocRef) return;
|
||||
|
||||
setUpdateTableSchema(
|
||||
() => (update: TableSchema, deleteFields?: string[]) => {
|
||||
const updateToDb = cloneDeep(update);
|
||||
|
||||
if (Array.isArray(deleteFields)) {
|
||||
for (const field of deleteFields) {
|
||||
// Use deterministic set firestore sentinel's on schema columns config
|
||||
// Required for nested columns
|
||||
// i.e field = "columns.base.nested.nested"
|
||||
// key: columns, rest: base.nested.nested
|
||||
// set columns["base.nested.nested"] instead columns.base.nested.nested
|
||||
const [key, ...rest] = field.split(".");
|
||||
if (key === "columns") {
|
||||
(updateToDb as any).columns[rest.join(".")] = deleteField();
|
||||
} else {
|
||||
set(updateToDb, field, deleteField());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI state to reflect changes immediately to prevent flickering effects
|
||||
setTableSchema((tableSchema) => ({ ...tableSchema, ...update }));
|
||||
|
||||
return setDoc(tableSchemaDocRef, updateToDb, { merge: true }).catch(
|
||||
(e) => {
|
||||
enqueueSnackbar((e as Error).message, { variant: "error" });
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
setUpdateTableSchema(undefined);
|
||||
};
|
||||
}, [tableSchemaDocRef, setTableSchema, setUpdateTableSchema, enqueueSnackbar]);
|
||||
|
||||
// Get tableSchema and store in tableSchemaAtom.
|
||||
// If it doesn’t exist, initialize columns
|
||||
useFirestoreDocWithAtom(
|
||||
tableSchemaAtom,
|
||||
tableScope,
|
||||
getTableSchemaPath(tableSettings),
|
||||
{
|
||||
createIfNonExistent: { columns: {} },
|
||||
disableSuspense: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Get table sorts
|
||||
const [sorts] = useAtom(tableSortsAtom, tableScope);
|
||||
// Get documents from collection and store in tableRowsDbAtom
|
||||
// and handle some errors with snackbars
|
||||
const elevateError = useErrorHandler();
|
||||
const handleErrorCallback = useCallback(
|
||||
(error: FirestoreError) =>
|
||||
handleFirestoreError(error, enqueueSnackbar, elevateError),
|
||||
[enqueueSnackbar, elevateError]
|
||||
);
|
||||
useFirestoreDocAsCollectionWithAtom<TableRow>(
|
||||
tableRowsDbAtom,
|
||||
tableScope,
|
||||
tableSettings.collection,
|
||||
tableSettings.subTableKey || "",
|
||||
{
|
||||
sorts,
|
||||
onError: handleErrorCallback,
|
||||
updateDocAtom: _updateRowDbAtom,
|
||||
deleteDocAtom: _deleteRowDbAtom,
|
||||
}
|
||||
);
|
||||
useAuditChange();
|
||||
useBulkWriteDb();
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export default TableSourceFirestore2;
|
||||
29
src/types/table.d.ts
vendored
29
src/types/table.d.ts
vendored
@@ -31,7 +31,8 @@ export type UpdateDocFunction<T = TableRow> = (
|
||||
export type UpdateCollectionDocFunction<T = TableRow> = (
|
||||
path: string,
|
||||
update: Partial<T>,
|
||||
deleteFields?: string[]
|
||||
deleteFields?: string[],
|
||||
options?: ArrayTableRowData
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
@@ -39,7 +40,10 @@ export type UpdateCollectionDocFunction<T = TableRow> = (
|
||||
* @param path - The full path to the doc
|
||||
* @returns Promise
|
||||
*/
|
||||
export type DeleteCollectionDocFunction = (path: string) => Promise<void>;
|
||||
export type DeleteCollectionDocFunction = (
|
||||
path: string,
|
||||
options?: ArrayTableRowData
|
||||
) => Promise<void>;
|
||||
|
||||
export type BulkWriteOperation<T> =
|
||||
| { type: "delete"; path: string }
|
||||
@@ -71,6 +75,8 @@ export type TableSettings = {
|
||||
/** Roles that can see this table in the UI and navigate. Firestore Rules need to be set to give access to the data */
|
||||
roles: string[];
|
||||
|
||||
isNotACollection?: boolean;
|
||||
subTableKey?: string | undefined;
|
||||
section: string;
|
||||
description?: string;
|
||||
details?: string;
|
||||
@@ -187,6 +193,15 @@ export type TableFilter = {
|
||||
value: any;
|
||||
};
|
||||
|
||||
export const TableTools = [
|
||||
"import",
|
||||
"export",
|
||||
"webhooks",
|
||||
"extensions",
|
||||
"cloud_logs",
|
||||
] as const;
|
||||
export type TableToolsType = typeof Tools[number];
|
||||
|
||||
export type TableSort = {
|
||||
key: string;
|
||||
direction: Parameters<typeof orderBy>[1];
|
||||
@@ -197,10 +212,20 @@ export type TableRowRef = {
|
||||
path: string;
|
||||
} & Partial<DocumentReference>;
|
||||
|
||||
type ArrayTableOperations = {
|
||||
addRow?: "top" | "bottom";
|
||||
base?: TableRow;
|
||||
};
|
||||
export type ArrayTableRowData = {
|
||||
index: number;
|
||||
parentField?: string;
|
||||
operation?: ArrayTableOperations;
|
||||
};
|
||||
export type TableRow = DocumentData & {
|
||||
_rowy_ref: TableRowRef;
|
||||
_rowy_missingRequiredFields?: string[];
|
||||
_rowy_outOfOrder?: boolean;
|
||||
_rowy_arrayTableData?: ArrayTableRowData;
|
||||
};
|
||||
|
||||
export type FileValue = {
|
||||
|
||||
@@ -51,6 +51,7 @@ export const omitRowyFields = <T = Record<string, any>>(row: T) => {
|
||||
delete shallowClonedRow["_rowy_outOfOrder"];
|
||||
delete shallowClonedRow["_rowy_missingRequiredFields"];
|
||||
delete shallowClonedRow["_rowy_new"];
|
||||
delete shallowClonedRow["_rowy_arrayTableData"];
|
||||
|
||||
return shallowClonedRow as T;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user