Client Table update
* Columns are now specified using the ant design columns property instead of being Table.Column components. This should make this WoW a bit more sense, we're not pretending to be components anymore. column.tsx now only returns objects, not nodes. * Added filament/spool collapsing as a useTable queryOptions so that the output object is directly the one we want, which will make the types play better together. * Added a more unified record action system. This enables us to add actions to column rightclick/long press, which is better UX for mobile devices. * Actions column is now hidden on smaller screens thanks to the above bullet.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { Col, Row, Spin, Table } from "antd";
|
||||
import { ColumnProps as AntdColumnProps } from "antd/es/table";
|
||||
import { ColumnFilterItem } from "antd/es/table/interface";
|
||||
import { Button, Col, Dropdown, Row, Space, Spin } from "antd";
|
||||
import { ColumnFilterItem, ColumnType } from "antd/es/table/interface";
|
||||
import { getFiltersForField, typeFilters } from "../utils/filtering";
|
||||
import { TableState } from "../utils/saveload";
|
||||
import { getSortOrderForField, typeSorters } from "../utils/sorting";
|
||||
@@ -13,6 +12,7 @@ import SpoolIcon from "../icon_spool.svg?react";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { enrichText } from "../utils/parsing";
|
||||
import { UseQueryResult } from "@tanstack/react-query";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
@@ -27,7 +27,18 @@ const FilterDropdownLoading = () => {
|
||||
);
|
||||
};
|
||||
|
||||
interface BaseColumnProps<Obj> {
|
||||
interface Entity {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
link?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface BaseColumnProps<Obj extends Entity> {
|
||||
id: keyof Obj & string;
|
||||
dataId?: keyof Obj & string;
|
||||
i18ncat?: string;
|
||||
@@ -35,6 +46,7 @@ interface BaseColumnProps<Obj> {
|
||||
dataSource: Obj[];
|
||||
tableState: TableState;
|
||||
width?: number;
|
||||
actions?: (record: Obj) => Action[];
|
||||
}
|
||||
|
||||
interface FilteredColumnProps {
|
||||
@@ -55,22 +67,28 @@ interface CustomColumnProps<Obj> {
|
||||
) => React.HTMLAttributes<any> | React.TdHTMLAttributes<any>;
|
||||
}
|
||||
|
||||
function Column<Obj>(props: BaseColumnProps<Obj> & FilteredColumnProps & CustomColumnProps<Obj>) {
|
||||
function Column<Obj extends Entity>(
|
||||
props: BaseColumnProps<Obj> & FilteredColumnProps & CustomColumnProps<Obj>
|
||||
): ColumnType<Obj> | undefined {
|
||||
const t = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Hide if not in showColumns
|
||||
if (props.tableState.showColumns && !props.tableState.showColumns.includes(props.id)) {
|
||||
return <></>;
|
||||
return undefined;
|
||||
}
|
||||
const typedSorters = typeSorters<Obj>(props.tableState.sorters);
|
||||
const columnProps: AntdColumnProps<Obj> = {
|
||||
|
||||
const columnProps: ColumnType<Obj> = {
|
||||
dataIndex: props.id,
|
||||
title: t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`),
|
||||
sorter: true,
|
||||
sortOrder: getSortOrderForField(typedSorters, props.dataId ?? props.id),
|
||||
sortOrder: getSortOrderForField(typeSorters<Obj>(props.tableState.sorters), props.dataId ?? props.id),
|
||||
filterMultiple: props.allowMultipleFilters ?? true,
|
||||
width: props.width ?? undefined,
|
||||
onCell: props.onCell ?? undefined,
|
||||
};
|
||||
if (props.width) {
|
||||
columnProps.width = props.width;
|
||||
}
|
||||
|
||||
// Filter
|
||||
if (props.filters && props.filteredValue) {
|
||||
columnProps.filters = props.filters;
|
||||
columnProps.filteredValue = props.filteredValue;
|
||||
@@ -86,20 +104,50 @@ function Column<Obj>(props: BaseColumnProps<Obj> & FilteredColumnProps & CustomC
|
||||
columnProps.key = props.dataId;
|
||||
}
|
||||
}
|
||||
if (props.render) {
|
||||
columnProps.render = props.render;
|
||||
}
|
||||
if (props.onCell) {
|
||||
columnProps.onCell = props.onCell;
|
||||
}
|
||||
return <Table.Column<Obj> {...columnProps} />;
|
||||
|
||||
// Render
|
||||
const render = props.render ?? ((value) => <>{value}</>);
|
||||
columnProps.render = (value, record, index) => {
|
||||
if (!props.actions) {
|
||||
return render(value, record, index);
|
||||
}
|
||||
|
||||
const actions = props.actions(record);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: actions.map((action) => ({
|
||||
key: action.name,
|
||||
label: action.name,
|
||||
icon: action.icon,
|
||||
})),
|
||||
onClick: (item) => {
|
||||
const action = actions.find((action) => action.name === item.key);
|
||||
if (action) {
|
||||
if (action.link) {
|
||||
navigate(action.link);
|
||||
} else if (action.onClick) {
|
||||
action.onClick();
|
||||
}
|
||||
}
|
||||
},
|
||||
}}
|
||||
trigger={["contextMenu"]}
|
||||
>
|
||||
<div>{render(value, record, index)}</div>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
return columnProps;
|
||||
}
|
||||
|
||||
export function SortedColumn<Obj>(props: BaseColumnProps<Obj>) {
|
||||
export function SortedColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
|
||||
return Column(props);
|
||||
}
|
||||
|
||||
export function RichColumn<Obj>(props: BaseColumnProps<Obj>) {
|
||||
export function RichColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
|
||||
return Column({
|
||||
...props,
|
||||
render: (value: string | undefined) => {
|
||||
@@ -108,12 +156,12 @@ export function RichColumn<Obj>(props: BaseColumnProps<Obj>) {
|
||||
});
|
||||
}
|
||||
|
||||
interface FilteredQueryColumnProps<Obj> extends BaseColumnProps<Obj> {
|
||||
interface FilteredQueryColumnProps<Obj extends Entity> extends BaseColumnProps<Obj> {
|
||||
filterValueQuery: UseQueryResult<string[] | ColumnFilterItem[]>;
|
||||
allowMultipleFilters?: boolean;
|
||||
}
|
||||
|
||||
export function FilteredQueryColumn<Obj>(props: FilteredQueryColumnProps<Obj>) {
|
||||
export function FilteredQueryColumn<Obj extends Entity>(props: FilteredQueryColumnProps<Obj>) {
|
||||
const query = props.filterValueQuery;
|
||||
|
||||
let filters: ColumnFilterItem[] = [];
|
||||
@@ -143,13 +191,13 @@ export function FilteredQueryColumn<Obj>(props: FilteredQueryColumnProps<Obj>) {
|
||||
return Column({ ...props, filters, filteredValue, onFilterDropdownOpen, loadingFilters: query.isLoading });
|
||||
}
|
||||
|
||||
interface NumberColumnProps<Obj> extends BaseColumnProps<Obj> {
|
||||
interface NumberColumnProps<Obj extends Entity> extends BaseColumnProps<Obj> {
|
||||
unit: string;
|
||||
decimals?: number;
|
||||
defaultText?: string;
|
||||
}
|
||||
|
||||
export function NumberColumn<Obj>(props: NumberColumnProps<Obj>) {
|
||||
export function NumberColumn<Obj extends Entity>(props: NumberColumnProps<Obj>) {
|
||||
return Column({
|
||||
...props,
|
||||
render: (value) => {
|
||||
@@ -170,7 +218,7 @@ export function NumberColumn<Obj>(props: NumberColumnProps<Obj>) {
|
||||
});
|
||||
}
|
||||
|
||||
export function DateColumn<Obj>(props: BaseColumnProps<Obj>) {
|
||||
export function DateColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
|
||||
return Column({
|
||||
...props,
|
||||
render: (value) => {
|
||||
@@ -186,11 +234,43 @@ export function DateColumn<Obj>(props: BaseColumnProps<Obj>) {
|
||||
});
|
||||
}
|
||||
|
||||
interface SpoolIconColumnProps<Obj> extends FilteredQueryColumnProps<Obj> {
|
||||
export function ActionsColumn<Obj extends Entity>(actionsFn: (record: Obj) => Action[]): ColumnType<Obj> | undefined {
|
||||
const t = useTranslate();
|
||||
|
||||
return {
|
||||
title: t("table.actions"),
|
||||
responsive: ["lg"],
|
||||
render: (_, record) => {
|
||||
const buttons = actionsFn(record).map((action) => {
|
||||
if (action.link) {
|
||||
return (
|
||||
<Link key={action.name} to={action.link}>
|
||||
<Button icon={action.icon} title={action.name} size="small" />
|
||||
</Link>
|
||||
);
|
||||
} else if (action.onClick) {
|
||||
return (
|
||||
<Button
|
||||
key={action.name}
|
||||
icon={action.icon}
|
||||
title={action.name}
|
||||
size="small"
|
||||
onClick={() => action.onClick!()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return <Space>{buttons}</Space>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface SpoolIconColumnProps<Obj extends Entity> extends FilteredQueryColumnProps<Obj> {
|
||||
color: (record: Obj) => string | undefined;
|
||||
}
|
||||
|
||||
export function SpoolIconColumn<Obj>(props: SpoolIconColumnProps<Obj>) {
|
||||
export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<Obj>) {
|
||||
const query = props.filterValueQuery;
|
||||
|
||||
let filters: ColumnFilterItem[] = [];
|
||||
|
||||
Reference in New Issue
Block a user