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.
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +1 -0
- package/dist/create-workspace.js.map +1 -1
- package/package.json +4 -4
- package/template/AGENTS.md +1 -1
- package/template/README.md +7 -7
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +1 -3
- package/template/apps/server/test/smoke.test.ts +6 -23
- package/template/apps/web/e2e/resources.spec.ts +20 -0
- package/template/apps/web/index.html +1 -1
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/AuthoritativeSelector.tsx +38 -29
- package/template/apps/web/src/Shell.tsx +39 -211
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +26 -5
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +32 -1
- package/template/apps/web/src/data-provider.ts +23 -102
- package/template/apps/web/src/main.tsx +22 -82
- package/template/apps/web/src/platform-client.ts +71 -415
- package/template/apps/web/src/runtime-meta.ts +1 -1
- package/template/apps/web/src/styles.css +3 -3
- package/template/apps/web/test/contracts.test.ts +31 -769
- package/template/openxiangda.config.ts +14 -81
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +7 -804
- package/template/apps/server/src/instrument-context.controller.ts +0 -25
- package/template/apps/web/e2e/instruments.spec.ts +0 -1338
- package/template/apps/web/src/CollegePage.tsx +0 -181
- package/template/apps/web/src/InstrumentDetailPage.tsx +0 -176
- package/template/apps/web/src/InstrumentForm.tsx +0 -380
- package/template/apps/web/src/InstrumentFormPage.tsx +0 -82
- package/template/apps/web/src/InstrumentListPage.tsx +0 -457
- package/template/apps/web/src/fields.ts +0 -18
- package/template/apps/web/src/instrument.ts +0 -56
- package/template/platform/data/colleges.ts +0 -21
- package/template/platform/data/instrument-surface.ts +0 -125
- package/template/platform/data/instruments.ts +0 -83
|
@@ -1,69 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
24
|
+
const definition = currentDefinition(location.pathname);
|
|
77
25
|
useEffect(() => {
|
|
78
26
|
let active = true;
|
|
79
|
-
void resolveDirectory('user', [identity.userId])
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
? (
|
|
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(
|
|
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 =>
|
|
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
|
|
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
|
|
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
|
|
21
|
-
const definition =
|
|
22
|
-
if (!definition
|
|
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
|
|
29
|
-
getApiUrl: () =>
|
|
30
|
-
|
|
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
|
|
73
|
-
...query,
|
|
31
|
+
const result = await clientFor(resource).list({
|
|
74
32
|
page: pagination?.currentPage || 1,
|
|
75
33
|
pageSize: pagination?.pageSize || 20,
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
data: await
|
|
44
|
+
deleteOne: async ({ resource, id, meta }: DeleteParams) => ({
|
|
45
|
+
data: await clientFor(resource).remove(String(id), Number(meta?.expectedRevision)),
|
|
87
46
|
}),
|
|
88
|
-
|
|
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;
|