@ifto/mantine-kit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/dist/component/organism/Table/Table.js +3 -1
- package/dist/component/organism/Table/Table.js.map +1 -1
- package/dist/component/organism/ViewTable/ViewTable.js +6 -14
- package/dist/component/organism/ViewTable/ViewTable.js.map +1 -1
- package/dist/component/organism/ViewTable/useRowKey.d.ts +8 -0
- package/dist/component/organism/ViewTable/useRowKey.js +24 -0
- package/dist/component/organism/ViewTable/useRowKey.js.map +1 -0
- package/dist/component/template/FormPage/FormPage.d.ts +1 -1
- package/dist/component/template/FormPage/FormPage.js.map +1 -1
- package/dist/hook/useService.d.ts +21 -6
- package/dist/hook/useService.js +3 -9
- package/dist/hook/useService.js.map +1 -1
- package/dist/util/DateUtil.d.ts +3 -2
- package/dist/util/DateUtil.js +2 -0
- package/dist/util/DateUtil.js.map +1 -1
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -38,14 +38,17 @@ a REST HTTP client.
|
|
|
38
38
|
**1. Install** the library and its required peer dependencies:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
yarn add @ifto/mantine-kit @mantine/core @mantine/hooks @mantine/
|
|
42
|
-
|
|
41
|
+
yarn add @ifto/mantine-kit @mantine/core @mantine/hooks @mantine/notifications @tabler/icons-react clsx dayjs \
|
|
42
|
+
react react-dom
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Install the optional ones only if you use the components that need them:
|
|
46
46
|
|
|
47
47
|
| Package | Needed by |
|
|
48
48
|
|---|---|
|
|
49
|
+
| `@mantine/form` | the form templates (`FormPage`, `CreateFormPage`, `EditFormPage`) and `DataForm`, which take a `useForm` form |
|
|
50
|
+
| `@mantine/modals` | the confirmations of `Table` (delete), `ListPage` and `DataForm` |
|
|
51
|
+
| `@mantine/dates` | `DateField` |
|
|
49
52
|
| `mantine-datatable` | `Table`, `ViewTable`, `ListPage` |
|
|
50
53
|
| `echarts` | `EChart` and the chart types |
|
|
51
54
|
| `exceljs` | Excel export |
|
|
@@ -463,9 +466,10 @@ git tag v0.1.0
|
|
|
463
466
|
git push origin v0.1.0
|
|
464
467
|
```
|
|
465
468
|
|
|
466
|
-
Versions with a suffix (e.g. `v0.2.0-beta.1`) are published with the `next` dist-tag. The workflow
|
|
467
|
-
|
|
468
|
-
the repository
|
|
469
|
+
Versions with a suffix (e.g. `v0.2.0-beta.1`) are published with the `next` dist-tag. The workflow authenticates
|
|
470
|
+
with [npm trusted publishing](https://docs.npmjs.com/trusted-publishers): the package trusts this workflow, which uses a
|
|
471
|
+
short-lived token from GitHub, so no npm token is stored in the repository, and every release has a provenance
|
|
472
|
+
statement.
|
|
469
473
|
|
|
470
474
|
## License
|
|
471
475
|
|
|
@@ -10,6 +10,8 @@ import { jsx } from "react/jsx-runtime";
|
|
|
10
10
|
import { Box } from "@mantine/core";
|
|
11
11
|
// src/component/organism/Table/Table.tsx
|
|
12
12
|
var ROW_HOVER_COLOR = "var(--mantine-primary-color-light)";
|
|
13
|
+
/** Initial records of the table, while they are fetched; a single constant, so it is stable across renders. */
|
|
14
|
+
var NO_RECORDS = [];
|
|
13
15
|
/** Every action shown, used for the settings the caller does not give. */
|
|
14
16
|
var DEFAULT_ACTIONS_SETTINGS = {
|
|
15
17
|
canShowActions: true,
|
|
@@ -65,7 +67,7 @@ var Table = (props) => {
|
|
|
65
67
|
...DEFAULT_ACTIONS_SETTINGS,
|
|
66
68
|
...props.actionsSettings
|
|
67
69
|
};
|
|
68
|
-
const { data: fetchedData, loading, error } = useService(fetchMethod);
|
|
70
|
+
const { data: fetchedData, loading, error } = useService(fetchMethod, NO_RECORDS);
|
|
69
71
|
const [data, setData] = useState([]);
|
|
70
72
|
useEffect(() => setData(fetchedData), [fetchedData]);
|
|
71
73
|
const deleteRecord = useMemo(() => getDeleteRecordFn(setData, service, fetchMethod, messages), [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Table.js","names":[],"sources":["../../../../src/component/organism/Table/Table.tsx"],"sourcesContent":["'use client';\n\nimport { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';\n\nimport { Box } from '@mantine/core';\nimport { DataTableColumn } from 'mantine-datatable';\n\nimport ViewTable from '../ViewTable/ViewTable';\nimport RowActions from './RowActions';\nimport { ListDataSource } from '../../../data/ListDataSource';\nimport { KitMessages } from '../../../i18n/KitMessages';\nimport { Identifiable } from '../../../model/Identifiable';\nimport { useKitConfig } from '../../../provider/useKit';\nimport { FormRouteSegments } from '../../../router/FormRouteSegments';\nimport ErrorAlert from '../../atom/ErrorAlert/ErrorAlert';\nimport useService from '../../../hook/useService';\nimport { showErrorNotification, showSuccessNotification } from '../../../util/Notifications';\n\n/*\nColor of the row under the mouse. The default color of mantine-datatable (gray-1) is barely distinguishable from the\nzebra stripes (gray-0), so the highlight went unnoticed. The light variant of the primary color adapts to the light and\ndark color schemes.\n*/\nconst ROW_HOVER_COLOR = 'var(--mantine-primary-color-light)';\n\n/** Which actions the table shows for each record. */\nexport type ActionsSettings = {\n /** Whether the actions column is shown. */\n canShowActions: boolean;\n\n /** Whether the view action is shown. */\n canView: boolean;\n\n /** Whether the edit action is shown. */\n canEdit: boolean;\n\n /** Whether the delete action is shown. */\n canDelete: boolean;\n};\n\n/** Every action shown, used for the settings the caller does not give. */\nconst DEFAULT_ACTIONS_SETTINGS: ActionsSettings = {\n canShowActions: true,\n canView: true,\n canEdit: true,\n canDelete: true\n};\n\n/** Props of the {@link Table}. */\ntype TableProps<M extends Identifiable, S extends ListDataSource<M>> = {\n /** Source of the records and of their deletion (e.g. a service that accesses an API). */\n service: S;\n\n /**\n * Fetches the records shown, instead of `service.findAll()`. To show data already available, use\n * `() => Promise.resolve(data)`.\n */\n customFetchMethod?: () => Promise<M[]>;\n\n /** Columns of the table. */\n columns: DataTableColumn<M>[];\n\n /** Base path of the pages of the records (e.g. `/region`), used to build the view and edit URLs. */\n basePath: string;\n\n /** Which actions are shown; the ones not given are shown. */\n actionsSettings?: Partial<ActionsSettings>;\n\n /** Whether the table is paginated. */\n pagination?: boolean;\n\n /**\n * Called by the edit action instead of opening the edit page of the record.\n * @param itemId id of the record to edit\n */\n customEditFunction?: (itemId: M['id']) => void;\n\n /**\n * Builds the URL opened to view a record (click on the row or on the view icon).\n * If absent, the default route is used: `${basePath}/<view segment>/${item.id}`.\n */\n getViewUrl?: (item: M) => string;\n};\n\n/**\n * Returns the view URL of a record: the one of `getViewUrl`, if given; otherwise, the default view route.\n * @param props props of the table\n * @param item record\n * @param segments URL segments of the form pages\n */\nfunction viewUrl<M extends Identifiable, S extends ListDataSource<M>>(\n props: TableProps<M, S>,\n item: M,\n segments: FormRouteSegments\n): string {\n return props.getViewUrl ? props.getViewUrl(item) : `${props.basePath}/${segments.view}/${item.id}`;\n}\n\n/**\n * Creates the function that deletes a record and reloads the table.\n * @param setData function that sets the records of the table, obtained from `useState`\n * @param service source of the records, which deletes them\n * @param fetchMethod fetches the records again after the deletion\n * @param messages texts of the locale\n * @return a function that receives the record to delete and returns a promise resolved when it is deleted\n */\nfunction getDeleteRecordFn<M extends Identifiable, S extends ListDataSource<M>>(\n setData: (value: ((prevState: M[]) => M[]) | M[]) => void,\n service: S,\n fetchMethod: () => Promise<M[]>,\n messages: KitMessages\n): (item: M) => Promise<void> {\n return async (item: M) => {\n const id = item?.id ?? 0;\n\n // Without an id, the record was added only to the array and was not sent to the backend: just remove it.\n if (!id) {\n setData(prevData => prevData.filter(current => current !== item));\n return;\n }\n\n try {\n await service.delete(id);\n showSuccessNotification(messages.table.deleteSuccess, messages.common.success);\n setData(await fetchMethod());\n } catch (e) {\n showErrorNotification(e, undefined, messages.common.error);\n }\n };\n}\n\n/**\n * A table that shows the records of a data source, with the view, edit and delete actions. Clicking a row opens the\n * record.\n * @template M type of the records, shown one per row\n * @template S source of the records (e.g. a service that accesses an API)\n */\nconst Table = <M extends Identifiable, S extends ListDataSource<M>>(props: TableProps<M, S>): ReactElement => {\n const { customFetchMethod, service, columns, basePath, customEditFunction } = props;\n const fetchMethod = useCallback(\n () => (customFetchMethod ? customFetchMethod() : service.findAll()),\n [customFetchMethod, service]\n );\n const { router, messages, formRouteSegments } = useKitConfig();\n const navigate = router.useNavigate();\n const settings: ActionsSettings = { ...DEFAULT_ACTIONS_SETTINGS, ...props.actionsSettings };\n\n const { data: fetchedData, loading, error } = useService(fetchMethod);\n const [data, setData] = useState<M[]>([]);\n\n useEffect(() => setData(fetchedData), [fetchedData]);\n\n const deleteRecord = useMemo(\n () => getDeleteRecordFn(setData, service, fetchMethod, messages),\n [service, fetchMethod, messages]\n );\n\n const actionsColumn: DataTableColumn<M> = {\n accessor: 'actions',\n title: <Box mr={6}>{messages.table.actions}</Box>,\n textAlign: 'right',\n render: item => (\n <RowActions\n {...settings}\n viewUrl={viewUrl(props, item, formRouteSegments)}\n editUrl={item.id ? `${basePath}/${formRouteSegments.edit}/${item.id}` : undefined}\n onEdit={customEditFunction && item.id ? () => customEditFunction(item.id) : undefined}\n onDelete={() => deleteRecord(item)}\n />\n )\n };\n const effectiveColumns = settings.canShowActions ? [...columns, actionsColumn] : columns;\n\n if (error) {\n return <ErrorAlert title={messages.common.loadError} message={String(error)} />;\n }\n\n return (\n <ViewTable\n data={data}\n columns={effectiveColumns}\n loading={loading}\n pagination={props.pagination}\n // Highlights the row under the mouse, to show that clicking it opens the record, as the view icon does.\n highlightOnHover\n highlightOnHoverColor={ROW_HOVER_COLOR}\n onRowClick={({ record }) => navigate(viewUrl(props, record, formRouteSegments))}\n />\n );\n};\n\nexport default Table;\n"],"mappings":";;;;;;;;;;;AAuBA,IAAM,kBAAkB;;
|
|
1
|
+
{"version":3,"file":"Table.js","names":[],"sources":["../../../../src/component/organism/Table/Table.tsx"],"sourcesContent":["'use client';\n\nimport { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';\n\nimport { Box } from '@mantine/core';\nimport { DataTableColumn } from 'mantine-datatable';\n\nimport ViewTable from '../ViewTable/ViewTable';\nimport RowActions from './RowActions';\nimport { ListDataSource } from '../../../data/ListDataSource';\nimport { KitMessages } from '../../../i18n/KitMessages';\nimport { Identifiable } from '../../../model/Identifiable';\nimport { useKitConfig } from '../../../provider/useKit';\nimport { FormRouteSegments } from '../../../router/FormRouteSegments';\nimport ErrorAlert from '../../atom/ErrorAlert/ErrorAlert';\nimport useService from '../../../hook/useService';\nimport { showErrorNotification, showSuccessNotification } from '../../../util/Notifications';\n\n/*\nColor of the row under the mouse. The default color of mantine-datatable (gray-1) is barely distinguishable from the\nzebra stripes (gray-0), so the highlight went unnoticed. The light variant of the primary color adapts to the light and\ndark color schemes.\n*/\nconst ROW_HOVER_COLOR = 'var(--mantine-primary-color-light)';\n\n/** Initial records of the table, while they are fetched; a single constant, so it is stable across renders. */\nconst NO_RECORDS: readonly Identifiable[] = [];\n\n/** Which actions the table shows for each record. */\nexport type ActionsSettings = {\n /** Whether the actions column is shown. */\n canShowActions: boolean;\n\n /** Whether the view action is shown. */\n canView: boolean;\n\n /** Whether the edit action is shown. */\n canEdit: boolean;\n\n /** Whether the delete action is shown. */\n canDelete: boolean;\n};\n\n/** Every action shown, used for the settings the caller does not give. */\nconst DEFAULT_ACTIONS_SETTINGS: ActionsSettings = {\n canShowActions: true,\n canView: true,\n canEdit: true,\n canDelete: true\n};\n\n/** Props of the {@link Table}. */\ntype TableProps<M extends Identifiable, S extends ListDataSource<M>> = {\n /** Source of the records and of their deletion (e.g. a service that accesses an API). */\n service: S;\n\n /**\n * Fetches the records shown, instead of `service.findAll()`. To show data already available, use\n * `() => Promise.resolve(data)`.\n */\n customFetchMethod?: () => Promise<M[]>;\n\n /** Columns of the table. */\n columns: DataTableColumn<M>[];\n\n /** Base path of the pages of the records (e.g. `/region`), used to build the view and edit URLs. */\n basePath: string;\n\n /** Which actions are shown; the ones not given are shown. */\n actionsSettings?: Partial<ActionsSettings>;\n\n /** Whether the table is paginated. */\n pagination?: boolean;\n\n /**\n * Called by the edit action instead of opening the edit page of the record.\n * @param itemId id of the record to edit\n */\n customEditFunction?: (itemId: M['id']) => void;\n\n /**\n * Builds the URL opened to view a record (click on the row or on the view icon).\n * If absent, the default route is used: `${basePath}/<view segment>/${item.id}`.\n */\n getViewUrl?: (item: M) => string;\n};\n\n/**\n * Returns the view URL of a record: the one of `getViewUrl`, if given; otherwise, the default view route.\n * @param props props of the table\n * @param item record\n * @param segments URL segments of the form pages\n */\nfunction viewUrl<M extends Identifiable, S extends ListDataSource<M>>(\n props: TableProps<M, S>,\n item: M,\n segments: FormRouteSegments\n): string {\n return props.getViewUrl ? props.getViewUrl(item) : `${props.basePath}/${segments.view}/${item.id}`;\n}\n\n/**\n * Creates the function that deletes a record and reloads the table.\n * @param setData function that sets the records of the table, obtained from `useState`\n * @param service source of the records, which deletes them\n * @param fetchMethod fetches the records again after the deletion\n * @param messages texts of the locale\n * @return a function that receives the record to delete and returns a promise resolved when it is deleted\n */\nfunction getDeleteRecordFn<M extends Identifiable, S extends ListDataSource<M>>(\n setData: (value: ((prevState: M[]) => M[]) | M[]) => void,\n service: S,\n fetchMethod: () => Promise<M[]>,\n messages: KitMessages\n): (item: M) => Promise<void> {\n return async (item: M) => {\n const id = item?.id ?? 0;\n\n // Without an id, the record was added only to the array and was not sent to the backend: just remove it.\n if (!id) {\n setData(prevData => prevData.filter(current => current !== item));\n return;\n }\n\n try {\n await service.delete(id);\n showSuccessNotification(messages.table.deleteSuccess, messages.common.success);\n setData(await fetchMethod());\n } catch (e) {\n showErrorNotification(e, undefined, messages.common.error);\n }\n };\n}\n\n/**\n * A table that shows the records of a data source, with the view, edit and delete actions. Clicking a row opens the\n * record.\n * @template M type of the records, shown one per row\n * @template S source of the records (e.g. a service that accesses an API)\n */\nconst Table = <M extends Identifiable, S extends ListDataSource<M>>(props: TableProps<M, S>): ReactElement => {\n const { customFetchMethod, service, columns, basePath, customEditFunction } = props;\n const fetchMethod = useCallback(\n () => (customFetchMethod ? customFetchMethod() : service.findAll()),\n [customFetchMethod, service]\n );\n const { router, messages, formRouteSegments } = useKitConfig();\n const navigate = router.useNavigate();\n const settings: ActionsSettings = { ...DEFAULT_ACTIONS_SETTINGS, ...props.actionsSettings };\n\n const { data: fetchedData, loading, error } = useService(fetchMethod, NO_RECORDS as M[]);\n const [data, setData] = useState<M[]>([]);\n\n useEffect(() => setData(fetchedData), [fetchedData]);\n\n const deleteRecord = useMemo(\n () => getDeleteRecordFn(setData, service, fetchMethod, messages),\n [service, fetchMethod, messages]\n );\n\n const actionsColumn: DataTableColumn<M> = {\n accessor: 'actions',\n title: <Box mr={6}>{messages.table.actions}</Box>,\n textAlign: 'right',\n render: item => (\n <RowActions\n {...settings}\n viewUrl={viewUrl(props, item, formRouteSegments)}\n editUrl={item.id ? `${basePath}/${formRouteSegments.edit}/${item.id}` : undefined}\n onEdit={customEditFunction && item.id ? () => customEditFunction(item.id) : undefined}\n onDelete={() => deleteRecord(item)}\n />\n )\n };\n const effectiveColumns = settings.canShowActions ? [...columns, actionsColumn] : columns;\n\n if (error) {\n return <ErrorAlert title={messages.common.loadError} message={String(error)} />;\n }\n\n return (\n <ViewTable\n data={data}\n columns={effectiveColumns}\n loading={loading}\n pagination={props.pagination}\n // Highlights the row under the mouse, to show that clicking it opens the record, as the view icon does.\n highlightOnHover\n highlightOnHoverColor={ROW_HOVER_COLOR}\n onRowClick={({ record }) => navigate(viewUrl(props, record, formRouteSegments))}\n />\n );\n};\n\nexport default Table;\n"],"mappings":";;;;;;;;;;;AAuBA,IAAM,kBAAkB;;AAGxB,IAAM,aAAsC,CAAC;;AAkB7C,IAAM,2BAA4C;CAC9C,gBAAgB;CAChB,SAAS;CACT,SAAS;CACT,WAAW;AACf;;;;;;;AA4CA,SAAS,QACL,OACA,MACA,UACM;CACN,OAAO,MAAM,aAAa,MAAM,WAAW,IAAI,IAAI,GAAG,MAAM,SAAS,GAAG,SAAS,KAAK,GAAG,KAAK;AAClG;;;;;;;;;AAUA,SAAS,kBACL,SACA,SACA,aACA,UAC0B;CAC1B,OAAO,OAAO,SAAY;EACtB,MAAM,KAAK,MAAM,MAAM;EAGvB,IAAI,CAAC,IAAI;GACL,SAAQ,aAAY,SAAS,QAAO,YAAW,YAAY,IAAI,CAAC;GAChE;EACJ;EAEA,IAAI;GACA,MAAM,QAAQ,OAAO,EAAE;GACvB,wBAAwB,SAAS,MAAM,eAAe,SAAS,OAAO,OAAO;GAC7E,QAAQ,MAAM,YAAY,CAAC;EAC/B,SAAS,GAAG;GACR,sBAAsB,GAAG,KAAA,GAAW,SAAS,OAAO,KAAK;EAC7D;CACJ;AACJ;;;;;;;AAQA,IAAM,SAA8D,UAA0C;CAC1G,MAAM,EAAE,mBAAmB,SAAS,SAAS,UAAU,uBAAuB;CAC9E,MAAM,cAAc,kBACT,oBAAoB,kBAAkB,IAAI,QAAQ,QAAQ,GACjE,CAAC,mBAAmB,OAAO,CAC/B;CACA,MAAM,EAAE,QAAQ,UAAU,sBAAsB,aAAa;CAC7D,MAAM,WAAW,OAAO,YAAY;CACpC,MAAM,WAA4B;EAAE,GAAG;EAA0B,GAAG,MAAM;CAAgB;CAE1F,MAAM,EAAE,MAAM,aAAa,SAAS,UAAU,WAAW,aAAa,UAAiB;CACvF,MAAM,CAAC,MAAM,WAAW,SAAc,CAAC,CAAC;CAExC,gBAAgB,QAAQ,WAAW,GAAG,CAAC,WAAW,CAAC;CAEnD,MAAM,eAAe,cACX,kBAAkB,SAAS,SAAS,aAAa,QAAQ,GAC/D;EAAC;EAAS;EAAa;CAAQ,CACnC;CAEA,MAAM,gBAAoC;EACtC,UAAU;EACV,OAAO,oBAAC,KAAD;GAAK,IAAI;GAAI,UAAA,SAAS,MAAM;EAAa,CAAA;EAChD,WAAW;EACX,SAAQ,SACJ,oBAAC,YAAD;GACI,GAAI;GACJ,SAAS,QAAQ,OAAO,MAAM,iBAAiB;GAC/C,SAAS,KAAK,KAAK,GAAG,SAAS,GAAG,kBAAkB,KAAK,GAAG,KAAK,OAAO,KAAA;GACxE,QAAQ,sBAAsB,KAAK,WAAW,mBAAmB,KAAK,EAAE,IAAI,KAAA;GAC5E,gBAAgB,aAAa,IAAI;EACpC,CAAA;CAET;CACA,MAAM,mBAAmB,SAAS,iBAAiB,CAAC,GAAG,SAAS,aAAa,IAAI;CAEjF,IAAI,OACA,OAAO,oBAAC,YAAD;EAAY,OAAO,SAAS,OAAO;EAAW,SAAS,OAAO,KAAK;CAAI,CAAA;CAGlF,OACI,oBAAC,WAAD;EACU;EACN,SAAS;EACA;EACT,YAAY,MAAM;EAElB,kBAAA;EACA,uBAAuB;EACvB,aAAa,EAAE,aAAa,SAAS,QAAQ,OAAO,QAAQ,iBAAiB,CAAC;CACjF,CAAA;AAET"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useKitConfig } from "../../../provider/useKit.js";
|
|
3
3
|
import { columnComparator } from "./ColumnComparator.js";
|
|
4
|
+
import { useRowKey } from "./useRowKey.js";
|
|
4
5
|
import { useEffect, useMemo, useState } from "react";
|
|
5
6
|
import { jsx } from "react/jsx-runtime";
|
|
6
7
|
import { DataTable } from "mantine-datatable";
|
|
@@ -26,17 +27,6 @@ var NO_COLUMN_SORT_STATUS = {
|
|
|
26
27
|
direction: "asc"
|
|
27
28
|
};
|
|
28
29
|
/**
|
|
29
|
-
* Adds a unique key to each record shown in the table.
|
|
30
|
-
* The array is changed in place, instead of copying each object with destructuring: a copy is a plain object, which
|
|
31
|
-
* loses the accessors (get and set) of class instances, so reading an attribute through them would return nothing.
|
|
32
|
-
* @param data records shown in the table
|
|
33
|
-
* @return the same array, now with a `key` in each object
|
|
34
|
-
*/
|
|
35
|
-
function addKeyToDataArray(data) {
|
|
36
|
-
data.forEach((element, index) => Object.assign(element, { key: index.toString() }));
|
|
37
|
-
return data;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
30
|
* The ViewTable component is a wrapper for the Mantine DataTable component.
|
|
41
31
|
* @template M - Model that extends from {@link Identifiable}
|
|
42
32
|
* @param data the data to be displayed in this ViewTable
|
|
@@ -64,7 +54,7 @@ var ViewTable = ({ data, columns, loading, pagination = true, showIdColumn = tru
|
|
|
64
54
|
compare,
|
|
65
55
|
locale
|
|
66
56
|
]);
|
|
67
|
-
const
|
|
57
|
+
const records = useMemo(() => {
|
|
68
58
|
if (!pagination) return sortedData;
|
|
69
59
|
const from = (page - 1) * pageSize;
|
|
70
60
|
return sortedData.slice(from, from + pageSize);
|
|
@@ -73,7 +63,8 @@ var ViewTable = ({ data, columns, loading, pagination = true, showIdColumn = tru
|
|
|
73
63
|
pagination,
|
|
74
64
|
page,
|
|
75
65
|
pageSize
|
|
76
|
-
])
|
|
66
|
+
]);
|
|
67
|
+
const rowKey = useRowKey();
|
|
77
68
|
function changeSortStatus(status) {
|
|
78
69
|
setCompareActive(false);
|
|
79
70
|
setSortStatus(status);
|
|
@@ -108,7 +99,8 @@ var ViewTable = ({ data, columns, loading, pagination = true, showIdColumn = tru
|
|
|
108
99
|
verticalSpacing: "sm",
|
|
109
100
|
striped: true,
|
|
110
101
|
columns: effectiveColumns,
|
|
111
|
-
records
|
|
102
|
+
records,
|
|
103
|
+
idAccessor: rowKey,
|
|
112
104
|
noRecordsText: messages.table.noRecords,
|
|
113
105
|
fetching: loading,
|
|
114
106
|
sortStatus,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ViewTable.js","names":[],"sources":["../../../../src/component/organism/ViewTable/ViewTable.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\n\nimport {\n DataTable,\n DataTableColumn,\n DataTablePaginationProps,\n DataTableProps,\n DataTableSortStatus\n} from 'mantine-datatable';\n\nimport { Identifiable } from '../../../model/Identifiable';\nimport { useKitConfig } from '../../../provider/useKit';\nimport { columnComparator, Comparator } from './ColumnComparator';\n\n/** Options of records per page. The largest one lets long tables (and end-to-end tests) show every row in one page. */\nconst PAGE_SIZES = [10, 20, 50, 200];\n\n/** Default sort when neither `defaultSortStatus` nor `compare` is given. */\nconst DEFAULT_SORT_STATUS = { columnAccessor: 'id', direction: 'asc' } as const;\n\n/**\n * Sort used while the `compare` function applies, when `defaultSortStatus` is not given: it matches no column, so no\n * header shows the sort indicator, and the first click on any column sorts it in ascending order.\n */\nconst NO_COLUMN_SORT_STATUS = { columnAccessor: '', direction: 'asc' } as const;\n\n/** Function that defines extra attributes (e.g. aria-label) for each pagination control button. */\ntype PaginationControlPropsGetter = Extract<DataTablePaginationProps, { page: number }>['getPaginationControlProps'];\n\n/**\n * Props of the {@link DataTable} passed straight through by the ViewTable.\n * Only the props actually used by the callers are accepted: using `Partial<DataTableProps<M>>` makes TypeScript combine\n * the variants of the `DataTableProps` union with the ones of `paginationProps`, which exhausts the memory of `tsc`.\n */\ntype DataTablePassThroughProps<M extends Identifiable> = Pick<\n DataTableProps<M>,\n | 'onRowClick'\n | 'rowStyle'\n | 'noRecordsText'\n | 'highlightOnHover'\n | 'highlightOnHoverColor'\n | 'className'\n | 'minHeight'\n | 'withColumnBorders'\n | 'verticalSpacing'\n | 'horizontalSpacing'\n>;\n\n/**\n * TableProps is a generic type for the ViewTable component props.\n */\ntype TableProps<M extends Identifiable> = {\n data: M[];\n columns: DataTableColumn<M>[];\n loading?: boolean;\n pagination?: boolean;\n /** Shows the ID column, automatically added to the given columns. */\n showIdColumn?: boolean;\n /**\n * Initial sort of the table. Default: by `id`, ascending.\n * Give another value when the natural order of the data should not be by `id` (e.g. when the backend already\n * returns the data in an order to keep). Along with `compare`, it only tells which header shows the sort indicator\n * in the initial order.\n */\n defaultSortStatus?: DataTableSortStatus<M>;\n /**\n * Comparison function that defines the initial order, instead of the column of `defaultSortStatus`. It allows\n * sorting by more than one criterion (e.g. city and, in the same city, year descending). After the user clicks the\n * header of a column, the sort of the column applies.\n */\n compare?: Comparator<M>;\n /** Extra attributes for the pagination control buttons. It only has effect when `pagination` is true. */\n getPaginationControlProps?: PaginationControlPropsGetter;\n} & DataTablePassThroughProps<M>;\n\n/**\n * Adds a unique key to each record shown in the table.\n * The array is changed in place, instead of copying each object with destructuring: a copy is a plain object, which\n * loses the accessors (get and set) of class instances, so reading an attribute through them would return nothing.\n * @param data records shown in the table\n * @return the same array, now with a `key` in each object\n */\nfunction addKeyToDataArray<M extends Identifiable>(data: M[]): M[] {\n data.forEach((element, index) => Object.assign(element, { key: index.toString() }));\n return data;\n}\n\n/**\n * The ViewTable component is a wrapper for the Mantine DataTable component.\n * @template M - Model that extends from {@link Identifiable}\n * @param data the data to be displayed in this ViewTable\n * @param {DataTableColumn<M>[]} columns - The columns to be displayed in this ViewTable\n * @param loading Indicates if the data to be displayed is being loaded yet or not\n * @param pagination Indicates whether to show pagination or not.\n * @param getPaginationControlProps extra attributes for the pagination control buttons\n * @param compare comparison function of the initial order, replaced by the sort of the column chosen in the header\n * @param rest remaining props, passed to the DataTable\n * @return the table with the data, sortable and (optionally) paginated\n */\nconst ViewTable = <M extends Identifiable>({\n data,\n columns,\n loading,\n pagination = true,\n showIdColumn = true,\n defaultSortStatus,\n compare,\n getPaginationControlProps,\n ...rest\n}: TableProps<M>) => {\n const { locale, messages } = useKitConfig();\n const [page, setPage] = useState(1);\n const [pageSize, setPageSize] = useState(PAGE_SIZES[0]);\n const [sortStatus, setSortStatus] = useState<DataTableSortStatus<M>>(\n defaultSortStatus ?? (compare ? NO_COLUMN_SORT_STATUS : DEFAULT_SORT_STATUS)\n );\n // The `compare` function applies until the user chooses a column in the header.\n const [compareActive, setCompareActive] = useState(compare !== undefined);\n\n /*\n * Sorts all records before cutting the page, so each page (including in descending order) shows its part of the\n * whole set. The copy keeps the order of the given array. It is separate from the page cut, so not everything is\n * sorted again on every change of page or page size.\n */\n const sortedData = useMemo(() => {\n const comparator = compareActive && compare ? compare : columnComparator(sortStatus, locale);\n\n return [...data].sort(comparator);\n }, [data, sortStatus, compareActive, compare, locale]);\n\n // Without pagination, shows all records (e.g. a printed report), not only the ones of the first page.\n const records = useMemo(() => {\n if (!pagination) return sortedData;\n\n const from = (page - 1) * pageSize;\n return sortedData.slice(from, from + pageSize);\n }, [sortedData, pagination, page, pageSize]);\n const recordsWithKeys = addKeyToDataArray(records);\n\n function changeSortStatus(status: DataTableSortStatus<M>): void {\n setCompareActive(false);\n setSortStatus(status);\n }\n\n const effectiveColumns: DataTableColumn<M>[] = [\n {\n title: 'ID',\n accessor: 'id',\n hidden: !showIdColumn,\n sortable: true,\n render: item => <span>{item.id}</span>\n },\n ...columns\n ];\n\n useEffect(() => {\n setPage(1);\n }, [pageSize]);\n\n /* If the data shrinks, goes back to the last existing page. The functional update avoids using an outdated `page`\n when the effect above has just gone back to page 1. */\n useEffect(() => {\n const totalPages = Math.ceil(data.length / pageSize);\n setPage(currentPage => (totalPages && currentPage > totalPages ? totalPages : currentPage));\n }, [data.length, pageSize]);\n\n /* Typed only with the pagination props (and not as Partial<DataTableProps>) to not exhaust the memory of tsc.\n See the comment of DataTablePassThroughProps. */\n const paginationProps: DataTablePaginationProps = pagination\n ? {\n page,\n onPageChange: p => setPage(p),\n totalRecords: data.length,\n recordsPerPage: pageSize,\n recordsPerPageOptions: PAGE_SIZES,\n onRecordsPerPageChange: setPageSize,\n recordsPerPageLabel: messages.table.recordsPerPage,\n paginationText: messages.table.pagination,\n getPaginationControlProps\n }\n : {};\n\n // This DataTable is the one of https://www.npmjs.com/package/mantine-datatable (there are other libraries with this name).\n return (\n <DataTable\n minHeight={200}\n verticalSpacing=\"sm\"\n striped\n columns={effectiveColumns}\n records={recordsWithKeys}\n noRecordsText={messages.table.noRecords}\n fetching={loading}\n sortStatus={sortStatus}\n onSortStatusChange={changeSortStatus}\n customRowAttributes={record => ({ 'data-testid': `row-${record.id}` })}\n {...paginationProps}\n {...rest}\n />\n );\n};\n\nexport default ViewTable;\n"],"mappings":";;;;;;;;AAiBA,IAAM,aAAa;CAAC;CAAI;CAAI;CAAI;AAAG;;AAGnC,IAAM,sBAAsB;CAAE,gBAAgB;CAAM,WAAW;AAAM;;;;;AAMrE,IAAM,wBAAwB;CAAE,gBAAgB;CAAI,WAAW;AAAM;;;;;;;;AA0DrE,SAAS,kBAA0C,MAAgB;CAC/D,KAAK,SAAS,SAAS,UAAU,OAAO,OAAO,SAAS,EAAE,KAAK,MAAM,SAAS,EAAE,CAAC,CAAC;CAClF,OAAO;AACX;;;;;;;;;;;;;AAcA,IAAM,aAAqC,EACvC,MACA,SACA,SACA,aAAa,MACb,eAAe,MACf,mBACA,SACA,2BACA,GAAG,WACc;CACjB,MAAM,EAAE,QAAQ,aAAa,aAAa;CAC1C,MAAM,CAAC,MAAM,WAAW,SAAS,CAAC;CAClC,MAAM,CAAC,UAAU,eAAe,SAAS,WAAW,EAAE;CACtD,MAAM,CAAC,YAAY,iBAAiB,SAChC,sBAAsB,UAAU,wBAAwB,oBAC5D;CAEA,MAAM,CAAC,eAAe,oBAAoB,SAAS,YAAY,KAAA,CAAS;CAOxE,MAAM,aAAa,cAAc;EAC7B,MAAM,aAAa,iBAAiB,UAAU,UAAU,iBAAiB,YAAY,MAAM;EAE3F,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,UAAU;CACpC,GAAG;EAAC;EAAM;EAAY;EAAe;EAAS;CAAM,CAAC;CASrD,MAAM,kBAAkB,kBANR,cAAc;EAC1B,IAAI,CAAC,YAAY,OAAO;EAExB,MAAM,QAAQ,OAAO,KAAK;EAC1B,OAAO,WAAW,MAAM,MAAM,OAAO,QAAQ;CACjD,GAAG;EAAC;EAAY;EAAY;EAAM;CAAQ,CACA,CAAO;CAEjD,SAAS,iBAAiB,QAAsC;EAC5D,iBAAiB,KAAK;EACtB,cAAc,MAAM;CACxB;CAEA,MAAM,mBAAyC,CAC3C;EACI,OAAO;EACP,UAAU;EACV,QAAQ,CAAC;EACT,UAAU;EACV,SAAQ,SAAQ,oBAAC,QAAD,EAAA,UAAO,KAAK,GAAS,CAAA;CACzC,GACA,GAAG,OACP;CAEA,gBAAgB;EACZ,QAAQ,CAAC;CACb,GAAG,CAAC,QAAQ,CAAC;CAIb,gBAAgB;EACZ,MAAM,aAAa,KAAK,KAAK,KAAK,SAAS,QAAQ;EACnD,SAAQ,gBAAgB,cAAc,cAAc,aAAa,aAAa,WAAY;CAC9F,GAAG,CAAC,KAAK,QAAQ,QAAQ,CAAC;CAI1B,MAAM,kBAA4C,aAC5C;EACI;EACA,eAAc,MAAK,QAAQ,CAAC;EAC5B,cAAc,KAAK;EACnB,gBAAgB;EAChB,uBAAuB;EACvB,wBAAwB;EACxB,qBAAqB,SAAS,MAAM;EACpC,gBAAgB,SAAS,MAAM;EAC/B;CACJ,IACA,CAAC;CAGP,OACI,oBAAC,WAAD;EACI,WAAW;EACX,iBAAgB;EAChB,SAAA;EACA,SAAS;EACT,SAAS;EACT,eAAe,SAAS,MAAM;EAC9B,UAAU;EACE;EACZ,oBAAoB;EACpB,sBAAqB,YAAW,EAAE,eAAe,OAAO,OAAO,KAAK;EACpE,GAAI;EACJ,GAAI;CACP,CAAA;AAET"}
|
|
1
|
+
{"version":3,"file":"ViewTable.js","names":[],"sources":["../../../../src/component/organism/ViewTable/ViewTable.tsx"],"sourcesContent":["'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\n\nimport {\n DataTable,\n DataTableColumn,\n DataTablePaginationProps,\n DataTableProps,\n DataTableSortStatus\n} from 'mantine-datatable';\n\nimport { Identifiable } from '../../../model/Identifiable';\nimport { useKitConfig } from '../../../provider/useKit';\nimport { columnComparator, Comparator } from './ColumnComparator';\nimport { useRowKey } from './useRowKey';\n\n/** Options of records per page. The largest one lets long tables (and end-to-end tests) show every row in one page. */\nconst PAGE_SIZES = [10, 20, 50, 200];\n\n/** Default sort when neither `defaultSortStatus` nor `compare` is given. */\nconst DEFAULT_SORT_STATUS = { columnAccessor: 'id', direction: 'asc' } as const;\n\n/**\n * Sort used while the `compare` function applies, when `defaultSortStatus` is not given: it matches no column, so no\n * header shows the sort indicator, and the first click on any column sorts it in ascending order.\n */\nconst NO_COLUMN_SORT_STATUS = { columnAccessor: '', direction: 'asc' } as const;\n\n/** Function that defines extra attributes (e.g. aria-label) for each pagination control button. */\ntype PaginationControlPropsGetter = Extract<DataTablePaginationProps, { page: number }>['getPaginationControlProps'];\n\n/**\n * Props of the {@link DataTable} passed straight through by the ViewTable.\n * Only the props actually used by the callers are accepted: using `Partial<DataTableProps<M>>` makes TypeScript combine\n * the variants of the `DataTableProps` union with the ones of `paginationProps`, which exhausts the memory of `tsc`.\n */\ntype DataTablePassThroughProps<M extends Identifiable> = Pick<\n DataTableProps<M>,\n | 'onRowClick'\n | 'rowStyle'\n | 'noRecordsText'\n | 'highlightOnHover'\n | 'highlightOnHoverColor'\n | 'className'\n | 'minHeight'\n | 'withColumnBorders'\n | 'verticalSpacing'\n | 'horizontalSpacing'\n>;\n\n/**\n * TableProps is a generic type for the ViewTable component props.\n */\ntype TableProps<M extends Identifiable> = {\n data: M[];\n columns: DataTableColumn<M>[];\n loading?: boolean;\n pagination?: boolean;\n /** Shows the ID column, automatically added to the given columns. */\n showIdColumn?: boolean;\n /**\n * Initial sort of the table. Default: by `id`, ascending.\n * Give another value when the natural order of the data should not be by `id` (e.g. when the backend already\n * returns the data in an order to keep). Along with `compare`, it only tells which header shows the sort indicator\n * in the initial order.\n */\n defaultSortStatus?: DataTableSortStatus<M>;\n /**\n * Comparison function that defines the initial order, instead of the column of `defaultSortStatus`. It allows\n * sorting by more than one criterion (e.g. city and, in the same city, year descending). After the user clicks the\n * header of a column, the sort of the column applies.\n */\n compare?: Comparator<M>;\n /** Extra attributes for the pagination control buttons. It only has effect when `pagination` is true. */\n getPaginationControlProps?: PaginationControlPropsGetter;\n} & DataTablePassThroughProps<M>;\n\n/**\n * The ViewTable component is a wrapper for the Mantine DataTable component.\n * @template M - Model that extends from {@link Identifiable}\n * @param data the data to be displayed in this ViewTable\n * @param {DataTableColumn<M>[]} columns - The columns to be displayed in this ViewTable\n * @param loading Indicates if the data to be displayed is being loaded yet or not\n * @param pagination Indicates whether to show pagination or not.\n * @param getPaginationControlProps extra attributes for the pagination control buttons\n * @param compare comparison function of the initial order, replaced by the sort of the column chosen in the header\n * @param rest remaining props, passed to the DataTable\n * @return the table with the data, sortable and (optionally) paginated\n */\nconst ViewTable = <M extends Identifiable>({\n data,\n columns,\n loading,\n pagination = true,\n showIdColumn = true,\n defaultSortStatus,\n compare,\n getPaginationControlProps,\n ...rest\n}: TableProps<M>) => {\n const { locale, messages } = useKitConfig();\n const [page, setPage] = useState(1);\n const [pageSize, setPageSize] = useState(PAGE_SIZES[0]);\n const [sortStatus, setSortStatus] = useState<DataTableSortStatus<M>>(\n defaultSortStatus ?? (compare ? NO_COLUMN_SORT_STATUS : DEFAULT_SORT_STATUS)\n );\n // The `compare` function applies until the user chooses a column in the header.\n const [compareActive, setCompareActive] = useState(compare !== undefined);\n\n /*\n * Sorts all records before cutting the page, so each page (including in descending order) shows its part of the\n * whole set. The copy keeps the order of the given array. It is separate from the page cut, so not everything is\n * sorted again on every change of page or page size.\n */\n const sortedData = useMemo(() => {\n const comparator = compareActive && compare ? compare : columnComparator(sortStatus, locale);\n\n return [...data].sort(comparator);\n }, [data, sortStatus, compareActive, compare, locale]);\n\n // Without pagination, shows all records (e.g. a printed report), not only the ones of the first page.\n const records = useMemo(() => {\n if (!pagination) return sortedData;\n\n const from = (page - 1) * pageSize;\n return sortedData.slice(from, from + pageSize);\n }, [sortedData, pagination, page, pageSize]);\n const rowKey = useRowKey<M>();\n\n function changeSortStatus(status: DataTableSortStatus<M>): void {\n setCompareActive(false);\n setSortStatus(status);\n }\n\n const effectiveColumns: DataTableColumn<M>[] = [\n {\n title: 'ID',\n accessor: 'id',\n hidden: !showIdColumn,\n sortable: true,\n render: item => <span>{item.id}</span>\n },\n ...columns\n ];\n\n useEffect(() => {\n setPage(1);\n }, [pageSize]);\n\n /* If the data shrinks, goes back to the last existing page. The functional update avoids using an outdated `page`\n when the effect above has just gone back to page 1. */\n useEffect(() => {\n const totalPages = Math.ceil(data.length / pageSize);\n setPage(currentPage => (totalPages && currentPage > totalPages ? totalPages : currentPage));\n }, [data.length, pageSize]);\n\n /* Typed only with the pagination props (and not as Partial<DataTableProps>) to not exhaust the memory of tsc.\n See the comment of DataTablePassThroughProps. */\n const paginationProps: DataTablePaginationProps = pagination\n ? {\n page,\n onPageChange: p => setPage(p),\n totalRecords: data.length,\n recordsPerPage: pageSize,\n recordsPerPageOptions: PAGE_SIZES,\n onRecordsPerPageChange: setPageSize,\n recordsPerPageLabel: messages.table.recordsPerPage,\n paginationText: messages.table.pagination,\n getPaginationControlProps\n }\n : {};\n\n // This DataTable is the one of https://www.npmjs.com/package/mantine-datatable (there are other libraries with this name).\n return (\n <DataTable\n minHeight={200}\n verticalSpacing=\"sm\"\n striped\n columns={effectiveColumns}\n records={records}\n idAccessor={rowKey}\n noRecordsText={messages.table.noRecords}\n fetching={loading}\n sortStatus={sortStatus}\n onSortStatusChange={changeSortStatus}\n customRowAttributes={record => ({ 'data-testid': `row-${record.id}` })}\n {...paginationProps}\n {...rest}\n />\n );\n};\n\nexport default ViewTable;\n"],"mappings":";;;;;;;;;AAkBA,IAAM,aAAa;CAAC;CAAI;CAAI;CAAI;AAAG;;AAGnC,IAAM,sBAAsB;CAAE,gBAAgB;CAAM,WAAW;AAAM;;;;;AAMrE,IAAM,wBAAwB;CAAE,gBAAgB;CAAI,WAAW;AAAM;;;;;;;;;;;;;AA+DrE,IAAM,aAAqC,EACvC,MACA,SACA,SACA,aAAa,MACb,eAAe,MACf,mBACA,SACA,2BACA,GAAG,WACc;CACjB,MAAM,EAAE,QAAQ,aAAa,aAAa;CAC1C,MAAM,CAAC,MAAM,WAAW,SAAS,CAAC;CAClC,MAAM,CAAC,UAAU,eAAe,SAAS,WAAW,EAAE;CACtD,MAAM,CAAC,YAAY,iBAAiB,SAChC,sBAAsB,UAAU,wBAAwB,oBAC5D;CAEA,MAAM,CAAC,eAAe,oBAAoB,SAAS,YAAY,KAAA,CAAS;CAOxE,MAAM,aAAa,cAAc;EAC7B,MAAM,aAAa,iBAAiB,UAAU,UAAU,iBAAiB,YAAY,MAAM;EAE3F,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,UAAU;CACpC,GAAG;EAAC;EAAM;EAAY;EAAe;EAAS;CAAM,CAAC;CAGrD,MAAM,UAAU,cAAc;EAC1B,IAAI,CAAC,YAAY,OAAO;EAExB,MAAM,QAAQ,OAAO,KAAK;EAC1B,OAAO,WAAW,MAAM,MAAM,OAAO,QAAQ;CACjD,GAAG;EAAC;EAAY;EAAY;EAAM;CAAQ,CAAC;CAC3C,MAAM,SAAS,UAAa;CAE5B,SAAS,iBAAiB,QAAsC;EAC5D,iBAAiB,KAAK;EACtB,cAAc,MAAM;CACxB;CAEA,MAAM,mBAAyC,CAC3C;EACI,OAAO;EACP,UAAU;EACV,QAAQ,CAAC;EACT,UAAU;EACV,SAAQ,SAAQ,oBAAC,QAAD,EAAA,UAAO,KAAK,GAAS,CAAA;CACzC,GACA,GAAG,OACP;CAEA,gBAAgB;EACZ,QAAQ,CAAC;CACb,GAAG,CAAC,QAAQ,CAAC;CAIb,gBAAgB;EACZ,MAAM,aAAa,KAAK,KAAK,KAAK,SAAS,QAAQ;EACnD,SAAQ,gBAAgB,cAAc,cAAc,aAAa,aAAa,WAAY;CAC9F,GAAG,CAAC,KAAK,QAAQ,QAAQ,CAAC;CAI1B,MAAM,kBAA4C,aAC5C;EACI;EACA,eAAc,MAAK,QAAQ,CAAC;EAC5B,cAAc,KAAK;EACnB,gBAAgB;EAChB,uBAAuB;EACvB,wBAAwB;EACxB,qBAAqB,SAAS,MAAM;EACpC,gBAAgB,SAAS,MAAM;EAC/B;CACJ,IACA,CAAC;CAGP,OACI,oBAAC,WAAD;EACI,WAAW;EACX,iBAAgB;EAChB,SAAA;EACA,SAAS;EACA;EACT,YAAY;EACZ,eAAe,SAAS,MAAM;EAC9B,UAAU;EACE;EACZ,oBAAoB;EACpB,sBAAqB,YAAW,EAAE,eAAe,OAAO,OAAO,KAAK;EACpE,GAAI;EACJ,GAAI;CACP,CAAA;AAET"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Key } from 'react';
|
|
2
|
+
import { Identifiable } from '../../../model/Identifiable';
|
|
3
|
+
/**
|
|
4
|
+
* Returns the function that gives each record of a table a unique and stable key, without changing the record: its id
|
|
5
|
+
* or, for a record not saved yet (without id), a key kept per object while the table is mounted.
|
|
6
|
+
* @return a function that returns the key of a record, to use as the `idAccessor` of mantine-datatable
|
|
7
|
+
*/
|
|
8
|
+
export declare function useRowKey<M extends Identifiable>(): (record: M) => Key;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useRef } from "react";
|
|
3
|
+
// src/component/organism/ViewTable/useRowKey.ts
|
|
4
|
+
/**
|
|
5
|
+
* Returns the function that gives each record of a table a unique and stable key, without changing the record: its id
|
|
6
|
+
* or, for a record not saved yet (without id), a key kept per object while the table is mounted.
|
|
7
|
+
* @return a function that returns the key of a record, to use as the `idAccessor` of mantine-datatable
|
|
8
|
+
*/
|
|
9
|
+
function useRowKey() {
|
|
10
|
+
const generatedKeys = useRef(/* @__PURE__ */ new WeakMap());
|
|
11
|
+
const nextKey = useRef(0);
|
|
12
|
+
return useCallback((record) => {
|
|
13
|
+
if (record.id !== null && record.id !== void 0 && record.id !== "") return record.id;
|
|
14
|
+
let key = generatedKeys.current.get(record);
|
|
15
|
+
if (key === void 0) {
|
|
16
|
+
key = `unsaved-${nextKey.current++}`;
|
|
17
|
+
generatedKeys.current.set(record, key);
|
|
18
|
+
}
|
|
19
|
+
return key;
|
|
20
|
+
}, []);
|
|
21
|
+
}
|
|
22
|
+
export { useRowKey };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=useRowKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRowKey.js","names":[],"sources":["../../../../src/component/organism/ViewTable/useRowKey.ts"],"sourcesContent":["import { Key, useCallback, useRef } from 'react';\n\nimport { Identifiable } from '../../../model/Identifiable';\n\n/**\n * Returns the function that gives each record of a table a unique and stable key, without changing the record: its id\n * or, for a record not saved yet (without id), a key kept per object while the table is mounted.\n * @return a function that returns the key of a record, to use as the `idAccessor` of mantine-datatable\n */\nexport function useRowKey<M extends Identifiable>(): (record: M) => Key {\n // Keys of the records without id, by object: they disappear with the object, without keeping it in memory.\n const generatedKeys = useRef(new WeakMap<M, string>());\n const nextKey = useRef(0);\n\n return useCallback((record: M) => {\n if (record.id !== null && record.id !== undefined && record.id !== '') return record.id;\n\n let key = generatedKeys.current.get(record);\n if (key === undefined) {\n key = `unsaved-${nextKey.current++}`;\n generatedKeys.current.set(record, key);\n }\n\n return key;\n }, []);\n}\n"],"mappings":";;;;;;;;AASA,SAAgB,YAAwD;CAEpE,MAAM,gBAAgB,uBAAO,IAAI,QAAmB,CAAC;CACrD,MAAM,UAAU,OAAO,CAAC;CAExB,OAAO,aAAa,WAAc;EAC9B,IAAI,OAAO,OAAO,QAAQ,OAAO,OAAO,KAAA,KAAa,OAAO,OAAO,IAAI,OAAO,OAAO;EAErF,IAAI,MAAM,cAAc,QAAQ,IAAI,MAAM;EAC1C,IAAI,QAAQ,KAAA,GAAW;GACnB,MAAM,WAAW,QAAQ;GACzB,cAAc,QAAQ,IAAI,QAAQ,GAAG;EACzC;EAEA,OAAO;CACX,GAAG,CAAC,CAAC;AACT"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { ReactElement } from 'react';
|
|
2
|
-
import { UseFormReturnType } from '@mantine/form';
|
|
2
|
+
import type { UseFormReturnType } from '@mantine/form';
|
|
3
3
|
import { SaveDataSource } from '../../../data/SaveDataSource';
|
|
4
4
|
import { Identifiable } from '../../../model/Identifiable';
|
|
5
5
|
import { FormType } from './FormType';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormPage.js","names":["_Fragment"],"sources":["../../../../src/component/template/FormPage/FormPage.tsx"],"sourcesContent":["import React, { ReactElement, useState } from 'react';\n\nimport { Alert, Box, Button, Grid, Group, LoadingOverlay, Stack, Text } from '@mantine/core';\nimport { UseFormReturnType } from '@mantine/form';\nimport { IconArrowBack, IconDeviceFloppy, IconInfoCircle } from '@tabler/icons-react';\n\nimport { SaveDataSource } from '../../../data/SaveDataSource';\nimport { FormAction } from '../../../i18n/KitMessages';\nimport { Identifiable } from '../../../model/Identifiable';\nimport { useDocumentTitle, useKitConfig } from '../../../provider/useKit';\nimport { formatValues } from '../../../util/TransformData';\nimport { showErrorNotification, showSuccessNotification } from '../../../util/Notifications';\nimport PaperCard from '../../molecule/PaperCard/PaperCard';\nimport PageHeader from '../../organism/PageHeader/PageHeader';\nimport { FormType } from './FormType';\n\ninterface FormPageProps<T extends Identifiable, D extends object, S extends SaveDataSource<T, D>> {\n /** Destination of the data (e.g. a service that accesses an API). */\n service: S;\n\n /** Name of the record (e.g. \"Region\"), shown after the action verb in the title. */\n title: string;\n formType: FormType;\n form: UseFormReturnType<D & Identifiable>;\n\n /** Page opened after saving and by the back button; absent for the first segment of the current path. */\n backRoute?: string;\n\n /** Shows a loading overlay over the form (e.g. while the record is fetched). */\n isLoading?: boolean;\n\n /** Whether the form takes the whole width, instead of half of it on large screens. */\n isFullWidth?: boolean;\n}\n\n/** Action of each form type, the key of its verb in the texts. */\nconst FORM_ACTIONS: Record<FormType, FormAction> = {\n [FormType.Create]: 'create',\n [FormType.Edit]: 'edit',\n [FormType.View]: 'view'\n};\n\n/**\n * CRUD form page to create, edit or view a record (according to `formType`), which saves the data in `service` and\n * goes back to the list page.\n */\nfunction FormPage<T extends Identifiable, D extends object, S extends SaveDataSource<T, D>>(\n props: React.PropsWithChildren<FormPageProps<T, D, S>>\n): ReactElement {\n const { title, formType, service, form, backRoute, isLoading, isFullWidth, children } = props;\n const { router, messages } = useKitConfig();\n const { Link } = router;\n const navigate = router.useNavigate();\n const pathname = router.usePathname();\n\n /**\n * Checks whether a field of the validation schema is missing in the form.\n * @param fieldName name of the field to find\n */\n function isFormFieldMissing(fieldName: string): boolean {\n return !form.getInputNode(fieldName);\n }\n\n /**\n * Shows the errors of the fields that are in the validation schema given to `useForm`, but not in the form.\n * Without it, these errors are not shown to the user (not even in the console), so neither the user nor the\n * developer can understand why the form is not submitted.\n */\n function showErrorsForMissingFormFields(): ReactElement | false {\n const errors = Object.entries(form.errors)\n .map(([fieldName, errorMsg]) => ({ fieldName, errorMsg }))\n .filter(({ fieldName }) => isFormFieldMissing(fieldName))\n .map(({ fieldName, errorMsg }) => <li key={fieldName}>{errorMsg}</li>);\n\n const icon = <IconInfoCircle />;\n return (\n errors.length > 0 && (\n <Alert color=\"red\" icon={icon}>\n <ul>{errors}</ul>\n </Alert>\n )\n );\n }\n\n const { id } = form.values;\n\n const [loading, setLoading] = useState(false);\n const effectiveBackRoute = backRoute || `/${pathname.split('/').at(1)}`;\n\n const canShowSubmitButton = formType === FormType.Create || formType === FormType.Edit;\n const formTypeTitle = messages.form.action[FORM_ACTIONS[formType] ?? 'custom'];\n const documentTitle = useDocumentTitle(`${formTypeTitle} ${title}`);\n\n const handleSubmit = async (data: D) => {\n setLoading(true);\n try {\n const effectiveData = formatValues(data) as D;\n await service.save(id, effectiveData);\n showSuccessNotification(messages.form.saveSuccess(formTypeTitle), messages.common.success);\n\n if (effectiveBackRoute) navigate(effectiveBackRoute);\n } catch (e) {\n showErrorNotification(e, messages.form.saveError(formTypeTitle), messages.common.error);\n } finally {\n setLoading(false);\n }\n };\n\n return (\n <>\n <title>{documentTitle}</title>\n <Stack gap=\"lg\">\n <PageHeader\n title={`${formTypeTitle} ${title}`}\n breadcrumbItems={[\n { title, href: effectiveBackRoute },\n { title: `${formTypeTitle} ${title}`, href: '#' }\n ]}\n />\n <Grid>\n <Grid.Col span={{ base: 12, lg: isFullWidth ? 12 : 6 }}>\n <PaperCard>\n <Box component=\"form\" onSubmit={form.onSubmit(handleSubmit)} pos=\"relative\">\n <LoadingOverlay\n visible={isLoading}\n zIndex={1000}\n overlayProps={{ radius: 'sm', blur: 2 }}\n />\n\n <Stack>\n <Text size=\"lg\" fw={600}>\n {formTypeTitle} {title}\n </Text>\n\n {children}\n {showErrorsForMissingFormFields()}\n\n <Group>\n <Button\n leftSection={<IconArrowBack size={16} />}\n component={Link}\n href={effectiveBackRoute}\n variant=\"default\"\n >\n {messages.common.back}\n </Button>\n {canShowSubmitButton && (\n <Button\n type=\"submit\"\n leftSection={<IconDeviceFloppy size={16} />}\n loading={loading}\n >\n {formTypeTitle}\n </Button>\n )}\n </Group>\n </Stack>\n </Box>\n </PaperCard>\n </Grid.Col>\n </Grid>\n </Stack>\n </>\n );\n}\n\nexport default FormPage;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FormPage.js","names":["_Fragment"],"sources":["../../../../src/component/template/FormPage/FormPage.tsx"],"sourcesContent":["import React, { ReactElement, useState } from 'react';\n\nimport { Alert, Box, Button, Grid, Group, LoadingOverlay, Stack, Text } from '@mantine/core';\nimport type { UseFormReturnType } from '@mantine/form';\nimport { IconArrowBack, IconDeviceFloppy, IconInfoCircle } from '@tabler/icons-react';\n\nimport { SaveDataSource } from '../../../data/SaveDataSource';\nimport { FormAction } from '../../../i18n/KitMessages';\nimport { Identifiable } from '../../../model/Identifiable';\nimport { useDocumentTitle, useKitConfig } from '../../../provider/useKit';\nimport { formatValues } from '../../../util/TransformData';\nimport { showErrorNotification, showSuccessNotification } from '../../../util/Notifications';\nimport PaperCard from '../../molecule/PaperCard/PaperCard';\nimport PageHeader from '../../organism/PageHeader/PageHeader';\nimport { FormType } from './FormType';\n\ninterface FormPageProps<T extends Identifiable, D extends object, S extends SaveDataSource<T, D>> {\n /** Destination of the data (e.g. a service that accesses an API). */\n service: S;\n\n /** Name of the record (e.g. \"Region\"), shown after the action verb in the title. */\n title: string;\n formType: FormType;\n form: UseFormReturnType<D & Identifiable>;\n\n /** Page opened after saving and by the back button; absent for the first segment of the current path. */\n backRoute?: string;\n\n /** Shows a loading overlay over the form (e.g. while the record is fetched). */\n isLoading?: boolean;\n\n /** Whether the form takes the whole width, instead of half of it on large screens. */\n isFullWidth?: boolean;\n}\n\n/** Action of each form type, the key of its verb in the texts. */\nconst FORM_ACTIONS: Record<FormType, FormAction> = {\n [FormType.Create]: 'create',\n [FormType.Edit]: 'edit',\n [FormType.View]: 'view'\n};\n\n/**\n * CRUD form page to create, edit or view a record (according to `formType`), which saves the data in `service` and\n * goes back to the list page.\n */\nfunction FormPage<T extends Identifiable, D extends object, S extends SaveDataSource<T, D>>(\n props: React.PropsWithChildren<FormPageProps<T, D, S>>\n): ReactElement {\n const { title, formType, service, form, backRoute, isLoading, isFullWidth, children } = props;\n const { router, messages } = useKitConfig();\n const { Link } = router;\n const navigate = router.useNavigate();\n const pathname = router.usePathname();\n\n /**\n * Checks whether a field of the validation schema is missing in the form.\n * @param fieldName name of the field to find\n */\n function isFormFieldMissing(fieldName: string): boolean {\n return !form.getInputNode(fieldName);\n }\n\n /**\n * Shows the errors of the fields that are in the validation schema given to `useForm`, but not in the form.\n * Without it, these errors are not shown to the user (not even in the console), so neither the user nor the\n * developer can understand why the form is not submitted.\n */\n function showErrorsForMissingFormFields(): ReactElement | false {\n const errors = Object.entries(form.errors)\n .map(([fieldName, errorMsg]) => ({ fieldName, errorMsg }))\n .filter(({ fieldName }) => isFormFieldMissing(fieldName))\n .map(({ fieldName, errorMsg }) => <li key={fieldName}>{errorMsg}</li>);\n\n const icon = <IconInfoCircle />;\n return (\n errors.length > 0 && (\n <Alert color=\"red\" icon={icon}>\n <ul>{errors}</ul>\n </Alert>\n )\n );\n }\n\n const { id } = form.values;\n\n const [loading, setLoading] = useState(false);\n const effectiveBackRoute = backRoute || `/${pathname.split('/').at(1)}`;\n\n const canShowSubmitButton = formType === FormType.Create || formType === FormType.Edit;\n const formTypeTitle = messages.form.action[FORM_ACTIONS[formType] ?? 'custom'];\n const documentTitle = useDocumentTitle(`${formTypeTitle} ${title}`);\n\n const handleSubmit = async (data: D) => {\n setLoading(true);\n try {\n const effectiveData = formatValues(data) as D;\n await service.save(id, effectiveData);\n showSuccessNotification(messages.form.saveSuccess(formTypeTitle), messages.common.success);\n\n if (effectiveBackRoute) navigate(effectiveBackRoute);\n } catch (e) {\n showErrorNotification(e, messages.form.saveError(formTypeTitle), messages.common.error);\n } finally {\n setLoading(false);\n }\n };\n\n return (\n <>\n <title>{documentTitle}</title>\n <Stack gap=\"lg\">\n <PageHeader\n title={`${formTypeTitle} ${title}`}\n breadcrumbItems={[\n { title, href: effectiveBackRoute },\n { title: `${formTypeTitle} ${title}`, href: '#' }\n ]}\n />\n <Grid>\n <Grid.Col span={{ base: 12, lg: isFullWidth ? 12 : 6 }}>\n <PaperCard>\n <Box component=\"form\" onSubmit={form.onSubmit(handleSubmit)} pos=\"relative\">\n <LoadingOverlay\n visible={isLoading}\n zIndex={1000}\n overlayProps={{ radius: 'sm', blur: 2 }}\n />\n\n <Stack>\n <Text size=\"lg\" fw={600}>\n {formTypeTitle} {title}\n </Text>\n\n {children}\n {showErrorsForMissingFormFields()}\n\n <Group>\n <Button\n leftSection={<IconArrowBack size={16} />}\n component={Link}\n href={effectiveBackRoute}\n variant=\"default\"\n >\n {messages.common.back}\n </Button>\n {canShowSubmitButton && (\n <Button\n type=\"submit\"\n leftSection={<IconDeviceFloppy size={16} />}\n loading={loading}\n >\n {formTypeTitle}\n </Button>\n )}\n </Group>\n </Stack>\n </Box>\n </PaperCard>\n </Grid.Col>\n </Grid>\n </Stack>\n </>\n );\n}\n\nexport default FormPage;\n"],"mappings":"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"}
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
+
/** Values returned by {@link useService}. */
|
|
2
|
+
export type UseService<D> = {
|
|
3
|
+
/** Data of the fetch, or the initial value while it has not finished (or if it failed). */
|
|
4
|
+
data: D;
|
|
5
|
+
/** Error of the fetch; `null` if it has not finished yet or succeeded. */
|
|
6
|
+
error: unknown | null;
|
|
7
|
+
/** Whether the fetch has not finished yet. */
|
|
8
|
+
loading: boolean;
|
|
9
|
+
};
|
|
1
10
|
/**
|
|
2
11
|
* Fetches data when the component mounts and every time `fetchMethod` changes.
|
|
12
|
+
* Without an initial value, `data` is `undefined` while loading (or after a failure), so the caller must handle it.
|
|
3
13
|
* @param fetchMethod function that fetches the data. It must be stable (e.g. with `useCallback`), since a new function
|
|
4
14
|
* triggers a new fetch
|
|
5
|
-
* @return the data (
|
|
15
|
+
* @return the data (`undefined` until the fetch finishes), the error and whether the fetch is still running
|
|
6
16
|
*/
|
|
7
|
-
declare
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
declare function useService<T>(fetchMethod: () => Promise<T>): UseService<T | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* Fetches data when the component mounts and every time `fetchMethod` changes, starting with the given value (e.g. an
|
|
20
|
+
* empty array for a list), so `data` always has the type of the fetched data.
|
|
21
|
+
* @param fetchMethod function that fetches the data. It must be stable (e.g. with `useCallback`), since a new function
|
|
22
|
+
* triggers a new fetch
|
|
23
|
+
* @param initialData value of `data` until the fetch finishes (and after a failure)
|
|
24
|
+
* @return the data, the error and whether the fetch is still running
|
|
25
|
+
*/
|
|
26
|
+
declare function useService<T>(fetchMethod: () => Promise<T>, initialData: T): UseService<T>;
|
|
12
27
|
export default useService;
|
package/dist/hook/useService.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
// src/hook/useService.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* @param fetchMethod function that fetches the data. It must be stable (e.g. with `useCallback`), since a new function
|
|
7
|
-
* triggers a new fetch
|
|
8
|
-
* @return the data (an empty array until the fetch finishes), the error and whether the fetch is still running
|
|
9
|
-
*/
|
|
10
|
-
var useService = (fetchMethod) => {
|
|
11
|
-
const [data, setData] = useState([]);
|
|
4
|
+
function useService(fetchMethod, initialData) {
|
|
5
|
+
const [data, setData] = useState(initialData);
|
|
12
6
|
const [error, setError] = useState(null);
|
|
13
7
|
const [loading, setLoading] = useState(true);
|
|
14
8
|
useEffect(() => {
|
|
@@ -23,7 +17,7 @@ var useService = (fetchMethod) => {
|
|
|
23
17
|
error,
|
|
24
18
|
loading
|
|
25
19
|
};
|
|
26
|
-
}
|
|
20
|
+
}
|
|
27
21
|
export { useService as default };
|
|
28
22
|
|
|
29
23
|
//# sourceMappingURL=useService.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useService.js","names":[],"sources":["../../src/hook/useService.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n/**\n * Fetches data when the component mounts and every time `fetchMethod` changes.\n * @param fetchMethod function that fetches the data. It must be stable (e.g. with `useCallback`), since a new function\n * triggers a new fetch\n * @return the data (an empty array until the fetch finishes), the error and whether the fetch is still running\n */\
|
|
1
|
+
{"version":3,"file":"useService.js","names":[],"sources":["../../src/hook/useService.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n/** Values returned by {@link useService}. */\nexport type UseService<D> = {\n /** Data of the fetch, or the initial value while it has not finished (or if it failed). */\n data: D;\n\n /** Error of the fetch; `null` if it has not finished yet or succeeded. */\n error: unknown | null;\n\n /** Whether the fetch has not finished yet. */\n loading: boolean;\n};\n\n/**\n * Fetches data when the component mounts and every time `fetchMethod` changes.\n * Without an initial value, `data` is `undefined` while loading (or after a failure), so the caller must handle it.\n * @param fetchMethod function that fetches the data. It must be stable (e.g. with `useCallback`), since a new function\n * triggers a new fetch\n * @return the data (`undefined` until the fetch finishes), the error and whether the fetch is still running\n */\nfunction useService<T>(fetchMethod: () => Promise<T>): UseService<T | undefined>;\n\n/**\n * Fetches data when the component mounts and every time `fetchMethod` changes, starting with the given value (e.g. an\n * empty array for a list), so `data` always has the type of the fetched data.\n * @param fetchMethod function that fetches the data. It must be stable (e.g. with `useCallback`), since a new function\n * triggers a new fetch\n * @param initialData value of `data` until the fetch finishes (and after a failure)\n * @return the data, the error and whether the fetch is still running\n */\nfunction useService<T>(fetchMethod: () => Promise<T>, initialData: T): UseService<T>;\n\nfunction useService<T>(fetchMethod: () => Promise<T>, initialData?: T): UseService<T | undefined> {\n const [data, setData] = useState<T | undefined>(initialData);\n const [error, setError] = useState<unknown | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n /* When fetchMethod changes (e.g. another filter), the previous fetch becomes obsolete. If its response arrives\n after the one of the new fetch, it cannot overwrite the current data. */\n let obsolete = false;\n fetchMethod()\n .then(responseData => !obsolete && setData(responseData))\n .catch(e => !obsolete && setError(e))\n .finally(() => !obsolete && setLoading(false));\n\n return () => {\n obsolete = true;\n };\n }, [fetchMethod]);\n\n return { data, error, loading };\n}\n\nexport default useService;\n"],"mappings":";;;AAiCA,SAAS,WAAc,aAA+B,aAA4C;CAC9F,MAAM,CAAC,MAAM,WAAW,SAAwB,WAAW;CAC3D,MAAM,CAAC,OAAO,YAAY,SAAyB,IAAI;CACvD,MAAM,CAAC,SAAS,cAAc,SAAS,IAAI;CAE3C,gBAAgB;EAGZ,IAAI,WAAW;EACf,YAAY,CAAC,CACR,MAAK,iBAAgB,CAAC,YAAY,QAAQ,YAAY,CAAC,CAAC,CACxD,OAAM,MAAK,CAAC,YAAY,SAAS,CAAC,CAAC,CAAC,CACpC,cAAc,CAAC,YAAY,WAAW,KAAK,CAAC;EAEjD,aAAa;GACT,WAAW;EACf;CACJ,GAAG,CAAC,WAAW,CAAC;CAEhB,OAAO;EAAE;EAAM;EAAO;CAAQ;AAClC"}
|
package/dist/util/DateUtil.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/** Value of a date field: a `Date`, a text in the given format, or nothing. */
|
|
2
|
+
export type DateFieldValue = Date | string | null | undefined;
|
|
2
3
|
export declare const BR_DATE_FORMAT = "DD/MM/YYYY";
|
|
3
4
|
export declare const ISO_DATE_FORMAT = "YYYY-MM-DD";
|
|
4
5
|
/**
|
|
@@ -25,7 +26,7 @@ export declare function formatDateToISO(date: Date | undefined | null): string |
|
|
|
25
26
|
* @param format format of the date, in Day.js tokens; defaults to {@link BR_DATE_FORMAT}
|
|
26
27
|
* @return the date, or `null` if there is no date
|
|
27
28
|
*/
|
|
28
|
-
export declare function parseDate(dateValue:
|
|
29
|
+
export declare function parseDate(dateValue: DateFieldValue, format?: string): Date | null;
|
|
29
30
|
/** @return the current year and month in the `YYYY-MM` format */
|
|
30
31
|
export declare function getCurrentYearMonth(): string;
|
|
31
32
|
/**
|
package/dist/util/DateUtil.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DateUtil.js","names":[],"sources":["../../src/util/DateUtil.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport
|
|
1
|
+
{"version":3,"file":"DateUtil.js","names":[],"sources":["../../src/util/DateUtil.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport customParseFormat from 'dayjs/plugin/customParseFormat.js';\n\n/* Without this plugin, Day.js ignores the format given to `dayjs(value, format)` and parses the text month first. */\ndayjs.extend(customParseFormat);\n\n/** Value of a date field: a `Date`, a text in the given format, or nothing. */\nexport type DateFieldValue = Date | string | null | undefined;\n\nexport const BR_DATE_FORMAT = 'DD/MM/YYYY';\nexport const ISO_DATE_FORMAT = 'YYYY-MM-DD';\n\n/**\n * Minimum value for a date to be considered valid when Zod coerces it. It is later than the date created by\n * `new Date(null)` (12/31/1969), which Zod's `coerce` produces when a date field is not filled and receives `null`.\n */\nexport const MIN_VALID_DATE = new Date('01/01/1970');\n\n/**\n * Formats a date.\n * @param date date to format\n * @param format format, in Day.js tokens; defaults to {@link BR_DATE_FORMAT}\n * @return the formatted date, or `null` if there is no date\n */\nexport function formatDate(date: Date | undefined | null, format: string = BR_DATE_FORMAT): string | null {\n return date ? dayjs(date).format(format) : null;\n}\n\n/**\n * Formats a date in the ISO 8601 format.\n * @param date date to format\n * @see #ISO_DATE_FORMAT\n */\nexport function formatDateToISO(date: Date | undefined | null): string | null {\n return date ? dayjs(date).format(ISO_DATE_FORMAT) : null;\n}\n\n/**\n * Converts a date in the given format to a `Date` object.\n * @param dateValue date to convert\n * @param format format of the date, in Day.js tokens; defaults to {@link BR_DATE_FORMAT}\n * @return the date, or `null` if there is no date\n */\nexport function parseDate(dateValue: DateFieldValue, format: string = BR_DATE_FORMAT): Date | null {\n return dateValue ? dayjs(dateValue, format).toDate() : null;\n}\n\n/** @return the current year and month in the `YYYY-MM` format */\nexport function getCurrentYearMonth(): string {\n const currentDate = new Date();\n const year = currentDate.getFullYear();\n const month = (currentDate.getMonth() + 1).toString().padStart(2, '0');\n return `${year}-${month}`;\n}\n\n/**\n * Checks whether the end date is after the start date.\n * @param startDate start of the interval\n * @param endDate end of the interval; an absent or empty (before {@link MIN_VALID_DATE}) end date is valid\n * @return `true` if the interval is valid\n */\nexport function validDateInterval(startDate: Date, endDate: Date | undefined): boolean {\n return !endDate || endDate <= MIN_VALID_DATE || endDate > startDate;\n}\n"],"mappings":";;;;AAIA,MAAM,OAAO,iBAAiB;AAK9B,IAAa,iBAAiB;AAC9B,IAAa,kBAAkB;;;;;AAM/B,IAAa,iCAAiB,IAAI,KAAK,YAAY;;;;;;;AAQnD,SAAgB,WAAW,MAA+B,SAAiB,gBAA+B;CACtG,OAAO,OAAO,MAAM,IAAI,CAAC,CAAC,OAAO,MAAM,IAAI;AAC/C;;;;;;AAOA,SAAgB,gBAAgB,MAA8C;CAC1E,OAAO,OAAO,MAAM,IAAI,CAAC,CAAC,OAAO,eAAe,IAAI;AACxD;;;;;;;AAQA,SAAgB,UAAU,WAA2B,SAAiB,gBAA6B;CAC/F,OAAO,YAAY,MAAM,WAAW,MAAM,CAAC,CAAC,OAAO,IAAI;AAC3D;;AAGA,SAAgB,sBAA8B;CAC1C,MAAM,8BAAc,IAAI,KAAK;CAG7B,OAAO,GAFM,YAAY,YAEf,EAAK,IADA,YAAY,SAAS,IAAI,EAAA,CAAG,SAAS,CAAC,CAAC,SAAS,GAAG,GAChD;AACtB;;;;;;;AAQA,SAAgB,kBAAkB,WAAiB,SAAoC;CACnF,OAAO,CAAC,WAAW,WAAW,kBAAkB,UAAU;AAC9D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifto/mantine-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "General purpose Mantine components for React apps: CRUD tables and forms, layout, ECharts charts, Excel/CSV export, print pages and a REST HTTP client, with i18n (en-US and pt-BR).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "IFTO - Campus Palmas",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"packageManager": "yarn@4.18.1",
|
|
25
25
|
"type": "module",
|
|
26
26
|
"engines": {
|
|
27
|
-
"node": ">=
|
|
27
|
+
"node": ">=20"
|
|
28
28
|
},
|
|
29
29
|
"sideEffects": [
|
|
30
30
|
"**/*.css"
|
|
@@ -79,6 +79,15 @@
|
|
|
79
79
|
"zod": "^4.6.6"
|
|
80
80
|
},
|
|
81
81
|
"peerDependenciesMeta": {
|
|
82
|
+
"@mantine/dates": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"@mantine/form": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@mantine/modals": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
82
91
|
"axios": {
|
|
83
92
|
"optional": true
|
|
84
93
|
},
|