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:
Donkie
2023-10-29 20:37:58 +01:00
parent e31aeebe97
commit 151c3c7f4f
5 changed files with 589 additions and 432 deletions

View File

@@ -1,6 +1,5 @@
import { Col, Row, Spin, Table } from "antd"; import { Button, Col, Dropdown, Row, Space, Spin } from "antd";
import { ColumnProps as AntdColumnProps } from "antd/es/table"; import { ColumnFilterItem, ColumnType } from "antd/es/table/interface";
import { ColumnFilterItem } from "antd/es/table/interface";
import { getFiltersForField, typeFilters } from "../utils/filtering"; import { getFiltersForField, typeFilters } from "../utils/filtering";
import { TableState } from "../utils/saveload"; import { TableState } from "../utils/saveload";
import { getSortOrderForField, typeSorters } from "../utils/sorting"; import { getSortOrderForField, typeSorters } from "../utils/sorting";
@@ -13,6 +12,7 @@ import SpoolIcon from "../icon_spool.svg?react";
import { useTranslate } from "@refinedev/core"; import { useTranslate } from "@refinedev/core";
import { enrichText } from "../utils/parsing"; import { enrichText } from "../utils/parsing";
import { UseQueryResult } from "@tanstack/react-query"; import { UseQueryResult } from "@tanstack/react-query";
import { Link, useNavigate } from "react-router-dom";
dayjs.extend(utc); 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; id: keyof Obj & string;
dataId?: keyof Obj & string; dataId?: keyof Obj & string;
i18ncat?: string; i18ncat?: string;
@@ -35,6 +46,7 @@ interface BaseColumnProps<Obj> {
dataSource: Obj[]; dataSource: Obj[];
tableState: TableState; tableState: TableState;
width?: number; width?: number;
actions?: (record: Obj) => Action[];
} }
interface FilteredColumnProps { interface FilteredColumnProps {
@@ -55,22 +67,28 @@ interface CustomColumnProps<Obj> {
) => React.HTMLAttributes<any> | React.TdHTMLAttributes<any>; ) => 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 t = useTranslate();
const navigate = useNavigate();
// Hide if not in showColumns
if (props.tableState.showColumns && !props.tableState.showColumns.includes(props.id)) { 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, dataIndex: props.id,
title: t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`), title: t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`),
sorter: true, sorter: true,
sortOrder: getSortOrderForField(typedSorters, props.dataId ?? props.id), sortOrder: getSortOrderForField(typeSorters<Obj>(props.tableState.sorters), props.dataId ?? props.id),
filterMultiple: props.allowMultipleFilters ?? true, 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) { if (props.filters && props.filteredValue) {
columnProps.filters = props.filters; columnProps.filters = props.filters;
columnProps.filteredValue = props.filteredValue; columnProps.filteredValue = props.filteredValue;
@@ -86,20 +104,50 @@ function Column<Obj>(props: BaseColumnProps<Obj> & FilteredColumnProps & CustomC
columnProps.key = props.dataId; columnProps.key = props.dataId;
} }
} }
if (props.render) {
columnProps.render = props.render; // Render
} const render = props.render ?? ((value) => <>{value}</>);
if (props.onCell) { columnProps.render = (value, record, index) => {
columnProps.onCell = props.onCell; if (!props.actions) {
} return render(value, record, index);
return <Table.Column<Obj> {...columnProps} />; }
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); return Column(props);
} }
export function RichColumn<Obj>(props: BaseColumnProps<Obj>) { export function RichColumn<Obj extends Entity>(props: BaseColumnProps<Obj>) {
return Column({ return Column({
...props, ...props,
render: (value: string | undefined) => { 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[]>; filterValueQuery: UseQueryResult<string[] | ColumnFilterItem[]>;
allowMultipleFilters?: boolean; allowMultipleFilters?: boolean;
} }
export function FilteredQueryColumn<Obj>(props: FilteredQueryColumnProps<Obj>) { export function FilteredQueryColumn<Obj extends Entity>(props: FilteredQueryColumnProps<Obj>) {
const query = props.filterValueQuery; const query = props.filterValueQuery;
let filters: ColumnFilterItem[] = []; let filters: ColumnFilterItem[] = [];
@@ -143,13 +191,13 @@ export function FilteredQueryColumn<Obj>(props: FilteredQueryColumnProps<Obj>) {
return Column({ ...props, filters, filteredValue, onFilterDropdownOpen, loadingFilters: query.isLoading }); 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; unit: string;
decimals?: number; decimals?: number;
defaultText?: string; defaultText?: string;
} }
export function NumberColumn<Obj>(props: NumberColumnProps<Obj>) { export function NumberColumn<Obj extends Entity>(props: NumberColumnProps<Obj>) {
return Column({ return Column({
...props, ...props,
render: (value) => { 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({ return Column({
...props, ...props,
render: (value) => { 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; 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; const query = props.filterValueQuery;
let filters: ColumnFilterItem[] = []; let filters: ColumnFilterItem[] = [];

View File

@@ -1,11 +1,11 @@
import React from "react"; import React from "react";
import { IResourceComponentsProps, BaseRecord, useTranslate, useInvalidate } from "@refinedev/core"; import { IResourceComponentsProps, useTranslate, useInvalidate, useNavigation } from "@refinedev/core";
import { useTable, List, EditButton, ShowButton, CloneButton } from "@refinedev/antd"; import { useTable, List } from "@refinedev/antd";
import { Table, Space, Button, Dropdown } from "antd"; import { Table, Button, Dropdown } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import { IFilament } from "./model"; import { IFilament } from "./model";
import { EditOutlined, FilterOutlined } from "@ant-design/icons"; import { EditOutlined, EyeOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload"; import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
import { import {
DateColumn, DateColumn,
@@ -14,6 +14,7 @@ import {
RichColumn, RichColumn,
SortedColumn, SortedColumn,
SpoolIconColumn, SpoolIconColumn,
ActionsColumn,
} from "../../components/column"; } from "../../components/column";
import { import {
useSpoolmanArticleNumbers, useSpoolmanArticleNumbers,
@@ -22,6 +23,7 @@ import {
useSpoolmanVendors, useSpoolmanVendors,
} from "../../components/otherModels"; } from "../../components/otherModels";
import { useLiveify } from "../../components/liveify"; import { useLiveify } from "../../components/liveify";
import { removeUndefined } from "../../utils/filtering";
dayjs.extend(utc); dayjs.extend(utc);
@@ -46,6 +48,26 @@ function translateColumnI18nKey(columnName: string): string {
const namespace = "filamentList-v2"; const namespace = "filamentList-v2";
const allColumns: (keyof IFilamentCollapsed & string)[] = [
"id",
"vendor.name",
"name",
"material",
"price",
"density",
"diameter",
"weight",
"spool_weight",
"article_number",
"settings_extruder_temp",
"settings_bed_temp",
"registered",
"comment",
];
const defaultColumns = allColumns.filter(
(column_id) => ["registered", "density", "diameter", "spool_weight"].indexOf(column_id) === -1
);
export const FilamentList: React.FC<IResourceComponentsProps> = () => { export const FilamentList: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate(); const t = useTranslate();
const invalidate = useInvalidate(); const invalidate = useInvalidate();
@@ -57,54 +79,43 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
// To provide the live updates, we use a custom solution (useLiveify) instead of the built-in refine "liveMode" feature. // To provide the live updates, we use a custom solution (useLiveify) instead of the built-in refine "liveMode" feature.
// This is because the built-in feature does not call the liveProvider subscriber with a list of IDs, but instead // This is because the built-in feature does not call the liveProvider subscriber with a list of IDs, but instead
// calls it with a list of filters, sorters, etc. This means the server-side has to support this, which is quite hard. // calls it with a list of filters, sorters, etc. This means the server-side has to support this, which is quite hard.
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } = useTable<IFilament>({ const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } =
syncWithLocation: false, useTable<IFilamentCollapsed>({
pagination: { syncWithLocation: false,
mode: "server", pagination: {
current: initialState.pagination.current, mode: "server",
pageSize: initialState.pagination.pageSize, current: initialState.pagination.current,
}, pageSize: initialState.pagination.pageSize,
sorters: { },
mode: "server", sorters: {
initial: initialState.sorters, mode: "server",
}, initial: initialState.sorters,
filters: { },
mode: "server", filters: {
initial: initialState.filters, mode: "server",
}, initial: initialState.filters,
liveMode: "manual", },
onLiveEvent(event) { liveMode: "manual",
if (event.type === "created" || event.type === "deleted") { onLiveEvent(event) {
// updated is handled by the liveify if (event.type === "created" || event.type === "deleted") {
invalidate({ // updated is handled by the liveify
resource: "filament", invalidate({
invalidates: ["list"], resource: "filament",
}); invalidates: ["list"],
} });
}, }
}); },
queryOptions: {
select(data) {
return {
total: data.total,
data: data.data.map(collapseFilament),
};
},
},
});
// Create state for the columns to show // Create state for the columns to show
const allColumns: (keyof IFilamentCollapsed & string)[] = [
"id",
"vendor.name",
"name",
"material",
"price",
"density",
"diameter",
"weight",
"spool_weight",
"article_number",
"settings_extruder_temp",
"settings_bed_temp",
"registered",
"comment",
];
const defaultColumns = allColumns.filter(
(column_id) => ["registered", "density", "diameter", "spool_weight"].indexOf(column_id) === -1
);
const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? defaultColumns); const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? defaultColumns);
// Store state in local storage // Store state in local storage
@@ -116,9 +127,9 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
}; };
useStoreInitialState(namespace, tableState); useStoreInitialState(namespace, tableState);
// Collapse the dataSource to a mutable list and add a filament_name field // Collapse the dataSource to a mutable list
const queryDataSource: IFilamentCollapsed[] = React.useMemo( const queryDataSource: IFilamentCollapsed[] = React.useMemo(
() => (tableProps.dataSource ?? []).map(collapseFilament), () => (tableProps.dataSource || []).map((record) => ({ ...record })),
[tableProps.dataSource] [tableProps.dataSource]
); );
const dataSource = useLiveify("filament", queryDataSource, collapseFilament); const dataSource = useLiveify("filament", queryDataSource, collapseFilament);
@@ -127,6 +138,13 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
tableProps.pagination.showSizeChanger = true; tableProps.pagination.showSizeChanger = true;
} }
const { editUrl, showUrl, cloneUrl } = useNavigation();
const actions = (record: IFilamentCollapsed) => [
{ name: t("buttons.edit"), icon: <EditOutlined />, link: editUrl("filament", record.id) },
{ name: t("buttons.show"), icon: <EyeOutlined />, link: showUrl("filament", record.id) },
{ name: t("buttons.clone"), icon: <PlusSquareOutlined />, link: cloneUrl("filament", record.id) },
];
return ( return (
<List <List
headerButtons={({ defaultButtons }) => ( headerButtons={({ defaultButtons }) => (
@@ -168,130 +186,142 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
</> </>
)} )}
> >
<Table {...tableProps} sticky scroll={{ x: "max-content" }} dataSource={dataSource} rowKey="id"> <Table<IFilamentCollapsed>
{SortedColumn({ {...tableProps}
id: "id", sticky
i18ncat: "filament", scroll={{ x: "max-content" }}
dataSource, dataSource={dataSource}
tableState, rowKey="id"
width: 70, columns={removeUndefined([
})} SortedColumn({
{FilteredQueryColumn({ id: "id",
id: "vendor.name", i18ncat: "filament",
i18nkey: "filament.fields.vendor_name", actions,
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanVendors(), width: 70,
})} }),
{SpoolIconColumn({ FilteredQueryColumn({
id: "name", id: "vendor.name",
i18ncat: "filament", i18nkey: "filament.fields.vendor_name",
color: (record: IFilamentCollapsed) => record.color_hex, actions,
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanFilamentNames(), filterValueQuery: useSpoolmanVendors(),
})} }),
{FilteredQueryColumn({ SpoolIconColumn({
id: "material", id: "name",
i18ncat: "filament", i18ncat: "filament",
dataSource, color: (record: IFilamentCollapsed) => record.color_hex,
tableState, actions,
filterValueQuery: useSpoolmanMaterials(), dataSource,
width: 110, tableState,
})} filterValueQuery: useSpoolmanFilamentNames(),
{SortedColumn({ }),
id: "price", FilteredQueryColumn({
i18ncat: "filament", id: "material",
dataSource, i18ncat: "filament",
tableState, actions,
width: 80, dataSource,
})} tableState,
{NumberColumn({ filterValueQuery: useSpoolmanMaterials(),
id: "density", width: 110,
i18ncat: "filament", }),
unit: "g/cm³", SortedColumn({
decimals: 2, id: "price",
dataSource, i18ncat: "filament",
tableState, actions,
width: 100, dataSource,
})} tableState,
{NumberColumn({ width: 80,
id: "diameter", }),
i18ncat: "filament", NumberColumn({
unit: "mm", id: "density",
decimals: 2, i18ncat: "filament",
dataSource, unit: "g/cm³",
tableState, decimals: 2,
width: 100, actions,
})} dataSource,
{NumberColumn({ tableState,
id: "weight", width: 100,
i18ncat: "filament", }),
unit: "g", NumberColumn({
decimals: 1, id: "diameter",
dataSource, i18ncat: "filament",
tableState, unit: "mm",
width: 100, decimals: 2,
})} actions,
{NumberColumn({ dataSource,
id: "spool_weight", tableState,
i18ncat: "filament", width: 100,
unit: "g", }),
decimals: 1, NumberColumn({
dataSource, id: "weight",
tableState, i18ncat: "filament",
width: 100, unit: "g",
})} decimals: 1,
{FilteredQueryColumn({ actions,
id: "article_number", dataSource,
i18ncat: "filament", tableState,
dataSource, width: 100,
tableState, }),
filterValueQuery: useSpoolmanArticleNumbers(), NumberColumn({
width: 130, id: "spool_weight",
})} i18ncat: "filament",
{NumberColumn({ unit: "g",
id: "settings_extruder_temp", decimals: 1,
i18ncat: "filament", actions,
unit: "°C", dataSource,
decimals: 0, tableState,
dataSource, width: 100,
tableState, }),
width: 100, FilteredQueryColumn({
})} id: "article_number",
{NumberColumn({ i18ncat: "filament",
id: "settings_bed_temp", actions,
i18ncat: "filament", dataSource,
unit: "°C", tableState,
decimals: 0, filterValueQuery: useSpoolmanArticleNumbers(),
dataSource, width: 130,
tableState, }),
width: 100, NumberColumn({
})} id: "settings_extruder_temp",
{DateColumn({ i18ncat: "filament",
id: "registered", unit: "°C",
i18ncat: "filament", decimals: 0,
dataSource, actions,
tableState, dataSource,
})} tableState,
{RichColumn({ width: 100,
id: "comment", }),
i18ncat: "filament", NumberColumn({
dataSource, id: "settings_bed_temp",
tableState, i18ncat: "filament",
width: 150, unit: "°C",
})} decimals: 0,
<Table.Column actions,
title={t("table.actions")} dataSource,
render={(_, record: BaseRecord) => ( tableState,
<Space> width: 100,
<EditButton hideText title={t("buttons.edit")} size="small" recordItemId={record.id} /> }),
<ShowButton hideText title={t("buttons.show")} size="small" recordItemId={record.id} /> DateColumn({
<CloneButton hideText title={t("buttons.clone")} size="small" recordItemId={record.id} /> id: "registered",
</Space> i18ncat: "filament",
)} actions,
/> dataSource,
</Table> tableState,
}),
RichColumn({
id: "comment",
i18ncat: "filament",
actions,
dataSource,
tableState,
width: 150,
}),
ActionsColumn(actions),
])}
/>
</List> </List>
); );
}; };

View File

@@ -1,12 +1,19 @@
import React from "react"; import React from "react";
import { IResourceComponentsProps, useInvalidate, useTranslate } from "@refinedev/core"; import { IResourceComponentsProps, useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
import { useTable, List, EditButton, ShowButton, CloneButton } from "@refinedev/antd"; import { useTable, List } from "@refinedev/antd";
import { Table, Space, Button, Dropdown, Modal } from "antd"; import { Table, Button, Dropdown, Modal } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import { ISpool } from "./model"; import { ISpool } from "./model";
import { TableState, useInitialTableState, useSavedState, useStoreInitialState } from "../../utils/saveload"; import { TableState, useInitialTableState, useSavedState, useStoreInitialState } from "../../utils/saveload";
import { EditOutlined, FilterOutlined, InboxOutlined, ToTopOutlined } from "@ant-design/icons"; import {
EditOutlined,
EyeOutlined,
FilterOutlined,
InboxOutlined,
PlusSquareOutlined,
ToTopOutlined,
} from "@ant-design/icons";
import { import {
DateColumn, DateColumn,
FilteredQueryColumn, FilteredQueryColumn,
@@ -14,6 +21,8 @@ import {
RichColumn, RichColumn,
SortedColumn, SortedColumn,
SpoolIconColumn, SpoolIconColumn,
Action,
ActionsColumn,
} from "../../components/column"; } from "../../components/column";
import { setSpoolArchived } from "./functions"; import { setSpoolArchived } from "./functions";
import SelectAndPrint from "../../components/selectAndPrintDialog"; import SelectAndPrint from "../../components/selectAndPrintDialog";
@@ -24,6 +33,7 @@ import {
useSpoolmanMaterials, useSpoolmanMaterials,
} from "../../components/otherModels"; } from "../../components/otherModels";
import { useLiveify } from "../../components/liveify"; import { useLiveify } from "../../components/liveify";
import { removeUndefined } from "../../utils/filtering";
dayjs.extend(utc); dayjs.extend(utc);
@@ -59,6 +69,25 @@ function translateColumnI18nKey(columnName: string): string {
const namespace = "spoolList-v2"; const namespace = "spoolList-v2";
const allColumns: (keyof ISpoolCollapsed & string)[] = [
"id",
"combined_name",
"filament.material",
"used_weight",
"remaining_weight",
"used_length",
"remaining_length",
"location",
"lot_nr",
"first_used",
"last_used",
"registered",
"comment",
];
const defaultColumns = allColumns.filter(
(column_id) => ["registered", "used_length", "remaining_length", "lot_nr"].indexOf(column_id) === -1
);
export const SpoolList: React.FC<IResourceComponentsProps> = () => { export const SpoolList: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate(); const t = useTranslate();
const invalidate = useInvalidate(); const invalidate = useInvalidate();
@@ -73,57 +102,48 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
// To provide the live updates, we use a custom solution (useLiveify) instead of the built-in refine "liveMode" feature. // To provide the live updates, we use a custom solution (useLiveify) instead of the built-in refine "liveMode" feature.
// This is because the built-in feature does not call the liveProvider subscriber with a list of IDs, but instead // This is because the built-in feature does not call the liveProvider subscriber with a list of IDs, but instead
// calls it with a list of filters, sorters, etc. This means the server-side has to support this, which is quite hard. // calls it with a list of filters, sorters, etc. This means the server-side has to support this, which is quite hard.
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } = useTable<ISpool>({ const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } =
meta: { useTable<ISpoolCollapsed>({
queryParams: { meta: {
["allow_archived"]: showArchived, queryParams: {
["allow_archived"]: showArchived,
},
}, },
}, syncWithLocation: false,
syncWithLocation: false, pagination: {
pagination: { mode: "server",
mode: "server", current: initialState.pagination.current,
current: initialState.pagination.current, pageSize: initialState.pagination.pageSize,
pageSize: initialState.pagination.pageSize, },
}, sorters: {
sorters: { mode: "server",
mode: "server", initial: initialState.sorters,
initial: initialState.sorters, },
}, filters: {
filters: { mode: "server",
mode: "server", initial: initialState.filters,
initial: initialState.filters, },
}, liveMode: "manual",
liveMode: "manual", onLiveEvent(event) {
onLiveEvent(event) { if (event.type === "created" || event.type === "deleted") {
if (event.type === "created" || event.type === "deleted") { // updated is handled by the liveify
// updated is handled by the liveify invalidate({
invalidate({ resource: "spool",
resource: "spool", invalidates: ["list"],
invalidates: ["list"], });
}); }
} },
}, queryOptions: {
}); select(data) {
return {
total: data.total,
data: data.data.map(collapseSpool),
};
},
},
});
// Create state for the columns to show // Create state for the columns to show
const allColumns: (keyof ISpoolCollapsed & string)[] = [
"id",
"combined_name",
"filament.material",
"used_weight",
"remaining_weight",
"used_length",
"remaining_length",
"location",
"lot_nr",
"first_used",
"last_used",
"registered",
"comment",
];
const defaultColumns = allColumns.filter(
(column_id) => ["registered", "used_length", "remaining_length", "lot_nr"].indexOf(column_id) === -1
);
const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? defaultColumns); const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? defaultColumns);
// Store state in local storage // Store state in local storage
@@ -135,9 +155,9 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
}; };
useStoreInitialState(namespace, tableState); useStoreInitialState(namespace, tableState);
// Collapse the dataSource to a mutable list and add a filament_name field // Collapse the dataSource to a mutable list
const queryDataSource = React.useMemo( const queryDataSource: ISpoolCollapsed[] = React.useMemo(
() => (tableProps.dataSource ?? []).map(collapseSpool), () => (tableProps.dataSource || []).map((record) => ({ ...record })),
[tableProps.dataSource] [tableProps.dataSource]
); );
const dataSource = useLiveify("spool", queryDataSource, collapseSpool); const dataSource = useLiveify("spool", queryDataSource, collapseSpool);
@@ -174,6 +194,25 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
tableProps.pagination.showSizeChanger = true; tableProps.pagination.showSizeChanger = true;
} }
const { editUrl, showUrl, cloneUrl } = useNavigation();
const actions = (record: ISpoolCollapsed) => {
const actions: Action[] = [
{ name: t("buttons.edit"), icon: <EditOutlined />, link: editUrl("spool", record.id) },
{ name: t("buttons.show"), icon: <EyeOutlined />, link: showUrl("spool", record.id) },
{ name: t("buttons.clone"), icon: <PlusSquareOutlined />, link: cloneUrl("spool", record.id) },
];
if (record.archived) {
actions.push({
name: t("buttons.unArchive"),
icon: <ToTopOutlined />,
onClick: () => archiveSpool(record, false),
});
} else {
actions.push({ name: t("buttons.archive"), icon: <InboxOutlined />, onClick: () => archiveSpoolPopup(record) });
}
return actions;
};
return ( return (
<List <List
headerButtons={({ defaultButtons }) => ( headerButtons={({ defaultButtons }) => (
@@ -244,136 +283,126 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
return {}; return {};
} }
}} }}
> columns={removeUndefined([
{SortedColumn({ SortedColumn({
id: "id", id: "id",
i18ncat: "spool", i18ncat: "spool",
dataSource, actions,
tableState, dataSource,
width: 70, tableState,
})} width: 70,
{SpoolIconColumn({ }),
id: "combined_name", SpoolIconColumn({
i18nkey: "spool.fields.filament_name", id: "combined_name",
color: (record: ISpoolCollapsed) => record.filament.color_hex, i18nkey: "spool.fields.filament_name",
dataSource, color: (record: ISpoolCollapsed) => record.filament.color_hex,
tableState, actions,
dataId: "filament.id", dataSource,
filterValueQuery: useSpoolmanFilamentFilter(), tableState,
})} dataId: "filament.id",
{FilteredQueryColumn({ filterValueQuery: useSpoolmanFilamentFilter(),
id: "filament.material", }),
i18nkey: "spool.fields.material", FilteredQueryColumn({
dataSource, id: "filament.material",
tableState, i18nkey: "spool.fields.material",
filterValueQuery: useSpoolmanMaterials(), actions,
width: 120, dataSource,
})} tableState,
{NumberColumn({ filterValueQuery: useSpoolmanMaterials(),
id: "used_weight", width: 120,
i18ncat: "spool", }),
unit: "g", NumberColumn({
decimals: 1, id: "used_weight",
dataSource, i18ncat: "spool",
tableState, unit: "g",
width: 110, decimals: 1,
})} actions,
{NumberColumn({ dataSource,
id: "remaining_weight", tableState,
i18ncat: "spool", width: 110,
unit: "g", }),
decimals: 1, NumberColumn({
defaultText: t("unknown"), id: "remaining_weight",
dataSource, i18ncat: "spool",
tableState, unit: "g",
width: 110, decimals: 1,
})} defaultText: t("unknown"),
{NumberColumn({ actions,
id: "used_length", dataSource,
i18ncat: "spool", tableState,
unit: "mm", width: 110,
decimals: 1, }),
dataSource, NumberColumn({
tableState, id: "used_length",
width: 120, i18ncat: "spool",
})} unit: "mm",
{NumberColumn({ decimals: 1,
id: "remaining_length", actions,
i18ncat: "spool", dataSource,
unit: "mm", tableState,
decimals: 1, width: 120,
defaultText: t("unknown"), }),
dataSource, NumberColumn({
tableState, id: "remaining_length",
width: 120, i18ncat: "spool",
})} unit: "mm",
{FilteredQueryColumn({ decimals: 1,
id: "location", defaultText: t("unknown"),
i18ncat: "spool", actions,
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanLocations(), width: 120,
width: 120, }),
})} FilteredQueryColumn({
{FilteredQueryColumn({ id: "location",
id: "lot_nr", i18ncat: "spool",
i18ncat: "spool", actions,
dataSource, dataSource,
tableState, tableState,
filterValueQuery: useSpoolmanLotNumbers(), filterValueQuery: useSpoolmanLocations(),
width: 120, width: 120,
})} }),
{DateColumn({ FilteredQueryColumn({
id: "first_used", id: "lot_nr",
i18ncat: "spool", i18ncat: "spool",
dataSource, actions,
tableState, dataSource,
})} tableState,
{DateColumn({ filterValueQuery: useSpoolmanLotNumbers(),
id: "last_used", width: 120,
i18ncat: "spool", }),
dataSource, DateColumn({
tableState, id: "first_used",
})} i18ncat: "spool",
{DateColumn({ actions,
id: "registered", dataSource,
i18ncat: "spool", tableState,
dataSource, }),
tableState, DateColumn({
})} id: "last_used",
{RichColumn({ i18ncat: "spool",
id: "comment", actions,
i18ncat: "spool", dataSource,
dataSource, tableState,
tableState, }),
width: 150, DateColumn({
})} id: "registered",
<Table.Column i18ncat: "spool",
title={t("table.actions")} actions,
render={(_, record: ISpoolCollapsed) => ( dataSource,
<Space> tableState,
<EditButton hideText title={t("buttons.edit")} size="small" recordItemId={record.id} /> }),
<ShowButton hideText title={t("buttons.show")} size="small" recordItemId={record.id} /> RichColumn({
<CloneButton hideText title={t("buttons.clone")} size="small" recordItemId={record.id} /> id: "comment",
{record.archived ? ( i18ncat: "spool",
<Button actions,
icon={<ToTopOutlined />} dataSource,
title={t("buttons.unArchive")} tableState,
size="small" width: 150,
onClick={() => archiveSpool(record, false)} }),
/> ActionsColumn(actions),
) : ( ])}
<Button />
icon={<InboxOutlined />}
title={t("buttons.archive")}
size="small"
onClick={() => archiveSpoolPopup(record)}
/>
)}
</Space>
)}
/>
</Table>
</List> </List>
); );
}; };

View File

@@ -1,19 +1,22 @@
import React from "react"; import React from "react";
import { IResourceComponentsProps, BaseRecord, useTranslate, useInvalidate } from "@refinedev/core"; import { IResourceComponentsProps, useTranslate, useInvalidate, useNavigation } from "@refinedev/core";
import { useTable, List, EditButton, ShowButton, CloneButton } from "@refinedev/antd"; import { useTable, List } from "@refinedev/antd";
import { Table, Space, Button, Dropdown } from "antd"; import { Table, Button, Dropdown } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import { IVendor } from "./model"; import { IVendor } from "./model";
import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload"; import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
import { EditOutlined, FilterOutlined } from "@ant-design/icons"; import { EditOutlined, EyeOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
import { DateColumn, RichColumn, SortedColumn } from "../../components/column"; import { DateColumn, RichColumn, SortedColumn, ActionsColumn } from "../../components/column";
import { useLiveify } from "../../components/liveify"; import { useLiveify } from "../../components/liveify";
import { removeUndefined } from "../../utils/filtering";
dayjs.extend(utc); dayjs.extend(utc);
const namespace = "vendorList-v2"; const namespace = "vendorList-v2";
const allColumns: (keyof IVendor & string)[] = ["id", "name", "registered", "comment"];
export const VendorList: React.FC<IResourceComponentsProps> = () => { export const VendorList: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate(); const t = useTranslate();
const invalidate = useInvalidate(); const invalidate = useInvalidate();
@@ -50,7 +53,6 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
}); });
// Create state for the columns to show // Create state for the columns to show
const allColumns: (keyof IVendor & string)[] = ["id", "name", "registered", "comment"];
const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? allColumns); const [showColumns, setShowColumns] = React.useState<string[]>(initialState.showColumns ?? allColumns);
// Store state in local storage // Store state in local storage
@@ -73,6 +75,13 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
tableProps.pagination.showSizeChanger = true; tableProps.pagination.showSizeChanger = true;
} }
const { editUrl, showUrl, cloneUrl } = useNavigation();
const actions = (record: IVendor) => [
{ name: t("buttons.edit"), icon: <EditOutlined />, link: editUrl("vendor", record.id) },
{ name: t("buttons.show"), icon: <EyeOutlined />, link: showUrl("vendor", record.id) },
{ name: t("buttons.clone"), icon: <PlusSquareOutlined />, link: cloneUrl("vendor", record.id) },
];
return ( return (
<List <List
headerButtons={({ defaultButtons }) => ( headerButtons={({ defaultButtons }) => (
@@ -114,43 +123,45 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
</> </>
)} )}
> >
<Table {...tableProps} sticky scroll={{ x: "max-content" }} dataSource={dataSource} rowKey="id"> <Table
{SortedColumn({ {...tableProps}
id: "id", sticky
i18ncat: "vendor", scroll={{ x: "max-content" }}
dataSource, dataSource={dataSource}
tableState, rowKey="id"
width: 70, columns={removeUndefined([
})} SortedColumn({
{SortedColumn({ id: "id",
id: "name", i18ncat: "vendor",
i18ncat: "vendor", actions,
dataSource, dataSource,
tableState, tableState,
})} width: 70,
{DateColumn({ }),
id: "registered", SortedColumn({
i18ncat: "vendor", id: "name",
dataSource, i18ncat: "vendor",
tableState, actions,
})} dataSource,
{RichColumn({ tableState,
id: "comment", }),
i18ncat: "vendor", DateColumn({
dataSource, id: "registered",
tableState, i18ncat: "vendor",
})} actions,
<Table.Column dataSource,
title={t("table.actions")} tableState,
render={(_, record: BaseRecord) => ( }),
<Space> RichColumn({
<EditButton hideText title={t("buttons.edit")} size="small" recordItemId={record.id} /> id: "comment",
<ShowButton hideText title={t("buttons.show")} size="small" recordItemId={record.id} /> i18ncat: "vendor",
<CloneButton hideText title={t("buttons.clone")} size="small" recordItemId={record.id} /> actions,
</Space> dataSource,
)} tableState,
/> }),
</Table> ActionsColumn<IVendor>(actions),
])}
/>
</List> </List>
); );
}; };

View File

@@ -28,3 +28,10 @@ export function getFiltersForField<Obj, Field extends keyof Obj>(
}); });
return filterValues; return filterValues;
} }
/**
* Function that returns an array with all undefined values removed.
*/
export function removeUndefined<T>(array: (T | undefined)[]): T[] {
return array.filter((value) => value !== undefined) as T[];
}