openxiangda 1.0.16 → 1.0.18

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.
@@ -69,6 +69,8 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
69
69
  - Publish normal form pages, workflow form pages, and custom code pages through `openxiangda workspace publish --profile <name>` from the app workspace.
70
70
  - Never store token data in the project directory. User tokens live in `~/.openxiangda/profiles.json`; project state lives in `.openxiangda/state.json` and stores only IDs and mappings.
71
71
  - Use logical resource codes in local files. Platform-specific IDs such as `formUuid`, `pageId`, `workflowId`, and `automationId` must be isolated by profile.
72
+ - Put engineering-managed resources in `src/resources/` and use `openxiangda resource validate|plan|publish|pull`. `workspace publish` publishes workspace forms/pages first, then runs non-destructive resource upsert. Only pass `--prune` when the user explicitly wants local manifests to delete platform-side extras.
73
+ - For external APIs, create a connector manifest in `src/resources/connectors/` and call it from pages through `sdk.connector`; never put third-party API keys in page source.
72
74
  - Before publishing to another platform, verify the workspace is bound for that profile. Resource IDs from one profile must not be reused for another profile.
73
75
  - Use `openxiangda app snapshot <APP_XXX> --profile <name> --json` for diagnosis before changing an existing app.
74
76
  - Run write commands that update `.openxiangda/state.json` sequentially within the same workspace. Read-only commands can run in parallel.
@@ -100,6 +102,7 @@ Core CLI / state:
100
102
 
101
103
  - `references/openxiangda-api.md` — `/openxiangda-api/v1` request and response fields.
102
104
  - `references/workspace-state.md` — `.openxiangda/state.json` shape and profile isolation rules.
105
+ - `references/connector-resources.md` — `src/resources` manifests, connector schema, and SDK connector calls.
103
106
 
104
107
  Form authoring:
105
108
 
@@ -0,0 +1,68 @@
1
+ # Connector Resources
2
+
3
+ OpenXiangda engineering resources live under `src/resources/`.
4
+
5
+ Common folders:
6
+
7
+ - `connectors`
8
+ - `roles`
9
+ - `menus`
10
+ - `workflows`
11
+ - `automations`
12
+ - `permissions/page-groups`
13
+ - `permissions/form-groups`
14
+ - `settings/forms`
15
+
16
+ Commands:
17
+
18
+ ```bash
19
+ openxiangda resource validate --profile dev
20
+ openxiangda resource plan --profile dev
21
+ openxiangda resource publish --profile dev
22
+ openxiangda resource pull --profile dev
23
+ ```
24
+
25
+ Connector manifests use stable `code` values. The platform maps connector `code` to the existing connector `methodName`, and API `code` to the existing connector API `methodName`.
26
+
27
+ ```json
28
+ {
29
+ "code": "crm",
30
+ "name": "CRM Service",
31
+ "url": "https://crm.internal.example.com/api",
32
+ "authType": "apiKey",
33
+ "authConfig": {
34
+ "apiKey": {
35
+ "key": "X-API-Key",
36
+ "value": "internal-secret",
37
+ "in": "header"
38
+ }
39
+ },
40
+ "userContext": {
41
+ "enabled": true,
42
+ "inject": [
43
+ { "target": "header", "key": "X-User-Id", "value": "userId" },
44
+ { "target": "body", "key": "operator", "value": "user" }
45
+ ]
46
+ },
47
+ "apis": [
48
+ {
49
+ "code": "getCustomer",
50
+ "name": "Get Customer",
51
+ "method": "POST",
52
+ "path": "/customers/search",
53
+ "requestBodyType": "json",
54
+ "responseType": "json"
55
+ }
56
+ ]
57
+ }
58
+ ```
59
+
60
+ Runtime page code:
61
+
62
+ ```ts
63
+ await sdk.connector.call("crm.getCustomer", {
64
+ body: { keyword: "Acme" },
65
+ })
66
+ ```
67
+
68
+ Do not put third-party domains or API keys in page source. The runtime page calls `/:appType/v1/connectors/actions/invoke` for normal responses and `/:appType/v1/connectors/actions/download` for binary downloads; the platform backend applies auth, user context, and redaction.
@@ -6,6 +6,7 @@ Guidelines:
6
6
 
7
7
  - Do not hardcode `/openxiangda-api` calls inside end-user page components unless the page is explicitly an admin tool.
8
8
  - Prefer SDK modules for form data, user context, permissions, and platform navigation.
9
+ - Use `sdk.connector.invoke`, `sdk.connector.call("connector.api")`, or `sdk.connector.download` for external services. The SDK calls the platform runtime connector endpoint; it must not call third-party domains directly.
9
10
  - For the current user's department hierarchy, use `sdk.department.getCurrentUserParentDepartments()`; do not hardcode `GET /department/:id/parentDepartments` in page code.
10
11
  - Keep API calls behind small local functions so generated UI stays testable.
11
12
  - Treat user context and tenant context as runtime-provided values.
@@ -28,7 +28,12 @@ Tokens never belong in the project. User tokens live in `~/.openxiangda/profiles
28
28
  },
29
29
  "workflows": {},
30
30
  "automations": {},
31
- "menus": {}
31
+ "menus": {},
32
+ "roles": {},
33
+ "connectors": {},
34
+ "pagePermissionGroups": {},
35
+ "formPermissionGroups": {},
36
+ "formSettings": {}
32
37
  }
33
38
  }
34
39
  }
@@ -40,6 +45,7 @@ Tokens never belong in the project. User tokens live in `~/.openxiangda/profiles
40
45
  - Profile is the deployment boundary.
41
46
  - `baseUrl` is the backend API base. On standard private deployments it is `<origin>/service`; management pages use `/platform`, and app runtime pages use `/view`.
42
47
  - Local resource keys are logical codes.
43
- - Live IDs are nested under the profile that produced them.
48
+ - Live IDs and lightweight runtime aliases are nested under the profile that produced them.
49
+ - Do not store business configuration or secrets in `.openxiangda/state.json`; store those in `src/resources/`.
44
50
  - A prod publish must not read dev IDs.
45
51
  - Before publishing to another platform, run `openxiangda workspace bind --profile <name> --app-type <APP_XXX>`.
@@ -60,6 +60,8 @@ openxiangda workspace bind --profile dev --app-type APP_DEV
60
60
  openxiangda workspace bind --profile prod --app-type APP_PROD
61
61
  cd /path/to/sy-lowcode-app-workspace
62
62
  openxiangda workspace publish --profile dev
63
+ openxiangda resource validate --profile dev
64
+ openxiangda resource plan --profile dev
63
65
  ```
64
66
 
65
67
  Read-only diagnosis can use resource commands:
@@ -93,8 +95,10 @@ Project state is stored in `.openxiangda/state.json`:
93
95
  "automations": {},
94
96
  "menus": {},
95
97
  "roles": {},
98
+ "connectors": {},
96
99
  "pagePermissionGroups": {},
97
- "formPermissionGroups": {}
100
+ "formPermissionGroups": {},
101
+ "formSettings": {}
98
102
  }
99
103
  }
100
104
  }
@@ -60,3 +60,6 @@ Read these references only when editing page code:
60
60
  - For list / detail / CRUD pages, follow `../../references/architecture-patterns.md` (e.g. `DataManagementList`) before writing custom data-fetching loops.
61
61
  - Pick components per `../../references/component-guide.md`: prefer the platform component, fall back to Ant Design, and only build a custom component when neither fits.
62
62
  - When a page misbehaves (lost styles, `options is undefined`, option labels showing raw values, etc.), consult `../../references/troubleshooting.md` before patching symptoms.
63
+ - For custom portal pages and business modals, do not temporarily embed a single `FormProvider` field component from the standard form runtime. If a standard component is required, navigate to a full standard form page or render a complete `StandardFormPage` inside a dedicated carrier route.
64
+ - Keep PC and mobile portal styles separate. Do not share one form UI implementation across both viewports unless the surrounding shell, modal layer, date picker, and bottom-sheet behavior have been tested in both contexts.
65
+ - After introducing Ant Design overlays, date/datetime fields, or mobile bottom-sheet selectors, verify that global button, modal, picker, and sheet styles do not leak into the host business page.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -54,6 +54,7 @@
54
54
  "check": "node --check bin/openxiangda.js && node --check lib/*.js && node --check packages/sdk/src/build-source/src/cli.mjs && node --check packages/sdk/src/build-source/scripts/*.mjs && node --check packages/sdk/src/build-source/scripts/utils/*.mjs",
55
55
  "prepack": "npm run build:sdk",
56
56
  "test:profile-isolation": "bash scripts/profile-isolation-smoke.sh",
57
+ "test:resource-plan": "node scripts/resource-plan-smoke.mjs",
57
58
  "test:skill-install": "bash scripts/skill-install-smoke.sh",
58
59
  "test:workspace-init": "bash scripts/workspace-init-smoke.sh"
59
60
  },
@@ -31713,26 +31713,35 @@ var secondOptions = Array.from({ length: 60 }, (_, second) => ({
31713
31713
  label: `${second}\u79D2`,
31714
31714
  value: pad2(second)
31715
31715
  }));
31716
- function formatMobileTime(date4) {
31717
- return (0, import_dayjs12.default)(date4).format("HH:mm:ss");
31716
+ function resolveTimePickerPrecision(dateFormat) {
31717
+ return dateFormat && /s/i.test(dateFormat) ? "second" : "minute";
31718
31718
  }
31719
- function getTimePickerValue(date4) {
31720
- return [(0, import_dayjs12.default)(date4).format("HH"), (0, import_dayjs12.default)(date4).format("mm"), (0, import_dayjs12.default)(date4).format("ss")];
31719
+ function formatMobileTime(date4, dateFormat) {
31720
+ return (0, import_dayjs12.default)(date4).format(resolveTimePickerPrecision(dateFormat) === "second" ? "HH:mm:ss" : "HH:mm");
31721
31721
  }
31722
- function applyTimePickerValue(base, value) {
31722
+ function getTimePickerColumns(precision = "second") {
31723
+ return precision === "second" ? [hourOptions, minuteOptions, secondOptions] : [hourOptions, minuteOptions];
31724
+ }
31725
+ function getTimePickerValue(date4, precision = "second") {
31726
+ const base = [(0, import_dayjs12.default)(date4).format("HH"), (0, import_dayjs12.default)(date4).format("mm")];
31727
+ return precision === "second" ? [...base, (0, import_dayjs12.default)(date4).format("ss")] : base;
31728
+ }
31729
+ function applyTimePickerValue(base, value, precision = "second") {
31723
31730
  const [hour, minute, second] = value;
31724
- return (0, import_dayjs12.default)(base).hour(Number(hour ?? 0)).minute(Number(minute ?? 0)).second(Number(second ?? 0)).millisecond(0).toDate();
31731
+ return (0, import_dayjs12.default)(base).hour(Number(hour ?? 0)).minute(Number(minute ?? 0)).second(precision === "second" ? Number(second ?? 0) : 0).millisecond(0).toDate();
31725
31732
  }
31726
31733
  function MobileTimePickerView({
31727
31734
  value,
31735
+ dateFormat,
31728
31736
  onChange
31729
31737
  }) {
31738
+ const precision = resolveTimePickerPrecision(dateFormat);
31730
31739
  return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
31731
31740
  picker_view_default,
31732
31741
  {
31733
- columns: [hourOptions, minuteOptions, secondOptions],
31734
- value: getTimePickerValue(value),
31735
- onChange: (nextValue) => onChange(applyTimePickerValue(value, nextValue)),
31742
+ columns: getTimePickerColumns(precision),
31743
+ value: getTimePickerValue(value, precision),
31744
+ onChange: (nextValue) => onChange(applyTimePickerValue(value, nextValue, precision)),
31736
31745
  className: "sy-mobile-time-picker",
31737
31746
  renderLabel: (item) => item.label
31738
31747
  }
@@ -31769,30 +31778,32 @@ function getDateOptions(center, min2, max2) {
31769
31778
  };
31770
31779
  });
31771
31780
  }
31772
- function getDateTimePickerValue(date4) {
31773
- return [(0, import_dayjs12.default)(date4).format("YYYY-MM-DD"), ...getTimePickerValue(date4)];
31781
+ function getDateTimePickerValue(date4, precision = "second") {
31782
+ return [(0, import_dayjs12.default)(date4).format("YYYY-MM-DD"), ...getTimePickerValue(date4, precision)];
31774
31783
  }
31775
- function applyDateTimePickerValue(base, value) {
31784
+ function applyDateTimePickerValue(base, value, precision = "second") {
31776
31785
  const [dateValue, hour, minute, second] = value;
31777
31786
  const date4 = (0, import_dayjs12.default)(String(dateValue || (0, import_dayjs12.default)(base).format("YYYY-MM-DD")));
31778
- return date4.hour(Number(hour ?? 0)).minute(Number(minute ?? 0)).second(Number(second ?? 0)).millisecond(0).toDate();
31787
+ return date4.hour(Number(hour ?? 0)).minute(Number(minute ?? 0)).second(precision === "second" ? Number(second ?? 0) : 0).millisecond(0).toDate();
31779
31788
  }
31780
31789
  function MobileDateTimePickerView({
31781
31790
  value,
31791
+ dateFormat,
31782
31792
  min: min2,
31783
31793
  max: max2,
31784
31794
  onChange
31785
31795
  }) {
31796
+ const precision = resolveTimePickerPrecision(dateFormat);
31786
31797
  const columns = import_react216.default.useMemo(
31787
- () => [getDateOptions(value, min2, max2), hourOptions, minuteOptions, secondOptions],
31788
- [value, min2, max2]
31798
+ () => [getDateOptions(value, min2, max2), ...getTimePickerColumns(precision)],
31799
+ [value, min2, max2, precision]
31789
31800
  );
31790
31801
  return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
31791
31802
  picker_view_default,
31792
31803
  {
31793
31804
  columns,
31794
- value: getDateTimePickerValue(value),
31795
- onChange: (nextValue) => onChange(applyDateTimePickerValue(value, nextValue)),
31805
+ value: getDateTimePickerValue(value, precision),
31806
+ onChange: (nextValue) => onChange(applyDateTimePickerValue(value, nextValue, precision)),
31796
31807
  className: "sy-mobile-time-picker sy-mobile-date-time-picker",
31797
31808
  renderLabel: (item) => item.label
31798
31809
  }
@@ -31873,10 +31884,10 @@ function DateFieldMobile({
31873
31884
  }
31874
31885
  ),
31875
31886
  mode === "time" ? /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
31876
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(MobileTimePickerView, { value: tempDate, onChange: setTempDate }),
31887
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(MobileTimePickerView, { value: tempDate, dateFormat: format2, onChange: setTempDate }),
31877
31888
  /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "sy-mobile-date-time-footer", children: [
31878
31889
  /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { children: "\u9009\u62E9\u65F6\u95F4" }),
31879
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", className: "sy-mobile-time-pill is-active", children: formatMobileTime(tempDate) })
31890
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("button", { type: "button", className: "sy-mobile-time-pill is-active", children: formatMobileTime(tempDate, format2) })
31880
31891
  ] })
31881
31892
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
31882
31893
  /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
@@ -31907,7 +31918,7 @@ function DateFieldMobile({
31907
31918
  type: "button",
31908
31919
  className: "sy-mobile-time-pill",
31909
31920
  onClick: () => setMode("time"),
31910
- children: formatMobileTime(tempDate)
31921
+ children: formatMobileTime(tempDate, format2)
31911
31922
  }
31912
31923
  )
31913
31924
  ] }) : null
@@ -32167,6 +32178,7 @@ function CascadeDateFieldMobile({
32167
32178
  MobileDateTimePickerView,
32168
32179
  {
32169
32180
  value: timeStep === "start" ? tempStart : tempEnd,
32181
+ dateFormat: format2,
32170
32182
  min: timeStep === "start" ? min2 : endMin,
32171
32183
  max: max2,
32172
32184
  onChange: timeStep === "start" ? updateTempStart : updateTempEnd
@@ -46094,6 +46106,7 @@ var StandardFormPage = ({
46094
46106
  mode,
46095
46107
  initialValues,
46096
46108
  permissions,
46109
+ api: externalApi,
46097
46110
  formUuid,
46098
46111
  appType,
46099
46112
  formInstanceId,
@@ -46107,7 +46120,7 @@ var StandardFormPage = ({
46107
46120
  const formType = normalizeStandardFormType(
46108
46121
  schema.template?.formType || (resolvedMode === "process" ? "process" : "form")
46109
46122
  );
46110
- const api = (0, import_react278.useMemo)(() => {
46123
+ const submitApi = (0, import_react278.useMemo)(() => {
46111
46124
  if (!onSubmit) return void 0;
46112
46125
  return {
46113
46126
  submitFormData: async (payload) => onSubmit(
@@ -46120,6 +46133,13 @@ var StandardFormPage = ({
46120
46133
  }
46121
46134
  };
46122
46135
  }, [formType, onSubmit]);
46136
+ const api = (0, import_react278.useMemo)(() => {
46137
+ if (!externalApi && !submitApi) return void 0;
46138
+ return {
46139
+ ...externalApi ?? {},
46140
+ ...submitApi ?? {}
46141
+ };
46142
+ }, [externalApi, submitApi]);
46123
46143
  if (resolvedMode === "process") {
46124
46144
  return /* @__PURE__ */ (0, import_jsx_runtime100.jsx)(
46125
46145
  ProcessDetailTemplate,