diff --git a/packages/admin-ui/package.json b/packages/admin-ui/package.json index 6fdba10e2e9..a8bb29fef99 100644 --- a/packages/admin-ui/package.json +++ b/packages/admin-ui/package.json @@ -26,6 +26,7 @@ "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-tooltip": "^1.1.2", + "@tanstack/react-table": "^8.20.6", "@webiny/react-composition": "0.0.0", "@webiny/react-router": "0.0.0", "@webiny/utils": "0.0.0", diff --git a/packages/admin-ui/src/Checkbox/CheckboxItemDto.ts b/packages/admin-ui/src/Checkbox/CheckboxItemDto.ts index 5626ac6af00..5236326caa1 100644 --- a/packages/admin-ui/src/Checkbox/CheckboxItemDto.ts +++ b/packages/admin-ui/src/Checkbox/CheckboxItemDto.ts @@ -2,7 +2,7 @@ import React from "react"; export interface CheckboxItemDto { id?: string; - label: string | React.ReactNode; + label?: string | React.ReactNode; value?: number | string; checked?: boolean; indeterminate?: boolean; diff --git a/packages/admin-ui/src/Checkbox/CheckboxItemFormatted.ts b/packages/admin-ui/src/Checkbox/CheckboxItemFormatted.ts index 294bc1f2de8..72ba219f87d 100644 --- a/packages/admin-ui/src/Checkbox/CheckboxItemFormatted.ts +++ b/packages/admin-ui/src/Checkbox/CheckboxItemFormatted.ts @@ -7,4 +7,5 @@ export interface CheckboxItemFormatted { checked: boolean; indeterminate: boolean; disabled: boolean; + hasLabel: boolean; } diff --git a/packages/admin-ui/src/Checkbox/CheckboxItemMapper.ts b/packages/admin-ui/src/Checkbox/CheckboxItemMapper.ts index 706250bedbc..8b98d2c007a 100644 --- a/packages/admin-ui/src/Checkbox/CheckboxItemMapper.ts +++ b/packages/admin-ui/src/Checkbox/CheckboxItemMapper.ts @@ -9,7 +9,8 @@ export class CheckboxItemMapper { value: item.value, checked: item.checked, indeterminate: item.indeterminate, - disabled: item.disabled + disabled: item.disabled, + hasLabel: !!item.label }; } } diff --git a/packages/admin-ui/src/Checkbox/CheckboxPresenter.test.ts b/packages/admin-ui/src/Checkbox/CheckboxPresenter.test.ts index d49166ddf0b..2c988a3cf26 100644 --- a/packages/admin-ui/src/Checkbox/CheckboxPresenter.test.ts +++ b/packages/admin-ui/src/Checkbox/CheckboxPresenter.test.ts @@ -13,13 +13,13 @@ describe("CheckboxPresenter", () => { label: "Label" }); expect(presenter.vm.item?.label).toEqual("Label"); + expect(presenter.vm.item?.hasLabel).toEqual(true); } // `id` { presenter.init({ onCheckedChange, - label: "Label", id: "id" }); expect(presenter.vm.item?.id).toEqual("id"); @@ -29,7 +29,6 @@ describe("CheckboxPresenter", () => { { presenter.init({ onCheckedChange, - label: "Label", checked: true }); expect(presenter.vm.item?.checked).toBeTruthy(); @@ -39,7 +38,6 @@ describe("CheckboxPresenter", () => { { presenter.init({ onCheckedChange, - label: "Label", indeterminate: true }); expect(presenter.vm.item?.indeterminate).toBeTruthy(); @@ -49,7 +47,6 @@ describe("CheckboxPresenter", () => { { presenter.init({ onCheckedChange, - label: "Label", disabled: true }); expect(presenter.vm.item?.disabled).toBeTruthy(); @@ -58,22 +55,21 @@ describe("CheckboxPresenter", () => { // default: only mandatory props { presenter.init({ - onCheckedChange, - label: "Label" + onCheckedChange }); expect(presenter.vm.item).toEqual({ id: expect.any(String), - label: "Label", checked: false, indeterminate: false, - disabled: false + disabled: false, + hasLabel: false }); } }); it("should call `onCheckedChange` callback when `changeChecked` is called", () => { const presenter = new CheckboxPresenter(); - presenter.init({ onCheckedChange, label: "Label" }); + presenter.init({ onCheckedChange }); presenter.changeChecked(true); expect(onCheckedChange).toHaveBeenCalledWith(true); }); diff --git a/packages/admin-ui/src/Checkbox/CheckboxPrimitive.tsx b/packages/admin-ui/src/Checkbox/CheckboxPrimitive.tsx index 26f4100a3f2..5b83f800802 100644 --- a/packages/admin-ui/src/Checkbox/CheckboxPrimitive.tsx +++ b/packages/admin-ui/src/Checkbox/CheckboxPrimitive.tsx @@ -25,8 +25,8 @@ const IndeterminateIcon = () => { */ const checkboxVariants = cva( [ - "group peer h-md w-md shrink-0 rounded-sm border-sm mt-xxs", - "border-neutral-muted bg-neutral-base fill-neutral-base ring-offset-background", + "group peer h-md w-md shrink-0 rounded-sm border-sm ", + "border-neutral-muted bg-neutral-base [&_svg]:!fill-neutral-base ring-offset-background", "hover:border-neutral-dark", "focus:outline-none focus-visible:border-accent-default focus-visible:ring-lg focus-visible:ring-primary-dimmed focus-visible:ring-offset-0", "disabled:cursor-not-allowed disabled:border-transparent disabled:bg-neutral-disabled", @@ -44,6 +44,9 @@ const checkboxVariants = cva( "data-[state=checked]:focus-visible:border-accent-default", "data-[state=checked]:disabled:border-transparent" ] + }, + hasLabel: { + true: "mt-xxs" } } } @@ -70,24 +73,27 @@ type CheckboxPrimitiveRendererProps = Omit, CheckboxPrimitiveRendererProps ->(({ label, id, indeterminate, changeChecked, className, ...props }, ref) => { +>(({ label, id, hasLabel, indeterminate, changeChecked, className, ...props }, ref) => { return (
- {indeterminate && } - - - + {indeterminate ? ( + + ) : ( + + + + )} -
); }); diff --git a/packages/admin-ui/src/DataTable/DataTable.stories.tsx b/packages/admin-ui/src/DataTable/DataTable.stories.tsx new file mode 100644 index 00000000000..8620a3e4d77 --- /dev/null +++ b/packages/admin-ui/src/DataTable/DataTable.stories.tsx @@ -0,0 +1,226 @@ +import React, { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { DataTableColumns, DataTable, DataTableDefaultData, DataTableSorting } from "./DataTable"; +import { Avatar } from "~/Avatar"; +import { Text } from "~/Text"; + +const meta: Meta = { + title: "Components/DataTable", + component: DataTable, + tags: ["autodocs"], + parameters: { + layout: "padded" + } +}; + +export default meta; +type Story & DataTableDefaultData> = StoryObj>; + +// Declare the data structure. +interface Entry { + id: string; + name: string; + createdBy: string; + lastModified: string; + status: string; + $selectable?: boolean; +} + +// Define the columns structure for the table. +const columns: DataTableColumns = { + name: { + header: "Title" + }, + createdBy: { + header: "Author" + }, + lastModified: { + header: "Last Modified" + }, + status: { + header: "Status" + } +}; + +// Define the data to display. +function generateEntries(count = 20) { + const statuses = ["Draft", "Published", "Unpublished"]; + const randomStatus = () => statuses[Math.floor(Math.random() * statuses.length)]; + const randomTime = () => { + const times = ["1 hour ago", "3 hours ago", "1 day ago", "3 days ago", "1 week ago"]; + return times[Math.floor(Math.random() * times.length)]; + }; + + const entries = []; + for (let i = 1; i <= count; i++) { + entries.push({ + id: `entry-${i}`, + name: `Entry ${i}`, + createdBy: "John Doe", + lastModified: randomTime(), + status: randomStatus() + }); + } + + return entries; +} + +const data = generateEntries(); + +export const Default: Story = { + args: { + data, + columns + } +}; + +export const Bordered: Story = { + args: { + ...Default.args, + bordered: true + } +}; + +export const WithStickyHeader: Story = { + args: { + ...Default.args, + stickyHeader: true + } +}; + +export const WithSelectableRows: Story = { + args: Default.args, + render: args => { + const [selectedRows, setSelectedRows] = useState([]); + + return ( + setSelectedRows(rows)} + /> + ); + } +}; + +export const WithLoading: Story = { + args: { + ...Default.args, + loading: true + } +}; + +export const WithLongColumnContent: Story = { + args: { + ...Default.args, + columns: { + ...columns, + name: { + ...columns.name, + header: "Name - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + } + }, + data: data.map(entry => ({ + ...entry, + name: `${entry.name} - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla facilisi. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.` + })) + } +}; + +export const WithCustomCellRenderer: Story = { + args: { + ...Default.args, + columns: { + ...columns, + name: { + ...columns.name, + cell: (entry: Entry) => { + return ( +
+ + } + fallback={{entry.name.charAt(0)}} + size={"xl"} + /> +
+ + +
+
+ ); + } + } + } + } +}; + +export const WithCustomColumnSize: Story = { + args: { + ...Default.args, + columns: { + ...columns, + name: { + ...columns.name, + size: 200 + } + } + } +}; + +export const WithCustomColumnClassName: Story = { + args: { + ...Default.args, + columns: { + ...columns, + lastModified: { + ...columns.lastModified, + className: "bg-primary-subtle" + }, + status: { + ...columns.status, + className: "text-right" + } + } + } +}; + +export const WithSorting: Story = { + args: { + ...Default.args, + columns: { + ...columns, + name: { + ...columns.name, + enableSorting: true + }, + lastModified: { + ...columns.lastModified, + enableSorting: true + } + }, + sorting: [ + { + id: "lastModified", + desc: true + } + ] + }, + render: ({ sorting: argsSorting = [], ...args }) => { + const [sorting, setSorting] = useState(argsSorting); + return ; + } +}; diff --git a/packages/admin-ui/src/DataTable/DataTable.tsx b/packages/admin-ui/src/DataTable/DataTable.tsx new file mode 100644 index 00000000000..3d7009e195a --- /dev/null +++ b/packages/admin-ui/src/DataTable/DataTable.tsx @@ -0,0 +1,510 @@ +import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + Cell, + Column, + ColumnDef, + ColumnSort, + flexRender, + getCoreRowModel, + getSortedRowModel, + OnChangeFn, + Row, + RowSelectionState, + SortingState, + useReactTable, + VisibilityState +} from "@tanstack/react-table"; +import { CheckboxPrimitive } from "~/Checkbox"; +import { Skeleton } from "~/Skeleton"; +import { Table } from "~/Table"; +import { ColumnSorter, ColumnsVisibility } from "./components"; +import { cn } from "~/utils"; + +interface DataTableColumn { + /* + * Column header component. + */ + header?: string | number | JSX.Element; + /* + * Cell renderer, receives the full row and returns the value to render inside the cell. + */ + cell?: (row: T) => string | number | JSX.Element | null; + /* + * Column size. + */ + size?: number; + /* + * Column class names. + */ + className?: string; + /* + * Enable column sorting. + */ + enableSorting?: boolean; + /* + * Enable column resizing. + */ + enableResizing?: boolean; + /* + * Enable column visibility toggling. + */ + enableHiding?: boolean; +} + +type DataTableColumns = { + [P in keyof T]?: DataTableColumn; +}; + +type DataTableDefaultData = { + id: string; + /* + * Define if a specific row can be selected. + */ + $selectable?: boolean; +}; + +type DataTableRow = Row; + +type DataTableSorting = SortingState; + +type DataTableColumnSort = ColumnSort; + +type OnDataTableSortingChange = OnChangeFn; + +type DataTableColumnVisibility = VisibilityState; + +type OnDataTableColumnVisibilityChange = OnChangeFn; + +interface DataTableProps { + /** + * Show or hide borders. + */ + bordered?: boolean; + /** + * Controls whether "select all" action is allowed. + */ + canSelectAllRows?: boolean; + /** + * Columns definition. + */ + columns: DataTableColumns; + /** + * The column visibility state. + */ + columnVisibility?: DataTableColumnVisibility; + /** + * Callback that receives current column visibility state. + */ + onColumnVisibilityChange?: OnDataTableColumnVisibilityChange; + /** + * Data to display into DataTable body. + */ + data: TEntry[]; + /** + * Callback that is called to determine if the row is selectable. + */ + isRowSelectable?: (row: Row) => boolean; + /** + * Render the skeleton state while data are loading. + */ + loading?: boolean; + /** + * Callback that receives the selected rows. + */ + onSelectRow?: (rows: TEntry[]) => void; + /** + * Callback that receives the toggled row. + */ + onToggleRow?: (row: TEntry) => void; + /** + * Callback that receives current sorting state. + */ + onSortingChange?: OnDataTableSortingChange; + /** + * Selected rows. + */ + selectedRows?: TEntry[]; + /** + * Sorting state. + */ + sorting?: DataTableSorting; + /** + * Initial sorting state. + */ + initialSorting?: DataTableSorting; + /** + * Enable sticky header. + */ + stickyHeader?: boolean; +} + +interface DefineColumnsOptions { + canSelectAllRows: boolean; + onSelectRow?: DataTableProps["onSelectRow"]; + onToggleRow: DataTableProps["onToggleRow"]; + loading: DataTableProps["loading"]; +} + +const defineColumns = ( + columns: DataTableProps["columns"], + options: DefineColumnsOptions +): ColumnDef[] => { + const { canSelectAllRows, onSelectRow, onToggleRow, loading } = options; + + return useMemo(() => { + const columnsList = Object.keys(columns).map(key => ({ + id: key, + ...columns[key as keyof typeof columns] + })); + + const defaults: ColumnDef[] = columnsList.map(column => { + const { + cell, + className, + enableHiding = true, + enableResizing = true, + enableSorting = false, + header, + id, + size = 100 + } = column; + + return { + id, + accessorKey: id, + header: () => header, + cell: props => { + if (cell && typeof cell === "function") { + return cell(props.row.original); + } else { + // Automatically convert any cell value to a string for rendering, + // ensuring the table displays values correctly. This aligns with React's + // rendering, which expects JSX, strings or null. + // https://github.com/TanStack/table/issues/1042 + return props.getValue() ? String(props.getValue()) : null; + } + }, + enableSorting, + meta: { + className + }, + enableResizing, + size, + enableHiding + }; + }); + + let columnsDefs = defaults; + const firstColumn = defaults[0]; + const isSelectable = onToggleRow || onSelectRow; + + if (isSelectable && firstColumn) { + columnsDefs = [ + { + ...firstColumn, + accessorKey: firstColumn.id as string, + header: props => { + return ( +
+ e.stopPropagation()} + /> + {firstColumn.header + ? React.createElement(firstColumn.header, props) + : null} +
+ ); + }, + cell: props => { + return ( +
+ props.row.toggleSelected(!!value)} + disabled={!props.row.getCanSelect()} + aria-label="Select row" + className={cn(!props.row.getCanSelect() ? "invisible" : "")} + /> + {firstColumn.cell + ? React.createElement(firstColumn.cell, props) + : null} +
+ ); + } + }, + ...defaults.slice(1) + ]; + } + + return columnsDefs.map(column => { + if (loading) { + return { + ...column, + cell: () => + }; + } + + return column; + }); + }, [columns, onSelectRow, onToggleRow, loading]); +}; + +const typedMemo: (component: T) => T = memo; + +interface TableCellProps { + cell: Cell; + getColumnWidth: (column: Column) => number; +} + +const TableCell = ({ cell, getColumnWidth }: TableCellProps) => { + const width = getColumnWidth(cell.column); + + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); +}; + +const MemoTableCell = typedMemo(TableCell); + +interface TableRowProps { + selected: boolean; + cells: Cell[]; + getColumnWidth: (column: Column) => number; +} + +const TableRow = ({ selected, cells, getColumnWidth }: TableRowProps) => { + return ( + + {cells.map(cell => ( + key={cell.id} cell={cell} getColumnWidth={getColumnWidth} /> + ))} + + ); +}; + +const MemoTableRow = typedMemo(TableRow); + +/** + * Empty array must be defined outside the React component so it does not force rerendering of the DataTable + */ +const emptyArray = Array(10).fill({}); + +const DataTable = & DataTableDefaultData>({ + bordered, + canSelectAllRows = true, + columnVisibility, + columns: initialColumns, + data: initialData, + initialSorting, + isRowSelectable, + loading, + onColumnVisibilityChange, + onSelectRow, + onSortingChange, + onToggleRow, + selectedRows = [], + sorting, + stickyHeader +}: DataTableProps) => { + const tableRef = useRef(null); + const [tableWidth, setTableWidth] = useState(1); + + const data = loading ? emptyArray : initialData; + + useEffect(() => { + const updateElementWidth = () => { + if (tableRef.current) { + const width = tableRef.current.clientWidth; + setTableWidth(width); + } + }; + + updateElementWidth(); + + window.addEventListener("resize", updateElementWidth); + + return () => { + window.removeEventListener("resize", updateElementWidth); + }; + }, [tableRef.current]); + + const rowSelection = useMemo(() => { + return selectedRows.reduce((acc, item) => { + const recordIndex = data.findIndex(rec => rec.id === item.id); + return { ...acc, [recordIndex]: true }; + }, {}); + }, [selectedRows, data]); + + const onRowSelectionChange: OnChangeFn = updater => { + const newSelection = typeof updater === "function" ? updater(rowSelection) : updater; + + /** + * `@tanstack/react-table` isn't telling us what row was selected or deselected. It simply gives us + * the new selection state (an object with row indexes that are currently selected). + * + * To figure out what row was toggled, we need to calculate the difference between the old selection + * and the new selection. What we're doing here is: + * - find all items that were present in the previous selection, but are no longer present in the new selection + * - find all items that are present in the new selection, but were not present in the previous selection + */ + const toggledRows = [ + ...Object.keys(rowSelection).filter(x => !(x in newSelection)), + ...Object.keys(newSelection).filter(x => !(x in rowSelection)) + ]; + + // If the difference is only 1 item, and `onToggleRow` is available, execute that. + if (toggledRows.length === 1 && typeof onToggleRow === "function") { + onToggleRow(data[parseInt(toggledRows[0])]); + return; + } else if (typeof onSelectRow === "function") { + const selection = Object.keys(newSelection).map(key => data[parseInt(key)]); + onSelectRow(selection); + } + }; + + const tableSorting = useMemo(() => { + if (!Array.isArray(sorting) || !sorting.length) { + return initialSorting; + } + return sorting; + }, [sorting]); + + const columns = defineColumns(initialColumns, { + canSelectAllRows, + onSelectRow, + onToggleRow, + loading + }); + + const table = useReactTable({ + columnResizeMode: "onChange", + columns, + data, + enableColumnResizing: true, + enableHiding: !!onColumnVisibilityChange, + enableRowSelection: isRowSelectable, + enableSorting: !!onSortingChange, + enableSortingRemoval: false, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + manualSorting: true, + onColumnVisibilityChange, + onRowSelectionChange, + onSortingChange, + state: { + columnVisibility, + rowSelection, + sorting: tableSorting + } + }); + + const getColumnWidth = useCallback( + (column: Column): number => { + if (!column.getCanResize()) { + return column.getSize(); + } + + const tableSize = table.getTotalSize(); + const columnSize = column.getSize(); + + return Math.ceil((columnSize * tableWidth) / tableSize); + }, + [table, tableWidth] + ); + + /** + * Had to memoize the rows to avoid browser freeze. + */ + const tableRows = useMemo(() => { + return table.getRowModel().rows; + }, [table, data, columns]); + + return ( +
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map((header, index) => { + const isLastCell = index === headerGroup.headers.length - 1; + const width = getColumnWidth(header.column); + + return ( + + {header.isPlaceholder ? null : ( + +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ + {isLastCell && ( + + )} +
+ )} + {header.column.getCanResize() && ( + + )} +
+ ); + })} +
+ ))} +
+ + {tableRows.map(row => { + const id = row.original.id || row.id; + return ( + + key={id} + cells={row.getVisibleCells()} + selected={row.getIsSelected()} + getColumnWidth={getColumnWidth} + /> + ); + })} + +
+
+ ); +}; + +export { + DataTable, + type DataTableProps, + type DataTableColumn, + type DataTableColumns, + type DataTableDefaultData, + type DataTableRow, + type DataTableSorting, + type DataTableColumnSort, + type OnDataTableSortingChange, + type DataTableColumnVisibility, + type OnDataTableColumnVisibilityChange +}; diff --git a/packages/admin-ui/src/DataTable/components/ColumnSorter.tsx b/packages/admin-ui/src/DataTable/components/ColumnSorter.tsx new file mode 100644 index 00000000000..6f7395dbc55 --- /dev/null +++ b/packages/admin-ui/src/DataTable/components/ColumnSorter.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { cn, cva, VariantProps } from "~/utils"; + +const columnSorterVariants = cva("flex items-center gap-xxs cursor-auto", { + variants: { + sortable: { + true: "cursor-pointer" + } + } +}); + +interface ColumnSorterProps + extends React.HTMLAttributes, + VariantProps {} + +const ColumnSorter = ({ className, children, sortable, ...props }: ColumnSorterProps) => { + return ( +
+ {children} +
+ ); +}; + +export { ColumnSorter }; diff --git a/packages/ui/src/DataTable/ColumnsVisibility.tsx b/packages/admin-ui/src/DataTable/components/ColumnsVisibility.tsx similarity index 70% rename from packages/ui/src/DataTable/ColumnsVisibility.tsx rename to packages/admin-ui/src/DataTable/components/ColumnsVisibility.tsx index 6e93f63f95d..4cbea237f64 100644 --- a/packages/ui/src/DataTable/ColumnsVisibility.tsx +++ b/packages/admin-ui/src/DataTable/components/ColumnsVisibility.tsx @@ -3,8 +3,7 @@ import { ReactComponent as SettingsIcon } from "@material-design-icons/svg/outli import { Column } from "@tanstack/react-table"; import { IconButton } from "~/Button"; import { Checkbox } from "~/Checkbox"; -import { Menu, MenuDivider } from "~/Menu"; -import { ColumnsVisibilityMenuHeader, ColumnsVisibilityMenuItem } from "~/DataTable/styled"; +import { DropdownMenu } from "~/DropdownMenu"; interface ColumnsVisibilityProps { columns: Column[]; @@ -56,22 +55,24 @@ export const ColumnsVisibility = (props: ColumnsVisibilityProps) => { } return ( - } />}> - - {"Toggle column visibility"} - - + } variant={"ghost"} size={"xs"} />} + > + {options.map(option => { return ( - - - + + } + /> ); })} - + ); }; diff --git a/packages/admin-ui/src/DataTable/components/index.ts b/packages/admin-ui/src/DataTable/components/index.ts new file mode 100644 index 00000000000..c200ea2631b --- /dev/null +++ b/packages/admin-ui/src/DataTable/components/index.ts @@ -0,0 +1,2 @@ +export * from "./ColumnSorter"; +export * from "./ColumnsVisibility"; diff --git a/packages/admin-ui/src/DataTable/index.ts b/packages/admin-ui/src/DataTable/index.ts new file mode 100644 index 00000000000..4cb9855d0f8 --- /dev/null +++ b/packages/admin-ui/src/DataTable/index.ts @@ -0,0 +1 @@ +export * from "./DataTable"; diff --git a/packages/admin-ui/src/Skeleton/Skeleton.stories.tsx b/packages/admin-ui/src/Skeleton/Skeleton.stories.tsx new file mode 100644 index 00000000000..6a0abaa7320 --- /dev/null +++ b/packages/admin-ui/src/Skeleton/Skeleton.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Skeleton } from "./Skeleton"; + +const meta: Meta = { + title: "Components/Skeleton", + component: Skeleton, + tags: ["autodocs"], + parameters: { + layout: "padded" + } +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + className: "h-lg" + } +}; diff --git a/packages/admin-ui/src/Skeleton/Skeleton.tsx b/packages/admin-ui/src/Skeleton/Skeleton.tsx new file mode 100644 index 00000000000..05ddf98233e --- /dev/null +++ b/packages/admin-ui/src/Skeleton/Skeleton.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { cn, makeDecoratable } from "~/utils"; + +const DecoratableSkeleton = ({ className, ...props }: React.HTMLAttributes) => { + return ( +
+ ); +}; + +const Skeleton = makeDecoratable("Skeleton", DecoratableSkeleton); + +export { Skeleton }; diff --git a/packages/admin-ui/src/Skeleton/index.ts b/packages/admin-ui/src/Skeleton/index.ts new file mode 100644 index 00000000000..55879b8439f --- /dev/null +++ b/packages/admin-ui/src/Skeleton/index.ts @@ -0,0 +1 @@ +export * from "./Skeleton"; diff --git a/packages/admin-ui/src/Table/Table.tsx b/packages/admin-ui/src/Table/Table.tsx new file mode 100644 index 00000000000..8b68504060d --- /dev/null +++ b/packages/admin-ui/src/Table/Table.tsx @@ -0,0 +1,55 @@ +import * as React from "react"; +import { cn, cva, VariantProps, withStaticProps } from "~/utils"; +import { + Body, + Caption, + Cell, + Direction, + Footer, + Head, + Header, + Resizer, + Row +} from "~/Table/components"; + +const tableWrapperVariants = cva("relative w-full overflow-auto", { + variants: { + sticky: { + true: "overflow-clip" + } + } +}); + +const tableVariants = cva("w-full caption-bottom text-sm bg-white", { + variants: { + bordered: { + true: "border-neutral-dimmed border-solid border-sm" + } + } +}); + +interface TableProps + extends React.HTMLAttributes, + VariantProps { + sticky?: boolean; +} + +const BaseTable = ({ className, bordered, sticky, ...props }: TableProps) => ( +
+ + +); + +const Table = withStaticProps(BaseTable, { + Body, + Caption, + Cell, + Direction, + Footer, + Head, + Header, + Resizer, + Row +}); + +export { Table, type TableProps }; diff --git a/packages/admin-ui/src/Table/components/Body.tsx b/packages/admin-ui/src/Table/components/Body.tsx new file mode 100644 index 00000000000..c7cc397bba9 --- /dev/null +++ b/packages/admin-ui/src/Table/components/Body.tsx @@ -0,0 +1,8 @@ +import * as React from "react"; +import { cn } from "~/utils"; + +const Body = ({ className, ...props }: React.HTMLAttributes) => ( + +); + +export { Body }; diff --git a/packages/admin-ui/src/Table/components/Caption.tsx b/packages/admin-ui/src/Table/components/Caption.tsx new file mode 100644 index 00000000000..afd24fb4249 --- /dev/null +++ b/packages/admin-ui/src/Table/components/Caption.tsx @@ -0,0 +1,8 @@ +import * as React from "react"; +import { cn } from "~/utils"; + +const Caption = ({ className, ...props }: React.HTMLAttributes) => ( + tr]:last:border-b-0", className)} + {...props} + /> +); + +export { Footer }; diff --git a/packages/admin-ui/src/Table/components/Head.tsx b/packages/admin-ui/src/Table/components/Head.tsx new file mode 100644 index 00000000000..c7379f0b858 --- /dev/null +++ b/packages/admin-ui/src/Table/components/Head.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; +import { cn } from "~/utils"; + +const Head = ({ className, children, ...props }: React.ThHTMLAttributes) => ( + +); + +export { Head }; diff --git a/packages/admin-ui/src/Table/components/Header.tsx b/packages/admin-ui/src/Table/components/Header.tsx new file mode 100644 index 00000000000..17214bd7dfe --- /dev/null +++ b/packages/admin-ui/src/Table/components/Header.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import { cn, cva, VariantProps } from "~/utils"; + +const headerVariants = cva("[&_tr]:hover:bg-transparent", { + variants: { + sticky: { + true: "[&_tr]:bg-white [&_tr]:hover:bg-white sticky top-0" + } + } +}); + +interface HeaderProps + extends React.HTMLAttributes, + VariantProps {} + +const Header = ({ className, sticky, ...props }: HeaderProps) => ( + +); + +export { Header, type HeaderProps }; diff --git a/packages/admin-ui/src/Table/components/Resizer.tsx b/packages/admin-ui/src/Table/components/Resizer.tsx new file mode 100644 index 00000000000..7b5336a31a5 --- /dev/null +++ b/packages/admin-ui/src/Table/components/Resizer.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { cn, cva, VariantProps } from "~/utils"; + +const resizerVariants = cva( + [ + "absolute right-0 top-0 w-md h-full border-r-md border-r-accent-default cursor-col-resize select-none touch-none", + "opacity-0 bg-transparent", + "hover:opacity-100" + ], + { + variants: { + isResizing: { + true: "opacity-100" + } + } + } +); + +interface ResizerProps + extends React.HTMLAttributes, + VariantProps {} + +const Resizer = ({ className, isResizing, ...props }: ResizerProps) => ( +
+); + +export { Resizer, type ResizerProps }; diff --git a/packages/admin-ui/src/Table/components/Row.tsx b/packages/admin-ui/src/Table/components/Row.tsx new file mode 100644 index 00000000000..08a37ecb8f5 --- /dev/null +++ b/packages/admin-ui/src/Table/components/Row.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { cn, cva, VariantProps } from "~/utils"; + +const rowVariants = cva( + "border-neutral-dimmed border-solid border-b-sm transition-colors hover:bg-neutral-subtle", + { + variants: { + selected: { + true: "bg-neutral-light" + } + } + } +); + +interface RowProps + extends React.HTMLAttributes, + VariantProps {} + +const Row = ({ className, selected, ...props }: RowProps) => ( +
+); + +export { Row, type RowProps }; diff --git a/packages/admin-ui/src/Table/components/index.ts b/packages/admin-ui/src/Table/components/index.ts new file mode 100644 index 00000000000..96142386b44 --- /dev/null +++ b/packages/admin-ui/src/Table/components/index.ts @@ -0,0 +1,9 @@ +export * from "./Body"; +export * from "./Caption"; +export * from "./Cell"; +export * from "./Direction"; +export * from "./Footer"; +export * from "./Head"; +export * from "./Header"; +export * from "./Resizer"; +export * from "./Row"; diff --git a/packages/admin-ui/src/Table/index.ts b/packages/admin-ui/src/Table/index.ts new file mode 100644 index 00000000000..e40efa4761d --- /dev/null +++ b/packages/admin-ui/src/Table/index.ts @@ -0,0 +1 @@ +export * from "./Table"; diff --git a/packages/admin-ui/src/index.ts b/packages/admin-ui/src/index.ts index 12505a47c3e..722dd5ab1bb 100644 --- a/packages/admin-ui/src/index.ts +++ b/packages/admin-ui/src/index.ts @@ -5,6 +5,7 @@ export * from "./Button"; export * from "./Card"; export * from "./Checkbox"; export * from "./CodeEditor"; +export * from "./DataTable"; export * from "./Dialog"; export * from "./DropdownMenu"; export * from "./FormComponent"; @@ -21,8 +22,10 @@ export * from "./RadioGroup"; export * from "./RangeSlider"; export * from "./Select"; export * from "./Separator"; +export * from "./Skeleton"; export * from "./Slider"; export * from "./Switch"; +export * from "./Table"; export * from "./Tabs"; export * from "./Tag"; export * from "./Text"; diff --git a/packages/ui/package.json b/packages/ui/package.json index d0999c753cc..409d74cdf33 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -17,8 +17,6 @@ "@editorjs/editorjs": "2.26.5", "@emotion/react": "11.10.8", "@emotion/styled": "11.10.6", - "@material-design-icons/svg": "^0.14.3", - "@rmwc/data-table": "^14.2.2", "@rmwc/drawer": "^14.2.2", "@rmwc/grid": "^14.2.2", "@rmwc/list": "^14.2.2", @@ -27,7 +25,6 @@ "@rmwc/touch-target": "^14.2.2", "@rmwc/typography": "^14.2.2", "@svgr/webpack": "^6.1.1", - "@tanstack/react-table": "^8.5.22", "@webiny/admin-ui": "0.0.0", "@webiny/utils": "0.0.0", "classnames": "^2.5.1", @@ -43,7 +40,6 @@ "react-color": "^2.19.3", "react-columned": "^1.1.3", "react-custom-scrollbars": "^4.2.1", - "react-loading-skeleton": "^3.1.0", "react-transition-group": "^4.4.5", "timeago-react": "^3.0.6" }, diff --git a/packages/ui/src/DataTable/ColumnDirection.tsx b/packages/ui/src/DataTable/ColumnDirection.tsx deleted file mode 100644 index 1c0fe21b2d0..00000000000 --- a/packages/ui/src/DataTable/ColumnDirection.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import { ColumnDirectionIcon, ColumnDirectionWrapper } from "~/DataTable/styled"; - -export interface ColumnDirectionProps { - direction?: "asc" | "desc"; -} - -export const ColumnDirection = (props: ColumnDirectionProps) => { - if (props.direction) { - return ( - - - - ); - } - - return null; -}; diff --git a/packages/ui/src/DataTable/DataTable.tsx b/packages/ui/src/DataTable/DataTable.tsx index bb23a3cced9..8f52b958b22 100644 --- a/packages/ui/src/DataTable/DataTable.tsx +++ b/packages/ui/src/DataTable/DataTable.tsx @@ -1,552 +1,91 @@ -import React, { - memo, - ReactElement, - useCallback, - useEffect, - useMemo, - useRef, - useState -} from "react"; - -import { - DataTableBody, - DataTableCellProps, - DataTableContent, - DataTableHead, - DataTableRow -} from "@rmwc/data-table"; - -import { - Cell, - Column as DefaultColumn, - ColumnDef, - ColumnSort, - flexRender, - getCoreRowModel, - getSortedRowModel, - OnChangeFn, - Row, - RowSelectionState, - SortingState, - useReactTable, - VisibilityState -} from "@tanstack/react-table"; - -import { Checkbox } from "~/Checkbox"; -import { Skeleton } from "~/Skeleton"; -import { ColumnDirectionProps } from "~/DataTable/ColumnDirection"; -import { ColumnsVisibility } from "~/DataTable/ColumnsVisibility"; - -import "@rmwc/data-table/data-table.css"; +import React from "react"; import { - ColumnCellWrapper, - ColumnDirectionIcon, - ColumnDirectionWrapper, - ColumnHeaderWrapper, - DataTableCell, - Resizer, - Table, - TableHeadCell -} from "./styled"; - -export interface Column { - /* - * Column header component. - */ - header: string | number | JSX.Element; - /* - * Cell renderer, receives the full row and returns the value to render inside the cell. - */ - cell?: (row: T) => string | number | JSX.Element | null; - /* - * Additional props to add to both header and row cells. Refer to RMWC documentation. - */ - meta?: DataTableCellProps; - /* - * Column size. - */ - size?: number; - /* - * Column class names. - */ - className?: string; - /* - * Enable column sorting. - */ - enableSorting?: boolean; - /* - * Enable column resizing. - */ - enableResizing?: boolean; - /* - * Enable column visibility toggling. - */ - enableHiding?: boolean; -} - -export type Columns = { - [P in keyof T]?: Column; -}; + DataTable as AdminDataTable, + DataTableColumn, + DataTableColumns, + DataTableColumnSort, + DataTableColumnVisibility, + DataTableDefaultData, + DataTableProps as AdminDataTableProps, + DataTableRow, + DataTableSorting, + OnDataTableColumnVisibilityChange, + OnDataTableSortingChange +} from "@webiny/admin-ui"; -export type DefaultData = { - id: string; - /* - * Define if a specific row can be selected. - */ - $selectable?: boolean; -}; - -export type TableRow = Row; - -export type Sorting = SortingState; - -export { ColumnSort }; - -export type OnSortingChange = OnChangeFn; - -export type ColumnVisibility = VisibilityState; - -export type OnColumnVisibilityChange = OnChangeFn; +/** + * @deprecated @webiny/ui package is deprecated and will be removed in future releases. + * Please check `DataTable` types from the `@webiny/admin-ui` package instead. + */ +type Column = DataTableColumn; +/** + * @deprecated @webiny/ui package is deprecated and will be removed in future releases. + * Please check `DataTable` types from the `@webiny/admin-ui` package instead. + */ +type ColumnSort = DataTableColumnSort; +/** + * @deprecated @webiny/ui package is deprecated and will be removed in future releases. + * Please check `DataTable` types from the `@webiny/admin-ui` package instead. + */ +type ColumnVisibility = DataTableColumnVisibility; +/** + * @deprecated @webiny/ui package is deprecated and will be removed in future releases. + * Please check `DataTable` types from the `@webiny/admin-ui` package instead. + */ +type Columns = DataTableColumns; +/** + * @deprecated @webiny/ui package is deprecated and will be removed in future releases. + * Please check `DataTable` types from the `@webiny/admin-ui` package instead. + */ +type DefaultData = DataTableDefaultData; +/** + * @deprecated @webiny/ui package is deprecated and will be removed in future releases. + * Please check `DataTable` types from the `@webiny/admin-ui` package instead. + */ +type OnColumnVisibilityChange = OnDataTableColumnVisibilityChange; +/** + * @deprecated @webiny/ui package is deprecated and will be removed in future releases. + * Please check `DataTable` types from the `@webiny/admin-ui` package instead. + */ +type OnSortingChange = OnDataTableSortingChange; +/** + * @deprecated @webiny/ui package is deprecated and will be removed in future releases. + * Please check `DataTable` types from the `@webiny/admin-ui` package instead. + */ +type Sorting = DataTableSorting; +/** + * @deprecated @webiny/ui package is deprecated and will be removed in future releases. + * Please check `DataTable` types from the `@webiny/admin-ui` package instead. + */ +type TableRow = DataTableRow; -interface Props { - /** - * Show or hide borders. - */ - bordered?: boolean; - /** - * Controls whether "select all" action is allowed. - */ - canSelectAllRows?: boolean; - /** - * Columns definition. - */ - columns: Columns; - /** - * The column visibility state. - */ - columnVisibility?: ColumnVisibility; - /** - * Callback that receives current column visibility state. - */ - onColumnVisibilityChange?: OnColumnVisibilityChange; - /** - * Data to display into DataTable body. - */ - data: T[]; - /** - * Callback that is called to determine if the row is selectable. - */ - isRowSelectable?: (row: Row) => boolean; - /** - * Render the skeleton state at the initial data loading. - */ - loadingInitial?: boolean; - /** - * Callback that receives the selected rows. - */ - onSelectRow?: (rows: T[]) => void; - /** - * Callback that receives the toggled row. - */ - onToggleRow?: (row: T) => void; - /** - * Callback that receives current sorting state. - */ - onSortingChange?: OnSortingChange; - /** - * Selected rows. - */ - selectedRows?: T[]; - /** - * Sorting state. - */ - sorting?: Sorting; - /** - * Initial sorting state. - */ - initialSorting?: Sorting; - /** - * The number of columns to affix to the side of the table when scrolling. - */ - stickyColumns?: number; - /** - * The number of rows to affix to the top of the table when scrolling. - */ +interface DataTableProps + extends Omit, "loading" | "stickyHeader"> { + loadingInitial?: AdminDataTableProps["loading"]; stickyRows?: number; } -interface DefineColumnsOptions { - canSelectAllRows: boolean; - onSelectRow?: Props["onSelectRow"]; - onToggleRow: Props["onToggleRow"]; - loadingInitial: Props["loadingInitial"]; -} - -const defineColumns = ( - columns: Props["columns"], - options: DefineColumnsOptions -): ColumnDef[] => { - const { canSelectAllRows, onSelectRow, onToggleRow, loadingInitial } = options; - - return useMemo(() => { - const columnsList = Object.keys(columns).map(key => ({ - id: key, - ...columns[key as keyof typeof columns] - })); - - const defaults: ColumnDef[] = columnsList.map(column => { - const { - cell, - className, - enableHiding = true, - enableResizing = true, - enableSorting = false, - header, - id, - meta, - size = 100 - } = column; - - return { - id, - accessorKey: id, - header: () => header, - cell: info => { - if (cell && typeof cell === "function") { - return cell(info.row.original); - } else { - // Automatically convert any cell value to a string for rendering, - // ensuring the table displays values correctly. This aligns with React's - // rendering, which expects JSX, strings or null. - // https://github.com/TanStack/table/issues/1042 - return info.getValue() ? String(info.getValue()) : null; - } - }, - enableSorting, - meta: { - ...meta, - className - }, - enableResizing, - size, - enableHiding - }; - }); - - const isSelectable = onToggleRow || onSelectRow; - - const select: ColumnDef[] = isSelectable - ? [ - { - id: "datatable-select-column", - header: ({ table }) => { - if (!canSelectAllRows) { - return null; - } - - return ( - !loadingInitial && ( - table.toggleAllPageRowsSelected(e)} - /> - ) - ); - }, - cell: info => { - if (!info.row.getCanSelect()) { - return <>; - } - return ( - - ); - }, - enableSorting: false, - enableResizing: false, - enableHiding: false, - size: 56 - } - ] - : []; - - return [...select, ...defaults].map(column => { - if (loadingInitial) { - return { - ...column, - cell: () => - }; - } - - return column; - }); - }, [columns, onSelectRow, onToggleRow, loadingInitial]); -}; - -const ColumnDirection = ({ direction }: ColumnDirectionProps): ReactElement | null => { - if (direction) { - return ( - - - - ); - } - - return null; -}; - -const typedMemo: (component: T) => T = memo; - -interface TableCellProps { - cell: Cell; - getColumnWidth: (column: DefaultColumn) => number; - selected: boolean; -} - -const TableCell = ({ cell, getColumnWidth }: TableCellProps) => { - const width = getColumnWidth(cell.column); - - return ( - - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - - ); -}; - -const MemoTableCell = typedMemo(TableCell); - -interface TableRowProps { - selected: boolean; - cells: Cell[]; - getColumnWidth: (column: DefaultColumn) => number; -} - -const TableRow = ({ selected, cells, getColumnWidth }: TableRowProps) => { - return ( - - {cells.map(cell => ( - - key={cell.id} - cell={cell} - getColumnWidth={getColumnWidth} - selected={selected} - /> - ))} - - ); -}; - -const MemoTableRow = typedMemo(TableRow); - /** - * Empty array must be defined outside of the React component so it does not force rerendering of the DataTable + * @deprecated This component is deprecated and will be removed in future releases. + * Please use the `DataTable` component from the `@webiny/admin-ui` package instead. */ -const emptyArray = Array(10).fill({}); - -export const DataTable = & DefaultData>({ - data: initialData, - columns: initialColumns, - onSelectRow, - onToggleRow, +const DataTable = & DataTableDefaultData>({ loadingInitial, - stickyColumns, stickyRows, - bordered, - sorting, - columnVisibility, - onColumnVisibilityChange, - onSortingChange, - isRowSelectable, - canSelectAllRows = true, - selectedRows = [], - initialSorting -}: Props) => { - const tableRef = useRef(null); - const [tableWidth, setTableWidth] = useState(1); - - const data = loadingInitial ? emptyArray : initialData; - - useEffect(() => { - const updateElementWidth = () => { - if (tableRef.current) { - const width = tableRef.current.clientWidth; - setTableWidth(width); - } - }; - - updateElementWidth(); - - window.addEventListener("resize", updateElementWidth); - - return () => { - window.removeEventListener("resize", updateElementWidth); - }; - }, [tableRef.current]); - - const rowSelection = useMemo(() => { - return selectedRows.reduce((acc, item) => { - const recordIndex = data.findIndex(rec => rec.id === item.id); - return { ...acc, [recordIndex]: true }; - }, {}); - }, [selectedRows, data]); - - const onRowSelectionChange: OnChangeFn = updater => { - const newSelection = typeof updater === "function" ? updater(rowSelection) : updater; - - /** - * `@tanstack/react-table` isn't telling us what row was selected or deselected. It simply gives us - * the new selection state (an object with row indexes that are currently selected). - * - * To figure out what row was toggled, we need to calculate the difference between the old selection - * and the new selection. What we're doing here is: - * - find all items that were present in the previous selection, but are no longer present in the new selection - * - find all items that are present in the new selection, but were not present in the previous selection - */ - const toggledRows = [ - ...Object.keys(rowSelection).filter(x => !(x in newSelection)), - ...Object.keys(newSelection).filter(x => !(x in rowSelection)) - ]; - - // If the difference is only 1 item, and `onToggleRow` is available, execute that. - if (toggledRows.length === 1 && typeof onToggleRow === "function") { - onToggleRow(data[parseInt(toggledRows[0])]); - return; - } else if (typeof onSelectRow === "function") { - const selection = Object.keys(newSelection).map(key => data[parseInt(key)]); - onSelectRow(selection); - } - }; - - const tableSorting = useMemo(() => { - if (!Array.isArray(sorting) || !sorting.length) { - return initialSorting; - } - return sorting; - }, [sorting]); - - const columns = defineColumns(initialColumns, { - canSelectAllRows, - onSelectRow, - onToggleRow, - loadingInitial - }); - - const table = useReactTable({ - data, - columns, - enableColumnResizing: true, - columnResizeMode: "onChange", - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - state: { - rowSelection, - sorting: tableSorting, - columnVisibility - }, - enableRowSelection: isRowSelectable, - onRowSelectionChange, - enableSorting: !!onSortingChange, - enableSortingRemoval: false, - manualSorting: true, - onSortingChange, - enableHiding: !!onColumnVisibilityChange, - onColumnVisibilityChange - }); - - const getColumnWidth = useCallback( - (column: DefaultColumn): number => { - if (!column.getCanResize()) { - return column.getSize(); - } - - const tableSize = table.getTotalSize(); - const columnSize = column.getSize(); - - return Math.ceil((columnSize * tableWidth) / tableSize); - }, - [table, tableWidth] - ); - /** - * Had to memoize the rows to avoid browser freeze. - */ - const tableRows = useMemo(() => { - return table.getRowModel().rows; - }, [table, data, columns]); - - return ( -
-
+); + +export { Caption }; diff --git a/packages/admin-ui/src/Table/components/Cell.tsx b/packages/admin-ui/src/Table/components/Cell.tsx new file mode 100644 index 00000000000..de6e2cec3b9 --- /dev/null +++ b/packages/admin-ui/src/Table/components/Cell.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; +import { cn } from "~/utils"; + +const Cell = ({ className, ...props }: React.TdHTMLAttributes) => ( +
+); + +export { Cell }; diff --git a/packages/admin-ui/src/Table/components/Direction.tsx b/packages/admin-ui/src/Table/components/Direction.tsx new file mode 100644 index 00000000000..a2f884044c2 --- /dev/null +++ b/packages/admin-ui/src/Table/components/Direction.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { ReactComponent as ArrowDown } from "@material-design-icons/svg/outlined/keyboard_arrow_up.svg"; +import { Icon } from "~/Icon"; +import { cn, cva, VariantProps } from "~/utils"; + +const directionVariants = cva("inline", { + variants: { + direction: { + asc: "rotate-0", + desc: "rotate-180" + } + } +}); + +interface DirectionProps + extends React.HTMLAttributes, + VariantProps {} + +const Direction = ({ className, direction }: DirectionProps) => { + if (!direction) { + return null; + } + + return ( + } + label={"Sort column"} + color={"neutral-strong"} + /> + ); +}; + +export { Direction, type DirectionProps }; diff --git a/packages/admin-ui/src/Table/components/Footer.tsx b/packages/admin-ui/src/Table/components/Footer.tsx new file mode 100644 index 00000000000..91e035dc03e --- /dev/null +++ b/packages/admin-ui/src/Table/components/Footer.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { cn } from "~/utils"; + +const Footer = ({ className, ...props }: React.HTMLAttributes) => ( +
+ {children} +
- - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map((header, index) => { - const isLastCell = index === headerGroup.headers.length - 1; - const width = getColumnWidth(header.column); + ...props +}: DataTableProps) => { + return ; +}; - return ( - - {header.isPlaceholder ? null : ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - {isLastCell && ( - - )} - - )} - {header.column.getCanResize() && ( - - )} - - ); - })} - - ))} - - - {tableRows.map(row => { - const id = row.original.id || row.id; - return ( - - key={id} - cells={row.getVisibleCells()} - selected={row.getIsSelected()} - getColumnWidth={getColumnWidth} - /> - ); - })} - - -
-
- ); +export { + DataTable, + type Column, + type ColumnSort, + type ColumnVisibility, + type Columns, + type DefaultData, + type OnColumnVisibilityChange, + type OnSortingChange, + type Sorting, + type TableRow }; diff --git a/packages/ui/src/DataTable/README.md b/packages/ui/src/DataTable/README.md deleted file mode 100644 index ea0d2535a9b..00000000000 --- a/packages/ui/src/DataTable/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# DataTable - -### Design - -https://material.io/components/data-tables - -### Description - -Use `DataTable` components to display sets of data across rows and columns. - -### Usage - -```tsx -import { DataTable, Columns } from "@webiny/ui/DataTable"; - -// Declare the data structure. -interface Entry { - name: string; - createdBy: string; - lastModified: string; - status: string; -} - -// Define the data you want to display. -const data: Entry[] = [ - { - name: "Page 1", - createdBy: "John Doe", - lastModified: "3 days ago", - status: "Draft" - }, - { - name: "Page 2", - createdBy: "John Doe", - lastModified: "1 day ago", - status: "Published" - }, - { - name: "Page 3", - createdBy: "John Doe", - lastModified: "1 hour ago", - status: "Published" - } -]; - -// Define the columns structure for your table. -const columns: Columns = { - name: { - header: "Title" - }, - createdBy: { - header: "Author", - cell: row => {row.createdBy.toUpperCase()} - }, - lastModified: { - header: "Last Modified" - }, - status: { - header: "Status", - meta: { - alignEnd: true - } - } -}; - -... - -// Use the component within your code. -return( - -) -``` diff --git a/packages/ui/src/DataTable/styled.tsx b/packages/ui/src/DataTable/styled.tsx deleted file mode 100644 index f67d5cc331e..00000000000 --- a/packages/ui/src/DataTable/styled.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { CSSProperties } from "react"; -import styled from "@emotion/styled"; - -import { ReactComponent as ArrowDown } from "@material-design-icons/svg/outlined/arrow_downward.svg"; -import { - DataTable as RmwcDataTable, - DataTableCell as RmwcDataTableCell, - DataTableHeadCell as RmwcDataTableHeadCell, - DataTableProps, - DataTableHeadCellProps, - DataTableCellProps -} from "@rmwc/data-table"; -import { ColumnDirectionProps } from "~/DataTable/ColumnDirection"; -import { Typography } from "~/Typography"; - -interface TableProps extends DataTableProps { - bordered?: boolean; -} - -export const Table = styled(RmwcDataTable)` - overflow-y: hidden; - overflow-x: scroll; - max-width: 100%; - display: block !important; - border-width: ${props => (props.bordered ? "1px" : "0px")}; - - th, - td { - vertical-align: middle; - } -`; - -interface ResizerProps { - isResizing: boolean; -} - -export const Resizer = styled("div")` - position: absolute; - right: 0; - top: 0; - height: 100%; - width: 4px; - background: ${props => - props.isResizing - ? "var(--mdc-theme-text-hint-on-light)" - : "var(--mdc-theme-on-background)"}; - cursor: col-resize; - user-select: none; - touch-action: none; - opacity: ${props => (props.isResizing ? 1 : 0)}; -`; - -interface TableHeadCell extends DataTableHeadCellProps { - colSpan: number; - style?: CSSProperties; -} - -export const TableHeadCell = styled(RmwcDataTableHeadCell)` - position: relative; - width: auto; - - &:hover ${Resizer} { - opacity: 1; - } -`; - -interface DataTableCell extends DataTableCellProps { - style?: CSSProperties; -} - -export const DataTableCell = styled(RmwcDataTableCell)` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; - -export const ColumnCellWrapper = styled.div` - display: flex; - align-items: center; - justify-content: start; - - // TODO: refactor in case of RMWC removal - .rmwc-data-table__cell--align-start & { - justify-content: start; - } - - .rmwc-data-table__cell--align-middle & { - justify-content: center; - } - - .rmwc-data-table__cell--align-end & { - justify-content: flex-end; - } - - // Needed to solve skeleton width equal to 0 (https://github.com/dvtng/react-loading-skeleton#troubleshooting) - .table-skeleton-container { - flex: 1; - } -`; - -interface ColumnHeaderWrapperProps { - sortable: boolean; - isLastCell: boolean; -} - -export const ColumnHeaderWrapper = styled("div")` - cursor: ${props => (props.sortable ? "pointer" : "cursor")}; - display: flex; - align-items: center; - justify-content: ${props => (props.isLastCell ? "flex-end" : "start")}; - - // TODO: refactor in case of RMWC removal - .rmwc-data-table__cell--align-start & { - justify-content: start; - } - - .rmwc-data-table__cell--align-middle & { - justify-content: center; - } - - .rmwc-data-table__cell--align-end & { - justify-content: flex-end; - } -`; - -export const ColumnDirectionWrapper = styled("span")` - display: inline-flex; - align-items: center; - justify-content: center; - height: 16px; - width: 16px; - margin: 0 0 0 4px; -`; - -export const ColumnDirectionIcon = styled(ArrowDown)` - transform: ${props => (props.direction === "asc" ? "rotate(180deg)" : "rotate(0deg)")}; -`; - -export const ColumnsVisibilityMenuHeader = styled(Typography)` - padding: 4px 16px 12px; - font-weight: 600; -`; - -export const ColumnsVisibilityMenuItem = styled("div")` - padding: 0 16px; -`; diff --git a/packages/ui/src/List/DataList/Loader.tsx b/packages/ui/src/List/DataList/Loader.tsx index a77f9960837..f01a0336720 100644 --- a/packages/ui/src/List/DataList/Loader.tsx +++ b/packages/ui/src/List/DataList/Loader.tsx @@ -13,39 +13,37 @@ const LoaderWrapper = styled("div")` display: flex; width: 100%; align-items: center; - justify-content: space-around; + justify-content: start; + gap: 24px; `; const Graphic = styled("div")` width: 36px; + height: 36px; `; const Data = styled("div")` - width: calc(-42px + 75%); + flex: 1; + height: 36px; + display: flex; + flex-direction: column; + justify-content: space-between; +`; - .data-skeleton-container { - height: 36px; - display: flex; - flex-direction: column; - justify-content: space-between; - } +const ActionsContainer = styled("div")` + justify-self: end; `; const Actions = styled("div")` - width: calc(-42px + 25%); - margin-left: 10px; text-align: right; - - .actions-skeleton-container { - height: 24px; - display: flex; - justify-content: end; - } + height: 24px; + display: flex; + justify-content: end; + gap: 8px; .actions-skeleton { width: 24px; height: 24px; - margin-left: 16px; } `; @@ -58,23 +56,19 @@ const Loader = (): ReactElement => {
  • - + - + + - - - + + {" "} + + + + +
  • ))} diff --git a/packages/ui/src/Skeleton/README.md b/packages/ui/src/Skeleton/README.md deleted file mode 100644 index 369b708cf96..00000000000 --- a/packages/ui/src/Skeleton/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Skeleton - -### Additional information - -[https://www.npmjs.com/package/react-loading-skeleton](https://www.npmjs.com/package/react-loading-skeleton) - -### Description - -`Skeleton` component is used to render animated loading skeletons within Webiny. - -### Usage - -```jsx -import { Skeleton } from "@webiny/ui/Skeleton"; - -... - -return ( - -) -``` diff --git a/packages/ui/src/Skeleton/Skeleton.tsx b/packages/ui/src/Skeleton/Skeleton.tsx index a2c6ff477ac..2f1ce382dc2 100644 --- a/packages/ui/src/Skeleton/Skeleton.tsx +++ b/packages/ui/src/Skeleton/Skeleton.tsx @@ -1,26 +1,33 @@ -import React, { ReactElement } from "react"; -import ReactLoadingSkeleton, { - SkeletonProps as BaseSkeletonProps, - SkeletonTheme -} from "react-loading-skeleton"; +import React, { CSSProperties, PropsWithChildren } from "react"; +import { Skeleton as AdminSkeleton } from "@webiny/admin-ui"; -import "react-loading-skeleton/dist/skeleton.css"; - -interface SkeletonProps extends BaseSkeletonProps { +export interface SkeletonProps { + //SkeletonStyleProps from "react-loading-skeleton" + baseColor?: string; + highlightColor?: string; + width?: string | number; + height?: string | number; + borderRadius?: string | number; + inline?: boolean; + duration?: number; + direction?: "ltr" | "rtl"; + enableAnimation?: boolean; + // SkeletonProps from "react-loading-skeleton" + count?: number; + wrapper?: React.FunctionComponent>; + className?: string; + containerClassName?: string; + containerTestId?: string; + circle?: boolean; + style?: CSSProperties; + // Custom props theme?: "dark" | "light"; } -export const Skeleton = ({ theme, ...props }: SkeletonProps): ReactElement => { - return ( - - - - ); +/** + * @deprecated This component is deprecated and will be removed in future releases. + * Please use the `Skeleton` component from the `@webiny/admin-ui` package instead. + */ +export const Skeleton = ({ className }: SkeletonProps) => { + return ; }; diff --git a/yarn.lock b/yarn.lock index 48fc68b9f20..4a301c74286 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13005,6 +13005,18 @@ __metadata: languageName: node linkType: hard +"@tanstack/react-table@npm:^8.20.6": + version: 8.20.6 + resolution: "@tanstack/react-table@npm:8.20.6" + dependencies: + "@tanstack/table-core": "npm:8.20.5" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 10/7ff2daf480bf2135ed072152cd8bc3f8ac342ca333831fd1b28603a8a8da0a858609d2fca18804f83ab62bbcf28041d5b98a0f2b7b283ec7229f99a57047a49f + languageName: node + linkType: hard + "@tanstack/react-table@npm:^8.5.22": version: 8.7.9 resolution: "@tanstack/react-table@npm:8.7.9" @@ -13017,6 +13029,13 @@ __metadata: languageName: node linkType: hard +"@tanstack/table-core@npm:8.20.5": + version: 8.20.5 + resolution: "@tanstack/table-core@npm:8.20.5" + checksum: 10/5408237920d5796951e925278edbbe76f71006627a4e3da248a810970256f75d973538fe7ae75a32155d4a25a95abc4fffaea337b5120f7940d7e664dc9da87f + languageName: node + linkType: hard + "@tanstack/table-core@npm:8.7.9": version: 8.7.9 resolution: "@tanstack/table-core@npm:8.7.9" @@ -15300,6 +15319,7 @@ __metadata: "@storybook/react-webpack5": "npm:7.6.20" "@storybook/theming": "npm:7.6.20" "@svgr/webpack": "npm:^6.1.1" + "@tanstack/react-table": "npm:^8.20.6" "@types/react": "npm:18.2.79" "@webiny/cli": "npm:0.0.0" "@webiny/project-utils": "npm:0.0.0" @@ -19626,8 +19646,6 @@ __metadata: "@emotion/babel-plugin": "npm:^11.11.0" "@emotion/react": "npm:11.10.8" "@emotion/styled": "npm:11.10.6" - "@material-design-icons/svg": "npm:^0.14.3" - "@rmwc/data-table": "npm:^14.2.2" "@rmwc/drawer": "npm:^14.2.2" "@rmwc/grid": "npm:^14.2.2" "@rmwc/list": "npm:^14.2.2" @@ -19636,7 +19654,6 @@ __metadata: "@rmwc/touch-target": "npm:^14.2.2" "@rmwc/typography": "npm:^14.2.2" "@svgr/webpack": "npm:^6.1.1" - "@tanstack/react-table": "npm:^8.5.22" "@testing-library/react": "npm:^15.0.7" "@types/react-color": "npm:^2.17.11" "@types/react-custom-scrollbars": "npm:^4.0.10" @@ -19664,7 +19681,6 @@ __metadata: react-color: "npm:^2.19.3" react-columned: "npm:^1.1.3" react-custom-scrollbars: "npm:^4.2.1" - react-loading-skeleton: "npm:^3.1.0" react-transition-group: "npm:^4.4.5" rimraf: "npm:^6.0.1" timeago-react: "npm:^3.0.6"