@antelopejs/dms 0.7.9 → 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 +29 -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/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 +2 -0
- 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/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 +104 -98
- 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/useTableViewEmptyState.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
- 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/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-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
|
@@ -4,6 +4,8 @@ import type { LocationQuery } from "vue-router";
|
|
|
4
4
|
import { useAccumulatedPages } from "../layers/dms-ui/app/build/composables/table-view/useAccumulatedPages";
|
|
5
5
|
import { usePersistedPagination } from "../layers/dms-ui/app/build/composables/table-view/usePersistedPagination";
|
|
6
6
|
import { usePreferences } from "../layers/dms-core/app/composables/user/usePreferences";
|
|
7
|
+
import { useTablePreferences } from "../layers/dms-ui/app/build/composables/table-view/useTablePreferences";
|
|
8
|
+
import { tablePreferenceId } from "../layers/dms-core/app/build/composables/user/tablePreferences";
|
|
7
9
|
|
|
8
10
|
const PAGE_SIZE = 10;
|
|
9
11
|
const THIRD_PAGE = 2;
|
|
@@ -16,19 +18,18 @@ const hiddenFiltersOf = (query: () => LocationQuery) =>
|
|
|
16
18
|
: [{ accessorKey: "status", mode: "is", value: query().status }],
|
|
17
19
|
);
|
|
18
20
|
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
+
const COMPONENT_ID = "tasks";
|
|
22
|
+
const PAGE_ID = "query-filters";
|
|
21
23
|
|
|
22
|
-
// The `
|
|
23
|
-
let cookie: Ref<
|
|
24
|
+
// The `dms-preferences` cookie, surviving the remounts of a page.
|
|
25
|
+
let cookie: Ref<string>;
|
|
24
26
|
|
|
25
27
|
// What a table view does on mount: its page, read back from the cookie,
|
|
26
28
|
// under the hidden filters the URL sets.
|
|
27
29
|
const mountTable = (query: Ref<LocationQuery>) => {
|
|
28
30
|
const hiddenFilters = hiddenFiltersOf(() => query.value);
|
|
29
31
|
const pagination = usePersistedPagination({
|
|
30
|
-
preferences:
|
|
31
|
-
preferenceKey,
|
|
32
|
+
preferences: useTablePreferences(COMPONENT_ID, PAGE_ID),
|
|
32
33
|
defaults: { pageIndex: 0, pageSize: PAGE_SIZE },
|
|
33
34
|
scope: () => [hiddenFilters.value, []],
|
|
34
35
|
});
|
|
@@ -36,8 +37,9 @@ const mountTable = (query: Ref<LocationQuery>) => {
|
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
beforeEach(() => {
|
|
39
|
-
cookie = ref(
|
|
40
|
+
cookie = ref("");
|
|
40
41
|
vi.stubGlobal("useDmsCookie", () => cookie);
|
|
42
|
+
vi.stubGlobal("usePreferences", usePreferences);
|
|
41
43
|
vi.stubGlobal("ref", ref);
|
|
42
44
|
vi.stubGlobal("readonly", readonly);
|
|
43
45
|
vi.stubGlobal("computed", computed);
|
|
@@ -100,16 +102,11 @@ describe("a hidden filter changed by the URL", () => {
|
|
|
100
102
|
expect(again.pagination.value.pageIndex).toBe(THIRD_PAGE);
|
|
101
103
|
});
|
|
102
104
|
|
|
103
|
-
it("keeps a page stored
|
|
104
|
-
|
|
105
|
-
tables
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
pagination: { pageIndex: THIRD_PAGE, pageSize: PAGE_SIZE },
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
};
|
|
105
|
+
it("keeps a page stored without its hidden filters", () => {
|
|
106
|
+
usePreferences().setPreference(
|
|
107
|
+
`tables.${tablePreferenceId(COMPONENT_ID, PAGE_ID)}`,
|
|
108
|
+
{ p: [PAGE_SIZE, THIRD_PAGE] },
|
|
109
|
+
);
|
|
113
110
|
const table = mountTable(ref<LocationQuery>({}));
|
|
114
111
|
expect(table.pagination.value.pageIndex).toBe(THIRD_PAGE);
|
|
115
112
|
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pinnedRows`: the rows a rule accepts lead the list whatever the sort — the
|
|
3
|
+
* base language of a languages table stays on top whichever column the user
|
|
4
|
+
* sorts on, and on the first page.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { describe, expect, it, vi } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
processSourceRows,
|
|
11
|
+
useTableRows,
|
|
12
|
+
} from "../layers/dms-ui/app/build/composables/table-view/useTableRows";
|
|
13
|
+
import { pinRows } from "../layers/dms-ui/app/build/composables/table-view/utils/pinnedRows";
|
|
14
|
+
import type { TableViewColumn } from "../layers/dms-ui/app/composables/table-view/types";
|
|
15
|
+
|
|
16
|
+
interface LanguageRow extends Record<string, unknown> {
|
|
17
|
+
_id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
coverage: number;
|
|
20
|
+
isBase: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LANGUAGES: LanguageRow[] = [
|
|
24
|
+
{ _id: "de", name: "Deutsch", coverage: 40, isBase: false },
|
|
25
|
+
{ _id: "en", name: "English", coverage: 100, isBase: true },
|
|
26
|
+
{ _id: "fr", name: "Français", coverage: 90, isBase: false },
|
|
27
|
+
{ _id: "es", name: "Español", coverage: 10, isBase: false },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const COLUMNS = [
|
|
31
|
+
{ id: "name", accessorKey: "name" },
|
|
32
|
+
{ id: "coverage", accessorKey: "coverage" },
|
|
33
|
+
] as TableViewColumn[];
|
|
34
|
+
|
|
35
|
+
const BASE = { field: "isBase", equals: true };
|
|
36
|
+
|
|
37
|
+
type Api = Parameters<typeof useTableRows>[0]["api"];
|
|
38
|
+
|
|
39
|
+
const ids = (rows: LanguageRow[]) => rows.map((row) => row._id);
|
|
40
|
+
|
|
41
|
+
describe("pinRows", () => {
|
|
42
|
+
it("puts the rows the rule accepts first, each part in its order", () => {
|
|
43
|
+
expect(
|
|
44
|
+
ids(pinRows(LANGUAGES, { field: "coverage", in: [10, 100] })),
|
|
45
|
+
).toEqual(["en", "es", "de", "fr"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("leaves the rows as they are without a rule, or when none matches", () => {
|
|
49
|
+
expect(pinRows(LANGUAGES, undefined)).toBe(LANGUAGES);
|
|
50
|
+
expect(pinRows(LANGUAGES, { field: "_id", equals: "it" })).toBe(LANGUAGES);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("a source sorted in the browser", () => {
|
|
55
|
+
it("keeps the pinned row first in either direction", () => {
|
|
56
|
+
const ascending = processSourceRows(
|
|
57
|
+
LANGUAGES,
|
|
58
|
+
{ sortKey: "coverage", sortDirection: "asc" },
|
|
59
|
+
{},
|
|
60
|
+
COLUMNS,
|
|
61
|
+
BASE,
|
|
62
|
+
);
|
|
63
|
+
const descending = processSourceRows(
|
|
64
|
+
LANGUAGES,
|
|
65
|
+
{ sortKey: "name", sortDirection: "desc" },
|
|
66
|
+
{},
|
|
67
|
+
COLUMNS,
|
|
68
|
+
BASE,
|
|
69
|
+
);
|
|
70
|
+
expect(ids(ascending.results)).toEqual(["en", "es", "de", "fr"]);
|
|
71
|
+
expect(ids(descending.results)).toEqual(["en", "fr", "es", "de"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("pins before the page is cut: the pinned row opens the first page", () => {
|
|
75
|
+
const query = { sortKey: "coverage", sortDirection: "asc", limit: 2 };
|
|
76
|
+
const first = processSourceRows(LANGUAGES, query, {}, COLUMNS, BASE);
|
|
77
|
+
const second = processSourceRows(
|
|
78
|
+
LANGUAGES,
|
|
79
|
+
{ ...query, offset: 2 },
|
|
80
|
+
{},
|
|
81
|
+
COLUMNS,
|
|
82
|
+
BASE,
|
|
83
|
+
);
|
|
84
|
+
expect(ids(first.results)).toEqual(["en", "es"]);
|
|
85
|
+
expect(ids(second.results)).toEqual(["de", "fr"]);
|
|
86
|
+
expect(first.total).toBe(4);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("rows a route sorts", () => {
|
|
91
|
+
it("pins within the list a source route sorted itself", async () => {
|
|
92
|
+
const sortedByRoute = [...LANGUAGES].sort(
|
|
93
|
+
(a, b) => a.coverage - b.coverage,
|
|
94
|
+
);
|
|
95
|
+
const api = vi.fn(async () => ({ results: sortedByRoute, total: 4 }));
|
|
96
|
+
const rows = useTableRows<LanguageRow>({
|
|
97
|
+
api: api as unknown as Api,
|
|
98
|
+
location: "/api/languages",
|
|
99
|
+
source: { fetchUrl: "/api/languages", capabilities: { sort: true } },
|
|
100
|
+
columns: COLUMNS,
|
|
101
|
+
pinnedRows: () => BASE,
|
|
102
|
+
});
|
|
103
|
+
const page = await rows.list({ sortKey: "coverage", sortDirection: "asc" });
|
|
104
|
+
expect(ids(page!.results)).toEqual(["en", "es", "de", "fr"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("pins within the page a controller answers, read on each request", async () => {
|
|
108
|
+
const api = vi.fn(async () => ({
|
|
109
|
+
results: LANGUAGES,
|
|
110
|
+
total: 40,
|
|
111
|
+
offset: 0,
|
|
112
|
+
limit: 4,
|
|
113
|
+
}));
|
|
114
|
+
let pinned: typeof BASE | undefined = BASE;
|
|
115
|
+
const rows = useTableRows<LanguageRow>({
|
|
116
|
+
api: api as unknown as Api,
|
|
117
|
+
location: "/api/languages",
|
|
118
|
+
columns: COLUMNS,
|
|
119
|
+
pinnedRows: () => pinned,
|
|
120
|
+
});
|
|
121
|
+
expect(ids((await rows.list({}))!.results)).toEqual([
|
|
122
|
+
"en",
|
|
123
|
+
"de",
|
|
124
|
+
"fr",
|
|
125
|
+
"es",
|
|
126
|
+
]);
|
|
127
|
+
pinned = undefined;
|
|
128
|
+
expect(ids((await rows.list({}))!.results)).toEqual(ids(LANGUAGES));
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("hands the table view's pinnedRows to its rows, except while grouped", () => {
|
|
133
|
+
const source = readFileSync(
|
|
134
|
+
resolve(
|
|
135
|
+
__dirname,
|
|
136
|
+
"../layers/dms-ui/app/components/table-view/TableView.vue",
|
|
137
|
+
),
|
|
138
|
+
"utf8",
|
|
139
|
+
);
|
|
140
|
+
expect(source).toMatch(
|
|
141
|
+
/pinnedRows: \(\) => \(isGroupedDisplay\.value \? undefined : props\.pinnedRows\)/,
|
|
142
|
+
);
|
|
143
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
1
3
|
import { describe, expect, it, vi } from "vitest";
|
|
2
4
|
import enGB from "../layers/dms-ui/i18n/locales/ui-en-GB.json";
|
|
3
5
|
import frFR from "../layers/dms-ui/i18n/locales/ui-fr-FR.json";
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
sanitizeSorting,
|
|
14
16
|
sortableColumnIds,
|
|
15
17
|
sortDirectionLabelKey,
|
|
18
|
+
sortingOrDefault,
|
|
16
19
|
sortValueKind,
|
|
17
20
|
} from "../layers/dms-ui/app/build/composables/table-view/utils/sortableColumns";
|
|
18
21
|
|
|
@@ -92,6 +95,48 @@ describe("sanitizeSorting", () => {
|
|
|
92
95
|
});
|
|
93
96
|
});
|
|
94
97
|
|
|
98
|
+
describe("sortingOrDefault", () => {
|
|
99
|
+
const byDueDate = [{ id: "due_date", desc: true }];
|
|
100
|
+
|
|
101
|
+
it("keeps a sort the route serves", () => {
|
|
102
|
+
const sorting = [{ id: "completion_percentage", desc: false }];
|
|
103
|
+
expect(sortingOrDefault(sorting, sortable, byDueDate)).toEqual(sorting);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Clearing the sort (a third click on a header) used to be remembered as
|
|
107
|
+
// `[]`: the table then listed in the database's natural order for good.
|
|
108
|
+
it("goes back to the default sort once the user clears theirs", () => {
|
|
109
|
+
expect(sortingOrDefault([], sortable, byDueDate)).toEqual(byDueDate);
|
|
110
|
+
expect(sortingOrDefault(null, sortable, byDueDate)).toEqual(byDueDate);
|
|
111
|
+
expect(
|
|
112
|
+
sortingOrDefault([{ id: "name", desc: false }], sortable, byDueDate),
|
|
113
|
+
).toEqual(byDueDate);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("stays unsorted without a default, or when the route refuses it", () => {
|
|
117
|
+
expect(sortingOrDefault([], sortable, [])).toEqual([]);
|
|
118
|
+
expect(
|
|
119
|
+
sortingOrDefault([], new Set(["completion_percentage"]), byDueDate),
|
|
120
|
+
).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("is what the table view restores, and corrects, its sort with", () => {
|
|
124
|
+
const tableView = readFileSync(
|
|
125
|
+
resolve(
|
|
126
|
+
__dirname,
|
|
127
|
+
"../layers/dms-ui/app/components/table-view/TableView.vue",
|
|
128
|
+
),
|
|
129
|
+
"utf8",
|
|
130
|
+
);
|
|
131
|
+
expect(tableView).toMatch(
|
|
132
|
+
/const sorting = ref<SortingState>\(\s*sortingOrDefault\(/,
|
|
133
|
+
);
|
|
134
|
+
expect(tableView).toMatch(
|
|
135
|
+
/const sanitized = sortingOrDefault\(\s*next,\s*sortableIds\.value,\s*defaultSortState,\s*\);/,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
95
140
|
describe("resolveDefaultSort", () => {
|
|
96
141
|
it("uses a sortable default sort", () => {
|
|
97
142
|
expect(
|
|
@@ -15,6 +15,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
15
15
|
import TableViews from "../layers/dms-ui/app/build/components/table/Views.vue";
|
|
16
16
|
import { mergeColumnOrder } from "../layers/dms-ui/app/build/composables/table/utils/columnOrder";
|
|
17
17
|
import { useTableViews } from "../layers/dms-ui/app/build/composables/table-view/useTableViews";
|
|
18
|
+
import type { TablePreferences } from "../layers/dms-ui/app/build/composables/table-view/useTablePreferences";
|
|
18
19
|
import {
|
|
19
20
|
isSameTableState,
|
|
20
21
|
readTableUrlKey,
|
|
@@ -211,12 +212,22 @@ describe("useTableViews", () => {
|
|
|
211
212
|
const preferences = new Map<string, unknown>(
|
|
212
213
|
Object.entries(options.preferences ?? {}),
|
|
213
214
|
);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
preferences.
|
|
219
|
-
|
|
215
|
+
// `useTablePreferences`: the browser-only fields come back once mounted.
|
|
216
|
+
const restores: Array<() => void> = [];
|
|
217
|
+
const tablePreferences: TablePreferences = {
|
|
218
|
+
get: <V>(field: string, fallback: V) =>
|
|
219
|
+
(preferences.has(field) ? preferences.get(field) : fallback) as V,
|
|
220
|
+
set: (field, value) => {
|
|
221
|
+
preferences.set(field, value);
|
|
222
|
+
},
|
|
223
|
+
restore: (field, state) => {
|
|
224
|
+
restores.push(() => {
|
|
225
|
+
if (preferences.has(field)) {
|
|
226
|
+
state.value = preferences.get(field) as typeof state.value;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
};
|
|
220
231
|
vi.stubGlobal("useUserSession", () => ({
|
|
221
232
|
user: ref({ _id: options.userId ?? "u1" }),
|
|
222
233
|
}));
|
|
@@ -244,11 +255,15 @@ describe("useTableViews", () => {
|
|
|
244
255
|
app = createApp(
|
|
245
256
|
defineComponent({
|
|
246
257
|
setup() {
|
|
258
|
+
onMounted(() => restores.forEach((restore) => restore()));
|
|
259
|
+
// As the table view does, before its views.
|
|
260
|
+
tablePreferences.restore("density", state.density);
|
|
261
|
+
tablePreferences.restore("columnOrder", state.columnOrder);
|
|
247
262
|
api = useTableViews({
|
|
248
263
|
views: options.views,
|
|
249
264
|
defaults: DEFAULTS,
|
|
250
265
|
state,
|
|
251
|
-
|
|
266
|
+
preferences: tablePreferences,
|
|
252
267
|
urlScope: { tableId: "content", isSoleTableView: true },
|
|
253
268
|
});
|
|
254
269
|
return () => h("div");
|
|
@@ -300,7 +315,7 @@ describe("useTableViews", () => {
|
|
|
300
315
|
const { api, state } = await mountViews({
|
|
301
316
|
views: VIEWS,
|
|
302
317
|
query: { view: "mine" },
|
|
303
|
-
preferences: {
|
|
318
|
+
preferences: { activeView: "all" },
|
|
304
319
|
});
|
|
305
320
|
expect(api.activeViewId.value).toBe("mine");
|
|
306
321
|
expect(state.columnFilters.value).toEqual([
|
|
@@ -323,13 +338,43 @@ describe("useTableViews", () => {
|
|
|
323
338
|
]);
|
|
324
339
|
});
|
|
325
340
|
|
|
341
|
+
it("opens a user view the URL names once its views are restored in the browser", async () => {
|
|
342
|
+
const { api } = await mountViews({
|
|
343
|
+
views: VIEWS,
|
|
344
|
+
query: { view: "user-mine" },
|
|
345
|
+
preferences: {
|
|
346
|
+
userViews: [
|
|
347
|
+
{ id: "user-mine", label: "Mine", ownerId: "u1", search: "x" },
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
expect(api.activeViewId.value).toBe("user-mine");
|
|
352
|
+
expect(toasts).toEqual([]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("restores the browser-only state, a view opened on arrival keeping its own", async () => {
|
|
356
|
+
const opened = await mountViews({
|
|
357
|
+
views: VIEWS,
|
|
358
|
+
query: { view: "all" },
|
|
359
|
+
preferences: { density: "compact" },
|
|
360
|
+
});
|
|
361
|
+
expect(opened.state.density.value).toBe("default");
|
|
362
|
+
app?.unmount();
|
|
363
|
+
|
|
364
|
+
const kept = await mountViews({
|
|
365
|
+
views: VIEWS,
|
|
366
|
+
preferences: { density: "compact", activeView: "" },
|
|
367
|
+
});
|
|
368
|
+
expect(kept.state.density.value).toBe("compact");
|
|
369
|
+
});
|
|
370
|
+
|
|
326
371
|
it("opens the default view on a first visit only", async () => {
|
|
327
372
|
const first = await mountViews({ views: VIEWS });
|
|
328
373
|
expect(first.api.activeViewId.value).toBe("all");
|
|
329
374
|
app?.unmount();
|
|
330
375
|
const later = await mountViews({
|
|
331
376
|
views: VIEWS,
|
|
332
|
-
preferences: {
|
|
377
|
+
preferences: { activeView: "" },
|
|
333
378
|
});
|
|
334
379
|
expect(later.api.activeViewId.value).toBe(undefined);
|
|
335
380
|
});
|
|
@@ -354,9 +399,7 @@ describe("useTableViews", () => {
|
|
|
354
399
|
views: VIEWS,
|
|
355
400
|
viewName: " Printers ",
|
|
356
401
|
preferences: {
|
|
357
|
-
|
|
358
|
-
{ id: "user-x", label: "Theirs", ownerId: "u2" },
|
|
359
|
-
],
|
|
402
|
+
userViews: [{ id: "user-x", label: "Theirs", ownerId: "u2" }],
|
|
360
403
|
},
|
|
361
404
|
});
|
|
362
405
|
state.globalFilter.value = "printer";
|
|
@@ -373,9 +416,7 @@ describe("useTableViews", () => {
|
|
|
373
416
|
"i18n:tickets.mine",
|
|
374
417
|
"Printers",
|
|
375
418
|
]);
|
|
376
|
-
expect(
|
|
377
|
-
(preferences.get("tables.content.page.userViews") as unknown[]).length,
|
|
378
|
-
).toBe(2);
|
|
419
|
+
expect((preferences.get("userViews") as unknown[]).length).toBe(2);
|
|
379
420
|
|
|
380
421
|
state.density.value = "compact";
|
|
381
422
|
await nextTick();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A TopListCard writes its unit after each formatted value (`valueSuffix`),
|
|
3
|
+
* and a row's `valueLabel` replaces the formatted value outright.
|
|
4
|
+
*/
|
|
5
|
+
import { createSSRApp, defineComponent, h, ref } from "vue";
|
|
6
|
+
import { renderToString } from "vue/server-renderer";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import TopListCard from "../layers/dms-ui/app/components/top-list/TopListCard.vue";
|
|
9
|
+
|
|
10
|
+
const rows = ref<Array<Record<string, unknown>>>([]);
|
|
11
|
+
|
|
12
|
+
vi.mock("../layers/dms-ui/app/composables/chart/useChartFetch", () => ({
|
|
13
|
+
useChartFetch: () => ({
|
|
14
|
+
data: ref({ items: rows.value }),
|
|
15
|
+
isLoading: ref(false),
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const Slot = defineComponent({
|
|
20
|
+
setup:
|
|
21
|
+
(_, { slots }) =>
|
|
22
|
+
() =>
|
|
23
|
+
h("div", slots.default?.()),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
async function render(props: Record<string, unknown>): Promise<string> {
|
|
27
|
+
const app = createSSRApp({
|
|
28
|
+
render: () =>
|
|
29
|
+
h(TopListCard, {
|
|
30
|
+
title: "Slowest routes",
|
|
31
|
+
componentId: "slowest",
|
|
32
|
+
pageId: "overview",
|
|
33
|
+
showDelta: false,
|
|
34
|
+
valuePrecision: 0,
|
|
35
|
+
...props,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
for (const name of [
|
|
39
|
+
"DmsCard",
|
|
40
|
+
"DmsLink",
|
|
41
|
+
"UBadge",
|
|
42
|
+
"UIcon",
|
|
43
|
+
"USkeleton",
|
|
44
|
+
"UButton",
|
|
45
|
+
]) {
|
|
46
|
+
app.component(name, Slot);
|
|
47
|
+
}
|
|
48
|
+
return renderToString(app);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The text of each row's value cell. */
|
|
52
|
+
function values(html: string): string[] {
|
|
53
|
+
return [...html.matchAll(/<span class="text-toned[^"]*"[^>]*>([^<]*)</g)].map(
|
|
54
|
+
(match) => (match[1] ?? "").trim(),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
rows.value = [
|
|
60
|
+
{ id: "a", title: "GET /orders", value: 1864 },
|
|
61
|
+
{ id: "b", title: "GET /cart", value: 296 },
|
|
62
|
+
];
|
|
63
|
+
vi.stubGlobal("useI18n", () => ({ locale: ref("en-US"), t: String }));
|
|
64
|
+
vi.stubGlobal("useTranslation", () => ({
|
|
65
|
+
processI18n: (text: string) => (text.startsWith("$") ? `t(${text})` : text),
|
|
66
|
+
}));
|
|
67
|
+
vi.stubGlobal("useComponentEvent", () => ({}));
|
|
68
|
+
vi.stubGlobal("useWatch", () => ({ state: ref({}) }));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
72
|
+
|
|
73
|
+
describe("TopListCard value unit", () => {
|
|
74
|
+
it("writes the bare formatted value by default", async () => {
|
|
75
|
+
expect(values(await render({}))).toEqual(["1,864", "296"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("writes the suffix right after each value", async () => {
|
|
79
|
+
expect(values(await render({ valueSuffix: " ms" }))).toEqual([
|
|
80
|
+
"1,864 ms",
|
|
81
|
+
"296 ms",
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("translates a `$` suffix", async () => {
|
|
86
|
+
expect(values(await render({ valueSuffix: "$units.ms" }))).toEqual([
|
|
87
|
+
"1,864t($units.ms)",
|
|
88
|
+
"296t($units.ms)",
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("writes a row's valueLabel as is, without the suffix", async () => {
|
|
93
|
+
rows.value = [
|
|
94
|
+
{
|
|
95
|
+
id: "a",
|
|
96
|
+
title: "GET /orders",
|
|
97
|
+
value: 1864,
|
|
98
|
+
valueLabel: "1.9 s (partial)",
|
|
99
|
+
},
|
|
100
|
+
{ id: "b", title: "GET /cart", value: 296 },
|
|
101
|
+
];
|
|
102
|
+
expect(values(await render({ valueSuffix: " ms" }))).toEqual([
|
|
103
|
+
"1.9 s (partial)",
|
|
104
|
+
"296 ms",
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -2,7 +2,7 @@ import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
|
|
5
|
-
const EXPECTED_VUE_FILES =
|
|
5
|
+
const EXPECTED_VUE_FILES = 278;
|
|
6
6
|
|
|
7
7
|
function walk(directory: string): string[] {
|
|
8
8
|
return readdirSync(directory).flatMap((name) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/AntelopeJS/dms.git"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@antelopejs/interface-data-api": ">=0.2.0 <1.0.0",
|
|
38
38
|
"@antelopejs/interface-database": ">=0.1.8 <1.0.0",
|
|
39
39
|
"@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
|
|
40
|
-
"@antelopejs/interface-dms": ">=0.5.
|
|
40
|
+
"@antelopejs/interface-dms": ">=0.5.6 <0.6.0",
|
|
41
41
|
"@antelopejs/interface-email": ">=0.0.5 <1.0.0",
|
|
42
42
|
"@antelopejs/interface-file-storage": ">=0.1.3 <1.0.0",
|
|
43
43
|
"archiver": "^7.0.1",
|
package/skills/dms/SKILL.md
CHANGED
|
@@ -39,9 +39,15 @@ For the framework underneath (the core, `ajs` CLI, interfaces as versioned contr
|
|
|
39
39
|
(which keeps its period in the page URL, `?<id>.period=`), and
|
|
40
40
|
`realtimeTopic` re-fetches a chart, list block, `Meter`, `Tab` badges or `TableView` on each `PublishMessage`.
|
|
41
41
|
A monitoring page that must not refetch on every event declares `live: { maxRate }` in its page options
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`
|
|
42
|
+
(`slowRateAfter: { period: "7d", maxRate: 60 }` for long periods, `rememberKey: "<module>"` to share the
|
|
43
|
+
choice between pages; `maxRate` is also the polling interval while the stream is down) and places a
|
|
44
|
+
`LiveToggle()` (Live / Pause, L key; `variant: "pill"`, `missedLabel` for "N new requests").
|
|
45
|
+
`liveHold: true` keeps a table's rows while paused; `liveHold: { whileRowOpen, whileScrolled }` also
|
|
46
|
+
holds the table alone. Paused, an event counts as its `payload.ids` rows or its `payload.count`, so a
|
|
47
|
+
module may batch publishes with `{ count }`. A hidden tab and `?live=0` pause the page too.
|
|
48
|
+
A custom component follows that state with `usePageLive()`, holds the page with `hold(reason)` /
|
|
49
|
+
`release()` and adds a breakdown with `setMissedDetail()`; `refreshPageBlocks()` (user actions) always
|
|
50
|
+
reads, `refreshLivePageBlocks()` (timers, feeds) is held while paused.
|
|
45
51
|
A status the backend computes (health, outage) is a `Banner({ fetchUrl })`: the route answers
|
|
46
52
|
a `BannerContent` (its buttons may run `api` / modal / quick-action targets with `confirm`)
|
|
47
53
|
or `null` for no banner — the block then draws nothing.
|
|
@@ -82,6 +82,11 @@ releases the frontend module runs on go in the frontend module's own `package.js
|
|
|
82
82
|
`"engines": { "@antelopejs/dms-frontend": ">=0.4.0 <0.5.0" }`: the loader refuses the module
|
|
83
83
|
outside that range. Cap it below the next loader minor while the loader is 0.x.
|
|
84
84
|
|
|
85
|
+
An absolute link (a public file, a snippet to copy, a mail) is built on `await GetApiBaseUrl()`
|
|
86
|
+
(`@antelopejs/interface-dms/api-base-url`, the backend) or `await GetClientBaseUrl()`
|
|
87
|
+
(`/client-base-url`, the dashboard), never on `Host` / `X-Forwarded-*`: a reverse proxy may not
|
|
88
|
+
forward them. Both answer the DMS config (`apiBaseUrl`, `clientBaseUrl`).
|
|
89
|
+
|
|
85
90
|
## Navigation & access — module pages are owner-only
|
|
86
91
|
|
|
87
92
|
Three constraints are enforced at registration: a page's `category` must **descend from the
|
|
@@ -9,7 +9,7 @@ Main builders: `Form`; `TableView(controller, options)`; the charts (`ChartLine`
|
|
|
9
9
|
`ChartArea`, `ChartBar`, `ChartColumn`, `ChartDonut`, `ChartPie`, `ChartScatter`,
|
|
10
10
|
`ChartMixed`, `ChartRadar`, `ChartRadialBar`, `ChartHeatmap`, `ChartCandlestick`, usually
|
|
11
11
|
inside `ChartCard`); layout `Grid` + `GridRow`, `HStack` / `VStack` / `Spacer`, `Tab`,
|
|
12
|
-
`Placeholder`; dashboard widgets `KpiCard`, `TopListCard`, `PeriodSelector`; display blocks
|
|
12
|
+
`Guard`, `Placeholder`; dashboard widgets `KpiCard`, `TopListCard`, `PeriodSelector`; display blocks
|
|
13
13
|
`StatGroup`, `KeyValueList`, `NavCardGrid`, `EmptyState` (its `code` shows a snippet to
|
|
14
14
|
start with), `Banner`, `Card`, and `CodeBlock` (read-only highlighted code with a copy
|
|
15
15
|
button: `code` + `language`, or `fetchUrl` answering `{ code, language? }` — see
|
|
@@ -19,6 +19,15 @@ button: `code` + `language`, or `fetchUrl` answering `{ code, language? }` — s
|
|
|
19
19
|
behavioral DSL — permissioned `.action()`s and `.watch()` reactivity — see
|
|
20
20
|
`docs/02.building/05.actions-and-reactivity.md`.
|
|
21
21
|
|
|
22
|
+
`Guard({ fetchUrl, field?, when?, fallback? })` wraps any blocks and shows one `EmptyState`
|
|
23
|
+
(`fallback`) in their place when the route says there is nothing: `when: "empty"` (default)
|
|
24
|
+
checks the value at `field` (absent / `null` / `false` / `0` / `""` / `[]`), `when: "notFound"`
|
|
25
|
+
a 404. The children show while loading and on any other failure; an `emptyState` in the
|
|
26
|
+
answer is laid over `fallback` — `docs/04.components/03.layout-and-containers.md#guard`.
|
|
27
|
+
A block whose `fetchUrl` answers 404 shows its own "Not found" (no retry), the 404 body's
|
|
28
|
+
`message` / `emptyState` over it; a `Guard` with `when: "notFound"` draws the same state once
|
|
29
|
+
for the blocks it wraps — `docs/04.components/11.display-blocks.md#not-found`.
|
|
30
|
+
|
|
22
31
|
A `TableView` scoped by the page URL takes `queryParamFilters: { status: { field: "status" } }`
|
|
23
32
|
(hidden filters read from `?status=`; `routeParamFilters` for slug params like `:id`). No
|
|
24
33
|
glue is needed around it: an active query filter shows the `filtered` empty state, and a
|
|
@@ -26,16 +35,28 @@ link changing its value sends the table back to page 1. A drawer or modal row ac
|
|
|
26
35
|
gets `navigation` (J / K, `index` / `total` in the order shown) —
|
|
27
36
|
`docs/04.components/06.tables/8.row-drawers.md`.
|
|
28
37
|
|
|
38
|
+
A page's `headerActions` follow its URL: a target URL takes `{{params.X}}` / `{{query.X}}`
|
|
39
|
+
(`?workspace={{query.workspace}}`, a pair the page has no value for is dropped), and an
|
|
40
|
+
`availability` reads `context.query`, asked again when the query changes — no need to build
|
|
41
|
+
the header in Vue with `usePageHeaderActions` to carry the scope —
|
|
42
|
+
`docs/07.interfaces/3.base/6.layout.md#buttons-that-follow-the-page-url`.
|
|
43
|
+
|
|
29
44
|
A button or row action (`customButtons`, `rowActions.custom`, a page's `headerActions`)
|
|
30
45
|
points at an `ActionTarget`. A `drawer` target opens from the bottom; give it
|
|
31
46
|
`direction: "right"` for a side sheet (an inspector, a detail panel) instead of hosting your
|
|
32
47
|
own drawer in a `CustomComponent`.
|
|
33
48
|
To read rows one after the other next to the table (a request log), use a `sidePanel`
|
|
34
49
|
target instead: a non-modal panel docked on the right whose header the DMS draws (title,
|
|
35
|
-
`positionLabel` "x of y", previous / next, J / K, close), `?record=` kept by default (`deepLink: false` opts out) and the table's
|
|
50
|
+
`rowTitle` naming the row from its fields, `positionLabel` "x of y", previous / next, J / K, close), `?record=` kept by default (`deepLink: false` opts out), a "not found" state on a 404 (`notFound: { title, description }`) and the table's
|
|
36
51
|
`rowClick: "single"` (a `TableView.fromSource` table reads a deep-linked row beyond its first page through `fetchRowUrl: "/api/x/{id}"`) — never bridge the table's DOM to build one —
|
|
37
52
|
`docs/04.components/06.tables/8.row-drawers.md#side-panel`.
|
|
38
53
|
|
|
54
|
+
A row that must stay on top of a table under any sort (a base language, a default
|
|
55
|
+
workspace) takes `pinnedRows: { field: "isBase", equals: true }` — never sort on the
|
|
56
|
+
server for it. A row action names its row with `{field}` braces in its `label`
|
|
57
|
+
(`"Translate {name}"`), and `rowActions.menuLabel` (`"Actions for {name}"`) names the
|
|
58
|
+
row's ⋯ button — `docs/04.components/06.tables/0.index.md#pinned-rows`.
|
|
59
|
+
|
|
39
60
|
## DataTypes
|
|
40
61
|
|
|
41
62
|
A field's `type` is a **DataType instance**: `new DefaultDataTypes.StringType({ … })`. The
|