@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.
- package/dist/components/Badge.d.ts +2 -1
- package/dist/components/Badge.d.ts.map +1 -1
- package/dist/components/Badge.js +2 -2
- package/dist/components/ConfirmDialog.d.ts +6 -1
- package/dist/components/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/ConfirmDialog.js +35 -6
- package/dist/components/DataTable.d.ts +35 -2
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/DataTable.js +64 -38
- package/dist/components/DropdownButton.d.ts +11 -0
- package/dist/components/DropdownButton.d.ts.map +1 -0
- package/dist/components/DropdownButton.js +9 -0
- package/dist/components/ErrorBoundary.d.ts +18 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.js +19 -0
- package/dist/components/ErrorState.d.ts +6 -0
- package/dist/components/ErrorState.d.ts.map +1 -0
- package/dist/components/ErrorState.js +25 -0
- package/dist/components/FilterBar.d.ts +2 -0
- package/dist/components/FilterBar.d.ts.map +1 -1
- package/dist/components/FilterBar.js +3 -2
- package/dist/components/FormField.d.ts +7 -2
- package/dist/components/FormField.d.ts.map +1 -1
- package/dist/components/FormField.js +16 -5
- package/dist/components/GroupedDataTable.d.ts.map +1 -1
- package/dist/components/GroupedDataTable.js +40 -59
- package/dist/components/Pagination.js +1 -1
- package/dist/components/PivotDataTable.d.ts.map +1 -1
- package/dist/components/PivotDataTable.js +42 -65
- package/dist/components/Tag.d.ts +2 -1
- package/dist/components/Tag.d.ts.map +1 -1
- package/dist/components/Tag.js +2 -2
- package/dist/components/WorkflowSummary.js +6 -6
- package/dist/components/readFormatting.d.ts +2 -0
- package/dist/components/readFormatting.d.ts.map +1 -0
- package/dist/components/readFormatting.js +1 -0
- package/dist/derivations.d.ts +3 -0
- package/dist/derivations.d.ts.map +1 -0
- package/dist/derivations.js +1 -0
- package/dist/hooks/browserStorage.d.ts +3 -0
- package/dist/hooks/browserStorage.d.ts.map +1 -0
- package/dist/hooks/browserStorage.js +1 -0
- package/dist/hooks/deleteConfirmation.d.ts +20 -0
- package/dist/hooks/deleteConfirmation.d.ts.map +1 -0
- package/dist/hooks/deleteConfirmation.js +21 -0
- package/dist/hooks/exportDownload.d.ts +16 -0
- package/dist/hooks/exportDownload.d.ts.map +1 -0
- package/dist/hooks/exportDownload.js +55 -0
- package/dist/hooks/formDerivations.d.ts +7 -0
- package/dist/hooks/formDerivations.d.ts.map +1 -0
- package/dist/hooks/formDerivations.js +23 -0
- package/dist/hooks/formSeeds.d.ts +12 -0
- package/dist/hooks/formSeeds.d.ts.map +1 -0
- package/dist/hooks/formSeeds.js +26 -0
- package/dist/hooks/navigation.d.ts +37 -0
- package/dist/hooks/navigation.d.ts.map +1 -1
- package/dist/hooks/navigation.js +107 -70
- package/dist/hooks/readModelStore.d.ts +29 -0
- package/dist/hooks/readModelStore.d.ts.map +1 -0
- package/dist/hooks/readModelStore.js +135 -0
- package/dist/hooks/recordScopedWorkflow.d.ts +13 -0
- package/dist/hooks/recordScopedWorkflow.d.ts.map +1 -0
- package/dist/hooks/recordScopedWorkflow.js +40 -0
- package/dist/hooks/resourceClient.d.ts +39 -0
- package/dist/hooks/resourceClient.d.ts.map +1 -1
- package/dist/hooks/resourceClient.js +205 -71
- package/dist/hooks/resourceErrors.d.ts +13 -0
- package/dist/hooks/resourceErrors.d.ts.map +1 -0
- package/dist/hooks/resourceErrors.js +140 -0
- package/dist/hooks/resourceRowActions.d.ts +17 -0
- package/dist/hooks/resourceRowActions.d.ts.map +1 -0
- package/dist/hooks/resourceRowActions.js +48 -0
- package/dist/hooks/resourceStore.d.ts +2 -24
- package/dist/hooks/resourceStore.d.ts.map +1 -1
- package/dist/hooks/resourceStore.js +1 -164
- package/dist/hooks/resourceTarget.d.ts +1 -1
- package/dist/hooks/resourceTarget.d.ts.map +1 -1
- package/dist/hooks/resourceTarget.js +1 -22
- package/dist/hooks/serverListStore.d.ts +3 -0
- package/dist/hooks/serverListStore.d.ts.map +1 -0
- package/dist/hooks/serverListStore.js +1 -0
- package/dist/hooks/sessionCaches.d.ts +6 -0
- package/dist/hooks/sessionCaches.d.ts.map +1 -0
- package/dist/hooks/sessionCaches.js +14 -0
- package/dist/hooks/transitionedSetter.d.ts +2 -0
- package/dist/hooks/transitionedSetter.d.ts.map +1 -0
- package/dist/hooks/transitionedSetter.js +9 -0
- package/dist/hooks/useAuth.d.ts +18 -0
- package/dist/hooks/useAuth.d.ts.map +1 -1
- package/dist/hooks/useAuth.js +30 -0
- package/dist/hooks/useCollectionView.d.ts +2 -8
- package/dist/hooks/useCollectionView.d.ts.map +1 -1
- package/dist/hooks/useCollectionView.js +8 -36
- package/dist/hooks/useDocumentMetadata.d.ts.map +1 -1
- package/dist/hooks/useDocumentMetadata.js +57 -0
- package/dist/hooks/useGroupedCollectionView.d.ts +2 -7
- package/dist/hooks/useGroupedCollectionView.d.ts.map +1 -1
- package/dist/hooks/useGroupedCollectionView.js +8 -52
- package/dist/hooks/useI18n.d.ts +21 -0
- package/dist/hooks/useI18n.d.ts.map +1 -0
- package/dist/hooks/useI18n.js +86 -0
- package/dist/hooks/useReadModel.d.ts +15 -0
- package/dist/hooks/useReadModel.d.ts.map +1 -1
- package/dist/hooks/useReadModel.js +127 -40
- package/dist/hooks/useResource.d.ts +7 -0
- package/dist/hooks/useResource.d.ts.map +1 -1
- package/dist/hooks/useResource.js +159 -3
- package/dist/hooks/useStoredState.d.ts +15 -0
- package/dist/hooks/useStoredState.d.ts.map +1 -0
- package/dist/hooks/useStoredState.js +28 -0
- package/dist/hooks/useToast.d.ts.map +1 -1
- package/dist/hooks/useToast.js +1 -1
- package/dist/hooks/workflowTransition.d.ts +21 -0
- package/dist/hooks/workflowTransition.d.ts.map +1 -0
- package/dist/hooks/workflowTransition.js +54 -0
- package/dist/index.d.ts +35 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -5
- package/dist/policies/can.d.ts +2 -14
- package/dist/policies/can.d.ts.map +1 -1
- package/dist/policies/can.js +1 -160
- package/dist/workflow.d.ts +3 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +1 -0
- 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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
78
|
-
if (
|
|
79
|
-
return
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|
107
|
-
const
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
headers: requestHeaders,
|
|
142
|
+
path: api,
|
|
112
143
|
});
|
|
113
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2
|
-
export
|
|
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,
|
|
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"}
|