openxiangda-cli 2.0.0-alpha.177 → 2.0.0-alpha.178

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda-cli",
3
- "version": "2.0.0-alpha.177",
3
+ "version": "2.0.0-alpha.178",
4
4
  "description": "Thin application-level CLI for OpenXiangda 2.0.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,7 +17,7 @@ const correctionFields = {
17
17
  async function workflowEntryPlatform(page: Page) {
18
18
  await mockPlatform(page, false, undefined, true);
19
19
  const state = {
20
- admin: true,
20
+ admin: true, instanceStatus: 'approved',
21
21
  record: { id: 'record-1', revision: 8, title: '原业务申请', amount: 28600, decision: '已同意', internalNote: '不得显示', systemValue: '内部标识', attachments: [], richText: '<p>原说明</p>' },
22
22
  complexFields: false, uploads: [] as any[], childQueries: [] as any[], auditReads: 0,
23
23
  children: [
@@ -61,7 +61,7 @@ async function workflowEntryPlatform(page: Page) {
61
61
  if (path.endsWith(`/workflow/records/${resourceCode}/${state.record.id}/detail`)) {
62
62
  const childFields = { name: { label: '品名', type: 'text.short', widget: 'text', ...fieldCapabilities }, quantity: { label: '数量', type: 'number.integer', widget: 'number', ...fieldCapabilities }, detailNote: { label: '仅详情说明', type: 'text.short', widget: 'text', ...fieldCapabilities } };
63
63
  const fields = { ...correctionFields, decision: { label: '审批结论', type: 'text.short', widget: 'readonly', ...fieldCapabilities }, ...(state.complexFields ? { items: { label: '采购明细', type: 'subtable', widget: 'subtable', subtable: { resourceCode: 'purchase-lines', foreignKey: 'purchaseId', orderField: 'sortOrder', maxRows: 20 }, ...fieldCapabilities } } : {}) };
64
- const instance = { id: instanceId, appCode, environmentKey: 'preproduction', status: 'approved', outcome: 'approved', startedAt: '2026-09-06T01:00:00Z', completedAt: '2026-09-06T02:38:00Z', dataRef: { resourceCode, recordId: state.record.id } };
64
+ const instance = { id: instanceId, appCode, environmentKey: 'preproduction', status: state.instanceStatus, outcome: state.instanceStatus === 'approved' ? 'approved' : null, startedAt: '2026-09-06T01:00:00Z', completedAt: state.instanceStatus === 'approved' ? '2026-09-06T02:38:00Z' : null, dataRef: { resourceCode, recordId: state.record.id } };
65
65
  const surface = { schemaVersion: 'openxiangda.workflow-surface/v2', protocolVersion: 'workflow_surface_v2', engineVersion: '2.0', surfaceRevision: 'a'.repeat(64), instanceSequence: 4, instance, task: null, operations: [],
66
66
  detailNavigation: { desktopPath: `/admin/purchases/${state.record.id}`, mobilePath: `/m/admin/purchases/${state.record.id}`, custom: false },
67
67
  presentation: { summary: { title: '采购申请', initiatorDisplayName: '陈晨', departmentDisplayName: '办公室', submittedAt: instance.startedAt }, businessDetail: {
@@ -538,3 +538,19 @@ test('mobile child editing selects form fields while readonly details retain the
538
538
  { operation: 'update', resourceCode: 'purchase-lines', id: 'line-1', expectedRevision: 3, data: { name: '移动明细修改' } },
539
539
  ]);
540
540
  });
541
+
542
+ for (const mobile of [false, true]) {
543
+ test(`${mobile ? 'mobile' : 'desktop'} workflow editing is only offered after completion`, async ({ page }) => {
544
+ const state = await workflowEntryPlatform(page);
545
+ await page.setViewportSize(mobile ? { width: 390, height: 600 } : { width: 1440, height: 900 });
546
+ for (const status of ['running', 'returned', 'approved']) {
547
+ state.instanceStatus = status;
548
+ await page.goto(`/workflow-entry.e2e.html?mode=${mobile ? 'mobile' : 'desktop'}-detail`);
549
+ await expect(page.locator('.oxa-record-detail-header')).toBeVisible();
550
+ await expect(page.locator('.oxa-record-detail-header').getByRole('button', { name: '编辑', exact: true })).toHaveCount(status === 'approved' ? 1 : 0);
551
+ await expect(page.locator('.oxa-record-detail-footer').getByRole('button', { name: /关\s*闭/ })).toHaveCount(0);
552
+ await expect(page.locator('.oxa-workflow-current-node')).toHaveCount(0);
553
+ }
554
+ expect(state.correctionReads).toBe(0);
555
+ });
556
+ }
@@ -816,46 +816,88 @@ test('renders a paged desktop work center from the current-user role union', asy
816
816
  }
817
817
  });
818
818
 
819
- test('a current task handler can send CC with the Surface instance command and multiple directory users', async ({ page }) => {
819
+ for (const mobile of [false, true]) {
820
+ for (const multiple of [false, true]) {
821
+ test(`${mobile ? 'mobile' : 'desktop'} ${multiple ? 'CC' : 'transfer'} uses the standard member picker`, async ({ page }) => {
822
+ await page.setViewportSize(mobile ? { width: 320, height: 480 } : { width: 1440, height: 900 });
820
823
  await mockWorkflow(page);
821
824
  const current = detail();
825
+ const key = multiple ? 'cc' : 'transfer';
826
+ const label = multiple ? '抄送' : '转交';
827
+ const field = multiple ? 'userIds' : 'userId';
822
828
  const cc = {
823
- key: 'cc', kind: 'workflow_command', label: '抄送', group: 'instance_control', audience: 'participant',
829
+ key, kind: 'workflow_command', label, group: 'instance_control', audience: 'participant',
824
830
  placement: 'overflow', emphasis: 'neutral', tone: 'neutral', visible: true, enabled: true,
825
- inputSchema: { type: 'object', additionalProperties: false, required: ['userIds'], properties: {
826
- userIds: { type: 'array', title: '抄送给', minItems: 1, maxItems: 20, uniqueItems: true, items: { type: 'string' } },
831
+ inputSchema: { type: 'object', additionalProperties: false, required: [field], properties: {
832
+ [field]: multiple ? { type: 'array', title: '抄送给', minItems: 1, maxItems: 20, uniqueItems: true, items: { type: 'string' } } : { type: 'string', title: '转交给' },
827
833
  comment: { type: 'string', title: '留言', maxLength: 4000 },
828
834
  } },
829
- uiSchema: { presentation: 'dialog', confirmText: '确认抄送', properties: { userIds: { component: 'user_select', mode: 'multiple' } } },
830
- execute: { method: 'POST', href: `/openxiangda-api/v2/applications/${appCode}/workflow/instances/${instanceId}/commands/cc`, idempotencyRequired: true },
835
+ uiSchema: { presentation: 'dialog', confirmText: `确认${label}`, properties: { [field]: { component: 'user_select', mode: multiple ? 'multiple' : 'single' } } },
836
+ execute: { method: 'POST', href: `/openxiangda-api/v2/applications/${appCode}/workflow/${multiple ? `instances/${instanceId}` : `tasks/${taskId}`}/commands/${key}`, idempotencyRequired: true },
831
837
  refresh: ['surface', 'timeline', 'work_center'],
832
838
  };
833
- const augmented = { ...current, operations: [...current.operations, cc], surface: { ...current.surface, operations: [...current.surface.operations, cc] } };
839
+ const augmented = { ...current, operations: [...current.operations.filter(item => item.key !== key), cc], surface: { ...current.surface, operations: [...current.surface.operations.filter(item => item.key !== key), cc] } };
834
840
  await page.route('**/workflow/tasks/*/detail', route => route.fulfill({ json: envelope(augmented) }));
835
- await page.route('**/directory/users?**', route => route.fulfill({ json: envelope({ items: [{ id: 'cc-user-1', label: '陈老师' }, { id: 'cc-user-2', label: '李老师' }], nextCursor: null }) }));
841
+ await page.route('**/directory/**', route => {
842
+ const tree = new URL(route.request().url()).pathname.endsWith('/tree');
843
+ const items = tree ? [] : ['陈老师', '李老师'].map((label, index) => ({ kind: 'user', id: `cc-user-${index + 1}`, label, selectable: true, snapshot: { value: `cc-user-${index + 1}`, label } }));
844
+ return route.fulfill({ json: envelope({ schemaVersion: 'openxiangda.directory-entry-page/v2', kind: tree ? 'department' : 'user', items, nextCursor: null }) });
845
+ });
836
846
  let writes = 0;
837
- await page.route('**/workflow/instances/*/commands/cc', async route => {
847
+ await page.route(`**/commands/${key}`, async route => {
838
848
  writes += 1;
839
- expect(route.request().postDataJSON()).toMatchObject({ commandToken: current.surface.commandToken, input: { userIds: ['cc-user-1', 'cc-user-2'], comment: '请查阅' } });
849
+ expect(route.request().postDataJSON()).toMatchObject({ commandToken: current.surface.commandToken, input: { [field]: multiple ? ['cc-user-1', 'cc-user-2'] : 'cc-user-1', comment: '请查阅' } });
840
850
  expect(route.request().postDataJSON().idempotencyKey).toBeTruthy();
841
851
  expect(route.request().headers()['x-openxiangda-csrf-token']).toBe('workflow-csrf-1');
842
852
  await route.fulfill({ json: envelope({ status: 'completed', advanced: false, instanceId }) });
843
853
  });
844
- await page.goto(workflowFixtureUrl(`/tasks/${taskId}`));
854
+ await page.goto(workflowFixtureUrl(`${mobile ? '/m' : ''}/tasks/${taskId}`));
855
+ const footer = page.locator('.oxa-record-detail-footer');
856
+ await expect(footer).toBeVisible();
857
+ await expect(footer.getByRole('button', { name: /关\s*闭|编\s*辑/ })).toHaveCount(0);
858
+ if (mobile) {
859
+ const boxes = await footer.locator('.oxa-workflow-actions > button').evaluateAll(elements => elements.map(element => element.getBoundingClientRect().y));
860
+ expect(boxes).toHaveLength(3);
861
+ expect(Math.max(...boxes) - Math.min(...boxes)).toBeLessThan(2);
862
+ }
845
863
  await page.getByRole('button', { name: '更多操作' }).click();
846
- await page.getByRole('button', { name: '抄送', exact: true }).click();
847
- await page.getByRole('button', { name: '确认抄送' }).click();
864
+ await page.getByRole('button', { name: label, exact: true }).click();
865
+ await page.getByRole('button', { name: `确认${label}` }).click();
866
+ expect(writes).toBe(0);
867
+ await expect(page.getByRole('combobox')).toHaveCount(0);
868
+ if (mobile) {
869
+ const bounds = await page.locator('.oxa-workflow-operation-drawer .ant-drawer-content-wrapper').boundingBox();
870
+ expect(bounds!.y).toBeGreaterThanOrEqual(0);
871
+ expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(481);
872
+ await expect(page.getByRole('button', { name: `确认${label}` })).toBeInViewport();
873
+ }
874
+ const trigger = page.locator(mobile ? '.oxa-mobile-choice-field button' : '.oxa-directory-trigger').first();
875
+ await trigger.click();
876
+ if (mobile) {
877
+ await expect(page.locator('.oxa-mobile-selection-sheet')).toBeInViewport();
878
+ await expect(page.getByRole('button', { name: '关闭', exact: true }).last()).toBeInViewport();
879
+ await expect(page.getByRole('button', { name: '确定', exact: true })).toBeInViewport();
880
+ }
881
+ await page.getByPlaceholder(mobile ? /搜索/ : '搜索成员姓名或工号').fill('老师');
882
+ await page.getByText('陈老师', { exact: true }).click();
883
+ await page.getByRole('button', { name: mobile ? '关闭' : /取\s*消/, exact: true }).last().click();
884
+ await page.getByRole('button', { name: `确认${label}` }).click();
848
885
  expect(writes).toBe(0);
849
- const select = page.getByRole('dialog').getByRole('combobox');
850
- await select.fill('老师');
851
- await page.getByTitle('陈老师', { exact: true }).click();
852
- await page.getByTitle('李老师', { exact: true }).click();
886
+ await trigger.click();
887
+ await page.getByPlaceholder(mobile ? /搜索/ : '搜索成员姓名或工号').fill('老师');
888
+ await page.getByText('陈老师', { exact: true }).click();
889
+ if (multiple) await page.getByText('李老师', { exact: true }).click();
890
+ await page.getByRole('button', { name: mobile ? `确定(${multiple ? 2 : 1})` : /确\s*定/, exact: true }).last().click();
853
891
  await page.getByLabel('留言').fill('请查阅');
854
- await page.getByRole('button', { name: '确认抄送' }).click();
892
+ await page.getByRole('button', { name: `确认${label}` }).click();
855
893
  await expect.poll(() => writes).toBe(1);
856
894
  await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
857
895
  });
858
896
 
897
+ }
898
+
899
+ }
900
+
859
901
  test('renders the generic desktop and mobile application todo center without a second router', async ({
860
902
  page,
861
903
  }) => {
@@ -14,7 +14,7 @@
14
14
  "@ant-design/icons": "6.2.3",
15
15
  "@refinedev/core": "5.0.12",
16
16
  "antd": "6.4.3",
17
- "openxiangda": "2.0.0-alpha.104",
17
+ "openxiangda": "2.0.0-alpha.105",
18
18
  "react": "19.2.8",
19
19
  "react-dom": "19.2.8",
20
20
  "react-router-dom": "7.8.2"
@@ -22,7 +22,7 @@
22
22
  "build": "pnpm --recursive build"
23
23
  },
24
24
  "devDependencies": {
25
- "openxiangda": "2.0.0-alpha.104",
25
+ "openxiangda": "2.0.0-alpha.105",
26
26
  "typescript": "5.9.3"
27
27
  }
28
28
  }