@nocobase/plugin-flow-engine 2.2.0-alpha.11 → 2.2.0-alpha.12

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.
@@ -1,23 +1,51 @@
1
1
  ---
2
2
  title: "ctx.ai"
3
- description: "Use ctx.ai in RunJS to trigger AI employee tasks, either with inline task content or with tasks configured on an AI employee action."
4
- keywords: "ctx.ai,AI employee,triggerTask,triggerModelTask,RunJS,NocoBase"
3
+ description: "Use ctx.ai in RunJS to trigger AI employee tasks in the global conversation or a specified AI Chat Box, either with inline task content or with tasks configured on an AI employee action."
4
+ keywords: "ctx.ai,AI employee,uploadFile,attachments,triggerTask,triggerModelTask,onResponseLoadingChange,chatBoxUid,AI Chat Box,RunJS,NocoBase"
5
5
  ---
6
6
 
7
7
  # ctx.ai
8
8
 
9
9
  Use `ctx.ai` in RunJS to trigger **AI employee tasks**. It works well in JSBlock, JSAction, and other interactions where a button, form, or business flow needs to hand work to a specific AI employee.
10
10
 
11
- `ctx.ai` only triggers tasks. It does not return the execution result of the AI employee task. After the call, the task enters the AI employee conversation flow, and the result is handled by the AI employee session.
11
+ `ctx.ai` uploads AI task attachments and triggers tasks. File uploads can be awaited, but task triggering does not return the execution result of the AI employee task. After the call, the task enters the AI employee conversation flow, and the result is handled by the AI employee session.
12
12
 
13
13
  :::warning Note
14
14
 
15
- `ctx.ai` is provided by the AI plugin. If the AI plugin is not enabled, or the current RunJS environment has not loaded the corresponding client capability, `ctx.ai` may not exist. You can check `ctx.ai?.triggerTask` or `ctx.ai?.triggerModelTask` before calling it.
15
+ `ctx.ai` is provided by the AI plugin. If the AI plugin is not enabled, or the current RunJS environment has not loaded the corresponding client capability, `ctx.ai` may not exist. You can check `ctx.ai?.uploadFile`, `ctx.ai?.triggerTask`, or `ctx.ai?.triggerModelTask` before calling it.
16
16
 
17
17
  :::
18
18
 
19
19
  ## Methods
20
20
 
21
+ ### ctx.ai.uploadFile()
22
+
23
+ Upload one file and return an attachment object that can be passed directly to an AI employee task.
24
+
25
+ ```ts
26
+ const attachment = await ctx.ai.uploadFile(file, options);
27
+ ```
28
+
29
+ | Parameter | Type | Description |
30
+ |------|------|------|
31
+ | `file` | `File` | Browser file object to upload. |
32
+ | `options.onProgress` | `(percent: number) => void` | Upload progress callback. `percent` ranges from `0` to `100`. |
33
+ | `options.signal` | `AbortSignal` | Signal used to cancel the upload. |
34
+
35
+ The upload uses the file storage configured by the AI plugin and creates a record in `aiFiles`. The returned object includes fields such as `id`, `filename`, `url`, and `source`:
36
+
37
+ ```ts
38
+ const attachment = await ctx.ai.uploadFile(file, {
39
+ onProgress(percent) {
40
+ console.log('upload progress', percent);
41
+ },
42
+ });
43
+
44
+ // attachment can be placed directly in message.attachments
45
+ ```
46
+
47
+ The Promise is rejected when the upload fails. Removing an attachment from the local UI does not delete the record already created in `aiFiles`, matching the behavior of the default AI chat window.
48
+
21
49
  ### ctx.ai.triggerTask()
22
50
 
23
51
  Trigger an AI employee task directly.
@@ -30,8 +58,10 @@ ctx.ai.triggerTask(options: TriggerTaskOptions): void
30
58
  |------|------|------|
31
59
  | `aiEmployee` | `string \| AIEmployee` | AI employee. When a string is passed, NocoBase matches `AIEmployee.username` exactly, and the AI employee must be accessible to the current user. |
32
60
  | `tasks` | `Task[]` | Tasks to trigger. |
61
+ | `chatBoxUid` | `string` | FlowModel uid of the AI Chat Box block that should receive the task. |
33
62
  | `open` | `boolean` | Whether to open the AI employee conversation panel. |
34
63
  | `auto` | `boolean` | Whether to use the auto-trigger semantics of an AI employee action. |
64
+ | `onResponseLoadingChange` | `(loading: boolean) => void` | Model response loading callback. It only runs when this task is sent automatically. |
35
65
 
36
66
  Common `Task` fields:
37
67
 
@@ -40,12 +70,160 @@ Common `Task` fields:
40
70
  | `title` | `string` | Task title. |
41
71
  | `message.system` | `string` | System message, used to constrain the AI employee's role and output requirements. |
42
72
  | `message.user` | `string` | User message, which is the main instruction for this task. |
73
+ | `message.attachments` | `Attachment[]` | Attachments used by the task, usually returned by `ctx.ai.uploadFile()`. |
43
74
  | `message.workContext` | `ContextItem[]` | Page block context used by the task. |
44
75
  | `autoSend` | `boolean` | Whether to send the task message automatically. |
45
76
  | `webSearch` | `boolean` | Whether Web search is allowed for this task. |
46
77
  | `model` | `{ llmService: string; model: string } \| null` | Model used by this task. |
47
78
  | `skillSettings` | `SkillSettings` | Skills and tools available to this task. |
48
79
 
80
+ ### Track the Response Loading State
81
+
82
+ Pass `onResponseLoadingChange` in the top-level options to track the model response loading state for this task. The callback receives `true` when NocoBase starts waiting for the model response, and `false` when the response completes, is canceled, or fails. If the React component has declared `setResponseLoading` with `useState`, you can write:
83
+
84
+ ```tsx
85
+ ctx.ai.triggerTask({
86
+ aiEmployee: 'nathan',
87
+ open: true,
88
+ tasks: [
89
+ {
90
+ title: ctx.t('Review current page'),
91
+ message: {
92
+ user: 'Review the current page and summarize the main risks.',
93
+ },
94
+ autoSend: true,
95
+ },
96
+ ],
97
+ onResponseLoadingChange(loading) {
98
+ setResponseLoading(loading);
99
+ },
100
+ });
101
+ ```
102
+
103
+ `onResponseLoadingChange` only tracks the model response started directly by this `triggerTask()` call. With `autoSend: false`, the task is placed in the chat draft and the callback does not run. If the user sends the draft later, that manual send does not reuse this callback.
104
+
105
+ In a React component inside a JS block, this state update rerenders the component while it remains mounted.
106
+
107
+ ### Target an AI Chat Box
108
+
109
+ Set `chatBoxUid` on the top-level `triggerTask()` options to trigger the task in a mounted AI Chat Box block instead of the global AI employee dialog.
110
+
111
+ ```ts
112
+ ctx.ai.triggerTask({
113
+ aiEmployee: 'nathan',
114
+ chatBoxUid: 'AI_CHAT_BOX_BLOCK_UID',
115
+ open: true,
116
+ tasks: [
117
+ {
118
+ title: ctx.t('Review current page'),
119
+ message: {
120
+ user: 'Review the current page and summarize the main risks.',
121
+ },
122
+ },
123
+ ],
124
+ });
125
+ ```
126
+
127
+ The uid must belong to the outer AI Chat Box block currently mounted on the page. Do not put this routing value inside `tasks`. If the target block cannot be found, NocoBase reports an error and does not fall back to the global dialog. When `chatBoxUid` is omitted, the task uses the global AI employee dialog.
128
+
129
+ ### Upload and Send Attachments in JSBlock
130
+
131
+ The following example renders file upload, task instructions, and a send button in JSBlock. Uploaded files are passed to the AI employee through `message.attachments`:
132
+
133
+ ```tsx
134
+ if (!ctx.ai?.uploadFile || !ctx.ai?.triggerTask) {
135
+ ctx.message.error(ctx.t('AI employee task API is not available.'));
136
+ return;
137
+ }
138
+
139
+ const { React } = ctx.libs;
140
+ const { useState } = React;
141
+ const { Button, Card, Input, Space, Upload } = ctx.libs.antd;
142
+ const { InboxOutlined, SendOutlined } = ctx.libs.antdIcons;
143
+
144
+ const AttachmentTask = () => {
145
+ const [prompt, setPrompt] = useState('');
146
+ const [fileList, setFileList] = useState([]);
147
+
148
+ const uploadAttachment = async ({ file, onError, onProgress, onSuccess }) => {
149
+ try {
150
+ const attachment = await ctx.ai.uploadFile(file, {
151
+ onProgress(percent) {
152
+ onProgress?.({ percent });
153
+ },
154
+ });
155
+ onSuccess?.(attachment);
156
+ } catch (error) {
157
+ onError?.(error instanceof Error ? error : new Error(ctx.t('File upload failed')));
158
+ }
159
+ };
160
+
161
+ const sendTask = () => {
162
+ const attachments = fileList
163
+ .filter((file) => file.status === 'done' && file.response)
164
+ .map((file) => file.response);
165
+
166
+ if (!prompt.trim()) {
167
+ ctx.message.warning(ctx.t('Enter task instructions'));
168
+ return;
169
+ }
170
+
171
+ ctx.ai.triggerTask({
172
+ aiEmployee: 'viz',
173
+ open: true,
174
+ tasks: [
175
+ {
176
+ title: ctx.t('Analyze uploaded files'),
177
+ message: {
178
+ user: prompt.trim(),
179
+ attachments,
180
+ },
181
+ autoSend: true,
182
+ },
183
+ ],
184
+ });
185
+ setPrompt('');
186
+ setFileList([]);
187
+ };
188
+
189
+ const uploading = fileList.some((file) => file.status === 'uploading');
190
+
191
+ return (
192
+ <Card title={ctx.t('AI file analysis')}>
193
+ <Space direction="vertical" size="middle" style={{ width: '100%' }}>
194
+ <Upload.Dragger
195
+ multiple
196
+ fileList={fileList}
197
+ customRequest={uploadAttachment}
198
+ onChange={({ fileList: nextFileList }) => setFileList(nextFileList)}
199
+ >
200
+ <p className="ant-upload-drag-icon"><InboxOutlined /></p>
201
+ <p>{ctx.t('Click or drag files here to upload')}</p>
202
+ </Upload.Dragger>
203
+ <Input.TextArea
204
+ value={prompt}
205
+ onChange={(event) => setPrompt(event.target.value)}
206
+ placeholder={ctx.t('Describe the task for the AI employee')}
207
+ autoSize={{ minRows: 3, maxRows: 8 }}
208
+ />
209
+ <Button
210
+ type="primary"
211
+ icon={<SendOutlined />}
212
+ disabled={uploading || !prompt.trim()}
213
+ onClick={sendTask}
214
+ >
215
+ {ctx.t('Send to AI')}
216
+ </Button>
217
+ </Space>
218
+ </Card>
219
+ );
220
+ };
221
+
222
+ ctx.render(<AttachmentTask />);
223
+ ```
224
+
225
+ With `autoSend: false`, the attachments and task instructions are placed in the AI chat draft instead of being sent immediately.
226
+
49
227
  ### Add Page Block Context
50
228
 
51
229
  `message.workContext` currently passes page blocks. Put the FlowModel uid of the target page block into it:
@@ -163,9 +341,15 @@ ctx.ai.triggerModelTask(uid: string, taskIndex: number, options?: TriggerModelTa
163
341
  | `taskIndex` | `number` | Task index, starting from `0`. |
164
342
  | `options.open` | `boolean` | Whether to open the AI employee conversation panel. |
165
343
  | `options.auto` | `boolean` | Whether to use the auto-trigger semantics of an AI employee action. |
344
+ | `options.attachments` | `Attachment[]` | Attachments dynamically appended to the configured task. |
345
+ | `options.onResponseLoadingChange` | `(loading: boolean) => void` | Model response loading callback. It only runs when the configured task is sent automatically. |
166
346
 
167
347
  This method reads the AI employee and task configuration from the target model. It is useful when the task has already been configured on an AI employee action on the page, and RunJS only needs to trigger it.
168
348
 
349
+ The public `triggerModelTask()` options do not accept `chatBoxUid`. To target an AI Chat Box, configure `chatBoxUid` on the preset task of the AI employee action. `triggerModelTask()` continues to reuse that preset value.
350
+
351
+ `options.onResponseLoadingChange` behaves the same as the option on `triggerTask()`. Whether it runs depends on the configured task's `autoSend` value. It does not run when that task uses `autoSend: false`.
352
+
169
353
  ```ts
170
354
  if (!ctx.ai?.triggerModelTask) {
171
355
  ctx.message.error(ctx.t('AI employee task API is not available.'));
@@ -176,6 +360,7 @@ const weeklyReviewActionUid = 'AI_EMPLOYEE_ACTION_MODEL_UID';
176
360
 
177
361
  ctx.ai.triggerModelTask(weeklyReviewActionUid, 0, {
178
362
  open: true,
363
+ attachments,
179
364
  });
180
365
 
181
366
  ctx.message.success(ctx.t('Configured AI employee task triggered.'));
@@ -195,9 +380,14 @@ If the target model does not exist, has no AI employee configured, or the specif
195
380
  ## Notes
196
381
 
197
382
  - `triggerTask()` and `triggerModelTask()` are fire-and-forget. They do not return the execution result of the AI employee task.
383
+ - `uploadFile()` returns a Promise. Wait for the upload to finish before triggering a task that uses the attachment.
198
384
  - `aiEmployee` strings only match `AIEmployee.username` exactly. They do not match nicknames, job titles, or translated names.
199
385
  - `triggerModelTask()` uses a `0`-based `taskIndex`.
200
386
  - `triggerModelTask()` reads task configuration from the target AI employee action model. If the task needs work context, configure `message.workContext` on that task.
387
+ - Top-level `triggerTask().chatBoxUid` must reference an AI Chat Box block currently mounted on the page.
388
+ - `triggerModelTask()` keeps using `chatBoxUid` configured on its preset task.
389
+ - Dynamic `triggerModelTask()` attachments are appended to the preset task's existing `message.attachments` without changing the saved task configuration.
390
+ - `onResponseLoadingChange` only tracks a model response automatically sent by the current call. It does not track a message the user sends manually later.
201
391
 
202
392
  ## Related
203
393
 
@@ -8,16 +8,16 @@
8
8
  */
9
9
 
10
10
  module.exports = {
11
- "@nocobase/client": "2.2.0-alpha.11",
11
+ "@nocobase/client": "2.2.0-alpha.12",
12
12
  "lodash": "4.18.1",
13
- "@nocobase/database": "2.2.0-alpha.11",
14
- "@nocobase/data-source-manager": "2.2.0-alpha.11",
15
- "@nocobase/resourcer": "2.2.0-alpha.11",
16
- "@nocobase/utils": "2.2.0-alpha.11",
17
- "@nocobase/cache": "2.2.0-alpha.11",
18
- "@nocobase/server": "2.2.0-alpha.11",
19
- "@nocobase/actions": "2.2.0-alpha.11",
13
+ "@nocobase/database": "2.2.0-alpha.12",
14
+ "@nocobase/data-source-manager": "2.2.0-alpha.12",
15
+ "@nocobase/resourcer": "2.2.0-alpha.12",
16
+ "@nocobase/utils": "2.2.0-alpha.12",
17
+ "@nocobase/cache": "2.2.0-alpha.12",
18
+ "@nocobase/server": "2.2.0-alpha.12",
19
+ "@nocobase/actions": "2.2.0-alpha.12",
20
20
  "sequelize": "6.35.2",
21
21
  "@formily/json-schema": "2.3.7",
22
- "@nocobase/ai": "2.2.0-alpha.11"
22
+ "@nocobase/ai": "2.2.0-alpha.12"
23
23
  };
@@ -1 +1 @@
1
- {"name":"@ant-design/icons-svg","version":"4.3.1","sideEffects":false,"description":"Abstract nodes for ant design icons.","contributors":["HeskeyBaozi <hezhiyu233@foxmail.com>"],"homepage":"https://github.com/ant-design/ant-design-icons/","license":"MIT","main":"lib/index.js","module":"es/index.js","files":["lib","es","inline-svg","inline-namespaced-svg"],"repository":{"type":"git","url":"git+https://github.com/ant-design/ant-design-icons.git"},"scripts":{"g":"npm run generate","generate":"cross-env NODE_ENV=production gulp --require ts-node/register/transpile-only","type-check":"tsc --noEmit","build:es":"tsc --project tsconfig.build.json --module esnext --outDir es","build:lib":"tsc --project tsconfig.build.json --module commonjs --outDir lib","build":"cross-env NODE_ENV=production && npm run build:es && npm run build:lib","test":"cross-env NODE_ENV=test && npm run test:unit","test:unit":"jest","prepublishOnly":"npm run test && npm run g && npm run build && npm run type-check && npm run digest","digest":"node scripts/digest.js"},"bugs":{"url":"https://github.com/ant-design/ant-design-icons/issues"},"devDependencies":{"@babel/core":"^7.8.6","@babel/preset-env":"^7.8.6","@babel/preset-typescript":"^7.8.3","@rgrove/parse-xml":"^2.0.2","@types/gulp":"^4.0.6","@types/gulp-concat":"^0.0.32","@types/gulp-rename":"^0.0.33","@types/jest":"^25.1.3","@types/lodash.camelcase":"^4.3.6","@types/lodash.template":"^4.4.6","@types/lodash.upperfirst":"^4.3.6","@types/node":"^13.9.3","@types/ramda":"^0.26.43","@types/rgrove__parse-xml":"^1.1.0","@types/svgo":"^1.3.1","@types/through2":"^2.0.34","@typescript-eslint/eslint-plugin":"^2.21.0","@typescript-eslint/parser":"^2.21.0","babel-jest":"^25.1.0","cross-env":"^7.0.0","del":"^5.1.0","eslint":"^7.16.0","globby":"^11.0.0","gulp":"^4.0.2","gulp-clone":"^2.0.1","gulp-concat":"^2.6.1","gulp-header":"^2.0.9","gulp-rename":"^2.0.0","jest":"^25.1.0","lodash.camelcase":"^4.3.0","lodash.template":"^4.5.0","lodash.upperfirst":"^4.3.1","ramda":"^0.27.0","svgo":"^1.3.2","through2":"^3.0.1","ts-node":"^8.6.2","typescript":"^4.0.0","vinyl":"^2.2.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_lastModified":"2026-07-28T07:40:06.190Z"}
1
+ {"name":"@ant-design/icons-svg","version":"4.3.1","sideEffects":false,"description":"Abstract nodes for ant design icons.","contributors":["HeskeyBaozi <hezhiyu233@foxmail.com>"],"homepage":"https://github.com/ant-design/ant-design-icons/","license":"MIT","main":"lib/index.js","module":"es/index.js","files":["lib","es","inline-svg","inline-namespaced-svg"],"repository":{"type":"git","url":"git+https://github.com/ant-design/ant-design-icons.git"},"scripts":{"g":"npm run generate","generate":"cross-env NODE_ENV=production gulp --require ts-node/register/transpile-only","type-check":"tsc --noEmit","build:es":"tsc --project tsconfig.build.json --module esnext --outDir es","build:lib":"tsc --project tsconfig.build.json --module commonjs --outDir lib","build":"cross-env NODE_ENV=production && npm run build:es && npm run build:lib","test":"cross-env NODE_ENV=test && npm run test:unit","test:unit":"jest","prepublishOnly":"npm run test && npm run g && npm run build && npm run type-check && npm run digest","digest":"node scripts/digest.js"},"bugs":{"url":"https://github.com/ant-design/ant-design-icons/issues"},"devDependencies":{"@babel/core":"^7.8.6","@babel/preset-env":"^7.8.6","@babel/preset-typescript":"^7.8.3","@rgrove/parse-xml":"^2.0.2","@types/gulp":"^4.0.6","@types/gulp-concat":"^0.0.32","@types/gulp-rename":"^0.0.33","@types/jest":"^25.1.3","@types/lodash.camelcase":"^4.3.6","@types/lodash.template":"^4.4.6","@types/lodash.upperfirst":"^4.3.6","@types/node":"^13.9.3","@types/ramda":"^0.26.43","@types/rgrove__parse-xml":"^1.1.0","@types/svgo":"^1.3.1","@types/through2":"^2.0.34","@typescript-eslint/eslint-plugin":"^2.21.0","@typescript-eslint/parser":"^2.21.0","babel-jest":"^25.1.0","cross-env":"^7.0.0","del":"^5.1.0","eslint":"^7.16.0","globby":"^11.0.0","gulp":"^4.0.2","gulp-clone":"^2.0.1","gulp-concat":"^2.6.1","gulp-header":"^2.0.9","gulp-rename":"^2.0.0","jest":"^25.1.0","lodash.camelcase":"^4.3.0","lodash.template":"^4.5.0","lodash.upperfirst":"^4.3.1","ramda":"^0.27.0","svgo":"^1.3.2","through2":"^3.0.1","ts-node":"^8.6.2","typescript":"^4.0.0","vinyl":"^2.2.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_lastModified":"2026-07-30T03:54:05.520Z"}
@@ -1 +1 @@
1
- {"name":"acorn","description":"ECMAScript parser","homepage":"https://github.com/acornjs/acorn","main":"dist/acorn.js","types":"dist/acorn.d.ts","module":"dist/acorn.mjs","exports":{".":[{"import":"./dist/acorn.mjs","require":"./dist/acorn.js","default":"./dist/acorn.js"},"./dist/acorn.js"],"./package.json":"./package.json"},"version":"8.11.2","engines":{"node":">=0.4.0"},"maintainers":[{"name":"Marijn Haverbeke","email":"marijnh@gmail.com","web":"https://marijnhaverbeke.nl"},{"name":"Ingvar Stepanyan","email":"me@rreverser.com","web":"https://rreverser.com/"},{"name":"Adrian Heine","web":"http://adrianheine.de"}],"repository":{"type":"git","url":"https://github.com/acornjs/acorn.git"},"license":"MIT","scripts":{"prepare":"cd ..; npm run build:main"},"bin":{"acorn":"./bin/acorn"},"_lastModified":"2026-07-28T07:40:07.187Z"}
1
+ {"name":"acorn","description":"ECMAScript parser","homepage":"https://github.com/acornjs/acorn","main":"dist/acorn.js","types":"dist/acorn.d.ts","module":"dist/acorn.mjs","exports":{".":[{"import":"./dist/acorn.mjs","require":"./dist/acorn.js","default":"./dist/acorn.js"},"./dist/acorn.js"],"./package.json":"./package.json"},"version":"8.11.2","engines":{"node":">=0.4.0"},"maintainers":[{"name":"Marijn Haverbeke","email":"marijnh@gmail.com","web":"https://marijnhaverbeke.nl"},{"name":"Ingvar Stepanyan","email":"me@rreverser.com","web":"https://rreverser.com/"},{"name":"Adrian Heine","web":"http://adrianheine.de"}],"repository":{"type":"git","url":"https://github.com/acornjs/acorn.git"},"license":"MIT","scripts":{"prepare":"cd ..; npm run build:main"},"bin":{"acorn":"./bin/acorn"},"_lastModified":"2026-07-30T03:54:06.651Z"}
@@ -1 +1 @@
1
- {"name":"acorn-jsx","description":"Modern, fast React.js JSX parser","homepage":"https://github.com/acornjs/acorn-jsx","version":"5.3.2","maintainers":[{"name":"Ingvar Stepanyan","email":"me@rreverser.com","web":"http://rreverser.com/"}],"repository":{"type":"git","url":"https://github.com/acornjs/acorn-jsx"},"license":"MIT","scripts":{"test":"node test/run.js"},"peerDependencies":{"acorn":"^6.0.0 || ^7.0.0 || ^8.0.0"},"devDependencies":{"acorn":"^8.0.1"},"_lastModified":"2026-07-28T07:40:07.667Z"}
1
+ {"name":"acorn-jsx","description":"Modern, fast React.js JSX parser","homepage":"https://github.com/acornjs/acorn-jsx","version":"5.3.2","maintainers":[{"name":"Ingvar Stepanyan","email":"me@rreverser.com","web":"http://rreverser.com/"}],"repository":{"type":"git","url":"https://github.com/acornjs/acorn-jsx"},"license":"MIT","scripts":{"test":"node test/run.js"},"peerDependencies":{"acorn":"^6.0.0 || ^7.0.0 || ^8.0.0"},"devDependencies":{"acorn":"^8.0.1"},"_lastModified":"2026-07-30T03:54:07.073Z"}
@@ -1 +1 @@
1
- {"name":"acorn-walk","description":"ECMAScript (ESTree) AST walker","homepage":"https://github.com/acornjs/acorn","main":"dist/walk.js","types":"dist/walk.d.ts","module":"dist/walk.mjs","exports":{".":[{"import":"./dist/walk.mjs","require":"./dist/walk.js","default":"./dist/walk.js"},"./dist/walk.js"],"./package.json":"./package.json"},"version":"8.3.4","engines":{"node":">=0.4.0"},"dependencies":{"acorn":"^8.11.0"},"maintainers":[{"name":"Marijn Haverbeke","email":"marijnh@gmail.com","web":"https://marijnhaverbeke.nl"},{"name":"Ingvar Stepanyan","email":"me@rreverser.com","web":"https://rreverser.com/"},{"name":"Adrian Heine","web":"http://adrianheine.de"}],"repository":{"type":"git","url":"https://github.com/acornjs/acorn.git"},"scripts":{"prepare":"cd ..; npm run build:walk"},"license":"MIT","_lastModified":"2026-07-28T07:40:07.791Z"}
1
+ {"name":"acorn-walk","description":"ECMAScript (ESTree) AST walker","homepage":"https://github.com/acornjs/acorn","main":"dist/walk.js","types":"dist/walk.d.ts","module":"dist/walk.mjs","exports":{".":[{"import":"./dist/walk.mjs","require":"./dist/walk.js","default":"./dist/walk.js"},"./dist/walk.js"],"./package.json":"./package.json"},"version":"8.3.4","engines":{"node":">=0.4.0"},"dependencies":{"acorn":"^8.11.0"},"maintainers":[{"name":"Marijn Haverbeke","email":"marijnh@gmail.com","web":"https://marijnhaverbeke.nl"},{"name":"Ingvar Stepanyan","email":"me@rreverser.com","web":"https://rreverser.com/"},{"name":"Adrian Heine","web":"http://adrianheine.de"}],"repository":{"type":"git","url":"https://github.com/acornjs/acorn.git"},"scripts":{"prepare":"cd ..; npm run build:walk"},"license":"MIT","_lastModified":"2026-07-30T03:54:07.185Z"}
@@ -1 +1 @@
1
- {"name":"ses","version":"1.14.0","description":"Hardened JavaScript for Fearless Cooperation","keywords":["lockdown","harden","Compartment","assert","security","confinement","isolation","object capabilities","ocaps","secure execution","third-party code","prototype pollution","supply-chain attack","plugin"],"author":"Agoric","license":"Apache-2.0","homepage":"https://github.com/Agoric/SES-shim/tree/master/packages/ses#readme","repository":{"type":"git","url":"git+https://github.com/endojs/endo.git","directory":"packages/ses"},"bugs":{"url":"https://github.com/endojs/endo/issues"},"type":"module","main":"./dist/ses.cjs","module":"./index.js","unpkg":"./dist/ses.umd.js","types":"./types.d.ts","exports":{".":{"import":{"types":"./types.d.ts","xs":"./src-xs/index.js","default":"./index.js"},"require":{"types":"./dist/types.d.cts","default":"./dist/ses.cjs"}},"./lockdown":{"import":{"types":"./types.d.ts","default":"./index.js"},"require":{"types":"./dist/types.d.cts","default":"./dist/ses.cjs"}},"./hermes":{"require":{"types":"./dist/types.d.cts","default":"./dist/ses-hermes.cjs"}},"./tools.js":"./tools.js","./assert-shim.js":"./assert-shim.js","./lockdown-shim.js":{"xs":"./src-xs/lockdown-shim.js","default":"./lockdown-shim.js"},"./compartment-shim.js":{"xs":"./src-xs/compartment-shim.js","default":"./compartment-shim.js"},"./console-shim.js":"./console-shim.js","./package.json":"./package.json"},"scripts":{"build:vanilla":"node scripts/bundle.js","build:hermes":"node scripts/bundle.js hermes","build":"yarn build:vanilla && yarn build:hermes","clean":"rm -rf dist","cover":"c8 ava","demo":"python3 -m http.server","lint":"yarn lint:types && yarn lint:eslint","lint-fix":"eslint --fix .","lint:eslint":"eslint .","lint:types":"tsc","prepare":"npm run clean && npm run build","qt":"ava","test":"tsd && ava","test:hermes":"./scripts/hermes-test.sh","test:xs":"xst dist/ses.umd.js test/_lockdown-safe.js && node scripts/generate-test-xs.js && xst tmp/test-xs.js && rm -rf tmp","postpack":"git clean -fX \"*.d.ts*\" \"*.d.cts*\" \"*.d.mts*\" \"*.tsbuildinfo\""},"dependencies":{"@endo/cache-map":"^1.1.0","@endo/env-options":"^1.1.11","@endo/immutable-arraybuffer":"^1.1.2"},"devDependencies":{"@babel/generator":"^7.26.3","@babel/parser":"~7.26.2","@babel/traverse":"~7.25.9","@babel/types":"~7.26.0","@endo/compartment-mapper":"^1.6.3","@endo/module-source":"^1.3.3","@endo/test262-runner":"^0.1.48","@types/babel__traverse":"^7.20.5","ava":"^6.1.3","babel-eslint":"^10.1.0","c8":"^7.14.0","core-js":"^3.31.0","eslint":"^8.57.1","eslint-config-airbnb-base":"^15.0.0","eslint-config-prettier":"^9.1.0","eslint-plugin-eslint-comments":"^3.2.0","eslint-plugin-import":"^2.31.0","hermes-engine-cli":"^0.12.0","prettier":"^3.5.3","terser":"^5.16.6","tsd":"^0.31.2","typescript":"~5.8.3"},"files":["./*.d.ts","./*.js","./*.map","LICENSE*","SECURITY*","dist","lib","src","tools"],"publishConfig":{"access":"public"},"eslintConfig":{"extends":["plugin:@endo/ses"]},"ava":{"files":["test/**/*.test.*"],"timeout":"2m"},"typeCoverage":{"atLeast":81.17},"gitHead":"9815aea9541f241389d2135c6097a7442bdffa17","_lastModified":"2026-07-28T07:40:06.746Z"}
1
+ {"name":"ses","version":"1.14.0","description":"Hardened JavaScript for Fearless Cooperation","keywords":["lockdown","harden","Compartment","assert","security","confinement","isolation","object capabilities","ocaps","secure execution","third-party code","prototype pollution","supply-chain attack","plugin"],"author":"Agoric","license":"Apache-2.0","homepage":"https://github.com/Agoric/SES-shim/tree/master/packages/ses#readme","repository":{"type":"git","url":"git+https://github.com/endojs/endo.git","directory":"packages/ses"},"bugs":{"url":"https://github.com/endojs/endo/issues"},"type":"module","main":"./dist/ses.cjs","module":"./index.js","unpkg":"./dist/ses.umd.js","types":"./types.d.ts","exports":{".":{"import":{"types":"./types.d.ts","xs":"./src-xs/index.js","default":"./index.js"},"require":{"types":"./dist/types.d.cts","default":"./dist/ses.cjs"}},"./lockdown":{"import":{"types":"./types.d.ts","default":"./index.js"},"require":{"types":"./dist/types.d.cts","default":"./dist/ses.cjs"}},"./hermes":{"require":{"types":"./dist/types.d.cts","default":"./dist/ses-hermes.cjs"}},"./tools.js":"./tools.js","./assert-shim.js":"./assert-shim.js","./lockdown-shim.js":{"xs":"./src-xs/lockdown-shim.js","default":"./lockdown-shim.js"},"./compartment-shim.js":{"xs":"./src-xs/compartment-shim.js","default":"./compartment-shim.js"},"./console-shim.js":"./console-shim.js","./package.json":"./package.json"},"scripts":{"build:vanilla":"node scripts/bundle.js","build:hermes":"node scripts/bundle.js hermes","build":"yarn build:vanilla && yarn build:hermes","clean":"rm -rf dist","cover":"c8 ava","demo":"python3 -m http.server","lint":"yarn lint:types && yarn lint:eslint","lint-fix":"eslint --fix .","lint:eslint":"eslint .","lint:types":"tsc","prepare":"npm run clean && npm run build","qt":"ava","test":"tsd && ava","test:hermes":"./scripts/hermes-test.sh","test:xs":"xst dist/ses.umd.js test/_lockdown-safe.js && node scripts/generate-test-xs.js && xst tmp/test-xs.js && rm -rf tmp","postpack":"git clean -fX \"*.d.ts*\" \"*.d.cts*\" \"*.d.mts*\" \"*.tsbuildinfo\""},"dependencies":{"@endo/cache-map":"^1.1.0","@endo/env-options":"^1.1.11","@endo/immutable-arraybuffer":"^1.1.2"},"devDependencies":{"@babel/generator":"^7.26.3","@babel/parser":"~7.26.2","@babel/traverse":"~7.25.9","@babel/types":"~7.26.0","@endo/compartment-mapper":"^1.6.3","@endo/module-source":"^1.3.3","@endo/test262-runner":"^0.1.48","@types/babel__traverse":"^7.20.5","ava":"^6.1.3","babel-eslint":"^10.1.0","c8":"^7.14.0","core-js":"^3.31.0","eslint":"^8.57.1","eslint-config-airbnb-base":"^15.0.0","eslint-config-prettier":"^9.1.0","eslint-plugin-eslint-comments":"^3.2.0","eslint-plugin-import":"^2.31.0","hermes-engine-cli":"^0.12.0","prettier":"^3.5.3","terser":"^5.16.6","tsd":"^0.31.2","typescript":"~5.8.3"},"files":["./*.d.ts","./*.js","./*.map","LICENSE*","SECURITY*","dist","lib","src","tools"],"publishConfig":{"access":"public"},"eslintConfig":{"extends":["plugin:@endo/ses"]},"ava":{"files":["test/**/*.test.*"],"timeout":"2m"},"typeCoverage":{"atLeast":81.17},"gitHead":"9815aea9541f241389d2135c6097a7442bdffa17","_lastModified":"2026-07-30T03:54:06.206Z"}
@@ -1 +1 @@
1
- {"name":"zod","version":"4.3.5","type":"module","license":"MIT","author":"Colin McDonnell <zod@colinhacks.com>","description":"TypeScript-first schema declaration and validation library with static type inference","homepage":"https://zod.dev","llms":"https://zod.dev/llms.txt","llmsFull":"https://zod.dev/llms-full.txt","mcpServer":"https://mcp.inkeep.com/zod/mcp","funding":"https://github.com/sponsors/colinhacks","sideEffects":false,"files":["src","**/*.js","**/*.mjs","**/*.cjs","**/*.d.ts","**/*.d.mts","**/*.d.cts","**/package.json"],"keywords":["typescript","schema","validation","type","inference"],"main":"./index.cjs","types":"./index.d.cts","module":"./index.js","zshy":{"exports":{"./package.json":"./package.json",".":"./src/index.ts","./mini":"./src/mini/index.ts","./locales":"./src/locales/index.ts","./v3":"./src/v3/index.ts","./v4":"./src/v4/index.ts","./v4-mini":"./src/v4-mini/index.ts","./v4/mini":"./src/v4/mini/index.ts","./v4/core":"./src/v4/core/index.ts","./v4/locales":"./src/v4/locales/index.ts","./v4/locales/*":"./src/v4/locales/*"},"conditions":{"@zod/source":"src"}},"exports":{"./package.json":"./package.json",".":{"@zod/source":"./src/index.ts","types":"./index.d.cts","import":"./index.js","require":"./index.cjs"},"./mini":{"@zod/source":"./src/mini/index.ts","types":"./mini/index.d.cts","import":"./mini/index.js","require":"./mini/index.cjs"},"./locales":{"@zod/source":"./src/locales/index.ts","types":"./locales/index.d.cts","import":"./locales/index.js","require":"./locales/index.cjs"},"./v3":{"@zod/source":"./src/v3/index.ts","types":"./v3/index.d.cts","import":"./v3/index.js","require":"./v3/index.cjs"},"./v4":{"@zod/source":"./src/v4/index.ts","types":"./v4/index.d.cts","import":"./v4/index.js","require":"./v4/index.cjs"},"./v4-mini":{"@zod/source":"./src/v4-mini/index.ts","types":"./v4-mini/index.d.cts","import":"./v4-mini/index.js","require":"./v4-mini/index.cjs"},"./v4/mini":{"@zod/source":"./src/v4/mini/index.ts","types":"./v4/mini/index.d.cts","import":"./v4/mini/index.js","require":"./v4/mini/index.cjs"},"./v4/core":{"@zod/source":"./src/v4/core/index.ts","types":"./v4/core/index.d.cts","import":"./v4/core/index.js","require":"./v4/core/index.cjs"},"./v4/locales":{"@zod/source":"./src/v4/locales/index.ts","types":"./v4/locales/index.d.cts","import":"./v4/locales/index.js","require":"./v4/locales/index.cjs"},"./v4/locales/*":{"@zod/source":"./src/v4/locales/*","types":"./v4/locales/*","import":"./v4/locales/*","require":"./v4/locales/*"}},"repository":{"type":"git","url":"git+https://github.com/colinhacks/zod.git"},"bugs":{"url":"https://github.com/colinhacks/zod/issues"},"support":{"backing":{"npm-funding":true}},"scripts":{"clean":"git clean -xdf . -e node_modules","build":"zshy --project tsconfig.build.json","postbuild":"tsx ../../scripts/write-stub-package-jsons.ts && pnpm biome check --write .","test:watch":"pnpm vitest","test":"pnpm vitest run","prepublishOnly":"tsx ../../scripts/check-versions.ts"},"_lastModified":"2026-07-28T07:40:09.044Z"}
1
+ {"name":"zod","version":"4.3.5","type":"module","license":"MIT","author":"Colin McDonnell <zod@colinhacks.com>","description":"TypeScript-first schema declaration and validation library with static type inference","homepage":"https://zod.dev","llms":"https://zod.dev/llms.txt","llmsFull":"https://zod.dev/llms-full.txt","mcpServer":"https://mcp.inkeep.com/zod/mcp","funding":"https://github.com/sponsors/colinhacks","sideEffects":false,"files":["src","**/*.js","**/*.mjs","**/*.cjs","**/*.d.ts","**/*.d.mts","**/*.d.cts","**/package.json"],"keywords":["typescript","schema","validation","type","inference"],"main":"./index.cjs","types":"./index.d.cts","module":"./index.js","zshy":{"exports":{"./package.json":"./package.json",".":"./src/index.ts","./mini":"./src/mini/index.ts","./locales":"./src/locales/index.ts","./v3":"./src/v3/index.ts","./v4":"./src/v4/index.ts","./v4-mini":"./src/v4-mini/index.ts","./v4/mini":"./src/v4/mini/index.ts","./v4/core":"./src/v4/core/index.ts","./v4/locales":"./src/v4/locales/index.ts","./v4/locales/*":"./src/v4/locales/*"},"conditions":{"@zod/source":"src"}},"exports":{"./package.json":"./package.json",".":{"@zod/source":"./src/index.ts","types":"./index.d.cts","import":"./index.js","require":"./index.cjs"},"./mini":{"@zod/source":"./src/mini/index.ts","types":"./mini/index.d.cts","import":"./mini/index.js","require":"./mini/index.cjs"},"./locales":{"@zod/source":"./src/locales/index.ts","types":"./locales/index.d.cts","import":"./locales/index.js","require":"./locales/index.cjs"},"./v3":{"@zod/source":"./src/v3/index.ts","types":"./v3/index.d.cts","import":"./v3/index.js","require":"./v3/index.cjs"},"./v4":{"@zod/source":"./src/v4/index.ts","types":"./v4/index.d.cts","import":"./v4/index.js","require":"./v4/index.cjs"},"./v4-mini":{"@zod/source":"./src/v4-mini/index.ts","types":"./v4-mini/index.d.cts","import":"./v4-mini/index.js","require":"./v4-mini/index.cjs"},"./v4/mini":{"@zod/source":"./src/v4/mini/index.ts","types":"./v4/mini/index.d.cts","import":"./v4/mini/index.js","require":"./v4/mini/index.cjs"},"./v4/core":{"@zod/source":"./src/v4/core/index.ts","types":"./v4/core/index.d.cts","import":"./v4/core/index.js","require":"./v4/core/index.cjs"},"./v4/locales":{"@zod/source":"./src/v4/locales/index.ts","types":"./v4/locales/index.d.cts","import":"./v4/locales/index.js","require":"./v4/locales/index.cjs"},"./v4/locales/*":{"@zod/source":"./src/v4/locales/*","types":"./v4/locales/*","import":"./v4/locales/*","require":"./v4/locales/*"}},"repository":{"type":"git","url":"git+https://github.com/colinhacks/zod.git"},"bugs":{"url":"https://github.com/colinhacks/zod/issues"},"support":{"backing":{"npm-funding":true}},"scripts":{"clean":"git clean -xdf . -e node_modules","build":"zshy --project tsconfig.build.json","postbuild":"tsx ../../scripts/write-stub-package-jsons.ts && pnpm biome check --write .","test:watch":"pnpm vitest","test":"pnpm vitest run","prepublishOnly":"tsx ../../scripts/check-versions.ts"},"_lastModified":"2026-07-30T03:54:08.193Z"}
@@ -12,6 +12,7 @@ export declare const DEFAULT_MOBILE_UI_LAYOUT_UID = "mobile-layout-model";
12
12
  export declare const DEFAULT_ADMIN_MULTI_PORTAL_UID = "__default_admin__";
13
13
  export declare const DEFAULT_MOBILE_MULTI_PORTAL_UID = "__default_mobile__";
14
14
  export type FlowSurfaceNavigationRequestRoles = readonly string[] | string;
15
+ type FlowSurfacePortalRouteScopeKind = 'layout' | 'portal';
15
16
  export type FlowSurfaceResolvedMultiPortal = {
16
17
  uid: string;
17
18
  title: string;
@@ -22,6 +23,8 @@ export type FlowSurfaceResolvedMultiPortal = {
22
23
  enabled: true;
23
24
  layoutUid: string;
24
25
  layoutType?: string;
26
+ portalType: 'no-code';
27
+ routeScopeKind: FlowSurfacePortalRouteScopeKind;
25
28
  };
26
29
  export type FlowSurfaceNavigationTarget = {
27
30
  kind: 'layout' | 'portal';
@@ -56,6 +59,7 @@ export declare class FlowSurfaceNavigationTargetsService {
56
59
  hasUiLayoutCapability(): boolean;
57
60
  normalizePortalUid(value: unknown): string;
58
61
  listNavigationTargets(currentRoles?: FlowSurfaceNavigationRequestRoles, transaction?: Transaction): Promise<FlowSurfaceNavigationTargetsResult>;
62
+ resolveDefaultPortal(options: Omit<PortalResolveOptions, 'path'>): Promise<FlowSurfaceResolvedMultiPortal>;
59
63
  resolvePortal(portalUidValue: unknown, options: PortalResolveOptions): Promise<FlowSurfaceResolvedMultiPortal>;
60
64
  readRoutePortalUids(routeId: unknown, transaction?: Transaction): Promise<string[]>;
61
65
  assertRouteBelongsToPortal(actionName: string, routeId: unknown, portalUid: string, path: string, transaction?: Transaction): Promise<void>;
@@ -64,7 +68,10 @@ export declare class FlowSurfaceNavigationTargetsService {
64
68
  transaction?: Transaction;
65
69
  }): Promise<void>;
66
70
  private listLayoutTargets;
71
+ private getEnabledPortalInventory;
67
72
  private listAccessiblePortalTargets;
73
+ private comparePortalPriority;
74
+ private getPortalPriority;
68
75
  private canAccessPortal;
69
76
  private collectRouteTreeIds;
70
77
  private removeDefaultLayoutRoutePermissions;
@@ -38,7 +38,6 @@ const DEFAULT_ADMIN_UI_LAYOUT_UID = "admin-layout-model";
38
38
  const DEFAULT_MOBILE_UI_LAYOUT_UID = "mobile-layout-model";
39
39
  const DEFAULT_ADMIN_MULTI_PORTAL_UID = "__default_admin__";
40
40
  const DEFAULT_MOBILE_MULTI_PORTAL_UID = "__default_mobile__";
41
- const RESERVED_MULTI_PORTAL_UIDS = /* @__PURE__ */ new Set([DEFAULT_ADMIN_MULTI_PORTAL_UID, DEFAULT_MOBILE_MULTI_PORTAL_UID]);
42
41
  function readRecordField(record, field) {
43
42
  if (!record || typeof record !== "object") {
44
43
  return void 0;
@@ -60,6 +59,9 @@ function normalizeRoles(currentRoles) {
60
59
  function normalizeRelationRouteId(routeId) {
61
60
  return routeId === null || typeof routeId === "undefined" ? void 0 : routeId;
62
61
  }
62
+ function isDefaultLayoutMultiPortalUid(uid) {
63
+ return uid === DEFAULT_ADMIN_MULTI_PORTAL_UID || uid === DEFAULT_MOBILE_MULTI_PORTAL_UID;
64
+ }
63
65
  class FlowSurfaceNavigationTargetsService {
64
66
  constructor(db) {
65
67
  this.db = db;
@@ -85,9 +87,17 @@ class FlowSurfaceNavigationTargetsService {
85
87
  return portalUid || void 0;
86
88
  }
87
89
  async listNavigationTargets(currentRoles, transaction) {
88
- const layoutTargets = await this.listLayoutTargets(transaction);
90
+ var _a;
91
+ let layoutTargets = await this.listLayoutTargets(transaction);
89
92
  const multiPortal = this.hasMultiPortalCapability();
90
- const portalTargets = multiPortal ? await this.listAccessiblePortalTargets(currentRoles, transaction) : [];
93
+ const portalInventory = multiPortal ? await this.getEnabledPortalInventory(currentRoles, transaction) : { accessible: [] };
94
+ const portalTargets = multiPortal ? await this.listAccessiblePortalTargets(portalInventory.accessible, transaction) : [];
95
+ if (multiPortal) {
96
+ layoutTargets = layoutTargets.map(({ default: _default, ...target }) => target);
97
+ if (portalInventory.accessible.length === 1 && ((_a = portalTargets[0]) == null ? void 0 : _a.uid) === readStringField(portalInventory.accessible[0], "uid")) {
98
+ portalTargets[0] = { ...portalTargets[0], default: true };
99
+ }
100
+ }
91
101
  return {
92
102
  version: "1",
93
103
  capabilities: {
@@ -96,6 +106,60 @@ class FlowSurfaceNavigationTargetsService {
96
106
  targets: [...layoutTargets, ...portalTargets]
97
107
  };
98
108
  }
109
+ async resolveDefaultPortal(options) {
110
+ if (!this.hasMultiPortalCapability()) {
111
+ (0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} requires the Multi-portal capability`, {
112
+ ruleId: "navigation-portal-unsupported",
113
+ path: "navigation"
114
+ });
115
+ }
116
+ const inventory = await this.getEnabledPortalInventory(options.currentRoles, options.transaction);
117
+ if (!inventory.enabled.length) {
118
+ (0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} requires an enabled portal`, {
119
+ ruleId: "navigation-portal-not-found",
120
+ path: "navigation",
121
+ details: {
122
+ uiBuilderAllowed: false,
123
+ adminLayoutFallbackAllowed: false,
124
+ agentInstruction: "Stop this Flow Surfaces write. Ask the user to create and enable a Portal before retrying; do not fall back to the Admin layout."
125
+ }
126
+ });
127
+ }
128
+ if (!inventory.accessible.length) {
129
+ (0, import_errors.throwForbidden)(
130
+ `flowSurfaces ${options.actionName} current roles cannot access an enabled portal`,
131
+ "FLOW_SURFACE_NAVIGATION_PORTAL_FORBIDDEN",
132
+ {
133
+ ruleId: "navigation-portal-forbidden",
134
+ path: "navigation"
135
+ }
136
+ );
137
+ }
138
+ if (inventory.accessible.length > 1) {
139
+ (0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} requires an explicit portal selection`, {
140
+ ruleId: "navigation-portal-selection-required",
141
+ path: "navigation",
142
+ details: {
143
+ candidates: inventory.accessible.map((portal) => {
144
+ const uid = readStringField(portal, "uid");
145
+ return {
146
+ uid,
147
+ portalName: readStringField(portal, "portalName") || null,
148
+ title: readStringField(portal, "title") || uid,
149
+ portalType: readStringField(portal, "portalType") || null
150
+ };
151
+ }),
152
+ uiBuilderAllowed: false,
153
+ adminLayoutFallbackAllowed: false,
154
+ agentInstruction: "Stop this Flow Surfaces write and ask the user to select one candidate before retrying; do not choose by Portal type or fall back to the Admin layout."
155
+ }
156
+ });
157
+ }
158
+ return this.resolvePortal(readStringField(inventory.accessible[0], "uid"), {
159
+ ...options,
160
+ path: "navigation"
161
+ });
162
+ }
99
163
  async resolvePortal(portalUidValue, options) {
100
164
  const portalUid = this.normalizePortalUid(portalUidValue);
101
165
  if (!portalUid) {
@@ -111,19 +175,9 @@ class FlowSurfaceNavigationTargetsService {
111
175
  details: { portalUid }
112
176
  });
113
177
  }
114
- if (RESERVED_MULTI_PORTAL_UIDS.has(portalUid)) {
115
- (0, import_errors.throwBadRequest)(
116
- `flowSurfaces ${options.actionName} ${options.path} cannot use reserved default portal uid '${portalUid}'; use layoutUid semantics instead`,
117
- {
118
- ruleId: "navigation-portal-reserved",
119
- path: options.path,
120
- details: { portalUid }
121
- }
122
- );
123
- }
124
178
  const portal = await this.db.getRepository("multiPortals").findOne({
125
179
  filter: { uid: portalUid },
126
- fields: ["uid", "title", "icon", "routeName", "routePath", "authCheck", "enabled", "uiLayoutUid"],
180
+ fields: ["uid", "title", "icon", "portalType", "portalName", "routePath", "authCheck", "enabled", "uiLayoutUid"],
127
181
  transaction: options.transaction
128
182
  });
129
183
  if (!portal) {
@@ -133,6 +187,18 @@ class FlowSurfaceNavigationTargetsService {
133
187
  details: { portalUid }
134
188
  });
135
189
  }
190
+ const routeScopeKind = isDefaultLayoutMultiPortalUid(portalUid) ? "layout" : "portal";
191
+ if (routeScopeKind === "portal" && !await this.canAccessPortal(portalUid, options.currentRoles, options.transaction)) {
192
+ (0, import_errors.throwForbidden)(
193
+ `flowSurfaces ${options.actionName} current roles cannot access portal '${portalUid}'`,
194
+ "FLOW_SURFACE_NAVIGATION_PORTAL_FORBIDDEN",
195
+ {
196
+ ruleId: "navigation-portal-forbidden",
197
+ path: options.path,
198
+ details: { portalUid }
199
+ }
200
+ );
201
+ }
136
202
  if (readRecordField(portal, "enabled") !== true) {
137
203
  (0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} portal '${portalUid}' is disabled`, {
138
204
  ruleId: "navigation-portal-disabled",
@@ -140,6 +206,33 @@ class FlowSurfaceNavigationTargetsService {
140
206
  details: { portalUid }
141
207
  });
142
208
  }
209
+ const portalType = readStringField(portal, "portalType");
210
+ if (portalType !== "no-code") {
211
+ if (portalType === "ai") {
212
+ (0, import_errors.throwBadRequest)(
213
+ `flowSurfaces ${options.actionName} cannot use UI Builder or Flow Surfaces for AI Portal '${portalUid}'; stop and implement the request in the Portal source code`,
214
+ {
215
+ ruleId: "navigation-portal-type-unsupported",
216
+ path: options.path,
217
+ details: {
218
+ portalUid,
219
+ portalType,
220
+ portalName: readStringField(portal, "portalName") || null,
221
+ expectedPortalType: "no-code",
222
+ uiBuilderAllowed: false,
223
+ adminLayoutFallbackAllowed: false,
224
+ implementationPath: "ai-portal-source",
225
+ agentInstruction: "Stop this Flow Surfaces write. Run nb portal info <portalName> -j to resolve localPath, then implement the request in that AI Portal source code. Do not retry with an Admin layout."
226
+ }
227
+ }
228
+ );
229
+ }
230
+ (0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} portal '${portalUid}' does not support no-code routes`, {
231
+ ruleId: "navigation-portal-type-unsupported",
232
+ path: options.path,
233
+ details: { portalUid, portalType: portalType || null }
234
+ });
235
+ }
143
236
  const layoutUid = readStringField(portal, "uiLayoutUid");
144
237
  if (!layoutUid || !this.db.getCollection("uiLayouts")) {
145
238
  (0, import_errors.throwBadRequest)(`flowSurfaces ${options.actionName} portal '${portalUid}' has no available backing UI layout`, {
@@ -173,27 +266,18 @@ class FlowSurfaceNavigationTargetsService {
173
266
  }
174
267
  );
175
268
  }
176
- if (!await this.canAccessPortal(portalUid, options.currentRoles, options.transaction)) {
177
- (0, import_errors.throwForbidden)(
178
- `flowSurfaces ${options.actionName} current roles cannot access portal '${portalUid}'`,
179
- "FLOW_SURFACE_NAVIGATION_PORTAL_FORBIDDEN",
180
- {
181
- ruleId: "navigation-portal-forbidden",
182
- path: options.path,
183
- details: { portalUid }
184
- }
185
- );
186
- }
187
269
  return {
188
270
  uid: portalUid,
189
271
  title: readStringField(portal, "title") || portalUid,
190
272
  icon: readStringField(portal, "icon") || null,
191
- routeName: readStringField(portal, "routeName"),
273
+ routeName: readStringField(portal, "portalName"),
192
274
  routePath: readStringField(portal, "routePath"),
193
275
  authCheck: readRecordField(portal, "authCheck") === true,
194
276
  enabled: true,
195
277
  layoutUid,
196
- layoutType: readStringField(layout, "layoutType")
278
+ layoutType: readStringField(layout, "layoutType"),
279
+ portalType,
280
+ routeScopeKind
197
281
  };
198
282
  }
199
283
  async readRoutePortalUids(routeId, transaction) {
@@ -293,43 +377,48 @@ class FlowSurfaceNavigationTargetsService {
293
377
  return left.uid.localeCompare(right.uid);
294
378
  });
295
379
  }
296
- async listAccessiblePortalTargets(currentRoles, transaction) {
380
+ async getEnabledPortalInventory(currentRoles, transaction) {
381
+ const enabled = await this.db.getRepository("multiPortals").find({
382
+ filter: { enabled: true },
383
+ fields: ["uid", "title", "icon", "portalType", "portalName", "routePath", "authCheck", "enabled", "uiLayoutUid"],
384
+ sort: ["uid"],
385
+ transaction
386
+ });
387
+ enabled.sort(
388
+ (left, right) => String(readStringField(left, "uid") || "").localeCompare(String(readStringField(right, "uid") || ""))
389
+ );
297
390
  const roles = normalizeRoles(currentRoles);
298
- let accessiblePortalUids;
299
- if (!roles.includes("root")) {
300
- if (!roles.length || !this.db.getCollection("rolesMultiPortals")) {
301
- return [];
302
- }
391
+ const isRoot = roles.includes("root");
392
+ const accessiblePortalUids = /* @__PURE__ */ new Set();
393
+ if (!isRoot && roles.length && this.db.getCollection("rolesMultiPortals")) {
303
394
  const grants = await this.db.getRepository("rolesMultiPortals").find({
304
- fields: ["multiPortalUid"],
305
395
  filter: { roleName: roles },
396
+ fields: ["multiPortalUid"],
306
397
  transaction
307
398
  });
308
- accessiblePortalUids = Array.from(
309
- new Set(
310
- grants.map((grant) => readStringField(grant, "multiPortalUid")).filter((portalUid) => !!portalUid)
311
- )
312
- );
313
- if (!accessiblePortalUids.length) {
314
- return [];
399
+ for (const grant of grants) {
400
+ const portalUid = readStringField(grant, "multiPortalUid");
401
+ if (portalUid) {
402
+ accessiblePortalUids.add(portalUid);
403
+ }
315
404
  }
316
405
  }
317
- const accessiblePortalUidSet = accessiblePortalUids ? new Set(accessiblePortalUids) : void 0;
318
- const filter = {
319
- enabled: true,
320
- "uid.$notIn": Array.from(RESERVED_MULTI_PORTAL_UIDS)
321
- };
322
- const portals = await this.db.getRepository("multiPortals").find({
323
- filter,
324
- fields: ["uid", "title", "icon", "routeName", "routePath", "authCheck", "enabled", "uiLayoutUid"],
325
- sort: ["uid"],
326
- transaction
406
+ const accessible = enabled.filter((portal) => {
407
+ const portalUid = readStringField(portal, "uid");
408
+ return !!portalUid && (isRoot || isDefaultLayoutMultiPortalUid(portalUid) || accessiblePortalUids.has(portalUid));
327
409
  });
410
+ return { enabled, accessible };
411
+ }
412
+ async listAccessiblePortalTargets(portals, transaction) {
328
413
  const targets = [];
329
414
  for (const portal of portals) {
330
415
  const portalUid = readStringField(portal, "uid");
331
416
  const layoutUid = readStringField(portal, "uiLayoutUid");
332
- if (!portalUid || accessiblePortalUidSet && !accessiblePortalUidSet.has(portalUid) || !layoutUid || !this.db.getCollection("uiLayouts")) {
417
+ const portalType = readStringField(portal, "portalType");
418
+ if (!portalUid || portalType !== "no-code") {
419
+ continue;
420
+ }
421
+ if (!layoutUid || !this.db.getCollection("uiLayouts")) {
333
422
  continue;
334
423
  }
335
424
  const layout = await this.db.getRepository("uiLayouts").findOne({
@@ -348,12 +437,30 @@ class FlowSurfaceNavigationTargetsService {
348
437
  icon: readStringField(portal, "icon") || null,
349
438
  layoutUid,
350
439
  layoutType: readStringField(layout, "layoutType"),
351
- routeName: readStringField(portal, "routeName"),
440
+ routeName: readStringField(portal, "portalName"),
352
441
  routePath: readStringField(portal, "routePath"),
353
442
  authCheck: readRecordField(portal, "authCheck") === true
354
443
  });
355
444
  }
356
- return targets;
445
+ return targets.sort((left, right) => this.comparePortalPriority(left, right));
446
+ }
447
+ comparePortalPriority(left, right) {
448
+ const priorityDelta = this.getPortalPriority(left) - this.getPortalPriority(right);
449
+ if (priorityDelta) {
450
+ return priorityDelta;
451
+ }
452
+ return String(readStringField(left, "uid") || "").localeCompare(String(readStringField(right, "uid") || ""));
453
+ }
454
+ getPortalPriority(portal) {
455
+ const uiLayout = readRecordField(portal, "uiLayout");
456
+ const layoutType = readStringField(portal, "layoutType") || readStringField(uiLayout, "layoutType");
457
+ if (readStringField(portal, "uid") === DEFAULT_ADMIN_MULTI_PORTAL_UID) {
458
+ return 0;
459
+ }
460
+ if (layoutType !== "mobile") {
461
+ return 1;
462
+ }
463
+ return 2;
357
464
  }
358
465
  async canAccessPortal(portalUid, currentRoles, transaction) {
359
466
  const roles = normalizeRoles(currentRoles);
@@ -96,6 +96,12 @@ export declare class FlowSurfacesService {
96
96
  private resolveDesktopRouteLayoutUidFilter;
97
97
  private assertNavigationTargetIsExclusive;
98
98
  private readDesktopRouteScope;
99
+ private buildDesktopRouteScopeForPortal;
100
+ private assertAdminLayoutCreateScope;
101
+ private getPortalRouteFilterUid;
102
+ private assertRouteBelongsToResolvedPortal;
103
+ private resolveDefaultDesktopRouteScope;
104
+ private resolveDesktopRouteMatchScope;
99
105
  private resolveRequestedDesktopRouteScope;
100
106
  private resolveExistingDesktopRouteScope;
101
107
  private attachDesktopRouteScopeToRouteAndChildren;
@@ -227,6 +233,7 @@ export declare class FlowSurfacesService {
227
233
  private dispatchPlanOnlyAction;
228
234
  private resolveApplyBlueprintReplaceTargetInfo;
229
235
  private resolveApplyBlueprintCreateNavigationGroup;
236
+ private resolveApplyBlueprintDefaultNavigationTarget;
230
237
  private normalizeApplyBlueprintCreateMobileNavigation;
231
238
  private assertApplyBlueprintCreateNavigationTarget;
232
239
  private resolveApplyBlueprintCreatePageIdentity;
@@ -1204,6 +1204,83 @@ class FlowSurfacesService {
1204
1204
  layoutUids: await this.readDesktopRouteUiLayoutUids(routeId, transaction)
1205
1205
  };
1206
1206
  }
1207
+ buildDesktopRouteScopeForPortal(portal) {
1208
+ return {
1209
+ portalUids: portal.routeScopeKind === "portal" ? [portal.uid] : [],
1210
+ layoutUids: portal.routeScopeKind === "layout" ? [portal.layoutUid] : [],
1211
+ selectedPortal: portal,
1212
+ layoutType: portal.layoutType
1213
+ };
1214
+ }
1215
+ assertAdminLayoutCreateScope(actionName, scope, path) {
1216
+ if (!this.navigationTargets.hasMultiPortalCapability() || scope.selectedPortal || !scope.layoutUids.includes(import_navigation_targets.DEFAULT_ADMIN_UI_LAYOUT_UID)) {
1217
+ return;
1218
+ }
1219
+ (0, import_errors.throwBadRequest)(`flowSurfaces ${actionName} cannot create navigation directly in the Admin layout`, {
1220
+ ruleId: "navigation-admin-layout-not-portal-target",
1221
+ path,
1222
+ details: {
1223
+ layoutUid: import_navigation_targets.DEFAULT_ADMIN_UI_LAYOUT_UID,
1224
+ uiBuilderAllowed: false,
1225
+ adminLayoutFallbackAllowed: false,
1226
+ agentInstruction: "Resolve the target Portal first. For a no-code Portal, pass portalUid (navigation.portalUid in applyBlueprint); for an AI Portal, stop this Flow Surfaces write and implement the request in the Portal source code."
1227
+ }
1228
+ });
1229
+ }
1230
+ getPortalRouteFilterUid(portal) {
1231
+ return (portal == null ? void 0 : portal.routeScopeKind) === "portal" ? portal.uid : void 0;
1232
+ }
1233
+ async assertRouteBelongsToResolvedPortal(actionName, route, portal, path, transaction) {
1234
+ if (portal.routeScopeKind === "layout") {
1235
+ await this.assertDesktopRouteBelongsToUiLayout(actionName, route, portal.layoutUid, path, transaction);
1236
+ return;
1237
+ }
1238
+ await this.navigationTargets.assertRouteBelongsToPortal(
1239
+ actionName,
1240
+ this.readRouteField(route, "id"),
1241
+ portal.uid,
1242
+ path,
1243
+ transaction
1244
+ );
1245
+ }
1246
+ async resolveDefaultDesktopRouteScope(actionName, options = {}) {
1247
+ if (this.navigationTargets.hasMultiPortalCapability()) {
1248
+ const portal = await this.navigationTargets.resolveDefaultPortal({
1249
+ actionName,
1250
+ currentRoles: options.currentRoles,
1251
+ transaction: options.transaction
1252
+ });
1253
+ return this.buildDesktopRouteScopeForPortal(portal);
1254
+ }
1255
+ const layoutUids = await this.resolveDefaultDesktopRouteUiLayoutUids(options.transaction);
1256
+ return {
1257
+ portalUids: [],
1258
+ layoutUids,
1259
+ layoutType: layoutUids.length === 1 ? await this.getDesktopRouteUiLayoutTypeByUid(layoutUids[0], options.transaction) : void 0
1260
+ };
1261
+ }
1262
+ async resolveDesktopRouteMatchScope(layoutUid, portalUid, actionName, options = {}) {
1263
+ if (portalUid) {
1264
+ const portal = await this.navigationTargets.resolvePortal(portalUid, {
1265
+ actionName,
1266
+ path: "values.navigation.portalUid",
1267
+ currentRoles: options.currentRoles,
1268
+ transaction: options.transaction
1269
+ });
1270
+ return {
1271
+ layoutUids: portal.routeScopeKind === "layout" ? [portal.layoutUid] : void 0,
1272
+ portalUid: this.getPortalRouteFilterUid(portal)
1273
+ };
1274
+ }
1275
+ if (this.normalizeDesktopRouteLayoutUidFilter(layoutUid).length) {
1276
+ return { layoutUids: layoutUid, portalUid: void 0 };
1277
+ }
1278
+ const defaultScope = await this.resolveDefaultDesktopRouteScope(actionName, options);
1279
+ return {
1280
+ layoutUids: defaultScope.layoutUids.length ? defaultScope.layoutUids : void 0,
1281
+ portalUid: this.getPortalRouteFilterUid(defaultScope.selectedPortal)
1282
+ };
1283
+ }
1207
1284
  async resolveRequestedDesktopRouteScope(values, parentRoute, actionName, options = {}) {
1208
1285
  const layoutUid = this.normalizeExplicitDesktopRouteLayoutUid(values.layoutUid);
1209
1286
  const portalUid = this.navigationTargets.normalizePortalUid(values.portalUid);
@@ -1219,22 +1296,19 @@ class FlowSurfacesService {
1219
1296
  transaction: options.transaction
1220
1297
  });
1221
1298
  if (parentRoute) {
1222
- await this.navigationTargets.assertRouteBelongsToPortal(
1299
+ await this.assertRouteBelongsToResolvedPortal(
1223
1300
  actionName,
1224
- this.readRouteField(parentRoute, "id"),
1225
- portal.uid,
1301
+ parentRoute,
1302
+ portal,
1226
1303
  "values.parentMenuRouteId",
1227
1304
  options.transaction
1228
1305
  );
1229
1306
  }
1230
- return {
1231
- portalUids: [portal.uid],
1232
- layoutUids: [],
1233
- selectedPortal: portal,
1234
- layoutType: portal.layoutType
1235
- };
1307
+ return this.buildDesktopRouteScopeForPortal(portal);
1236
1308
  }
1237
1309
  if (layoutUid) {
1310
+ const scope2 = { portalUids: [], layoutUids: [layoutUid] };
1311
+ this.assertAdminLayoutCreateScope(actionName, scope2, "values.layoutUid");
1238
1312
  await this.assertEnabledDesktopRouteUiLayoutUid(layoutUid, actionName, "values.layoutUid", options.transaction);
1239
1313
  if (parentRoute) {
1240
1314
  await this.assertDesktopRouteBelongsToUiLayout(
@@ -1246,8 +1320,7 @@ class FlowSurfacesService {
1246
1320
  );
1247
1321
  }
1248
1322
  return {
1249
- portalUids: [],
1250
- layoutUids: [layoutUid],
1323
+ ...scope2,
1251
1324
  layoutType: await this.getDesktopRouteUiLayoutTypeByUid(layoutUid, options.transaction)
1252
1325
  };
1253
1326
  }
@@ -1280,10 +1353,17 @@ class FlowSurfacesService {
1280
1353
  };
1281
1354
  }
1282
1355
  }
1356
+ if (!parentRoute) {
1357
+ return this.resolveDefaultDesktopRouteScope(actionName, options);
1358
+ }
1283
1359
  const layoutUids = await this.resolveInheritedDesktopRouteUiLayoutUids(parentRoute, options.transaction);
1284
- return {
1360
+ const scope = {
1285
1361
  portalUids: [],
1286
- layoutUids,
1362
+ layoutUids
1363
+ };
1364
+ this.assertAdminLayoutCreateScope(actionName, scope, "values.parentMenuRouteId");
1365
+ return {
1366
+ ...scope,
1287
1367
  layoutType: layoutUids.length === 1 ? await this.getDesktopRouteUiLayoutTypeByUid(layoutUids[0], options.transaction) : void 0
1288
1368
  };
1289
1369
  }
@@ -1302,20 +1382,18 @@ class FlowSurfacesService {
1302
1382
  currentRoles: options.currentRoles,
1303
1383
  transaction: options.transaction
1304
1384
  });
1305
- await this.navigationTargets.assertRouteBelongsToPortal(
1385
+ await this.assertRouteBelongsToResolvedPortal(
1306
1386
  actionName,
1307
- this.readRouteField(route, "id"),
1308
- portal.uid,
1387
+ route,
1388
+ portal,
1309
1389
  "values.menuRouteId",
1310
1390
  options.transaction
1311
1391
  );
1312
- return {
1313
- ...routeScope,
1314
- selectedPortal: portal,
1315
- layoutType: portal.layoutType
1316
- };
1392
+ return this.buildDesktopRouteScopeForPortal(portal);
1317
1393
  }
1318
1394
  if (layoutUid) {
1395
+ const scope2 = { ...routeScope, layoutUids: [layoutUid] };
1396
+ this.assertAdminLayoutCreateScope(actionName, scope2, "values.layoutUid");
1319
1397
  await this.assertEnabledDesktopRouteUiLayoutUid(layoutUid, actionName, "values.layoutUid", options.transaction);
1320
1398
  await this.assertDesktopRouteBelongsToUiLayout(
1321
1399
  actionName,
@@ -1325,7 +1403,7 @@ class FlowSurfacesService {
1325
1403
  options.transaction
1326
1404
  );
1327
1405
  return {
1328
- ...routeScope,
1406
+ ...scope2,
1329
1407
  layoutType: await this.getDesktopRouteUiLayoutTypeByUid(layoutUid, options.transaction)
1330
1408
  };
1331
1409
  }
@@ -1355,10 +1433,22 @@ class FlowSurfacesService {
1355
1433
  layoutType: portal.layoutType
1356
1434
  };
1357
1435
  }
1358
- const layoutUids = routeScope.layoutUids.length ? routeScope.layoutUids : await this.ensureDesktopRouteUiLayouts(route, options.transaction);
1359
- return {
1436
+ let layoutUids = routeScope.layoutUids;
1437
+ if (!layoutUids.length) {
1438
+ const parentRouteId = this.readRouteField(route, "parentId");
1439
+ const parentRoute = import_lodash.default.isNil(parentRouteId) ? null : await this.findMenuRouteById(parentRouteId, options.transaction);
1440
+ layoutUids = await this.resolveInheritedDesktopRouteUiLayoutUids(parentRoute, options.transaction);
1441
+ }
1442
+ const scope = {
1360
1443
  portalUids: [],
1361
- layoutUids,
1444
+ layoutUids
1445
+ };
1446
+ this.assertAdminLayoutCreateScope(actionName, scope, "values.menuRouteId");
1447
+ if (!routeScope.layoutUids.length) {
1448
+ await this.setDesktopRouteUiLayouts(this.readRouteField(route, "id"), layoutUids, options.transaction);
1449
+ }
1450
+ return {
1451
+ ...scope,
1362
1452
  layoutType: layoutUids.length === 1 ? await this.getDesktopRouteUiLayoutTypeByUid(layoutUids[0], options.transaction) : void 0
1363
1453
  };
1364
1454
  }
@@ -1518,7 +1608,6 @@ class FlowSurfacesService {
1518
1608
  }
1519
1609
  }
1520
1610
  async createFlowMenuGroup(values, options = {}) {
1521
- var _a;
1522
1611
  const parentRoute = await this.assertMenuParentIsGroup(values.parentMenuRouteId, options.transaction);
1523
1612
  const parentId = this.readRouteField(parentRoute, "id") ?? null;
1524
1613
  const routeScope = await this.resolveRequestedDesktopRouteScope(values, parentRoute, "createMenu", options);
@@ -1528,7 +1617,7 @@ class FlowSurfacesService {
1528
1617
  title,
1529
1618
  options.transaction,
1530
1619
  routeScope.layoutUids,
1531
- (_a = routeScope.selectedPortal) == null ? void 0 : _a.uid
1620
+ this.getPortalRouteFilterUid(routeScope.selectedPortal)
1532
1621
  );
1533
1622
  if (existingGroups.length === 1) {
1534
1623
  return this.buildMenuResult(existingGroups[0]);
@@ -3569,7 +3658,7 @@ class FlowSurfacesService {
3569
3658
  tabs
3570
3659
  };
3571
3660
  }
3572
- async resolveApplyBlueprintCreateNavigationGroup(document, transaction) {
3661
+ async resolveApplyBlueprintCreateNavigationGroup(document, options = {}) {
3573
3662
  var _a;
3574
3663
  if (document.mode !== "create" || !import_lodash.default.isPlainObject((_a = document.navigation) == null ? void 0 : _a.group)) {
3575
3664
  return document;
@@ -3583,12 +3672,13 @@ class FlowSurfacesService {
3583
3672
  }
3584
3673
  const layoutUid = this.normalizeExplicitDesktopRouteLayoutUid(document.navigation.layoutUid);
3585
3674
  const portalUid = this.navigationTargets.normalizePortalUid(document.navigation.portalUid);
3675
+ const matchScope = await this.resolveDesktopRouteMatchScope(layoutUid, portalUid, "applyBlueprint", options);
3586
3676
  const matchedRoutes = await this.findMenuGroupRoutesByParentIdAndTitle(
3587
3677
  null,
3588
3678
  groupTitle,
3589
- transaction,
3590
- layoutUid || await this.resolveDefaultDesktopRouteUiLayoutUids(transaction),
3591
- portalUid
3679
+ options.transaction,
3680
+ matchScope.layoutUids,
3681
+ matchScope.portalUid
3592
3682
  );
3593
3683
  if (!matchedRoutes.length) {
3594
3684
  return document;
@@ -3614,6 +3704,24 @@ class FlowSurfacesService {
3614
3704
  }
3615
3705
  };
3616
3706
  }
3707
+ async resolveApplyBlueprintDefaultNavigationTarget(document, options = {}) {
3708
+ var _a, _b, _c, _d;
3709
+ if (document.mode !== "create" || ((_a = document.navigation) == null ? void 0 : _a.layoutUid) || ((_b = document.navigation) == null ? void 0 : _b.portalUid) || !import_lodash.default.isUndefined((_d = (_c = document.navigation) == null ? void 0 : _c.group) == null ? void 0 : _d.routeId) || !this.navigationTargets.hasMultiPortalCapability()) {
3710
+ return document;
3711
+ }
3712
+ const portal = await this.navigationTargets.resolveDefaultPortal({
3713
+ actionName: "applyBlueprint",
3714
+ currentRoles: options.currentRoles,
3715
+ transaction: options.transaction
3716
+ });
3717
+ return {
3718
+ ...document,
3719
+ navigation: {
3720
+ ...document.navigation,
3721
+ portalUid: portal.uid
3722
+ }
3723
+ };
3724
+ }
3617
3725
  async normalizeApplyBlueprintCreateMobileNavigation(document, options = {}) {
3618
3726
  var _a, _b;
3619
3727
  if (document.mode !== "create") {
@@ -3669,15 +3777,20 @@ class FlowSurfacesService {
3669
3777
  return;
3670
3778
  }
3671
3779
  const groupRoute2 = await this.assertMenuParentIsGroup(groupRouteId2, options.transaction);
3672
- await this.navigationTargets.assertRouteBelongsToPortal(
3780
+ await this.assertRouteBelongsToResolvedPortal(
3673
3781
  "applyBlueprint",
3674
- this.readRouteField(groupRoute2, "id"),
3675
- portal.uid,
3782
+ groupRoute2,
3783
+ portal,
3676
3784
  "values.navigation.group.routeId",
3677
3785
  options.transaction
3678
3786
  );
3679
3787
  return;
3680
3788
  }
3789
+ this.assertAdminLayoutCreateScope(
3790
+ "applyBlueprint",
3791
+ { portalUids: [], layoutUids: [layoutUid] },
3792
+ "values.navigation.layoutUid"
3793
+ );
3681
3794
  await this.assertEnabledDesktopRouteUiLayoutUid(
3682
3795
  layoutUid,
3683
3796
  "applyBlueprint",
@@ -3700,7 +3813,7 @@ class FlowSurfacesService {
3700
3813
  options.transaction
3701
3814
  );
3702
3815
  }
3703
- async resolveApplyBlueprintCreatePageIdentity(document, transaction) {
3816
+ async resolveApplyBlueprintCreatePageIdentity(document, options = {}) {
3704
3817
  var _a, _b, _c, _d, _e, _f, _g;
3705
3818
  if (document.mode !== "create") {
3706
3819
  return document;
@@ -3713,12 +3826,10 @@ class FlowSurfacesService {
3713
3826
  if (!pageTitle || !hasGroupRouteId && !layoutUid && !portalUid) {
3714
3827
  return document;
3715
3828
  }
3716
- const groupRoute = hasGroupRouteId ? await this.findMenuRouteById(groupRouteId, transaction) : null;
3829
+ const groupRoute = hasGroupRouteId ? await this.findMenuRouteById(groupRouteId, options.transaction) : null;
3830
+ const groupScope = groupRoute && !portalUid ? await this.readDesktopRouteScope(groupRoute, options.transaction) : void 0;
3717
3831
  if (!portalUid && groupRoute) {
3718
- const groupPortalUids = await this.navigationTargets.readRoutePortalUids(
3719
- this.readRouteField(groupRoute, "id"),
3720
- transaction
3721
- );
3832
+ const groupPortalUids = (groupScope == null ? void 0 : groupScope.portalUids) || [];
3722
3833
  if (groupPortalUids.length > 1) {
3723
3834
  (0, import_errors.throwBadRequest)(
3724
3835
  `flowSurfaces applyBlueprint navigation.group.routeId '${groupRouteId}' belongs to multiple portals; pass navigation.portalUid explicitly`,
@@ -3734,13 +3845,23 @@ class FlowSurfacesService {
3734
3845
  }
3735
3846
  portalUid = groupPortalUids[0];
3736
3847
  }
3737
- const layoutUidFilter = portalUid ? void 0 : layoutUid || (groupRoute ? await this.resolveInheritedDesktopRouteUiLayoutUids(groupRoute, transaction) : void 0);
3848
+ const portalMatchScope = portalUid ? await this.resolveDesktopRouteMatchScope(layoutUid, portalUid, "applyBlueprint", options) : void 0;
3849
+ let layoutUidFilter = (portalMatchScope == null ? void 0 : portalMatchScope.layoutUids) || layoutUid;
3850
+ if (!portalMatchScope && !layoutUid && groupRoute) {
3851
+ layoutUidFilter = (groupScope == null ? void 0 : groupScope.layoutUids.length) ? groupScope.layoutUids : await this.resolveInheritedDesktopRouteUiLayoutUids(groupRoute, options.transaction);
3852
+ this.assertAdminLayoutCreateScope(
3853
+ "applyBlueprint",
3854
+ { portalUids: [], layoutUids: this.normalizeDesktopRouteLayoutUidFilter(layoutUidFilter) },
3855
+ "values.navigation.group.routeId"
3856
+ );
3857
+ }
3858
+ const portalUidFilter = portalMatchScope == null ? void 0 : portalMatchScope.portalUid;
3738
3859
  const matchedPages = await this.findFlowPageRoutesByParentIdAndTitle(
3739
3860
  hasGroupRouteId ? groupRouteId : null,
3740
3861
  pageTitle,
3741
- transaction,
3862
+ options.transaction,
3742
3863
  layoutUidFilter,
3743
- portalUid
3864
+ portalUidFilter
3744
3865
  );
3745
3866
  if (!matchedPages.length) {
3746
3867
  return document;
@@ -3769,14 +3890,15 @@ class FlowSurfacesService {
3769
3890
  }
3770
3891
  async prepareApplyBlueprintRequest(values, options = {}, createdKanbanSortFields) {
3771
3892
  var _a;
3772
- const initialDocument = (0, import_blueprint.prepareFlowSurfaceApplyBlueprintDocument)(values);
3893
+ const parsedDocument = (0, import_blueprint.prepareFlowSurfaceApplyBlueprintDocument)(values);
3894
+ const initialDocument = await this.resolveApplyBlueprintDefaultNavigationTarget(parsedDocument, options);
3773
3895
  await this.assertApplyBlueprintCreateNavigationTarget(initialDocument, options);
3774
3896
  const mobileNormalizedDocument = await this.normalizeApplyBlueprintCreateMobileNavigation(initialDocument, options);
3775
3897
  const groupResolvedDocument = await this.resolveApplyBlueprintCreateNavigationGroup(
3776
3898
  mobileNormalizedDocument,
3777
- options.transaction
3899
+ options
3778
3900
  );
3779
- const document = await this.resolveApplyBlueprintCreatePageIdentity(groupResolvedDocument, options.transaction);
3901
+ const document = await this.resolveApplyBlueprintCreatePageIdentity(groupResolvedDocument, options);
3780
3902
  await this.prepareApplyBlueprintKanbanBlocks(document, options.transaction, createdKanbanSortFields);
3781
3903
  const replaceTarget = document.mode === "replace" && document.target ? await this.resolveApplyBlueprintReplaceTargetInfo(document.target.pageSchemaUid, options.transaction) : void 0;
3782
3904
  if (document.mode === "replace" && document.tabs.length > 1 && typeof ((_a = document.page) == null ? void 0 : _a.enableTabs) === "undefined" && (replaceTarget == null ? void 0 : replaceTarget.pageEnableTabs) === false) {
@@ -4445,7 +4567,18 @@ class FlowSurfacesService {
4445
4567
  await (0, import_authoring_validation.assertFlowSurfaceAuthoringPayload)("applyBlueprint", values, {
4446
4568
  transaction: options.transaction,
4447
4569
  enabledPackages,
4448
- findMenuGroupRoutesByTitle: (title, transaction, targetLayoutUid, targetPortalUid) => this.findMenuGroupRoutesByTitle(title, transaction, targetLayoutUid, targetPortalUid),
4570
+ findMenuGroupRoutesByTitle: async (title, transaction, targetLayoutUid, targetPortalUid) => {
4571
+ const matchScope = await this.resolveDesktopRouteMatchScope(
4572
+ targetLayoutUid,
4573
+ targetPortalUid,
4574
+ "applyBlueprint",
4575
+ {
4576
+ transaction,
4577
+ currentRoles: options.currentRoles
4578
+ }
4579
+ );
4580
+ return this.findMenuGroupRoutesByTitle(title, transaction, matchScope.layoutUids, matchScope.portalUid);
4581
+ },
4449
4582
  getUiLayoutTypeByUid: (layoutUid2, transaction) => this.getDesktopRouteUiLayoutTypeByUid(layoutUid2, transaction),
4450
4583
  getPortalLayoutTypeByUid: async (targetPortalUid, transaction) => {
4451
4584
  if ((resolvedPortal == null ? void 0 : resolvedPortal.uid) === targetPortalUid) {
@@ -6515,18 +6648,18 @@ class FlowSurfacesService {
6515
6648
  currentRoles: options.currentRoles,
6516
6649
  transaction: options.transaction
6517
6650
  });
6518
- await this.navigationTargets.assertRouteBelongsToPortal(
6651
+ await this.assertRouteBelongsToResolvedPortal(
6519
6652
  "updateMenu",
6520
- this.readRouteField(route, "id"),
6521
- portal.uid,
6653
+ route,
6654
+ portal,
6522
6655
  "values.menuRouteId",
6523
6656
  options.transaction
6524
6657
  );
6525
6658
  if (nextParentRoute) {
6526
- await this.navigationTargets.assertRouteBelongsToPortal(
6659
+ await this.assertRouteBelongsToResolvedPortal(
6527
6660
  "updateMenu",
6528
- this.readRouteField(nextParentRoute, "id"),
6529
- portal.uid,
6661
+ nextParentRoute,
6662
+ portal,
6530
6663
  "values.parentMenuRouteId",
6531
6664
  options.transaction
6532
6665
  );
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "displayName.zh-CN": "前端流引擎",
5
5
  "description": "",
6
6
  "description.zh-CN": "",
7
- "version": "2.2.0-alpha.11",
7
+ "version": "2.2.0-alpha.12",
8
8
  "main": "./dist/server/index.js",
9
9
  "license": "Apache-2.0",
10
10
  "devDependencies": {
@@ -26,5 +26,5 @@
26
26
  "@nocobase/test": "2.x",
27
27
  "@nocobase/utils": "2.x"
28
28
  },
29
- "gitHead": "c9ff1f51e5c33bc6437b64eb779212be71b78f58"
29
+ "gitHead": "96ae69754a6398de63037ead3a4aad5e5d0ab918"
30
30
  }