@olenbetong/appframe-ds 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/package.json +8 -7
- package/scripts/copyAssets.mjs +27 -55
- package/src/AfLookup/Lookup.css +129 -125
- package/src/AfLookup/Lookup.tsx +184 -162
- 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 -0
- package/src/cutoff/CutOffPicker.tsx +165 -0
- package/src/cutoff/context.tsx +194 -0
- package/src/cutoff/dsCutOffDates.ts +21 -0
- package/src/cutoff/index.ts +4 -0
- package/src/cutoff/utils.test.ts +59 -0
- package/src/cutoff/utils.ts +276 -0
- 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 -74
- 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 -77
- 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 -222
- 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 -151
- package/src/grid/AfHeaderFilterPanel.tsx +111 -111
- package/src/grid/GridEditLookup.css +12 -0
- package/src/grid/GridEditLookup.tsx +121 -0
- 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 +489 -484
- 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.ts +145 -145
- package/src/grid/useAfSortModel.ts +84 -84
- package/src/i18n.ts +13 -0
- package/src/index.ts +14 -12
- 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 -75
- package/src/layout/AppMenuDrawer.css +70 -70
- package/src/layout/AppMenuDrawer.tsx +122 -122
- package/src/layout/ErrorBoundary.tsx +162 -162
- package/src/layout/ResponsiveDialog.css +86 -0
- package/src/layout/ResponsiveDialog.tsx +55 -0
- package/src/layout/index.tsx +69 -68
- package/src/link/Link.tsx +23 -0
- package/src/link/index.ts +1 -0
- package/src/mdi/Mdi.css +138 -138
- package/src/mdi/Mdi.test.tsx +91 -87
- 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 -82
- 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 -116
- package/src/page/PageTitle.css +5 -0
- package/src/page/PageTitle.tsx +21 -0
- package/src/page/index.ts +4 -3
- 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 -86
- 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,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,12 +1,12 @@
|
|
|
1
|
-
import { fixHappyDomDocumentClass } from "./happy-dom-document-class.js";
|
|
2
|
-
|
|
3
|
-
fixHappyDomDocumentClass();
|
|
4
|
-
|
|
5
|
-
import "@testing-library/jest-dom/vitest";
|
|
6
|
-
|
|
7
|
-
import { cleanup } from "@testing-library/react";
|
|
8
|
-
import { afterEach } from "vitest";
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
cleanup();
|
|
12
|
-
});
|
|
1
|
+
import { fixHappyDomDocumentClass } from "./happy-dom-document-class.js";
|
|
2
|
+
|
|
3
|
+
fixHappyDomDocumentClass();
|
|
4
|
+
|
|
5
|
+
import "@testing-library/jest-dom/vitest";
|
|
6
|
+
|
|
7
|
+
import { cleanup } from "@testing-library/react";
|
|
8
|
+
import { afterEach } from "vitest";
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
cleanup();
|
|
12
|
+
});
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
|
|
3
|
-
export function getDsColorScheme(element?: Element | null): "light" | "dark" {
|
|
4
|
-
let scoped = element?.closest<HTMLElement>("[data-color-scheme]");
|
|
5
|
-
let scheme =
|
|
6
|
-
scoped?.dataset.colorScheme ??
|
|
7
|
-
document.body?.dataset.colorScheme ??
|
|
8
|
-
document.documentElement?.dataset.colorScheme ??
|
|
9
|
-
"light";
|
|
10
|
-
|
|
11
|
-
if (scheme === "auto") {
|
|
12
|
-
return globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return scheme === "dark" ? "dark" : "light";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Tracks the color scheme Designsystemet is rendering with. The article template
|
|
20
|
-
* sets `data-color-scheme` on `<body>`, but it may also be scoped to a subtree,
|
|
21
|
-
* so resolve it from `element` upwards when one is given. The OS preference is
|
|
22
|
-
* watched too, since it decides the result when the scheme is `auto`.
|
|
23
|
-
*/
|
|
24
|
-
export function useDsColorScheme(element?: Element | null) {
|
|
25
|
-
let [scheme, setScheme] = useState<"light" | "dark">(() => getDsColorScheme(element));
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
function update() {
|
|
29
|
-
setScheme(getDsColorScheme(element));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
update();
|
|
33
|
-
|
|
34
|
-
// `data-color-scheme` may sit on any ancestor, so watch the whole document
|
|
35
|
-
// for that one attribute rather than just `<html>` and `<body>`.
|
|
36
|
-
let observer = new MutationObserver(update);
|
|
37
|
-
observer.observe(document.documentElement, {
|
|
38
|
-
attributes: true,
|
|
39
|
-
attributeFilter: ["data-color-scheme"],
|
|
40
|
-
subtree: true,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
let media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
|
|
44
|
-
media?.addEventListener("change", update);
|
|
45
|
-
|
|
46
|
-
return () => {
|
|
47
|
-
observer.disconnect();
|
|
48
|
-
media?.removeEventListener("change", update);
|
|
49
|
-
};
|
|
50
|
-
}, [element]);
|
|
51
|
-
|
|
52
|
-
return scheme;
|
|
53
|
-
}
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function getDsColorScheme(element?: Element | null): "light" | "dark" {
|
|
4
|
+
let scoped = element?.closest<HTMLElement>("[data-color-scheme]");
|
|
5
|
+
let scheme =
|
|
6
|
+
scoped?.dataset.colorScheme ??
|
|
7
|
+
document.body?.dataset.colorScheme ??
|
|
8
|
+
document.documentElement?.dataset.colorScheme ??
|
|
9
|
+
"light";
|
|
10
|
+
|
|
11
|
+
if (scheme === "auto") {
|
|
12
|
+
return globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return scheme === "dark" ? "dark" : "light";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tracks the color scheme Designsystemet is rendering with. The article template
|
|
20
|
+
* sets `data-color-scheme` on `<body>`, but it may also be scoped to a subtree,
|
|
21
|
+
* so resolve it from `element` upwards when one is given. The OS preference is
|
|
22
|
+
* watched too, since it decides the result when the scheme is `auto`.
|
|
23
|
+
*/
|
|
24
|
+
export function useDsColorScheme(element?: Element | null) {
|
|
25
|
+
let [scheme, setScheme] = useState<"light" | "dark">(() => getDsColorScheme(element));
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
function update() {
|
|
29
|
+
setScheme(getDsColorScheme(element));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
update();
|
|
33
|
+
|
|
34
|
+
// `data-color-scheme` may sit on any ancestor, so watch the whole document
|
|
35
|
+
// for that one attribute rather than just `<html>` and `<body>`.
|
|
36
|
+
let observer = new MutationObserver(update);
|
|
37
|
+
observer.observe(document.documentElement, {
|
|
38
|
+
attributes: true,
|
|
39
|
+
attributeFilter: ["data-color-scheme"],
|
|
40
|
+
subtree: true,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
|
|
44
|
+
media?.addEventListener("change", update);
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
observer.disconnect();
|
|
48
|
+
media?.removeEventListener("change", update);
|
|
49
|
+
};
|
|
50
|
+
}, [element]);
|
|
51
|
+
|
|
52
|
+
return scheme;
|
|
53
|
+
}
|
package/tsconfig.build.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../tsconfig.build.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "./src",
|
|
5
|
-
"outDir": "./es"
|
|
6
|
-
},
|
|
7
|
-
"include": ["src"],
|
|
8
|
-
"exclude": ["es", "node_modules"]
|
|
9
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.build.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"outDir": "./es"
|
|
6
|
+
},
|
|
7
|
+
"include": ["src"],
|
|
8
|
+
"exclude": ["es", "node_modules"]
|
|
9
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../tsconfig.build.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "./src",
|
|
5
|
-
"outDir": "./es"
|
|
6
|
-
},
|
|
7
|
-
"include": ["src"]
|
|
8
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.build.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"outDir": "./es"
|
|
6
|
+
},
|
|
7
|
+
"include": ["src"]
|
|
8
|
+
}
|
package/vitest.config.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
include: ["src/**/*.test.tsx", "src/**/*.test.ts"],
|
|
6
|
-
environment: "happy-dom",
|
|
7
|
-
setupFiles: ["src/test/setup.ts"],
|
|
8
|
-
env: { NODE_ENV: "test" },
|
|
9
|
-
},
|
|
10
|
-
});
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
include: ["src/**/*.test.tsx", "src/**/*.test.ts"],
|
|
6
|
+
environment: "happy-dom",
|
|
7
|
+
setupFiles: ["src/test/setup.ts"],
|
|
8
|
+
env: { NODE_ENV: "test" },
|
|
9
|
+
},
|
|
10
|
+
});
|