@antelopejs/dms 0.7.0 → 0.7.2
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 +43 -0
- package/dist/hooks/tenant-instances.d.ts +1 -0
- package/dist/hooks/tenant-instances.js +13 -0
- package/dist/hooks/tenant-instances.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/test/unit/interfaces/dms/page-topics.test.js +67 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js +1 -0
- package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js +46 -0
- package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/column-display.test.js +11 -0
- package/dist/test/unit/interfaces/dms-base/column-display.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js +31 -0
- package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js.map +1 -1
- package/dist/test/unit/utils/tenant-instances.test.d.ts +1 -0
- package/dist/test/unit/utils/tenant-instances.test.js +53 -0
- package/dist/test/unit/utils/tenant-instances.test.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/tenant-instances.d.ts +2 -0
- package/dist/utils/tenant-instances.js +38 -0
- package/dist/utils/tenant-instances.js.map +1 -0
- package/dist/validation/auth.schema.d.ts +2 -2
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +4 -0
- package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +35 -23
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockActions.vue +34 -6
- package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +26 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +3 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/MonoCell.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +24 -13
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +12 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useRefreshTriggers.ts +40 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +15 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +85 -22
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +10 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +94 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +41 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +3 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeEditor.ts +2 -13
- package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +59 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeLanguages.ts +36 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codePalette.ts +32 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +77 -20
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +15 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +111 -25
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +95 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/EmptyStateBlock.vue +18 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +8 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +21 -9
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +8 -0
- package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +97 -0
- package/frontend-vue/layers/dms-ui/app/components/empty-state/EmptyState.vue +13 -4
- package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +14 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +2 -8
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +19 -0
- package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +54 -48
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +24 -9
- package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +66 -74
- package/frontend-vue/layers/dms-ui/app/components/top-list/internal/TopListRow.vue +4 -3
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +5 -2
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +2 -29
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +2 -1
- package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +4 -0
- package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +2 -2
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
- package/frontend-vue/package.json +2 -2
- package/frontend-vue/pnpm-lock.yaml +5 -5
- package/frontend-vue/tests/activity-feed-days.test.ts +26 -12
- package/frontend-vue/tests/banner-block-fetch.test.ts +162 -0
- package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +100 -0
- package/frontend-vue/tests/block-items-period-realtime.test.ts +147 -0
- package/frontend-vue/tests/block-route-params.test.ts +48 -0
- package/frontend-vue/tests/code-block.test.ts +155 -0
- package/frontend-vue/tests/code-highlight.test.ts +72 -0
- package/frontend-vue/tests/composed-text-blocks.test.ts +5 -0
- package/frontend-vue/tests/composed-text-more-blocks.test.ts +391 -0
- package/frontend-vue/tests/drawer-target-direction.test.ts +205 -0
- package/frontend-vue/tests/empty-state-late-slots.test.ts +53 -0
- package/frontend-vue/tests/flow-canvas-delete-badge.test.ts +88 -0
- package/frontend-vue/tests/form-redirect-placeholders.test.ts +11 -0
- package/frontend-vue/tests/key-value-list-copy.test.ts +114 -0
- package/frontend-vue/tests/master-detail-layout.test.ts +53 -0
- package/frontend-vue/tests/mono-cell-truncate.test.ts +50 -0
- package/frontend-vue/tests/nav-card-grid-empty-category.test.ts +1 -0
- package/frontend-vue/tests/page-fill-height.test.ts +20 -3
- package/frontend-vue/tests/route-param-occurrences.test.ts +9 -0
- package/frontend-vue/tests/table-actions-search-label.test.ts +147 -0
- package/frontend-vue/tests/table-clear-filters.test.ts +22 -0
- package/frontend-vue/tests/table-empty-body-viewport.test.ts +25 -0
- package/frontend-vue/tests/table-metric-cells.test.ts +22 -0
- package/frontend-vue/tests/table-pills-cell.test.ts +126 -0
- package/frontend-vue/tests/table-source-navigation.test.ts +123 -0
- package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +137 -0
- package/frontend-vue/tests/table-view-refresh.test.ts +84 -0
- package/frontend-vue/tests/table-view-sources.test.ts +67 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +14 -3
- package/skills/dms-pages/REFERENCE.md +17 -1
|
@@ -21,6 +21,12 @@ export interface TableNarrowingState {
|
|
|
21
21
|
* search neither narrows the rows nor needs offering to clear.
|
|
22
22
|
*/
|
|
23
23
|
searchApplies?: Ref<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Filters the table applies without showing them (the page URL's
|
|
26
|
+
* `queryParamFilters`) narrow the rows: they count as filters, though no
|
|
27
|
+
* clear action reaches them.
|
|
28
|
+
*/
|
|
29
|
+
hiddenFiltersApply?: Ref<boolean>;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/** What a clear would change. */
|
|
@@ -57,9 +63,11 @@ export const clearableTableFilters = (
|
|
|
57
63
|
search: hasSearch(state),
|
|
58
64
|
});
|
|
59
65
|
|
|
60
|
-
/** The rows are narrowed by a search or a filter
|
|
66
|
+
/** The rows are narrowed by a search, a filter chip or a hidden filter. */
|
|
61
67
|
export const isTableNarrowed = (state: TableNarrowingState): boolean =>
|
|
62
|
-
hasSearch(state) ||
|
|
68
|
+
hasSearch(state) ||
|
|
69
|
+
state.columnFilters.value.some(isFilterEffective) ||
|
|
70
|
+
!!state.hiddenFiltersApply?.value;
|
|
63
71
|
|
|
64
72
|
/** i18n key of a clear action, naming what it clears. */
|
|
65
73
|
export const clearTableFiltersLabelKey = (
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { PaginationState } from "@tanstack/vue-table";
|
|
2
|
+
import { computed, ref, watch, type Ref } from "vue";
|
|
3
|
+
|
|
4
|
+
const FIRST_PAGE_INDEX = 0;
|
|
5
|
+
const PAGINATION_PREFERENCE = "pagination";
|
|
6
|
+
// The hidden filters the stored page was reached under, next to it.
|
|
7
|
+
const PAGINATION_SCOPE_PREFERENCE = "paginationScope";
|
|
8
|
+
|
|
9
|
+
/** The preference store the page is kept in (`usePreferences`). */
|
|
10
|
+
export interface PaginationPreferences {
|
|
11
|
+
getPreference: <V>(keyPath: string, defaultValue?: V) => V;
|
|
12
|
+
setPreference: (keyPath: string, value: unknown) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PersistedPaginationOptions {
|
|
16
|
+
preferences: PaginationPreferences;
|
|
17
|
+
/** The table's preference key of a setting. */
|
|
18
|
+
preferenceKey: (suffix: string) => string;
|
|
19
|
+
defaults: PaginationState;
|
|
20
|
+
/**
|
|
21
|
+
* What lists other rows without the user picking it: the hidden filters
|
|
22
|
+
* the page URL sets. A change of value lists from the first page.
|
|
23
|
+
*/
|
|
24
|
+
scope: () => unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const firstPage = (pagination: PaginationState): PaginationState => ({
|
|
28
|
+
...pagination,
|
|
29
|
+
pageIndex: FIRST_PAGE_INDEX,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The table's page, kept in the user's preferences with the hidden filters
|
|
34
|
+
* it was reached under. A page restored under other hidden filters (a link
|
|
35
|
+
* that remounted the page with another `?status=`) starts at the first
|
|
36
|
+
* page, as does a change of their value while the table is shown; compared
|
|
37
|
+
* by value, so another query parameter changing keeps the page.
|
|
38
|
+
*/
|
|
39
|
+
export function usePersistedPagination(
|
|
40
|
+
options: PersistedPaginationOptions,
|
|
41
|
+
): Ref<PaginationState> {
|
|
42
|
+
const { preferences, preferenceKey } = options;
|
|
43
|
+
const scopeKey = computed(() => JSON.stringify(options.scope() ?? null));
|
|
44
|
+
const stored = preferences.getPreference<PaginationState>(
|
|
45
|
+
preferenceKey(PAGINATION_PREFERENCE),
|
|
46
|
+
options.defaults,
|
|
47
|
+
);
|
|
48
|
+
// A page stored before its scope was: kept, as it always was.
|
|
49
|
+
const storedScope = preferences.getPreference<string>(
|
|
50
|
+
preferenceKey(PAGINATION_SCOPE_PREFERENCE),
|
|
51
|
+
scopeKey.value,
|
|
52
|
+
);
|
|
53
|
+
const pagination = ref<PaginationState>(
|
|
54
|
+
storedScope === scopeKey.value ? stored : firstPage(stored),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Synchronous, so the list is queried once, with both changes.
|
|
58
|
+
watch(
|
|
59
|
+
scopeKey,
|
|
60
|
+
() => {
|
|
61
|
+
if (pagination.value.pageIndex === FIRST_PAGE_INDEX) return;
|
|
62
|
+
pagination.value = firstPage(pagination.value);
|
|
63
|
+
},
|
|
64
|
+
{ flush: "sync" },
|
|
65
|
+
);
|
|
66
|
+
watch(
|
|
67
|
+
[pagination, scopeKey],
|
|
68
|
+
([value, scope]) => {
|
|
69
|
+
preferences.setPreference(preferenceKey(PAGINATION_PREFERENCE), value);
|
|
70
|
+
preferences.setPreference(
|
|
71
|
+
preferenceKey(PAGINATION_SCOPE_PREFERENCE),
|
|
72
|
+
scope,
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
{ deep: true },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return pagination;
|
|
79
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ComputedRef } from "vue";
|
|
2
|
+
import type { TableViewListResponse } from "../../../composables/table-view/types";
|
|
3
|
+
import { useServerRenderedAsyncData } from "./useServerRenderedAsyncData";
|
|
4
|
+
import type { useTableRows } from "./useTableRows";
|
|
5
|
+
import { buildTableDataKey } from "./utils/tableQuery";
|
|
6
|
+
|
|
7
|
+
type Query = Record<string, unknown>;
|
|
8
|
+
type TableRows<T extends Record<string, unknown>> = ReturnType<
|
|
9
|
+
typeof useTableRows<T>
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
/** What the list and the tab counters of a table view are read for. */
|
|
13
|
+
export interface TableListDataOptions<T extends Record<string, unknown>> {
|
|
14
|
+
componentId: string | undefined;
|
|
15
|
+
pageId: string | undefined;
|
|
16
|
+
rows: TableRows<T>;
|
|
17
|
+
query: ComputedRef<object>;
|
|
18
|
+
archiveQuery: ComputedRef<Query>;
|
|
19
|
+
/** A self-managed display (the kanban) reads its own rows. */
|
|
20
|
+
isSelfManaged: ComputedRef<boolean>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One counter: its id and its list query, as the table view builds it. */
|
|
24
|
+
export interface TableCountRequest {
|
|
25
|
+
id: string;
|
|
26
|
+
query: object;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TableCountsDataOptions<T extends Record<string, unknown>> {
|
|
30
|
+
componentId: string | undefined;
|
|
31
|
+
pageId: string | undefined;
|
|
32
|
+
rows: TableRows<T>;
|
|
33
|
+
queries: ComputedRef<TableCountRequest[]>;
|
|
34
|
+
archiveQuery: ComputedRef<Query>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const EMPTY_LIST_RESULT: TableViewListResponse<never> = {
|
|
38
|
+
results: [],
|
|
39
|
+
total: 0,
|
|
40
|
+
offset: 0,
|
|
41
|
+
limit: 0,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The page of rows a table view shows. The key names the source URL: a
|
|
46
|
+
* navigation that remounts the page while the previous one is still on screen
|
|
47
|
+
* binds to the cached entry of its key, so another URL needs another key to be
|
|
48
|
+
* requested at all, and the watch covers a page kept mounted.
|
|
49
|
+
*/
|
|
50
|
+
export function useTableListData<T extends Record<string, unknown>>(
|
|
51
|
+
options: TableListDataOptions<T>,
|
|
52
|
+
) {
|
|
53
|
+
const { rows, query, archiveQuery, isSelfManaged } = options;
|
|
54
|
+
const key = buildTableDataKey({
|
|
55
|
+
componentId: options.componentId,
|
|
56
|
+
pageId: options.pageId,
|
|
57
|
+
query: query.value,
|
|
58
|
+
archiveQuery: archiveQuery.value,
|
|
59
|
+
isSelfManaged: isSelfManaged.value,
|
|
60
|
+
sourceUrl: rows.sourceUrl.value,
|
|
61
|
+
});
|
|
62
|
+
return useServerRenderedAsyncData(
|
|
63
|
+
key,
|
|
64
|
+
(): Promise<TableViewListResponse<T>> =>
|
|
65
|
+
isSelfManaged.value
|
|
66
|
+
? Promise.resolve(EMPTY_LIST_RESULT)
|
|
67
|
+
: rows.list({ ...query.value, ...archiveQuery.value }),
|
|
68
|
+
{ watch: [query, archiveQuery, isSelfManaged, rows.sourceUrl] },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The counters of a table view's tabs and views, keyed by id; `null` until
|
|
74
|
+
* they arrive. Keyed by source URL like the list (see useTableListData).
|
|
75
|
+
*/
|
|
76
|
+
export function useTableCountsData<T extends Record<string, unknown>>(
|
|
77
|
+
options: TableCountsDataOptions<T>,
|
|
78
|
+
) {
|
|
79
|
+
const { rows, queries, archiveQuery } = options;
|
|
80
|
+
const sourceSuffix = rows.sourceUrl.value ? `-${rows.sourceUrl.value}` : "";
|
|
81
|
+
return useServerRenderedAsyncData<Record<string, number>>(
|
|
82
|
+
`table-view-${options.componentId}-${options.pageId}-tab-counts${sourceSuffix}`,
|
|
83
|
+
async () => {
|
|
84
|
+
if (queries.value.length === 0) return {};
|
|
85
|
+
return await rows.countBatch(
|
|
86
|
+
queries.value.map(({ id, query }) => ({
|
|
87
|
+
id,
|
|
88
|
+
query: { ...query, ...archiveQuery.value },
|
|
89
|
+
})),
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
{ watch: [queries, archiveQuery, rows.sourceUrl] },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { get } from "@nuxt/ui/runtime/utils/index.js";
|
|
2
|
+
import { computed } from "vue";
|
|
2
3
|
import type {
|
|
3
4
|
TableViewColumn,
|
|
4
5
|
TableViewListResponse,
|
|
5
6
|
TableViewSourceConfig,
|
|
6
7
|
} from "../../../composables/table-view/types";
|
|
8
|
+
import {
|
|
9
|
+
type ReplaceUrlVariablesContext,
|
|
10
|
+
resolveUrlVariables,
|
|
11
|
+
} from "../../utils/urlVariables";
|
|
7
12
|
|
|
8
13
|
type Query = Record<string, unknown>;
|
|
9
14
|
|
|
@@ -20,6 +25,11 @@ export interface TableRowsOptions {
|
|
|
20
25
|
/** A `TableView.fromSource` table's route, instead of a controller. */
|
|
21
26
|
source?: TableViewSourceConfig;
|
|
22
27
|
columns: TableViewColumn[];
|
|
28
|
+
/**
|
|
29
|
+
* The page route a source's `fetchUrl` names: its parameters and query,
|
|
30
|
+
* read again on every request.
|
|
31
|
+
*/
|
|
32
|
+
route?: () => ReplaceUrlVariablesContext;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
const FILTER_PREFIX = "filter_";
|
|
@@ -27,6 +37,7 @@ const SEARCH_KEY = "search";
|
|
|
27
37
|
const SORT_KEYS = ["sortKey", "sortDirection"];
|
|
28
38
|
const PAGE_KEYS = ["offset", "limit"];
|
|
29
39
|
const DESCENDING = "desc";
|
|
40
|
+
const NO_ROUTE_VALUES: ReplaceUrlVariablesContext = { routeQuery: {} };
|
|
30
41
|
|
|
31
42
|
type SourceCapabilities = TableViewSourceConfig["capabilities"];
|
|
32
43
|
|
|
@@ -107,26 +118,51 @@ export function processSourceRows<T extends Record<string, unknown>>(
|
|
|
107
118
|
};
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
/** A page with no rows: what a source shows while its URL is unresolved. */
|
|
122
|
+
function emptySourcePage<T>(query: Query): TableViewListResponse<T> {
|
|
123
|
+
return {
|
|
124
|
+
results: [],
|
|
125
|
+
total: 0,
|
|
126
|
+
offset: Number(query.offset) || 0,
|
|
127
|
+
limit: Number(query.limit) || 0,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
110
131
|
/**
|
|
111
132
|
* Where a table view reads its rows: its data controller's routes, or the
|
|
112
133
|
* route of a `TableView.fromSource` table — which answers `{ results, total }`
|
|
113
134
|
* for the part of the query it handles. A route answering every row leaves
|
|
114
135
|
* the rest to the browser; a route answering one page is taken as it is.
|
|
136
|
+
*
|
|
137
|
+
* A source's `{{params.X}}` and `{{query.X}}` tokens are filled from `route`;
|
|
138
|
+
* while one has no value nothing is requested and the list is empty.
|
|
139
|
+
* `sourceUrl` is the URL requested, for the caller to list again when it
|
|
140
|
+
* changes.
|
|
115
141
|
*/
|
|
116
142
|
export function useTableRows<T extends Record<string, unknown>>(
|
|
117
143
|
options: TableRowsOptions,
|
|
118
144
|
) {
|
|
119
145
|
const { api, location, source, columns } = options;
|
|
120
146
|
|
|
147
|
+
const sourceUrl = computed(() =>
|
|
148
|
+
source
|
|
149
|
+
? resolveUrlVariables(
|
|
150
|
+
source.fetchUrl,
|
|
151
|
+
options.route?.() ?? NO_ROUTE_VALUES,
|
|
152
|
+
)
|
|
153
|
+
: undefined,
|
|
154
|
+
);
|
|
155
|
+
|
|
121
156
|
const listFromSource = async (
|
|
122
157
|
sourceConfig: TableViewSourceConfig,
|
|
123
158
|
query: Query,
|
|
124
159
|
): Promise<TableViewListResponse<T>> => {
|
|
160
|
+
const url = sourceUrl.value;
|
|
161
|
+
if (!url) return emptySourcePage(query);
|
|
125
162
|
const { capabilities } = sourceConfig;
|
|
126
|
-
const response = await api<{ results: T[]; total: number }>(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
);
|
|
163
|
+
const response = await api<{ results: T[]; total: number }>(url, {
|
|
164
|
+
query: sourceRouteQuery(query, capabilities),
|
|
165
|
+
});
|
|
130
166
|
if (capabilities.paginate) {
|
|
131
167
|
return {
|
|
132
168
|
...response,
|
|
@@ -171,5 +207,5 @@ export function useTableRows<T extends Record<string, unknown>>(
|
|
|
171
207
|
? Promise.resolve(undefined)
|
|
172
208
|
: api<T>(`${location}/get`, { query: { id } });
|
|
173
209
|
|
|
174
|
-
return { isSource: !!source, list, countBatch, getRow };
|
|
210
|
+
return { isSource: !!source, sourceUrl, list, countBatch, getRow };
|
|
175
211
|
}
|
|
@@ -17,6 +17,8 @@ interface TableDataKeyConfig {
|
|
|
17
17
|
query: unknown;
|
|
18
18
|
archiveQuery: unknown;
|
|
19
19
|
isSelfManaged: boolean;
|
|
20
|
+
/** The resolved URL of a `TableView.fromSource` table. */
|
|
21
|
+
sourceUrl?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const NO_VALUE_COMPARE_MODES = new Set(["is_empty", "is_not_empty"]);
|
|
@@ -72,4 +74,5 @@ export const buildTableDataKey = (config: TableDataKeyConfig): string =>
|
|
|
72
74
|
query: config.query,
|
|
73
75
|
archiveQuery: config.archiveQuery,
|
|
74
76
|
isSelfManaged: config.isSelfManaged,
|
|
77
|
+
sourceUrl: config.sourceUrl,
|
|
75
78
|
})}`;
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
placeholder as placeholderText,
|
|
36
36
|
tooltips,
|
|
37
37
|
} from "@codemirror/view";
|
|
38
|
-
import {
|
|
38
|
+
import { CODE_PALETTE } from "./codePalette";
|
|
39
39
|
|
|
40
40
|
/** A suggestion of the autocomplete (interface-dms `CodeCompletion`). */
|
|
41
41
|
export interface CodeEditorCompletion {
|
|
@@ -84,18 +84,7 @@ const LANGUAGES: Record<string, () => Extension> = {
|
|
|
84
84
|
javascript: () => javascript(),
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
// dark follow the app.
|
|
89
|
-
const HIGHLIGHT = HighlightStyle.define([
|
|
90
|
-
{ tag: [tags.keyword, tags.operatorKeyword], color: "var(--ui-primary)" },
|
|
91
|
-
{ tag: [tags.string, tags.special(tags.string)], color: "var(--ui-success)" },
|
|
92
|
-
{ tag: [tags.number, tags.bool, tags.null], color: "var(--ui-warning)" },
|
|
93
|
-
{ tag: [tags.comment], color: "var(--ui-text-dimmed)", fontStyle: "italic" },
|
|
94
|
-
{ tag: [tags.propertyName, tags.attributeName], color: "var(--ui-info)" },
|
|
95
|
-
{ tag: [tags.function(tags.variableName)], color: "var(--ui-primary)" },
|
|
96
|
-
{ tag: [tags.tagName], color: "var(--ui-primary)" },
|
|
97
|
-
{ tag: [tags.invalid], color: "var(--ui-error)" },
|
|
98
|
-
]);
|
|
87
|
+
const HIGHLIGHT = HighlightStyle.define(CODE_PALETTE);
|
|
99
88
|
|
|
100
89
|
const THEME = EditorView.theme({
|
|
101
90
|
"&": {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Read-only highlighting: the code parsed by the editor's own CodeMirror
|
|
2
|
+
// languages and cut into styled tokens, without mounting an editor. Loaded on
|
|
3
|
+
// demand, like the editor: a page only pays for the parsers once it shows
|
|
4
|
+
// code.
|
|
5
|
+
import { htmlLanguage } from "@codemirror/lang-html";
|
|
6
|
+
import {
|
|
7
|
+
javascriptLanguage,
|
|
8
|
+
typescriptLanguage,
|
|
9
|
+
} from "@codemirror/lang-javascript";
|
|
10
|
+
import { jsonLanguage } from "@codemirror/lang-json";
|
|
11
|
+
import { StandardSQL } from "@codemirror/lang-sql";
|
|
12
|
+
import { StreamLanguage } from "@codemirror/language";
|
|
13
|
+
import type { Parser } from "@lezer/common";
|
|
14
|
+
import { highlightTree, tagHighlighter } from "@lezer/highlight";
|
|
15
|
+
import { CODE_PALETTE, type CodeTokenStyle } from "./codePalette";
|
|
16
|
+
import { shellParser } from "./shellParser";
|
|
17
|
+
|
|
18
|
+
/** A run of code drawn in one style; unstyled text has no `style`. */
|
|
19
|
+
export interface CodeToken {
|
|
20
|
+
text: string;
|
|
21
|
+
style?: CodeTokenStyle;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PARSERS: Record<string, Parser> = {
|
|
25
|
+
json: jsonLanguage.parser,
|
|
26
|
+
javascript: javascriptLanguage.parser,
|
|
27
|
+
typescript: typescriptLanguage.parser,
|
|
28
|
+
html: htmlLanguage.parser,
|
|
29
|
+
sql: StandardSQL.language.parser,
|
|
30
|
+
shell: StreamLanguage.define(shellParser).parser,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Each palette entry's position is its class, read back into its style.
|
|
34
|
+
const HIGHLIGHTER = tagHighlighter(
|
|
35
|
+
CODE_PALETTE.map((entry, index) => ({ tag: entry.tag, class: `${index}` })),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
function styleOf(classes: string): CodeTokenStyle {
|
|
39
|
+
const { color, fontStyle } = CODE_PALETTE[Number(classes.split(" ")[0])];
|
|
40
|
+
return { color, fontStyle };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cuts `code` into tokens styled for `language` (a resolved name, see
|
|
45
|
+
* `resolveCodeLanguage`); a language without a parser is one plain token.
|
|
46
|
+
*/
|
|
47
|
+
export function highlightCode(code: string, language: string): CodeToken[] {
|
|
48
|
+
const parser = PARSERS[language];
|
|
49
|
+
if (!parser || !code) return [{ text: code }];
|
|
50
|
+
const tokens: CodeToken[] = [];
|
|
51
|
+
let cursor = 0;
|
|
52
|
+
highlightTree(parser.parse(code), HIGHLIGHTER, (from, to, classes) => {
|
|
53
|
+
if (from > cursor) tokens.push({ text: code.slice(cursor, from) });
|
|
54
|
+
tokens.push({ text: code.slice(from, to), style: styleOf(classes) });
|
|
55
|
+
cursor = to;
|
|
56
|
+
});
|
|
57
|
+
if (cursor < code.length) tokens.push({ text: code.slice(cursor) });
|
|
58
|
+
return tokens;
|
|
59
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The languages the code views name, shared by the editor's status line and
|
|
2
|
+
// the read-only snippets. Kept apart from the highlighters so a view can
|
|
3
|
+
// label its language before CodeMirror has loaded.
|
|
4
|
+
|
|
5
|
+
/** The language a snippet is shown as written in. */
|
|
6
|
+
export const PLAIN_CODE_LANGUAGE = "text";
|
|
7
|
+
|
|
8
|
+
const LANGUAGE_ALIASES: Record<string, string> = {
|
|
9
|
+
js: "javascript",
|
|
10
|
+
ts: "typescript",
|
|
11
|
+
bash: "shell",
|
|
12
|
+
sh: "shell",
|
|
13
|
+
plain: PLAIN_CODE_LANGUAGE,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// The label of each language, as the status line and the snippet head show it.
|
|
17
|
+
const CODE_LANGUAGE_LABELS: Record<string, string> = {
|
|
18
|
+
json: "JSON",
|
|
19
|
+
sql: "SQL",
|
|
20
|
+
html: "HTML",
|
|
21
|
+
javascript: "JavaScript",
|
|
22
|
+
typescript: "TypeScript",
|
|
23
|
+
shell: "Shell",
|
|
24
|
+
text: "Text",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** The language an alias (`bash`, `ts`, `plain`) stands for; `text` when unset. */
|
|
28
|
+
export function resolveCodeLanguage(language: string | undefined): string {
|
|
29
|
+
const name = language?.trim().toLowerCase() || PLAIN_CODE_LANGUAGE;
|
|
30
|
+
return LANGUAGE_ALIASES[name] ?? name;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The label of a language; an unknown one is shown as written. */
|
|
34
|
+
export function codeLanguageLabel(language: string): string {
|
|
35
|
+
return CODE_LANGUAGE_LABELS[language] ?? language;
|
|
36
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Tag } from "@lezer/highlight";
|
|
2
|
+
import { tags } from "@lezer/highlight";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How one kind of token is drawn: its colour, and italic for comments. A type
|
|
6
|
+
* rather than an interface, so it binds to a `style` attribute as it is.
|
|
7
|
+
*/
|
|
8
|
+
export type CodeTokenStyle = {
|
|
9
|
+
color: string;
|
|
10
|
+
fontStyle?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** The tokens one entry of the palette colours. */
|
|
14
|
+
export interface CodePaletteEntry extends CodeTokenStyle {
|
|
15
|
+
tag: Tag | Tag[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The query console's palette, read from the theme's variables so light and
|
|
20
|
+
* dark follow the app: the editor and the read-only snippets draw code alike.
|
|
21
|
+
*/
|
|
22
|
+
export const CODE_PALETTE: CodePaletteEntry[] = [
|
|
23
|
+
{ tag: [tags.keyword, tags.operatorKeyword], color: "var(--ui-primary)" },
|
|
24
|
+
{ tag: [tags.string, tags.special(tags.string)], color: "var(--ui-success)" },
|
|
25
|
+
{ tag: [tags.number, tags.bool, tags.null], color: "var(--ui-warning)" },
|
|
26
|
+
{ tag: [tags.comment], color: "var(--ui-text-dimmed)", fontStyle: "italic" },
|
|
27
|
+
{ tag: [tags.propertyName, tags.attributeName], color: "var(--ui-info)" },
|
|
28
|
+
{ tag: [tags.function(tags.variableName)], color: "var(--ui-primary)" },
|
|
29
|
+
{ tag: [tags.tagName], color: "var(--ui-primary)" },
|
|
30
|
+
{ tag: [tags.invalid], color: "var(--ui-error)" },
|
|
31
|
+
{ tag: [tags.special(tags.variableName)], color: "var(--ui-warning)" },
|
|
32
|
+
];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { StreamParser, StringStream } from "@codemirror/language";
|
|
2
|
+
|
|
3
|
+
// A shell snippet as read for highlighting only: the command of each
|
|
4
|
+
// pipeline step, its flags, quoted strings, variables and comments.
|
|
5
|
+
|
|
6
|
+
interface ShellState {
|
|
7
|
+
/** The next word is a command: at a line start, after `|`, `&&` or `;`. */
|
|
8
|
+
isAtCommand: boolean;
|
|
9
|
+
/** The line ends with `\`: the next one carries on the same command. */
|
|
10
|
+
isContinued: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const COMMENT = /^#.*/;
|
|
14
|
+
const VARIABLE = /^\$(?:\{[^}]*\}|\w+|[?!#$@*-])/;
|
|
15
|
+
const STRING = /^(?:"(?:[^"\\]|\\.)*"?|'[^']*'?)/;
|
|
16
|
+
const FLAG = /^--?[\w-]+/;
|
|
17
|
+
const SEPARATOR = /^(?:\|\|?|&&?|;)/;
|
|
18
|
+
const WORD = /^[^\s|&;"'$]+/;
|
|
19
|
+
const CONTINUATION = /^\\$/;
|
|
20
|
+
|
|
21
|
+
function readToken(stream: StringStream, state: ShellState): string | null {
|
|
22
|
+
if (stream.match(COMMENT)) return "comment";
|
|
23
|
+
if (stream.match(STRING)) return "string";
|
|
24
|
+
if (stream.match(VARIABLE)) return "variableName.special";
|
|
25
|
+
if (stream.match(SEPARATOR)) {
|
|
26
|
+
state.isAtCommand = true;
|
|
27
|
+
return "operator";
|
|
28
|
+
}
|
|
29
|
+
if (stream.match(FLAG)) return "attributeName";
|
|
30
|
+
if (stream.match(CONTINUATION)) {
|
|
31
|
+
state.isContinued = true;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!stream.match(WORD)) {
|
|
35
|
+
stream.next();
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const isCommand = state.isAtCommand;
|
|
39
|
+
state.isAtCommand = false;
|
|
40
|
+
return isCommand ? "keyword" : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const shellParser: StreamParser<ShellState> = {
|
|
44
|
+
name: "shell",
|
|
45
|
+
startState: () => ({ isAtCommand: true, isContinued: false }),
|
|
46
|
+
copyState: (state) => ({ ...state }),
|
|
47
|
+
token(stream, state) {
|
|
48
|
+
if (stream.sol()) {
|
|
49
|
+
state.isAtCommand = !state.isContinued;
|
|
50
|
+
state.isContinued = false;
|
|
51
|
+
}
|
|
52
|
+
if (stream.eatSpace()) return null;
|
|
53
|
+
return readToken(stream, state);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -13,34 +13,78 @@ export interface ReplaceUrlVariablesContext {
|
|
|
13
13
|
response?: Record<string, unknown>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
type EncodeValue = (value: string) => string;
|
|
17
|
+
|
|
18
|
+
/** How each kind of route value is written into the text. */
|
|
19
|
+
interface TokenEncoding {
|
|
20
|
+
param: EncodeValue;
|
|
21
|
+
query: EncodeValue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const keepValue: EncodeValue = (value) => value;
|
|
25
|
+
|
|
26
|
+
// A route parameter is a segment of the page path, which the router keeps
|
|
27
|
+
// percent-encoded: it is decoded first so that it is not encoded twice.
|
|
28
|
+
const encodeParam: EncodeValue = (value) => {
|
|
29
|
+
try {
|
|
30
|
+
return encodeURIComponent(decodeURIComponent(value));
|
|
31
|
+
} catch {
|
|
32
|
+
return encodeURIComponent(value);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const URL_ENCODING: TokenEncoding = {
|
|
37
|
+
param: encodeParam,
|
|
38
|
+
query: encodeURIComponent,
|
|
39
|
+
};
|
|
40
|
+
const TEXT_ENCODING: TokenEncoding = { param: keepValue, query: keepValue };
|
|
41
|
+
|
|
42
|
+
function fillRouteTokens(
|
|
43
|
+
text: string,
|
|
44
|
+
context: ReplaceUrlVariablesContext,
|
|
45
|
+
encoding: TokenEncoding,
|
|
46
|
+
): string {
|
|
47
|
+
return text
|
|
48
|
+
.replace(PARAM_TOKEN, (match, key) => {
|
|
49
|
+
const value = context.routeParams?.[key];
|
|
50
|
+
return value ? encoding.param(value) : match;
|
|
51
|
+
})
|
|
52
|
+
.replace(QUERY_TOKEN, (match, key) => {
|
|
53
|
+
const value = context.routeQuery[key];
|
|
54
|
+
return typeof value === "string" ? encoding.query(value) : match;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Written as answered: a response may hand back a whole path to open next
|
|
59
|
+
// (`redirectOnSuccess: "{{response.redirectPath}}"`), which encoding would break.
|
|
60
|
+
function fillResponseTokens(
|
|
61
|
+
text: string,
|
|
62
|
+
response: Record<string, unknown> | undefined,
|
|
63
|
+
): string {
|
|
64
|
+
if (!response) return text;
|
|
65
|
+
return text.replace(RESPONSE_TOKEN, (match, key) => {
|
|
66
|
+
const value = response[key];
|
|
67
|
+
return typeof value === "string" || typeof value === "number"
|
|
68
|
+
? String(value)
|
|
69
|
+
: match;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
16
73
|
/**
|
|
17
74
|
* Fills the `{{params.X}}`, `{{query.X}}` and (with a response)
|
|
18
|
-
* `{{response.X}}` tokens of a URL.
|
|
75
|
+
* `{{response.X}}` tokens of a URL. Route values are percent-encoded, so a
|
|
76
|
+
* value such as `GET /api/a?b=c` stays one query value or one path segment;
|
|
77
|
+
* response values are written as answered. A token with no value is left as
|
|
78
|
+
* written.
|
|
19
79
|
*/
|
|
20
80
|
export function replaceUrlVariables(
|
|
21
81
|
url: string,
|
|
22
82
|
context: ReplaceUrlVariablesContext,
|
|
23
83
|
): string {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
84
|
+
return fillResponseTokens(
|
|
85
|
+
fillRouteTokens(url, context, URL_ENCODING),
|
|
86
|
+
context.response,
|
|
27
87
|
);
|
|
28
|
-
|
|
29
|
-
processedUrl = processedUrl.replace(QUERY_TOKEN, (match, key) => {
|
|
30
|
-
const value = context.routeQuery[key];
|
|
31
|
-
return typeof value === "string" ? value : match;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
if (context.response) {
|
|
35
|
-
processedUrl = processedUrl.replace(RESPONSE_TOKEN, (match, key) => {
|
|
36
|
-
const value = context.response?.[key];
|
|
37
|
-
return typeof value === "string" || typeof value === "number"
|
|
38
|
-
? String(value)
|
|
39
|
-
: match;
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return processedUrl;
|
|
44
88
|
}
|
|
45
89
|
|
|
46
90
|
/** Whether a URL still names a token: written so, or left unresolved. */
|
|
@@ -61,3 +105,16 @@ export function resolveUrlVariables(
|
|
|
61
105
|
const resolved = replaceUrlVariables(url, context);
|
|
62
106
|
return hasUrlVariables(resolved) ? undefined : resolved;
|
|
63
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A plain value (not a URL) with its route tokens filled as they are, without
|
|
111
|
+
* encoding, or `undefined` while one is left: a submit default such as
|
|
112
|
+
* `{{query.status}}` is a field value.
|
|
113
|
+
*/
|
|
114
|
+
export function resolveTextVariables(
|
|
115
|
+
text: string,
|
|
116
|
+
context: ReplaceUrlVariablesContext,
|
|
117
|
+
): string | undefined {
|
|
118
|
+
const resolved = fillRouteTokens(text, context, TEXT_ENCODING);
|
|
119
|
+
return hasUrlVariables(resolved) ? undefined : resolved;
|
|
120
|
+
}
|