@antelopejs/dms 0.7.8 → 0.7.10
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 +45 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/api-base-url.d.ts +1 -0
- package/dist/implementations/dms/api-base-url.js +6 -0
- package/dist/implementations/dms/api-base-url.js.map +1 -0
- package/dist/implementations/dms/frontend-modules.js +7 -6
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/page.js +8 -2
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/test/attachment-host/capabilities.d.ts +25 -0
- package/dist/test/attachment-host/capabilities.js +53 -1
- package/dist/test/attachment-host/capabilities.js.map +1 -1
- package/dist/test/integration/table-view-capabilities.test.js +13 -0
- package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
- package/dist/test/unit/api-base-url.test.d.ts +1 -0
- package/dist/test/unit/api-base-url.test.js +31 -0
- package/dist/test/unit/api-base-url.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
- package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +8 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
- package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
- package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
- package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
- package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
- package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
- package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
- package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
- package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
- package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +5 -4
- package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
- package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +74 -0
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +113 -99
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
- package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +8 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
- package/frontend-vue/layers/dms-ui/app/types/row-action.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/utils/componentId.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/utils/rowClickAction.ts +12 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
- package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
- package/frontend-vue/tests/block-not-found.test.ts +220 -0
- package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
- package/frontend-vue/tests/component-id.test.ts +18 -0
- package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
- package/frontend-vue/tests/guard.test.ts +242 -0
- package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
- package/frontend-vue/tests/live-toggle.test.ts +87 -1
- package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
- package/frontend-vue/tests/page-live-parity.test.ts +367 -0
- package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
- package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
- package/frontend-vue/tests/period-selector-url.test.ts +84 -6
- package/frontend-vue/tests/period-url-query.test.ts +52 -0
- package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
- package/frontend-vue/tests/row-side-panel.test.ts +109 -10
- package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
- package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
- package/frontend-vue/tests/table-density-menu.test.ts +4 -3
- package/frontend-vue/tests/table-live-hold.test.ts +95 -5
- package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
- package/frontend-vue/tests/table-row-hover.test.ts +32 -0
- package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
- package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
- package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
- package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
- package/frontend-vue/tests/table-view-views.test.ts +56 -15
- package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +9 -3
- package/skills/dms-module/SKILL.md +5 -0
- package/skills/dms-pages/REFERENCE.md +23 -2
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { isBranch } from "./preferencesCookie";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
* The state of a table view that the server render reads to list and draw
|
|
6
|
+
* the right rows: kept in the preferences cookie, compactly.
|
|
7
|
+
*/
|
|
8
|
+
export type ServerTableField =
|
|
9
|
+
| "sorting"
|
|
10
|
+
| "columnFilters"
|
|
11
|
+
| "columnVisibility"
|
|
12
|
+
| "pagination"
|
|
13
|
+
| "paginationScope"
|
|
14
|
+
| "activeTab"
|
|
15
|
+
| "viewMode"
|
|
16
|
+
| "activeView";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @internal
|
|
20
|
+
* The state of a table view that only the browser draws: kept in
|
|
21
|
+
* `localStorage`, applied once the table is mounted.
|
|
22
|
+
*/
|
|
23
|
+
export type LocalTableField =
|
|
24
|
+
| "columnOrder"
|
|
25
|
+
| "columnSizing"
|
|
26
|
+
| "density"
|
|
27
|
+
| "filtersOpen"
|
|
28
|
+
| "kanbanGroupBy"
|
|
29
|
+
| "groupedGroupBy"
|
|
30
|
+
| "userViews";
|
|
31
|
+
|
|
32
|
+
/** @internal */
|
|
33
|
+
export type TableField = ServerTableField | LocalTableField;
|
|
34
|
+
|
|
35
|
+
/** @internal A table's entry in the cookie, by short field key. */
|
|
36
|
+
export type CompactTableEntry = Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
/** @internal The collection of the preferences tree holding table entries. */
|
|
39
|
+
export const TABLES_PREFERENCE_KEY = "tables";
|
|
40
|
+
|
|
41
|
+
/** @internal The prefix of a table's `localStorage` key, versioned. */
|
|
42
|
+
export const LOCAL_TABLE_KEY_PREFIX = "dms-table-preferences:v1:";
|
|
43
|
+
|
|
44
|
+
const TABLE_ID_LENGTH = 8;
|
|
45
|
+
const HASH_SEEDS = [0xdeadbeef, 0x41c6ce57];
|
|
46
|
+
const HASH_MULTIPLIERS = [2654435761, 1597334677];
|
|
47
|
+
const HASH_MIX = [2246822507, 3266489909];
|
|
48
|
+
const HASH_RADIX = 36;
|
|
49
|
+
const HIGH_BITS = 2097151;
|
|
50
|
+
const HIGH_BITS_SHIFT = 4294967296;
|
|
51
|
+
const DESCENDING_PREFIX = "-";
|
|
52
|
+
const HIDDEN_PREFIX = "-";
|
|
53
|
+
const SHOWN_PREFIX = "+";
|
|
54
|
+
const DEFAULT_FILTER_MODE = "is";
|
|
55
|
+
const FIRST_PAGE_INDEX = 0;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @internal
|
|
59
|
+
* A 53-bit hash of `text` in base 36 (cyrb53): short, stable on the server
|
|
60
|
+
* and in the browser.
|
|
61
|
+
*/
|
|
62
|
+
export const hashText = (text: string): string => {
|
|
63
|
+
let [first, second] = HASH_SEEDS as [number, number];
|
|
64
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
65
|
+
const code = text.charCodeAt(index);
|
|
66
|
+
first = Math.imul(first ^ code, HASH_MULTIPLIERS[0]!);
|
|
67
|
+
second = Math.imul(second ^ code, HASH_MULTIPLIERS[1]!);
|
|
68
|
+
}
|
|
69
|
+
first = Math.imul(first ^ (first >>> 16), HASH_MIX[0]!);
|
|
70
|
+
first ^= Math.imul(second ^ (second >>> 13), HASH_MIX[1]!);
|
|
71
|
+
second = Math.imul(second ^ (second >>> 16), HASH_MIX[0]!);
|
|
72
|
+
second ^= Math.imul(first ^ (first >>> 13), HASH_MIX[1]!);
|
|
73
|
+
return (HIGH_BITS_SHIFT * (HIGH_BITS & second) + (first >>> 0)).toString(
|
|
74
|
+
HASH_RADIX,
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @internal
|
|
80
|
+
* The short key of a table view's preferences: its component and its page,
|
|
81
|
+
* hashed, so that neither a long id nor a dotted page id reaches the cookie.
|
|
82
|
+
*/
|
|
83
|
+
export const tablePreferenceId = (
|
|
84
|
+
componentId: string,
|
|
85
|
+
pageId: string,
|
|
86
|
+
): string =>
|
|
87
|
+
hashText(`${componentId}\u0000${pageId}`)
|
|
88
|
+
.padStart(TABLE_ID_LENGTH, "0")
|
|
89
|
+
.slice(0, TABLE_ID_LENGTH);
|
|
90
|
+
|
|
91
|
+
interface SortEntry {
|
|
92
|
+
id: string;
|
|
93
|
+
desc: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface FilterEntry {
|
|
97
|
+
accessorKey: string;
|
|
98
|
+
value?: unknown;
|
|
99
|
+
mode: string;
|
|
100
|
+
pinned?: boolean;
|
|
101
|
+
initialValue?: unknown;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface PaginationEntry {
|
|
105
|
+
pageIndex: number;
|
|
106
|
+
pageSize: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @internal
|
|
111
|
+
* How a server field is kept: its key in the table's entry, and its compact
|
|
112
|
+
* form. `encode` gets the field's default so that it can keep only what
|
|
113
|
+
* differs from it; a value whose form equals the default's is not written.
|
|
114
|
+
*/
|
|
115
|
+
export interface ServerFieldCodec {
|
|
116
|
+
key: string;
|
|
117
|
+
encode: (value: unknown, defaultValue: unknown) => unknown;
|
|
118
|
+
decode: (stored: unknown, defaultValue: unknown) => unknown;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const encodeSorting = (value: unknown): unknown => {
|
|
122
|
+
const sorts = (Array.isArray(value) ? value : []) as SortEntry[];
|
|
123
|
+
const tokens = sorts.map(
|
|
124
|
+
(sort) => `${sort.desc ? DESCENDING_PREFIX : ""}${sort.id}`,
|
|
125
|
+
);
|
|
126
|
+
return tokens.length === 1 ? tokens[0] : tokens;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const decodeSorting = (stored: unknown): SortEntry[] => {
|
|
130
|
+
const tokens = typeof stored === "string" ? [stored] : stored;
|
|
131
|
+
if (!Array.isArray(tokens)) throw new TypeError("sorting");
|
|
132
|
+
return tokens.map((token: string) => ({
|
|
133
|
+
id: token.startsWith(DESCENDING_PREFIX) ? token.slice(1) : token,
|
|
134
|
+
desc: token.startsWith(DESCENDING_PREFIX),
|
|
135
|
+
}));
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const isSameValue = (left: unknown, right: unknown): boolean =>
|
|
139
|
+
JSON.stringify(left) === JSON.stringify(right);
|
|
140
|
+
|
|
141
|
+
// A pinned filter still on its initial value comes back from the module's
|
|
142
|
+
// own declaration: only the user's own filters and changed values are kept.
|
|
143
|
+
const encodeFilters = (value: unknown): unknown =>
|
|
144
|
+
((Array.isArray(value) ? value : []) as FilterEntry[])
|
|
145
|
+
.filter(
|
|
146
|
+
(filter) =>
|
|
147
|
+
!filter.pinned || !isSameValue(filter.value, filter.initialValue),
|
|
148
|
+
)
|
|
149
|
+
.map((filter) =>
|
|
150
|
+
filter.mode === DEFAULT_FILTER_MODE
|
|
151
|
+
? [filter.accessorKey, filter.value ?? null]
|
|
152
|
+
: [filter.accessorKey, filter.value ?? null, filter.mode],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const decodeFilters = (stored: unknown): FilterEntry[] => {
|
|
156
|
+
if (!Array.isArray(stored)) throw new TypeError("columnFilters");
|
|
157
|
+
return stored.map(([accessorKey, value, mode]: unknown[]) => ({
|
|
158
|
+
accessorKey: String(accessorKey),
|
|
159
|
+
value,
|
|
160
|
+
mode: typeof mode === "string" ? mode : DEFAULT_FILTER_MODE,
|
|
161
|
+
}));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const visibilityOf = (value: unknown): Record<string, boolean> =>
|
|
165
|
+
isBranch(value) ? (value as Record<string, boolean>) : {};
|
|
166
|
+
|
|
167
|
+
// A column missing from a visibility state is shown.
|
|
168
|
+
const encodeVisibility = (value: unknown, defaultValue: unknown): unknown => {
|
|
169
|
+
const defaults = visibilityOf(defaultValue);
|
|
170
|
+
return Object.entries(visibilityOf(value))
|
|
171
|
+
.filter(([column, isShown]) => isShown !== (defaults[column] ?? true))
|
|
172
|
+
.map(
|
|
173
|
+
([column, isShown]) =>
|
|
174
|
+
`${isShown ? SHOWN_PREFIX : HIDDEN_PREFIX}${column}`,
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const decodeVisibility = (stored: unknown, defaultValue: unknown): unknown => {
|
|
179
|
+
if (!Array.isArray(stored)) throw new TypeError("columnVisibility");
|
|
180
|
+
const changes = stored.map((token: string) => [
|
|
181
|
+
token.slice(1),
|
|
182
|
+
token.startsWith(SHOWN_PREFIX),
|
|
183
|
+
]);
|
|
184
|
+
return { ...visibilityOf(defaultValue), ...Object.fromEntries(changes) };
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const encodePagination = (value: unknown): unknown => {
|
|
188
|
+
const { pageIndex, pageSize } = value as PaginationEntry;
|
|
189
|
+
return pageIndex === FIRST_PAGE_INDEX ? pageSize : [pageSize, pageIndex];
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const decodePagination = (stored: unknown): PaginationEntry => {
|
|
193
|
+
const [pageSize, pageIndex] = Array.isArray(stored)
|
|
194
|
+
? stored
|
|
195
|
+
: [stored, FIRST_PAGE_INDEX];
|
|
196
|
+
if (typeof pageSize !== "number" || typeof pageIndex !== "number") {
|
|
197
|
+
throw new TypeError("pagination");
|
|
198
|
+
}
|
|
199
|
+
return { pageIndex, pageSize };
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const plainField = (key: string): ServerFieldCodec => ({
|
|
203
|
+
key,
|
|
204
|
+
encode: (value) => value,
|
|
205
|
+
decode: (stored) => stored,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @internal
|
|
210
|
+
* The server fields, each with its one-letter key and compact form: a sort is
|
|
211
|
+
* `"-name"`, a filter `["status", "open"]`, a hidden column `"-email"`, a
|
|
212
|
+
* page size `50`.
|
|
213
|
+
*/
|
|
214
|
+
export const SERVER_TABLE_FIELDS: Record<ServerTableField, ServerFieldCodec> = {
|
|
215
|
+
sorting: { key: "s", encode: encodeSorting, decode: decodeSorting },
|
|
216
|
+
columnFilters: { key: "f", encode: encodeFilters, decode: decodeFilters },
|
|
217
|
+
columnVisibility: {
|
|
218
|
+
key: "v",
|
|
219
|
+
encode: encodeVisibility,
|
|
220
|
+
decode: decodeVisibility,
|
|
221
|
+
},
|
|
222
|
+
pagination: { key: "p", encode: encodePagination, decode: decodePagination },
|
|
223
|
+
paginationScope: plainField("q"),
|
|
224
|
+
activeTab: plainField("t"),
|
|
225
|
+
viewMode: plainField("d"),
|
|
226
|
+
activeView: plainField("w"),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/** @internal Whether `field` is kept in the cookie. */
|
|
230
|
+
export const isServerTableField = (
|
|
231
|
+
field: TableField,
|
|
232
|
+
): field is ServerTableField => field in SERVER_TABLE_FIELDS;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @internal
|
|
236
|
+
* `value`'s form in the entry, or `undefined` when it equals the default's
|
|
237
|
+
* and is not to be written.
|
|
238
|
+
*/
|
|
239
|
+
export const encodeServerField = (
|
|
240
|
+
field: ServerTableField,
|
|
241
|
+
value: unknown,
|
|
242
|
+
defaultValue: unknown,
|
|
243
|
+
): unknown => {
|
|
244
|
+
const codec = SERVER_TABLE_FIELDS[field];
|
|
245
|
+
if (value === undefined) return undefined;
|
|
246
|
+
const encoded = codec.encode(value, defaultValue);
|
|
247
|
+
const encodedDefault =
|
|
248
|
+
defaultValue === undefined
|
|
249
|
+
? undefined
|
|
250
|
+
: codec.encode(defaultValue, defaultValue);
|
|
251
|
+
return isSameValue(encoded, encodedDefault) ? undefined : encoded;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/** @internal The value `entry` keeps for `field`, else `defaultValue`. */
|
|
255
|
+
export const decodeServerField = <T>(
|
|
256
|
+
field: ServerTableField,
|
|
257
|
+
entry: CompactTableEntry | undefined,
|
|
258
|
+
defaultValue: T,
|
|
259
|
+
): T => {
|
|
260
|
+
const codec = SERVER_TABLE_FIELDS[field];
|
|
261
|
+
const stored = entry?.[codec.key];
|
|
262
|
+
if (stored === undefined) return defaultValue;
|
|
263
|
+
try {
|
|
264
|
+
return codec.decode(stored, defaultValue) as T;
|
|
265
|
+
} catch {
|
|
266
|
+
return defaultValue;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/** @internal Whether `value` is the field's default, not to be written. */
|
|
271
|
+
export const isDefaultValue = (
|
|
272
|
+
value: unknown,
|
|
273
|
+
defaultValue: unknown,
|
|
274
|
+
): boolean => value === undefined || isSameValue(value, defaultValue);
|
|
275
|
+
|
|
276
|
+
const localKey = (id: string): string => `${LOCAL_TABLE_KEY_PREFIX}${id}`;
|
|
277
|
+
|
|
278
|
+
const browserStorage = (): Storage | undefined =>
|
|
279
|
+
typeof localStorage === "undefined" ? undefined : localStorage;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @internal
|
|
283
|
+
* The browser-only state of a table; empty on the server, or when the
|
|
284
|
+
* storage is unavailable or holds something else.
|
|
285
|
+
*/
|
|
286
|
+
export const readLocalTableEntry = (id: string): Record<string, unknown> => {
|
|
287
|
+
try {
|
|
288
|
+
const stored = browserStorage()?.getItem(localKey(id));
|
|
289
|
+
const entry: unknown = stored ? JSON.parse(stored) : {};
|
|
290
|
+
return isBranch(entry) ? entry : {};
|
|
291
|
+
} catch {
|
|
292
|
+
return {};
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @internal
|
|
298
|
+
* Writes the browser-only state of a table, removing its key once empty. A
|
|
299
|
+
* storage that refuses the write (full, disabled) leaves the table as it is.
|
|
300
|
+
*/
|
|
301
|
+
export const writeLocalTableEntry = (
|
|
302
|
+
id: string,
|
|
303
|
+
entry: Record<string, unknown>,
|
|
304
|
+
): void => {
|
|
305
|
+
try {
|
|
306
|
+
const storage = browserStorage();
|
|
307
|
+
if (Object.keys(entry).length === 0) storage?.removeItem(localKey(id));
|
|
308
|
+
else storage?.setItem(localKey(id), JSON.stringify(entry));
|
|
309
|
+
} catch {
|
|
310
|
+
// The state is lost on the next visit, not the current one.
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/** @internal Drops the browser-only state of every table. */
|
|
315
|
+
export const clearLocalTableEntries = (): void => {
|
|
316
|
+
const storage = browserStorage();
|
|
317
|
+
if (!storage) return;
|
|
318
|
+
Object.keys(storage)
|
|
319
|
+
.filter((key) => key.startsWith(LOCAL_TABLE_KEY_PREFIX))
|
|
320
|
+
.forEach((key) => storage.removeItem(key));
|
|
321
|
+
};
|
|
@@ -65,14 +65,15 @@ const COMPARISON_RESOLVERS: Record<
|
|
|
65
65
|
* `previous-period` returns the equal-length span immediately before the
|
|
66
66
|
* range, offset by a single millisecond so the two never overlap. Ranges of
|
|
67
67
|
* whole days are measured in calendar days, so daylight saving changes don't
|
|
68
|
-
* move their midnight boundaries.
|
|
68
|
+
* move their midnight boundaries. A range with no bounds (`all`) has nothing
|
|
69
|
+
* to compare against: `null`.
|
|
69
70
|
*/
|
|
70
71
|
export function resolveComparisonRange(
|
|
71
72
|
comparison: PeriodComparison,
|
|
72
|
-
range: PeriodRange,
|
|
73
|
+
range: PeriodRange | null,
|
|
73
74
|
customRange: PeriodRange | null,
|
|
74
75
|
): PeriodRange | null {
|
|
75
|
-
if (comparison === "none") return null;
|
|
76
|
+
if (comparison === "none" || !range) return null;
|
|
76
77
|
if (comparison === "custom") return customRange;
|
|
77
78
|
return COMPARISON_RESOLVERS[comparison](range);
|
|
78
79
|
}
|
|
@@ -67,7 +67,7 @@ function endOfYear(date: Date, yearOffset = 0): Date {
|
|
|
67
67
|
|
|
68
68
|
const PRESET_RANGE_RESOLVERS: Record<
|
|
69
69
|
Exclude<PeriodPreset, "custom">,
|
|
70
|
-
(now: Date) => PeriodRange
|
|
70
|
+
(now: Date) => PeriodRange | null
|
|
71
71
|
> = {
|
|
72
72
|
"last-hour": (now) => ({ from: addHours(now, -1), to: now }),
|
|
73
73
|
"last-24h": (now) => ({ from: addDays(now, -1), to: now }),
|
|
@@ -112,6 +112,7 @@ const PRESET_RANGE_RESOLVERS: Record<
|
|
|
112
112
|
from: startOfYear(now, -1),
|
|
113
113
|
to: endOfYear(now, -1),
|
|
114
114
|
}),
|
|
115
|
+
all: () => null,
|
|
115
116
|
};
|
|
116
117
|
|
|
117
118
|
const RELATIVE_PRESETS = new Set<PeriodPreset>(["last-hour", "last-24h"]);
|
|
@@ -130,13 +131,13 @@ export function isRelativePreset(preset: PeriodPreset): boolean {
|
|
|
130
131
|
*
|
|
131
132
|
* Calendar presets snap to day boundaries (start-of-day to end-of-day), while
|
|
132
133
|
* the relative ones (`last-hour`, `last-24h`) keep instant bounds so an ops
|
|
133
|
-
* dashboard can read sub-day granularity.
|
|
134
|
+
* dashboard can read sub-day granularity. `all` has no bounds: `null`.
|
|
134
135
|
*/
|
|
135
136
|
export function resolvePresetRange(
|
|
136
137
|
preset: PeriodPreset,
|
|
137
138
|
now: Date,
|
|
138
139
|
fallbackRange: PeriodRange,
|
|
139
|
-
): PeriodRange {
|
|
140
|
+
): PeriodRange | null {
|
|
140
141
|
if (preset === "custom") return fallbackRange;
|
|
141
142
|
return PRESET_RANGE_RESOLVERS[preset](now);
|
|
142
143
|
}
|
|
@@ -12,6 +12,7 @@ export type PeriodPreset =
|
|
|
12
12
|
| "last-quarter"
|
|
13
13
|
| "ytd"
|
|
14
14
|
| "last-year"
|
|
15
|
+
| "all"
|
|
15
16
|
| "custom";
|
|
16
17
|
|
|
17
18
|
export type PeriodComparison =
|
|
@@ -28,7 +29,8 @@ export interface PeriodRange {
|
|
|
28
29
|
export interface PeriodState {
|
|
29
30
|
preset: PeriodPreset;
|
|
30
31
|
comparison: PeriodComparison;
|
|
31
|
-
|
|
32
|
+
/** The window, or `null` for `all`: every period, with no bounds. */
|
|
33
|
+
range: PeriodRange | null;
|
|
32
34
|
compareRange: PeriodRange | null;
|
|
33
35
|
key: string;
|
|
34
36
|
}
|
|
@@ -100,8 +100,8 @@ function buildKey(state: Omit<PeriodState, "key">): string {
|
|
|
100
100
|
return [
|
|
101
101
|
state.preset,
|
|
102
102
|
state.comparison,
|
|
103
|
-
state.range.from.toISOString(),
|
|
104
|
-
state.range.to.toISOString(),
|
|
103
|
+
state.range ? state.range.from.toISOString() : "-",
|
|
104
|
+
state.range ? state.range.to.toISOString() : "-",
|
|
105
105
|
state.compareRange ? state.compareRange.from.toISOString() : "-",
|
|
106
106
|
state.compareRange ? state.compareRange.to.toISOString() : "-",
|
|
107
107
|
].join("|");
|
|
@@ -67,14 +67,21 @@ export function usePeriodScope(
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* `url` with `state` as query parameters: `from` / `to`, `compareFrom` /
|
|
72
|
+
* `compareTo` when a comparison range is set, `preset` and `comparison`.
|
|
73
|
+
* Every period (`all`) has no bounds, so it sends no `from` / `to`.
|
|
74
|
+
*/
|
|
70
75
|
export function appendPeriodToUrl(
|
|
71
76
|
url: string | undefined,
|
|
72
77
|
state: PeriodState | null,
|
|
73
78
|
): string | undefined {
|
|
74
79
|
if (!url || !state) return url;
|
|
75
80
|
const params = new URLSearchParams();
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
if (state.range) {
|
|
82
|
+
params.set("from", state.range.from.toISOString());
|
|
83
|
+
params.set("to", state.range.to.toISOString());
|
|
84
|
+
}
|
|
78
85
|
if (state.compareRange) {
|
|
79
86
|
params.set("compareFrom", state.compareRange.from.toISOString());
|
|
80
87
|
params.set("compareTo", state.compareRange.to.toISOString());
|
|
@@ -12,27 +12,59 @@ export interface PageRealtimeApi {
|
|
|
12
12
|
pageId: Ref<string | undefined>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
interface PageClaim {
|
|
16
|
+
pageId: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every mounted page instance holding a page id, in the order each last set it.
|
|
21
|
+
* A page remounted at the same path (only its query changed) sets up its new
|
|
22
|
+
* instance before or after the old one is disposed, depending on the
|
|
23
|
+
* transition; the session follows the latest live claim, so neither order
|
|
24
|
+
* drops the page.
|
|
25
|
+
*/
|
|
26
|
+
const pageClaims: PageClaim[] = [];
|
|
27
|
+
|
|
28
|
+
const applyLatestClaim = (user: UserRealtimeApi): void => {
|
|
29
|
+
void user.setPage(pageClaims.at(-1)?.pageId ?? null);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const raiseClaim = (claim: PageClaim): void => {
|
|
33
|
+
const index = pageClaims.indexOf(claim);
|
|
34
|
+
if (index !== -1) pageClaims.splice(index, 1);
|
|
35
|
+
pageClaims.push(claim);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const dropClaim = (claim: PageClaim): void => {
|
|
39
|
+
const index = pageClaims.indexOf(claim);
|
|
40
|
+
if (index !== -1) pageClaims.splice(index, 1);
|
|
41
|
+
};
|
|
42
|
+
|
|
15
43
|
export function usePageRealtime(
|
|
16
44
|
pageId: MaybeRefOrGetter<string>,
|
|
17
45
|
): PageRealtimeApi {
|
|
18
46
|
const user: UserRealtimeApi = useUserRealtime();
|
|
19
47
|
const resolvedPageId = computed(() => toValue(pageId) || undefined);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
resolvedPageId
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
|
|
49
|
+
if (import.meta.env.SSR) {
|
|
50
|
+
// No dispose runs on the server, so a claim there would never be released.
|
|
51
|
+
void user.setPage(resolvedPageId.value ?? null);
|
|
52
|
+
} else {
|
|
53
|
+
const claim: PageClaim = { pageId: undefined };
|
|
54
|
+
watch(
|
|
55
|
+
resolvedPageId,
|
|
56
|
+
(next) => {
|
|
57
|
+
claim.pageId = next;
|
|
58
|
+
raiseClaim(claim);
|
|
59
|
+
applyLatestClaim(user);
|
|
60
|
+
},
|
|
61
|
+
{ immediate: true },
|
|
62
|
+
);
|
|
63
|
+
onScopeDispose(() => {
|
|
64
|
+
dropClaim(claim);
|
|
65
|
+
applyLatestClaim(user);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
36
68
|
|
|
37
69
|
return {
|
|
38
70
|
subscribe: user.subscribe,
|
|
@@ -60,6 +60,7 @@ interface UserRealtimeState {
|
|
|
60
60
|
appliedPageId: Ref<string | null>;
|
|
61
61
|
handlers: Map<string, Set<RealtimeTopicHandler>>;
|
|
62
62
|
snapshots: Map<string, RealtimeSnapshotEvent>;
|
|
63
|
+
reconciling: Promise<void>;
|
|
63
64
|
cancelConnect?: () => void;
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -247,7 +248,7 @@ const callPageEndpoint = async (
|
|
|
247
248
|
});
|
|
248
249
|
};
|
|
249
250
|
|
|
250
|
-
const
|
|
251
|
+
const applyDesiredPage = async (
|
|
251
252
|
state: UserRealtimeState,
|
|
252
253
|
baseUrl: string,
|
|
253
254
|
): Promise<void> => {
|
|
@@ -259,9 +260,24 @@ const reconcilePage = async (
|
|
|
259
260
|
// A reconnect during the call replaced the session: the page is applied to
|
|
260
261
|
// the dead one, and the new one still needs it.
|
|
261
262
|
if (state.sessionId.value !== sessionId) return;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
state.appliedPageId.value = desired;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Runs one page call at a time. Two overlapping calls would each compare the
|
|
268
|
+
* desired page with what the server held before either landed: an unsubscribe
|
|
269
|
+
* in flight followed by a subscribe to the same page would skip the
|
|
270
|
+
* subscribe, and the session would end up with no page at all.
|
|
271
|
+
*/
|
|
272
|
+
const reconcilePage = (
|
|
273
|
+
state: UserRealtimeState,
|
|
274
|
+
baseUrl: string,
|
|
275
|
+
): Promise<void> => {
|
|
276
|
+
const run = state.reconciling
|
|
277
|
+
.catch(() => undefined)
|
|
278
|
+
.then(() => applyDesiredPage(state, baseUrl));
|
|
279
|
+
state.reconciling = run;
|
|
280
|
+
return run;
|
|
265
281
|
};
|
|
266
282
|
|
|
267
283
|
/**
|
|
@@ -290,6 +306,7 @@ const buildApi = (): UserRealtimeApi => {
|
|
|
290
306
|
appliedPageId,
|
|
291
307
|
handlers: new Map(),
|
|
292
308
|
snapshots: new Map(),
|
|
309
|
+
reconciling: Promise.resolve(),
|
|
293
310
|
};
|
|
294
311
|
|
|
295
312
|
const config = useDmsRuntimeConfig();
|