@jskit-ai/http-web 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fixtures/crud-list-date-filters/App.vue +107 -0
- package/fixtures/crud-list-date-filters/index.html +12 -0
- package/fixtures/crud-list-date-filters/main.js +24 -0
- package/fixtures/crud-list-date-filters/vite.config.mjs +18 -0
- package/package.json +150 -0
- package/src/client/bulkActions.js +47 -0
- package/src/client/components/CrudAddEditScreen.vue +186 -0
- package/src/client/components/CrudDeleteAction.vue +93 -0
- package/src/client/components/CrudListBulkActionSurface.vue +126 -0
- package/src/client/components/CrudListDateFilterControl.vue +188 -0
- package/src/client/components/CrudListFilterSurface.vue +388 -0
- package/src/client/components/CrudListRecordActionMenu.vue +120 -0
- package/src/client/components/CrudListScreen.vue +501 -0
- package/src/client/components/CrudViewScreen.vue +197 -0
- package/src/client/composables/crud/crudBindingSupport.js +75 -0
- package/src/client/composables/crud/crudHttpClientSupport.js +57 -0
- package/src/client/composables/crud/crudJsonApiTransportSupport.js +145 -0
- package/src/client/composables/crud/crudLookupFieldLabelSupport.js +151 -0
- package/src/client/composables/crud/crudLookupFieldRuntime.js +251 -0
- package/src/client/composables/crud/crudSchemaFormHelpers.js +464 -0
- package/src/client/composables/internal/crudListFilterLookupSupport.js +161 -0
- package/src/client/composables/internal/crudListParentTitleSupport.js +171 -0
- package/src/client/composables/internal/useOperationScope.js +144 -0
- package/src/client/composables/records/useAddEdit.js +224 -0
- package/src/client/composables/records/useCrudAddEdit.js +227 -0
- package/src/client/composables/records/useCrudList.js +101 -0
- package/src/client/composables/records/useCrudView.js +47 -0
- package/src/client/composables/records/useList.js +500 -0
- package/src/client/composables/records/useView.js +164 -0
- package/src/client/composables/runtime/addEditUiRuntime.js +130 -0
- package/src/client/composables/runtime/listUiRuntime.js +123 -0
- package/src/client/composables/runtime/modelStateHelpers.js +90 -0
- package/src/client/composables/runtime/operationAdapters.js +34 -0
- package/src/client/composables/runtime/operationUiHelpers.js +125 -0
- package/src/client/composables/runtime/operationValidationHelpers.js +51 -0
- package/src/client/composables/runtime/useAddEditCore.js +112 -0
- package/src/client/composables/runtime/useCommandCore.js +123 -0
- package/src/client/composables/runtime/useEndpointResource.js +199 -0
- package/src/client/composables/runtime/useFieldErrorBag.js +61 -0
- package/src/client/composables/runtime/useListCore.js +112 -0
- package/src/client/composables/runtime/useUiFeedback.js +98 -0
- package/src/client/composables/runtime/useViewCore.js +93 -0
- package/src/client/composables/runtime/viewUiRuntime.js +96 -0
- package/src/client/composables/support/errorMessageHelpers.js +66 -0
- package/src/client/composables/support/listQueryParamSupport.js +473 -0
- package/src/client/composables/support/listSearchSupport.js +70 -0
- package/src/client/composables/support/refValueHelpers.js +19 -0
- package/src/client/composables/support/requestQueryRuntimeSupport.js +141 -0
- package/src/client/composables/support/resourceLoadStateHelpers.js +208 -0
- package/src/client/composables/support/routeTemplateHelpers.js +285 -0
- package/src/client/composables/support/scopeHelpers.js +145 -0
- package/src/client/composables/useAccess.js +109 -0
- package/src/client/composables/useCommand.js +118 -0
- package/src/client/composables/useCrudAddEditScreen.js +88 -0
- package/src/client/composables/useCrudDeleteAction.js +160 -0
- package/src/client/composables/useCrudListBulkActions.js +147 -0
- package/src/client/composables/useCrudListFilterLookups.js +146 -0
- package/src/client/composables/useCrudListFilters.js +339 -0
- package/src/client/composables/useCrudListParentTitle.js +151 -0
- package/src/client/composables/useCrudListRowActions.js +144 -0
- package/src/client/composables/useCrudListScreen.js +237 -0
- package/src/client/composables/useCrudViewScreen.js +77 -0
- package/src/client/composables/usePagedCollection.js +181 -0
- package/src/client/composables/useRealtimeQueryInvalidation.js +156 -0
- package/src/client/composables/useScopeRuntime.js +135 -0
- package/src/client/filters.js +15 -0
- package/src/client/index.js +24 -0
- package/src/client/lib/httpClient.js +47 -0
- package/src/client/lib/permissions.js +34 -0
- package/src/client/rowActions.js +47 -0
- package/src/client/support/contractGuards.js +34 -0
- package/src/client/support/crudListDateFilterSupport.js +91 -0
- package/test/addEditUiRuntime.test.js +110 -0
- package/test/crudBindingSupport.test.js +110 -0
- package/test/crudHttpClientSupport.test.js +77 -0
- package/test/crudJsonApiTransportSupport.test.js +179 -0
- package/test/crudListBulkActionSurface.test.js +27 -0
- package/test/crudListDateFilterSupport.test.js +92 -0
- package/test/crudListDateFilterSurface.browser.test.js +182 -0
- package/test/crudListFilterSurface.test.js +90 -0
- package/test/crudLookupFieldRuntime.test.js +264 -0
- package/test/crudScreenComponents.test.js +110 -0
- package/test/errorIntentContract.test.js +31 -0
- package/test/errorMessageHelpers.test.js +28 -0
- package/test/exportsContract.test.js +68 -0
- package/test/listQueryParamSupport.test.js +223 -0
- package/test/listUiRuntime.test.js +102 -0
- package/test/operationValidationHelpers.test.js +64 -0
- package/test/packageBoundary.test.js +31 -0
- package/test/permissions.test.js +35 -0
- package/test/refValueHelpers.test.js +14 -0
- package/test/requestTransportOptions.test.js +457 -0
- package/test/resourceLoadStateHelpers.test.js +189 -0
- package/test/routeTemplateHelpers.test.js +85 -0
- package/test/scopeHelpers.test.js +57 -0
- package/test/useAddEditCore.test.js +124 -0
- package/test/useAddEditRequestQueryParams.test.js +190 -0
- package/test/useCrudAddEdit.test.js +415 -0
- package/test/useCrudDeleteAction.test.js +139 -0
- package/test/useCrudListBulkActions.test.js +65 -0
- package/test/useCrudListFilterLookups.test.js +114 -0
- package/test/useCrudListFilters.test.js +293 -0
- package/test/useCrudListParentTitle.test.js +262 -0
- package/test/useCrudListRowActions.test.js +77 -0
- package/test/useListRouteInitialization.test.js +337 -0
- package/test/useListSearchSupport.test.js +61 -0
- package/test/usePagedCollection.test.js +53 -0
- package/test/useViewRequestQueryParams.test.js +20 -0
- package/test/viewCoreLoading.test.js +44 -0
- package/test/viewUiRuntime.test.js +95 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { computed, unref } from "vue";
|
|
2
|
+
import { useInfiniteQuery, useQueryClient } from "@tanstack/vue-query";
|
|
3
|
+
import { asPlainObject } from "./support/scopeHelpers.js";
|
|
4
|
+
import { resolveEnabledRef } from "./support/refValueHelpers.js";
|
|
5
|
+
import { toQueryErrorMessage } from "./support/errorMessageHelpers.js";
|
|
6
|
+
import { mergeQueryMeta } from "./support/resourceLoadStateHelpers.js";
|
|
7
|
+
|
|
8
|
+
function defaultSelectItems(page) {
|
|
9
|
+
return Array.isArray(page?.items) ? page.items : [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function defaultGetNextPageParam(lastPage) {
|
|
13
|
+
return lastPage?.nextCursor ?? null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeQueryKeyValue(queryKey = null) {
|
|
17
|
+
const resolved = unref(queryKey);
|
|
18
|
+
if (Array.isArray(resolved)) {
|
|
19
|
+
return resolved;
|
|
20
|
+
}
|
|
21
|
+
if (resolved === null || resolved === undefined || resolved === "") {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return [resolved];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function trimInfinitePagesToFirst(data = null) {
|
|
29
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sourcePages = Array.isArray(data.pages) ? data.pages : [];
|
|
34
|
+
const sourcePageParams = Array.isArray(data.pageParams) ? data.pageParams : [];
|
|
35
|
+
if (sourcePages.length < 2 && sourcePageParams.length < 2) {
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
const pages = sourcePages.slice(0, 1);
|
|
39
|
+
const pageParams = sourcePageParams.length > 0
|
|
40
|
+
? sourcePageParams.slice(0, 1)
|
|
41
|
+
: pages.length > 0
|
|
42
|
+
? [null]
|
|
43
|
+
: [];
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
...data,
|
|
47
|
+
pages,
|
|
48
|
+
pageParams
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function usePagedCollection({
|
|
53
|
+
queryKey,
|
|
54
|
+
enabled = true,
|
|
55
|
+
initialPageParam = null,
|
|
56
|
+
queryFn,
|
|
57
|
+
getNextPageParam = defaultGetNextPageParam,
|
|
58
|
+
selectItems = defaultSelectItems,
|
|
59
|
+
dedupeBy = null,
|
|
60
|
+
queryOptions = null,
|
|
61
|
+
fallbackLoadError = "Unable to load list."
|
|
62
|
+
} = {}) {
|
|
63
|
+
if (typeof queryFn !== "function") {
|
|
64
|
+
throw new TypeError("usePagedCollection requires queryFn().");
|
|
65
|
+
}
|
|
66
|
+
if (typeof getNextPageParam !== "function") {
|
|
67
|
+
throw new TypeError("usePagedCollection requires getNextPageParam().");
|
|
68
|
+
}
|
|
69
|
+
if (typeof selectItems !== "function") {
|
|
70
|
+
throw new TypeError("usePagedCollection requires selectItems().");
|
|
71
|
+
}
|
|
72
|
+
if (dedupeBy != null && typeof dedupeBy !== "function") {
|
|
73
|
+
throw new TypeError("usePagedCollection dedupeBy must be a function when provided.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const queryClient = useQueryClient();
|
|
77
|
+
const queryEnabled = computed(() => resolveEnabledRef(enabled));
|
|
78
|
+
const normalizedQueryKey = computed(() => normalizeQueryKeyValue(queryKey));
|
|
79
|
+
|
|
80
|
+
const query = useInfiniteQuery({
|
|
81
|
+
queryKey,
|
|
82
|
+
initialPageParam,
|
|
83
|
+
enabled: queryEnabled,
|
|
84
|
+
queryFn,
|
|
85
|
+
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
|
86
|
+
getNextPageParam(lastPage, allPages, lastPageParam, allPageParams),
|
|
87
|
+
...mergeQueryMeta(asPlainObject(queryOptions), {
|
|
88
|
+
jskit: {
|
|
89
|
+
refreshOnPull: true
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const pages = computed(() => {
|
|
95
|
+
const pageList = query.data.value?.pages;
|
|
96
|
+
return Array.isArray(pageList) ? pageList : [];
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const items = computed(() => {
|
|
100
|
+
const result = [];
|
|
101
|
+
const seen = dedupeBy ? new Set() : null;
|
|
102
|
+
|
|
103
|
+
for (const page of pages.value) {
|
|
104
|
+
const pageItems = selectItems(page);
|
|
105
|
+
if (!Array.isArray(pageItems)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!seen) {
|
|
110
|
+
result.push(...pageItems);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const item of pageItems) {
|
|
115
|
+
const key = dedupeBy(item);
|
|
116
|
+
const normalizedKey = String(key ?? "").trim();
|
|
117
|
+
if (!normalizedKey) {
|
|
118
|
+
result.push(item);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (seen.has(normalizedKey)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
seen.add(normalizedKey);
|
|
125
|
+
result.push(item);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const isInitialLoading = computed(() => Boolean(query.isPending.value));
|
|
133
|
+
const isFetching = computed(() => Boolean(query.isFetching.value));
|
|
134
|
+
const isRefetching = computed(() => Boolean(isFetching.value && !isInitialLoading.value));
|
|
135
|
+
const isLoading = computed(() => Boolean(isInitialLoading.value || isFetching.value));
|
|
136
|
+
const isLoadingMore = computed(() => Boolean(query.isFetchingNextPage.value));
|
|
137
|
+
const hasMore = computed(() => Boolean(query.hasNextPage.value));
|
|
138
|
+
const loadError = computed(() => toQueryErrorMessage(query.error.value, fallbackLoadError, "Unable to load list."));
|
|
139
|
+
|
|
140
|
+
async function reload() {
|
|
141
|
+
return query.refetch();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function loadMore() {
|
|
145
|
+
if (!hasMore.value || isLoadingMore.value) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return query.fetchNextPage();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function trimToFirstPage() {
|
|
153
|
+
const key = normalizedQueryKey.value;
|
|
154
|
+
if (key.length < 1) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
queryClient.setQueryData(key, (data) => trimInfinitePagesToFirst(data));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
query,
|
|
163
|
+
pages,
|
|
164
|
+
items,
|
|
165
|
+
isInitialLoading,
|
|
166
|
+
isFetching,
|
|
167
|
+
isRefetching,
|
|
168
|
+
isLoading,
|
|
169
|
+
isLoadingMore,
|
|
170
|
+
hasMore,
|
|
171
|
+
loadError,
|
|
172
|
+
reload,
|
|
173
|
+
loadMore,
|
|
174
|
+
trimToFirstPage
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export {
|
|
179
|
+
usePagedCollection,
|
|
180
|
+
trimInfinitePagesToFirst
|
|
181
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { computed, unref } from "vue";
|
|
2
|
+
import { useQueryClient } from "@tanstack/vue-query";
|
|
3
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import { useRealtimeEvent } from "@jskit-ai/realtime/client/composables/useRealtimeEvent";
|
|
5
|
+
|
|
6
|
+
function normalizeRealtimeOptions(value = {}) {
|
|
7
|
+
if (value === null || value === undefined || value === false) {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
11
|
+
throw new TypeError("realtime must be an object when configured.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasRealtimeEventConfig(value = {}) {
|
|
18
|
+
const source = normalizeRealtimeOptions(value);
|
|
19
|
+
return Object.hasOwn(source, "event") || Object.hasOwn(source, "events");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveOperationRealtimeOptions({
|
|
23
|
+
realtime = undefined,
|
|
24
|
+
fallbackRealtime = null
|
|
25
|
+
} = {}) {
|
|
26
|
+
if (realtime === false) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const fallback = normalizeRealtimeOptions(fallbackRealtime);
|
|
31
|
+
const explicit = realtime == null ? {} : normalizeRealtimeOptions(realtime);
|
|
32
|
+
if (hasRealtimeEventConfig(explicit) || !hasRealtimeEventConfig(fallback)) {
|
|
33
|
+
return Object.keys(explicit).length > 0 ? explicit : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
...fallback,
|
|
38
|
+
...explicit
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveEnabled(value) {
|
|
43
|
+
if (typeof value === "undefined") {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return Boolean(unref(value));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toQueryKeyList(value) {
|
|
51
|
+
const resolved = typeof value === "function" ? value() : unref(value);
|
|
52
|
+
if (!Array.isArray(resolved) || resolved.length < 1) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(resolved[0])) {
|
|
57
|
+
return resolved
|
|
58
|
+
.filter((entry) => Array.isArray(entry) && entry.length > 0)
|
|
59
|
+
.map((entry) => Object.freeze([...entry]));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return [Object.freeze([...resolved])];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function useRealtimeQueryInvalidation({
|
|
66
|
+
event = "",
|
|
67
|
+
enabled = true,
|
|
68
|
+
matches = null,
|
|
69
|
+
queryKey = null,
|
|
70
|
+
onEvent = null
|
|
71
|
+
} = {}) {
|
|
72
|
+
const queryClient = useQueryClient();
|
|
73
|
+
const normalizedEvent = computed(() => normalizeText(unref(event)));
|
|
74
|
+
const active = computed(() => resolveEnabled(enabled) && Boolean(normalizedEvent.value));
|
|
75
|
+
|
|
76
|
+
const listener = useRealtimeEvent({
|
|
77
|
+
event: normalizedEvent,
|
|
78
|
+
enabled: active,
|
|
79
|
+
matches,
|
|
80
|
+
onEvent: async (context) => {
|
|
81
|
+
if (typeof onEvent === "function") {
|
|
82
|
+
await onEvent(context);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const keys = toQueryKeyList(queryKey);
|
|
86
|
+
for (const key of keys) {
|
|
87
|
+
await queryClient.invalidateQueries({
|
|
88
|
+
queryKey: key
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
active: listener.active
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function useOperationRealtime({
|
|
100
|
+
realtime = null,
|
|
101
|
+
queryKey = null,
|
|
102
|
+
enabled = true
|
|
103
|
+
} = {}) {
|
|
104
|
+
const source = normalizeRealtimeOptions(realtime);
|
|
105
|
+
const eventList = [];
|
|
106
|
+
if (Object.hasOwn(source, "event")) {
|
|
107
|
+
const normalizedEvent = normalizeText(source.event);
|
|
108
|
+
if (normalizedEvent) {
|
|
109
|
+
eventList.push(normalizedEvent);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (Object.hasOwn(source, "events")) {
|
|
113
|
+
if (!Array.isArray(source.events)) {
|
|
114
|
+
throw new TypeError("realtime.events must be an array when configured.");
|
|
115
|
+
}
|
|
116
|
+
for (const entry of source.events) {
|
|
117
|
+
const normalizedEvent = normalizeText(entry);
|
|
118
|
+
if (!normalizedEvent || eventList.includes(normalizedEvent)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
eventList.push(normalizedEvent);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (eventList.length < 1) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const matches = typeof source.matches === "function" ? source.matches : null;
|
|
130
|
+
const onEvent = typeof source.onEvent === "function" ? source.onEvent : null;
|
|
131
|
+
const resolvedQueryKey = Object.hasOwn(source, "queryKey") ? source.queryKey : queryKey;
|
|
132
|
+
const active = computed(() => {
|
|
133
|
+
const sourceEnabled = Object.hasOwn(source, "enabled") ? source.enabled : true;
|
|
134
|
+
return resolveEnabled(enabled) && resolveEnabled(sourceEnabled);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const bindings = eventList.map((event) =>
|
|
138
|
+
useRealtimeQueryInvalidation({
|
|
139
|
+
event,
|
|
140
|
+
enabled: active,
|
|
141
|
+
matches,
|
|
142
|
+
queryKey: resolvedQueryKey,
|
|
143
|
+
onEvent
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return Object.freeze({
|
|
148
|
+
active: computed(() => bindings.some((binding) => Boolean(binding?.active?.value)))
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
resolveOperationRealtimeOptions,
|
|
154
|
+
useRealtimeQueryInvalidation,
|
|
155
|
+
useOperationRealtime
|
|
156
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { normalizeSurfaceId, resolveScopedRouteBase } from "@jskit-ai/kernel/shared/surface";
|
|
3
|
+
import {
|
|
4
|
+
ROUTE_VISIBILITY_WORKSPACE
|
|
5
|
+
} from "@jskit-ai/kernel/shared/support/visibility";
|
|
6
|
+
import { useAccess } from "./useAccess.js";
|
|
7
|
+
import { usePaths } from "@jskit-ai/shell-web/client/navigation/usePaths";
|
|
8
|
+
import { useSurfaceRouteContext } from "@jskit-ai/shell-web/client/navigation/useSurfaceRouteContext";
|
|
9
|
+
import { resolveSurfaceDefinitionFromPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
10
|
+
import {
|
|
11
|
+
asPlainObject,
|
|
12
|
+
ensureAccessModeCompatibility,
|
|
13
|
+
resolveAccessModeEnabled,
|
|
14
|
+
normalizeOwnershipFilter,
|
|
15
|
+
resolveApiSuffix
|
|
16
|
+
} from "./support/scopeHelpers.js";
|
|
17
|
+
import { extractRouteParamNames, toRouteParamValue } from "./support/routeTemplateHelpers.js";
|
|
18
|
+
|
|
19
|
+
function resolveScopedRouteParamNames(placementContext = null, surfaceId = "") {
|
|
20
|
+
const routeBase = resolveSurfaceDefinitionFromPlacementContext(placementContext, surfaceId)?.routeBase || "/";
|
|
21
|
+
return extractRouteParamNames(resolveScopedRouteBase(routeBase));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function useScopeRuntime({
|
|
25
|
+
ownershipFilter = ROUTE_VISIBILITY_WORKSPACE,
|
|
26
|
+
surfaceId = "",
|
|
27
|
+
accessMode = "auto",
|
|
28
|
+
hasPermissionRequirements = false,
|
|
29
|
+
placementSource = "http-web.scope-runtime"
|
|
30
|
+
} = {}) {
|
|
31
|
+
const normalizedOwnershipFilter = normalizeOwnershipFilter(ownershipFilter);
|
|
32
|
+
const normalizedAccessMode = ensureAccessModeCompatibility({
|
|
33
|
+
accessMode,
|
|
34
|
+
hasPermissionRequirements,
|
|
35
|
+
caller: "useScopeRuntime"
|
|
36
|
+
});
|
|
37
|
+
const accessRequired = resolveAccessModeEnabled(normalizedAccessMode, {
|
|
38
|
+
hasPermissionRequirements
|
|
39
|
+
});
|
|
40
|
+
const routeContext = useSurfaceRouteContext();
|
|
41
|
+
const paths = usePaths({
|
|
42
|
+
routeContext
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const resolvedSurfaceId = computed(() => {
|
|
46
|
+
const explicitSurfaceId = normalizeSurfaceId(surfaceId);
|
|
47
|
+
if (explicitSurfaceId) {
|
|
48
|
+
return explicitSurfaceId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return normalizeSurfaceId(routeContext.currentSurfaceId.value);
|
|
52
|
+
});
|
|
53
|
+
const scopedRouteParamNames = computed(() =>
|
|
54
|
+
resolveScopedRouteParamNames(routeContext.placementContext.value, resolvedSurfaceId.value)
|
|
55
|
+
);
|
|
56
|
+
const routeScopeParams = computed(() => {
|
|
57
|
+
const source = paths.routeParams.value;
|
|
58
|
+
const next = {};
|
|
59
|
+
for (const paramName of scopedRouteParamNames.value) {
|
|
60
|
+
const paramValue = toRouteParamValue(source[paramName]);
|
|
61
|
+
if (paramValue) {
|
|
62
|
+
next[paramName] = paramValue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return Object.freeze(next);
|
|
66
|
+
});
|
|
67
|
+
const missingScopedRouteParamNames = computed(() =>
|
|
68
|
+
scopedRouteParamNames.value.filter((paramName) => !routeScopeParams.value[paramName])
|
|
69
|
+
);
|
|
70
|
+
const requiresScopedRouteParams = computed(() => scopedRouteParamNames.value.length > 0);
|
|
71
|
+
const hasRequiredRouteScope = computed(() => missingScopedRouteParamNames.value.length < 1);
|
|
72
|
+
const scopeParamValue = computed(() => {
|
|
73
|
+
const [primaryScopeParamName = ""] = scopedRouteParamNames.value;
|
|
74
|
+
return primaryScopeParamName ? routeScopeParams.value[primaryScopeParamName] || "" : "";
|
|
75
|
+
});
|
|
76
|
+
const routeScopeError = computed(() => {
|
|
77
|
+
if (!requiresScopedRouteParams.value || hasRequiredRouteScope.value) {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const missingParams = missingScopedRouteParamNames.value.join(", ");
|
|
82
|
+
return `Route parameters ${missingParams} are required for surface "${resolvedSurfaceId.value || "<unknown>"}".`;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const accessRuntime = useAccess({
|
|
86
|
+
scopeParamValue,
|
|
87
|
+
enabled: computed(() => accessRequired && hasRequiredRouteScope.value),
|
|
88
|
+
access: normalizedAccessMode,
|
|
89
|
+
hasPermissionRequirements,
|
|
90
|
+
placementSource: String(placementSource || "http-web.scope-runtime")
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function resolveApiPath(apiSuffix = "", context = {}) {
|
|
94
|
+
if (routeScopeError.value) {
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const suffix = resolveApiSuffix(apiSuffix, {
|
|
99
|
+
surfaceId: routeContext.currentSurfaceId.value,
|
|
100
|
+
scopeParamValue: scopeParamValue.value,
|
|
101
|
+
ownershipFilter: normalizedOwnershipFilter,
|
|
102
|
+
...asPlainObject(context)
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return paths.api(suffix, {
|
|
106
|
+
surface: resolvedSurfaceId.value,
|
|
107
|
+
params: routeScopeParams.value
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function requireRouteScope(caller = "useScopeRuntime") {
|
|
112
|
+
if (routeScopeError.value) {
|
|
113
|
+
throw new Error(`${caller}: ${routeScopeError.value}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
normalizedOwnershipFilter,
|
|
119
|
+
requiresScopedRouteParams: requiresScopedRouteParams.value,
|
|
120
|
+
resolvedSurfaceId,
|
|
121
|
+
accessMode: normalizedAccessMode,
|
|
122
|
+
accessRequired,
|
|
123
|
+
routeContext,
|
|
124
|
+
scopedRouteParamNames,
|
|
125
|
+
routeScopeParams,
|
|
126
|
+
scopeParamValue,
|
|
127
|
+
hasRequiredRouteScope,
|
|
128
|
+
routeScopeError,
|
|
129
|
+
access: accessRuntime,
|
|
130
|
+
resolveApiPath,
|
|
131
|
+
requireRouteScope
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { useScopeRuntime };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
CRUD_LIST_FILTER_TYPE_FLAG,
|
|
3
|
+
CRUD_LIST_FILTER_TYPE_ENUM,
|
|
4
|
+
CRUD_LIST_FILTER_TYPE_ENUM_MANY,
|
|
5
|
+
CRUD_LIST_FILTER_TYPE_RECORD_ID,
|
|
6
|
+
CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY,
|
|
7
|
+
CRUD_LIST_FILTER_TYPE_DATE,
|
|
8
|
+
CRUD_LIST_FILTER_TYPE_DATE_RANGE,
|
|
9
|
+
CRUD_LIST_FILTER_TYPE_NUMBER_RANGE,
|
|
10
|
+
CRUD_LIST_FILTER_TYPE_PRESENCE,
|
|
11
|
+
CRUD_LIST_FILTER_PRESENCE_PRESENT,
|
|
12
|
+
CRUD_LIST_FILTER_PRESENCE_MISSING,
|
|
13
|
+
CRUD_LIST_FILTER_PRESENCE_OPTIONS,
|
|
14
|
+
defineCrudListFilters
|
|
15
|
+
} from "@jskit-ai/resource-crud-core/shared/crudListFilters";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { default as CrudAddEditScreen } from "./components/CrudAddEditScreen.vue";
|
|
2
|
+
export { default as CrudDeleteAction } from "./components/CrudDeleteAction.vue";
|
|
3
|
+
export { default as CrudListBulkActionSurface } from "./components/CrudListBulkActionSurface.vue";
|
|
4
|
+
export { default as CrudListFilterSurface } from "./components/CrudListFilterSurface.vue";
|
|
5
|
+
export { default as CrudListScreen } from "./components/CrudListScreen.vue";
|
|
6
|
+
export { default as CrudViewScreen } from "./components/CrudViewScreen.vue";
|
|
7
|
+
export { defineCrudListBulkActions } from "./bulkActions.js";
|
|
8
|
+
export { defineCrudListFilters } from "./filters.js";
|
|
9
|
+
export { defineCrudListRowActions } from "./rowActions.js";
|
|
10
|
+
export { useAddEdit } from "./composables/records/useAddEdit.js";
|
|
11
|
+
export { useAccess } from "./composables/useAccess.js";
|
|
12
|
+
export { useCommand } from "./composables/useCommand.js";
|
|
13
|
+
export { useEndpointResource } from "./composables/runtime/useEndpointResource.js";
|
|
14
|
+
export { useList } from "./composables/records/useList.js";
|
|
15
|
+
export { usePagedCollection } from "./composables/usePagedCollection.js";
|
|
16
|
+
export { useRealtimeQueryInvalidation } from "./composables/useRealtimeQueryInvalidation.js";
|
|
17
|
+
export { useUiFeedback } from "./composables/runtime/useUiFeedback.js";
|
|
18
|
+
export { useView } from "./composables/records/useView.js";
|
|
19
|
+
export {
|
|
20
|
+
configureHttpWebClient,
|
|
21
|
+
createHttpWebClient,
|
|
22
|
+
getHttpWebClient,
|
|
23
|
+
httpWebClient
|
|
24
|
+
} from "./lib/httpClient.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createTransientRetryHttpClient } from "@jskit-ai/http-runtime/client";
|
|
2
|
+
|
|
3
|
+
function normalizeOptions(value = null) {
|
|
4
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function createHttpWebClient(options = {}) {
|
|
8
|
+
const source = normalizeOptions(options);
|
|
9
|
+
const sourceCsrf = normalizeOptions(source.csrf);
|
|
10
|
+
|
|
11
|
+
return createTransientRetryHttpClient({
|
|
12
|
+
...source,
|
|
13
|
+
credentials: source.credentials ?? "include",
|
|
14
|
+
csrf: {
|
|
15
|
+
sessionPath: "/api/session",
|
|
16
|
+
...sourceCsrf
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let httpWebClient = createHttpWebClient();
|
|
22
|
+
|
|
23
|
+
function configureHttpWebClient(optionsOrClient = {}) {
|
|
24
|
+
const source = normalizeOptions(optionsOrClient);
|
|
25
|
+
httpWebClient =
|
|
26
|
+
typeof source.request === "function"
|
|
27
|
+
? source
|
|
28
|
+
: createHttpWebClient(source);
|
|
29
|
+
return httpWebClient;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getHttpWebClient() {
|
|
33
|
+
return httpWebClient;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resetHttpWebClientForTests() {
|
|
37
|
+
httpWebClient = createHttpWebClient();
|
|
38
|
+
return httpWebClient;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
configureHttpWebClient,
|
|
43
|
+
createHttpWebClient,
|
|
44
|
+
getHttpWebClient,
|
|
45
|
+
resetHttpWebClientForTests,
|
|
46
|
+
httpWebClient
|
|
47
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasPermission,
|
|
3
|
+
normalizePermissionList
|
|
4
|
+
} from "@jskit-ai/kernel/shared/support";
|
|
5
|
+
|
|
6
|
+
function toPermissionSet(values) {
|
|
7
|
+
const normalized = normalizePermissionList(values);
|
|
8
|
+
if (normalized.length < 1) {
|
|
9
|
+
return new Set();
|
|
10
|
+
}
|
|
11
|
+
return new Set(normalized);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function arePermissionListsEqual(left, right) {
|
|
15
|
+
const leftSet = toPermissionSet(left);
|
|
16
|
+
const rightSet = toPermissionSet(right);
|
|
17
|
+
if (leftSet.size !== rightSet.size) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const permission of leftSet) {
|
|
22
|
+
if (!rightSet.has(permission)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
normalizePermissionList,
|
|
32
|
+
arePermissionListsEqual,
|
|
33
|
+
hasPermission
|
|
34
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function normalizeCrudListRowAction(rawAction = {}, index = 0) {
|
|
4
|
+
if (!rawAction || typeof rawAction !== "object" || Array.isArray(rawAction)) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const key = normalizeText(rawAction.key || rawAction.id || `action-${index + 1}`);
|
|
9
|
+
const label = normalizeText(rawAction.label || rawAction.title);
|
|
10
|
+
if (!key || !label) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
key,
|
|
16
|
+
label,
|
|
17
|
+
icon: normalizeText(rawAction.icon),
|
|
18
|
+
color: normalizeText(rawAction.color, { fallback: "primary" }),
|
|
19
|
+
run: typeof rawAction.run === "function" ? rawAction.run : null,
|
|
20
|
+
visible: rawAction.visible,
|
|
21
|
+
disabled: rawAction.disabled,
|
|
22
|
+
loading: rawAction.loading
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function defineCrudListRowActions(actions = []) {
|
|
27
|
+
if (!Array.isArray(actions)) {
|
|
28
|
+
throw new TypeError("defineCrudListRowActions requires an array.");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const normalizedActions = [];
|
|
32
|
+
const seenKeys = new Set();
|
|
33
|
+
|
|
34
|
+
actions.forEach((rawAction, index) => {
|
|
35
|
+
const action = normalizeCrudListRowAction(rawAction, index);
|
|
36
|
+
if (!action || seenKeys.has(action.key)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
seenKeys.add(action.key);
|
|
41
|
+
normalizedActions.push(action);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return Object.freeze(normalizedActions);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { defineCrudListRowActions };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { unref } from "vue";
|
|
2
|
+
|
|
3
|
+
function requireRecord(value, label = "value", owner = "Contract") {
|
|
4
|
+
const resolved = unref(value);
|
|
5
|
+
if (!resolved || typeof resolved !== "object" || Array.isArray(resolved)) {
|
|
6
|
+
throw new TypeError(`${owner} expects ${label} to be an object.`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return resolved;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function requireBoolean(value, label = "value", owner = "Contract") {
|
|
13
|
+
const resolved = unref(value);
|
|
14
|
+
if (typeof resolved !== "boolean") {
|
|
15
|
+
throw new TypeError(`${owner} expects ${label} to be a boolean.`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requireFunction(value, label = "value", owner = "Contract") {
|
|
22
|
+
const resolved = unref(value);
|
|
23
|
+
if (typeof resolved !== "function") {
|
|
24
|
+
throw new TypeError(`${owner} expects ${label} to be a function.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return resolved;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
requireRecord,
|
|
32
|
+
requireBoolean,
|
|
33
|
+
requireFunction
|
|
34
|
+
};
|