@olenbetong/appframe-ds 0.8.0 → 0.9.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/package.json +50 -6
  3. package/scripts/copyAssets.mjs +13 -0
  4. package/src/AfLookup/LookupCombobox.tsx +3 -1
  5. package/src/filter/ChipListInput.tsx +79 -0
  6. package/src/filter/FieldFilterPanel.css +127 -0
  7. package/src/filter/FieldFilterPanel.tsx +309 -0
  8. package/src/filter/FilterBuilder.css +152 -0
  9. package/src/filter/FilterBuilder.test.tsx +153 -0
  10. package/src/filter/FilterBuilder.tsx +435 -0
  11. package/src/filter/FilterEditor.css +231 -0
  12. package/src/filter/FilterEditor.test.tsx +259 -0
  13. package/src/filter/FilterEditor.tsx +74 -0
  14. package/src/filter/FilterGroupEditor.tsx +160 -0
  15. package/src/filter/FilterNameDialog.css +22 -0
  16. package/src/filter/FilterNameDialog.tsx +81 -0
  17. package/src/filter/FilterRow.tsx +117 -0
  18. package/src/filter/FilterShareDialog.css +28 -0
  19. package/src/filter/FilterShareDialog.tsx +201 -0
  20. package/src/filter/FilterStringField.tsx +77 -0
  21. package/src/filter/FilterValueEditor.tsx +220 -0
  22. package/src/filter/SavedFilterTree.css +107 -0
  23. package/src/filter/SavedFilterTree.test.tsx +94 -0
  24. package/src/filter/SavedFilterTree.tsx +222 -0
  25. package/src/filter/fieldFilter.test.ts +158 -0
  26. package/src/filter/fieldFilter.ts +139 -0
  27. package/src/filter/index.ts +27 -0
  28. package/src/filter/types.ts +38 -0
  29. package/src/filter/useDistinctValues.test.ts +102 -0
  30. package/src/filter/useDistinctValues.ts +230 -0
  31. package/src/global.d.ts +11 -0
  32. package/src/grid/AfGridColumnsPanel.tsx +118 -0
  33. package/src/grid/AfGridContext.tsx +51 -0
  34. package/src/grid/AfGridError.tsx +46 -0
  35. package/src/grid/AfHeaderFilterCell.tsx +151 -0
  36. package/src/grid/AfHeaderFilterPanel.tsx +111 -0
  37. package/src/grid/Toolbar.tsx +233 -0
  38. package/src/grid/editing.ts +90 -0
  39. package/src/grid/filter.ts +78 -0
  40. package/src/grid/formatters.ts +48 -0
  41. package/src/grid/index.css +187 -0
  42. package/src/grid/index.tsx +376 -0
  43. package/src/grid/license.ts +48 -0
  44. package/src/grid/localization.ts +369 -0
  45. package/src/grid/slots/index.tsx +275 -0
  46. package/src/grid/slots/slots.css +53 -0
  47. package/src/grid/theme.tsx +185 -0
  48. package/src/grid/useAfColumns.tsx +313 -0
  49. package/src/grid/useAfCurrentIndex.ts +55 -0
  50. package/src/grid/useAfData.ts +20 -0
  51. package/src/grid/useAfFilter.ts +160 -0
  52. package/src/grid/useAfFilterFields.ts +55 -0
  53. package/src/grid/useAfGridApi.ts +66 -0
  54. package/src/grid/useAfKeyBindings.ts +36 -0
  55. package/src/grid/useAfPagination.ts +53 -0
  56. package/src/grid/useAfPersistedState.ts +222 -0
  57. package/src/grid/useAfRowEditModel.ts +137 -0
  58. package/src/grid/useAfSortModel.ts +72 -0
  59. package/src/index.ts +1 -0
  60. package/src/input/InputAdornments.tsx +6 -1
  61. package/src/report-downloader/ReportDownloader.css +67 -0
  62. package/src/report-downloader/components/StatusItem.tsx +86 -0
  63. package/src/report-downloader/components/StatusList.tsx +28 -0
  64. package/src/report-downloader/index.ts +41 -0
  65. package/src/report-downloader/status.tsx +92 -0
  66. package/src/test/setup.ts +8 -0
  67. package/vitest.config.ts +10 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,75 @@
1
1
  # @olenbetong/appframe-ds
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cfc94c7: Rework the `AfGrid` header filter, and stop the toolbar refresh button from shifting while loading.
8
+
9
+ The header filters and the toolbar's filter builder now edit **one** filter tree in a single named
10
+ filter slot keyed on the grid `id`, instead of writing two slots that could not see each other. MUI's
11
+ `GridFilterModel` is no longer used as a transport — it cannot represent nested groups or the full
12
+ Appframe operator set. `useAfFilterModel` is replaced by `useAfFilter`, which exposes the tree plus
13
+ per-field helpers.
14
+
15
+ Each header filter cell is now an unlabelled input with a funnel button:
16
+ - one criterion taking a single value is edited straight from the input, debounced,
17
+ - anything else shows a read-only summary such as `In list (3)`, with a clear button,
18
+ - the funnel opens a per-column panel with a row per criterion, a `+ Criteria` button, and — for
19
+ string, number and uniqueidentifier columns — a searchable list of the column's distinct values
20
+ with row counts, checked to build an `in list` criterion.
21
+
22
+ The distinct list respects every other active filter but ignores the column's own criteria, so
23
+ unchecking a value does not make the remaining values disappear. Counts are best effort: views that
24
+ cannot be grouped fall back to a plain distinct list. Panel edits are staged and only applied on
25
+ `Apply`, so a half-built criterion never reaches the server.
26
+
27
+ `GridToolbarRefreshButton` now hides its icon while loading so the spinner replaces it instead of
28
+ being added next to it, and the columns panel has spacing between its search field and the column
29
+ list. `InputAdornments` accepts a `ref`, which is forwarded to the input element.
30
+
31
+ The panel body is exported on its own as **`FieldFilterPanel`**, a controlled component with no
32
+ dependency on the grid, so a single field can be filtered anywhere — a report parameter, a sidebar
33
+ facet, a mobile filter sheet. Its distinct value query is exported too, as `useDistinctValues`.
34
+
35
+ The header filter input no longer drives MUI's header filter edit mode from `onFocus`/`onBlur`. The
36
+ header filter cell refocuses itself whenever that state changes, so any mousedown leaving the input
37
+ pulled focus straight back in — inputs in the filter panel could not be focused, and starting a
38
+ column drag from a header cancelled the drag. Nothing is lost: MUI focuses the cell's focusable
39
+ element anyway, so typing and keyboard navigation still work.
40
+
41
+ - cfc94c7: Add `@olenbetong/appframe-ds/grid`, a Designsystemet flavoured `AfGrid`.
42
+
43
+ The grid keeps MUI X Data Grid Pro as its engine — Designsystemet has no data grid — but replaces
44
+ the chrome:
45
+ - the base slots (checkbox, button, icon button, text field, select, switch, tooltip, chip, badge,
46
+ progress, skeleton, divider) render Designsystemet components,
47
+ - the toolbar and the column panel are rebuilt with Designsystemet components and Aksel icons,
48
+ - the filter panel is replaced by `FilterBuilder` in dialog mode,
49
+ - the MUI theme is derived from the `--ds-color-*` tokens and follows `data-color-scheme`, so the
50
+ grid works in light and dark mode without an app-level `ThemeProvider`,
51
+ - the MUI X license is registered by the grid itself, from a build-time injected
52
+ `__MUI_X_LICENSE_KEY__` (supplied by the `appframe()` Vite plugin) or
53
+ `import.meta.env.VITE_MUI_X_LICENSE_KEY`.
54
+
55
+ The API matches the `@olenbetong/appframe-mui` `AfGrid`, so migrating is an import path change plus
56
+ an optional `filterViewName` for saved filters. Layout persistence uses the same store, so users
57
+ keep their saved column layouts.
58
+
59
+ The filter builder (`FilterBuilder`, `FilterEditor`, `SavedFilterTree`, …) moved here from
60
+ `@olenbetong/synergi-react` and is exported from the package root.
61
+
62
+ `ReportDownloader` (`openDataObjectReportWithProgress`) is available from
63
+ `@olenbetong/appframe-ds/report-downloader`, built with Designsystemet components.
64
+
65
+ ### Patch Changes
66
+
67
+ - Updated dependencies [62a0ad5]
68
+ - Updated dependencies [5155cdb]
69
+ - @olenbetong/appframe-data@1.6.1
70
+ - @olenbetong/appframe-core@2.13.0
71
+ - @olenbetong/appframe-react@2.1.1
72
+
3
73
  ## 0.8.0
4
74
 
5
75
  ### Minor Changes
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-ds",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Components that bind Designsystemet components to Appframe data objects",
5
5
  "type": "module",
6
6
  "types": "./es/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
+ "types": "./es/index.d.ts",
9
10
  "import": "./es/index.js"
11
+ },
12
+ "./grid": {
13
+ "types": "./es/grid/index.d.ts",
14
+ "import": "./es/grid/index.js"
15
+ },
16
+ "./report-downloader": {
17
+ "types": "./es/report-downloader/index.d.ts",
18
+ "import": "./es/report-downloader/index.js"
10
19
  }
11
20
  },
12
21
  "sideEffects": [
@@ -16,29 +25,63 @@
16
25
  "license": "MIT",
17
26
  "peerDependencies": {
18
27
  "@digdir/designsystemet-react": ">=1.18.0",
28
+ "@emotion/react": ">=11.14.0",
29
+ "@emotion/styled": ">=11.14.1",
30
+ "@mui/material": ">=9.0.0",
31
+ "@mui/x-data-grid-pro": ">=8.0.0 <9.0.0",
32
+ "@mui/x-license": ">=8.0.0 <9.0.0",
19
33
  "@navikt/aksel-icons": ">=8.16.1",
20
34
  "react": ">=19.2.8",
21
35
  "react-dom": ">=19.2.8",
22
36
  "react-error-boundary": ">=6.1.2",
23
37
  "react-router": ">=8.3.0"
24
38
  },
39
+ "peerDependenciesMeta": {
40
+ "@emotion/react": {
41
+ "optional": true
42
+ },
43
+ "@emotion/styled": {
44
+ "optional": true
45
+ },
46
+ "@mui/material": {
47
+ "optional": true
48
+ },
49
+ "@mui/x-data-grid-pro": {
50
+ "optional": true
51
+ },
52
+ "@mui/x-license": {
53
+ "optional": true
54
+ }
55
+ },
25
56
  "dependencies": {
26
57
  "@atlaskit/pragmatic-drag-and-drop": "2.0.2",
27
58
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "2.0.0",
28
59
  "@base-ui/react": "1.7.0",
29
60
  "clsx": "^2.1.1",
61
+ "localforage": "^1.10.0",
62
+ "mitt": "^3.0.1",
30
63
  "react-virtualized-auto-sizer": "^1.0.26",
31
64
  "react-window": "^1.8.11",
32
- "@olenbetong/appframe-data": "1.6.0",
33
- "@olenbetong/appframe-core": "2.12.0",
34
- "@olenbetong/appframe-react": "2.1.0"
65
+ "@olenbetong/appframe-core": "2.13.0",
66
+ "@olenbetong/appframe-react": "2.1.1",
67
+ "@olenbetong/appframe-data": "1.6.1"
35
68
  },
36
69
  "devDependencies": {
37
70
  "@digdir/designsystemet-react": "1.18.0",
71
+ "@emotion/react": "^11.14.0",
72
+ "@emotion/styled": "^11.14.1",
73
+ "@mui/material": "9.3.1",
74
+ "@mui/x-data-grid-pro": "8.29.2",
75
+ "@mui/x-license": "8.29.2",
38
76
  "@navikt/aksel-icons": "8.16.1",
77
+ "@testing-library/dom": "^10.4.1",
78
+ "@testing-library/jest-dom": "^7.0.1",
79
+ "@testing-library/react": "16.3.2",
80
+ "@testing-library/user-event": "^14.6.3",
39
81
  "@types/react": "19.2.18",
40
82
  "@types/react-dom": "19.2.4",
41
83
  "@types/react-window": "^1.8.8",
84
+ "happy-dom": "^20.11.1",
42
85
  "react": "19.2.8",
43
86
  "react-dom": "19.2.8",
44
87
  "react-error-boundary": "6.1.2",
@@ -46,8 +89,9 @@
46
89
  "typescript": "7.0.2"
47
90
  },
48
91
  "scripts": {
49
- "build": "pnpm run build:tsc && pnpm run build:css",
92
+ "build": "pnpm run build:tsc && pnpm test && pnpm run build:css",
50
93
  "build:tsc": "node --no-warnings ../../scripts/clean.ts ./es tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
51
- "build:css": "node scripts/copyAssets.mjs"
94
+ "build:css": "node scripts/copyAssets.mjs",
95
+ "test": "vitest run"
52
96
  }
53
97
  }
@@ -22,6 +22,15 @@ const files = [
22
22
  ["src/sortable/DropIndicator.css", "es/sortable/DropIndicator.css"],
23
23
  ["src/sortable/SortableItemList.css", "es/sortable/SortableItemList.css"],
24
24
  ["src/autocomplete/Autocomplete.css", "es/autocomplete/Autocomplete.css"],
25
+ ["src/filter/FilterBuilder.css", "es/filter/FilterBuilder.css"],
26
+ ["src/filter/FieldFilterPanel.css", "es/filter/FieldFilterPanel.css"],
27
+ ["src/filter/FilterEditor.css", "es/filter/FilterEditor.css"],
28
+ ["src/filter/FilterNameDialog.css", "es/filter/FilterNameDialog.css"],
29
+ ["src/filter/FilterShareDialog.css", "es/filter/FilterShareDialog.css"],
30
+ ["src/filter/SavedFilterTree.css", "es/filter/SavedFilterTree.css"],
31
+ ["src/grid/index.css", "es/grid/index.css"],
32
+ ["src/grid/slots/slots.css", "es/grid/slots/slots.css"],
33
+ ["src/report-downloader/ReportDownloader.css", "es/report-downloader/ReportDownloader.css"],
25
34
  ];
26
35
 
27
36
  mkdirSync("es/layout", { recursive: true });
@@ -32,6 +41,10 @@ mkdirSync("es/paper", { recursive: true });
32
41
  mkdirSync("es/progress", { recursive: true });
33
42
  mkdirSync("es/sortable", { recursive: true });
34
43
  mkdirSync("es/autocomplete", { recursive: true });
44
+ mkdirSync("es/filter", { recursive: true });
45
+ mkdirSync("es/grid", { recursive: true });
46
+ mkdirSync("es/grid/slots", { recursive: true });
47
+ mkdirSync("es/report-downloader", { recursive: true });
35
48
 
36
49
  for (let [src, dest] of files) {
37
50
  cpSync(src, dest);
@@ -39,7 +39,9 @@ export interface LookupComboboxGridProps<T extends Record<string, unknown>> exte
39
39
  columns: LookupColumn<T>[];
40
40
  }
41
41
 
42
- export type LookupComboboxProps<T extends Record<string, unknown>> = LookupComboboxListProps<T> | LookupComboboxGridProps<T>;
42
+ export type LookupComboboxProps<T extends Record<string, unknown>> =
43
+ | LookupComboboxListProps<T>
44
+ | LookupComboboxGridProps<T>;
43
45
 
44
46
  export function LookupCombobox<T extends Record<string, unknown>>(props: LookupComboboxProps<T>) {
45
47
  let {
@@ -0,0 +1,79 @@
1
+ import { ChipRemovable, Textfield } from "@digdir/designsystemet-react";
2
+ import { getLocalizedString } from "@olenbetong/appframe-core";
3
+ import { useState } from "react";
4
+
5
+ export type ChipListInputProps = {
6
+ value: string[];
7
+ onChange: (value: string[]) => void;
8
+ readOnly?: boolean;
9
+ "aria-label": string;
10
+ "data-size"?: "sm" | "md" | "lg";
11
+ type?: "text" | "number" | "date" | "datetime-local";
12
+ };
13
+
14
+ /**
15
+ * Free text editor for the list operators. Values are added with Enter and
16
+ * rendered as removable chips.
17
+ */
18
+ export function ChipListInput({
19
+ value,
20
+ onChange,
21
+ readOnly,
22
+ type = "text",
23
+ "aria-label": ariaLabel,
24
+ "data-size": size,
25
+ }: ChipListInputProps) {
26
+ let [draft, setDraft] = useState("");
27
+
28
+ function commit() {
29
+ let trimmed = draft.trim();
30
+ if (!trimmed || value.includes(trimmed)) {
31
+ setDraft("");
32
+ return;
33
+ }
34
+
35
+ onChange([...value, trimmed]);
36
+ setDraft("");
37
+ }
38
+
39
+ function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
40
+ if (event.key === "Enter") {
41
+ // The editor is often rendered inside a dialog; committing a value
42
+ // must not submit or close it.
43
+ event.preventDefault();
44
+ commit();
45
+ } else if (event.key === "Backspace" && draft === "" && value.length > 0) {
46
+ onChange(value.slice(0, -1));
47
+ }
48
+ }
49
+
50
+ return (
51
+ <div className="ObFilterEditor-chipList">
52
+ {value.map((item, index) => (
53
+ <ChipRemovable
54
+ // Values are deduplicated on add, so the value is a stable key.
55
+ key={item}
56
+ data-size={size}
57
+ disabled={readOnly}
58
+ aria-label={`${getLocalizedString("Remove")} ${item}`}
59
+ onClick={() => onChange(value.filter((_, i) => i !== index))}
60
+ >
61
+ {item}
62
+ </ChipRemovable>
63
+ ))}
64
+ {!readOnly && (
65
+ <Textfield
66
+ className="ObFilterEditor-chipListInput"
67
+ aria-label={ariaLabel}
68
+ data-size={size}
69
+ type={type}
70
+ value={draft}
71
+ placeholder={getLocalizedString("Add value")}
72
+ onChange={(event) => setDraft(event.target.value)}
73
+ onKeyDown={handleKeyDown}
74
+ onBlur={commit}
75
+ />
76
+ )}
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,127 @@
1
+ .ObFieldFilterPanel-root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--ds-size-3, 0.75rem);
5
+
6
+ /*
7
+ * The panel is sized by whatever it is put in — a popover, a dialog, a
8
+ * sidebar. It fills that container and lets the distinct list take up the
9
+ * slack, so the list grows and shrinks with the container instead of
10
+ * carrying a height of its own.
11
+ */
12
+ block-size: 100%;
13
+ min-block-size: 0;
14
+ }
15
+
16
+ .ObFieldFilterPanel-heading {
17
+ font-weight: var(--ds-font-weight-medium, 500);
18
+ color: var(--ds-color-neutral-text-default);
19
+ }
20
+
21
+ .ObFieldFilterPanel-criteria {
22
+ display: flex;
23
+ flex-direction: column;
24
+ flex: 0 0 auto;
25
+ gap: var(--ds-size-2, 0.5rem);
26
+ }
27
+
28
+ .ObFieldFilterPanel-criterion {
29
+ display: flex;
30
+ flex-direction: column;
31
+ gap: var(--ds-size-1, 0.25rem);
32
+ }
33
+
34
+ /*
35
+ * The filter editor's value cell is written for the filter builder's horizontal
36
+ * rows, where growing is what makes it fill the row. Stacked vertically that
37
+ * same `flex-grow` makes it stretch to fill the whole panel instead.
38
+ */
39
+ .ObFieldFilterPanel-criterion > .ObFilterEditor-valueCell {
40
+ flex: 0 0 auto;
41
+ }
42
+
43
+ .ObFieldFilterPanel-distinct {
44
+ display: flex;
45
+ flex-direction: column;
46
+ flex: 1 1 auto;
47
+ gap: var(--ds-size-2, 0.5rem);
48
+ min-block-size: 0;
49
+ padding-block-start: var(--ds-size-2, 0.5rem);
50
+ border-block-start: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-subtle, #e7e7e7);
51
+ }
52
+
53
+ .ObFieldFilterPanel-distinctToolbar {
54
+ display: flex;
55
+ flex: 0 0 auto;
56
+ gap: var(--ds-size-1, 0.25rem);
57
+ align-items: center;
58
+ }
59
+
60
+ .ObFieldFilterPanel-distinctToolbar .ds-search {
61
+ flex: 1;
62
+ min-inline-size: 0;
63
+ }
64
+
65
+ /*
66
+ * The active sort is held down, so it is clear which of the two is in effect.
67
+ * Neutral tokens rather than accent ones: the accent scale is only defined
68
+ * inside a `data-color` scope, and the panel can be dropped anywhere.
69
+ */
70
+ .ObFieldFilterPanel-distinctToolbar .ds-button[aria-pressed="true"] {
71
+ color: var(--ds-color-neutral-text-default, #292c30);
72
+ background: var(--ds-color-neutral-surface-active, #c9cacb);
73
+ box-shadow: inset 0 0 0 var(--ds-border-width-default, 1px) var(--ds-color-neutral-border-subtle, #e7e7e7);
74
+ }
75
+
76
+ /*
77
+ * Both the list and the spinner that replaces it while loading use this box, so
78
+ * the panel does not jump when the values arrive. It is the part that takes up
79
+ * whatever room the container has left, and it never collapses to nothing.
80
+ */
81
+ .ObFieldFilterPanel-valuesBox {
82
+ flex: 1 1 auto;
83
+ min-block-size: 8rem;
84
+ border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-subtle, #e7e7e7);
85
+ border-radius: var(--ds-border-radius-md, 0.25rem);
86
+ background: var(--ds-color-neutral-surface-tinted, rgb(0 0 0 / 2%));
87
+ }
88
+
89
+ .ObFieldFilterPanel-values {
90
+ display: flex;
91
+ flex-direction: column;
92
+ gap: var(--ds-size-1, 0.25rem);
93
+ margin: 0;
94
+ padding: var(--ds-size-1, 0.25rem);
95
+ overflow-y: auto;
96
+ list-style: none;
97
+ }
98
+
99
+ .ObFieldFilterPanel-value {
100
+ padding-inline: var(--ds-size-1, 0.25rem);
101
+ }
102
+
103
+ .ObFieldFilterPanel-loading {
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ padding: var(--ds-size-4, 1rem);
108
+ }
109
+
110
+ .ObFieldFilterPanel-error {
111
+ margin: 0;
112
+ color: var(--ds-color-danger-text-default);
113
+ }
114
+
115
+ .ObFieldFilterPanel-hint {
116
+ margin: 0;
117
+ flex: 0 0 auto;
118
+ font-size: var(--ds-font-size-1, 0.75rem);
119
+ color: var(--ds-color-neutral-text-subtle, #68707c);
120
+ }
121
+
122
+ .ObFieldFilterPanel-actions {
123
+ display: flex;
124
+ flex: 0 0 auto;
125
+ gap: var(--ds-size-2, 0.5rem);
126
+ justify-content: flex-end;
127
+ }