openxiangda-cli 2.0.0-alpha.119 → 2.0.0-alpha.120
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 +1 -1
- package/template/README.md +5 -0
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/data-api-live-fixture.tsx +18 -9
- package/template/apps/web/e2e/data-api-live.spec.ts +9 -3
- package/template/apps/web/e2e/resource-experience-fixture.tsx +35 -20
- package/template/apps/web/e2e/resources.spec.ts +30 -6
- package/template/apps/web/package.json +1 -1
- package/template/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openxiangda-cli",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.120",
|
|
4
4
|
"description": "Thin application-level CLI for OpenXiangda 2.0.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@oclif/core": "4.13.3",
|
|
24
24
|
"openxiangda-contracts": "2.0.0-alpha.58",
|
|
25
|
-
"openxiangda-devkit-core": "2.0.0-alpha.
|
|
26
|
-
"openxiangda-mcp": "2.0.0-alpha.
|
|
27
|
-
"openxiangda-skill-kit": "2.0.0-alpha.
|
|
25
|
+
"openxiangda-devkit-core": "2.0.0-alpha.76",
|
|
26
|
+
"openxiangda-mcp": "2.0.0-alpha.76",
|
|
27
|
+
"openxiangda-skill-kit": "2.0.0-alpha.95"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsx": "4.23.12",
|
package/template/AGENTS.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- Use only the workspace-pinned CLI: `pnpm openxiangda <command>`. Never invoke a bare global `openxiangda` inside a 2.0 task.
|
|
4
4
|
- `appspec/` is the optional OpenXiangda 2.0 business-intent layer. Before changing observable behavior, read the bounded index with `pnpm openxiangda spec context --json`, then select only the relevant stable ID; use no ChangeSpec for L0 work, one short ChangeSpec for L1, and add permission/rollback/concurrency detail only for L2/L3. Before close, the user—not AI—confirms `currentSpec=merged` or `not-applicable`. AppSpec is advisory and never a release gate. Never read, import or migrate 1.x `openspec/` or SDD.
|
|
5
5
|
- Use Vite, React Router, Refine Core and Ant Design. Do not add Umi, ProComponents or another admin shell.
|
|
6
|
-
- Bind every generated `appRoutes` entry to its local page with `defineApplicationContributions`; desktop `admin` routes stay inside the platform Shell, while `user` routes render without an admin Shell for independent mobile/user experiences. Use only
|
|
6
|
+
- Bind every generated `appRoutes` entry to its local page with `defineApplicationContributions`; desktop `admin` routes stay inside the platform Shell, while `user` routes render without an admin Shell for independent mobile/user experiences. Generated resource CRUD routes are compiler-owned under `/admin/resources/<resourceCode>...` and `/m/admin/resources/<resourceCode>...`; never recreate root resource paths, aliases or redirects. Explicit routes that have the same canonical shape as another explicit or generated route fail compilation, even when dynamic parameter names differ. Use only the typed `toolbar`, `row` and `detail` resource slots for generated resource actions. Declare the complete editable admin menu with `defineAdminNavigation` and its page/group helpers; the Shell renders only generated `adminNavigation` references and permissions only filter them. Do not create another router, menu store, layout, identity provider, permission store or copied CRUD page; route/action access uses capability or `allOf`/`anyOf`, while Data/App API and Workflow authorization remain server-owned.
|
|
7
7
|
- Declare each resource once in `openxiangda.config.ts` with only `code`, `name`, `fields` and optional `mutationOwner`, generated/list/layout/data-policy settings. Each field owns type, label, required state, Surface flags, reference/file metadata and access. Resource codes are lower kebab-case. Use `native` for direct Data API mutations, `action`, `readonly` or `workflow` for non-Native ownership; never grant or generate Native mutation for a non-Native owner.
|
|
8
8
|
- Never write resource-level `schemaVersion`, `appCode`, `schema`, `surface`, `capabilities`, `fieldPolicies` or `platform/data` modules. The compiler derives the strict DataResource, CRUD capabilities, Surface and AI Schema. The application manifest still starts with its one top-level `schemaVersion: 3`.
|
|
9
9
|
- Ordinary list/get/create/update/delete, filters, export and batch operations use the platform Native Data API. Do not create Function CRUD or NestJS wrappers.
|
package/template/README.md
CHANGED
|
@@ -26,6 +26,11 @@ BrowserRouter、RuntimeBoundary、Refine、generated routes 和后台 Shell
|
|
|
26
26
|
在显式后台导航组中加入 `adminApplicationTodoCenterPage()`;平台生成
|
|
27
27
|
`/admin/todos` 与 `/m/todos`,只读取当前用户的消息/待办投影并统一解析目标页面。
|
|
28
28
|
|
|
29
|
+
生成式资源后台路径固定为 `/admin/resources/<resourceCode>...`,移动后台面为
|
|
30
|
+
`/m/admin/resources/<resourceCode>...`;普通根路径和 `/m/...` 产品路径可由显式
|
|
31
|
+
`user` route 使用。生成页内部跳转来自编译后的 `adminPages`,不要手写旧根路径、别名或
|
|
32
|
+
重定向。显式 route 与任一生成 route 的动态路径形状重复时,配置编译会直接失败。
|
|
33
|
+
|
|
29
34
|
前端一次读取当前登录用户在当前环境的完整应用角色并集和 capability。应用可在 `perspectives` 中声明工作视角;视角只投影页面、行和字段的读取范围,不改变真实身份,也不收窄新增、编辑、删除、流程与自定义动作权限。标准 Data API 自动识别视角;只有绕过标准 Data API 的自定义 Nest 查询才需要通过 `@CurrentPerspective()` 显式实现同等读取投影。字段默认继承资源 read/create/update;`access` 数组为 all-of,`false` 为明确拒绝,无权字段在提交前从 payload 删除。
|
|
30
35
|
|
|
31
36
|
```bash
|
|
@@ -99,14 +99,23 @@ export const liveDefinition: GeneratedResourceDefinition = {
|
|
|
99
99
|
|
|
100
100
|
const definitions = { [code]: liveDefinition };
|
|
101
101
|
const provider = createApplicationProvider(definitions);
|
|
102
|
+
const routeBase = `/admin/resources/${code}`;
|
|
103
|
+
const routePaths = {
|
|
104
|
+
fallback: '/admin',
|
|
105
|
+
list: routeBase,
|
|
106
|
+
create: `${routeBase}/new`,
|
|
107
|
+
detail: `${routeBase}/:id`,
|
|
108
|
+
edit: `${routeBase}/:id/edit`,
|
|
109
|
+
};
|
|
102
110
|
const initialPath =
|
|
103
|
-
new URLSearchParams(window.location.search).get('initial') ||
|
|
111
|
+
new URLSearchParams(window.location.search).get('initial') || routeBase;
|
|
104
112
|
|
|
105
113
|
function resourcePage(mode: 'list' | 'create' | 'edit' | 'detail') {
|
|
106
114
|
return (
|
|
107
115
|
<GeneratedResourcePage
|
|
108
116
|
definition={liveDefinition}
|
|
109
117
|
mode={mode}
|
|
118
|
+
paths={routePaths}
|
|
110
119
|
resourceCode={code}
|
|
111
120
|
/>
|
|
112
121
|
);
|
|
@@ -124,22 +133,22 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
124
133
|
resources={[
|
|
125
134
|
{
|
|
126
135
|
name: code,
|
|
127
|
-
list:
|
|
128
|
-
create:
|
|
129
|
-
edit:
|
|
130
|
-
show:
|
|
136
|
+
list: routePaths.list,
|
|
137
|
+
create: routePaths.create,
|
|
138
|
+
edit: routePaths.edit,
|
|
139
|
+
show: routePaths.detail,
|
|
131
140
|
},
|
|
132
141
|
]}
|
|
133
142
|
>
|
|
134
143
|
<Routes>
|
|
135
|
-
<Route path={
|
|
136
|
-
<Route path={
|
|
144
|
+
<Route path={routePaths.list} element={resourcePage('list')} />
|
|
145
|
+
<Route path={routePaths.create} element={resourcePage('create')} />
|
|
137
146
|
<Route
|
|
138
|
-
path={
|
|
147
|
+
path={routePaths.detail}
|
|
139
148
|
element={resourcePage('detail')}
|
|
140
149
|
/>
|
|
141
150
|
<Route
|
|
142
|
-
path={
|
|
151
|
+
path={routePaths.edit}
|
|
143
152
|
element={resourcePage('edit')}
|
|
144
153
|
/>
|
|
145
154
|
</Routes>
|
|
@@ -31,7 +31,9 @@ test.describe('@live-data-api standard resource UI', () => {
|
|
|
31
31
|
latitude: 30.234567,
|
|
32
32
|
accuracy: 4,
|
|
33
33
|
});
|
|
34
|
-
await page.goto(
|
|
34
|
+
await page.goto(
|
|
35
|
+
'/data-api-live.e2e.html?initial=/admin/resources/field-records',
|
|
36
|
+
);
|
|
35
37
|
await expect(page.locator('.oxa-list-surface')).toBeVisible();
|
|
36
38
|
|
|
37
39
|
await page.getByRole('button', { name: '新增字段协议记录' }).click();
|
|
@@ -102,7 +104,9 @@ test.describe('@live-data-api standard resource UI', () => {
|
|
|
102
104
|
path: '/',
|
|
103
105
|
},
|
|
104
106
|
]);
|
|
105
|
-
await page.goto(
|
|
107
|
+
await page.goto(
|
|
108
|
+
'/data-api-live.e2e.html?initial=/admin/resources/field-records',
|
|
109
|
+
);
|
|
106
110
|
await expect(page.locator('.oxa-list-surface')).toBeVisible();
|
|
107
111
|
await page.locator('.oxa-search-panel .ant-select').first().click();
|
|
108
112
|
await page.getByText('标题', { exact: true }).last().click();
|
|
@@ -129,7 +133,9 @@ test.describe('@live-data-api standard resource UI', () => {
|
|
|
129
133
|
path: '/',
|
|
130
134
|
},
|
|
131
135
|
]);
|
|
132
|
-
await page.goto(
|
|
136
|
+
await page.goto(
|
|
137
|
+
'/data-api-live.e2e.html?initial=/admin/resources/field-records',
|
|
138
|
+
);
|
|
133
139
|
await expect(
|
|
134
140
|
page.getByText('当前平台用户无字段协议记录页面权限', { exact: true })
|
|
135
141
|
).toBeVisible();
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import ReactDOM from 'react-dom/client';
|
|
3
3
|
import { Button, Typography } from 'antd';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
defineApplicationContributions,
|
|
6
6
|
OpenXiangdaApplication,
|
|
7
7
|
} from 'openxiangda/react';
|
|
8
8
|
import type { DataFieldSurface, DataResourceSurface } from 'openxiangda/core';
|
|
@@ -145,7 +145,11 @@ function ContentOperationsPage() {
|
|
|
145
145
|
return <Typography.Title level={2}>内容业务操作</Typography.Title>;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
function ResourcePortalPage() {
|
|
149
|
+
return <Typography.Title level={2}>用户端内容门户</Typography.Title>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const applicationContributions = defineApplicationContributions(
|
|
149
153
|
{
|
|
150
154
|
contentOperations: {
|
|
151
155
|
code: 'content-operations',
|
|
@@ -156,9 +160,20 @@ const adminContributions = defineAdminContributions(
|
|
|
156
160
|
tabPersistence: 'session',
|
|
157
161
|
keepAlive: 'none',
|
|
158
162
|
},
|
|
163
|
+
resourcePortal: {
|
|
164
|
+
code: 'resource-portal',
|
|
165
|
+
path: '/resource-01',
|
|
166
|
+
label: '用户端内容门户',
|
|
167
|
+
surface: 'user',
|
|
168
|
+
tabPersistence: 'session',
|
|
169
|
+
keepAlive: 'none',
|
|
170
|
+
},
|
|
159
171
|
} as const,
|
|
160
172
|
{
|
|
161
|
-
pages: {
|
|
173
|
+
pages: {
|
|
174
|
+
contentOperations: ContentOperationsPage,
|
|
175
|
+
resourcePortal: ResourcePortalPage,
|
|
176
|
+
},
|
|
162
177
|
resources: {
|
|
163
178
|
'resource-01': {
|
|
164
179
|
toolbar: [
|
|
@@ -177,7 +192,7 @@ const adminContributions = defineAdminContributions(
|
|
|
177
192
|
);
|
|
178
193
|
|
|
179
194
|
if (location.pathname.endsWith('/resource-experience.e2e.html')) {
|
|
180
|
-
history.replaceState({}, '', '/resource-01');
|
|
195
|
+
history.replaceState({}, '', '/admin/resources/resource-01');
|
|
181
196
|
}
|
|
182
197
|
|
|
183
198
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
@@ -216,7 +231,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
216
231
|
{
|
|
217
232
|
code: 'resource:resource-01:list',
|
|
218
233
|
kind: 'resource-list',
|
|
219
|
-
path: '/resource-01',
|
|
234
|
+
path: '/admin/resources/resource-01',
|
|
220
235
|
label: '资源 01',
|
|
221
236
|
navigationEligible: true,
|
|
222
237
|
capability: resourceDefinitions['resource-01'].capabilities.read,
|
|
@@ -225,7 +240,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
225
240
|
{
|
|
226
241
|
code: 'resource:resource-01:detail',
|
|
227
242
|
kind: 'resource-detail',
|
|
228
|
-
path: '/resource-01/:id',
|
|
243
|
+
path: '/admin/resources/resource-01/:id',
|
|
229
244
|
label: '资源 01 详情',
|
|
230
245
|
navigationEligible: false,
|
|
231
246
|
capability: resourceDefinitions['resource-01'].capabilities.read,
|
|
@@ -234,7 +249,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
234
249
|
{
|
|
235
250
|
code: 'resource:resource-01:create',
|
|
236
251
|
kind: 'resource-create',
|
|
237
|
-
path: '/resource-01/new',
|
|
252
|
+
path: '/admin/resources/resource-01/new',
|
|
238
253
|
label: '新增资源 01',
|
|
239
254
|
navigationEligible: false,
|
|
240
255
|
capability: resourceDefinitions['resource-01'].capabilities.create,
|
|
@@ -243,7 +258,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
243
258
|
{
|
|
244
259
|
code: 'resource:resource-01:update',
|
|
245
260
|
kind: 'resource-update',
|
|
246
|
-
path: '/resource-01/:id/edit',
|
|
261
|
+
path: '/admin/resources/resource-01/:id/edit',
|
|
247
262
|
label: '编辑资源 01',
|
|
248
263
|
navigationEligible: false,
|
|
249
264
|
capability: resourceDefinitions['resource-01'].capabilities.update,
|
|
@@ -252,7 +267,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
252
267
|
{
|
|
253
268
|
code: 'resource:resource-02:list',
|
|
254
269
|
kind: 'resource-list',
|
|
255
|
-
path: '/resource-02',
|
|
270
|
+
path: '/admin/resources/resource-02',
|
|
256
271
|
label: '内容文章',
|
|
257
272
|
navigationEligible: true,
|
|
258
273
|
capability: resourceDefinitions['resource-02'].capabilities.read,
|
|
@@ -261,7 +276,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
261
276
|
{
|
|
262
277
|
code: 'resource:resource-03:list',
|
|
263
278
|
kind: 'resource-list',
|
|
264
|
-
path: '/resource-03',
|
|
279
|
+
path: '/admin/resources/resource-03',
|
|
265
280
|
label: '内容专题',
|
|
266
281
|
navigationEligible: true,
|
|
267
282
|
capability: resourceDefinitions['resource-03'].capabilities.read,
|
|
@@ -270,7 +285,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
270
285
|
{
|
|
271
286
|
code: 'resource:resource-04:list',
|
|
272
287
|
kind: 'resource-list',
|
|
273
|
-
path: '/resource-04',
|
|
288
|
+
path: '/admin/resources/resource-04',
|
|
274
289
|
label: '会员档案',
|
|
275
290
|
navigationEligible: true,
|
|
276
291
|
capability: resourceDefinitions['resource-04'].capabilities.read,
|
|
@@ -279,7 +294,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
279
294
|
{
|
|
280
295
|
code: 'resource:resource-05:list',
|
|
281
296
|
kind: 'resource-list',
|
|
282
|
-
path: '/resource-05',
|
|
297
|
+
path: '/admin/resources/resource-05',
|
|
283
298
|
label: '门户轮播',
|
|
284
299
|
navigationEligible: true,
|
|
285
300
|
capability: resourceDefinitions['resource-05'].capabilities.read,
|
|
@@ -288,7 +303,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
288
303
|
{
|
|
289
304
|
code: 'resource:resource-06:list',
|
|
290
305
|
kind: 'resource-list',
|
|
291
|
-
path: '/resource-06',
|
|
306
|
+
path: '/admin/resources/resource-06',
|
|
292
307
|
label: '工会小组',
|
|
293
308
|
navigationEligible: true,
|
|
294
309
|
capability: resourceDefinitions['resource-06'].capabilities.read,
|
|
@@ -297,7 +312,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
297
312
|
{
|
|
298
313
|
code: 'resource:resource-07:list',
|
|
299
314
|
kind: 'resource-list',
|
|
300
|
-
path: '/resource-07',
|
|
315
|
+
path: '/admin/resources/resource-07',
|
|
301
316
|
label: '场地预约',
|
|
302
317
|
navigationEligible: true,
|
|
303
318
|
capability: resourceDefinitions['resource-07'].capabilities.read,
|
|
@@ -306,7 +321,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
306
321
|
{
|
|
307
322
|
code: 'resource:resource-08:list',
|
|
308
323
|
kind: 'resource-list',
|
|
309
|
-
path: '/resource-08',
|
|
324
|
+
path: '/admin/resources/resource-08',
|
|
310
325
|
label: '场地不可用时段',
|
|
311
326
|
navigationEligible: true,
|
|
312
327
|
capability: resourceDefinitions['resource-08'].capabilities.read,
|
|
@@ -315,7 +330,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
315
330
|
{
|
|
316
331
|
code: 'resource:resource-09:list',
|
|
317
332
|
kind: 'resource-list',
|
|
318
|
-
path: '/resource-09',
|
|
333
|
+
path: '/admin/resources/resource-09',
|
|
319
334
|
label: '场地档案',
|
|
320
335
|
navigationEligible: true,
|
|
321
336
|
capability: resourceDefinitions['resource-09'].capabilities.read,
|
|
@@ -324,7 +339,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
324
339
|
{
|
|
325
340
|
code: 'resource:resource-10:list',
|
|
326
341
|
kind: 'resource-list',
|
|
327
|
-
path: '/resource-10',
|
|
342
|
+
path: '/admin/resources/resource-10',
|
|
328
343
|
label: '通知公告',
|
|
329
344
|
navigationEligible: true,
|
|
330
345
|
capability: resourceDefinitions['resource-10'].capabilities.read,
|
|
@@ -333,7 +348,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
333
348
|
{
|
|
334
349
|
code: 'resource:resource-11:list',
|
|
335
350
|
kind: 'resource-list',
|
|
336
|
-
path: '/resource-11',
|
|
351
|
+
path: '/admin/resources/resource-11',
|
|
337
352
|
label: '活动报名',
|
|
338
353
|
navigationEligible: true,
|
|
339
354
|
capability: resourceDefinitions['resource-11'].capabilities.read,
|
|
@@ -342,7 +357,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
342
357
|
{
|
|
343
358
|
code: 'resource:resource-12:list',
|
|
344
359
|
kind: 'resource-list',
|
|
345
|
-
path: '/resource-12',
|
|
360
|
+
path: '/admin/resources/resource-12',
|
|
346
361
|
label: '资料下载',
|
|
347
362
|
navigationEligible: true,
|
|
348
363
|
capability: resourceDefinitions['resource-12'].capabilities.read,
|
|
@@ -358,7 +373,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
358
373
|
routeCode: 'content-operations',
|
|
359
374
|
},
|
|
360
375
|
]}
|
|
361
|
-
contributions={
|
|
376
|
+
contributions={applicationContributions}
|
|
362
377
|
resourceDefinitions={resourceDefinitions}
|
|
363
378
|
/>
|
|
364
379
|
</React.StrictMode>
|
|
@@ -221,7 +221,7 @@ test('renders the compiled desktop resource contract and shared workbench', asyn
|
|
|
221
221
|
const definition = definitions[code];
|
|
222
222
|
await page.goto(`${runtimeBase}/admin`);
|
|
223
223
|
await expect(page.locator('.oxa-list-surface')).toBeVisible();
|
|
224
|
-
await expect(page).toHaveURL(`${runtimeBase}/${code}`);
|
|
224
|
+
await expect(page).toHaveURL(`${runtimeBase}/admin/resources/${code}`);
|
|
225
225
|
await expect(page.locator('.oxa-sider').getByText(definition.name, { exact: true })).toBeVisible();
|
|
226
226
|
await expect(page.getByRole('button', { name: /导出$/ })).toBeVisible();
|
|
227
227
|
await expect(page.getByRole('button', { name: /列设置$/ })).toBeVisible();
|
|
@@ -263,7 +263,9 @@ test('opens the stable admin entry under the preproduction gateway mount', async
|
|
|
263
263
|
await mockPlatform(page, true, devRuntimeBase);
|
|
264
264
|
await page.goto(`${devRuntimeBase}/admin`);
|
|
265
265
|
await expect(page.locator('.oxa-list-surface')).toBeVisible();
|
|
266
|
-
await expect(page).toHaveURL(
|
|
266
|
+
await expect(page).toHaveURL(
|
|
267
|
+
`${devRuntimeBase}/admin/resources/${codes[0]}`,
|
|
268
|
+
);
|
|
267
269
|
});
|
|
268
270
|
|
|
269
271
|
test('uses the complete current-user role union when no Perspective is selected', async ({ page }) => {
|
|
@@ -339,7 +341,7 @@ test('preserves the left menu scroll position when switching resource pages', as
|
|
|
339
341
|
.locator('.oxa-sider')
|
|
340
342
|
.getByText('资料下载', { exact: true })
|
|
341
343
|
.evaluate(node => (node as HTMLElement).click());
|
|
342
|
-
await expect(page).toHaveURL('/resource-12');
|
|
344
|
+
await expect(page).toHaveURL('/admin/resources/resource-12');
|
|
343
345
|
await expect.poll(() => scroller.evaluate(node => node.scrollTop)).toBe(240);
|
|
344
346
|
});
|
|
345
347
|
|
|
@@ -357,6 +359,26 @@ test('mounts generated operation routes and typed actions in the unified Shell',
|
|
|
357
359
|
await expect(page.locator('.oxa-sider')).toBeVisible();
|
|
358
360
|
});
|
|
359
361
|
|
|
362
|
+
test('keeps the user portal and namespaced generated admin resource in one router', async ({ page }) => {
|
|
363
|
+
await mockPlatform(page, true, runtimeBase, true);
|
|
364
|
+
await page.goto('/resource-experience.e2e.html');
|
|
365
|
+
await page.evaluate(() => {
|
|
366
|
+
history.pushState({}, '', '/resource-01');
|
|
367
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
368
|
+
});
|
|
369
|
+
await expect(
|
|
370
|
+
page.getByRole('heading', { name: '用户端内容门户' }),
|
|
371
|
+
).toBeVisible();
|
|
372
|
+
await expect(page.locator('.oxa-sider')).toHaveCount(0);
|
|
373
|
+
|
|
374
|
+
await page.evaluate(() => {
|
|
375
|
+
history.pushState({}, '', '/admin/resources/resource-01');
|
|
376
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
377
|
+
});
|
|
378
|
+
await expect(page.locator('.oxa-list-surface')).toBeVisible();
|
|
379
|
+
await expect(page.locator('.oxa-sider')).toHaveCount(1);
|
|
380
|
+
});
|
|
381
|
+
|
|
360
382
|
test('fails closed on a direct operation route without its capability', async ({ page }) => {
|
|
361
383
|
await mockPlatform(page, false, runtimeBase, false);
|
|
362
384
|
await page.goto('/resource-experience.e2e.html');
|
|
@@ -374,7 +396,7 @@ test('keeps platform implementation guidance out of generated forms', async ({ p
|
|
|
374
396
|
await mockPlatform(page, true, runtimeBase, true);
|
|
375
397
|
await page.goto('/resource-experience.e2e.html');
|
|
376
398
|
await page.getByRole('button', { name: '新增内容栏目' }).click();
|
|
377
|
-
await expect(page).toHaveURL('/resource-01/new');
|
|
399
|
+
await expect(page).toHaveURL('/admin/resources/resource-01/new');
|
|
378
400
|
const formLabels = page.locator('.ant-form-item-label label');
|
|
379
401
|
await expect(formLabels.first()).toBeVisible();
|
|
380
402
|
const labels = await formLabels.allTextContents();
|
|
@@ -419,7 +441,9 @@ test('renders platform mobile controls from declared field widgets', async ({ pa
|
|
|
419
441
|
test.skip(!candidate, 'No mobile platform field is declared.');
|
|
420
442
|
await mockPlatform(page);
|
|
421
443
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
422
|
-
await page.goto(
|
|
444
|
+
await page.goto(
|
|
445
|
+
`${runtimeBase}/m/admin/resources/${candidate!.code}/new`,
|
|
446
|
+
);
|
|
423
447
|
await expect(page.locator('.oxa-mobile-form')).toBeVisible();
|
|
424
448
|
|
|
425
449
|
const widgets = new Set(Object.values(candidate!.definition.surface.fields).map(field => field.widget));
|
|
@@ -448,7 +472,7 @@ test('fails closed when the current user lacks the declared read capability', as
|
|
|
448
472
|
test.skip(!codes.length, 'No resource permission exists in an empty application.');
|
|
449
473
|
await mockPlatform(page, false);
|
|
450
474
|
const code = codes[0];
|
|
451
|
-
await page.goto(`${runtimeBase}/${code}`);
|
|
475
|
+
await page.goto(`${runtimeBase}/admin/resources/${code}`);
|
|
452
476
|
await expect(page.getByText(new RegExp(`当前平台用户无${definitions[code].name}页面权限`))).toBeVisible();
|
|
453
477
|
await expect(page.locator('.oxa-list-surface')).toHaveCount(0);
|
|
454
478
|
});
|