diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 23e3f97..21c1326 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -41,7 +41,9 @@ "deleteError": "Error when deleting {{resource}} (status code: {{statusCode}})", "editSuccess": "Successfully edited {{resource}}", "editError": "Error when editing {{resource}} (status code: {{statusCode}})", - "importProgress": "Importing: {{processed}}/{{total}}" + "importProgress": "Importing: {{processed}}/{{total}}", + "saveSuccessful": "Save successful!", + "validationError": "Validation error: {{error}}" }, "kofi": "Tip me on Ko-fi", "loading": "Loading", @@ -252,6 +254,46 @@ "table": { "actions": "Actions" }, + "settings": { + "header": "Settings", + "general": { + "tab": "General", + "currency": { + "label": "Currency" + } + }, + "extra_fields": { + "tab": "Extra Fields", + "description": "

Here you can add extra custom fields to your entities.

Once a field is added, you can not change its key or type, and for choice type fields you can not remove choices or change the multi choice state. If you remove a field, the associated data for all entities will be deleted.

The key is what other programs read/write the data as, so if your custom field is supposed to integrate with a third-party program, make sure to set it correctly. Default value is only applied to new items.

Extra fields can not be sorted or filtered in the table views.

", + "params": { + "key": "Key", + "name": "Name", + "field_type": "Type", + "unit": "Unit", + "order": "Order", + "default_value": "Default Value", + "choices": "Choices", + "multi_choice": "Multi Choice" + }, + "field_type": { + "text": "Text", + "integer": "Integer", + "integer_range": "Integer Range", + "float": "Float", + "float_range": "Float Range", + "datetime": "Datetime", + "boolean": "Boolean", + "choice": "Choice" + }, + "boolean_true": "Yes", + "boolean_false": "No", + "choices_missing_error": "You cannot remove choices. The following choices are missing: {{choices}}", + "non_unique_key_error": "The key must be unique.", + "key_not_changed": "Please change the key to something else.", + "delete_confirm": "Delete field {{name}}?", + "delete_confirm_description": "This will delete the field and all associated data for all entities." + } + }, "documentTitle": { "default": "Spoolman", "suffix": " | Spoolman", diff --git a/client/public/locales/sv/common.json b/client/public/locales/sv/common.json index ba75663..a21903b 100644 --- a/client/public/locales/sv/common.json +++ b/client/public/locales/sv/common.json @@ -41,7 +41,8 @@ "deleteError": "Det gick inte att ta bort {{resource}} (statuskod: {{statusCode}})", "editSuccess": "Lyckades ändra {{resource}}", "editError": "Det gick inte att ändra {{resource}} (statuskod: {{statusCode}})", - "importProgress": "Importerar: {{processed}}/{{total}}" + "importProgress": "Importerar: {{processed}}/{{total}}", + "saveSuccessful": "Sparning lyckades!" }, "loading": "Laddar", "version": "Version", @@ -240,6 +241,18 @@ "table": { "actions": "Åtgärder" }, + "settings": { + "header": "Inställningar", + "generic": { + "tab": "Allmänt", + "currency": { + "label": "Valuta" + } + }, + "extra_fields": { + "tab": "Extra Fält" + } + }, "documentTitle": { "default": "Spoolman", "suffix": " | Spoolman", diff --git a/client/src/App.tsx b/client/src/App.tsx index bc2c03e..fbcddf3 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,7 +10,14 @@ import dataProvider from "./components/dataProvider"; import { useTranslation } from "react-i18next"; import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom"; import { ColorModeContextProvider } from "./contexts/color-mode"; -import { FileOutlined, HighlightOutlined, HomeOutlined, QuestionOutlined, UserOutlined } from "@ant-design/icons"; +import { + FileOutlined, + HighlightOutlined, + HomeOutlined, + QuestionOutlined, + ToolOutlined, + UserOutlined, +} from "@ant-design/icons"; import { ConfigProvider } from "antd"; import React from "react"; import { Locale } from "antd/es/locale"; @@ -139,6 +146,14 @@ function App() { icon: , }, }, + { + name: "settings", + list: "/settings", + meta: { + canDelete: false, + icon: , + }, + }, { name: "help", list: "/help", @@ -202,6 +217,7 @@ function App() { } /> } /> + } /> } /> } /> diff --git a/client/src/components/column.tsx b/client/src/components/column.tsx index 53f89a7..a773e3d 100644 --- a/client/src/components/column.tsx +++ b/client/src/components/column.tsx @@ -3,7 +3,7 @@ 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"; -import { NumberFieldUnit } from "./numberField"; +import { NumberFieldUnit, NumberFieldUnitRange } from "./numberField"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import { DateField, TextField } from "@refinedev/antd"; @@ -12,7 +12,8 @@ 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"; +import { Link } from "react-router-dom"; +import { Field, FieldType } from "../utils/queryFields"; dayjs.extend(utc); @@ -39,14 +40,19 @@ export interface Action { } interface BaseColumnProps { - id: keyof Obj & string; + id: string | string[]; dataId?: keyof Obj & string; i18ncat?: string; i18nkey?: string; + title?: string; + sorter?: boolean; + t: (key: string) => string; + navigate: (link: string) => void; dataSource: Obj[]; tableState: TableState; width?: number; actions?: (record: Obj) => Action[]; + transform?: (value: unknown) => unknown; } interface FilteredColumnProps { @@ -70,24 +76,32 @@ interface CustomColumnProps { function Column( props: BaseColumnProps & FilteredColumnProps & CustomColumnProps ): ColumnType | undefined { - const t = useTranslate(); - const navigate = useNavigate(); + const t = props.t; + const navigate = props.navigate; // Hide if not in showColumns - if (props.tableState.showColumns && !props.tableState.showColumns.includes(props.id)) { + const id = Array.isArray(props.id) ? props.id.join(".") : props.id; + if (props.tableState.showColumns && !props.tableState.showColumns.includes(id)) { return undefined; } const columnProps: ColumnType = { dataIndex: props.id, - title: t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`), - sorter: true, - sortOrder: getSortOrderForField(typeSorters(props.tableState.sorters), props.dataId ?? props.id), + title: props.title ?? t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`), filterMultiple: props.allowMultipleFilters ?? true, width: props.width ?? undefined, onCell: props.onCell ?? undefined, }; + // Sorting + if (props.sorter) { + columnProps.sorter = true; + columnProps.sortOrder = getSortOrderForField( + typeSorters(props.tableState.sorters), + props.dataId ?? (props.id as keyof Obj) + ); + } + // Filter if (props.filters && props.filteredValue) { columnProps.filters = props.filters; @@ -106,7 +120,12 @@ function Column( } // Render - const render = props.render ?? ((value) => <>{value}); + const render = + props.render ?? + ((rawValue) => { + const value = props.transform ? props.transform(rawValue) : rawValue; + return <>{value}; + }); columnProps.render = (value, record, index) => { if (!props.actions) { return render(value, record, index); @@ -144,13 +163,19 @@ function Column( } export function SortedColumn(props: BaseColumnProps) { - return Column(props); -} - -export function RichColumn(props: BaseColumnProps) { return Column({ ...props, - render: (value: string | undefined) => { + sorter: true, + }); +} + +export function RichColumn( + props: Omit, "transform"> & { transform?: (value: unknown) => string } +) { + return Column({ + ...props, + render: (rawValue: string | undefined) => { + const value = props.transform ? props.transform(rawValue) : rawValue; return enrichText(value); }, }); @@ -182,7 +207,7 @@ export function FilteredQueryColumn(props: FilteredQueryColu }); const typedFilters = typeFilters(props.tableState.filters); - const filteredValue = getFiltersForField(typedFilters, props.dataId ?? props.id); + const filteredValue = getFiltersForField(typedFilters, props.dataId ?? (props.id as keyof Obj)); const onFilterDropdownOpen = () => { query.refetch(); @@ -193,14 +218,16 @@ export function FilteredQueryColumn(props: FilteredQueryColu interface NumberColumnProps extends BaseColumnProps { unit: string; - decimals?: number; + maxDecimals?: number; + minDecimals?: number; defaultText?: string; } export function NumberColumn(props: NumberColumnProps) { return Column({ ...props, - render: (value) => { + render: (rawValue) => { + const value = props.transform ? props.transform(rawValue) : rawValue; if (value === null || value === undefined) { return ; } @@ -209,8 +236,8 @@ export function NumberColumn(props: NumberColumnProps) value={value} unit={props.unit} options={{ - maximumFractionDigits: props.decimals ?? 0, - minimumFractionDigits: props.decimals ?? 0, + maximumFractionDigits: props.maxDecimals ?? 0, + minimumFractionDigits: props.minDecimals ?? props.maxDecimals ?? 0, }} /> ); @@ -221,7 +248,8 @@ export function NumberColumn(props: NumberColumnProps) export function DateColumn(props: BaseColumnProps) { return Column({ ...props, - render: (value) => { + render: (rawValue) => { + const value = props.transform ? props.transform(rawValue) : rawValue; return (