diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index b56397907..a655f49e6 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -2,7 +2,11 @@ import { atom } from "jotai"; import { atomWithStorage, atomWithHash } from "jotai/utils"; import type { PopoverProps } from "@mui/material"; -import type { ColumnConfig, TableFilter } from "@src/types/table"; +import type { + ColumnConfig, + TableBulkEdit, + TableFilter, +} from "@src/types/table"; import { SEVERITY_LEVELS } from "@src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon"; /** @@ -72,6 +76,33 @@ export const tableFiltersPopoverAtom = atom( } ); +export type BulkEditPopoverState = { + open: boolean; + defaultQuery?: TableBulkEdit; +}; + +/** + * Store bulk edit popover state. + * Calling the set function resets props. + * + * @example Basic usage: + * ``` + * const openBulkEditltersPopover = useSetAtom(bulkEditPopoverAtom, projectScope); + * openBulkEditltersPopover({ query: ... }); + * ``` + * + * @example Close: + * ``` + * openBulkEditltersPopover({ open: false }) + * ``` + */ +export const bulkEditPopoverAtom = atom( + { open: false } as BulkEditPopoverState, + (_, set, update?: Partial) => { + set(bulkEditPopoverAtom, { open: true, ...update }); + } +); + /** Store whether to show hidden fields (override) in side drawer */ export const sideDrawerShowHiddenFieldsAtom = atomWithStorage( "__ROWY__SIDE_DRAWER_SHOW_HIDDEN_FIELDS", diff --git a/src/components/TableToolbar/BulkEdit/BulkEdit.tsx b/src/components/TableToolbar/BulkEdit/BulkEdit.tsx new file mode 100644 index 000000000..c65d19484 --- /dev/null +++ b/src/components/TableToolbar/BulkEdit/BulkEdit.tsx @@ -0,0 +1,189 @@ +import { Button, Grid, InputLabel, Stack } from "@mui/material"; + +import BulkEditPopover from "./BulkEditPopover"; +import ColumnSelect from "@src/components/Table/ColumnSelect"; +import { TableBulkEdit } from "@src/types/table"; +import { useAtom, useSetAtom } from "jotai"; +import { useSnackbar } from "notistack"; + +import { + tableColumnsOrderedAtom, + tableScope, + updateFieldAtom, +} from "@src/atoms/tableScope"; +import { generateId } from "@src/utils/table"; +import { getFieldType, getFieldProp } from "@src/components/fields"; +import { Suspense, createElement, useMemo, useState } from "react"; +import { find } from "lodash-es"; +import { ErrorBoundary } from "react-error-boundary"; +import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; +import { InlineErrorFallback } from "@src/components/ErrorFallback"; +import { RowSelectionState } from "@tanstack/react-table"; + +export const NON_EDITABLE_TYPES: string[] = [ + "ID", + "CREATED_AT", + "UPDATED_AT", + "UPDATED_BY", + "CREATED_BY", + "COLOR", + "ARRAY_SUB_TABLE", + "SUB_TABLE", + "GEO_POINT", +]; + +export default function BulkEdit({ + selectedRows, +}: { + selectedRows: RowSelectionState; +}) { + const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + const updateField = useSetAtom(updateFieldAtom, tableScope); + + const { enqueueSnackbar } = useSnackbar(); + + const filterColumns = useMemo(() => { + return tableColumnsOrdered + .filter((col) => !NON_EDITABLE_TYPES.includes(col.type)) + .map((c) => ({ + value: c.key, + label: c.name, + type: c.type, + key: c.key, + index: c.index, + config: c.config || {}, + })); + }, [tableColumnsOrdered]); + + const INITIAL_QUERY: TableBulkEdit | null = + filterColumns.length > 0 + ? { + key: filterColumns[0].key, + value: "", + id: generateId(), + } + : null; + + const [query, setQuery] = useState(INITIAL_QUERY); + const selectedColumn = find(filterColumns, ["key", query?.key]); + const columnType = selectedColumn ? getFieldType(selectedColumn) : null; + + const handleColumnChange = (newKey: string | null) => { + const column = find(filterColumns, ["key", newKey]); + if (column && newKey) { + const updatedQuery: TableBulkEdit = { + key: newKey, + value: "", + id: generateId(), + }; + setQuery(updatedQuery); + } + }; + + const handleBulkUpdate = async (): Promise => { + const selectedRowsArr = Object.keys(selectedRows); + try { + const updatePromises = selectedRowsArr.map(async (rowKey) => { + return updateField({ + path: rowKey, + fieldName: query?.key ?? "", + value: query?.value, + }); + }); + + await Promise.all(updatePromises); + } catch (e) { + enqueueSnackbar((e as Error).message, { variant: "error" }); + } + }; + + return ( + <> + {filterColumns.length > 0 && INITIAL_QUERY && ( + + {({ handleClose }) => ( +
+ + + + handleColumnChange(newKey ?? INITIAL_QUERY.key) + } + disabled={false} + /> + + + {query?.key && ( + + + Value + + + }> + {columnType && + createElement( + getFieldProp( + "filter.customInput" as any, + columnType + ) || getFieldProp("SideDrawerField", columnType), + { + column: find(filterColumns, ["key", query.key]), + _rowy_ref: {}, + value: query.value, + onSubmit: () => {}, + onChange: (value: any) => { + const newQuery = { + ...query, + value, + }; + setQuery(newQuery); + }, + } + )} + + + )} + + + + + + + + +
+ )} +
+ )} + + ); +} diff --git a/src/components/TableToolbar/BulkEdit/BulkEditPopover.tsx b/src/components/TableToolbar/BulkEdit/BulkEditPopover.tsx new file mode 100644 index 000000000..404aededb --- /dev/null +++ b/src/components/TableToolbar/BulkEdit/BulkEditPopover.tsx @@ -0,0 +1,50 @@ +import { Button, Popover, Tooltip } from "@mui/material"; +import EditIcon from "@mui/icons-material/Edit"; +import { useAtom } from "jotai"; +import { bulkEditPopoverAtom, tableScope } from "@src/atoms/tableScope"; +import { useRef } from "react"; + +export interface IBulkEditPopoverProps { + children: (props: { handleClose: () => void }) => React.ReactNode; +} + +export default function BulkEditPopover({ children }: IBulkEditPopoverProps) { + const [{ open }, setBulkEditPopoverState] = useAtom( + bulkEditPopoverAtom, + tableScope + ); + + const handleClose = () => setBulkEditPopoverState({ open: false }); + const anchorEl = useRef(null); + const popoverId = open ? "bulkEdit-popover" : undefined; + + return ( + <> + + + + + {children({ handleClose })} + + + ); +} diff --git a/src/components/TableToolbar/TableToolbar.tsx b/src/components/TableToolbar/TableToolbar.tsx index 2adb90f1d..fe040f39f 100644 --- a/src/components/TableToolbar/TableToolbar.tsx +++ b/src/components/TableToolbar/TableToolbar.tsx @@ -49,6 +49,10 @@ const Sort = lazy(() => import("./Sort" /* webpackChunkName: "Filters" */)); // prettier-ignore const Filters = lazy(() => import("./Filters" /* webpackChunkName: "Filters" */)); + +// prettier-ignore +const BulkEdit = lazy(() => import("./BulkEdit/BulkEdit" /* webpackChunkName: "Filters" */)); + // prettier-ignore const ImportData = lazy(() => import("./ImportData/ImportData" /* webpackChunkName: "ImportData" */)); @@ -129,6 +133,8 @@ function RowSelectedToolBar({ Delete + + ); } diff --git a/src/types/table.d.ts b/src/types/table.d.ts index 2fd8b5324..3f151d7f5 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -203,6 +203,12 @@ export type TableFilter = { id: string; }; +export type TableBulkEdit = { + key: string; + value: any; + id: string; +}; + export const TableTools = [ "import", "export",