@gadmin2n/schematics 0.0.107 → 0.0.109

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.
Files changed (45) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/.types.d.ts +8 -9
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/AgendaJob.ts +17 -15
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Audit.ts +13 -17
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Event.ts +48 -17
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Game.ts +1 -2
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/ITActivityDay.ts +14 -18
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Log.ts +0 -1
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Page.ts +42 -18
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/PageResource.ts +28 -18
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Resource.ts +42 -18
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Role.ts +1 -2
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/RolePages.ts +14 -18
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/RoleResource.ts +28 -18
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/SavedQuery.ts +13 -17
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/User.ts +14 -18
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/WorkflowEventOutbox.ts +17 -17
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/WorkflowNodeInstance.ts +9 -14
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/WorkflowNodeType.ts +9 -14
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +1 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +2 -2
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +7 -1
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/App.tsx +75 -71
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/BulkActions.tsx +36 -6
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/ListPageHeader.tsx +41 -14
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/RowActions.tsx +153 -144
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/inspectorActions.ts +3 -3
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/agentAllowed.tsx +35 -0
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/env.ts +2 -2
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/DevShell.tsx +8 -2
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/http.ts +20 -1
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/list.tsx +48 -0
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +40 -2
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +3 -2
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/list.tsx +6 -6
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasCell.tsx +4 -3
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +4 -3
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +99 -5
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +28 -30
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/CanvasAiModal.tsx +80 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/game/list.tsx +6 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +2 -1
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +2 -1
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +2 -1
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/list.tsx +6 -6
  45. package/package.json +1 -1
@@ -9,19 +9,18 @@ import {
9
9
  } from '@refinedev/antd';
10
10
  import { RowAction } from '../hooks/types';
11
11
  import { whenIsTrue } from '@gadmin2n/react-common';
12
+ import {
13
+ actionsToShowFlags,
14
+ interpolateActionUrl,
15
+ splitVisibleActions,
16
+ } from '../helpers/list';
12
17
 
13
18
  export interface RowActionsRenderContext<T = any> {
14
- /** Current record */
15
19
  record: T;
16
- /** Record ID */
17
20
  recordId: number | string;
18
- /** Translation function */
19
21
  t: (key: string, defaultValue?: string) => string;
20
- /** Resource name */
21
22
  resourceName: string;
22
- /** Update handler */
23
23
  onUpdate: (values: any) => void;
24
- /** Is update loading */
25
24
  updateLoading: boolean;
26
25
  }
27
26
 
@@ -30,8 +29,13 @@ export interface RowActionsProps<T = any> {
30
29
  record: T;
31
30
  /** Record ID field name */
32
31
  idField?: keyof T;
33
- /** Row actions from config */
32
+ /**
33
+ * 行操作的 actions 数组(来自 config 的 rowActions.actions)。
34
+ * actions 是按钮显示与否、按钮顺序的唯一真相源。
35
+ */
34
36
  actions?: RowAction[];
37
+ /** 主按钮可见数量;超出部分塞入 More dropdown */
38
+ visibleNum?: number;
35
39
  /** Handler for row update */
36
40
  onUpdate?: (id: number, values: any) => void;
37
41
  /** Is update loading */
@@ -40,15 +44,12 @@ export interface RowActionsProps<T = any> {
40
44
  t?: (key: string, defaultValue?: string) => string;
41
45
  /** Resource name for i18n keys */
42
46
  resourceName?: string;
43
- /** Show edit button */
47
+ /** 强制显式覆盖:传 boolean 时优先于 actions 推导值 */
44
48
  showEdit?: boolean;
45
- /** Show detail/show button */
46
49
  showDetail?: boolean;
47
- /** Show clone button */
48
50
  showClone?: boolean;
49
- /** Show delete button */
50
51
  showDelete?: boolean;
51
- /** Show history/log button */
52
+ /** History/Log button —— actions 数组里没有,需要通过 LogButton 或显式 showHistory 开启 */
52
53
  showHistory?: boolean;
53
54
  /** Custom children - if provided, replaces entire default UI */
54
55
  children?:
@@ -56,17 +57,11 @@ export interface RowActionsProps<T = any> {
56
57
  | ((context: RowActionsRenderContext<T>) => React.ReactNode);
57
58
 
58
59
  // Render props for fine-grained customization
59
- /** Custom render for edit button */
60
60
  renderEditButton?: (recordId: number | string) => React.ReactNode;
61
- /** Custom render for detail/show button */
62
61
  renderDetailButton?: (recordId: number | string) => React.ReactNode;
63
- /** Custom render for clone button */
64
62
  renderCloneButton?: (recordId: number | string) => React.ReactNode;
65
- /** Custom render for delete button */
66
63
  renderDeleteButton?: (recordId: number | string) => React.ReactNode;
67
- /** Custom render for history/log button */
68
64
  renderHistoryButton?: (recordId: number | string) => React.ReactNode;
69
- /** Custom render for status action buttons */
70
65
  renderStatusAction?: (
71
66
  action: RowAction,
72
67
  record: T,
@@ -74,60 +69,32 @@ export interface RowActionsProps<T = any> {
74
69
  loading: boolean,
75
70
  disabled: boolean,
76
71
  ) => React.ReactNode;
77
- /** Custom render for more dropdown trigger */
78
72
  renderMoreTrigger?: () => React.ReactNode;
79
- /** Extra dropdown items to add */
73
+ /** 额外塞入 More dropdown 的菜单项 */
80
74
  extraDropdownItems?: Array<{ key: string; label: React.ReactNode }>;
81
- /** Extra primary actions (rendered before More dropdown) */
75
+ /** 主按钮区追加的内容(在 More dropdown 之前) */
82
76
  extraPrimaryActions?: React.ReactNode;
83
- /** Log button component (optional, for backward compatibility) */
77
+ /** Log button component */
84
78
  LogButton?: React.ComponentType<any>;
85
79
  }
86
80
 
87
81
  /**
88
- * Row actions component with edit, show, and more dropdown
89
- *
90
- * @example
91
- * // Basic usage
92
- * <RowActions
93
- * record={record}
94
- * actions={tableConfig.rowActions.actions}
95
- * t={t}
96
- * resourceName="audit"
97
- * />
98
- *
99
- * @example
100
- * // With custom render props
101
- * <RowActions
102
- * {...props}
103
- * renderEditButton={(id) => <MyEditButton id={id} />}
104
- * renderDeleteButton={(id) => <MyDeleteButton id={id} />}
105
- * />
106
- *
107
- * @example
108
- * // Completely custom UI using children
109
- * <RowActions record={record}>
110
- * {({ record, recordId, onUpdate }) => (
111
- * <MyCustomActionsUI
112
- * record={record}
113
- * onEdit={() => navigate(`/edit/${recordId}`)}
114
- * onUpdate={onUpdate}
115
- * />
116
- * )}
117
- * </RowActions>
82
+ * 行操作组件:按 config.rowActions.actions 数组顺序渲染按钮,
83
+ * 前 visibleNum 个为主按钮,其余进 More dropdown。
118
84
  */
119
85
  export function RowActions<T extends { id: number | string }>({
120
86
  record,
121
87
  idField = 'id' as keyof T,
122
88
  actions = [],
89
+ visibleNum,
123
90
  onUpdate,
124
91
  updateLoading = false,
125
92
  t = (key, defaultValue) => defaultValue || key,
126
93
  resourceName = '',
127
- showEdit = true,
128
- showDetail = true,
129
- showClone = true,
130
- showDelete = true,
94
+ showEdit,
95
+ showDetail,
96
+ showClone,
97
+ showDelete,
131
98
  showHistory = false,
132
99
  children,
133
100
  renderEditButton,
@@ -152,40 +119,35 @@ export function RowActions<T extends { id: number | string }>({
152
119
  updateLoading,
153
120
  };
154
121
 
155
- // If children is a function, call it with context
156
122
  if (typeof children === 'function') {
157
123
  return <>{children(context)}</>;
158
124
  }
159
-
160
- // If children is provided as ReactNode, render it directly
161
125
  if (children) {
162
126
  return <>{children}</>;
163
127
  }
164
128
 
165
- // Default rendering
166
- const statusActions = actions.filter(
167
- (action) => action.action === 'CHANGE_STATUS',
168
- );
129
+ // actions 是显示与否的唯一真相源;显式 show* 可强制覆盖
130
+ const flags = actionsToShowFlags(actions);
131
+ const resolvedShowEdit = showEdit ?? flags.showEdit;
132
+ const resolvedShowDetail = showDetail ?? flags.showDetail;
133
+ const resolvedShowClone = showClone ?? flags.showClone;
134
+ const resolvedShowDelete = showDelete ?? flags.showDelete;
169
135
 
170
- // Default button renderers
136
+ // ---------- 默认按钮渲染器 ----------
171
137
  const defaultEditButton = (id: number | string) => (
172
138
  <EditButton icon={false} type="text" size="small" recordItemId={id} />
173
139
  );
174
-
175
140
  const defaultDetailButton = (id: number | string) => (
176
141
  <ShowButton icon={false} type="text" size="small" recordItemId={id}>
177
142
  {t('table.detail', 'Detail')}
178
143
  </ShowButton>
179
144
  );
180
-
181
145
  const defaultCloneButton = (id: number | string) => (
182
146
  <CloneButton icon={false} type="text" size="small" recordItemId={id} />
183
147
  );
184
-
185
148
  const defaultDeleteButton = (id: number | string) => (
186
149
  <DeleteButton icon={false} type="text" size="small" recordItemId={id} />
187
150
  );
188
-
189
151
  const defaultHistoryButton = (id: number | string) =>
190
152
  LogButton ? (
191
153
  <LogButton
@@ -196,7 +158,6 @@ export function RowActions<T extends { id: number | string }>({
196
158
  recordItemId={id}
197
159
  />
198
160
  ) : null;
199
-
200
161
  const defaultStatusAction = (
201
162
  action: RowAction,
202
163
  _record: T,
@@ -217,7 +178,6 @@ export function RowActions<T extends { id: number | string }>({
217
178
  </Button>
218
179
  </Popconfirm>
219
180
  );
220
-
221
181
  const defaultMoreTrigger = () => (
222
182
  <Button type="text" size="small">
223
183
  {t('table.more', 'More')}
@@ -225,87 +185,136 @@ export function RowActions<T extends { id: number | string }>({
225
185
  </Button>
226
186
  );
227
187
 
228
- // Build dropdown items
229
- const dropdownItems = [
230
- // Status change actions
231
- ...statusActions.map((action) => {
232
- const disabled = !whenIsTrue(action.when, record);
233
- const onConfirm = () => onUpdate?.(Number(recordId), action.values);
188
+ // ---------- actions 顺序构造可渲染项 ----------
189
+ type Renderable = { key: string; node: React.ReactNode };
234
190
 
235
- return {
236
- key: action.desc,
237
- label: renderStatusAction
238
- ? renderStatusAction(
239
- action,
240
- record,
241
- onConfirm,
242
- updateLoading,
243
- disabled,
244
- )
245
- : defaultStatusAction(
246
- action,
247
- record,
248
- onConfirm,
249
- updateLoading,
250
- disabled,
251
- ),
252
- };
253
- }),
254
- // Clone button
255
- ...(showClone
256
- ? [
257
- {
258
- key: 'clone',
259
- label: renderCloneButton
260
- ? renderCloneButton(recordId)
261
- : defaultCloneButton(recordId),
262
- },
263
- ]
264
- : []),
265
- // History/Log button
266
- ...(showHistory || LogButton
267
- ? [
268
- {
269
- key: 'history',
270
- label: renderHistoryButton
271
- ? renderHistoryButton(recordId)
272
- : defaultHistoryButton(recordId),
273
- },
274
- ]
275
- : []),
276
- // Delete button
277
- ...(showDelete
278
- ? [
279
- {
280
- key: 'delete',
281
- label: renderDeleteButton
282
- ? renderDeleteButton(recordId)
283
- : defaultDeleteButton(recordId),
284
- },
285
- ]
286
- : []),
287
- // Extra items
191
+ const renderAction = (action: RowAction, idx: number): Renderable | null => {
192
+ const whenOk = action.when == null ? true : whenIsTrue(action.when, record);
193
+ if (!whenOk) return null;
194
+
195
+ const key = `${action.action}-${action.desc}-${idx}`;
196
+ switch (action.action) {
197
+ case 'EDIT':
198
+ return resolvedShowEdit
199
+ ? {
200
+ key,
201
+ node: renderEditButton
202
+ ? renderEditButton(recordId)
203
+ : defaultEditButton(recordId),
204
+ }
205
+ : null;
206
+ case 'DETAIL':
207
+ return resolvedShowDetail
208
+ ? {
209
+ key,
210
+ node: renderDetailButton
211
+ ? renderDetailButton(recordId)
212
+ : defaultDetailButton(recordId),
213
+ }
214
+ : null;
215
+ case 'DELETE':
216
+ return resolvedShowDelete
217
+ ? {
218
+ key,
219
+ node: renderDeleteButton
220
+ ? renderDeleteButton(recordId)
221
+ : defaultDeleteButton(recordId),
222
+ }
223
+ : null;
224
+ case 'CHANGE_STATUS': {
225
+ const disabled = !whenIsTrue(action.when, record);
226
+ const onConfirm = () => onUpdate?.(Number(recordId), action.values);
227
+ return {
228
+ key,
229
+ node: renderStatusAction
230
+ ? renderStatusAction(
231
+ action,
232
+ record,
233
+ onConfirm,
234
+ updateLoading,
235
+ disabled,
236
+ )
237
+ : defaultStatusAction(
238
+ action,
239
+ record,
240
+ onConfirm,
241
+ updateLoading,
242
+ disabled,
243
+ ),
244
+ };
245
+ }
246
+ case 'JUMP': {
247
+ const url = interpolateActionUrl(
248
+ (action as any).url || '',
249
+ record as any,
250
+ );
251
+ const isExternal = /^https?:\/\//.test(url);
252
+ return {
253
+ key,
254
+ node: (
255
+ <Button
256
+ type="link"
257
+ size="small"
258
+ onClick={() => {
259
+ if (!url) return;
260
+ if (isExternal) {
261
+ window.open(url, '_blank', 'noopener,noreferrer');
262
+ } else {
263
+ window.location.href = url;
264
+ }
265
+ }}
266
+ >
267
+ {t(
268
+ `resources.${resourceName}.actions.${action.desc}`,
269
+ action.desc,
270
+ )}
271
+ </Button>
272
+ ),
273
+ };
274
+ }
275
+ default:
276
+ return null;
277
+ }
278
+ };
279
+
280
+ const renderables = actions
281
+ .map((a, i) => renderAction(a, i))
282
+ .filter((x): x is Renderable => x != null);
283
+
284
+ // 隐式按钮(actions 数组里通常没有):CLONE 由显式 showClone 控制;HISTORY 由 showHistory 或 LogButton 触发
285
+ if (resolvedShowClone) {
286
+ renderables.push({
287
+ key: 'clone-implicit',
288
+ node: renderCloneButton
289
+ ? renderCloneButton(recordId)
290
+ : defaultCloneButton(recordId),
291
+ });
292
+ }
293
+ if (showHistory || LogButton) {
294
+ const node = renderHistoryButton
295
+ ? renderHistoryButton(recordId)
296
+ : defaultHistoryButton(recordId);
297
+ if (node != null) {
298
+ renderables.push({ key: 'history-implicit', node });
299
+ }
300
+ }
301
+
302
+ const { visible, overflow } = splitVisibleActions(
303
+ renderables,
304
+ visibleNum ?? renderables.length,
305
+ );
306
+
307
+ const dropdownItems = [
308
+ ...overflow.map((r) => ({ key: r.key, label: r.node })),
288
309
  ...extraDropdownItems,
289
310
  ].filter((item) => item.label != null);
290
311
 
291
312
  return (
292
313
  <Space>
293
- {/* Edit button */}
294
- {showEdit &&
295
- (renderEditButton
296
- ? renderEditButton(recordId)
297
- : defaultEditButton(recordId))}
298
-
299
- {/* Show/Detail button */}
300
- {showDetail &&
301
- (renderDetailButton
302
- ? renderDetailButton(recordId)
303
- : defaultDetailButton(recordId))}
304
-
305
- {/* Extra primary actions */}
314
+ {visible.map((r) => (
315
+ <React.Fragment key={r.key}>{r.node}</React.Fragment>
316
+ ))}
306
317
  {extraPrimaryActions}
307
-
308
- {/* More dropdown */}
309
318
  {dropdownItems.length > 0 && (
310
319
  <Dropdown menu={{ items: dropdownItems as any }} trigger={['click']}>
311
320
  {renderMoreTrigger ? renderMoreTrigger() : defaultMoreTrigger()}
@@ -288,14 +288,14 @@ export function getInspectorActions(
288
288
  {
289
289
  label: '添加表头搜索',
290
290
  skill: 'ui-config-update',
291
- promptTemplate: `为 ${col(r, f)} 添加搜索过滤`,
291
+ promptTemplate: `为 ${col(r, f)} 添加搜索过滤(修改 ui配置中的 header.filter)`,
292
292
  requiresConfirm: true,
293
293
  confirmDescription: `将为 ${r} 列表的 "${f}" 列添加表头搜索过滤。`,
294
294
  },
295
295
  {
296
296
  label: '移除表头搜索',
297
297
  skill: 'ui-config-update',
298
- promptTemplate: `移除 ${col(r, f)} 的搜索过滤`,
298
+ promptTemplate: `移除 ${col(r, f)} 的搜索过滤(修改 ui配置中的 header.filter)`,
299
299
  requiresConfirm: true,
300
300
  confirmDescription: `将移除 ${r} 列表 "${f}" 列的表头搜索过滤。`,
301
301
  },
@@ -528,7 +528,7 @@ export function getInspectorActions(
528
528
  {
529
529
  label: '增加 模糊匹配 过滤',
530
530
  skill: 'list-search-filter',
531
- promptTemplate: `为 ${r} 搜索栏添加模糊匹配过滤(通过修改ui配置)`,
531
+ promptTemplate: `为 ${r} 搜索栏添加模糊匹配过滤(修改 ui配置中的 toolbar.searchBar)`,
532
532
  promptPrefix: '要添加的字段:',
533
533
  requiresInput: true,
534
534
  inputPlaceholder: '请填写要添加模糊搜索的字段名,例如:name、title',
@@ -0,0 +1,35 @@
1
+ import React, { createContext, useContext, useEffect, useState } from 'react';
2
+ import { authProvider } from '../authProvider';
3
+ import { isAgentEnabled } from './env';
4
+
5
+ // Shared singleton promise — resolves to true only if the logged-in userid
6
+ // matches VITE_BRANCH_OWNER. Both the React context and DevShell (separate root)
7
+ // use this same promise so the fetch only happens once.
8
+ const branchOwner = import.meta.env.VITE_BRANCH_OWNER;
9
+ export const agentAllowedPromise: Promise<boolean> = isAgentEnabled
10
+ ? branchOwner
11
+ ? authProvider.getIdentity!().then(
12
+ (identity: any) => identity?.id === branchOwner,
13
+ )
14
+ : Promise.resolve(true)
15
+ : Promise.resolve(false);
16
+
17
+ const AgentAllowedContext = createContext(false);
18
+
19
+ export const AgentAllowedProvider: React.FC<{ children: React.ReactNode }> = ({
20
+ children,
21
+ }) => {
22
+ const [allowed, setAllowed] = useState(false);
23
+
24
+ useEffect(() => {
25
+ agentAllowedPromise.then(setAllowed);
26
+ }, []);
27
+
28
+ return (
29
+ <AgentAllowedContext.Provider value={allowed}>
30
+ {children}
31
+ </AgentAllowedContext.Provider>
32
+ );
33
+ };
34
+
35
+ export const useIsAgentAllowed = () => useContext(AgentAllowedContext);
@@ -1,4 +1,4 @@
1
- // Agent panel is enabled in dev mode unless explicitly disabled via VITE_ENABLE_AGENT=false.
2
- // To disable: set VITE_ENABLE_AGENT=false in web/.env.local or pass it at startup.
1
+ // Agent panel is enabled in dev mode unless VITE_ENABLE_AGENT=false.
2
+ // If VITE_BRANCH_OWNER is set, only the matching user can use it.
3
3
  export const isAgentEnabled =
4
4
  import.meta.env.DEV && import.meta.env.VITE_ENABLE_AGENT !== 'false';
@@ -12,7 +12,7 @@ import DeleteDataConfirm from './DeleteDataConfirm';
12
12
  import EditDataModal from './EditDataModal';
13
13
  import { resolvePagePaths } from '../components/agentPanel/pagePathUtils';
14
14
  import SkillMenu from './SkillMenu';
15
- import { isAgentEnabled } from '../config/env';
15
+ import { agentAllowedPromise } from '../config/agentAllowed';
16
16
  import './style.css';
17
17
  import UndoConfirm from './UndoConfirm';
18
18
 
@@ -61,7 +61,13 @@ export type ModalType =
61
61
  | null;
62
62
 
63
63
  export default function DevShell() {
64
- if (!isAgentEnabled) return null;
64
+ const [allowed, setAllowed] = useState(false);
65
+
66
+ useEffect(() => {
67
+ agentAllowedPromise.then(setAllowed);
68
+ }, []);
69
+
70
+ if (!allowed) return null;
65
71
  return <DevShellInner />;
66
72
  }
67
73
 
@@ -1,10 +1,11 @@
1
1
  import axios from 'axios';
2
+ import { Modal } from 'antd';
2
3
  import {
3
4
  GadminCrud as gadminDataProvider,
4
5
  applyErrorInterceptor,
5
6
  customRequest as baseCustomRequest,
6
7
  } from '@gadmin2n/react-common';
7
- import { login, requestHeaders } from './login';
8
+ import { login, requestHeaders, isWoaDomain } from './login';
8
9
  import { getApiUrl } from 'config/http';
9
10
  // Re-export Prisma helpers from react-common for backward compatibility
10
11
  export { convertPrismaDecimal, isPrismaDecimal } from '@gadmin2n/react-common';
@@ -13,6 +14,8 @@ const apiUrl = getApiUrl();
13
14
 
14
15
  const axiosInstance = axios.create();
15
16
 
17
+ let woaSessionExpiredModalShown = false;
18
+
16
19
  // 注入 auth header
17
20
  axiosInstance.interceptors.request.use(
18
21
  (config) => {
@@ -29,6 +32,22 @@ axiosInstance.interceptors.response.use(
29
32
  (response) => response,
30
33
  (error) => {
31
34
  if (error?.response?.status === 401) {
35
+ if (isWoaDomain()) {
36
+ if (!woaSessionExpiredModalShown) {
37
+ woaSessionExpiredModalShown = true;
38
+ Modal.confirm({
39
+ title: '登录态已过期',
40
+ content: '您的登录状态已失效,请刷新页面重新登录。',
41
+ okText: '刷新',
42
+ cancelText: '取消',
43
+ onOk: () => window.location.reload(),
44
+ onCancel: () => {
45
+ woaSessionExpiredModalShown = false;
46
+ },
47
+ });
48
+ }
49
+ return new Promise(() => {});
50
+ }
32
51
  console.log('login expired');
33
52
  login();
34
53
  // 返回pending状态的promise,以免后续代码出错
@@ -35,6 +35,54 @@ import { ModelConfig } from 'generated/types/config.types';
35
35
  import { agentAttrs } from '../components/agentPanel/agentAttributes';
36
36
  import dayjs from 'dayjs';
37
37
 
38
+ /**
39
+ * 把 actions 数组解析成 show* 布尔标志集合。
40
+ * 用于让 config 中的 actions 数组成为「显示与否」的唯一真相源。
41
+ */
42
+ export function actionsToShowFlags(
43
+ actions: ReadonlyArray<{ action: string }> = [],
44
+ ) {
45
+ const set = new Set(actions.map((a) => a.action));
46
+ return {
47
+ showInsert: set.has('INSERT'),
48
+ showEdit: set.has('EDIT'),
49
+ showDelete: set.has('DELETE'),
50
+ showExport: set.has('EXPORT'),
51
+ showDetail: set.has('DETAIL'),
52
+ showClone: set.has('CLONE'),
53
+ showImport: set.has('IMPORT'),
54
+ showRefresh: set.has('REFRESH'),
55
+ };
56
+ }
57
+
58
+ /**
59
+ * 按 visibleNum 把 actions 切成「主区直接显示」和「溢出 More dropdown」两组。
60
+ */
61
+ export function splitVisibleActions<T>(
62
+ actions: ReadonlyArray<T> = [],
63
+ visibleNum = Number.POSITIVE_INFINITY,
64
+ ): { visible: T[]; overflow: T[] } {
65
+ const n = Number.isFinite(visibleNum)
66
+ ? Math.max(0, visibleNum)
67
+ : actions.length;
68
+ return {
69
+ visible: actions.slice(0, n),
70
+ overflow: actions.slice(n),
71
+ };
72
+ }
73
+
74
+ /**
75
+ * 用 record 字段值替换 `{{fieldName}}` 占位符(JUMP action 的 url)。
76
+ */
77
+ export function interpolateActionUrl(
78
+ template: string,
79
+ record: Record<string, any>,
80
+ ): string {
81
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) =>
82
+ String(record?.[key] ?? ''),
83
+ );
84
+ }
85
+
38
86
  export function getTableParams(filters: Record<string, string> = {}) {
39
87
  return stringifyTableParams({
40
88
  pagination: { pageSize: 10, current: 1 },
@@ -8,8 +8,9 @@ import type { Plugin } from 'vite';
8
8
  *
9
9
  * Features:
10
10
  * 1. POST /canvas-patch — agent pushes new component code to canvas via HMR
11
- * 2. "/" serves dev-shell-entry.html (agent panel outer shell)
12
- * 3. All other HTML navigation falls back to index.html (SPA router)
11
+ * 2. POST /canvas-add-item agent adds a new component to canvas via HMR
12
+ * 3. "/" serves dev-shell-entry.html (agent panel outer shell)
13
+ * 4. All other HTML navigation falls back to index.html (SPA router)
13
14
  */
14
15
  export function devShellPlugin(): Plugin {
15
16
  return {
@@ -49,6 +50,43 @@ export function devShellPlugin(): Plugin {
49
50
  return;
50
51
  }
51
52
 
53
+ // POST /canvas-add-item — agent 新增一个组件到画布(item + layout)
54
+ if (req.method === 'POST' && url === '/canvas-add-item') {
55
+ let body = '';
56
+ req.on('data', (chunk: Buffer) => {
57
+ body += chunk.toString();
58
+ });
59
+ req.on('end', () => {
60
+ try {
61
+ const { item, layout } = JSON.parse(body) as {
62
+ item: { id: string; componentType: string; code: string };
63
+ layout: { x: number; y: number; w: number; h: number };
64
+ };
65
+ if (
66
+ item?.id &&
67
+ item?.componentType &&
68
+ item?.code !== undefined &&
69
+ layout
70
+ ) {
71
+ server.hot.send('canvas:add-item', { item, layout });
72
+ res.setHeader('Content-Type', 'application/json');
73
+ res.end(JSON.stringify({ ok: true }));
74
+ } else {
75
+ res.statusCode = 400;
76
+ res.end(
77
+ JSON.stringify({
78
+ error: 'item (id, componentType, code) and layout required',
79
+ }),
80
+ );
81
+ }
82
+ } catch {
83
+ res.statusCode = 400;
84
+ res.end(JSON.stringify({ error: 'invalid JSON' }));
85
+ }
86
+ });
87
+ return;
88
+ }
89
+
52
90
  // "/" → dev-shell-entry.html(Agent 外壳),除非 agent 被禁用
53
91
  if (url === '/') {
54
92
  const agentDisabled = process.env.VITE_ENABLE_AGENT === 'false';