/ client / src / pages / filaments / list.tsx
list.tsx
  1  import { EditOutlined, EyeOutlined, FileOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
  2  import { List, useTable } from "@refinedev/antd";
  3  import { useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
  4  import { Button, Dropdown, Table } from "antd";
  5  import dayjs from "dayjs";
  6  import utc from "dayjs/plugin/utc";
  7  import { useMemo, useState } from "react";
  8  import { useNavigate } from "react-router";
  9  import {
 10    ActionsColumn,
 11    CustomFieldColumn,
 12    DateColumn,
 13    FilteredQueryColumn,
 14    NumberColumn,
 15    RichColumn,
 16    SortedColumn,
 17    SpoolIconColumn,
 18  } from "../../components/column";
 19  import { useLiveify } from "../../components/liveify";
 20  import {
 21    useSpoolmanArticleNumbers,
 22    useSpoolmanFilamentNames,
 23    useSpoolmanMaterials,
 24    useSpoolmanVendors,
 25  } from "../../components/otherModels";
 26  import { removeUndefined } from "../../utils/filtering";
 27  import { EntityType, useGetFields } from "../../utils/queryFields";
 28  import { TableState, useInitialTableState, useStoreInitialState } from "../../utils/saveload";
 29  import { useCurrencyFormatter } from "../../utils/settings";
 30  import { IFilament } from "./model";
 31  
 32  dayjs.extend(utc);
 33  
 34  interface IFilamentCollapsed extends Omit<IFilament, "vendor"> {
 35    "vendor.name": string | null;
 36  }
 37  
 38  function collapseFilament(element: IFilament): IFilamentCollapsed {
 39    let vendor_name: string | null;
 40    if (element.vendor) {
 41      vendor_name = element.vendor.name;
 42    } else {
 43      vendor_name = null;
 44    }
 45    return { ...element, "vendor.name": vendor_name };
 46  }
 47  
 48  function translateColumnI18nKey(columnName: string): string {
 49    columnName = columnName.replace(".", "_");
 50    return `filament.fields.${columnName}`;
 51  }
 52  
 53  const namespace = "filamentList-v2";
 54  
 55  const allColumns: (keyof IFilamentCollapsed & string)[] = [
 56    "id",
 57    "vendor.name",
 58    "name",
 59    "material",
 60    "price",
 61    "density",
 62    "diameter",
 63    "weight",
 64    "spool_weight",
 65    "article_number",
 66    "settings_extruder_temp",
 67    "settings_bed_temp",
 68    "registered",
 69    "comment",
 70  ];
 71  const defaultColumns = allColumns.filter(
 72    (column_id) => ["registered", "density", "diameter", "spool_weight"].indexOf(column_id) === -1,
 73  );
 74  
 75  export const FilamentList = () => {
 76    const t = useTranslate();
 77    const invalidate = useInvalidate();
 78    const navigate = useNavigate();
 79    const extraFields = useGetFields(EntityType.filament);
 80    const currencyFormatter = useCurrencyFormatter();
 81  
 82    const allColumnsWithExtraFields = [...allColumns, ...(extraFields.data?.map((field) => "extra." + field.key) ?? [])];
 83  
 84    // Load initial state
 85    const initialState = useInitialTableState(namespace);
 86  
 87    // Fetch data from the API
 88    // To provide the live updates, we use a custom solution (useLiveify) instead of the built-in refine "liveMode" feature.
 89    // This is because the built-in feature does not call the liveProvider subscriber with a list of IDs, but instead
 90    // calls it with a list of filters, sorters, etc. This means the server-side has to support this, which is quite hard.
 91    const { tableProps, sorters, setSorters, filters, setFilters, currentPage, pageSize, setCurrentPage } =
 92      useTable<IFilamentCollapsed>({
 93        syncWithLocation: false,
 94        pagination: {
 95          mode: "server",
 96          currentPage: initialState.pagination.currentPage,
 97          pageSize: initialState.pagination.pageSize,
 98        },
 99        sorters: {
100          mode: "server",
101          initial: initialState.sorters,
102        },
103        filters: {
104          mode: "server",
105          initial: initialState.filters,
106        },
107        liveMode: "manual",
108        onLiveEvent(event) {
109          if (event.type === "created" || event.type === "deleted") {
110            // updated is handled by the liveify
111            invalidate({
112              resource: "filament",
113              invalidates: ["list"],
114            });
115          }
116        },
117        queryOptions: {
118          select(data) {
119            return {
120              total: data.total,
121              data: data.data.map(collapseFilament),
122            };
123          },
124        },
125      });
126  
127    // Create state for the columns to show
128    const [showColumns, setShowColumns] = useState<string[]>(initialState.showColumns ?? defaultColumns);
129  
130    // Store state in local storage
131    const tableState: TableState = {
132      sorters,
133      filters,
134      pagination: { currentPage: currentPage, pageSize },
135      showColumns,
136    };
137    useStoreInitialState(namespace, tableState);
138  
139    // Collapse the dataSource to a mutable list
140    const queryDataSource: IFilamentCollapsed[] = useMemo(
141      () => (tableProps.dataSource || []).map((record) => ({ ...record })),
142      [tableProps.dataSource],
143    );
144    const dataSource = useLiveify("filament", queryDataSource, collapseFilament);
145  
146    if (tableProps.pagination) {
147      tableProps.pagination.showSizeChanger = true;
148    }
149  
150    const { editUrl, showUrl, cloneUrl } = useNavigation();
151    const filamentAddSpoolUrl = (id: number): string => `/spool/create?filament_id=${id}`;
152    const actions = (record: IFilamentCollapsed) => [
153      { name: t("buttons.show"), icon: <EyeOutlined />, link: showUrl("filament", record.id) },
154      { name: t("buttons.edit"), icon: <EditOutlined />, link: editUrl("filament", record.id) },
155      { name: t("buttons.clone"), icon: <PlusSquareOutlined />, link: cloneUrl("filament", record.id) },
156      { name: t("filament.buttons.add_spool"), icon: <FileOutlined />, link: filamentAddSpoolUrl(record.id) },
157    ];
158  
159    const commonProps = {
160      t,
161      navigate,
162      actions,
163      dataSource,
164      tableState,
165      sorter: true,
166    };
167  
168    return (
169      <List
170        headerButtons={({ defaultButtons }) => (
171          <>
172            <Button
173              type="primary"
174              icon={<FilterOutlined />}
175              onClick={() => {
176                setFilters([], "replace");
177                setSorters([{ field: "id", order: "asc" }]);
178                setCurrentPage(1);
179              }}
180            >
181              {t("buttons.clearFilters")}
182            </Button>
183            <Dropdown
184              trigger={["click"]}
185              menu={{
186                items: allColumnsWithExtraFields.map((column_id) => {
187                  if (column_id.indexOf("extra.") === 0) {
188                    const extraField = extraFields.data?.find((field) => "extra." + field.key === column_id);
189                    return {
190                      key: column_id,
191                      label: extraField?.name ?? column_id,
192                    };
193                  }
194  
195                  return {
196                    key: column_id,
197                    label: t(translateColumnI18nKey(column_id)),
198                  };
199                }),
200                selectedKeys: showColumns,
201                selectable: true,
202                multiple: true,
203                onDeselect: (keys) => {
204                  setShowColumns(keys.selectedKeys);
205                },
206                onSelect: (keys) => {
207                  setShowColumns(keys.selectedKeys);
208                },
209              }}
210            >
211              <Button type="primary" icon={<EditOutlined />}>
212                {t("buttons.hideColumns")}
213              </Button>
214            </Dropdown>
215            {defaultButtons}
216          </>
217        )}
218      >
219        <Table<IFilamentCollapsed>
220          {...tableProps}
221          sticky
222          tableLayout="auto"
223          scroll={{ x: "max-content" }}
224          dataSource={dataSource}
225          rowKey="id"
226          columns={removeUndefined([
227            SortedColumn({
228              ...commonProps,
229              id: "id",
230              i18ncat: "filament",
231              width: 70,
232            }),
233            FilteredQueryColumn({
234              ...commonProps,
235              id: "vendor.name",
236              i18nkey: "filament.fields.vendor_name",
237              filterValueQuery: useSpoolmanVendors(),
238            }),
239            SpoolIconColumn({
240              ...commonProps,
241              id: "name",
242              i18ncat: "filament",
243              color: (record: IFilamentCollapsed) =>
244                record.multi_color_hexes
245                  ? {
246                      colors: record.multi_color_hexes.split(","),
247                      vertical: record.multi_color_direction === "longitudinal",
248                    }
249                  : record.color_hex,
250              filterValueQuery: useSpoolmanFilamentNames(),
251            }),
252            FilteredQueryColumn({
253              ...commonProps,
254              id: "material",
255              i18ncat: "filament",
256              filterValueQuery: useSpoolmanMaterials(),
257              width: 110,
258            }),
259            SortedColumn({
260              ...commonProps,
261              id: "price",
262              i18ncat: "filament",
263              align: "right",
264              width: 80,
265              render: (_, obj: IFilamentCollapsed) => {
266                if (obj.price === undefined) {
267                  return "";
268                }
269                return currencyFormatter.format(obj.price);
270              },
271            }),
272            NumberColumn({
273              ...commonProps,
274              id: "density",
275              i18ncat: "filament",
276              unit: "g/cm³",
277              maxDecimals: 2,
278              width: 100,
279            }),
280            NumberColumn({
281              ...commonProps,
282              id: "diameter",
283              i18ncat: "filament",
284              unit: "mm",
285              maxDecimals: 2,
286              width: 100,
287            }),
288            NumberColumn({
289              ...commonProps,
290              id: "weight",
291              i18ncat: "filament",
292              unit: "g",
293              maxDecimals: 0,
294              width: 100,
295            }),
296            NumberColumn({
297              ...commonProps,
298              id: "spool_weight",
299              i18ncat: "filament",
300              unit: "g",
301              maxDecimals: 0,
302              width: 100,
303            }),
304            FilteredQueryColumn({
305              ...commonProps,
306              id: "article_number",
307              i18ncat: "filament",
308              filterValueQuery: useSpoolmanArticleNumbers(),
309              width: 130,
310            }),
311            NumberColumn({
312              ...commonProps,
313              id: "settings_extruder_temp",
314              i18ncat: "filament",
315              unit: "°C",
316              maxDecimals: 0,
317              width: 100,
318            }),
319            NumberColumn({
320              ...commonProps,
321              id: "settings_bed_temp",
322              i18ncat: "filament",
323              unit: "°C",
324              maxDecimals: 0,
325              width: 100,
326            }),
327            DateColumn({
328              ...commonProps,
329              id: "registered",
330              i18ncat: "filament",
331            }),
332            ...(extraFields.data?.map((field) => {
333              return CustomFieldColumn({
334                ...commonProps,
335                field,
336              });
337            }) ?? []),
338            RichColumn({
339              ...commonProps,
340              id: "comment",
341              i18ncat: "filament",
342              width: 150,
343            }),
344            ActionsColumn(t("table.actions"), actions),
345          ])}
346        />
347      </List>
348    );
349  };
350  
351  export default FilamentList;