openxiangda-cli 2.0.0-alpha.133 → 2.0.0-alpha.134
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 +5 -5
- package/template/AGENTS.md +1 -0
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/workflow-experience-fixture.tsx +25 -4
- package/template/apps/web/e2e/workflow.spec.ts +115 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/main.tsx +2 -0
- package/template/apps/web/test/contracts.test.ts +1 -0
- package/template/package.json +1 -1
- package/template/packages/contracts/src/generated.ts +2 -0
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.134",
|
|
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.
|
|
25
|
-
"openxiangda-devkit-core": "2.0.0-alpha.
|
|
26
|
-
"openxiangda-mcp": "2.0.0-alpha.
|
|
27
|
-
"openxiangda-skill-kit": "2.0.0-alpha.
|
|
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",
|
package/template/AGENTS.md
CHANGED
|
@@ -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.
|
|
@@ -245,11 +245,32 @@ const adminNavigation = [
|
|
|
245
245
|
},
|
|
246
246
|
] as const satisfies AdminNavigationInput;
|
|
247
247
|
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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,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 = [
|
package/template/package.json
CHANGED
|
@@ -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];
|