openxiangda-cli 2.0.0-alpha.61 → 2.0.0-alpha.62

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 (38) hide show
  1. package/dist/create-workspace.d.ts.map +1 -1
  2. package/dist/create-workspace.js +1 -0
  3. package/dist/create-workspace.js.map +1 -1
  4. package/package.json +4 -4
  5. package/template/AGENTS.md +1 -1
  6. package/template/README.md +7 -7
  7. package/template/apps/server/package.json +1 -1
  8. package/template/apps/server/src/app.module.ts +1 -3
  9. package/template/apps/server/test/smoke.test.ts +6 -23
  10. package/template/apps/web/e2e/resources.spec.ts +20 -0
  11. package/template/apps/web/index.html +1 -1
  12. package/template/apps/web/package.json +1 -1
  13. package/template/apps/web/src/AuthoritativeSelector.tsx +38 -29
  14. package/template/apps/web/src/Shell.tsx +39 -211
  15. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
  16. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +26 -5
  17. package/template/apps/web/src/components/resource/SurfaceFields.tsx +32 -1
  18. package/template/apps/web/src/data-provider.ts +23 -102
  19. package/template/apps/web/src/main.tsx +22 -82
  20. package/template/apps/web/src/platform-client.ts +71 -415
  21. package/template/apps/web/src/runtime-meta.ts +1 -1
  22. package/template/apps/web/src/styles.css +3 -3
  23. package/template/apps/web/test/contracts.test.ts +31 -769
  24. package/template/openxiangda.config.ts +14 -81
  25. package/template/package.json +3 -3
  26. package/template/packages/contracts/src/generated.ts +7 -804
  27. package/template/apps/server/src/instrument-context.controller.ts +0 -25
  28. package/template/apps/web/e2e/instruments.spec.ts +0 -1338
  29. package/template/apps/web/src/CollegePage.tsx +0 -181
  30. package/template/apps/web/src/InstrumentDetailPage.tsx +0 -176
  31. package/template/apps/web/src/InstrumentForm.tsx +0 -380
  32. package/template/apps/web/src/InstrumentFormPage.tsx +0 -82
  33. package/template/apps/web/src/InstrumentListPage.tsx +0 -457
  34. package/template/apps/web/src/fields.ts +0 -18
  35. package/template/apps/web/src/instrument.ts +0 -56
  36. package/template/platform/data/colleges.ts +0 -21
  37. package/template/platform/data/instrument-surface.ts +0 -125
  38. package/template/platform/data/instruments.ts +0 -83
@@ -1,69 +1,18 @@
1
- import {
2
- ApartmentOutlined,
3
- AppstoreOutlined,
4
- BookOutlined,
5
- DownOutlined,
6
- ExperimentOutlined,
7
- HomeOutlined,
8
- LeftOutlined,
9
- MenuUnfoldOutlined,
10
- SettingOutlined,
11
- TeamOutlined,
12
- } from '@ant-design/icons';
13
- import {
14
- Avatar,
15
- Breadcrumb,
16
- Button,
17
- Layout,
18
- Menu,
19
- Space,
20
- Typography,
21
- type MenuProps,
22
- } from 'antd';
1
+ import { AppstoreOutlined, DatabaseOutlined, LeftOutlined, MenuUnfoldOutlined, SettingOutlined } from '@ant-design/icons';
2
+ import { Avatar, Breadcrumb, Button, Empty, Layout, Menu, Space, Typography, type MenuProps } from 'antd';
23
3
  import type { ReactNode } from 'react';
24
4
  import { useEffect, useMemo, useState } from 'react';
25
5
  import { useLocation, useNavigate } from 'react-router-dom';
6
+ import { resourceDefinitions } from '../../../packages/contracts/src/generated.js';
26
7
  import { resolveDirectory } from './platform-client';
27
8
  import { useRuntime } from './runtime';
28
9
 
29
10
  const { Content, Header, Sider } = Layout;
11
+ type Definition = { code: string; name: string; capabilities: { read: string }; surface: { generated?: boolean } };
12
+ const definitions = resourceDefinitions as unknown as Record<string, Definition>;
30
13
 
31
- type PageMeta = { title: string; breadcrumbs: string[]; selectedKey: string };
32
-
33
- function pageMeta(pathname: string): PageMeta {
34
- if (pathname === '/colleges') {
35
- return {
36
- title: '学院字典',
37
- breadcrumbs: ['首页', '基础数据', '学院字典'],
38
- selectedKey: '/colleges',
39
- };
40
- }
41
- if (pathname.endsWith('/new')) {
42
- return {
43
- title: '新增仪器',
44
- breadcrumbs: ['首页', '仪器管理', '仪器资源', '新增仪器'],
45
- selectedKey: '/instruments',
46
- };
47
- }
48
- if (pathname.endsWith('/edit')) {
49
- return {
50
- title: '编辑仪器',
51
- breadcrumbs: ['首页', '仪器管理', '仪器资源', '编辑仪器'],
52
- selectedKey: '/instruments',
53
- };
54
- }
55
- if (/^\/instruments\/[^/]+$/.test(pathname)) {
56
- return {
57
- title: '仪器详情',
58
- breadcrumbs: ['首页', '仪器管理', '仪器资源', '仪器详情'],
59
- selectedKey: '/instruments',
60
- };
61
- }
62
- return {
63
- title: '仪器资源',
64
- breadcrumbs: ['首页', '仪器管理', '仪器资源'],
65
- selectedKey: '/instruments',
66
- };
14
+ function currentDefinition(pathname: string) {
15
+ return Object.values(definitions).find(definition => pathname === `/${definition.code}` || pathname.startsWith(`/${definition.code}/`) || pathname === `/m/${definition.code}` || pathname.startsWith(`/m/${definition.code}/`));
67
16
  }
68
17
 
69
18
  export function Shell({ children }: { children: ReactNode }) {
@@ -72,169 +21,48 @@ export function Shell({ children }: { children: ReactNode }) {
72
21
  const navigate = useNavigate();
73
22
  const [collapsed, setCollapsed] = useState(false);
74
23
  const [user, setUser] = useState({ label: '当前用户', description: '平台用户' });
75
- const meta = pageMeta(location.pathname);
76
-
24
+ const definition = currentDefinition(location.pathname);
77
25
  useEffect(() => {
78
26
  let active = true;
79
- void resolveDirectory('user', [identity.userId])
80
- .then(page => {
81
- const entry = page.items[0];
82
- if (active && entry) {
83
- setUser({
84
- label: entry.label || '当前用户',
85
- description: entry.description || '平台用户',
86
- });
87
- }
88
- })
89
- .catch(() => {
90
- if (active) setUser({ label: '当前用户', description: '平台用户' });
91
- });
92
- return () => {
93
- active = false;
94
- };
27
+ void resolveDirectory('user', [identity.userId]).then(page => {
28
+ const entry = page.items[0];
29
+ if (active && entry) setUser({ label: entry.label || '当前用户', description: entry.description || '平台用户' });
30
+ }).catch(() => undefined);
31
+ return () => { active = false; };
95
32
  }, [identity.userId]);
96
-
97
- const menuItems = useMemo<MenuProps['items']>(
98
- () => [
99
- {
100
- key: 'overview',
101
- icon: <AppstoreOutlined />,
102
- label: '总览',
103
- children: [
104
- { key: 'workbench', icon: <HomeOutlined />, label: '工作台' },
105
- ],
106
- },
107
- {
108
- key: 'instrument-management',
109
- icon: <ExperimentOutlined />,
110
- label: '仪器管理',
111
- children: [
112
- {
113
- key: '/instruments',
114
- icon: <ExperimentOutlined />,
115
- label: '仪器资源',
116
- },
117
- {
118
- key: 'open-records',
119
- icon: <BookOutlined />,
120
- label: '开放记录',
121
- disabled: true,
122
- },
123
- ],
124
- },
125
- {
126
- key: 'base-data',
127
- icon: <ApartmentOutlined />,
128
- label: '基础数据',
129
- children: hasCapability('app:instrument-center:data:colleges:read')
130
- ? [
131
- {
132
- key: '/colleges',
133
- icon: <ApartmentOutlined />,
134
- label: '学院字典',
135
- },
136
- ]
137
- : [],
138
- },
139
- {
140
- key: 'system-settings',
141
- icon: <SettingOutlined />,
142
- label: '系统设置',
143
- children: [
144
- {
145
- key: 'application-admins',
146
- icon: <TeamOutlined />,
147
- label: '应用管理员',
148
- disabled: true,
149
- },
150
- ],
151
- },
152
- ],
153
- [hasCapability]
154
- );
155
-
33
+ const menuItems = useMemo<MenuProps['items']>(() => [
34
+ { key: 'overview', icon: <AppstoreOutlined />, label: '总览', children: [{ key: 'home', label: '首页' }] },
35
+ {
36
+ key: 'resources',
37
+ icon: <DatabaseOutlined />,
38
+ label: '数据管理',
39
+ children: Object.values(definitions)
40
+ .filter(item => item.surface.generated !== false && hasCapability(item.capabilities.read))
41
+ .map(item => ({ key: `/${item.code}`, icon: <DatabaseOutlined />, label: item.name })),
42
+ },
43
+ { key: 'settings', icon: <SettingOutlined />, label: '系统设置', children: [{ key: 'settings-disabled', label: '应用设置', disabled: true }] },
44
+ ], [hasCapability]);
156
45
  return (
157
46
  <Layout className="oxa-app-layout">
158
- <Sider
159
- className="oxa-sider"
160
- breakpoint="lg"
161
- collapsed={collapsed}
162
- collapsedWidth={76}
163
- collapsible
164
- onBreakpoint={setCollapsed}
165
- theme="light"
166
- trigger={null}
167
- width={240}
168
- >
169
- <div className="oxa-brand">
170
- <div aria-hidden className="oxa-brand-mark">
171
- <span />
172
- </div>
173
- {!collapsed && (
174
- <div className="oxa-brand-copy">
175
- <strong>仪器资源管理</strong>
176
- <span>仪器、学院与开放管理</span>
177
- </div>
178
- )}
179
- </div>
180
- <Menu
181
- className="oxa-menu"
182
- defaultOpenKeys={[
183
- 'overview',
184
- 'instrument-management',
185
- 'base-data',
186
- 'system-settings',
187
- ]}
188
- inlineCollapsed={collapsed}
189
- items={menuItems}
190
- mode="inline"
191
- onClick={({ key }) => {
192
- if (key === 'workbench' || key === '/instruments') {
193
- navigate('/instruments');
194
- } else if (key === '/colleges') {
195
- navigate('/colleges');
196
- }
197
- }}
198
- selectedKeys={[meta.selectedKey]}
199
- />
200
- <div className="oxa-sider-footer">
201
- <Button
202
- aria-label={collapsed ? '展开侧边栏' : '收起侧边栏'}
203
- block
204
- icon={collapsed ? <MenuUnfoldOutlined /> : <LeftOutlined />}
205
- onClick={() => setCollapsed(value => !value)}
206
- type="text"
207
- >
208
- {!collapsed && '收起侧边栏'}
209
- </Button>
210
- </div>
47
+ <Sider className="oxa-sider" collapsed={collapsed} collapsedWidth={76} collapsible onBreakpoint={setCollapsed} theme="light" trigger={null} width={240}>
48
+ <div className="oxa-brand"><div aria-hidden className="oxa-brand-mark"><span /></div>{!collapsed && <div className="oxa-brand-copy"><strong>OpenXiangda 应用</strong><span>标准资源管理工作台</span></div>}</div>
49
+ <Menu className="oxa-menu" defaultOpenKeys={['overview', 'resources', 'settings']} inlineCollapsed={collapsed} items={menuItems} mode="inline" onClick={({ key }) => {
50
+ if (key === 'home') navigate(Object.keys(definitions)[0] ? `/${Object.keys(definitions)[0]}` : '/');
51
+ else if (typeof key === 'string' && key.startsWith('/')) navigate(key);
52
+ }} selectedKeys={definition ? [`/${definition.code}`] : []} />
53
+ <div className="oxa-sider-footer"><Button aria-label={collapsed ? '展开侧边栏' : '收起侧边栏'} block icon={collapsed ? <MenuUnfoldOutlined /> : <LeftOutlined />} onClick={() => setCollapsed(value => !value)} type="text">{!collapsed && '收起侧边栏'}</Button></div>
211
54
  </Sider>
212
55
  <Layout className="oxa-workspace">
213
56
  <Header className="oxa-topbar">
214
- <div className="oxa-topbar-page">
215
- <Breadcrumb items={meta.breadcrumbs.map(title => ({ title }))} />
216
- <Typography.Title
217
- data-testid={meta.title === '仪器资源' ? 'instrument-title' : undefined}
218
- level={3}
219
- >
220
- {meta.title}
221
- </Typography.Title>
222
- </div>
223
- <Space className="oxa-current-user" size={12}>
224
- <Avatar className="oxa-current-user-avatar" size={42}>
225
- {user.label.slice(0, 1)}
226
- </Avatar>
227
- <div className="oxa-current-user-copy">
228
- <strong>{user.label}</strong>
229
- <span>{user.description}</span>
230
- </div>
231
- <DownOutlined className="oxa-current-user-chevron" />
232
- </Space>
57
+ <div className="oxa-topbar-page"><Breadcrumb items={[{ title: '首页' }, ...(definition ? [{ title: definition.name }] : [])]} /><Typography.Title level={3}>{definition?.name || '应用首页'}</Typography.Title></div>
58
+ <Space className="oxa-current-user" size={12}><Avatar className="oxa-current-user-avatar" size={42}>{user.label.slice(0, 1)}</Avatar><div className="oxa-current-user-copy"><strong>{user.label}</strong><span>{user.description}</span></div></Space>
233
59
  </Header>
234
- <Content className="oxa-content">
235
- <main className="oxa-main">{children}</main>
236
- </Content>
60
+ <Content className="oxa-content"><main className="oxa-main">{children}</main></Content>
237
61
  </Layout>
238
62
  </Layout>
239
63
  );
240
64
  }
65
+
66
+ export function EmptyApplicationPage() {
67
+ return <Shell><Empty description="还没有声明数据资源;请在 platform/data 中添加 DataResource" /></Shell>;
68
+ }
@@ -78,7 +78,7 @@ function openIsolatedWindow(url: string) {
78
78
 
79
79
  export function AttachmentFileList({
80
80
  files,
81
- resourceCode = 'instruments',
81
+ resourceCode = 'resources',
82
82
  removable = false,
83
83
  onRemove,
84
84
  }: {
@@ -98,13 +98,27 @@ function normalizeFormValues(values: Record<string, unknown>, surface: DataResou
98
98
  const widget = surface.fields[key]?.widget;
99
99
  return widget === 'date' || widget === 'datetime'
100
100
  ? [key, value && typeof value === 'object' && 'format' in value
101
- ? (value as { format: (pattern: string) => string }).format(widget === 'datetime' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD')
101
+ ? (widget === 'datetime'
102
+ ? dayjs(value as never).toISOString()
103
+ : (value as { format: (pattern: string) => string }).format('YYYY-MM-DD'))
102
104
  : value]
103
105
  : [key, value];
104
106
  })
105
107
  );
106
108
  }
107
109
 
110
+ function fieldWritable(
111
+ field: SurfaceField,
112
+ mode: 'create' | 'edit',
113
+ hasCapability: (code: string) => boolean,
114
+ isAppSuperAdmin: boolean
115
+ ) {
116
+ const capability = mode === 'create' ? field.createCapability : field.updateCapability;
117
+ if (isAppSuperAdmin) return true;
118
+ if (capability) return hasCapability(capability);
119
+ return mode === 'create';
120
+ }
121
+
108
122
  function useGeneratedList(definition: GeneratedDefinition) {
109
123
  const { code, surface } = definition;
110
124
  const defaultSort = surface.list?.defaultSort || {
@@ -152,7 +166,7 @@ function GeneratedResourceListPage({ definition, variant }: { definition: Genera
152
166
  function GeneratedDesktopList({ definition }: { definition: GeneratedDefinition }) {
153
167
  const navigate = useNavigate();
154
168
  const { message } = App.useApp();
155
- const { hasCapability } = useRuntime();
169
+ const { hasCapability, identity } = useRuntime();
156
170
  const state = useGeneratedList(definition);
157
171
  const { code, name, surface, capabilities } = definition;
158
172
  const fields = listSurfaceFields(surface.fields);
@@ -244,7 +258,7 @@ function GeneratedResourceFormPage({ definition, mode, variant }: { definition:
244
258
  const { id = '' } = useParams();
245
259
  const navigate = useNavigate();
246
260
  const { message } = App.useApp();
247
- const { hasCapability } = useRuntime();
261
+ const { hasCapability, identity } = useRuntime();
248
262
  const [form] = Form.useForm();
249
263
  const create = useCreate<ResourceRecord>();
250
264
  const update = useUpdate<ResourceRecord>();
@@ -257,14 +271,21 @@ function GeneratedResourceFormPage({ definition, mode, variant }: { definition:
257
271
  const Page = variant === 'mobile' ? MobileResourceFormPage : ResourceFormPage;
258
272
  if (!hasCapability(mode === 'create' ? capabilities.create : capabilities.update)) return <Result status="403" title="当前平台用户无此操作权限" />;
259
273
  const submit = async (values: Record<string, unknown>) => {
260
- const next = normalizeFormValues(values, surface);
274
+ const next = normalizeFormValues(
275
+ Object.fromEntries(
276
+ Object.entries(values).filter(([key]) =>
277
+ fieldWritable(surface.fields[key] ? { key, ...surface.fields[key] } : { key, label: key, widget: 'text' }, mode, hasCapability, identity.isAppSuperAdmin)
278
+ )
279
+ ),
280
+ surface
281
+ );
261
282
  if (mode === 'create') { await create.mutateAsync({ resource: code, values: next }); message.success(`${name}已创建`); navigate(listPath); }
262
283
  else { const result = await update.mutateAsync({ resource: code, id, values: next, meta: { expectedRevision: record?.revision } }); message.success('修改已保存'); navigate(`/${code}/${result.data.id}`); }
263
284
  };
264
285
  return (
265
286
  <Page backLabel={mode === 'edit' ? `返回${name}详情` : `返回${name}`} backPath={mode === 'edit' ? detailPath : listPath} capability={mode === 'create' ? capabilities.create : capabilities.update} detailPath={record ? detailPath : undefined} loading={mode === 'edit' && query.query.isLoading} mode={mode} notFound={mode === 'edit' && !record} recordSummary={record ? `${name} · 当前版本 ${record.revision}` : undefined} resource={code} surface={surface} title={name}>
266
287
  <Form className={variant === 'mobile' ? 'oxa-mobile-form' : 'oxa-form-full'} form={form} initialValues={undefined} layout="vertical" onFinish={values => void submit(values as Record<string, unknown>)}>
267
- <div className="oxa-sections">{fieldsBySection(surface).map(group => <Card className="oxa-section-card" key={group.section} title={group.section === 'default' ? '基本信息' : group.section}><div className="oxa-grid">{group.fields.map(field => { const capability = mode === 'create' ? field.createCapability : field.updateCapability; return <SurfaceFieldControl key={field.key} disabled={Boolean(capability) && !hasCapability(capability!)} field={field} operation={mode === 'create' ? 'create' : 'update'} recordId={record?.id} renderers={{ upload: (current, file, recordId) => createNativeResourceClient(code, surface).upload(current.key, file, recordId) }} />; })}</div></Card>)}</div>
288
+ <div className="oxa-sections">{fieldsBySection(surface).map(group => <Card className="oxa-section-card" key={group.section} title={group.section === 'default' ? '基本信息' : group.section}><div className="oxa-grid">{group.fields.map(field => <SurfaceFieldControl key={field.key} disabled={!fieldWritable(field, mode, hasCapability, identity.isAppSuperAdmin)} field={field} operation={mode === 'create' ? 'create' : 'update'} recordId={record?.id} renderers={{ upload: (current, file, recordId) => createNativeResourceClient(code, surface).upload(current.key, file, recordId) }} />)}</div></Card>)}</div>
268
289
  <div className="oxa-actions"><Button onClick={() => navigate(mode === 'edit' ? detailPath : listPath)}>取消</Button><Button htmlType="submit" type="primary">{mode === 'create' ? '保存' : '保存修改'}</Button></div>
269
290
  </Form>
270
291
  </Page>
@@ -21,7 +21,7 @@ import type {
21
21
  DataFileRef,
22
22
  } from 'openxiangda-contracts/browser';
23
23
  import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
24
- import { ResolvedValueText } from '../../AuthoritativeSelector';
24
+ import { AuthoritativeSelector, ResolvedValueText } from '../../AuthoritativeSelector';
25
25
  import { AttachmentFileList } from '../platform-fields/AttachmentFileList';
26
26
  import { PlatformDirectoryPicker } from '../platform-fields/PlatformDirectoryPicker';
27
27
 
@@ -124,6 +124,8 @@ export function SurfaceFilterControl({
124
124
  case 'directory-user':
125
125
  case 'directory-department':
126
126
  return <PlatformDirectoryPicker kind={field.widget === 'directory-user' ? 'user' : 'department'} multiple={field.multiple} onChange={onChange as (value: string | string[] | undefined) => void} placeholder={placeholder} value={value as string | string[] | undefined} />;
127
+ case 'resource':
128
+ return <AuthoritativeSelector source="resource" resourceCode={field.reference?.kind === 'resource' ? field.reference.resourceCode : undefined} labelField={field.reference?.kind === 'resource' ? field.reference.labelField : undefined} multiple={field.multiple} onChange={onChange as (value: string | string[] | undefined) => void} placeholder={placeholder} value={value as string | string[] | undefined} operation="update" />;
127
129
  default:
128
130
  return <Input allowClear value={value as string | undefined} onChange={event => onChange(event.target.value)} placeholder={placeholder} />;
129
131
  }
@@ -193,6 +195,19 @@ export function SurfaceFieldControl({
193
195
  />
194
196
  );
195
197
  break;
198
+ case 'resource':
199
+ control = (
200
+ <AuthoritativeSelector
201
+ disabled={disabled}
202
+ labelField={field.reference?.kind === 'resource' ? field.reference.labelField : undefined}
203
+ multiple={field.multiple}
204
+ operation={operation}
205
+ placeholder={`搜索并选择${field.label}`}
206
+ resourceCode={field.reference?.kind === 'resource' ? field.reference.resourceCode : undefined}
207
+ source="resource"
208
+ />
209
+ );
210
+ break;
196
211
  case 'file':
197
212
  control = (
198
213
  <ManagedFileField
@@ -304,6 +319,19 @@ export function MobileSurfaceFieldControl({
304
319
  />
305
320
  );
306
321
  break;
322
+ case 'resource':
323
+ control = (
324
+ <AuthoritativeSelector
325
+ disabled={disabled}
326
+ labelField={field.reference?.kind === 'resource' ? field.reference.labelField : undefined}
327
+ multiple={field.multiple}
328
+ operation={operation}
329
+ placeholder={`搜索并选择${field.label}`}
330
+ resourceCode={field.reference?.kind === 'resource' ? field.reference.resourceCode : undefined}
331
+ source="resource"
332
+ />
333
+ );
334
+ break;
307
335
  case 'file':
308
336
  control = (
309
337
  <ManagedFileField
@@ -351,6 +379,9 @@ export function SurfaceFieldValue({
351
379
  />
352
380
  );
353
381
  }
382
+ if (field.widget === 'resource' && field.reference?.kind === 'resource') {
383
+ return <ResolvedValueText labelField={field.reference.labelField} resourceCode={field.reference.resourceCode} source="resource" value={Array.isArray(value) ? value.map(String) : String(value)} />;
384
+ }
354
385
  if (field.widget === 'file') {
355
386
  const values = (Array.isArray(value) ? value : [value]).filter(isFileRef);
356
387
  return values.length ? <AttachmentFileList files={values} /> : <>-</>;
@@ -1,10 +1,5 @@
1
1
  import type { DataProvider } from '@refinedev/core';
2
- import type { InstrumentListQuery, InstrumentRecord } from './instrument';
3
- import {
4
- createNativeResourceClient,
5
- instrumentDataApi,
6
- type GenericResourceQuery,
7
- } from './platform-client';
2
+ import { createNativeResourceClient, type GenericResourceQuery } from './platform-client';
8
3
  import { applicationCode } from './runtime-meta';
9
4
  import { resourceDefinitions } from '../../../packages/contracts/src/generated.js';
10
5
 
@@ -15,112 +10,38 @@ type GeneratedDefinition = {
15
10
  surface: import('openxiangda-contracts/browser').DataResourceSurface;
16
11
  };
17
12
 
18
- const generatedDefinitions = resourceDefinitions as unknown as Record<string, GeneratedDefinition>;
13
+ const definitions = resourceDefinitions as unknown as Record<string, GeneratedDefinition>;
14
+ type ListParams = Parameters<NonNullable<DataProvider['getList']>>[0];
15
+ type OneParams = Parameters<NonNullable<DataProvider['getOne']>>[0];
16
+ type CreateParams = Parameters<NonNullable<DataProvider['create']>>[0];
17
+ type UpdateParams = Parameters<NonNullable<DataProvider['update']>>[0];
18
+ type DeleteParams = Parameters<NonNullable<DataProvider['deleteOne']>>[0];
19
19
 
20
- function generatedClient(resource: string) {
21
- const definition = generatedDefinitions[resource];
22
- if (!definition || definition.surface.generated === false) {
23
- throw new Error(`OPENXIANGDA_GENERATED_RESOURCE_NOT_DECLARED:${resource}`);
24
- }
20
+ function clientFor(resource: string) {
21
+ const definition = definitions[resource];
22
+ if (!definition) throw new Error(`OPENXIANGDA_GENERATED_RESOURCE_NOT_DECLARED:${resource}`);
25
23
  return createNativeResourceClient(resource, definition.surface);
26
24
  }
27
25
 
28
- export const instrumentProvider = {
29
- getApiUrl: () =>
30
- `/service/openxiangda-api/v2/applications/${applicationCode()}/native/data`,
31
- getList: async ({ pagination, sorters, meta }) => {
32
- const filters = (meta?.query || {}) as Partial<InstrumentListQuery>;
33
- const sorter = sorters?.[0];
34
- const result = await instrumentDataApi.list({
35
- ...filters,
36
- page: pagination?.currentPage || 1,
37
- pageSize: pagination?.pageSize || 20,
38
- sort: sorter
39
- ? {
40
- field: String(sorter.field),
41
- order: sorter.order,
42
- }
43
- : { field: 'instrumentCode', order: 'asc' },
44
- });
45
- return { data: result.rows, total: result.total };
46
- },
47
- getOne: async ({ id }) => ({ data: await instrumentDataApi.get(String(id)) }),
48
- create: async ({ variables }) => ({
49
- data: await instrumentDataApi.create(variables as Partial<InstrumentRecord>),
50
- }),
51
- update: async ({ id, variables, meta }) => ({
52
- data: await instrumentDataApi.update(
53
- String(id),
54
- Number(meta?.expectedRevision),
55
- variables as Partial<InstrumentRecord>
56
- ),
57
- }),
58
- deleteOne: async ({ id, meta }) => ({
59
- data: await instrumentDataApi.remove(
60
- String(id),
61
- Number(meta?.expectedRevision)
62
- ),
63
- }),
64
- } as DataProvider;
65
-
66
- const generatedProvider = {
67
- getApiUrl: () =>
68
- `/service/openxiangda-api/v2/applications/${applicationCode()}/native/data`,
69
- getList: async ({ resource, pagination, sorters, meta }) => {
26
+ export const applicationProvider = {
27
+ getApiUrl: () => `/service/openxiangda-api/v2/applications/${applicationCode()}/native/data`,
28
+ getList: async ({ resource, pagination, sorters, meta }: ListParams) => {
70
29
  const query = (meta?.query || {}) as Partial<GenericResourceQuery>;
71
30
  const sorter = sorters?.[0];
72
- const result = await generatedClient(resource).list({
73
- ...query,
31
+ const result = await clientFor(resource).list({
74
32
  page: pagination?.currentPage || 1,
75
33
  pageSize: pagination?.pageSize || 20,
76
- sort: sorter
77
- ? { field: String(sorter.field), order: sorter.order || 'asc' }
78
- : query.sort,
34
+ ...query,
35
+ ...(sorter ? { sort: { field: String(sorter.field), order: (sorter.order || 'asc') as 'asc' | 'desc' } } : {}),
79
36
  });
80
37
  return { data: result.rows, total: result.total };
81
38
  },
82
- getOne: async ({ resource, id }) => ({
83
- data: await generatedClient(resource).get(String(id)),
39
+ getOne: async ({ resource, id }: OneParams) => ({ data: await clientFor(resource).get(String(id)) }),
40
+ create: async ({ resource, variables }: CreateParams) => ({ data: await clientFor(resource).create(variables as Record<string, unknown>) }),
41
+ update: async ({ resource, id, variables, meta }: UpdateParams) => ({
42
+ data: await clientFor(resource).update(String(id), Number(meta?.expectedRevision), variables as Record<string, unknown>),
84
43
  }),
85
- create: async ({ resource, variables }) => ({
86
- data: await generatedClient(resource).create(variables as Record<string, unknown>),
44
+ deleteOne: async ({ resource, id, meta }: DeleteParams) => ({
45
+ data: await clientFor(resource).remove(String(id), Number(meta?.expectedRevision)),
87
46
  }),
88
- update: async ({ resource, id, variables, meta }) => ({
89
- data: await generatedClient(resource).update(
90
- String(id),
91
- Number(meta?.expectedRevision),
92
- variables as Record<string, unknown>
93
- ),
94
- }),
95
- deleteOne: async ({ resource, id, meta }) => ({
96
- data: await generatedClient(resource).remove(
97
- String(id),
98
- Number(meta?.expectedRevision)
99
- ),
100
- }),
101
- } as DataProvider;
102
-
103
- /** One Refine provider: golden modules may override a resource, simple resources use generated CRUD. */
104
- export const applicationProvider = {
105
- getApiUrl: instrumentProvider.getApiUrl,
106
- getList: (args: Parameters<NonNullable<DataProvider['getList']>>[0]) =>
107
- args.resource === 'instruments'
108
- ? instrumentProvider.getList!(args)
109
- : generatedProvider.getList!(args),
110
- getOne: (args: Parameters<NonNullable<DataProvider['getOne']>>[0]) =>
111
- args.resource === 'instruments'
112
- ? instrumentProvider.getOne!(args)
113
- : generatedProvider.getOne!(args),
114
- create: (args: Parameters<NonNullable<DataProvider['create']>>[0]) =>
115
- args.resource === 'instruments'
116
- ? instrumentProvider.create!(args)
117
- : generatedProvider.create!(args),
118
- update: (args: Parameters<NonNullable<DataProvider['update']>>[0]) =>
119
- args.resource === 'instruments'
120
- ? instrumentProvider.update!(args)
121
- : generatedProvider.update!(args),
122
- deleteOne: (args: Parameters<NonNullable<DataProvider['deleteOne']>>[0]) =>
123
- args.resource === 'instruments'
124
- ? instrumentProvider.deleteOne!(args)
125
- : generatedProvider.deleteOne!(args),
126
- } as DataProvider;
47
+ } as unknown as DataProvider;