Compare commits

...

1 Commits

Author SHA1 Message Date
Sloan Sparger
4266144274 hackin', yah know? 2024-08-30 10:26:04 -05:00
9 changed files with 316 additions and 544 deletions

View File

@@ -1,10 +1,13 @@
import type { PropsWithChildren } from "react";
import { type PropsWithChildren } from "react";
import { useSelector } from "metabase/lib/redux";
import { getIsDndAvailable } from "metabase/selectors/app";
import type { BaseItemsTableProps } from "metabase/components/ItemsTable/BaseItemsTable";
import { DefaultItemRenderer } from "metabase/components/ItemsTable/DefaultItemRenderer";
import ItemDragSource from "metabase/containers/dnd/ItemDragSource";
import type { CollectionItem } from "metabase-types/api";
// TODO: reduce props to what are actually needed
type BaseItemTableRowProps = PropsWithChildren<
{
testIdPrefix: string;
@@ -19,7 +22,6 @@ type BaseItemTableRowProps = PropsWithChildren<
| "collection"
| "selectedItems"
| "onDrop"
| "ItemComponent"
| "databases"
| "bookmarks"
| "createBookmark"
@@ -33,91 +35,37 @@ type BaseItemTableRowProps = PropsWithChildren<
>;
export const TableRow = ({
testIdPrefix,
databases,
bookmarks,
createBookmark,
deleteBookmark,
ItemComponent = DefaultItemRenderer,
isPinned,
onCopy,
onMove,
onToggleSelected,
item,
isSelected,
itemKey,
collection,
onClick,
showActionMenu,
}: BaseItemTableRowProps) => (
<tr key={itemKey} data-testid={testIdPrefix} style={{ height: 48 }}>
<ItemComponent
onClick={onClick}
testIdPrefix={testIdPrefix}
item={item}
isSelected={isSelected}
databases={databases}
bookmarks={bookmarks}
createBookmark={createBookmark}
deleteBookmark={deleteBookmark}
collection={collection}
isPinned={isPinned}
onCopy={onCopy}
onMove={onMove}
onToggleSelected={onToggleSelected}
showActionMenu={showActionMenu}
/>
</tr>
);
export const ItemDragSourceTableRow = ({
testIdPrefix,
databases,
bookmarks,
createBookmark,
deleteBookmark,
ItemComponent = DefaultItemRenderer,
isPinned,
onCopy,
onMove,
onToggleSelected,
item,
isSelected,
item,
itemKey,
collection,
onClick,
selectedItems,
onDrop,
showActionMenu,
selectedItems,
testIdPrefix,
children,
}: BaseItemTableRowProps) => {
return (
<ItemDragSource
item={item}
collection={collection}
isSelected={isSelected}
selected={selectedItems}
onDrop={onDrop}
key={`item-drag-source-${itemKey}`}
>
{/* We can't use <TableRow> due to React DnD throwing an error: Only native element nodes can now be passed to React DnD connectors. */}
<tr key={itemKey} data-testid={testIdPrefix} style={{ height: 48 }}>
<ItemComponent
testIdPrefix={testIdPrefix}
item={item}
isSelected={isSelected}
databases={databases}
bookmarks={bookmarks}
createBookmark={createBookmark}
deleteBookmark={deleteBookmark}
collection={collection}
isPinned={isPinned}
onCopy={onCopy}
onMove={onMove}
onToggleSelected={onToggleSelected}
onClick={onClick}
showActionMenu={showActionMenu}
/>
</tr>
</ItemDragSource>
const isDndAvailable = useSelector(getIsDndAvailable);
const tr = (
<tr key={itemKey} data-testid={testIdPrefix} style={{ height: 48 }}>
{children}
</tr>
);
if (isDndAvailable) {
return (
<ItemDragSource
item={item}
collection={collection}
isSelected={isSelected}
selected={selectedItems}
onDrop={onDrop}
key={`item-drag-source-${itemKey}`}
>
{tr}
</ItemDragSource>
);
}
return tr;
};

View File

@@ -1,5 +1,10 @@
import type { HTMLAttributes, PropsWithChildren } from "react";
import { useMemo } from "react";
import type { HTMLAttributes, PropsWithChildren } from "react";
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import type {
CreateBookmark,
@@ -9,9 +14,6 @@ import type {
OnToggleSelectedWithItem,
} from "metabase/collections/types";
import { isTrashedCollection } from "metabase/collections/utils";
import { BaseItemsTableBody } from "metabase/components/ItemsTable/BaseItemsTableBody/BaseItemsTableBody";
import type { ItemRendererProps } from "metabase/components/ItemsTable/DefaultItemRenderer";
import { DefaultItemRenderer } from "metabase/components/ItemsTable/DefaultItemRenderer";
import type Database from "metabase-lib/v1/metadata/Database";
import type { Bookmark, Collection, CollectionItem } from "metabase-types/api";
import {
@@ -19,6 +21,7 @@ import {
SortDirection,
type SortingOptions,
} from "metabase-types/api/sorting";
import { TableRow } from "metabase/components/ItemsTable/BaseItemTableRow";
import {
ColumnHeader,
@@ -26,8 +29,9 @@ import {
SortingIcon,
Table,
} from "../BaseItemsTable.styled";
import { Columns } from "../Columns";
import { getColumns } from "../Columns";
import type { ResponsiveProps } from "../utils";
import { TBody } from "../BaseItemsTable.styled";
export type SortableColumnHeaderProps = {
name?: SortColumn;
@@ -116,7 +120,6 @@ export type BaseItemsTableProps = {
/** Used for dragging */
headless?: boolean;
isInDragLayer?: boolean;
ItemComponent?: (props: ItemRendererProps) => JSX.Element;
includeColGroup?: boolean;
onClick?: (item: CollectionItem) => void;
showActionMenu?: boolean;
@@ -143,7 +146,6 @@ export const BaseItemsTable = ({
getIsSelected = () => false,
headless = false,
isInDragLayer = false,
ItemComponent = DefaultItemRenderer,
includeColGroup = true,
showActionMenu = true,
onClick,
@@ -153,77 +155,104 @@ export const BaseItemsTable = ({
collection?.can_write && typeof onToggleSelected === "function";
const isTrashed = !!collection && isTrashedCollection(collection);
const table = useReactTable({
data: items,
columns: getColumns({
sortingOptions,
onSortingOptionsChange,
isTrashed,
collection,
databases,
bookmarks,
onCopy,
onMove,
createBookmark,
deleteBookmark,
showActionMenu,
selectedItems,
hasUnselected,
onSelectAll,
onSelectNone,
onToggleSelected,
canSelect,
isInDragLayer,
getIsSelected,
}),
getCoreRowModel: getCoreRowModel(),
});
return (
<Table isInDragLayer={isInDragLayer} {...props}>
{includeColGroup && (
<colgroup>
{canSelect && <Columns.Select.Col />}
<Columns.Type.Col />
<Columns.Name.Col isInDragLayer={isInDragLayer} />
<Columns.LastEditedBy.Col />
<Columns.LastEditedAt.Col />
{showActionMenu && <Columns.ActionMenu.Col />}
<Columns.RightEdge.Col />
</colgroup>
)}
{includeColGroup &&
table
.getHeaderGroups()
.map(headerGroup => (
<colgroup key={headerGroup.id}>
{headerGroup.headers.map(header =>
header.column.columnDef.size !== 0 ? (
<col
key={header.id}
style={{ width: header.column.columnDef.size }}
/>
) : null,
)}
</colgroup>
))}
{!headless && (
<thead
data-testid={
isPinned ? "pinned-items-table-head" : "items-table-head"
}
>
<tr>
{canSelect && (
<Columns.Select.Header
selectedItems={selectedItems}
hasUnselected={hasUnselected}
onSelectAll={onSelectAll}
onSelectNone={onSelectNone}
/>
)}
<Columns.Type.Header
sortingOptions={sortingOptions}
onSortingOptionsChange={onSortingOptionsChange}
/>
<Columns.Name.Header
sortingOptions={sortingOptions}
onSortingOptionsChange={onSortingOptionsChange}
/>
<Columns.LastEditedBy.Header
sortingOptions={sortingOptions}
onSortingOptionsChange={onSortingOptionsChange}
isTrashed={isTrashed}
/>
<Columns.LastEditedAt.Header
sortingOptions={sortingOptions}
onSortingOptionsChange={onSortingOptionsChange}
isTrashed={isTrashed}
/>
{showActionMenu && <Columns.ActionMenu.Header />}
<Columns.RightEdge.Header />
</tr>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header =>
flexRender(header.column.columnDef.header, header.getContext()),
)}
</tr>
))}
</thead>
)}
<BaseItemsTableBody
items={items}
getIsSelected={getIsSelected}
isPinned={isPinned}
collection={collection}
selectedItems={selectedItems}
onDrop={onDrop}
ItemComponent={ItemComponent}
databases={databases}
bookmarks={bookmarks}
createBookmark={createBookmark}
deleteBookmark={deleteBookmark}
onCopy={onCopy}
onMove={onMove}
onToggleSelected={onToggleSelected}
onClick={onClick}
showActionMenu={showActionMenu}
/>
<TBody>
{table.getRowModel().rows.map(row => {
const item = row.original;
const isSelected = getIsSelected(item);
const testIdPrefix = `${isPinned ? "pinned-" : ""}collection-entry`;
const itemKey = `${item.model}-${item.id}`;
return (
<TableRow
key={itemKey}
itemKey={itemKey}
testIdPrefix={testIdPrefix}
item={item}
isSelected={isSelected}
selectedItems={selectedItems}
onDrop={onDrop}
collection={collection}
databases={databases}
bookmarks={bookmarks}
createBookmark={createBookmark}
deleteBookmark={deleteBookmark}
onCopy={onCopy}
onMove={onMove}
onToggleSelected={onToggleSelected}
items={items}
onClick={onClick}
showActionMenu={showActionMenu}
>
{row
.getVisibleCells()
.map(cell =>
flexRender(cell.column.columnDef.cell, cell.getContext()),
)}
</TableRow>
);
})}
</TBody>
</Table>
);
};
BaseItemsTable.Item = DefaultItemRenderer;

View File

@@ -1,86 +0,0 @@
import {
ItemDragSourceTableRow,
TableRow,
} from "metabase/components/ItemsTable/BaseItemTableRow";
import type { BaseItemsTableProps } from "metabase/components/ItemsTable/BaseItemsTable";
import { DefaultItemRenderer } from "metabase/components/ItemsTable/DefaultItemRenderer";
import { useSelector } from "metabase/lib/redux";
import { getIsDndAvailable } from "metabase/selectors/app";
import type { CollectionItem } from "metabase-types/api";
import { TBody } from "../BaseItemsTable.styled";
export const BaseItemsTableBody = ({
items,
getIsSelected = () => false,
isPinned,
collection,
selectedItems,
onDrop,
ItemComponent = DefaultItemRenderer,
databases,
bookmarks,
createBookmark,
deleteBookmark,
onCopy,
onMove,
onToggleSelected,
onClick,
showActionMenu,
}: Pick<
BaseItemsTableProps,
| "onClick"
| "items"
| "getIsSelected"
| "isPinned"
| "collection"
| "selectedItems"
| "onDrop"
| "ItemComponent"
| "databases"
| "bookmarks"
| "createBookmark"
| "deleteBookmark"
| "onCopy"
| "onMove"
| "onToggleSelected"
| "showActionMenu"
>) => {
const isDndAvailable = useSelector(getIsDndAvailable);
const TableRowComponent = isDndAvailable ? ItemDragSourceTableRow : TableRow;
return (
<TBody>
{items.map((item: CollectionItem) => {
const isSelected = getIsSelected(item);
const testIdPrefix = `${isPinned ? "pinned-" : ""}collection-entry`;
const itemKey = `${item.model}-${item.id}`;
return (
<TableRowComponent
key={itemKey}
itemKey={itemKey}
testIdPrefix={testIdPrefix}
item={item}
isSelected={isSelected}
selectedItems={selectedItems}
onDrop={onDrop}
collection={collection}
ItemComponent={ItemComponent}
databases={databases}
bookmarks={bookmarks}
createBookmark={createBookmark}
deleteBookmark={deleteBookmark}
onCopy={onCopy}
onMove={onMove}
onToggleSelected={onToggleSelected}
items={items}
onClick={onClick}
showActionMenu={showActionMenu}
/>
);
})}
</TBody>
);
};

View File

@@ -1,6 +1,7 @@
import type { PropsWithChildren } from "react";
import { useCallback, type PropsWithChildren } from "react";
import { c, t } from "ttag";
import { color } from "metabase/lib/colors";
import type { ActionMenuProps } from "metabase/collections/components/ActionMenu";
import ActionMenu from "metabase/collections/components/ActionMenu";
import DateTime from "metabase/components/DateTime";
@@ -14,10 +15,8 @@ import { useSelector } from "metabase/lib/redux";
import { getFullName } from "metabase/lib/user";
import { PLUGIN_MODERATION } from "metabase/plugins";
import { getIsEmbeddingSdk } from "metabase/selectors/embed";
import type { IconProps } from "metabase/ui";
import type { CollectionItem, SearchResult } from "metabase-types/api";
import type { CollectionItem } from "metabase-types/api";
import type { SortableColumnHeaderProps } from "./BaseItemsTable";
import { SortableColumnHeader } from "./BaseItemsTable";
import {
BulkSelectWrapper,
@@ -32,8 +31,7 @@ import {
RowActionsContainer,
TableColumn,
} from "./BaseItemsTable.styled";
type HeaderProps = Omit<SortableColumnHeaderProps, "name">;
import { createColumnHelper } from "@tanstack/react-table";
const ItemLinkComponent = ({
onClick,
@@ -55,92 +53,125 @@ const ItemLinkComponent = ({
);
};
export const Columns = {
Select: {
Col: () => <col style={{ width: "70px" }} />,
Header: ({
selectedItems,
hasUnselected,
onSelectAll,
onSelectNone,
}: {
selectedItems?: (CollectionItem | SearchResult)[];
hasUnselected?: boolean;
onSelectAll?: () => void;
onSelectNone?: () => void;
}) => (
<ColumnHeader>
<BulkSelectWrapper>
<CheckBox
checked={!!selectedItems?.length}
indeterminate={!!selectedItems?.length && !!hasUnselected}
onChange={hasUnselected ? onSelectAll : onSelectNone}
aria-label={t`Select all items`}
const columnHelper = createColumnHelper<CollectionItem>();
// TODO: make this not a function and hoist this up the table component so it doesn't have to be prop drilled
export const getColumns = ({
sortingOptions,
onSortingOptionsChange,
isTrashed,
collection,
databases,
bookmarks,
onCopy,
onMove,
createBookmark,
deleteBookmark,
showActionMenu,
selectedItems,
hasUnselected,
onSelectAll,
onSelectNone,
onToggleSelected,
canSelect,
isInDragLayer,
getIsSelected,
}: {
sortingOptions: any;
onSortingOptionsChange: any;
isTrashed: any;
showActionMenu: any;
selectedItems: any;
hasUnselected: any;
onSelectAll: any;
onSelectNone: any;
onToggleSelected: any;
canSelect: any;
isInDragLayer: any;
getIsSelected: any;
} & Omit<ActionMenuProps, "item">) => [
columnHelper.accessor(row => row, {
id: "select",
size: canSelect ? 70 : 0,
header: () =>
canSelect ? (
<ColumnHeader>
<BulkSelectWrapper>
<CheckBox
checked={!!selectedItems?.length}
indeterminate={!!selectedItems?.length && !!hasUnselected}
onChange={hasUnselected ? onSelectAll : onSelectNone}
aria-label={t`Select all items`}
/>
</BulkSelectWrapper>
</ColumnHeader>
) : null,
cell: ({ row }) => {
const item = row.original;
const icon = item.getIcon();
if (item.model === "card" || item.archived) {
icon.color = color("text-light");
}
const isPinned = false; // TODO: figure out how to determine if item is pinned
const isSelected = getIsSelected(item);
const handleSelectionToggled = useCallback(() => {
onToggleSelected?.(item);
}, [item, onToggleSelected]);
if (!canSelect) {
return null;
}
return (
<ItemCell data-testid={`table-check`}>
<EntityIconCheckBox
variant="list"
icon={icon}
pinned={isPinned}
selected={isSelected}
onToggleSelected={handleSelectionToggled}
selectable
showCheckbox
/>
</BulkSelectWrapper>
</ColumnHeader>
),
Cell: ({
testIdPrefix,
icon,
isPinned,
isSelected,
handleSelectionToggled,
}: {
testIdPrefix: string;
icon: IconProps;
isPinned?: boolean;
isSelected?: boolean;
handleSelectionToggled: () => void;
}) => (
<ItemCell data-testid={`${testIdPrefix}-check`}>
<EntityIconCheckBox
variant="list"
icon={icon}
pinned={isPinned}
selected={isSelected}
onToggleSelected={handleSelectionToggled}
selectable
showCheckbox
/>
</ItemCell>
),
},
Type: {
Col: () => <col style={{ width: "70px" }} />,
Header: ({
sortingOptions,
onSortingOptionsChange,
title = t`Type`,
}: HeaderProps) => (
</ItemCell>
);
},
}),
columnHelper.accessor(row => row.model, {
id: "type",
size: 70,
header: () => (
<SortableColumnHeader
name="model"
sortingOptions={sortingOptions}
onSortingOptionsChange={onSortingOptionsChange}
style={{ marginInlineStart: 6 }}
>
{title}
{t`Type`}
</SortableColumnHeader>
),
Cell: ({
testIdPrefix = "table",
icon,
isPinned,
}: {
testIdPrefix?: string;
icon: IconProps;
isPinned?: boolean;
}) => (
<ItemCell data-testid={`${testIdPrefix}-type`}>
<EntityIconCheckBox variant="list" icon={icon} pinned={isPinned} />
</ItemCell>
),
},
Name: {
Col: ({ isInDragLayer }: { isInDragLayer: boolean }) => (
<col style={{ width: isInDragLayer ? "10rem" : undefined }} />
),
Header: ({ sortingOptions, onSortingOptionsChange }: HeaderProps) => (
cell: ({ row }) => {
const item = row.original;
const isPinned = false; // TODO: figure out how to determine if item is pinned
const icon = item.getIcon();
if (item.model === "card" || item.archived) {
icon.color = color("text-light");
}
return (
<ItemCell data-testid={`table-type`}>
<EntityIconCheckBox variant="list" icon={icon} pinned={isPinned} />
</ItemCell>
);
},
}),
columnHelper.accessor(row => row.name, {
id: "name",
size: isInDragLayer ? 160 : undefined,
header: () => (
<SortableColumnHeader
name="name"
sortingOptions={sortingOptions}
@@ -149,56 +180,33 @@ export const Columns = {
{t`Name`}
</SortableColumnHeader>
),
Cell: ({
item,
testIdPrefix = "table",
includeDescription = true,
onClick,
}: {
item: CollectionItem;
testIdPrefix?: string;
includeDescription?: boolean;
onClick?: (item: CollectionItem) => void;
}) => {
return (
<ItemNameCell data-testid={`${testIdPrefix}-name`}>
<ItemLinkComponent onClick={onClick} item={item}>
<EntityItem.Name name={item.name} variant="list" />
<PLUGIN_MODERATION.ModerationStatusIcon
cell: ({ cell, row: { original: item } }) => (
<ItemNameCell data-testid={`tanstack-name`}>
<ItemLinkComponent onClick={() => console.log("TODO")} item={item}>
<EntityItem.Name name={cell.getValue()} variant="list" />
<PLUGIN_MODERATION.ModerationStatusIcon
size={16}
status={item.moderated_status}
/>
{item.description && (!!true || false) /*includeDescription*/ && (
<DescriptionIcon
name="info"
size={16}
status={item.moderated_status}
tooltip={
<Markdown dark disallowHeading unstyleLinks lineClamp={8}>
{item.description}
</Markdown>
}
/>
{item.description && includeDescription && (
<DescriptionIcon
name="info"
size={16}
tooltip={
<Markdown dark disallowHeading unstyleLinks lineClamp={8}>
{item.description}
</Markdown>
}
/>
)}
</ItemLinkComponent>
</ItemNameCell>
);
},
},
LastEditedBy: {
Col: () => (
<TableColumn
style={{ width: "140px" }}
hideAtContainerBreakpoint="sm"
containerName="ItemsTableContainer"
/>
)}
</ItemLinkComponent>
</ItemNameCell>
),
Header: ({
sortingOptions,
onSortingOptionsChange,
isTrashed,
}: HeaderProps & {
isTrashed: boolean;
}) => (
}),
columnHelper.accessor(row => row["last-edit-info"], {
id: "lastEditedBy",
size: 140,
header: () => (
<SortableColumnHeader
name="last_edited_by"
sortingOptions={sortingOptions}
@@ -211,42 +219,20 @@ export const Columns = {
: t`Last edited by`}
</SortableColumnHeader>
),
Cell: ({
testIdPrefix = "table",
item,
}: {
testIdPrefix?: string;
item: CollectionItem;
}) => {
const lastEditInfo = item["last-edit-info"];
const lastEditedBy = getLastEditedBy(lastEditInfo) ?? "";
return (
<ItemCell
data-testid={`${testIdPrefix}-last-edited-by`}
hideAtContainerBreakpoint="sm"
containerName="ItemsTableContainer"
>
<Ellipsified>{lastEditedBy}</Ellipsified>
</ItemCell>
);
},
},
LastEditedAt: {
Col: () => (
<TableColumn
style={{ width: "140px" }}
hideAtContainerBreakpoint="md"
cell: ({ cell }) => (
<ItemCell
data-testid={`table-last-edited-by`}
hideAtContainerBreakpoint="sm"
containerName="ItemsTableContainer"
/>
>
<Ellipsified>{getLastEditedBy(cell.getValue())}</Ellipsified>
</ItemCell>
),
Header: ({
sortingOptions,
onSortingOptionsChange,
isTrashed,
}: HeaderProps & {
isTrashed: boolean;
}) => (
}),
columnHelper.accessor(row => row["last-edit-info"], {
id: "lastEditedAt",
size: 140,
header: () => (
<SortableColumnHeader
name="last_edited_at"
sortingOptions={sortingOptions}
@@ -259,17 +245,11 @@ export const Columns = {
: t`Last edited at`}
</SortableColumnHeader>
),
Cell: ({
testIdPrefix,
item,
}: {
testIdPrefix: string;
item: CollectionItem;
}) => {
const lastEditInfo = item["last-edit-info"];
cell: ({ cell }) => {
const lastEditInfo = cell.getValue();
return (
<ItemCell
data-testid={`${testIdPrefix}-last-edited-at`}
data-testid={`table-last-edited-at`}
data-server-date
hideAtContainerBreakpoint="md"
containerName="ItemsTableContainer"
@@ -282,20 +262,14 @@ export const Columns = {
</ItemCell>
);
},
},
ActionMenu: {
Header: () => <th></th>,
Col: () => <col style={{ width: "100px" }} />,
Cell: ({
item,
collection,
databases,
bookmarks,
onCopy,
onMove,
createBookmark,
deleteBookmark,
}: ActionMenuProps) => {
}),
columnHelper.accessor(row => row, {
id: "actionMenu",
size: 100,
header: () => (showActionMenu ? <th></th> : null),
cell: ({ row: { original: item } }) => {
if (!showActionMenu) return null;
return (
<ItemCell>
<RowActionsContainer>
@@ -316,21 +290,22 @@ export const Columns = {
</ItemCell>
);
},
},
}),
/** Applies a border-radius to the right edge of the table.
* This is needed since columns can be hidden responsively,
* and so we can't just apply the border-radius to the last column in the DOM */
RightEdge: {
Header: () => <th></th>,
Col: () => <col style={{ width: "1rem" }} />,
Cell: () => <ItemCell />,
},
};
columnHelper.accessor(row => row, {
id: "rightEdge",
size: 16,
header: () => <th></th>,
cell: () => <ItemCell />,
}),
];
const getLastEditedBy = (lastEditInfo?: Edit) => {
const getLastEditedBy = (lastEditInfo?: Edit): string => {
if (!lastEditInfo) {
return "";
}
const name = getFullName(lastEditInfo);
return name || lastEditInfo.email;
return name || lastEditInfo.email || "";
};

View File

@@ -1,90 +0,0 @@
import { useCallback } from "react";
import type { ActionMenuProps } from "metabase/collections/components/ActionMenu";
import type { OnToggleSelectedWithItem } from "metabase/collections/types";
import type { BaseItemsTableProps } from "metabase/components/ItemsTable/BaseItemsTable";
import { Columns } from "metabase/components/ItemsTable/Columns";
import { color } from "metabase/lib/colors";
import type Database from "metabase-lib/v1/metadata/Database";
import type { Bookmark, Collection, CollectionItem } from "metabase-types/api";
export type ItemRendererProps = {
item: CollectionItem;
isSelected?: boolean;
isPinned?: boolean;
onToggleSelected?: OnToggleSelectedWithItem;
collection?: Collection;
draggable?: boolean;
testIdPrefix?: string;
databases?: Database[];
bookmarks?: Bookmark[];
} & ActionMenuProps &
Pick<BaseItemsTableProps, "onClick" | "showActionMenu">;
export const DefaultItemRenderer = ({
item,
isSelected,
isPinned,
onToggleSelected,
collection,
onCopy,
onMove,
createBookmark,
deleteBookmark,
databases,
bookmarks,
testIdPrefix = "item",
onClick,
showActionMenu,
}: ItemRendererProps) => {
const canSelect =
collection?.can_write && typeof onToggleSelected === "function";
const icon = item.getIcon();
if (item.model === "card" || item.archived) {
icon.color = color("text-light");
}
const handleSelectionToggled = useCallback(() => {
onToggleSelected?.(item);
}, [item, onToggleSelected]);
return (
<>
{canSelect && (
<Columns.Select.Cell
testIdPrefix={testIdPrefix}
icon={icon}
isPinned={isPinned}
isSelected={isSelected}
handleSelectionToggled={handleSelectionToggled}
/>
)}
<Columns.Type.Cell
testIdPrefix={testIdPrefix}
icon={icon}
isPinned={isPinned}
/>
<Columns.Name.Cell
item={item}
testIdPrefix={testIdPrefix}
onClick={onClick}
/>
<Columns.LastEditedBy.Cell item={item} testIdPrefix={testIdPrefix} />
<Columns.LastEditedAt.Cell item={item} testIdPrefix={testIdPrefix} />
{showActionMenu && (
<Columns.ActionMenu.Cell
item={item}
collection={collection}
databases={databases}
bookmarks={bookmarks}
onCopy={onCopy}
onMove={onMove}
createBookmark={createBookmark}
deleteBookmark={deleteBookmark}
/>
)}
<Columns.RightEdge.Cell />
</>
);
};

View File

@@ -1,5 +1,4 @@
import PinDropZone from "metabase/collections/components/PinDropZone";
import type { ItemRendererProps } from "metabase/components/ItemsTable/DefaultItemRenderer";
import CS from "metabase/css/core/index.css";
import type { CollectionItem } from "metabase-types/api";
@@ -7,28 +6,11 @@ import type { BaseItemsTableProps } from "./BaseItemsTable";
import { BaseItemsTable } from "./BaseItemsTable";
import { ItemsTableRoot } from "./ItemsTable.styled";
const Item = ({
item,
...props
}: {
item: CollectionItem;
} & ItemRendererProps) => {
return (
<BaseItemsTable.Item
key={`${item.model}-${item.id}`}
{...props}
item={item}
/>
);
};
export const ItemsTable = ({
items,
ItemComponent = Item,
...props
}: {
items: CollectionItem[];
ItemComponent?: (props: ItemRendererProps) => JSX.Element;
} & BaseItemsTableProps) => {
if (items.length === 0) {
return (
@@ -41,7 +23,7 @@ export const ItemsTable = ({
return (
<div className={CS.relative}>
<PinDropZone variant="unpin" />
<BaseItemsTable items={items} {...props} ItemComponent={ItemComponent} />
<BaseItemsTable items={items} {...props} />
</div>
);
};

View File

@@ -77,6 +77,7 @@ class DraggedItems extends Component {
return index >= 0;
};
// TODO: add back pinned items
renderItem = ({ item, ...itemProps }) => {
const isPinned = this.checkIsPinned(item);
const key = `${item.model}-${item.id}`;
@@ -94,6 +95,7 @@ class DraggedItems extends Component {
</td>
);
}
return (
<BaseItemsTable.Item
key={key}
@@ -119,7 +121,6 @@ class DraggedItems extends Component {
>
<BaseItemsTable
items={items}
ItemComponent={props => this.renderItem(props)}
headless
isInDragLayer
style={{ width: allPinned ? 400 : undefined }}

View File

@@ -23,6 +23,7 @@
"@react-oauth/google": "^0.11.1",
"@reduxjs/toolkit": "^2.2.5",
"@snowplow/browser-tracker": "^3.1.6",
"@tanstack/react-table": "^8.20.1",
"@tanstack/react-virtual": "^3.1.2",
"@tippyjs/react": "^4.2.6",
"@types/react-router": "3.0.27",

View File

@@ -4616,6 +4616,13 @@
dependencies:
defer-to-connect "^1.0.1"
"@tanstack/react-table@^8.20.1":
version "8.20.1"
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.20.1.tgz#bd2d549d8a18458fb8284025ce66a9b86176fa6b"
integrity sha512-PJK+07qbengObe5l7c8vCdtefXm8cyR4i078acWrHbdm8JKw1ES7YpmOtVt9ALUVEEFAHscdVpGRhRgikgFMbQ==
dependencies:
"@tanstack/table-core" "8.20.1"
"@tanstack/react-virtual@^3.1.2":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.1.2.tgz#eb62b73cc82e34860604cd3d682a17db590f3c45"
@@ -4623,6 +4630,11 @@
dependencies:
"@tanstack/virtual-core" "3.1.2"
"@tanstack/table-core@8.20.1":
version "8.20.1"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.1.tgz#74bfab10fa35bed51fa0bd2f3539a331d7e78f1b"
integrity sha512-5Ly5TIRHnWH7vSDell9B/OVyV380qqIJVg7H7R7jU4fPEmOD4smqAX7VRflpYI09srWR8aj5OLD2Ccs1pI5mTg==
"@tanstack/virtual-core@3.1.2":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.1.2.tgz#ca76f28f826fbd3310f88c3cd355d9c4aba80abb"