openxiangda-cli 2.0.0-alpha.133 → 2.0.0-alpha.135

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.133",
3
+ "version": "2.0.0-alpha.135",
4
4
  "description": "Thin application-level CLI for OpenXiangda 2.0.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,10 +21,10 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "@oclif/core": "4.13.3",
24
- "openxiangda-contracts": "2.0.0-alpha.66",
25
- "openxiangda-devkit-core": "2.0.0-alpha.86",
26
- "openxiangda-mcp": "2.0.0-alpha.86",
27
- "openxiangda-skill-kit": "2.0.0-alpha.107"
24
+ "openxiangda-contracts": "2.0.0-alpha.67",
25
+ "openxiangda-devkit-core": "2.0.0-alpha.87",
26
+ "openxiangda-mcp": "2.0.0-alpha.87",
27
+ "openxiangda-skill-kit": "2.0.0-alpha.108"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsx": "4.23.12",
@@ -5,6 +5,7 @@
5
5
  - Use Vite, React Router, Refine Core and Ant Design. Do not add Umi, ProComponents or another admin shell.
6
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
  - Application login is optional `frontend.authentication`: existing platform users only, registration rejected, exact desktop `/login` and mobile `/m/login`. Bind generated `authenticationSurfaces` to separate PC/mobile renderers through `defineApplicationContributions`; renderers own only brand visuals and call `ApplicationLoginSurfaceProps`. The platform alone owns passwords, providers, OAuth state/callbacks, secure cookies, current identity and authorization. Never put login in protected `appRoutes`, call a v1 auth route, store tokens, create users/roles, or add another Router/identity provider. Keep QA in generated `platformAuthManifest`, outside the protected route denominator.
8
+ - External users without platform accounts use only an exact static `surface: 'user'` route declared through `frontend.publicAccess`. Declare the one resource, bounded field sets, required operations, draft limits and named duplicate validations; consume only generated `anonymousPublicAccess` and `createAnonymousPublicClient`. `own.list`/`own.read` mean records submitted by the same platform-issued HttpOnly browser credential, not a verified natural person, and another browser or cleared cookie intentionally loses access. Never create a guest role/user, call the general Native Data API, expose an anonymous upload path, store identity locally or derive ownership from IP, user-agent or fingerprint.
8
9
  - 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.
9
10
  - 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`.
10
11
  - 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.
@@ -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": "2.0.0-alpha.48",
18
+ "openxiangda": "2.0.0-alpha.50",
19
19
  "reflect-metadata": "0.2.2",
20
20
  "rxjs": "7.8.2"
21
21
  },
@@ -245,11 +245,32 @@ const adminNavigation = [
245
245
  },
246
246
  ] as const satisfies AdminNavigationInput;
247
247
 
248
- const initialPath =
249
- new URLSearchParams(window.location.search).get('initial') ||
250
- '/admin/work-center';
248
+ const fixtureParams = new URLSearchParams(window.location.search);
249
+ const initialPath = fixtureParams.get('initial') || '/admin/work-center';
250
+ const runtimeBase = fixtureParams.get('base')?.trim();
251
+ if (runtimeBase) {
252
+ for (const [name, content] of [
253
+ ['openxiangda-runtime-base', runtimeBase],
254
+ ['openxiangda-app-code', appCode],
255
+ ['openxiangda-environment', 'preproduction'],
256
+ ] as const) {
257
+ const meta =
258
+ document.querySelector<HTMLMetaElement>(`meta[name="${name}"]`) ||
259
+ document.createElement('meta');
260
+ meta.name = name;
261
+ meta.content = content;
262
+ if (!meta.parentElement) document.head.appendChild(meta);
263
+ }
264
+ }
251
265
  if (location.pathname.endsWith('/workflow-experience.e2e.html')) {
252
- history.replaceState({}, '', initialPath);
266
+ const stateToken = fixtureParams.get('state');
267
+ history.replaceState(
268
+ stateToken
269
+ ? { usr: { routeNegotiationState: stateToken }, key: 'fixture-state' }
270
+ : {},
271
+ '',
272
+ initialPath,
273
+ );
253
274
  }
254
275
 
255
276
  ReactDOM.createRoot(document.getElementById('root')!).render(
@@ -22,6 +22,16 @@ function detailNavigation(custom = false) {
22
22
  };
23
23
  }
24
24
 
25
+ function workflowFixtureUrl(
26
+ initialPath: string,
27
+ options: { base?: string; state?: string } = {},
28
+ ) {
29
+ const params = new URLSearchParams({ initial: initialPath });
30
+ if (options.base) params.set('base', options.base);
31
+ if (options.state) params.set('state', options.state);
32
+ return `/workflow-experience.e2e.html?${params.toString()}`;
33
+ }
34
+
25
35
  function task(completed = false, customDetail = false) {
26
36
  return {
27
37
  schemaVersion: 'openxiangda.workflow-task/v2',
@@ -673,3 +683,108 @@ test('shows an explicit authorization error instead of an empty work center', as
673
683
  });
674
684
  await expect(page.getByRole('link', { name: '采购申请审批' })).toHaveCount(0);
675
685
  });
686
+
687
+ test('negotiates direct standard routes after BrowserRouter listener setup', async ({
688
+ page,
689
+ }) => {
690
+ await mockWorkflow(page);
691
+ await page.setViewportSize({ width: 360, height: 844 });
692
+ await page.goto(
693
+ workflowFixtureUrl('/admin/todos?view=pending#unread', {
694
+ state: 'deep-link',
695
+ }),
696
+ );
697
+
698
+ // The initial desktop URL is replaced by the mobile pair and the rendered
699
+ // surface follows the Router update. The fixture is StrictMode-mounted.
700
+ await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
701
+ await expect(page.locator('.oxa-todo-center-desktop')).toHaveCount(0);
702
+ await expect(page).toHaveURL(/\/m\/todos\?view=pending#unread$/);
703
+ const historyLength = await page.evaluate(() => window.history.length);
704
+ const negotiatedUrl = page.url();
705
+ await expect
706
+ .poll(() => page.url(), { timeout: 1_000 })
707
+ .toBe(negotiatedUrl);
708
+ expect(await page.evaluate(() => window.history.state?.usr)).toEqual({
709
+ routeNegotiationState: 'deep-link',
710
+ });
711
+
712
+ // 768px remains mobile; crossing to 769px switches the same manifest pair
713
+ // back to desktop without adding a browser-history entry.
714
+ await page.setViewportSize({ width: 768, height: 844 });
715
+ await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
716
+ await page.setViewportSize({ width: 769, height: 844 });
717
+ await expect(page.locator('.oxa-todo-center-desktop')).toBeVisible();
718
+ await expect(page).toHaveURL(/\/admin\/todos\?view=pending#unread$/);
719
+ expect(await page.evaluate(() => window.history.length)).toBe(historyLength);
720
+ expect(await page.evaluate(() => window.history.state?.usr)).toEqual({
721
+ routeNegotiationState: 'deep-link',
722
+ });
723
+ await page.goBack();
724
+ await expect(page).not.toHaveURL(/\/admin\/todos\?view=pending#unread$/);
725
+
726
+ // Direct task and instance URLs carry their dynamic parameter and deep-link
727
+ // context to the independent mobile renderer at the narrow width.
728
+ await page.setViewportSize({ width: 360, height: 844 });
729
+ await page.goto(
730
+ workflowFixtureUrl(`/admin/tasks/${taskId}?from=todo#approval`),
731
+ );
732
+ await expect(page.locator('.oxa-workflow-detail-mobile')).toBeVisible();
733
+ await expect(page).toHaveURL(
734
+ new RegExp(`/m/tasks/${taskId}\\?from=todo#approval$`),
735
+ );
736
+
737
+ await page.goto(
738
+ workflowFixtureUrl(`/admin/workflows/${instanceId}?from=todo#timeline`),
739
+ );
740
+ await expect(page.locator('.oxa-workflow-detail-mobile')).toBeVisible();
741
+ await expect(page).toHaveURL(
742
+ new RegExp(`/m/workflows/${instanceId}\\?from=todo#timeline$`),
743
+ );
744
+ });
745
+
746
+ test('negotiates launch pairs in both directions and preserves the mount basename', async ({
747
+ page,
748
+ }) => {
749
+ await mockWorkflow(page);
750
+ const base = '/runtime/route-negotiation';
751
+ await page.setViewportSize({ width: 360, height: 844 });
752
+ await page.goto(
753
+ workflowFixtureUrl(
754
+ `${base}/admin/workflows/purchase-approval/new?source=todo#launch`,
755
+ { base },
756
+ ),
757
+ );
758
+ await expect(page.locator('.oxa-workflow-submission-mobile')).toBeVisible();
759
+ await expect(page).toHaveURL(
760
+ new RegExp(
761
+ `${base}/m/workflows/purchase-approval/start\\?source=todo#launch$`,
762
+ ),
763
+ );
764
+
765
+ await page.setViewportSize({ width: 1_440, height: 900 });
766
+ await expect(page.locator('.oxa-workflow-submission-desktop')).toBeVisible();
767
+ await expect(page).toHaveURL(
768
+ new RegExp(
769
+ `${base}/admin/workflows/purchase-approval/new\\?source=todo#launch$`,
770
+ ),
771
+ );
772
+ });
773
+
774
+ test('does not negotiate non-standard application contribution routes', async ({
775
+ page,
776
+ }) => {
777
+ await mockWorkflow(page);
778
+ const base = '/runtime/route-negotiation';
779
+ await page.setViewportSize({ width: 360, height: 844 });
780
+ const customPath = `${base}/admin/operations/purchases/${instanceId}?taskId=${taskId}`;
781
+ await page.goto(workflowFixtureUrl(customPath, { base }));
782
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
783
+ `${instanceId} / ${taskId}`,
784
+ );
785
+ const customUrl = page.url();
786
+ await expect(page).toHaveURL(new RegExp(`${base}/admin/operations/`));
787
+ await expect
788
+ .poll(() => page.url(), { timeout: 1_000 })
789
+ .toBe(customUrl);
790
+ });
@@ -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.48",
17
+ "openxiangda": "2.0.0-alpha.50",
18
18
  "react": "19.2.8",
19
19
  "react-dom": "19.2.8",
20
20
  "react-router-dom": "7.8.2"
@@ -14,6 +14,7 @@ import {
14
14
  appRoutes,
15
15
  routeManifest,
16
16
  authenticationSurfaces,
17
+ anonymousPublicAccess,
17
18
  adminNavigation,
18
19
  adminPages,
19
20
  resourceDefinitions,
@@ -52,6 +53,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
52
53
  adminPages={adminPages}
53
54
  routeManifest={routeManifest}
54
55
  contributions={applicationContributions}
56
+ publicAccess={anonymousPublicAccess}
55
57
  perspectives={appPerspectives}
56
58
  resourceDefinitions={resourceDefinitions}
57
59
  workflows={workflowDefinitions}
@@ -35,6 +35,7 @@ test('clean template contains only application declarations and one React entry'
35
35
  assert.match(source, /DefaultDesktopApplicationLoginSurface/);
36
36
  assert.match(source, /DefaultMobileApplicationLoginSurface/);
37
37
  assert.match(source, /contributions=\{applicationContributions\}/);
38
+ assert.match(source, /publicAccess=\{anonymousPublicAccess\}/);
38
39
  assert.match(source, /from 'openxiangda\/react'/);
39
40
  assert.match(source, /openxiangda\/react\/styles\.css/);
40
41
  const legacyMarkers = [
@@ -23,7 +23,7 @@
23
23
  "build": "pnpm --recursive build"
24
24
  },
25
25
  "devDependencies": {
26
- "openxiangda": "2.0.0-alpha.48",
26
+ "openxiangda": "2.0.0-alpha.50",
27
27
  "typescript": "5.9.3"
28
28
  }
29
29
  }
@@ -84,6 +84,7 @@ export const applicationAuthentication = {
84
84
  }
85
85
  }
86
86
  } as const;
87
+ export const anonymousPublicAccess = undefined;
87
88
  export const platformAuthManifest = {
88
89
  "schemaVersion": "openxiangda.platform-auth-manifest/v2",
89
90
  "appCode": "openxiangda-application",
@@ -143,6 +144,7 @@ export type AppRouteCode = AppRoute["code"];
143
144
  export type ApplicationAuthenticationMethod = (typeof authenticationMethods)[number];
144
145
  export type ApplicationAuthenticationSurface = (typeof authenticationSurfaces)[keyof typeof authenticationSurfaces];
145
146
  export type ApplicationAuthenticationSurfaceCode = ApplicationAuthenticationSurface["routeCode"];
147
+ export type AnonymousPublicAccess = typeof anonymousPublicAccess;
146
148
  export type RouteManifest = typeof routeManifest;
147
149
  export type AdminPage = (typeof adminPages)[number];
148
150
  export type AdminNavigationGroup = (typeof adminNavigation)[number];