openxiangda-cli 2.0.0-alpha.67 → 2.0.0-alpha.69
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/package.json +4 -4
- package/template/AGENTS.md +12 -7
- package/template/README.md +7 -7
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/resources.spec.ts +11 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/scripts/check.mjs +18 -13
- package/template/apps/web/src/FilePreviewPage.tsx +3 -3
- package/template/apps/web/src/Shell.tsx +323 -33
- package/template/apps/web/src/appearance.tsx +147 -0
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +645 -109
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +319 -0
- package/template/apps/web/src/components/resource/StandardResourcePages.tsx +13 -52
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +1 -1
- package/template/apps/web/src/components/resource/resource-import.ts +205 -0
- package/template/apps/web/src/main.tsx +22 -21
- package/template/apps/web/src/platform-client.ts +195 -40
- package/template/apps/web/src/styles.css +455 -220
- package/template/apps/web/test/contracts.test.ts +52 -5
- package/template/apps/web/test/resource-import.test.ts +142 -0
- package/template/openxiangda.config.ts +2 -1
- package/template/package.json +4 -3
- package/template/scripts/verify-template-budget.mjs +5 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Refine } from '@refinedev/core';
|
|
2
|
-
import { App as AntdApp, ConfigProvider, Spin } from 'antd';
|
|
3
|
-
import zhCN from 'antd/locale/zh_CN';
|
|
4
2
|
import React from 'react';
|
|
5
3
|
import ReactDOM from 'react-dom/client';
|
|
6
4
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
|
7
5
|
import { resourceDefinitions } from '../../../packages/contracts/src/generated.js';
|
|
8
6
|
import { applicationProvider } from './data-provider';
|
|
7
|
+
import { AppearanceProvider } from './appearance';
|
|
9
8
|
import { GeneratedResourcePage } from './components/resource/GeneratedResourceCrud';
|
|
10
9
|
import { FilePreviewPage } from './FilePreviewPage';
|
|
11
10
|
import { EmptyApplicationPage } from './Shell';
|
|
@@ -20,9 +19,13 @@ const firstPath = codes[0] ? `/${codes[0]}` : '/';
|
|
|
20
19
|
function GlobalRequestLoading() {
|
|
21
20
|
const loading = useGlobalRequestLoading();
|
|
22
21
|
return loading ? (
|
|
23
|
-
<div
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
<div
|
|
23
|
+
aria-label="页面请求进行中"
|
|
24
|
+
aria-valuetext="正在加载"
|
|
25
|
+
className="oxa-global-request-loading"
|
|
26
|
+
role="progressbar"
|
|
27
|
+
>
|
|
28
|
+
<span />
|
|
26
29
|
</div>
|
|
27
30
|
) : null;
|
|
28
31
|
}
|
|
@@ -40,21 +43,19 @@ const resourceRoutes = codes.flatMap(resourceCode => [
|
|
|
40
43
|
|
|
41
44
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
42
45
|
<React.StrictMode>
|
|
43
|
-
<
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
</AntdApp>
|
|
58
|
-
</ConfigProvider>
|
|
46
|
+
<AppearanceProvider>
|
|
47
|
+
<GlobalRequestLoading />
|
|
48
|
+
<RuntimeBoundary>
|
|
49
|
+
<BrowserRouter basename={applicationBasename()}>
|
|
50
|
+
<Refine dataProvider={applicationProvider} resources={codes.map(code => ({ name: code, list: `/${code}`, create: `/${code}/new`, edit: `/${code}/:id/edit`, show: `/${code}/:id` }))}>
|
|
51
|
+
<Routes>
|
|
52
|
+
<Route path="/" element={codes.length ? <Navigate replace to={firstPath} /> : <EmptyApplicationPage />} />
|
|
53
|
+
<Route path="/files/:resourceCode/:fileId/preview" element={<FilePreviewPage />} />
|
|
54
|
+
{resourceRoutes}
|
|
55
|
+
</Routes>
|
|
56
|
+
</Refine>
|
|
57
|
+
</BrowserRouter>
|
|
58
|
+
</RuntimeBoundary>
|
|
59
|
+
</AppearanceProvider>
|
|
59
60
|
</React.StrictMode>,
|
|
60
61
|
);
|
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
type DataQuery,
|
|
9
9
|
type DataRecord,
|
|
10
10
|
type DataResourceSurface,
|
|
11
|
+
type DataTransactionOperation,
|
|
12
|
+
type DataTransactionRequest,
|
|
13
|
+
type DataTransactionResult,
|
|
11
14
|
type DirectoryEntryPage,
|
|
12
15
|
type DirectoryResolveRequest,
|
|
13
16
|
} from 'openxiangda-contracts/browser';
|
|
@@ -15,7 +18,7 @@ import { applicationCode, runtimeMount } from './runtime-meta';
|
|
|
15
18
|
import { useSyncExternalStore } from 'react';
|
|
16
19
|
|
|
17
20
|
interface PlatformEnvelope<T> {
|
|
18
|
-
code: number;
|
|
21
|
+
code: number | string;
|
|
19
22
|
message?: string;
|
|
20
23
|
errorCode?: string;
|
|
21
24
|
requestId?: string;
|
|
@@ -27,22 +30,22 @@ const globalRequestListeners = new Set<() => void>();
|
|
|
27
30
|
|
|
28
31
|
function beginGlobalRequest() {
|
|
29
32
|
globalRequestCount += 1;
|
|
30
|
-
globalRequestListeners.forEach(listener => listener());
|
|
33
|
+
globalRequestListeners.forEach((listener) => listener());
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
function endGlobalRequest() {
|
|
34
37
|
globalRequestCount = Math.max(0, globalRequestCount - 1);
|
|
35
|
-
globalRequestListeners.forEach(listener => listener());
|
|
38
|
+
globalRequestListeners.forEach((listener) => listener());
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export function useGlobalRequestLoading() {
|
|
39
42
|
return useSyncExternalStore(
|
|
40
|
-
listener => {
|
|
43
|
+
(listener) => {
|
|
41
44
|
globalRequestListeners.add(listener);
|
|
42
45
|
return () => globalRequestListeners.delete(listener);
|
|
43
46
|
},
|
|
44
47
|
() => globalRequestCount > 0,
|
|
45
|
-
() => false
|
|
48
|
+
() => false
|
|
46
49
|
);
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -59,12 +62,19 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
|
59
62
|
},
|
|
60
63
|
});
|
|
61
64
|
const payload = (await response.json().catch(() => null)) as PlatformEnvelope<T> | T | null;
|
|
62
|
-
const envelope = payload && typeof payload === 'object' && 'code' in payload ? payload as PlatformEnvelope<T> : null;
|
|
65
|
+
const envelope = payload && typeof payload === 'object' && 'code' in payload ? (payload as PlatformEnvelope<T>) : null;
|
|
63
66
|
if (!response.ok || (envelope && envelope.code !== 200)) {
|
|
64
67
|
const requestId = envelope?.requestId ? ` (requestId: ${envelope.requestId})` : '';
|
|
65
|
-
throw
|
|
68
|
+
throw Object.assign(
|
|
69
|
+
new Error(`${envelope?.errorCode || `HTTP_${response.status}`}: ${envelope?.message || '平台请求失败'}${requestId}`),
|
|
70
|
+
{
|
|
71
|
+
code: envelope?.errorCode || `HTTP_${response.status}`,
|
|
72
|
+
status: response.status,
|
|
73
|
+
data: envelope?.data ?? null,
|
|
74
|
+
}
|
|
75
|
+
);
|
|
66
76
|
}
|
|
67
|
-
return envelope ? envelope.data : payload as T;
|
|
77
|
+
return envelope ? envelope.data : (payload as T);
|
|
68
78
|
} finally {
|
|
69
79
|
endGlobalRequest();
|
|
70
80
|
}
|
|
@@ -103,8 +113,15 @@ let activeIdentity: RuntimeIdentity | undefined;
|
|
|
103
113
|
export async function loadCurrentIdentity(): Promise<RuntimeIdentity> {
|
|
104
114
|
const mount = runtimeMount();
|
|
105
115
|
if (mount) {
|
|
106
|
-
const current = await request<DeployedCurrent>(
|
|
107
|
-
|
|
116
|
+
const current = await request<DeployedCurrent>(
|
|
117
|
+
`${nativeBase()}/current-user?environmentKey=${encodeURIComponent(mount.environmentKey)}`
|
|
118
|
+
);
|
|
119
|
+
if (
|
|
120
|
+
current.schemaVersion !== 'openxiangda.current-user/v2' ||
|
|
121
|
+
current.appCode !== mount.appCode ||
|
|
122
|
+
current.environment.key !== mount.environmentKey ||
|
|
123
|
+
current.principal.type !== 'user'
|
|
124
|
+
) {
|
|
108
125
|
throw new Error('OPENXIANGDA_DEPLOYED_CURRENT_USER_INVALID');
|
|
109
126
|
}
|
|
110
127
|
activeIdentity = { ...current.principal, environment: current.environment };
|
|
@@ -115,6 +132,13 @@ export async function loadCurrentIdentity(): Promise<RuntimeIdentity> {
|
|
|
115
132
|
return activeIdentity;
|
|
116
133
|
}
|
|
117
134
|
|
|
135
|
+
export async function logoutCurrentUser() {
|
|
136
|
+
await request<null>('/api/auth/logout', {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
});
|
|
139
|
+
activeIdentity = undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
118
142
|
function currentEnvironmentKey() {
|
|
119
143
|
const key = activeIdentity?.environment.key || runtimeMount()?.environmentKey;
|
|
120
144
|
if (!key) throw new Error('OPENXIANGDA_CURRENT_ENVIRONMENT_REQUIRED');
|
|
@@ -124,29 +148,49 @@ function currentEnvironmentKey() {
|
|
|
124
148
|
export type DirectoryKind = 'user' | 'department';
|
|
125
149
|
|
|
126
150
|
export async function searchDirectory(kind: DirectoryKind, options: { keyword: string; cursor?: string }) {
|
|
127
|
-
const params = new URLSearchParams({
|
|
151
|
+
const params = new URLSearchParams({
|
|
152
|
+
environmentKey: currentEnvironmentKey(),
|
|
153
|
+
keyword: options.keyword.trim(),
|
|
154
|
+
limit: '20',
|
|
155
|
+
});
|
|
128
156
|
if (options.cursor) params.set('cursor', options.cursor);
|
|
129
157
|
return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/${kind}s?${params}`);
|
|
130
158
|
}
|
|
131
159
|
|
|
132
160
|
export async function browseDepartmentTree(options: { parentId?: string; cursor?: string }) {
|
|
133
|
-
const params = new URLSearchParams({
|
|
161
|
+
const params = new URLSearchParams({
|
|
162
|
+
environmentKey: currentEnvironmentKey(),
|
|
163
|
+
limit: '100',
|
|
164
|
+
});
|
|
134
165
|
if (options.parentId) params.set('parentId', options.parentId);
|
|
135
166
|
if (options.cursor) params.set('offset', options.cursor);
|
|
136
167
|
return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/departments/tree?${params}`);
|
|
137
168
|
}
|
|
138
169
|
|
|
139
170
|
export async function browseDepartmentUsers(departmentId: string, page = 1) {
|
|
140
|
-
const params = new URLSearchParams({
|
|
141
|
-
|
|
171
|
+
const params = new URLSearchParams({
|
|
172
|
+
environmentKey: currentEnvironmentKey(),
|
|
173
|
+
page: String(page),
|
|
174
|
+
limit: '20',
|
|
175
|
+
});
|
|
176
|
+
return request<DirectoryEntryPage>(
|
|
177
|
+
`${nativeBase().replace(/\/native$/, '')}/directory/departments/${encodeURIComponent(departmentId)}/users?${params}`
|
|
178
|
+
);
|
|
142
179
|
}
|
|
143
180
|
|
|
144
181
|
export async function resolveDirectory(kind: DirectoryKind, ids: string[]) {
|
|
145
182
|
const uniqueIds = [...new Set(ids)];
|
|
146
183
|
if (uniqueIds.length === 0) throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED');
|
|
147
184
|
if (uniqueIds.length > 50) throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED');
|
|
148
|
-
const body: DirectoryResolveRequest = {
|
|
149
|
-
|
|
185
|
+
const body: DirectoryResolveRequest = {
|
|
186
|
+
schemaVersion: SCHEMA_VERSIONS.directoryResolveRequest,
|
|
187
|
+
kind,
|
|
188
|
+
ids: uniqueIds,
|
|
189
|
+
};
|
|
190
|
+
return request<DirectoryEntryPage>(
|
|
191
|
+
`${nativeBase().replace(/\/native$/, '')}/directory/resolve?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`,
|
|
192
|
+
{ method: 'POST', body: JSON.stringify(body) }
|
|
193
|
+
);
|
|
150
194
|
}
|
|
151
195
|
|
|
152
196
|
export async function searchResource(resourceCode: string, labelField: string, options: { keyword: string; cursor?: string }) {
|
|
@@ -154,14 +198,32 @@ export async function searchResource(resourceCode: string, labelField: string, o
|
|
|
154
198
|
const offset = options.cursor ? Number(options.cursor) : 0;
|
|
155
199
|
const query: DataQuery = {
|
|
156
200
|
schemaVersion: SCHEMA_VERSIONS.dataQuery,
|
|
157
|
-
filters: options.keyword.trim()
|
|
201
|
+
filters: options.keyword.trim()
|
|
202
|
+
? [
|
|
203
|
+
{
|
|
204
|
+
field: labelField,
|
|
205
|
+
operator: 'ilike',
|
|
206
|
+
value: `%${options.keyword.trim()}%`,
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
: [],
|
|
158
210
|
order: [{ field: labelField, direction: 'asc' }],
|
|
159
211
|
limit: pageSize,
|
|
160
212
|
offset: Number.isSafeInteger(offset) && offset >= 0 ? offset : 0,
|
|
161
213
|
};
|
|
162
|
-
const page = await request<DataPage<Record<string, unknown>>>(`${dataBase(resourceCode)}/query`, {
|
|
214
|
+
const page = await request<DataPage<Record<string, unknown>>>(`${dataBase(resourceCode)}/query`, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
...query,
|
|
218
|
+
environmentKey: currentEnvironmentKey(),
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
163
221
|
return {
|
|
164
|
-
items: page.items.map(item => ({
|
|
222
|
+
items: page.items.map((item) => ({
|
|
223
|
+
id: String(item.id),
|
|
224
|
+
label: String(item[labelField] ?? item.id),
|
|
225
|
+
selectable: true,
|
|
226
|
+
})),
|
|
165
227
|
nextCursor: page.items.length >= pageSize ? String(page.offset + page.items.length) : null,
|
|
166
228
|
};
|
|
167
229
|
}
|
|
@@ -169,10 +231,18 @@ export async function searchResource(resourceCode: string, labelField: string, o
|
|
|
169
231
|
export async function resolveResource(resourceCode: string, ids: string[], labelField: string) {
|
|
170
232
|
if (ids.length === 0) throw new Error('OPENXIANGDA_RESOURCE_RESOLVE_IDS_REQUIRED');
|
|
171
233
|
if (ids.length > 50) throw new Error('OPENXIANGDA_RESOURCE_RESOLVE_IDS_EXCEEDED');
|
|
172
|
-
const items = await Promise.all(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
234
|
+
const items = await Promise.all(
|
|
235
|
+
ids.map(async (id) => {
|
|
236
|
+
const record = await request<DataRecord<Record<string, unknown>>>(
|
|
237
|
+
`${dataBase(resourceCode)}/records/${encodeURIComponent(id)}?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
|
|
238
|
+
);
|
|
239
|
+
return {
|
|
240
|
+
id,
|
|
241
|
+
label: String(record.data[labelField] ?? id),
|
|
242
|
+
selectable: true,
|
|
243
|
+
};
|
|
244
|
+
})
|
|
245
|
+
);
|
|
176
246
|
return { items, nextCursor: null };
|
|
177
247
|
}
|
|
178
248
|
|
|
@@ -197,7 +267,12 @@ export function createNativeResourceClient(code: string, surface: DataResourceSu
|
|
|
197
267
|
if (query.keyword?.trim()) {
|
|
198
268
|
const searchable = surface.list?.searchableFields || [];
|
|
199
269
|
const field = searchable.includes(query.searchField || '') ? query.searchField! : searchable[0];
|
|
200
|
-
if (field)
|
|
270
|
+
if (field)
|
|
271
|
+
filters.push({
|
|
272
|
+
field: assertField(field),
|
|
273
|
+
operator: 'ilike',
|
|
274
|
+
value: `%${query.keyword.trim()}%`,
|
|
275
|
+
});
|
|
201
276
|
}
|
|
202
277
|
for (const [fieldCode, value] of Object.entries(query.filters || {})) {
|
|
203
278
|
if (value === undefined || value === null || value === '') continue;
|
|
@@ -207,8 +282,10 @@ export function createNativeResourceClient(code: string, surface: DataResourceSu
|
|
|
207
282
|
if (!declaredFields.has(fieldCode)) continue;
|
|
208
283
|
const field = surface.fields[fieldCode];
|
|
209
284
|
if (Array.isArray(value) && value.length === 2 && ['date', 'datetime', 'number', 'money', 'percent'].includes(field?.widget || '')) {
|
|
210
|
-
if (value[0] !== undefined && value[0] !== null && value[0] !== '')
|
|
211
|
-
|
|
285
|
+
if (value[0] !== undefined && value[0] !== null && value[0] !== '')
|
|
286
|
+
filters.push({ field: fieldCode, operator: 'gte', value: value[0] });
|
|
287
|
+
if (value[1] !== undefined && value[1] !== null && value[1] !== '')
|
|
288
|
+
filters.push({ field: fieldCode, operator: 'lte', value: value[1] });
|
|
212
289
|
} else if (Array.isArray(value) && ['multi', 'directory-user', 'directory-department', 'resource'].includes(field?.widget || '')) {
|
|
213
290
|
filters.push({ field: fieldCode, operator: 'cs', value });
|
|
214
291
|
} else {
|
|
@@ -219,42 +296,112 @@ export function createNativeResourceClient(code: string, surface: DataResourceSu
|
|
|
219
296
|
};
|
|
220
297
|
return {
|
|
221
298
|
async list(query: GenericResourceQuery) {
|
|
222
|
-
const fallbackSort = surface.list?.defaultSort || {
|
|
299
|
+
const fallbackSort = surface.list?.defaultSort || {
|
|
300
|
+
field: Object.keys(surface.fields)[0] || 'id',
|
|
301
|
+
order: 'asc' as const,
|
|
302
|
+
};
|
|
223
303
|
const requestedSort = query.sort || fallbackSort;
|
|
224
304
|
const sort = declaredFields.has(requestedSort.field) ? requestedSort : fallbackSort;
|
|
225
|
-
const body: DataQuery = {
|
|
226
|
-
|
|
305
|
+
const body: DataQuery = {
|
|
306
|
+
schemaVersion: SCHEMA_VERSIONS.dataQuery,
|
|
307
|
+
filters: filtersFor(query),
|
|
308
|
+
order: [{ field: sort.field, direction: sort.order || 'asc' }],
|
|
309
|
+
limit: query.pageSize,
|
|
310
|
+
offset: (query.page - 1) * query.pageSize,
|
|
311
|
+
};
|
|
312
|
+
const page = await request<DataPage<Record<string, unknown>>>(`${base}/query`, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
...body,
|
|
316
|
+
environmentKey: currentEnvironmentKey(),
|
|
317
|
+
}),
|
|
318
|
+
});
|
|
227
319
|
return { rows: page.items, total: page.total };
|
|
228
320
|
},
|
|
229
321
|
async get(id: string) {
|
|
230
|
-
const record = await request<DataRecord<Record<string, unknown>>>(
|
|
322
|
+
const record = await request<DataRecord<Record<string, unknown>>>(
|
|
323
|
+
`${base}/records/${encodeURIComponent(id)}?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
|
|
324
|
+
);
|
|
231
325
|
return record.data;
|
|
232
326
|
},
|
|
233
327
|
async audit(id: string) {
|
|
234
|
-
return request<DataAuditPage>(
|
|
328
|
+
return request<DataAuditPage>(
|
|
329
|
+
`${base}/records/${encodeURIComponent(id)}/audit?limit=10&offset=0&environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
|
|
330
|
+
);
|
|
235
331
|
},
|
|
236
332
|
async create(data: Record<string, unknown>) {
|
|
237
|
-
const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records`, {
|
|
333
|
+
const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records`, {
|
|
334
|
+
method: 'POST',
|
|
335
|
+
body: JSON.stringify({
|
|
336
|
+
environmentKey: currentEnvironmentKey(),
|
|
337
|
+
data,
|
|
338
|
+
}),
|
|
339
|
+
});
|
|
238
340
|
return record.data;
|
|
239
341
|
},
|
|
240
342
|
async update(id: string, expectedRevision: number, data: Record<string, unknown>) {
|
|
241
|
-
const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records/${encodeURIComponent(id)}/update`, {
|
|
343
|
+
const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records/${encodeURIComponent(id)}/update`, {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
body: JSON.stringify({
|
|
346
|
+
environmentKey: currentEnvironmentKey(),
|
|
347
|
+
expectedRevision,
|
|
348
|
+
data,
|
|
349
|
+
}),
|
|
350
|
+
});
|
|
242
351
|
return record.data;
|
|
243
352
|
},
|
|
244
353
|
async remove(id: string, expectedRevision: number) {
|
|
245
|
-
const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records/${encodeURIComponent(id)}/delete`, {
|
|
354
|
+
const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records/${encodeURIComponent(id)}/delete`, {
|
|
355
|
+
method: 'POST',
|
|
356
|
+
body: JSON.stringify({
|
|
357
|
+
environmentKey: currentEnvironmentKey(),
|
|
358
|
+
expectedRevision,
|
|
359
|
+
}),
|
|
360
|
+
});
|
|
246
361
|
return record.data;
|
|
247
362
|
},
|
|
248
363
|
async upload(fieldCode: string, file: File, recordId?: string) {
|
|
249
364
|
assertField(fieldCode);
|
|
250
|
-
const plan = await request<DataFileUploadPlan>(`${base}/files/uploads/initiate`, {
|
|
251
|
-
|
|
365
|
+
const plan = await request<DataFileUploadPlan>(`${base}/files/uploads/initiate`, {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
body: JSON.stringify({
|
|
368
|
+
environmentKey: currentEnvironmentKey(),
|
|
369
|
+
fieldCode,
|
|
370
|
+
fileName: file.name,
|
|
371
|
+
fileSize: file.size,
|
|
372
|
+
contentType: file.type || 'application/octet-stream',
|
|
373
|
+
...(recordId ? { recordId } : {}),
|
|
374
|
+
}),
|
|
375
|
+
});
|
|
376
|
+
const uploaded = await fetch(plan.uploadUrl, {
|
|
377
|
+
method: plan.uploadMethod,
|
|
378
|
+
headers: plan.headers,
|
|
379
|
+
body: file,
|
|
380
|
+
});
|
|
252
381
|
if (!uploaded.ok) throw new Error(`FILE_UPLOAD_FAILED: HTTP_${uploaded.status}`);
|
|
253
|
-
return request<DataFileRef>(`${base}/files/${encodeURIComponent(plan.file.id)}/complete`, {
|
|
382
|
+
return request<DataFileRef>(`${base}/files/${encodeURIComponent(plan.file.id)}/complete`, {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
body: JSON.stringify({ environmentKey: currentEnvironmentKey() }),
|
|
385
|
+
});
|
|
254
386
|
},
|
|
255
387
|
};
|
|
256
388
|
}
|
|
257
389
|
|
|
390
|
+
export async function transactNativeData(operations: DataTransactionOperation[], idempotencyKey: string = crypto.randomUUID()) {
|
|
391
|
+
const transaction: DataTransactionRequest = {
|
|
392
|
+
schemaVersion: SCHEMA_VERSIONS.dataTransactionRequest,
|
|
393
|
+
idempotencyKey,
|
|
394
|
+
operations,
|
|
395
|
+
};
|
|
396
|
+
return request<DataTransactionResult>(`${nativeBase()}/data/transactions`, {
|
|
397
|
+
method: 'POST',
|
|
398
|
+
body: JSON.stringify({
|
|
399
|
+
...transaction,
|
|
400
|
+
environmentKey: currentEnvironmentKey(),
|
|
401
|
+
}),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
258
405
|
function nativeBase() {
|
|
259
406
|
return `/service/openxiangda-api/v2/applications/${applicationCode()}/native`;
|
|
260
407
|
}
|
|
@@ -264,16 +411,24 @@ function dataBase(code: string) {
|
|
|
264
411
|
}
|
|
265
412
|
|
|
266
413
|
export function dataFileContentUrl(resource: string, fileId: string, disposition: 'attachment' | 'inline' = 'attachment') {
|
|
267
|
-
const query = new URLSearchParams({
|
|
414
|
+
const query = new URLSearchParams({
|
|
415
|
+
environmentKey: currentEnvironmentKey(),
|
|
416
|
+
disposition,
|
|
417
|
+
});
|
|
268
418
|
return `${dataBase(resource)}/files/${encodeURIComponent(fileId)}/content?${query}`;
|
|
269
419
|
}
|
|
270
420
|
|
|
271
421
|
export async function loadDataFilePreview(resource: string, fileId: string) {
|
|
272
|
-
return request<DataFilePreview>(
|
|
422
|
+
return request<DataFilePreview>(
|
|
423
|
+
`${dataBase(resource)}/files/${encodeURIComponent(fileId)}/preview?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
|
|
424
|
+
);
|
|
273
425
|
}
|
|
274
426
|
|
|
275
427
|
export async function fetchDataFileBlob(resource: string, fileId: string) {
|
|
276
|
-
const response = await fetch(dataFileContentUrl(resource, fileId, 'inline'), {
|
|
428
|
+
const response = await fetch(dataFileContentUrl(resource, fileId, 'inline'), {
|
|
429
|
+
credentials: 'include',
|
|
430
|
+
headers: { accept: 'application/octet-stream,*/*' },
|
|
431
|
+
});
|
|
277
432
|
if (!response.ok) throw new Error(`FILE_CONTENT_READ_FAILED: HTTP_${response.status}`);
|
|
278
433
|
return response.blob();
|
|
279
434
|
}
|