@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.
Files changed (110) hide show
  1. package/fixtures/crud-list-date-filters/App.vue +107 -0
  2. package/fixtures/crud-list-date-filters/index.html +12 -0
  3. package/fixtures/crud-list-date-filters/main.js +24 -0
  4. package/fixtures/crud-list-date-filters/vite.config.mjs +18 -0
  5. package/package.json +150 -0
  6. package/src/client/bulkActions.js +47 -0
  7. package/src/client/components/CrudAddEditScreen.vue +186 -0
  8. package/src/client/components/CrudDeleteAction.vue +93 -0
  9. package/src/client/components/CrudListBulkActionSurface.vue +126 -0
  10. package/src/client/components/CrudListDateFilterControl.vue +188 -0
  11. package/src/client/components/CrudListFilterSurface.vue +388 -0
  12. package/src/client/components/CrudListRecordActionMenu.vue +120 -0
  13. package/src/client/components/CrudListScreen.vue +501 -0
  14. package/src/client/components/CrudViewScreen.vue +197 -0
  15. package/src/client/composables/crud/crudBindingSupport.js +75 -0
  16. package/src/client/composables/crud/crudHttpClientSupport.js +57 -0
  17. package/src/client/composables/crud/crudJsonApiTransportSupport.js +145 -0
  18. package/src/client/composables/crud/crudLookupFieldLabelSupport.js +151 -0
  19. package/src/client/composables/crud/crudLookupFieldRuntime.js +251 -0
  20. package/src/client/composables/crud/crudSchemaFormHelpers.js +464 -0
  21. package/src/client/composables/internal/crudListFilterLookupSupport.js +161 -0
  22. package/src/client/composables/internal/crudListParentTitleSupport.js +171 -0
  23. package/src/client/composables/internal/useOperationScope.js +144 -0
  24. package/src/client/composables/records/useAddEdit.js +224 -0
  25. package/src/client/composables/records/useCrudAddEdit.js +227 -0
  26. package/src/client/composables/records/useCrudList.js +101 -0
  27. package/src/client/composables/records/useCrudView.js +47 -0
  28. package/src/client/composables/records/useList.js +500 -0
  29. package/src/client/composables/records/useView.js +164 -0
  30. package/src/client/composables/runtime/addEditUiRuntime.js +130 -0
  31. package/src/client/composables/runtime/listUiRuntime.js +123 -0
  32. package/src/client/composables/runtime/modelStateHelpers.js +90 -0
  33. package/src/client/composables/runtime/operationAdapters.js +34 -0
  34. package/src/client/composables/runtime/operationUiHelpers.js +125 -0
  35. package/src/client/composables/runtime/operationValidationHelpers.js +51 -0
  36. package/src/client/composables/runtime/useAddEditCore.js +112 -0
  37. package/src/client/composables/runtime/useCommandCore.js +123 -0
  38. package/src/client/composables/runtime/useEndpointResource.js +199 -0
  39. package/src/client/composables/runtime/useFieldErrorBag.js +61 -0
  40. package/src/client/composables/runtime/useListCore.js +112 -0
  41. package/src/client/composables/runtime/useUiFeedback.js +98 -0
  42. package/src/client/composables/runtime/useViewCore.js +93 -0
  43. package/src/client/composables/runtime/viewUiRuntime.js +96 -0
  44. package/src/client/composables/support/errorMessageHelpers.js +66 -0
  45. package/src/client/composables/support/listQueryParamSupport.js +473 -0
  46. package/src/client/composables/support/listSearchSupport.js +70 -0
  47. package/src/client/composables/support/refValueHelpers.js +19 -0
  48. package/src/client/composables/support/requestQueryRuntimeSupport.js +141 -0
  49. package/src/client/composables/support/resourceLoadStateHelpers.js +208 -0
  50. package/src/client/composables/support/routeTemplateHelpers.js +285 -0
  51. package/src/client/composables/support/scopeHelpers.js +145 -0
  52. package/src/client/composables/useAccess.js +109 -0
  53. package/src/client/composables/useCommand.js +118 -0
  54. package/src/client/composables/useCrudAddEditScreen.js +88 -0
  55. package/src/client/composables/useCrudDeleteAction.js +160 -0
  56. package/src/client/composables/useCrudListBulkActions.js +147 -0
  57. package/src/client/composables/useCrudListFilterLookups.js +146 -0
  58. package/src/client/composables/useCrudListFilters.js +339 -0
  59. package/src/client/composables/useCrudListParentTitle.js +151 -0
  60. package/src/client/composables/useCrudListRowActions.js +144 -0
  61. package/src/client/composables/useCrudListScreen.js +237 -0
  62. package/src/client/composables/useCrudViewScreen.js +77 -0
  63. package/src/client/composables/usePagedCollection.js +181 -0
  64. package/src/client/composables/useRealtimeQueryInvalidation.js +156 -0
  65. package/src/client/composables/useScopeRuntime.js +135 -0
  66. package/src/client/filters.js +15 -0
  67. package/src/client/index.js +24 -0
  68. package/src/client/lib/httpClient.js +47 -0
  69. package/src/client/lib/permissions.js +34 -0
  70. package/src/client/rowActions.js +47 -0
  71. package/src/client/support/contractGuards.js +34 -0
  72. package/src/client/support/crudListDateFilterSupport.js +91 -0
  73. package/test/addEditUiRuntime.test.js +110 -0
  74. package/test/crudBindingSupport.test.js +110 -0
  75. package/test/crudHttpClientSupport.test.js +77 -0
  76. package/test/crudJsonApiTransportSupport.test.js +179 -0
  77. package/test/crudListBulkActionSurface.test.js +27 -0
  78. package/test/crudListDateFilterSupport.test.js +92 -0
  79. package/test/crudListDateFilterSurface.browser.test.js +182 -0
  80. package/test/crudListFilterSurface.test.js +90 -0
  81. package/test/crudLookupFieldRuntime.test.js +264 -0
  82. package/test/crudScreenComponents.test.js +110 -0
  83. package/test/errorIntentContract.test.js +31 -0
  84. package/test/errorMessageHelpers.test.js +28 -0
  85. package/test/exportsContract.test.js +68 -0
  86. package/test/listQueryParamSupport.test.js +223 -0
  87. package/test/listUiRuntime.test.js +102 -0
  88. package/test/operationValidationHelpers.test.js +64 -0
  89. package/test/packageBoundary.test.js +31 -0
  90. package/test/permissions.test.js +35 -0
  91. package/test/refValueHelpers.test.js +14 -0
  92. package/test/requestTransportOptions.test.js +457 -0
  93. package/test/resourceLoadStateHelpers.test.js +189 -0
  94. package/test/routeTemplateHelpers.test.js +85 -0
  95. package/test/scopeHelpers.test.js +57 -0
  96. package/test/useAddEditCore.test.js +124 -0
  97. package/test/useAddEditRequestQueryParams.test.js +190 -0
  98. package/test/useCrudAddEdit.test.js +415 -0
  99. package/test/useCrudDeleteAction.test.js +139 -0
  100. package/test/useCrudListBulkActions.test.js +65 -0
  101. package/test/useCrudListFilterLookups.test.js +114 -0
  102. package/test/useCrudListFilters.test.js +293 -0
  103. package/test/useCrudListParentTitle.test.js +262 -0
  104. package/test/useCrudListRowActions.test.js +77 -0
  105. package/test/useListRouteInitialization.test.js +337 -0
  106. package/test/useListSearchSupport.test.js +61 -0
  107. package/test/usePagedCollection.test.js +53 -0
  108. package/test/useViewRequestQueryParams.test.js +20 -0
  109. package/test/viewCoreLoading.test.js +44 -0
  110. 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
+ };