@loj-lang/rdsl-runtime 0.5.0 → 0.6.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 (125) hide show
  1. package/dist/components/Badge.d.ts +2 -1
  2. package/dist/components/Badge.d.ts.map +1 -1
  3. package/dist/components/Badge.js +2 -2
  4. package/dist/components/ConfirmDialog.d.ts +6 -1
  5. package/dist/components/ConfirmDialog.d.ts.map +1 -1
  6. package/dist/components/ConfirmDialog.js +35 -6
  7. package/dist/components/DataTable.d.ts +35 -2
  8. package/dist/components/DataTable.d.ts.map +1 -1
  9. package/dist/components/DataTable.js +64 -38
  10. package/dist/components/DropdownButton.d.ts +11 -0
  11. package/dist/components/DropdownButton.d.ts.map +1 -0
  12. package/dist/components/DropdownButton.js +9 -0
  13. package/dist/components/ErrorBoundary.d.ts +18 -0
  14. package/dist/components/ErrorBoundary.d.ts.map +1 -0
  15. package/dist/components/ErrorBoundary.js +19 -0
  16. package/dist/components/ErrorState.d.ts +6 -0
  17. package/dist/components/ErrorState.d.ts.map +1 -0
  18. package/dist/components/ErrorState.js +25 -0
  19. package/dist/components/FilterBar.d.ts +2 -0
  20. package/dist/components/FilterBar.d.ts.map +1 -1
  21. package/dist/components/FilterBar.js +3 -2
  22. package/dist/components/FormField.d.ts +7 -2
  23. package/dist/components/FormField.d.ts.map +1 -1
  24. package/dist/components/FormField.js +16 -5
  25. package/dist/components/GroupedDataTable.d.ts.map +1 -1
  26. package/dist/components/GroupedDataTable.js +40 -59
  27. package/dist/components/Pagination.js +1 -1
  28. package/dist/components/PivotDataTable.d.ts.map +1 -1
  29. package/dist/components/PivotDataTable.js +42 -65
  30. package/dist/components/Tag.d.ts +2 -1
  31. package/dist/components/Tag.d.ts.map +1 -1
  32. package/dist/components/Tag.js +2 -2
  33. package/dist/components/WorkflowSummary.js +6 -6
  34. package/dist/components/readFormatting.d.ts +2 -0
  35. package/dist/components/readFormatting.d.ts.map +1 -0
  36. package/dist/components/readFormatting.js +1 -0
  37. package/dist/derivations.d.ts +3 -0
  38. package/dist/derivations.d.ts.map +1 -0
  39. package/dist/derivations.js +1 -0
  40. package/dist/hooks/browserStorage.d.ts +3 -0
  41. package/dist/hooks/browserStorage.d.ts.map +1 -0
  42. package/dist/hooks/browserStorage.js +1 -0
  43. package/dist/hooks/deleteConfirmation.d.ts +20 -0
  44. package/dist/hooks/deleteConfirmation.d.ts.map +1 -0
  45. package/dist/hooks/deleteConfirmation.js +21 -0
  46. package/dist/hooks/exportDownload.d.ts +16 -0
  47. package/dist/hooks/exportDownload.d.ts.map +1 -0
  48. package/dist/hooks/exportDownload.js +55 -0
  49. package/dist/hooks/formDerivations.d.ts +7 -0
  50. package/dist/hooks/formDerivations.d.ts.map +1 -0
  51. package/dist/hooks/formDerivations.js +23 -0
  52. package/dist/hooks/formSeeds.d.ts +12 -0
  53. package/dist/hooks/formSeeds.d.ts.map +1 -0
  54. package/dist/hooks/formSeeds.js +26 -0
  55. package/dist/hooks/navigation.d.ts +37 -0
  56. package/dist/hooks/navigation.d.ts.map +1 -1
  57. package/dist/hooks/navigation.js +107 -70
  58. package/dist/hooks/readModelStore.d.ts +29 -0
  59. package/dist/hooks/readModelStore.d.ts.map +1 -0
  60. package/dist/hooks/readModelStore.js +135 -0
  61. package/dist/hooks/recordScopedWorkflow.d.ts +13 -0
  62. package/dist/hooks/recordScopedWorkflow.d.ts.map +1 -0
  63. package/dist/hooks/recordScopedWorkflow.js +40 -0
  64. package/dist/hooks/resourceClient.d.ts +39 -0
  65. package/dist/hooks/resourceClient.d.ts.map +1 -1
  66. package/dist/hooks/resourceClient.js +205 -71
  67. package/dist/hooks/resourceErrors.d.ts +13 -0
  68. package/dist/hooks/resourceErrors.d.ts.map +1 -0
  69. package/dist/hooks/resourceErrors.js +140 -0
  70. package/dist/hooks/resourceRowActions.d.ts +17 -0
  71. package/dist/hooks/resourceRowActions.d.ts.map +1 -0
  72. package/dist/hooks/resourceRowActions.js +48 -0
  73. package/dist/hooks/resourceStore.d.ts +2 -24
  74. package/dist/hooks/resourceStore.d.ts.map +1 -1
  75. package/dist/hooks/resourceStore.js +1 -164
  76. package/dist/hooks/resourceTarget.d.ts +1 -1
  77. package/dist/hooks/resourceTarget.d.ts.map +1 -1
  78. package/dist/hooks/resourceTarget.js +1 -22
  79. package/dist/hooks/serverListStore.d.ts +3 -0
  80. package/dist/hooks/serverListStore.d.ts.map +1 -0
  81. package/dist/hooks/serverListStore.js +1 -0
  82. package/dist/hooks/sessionCaches.d.ts +6 -0
  83. package/dist/hooks/sessionCaches.d.ts.map +1 -0
  84. package/dist/hooks/sessionCaches.js +14 -0
  85. package/dist/hooks/transitionedSetter.d.ts +2 -0
  86. package/dist/hooks/transitionedSetter.d.ts.map +1 -0
  87. package/dist/hooks/transitionedSetter.js +9 -0
  88. package/dist/hooks/useAuth.d.ts +18 -0
  89. package/dist/hooks/useAuth.d.ts.map +1 -1
  90. package/dist/hooks/useAuth.js +30 -0
  91. package/dist/hooks/useCollectionView.d.ts +2 -8
  92. package/dist/hooks/useCollectionView.d.ts.map +1 -1
  93. package/dist/hooks/useCollectionView.js +8 -36
  94. package/dist/hooks/useDocumentMetadata.d.ts.map +1 -1
  95. package/dist/hooks/useDocumentMetadata.js +57 -0
  96. package/dist/hooks/useGroupedCollectionView.d.ts +2 -7
  97. package/dist/hooks/useGroupedCollectionView.d.ts.map +1 -1
  98. package/dist/hooks/useGroupedCollectionView.js +8 -52
  99. package/dist/hooks/useI18n.d.ts +21 -0
  100. package/dist/hooks/useI18n.d.ts.map +1 -0
  101. package/dist/hooks/useI18n.js +86 -0
  102. package/dist/hooks/useReadModel.d.ts +15 -0
  103. package/dist/hooks/useReadModel.d.ts.map +1 -1
  104. package/dist/hooks/useReadModel.js +127 -40
  105. package/dist/hooks/useResource.d.ts +7 -0
  106. package/dist/hooks/useResource.d.ts.map +1 -1
  107. package/dist/hooks/useResource.js +159 -3
  108. package/dist/hooks/useStoredState.d.ts +15 -0
  109. package/dist/hooks/useStoredState.d.ts.map +1 -0
  110. package/dist/hooks/useStoredState.js +28 -0
  111. package/dist/hooks/useToast.d.ts.map +1 -1
  112. package/dist/hooks/useToast.js +1 -1
  113. package/dist/hooks/workflowTransition.d.ts +21 -0
  114. package/dist/hooks/workflowTransition.d.ts.map +1 -0
  115. package/dist/hooks/workflowTransition.js +54 -0
  116. package/dist/index.d.ts +35 -9
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +21 -5
  119. package/dist/policies/can.d.ts +2 -14
  120. package/dist/policies/can.d.ts.map +1 -1
  121. package/dist/policies/can.js +1 -160
  122. package/dist/workflow.d.ts +3 -0
  123. package/dist/workflow.d.ts.map +1 -0
  124. package/dist/workflow.js +1 -0
  125. package/package.json +142 -26
@@ -1,16 +1,20 @@
1
1
  import React from 'react';
2
+ import { LOJ_TRANSPORT_CONTRACT_HEADER } from '@loj-lang/shared-contracts';
3
+ import { hasUsableAuthSession, resolveUnauthorizedLoginHref, } from '../../../runtime-core/dist/auth.js';
4
+ import { buildCsvContent, normalizeListPagePayload as normalizeListPagePayloadCore, normalizeListPayload as normalizeListPayloadCore, normalizeRecordPayload as normalizeRecordPayloadCore, paginateMemoryCollection, } from '../../../runtime-core/dist/resourcePayload.js';
5
+ import { appendResourceQuery, createAuthorizationHeaders, exportAcceptHeader, mergeHeaders, resolveResourceUrl, resourceLeaf, } from '../../../runtime-core/dist/resourceRequest.js';
6
+ import { createHttpError, createNetworkError, readJsonPayload, ResourceClientError, waitForRetry, } from '../../../runtime-core/dist/resourceHttp.js';
7
+ export { ResourceClientError } from '../../../runtime-core/dist/resourceHttp.js';
8
+ import { parseContentDispositionFilename as parseContentDispositionFilenameCore, resolveTransportContractMismatchMessage, } from '../../../runtime-core/dist/transport.js';
2
9
  const ResourceClientContext = React.createContext(undefined);
3
10
  const memoryCollections = new Map();
4
11
  const fetchClientCache = new Map();
5
12
  let sameOriginFetchClient = null;
6
13
  let defaultMemoryClient = null;
14
+ const warnedTransportContractVersions = new Set();
7
15
  function isRecord(value) {
8
16
  return typeof value === 'object' && value !== null && !Array.isArray(value);
9
17
  }
10
- function resourceLeaf(api) {
11
- const parts = api.split('/').filter(Boolean);
12
- return parts[parts.length - 1] ?? 'record';
13
- }
14
18
  function createSyntheticId(api, collection) {
15
19
  const id = `${resourceLeaf(api)}-${collection.nextId}`;
16
20
  collection.nextId += 1;
@@ -27,75 +31,50 @@ function ensureMemoryCollection(api) {
27
31
  memoryCollections.set(api, created);
28
32
  return created;
29
33
  }
30
- function withStringId(payload, api, operation) {
31
- if (!isRecord(payload) || payload.id === undefined || payload.id === null) {
32
- throw new Error(`Invalid ${operation} response for ${api}: expected a record with an id`);
33
- }
34
- return {
35
- ...payload,
36
- id: String(payload.id),
37
- };
38
- }
39
34
  export function normalizeListPayload(payload, api) {
40
- const items = Array.isArray(payload)
41
- ? payload
42
- : isRecord(payload) && Array.isArray(payload.items)
43
- ? payload.items
44
- : isRecord(payload) && Array.isArray(payload.data)
45
- ? payload.data
46
- : null;
47
- if (!items) {
48
- throw new Error(`Invalid list response for ${api}: expected an array or { items: [] }`);
49
- }
50
- return items.map((item) => withStringId(item, api, 'list'));
35
+ return normalizeListPayloadCore(payload, api);
36
+ }
37
+ export function normalizeListPagePayload(payload, api, query = {}) {
38
+ return normalizeListPagePayloadCore(payload, api, query);
51
39
  }
52
40
  export function normalizeRecordPayload(payload, api, operation) {
53
- const record = isRecord(payload) && isRecord(payload.item)
54
- ? payload.item
55
- : isRecord(payload) && isRecord(payload.data)
56
- ? payload.data
57
- : payload;
58
- return withStringId(record, api, operation);
41
+ return normalizeRecordPayloadCore(payload, api, operation);
59
42
  }
60
43
  async function resolveHeaders(headers) {
61
44
  if (!headers)
62
45
  return undefined;
63
46
  return typeof headers === 'function' ? await headers() : headers;
64
47
  }
65
- function mergeHeaders(...headerSets) {
66
- const merged = new Headers();
67
- for (const headerSet of headerSets) {
68
- if (!headerSet)
69
- continue;
70
- const next = new Headers(headerSet);
71
- next.forEach((value, key) => {
72
- merged.set(key, value);
73
- });
74
- }
75
- return merged;
48
+ async function resolveAuthState(auth) {
49
+ if (!auth)
50
+ return undefined;
51
+ return typeof auth === 'function' ? await auth() : auth;
76
52
  }
77
- function resolveResourceUrl(api, baseUrl) {
78
- if (/^https?:\/\//i.test(api))
79
- return api;
80
- if (!baseUrl || baseUrl.trim() === '')
81
- return api;
82
- const normalizedBase = baseUrl.trim();
83
- if (!/^https?:\/\//i.test(normalizedBase)) {
84
- return api;
53
+ async function handleUnauthorized(authState) {
54
+ if (!authState)
55
+ return;
56
+ if (authState.onUnauthorized) {
57
+ await authState.onUnauthorized();
58
+ }
59
+ const loginHref = resolveUnauthorizedLoginHref(authState, typeof window !== 'undefined' ? window.location?.href : undefined);
60
+ if (loginHref && typeof window !== 'undefined' && window.location?.href !== loginHref) {
61
+ window.location.assign(loginHref);
85
62
  }
86
- const absoluteBase = normalizedBase.endsWith('/') ? normalizedBase : `${normalizedBase}/`;
87
- return new URL(api.startsWith('/') ? api.slice(1) : api, absoluteBase).toString();
88
63
  }
89
- async function readJsonPayload(response, api, method) {
90
- const text = await response.text();
91
- if (text.trim() === '')
92
- return undefined;
93
- try {
94
- return JSON.parse(text);
64
+ function parseContentDispositionFilename(value) {
65
+ return parseContentDispositionFilenameCore(value);
66
+ }
67
+ function inspectTransportContractHeader(response) {
68
+ const version = response.headers?.get(LOJ_TRANSPORT_CONTRACT_HEADER)?.trim();
69
+ const warningMessage = resolveTransportContractMismatchMessage(version);
70
+ if (!warningMessage || !version) {
71
+ return;
95
72
  }
96
- catch (error) {
97
- throw new Error(`Invalid JSON response for ${method} ${api}: ${error instanceof Error ? error.message : String(error)}`);
73
+ if (warnedTransportContractVersions.has(version)) {
74
+ return;
98
75
  }
76
+ warnedTransportContractVersions.add(version);
77
+ console.warn(warningMessage);
99
78
  }
100
79
  export function createFetchResourceClient(options = {}) {
101
80
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -103,21 +82,107 @@ export function createFetchResourceClient(options = {}) {
103
82
  throw new Error('Fetch is not available; provide options.fetch or use ResourceProvider with a custom client');
104
83
  }
105
84
  async function request(method, api, init = {}) {
106
- const requestHeaders = mergeHeaders(await resolveHeaders(options.headers), init.headers);
107
- const response = await fetchImpl(resolveResourceUrl(api, options.baseUrl), {
108
- ...init,
85
+ const staticHeaders = await resolveHeaders(options.headers);
86
+ const requestUrl = resolveResourceUrl(api, options.baseUrl);
87
+ const issueRequest = async (authState) => {
88
+ const requestHeaders = mergeHeaders(createAuthorizationHeaders(authState), staticHeaders, init.headers);
89
+ const response = await fetchImpl(requestUrl, {
90
+ ...init,
91
+ method,
92
+ credentials: options.credentials,
93
+ headers: requestHeaders,
94
+ });
95
+ inspectTransportContractHeader(response);
96
+ const payload = await readJsonPayload(response, api, method);
97
+ return { response: response, payload, authState };
98
+ };
99
+ const maxAttempts = method === 'GET' ? 3 : 1;
100
+ let authState = await resolveAuthState(options.auth);
101
+ let lastError = null;
102
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
103
+ try {
104
+ let { response, payload } = await issueRequest(authState);
105
+ if (!response.ok && response.status === 401 && authState?.refreshSession) {
106
+ const refreshedSession = await authState.refreshSession();
107
+ if (hasUsableAuthSession(refreshedSession)) {
108
+ authState = {
109
+ ...authState,
110
+ session: refreshedSession,
111
+ };
112
+ ({ response, payload } = await issueRequest(authState));
113
+ }
114
+ }
115
+ if (!response.ok) {
116
+ if (response.status === 401) {
117
+ await handleUnauthorized(authState);
118
+ }
119
+ const httpError = createHttpError(method, api, response.status, payload, response.headers);
120
+ if (attempt < maxAttempts && httpError.retryable) {
121
+ lastError = httpError;
122
+ await waitForRetry(attempt);
123
+ continue;
124
+ }
125
+ throw httpError;
126
+ }
127
+ return payload;
128
+ }
129
+ catch (error) {
130
+ const normalizedError = createNetworkError(method, api, error);
131
+ if (attempt < maxAttempts && normalizedError.retryable) {
132
+ lastError = normalizedError;
133
+ await waitForRetry(attempt);
134
+ continue;
135
+ }
136
+ throw normalizedError;
137
+ }
138
+ }
139
+ throw lastError ?? new ResourceClientError({
140
+ message: `${method} ${api} failed`,
109
141
  method,
110
- credentials: options.credentials,
111
- headers: requestHeaders,
142
+ path: api,
112
143
  });
113
- const payload = await readJsonPayload(response, api, method);
144
+ }
145
+ async function requestBinary(api, init = {}) {
146
+ const staticHeaders = await resolveHeaders(options.headers);
147
+ const requestUrl = resolveResourceUrl(api, options.baseUrl);
148
+ let authState = await resolveAuthState(options.auth);
149
+ const issueRequest = async (currentAuthState) => {
150
+ const requestHeaders = mergeHeaders(createAuthorizationHeaders(currentAuthState), staticHeaders, init.headers);
151
+ const response = await fetchImpl(requestUrl, {
152
+ ...init,
153
+ method: 'GET',
154
+ credentials: options.credentials,
155
+ headers: requestHeaders,
156
+ });
157
+ inspectTransportContractHeader(response);
158
+ return response;
159
+ };
160
+ let response = await issueRequest(authState);
161
+ if (!response.ok && response.status === 401 && authState?.refreshSession) {
162
+ const refreshedSession = await authState.refreshSession();
163
+ if (hasUsableAuthSession(refreshedSession)) {
164
+ authState = {
165
+ ...authState,
166
+ session: refreshedSession,
167
+ };
168
+ response = await issueRequest(authState);
169
+ }
170
+ }
114
171
  if (!response.ok) {
115
- const detail = isRecord(payload) && typeof payload.message === 'string'
116
- ? `: ${payload.message}`
117
- : '';
118
- throw new Error(`${method} ${api} failed with ${response.status}${detail}`);
172
+ const payload = await readJsonPayload(response, api, 'GET');
173
+ if (response.status === 401) {
174
+ await handleUnauthorized(authState);
175
+ }
176
+ throw createHttpError('GET', api, response.status, payload, response.headers);
119
177
  }
120
- return payload;
178
+ const arrayBuffer = response.arrayBuffer
179
+ ? await response.arrayBuffer()
180
+ : new TextEncoder().encode(await response.text()).buffer;
181
+ return {
182
+ bytes: arrayBuffer,
183
+ contentType: response.headers?.get('content-type') ?? undefined,
184
+ filename: parseContentDispositionFilename(response.headers?.get('content-disposition')),
185
+ };
121
186
  }
122
187
  return {
123
188
  async get(path) {
@@ -128,6 +193,11 @@ export function createFetchResourceClient(options = {}) {
128
193
  const payload = await request('GET', api);
129
194
  return normalizeListPayload(payload, api);
130
195
  },
196
+ async listPage(api, query = {}) {
197
+ const requestPath = appendResourceQuery(api, query);
198
+ const payload = await request('GET', requestPath);
199
+ return normalizeListPagePayload(payload, api, query);
200
+ },
131
201
  async create(api, input) {
132
202
  const payload = await request('POST', api, {
133
203
  body: JSON.stringify(input),
@@ -157,6 +227,23 @@ export function createFetchResourceClient(options = {}) {
157
227
  async delete(api, id) {
158
228
  await request('DELETE', `${api}/${encodeURIComponent(id)}`);
159
229
  },
230
+ async batch(api, action, ids) {
231
+ await request('POST', `${api}/batch`, {
232
+ body: JSON.stringify({ action, ids }),
233
+ headers: {
234
+ Accept: 'application/json',
235
+ 'Content-Type': 'application/json',
236
+ },
237
+ });
238
+ },
239
+ async export(api, format, query = {}) {
240
+ const requestPath = appendResourceQuery(`${api}/export?format=${encodeURIComponent(format)}`, query);
241
+ return requestBinary(requestPath, {
242
+ headers: {
243
+ Accept: exportAcceptHeader(format),
244
+ },
245
+ });
246
+ },
160
247
  async post(path, input) {
161
248
  const payload = await request('POST', path, {
162
249
  body: input === undefined ? undefined : JSON.stringify(input),
@@ -178,6 +265,10 @@ export function createMemoryResourceClient() {
178
265
  async list(api) {
179
266
  return ensureMemoryCollection(api).items.map((item) => ({ ...item }));
180
267
  },
268
+ async listPage(api, query = {}) {
269
+ const collection = ensureMemoryCollection(api).items.map((item) => ({ ...item }));
270
+ return paginateMemoryCollection(collection, query);
271
+ },
181
272
  async create(api, input) {
182
273
  const collection = ensureMemoryCollection(api);
183
274
  const nextRecord = {
@@ -205,6 +296,45 @@ export function createMemoryResourceClient() {
205
296
  const collection = ensureMemoryCollection(api);
206
297
  collection.items = collection.items.filter((item) => item.id !== id);
207
298
  },
299
+ async batch(api, action, ids) {
300
+ if (action !== 'delete') {
301
+ throw new Error(`Memory resource client does not support batch action: ${action}`);
302
+ }
303
+ const collection = ensureMemoryCollection(api);
304
+ const idSet = new Set(ids.map((id) => String(id)));
305
+ collection.items = collection.items.filter((item) => !idSet.has(String(item.id)));
306
+ },
307
+ async export(api, format, query = {}) {
308
+ const normalizedFormat = String(format ?? '').trim().toLowerCase();
309
+ if (normalizedFormat !== 'csv' && normalizedFormat !== 'xlsx') {
310
+ throw new Error(`Memory resource client does not support export format: ${format}`);
311
+ }
312
+ const page = await this.listPage(api, {
313
+ sort: query.sort,
314
+ filters: query.filters,
315
+ size: Number.MAX_SAFE_INTEGER,
316
+ });
317
+ const items = page.items;
318
+ const orderedFields = (query.fields ?? []).map((field) => String(field ?? '').trim()).filter((field) => field !== '');
319
+ const fallbackFields = Array.from(items.reduce((keys, item) => {
320
+ Object.keys(item).forEach((key) => keys.add(key));
321
+ return keys;
322
+ }, new Set()));
323
+ const fields = orderedFields.length > 0 ? orderedFields : fallbackFields;
324
+ const content = buildCsvContent(items, fields);
325
+ if (normalizedFormat === 'xlsx') {
326
+ return {
327
+ bytes: new TextEncoder().encode(content).buffer,
328
+ contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
329
+ filename: `${resourceLeaf(api)}.xlsx`,
330
+ };
331
+ }
332
+ return {
333
+ bytes: new TextEncoder().encode(content).buffer,
334
+ contentType: 'text/csv; charset=utf-8',
335
+ filename: `${resourceLeaf(api)}.csv`,
336
+ };
337
+ },
208
338
  async post(_path, _input) {
209
339
  throw new Error('Memory resource client does not support custom POST actions');
210
340
  },
@@ -251,9 +381,13 @@ export function useResourceClient() {
251
381
  ?? readGlobalResourceClient()
252
382
  ?? getDefaultResourceClient();
253
383
  }
254
- export function resetResourceClientTestState() {
384
+ export function clearResourceClientCaches() {
255
385
  memoryCollections.clear();
256
386
  fetchClientCache.clear();
257
387
  sameOriginFetchClient = null;
258
388
  defaultMemoryClient = null;
259
389
  }
390
+ export function resetResourceClientTestState() {
391
+ clearResourceClientCaches();
392
+ warnedTransportContractVersions.clear();
393
+ }
@@ -0,0 +1,13 @@
1
+ export declare function resolveDisplayableErrorMessage(error: unknown, fallbackMessage: string): string;
2
+ export declare function resolveWorkflowPreconditionError(error: unknown, fallbackMessage: string): string | null;
3
+ export declare function resolveWorkflowActionError(error: unknown, fallbackMessage: string): string | null;
4
+ export declare function resolveWorkflowFormError(error: unknown, options: {
5
+ preconditionMessage: string;
6
+ invalidTransitionMessage: string;
7
+ }): string | null;
8
+ export declare function resolveStructuredFormError(error: unknown, fieldNames: readonly string[]): {
9
+ fieldErrors: Record<string, string>;
10
+ formError: string | null;
11
+ } | null;
12
+ export declare function resolveVersionConflictFormError(error: unknown, fieldNames: readonly string[], fallbackMessage: string): string | null;
13
+ //# sourceMappingURL=resourceErrors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resourceErrors.d.ts","sourceRoot":"","sources":["../../src/hooks/resourceErrors.ts"],"names":[],"mappings":"AA+BA,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAc9F;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWvG;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBjG;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;IACP,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wBAAwB,EAAE,MAAM,CAAC;CAClC,GACA,MAAM,GAAG,IAAI,CAGf;AAED,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B;IAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CA4B1E;AAED,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,eAAe,EAAE,MAAM,GACtB,MAAM,GAAG,IAAI,CAqBf"}
@@ -0,0 +1,140 @@
1
+ import { ResourceClientError } from './resourceClient.js';
2
+ function firstStructuredErrorMessage(errors) {
3
+ for (const entry of errors) {
4
+ if (!entry || typeof entry !== 'object') {
5
+ continue;
6
+ }
7
+ const message = typeof entry.message === 'string'
8
+ ? entry.message.trim()
9
+ : '';
10
+ if (message !== '') {
11
+ return message;
12
+ }
13
+ }
14
+ return null;
15
+ }
16
+ function payloadMessage(payload) {
17
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
18
+ return null;
19
+ }
20
+ const record = payload;
21
+ for (const key of ['message', 'detail', 'error']) {
22
+ const value = record[key];
23
+ if (typeof value === 'string' && value.trim() !== '') {
24
+ return value.trim();
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+ export function resolveDisplayableErrorMessage(error, fallbackMessage) {
30
+ if (error instanceof ResourceClientError) {
31
+ if (Array.isArray(error.errors)) {
32
+ const structuredMessage = firstStructuredErrorMessage(error.errors);
33
+ if (structuredMessage) {
34
+ return structuredMessage;
35
+ }
36
+ }
37
+ const payloadText = payloadMessage(error.payload);
38
+ if (payloadText) {
39
+ return payloadText;
40
+ }
41
+ }
42
+ return fallbackMessage;
43
+ }
44
+ export function resolveWorkflowPreconditionError(error, fallbackMessage) {
45
+ if (!(error instanceof ResourceClientError) || error.status !== 422)
46
+ return null;
47
+ const payload = error.payload;
48
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
49
+ const record = payload;
50
+ if (record.preconditionError === false)
51
+ return null;
52
+ if (typeof record.message === 'string' && record.message.trim() !== '')
53
+ return record.message;
54
+ if (typeof record.detail === 'string' && record.detail.trim() !== '')
55
+ return record.detail;
56
+ if (typeof record.error === 'string' && record.error.trim() !== '')
57
+ return record.error;
58
+ }
59
+ return error.message.trim() !== '' ? error.message : fallbackMessage;
60
+ }
61
+ export function resolveWorkflowActionError(error, fallbackMessage) {
62
+ if (!(error instanceof ResourceClientError) || error.status !== 400)
63
+ return null;
64
+ if (Array.isArray(error.errors)) {
65
+ for (const entry of error.errors) {
66
+ const message = typeof entry.message === 'string' ? entry.message.trim() : '';
67
+ const fieldPath = typeof entry.field === 'string'
68
+ ? entry.field.trim()
69
+ : Array.isArray(entry.field)
70
+ ? entry.field
71
+ .filter((part) => typeof part === 'string' && part.trim() !== '')
72
+ .map((part) => part.trim())
73
+ .join('.')
74
+ : '';
75
+ if (message !== '' && (fieldPath === '' || fieldPath === '_form'))
76
+ return message;
77
+ }
78
+ }
79
+ return error.message.trim() !== '' ? error.message : fallbackMessage;
80
+ }
81
+ export function resolveWorkflowFormError(error, options) {
82
+ return resolveWorkflowPreconditionError(error, options.preconditionMessage)
83
+ ?? resolveWorkflowActionError(error, options.invalidTransitionMessage);
84
+ }
85
+ export function resolveStructuredFormError(error, fieldNames) {
86
+ if (!(error instanceof ResourceClientError) || !Array.isArray(error.errors) || error.errors.length === 0)
87
+ return null;
88
+ const availableFields = new Set(fieldNames);
89
+ const fieldErrors = {};
90
+ let formError = null;
91
+ for (const entry of error.errors) {
92
+ const message = typeof entry.message === 'string' ? entry.message.trim() : '';
93
+ if (message === '')
94
+ continue;
95
+ const fieldPath = typeof entry.field === 'string'
96
+ ? entry.field.trim()
97
+ : Array.isArray(entry.field)
98
+ ? entry.field
99
+ .filter((part) => typeof part === 'string' && part.trim() !== '')
100
+ .map((part) => part.trim())
101
+ .join('.')
102
+ : '';
103
+ if (fieldPath !== '' && availableFields.has(fieldPath) && !fieldErrors[fieldPath]) {
104
+ fieldErrors[fieldPath] = message;
105
+ continue;
106
+ }
107
+ if (!formError) {
108
+ formError = message;
109
+ }
110
+ }
111
+ if (Object.keys(fieldErrors).length === 0 && !formError) {
112
+ formError = error.message.trim() !== '' ? error.message : 'Please review the highlighted fields';
113
+ }
114
+ return { fieldErrors, formError };
115
+ }
116
+ export function resolveVersionConflictFormError(error, fieldNames, fallbackMessage) {
117
+ if (!(error instanceof ResourceClientError) || error.status !== 409 || !Array.isArray(error.errors) || fieldNames.length === 0)
118
+ return null;
119
+ const availableFields = new Set(fieldNames);
120
+ for (const entry of error.errors) {
121
+ const message = typeof entry.message === 'string' ? entry.message.trim() : '';
122
+ const code = typeof entry.code === 'string' ? entry.code.trim() : '';
123
+ const fieldPath = typeof entry.field === 'string'
124
+ ? entry.field.trim()
125
+ : Array.isArray(entry.field)
126
+ ? entry.field
127
+ .filter((part) => typeof part === 'string' && part.trim() !== '')
128
+ .map((part) => part.trim())
129
+ .join('.')
130
+ : '';
131
+ if (fieldPath === '' || !availableFields.has(fieldPath))
132
+ continue;
133
+ if (code === 'conflict' || code === 'missing') {
134
+ return fallbackMessage;
135
+ }
136
+ if (message !== '')
137
+ return message;
138
+ }
139
+ return null;
140
+ }
@@ -0,0 +1,17 @@
1
+ import type { DataTableAction } from '../components/DataTable.js';
2
+ export interface ResourceRowActionOptions {
3
+ resourceName: string;
4
+ includeView?: boolean;
5
+ includeEdit?: boolean;
6
+ includeWorkflow?: boolean;
7
+ includeReturnTo?: boolean;
8
+ enabled?: boolean;
9
+ deleteLabel?: string;
10
+ deleteVariant?: 'default' | 'danger';
11
+ deleteIntent?: 'default' | 'delete' | 'archive';
12
+ onDelete?: (id: string) => void | Promise<void>;
13
+ }
14
+ export declare function useResourceRowActions<T extends {
15
+ id: string;
16
+ }>({ resourceName, includeView, includeEdit, includeWorkflow, includeReturnTo, enabled, deleteLabel, deleteVariant, deleteIntent, onDelete, }: ResourceRowActionOptions): DataTableAction<T>[];
17
+ //# sourceMappingURL=resourceRowActions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resourceRowActions.d.ts","sourceRoot":"","sources":["../../src/hooks/resourceRowActions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGlE,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACrC,YAAY,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;IAChD,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAmBD,wBAAgB,qBAAqB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,EAC9D,YAAY,EACZ,WAAmB,EACnB,WAAmB,EACnB,eAAuB,EACvB,eAAuB,EACvB,OAAc,EACd,WAAW,EACX,aAAa,EACb,YAAY,EACZ,QAAQ,GACT,EAAE,wBAAwB,GAAG,eAAe,CAAC,CAAC,CAAC,EAAE,CAkCjD"}
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import { buildAppHrefWithSearchEntries, getCurrentAppHref, prefixAppBasePath } from './navigation.js';
3
+ function buildResourceRowHref(resourceName, rowId, surface, includeReturnTo) {
4
+ const basePath = surface === 'read'
5
+ ? `/${resourceName}/${rowId}`
6
+ : surface === 'edit'
7
+ ? `/${resourceName}/${rowId}/edit`
8
+ : `/${resourceName}/${rowId}/workflow`;
9
+ if (!includeReturnTo) {
10
+ return prefixAppBasePath(basePath);
11
+ }
12
+ return buildAppHrefWithSearchEntries(basePath, [['returnTo', getCurrentAppHref()]]);
13
+ }
14
+ export function useResourceRowActions({ resourceName, includeView = false, includeEdit = false, includeWorkflow = false, includeReturnTo = false, enabled = true, deleteLabel, deleteVariant, deleteIntent, onDelete, }) {
15
+ return React.useMemo(() => {
16
+ if (!enabled) {
17
+ return [];
18
+ }
19
+ const actions = [];
20
+ if (includeView) {
21
+ actions.push({
22
+ label: 'View',
23
+ href: (row) => buildResourceRowHref(resourceName, row.id, 'read', includeReturnTo),
24
+ });
25
+ }
26
+ if (includeEdit) {
27
+ actions.push({
28
+ label: 'Edit',
29
+ href: (row) => buildResourceRowHref(resourceName, row.id, 'edit', includeReturnTo),
30
+ });
31
+ }
32
+ if (includeWorkflow) {
33
+ actions.push({
34
+ label: 'Workflow',
35
+ href: (row) => buildResourceRowHref(resourceName, row.id, 'workflow', includeReturnTo),
36
+ });
37
+ }
38
+ if (deleteLabel && onDelete) {
39
+ actions.push({
40
+ label: deleteLabel,
41
+ onClick: (row) => onDelete(row.id),
42
+ variant: deleteVariant,
43
+ intent: deleteIntent,
44
+ });
45
+ }
46
+ return actions;
47
+ }, [deleteIntent, deleteLabel, deleteVariant, enabled, includeEdit, includeReturnTo, includeView, includeWorkflow, onDelete, resourceName]);
48
+ }
@@ -1,25 +1,3 @@
1
- import type { ResourceClient } from './resourceClient.js';
2
- export interface ResourceStoreSnapshot<T extends {
3
- id: string;
4
- }> {
5
- items: T[];
6
- error: unknown;
7
- pendingCount: number;
8
- resolvedOnce: boolean;
9
- }
10
- export interface ResourceStore<T extends {
11
- id: string;
12
- }> {
13
- subscribe(listener: () => void): () => void;
14
- getSnapshot(): ResourceStoreSnapshot<T>;
15
- getById(id: string): T | undefined;
16
- load(force?: boolean): Promise<T[]>;
17
- createItem(input: Partial<T>): Promise<T>;
18
- updateItem(id: string, input: Partial<T>): Promise<T>;
19
- deleteItem(id: string): Promise<void>;
20
- }
21
- export declare function getResourceStore<T extends {
22
- id: string;
23
- }>(client: ResourceClient, api: string): ResourceStore<T>;
24
- export declare function resetResourceStoreTestState(): void;
1
+ export type { ResourceStore, ResourceStoreClient, ResourceStoreSnapshot, } from '../../../runtime-core/dist/resourceStore.js';
2
+ export { clearResourceStores, getResourceStore, resetResourceStoreTestState, } from '../../../runtime-core/dist/resourceStore.js';
25
3
  //# sourceMappingURL=resourceStore.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resourceStore.d.ts","sourceRoot":"","sources":["../../src/hooks/resourceStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,MAAM,WAAW,qBAAqB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IAC7D,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IACrD,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,WAAW,IAAI,qBAAqB,CAAC,CAAC,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,IAAI,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACpC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACtD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAkLD,wBAAgB,gBAAgB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EACvD,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,MAAM,GACV,aAAa,CAAC,CAAC,CAAC,CAOlB;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAGlD"}
1
+ {"version":3,"file":"resourceStore.d.ts","sourceRoot":"","sources":["../../src/hooks/resourceStore.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,6CAA6C,CAAC;AACrD,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,2BAA2B,GAC5B,MAAM,6CAA6C,CAAC"}