openxiangda-cli 2.0.0-alpha.85 → 2.0.0-alpha.89

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 (65) hide show
  1. package/package.json +4 -4
  2. package/template/AGENTS.md +4 -4
  3. package/template/README.md +4 -4
  4. package/template/apps/server/package.json +1 -1
  5. package/template/apps/server/src/app.module.ts +14 -1
  6. package/template/apps/server/test/smoke.test.ts +3 -1
  7. package/template/apps/web/data-api-live.e2e.html +15 -0
  8. package/template/apps/web/e2e/data-api-live-fixture.tsx +141 -0
  9. package/template/apps/web/e2e/data-api-live.spec.ts +138 -0
  10. package/template/apps/web/e2e/field-protocol-fixture.tsx +280 -0
  11. package/template/apps/web/e2e/field-protocol.spec.ts +100 -0
  12. package/template/apps/web/e2e/resources.spec.ts +258 -20
  13. package/template/apps/web/field-protocol.e2e.html +12 -0
  14. package/template/apps/web/package.json +1 -1
  15. package/template/apps/web/playwright.config.ts +3 -1
  16. package/template/apps/web/scripts/check.mjs +8 -5
  17. package/template/apps/web/scripts/verify-build.mjs +1 -1
  18. package/template/apps/web/src/AuthoritativeSelector.tsx +165 -132
  19. package/template/apps/web/src/FilePreviewPage.tsx +30 -9
  20. package/template/apps/web/src/RoleSubjectSelect.tsx +128 -0
  21. package/template/apps/web/src/Shell.tsx +93 -6
  22. package/template/apps/web/src/components/platform-fields/AddressField.tsx +338 -0
  23. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +25 -6
  24. package/template/apps/web/src/components/platform-fields/CascadeField.tsx +49 -0
  25. package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +148 -0
  26. package/template/apps/web/src/components/platform-fields/JsonField.tsx +77 -0
  27. package/template/apps/web/src/components/platform-fields/LocationField.tsx +164 -0
  28. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +19 -44
  29. package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +67 -0
  30. package/template/apps/web/src/components/platform-fields/RichTextField.tsx +196 -0
  31. package/template/apps/web/src/components/platform-fields/SignatureField.tsx +315 -0
  32. package/template/apps/web/src/components/platform-fields/SubtableField.tsx +553 -0
  33. package/template/apps/web/src/components/platform-fields/address-value.ts +80 -0
  34. package/template/apps/web/src/components/platform-fields/cascade-value.ts +66 -0
  35. package/template/apps/web/src/components/platform-fields/directory-value.ts +53 -0
  36. package/template/apps/web/src/components/platform-fields/field-form-codec.ts +98 -0
  37. package/template/apps/web/src/components/platform-fields/location-value.ts +87 -0
  38. package/template/apps/web/src/components/platform-fields/resource-query.ts +131 -0
  39. package/template/apps/web/src/components/platform-fields/rich-text-value.ts +59 -0
  40. package/template/apps/web/src/components/platform-fields/subtable-value.ts +187 -0
  41. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +194 -108
  42. package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +8 -1
  43. package/template/apps/web/src/components/resource/SurfaceFields.tsx +385 -79
  44. package/template/apps/web/src/components/resource/generated-resource-definition.ts +18 -0
  45. package/template/apps/web/src/components/resource/resource-import.ts +4 -4
  46. package/template/apps/web/src/data-provider.ts +59 -34
  47. package/template/apps/web/src/platform-client.ts +341 -136
  48. package/template/apps/web/src/runtime.tsx +156 -17
  49. package/template/apps/web/src/styles.css +264 -0
  50. package/template/apps/web/test/address-field.test.ts +66 -0
  51. package/template/apps/web/test/cascade-value.test.ts +68 -0
  52. package/template/apps/web/test/contracts.test.ts +29 -5
  53. package/template/apps/web/test/directory-value.test.ts +55 -0
  54. package/template/apps/web/test/field-bounds.test.ts +17 -0
  55. package/template/apps/web/test/field-form-codec.test.ts +85 -0
  56. package/template/apps/web/test/location-field.test.ts +83 -0
  57. package/template/apps/web/test/resource-import.test.ts +8 -7
  58. package/template/apps/web/test/resource-query.test.ts +144 -0
  59. package/template/apps/web/test/rich-json-field.test.ts +53 -0
  60. package/template/apps/web/test/signature-serial-field.test.ts +40 -0
  61. package/template/apps/web/test/subtable-value.test.ts +144 -0
  62. package/template/openxiangda.config.ts +1 -1
  63. package/template/package.json +3 -3
  64. package/template/packages/contracts/src/generated.ts +95 -0
  65. package/template/scripts/verify-template-budget.mjs +11 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda-cli",
3
- "version": "2.0.0-alpha.85",
3
+ "version": "2.0.0-alpha.89",
4
4
  "description": "Thin application-level CLI for OpenXiangda 2.0.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,9 +23,9 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "@oclif/core": "4.13.3",
26
- "openxiangda-contracts": "2.0.0-alpha.39",
27
- "openxiangda-devkit-core": "2.0.0-alpha.52",
28
- "openxiangda-mcp": "2.0.0-alpha.52"
26
+ "openxiangda-contracts": "2.0.0-alpha.43",
27
+ "openxiangda-devkit-core": "2.0.0-alpha.56",
28
+ "openxiangda-mcp": "2.0.0-alpha.56"
29
29
  },
30
30
  "devDependencies": {
31
31
  "tsx": "4.23.12",
@@ -7,13 +7,13 @@
7
7
  - 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.
8
8
  - Add NestJS only for a named business action that needs a cross-resource transaction, an invariant or an external side effect.
9
9
  - A NestJS backend declares only `enabled`, `isolation: 'shared' | 'dedicated'` and `resourceProfile: 'light' | 'standard'`. Never put raw Kubernetes resources, replicas, ports or environment maps in application metadata; the platform owns capacity and scaling.
10
- - Use the current logged-in user and the union of application roles. Do not select RoleSession, persist platform Token or implement a second authorization path.
10
+ - Use the current logged-in user and the union of that user's application roles. The generated shell automatically carries the platform's opaque authorization context for Data, Directory and App API requests. Business code must not read or persist that identifier, a platform Token, or authorization results, and must not implement a second identity path.
11
11
  - Fields inherit the resource read/create/update capabilities. Use field `access` only to tighten them; arrays are all-of and `false` is explicit deny. There is no `write` fallback.
12
- - Field storage types are exactly `string`, `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `uuid`, `json` and `file`; there is no `number` type.
12
+ - Field `type` is semantic, never a hand-authored database type. The supported catalog is exactly `text.short`, `text.long`, `text.rich`, `number.integer`, `number.decimal`, `boolean`, `date`, `time`, `datetime`, `date-range`, `datetime-range`, `option.single`, `option.multiple`, `cascade.single`, `cascade.multiple`, `user.single`, `user.multiple`, `department.single`, `department.multiple`, `resource-ref.single`, `resource-ref.multiple`, `file`, `image`, `signature`, `address`, `location`, `json`, `serial-number`, `uuid` and `subtable`. The compiler alone derives PostgreSQL columns, constraints and indexes.
13
13
  - Declare every custom operation capability in `authz.capabilities` with `kind: 'backend'`, then reference that same code from the operation and its allowed roles. Generated resource CRUD capabilities do not go in this catalog.
14
14
  - Visitor duplicate protection uses `createVisitorReservation({ duplicateMatch: { fieldCode: submittedValue }, ... })`. `duplicateMatch` is a non-empty value map, never a field-name array, and no mutable pre-read is allowed.
15
- - Desktop and mobile pages share values, validation and authorization, but use separate renderers. Members, departments, resources and attachments use platform-owned selectors and stable IDs.
16
- - A single directory user reference uses a `string` field; a single department or same-app resource reference uses `uuid`; every `multiple: true` reference uses `json`. Roles that consume directory-backed fields explicitly include `app:<app-code>:directory:read`.
15
+ - Desktop and mobile pages share values, validation and authorization, but use separate renderers. Options, members, departments and resource references store their complete display snapshots; attachments, images and signatures use platform-managed file references.
16
+ - `option.*`, `user.*`, `department.*` and `resource-ref.*` values never collapse to scalar IDs. Single values store one labeled snapshot and multiple values store snapshot arrays. `location` accepts only exact WGS84 coordinates captured by DingTalk or browser geolocation; it has no manual input or `manual` source. Roles that consume directory-backed fields explicitly include `app:<app-code>:directory:read`.
17
17
  - Derive role grants with `resourceCapabilityCodes(appCode, resourceCode)`. Declare current-user rows only with `currentUserDataPolicy(...)`; do not invent operators, values or alternate current-user spellings.
18
18
  - Do not add compatibility aliases, migration branches or silent fallbacks for an earlier 2.0 alpha contract. Replace an incorrect contract and regenerate the application.
19
19
  - Run `pnpm openxiangda check` after contract changes. Deploy with `pnpm openxiangda deploy`, inspect with `pnpm openxiangda status` and `pnpm openxiangda logs`, and use the platform rollback command rather than mutating K3s directly.
@@ -8,13 +8,13 @@ pnpm openxiangda dev
8
8
 
9
9
  当前应用已经把 CLI 和全部 2.0 包精确锁定在 `package.json` 与 lockfile 中。创建另一个应用时使用当前安装的 OpenXiangda 2.0 Skill 给出的精确 CLI 版本,不使用 `latest`、`alpha` 或全局命令。
10
10
 
11
- 在 `openxiangda.config.ts` 的 `data.resources` 中只声明资源 `code`、`name`、`fields` 和可选列表/布局/数据策略。每个字段一次写完 typelabelrequired、列表/筛选标记、reference/file 与 access;不要再写 `schema`、`surface`、资源 capabilities、资源 fieldPolicies 或 `platform/data` 模块。编译器据此生成标准后台列表、筛选、表单、详情、审计、文件上传、独立移动端页面和 AI Schema。
11
+ 在 `openxiangda.config.ts` 的 `data.resources` 中只声明资源 `code`、`name`、`fields` 和可选列表/布局/数据策略。每个字段一次写完语义 `type`、`label`、`required`、列表/筛选标记、`options`/`source`/`file``access`;不要再写 `schema`、`surface`、PostgreSQL 类型、资源 capabilities、资源 fieldPolicies 或 `platform/data` 模块。编译器据此生成标准后台列表、筛选、表单、详情、审计、文件上传、独立移动端页面、Data API 查询计划和 AI Schema。
12
12
 
13
- 成员、部门和资源关系分别使用 `reference: { kind: 'directory-user' }`、`directory-department`、`resource`,只保存稳定 ID,不复制目录数据。单选成员字段使用 `string`,单选部门和资源引用使用 `uuid`,多选引用统一使用 `json`。需要目录选择器的业务角色必须显式拥有 `app:<app-code>:directory:read` 能力。用 `resourceCapabilityCodes` 生成资源 CRUD 权限,用 `currentUserDataPolicy` 声明唯一的当前用户行规则。
13
+ 成员、部门分别声明 `user.single`/`user.multiple` `department.single`/`department.multiple`;同应用动态资源声明 `resource-ref.single`/`resource-ref.multiple` 及编译期固定的 `source`。这些字段都保存完整 `{label,value,...}` 显示快照,资源引用还保存 `resourceCode` 和声明的 `snapshot` 字段;查询和权限以稳定 `value` 为比较键。需要目录选择器的业务角色必须显式拥有 `app:<app-code>:directory:read` 能力。用 `resourceCapabilityCodes` 生成资源 CRUD 权限,用 `currentUserDataPolicy` 声明唯一的当前用户行规则。
14
14
 
15
- 所有 CRUD 位于 `apps/web/src/platform-client.ts` 的 Data API adapter;Nest 只在需要复杂事务或外部集成时增加 App API。选择器显式携带当前环境、使用同源 Cookie,浏览器不读取 Token,也不发送 RoleSession header。
15
+ 所有 CRUD 位于 `apps/web/src/platform-client.ts` 的 Data API adapter;Nest 只在需要复杂事务或外部集成时增加 App API。选择器显式携带当前环境、使用同源 Cookie。标准客户端在内存中恢复平台 RoleSession,并自动为 Data、Directory、文件和 App API 请求携带 header;业务代码不读取或持久化 TokenRoleSession 标识与授权结果。
16
16
 
17
- 前端读取平台 current-user 的角色/能力并集进行展示保护,服务端始终权威。字段默认继承资源 read/create/update;`access` 数组为 all-of,`false` 为明确拒绝,无权字段在提交前从 payload 删除。
17
+ 前端读取当前单一应用角色和 capability 做展示保护;多角色用户从当前应用个人菜单切换,切换后页面、数据和操作上下文整体替换,不做权限并集。服务端始终权威。字段默认继承资源 read/create/update;`access` 数组为 all-of,`false` 为明确拒绝,无权字段在提交前从 payload 删除。
18
18
 
19
19
  ```bash
20
20
  pnpm openxiangda check
@@ -15,7 +15,7 @@
15
15
  "@nestjs/common": "11.1.29",
16
16
  "@nestjs/core": "11.1.29",
17
17
  "@nestjs/platform-fastify": "11.1.29",
18
- "openxiangda-nest": "2.0.0-alpha.48",
18
+ "openxiangda-nest": "2.0.0-alpha.52",
19
19
  "reflect-metadata": "0.2.2",
20
20
  "rxjs": "7.8.2"
21
21
  },
@@ -1,5 +1,13 @@
1
1
  import { Module } from '@nestjs/common';
2
- import { OpenXiangdaModule } from 'openxiangda-nest';
2
+ import {
3
+ eventHandlerManifest,
4
+ eventSchemas,
5
+ eventSubscriptionCodes,
6
+ } from '@app/contracts';
7
+ import {
8
+ OpenXiangdaModule,
9
+ eventSigningSecretsFromEnvironment,
10
+ } from 'openxiangda-nest';
3
11
 
4
12
  @Module({
5
13
  imports: [
@@ -24,6 +32,11 @@ import { OpenXiangdaModule } from 'openxiangda-nest';
24
32
  version: process.env.OPENXIANGDA_APP_VERSION || '0.1.0-dev',
25
33
  connectedDevelopment:
26
34
  process.env.OPENXIANGDA_CONNECTED_DEV === 'true',
35
+ eventHandlerManifest,
36
+ eventSchemas,
37
+ eventSigningSecrets: eventSigningSecretsFromEnvironment(
38
+ eventSubscriptionCodes
39
+ ),
27
40
  }),
28
41
  ],
29
42
  })
@@ -6,9 +6,11 @@ test('the clean server template only owns platform bootstrap', () => {
6
6
  const source = readFileSync(new URL('../src/app.module.ts', import.meta.url), 'utf8');
7
7
  const main = readFileSync(new URL('../src/main.ts', import.meta.url), 'utf8');
8
8
  assert.match(source, /OpenXiangdaModule\.forRoot/);
9
+ assert.match(source, /eventHandlerManifest/);
10
+ assert.match(source, /eventSigningSecretsFromEnvironment/);
9
11
  assert.match(main, /bootstrapOpenXiangdaApplication\(AppModule\)/);
10
12
  assert.doesNotMatch(main, /NestFactory|FastifyAdapter/);
11
- const legacyMarkers = ['oauth' + 'Client', 'event' + 'Signing', 'Workflow', 'instrument' + '-context'];
13
+ const legacyMarkers = ['oauth' + 'Client', 'Workflow', 'instrument' + '-context'];
12
14
  assert.doesNotMatch(source, new RegExp(legacyMarkers.join('|')));
13
15
  assert.doesNotMatch(source, /createData|updateData|deleteData|queryData/);
14
16
  });
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="openxiangda-runtime-base" content="/" />
7
+ <meta name="openxiangda-app-code" content="native-projection-app" />
8
+ <meta name="openxiangda-environment" content="production" />
9
+ <title>OpenXiangda Standard Data API Live Acceptance</title>
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/e2e/data-api-live-fixture.tsx"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,141 @@
1
+ import { Refine } from '@refinedev/core';
2
+ import { App as AntdApp } from 'antd';
3
+ import React from 'react';
4
+ import ReactDOM from 'react-dom/client';
5
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
6
+ import { createApplicationProvider } from '../src/data-provider';
7
+ import { GeneratedResourcePage } from '../src/components/resource/GeneratedResourceCrud';
8
+ import type { GeneratedResourceDefinition } from '../src/components/resource/generated-resource-definition';
9
+ import { AppearanceProvider } from '../src/appearance';
10
+ import { RuntimeBoundary } from '../src/runtime';
11
+ import '../src/styles.css';
12
+
13
+ const code = 'field-records';
14
+ const capabilityBase = `app:native-projection-app:data:${code}`;
15
+ const capabilities = {
16
+ read: `${capabilityBase}:read`,
17
+ create: `${capabilityBase}:create`,
18
+ update: `${capabilityBase}:update`,
19
+ delete: `${capabilityBase}:delete`,
20
+ };
21
+
22
+ function fieldCapabilities() {
23
+ return {
24
+ readCapabilities: [capabilities.read],
25
+ createCapabilities: [capabilities.create],
26
+ updateCapabilities: [capabilities.update],
27
+ };
28
+ }
29
+
30
+ export const liveDefinition: GeneratedResourceDefinition = {
31
+ code,
32
+ name: '字段协议记录',
33
+ capabilities,
34
+ surface: {
35
+ generated: true,
36
+ fields: {
37
+ text_short: {
38
+ label: '标题',
39
+ type: 'text.short',
40
+ widget: 'text',
41
+ requiredHint: true,
42
+ maxLength: 120,
43
+ list: true,
44
+ searchable: true,
45
+ sortable: true,
46
+ ...fieldCapabilities(),
47
+ },
48
+ option_single: {
49
+ label: '单选快照',
50
+ type: 'option.single',
51
+ widget: 'radio',
52
+ options: [
53
+ { label: 'Open', value: 'open' },
54
+ { label: 'Closed', value: 'closed' },
55
+ ],
56
+ list: true,
57
+ ...fieldCapabilities(),
58
+ },
59
+ option_multiple: {
60
+ label: '多选快照',
61
+ type: 'option.multiple',
62
+ widget: 'checkbox',
63
+ options: [
64
+ { label: 'Open', value: 'open' },
65
+ { label: 'Urgent', value: 'urgent' },
66
+ ],
67
+ list: true,
68
+ ...fieldCapabilities(),
69
+ },
70
+ location_value: {
71
+ label: '精确定位',
72
+ type: 'location',
73
+ widget: 'location',
74
+ list: true,
75
+ ...fieldCapabilities(),
76
+ },
77
+ },
78
+ list: {
79
+ defaultPageSize: 20,
80
+ searchableFields: ['text_short'],
81
+ filterFields: ['option_single'],
82
+ defaultSort: { field: 'text_short', order: 'asc' },
83
+ },
84
+ form: { layout: 'sections' },
85
+ detail: { layout: 'sections' },
86
+ mobile: { enabled: true },
87
+ },
88
+ };
89
+
90
+ const definitions = { [code]: liveDefinition };
91
+ const provider = createApplicationProvider(definitions);
92
+ const initialPath =
93
+ new URLSearchParams(window.location.search).get('initial') || `/${code}`;
94
+
95
+ function resourcePage(mode: 'list' | 'create' | 'edit' | 'detail') {
96
+ return (
97
+ <GeneratedResourcePage
98
+ definition={liveDefinition}
99
+ mode={mode}
100
+ resourceCode={code}
101
+ />
102
+ );
103
+ }
104
+
105
+ ReactDOM.createRoot(document.getElementById('root')!).render(
106
+ <React.StrictMode>
107
+ <AppearanceProvider>
108
+ <AntdApp>
109
+ <RuntimeBoundary>
110
+ <MemoryRouter initialEntries={[initialPath]}>
111
+ <Refine
112
+ dataProvider={provider}
113
+ resources={[
114
+ {
115
+ name: code,
116
+ list: `/${code}`,
117
+ create: `/${code}/new`,
118
+ edit: `/${code}/:id/edit`,
119
+ show: `/${code}/:id`,
120
+ },
121
+ ]}
122
+ >
123
+ <Routes>
124
+ <Route path={`/${code}`} element={resourcePage('list')} />
125
+ <Route path={`/${code}/new`} element={resourcePage('create')} />
126
+ <Route
127
+ path={`/${code}/:id`}
128
+ element={resourcePage('detail')}
129
+ />
130
+ <Route
131
+ path={`/${code}/:id/edit`}
132
+ element={resourcePage('edit')}
133
+ />
134
+ </Routes>
135
+ </Refine>
136
+ </MemoryRouter>
137
+ </RuntimeBoundary>
138
+ </AntdApp>
139
+ </AppearanceProvider>
140
+ </React.StrictMode>
141
+ );
@@ -0,0 +1,138 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ const liveEnabled = process.env.OPENXIANGDA_LIVE_DATA_API === '1';
4
+ const title = 'UI live field record';
5
+
6
+ test.describe('@live-data-api standard resource UI', () => {
7
+ test.setTimeout(90_000);
8
+ test.skip(
9
+ !liveEnabled,
10
+ 'Requires the disposable PostgreSQL Native Data API acceptance bridge.'
11
+ );
12
+
13
+ test.beforeEach(async ({ context }) => {
14
+ await context.addCookies([
15
+ {
16
+ name: 'openxiangda-field-ui-role',
17
+ value: 'super',
18
+ domain: '127.0.0.1',
19
+ path: '/',
20
+ },
21
+ ]);
22
+ });
23
+
24
+ test('runs create, list, query, get, update, audit and delete through the standard page', async ({
25
+ context,
26
+ page,
27
+ }) => {
28
+ await context.grantPermissions(['geolocation']);
29
+ await context.setGeolocation({
30
+ longitude: 120.123456,
31
+ latitude: 30.234567,
32
+ accuracy: 4,
33
+ });
34
+ await page.goto('/data-api-live.e2e.html?initial=/field-records');
35
+ await expect(page.locator('.oxa-list-surface')).toBeVisible();
36
+
37
+ await page.getByRole('button', { name: '新增字段协议记录' }).click();
38
+ await expect(page.getByRole('heading', { name: '新增字段协议记录' })).toBeVisible();
39
+ await page.getByLabel('标题').fill(title);
40
+ await page.getByRole('radio', { name: 'Open' }).check();
41
+ const openCheckbox = page.getByRole('checkbox', { name: 'Open' });
42
+ const urgentCheckbox = page.getByRole('checkbox', { name: 'Urgent' });
43
+ await openCheckbox.click();
44
+ await urgentCheckbox.click();
45
+ await expect(openCheckbox).toBeChecked();
46
+ await expect(urgentCheckbox).toBeChecked();
47
+ await page.getByRole('button', { name: '浏览器定位' }).click();
48
+ await expect(page.getByText('120.123456, 30.234567')).toBeVisible();
49
+ await page.getByRole('button', { name: /^保\s*存$/ }).click();
50
+
51
+ await expect(page.locator('.oxa-list-surface')).toBeVisible();
52
+ await page.locator('.oxa-search-panel .ant-select').first().click();
53
+ await page.getByText('标题', { exact: true }).last().click();
54
+ await page.getByPlaceholder('关键词').fill(title);
55
+ await page.getByRole('button', { name: /查询/ }).click();
56
+ const createdRow = page.getByRole('row').filter({ hasText: title });
57
+ await expect(createdRow).toHaveCount(1);
58
+ await expect(createdRow).toContainText('Open');
59
+ await expect(createdRow).toContainText('Urgent');
60
+ await expect(createdRow).toContainText('120.123456, 30.234567');
61
+
62
+ await createdRow.getByRole('button', { name: '查看' }).click();
63
+ await expect(page.getByRole('heading', { name: '字段协议记录详情' })).toBeVisible();
64
+ await expect(page.getByText(title, { exact: true })).toBeVisible();
65
+ await expect(page.getByText(/浏览器定位/)).toBeVisible();
66
+ await page.locator('.oxa-audit-collapse .ant-collapse-header').click();
67
+ await expect(page.getByText('创建', { exact: true })).toBeVisible();
68
+ await page.screenshot({
69
+ fullPage: true,
70
+ path: 'test-results/data-api-live-detail.png',
71
+ });
72
+
73
+ await page.getByRole('button', { name: /编辑/ }).click();
74
+ await page.getByLabel('标题').fill(`${title} updated`);
75
+ await page.getByRole('radio', { name: 'Closed' }).check();
76
+ await page.getByRole('checkbox', { name: 'Open' }).click();
77
+ await expect(page.getByRole('checkbox', { name: 'Open' })).not.toBeChecked();
78
+ await page.getByRole('button', { name: /^保存修改$/ }).click();
79
+ await expect(page.getByText(`${title} updated`, { exact: true })).toBeVisible();
80
+ await expect(page.getByText('Closed', { exact: true })).toBeVisible();
81
+
82
+ await page.getByRole('button', { name: '返回列表' }).click();
83
+ const updatedRow = page
84
+ .getByRole('row')
85
+ .filter({ hasText: `${title} updated` });
86
+ await expect(updatedRow).toHaveCount(1);
87
+ await updatedRow.getByRole('button', { name: '删除' }).click();
88
+ await page.getByRole('button', { name: /^确\s*定$/ }).click();
89
+ await expect(updatedRow).toHaveCount(0);
90
+ });
91
+
92
+ test('applies PostgreSQL row scope to the standard list and query UI', async ({
93
+ context,
94
+ page,
95
+ }) => {
96
+ await context.clearCookies();
97
+ await context.addCookies([
98
+ {
99
+ name: 'openxiangda-field-ui-role',
100
+ value: 'restricted',
101
+ domain: '127.0.0.1',
102
+ path: '/',
103
+ },
104
+ ]);
105
+ await page.goto('/data-api-live.e2e.html?initial=/field-records');
106
+ await expect(page.locator('.oxa-list-surface')).toBeVisible();
107
+ await page.locator('.oxa-search-panel .ant-select').first().click();
108
+ await page.getByText('标题', { exact: true }).last().click();
109
+ await page.getByPlaceholder('关键词').fill('UI RLS');
110
+ await page.getByRole('button', { name: /查询/ }).click();
111
+ await expect(page.getByText('UI RLS visible', { exact: true })).toBeVisible();
112
+ await expect(page.getByText('UI RLS hidden', { exact: true })).toHaveCount(0);
113
+ await page.screenshot({
114
+ fullPage: true,
115
+ path: 'test-results/data-api-live-rls.png',
116
+ });
117
+ });
118
+
119
+ test('fails closed in the standard page when the role has no read capability', async ({
120
+ context,
121
+ page,
122
+ }) => {
123
+ await context.clearCookies();
124
+ await context.addCookies([
125
+ {
126
+ name: 'openxiangda-field-ui-role',
127
+ value: 'denied',
128
+ domain: '127.0.0.1',
129
+ path: '/',
130
+ },
131
+ ]);
132
+ await page.goto('/data-api-live.e2e.html?initial=/field-records');
133
+ await expect(
134
+ page.getByText('当前平台用户无字段协议记录页面权限', { exact: true })
135
+ ).toBeVisible();
136
+ await expect(page.locator('.oxa-list-surface')).toHaveCount(0);
137
+ });
138
+ });