@olenbetong/appframe-ds 1.8.0 → 1.9.1
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 +14 -0
- package/package.json +2 -2
- package/src/AfLookup/Lookup.css +16 -1
- package/src/AfLookup/Lookup.tsx +37 -12
- package/src/AfLookup/LookupCombobox.tsx +1 -9
- package/src/AfLookup/LookupEdit.tsx +5 -4
- package/src/AfLookup/LookupGrid.css +3 -2
- package/src/AfLookup/LookupGrid.tsx +8 -4
- package/src/autocomplete/Autocomplete.css +6 -17
- package/src/binding/BoundNumberInput.tsx +42 -42
- 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.css +23 -23
- package/src/binding/DataEditToolbar.tsx +84 -84
- 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/binding/index.ts +20 -20
- package/src/container/Container.tsx +50 -50
- 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 +303 -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 +285 -285
- 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 +195 -201
- package/src/filter/FilterStringField.tsx +72 -78
- package/src/filter/FilterValueEditor.tsx +239 -239
- 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 +4 -7
- package/src/grid/AfGridError.tsx +2 -2
- package/src/grid/AfHeaderFilterCell.tsx +0 -3
- package/src/grid/AfHeaderFilterPanel.tsx +1 -3
- package/src/grid/GridEditLookup.tsx +3 -4
- package/src/grid/Toolbar.tsx +9 -38
- package/src/grid/index.css +44 -0
- package/src/grid/index.tsx +12 -3
- package/src/grid/slots/index.tsx +15 -40
- package/src/grid/useAfGridApi.ts +0 -1
- package/src/grid/useAfPersistedState.ts +4 -8
- package/src/grid/useAfSizeMode.ts +19 -0
- package/src/i18n.ts +13 -13
- package/src/index.ts +14 -14
- package/src/input/NumberInput.tsx +0 -2
- package/src/layout/AppMenu.tsx +74 -74
- package/src/layout/AppMenuDrawer.css +71 -71
- package/src/layout/AppMenuDrawer.tsx +122 -122
- package/src/layout/DialogMotion.css +79 -79
- package/src/layout/ErrorBoundary.tsx +162 -162
- package/src/layout/PageContainer.css +48 -48
- package/src/layout/PageContainer.tsx +41 -41
- package/src/layout/ResponsiveDialog.css +86 -86
- package/src/layout/ResponsiveDialog.tsx +55 -55
- package/src/layout/index.tsx +70 -70
- package/src/link/Link.tsx +23 -23
- package/src/link/index.ts +1 -1
- package/src/list/ItemList.css +2 -1
- package/src/mdi/Mdi.test.tsx +91 -91
- package/src/mdi/Mdi.tsx +141 -141
- package/src/mdi/MdiConfirmCloseDialog.tsx +41 -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 +57 -57
- package/src/page/PageBlock.css +64 -64
- package/src/page/PageBlock.tsx +93 -93
- 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 +121 -121
- package/src/progress/LinearProgress.tsx +106 -106
- 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 +27 -27
- package/src/theme/sizeMode.ts +62 -0
- package/src/theme/useDsColorScheme.ts +53 -53
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
import { combine } from "@atlaskit/pragmatic-drag-and-drop/utils/combine";
|
|
2
|
-
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/adapter/element-adapter";
|
|
3
|
-
import {
|
|
4
|
-
attachClosestEdge,
|
|
5
|
-
type Edge,
|
|
6
|
-
extractClosestEdge,
|
|
7
|
-
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
8
|
-
import { type RefObject, useEffect, useState } from "react";
|
|
9
|
-
|
|
10
|
-
import { isSortableDragData, type SortableDragData } from "./useSortableContainer.js";
|
|
11
|
-
import type { SortableRecord } from "./types.js";
|
|
12
|
-
|
|
13
|
-
export type SortableItemState =
|
|
14
|
-
| { type: "idle" }
|
|
15
|
-
| { type: "is-dragging" }
|
|
16
|
-
| { type: "is-dragging-over"; edge: Edge | null };
|
|
17
|
-
|
|
18
|
-
export interface UseSortableItemOptions<T extends SortableRecord> {
|
|
19
|
-
/** Identifies the list. Must match the type given to the container/list */
|
|
20
|
-
type: string;
|
|
21
|
-
record: T;
|
|
22
|
-
/** Element that acts as the drop target, usually the list item */
|
|
23
|
-
itemRef: RefObject<HTMLElement | null>;
|
|
24
|
-
/** Element that starts the drag, usually a drag handle button */
|
|
25
|
-
handleRef: RefObject<HTMLElement | null>;
|
|
26
|
-
enabled?: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Makes a single list item draggable and turns it into a drop target that
|
|
31
|
-
* reports which edge is closest to the pointer, so a drop indicator can be
|
|
32
|
-
* rendered.
|
|
33
|
-
*/
|
|
34
|
-
export function useSortableItem<T extends SortableRecord>({
|
|
35
|
-
type,
|
|
36
|
-
record,
|
|
37
|
-
itemRef,
|
|
38
|
-
handleRef,
|
|
39
|
-
enabled = true,
|
|
40
|
-
}: UseSortableItemOptions<T>): SortableItemState {
|
|
41
|
-
let [state, setState] = useState<SortableItemState>({ type: "idle" });
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (!enabled) return;
|
|
45
|
-
let item = itemRef.current;
|
|
46
|
-
let handle = handleRef.current;
|
|
47
|
-
if (!item || !handle) return;
|
|
48
|
-
|
|
49
|
-
return combine(
|
|
50
|
-
draggable({
|
|
51
|
-
element: handle,
|
|
52
|
-
getInitialData: () => ({ type, record }) satisfies SortableDragData<T>,
|
|
53
|
-
onDragStart: () => setState({ type: "is-dragging" }),
|
|
54
|
-
onDrop: () => setState({ type: "idle" }),
|
|
55
|
-
}),
|
|
56
|
-
dropTargetForElements({
|
|
57
|
-
element: item,
|
|
58
|
-
canDrop({ source }) {
|
|
59
|
-
if (!isSortableDragData<T>(source.data, type)) return false;
|
|
60
|
-
return source.data.record.PrimKey !== record.PrimKey;
|
|
61
|
-
},
|
|
62
|
-
getData({ input }) {
|
|
63
|
-
let data: SortableDragData<T> = { type, record };
|
|
64
|
-
return attachClosestEdge(data, {
|
|
65
|
-
element: item,
|
|
66
|
-
input,
|
|
67
|
-
allowedEdges: ["top", "bottom"],
|
|
68
|
-
});
|
|
69
|
-
},
|
|
70
|
-
getIsSticky: () => true,
|
|
71
|
-
onDragEnter({ self }) {
|
|
72
|
-
let edge = extractClosestEdge(self.data);
|
|
73
|
-
setState({ type: "is-dragging-over", edge });
|
|
74
|
-
},
|
|
75
|
-
onDrag({ self }) {
|
|
76
|
-
let edge = extractClosestEdge(self.data);
|
|
77
|
-
setState((current) => {
|
|
78
|
-
if (current.type === "is-dragging-over" && current.edge === edge) {
|
|
79
|
-
return current;
|
|
80
|
-
}
|
|
81
|
-
return { type: "is-dragging-over", edge };
|
|
82
|
-
});
|
|
83
|
-
},
|
|
84
|
-
onDragLeave() {
|
|
85
|
-
setState({ type: "idle" });
|
|
86
|
-
},
|
|
87
|
-
onDrop() {
|
|
88
|
-
setState({ type: "idle" });
|
|
89
|
-
},
|
|
90
|
-
}),
|
|
91
|
-
);
|
|
92
|
-
}, [enabled, record, type, itemRef, handleRef]);
|
|
93
|
-
|
|
94
|
-
return state;
|
|
95
|
-
}
|
|
1
|
+
import { combine } from "@atlaskit/pragmatic-drag-and-drop/utils/combine";
|
|
2
|
+
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/adapter/element-adapter";
|
|
3
|
+
import {
|
|
4
|
+
attachClosestEdge,
|
|
5
|
+
type Edge,
|
|
6
|
+
extractClosestEdge,
|
|
7
|
+
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
8
|
+
import { type RefObject, useEffect, useState } from "react";
|
|
9
|
+
|
|
10
|
+
import { isSortableDragData, type SortableDragData } from "./useSortableContainer.js";
|
|
11
|
+
import type { SortableRecord } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export type SortableItemState =
|
|
14
|
+
| { type: "idle" }
|
|
15
|
+
| { type: "is-dragging" }
|
|
16
|
+
| { type: "is-dragging-over"; edge: Edge | null };
|
|
17
|
+
|
|
18
|
+
export interface UseSortableItemOptions<T extends SortableRecord> {
|
|
19
|
+
/** Identifies the list. Must match the type given to the container/list */
|
|
20
|
+
type: string;
|
|
21
|
+
record: T;
|
|
22
|
+
/** Element that acts as the drop target, usually the list item */
|
|
23
|
+
itemRef: RefObject<HTMLElement | null>;
|
|
24
|
+
/** Element that starts the drag, usually a drag handle button */
|
|
25
|
+
handleRef: RefObject<HTMLElement | null>;
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Makes a single list item draggable and turns it into a drop target that
|
|
31
|
+
* reports which edge is closest to the pointer, so a drop indicator can be
|
|
32
|
+
* rendered.
|
|
33
|
+
*/
|
|
34
|
+
export function useSortableItem<T extends SortableRecord>({
|
|
35
|
+
type,
|
|
36
|
+
record,
|
|
37
|
+
itemRef,
|
|
38
|
+
handleRef,
|
|
39
|
+
enabled = true,
|
|
40
|
+
}: UseSortableItemOptions<T>): SortableItemState {
|
|
41
|
+
let [state, setState] = useState<SortableItemState>({ type: "idle" });
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!enabled) return;
|
|
45
|
+
let item = itemRef.current;
|
|
46
|
+
let handle = handleRef.current;
|
|
47
|
+
if (!item || !handle) return;
|
|
48
|
+
|
|
49
|
+
return combine(
|
|
50
|
+
draggable({
|
|
51
|
+
element: handle,
|
|
52
|
+
getInitialData: () => ({ type, record }) satisfies SortableDragData<T>,
|
|
53
|
+
onDragStart: () => setState({ type: "is-dragging" }),
|
|
54
|
+
onDrop: () => setState({ type: "idle" }),
|
|
55
|
+
}),
|
|
56
|
+
dropTargetForElements({
|
|
57
|
+
element: item,
|
|
58
|
+
canDrop({ source }) {
|
|
59
|
+
if (!isSortableDragData<T>(source.data, type)) return false;
|
|
60
|
+
return source.data.record.PrimKey !== record.PrimKey;
|
|
61
|
+
},
|
|
62
|
+
getData({ input }) {
|
|
63
|
+
let data: SortableDragData<T> = { type, record };
|
|
64
|
+
return attachClosestEdge(data, {
|
|
65
|
+
element: item,
|
|
66
|
+
input,
|
|
67
|
+
allowedEdges: ["top", "bottom"],
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
getIsSticky: () => true,
|
|
71
|
+
onDragEnter({ self }) {
|
|
72
|
+
let edge = extractClosestEdge(self.data);
|
|
73
|
+
setState({ type: "is-dragging-over", edge });
|
|
74
|
+
},
|
|
75
|
+
onDrag({ self }) {
|
|
76
|
+
let edge = extractClosestEdge(self.data);
|
|
77
|
+
setState((current) => {
|
|
78
|
+
if (current.type === "is-dragging-over" && current.edge === edge) {
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
81
|
+
return { type: "is-dragging-over", edge };
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
onDragLeave() {
|
|
85
|
+
setState({ type: "idle" });
|
|
86
|
+
},
|
|
87
|
+
onDrop() {
|
|
88
|
+
setState({ type: "idle" });
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}, [enabled, record, type, itemRef, handleRef]);
|
|
93
|
+
|
|
94
|
+
return state;
|
|
95
|
+
}
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
2
|
-
import type { DataObject } from "@olenbetong/appframe-data";
|
|
3
|
-
import { useData } from "@olenbetong/appframe-react";
|
|
4
|
-
import { useCallback, useMemo } from "react";
|
|
5
|
-
|
|
6
|
-
import { reorderSortOrder } from "./reorderSortOrder.js";
|
|
7
|
-
import { DEFAULT_SORT_ORDER_SPACING } from "./sortOrder.js";
|
|
8
|
-
import type { SortableRecord } from "./types.js";
|
|
9
|
-
import { useSortableContainer } from "./useSortableContainer.js";
|
|
10
|
-
|
|
11
|
-
export interface SortableListOptions<T extends SortableRecord> {
|
|
12
|
-
/** Data object holding the records to display */
|
|
13
|
-
dataObject: DataObject<T>;
|
|
14
|
-
/**
|
|
15
|
-
* Data object used to persist the new sort order, when the list is bound to
|
|
16
|
-
* a read-only view. Defaults to `dataObject`.
|
|
17
|
-
*/
|
|
18
|
-
updateDataObject?: DataObject<any>;
|
|
19
|
-
/**
|
|
20
|
-
* Identifies the list. Only items sharing the same type can be dropped on
|
|
21
|
-
* each other. Defaults to the data object id.
|
|
22
|
-
*/
|
|
23
|
-
type?: string;
|
|
24
|
-
/** Limits the list to a subset of the records, e.g. rows for the current master record */
|
|
25
|
-
filter?: (record: T) => boolean;
|
|
26
|
-
/** Disables dragging, e.g. while the list is read only */
|
|
27
|
-
enabled?: boolean;
|
|
28
|
-
/** Spacing used when appending to the end of the list */
|
|
29
|
-
spacing?: number;
|
|
30
|
-
/**
|
|
31
|
-
* Called when the list runs out of free sort order values (two records share
|
|
32
|
-
* the same sort order). Typically executes a stored procedure that renumbers
|
|
33
|
-
* the list with even spacing.
|
|
34
|
-
*/
|
|
35
|
-
onNormalize?: () => Promise<void>;
|
|
36
|
-
onError?: (error: unknown) => void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface SortableList<T extends SortableRecord> {
|
|
40
|
-
/** Records passing `filter`, sorted by `SortOrder` */
|
|
41
|
-
records: T[];
|
|
42
|
-
/** Identifies the list. Pass to `useSortableItem` when rendering items manually */
|
|
43
|
-
type: string;
|
|
44
|
-
enabled: boolean;
|
|
45
|
-
/**
|
|
46
|
-
* Moves a record relative to another record. `edge` decides whether it is
|
|
47
|
-
* placed before (`null`/`"top"`) or after (`"bottom"`) the target.
|
|
48
|
-
*/
|
|
49
|
-
moveItem: (sourcePrimKey: string, targetPrimKey: string, edge: Edge | null) => Promise<void>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Keeps a data object sorted by its `SortOrder` column and handles drops from
|
|
54
|
-
* the sortable items rendered inside it.
|
|
55
|
-
*
|
|
56
|
-
* Use {@link SortableItemList} for the common list layout, or this hook
|
|
57
|
-
* directly together with {@link useSortableItem} and {@link DropIndicator}
|
|
58
|
-
* when the items need custom markup.
|
|
59
|
-
*/
|
|
60
|
-
export function useSortableList<T extends SortableRecord>({
|
|
61
|
-
dataObject,
|
|
62
|
-
updateDataObject,
|
|
63
|
-
type,
|
|
64
|
-
filter,
|
|
65
|
-
enabled = true,
|
|
66
|
-
spacing = DEFAULT_SORT_ORDER_SPACING,
|
|
67
|
-
onNormalize,
|
|
68
|
-
onError,
|
|
69
|
-
}: SortableListOptions<T>): SortableList<T> {
|
|
70
|
-
let data = useData(dataObject);
|
|
71
|
-
let listType = type ?? dataObject.getDataSourceId();
|
|
72
|
-
let dataObjectForUpdate = updateDataObject ?? dataObject;
|
|
73
|
-
let records = useMemo(() => {
|
|
74
|
-
let filtered = filter ? data.filter(filter) : data;
|
|
75
|
-
return [...filtered].sort((a, b) => (a.SortOrder ?? spacing) - (b.SortOrder ?? spacing));
|
|
76
|
-
}, [data, filter, spacing]);
|
|
77
|
-
|
|
78
|
-
let moveItem = useCallback(
|
|
79
|
-
async (sourcePrimKey: string, targetPrimKey: string, edge: Edge | null) => {
|
|
80
|
-
try {
|
|
81
|
-
await reorderSortOrder({
|
|
82
|
-
records,
|
|
83
|
-
sourcePrimKey,
|
|
84
|
-
targetPrimKey,
|
|
85
|
-
edge,
|
|
86
|
-
spacing,
|
|
87
|
-
onNormalize,
|
|
88
|
-
refreshRecords: async () => {
|
|
89
|
-
await dataObject.refreshDataSource();
|
|
90
|
-
let refreshed = dataObject.getData();
|
|
91
|
-
let filtered = filter ? refreshed.filter(filter) : refreshed;
|
|
92
|
-
return [...filtered].sort((a, b) => (a.SortOrder ?? spacing) - (b.SortOrder ?? spacing));
|
|
93
|
-
},
|
|
94
|
-
listDataObject: dataObject,
|
|
95
|
-
updateDataObject: dataObjectForUpdate,
|
|
96
|
-
});
|
|
97
|
-
} catch (error) {
|
|
98
|
-
if (onError) {
|
|
99
|
-
onError(error);
|
|
100
|
-
} else {
|
|
101
|
-
throw error;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
[records, spacing, onNormalize, dataObject, dataObjectForUpdate, filter, onError],
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
useSortableContainer<T>({
|
|
109
|
-
type: listType,
|
|
110
|
-
enabled,
|
|
111
|
-
onDrop: ({ source, target, edge }) => {
|
|
112
|
-
void moveItem(source.PrimKey, target.PrimKey, edge);
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
return { records, type: listType, enabled, moveItem };
|
|
117
|
-
}
|
|
1
|
+
import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
2
|
+
import type { DataObject } from "@olenbetong/appframe-data";
|
|
3
|
+
import { useData } from "@olenbetong/appframe-react";
|
|
4
|
+
import { useCallback, useMemo } from "react";
|
|
5
|
+
|
|
6
|
+
import { reorderSortOrder } from "./reorderSortOrder.js";
|
|
7
|
+
import { DEFAULT_SORT_ORDER_SPACING } from "./sortOrder.js";
|
|
8
|
+
import type { SortableRecord } from "./types.js";
|
|
9
|
+
import { useSortableContainer } from "./useSortableContainer.js";
|
|
10
|
+
|
|
11
|
+
export interface SortableListOptions<T extends SortableRecord> {
|
|
12
|
+
/** Data object holding the records to display */
|
|
13
|
+
dataObject: DataObject<T>;
|
|
14
|
+
/**
|
|
15
|
+
* Data object used to persist the new sort order, when the list is bound to
|
|
16
|
+
* a read-only view. Defaults to `dataObject`.
|
|
17
|
+
*/
|
|
18
|
+
updateDataObject?: DataObject<any>;
|
|
19
|
+
/**
|
|
20
|
+
* Identifies the list. Only items sharing the same type can be dropped on
|
|
21
|
+
* each other. Defaults to the data object id.
|
|
22
|
+
*/
|
|
23
|
+
type?: string;
|
|
24
|
+
/** Limits the list to a subset of the records, e.g. rows for the current master record */
|
|
25
|
+
filter?: (record: T) => boolean;
|
|
26
|
+
/** Disables dragging, e.g. while the list is read only */
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
/** Spacing used when appending to the end of the list */
|
|
29
|
+
spacing?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Called when the list runs out of free sort order values (two records share
|
|
32
|
+
* the same sort order). Typically executes a stored procedure that renumbers
|
|
33
|
+
* the list with even spacing.
|
|
34
|
+
*/
|
|
35
|
+
onNormalize?: () => Promise<void>;
|
|
36
|
+
onError?: (error: unknown) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SortableList<T extends SortableRecord> {
|
|
40
|
+
/** Records passing `filter`, sorted by `SortOrder` */
|
|
41
|
+
records: T[];
|
|
42
|
+
/** Identifies the list. Pass to `useSortableItem` when rendering items manually */
|
|
43
|
+
type: string;
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Moves a record relative to another record. `edge` decides whether it is
|
|
47
|
+
* placed before (`null`/`"top"`) or after (`"bottom"`) the target.
|
|
48
|
+
*/
|
|
49
|
+
moveItem: (sourcePrimKey: string, targetPrimKey: string, edge: Edge | null) => Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Keeps a data object sorted by its `SortOrder` column and handles drops from
|
|
54
|
+
* the sortable items rendered inside it.
|
|
55
|
+
*
|
|
56
|
+
* Use {@link SortableItemList} for the common list layout, or this hook
|
|
57
|
+
* directly together with {@link useSortableItem} and {@link DropIndicator}
|
|
58
|
+
* when the items need custom markup.
|
|
59
|
+
*/
|
|
60
|
+
export function useSortableList<T extends SortableRecord>({
|
|
61
|
+
dataObject,
|
|
62
|
+
updateDataObject,
|
|
63
|
+
type,
|
|
64
|
+
filter,
|
|
65
|
+
enabled = true,
|
|
66
|
+
spacing = DEFAULT_SORT_ORDER_SPACING,
|
|
67
|
+
onNormalize,
|
|
68
|
+
onError,
|
|
69
|
+
}: SortableListOptions<T>): SortableList<T> {
|
|
70
|
+
let data = useData(dataObject);
|
|
71
|
+
let listType = type ?? dataObject.getDataSourceId();
|
|
72
|
+
let dataObjectForUpdate = updateDataObject ?? dataObject;
|
|
73
|
+
let records = useMemo(() => {
|
|
74
|
+
let filtered = filter ? data.filter(filter) : data;
|
|
75
|
+
return [...filtered].sort((a, b) => (a.SortOrder ?? spacing) - (b.SortOrder ?? spacing));
|
|
76
|
+
}, [data, filter, spacing]);
|
|
77
|
+
|
|
78
|
+
let moveItem = useCallback(
|
|
79
|
+
async (sourcePrimKey: string, targetPrimKey: string, edge: Edge | null) => {
|
|
80
|
+
try {
|
|
81
|
+
await reorderSortOrder({
|
|
82
|
+
records,
|
|
83
|
+
sourcePrimKey,
|
|
84
|
+
targetPrimKey,
|
|
85
|
+
edge,
|
|
86
|
+
spacing,
|
|
87
|
+
onNormalize,
|
|
88
|
+
refreshRecords: async () => {
|
|
89
|
+
await dataObject.refreshDataSource();
|
|
90
|
+
let refreshed = dataObject.getData();
|
|
91
|
+
let filtered = filter ? refreshed.filter(filter) : refreshed;
|
|
92
|
+
return [...filtered].sort((a, b) => (a.SortOrder ?? spacing) - (b.SortOrder ?? spacing));
|
|
93
|
+
},
|
|
94
|
+
listDataObject: dataObject,
|
|
95
|
+
updateDataObject: dataObjectForUpdate,
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (onError) {
|
|
99
|
+
onError(error);
|
|
100
|
+
} else {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
[records, spacing, onNormalize, dataObject, dataObjectForUpdate, filter, onError],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
useSortableContainer<T>({
|
|
109
|
+
type: listType,
|
|
110
|
+
enabled,
|
|
111
|
+
onDrop: ({ source, target, edge }) => {
|
|
112
|
+
void moveItem(source.PrimKey, target.PrimKey, edge);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return { records, type: listType, enabled, moveItem };
|
|
117
|
+
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* happy-dom exposes a `Document` constructor on the window that is *not* in the
|
|
3
|
-
* prototype chain of the actual `document` (which is an `HTMLDocument` built on
|
|
4
|
-
* happy-dom's internal `Document` class). `document instanceof Document` is
|
|
5
|
-
* therefore `false`, which is enough to hang libraries that walk up the tree
|
|
6
|
-
* until they reach the document — the `@oddbird/popover-polyfill` bundled into
|
|
7
|
-
* `@digdir/designsystemet-web` loops forever in `nearestInclusiveOpenPopover`,
|
|
8
|
-
* taking the whole worker with it.
|
|
9
|
-
*
|
|
10
|
-
* Point the global `Document` at the class the document really inherits from.
|
|
11
|
-
* Must run before anything that captures `Document`.
|
|
12
|
-
*/
|
|
13
|
-
export function fixHappyDomDocumentClass(): void {
|
|
14
|
-
if (typeof document === "undefined" || document instanceof Document) return;
|
|
15
|
-
|
|
16
|
-
for (let proto = Object.getPrototypeOf(document); proto; proto = Object.getPrototypeOf(proto)) {
|
|
17
|
-
let ctor = proto.constructor;
|
|
18
|
-
if (ctor?.name === "Document") {
|
|
19
|
-
globalThis.Document = ctor;
|
|
20
|
-
(globalThis as typeof globalThis & { window: typeof globalThis }).window.Document = ctor;
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* happy-dom exposes a `Document` constructor on the window that is *not* in the
|
|
3
|
+
* prototype chain of the actual `document` (which is an `HTMLDocument` built on
|
|
4
|
+
* happy-dom's internal `Document` class). `document instanceof Document` is
|
|
5
|
+
* therefore `false`, which is enough to hang libraries that walk up the tree
|
|
6
|
+
* until they reach the document — the `@oddbird/popover-polyfill` bundled into
|
|
7
|
+
* `@digdir/designsystemet-web` loops forever in `nearestInclusiveOpenPopover`,
|
|
8
|
+
* taking the whole worker with it.
|
|
9
|
+
*
|
|
10
|
+
* Point the global `Document` at the class the document really inherits from.
|
|
11
|
+
* Must run before anything that captures `Document`.
|
|
12
|
+
*/
|
|
13
|
+
export function fixHappyDomDocumentClass(): void {
|
|
14
|
+
if (typeof document === "undefined" || document instanceof Document) return;
|
|
15
|
+
|
|
16
|
+
for (let proto = Object.getPrototypeOf(document); proto; proto = Object.getPrototypeOf(proto)) {
|
|
17
|
+
let ctor = proto.constructor;
|
|
18
|
+
if (ctor?.name === "Document") {
|
|
19
|
+
globalThis.Document = ctor;
|
|
20
|
+
(globalThis as typeof globalThis & { window: typeof globalThis }).window.Document = ctor;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/test/setup.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { fixHappyDomDocumentClass } from "./happy-dom-document-class.js";
|
|
2
|
-
|
|
3
|
-
fixHappyDomDocumentClass();
|
|
4
|
-
|
|
5
|
-
import "@testing-library/jest-dom/vitest";
|
|
6
|
-
|
|
7
|
-
import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
|
|
8
|
-
|
|
9
|
-
import { cleanup } from "@testing-library/react";
|
|
10
|
-
import { afterEach } from "vitest";
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
cleanup();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
/*
|
|
17
|
-
* jest-dom 7.0.1 types its matchers onto the one-parameter `Assertion<T>` of Vitest 4, which
|
|
18
|
-
* Vitest 5 replaced with `Assertion<R, T>`; custom matchers now go on `Matchers<R, T>`. Until
|
|
19
|
-
* jest-dom ships Vitest 5 types, register them there. Remove this when its `vitest` entry
|
|
20
|
-
* types `Matchers` itself.
|
|
21
|
-
*/
|
|
22
|
-
declare module "vitest" {
|
|
23
|
-
interface Matchers<R extends void | Promise<void> = void | Promise<void>, T = unknown> extends TestingLibraryMatchers<
|
|
24
|
-
T,
|
|
25
|
-
R
|
|
26
|
-
> {}
|
|
27
|
-
}
|
|
1
|
+
import { fixHappyDomDocumentClass } from "./happy-dom-document-class.js";
|
|
2
|
+
|
|
3
|
+
fixHappyDomDocumentClass();
|
|
4
|
+
|
|
5
|
+
import "@testing-library/jest-dom/vitest";
|
|
6
|
+
|
|
7
|
+
import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
|
|
8
|
+
|
|
9
|
+
import { cleanup } from "@testing-library/react";
|
|
10
|
+
import { afterEach } from "vitest";
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* jest-dom 7.0.1 types its matchers onto the one-parameter `Assertion<T>` of Vitest 4, which
|
|
18
|
+
* Vitest 5 replaced with `Assertion<R, T>`; custom matchers now go on `Matchers<R, T>`. Until
|
|
19
|
+
* jest-dom ships Vitest 5 types, register them there. Remove this when its `vitest` entry
|
|
20
|
+
* types `Matchers` itself.
|
|
21
|
+
*/
|
|
22
|
+
declare module "vitest" {
|
|
23
|
+
interface Matchers<R extends void | Promise<void> = void | Promise<void>, T = unknown> extends TestingLibraryMatchers<
|
|
24
|
+
T,
|
|
25
|
+
R
|
|
26
|
+
> {}
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type RefObject, useLayoutEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/** Components whose `data-size` picks a variant of their own (text or avatar size), not the size mode. */
|
|
4
|
+
const SIZE_VARIANTS = ".ds-heading, .ds-paragraph, .ds-spinner, .ds-avatar";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Designsystemet size mode an element is in: the `data-size` of its closest ancestor (or
|
|
8
|
+
* itself) that sets one, or null when nothing does and the page default applies.
|
|
9
|
+
*/
|
|
10
|
+
export function getSizeMode(element: Element | null | undefined) {
|
|
11
|
+
return element?.closest(`[data-size]:not(${SIZE_VARIANTS})`)?.getAttribute("data-size") ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Gives an element rendered outside its owner - a dialog or popover portaled to `<body>` - the
|
|
16
|
+
* size mode of the element it belongs to, which it would otherwise not inherit.
|
|
17
|
+
*/
|
|
18
|
+
export function copySizeMode(target: HTMLElement | null | undefined, owner: Element | null | undefined) {
|
|
19
|
+
if (!target) return;
|
|
20
|
+
|
|
21
|
+
let size = getSizeMode(owner);
|
|
22
|
+
if (size) {
|
|
23
|
+
target.dataset.size = size;
|
|
24
|
+
} else {
|
|
25
|
+
delete target.dataset.size;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The pixel value of `--ds-size-<step>` at an element's position. The token is a `calc()` that
|
|
31
|
+
* a custom property hands over unresolved, so it is measured on a hidden probe instead. 0 when
|
|
32
|
+
* the Designsystemet theme is not loaded.
|
|
33
|
+
*/
|
|
34
|
+
export function measureDsSize(element: Element, step: number) {
|
|
35
|
+
let probe = element.ownerDocument.createElement("div");
|
|
36
|
+
probe.style.cssText = `position: absolute; visibility: hidden; block-size: var(--ds-size-${step});`;
|
|
37
|
+
element.append(probe);
|
|
38
|
+
let height = probe.getBoundingClientRect().height;
|
|
39
|
+
probe.remove();
|
|
40
|
+
|
|
41
|
+
return height;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A height for something sized in script (a virtualized row, a grid row) that follows the size
|
|
46
|
+
* mode the element sits in: `--ds-size-<step>`, or `fallback` until it is measured and when the
|
|
47
|
+
* theme is not loaded. Measured once, after mount and before paint: the size mode of a page does
|
|
48
|
+
* not change while it is open.
|
|
49
|
+
*/
|
|
50
|
+
export function useDsSize(ref: RefObject<Element | null>, step: number, fallback: number) {
|
|
51
|
+
let [size, setSize] = useState(fallback);
|
|
52
|
+
|
|
53
|
+
useLayoutEffect(() => {
|
|
54
|
+
let element = ref.current;
|
|
55
|
+
if (!element) return;
|
|
56
|
+
|
|
57
|
+
let measured = Math.round(measureDsSize(element, step));
|
|
58
|
+
if (measured) setSize(measured);
|
|
59
|
+
}, [ref, step]);
|
|
60
|
+
|
|
61
|
+
return size;
|
|
62
|
+
}
|