@olenbetong/appframe-ds 1.3.1 → 1.3.4

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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # @olenbetong/appframe-ds
2
2
 
3
+ ## 1.3.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 9c2ba29: The data grid no longer freezes the page for seconds when it loads. Its loading overlay mounts one skeleton per visible cell, and Designsystemet's `Skeleton` calls `document.getAnimations()` on mount to synchronize its animation, so hundreds of skeletons scanned hundreds of animations each. The grid's skeleton slot now renders the same markup without that hook.
8
+
9
+ ## 1.3.3
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [04ff05a]
14
+ - @olenbetong/appframe-core@2.15.1
15
+ - @olenbetong/appframe-data@1.7.4
16
+ - @olenbetong/appframe-react@2.2.4
17
+
18
+ ## 1.3.2
19
+
20
+ ### Patch Changes
21
+
22
+ - 48e6ac8: The filter builder adds the seconds to datetime values, which the datetime input leaves out. SQL Server rejected the value without them with "Conversion failed when converting date and/or time from character string".
23
+ - Updated dependencies [a63831a]
24
+ - @olenbetong/appframe-core@2.15.0
25
+ - @olenbetong/appframe-react@2.2.3
26
+ - @olenbetong/appframe-data@1.7.3
27
+
3
28
  ## 1.3.1
4
29
 
5
30
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-ds",
3
- "version": "1.3.1",
3
+ "version": "1.3.4",
4
4
  "description": "Components that bind Designsystemet components to Appframe data objects",
5
5
  "type": "module",
6
6
  "types": "./es/index.d.ts",
@@ -63,9 +63,9 @@
63
63
  "@atlaskit/pragmatic-drag-and-drop": "3.1.0",
64
64
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "2.2.2",
65
65
  "@base-ui/react": "1.8.0",
66
- "@olenbetong/appframe-core": "2.14.0",
67
- "@olenbetong/appframe-data": "1.7.2",
68
- "@olenbetong/appframe-react": "2.2.2",
66
+ "@olenbetong/appframe-core": "2.15.1",
67
+ "@olenbetong/appframe-data": "1.7.4",
68
+ "@olenbetong/appframe-react": "2.2.4",
69
69
  "clsx": "^2.1.1",
70
70
  "dockview-react": "8.3.1",
71
71
  "localforage": "^1.10.0",
@@ -76,13 +76,13 @@
76
76
  "temporal-polyfill": "1.0.5"
77
77
  },
78
78
  "devDependencies": {
79
- "@digdir/designsystemet-react": "1.22.0",
79
+ "@digdir/designsystemet-react": "1.23.0",
80
80
  "@emotion/react": "^11.14.0",
81
81
  "@emotion/styled": "^11.14.1",
82
82
  "@mui/material": "9.4.0",
83
83
  "@mui/x-data-grid-premium": "8.29.3",
84
84
  "@mui/x-license": "8.29.3",
85
- "@navikt/aksel-icons": "8.16.2",
85
+ "@navikt/aksel-icons": "8.17.2",
86
86
  "@testing-library/dom": "^10.4.2",
87
87
  "@testing-library/jest-dom": "^7.0.1",
88
88
  "@testing-library/react": "16.3.3",
@@ -5,7 +5,7 @@ import { MagnifyingGlassIcon, XMarkIcon } from "@navikt/aksel-icons";
5
5
  import { type DistributiveOmit, useLookupState } from "@olenbetong/appframe-react";
6
6
 
7
7
  import clsx from "clsx";
8
- import { useRef } from "react";
8
+ import { useState } from "react";
9
9
  import { createPortal } from "react-dom";
10
10
 
11
11
  import { LookupCombobox, type LookupComboboxProps } from "./LookupCombobox.js";
@@ -63,7 +63,7 @@ export function useLookup<T extends Record<string, unknown>>({
63
63
  getAnchorElement,
64
64
  scrollParentSelector: "[data-virtualized-scroller]",
65
65
  });
66
- let initialSearchValueRef = useRef<string | undefined>(undefined);
66
+ let [initialSearchValue, setInitialSearchValue] = useState<string | undefined>(undefined);
67
67
 
68
68
  function handleChange(item: T | null) {
69
69
  props.onChange?.(item);
@@ -71,7 +71,9 @@ export function useLookup<T extends Record<string, unknown>>({
71
71
  }
72
72
 
73
73
  function handleOpenDialog(searchValue?: string) {
74
- initialSearchValueRef.current = searchValue;
74
+ // openDialog renders synchronously (flushSync), which includes this update, so the combobox
75
+ // mounts with the search value already set.
76
+ setInitialSearchValue(searchValue);
75
77
  openDialog();
76
78
  }
77
79
 
@@ -93,7 +95,7 @@ export function useLookup<T extends Record<string, unknown>>({
93
95
  {open && (
94
96
  <LookupCombobox
95
97
  {...props}
96
- initialSearchValue={initialSearchValueRef.current}
98
+ initialSearchValue={initialSearchValue}
97
99
  onClose={() => dialogRef.current?.close()}
98
100
  onChange={handleChange}
99
101
  />
@@ -1,5 +1,5 @@
1
1
  import type { FieldMetadata, FilterExpression, FilterTree } from "@olenbetong/appframe-core";
2
- import { render, screen } from "@testing-library/react";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
3
  import userEvent from "@testing-library/user-event";
4
4
  import { describe, expect, it, vi } from "vitest";
5
5
 
@@ -131,6 +131,32 @@ describe("FilterEditor", () => {
131
131
  expect(firstExpression(onChange)).toMatchObject({ value: "Yesterday", valueType: "special" });
132
132
  });
133
133
 
134
+ it("adds the seconds a datetime input leaves out", () => {
135
+ let onChange = vi.fn();
136
+ render(
137
+ <FilterEditor fields={FIELDS} value={filterWith("Created", "equals", "", "datetime")} onChange={onChange} />,
138
+ );
139
+
140
+ fireEvent.change(screen.getByLabelText("Value"), { target: { value: "2026-09-25T00:00" } });
141
+
142
+ expect(firstExpression(onChange)).toMatchObject({ value: "2026-09-25T00:00:00" });
143
+ });
144
+
145
+ it("adds the seconds to both ends of a datetime range", () => {
146
+ let onChange = vi.fn();
147
+ render(
148
+ <FilterEditor
149
+ fields={FIELDS}
150
+ value={filterWith("Created", "between", ["2026-09-01T00:00:00", ""], "datetime")}
151
+ onChange={onChange}
152
+ />,
153
+ );
154
+
155
+ fireEvent.change(screen.getByLabelText("To"), { target: { value: "2026-09-30T23:59" } });
156
+
157
+ expect(firstExpression(onChange)).toMatchObject({ value: ["2026-09-01T00:00:00", "2026-09-30T23:59:00"] });
158
+ });
159
+
134
160
  it("does not offer relative values for numbers", () => {
135
161
  render(<FilterEditor fields={FIELDS} value={filterWith("Age", "equals", "1", "number")} />);
136
162
 
@@ -47,6 +47,25 @@ function toStringValue(value: FilterExpression["value"]): string {
47
47
  return String(value);
48
48
  }
49
49
 
50
+ /**
51
+ * A `datetime-local` input reports `2026-09-25T00:00`, leaving out the seconds
52
+ * when they are zero. The value reaches SQL Server as a string, and SQL Server
53
+ * only accepts the ISO 8601 form with a `T` when the seconds are present, so
54
+ * the filter fails with "Conversion failed when converting date and/or time
55
+ * from character string" unless they are added.
56
+ */
57
+ function withSeconds(value: string): string {
58
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value) ? `${value}:00` : value;
59
+ }
60
+
61
+ function toFilterValue(value: FilterExpression["value"], inputType: string): FilterExpression["value"] {
62
+ if (inputType !== "datetime-local") return value;
63
+ if (typeof value === "string") return withSeconds(value);
64
+ if (Array.isArray(value)) return value.map((item) => (typeof item === "string" ? withSeconds(item) : item));
65
+
66
+ return value;
67
+ }
68
+
50
69
  function toArrayValue(value: FilterExpression["value"], length?: number): string[] {
51
70
  let array = Array.isArray(value) ? value.map(toStringValue) : [];
52
71
  if (length === undefined) return array;
@@ -75,7 +94,7 @@ export function FilterValueEditor({
75
94
  let inputType = getInputType(field);
76
95
 
77
96
  function setValue(value: FilterExpression["value"]) {
78
- onChange({ ...expression, value });
97
+ onChange({ ...expression, value: isDynamic ? value : toFilterValue(value, inputType) });
79
98
  }
80
99
 
81
100
  function setDynamic(enabled: boolean) {
@@ -6,7 +6,6 @@ import {
6
6
  Checkbox,
7
7
  Divider,
8
8
  Select,
9
- Skeleton,
10
9
  Spinner,
11
10
  Switch,
12
11
  Tag,
@@ -272,11 +271,39 @@ function BaseLinearProgress(props: GridSlotProps["baseLinearProgress"]) {
272
271
  return <div {...(other as any)} role="progressbar" className={clsx("AfGrid-linearProgress", className)} />;
273
272
  }
274
273
 
274
+ /**
275
+ * Renders Designsystemet's skeleton markup rather than its `Skeleton`
276
+ * component. The component synchronizes its animation with every other
277
+ * skeleton by calling `document.getAnimations()` when it mounts, and the grid's
278
+ * loading overlay mounts one per visible cell in the same commit - hundreds of
279
+ * scans over hundreds of animations, which froze the page for seconds on every
280
+ * load. Skeletons mounted together start their animation together anyway.
281
+ */
275
282
  function BaseSkeleton(props: GridSlotProps["baseSkeleton"]) {
276
- let { variant, width, height, ...other } = props;
283
+ let { variant, width, height, className, style, ...other } = props;
284
+ let dsVariant = variant === "circular" ? "circle" : variant;
285
+
286
+ if (dsVariant === "text") {
287
+ return (
288
+ <span
289
+ {...(other as any)}
290
+ aria-hidden="true"
291
+ className={clsx("ds-skeleton", className)}
292
+ data-text={"-".repeat(Number(width) || 1)}
293
+ data-variant="text"
294
+ style={style}
295
+ />
296
+ );
297
+ }
277
298
 
278
299
  return (
279
- <Skeleton {...(other as any)} variant={variant === "circular" ? "circle" : variant} width={width} height={height} />
300
+ <span
301
+ {...(other as any)}
302
+ aria-hidden="true"
303
+ className={clsx("ds-skeleton", className)}
304
+ data-variant={dsVariant}
305
+ style={{ width, height, ...style }}
306
+ />
280
307
  );
281
308
  }
282
309
 
@@ -0,0 +1,39 @@
1
+ import { render } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { dsBaseSlots } from "./index.js";
5
+
6
+ const BaseSkeleton = dsBaseSlots.baseSkeleton as React.ComponentType<Record<string, unknown>>;
7
+
8
+ describe("baseSkeleton", () => {
9
+ afterEach(() => {
10
+ vi.restoreAllMocks();
11
+ });
12
+
13
+ it("renders Designsystemet's skeleton markup", () => {
14
+ let { container } = render(<BaseSkeleton variant="rectangular" width={80} height={12} />);
15
+ let skeleton = container.firstElementChild as HTMLElement;
16
+
17
+ expect(skeleton).toHaveClass("ds-skeleton");
18
+ expect(skeleton).toHaveAttribute("data-variant", "rectangular");
19
+ expect(skeleton).toHaveAttribute("aria-hidden", "true");
20
+ expect(skeleton.style.width).toBe("80px");
21
+ });
22
+
23
+ it("does not scan the page's animations when many mount at once", () => {
24
+ // The loading overlay mounts one skeleton per visible cell. Designsystemet's
25
+ // Skeleton calls document.getAnimations() for each, which froze the page.
26
+ let getAnimations = vi.fn(() => []);
27
+ Object.defineProperty(document, "getAnimations", { value: getAnimations, configurable: true });
28
+
29
+ render(
30
+ <>
31
+ {Array.from({ length: 200 }, (_, index) => (
32
+ <BaseSkeleton key={index} variant="text" width={10} />
33
+ ))}
34
+ </>,
35
+ );
36
+
37
+ expect(getAnimations).not.toHaveBeenCalled();
38
+ });
39
+ });