@olenbetong/appframe-ds 1.2.0 → 1.3.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/CHANGELOG.md +11 -0
- package/package.json +3 -3
- package/scripts/copyAssets.mjs +27 -27
- package/src/AfLookup/Lookup.css +129 -129
- package/src/AfLookup/Lookup.tsx +184 -184
- package/src/AfLookup/LookupCombobox.css +78 -78
- package/src/AfLookup/LookupCombobox.tsx +237 -237
- package/src/AfLookup/LookupEdit.tsx +130 -130
- package/src/AfLookup/LookupGrid.tsx +124 -124
- package/src/AfLookup/index.ts +5 -5
- package/src/autocomplete/Autocomplete.css +256 -256
- package/src/autocomplete/Autocomplete.tsx +320 -320
- package/src/autocomplete/AutocompleteFrame.tsx +168 -168
- package/src/autocomplete/Combobox.tsx +286 -286
- package/src/autocomplete/index.ts +4 -4
- package/src/autocomplete/types.ts +204 -204
- package/src/autocomplete/utils.ts +183 -183
- package/src/binding/BoundNumericTextField.tsx +102 -102
- package/src/binding/BoundTextField.tsx +49 -49
- package/src/binding/CancelButton.tsx +30 -30
- package/src/binding/CancelIconButton.tsx +30 -30
- package/src/binding/DataEditToolbar.tsx +92 -92
- package/src/binding/DeleteButton.tsx +35 -35
- package/src/binding/DeleteIconButton.tsx +35 -35
- package/src/binding/RefreshButton.tsx +31 -31
- package/src/binding/RefreshIconButton.tsx +33 -33
- package/src/binding/RefreshRowIconButton.tsx +33 -33
- package/src/binding/SaveButton.tsx +37 -37
- package/src/binding/SaveIconButton.tsx +38 -38
- package/src/container/Container.tsx +49 -49
- package/src/container/index.ts +1 -1
- package/src/cutoff/CutOffPicker.css +12 -12
- package/src/cutoff/CutOffPicker.tsx +165 -165
- package/src/cutoff/context.tsx +194 -194
- package/src/cutoff/dsCutOffDates.ts +21 -21
- package/src/cutoff/index.ts +4 -4
- package/src/cutoff/utils.test.ts +59 -59
- package/src/cutoff/utils.ts +276 -276
- package/src/filter/ChipListInput.tsx +79 -79
- package/src/filter/FieldFilterPanel.css +150 -150
- package/src/filter/FieldFilterPanel.tsx +309 -309
- package/src/filter/FilterBuilder.css +152 -152
- package/src/filter/FilterBuilder.test.tsx +153 -153
- package/src/filter/FilterBuilder.tsx +435 -435
- package/src/filter/FilterEditor.css +231 -231
- package/src/filter/FilterEditor.test.tsx +259 -259
- package/src/filter/FilterEditor.tsx +75 -75
- package/src/filter/FilterGroupEditor.tsx +160 -160
- package/src/filter/FilterNameDialog.css +22 -22
- package/src/filter/FilterNameDialog.tsx +81 -81
- package/src/filter/FilterRow.tsx +117 -117
- package/src/filter/FilterShareDialog.css +28 -28
- package/src/filter/FilterShareDialog.tsx +201 -201
- package/src/filter/FilterStringField.tsx +78 -78
- package/src/filter/FilterValueEditor.tsx +220 -220
- package/src/filter/SavedFilterTree.css +107 -107
- package/src/filter/SavedFilterTree.test.tsx +94 -94
- package/src/filter/SavedFilterTree.tsx +218 -218
- package/src/filter/fieldFilter.test.ts +158 -158
- package/src/filter/fieldFilter.ts +139 -139
- package/src/filter/index.ts +27 -27
- package/src/filter/types.ts +38 -38
- package/src/filter/useDistinctValues.test.ts +102 -102
- package/src/filter/useDistinctValues.ts +230 -230
- package/src/global.d.ts +11 -11
- package/src/grid/AfGridColumnsPanel.tsx +118 -118
- package/src/grid/AfGridContext.tsx +51 -51
- package/src/grid/AfGridError.tsx +46 -46
- package/src/grid/AfHeaderFilterCell.tsx +147 -147
- package/src/grid/AfHeaderFilterPanel.tsx +111 -111
- package/src/grid/GridEditLookup.css +12 -12
- package/src/grid/GridEditLookup.tsx +121 -121
- package/src/grid/Toolbar.tsx +233 -233
- package/src/grid/editing.ts +100 -100
- package/src/grid/filter.ts +78 -78
- package/src/grid/formatters.ts +48 -48
- package/src/grid/index.css +245 -245
- package/src/grid/index.tsx +503 -489
- package/src/grid/license.ts +48 -48
- package/src/grid/localization.ts +369 -369
- package/src/grid/slots/index.tsx +307 -307
- package/src/grid/slots/slots.css +64 -64
- package/src/grid/theme.tsx +138 -138
- package/src/grid/useAfColumns.tsx +371 -371
- package/src/grid/useAfCurrentIndex.ts +63 -63
- package/src/grid/useAfData.ts +20 -20
- package/src/grid/useAfFilter.ts +160 -160
- package/src/grid/useAfFilterFields.ts +55 -55
- package/src/grid/useAfGridApi.ts +71 -71
- package/src/grid/useAfKeyBindings.ts +36 -36
- package/src/grid/useAfNewItemRow.ts +258 -258
- package/src/grid/useAfPagination.ts +53 -53
- package/src/grid/useAfPersistedState.ts +230 -230
- package/src/grid/useAfRowEditModel.ts +145 -145
- package/src/grid/useAfRowGrouping.ts +47 -47
- package/src/grid/useAfServerAggregation.test.ts +68 -0
- package/src/grid/useAfServerAggregation.ts +158 -145
- package/src/grid/useAfSortModel.ts +84 -84
- package/src/i18n.ts +13 -13
- package/src/index.ts +14 -14
- package/src/input/InputAdornments.css +75 -75
- package/src/input/InputAdornments.tsx +102 -102
- package/src/input/index.ts +1 -1
- package/src/layout/AppMenu.tsx +74 -74
- package/src/layout/AppMenuDrawer.css +70 -70
- package/src/layout/AppMenuDrawer.tsx +122 -122
- package/src/layout/ErrorBoundary.tsx +162 -162
- package/src/layout/PageContainer.css +21 -2
- package/src/layout/PageContainer.tsx +18 -2
- package/src/layout/ResponsiveDialog.css +86 -86
- package/src/layout/ResponsiveDialog.tsx +55 -55
- package/src/layout/index.tsx +69 -69
- package/src/link/Link.tsx +23 -23
- package/src/link/index.ts +1 -1
- package/src/mdi/Mdi.css +138 -138
- package/src/mdi/Mdi.test.tsx +91 -91
- package/src/mdi/Mdi.tsx +141 -141
- package/src/mdi/MdiConfirmCloseDialog.tsx +47 -47
- package/src/mdi/MdiContext.tsx +59 -59
- package/src/mdi/MdiDocumentContext.tsx +60 -60
- package/src/mdi/MdiPanel.tsx +61 -61
- package/src/mdi/MdiRestore.test.tsx +86 -86
- package/src/mdi/MdiTab.tsx +30 -30
- package/src/mdi/controller.test.ts +382 -382
- package/src/mdi/controller.ts +656 -656
- package/src/mdi/dockviewHost.ts +132 -132
- package/src/mdi/fakeHost.ts +200 -200
- package/src/mdi/host.ts +44 -44
- package/src/mdi/ids.test.ts +20 -20
- package/src/mdi/ids.ts +18 -18
- package/src/mdi/index.ts +31 -31
- package/src/mdi/layoutRestore.test.ts +156 -156
- package/src/mdi/layoutRestore.ts +311 -311
- package/src/mdi/layoutSpec.test.ts +143 -143
- package/src/mdi/layoutSpec.ts +214 -214
- package/src/mdi/layoutStore.test.ts +93 -93
- package/src/mdi/layoutStore.ts +99 -99
- package/src/mdi/memorySettingsStore.ts +21 -21
- package/src/mdi/registry.ts +34 -34
- package/src/mdi/testTypes.ts +37 -37
- package/src/mdi/theme.ts +22 -22
- package/src/mdi/types.ts +214 -214
- package/src/page/Page.css +53 -53
- package/src/page/PageBlock.css +64 -64
- package/src/page/PageBlock.tsx +92 -92
- package/src/page/PageHeader.css +88 -88
- package/src/page/PageHeader.tsx +115 -115
- package/src/page/PageTitle.css +5 -5
- package/src/page/PageTitle.tsx +21 -21
- package/src/page/index.ts +4 -4
- package/src/paper/Paper.tsx +52 -52
- package/src/progress/LinearProgress.css +93 -93
- package/src/progress/LinearProgress.tsx +104 -104
- package/src/progress/index.ts +1 -1
- package/src/report-downloader/ReportDownloader.css +67 -67
- package/src/report-downloader/components/StatusItem.tsx +87 -87
- package/src/report-downloader/components/StatusList.tsx +28 -28
- package/src/report-downloader/index.ts +41 -41
- package/src/report-downloader/status.tsx +92 -92
- package/src/sortable/DropIndicator.css +51 -51
- package/src/sortable/DropIndicator.tsx +27 -27
- package/src/sortable/SortableItemList.css +21 -21
- package/src/sortable/SortableItemList.tsx +264 -264
- package/src/sortable/index.ts +8 -8
- package/src/sortable/reorderSortOrder.ts +126 -126
- package/src/sortable/sortOrder.ts +101 -101
- package/src/sortable/types.ts +9 -9
- package/src/sortable/useSortableContainer.ts +53 -53
- package/src/sortable/useSortableItem.ts +95 -95
- package/src/sortable/useSortableList.ts +117 -117
- package/src/test/happy-dom-document-class.ts +24 -24
- package/src/test/setup.ts +12 -12
- package/src/theme/useDsColorScheme.ts +53 -53
- package/tsconfig.build.json +9 -9
- package/tsconfig.json +8 -8
- package/vitest.config.ts +10 -10
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import { Badge, BadgePosition, Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
-
import { FloppydiskIcon } from "@navikt/aksel-icons";
|
|
3
|
-
import { useSaveButton } from "@olenbetong/appframe-react";
|
|
4
|
-
import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "react";
|
|
5
|
-
import { getLocalizedString } from "../i18n.js";
|
|
6
|
-
|
|
7
|
-
export const SaveButton: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
|
|
8
|
-
HTMLButtonElement,
|
|
9
|
-
ButtonProps
|
|
10
|
-
>(function SaveButton({ onClick, children, loading, ...props }, ref) {
|
|
11
|
-
let { dirty, saving, save } = useSaveButton();
|
|
12
|
-
let isLoading = saving || loading;
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<Button
|
|
16
|
-
loading={isLoading}
|
|
17
|
-
onClick={(evt) => {
|
|
18
|
-
onClick?.(evt);
|
|
19
|
-
if (!evt.defaultPrevented) {
|
|
20
|
-
save();
|
|
21
|
-
}
|
|
22
|
-
}}
|
|
23
|
-
{...props}
|
|
24
|
-
ref={ref}
|
|
25
|
-
>
|
|
26
|
-
{/* Icon is hidden while loading so the spinner takes its place instead of adding width */}
|
|
27
|
-
{!isLoading && (
|
|
28
|
-
<BadgePosition placement="top-right" overlap="circle">
|
|
29
|
-
<FloppydiskIcon aria-hidden="true" />
|
|
30
|
-
{/* Indicates unsaved changes on the current record */}
|
|
31
|
-
{dirty && <Badge data-color="warning" aria-hidden="true" />}
|
|
32
|
-
</BadgePosition>
|
|
33
|
-
)}
|
|
34
|
-
{children ?? getLocalizedString("Save")}
|
|
35
|
-
</Button>
|
|
36
|
-
);
|
|
37
|
-
});
|
|
1
|
+
import { Badge, BadgePosition, Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
+
import { FloppydiskIcon } from "@navikt/aksel-icons";
|
|
3
|
+
import { useSaveButton } from "@olenbetong/appframe-react";
|
|
4
|
+
import { type ForwardRefExoticComponent, forwardRef, type RefAttributes } from "react";
|
|
5
|
+
import { getLocalizedString } from "../i18n.js";
|
|
6
|
+
|
|
7
|
+
export const SaveButton: ForwardRefExoticComponent<ButtonProps & RefAttributes<HTMLButtonElement>> = forwardRef<
|
|
8
|
+
HTMLButtonElement,
|
|
9
|
+
ButtonProps
|
|
10
|
+
>(function SaveButton({ onClick, children, loading, ...props }, ref) {
|
|
11
|
+
let { dirty, saving, save } = useSaveButton();
|
|
12
|
+
let isLoading = saving || loading;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Button
|
|
16
|
+
loading={isLoading}
|
|
17
|
+
onClick={(evt) => {
|
|
18
|
+
onClick?.(evt);
|
|
19
|
+
if (!evt.defaultPrevented) {
|
|
20
|
+
save();
|
|
21
|
+
}
|
|
22
|
+
}}
|
|
23
|
+
{...props}
|
|
24
|
+
ref={ref}
|
|
25
|
+
>
|
|
26
|
+
{/* Icon is hidden while loading so the spinner takes its place instead of adding width */}
|
|
27
|
+
{!isLoading && (
|
|
28
|
+
<BadgePosition placement="top-right" overlap="circle">
|
|
29
|
+
<FloppydiskIcon aria-hidden="true" />
|
|
30
|
+
{/* Indicates unsaved changes on the current record */}
|
|
31
|
+
{dirty && <Badge data-color="warning" aria-hidden="true" />}
|
|
32
|
+
</BadgePosition>
|
|
33
|
+
)}
|
|
34
|
+
{children ?? getLocalizedString("Save")}
|
|
35
|
+
</Button>
|
|
36
|
+
);
|
|
37
|
+
});
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { Badge, BadgePosition, Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
-
import { FloppydiskIcon } from "@navikt/aksel-icons";
|
|
3
|
-
import { useSaveButton } from "@olenbetong/appframe-react";
|
|
4
|
-
import { forwardRef } from "react";
|
|
5
|
-
import { getLocalizedString } from "../i18n.js";
|
|
6
|
-
|
|
7
|
-
export const SaveIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps, "icon">>(function SaveIconButton(
|
|
8
|
-
{ onClick, children, loading, ...props },
|
|
9
|
-
ref,
|
|
10
|
-
) {
|
|
11
|
-
let { dirty, saving, save } = useSaveButton();
|
|
12
|
-
let isLoading = saving || loading;
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<Button
|
|
16
|
-
icon
|
|
17
|
-
loading={isLoading}
|
|
18
|
-
aria-label={getLocalizedString("Save")}
|
|
19
|
-
variant="tertiary"
|
|
20
|
-
onClick={(evt) => {
|
|
21
|
-
onClick?.(evt);
|
|
22
|
-
if (!evt.defaultPrevented) {
|
|
23
|
-
save();
|
|
24
|
-
}
|
|
25
|
-
}}
|
|
26
|
-
{...props}
|
|
27
|
-
ref={ref}
|
|
28
|
-
>
|
|
29
|
-
{children ?? (
|
|
30
|
-
<BadgePosition placement="top-right" overlap="circle">
|
|
31
|
-
<FloppydiskIcon />
|
|
32
|
-
{/* Indicates unsaved changes on the current record */}
|
|
33
|
-
{dirty && <Badge data-color="warning" aria-hidden="true" />}
|
|
34
|
-
</BadgePosition>
|
|
35
|
-
)}
|
|
36
|
-
</Button>
|
|
37
|
-
);
|
|
38
|
-
});
|
|
1
|
+
import { Badge, BadgePosition, Button, type ButtonProps } from "@digdir/designsystemet-react";
|
|
2
|
+
import { FloppydiskIcon } from "@navikt/aksel-icons";
|
|
3
|
+
import { useSaveButton } from "@olenbetong/appframe-react";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import { getLocalizedString } from "../i18n.js";
|
|
6
|
+
|
|
7
|
+
export const SaveIconButton = forwardRef<HTMLButtonElement, Omit<ButtonProps, "icon">>(function SaveIconButton(
|
|
8
|
+
{ onClick, children, loading, ...props },
|
|
9
|
+
ref,
|
|
10
|
+
) {
|
|
11
|
+
let { dirty, saving, save } = useSaveButton();
|
|
12
|
+
let isLoading = saving || loading;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Button
|
|
16
|
+
icon
|
|
17
|
+
loading={isLoading}
|
|
18
|
+
aria-label={getLocalizedString("Save")}
|
|
19
|
+
variant="tertiary"
|
|
20
|
+
onClick={(evt) => {
|
|
21
|
+
onClick?.(evt);
|
|
22
|
+
if (!evt.defaultPrevented) {
|
|
23
|
+
save();
|
|
24
|
+
}
|
|
25
|
+
}}
|
|
26
|
+
{...props}
|
|
27
|
+
ref={ref}
|
|
28
|
+
>
|
|
29
|
+
{children ?? (
|
|
30
|
+
<BadgePosition placement="top-right" overlap="circle">
|
|
31
|
+
<FloppydiskIcon />
|
|
32
|
+
{/* Indicates unsaved changes on the current record */}
|
|
33
|
+
{dirty && <Badge data-color="warning" aria-hidden="true" />}
|
|
34
|
+
</BadgePosition>
|
|
35
|
+
)}
|
|
36
|
+
</Button>
|
|
37
|
+
);
|
|
38
|
+
});
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
import { PageBlock, type PageBlockProps, type PageBlockWidth } from "../page/PageBlock.js";
|
|
2
|
-
|
|
3
|
-
export const containerMaxWidths = ["xs", "sm", "md", "lg", "xl"] as const;
|
|
4
|
-
|
|
5
|
-
export type ContainerMaxWidth = (typeof containerMaxWidths)[number];
|
|
6
|
-
|
|
7
|
-
const maxWidthToBlockWidth: Record<ContainerMaxWidth, PageBlockWidth> = {
|
|
8
|
-
xs: "text",
|
|
9
|
-
sm: "text",
|
|
10
|
-
md: "md",
|
|
11
|
-
lg: "lg",
|
|
12
|
-
xl: "xl",
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type ContainerProps = Omit<PageBlockProps, "width"> & {
|
|
16
|
-
/**
|
|
17
|
-
* Predefined max-width, using the same scale as MUI `Container`. Content is
|
|
18
|
-
* horizontally centered.
|
|
19
|
-
*
|
|
20
|
-
* - `xs`/`sm`: 36rem (576px) — recommended line-length for running text
|
|
21
|
-
* - `md`: 48rem (768px)
|
|
22
|
-
* - `lg`: 64rem (1024px)
|
|
23
|
-
* - `xl`: 80rem (1280px)
|
|
24
|
-
* - `false`: no max-width
|
|
25
|
-
*
|
|
26
|
-
* @default "lg"
|
|
27
|
-
*/
|
|
28
|
-
maxWidth?: ContainerMaxWidth | false;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Centered, max-width content wrapper — the MUI `Container` equivalent.
|
|
33
|
-
*
|
|
34
|
-
* A thin alias over `Page.Block` with gutters enabled by default, for the very
|
|
35
|
-
* common case of "center this content and give it sensible side padding"
|
|
36
|
-
* without having to reach for the full `Page` composition.
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```jsx
|
|
40
|
-
* <Container maxWidth="sm">
|
|
41
|
-
* <Heading>Expenses</Heading>
|
|
42
|
-
* </Container>
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
export function Container({ gutters = true, maxWidth = "lg", ...props }: ContainerProps) {
|
|
46
|
-
let width = maxWidth === false ? undefined : maxWidthToBlockWidth[maxWidth];
|
|
47
|
-
|
|
48
|
-
return <PageBlock gutters={gutters} width={width} {...props} />;
|
|
49
|
-
}
|
|
1
|
+
import { PageBlock, type PageBlockProps, type PageBlockWidth } from "../page/PageBlock.js";
|
|
2
|
+
|
|
3
|
+
export const containerMaxWidths = ["xs", "sm", "md", "lg", "xl"] as const;
|
|
4
|
+
|
|
5
|
+
export type ContainerMaxWidth = (typeof containerMaxWidths)[number];
|
|
6
|
+
|
|
7
|
+
const maxWidthToBlockWidth: Record<ContainerMaxWidth, PageBlockWidth> = {
|
|
8
|
+
xs: "text",
|
|
9
|
+
sm: "text",
|
|
10
|
+
md: "md",
|
|
11
|
+
lg: "lg",
|
|
12
|
+
xl: "xl",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ContainerProps = Omit<PageBlockProps, "width"> & {
|
|
16
|
+
/**
|
|
17
|
+
* Predefined max-width, using the same scale as MUI `Container`. Content is
|
|
18
|
+
* horizontally centered.
|
|
19
|
+
*
|
|
20
|
+
* - `xs`/`sm`: 36rem (576px) — recommended line-length for running text
|
|
21
|
+
* - `md`: 48rem (768px)
|
|
22
|
+
* - `lg`: 64rem (1024px)
|
|
23
|
+
* - `xl`: 80rem (1280px)
|
|
24
|
+
* - `false`: no max-width
|
|
25
|
+
*
|
|
26
|
+
* @default "lg"
|
|
27
|
+
*/
|
|
28
|
+
maxWidth?: ContainerMaxWidth | false;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Centered, max-width content wrapper — the MUI `Container` equivalent.
|
|
33
|
+
*
|
|
34
|
+
* A thin alias over `Page.Block` with gutters enabled by default, for the very
|
|
35
|
+
* common case of "center this content and give it sensible side padding"
|
|
36
|
+
* without having to reach for the full `Page` composition.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```jsx
|
|
40
|
+
* <Container maxWidth="sm">
|
|
41
|
+
* <Heading>Expenses</Heading>
|
|
42
|
+
* </Container>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function Container({ gutters = true, maxWidth = "lg", ...props }: ContainerProps) {
|
|
46
|
+
let width = maxWidth === false ? undefined : maxWidthToBlockWidth[maxWidth];
|
|
47
|
+
|
|
48
|
+
return <PageBlock gutters={gutters} width={width} {...props} />;
|
|
49
|
+
}
|
package/src/container/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./Container.js";
|
|
1
|
+
export * from "./Container.js";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
@layer ds {
|
|
2
|
-
.ObCutOffPicker-root {
|
|
3
|
-
display: flex;
|
|
4
|
-
align-items: center;
|
|
5
|
-
gap: var(--ds-size-2);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
.ObCutOffPicker-root > .ds-select,
|
|
9
|
-
.ObCutOffPicker-root > .ds-field {
|
|
10
|
-
flex: 0 1 auto;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
1
|
+
@layer ds {
|
|
2
|
+
.ObCutOffPicker-root {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: var(--ds-size-2);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.ObCutOffPicker-root > .ds-select,
|
|
9
|
+
.ObCutOffPicker-root > .ds-field {
|
|
10
|
+
flex: 0 1 auto;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
import "./CutOffPicker.css";
|
|
2
|
-
|
|
3
|
-
import { Select, Textfield } from "@digdir/designsystemet-react";
|
|
4
|
-
import clsx from "clsx";
|
|
5
|
-
import type { Temporal } from "temporal-polyfill";
|
|
6
|
-
|
|
7
|
-
import { getLocalizedString } from "../i18n.js";
|
|
8
|
-
import { useCutOff } from "./context.js";
|
|
9
|
-
import { type PeriodType, plainDateToCutOff } from "./utils.js";
|
|
10
|
-
|
|
11
|
-
export type CutOffPickerProps = {
|
|
12
|
-
className?: string;
|
|
13
|
-
disabled?: boolean;
|
|
14
|
-
/** Period types left out of the type select. */
|
|
15
|
-
disabledTypes?: PeriodType[];
|
|
16
|
-
max?: Temporal.PlainDate | string;
|
|
17
|
-
min?: Temporal.PlainDate | string;
|
|
18
|
-
/** Designsystemet size of both fields. */
|
|
19
|
-
"data-size"?: "sm" | "md" | "lg";
|
|
20
|
-
/**
|
|
21
|
-
* Do not sync the `co` and `period` search parameters with the selection - for a form that
|
|
22
|
-
* submits the period itself.
|
|
23
|
-
*/
|
|
24
|
-
disableSearchParams?: boolean;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function cutOffTypes(): [PeriodType, string][] {
|
|
28
|
-
return [
|
|
29
|
-
["AllDates", getLocalizedString("Day")],
|
|
30
|
-
["WS", getLocalizedString("Week")],
|
|
31
|
-
["MLD", getLocalizedString("Month")],
|
|
32
|
-
["MLS", `${getLocalizedString("Month")} (${getLocalizedString("Last sunday")})`],
|
|
33
|
-
["QLD", getLocalizedString("Quarter")],
|
|
34
|
-
["QLS", `${getLocalizedString("Quarter")} (${getLocalizedString("Last sunday")})`],
|
|
35
|
-
["YLD", getLocalizedString("Year")],
|
|
36
|
-
];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function getCutOffTypes(disabledTypes: PeriodType[]): [PeriodType, string][] {
|
|
40
|
-
return cutOffTypes().filter((co) => !disabledTypes.includes(co[0]));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Picks the app's cut-off period: a select for the period type (day, week, month, quarter,
|
|
45
|
-
* year) and a field for the period itself, native date/week/month inputs where the browser
|
|
46
|
-
* has them and a select for quarters and years. Reads and writes the period through
|
|
47
|
-
* {@link useCutOff}, so it needs a `CutOffProvider` above it.
|
|
48
|
-
*/
|
|
49
|
-
export function CutOffPicker({
|
|
50
|
-
className,
|
|
51
|
-
max,
|
|
52
|
-
min,
|
|
53
|
-
disabled,
|
|
54
|
-
disabledTypes,
|
|
55
|
-
"data-size": size,
|
|
56
|
-
disableSearchParams = false,
|
|
57
|
-
}: CutOffPickerProps) {
|
|
58
|
-
let { cutOffType, periods, setPeriod, setCutOffType } = useCutOff({ disableSearchParams });
|
|
59
|
-
|
|
60
|
-
function handlePeriodChanged(evt: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
|
|
61
|
-
if (evt.target.value) {
|
|
62
|
-
setPeriod(cutOffType, evt.target.value);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let thisYear = new Date().getFullYear();
|
|
67
|
-
let quarters = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
68
|
-
.flatMap((dy) => [4, 3, 2, 1].map((q) => `${thisYear - dy}-Q${q}`))
|
|
69
|
-
.filter((period) => {
|
|
70
|
-
if (max && period > plainDateToCutOff(max, "QLD")) return false;
|
|
71
|
-
if (min && period < plainDateToCutOff(min, "QLD")) return false;
|
|
72
|
-
return true;
|
|
73
|
-
});
|
|
74
|
-
let years = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
75
|
-
.map((dy) => thisYear - dy)
|
|
76
|
-
.filter((year) => {
|
|
77
|
-
if (max && year > Number(plainDateToCutOff(max, "YLD"))) return false;
|
|
78
|
-
if (min && year < Number(plainDateToCutOff(min, "YLD"))) return false;
|
|
79
|
-
return true;
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<div className={clsx("ObCutOffPicker-root", className)} data-size={size}>
|
|
84
|
-
<Select
|
|
85
|
-
name="co"
|
|
86
|
-
aria-label={getLocalizedString("Period type")}
|
|
87
|
-
value={cutOffType}
|
|
88
|
-
onChange={(evt) => setCutOffType(evt.target.value as PeriodType)}
|
|
89
|
-
disabled={disabled}
|
|
90
|
-
>
|
|
91
|
-
{getCutOffTypes(disabledTypes ?? []).map(([type, label]) => (
|
|
92
|
-
<Select.Option value={type} key={type}>
|
|
93
|
-
{label}
|
|
94
|
-
</Select.Option>
|
|
95
|
-
))}
|
|
96
|
-
</Select>
|
|
97
|
-
{cutOffType === "AllDates" && (
|
|
98
|
-
<Textfield
|
|
99
|
-
name="period"
|
|
100
|
-
aria-label={getLocalizedString("Day")}
|
|
101
|
-
type="date"
|
|
102
|
-
value={periods.AllDates}
|
|
103
|
-
onChange={handlePeriodChanged}
|
|
104
|
-
disabled={disabled}
|
|
105
|
-
max={max ? plainDateToCutOff(max, "AllDates") : undefined}
|
|
106
|
-
min={min ? plainDateToCutOff(min, "AllDates") : undefined}
|
|
107
|
-
/>
|
|
108
|
-
)}
|
|
109
|
-
{cutOffType === "WS" && (
|
|
110
|
-
<Textfield
|
|
111
|
-
name="period"
|
|
112
|
-
aria-label={getLocalizedString("Week")}
|
|
113
|
-
type="week"
|
|
114
|
-
value={periods.WS.indexOf("W") === -1 ? periods.WS.replace("-", "-W") : periods.WS}
|
|
115
|
-
onChange={handlePeriodChanged}
|
|
116
|
-
disabled={disabled}
|
|
117
|
-
max={max ? plainDateToCutOff(max, "WS") : undefined}
|
|
118
|
-
min={min ? plainDateToCutOff(min, "WS") : undefined}
|
|
119
|
-
/>
|
|
120
|
-
)}
|
|
121
|
-
{(cutOffType === "MLD" || cutOffType === "MLS") && (
|
|
122
|
-
<Textfield
|
|
123
|
-
name="period"
|
|
124
|
-
aria-label={getLocalizedString("Month")}
|
|
125
|
-
type="month"
|
|
126
|
-
value={periods.MLD}
|
|
127
|
-
onChange={handlePeriodChanged}
|
|
128
|
-
disabled={disabled}
|
|
129
|
-
max={max ? plainDateToCutOff(max, "MLD") : undefined}
|
|
130
|
-
min={min ? plainDateToCutOff(min, "MLD") : undefined}
|
|
131
|
-
/>
|
|
132
|
-
)}
|
|
133
|
-
{(cutOffType === "QLD" || cutOffType === "QLS") && (
|
|
134
|
-
<Select
|
|
135
|
-
name="period"
|
|
136
|
-
aria-label={getLocalizedString("Quarter")}
|
|
137
|
-
value={periods.QLD}
|
|
138
|
-
onChange={handlePeriodChanged}
|
|
139
|
-
disabled={disabled}
|
|
140
|
-
>
|
|
141
|
-
{quarters.map((quarter) => (
|
|
142
|
-
<Select.Option key={quarter} value={quarter}>
|
|
143
|
-
{quarter}
|
|
144
|
-
</Select.Option>
|
|
145
|
-
))}
|
|
146
|
-
</Select>
|
|
147
|
-
)}
|
|
148
|
-
{cutOffType === "YLD" && (
|
|
149
|
-
<Select
|
|
150
|
-
name="period"
|
|
151
|
-
aria-label={getLocalizedString("Year")}
|
|
152
|
-
value={periods.YLD}
|
|
153
|
-
onChange={handlePeriodChanged}
|
|
154
|
-
disabled={disabled}
|
|
155
|
-
>
|
|
156
|
-
{years.map((year) => (
|
|
157
|
-
<Select.Option key={year} value={String(year)}>
|
|
158
|
-
{year}
|
|
159
|
-
</Select.Option>
|
|
160
|
-
))}
|
|
161
|
-
</Select>
|
|
162
|
-
)}
|
|
163
|
-
</div>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
1
|
+
import "./CutOffPicker.css";
|
|
2
|
+
|
|
3
|
+
import { Select, Textfield } from "@digdir/designsystemet-react";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import type { Temporal } from "temporal-polyfill";
|
|
6
|
+
|
|
7
|
+
import { getLocalizedString } from "../i18n.js";
|
|
8
|
+
import { useCutOff } from "./context.js";
|
|
9
|
+
import { type PeriodType, plainDateToCutOff } from "./utils.js";
|
|
10
|
+
|
|
11
|
+
export type CutOffPickerProps = {
|
|
12
|
+
className?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** Period types left out of the type select. */
|
|
15
|
+
disabledTypes?: PeriodType[];
|
|
16
|
+
max?: Temporal.PlainDate | string;
|
|
17
|
+
min?: Temporal.PlainDate | string;
|
|
18
|
+
/** Designsystemet size of both fields. */
|
|
19
|
+
"data-size"?: "sm" | "md" | "lg";
|
|
20
|
+
/**
|
|
21
|
+
* Do not sync the `co` and `period` search parameters with the selection - for a form that
|
|
22
|
+
* submits the period itself.
|
|
23
|
+
*/
|
|
24
|
+
disableSearchParams?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function cutOffTypes(): [PeriodType, string][] {
|
|
28
|
+
return [
|
|
29
|
+
["AllDates", getLocalizedString("Day")],
|
|
30
|
+
["WS", getLocalizedString("Week")],
|
|
31
|
+
["MLD", getLocalizedString("Month")],
|
|
32
|
+
["MLS", `${getLocalizedString("Month")} (${getLocalizedString("Last sunday")})`],
|
|
33
|
+
["QLD", getLocalizedString("Quarter")],
|
|
34
|
+
["QLS", `${getLocalizedString("Quarter")} (${getLocalizedString("Last sunday")})`],
|
|
35
|
+
["YLD", getLocalizedString("Year")],
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getCutOffTypes(disabledTypes: PeriodType[]): [PeriodType, string][] {
|
|
40
|
+
return cutOffTypes().filter((co) => !disabledTypes.includes(co[0]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Picks the app's cut-off period: a select for the period type (day, week, month, quarter,
|
|
45
|
+
* year) and a field for the period itself, native date/week/month inputs where the browser
|
|
46
|
+
* has them and a select for quarters and years. Reads and writes the period through
|
|
47
|
+
* {@link useCutOff}, so it needs a `CutOffProvider` above it.
|
|
48
|
+
*/
|
|
49
|
+
export function CutOffPicker({
|
|
50
|
+
className,
|
|
51
|
+
max,
|
|
52
|
+
min,
|
|
53
|
+
disabled,
|
|
54
|
+
disabledTypes,
|
|
55
|
+
"data-size": size,
|
|
56
|
+
disableSearchParams = false,
|
|
57
|
+
}: CutOffPickerProps) {
|
|
58
|
+
let { cutOffType, periods, setPeriod, setCutOffType } = useCutOff({ disableSearchParams });
|
|
59
|
+
|
|
60
|
+
function handlePeriodChanged(evt: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
|
|
61
|
+
if (evt.target.value) {
|
|
62
|
+
setPeriod(cutOffType, evt.target.value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let thisYear = new Date().getFullYear();
|
|
67
|
+
let quarters = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
68
|
+
.flatMap((dy) => [4, 3, 2, 1].map((q) => `${thisYear - dy}-Q${q}`))
|
|
69
|
+
.filter((period) => {
|
|
70
|
+
if (max && period > plainDateToCutOff(max, "QLD")) return false;
|
|
71
|
+
if (min && period < plainDateToCutOff(min, "QLD")) return false;
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
let years = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
75
|
+
.map((dy) => thisYear - dy)
|
|
76
|
+
.filter((year) => {
|
|
77
|
+
if (max && year > Number(plainDateToCutOff(max, "YLD"))) return false;
|
|
78
|
+
if (min && year < Number(plainDateToCutOff(min, "YLD"))) return false;
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className={clsx("ObCutOffPicker-root", className)} data-size={size}>
|
|
84
|
+
<Select
|
|
85
|
+
name="co"
|
|
86
|
+
aria-label={getLocalizedString("Period type")}
|
|
87
|
+
value={cutOffType}
|
|
88
|
+
onChange={(evt) => setCutOffType(evt.target.value as PeriodType)}
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
>
|
|
91
|
+
{getCutOffTypes(disabledTypes ?? []).map(([type, label]) => (
|
|
92
|
+
<Select.Option value={type} key={type}>
|
|
93
|
+
{label}
|
|
94
|
+
</Select.Option>
|
|
95
|
+
))}
|
|
96
|
+
</Select>
|
|
97
|
+
{cutOffType === "AllDates" && (
|
|
98
|
+
<Textfield
|
|
99
|
+
name="period"
|
|
100
|
+
aria-label={getLocalizedString("Day")}
|
|
101
|
+
type="date"
|
|
102
|
+
value={periods.AllDates}
|
|
103
|
+
onChange={handlePeriodChanged}
|
|
104
|
+
disabled={disabled}
|
|
105
|
+
max={max ? plainDateToCutOff(max, "AllDates") : undefined}
|
|
106
|
+
min={min ? plainDateToCutOff(min, "AllDates") : undefined}
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
{cutOffType === "WS" && (
|
|
110
|
+
<Textfield
|
|
111
|
+
name="period"
|
|
112
|
+
aria-label={getLocalizedString("Week")}
|
|
113
|
+
type="week"
|
|
114
|
+
value={periods.WS.indexOf("W") === -1 ? periods.WS.replace("-", "-W") : periods.WS}
|
|
115
|
+
onChange={handlePeriodChanged}
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
max={max ? plainDateToCutOff(max, "WS") : undefined}
|
|
118
|
+
min={min ? plainDateToCutOff(min, "WS") : undefined}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
{(cutOffType === "MLD" || cutOffType === "MLS") && (
|
|
122
|
+
<Textfield
|
|
123
|
+
name="period"
|
|
124
|
+
aria-label={getLocalizedString("Month")}
|
|
125
|
+
type="month"
|
|
126
|
+
value={periods.MLD}
|
|
127
|
+
onChange={handlePeriodChanged}
|
|
128
|
+
disabled={disabled}
|
|
129
|
+
max={max ? plainDateToCutOff(max, "MLD") : undefined}
|
|
130
|
+
min={min ? plainDateToCutOff(min, "MLD") : undefined}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
{(cutOffType === "QLD" || cutOffType === "QLS") && (
|
|
134
|
+
<Select
|
|
135
|
+
name="period"
|
|
136
|
+
aria-label={getLocalizedString("Quarter")}
|
|
137
|
+
value={periods.QLD}
|
|
138
|
+
onChange={handlePeriodChanged}
|
|
139
|
+
disabled={disabled}
|
|
140
|
+
>
|
|
141
|
+
{quarters.map((quarter) => (
|
|
142
|
+
<Select.Option key={quarter} value={quarter}>
|
|
143
|
+
{quarter}
|
|
144
|
+
</Select.Option>
|
|
145
|
+
))}
|
|
146
|
+
</Select>
|
|
147
|
+
)}
|
|
148
|
+
{cutOffType === "YLD" && (
|
|
149
|
+
<Select
|
|
150
|
+
name="period"
|
|
151
|
+
aria-label={getLocalizedString("Year")}
|
|
152
|
+
value={periods.YLD}
|
|
153
|
+
onChange={handlePeriodChanged}
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
>
|
|
156
|
+
{years.map((year) => (
|
|
157
|
+
<Select.Option key={year} value={String(year)}>
|
|
158
|
+
{year}
|
|
159
|
+
</Select.Option>
|
|
160
|
+
))}
|
|
161
|
+
</Select>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|