Client: Added show/hide columns feature
This commit is contained in:
@@ -109,35 +109,68 @@
|
|||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard"
|
"title": "Dashboard"
|
||||||
},
|
},
|
||||||
"blog_posts": {
|
"spools": {
|
||||||
"blog_posts": "Blog Posts",
|
"spools": "Spools",
|
||||||
"fields": {
|
"fields": {
|
||||||
"id": "Id",
|
"id": "Id",
|
||||||
"title": "Title",
|
"filament_name": "Filament Name",
|
||||||
"content": "Content",
|
"used_weight": "Used Weight",
|
||||||
"status": "Status",
|
"remaining_weight": "Remaining Weight",
|
||||||
"category": "Category",
|
"location": "Location",
|
||||||
"createdAt": "Created At"
|
"lot_nr": "Lot Nr",
|
||||||
|
"first_used": "First Used",
|
||||||
|
"last_used": "Last Used",
|
||||||
|
"comment": "Comment"
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
"create": "Create Blog Post",
|
"create": "Create Spool",
|
||||||
"edit": "Edit Blog Post",
|
"clone": "Clone Spool",
|
||||||
"list": "Blog Posts",
|
"edit": "Edit Spool",
|
||||||
"show": "Show Blog Post"
|
"list": "Spools",
|
||||||
|
"show": "Show Spool"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"categories": {
|
"filaments": {
|
||||||
"categories": "Categories",
|
"filaments": "Filaments",
|
||||||
"fields": {
|
"fields": {
|
||||||
"id": "Id",
|
"id": "Id",
|
||||||
"title": "Title",
|
"vendor_name": "Vendor Name",
|
||||||
"createdAt": "Created At"
|
"name": "Name",
|
||||||
|
"material": "Material",
|
||||||
|
"price": "Price",
|
||||||
|
"density": "Density",
|
||||||
|
"diameter": "Diameter",
|
||||||
|
"weight": "Weight",
|
||||||
|
"spool_weight": "Spool Weight",
|
||||||
|
"article_number": "Article Number",
|
||||||
|
"registered": "Registered",
|
||||||
|
"comment": "Comment",
|
||||||
|
"settings_extruder_temp": "Extruder Temp",
|
||||||
|
"settings_bed_temp": "Bed Temp",
|
||||||
|
"color_hex": "Color"
|
||||||
},
|
},
|
||||||
"titles": {
|
"titles": {
|
||||||
"create": "Create Category",
|
"create": "Create Filament",
|
||||||
"edit": "Edit Category",
|
"clone": "Clone Filament",
|
||||||
"list": "Categories",
|
"edit": "Edit Filament",
|
||||||
"show": "Show Category"
|
"list": "Filaments",
|
||||||
|
"show": "Show Filament"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vendors": {
|
||||||
|
"vendors": "Vendors",
|
||||||
|
"fields": {
|
||||||
|
"id": "Id",
|
||||||
|
"name": "Name",
|
||||||
|
"registered": "Registered",
|
||||||
|
"comment": "Comment"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"create": "Create Vendor",
|
||||||
|
"clone": "Clone Vendor",
|
||||||
|
"edit": "Edit Vendor",
|
||||||
|
"list": "Vendors",
|
||||||
|
"show": "Show Vendor"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
170
client/src/components/column.tsx
Normal file
170
client/src/components/column.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { Col, Row, Table } from "antd";
|
||||||
|
import { ColumnProps as AntdColumnProps } from "antd/es/table";
|
||||||
|
import { ColumnFilterItem } from "antd/es/table/interface";
|
||||||
|
import {
|
||||||
|
getFiltersForField,
|
||||||
|
typeFilters,
|
||||||
|
useListFiltersForField,
|
||||||
|
} from "../utils/filtering";
|
||||||
|
import { TableState } from "../utils/saveload";
|
||||||
|
import { getSortOrderForField, typeSorters } from "../utils/sorting";
|
||||||
|
import { NumberFieldUnit } from "./numberField";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
|
import { DateField, TextField } from "@refinedev/antd";
|
||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import { ReactComponent as SpoolIcon } from "../icon_spool.svg";
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
|
||||||
|
interface BaseColumnProps<Obj> {
|
||||||
|
id: keyof Obj & string;
|
||||||
|
title: string;
|
||||||
|
dataSource: Obj[];
|
||||||
|
tableState: TableState;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilteredColumnProps {
|
||||||
|
filters?: ColumnFilterItem[];
|
||||||
|
filteredValue?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomColumnProps<Obj> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
render?: (value: any, record: Obj, index: number) => React.ReactNode;
|
||||||
|
onCell?: (
|
||||||
|
data: Obj,
|
||||||
|
index?: number
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
) => React.HTMLAttributes<any> | React.TdHTMLAttributes<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Column<Obj>(
|
||||||
|
props: BaseColumnProps<Obj> & FilteredColumnProps & CustomColumnProps<Obj>
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
props.tableState.showColumns &&
|
||||||
|
!props.tableState.showColumns.includes(props.id)
|
||||||
|
) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
const typedSorters = typeSorters<Obj>(props.tableState.sorters);
|
||||||
|
const columnProps: AntdColumnProps<Obj> = {
|
||||||
|
dataIndex: props.id,
|
||||||
|
title: props.title,
|
||||||
|
sorter: true,
|
||||||
|
sortOrder: getSortOrderForField(typedSorters, props.id),
|
||||||
|
};
|
||||||
|
if (props.filters && props.filteredValue) {
|
||||||
|
columnProps.filters = props.filters;
|
||||||
|
columnProps.filteredValue = props.filteredValue;
|
||||||
|
}
|
||||||
|
if (props.render) {
|
||||||
|
columnProps.render = props.render;
|
||||||
|
}
|
||||||
|
if (props.onCell) {
|
||||||
|
columnProps.onCell = props.onCell;
|
||||||
|
}
|
||||||
|
return <Table.Column<Obj> {...columnProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortedColumn<Obj>(props: BaseColumnProps<Obj>) {
|
||||||
|
// return <Table.Column dataIndex={["comment"]} title="Comment" />;
|
||||||
|
return Column(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FilteredColumn<Obj>(props: BaseColumnProps<Obj>) {
|
||||||
|
const typedFilters = typeFilters<Obj>(props.tableState.filters);
|
||||||
|
|
||||||
|
const filters = useListFiltersForField(props.dataSource, props.id);
|
||||||
|
const filteredValue = getFiltersForField(typedFilters, props.id);
|
||||||
|
|
||||||
|
return Column({ ...props, filters, filteredValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NumberColumnProps<Obj> extends BaseColumnProps<Obj> {
|
||||||
|
unit: string;
|
||||||
|
decimals?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NumberColumn<Obj>(props: NumberColumnProps<Obj>) {
|
||||||
|
return Column({
|
||||||
|
...props,
|
||||||
|
render: (value) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return <TextField value="Unknown" />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<NumberFieldUnit
|
||||||
|
value={value}
|
||||||
|
unit={props.unit}
|
||||||
|
options={{
|
||||||
|
maximumFractionDigits: props.decimals ?? 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DateColumn<Obj>(props: BaseColumnProps<Obj>) {
|
||||||
|
return Column({
|
||||||
|
...props,
|
||||||
|
render: (value) => {
|
||||||
|
return (
|
||||||
|
<DateField
|
||||||
|
hidden={!value}
|
||||||
|
value={dayjs.utc(value).local()}
|
||||||
|
title={dayjs.utc(value).local().format()}
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpoolIconColumnProps<Obj> extends BaseColumnProps<Obj> {
|
||||||
|
color: (record: Obj) => string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SpoolIconColumn<Obj>(props: SpoolIconColumnProps<Obj>) {
|
||||||
|
const typedFilters = typeFilters<Obj>(props.tableState.filters);
|
||||||
|
|
||||||
|
const filters = useListFiltersForField(props.dataSource, props.id);
|
||||||
|
const filteredValue = getFiltersForField(typedFilters, props.id);
|
||||||
|
|
||||||
|
return Column({
|
||||||
|
...props,
|
||||||
|
filters,
|
||||||
|
filteredValue,
|
||||||
|
onCell: () => {
|
||||||
|
return {
|
||||||
|
style: {
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render: (value, record: Obj) => {
|
||||||
|
const colorStr = props.color(record);
|
||||||
|
return (
|
||||||
|
<Row wrap={false} justify="space-around" align="middle">
|
||||||
|
{colorStr && (
|
||||||
|
<Col flex="none">
|
||||||
|
<Icon
|
||||||
|
component={SpoolIcon}
|
||||||
|
style={{
|
||||||
|
color: "#" + colorStr,
|
||||||
|
fontSize: 42,
|
||||||
|
marginRight: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
<Col flex="auto">{value}</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,35 +5,32 @@ import {
|
|||||||
List,
|
List,
|
||||||
EditButton,
|
EditButton,
|
||||||
ShowButton,
|
ShowButton,
|
||||||
DateField,
|
|
||||||
CloneButton,
|
CloneButton,
|
||||||
} from "@refinedev/antd";
|
} from "@refinedev/antd";
|
||||||
import { Table, Space, Button, Row, Col } from "antd";
|
import { Table, Space, Button, Dropdown } from "antd";
|
||||||
import { NumberFieldUnit } from "../../components/numberField";
|
|
||||||
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 { genericSorter, typeSorters } from "../../utils/sorting";
|
||||||
|
import { genericFilterer, typeFilters } from "../../utils/filtering";
|
||||||
|
import { EditOutlined, FilterOutlined } from "@ant-design/icons";
|
||||||
import {
|
import {
|
||||||
genericSorter,
|
TableState,
|
||||||
getSortOrderForField,
|
|
||||||
typeSorters,
|
|
||||||
} from "../../utils/sorting";
|
|
||||||
import {
|
|
||||||
genericFilterer,
|
|
||||||
getFiltersForField,
|
|
||||||
typeFilters,
|
|
||||||
useListFiltersForField,
|
|
||||||
} from "../../utils/filtering";
|
|
||||||
import Icon, { FilterOutlined } from "@ant-design/icons";
|
|
||||||
import {
|
|
||||||
useInitialTableState,
|
useInitialTableState,
|
||||||
useStoreInitialState,
|
useStoreInitialState,
|
||||||
} from "../../utils/saveload";
|
} from "../../utils/saveload";
|
||||||
import { ReactComponent as SpoolIcon } from "../../icon_spool.svg";
|
import {
|
||||||
|
DateColumn,
|
||||||
|
FilteredColumn,
|
||||||
|
NumberColumn,
|
||||||
|
SortedColumn,
|
||||||
|
SpoolIconColumn,
|
||||||
|
} from "../../components/column";
|
||||||
|
import i18n from "../../i18n";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
interface IFilamentCollapsed extends IFilament {
|
interface IFilamentCollapsed extends Omit<IFilament, "vendor"> {
|
||||||
vendor_name: string | null;
|
vendor_name: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,16 +66,37 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
"registered",
|
||||||
|
"comment",
|
||||||
|
];
|
||||||
|
const [showColumns, setShowColumns] = React.useState<string[]>(
|
||||||
|
initialState.showColumns ?? allColumns
|
||||||
|
);
|
||||||
|
|
||||||
// Type the sorters and filters
|
// Type the sorters and filters
|
||||||
const typedSorters = typeSorters<IFilamentCollapsed>(sorters);
|
const typedSorters = typeSorters<IFilamentCollapsed>(sorters);
|
||||||
const typedFilters = typeFilters<IFilamentCollapsed>(filters);
|
const typedFilters = typeFilters<IFilamentCollapsed>(filters);
|
||||||
|
|
||||||
// Store state in local storage
|
// Store state in local storage
|
||||||
useStoreInitialState("filamentList", {
|
const tableState: TableState = {
|
||||||
sorters,
|
sorters,
|
||||||
filters,
|
filters,
|
||||||
pagination: { current, pageSize },
|
pagination: { current, pageSize },
|
||||||
});
|
showColumns,
|
||||||
|
};
|
||||||
|
useStoreInitialState("filamentList", tableState);
|
||||||
|
|
||||||
// Collapse the dataSource to a mutable list and add a filament_name field
|
// Collapse the dataSource to a mutable list and add a filament_name field
|
||||||
const dataSource: IFilamentCollapsed[] = React.useMemo(
|
const dataSource: IFilamentCollapsed[] = React.useMemo(
|
||||||
@@ -117,6 +135,28 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
>
|
>
|
||||||
Clear Filters
|
Clear Filters
|
||||||
</Button>
|
</Button>
|
||||||
|
<Dropdown
|
||||||
|
trigger={["click"]}
|
||||||
|
menu={{
|
||||||
|
items: allColumns.map((column_id) => ({
|
||||||
|
key: column_id,
|
||||||
|
label: i18n.t(`filaments.fields.${column_id}`),
|
||||||
|
})),
|
||||||
|
selectedKeys: showColumns,
|
||||||
|
selectable: true,
|
||||||
|
multiple: true,
|
||||||
|
onDeselect: (keys) => {
|
||||||
|
setShowColumns(keys.selectedKeys);
|
||||||
|
},
|
||||||
|
onSelect: (keys) => {
|
||||||
|
setShowColumns(keys.selectedKeys);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" icon={<EditOutlined />}>
|
||||||
|
Hide Columns
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
{defaultButtons}
|
{defaultButtons}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -135,168 +175,87 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
}}
|
}}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
>
|
>
|
||||||
<Table.Column
|
{SortedColumn({
|
||||||
dataIndex="id"
|
id: "id",
|
||||||
title="Id"
|
title: "Id",
|
||||||
sorter={true}
|
dataSource,
|
||||||
sortOrder={getSortOrderForField(typedSorters, "id")}
|
tableState,
|
||||||
/>
|
})}
|
||||||
<Table.Column
|
{FilteredColumn({
|
||||||
dataIndex="vendor_name"
|
id: "vendor_name",
|
||||||
title="Vendor"
|
title: "Vendor",
|
||||||
sorter={true}
|
dataSource,
|
||||||
sortOrder={getSortOrderForField(typedSorters, "vendor_name")}
|
tableState,
|
||||||
filters={useListFiltersForField(dataSource, "vendor_name")}
|
})}
|
||||||
filteredValue={getFiltersForField(typedFilters, "vendor_name")}
|
{SpoolIconColumn({
|
||||||
/>
|
id: "name",
|
||||||
<Table.Column
|
title: "Name",
|
||||||
dataIndex="name"
|
color: (record: IFilamentCollapsed) => record.color_hex,
|
||||||
title="Name"
|
dataSource,
|
||||||
width={150}
|
tableState,
|
||||||
sorter={true}
|
})}
|
||||||
sortOrder={getSortOrderForField(typedSorters, "name")}
|
{FilteredColumn({
|
||||||
filters={useListFiltersForField(dataSource, "name")}
|
id: "material",
|
||||||
filteredValue={getFiltersForField(typedFilters, "name")}
|
title: "Material",
|
||||||
render={(value, record: IFilament) => {
|
dataSource,
|
||||||
return {
|
tableState,
|
||||||
props: {
|
})}
|
||||||
style: {
|
{SortedColumn({
|
||||||
paddingLeft: 0,
|
id: "price",
|
||||||
paddingTop: 0,
|
title: "Price",
|
||||||
paddingBottom: 0,
|
dataSource,
|
||||||
},
|
tableState,
|
||||||
},
|
})}
|
||||||
children: (
|
{NumberColumn({
|
||||||
<Row wrap={false} justify="space-around" align="middle">
|
id: "density",
|
||||||
{record.color_hex && (
|
title: "Density",
|
||||||
<Col flex="none">
|
unit: "g/cm³",
|
||||||
<Icon
|
decimals: 2,
|
||||||
component={SpoolIcon}
|
dataSource,
|
||||||
style={{
|
tableState,
|
||||||
color: "#" + record.color_hex,
|
})}
|
||||||
fontSize: 42,
|
{NumberColumn({
|
||||||
marginRight: 0,
|
id: "diameter",
|
||||||
}}
|
title: "Diameter",
|
||||||
/>
|
unit: "mm",
|
||||||
</Col>
|
decimals: 2,
|
||||||
)}
|
dataSource,
|
||||||
<Col flex="auto">{value}</Col>
|
tableState,
|
||||||
</Row>
|
})}
|
||||||
),
|
{NumberColumn({
|
||||||
};
|
id: "weight",
|
||||||
}}
|
title: "Weight",
|
||||||
/>
|
unit: "g",
|
||||||
<Table.Column
|
decimals: 1,
|
||||||
dataIndex="material"
|
dataSource,
|
||||||
title="Material"
|
tableState,
|
||||||
sorter={true}
|
})}
|
||||||
sortOrder={getSortOrderForField(typedSorters, "material")}
|
{NumberColumn({
|
||||||
filters={useListFiltersForField(dataSource, "material")}
|
id: "spool_weight",
|
||||||
filteredValue={getFiltersForField(typedFilters, "material")}
|
title: "Spool Weight",
|
||||||
/>
|
unit: "g",
|
||||||
<Table.Column
|
decimals: 1,
|
||||||
dataIndex="price"
|
dataSource,
|
||||||
title="Price"
|
tableState,
|
||||||
sorter={true}
|
})}
|
||||||
sortOrder={getSortOrderForField(typedSorters, "price")}
|
{FilteredColumn({
|
||||||
/>
|
id: "article_number",
|
||||||
<Table.Column
|
title: "Article Number",
|
||||||
dataIndex="density"
|
dataSource,
|
||||||
title="Density"
|
tableState,
|
||||||
sorter={true}
|
})}
|
||||||
sortOrder={getSortOrderForField(typedSorters, "density")}
|
{DateColumn({
|
||||||
render={(value) => (
|
id: "registered",
|
||||||
<NumberFieldUnit
|
title: "Registered",
|
||||||
value={value}
|
dataSource,
|
||||||
unit="g/cm³"
|
tableState,
|
||||||
options={{
|
})}
|
||||||
maximumFractionDigits: 2,
|
{SortedColumn({
|
||||||
}}
|
id: "comment",
|
||||||
/>
|
title: "Comment",
|
||||||
)}
|
dataSource,
|
||||||
/>
|
tableState,
|
||||||
<Table.Column
|
})}
|
||||||
dataIndex="diameter"
|
|
||||||
title="Diameter"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "diameter")}
|
|
||||||
render={(value) => (
|
|
||||||
<NumberFieldUnit
|
|
||||||
value={value}
|
|
||||||
unit="mm"
|
|
||||||
options={{
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
|
||||||
dataIndex="weight"
|
|
||||||
title="Weight"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "weight")}
|
|
||||||
render={(value) => {
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<NumberFieldUnit
|
|
||||||
value={value}
|
|
||||||
unit="g"
|
|
||||||
options={{
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
|
||||||
dataIndex="spool_weight"
|
|
||||||
title="Spool Weight"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "spool_weight")}
|
|
||||||
render={(value) => {
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<NumberFieldUnit
|
|
||||||
value={value}
|
|
||||||
unit="g"
|
|
||||||
options={{
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
|
||||||
dataIndex="article_number"
|
|
||||||
title="Article Number"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "article_number")}
|
|
||||||
filters={useListFiltersForField(dataSource, "article_number")}
|
|
||||||
filteredValue={getFiltersForField(typedFilters, "article_number")}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
|
||||||
dataIndex={["registered"]}
|
|
||||||
title="Registered"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "registered")}
|
|
||||||
render={(value) => (
|
|
||||||
<DateField
|
|
||||||
value={dayjs.utc(value).local()}
|
|
||||||
title={dayjs.utc(value).local().format()}
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
|
||||||
dataIndex={["comment"]}
|
|
||||||
title="Comment"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "comment")}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
<Table.Column
|
||||||
title="Actions"
|
title="Actions"
|
||||||
dataIndex="actions"
|
dataIndex="actions"
|
||||||
|
|||||||
@@ -5,32 +5,28 @@ import {
|
|||||||
List,
|
List,
|
||||||
EditButton,
|
EditButton,
|
||||||
ShowButton,
|
ShowButton,
|
||||||
DateField,
|
|
||||||
TextField,
|
|
||||||
CloneButton,
|
CloneButton,
|
||||||
} from "@refinedev/antd";
|
} from "@refinedev/antd";
|
||||||
import { Table, Space, Button, Row, Col } from "antd";
|
import { Table, Space, Button, Dropdown } from "antd";
|
||||||
import { NumberFieldUnit } from "../../components/numberField";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import {
|
import { genericSorter, typeSorters } from "../../utils/sorting";
|
||||||
genericSorter,
|
import { genericFilterer, typeFilters } from "../../utils/filtering";
|
||||||
getSortOrderForField,
|
|
||||||
typeSorters,
|
|
||||||
} from "../../utils/sorting";
|
|
||||||
import {
|
|
||||||
genericFilterer,
|
|
||||||
getFiltersForField,
|
|
||||||
typeFilters,
|
|
||||||
useListFiltersForField,
|
|
||||||
} from "../../utils/filtering";
|
|
||||||
import { ISpool } from "./model";
|
import { ISpool } from "./model";
|
||||||
import {
|
import {
|
||||||
|
TableState,
|
||||||
useInitialTableState,
|
useInitialTableState,
|
||||||
useStoreInitialState,
|
useStoreInitialState,
|
||||||
} from "../../utils/saveload";
|
} from "../../utils/saveload";
|
||||||
import Icon, { FilterOutlined } from "@ant-design/icons";
|
import { EditOutlined, FilterOutlined } from "@ant-design/icons";
|
||||||
import { ReactComponent as SpoolIcon } from "../../icon_spool.svg";
|
import {
|
||||||
|
DateColumn,
|
||||||
|
FilteredColumn,
|
||||||
|
NumberColumn,
|
||||||
|
SortedColumn,
|
||||||
|
SpoolIconColumn,
|
||||||
|
} from "../../components/column";
|
||||||
|
import i18n from "../../i18n";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@@ -70,16 +66,34 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create state for the columns to show
|
||||||
|
const allColumns: (keyof ISpoolCollapsed & string)[] = [
|
||||||
|
"id",
|
||||||
|
"filament_name",
|
||||||
|
"used_weight",
|
||||||
|
"remaining_weight",
|
||||||
|
"location",
|
||||||
|
"lot_nr",
|
||||||
|
"first_used",
|
||||||
|
"last_used",
|
||||||
|
"comment",
|
||||||
|
];
|
||||||
|
const [showColumns, setShowColumns] = React.useState<string[]>(
|
||||||
|
initialState.showColumns ?? allColumns
|
||||||
|
);
|
||||||
|
|
||||||
// Type the sorters and filters
|
// Type the sorters and filters
|
||||||
const typedSorters = typeSorters<ISpoolCollapsed>(sorters);
|
const typedSorters = typeSorters<ISpoolCollapsed>(sorters);
|
||||||
const typedFilters = typeFilters<ISpoolCollapsed>(filters);
|
const typedFilters = typeFilters<ISpoolCollapsed>(filters);
|
||||||
|
|
||||||
// Store state in local storage
|
// Store state in local storage
|
||||||
useStoreInitialState("spoolList", {
|
const tableState: TableState = {
|
||||||
sorters,
|
sorters,
|
||||||
filters,
|
filters,
|
||||||
pagination: { current, pageSize },
|
pagination: { current, pageSize },
|
||||||
});
|
showColumns,
|
||||||
|
};
|
||||||
|
useStoreInitialState("spoolList", tableState);
|
||||||
|
|
||||||
// Collapse the dataSource to a mutable list and add a filament_name field
|
// Collapse the dataSource to a mutable list and add a filament_name field
|
||||||
const dataSource: ISpoolCollapsed[] = React.useMemo(
|
const dataSource: ISpoolCollapsed[] = React.useMemo(
|
||||||
@@ -119,6 +133,28 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
>
|
>
|
||||||
Clear Filters
|
Clear Filters
|
||||||
</Button>
|
</Button>
|
||||||
|
<Dropdown
|
||||||
|
trigger={["click"]}
|
||||||
|
menu={{
|
||||||
|
items: allColumns.map((column) => ({
|
||||||
|
key: column,
|
||||||
|
label: i18n.t(`spools.fields.${column}`),
|
||||||
|
})),
|
||||||
|
selectedKeys: showColumns,
|
||||||
|
selectable: true,
|
||||||
|
multiple: true,
|
||||||
|
onDeselect: (keys) => {
|
||||||
|
setShowColumns(keys.selectedKeys);
|
||||||
|
},
|
||||||
|
onSelect: (keys) => {
|
||||||
|
setShowColumns(keys.selectedKeys);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" icon={<EditOutlined />}>
|
||||||
|
Hide Columns
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
{defaultButtons}
|
{defaultButtons}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -137,138 +173,67 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
}}
|
}}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
>
|
>
|
||||||
<Table.Column
|
{SortedColumn({
|
||||||
dataIndex="id"
|
id: "id",
|
||||||
title="Id"
|
title: "Id",
|
||||||
sorter={true}
|
dataSource,
|
||||||
sortOrder={getSortOrderForField(typedSorters, "id")}
|
tableState,
|
||||||
/>
|
})}
|
||||||
<Table.Column
|
{SpoolIconColumn({
|
||||||
dataIndex="filament_name"
|
id: "filament_name",
|
||||||
title="Filament"
|
title: "Filament",
|
||||||
sorter={true}
|
color: (record: ISpoolCollapsed) => record.filament.color_hex,
|
||||||
sortOrder={getSortOrderForField(typedSorters, "filament_name")}
|
dataSource,
|
||||||
filters={useListFiltersForField(dataSource, "filament_name")}
|
tableState,
|
||||||
filteredValue={getFiltersForField(typedFilters, "filament_name")}
|
})}
|
||||||
render={(value, record: ISpool) => {
|
{NumberColumn({
|
||||||
return {
|
id: "used_weight",
|
||||||
props: {
|
title: "Used Weight",
|
||||||
style: {
|
unit: "g",
|
||||||
paddingLeft: 0,
|
decimals: 1,
|
||||||
paddingTop: 0,
|
dataSource,
|
||||||
paddingBottom: 0,
|
tableState,
|
||||||
},
|
})}
|
||||||
},
|
{NumberColumn({
|
||||||
children: (
|
id: "remaining_weight",
|
||||||
<Row wrap={false} justify="space-around" align="middle">
|
title: "Estimated Remaining Weight",
|
||||||
{record.filament.color_hex && (
|
unit: "g",
|
||||||
<Col flex="none">
|
decimals: 1,
|
||||||
<Icon
|
dataSource,
|
||||||
component={SpoolIcon}
|
tableState,
|
||||||
style={{
|
})}
|
||||||
color: "#" + record.filament.color_hex,
|
{FilteredColumn({
|
||||||
fontSize: 42,
|
id: "location",
|
||||||
marginRight: 0,
|
title: "Location",
|
||||||
}}
|
dataSource,
|
||||||
/>
|
tableState,
|
||||||
</Col>
|
})}
|
||||||
)}
|
{FilteredColumn({
|
||||||
<Col flex="auto">{value}</Col>
|
id: "lot_nr",
|
||||||
</Row>
|
title: "Lot Nr",
|
||||||
),
|
dataSource,
|
||||||
};
|
tableState,
|
||||||
}}
|
})}
|
||||||
/>
|
{DateColumn({
|
||||||
<Table.Column
|
id: "first_used",
|
||||||
dataIndex="used_weight"
|
title: "First Used",
|
||||||
title="Used Weight"
|
dataSource,
|
||||||
sorter={true}
|
tableState,
|
||||||
sortOrder={getSortOrderForField(typedSorters, "used_weight")}
|
})}
|
||||||
render={(value) => {
|
{DateColumn({
|
||||||
return (
|
id: "last_used",
|
||||||
<NumberFieldUnit
|
title: "Last Used",
|
||||||
value={value}
|
dataSource,
|
||||||
unit="g"
|
tableState,
|
||||||
options={{
|
})}
|
||||||
maximumFractionDigits: 1,
|
{SortedColumn({
|
||||||
}}
|
id: "comment",
|
||||||
/>
|
title: "Comment",
|
||||||
);
|
dataSource,
|
||||||
}}
|
tableState,
|
||||||
/>
|
})}
|
||||||
<Table.Column
|
|
||||||
dataIndex="remaining_weight"
|
|
||||||
title="Estimated Remaining Weight"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "remaining_weight")}
|
|
||||||
render={(value) => {
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return <TextField value="Unknown" />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<NumberFieldUnit
|
|
||||||
value={Math.max(value, 0)}
|
|
||||||
unit="g"
|
|
||||||
options={{
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
|
||||||
dataIndex="location"
|
|
||||||
title="Location"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "location")}
|
|
||||||
filters={useListFiltersForField(dataSource, "location")}
|
|
||||||
filteredValue={getFiltersForField(typedFilters, "location")}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
|
||||||
dataIndex="lot_nr"
|
|
||||||
title="Lot Nr"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "lot_nr")}
|
|
||||||
filters={useListFiltersForField(dataSource, "lot_nr")}
|
|
||||||
filteredValue={getFiltersForField(typedFilters, "lot_nr")}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
|
||||||
dataIndex={["first_used"]}
|
|
||||||
title="First Used"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "first_used")}
|
|
||||||
render={(value) => (
|
|
||||||
<DateField
|
|
||||||
hidden={!value}
|
|
||||||
value={dayjs.utc(value).local()}
|
|
||||||
title={dayjs.utc(value).local().format()}
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
|
||||||
dataIndex={["last_used"]}
|
|
||||||
title="Last Used"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "last_used")}
|
|
||||||
render={(value) => (
|
|
||||||
<DateField
|
|
||||||
hidden={!value}
|
|
||||||
value={dayjs.utc(value).local()}
|
|
||||||
title={dayjs.utc(value).local().format()}
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
|
||||||
dataIndex={["comment"]}
|
|
||||||
title="Comment"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "comment")}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
<Table.Column
|
||||||
title="Actions"
|
title="Actions"
|
||||||
dataIndex="actions"
|
|
||||||
render={(_, record: BaseRecord) => (
|
render={(_, record: BaseRecord) => (
|
||||||
<Space>
|
<Space>
|
||||||
<EditButton
|
<EditButton
|
||||||
|
|||||||
108
client/src/pages/vendors/list.tsx
vendored
108
client/src/pages/vendors/list.tsx
vendored
@@ -5,24 +5,22 @@ import {
|
|||||||
List,
|
List,
|
||||||
EditButton,
|
EditButton,
|
||||||
ShowButton,
|
ShowButton,
|
||||||
DateField,
|
|
||||||
CloneButton,
|
CloneButton,
|
||||||
} from "@refinedev/antd";
|
} from "@refinedev/antd";
|
||||||
import { Table, Space, Button } from "antd";
|
import { Table, Space, 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 {
|
import { genericSorter, typeSorters } from "../../utils/sorting";
|
||||||
genericSorter,
|
|
||||||
getSortOrderForField,
|
|
||||||
typeSorters,
|
|
||||||
} from "../../utils/sorting";
|
|
||||||
import { genericFilterer, typeFilters } from "../../utils/filtering";
|
import { genericFilterer, typeFilters } from "../../utils/filtering";
|
||||||
import { IVendor } from "./model";
|
import { IVendor } from "./model";
|
||||||
import {
|
import {
|
||||||
|
TableState,
|
||||||
useInitialTableState,
|
useInitialTableState,
|
||||||
useStoreInitialState,
|
useStoreInitialState,
|
||||||
} from "../../utils/saveload";
|
} from "../../utils/saveload";
|
||||||
import { FilterOutlined } from "@ant-design/icons";
|
import { EditOutlined, FilterOutlined } from "@ant-design/icons";
|
||||||
|
import i18n from "../../i18n";
|
||||||
|
import { DateColumn, SortedColumn } from "../../components/column";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@@ -58,16 +56,29 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
|
||||||
// Type the sorters and filters
|
// Type the sorters and filters
|
||||||
const typedSorters = typeSorters<IVendor>(sorters);
|
const typedSorters = typeSorters<IVendor>(sorters);
|
||||||
const typedFilters = typeFilters<IVendor>(filters);
|
const typedFilters = typeFilters<IVendor>(filters);
|
||||||
|
|
||||||
// Store state in local storage
|
// Store state in local storage
|
||||||
useStoreInitialState("vendorList", {
|
const tableState: TableState = {
|
||||||
sorters,
|
sorters,
|
||||||
filters,
|
filters,
|
||||||
pagination: { current, pageSize },
|
pagination: { current, pageSize },
|
||||||
});
|
showColumns,
|
||||||
|
};
|
||||||
|
useStoreInitialState("vendorList", tableState);
|
||||||
|
|
||||||
// Collapse the dataSource to a mutable list
|
// Collapse the dataSource to a mutable list
|
||||||
const dataSource: IVendor[] = React.useMemo(
|
const dataSource: IVendor[] = React.useMemo(
|
||||||
@@ -97,6 +108,28 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
>
|
>
|
||||||
Clear Filters
|
Clear Filters
|
||||||
</Button>
|
</Button>
|
||||||
|
<Dropdown
|
||||||
|
trigger={["click"]}
|
||||||
|
menu={{
|
||||||
|
items: allColumns.map((column) => ({
|
||||||
|
key: column,
|
||||||
|
label: i18n.t(`vendors.fields.${column}`),
|
||||||
|
})),
|
||||||
|
selectedKeys: showColumns,
|
||||||
|
selectable: true,
|
||||||
|
multiple: true,
|
||||||
|
onDeselect: (keys) => {
|
||||||
|
setShowColumns(keys.selectedKeys);
|
||||||
|
},
|
||||||
|
onSelect: (keys) => {
|
||||||
|
setShowColumns(keys.selectedKeys);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" icon={<EditOutlined />}>
|
||||||
|
Hide Columns
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
{defaultButtons}
|
{defaultButtons}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -115,37 +148,30 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
|||||||
}}
|
}}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
>
|
>
|
||||||
<Table.Column
|
{SortedColumn({
|
||||||
dataIndex="id"
|
id: "id",
|
||||||
title="Id"
|
title: "Id",
|
||||||
sorter={true}
|
dataSource,
|
||||||
sortOrder={getSortOrderForField(typedSorters, "id")}
|
tableState,
|
||||||
/>
|
})}
|
||||||
<Table.Column
|
{SortedColumn({
|
||||||
dataIndex="name"
|
id: "name",
|
||||||
title="Name"
|
title: "Name",
|
||||||
sorter={true}
|
dataSource,
|
||||||
sortOrder={getSortOrderForField(typedSorters, "name")}
|
tableState,
|
||||||
/>
|
})}
|
||||||
<Table.Column
|
{DateColumn({
|
||||||
dataIndex={["registered"]}
|
id: "registered",
|
||||||
title="Registered"
|
title: "Registered",
|
||||||
sorter={true}
|
dataSource,
|
||||||
sortOrder={getSortOrderForField(typedSorters, "registered")}
|
tableState,
|
||||||
render={(value) => (
|
})}
|
||||||
<DateField
|
{SortedColumn({
|
||||||
value={dayjs.utc(value).local()}
|
id: "comment",
|
||||||
title={dayjs.utc(value).local().format()}
|
title: "Comment",
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
dataSource,
|
||||||
/>
|
tableState,
|
||||||
)}
|
})}
|
||||||
/>
|
|
||||||
<Table.Column
|
|
||||||
dataIndex={["comment"]}
|
|
||||||
title="Comment"
|
|
||||||
sorter={true}
|
|
||||||
sortOrder={getSortOrderForField(typedSorters, "comment")}
|
|
||||||
/>
|
|
||||||
<Table.Column
|
<Table.Column
|
||||||
title="Actions"
|
title="Actions"
|
||||||
dataIndex="actions"
|
dataIndex="actions"
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ interface Pagination {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableState {
|
export interface TableState {
|
||||||
sorters: CrudSort[];
|
sorters: CrudSort[];
|
||||||
filters: CrudFilter[];
|
filters: CrudFilter[];
|
||||||
pagination: Pagination;
|
pagination: Pagination;
|
||||||
|
showColumns?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInitialTableState(tableId: string): TableState {
|
export function useInitialTableState(tableId: string): TableState {
|
||||||
@@ -18,10 +19,13 @@ export function useInitialTableState(tableId: string): TableState {
|
|||||||
const savedSorters = isLocalStorageAvailable ? localStorage.getItem(`${tableId}-sorters`) : null;
|
const savedSorters = isLocalStorageAvailable ? localStorage.getItem(`${tableId}-sorters`) : null;
|
||||||
const savedFilters = isLocalStorageAvailable ? localStorage.getItem(`${tableId}-filters`) : null;
|
const savedFilters = isLocalStorageAvailable ? localStorage.getItem(`${tableId}-filters`) : null;
|
||||||
const savedPagination = isLocalStorageAvailable ? localStorage.getItem(`${tableId}-pagination`) : null;
|
const savedPagination = isLocalStorageAvailable ? localStorage.getItem(`${tableId}-pagination`) : null;
|
||||||
|
const savedShowColumns = isLocalStorageAvailable ? localStorage.getItem(`${tableId}-showColumns`) : null;
|
||||||
|
|
||||||
const sorters = savedSorters ? JSON.parse(savedSorters) : [{ field: "id", order: "asc" }];
|
const sorters = savedSorters ? JSON.parse(savedSorters) : [{ field: "id", order: "asc" }];
|
||||||
const filters = savedFilters ? JSON.parse(savedFilters) : [];
|
const filters = savedFilters ? JSON.parse(savedFilters) : [];
|
||||||
const pagination = savedPagination ? JSON.parse(savedPagination) : { page: 1, pageSize: 20 };
|
const pagination = savedPagination ? JSON.parse(savedPagination) : { page: 1, pageSize: 20 };
|
||||||
return { sorters, filters, pagination };
|
const showColumns = savedShowColumns ? JSON.parse(savedShowColumns) : undefined;
|
||||||
|
return { sorters, filters, pagination, showColumns };
|
||||||
});
|
});
|
||||||
return initialState;
|
return initialState;
|
||||||
}
|
}
|
||||||
@@ -44,4 +48,14 @@ export function useStoreInitialState(tableId: string, state: TableState) {
|
|||||||
localStorage.setItem(`${tableId}-pagination`, JSON.stringify(state.pagination));
|
localStorage.setItem(`${tableId}-pagination`, JSON.stringify(state.pagination));
|
||||||
}
|
}
|
||||||
}, [tableId, state.pagination]);
|
}, [tableId, state.pagination]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isLocalStorageAvailable) {
|
||||||
|
if (state.showColumns === undefined) {
|
||||||
|
localStorage.removeItem(`${tableId}-showColumns`);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(`${tableId}-showColumns`, JSON.stringify(state.showColumns));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [tableId, state.showColumns]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user