Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions packages/react/spec/auto/hooks/useTable.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ describe("useTable hook", () => {
"type": "String",
},
{
"field": undefined,
"header": "Custom column",
"identifier": "000-000-000-000-1",
"render": [Function],
Expand Down Expand Up @@ -849,6 +850,100 @@ describe("useTable hook", () => {
const customColumn2Result = render(<>{result.current[0].rows?.[0]?.[columnKeys[2]]}</>);
expect(customColumn2Result.container.textContent).toBe("some different stuff");
});

it("should not be sortable when no field is provided", async () => {
const result = getUseTableResult({
columns: [
"name",
{
header: "Custom column",
render: ({ record }) => <div>Custom: {record.name}</div>,
},
],
});
await loadMockWidgetModelMetadata();
await loadWidgetData();

const customColumn = result.current[0].columns?.find((col) => col.type === "CustomRenderer");
expect(customColumn?.sortable).toBe(false);
expect(customColumn?.field).toBeUndefined();
});

it("should be sortable when a sortable field is provided", async () => {
const result = getUseTableResult({
columns: [
{
header: "Custom Name",
render: ({ record }) => <div>Custom: {record.name}</div>,
field: "name",
},
],
});
await loadMockWidgetModelMetadata();
await loadWidgetData();

const customColumn = result.current[0].columns?.find((col) => col.type === "CustomRenderer");
expect(customColumn?.sortable).toBe(true);
expect(customColumn?.field).toBe("name");
});

it("should not be sortable when field is provided but sortable is set to false", async () => {
const result = getUseTableResult({
columns: [
{
header: "Custom Name",
render: ({ record }) => <div>Custom: {record.name}</div>,
field: "name",
sortable: false,
},
],
});
await loadMockWidgetModelMetadata();
await loadWidgetData();

const customColumn = result.current[0].columns?.find((col) => col.type === "CustomRenderer");
expect(customColumn?.sortable).toBe(false);
expect(customColumn?.field).toBe("name");
});

it("should not be sortable when the field itself is not sortable", async () => {
const result = getUseTableResult({
columns: [
{
header: "Custom ID",
render: ({ record }) => <div>Custom: {record.id}</div>,
field: "id",
},
],
});
await loadMockWidgetModelMetadata();
await loadWidgetData();

const customColumn = result.current[0].columns?.find((col) => col.type === "CustomRenderer");
expect(customColumn?.sortable).toBe(false);
expect(customColumn?.field).toBe("id");
});

it("should throw an error when trying to make a non-sortable field sortable", async () => {
let error: Error | undefined;
try {
getUseTableResult({
columns: [
{
header: "Custom ID",
render: ({ record }) => <div>Custom: {record.id}</div>,
field: "id",
sortable: true,
},
],
});
await loadMockWidgetModelMetadata();
} catch (err) {
error = err as Error;
}

expect(error!.message).toBe("Field 'id' is not sortable");
});
});

describe("select property", () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/auto/polaris/PolarisAutoTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const gadgetToPolarisDirection = (direction?: SortOrder) => {
};

const maybeGetColumnIndex = (columns: TableColumn[], apiIdentifier: string | undefined) => {
return columns.findIndex((column) => (column.type === "CustomRenderer" ? undefined : column.field === apiIdentifier));
return columns.findIndex((column) => column.field === apiIdentifier);
};

export type PolarisAutoTableProps<
Expand Down Expand Up @@ -130,7 +130,7 @@ const PolarisAutoTableComponent = <
const handleColumnSort = (headingIndex: number) => {
if (columns) {
const currentColumn = columns[headingIndex];
const columnApiIdentifier = currentColumn.type === "CustomRenderer" ? undefined : currentColumn.field;
const columnApiIdentifier = currentColumn.field;
if (columnApiIdentifier) {
sort.handleColumnSort(columnApiIdentifier);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/auto/shadcn/ShadcnAutoTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,12 @@ export const makeAutoTable = (elements: ShadcnElements) => {
<>
<TableHead key={column.identifier} className={`${stickyClass} bg-${isRowHovered ? "muted" : "background"}`}>
<div className={`flex flex-row items-center gap-2 z-10 `}>
{column.sortable ? (
{column.sortable && column.field ? (
<Button
variant="ghost"
size="sm"
className="cursor-pointer"
onClick={() => (column.type === "CustomRenderer" ? undefined : sort.handleColumnSort(column.field))}
onClick={() => sort.handleColumnSort(column.field!)}
{...hoverProps}
>
{ColumnHeaderLabel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ export const makeShadcnAutoTableColumnSortIndicator = (elements: ShadcnElements)

function ShadcnAutoTableColumnSortIndicator(props: { column: TableColumn; sortState: SortState; isHovered: boolean }) {
const { column, sortState, isHovered } = props;
const columnField = column.field;

const handleSort = useCallback(() => {
if (column.type === "CustomRenderer") {
if (!columnField) {
return;
}
sortState.handleColumnSort(column.field);
}, [sortState, column.type, column.type === "CustomRenderer" ? undefined : column.field]);
sortState.handleColumnSort(columnField);
}, [sortState, columnField]);

if (!column.sortable || column.type === "CustomRenderer") {
if (!column.sortable || !columnField) {
return null;
}

const isSorted = sortState.column === column.field;
const isSorted = sortState.column === columnField;
const direction = sortState.direction;

return (
Expand Down
17 changes: 16 additions & 1 deletion packages/react/src/use-table/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,27 @@ export const getTableColumns = (spec: Pick<TableSpec, "fieldMetadataTree" | "tar
for (const [i, targetColumn] of spec.targetColumns.entries()) {
if (isCustomCellColumn(targetColumn)) {
const identifier = crypto.randomUUID();

// If field is provided, determine sortability based on field metadata
let sortable = false;
if (targetColumn.field) {
const fieldMetadata = maybeGetFieldMetadataByColumnPath(spec.fieldMetadataTree, targetColumn.field);
if (fieldMetadata) {
sortable = isColumnSortable({
fieldMetadata: { ...fieldMetadata, apiIdentifier: targetColumn.field },
sortable: targetColumn.sortable,
isRelationshipField: isRelationshipField(fieldMetadata),
});
}
}

columns.push({
identifier,
render: targetColumn.render,
header: targetColumn.header,
type: "CustomRenderer",
sortable: false,
sortable,
field: targetColumn.field,
style: targetColumn.style,
});

Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/use-table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export type TableColumn = {
type: "CustomRenderer";
/** Custom render function */
render: CustomCellRenderer;
/** Optional field path that this custom column sorts by */
field?: string;
}
);

Expand Down Expand Up @@ -161,6 +163,16 @@ export type RelatedFieldColumn = {
export type CustomCellColumn = {
header: ReactNode;
render: CustomCellRenderer;
/**
* Optional field path to enable sorting for this custom column.
* When provided, the column will be sortable by this field.
*/
field?: string;
/**
* Whether the column should be sortable. Only applies when `field` is provided.
* Defaults to true if the field is sortable in the model metadata.
*/
sortable?: boolean;
style?: React.CSSProperties;
};

Expand Down