diff --git a/craco.config.js b/craco.config.js
index e71c336e..e17da462 100644
--- a/craco.config.js
+++ b/craco.config.js
@@ -64,4 +64,15 @@ module.exports = {
return jestConfig;
},
},
+ webpack: {
+ configure: {
+ resolve: {
+ // Need to add polyfill for csv-parse
+ fallback: {
+ stream: require.resolve("stream-browserify"),
+ buffer: require.resolve("buffer"),
+ },
+ },
+ },
+ },
};
diff --git a/emulators/auth_export/accounts.json b/emulators/auth_export/accounts.json
index a2e193a1..34127b9e 100644
--- a/emulators/auth_export/accounts.json
+++ b/emulators/auth_export/accounts.json
@@ -1 +1 @@
-{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"26CJMrwlouNRwkiLofNK07DNgKhw","createdAt":"1651022832613","lastLoginAt":"1652237658250","displayName":"Admin User","photoUrl":"","passwordHash":"fakeHash:salt=fakeSaltWjasmDYtQJU3vEm0cdg9:password=adminUser","salt":"fakeSaltWjasmDYtQJU3vEm0cdg9","passwordUpdatedAt":1652169642047,"customAttributes":"{\"roles\": [\"ADMIN\"]}","providerUserInfo":[{"providerId":"google.com","rawId":"abc123","federatedId":"abc123","displayName":"Admin User","email":"admin@example.com"},{"providerId":"password","email":"admin@example.com","federatedId":"admin@example.com","rawId":"admin@example.com","displayName":"Admin User","photoUrl":""}],"validSince":"1652169642","email":"admin@example.com","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-05-11T02:54:18.250Z"},{"localId":"3xTRVPnJGT2GE6lkiWKZp1jShuXj","createdAt":"1651023059442","lastLoginAt":"1651727720399","displayName":"Editor User","photoUrl":"","passwordHash":"fakeHash:salt=fakeSaltmNJg6BtcsUfyZKz6wZQY:password=editorUser","salt":"fakeSaltmNJg6BtcsUfyZKz6wZQY","passwordUpdatedAt":1652169642047,"providerUserInfo":[{"providerId":"google.com","rawId":"1535779573397289142795231390488730790451","federatedId":"1535779573397289142795231390488730790451","displayName":"Editor User","email":"editor@example.com"},{"providerId":"password","email":"editor@example.com","federatedId":"editor@example.com","rawId":"editor@example.com","displayName":"Editor User","photoUrl":""}],"validSince":"1652169642","email":"editor@example.com","emailVerified":true,"disabled":false}]}
\ No newline at end of file
+{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"26CJMrwlouNRwkiLofNK07DNgKhw","createdAt":"1651022832613","lastLoginAt":"1652766366467","displayName":"Admin User","photoUrl":"","passwordHash":"fakeHash:salt=fakeSaltWjasmDYtQJU3vEm0cdg9:password=adminUser","salt":"fakeSaltWjasmDYtQJU3vEm0cdg9","passwordUpdatedAt":1652670601103,"customAttributes":"{\"roles\": [\"ADMIN\"]}","providerUserInfo":[{"providerId":"google.com","rawId":"abc123","federatedId":"abc123","displayName":"Admin User","email":"admin@example.com"},{"providerId":"password","email":"admin@example.com","federatedId":"admin@example.com","rawId":"admin@example.com","displayName":"Admin User","photoUrl":""}],"validSince":"1652670601","email":"admin@example.com","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-05-19T05:13:45.818Z"},{"localId":"3xTRVPnJGT2GE6lkiWKZp1jShuXj","createdAt":"1651023059442","lastLoginAt":"1651727720399","displayName":"Editor User","photoUrl":"","passwordHash":"fakeHash:salt=fakeSaltmNJg6BtcsUfyZKz6wZQY:password=editorUser","salt":"fakeSaltmNJg6BtcsUfyZKz6wZQY","passwordUpdatedAt":1652670601104,"providerUserInfo":[{"providerId":"google.com","rawId":"1535779573397289142795231390488730790451","federatedId":"1535779573397289142795231390488730790451","displayName":"Editor User","email":"editor@example.com"},{"providerId":"password","email":"editor@example.com","federatedId":"editor@example.com","rawId":"editor@example.com","displayName":"Editor User","photoUrl":""}],"validSince":"1652670601","email":"editor@example.com","emailVerified":true,"disabled":false}]}
\ No newline at end of file
diff --git a/emulators/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata b/emulators/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata
index 5bbf1600..fa0ff791 100644
Binary files a/emulators/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata and b/emulators/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata differ
diff --git a/emulators/firestore_export/all_namespaces/all_kinds/output-0 b/emulators/firestore_export/all_namespaces/all_kinds/output-0
index eae31061..4d99baab 100644
Binary files a/emulators/firestore_export/all_namespaces/all_kinds/output-0 and b/emulators/firestore_export/all_namespaces/all_kinds/output-0 differ
diff --git a/package.json b/package.json
index 3b622810..820fa843 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,9 @@
"@mui/styles": "^5.6.2",
"@rowy/form-builder": "^0.6.1",
"@rowy/multiselect": "^0.3.0",
+ "buffer": "^6.0.3",
"compare-versions": "^4.1.3",
+ "csv-parse": "^5.0.4",
"date-fns": "^2.28.0",
"dompurify": "^2.3.6",
"firebase": "^9.6.11",
@@ -34,7 +36,10 @@
"react-color-palette": "^6.2.0",
"react-data-grid": "7.0.0-beta.5",
"react-div-100vh": "^0.7.0",
+ "react-dnd": "^16.0.1",
+ "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.0.0",
+ "react-dropzone": "^10",
"react-element-scroll-hook": "^1.1.0",
"react-error-boundary": "^3.1.4",
"react-helmet-async": "^1.3.0",
@@ -44,6 +49,7 @@
"react-router-hash-link": "^2.4.3",
"react-scripts": "^5.0.0",
"remark-gfm": "^3.0.1",
+ "stream-browserify": "^3.0.0",
"swr": "^1.3.0",
"tss-react": "^3.6.2",
"typescript": "^4.6.3",
diff --git a/src/App.tsx b/src/App.tsx
index e653e225..54dd2b49 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -36,7 +36,7 @@ const TableSettingsDialog = lazy(() => import("@src/components/TableSettingsDial
// prettier-ignore
const TablesPage = lazy(() => import("@src/pages/Tables" /* webpackChunkName: "TablesPage" */));
// prettier-ignore
-const TablePage = lazy(() => import("@src/pages/TableTest" /* webpackChunkName: "TablePage" */));
+const TablePage = lazy(() => import("@src/pages/Table" /* webpackChunkName: "TablePage" */));
// prettier-ignore
const UserSettingsPage = lazy(() => import("@src/pages/Settings/UserSettings" /* webpackChunkName: "UserSettingsPage" */));
@@ -119,7 +119,7 @@ export default function App() {
/>
{/* } /> */}
- } />
+ } />
} />
diff --git a/src/atoms/globalScope/ui.ts b/src/atoms/globalScope/ui.ts
index 0ee1ad3c..94e6039a 100644
--- a/src/atoms/globalScope/ui.ts
+++ b/src/atoms/globalScope/ui.ts
@@ -138,3 +138,18 @@ export const tableSettingsDialogSchemaAtom = atom(async (get) => {
if (!tableId || !getTableSchema) return {} as TableSchema;
return getTableSchema(tableId);
});
+
+/** Persist the state of the add row ID type */
+export const tableAddRowIdTypeAtom = atomWithStorage<
+ "decrement" | "random" | "custom"
+>("__ROWY__ADD_ROW_ID_TYPE", "decrement");
+/** Persist when the user dismissed the row out of order warning */
+export const tableOutOfOrderDismissedAtom = atomWithStorage(
+ "__ROWY__OUT_OF_ORDER_TOOLTIP_DISMISSED",
+ false
+);
+/** Store tables where user has dismissed the description tooltip */
+export const tableDescriptionDismissedAtom = atomWithStorage(
+ "__ROWY__TABLE_DESCRIPTION_DISMISSED",
+ []
+);
diff --git a/src/atoms/tableScope/columnActions.ts b/src/atoms/tableScope/columnActions.ts
index 8a25456c..f123627f 100644
--- a/src/atoms/tableScope/columnActions.ts
+++ b/src/atoms/tableScope/columnActions.ts
@@ -1,25 +1,13 @@
import { atom } from "jotai";
-import { orderBy, findIndex } from "lodash-es";
+import { findIndex } from "lodash-es";
-import { tableSchemaAtom, updateTableSchemaAtom } from "./table";
+import {
+ tableColumnsOrderedAtom,
+ tableColumnsReducer,
+ updateTableSchemaAtom,
+} from "./table";
import { ColumnConfig } from "@src/types/table";
-/** Store the table columns as an ordered array */
-export const tableColumnsOrderedAtom = atom((get) => {
- const tableSchema = get(tableSchemaAtom);
- if (!tableSchema || !tableSchema.columns) return [];
- return orderBy(Object.values(tableSchema?.columns ?? {}), "index");
-});
-/** Reducer function to convert from array of columns to columns object */
-export const tableColumnsReducer = (
- a: Record,
- c: ColumnConfig,
- index: number
-) => {
- a[c.key] = { ...c, index };
- return a;
-};
-
export interface IAddColumnOptions {
/** Column config to add. `config.index` is ignored */
config: Omit;
diff --git a/src/atoms/tableScope/rowActions.test.ts b/src/atoms/tableScope/rowActions.test.ts
index eab16fc5..946c4d14 100644
--- a/src/atoms/tableScope/rowActions.test.ts
+++ b/src/atoms/tableScope/rowActions.test.ts
@@ -233,6 +233,9 @@ describe("addRow", () => {
});
});
+ // TODO: NESTED FIELDS TESTS
+ // TODO: TEST _rowy_* fields are removed
+
describe("multiple", () => {
test("adds multiple rows with pre-defined id", async () => {
initRows(generatedRows);
diff --git a/src/atoms/tableScope/rowActions.ts b/src/atoms/tableScope/rowActions.ts
index 29ecd434..18e19e1b 100644
--- a/src/atoms/tableScope/rowActions.ts
+++ b/src/atoms/tableScope/rowActions.ts
@@ -5,13 +5,13 @@ import { currentUserAtom } from "@src/atoms/globalScope";
import {
auditChangeAtom,
tableSettingsAtom,
+ tableColumnsOrderedAtom,
tableFiltersAtom,
tableRowsLocalAtom,
tableRowsAtom,
_updateRowDbAtom,
_deleteRowDbAtom,
} from "./table";
-import { tableColumnsOrderedAtom } from "./columnActions";
import { TableRow } from "@src/types/table";
import {
rowyUser,
@@ -216,7 +216,7 @@ export interface IUpdateFieldOptions {
* 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}
+ * @param options - {@link IUpdateFieldOptions}
*
* @example Basic usage:
* ```
diff --git a/src/atoms/tableScope/table.ts b/src/atoms/tableScope/table.ts
index 353d2905..87fb7706 100644
--- a/src/atoms/tableScope/table.ts
+++ b/src/atoms/tableScope/table.ts
@@ -1,10 +1,18 @@
import { atom } from "jotai";
import { atomWithReducer } from "jotai/utils";
-import { uniqBy, sortBy, findIndex, cloneDeep, unset } from "lodash-es";
+import {
+ uniqBy,
+ sortBy,
+ findIndex,
+ cloneDeep,
+ unset,
+ orderBy,
+} from "lodash-es";
import {
TableSettings,
TableSchema,
+ ColumnConfig,
TableFilter,
TableOrder,
TableRow,
@@ -15,15 +23,37 @@ import {
import { updateRowData } from "@src/utils/table";
/** Root atom from which others are derived */
-export const tableIdAtom = atom(undefined);
+export const tableIdAtom = atom("");
/** Store tableSettings from project settings document */
-export const tableSettingsAtom = atom(undefined);
+export const tableSettingsAtom = atom({
+ id: "",
+ collection: "",
+ name: "",
+ roles: [],
+ section: "",
+ tableType: "primaryCollection",
+});
/** Store tableSchema from schema document */
export const tableSchemaAtom = atom({});
/** Store function to update tableSchema */
export const updateTableSchemaAtom = atom<
UpdateDocFunction | undefined
>(undefined);
+/** Store the table columns as an ordered array */
+export const tableColumnsOrderedAtom = atom((get) => {
+ const tableSchema = get(tableSchemaAtom);
+ if (!tableSchema || !tableSchema.columns) return [];
+ return orderBy(Object.values(tableSchema?.columns ?? {}), "index");
+});
+/** Reducer function to convert from array of columns to columns object */
+export const tableColumnsReducer = (
+ a: Record,
+ c: ColumnConfig,
+ index: number
+) => {
+ a[c.key] = { ...c, index };
+ return a;
+};
/** Filters applied to the local view */
export const tableFiltersAtom = atom([]);
diff --git a/src/components/ButtonWithStatus.tsx b/src/components/ButtonWithStatus.tsx
new file mode 100644
index 00000000..39805b2b
--- /dev/null
+++ b/src/components/ButtonWithStatus.tsx
@@ -0,0 +1,60 @@
+import React, { forwardRef } from "react";
+
+import { Button, ButtonProps } from "@mui/material";
+import { alpha } from "@mui/material/styles";
+
+export interface IButtonWithStatusProps extends ButtonProps {
+ active?: boolean;
+}
+
+export const ButtonWithStatus = forwardRef(function ButtonWithStatus_(
+ { active = false, className, ...props }: IButtonWithStatusProps,
+ ref: React.Ref
+) {
+ return (
+
+ {/* )}
+ PopoverProps={{
+ anchorOrigin: {
+ vertical: "bottom",
+ horizontal: "center",
+ },
+ transformOrigin: {
+ vertical: "top",
+ horizontal: "center",
+ },
+ }}
+ /> */}
+
+
+
+
+ or
+
+
+
+
+
+ You can manually add new columns and rows:
+
+
+ }
+ // onClick={(event) =>
+ // columnMenuRef?.current?.setSelectedColumnHeader({
+ // column: { isNew: true, key: "new", type: "LAST" } as any,
+ // anchorEl: event.currentTarget,
+ // })
+ // }
+ // disabled={!columnMenuRef?.current}
+ disabled
+ >
+ Add column
+
+
+ {/* */}
+
+
+ >
+ );
+ }
+
+ return (
+
+ {contents}
+
+ );
+}
diff --git a/src/components/Table/FinalColumnHeader.tsx b/src/components/Table/FinalColumnHeader.tsx
new file mode 100644
index 00000000..9b227cc0
--- /dev/null
+++ b/src/components/Table/FinalColumnHeader.tsx
@@ -0,0 +1,38 @@
+import { useAtom } from "jotai";
+import { Column } from "react-data-grid";
+
+import { Button } from "@mui/material";
+import AddColumnIcon from "@src/assets/icons/AddColumn";
+
+import { globalScope, userRolesAtom } from "@src/atoms/globalScope";
+
+const FinalColumnHeader: Column["headerRenderer"] = ({ column }) => {
+ const [userRoles] = useAtom(userRolesAtom, globalScope);
+ // FIXME: const { columnMenuRef } = useProjectContext();
+ // if (!columnMenuRef) return null;
+
+ if (!userRoles.includes("ADMIN")) return null;
+
+ const handleClick = (
+ event: React.MouseEvent
+ ) => {
+ // columnMenuRef?.current?.setSelectedColumnHeader({
+ // column,
+ // anchorEl: event.currentTarget,
+ // });
+ };
+
+ return (
+ }
+ style={{ zIndex: 1 }}
+ >
+ Add column
+
+ );
+};
+
+export default FinalColumnHeader;
diff --git a/src/components/Table/OutOfOrderIndicator.tsx b/src/components/Table/OutOfOrderIndicator.tsx
new file mode 100644
index 00000000..3b9367b6
--- /dev/null
+++ b/src/components/Table/OutOfOrderIndicator.tsx
@@ -0,0 +1,63 @@
+import { useAtom } from "jotai";
+
+import { styled } from "@mui/material/styles";
+import RichTooltip from "@src/components/RichTooltip";
+import WarningIcon from "@mui/icons-material/WarningAmber";
+import { OUT_OF_ORDER_MARGIN } from "./TableContainer";
+
+import {
+ globalScope,
+ tableOutOfOrderDismissedAtom,
+} from "@src/atoms/globalScope";
+
+const Dot = styled("div")(({ theme }) => ({
+ position: "absolute",
+ left: -6,
+ top: "50%",
+ transform: "translateY(-50%)",
+ zIndex: 1,
+
+ width: 12,
+ height: 12,
+
+ borderRadius: "50%",
+ backgroundColor: theme.palette.warning.main,
+}));
+
+export interface IOutOfOrderIndicatorProps {
+ top: number;
+ height: number;
+}
+
+export default function OutOfOrderIndicator({
+ top,
+ height,
+}: IOutOfOrderIndicatorProps) {
+ const [dismissed, setDismissed] = useAtom(
+ tableOutOfOrderDismissedAtom,
+ globalScope
+ );
+
+ return (
+
+ }
+ title="Row out of order"
+ message="This row will not appear on the top of the table after you reload this page"
+ placement="right"
+ render={({ openTooltip }) => }
+ defaultOpen={!dismissed}
+ onClose={() => setDismissed(true)}
+ />
+
+ ) : null;
+ };
+}
diff --git a/src/components/Table/editors/withSideDrawerEditor.tsx b/src/components/Table/editors/withSideDrawerEditor.tsx
new file mode 100644
index 00000000..641a0a73
--- /dev/null
+++ b/src/components/Table/editors/withSideDrawerEditor.tsx
@@ -0,0 +1,52 @@
+// import { useEffect } from "react";
+import { EditorProps } from "react-data-grid";
+import { get } from "lodash-es";
+
+import { IHeavyCellProps } from "@src/components/fields/types";
+
+/**
+ * Allow the cell to be editable, but disable react-data-grid’s default
+ * text editor to show. Opens the side drawer in the appropriate position.
+ *
+ * Displays the current HeavyCell or HeavyCell since it overwrites cell contents.
+ *
+ * Use for cells that do not support any type of in-cell editing.
+ */
+export default function withSideDrawerEditor(
+ HeavyCell?: React.ComponentType
+) {
+ return function SideDrawerEditor(props: EditorProps) {
+ const { row, column } = props;
+ // FIXME: const { sideDrawerRef } = useProjectContext();
+
+ // useEffect(() => {
+ // if (!sideDrawerRef?.current?.open && sideDrawerRef?.current?.setOpen)
+ // sideDrawerRef?.current?.setOpen(true);
+ // }, [column]);
+
+ return HeavyCell ? (
+