@gadmin2n/schematics 0.0.155 → 0.0.157

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 (19) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +1 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +3 -1
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/server/pnpm-workspace.yaml +3 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +37 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +7 -1
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferExportModal.tsx +11 -1
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferImportModal.tsx +3 -1
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ExportModal.tsx +11 -1
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ImportModal.tsx +3 -1
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExportModal.tsx +11 -1
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ImportModal.tsx +3 -1
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +15 -0
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +21 -8
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +34 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HttpRequestConfigEditor.tsx +2 -16
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ParameterBindingListEditor.tsx +22 -29
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/http-request-config.css +36 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/parameter-binding-list-editor.css +48 -0
  19. package/package.json +1 -1
@@ -62,6 +62,7 @@ server/src/
62
62
  ## 开发原则(必须遵守)
63
63
 
64
64
  - 使用 TypeScript 严格模式
65
+ - 包管理工具使用pnpm
65
66
  - 严格遵循前、后端既有技术栈,如无必要不要引入新库
66
67
  - 不确定时询问,不要猜;存在歧义时不要默默选择;有更简单的方案要说出来
67
68
  - 修改 `config/ui/` 或 `config/prisma/` 后,必须在**项目根目录**执行生成命令:
@@ -61,4 +61,6 @@ agent-eval/runs/
61
61
  agent-eval/baseline/
62
62
  tmp/
63
63
  CODEBUDDY.local.md
64
- .scratch/
64
+ .scratch/
65
+ age-sops-guides.md
66
+ .gadmin/.lock
@@ -21,3 +21,6 @@ allowBuilds:
21
21
  # 以下脚本仅打印捐赠/统计信息,与产物无关,保持禁止
22
22
  '@nestjs/core': false
23
23
  '@scarf/scarf': false
24
+
25
+ minimumReleaseAgeExclude:
26
+ - '@gadmin2n/prisma-nest-generator@0.0.50'
@@ -8,6 +8,43 @@ import { WorkflowCompilerService } from './workflow-compiler.service';
8
8
  import { WorkflowLifecycleService } from './workflow-lifecycle.service';
9
9
  import { WorkflowService } from './workflow.service';
10
10
 
11
+ describe('WorkflowService — instance detail', () => {
12
+ it('returns the namespace from Temporal service registration', async () => {
13
+ const prisma = {
14
+ workflowInstance: {
15
+ findUnique: jest.fn().mockResolvedValue({
16
+ id: BigInt(8),
17
+ versionId: BigInt(3),
18
+ status: 'COMPLETED',
19
+ workflow: { name: 'Example workflow' },
20
+ nodeExecutions: [],
21
+ }),
22
+ findMany: jest.fn().mockResolvedValue([]),
23
+ },
24
+ approvalRequest: { findMany: jest.fn().mockResolvedValue([]) },
25
+ temporalTenantConfig: {
26
+ findFirst: jest
27
+ .fn()
28
+ .mockResolvedValue({ namespace: 'registered-namespace' }),
29
+ },
30
+ workflowVersion: {
31
+ findUnique: jest.fn().mockResolvedValue({ dsl: null }),
32
+ },
33
+ };
34
+ const service = new WorkflowService(
35
+ prisma as any,
36
+ {} as any,
37
+ {} as any,
38
+ {} as any,
39
+ {} as any,
40
+ );
41
+
42
+ await expect(
43
+ service.getInstanceDetail(BigInt(8), 'test-user'),
44
+ ).resolves.toMatchObject({ temporalNamespace: 'registered-namespace' });
45
+ });
46
+ });
47
+
11
48
  describe('WorkflowService — workflow list run counts', () => {
12
49
  it('includes cancelled instances in the run summary', async () => {
13
50
  const prisma = {
@@ -2242,7 +2242,7 @@ export class WorkflowService {
2242
2242
 
2243
2243
  if (!instance) throw new NotFoundException('Instance not found');
2244
2244
 
2245
- const [approvableRequests, retryAttempts] = await Promise.all([
2245
+ const detailData = await Promise.all([
2246
2246
  this.prisma.approvalRequest.findMany({
2247
2247
  where: {
2248
2248
  workflowInstanceId: instanceId,
@@ -2262,7 +2262,12 @@ export class WorkflowService {
2262
2262
  createdAt: true,
2263
2263
  },
2264
2264
  }),
2265
+ this.prisma.temporalTenantConfig.findFirst({
2266
+ orderBy: { id: 'asc' },
2267
+ select: { namespace: true },
2268
+ }),
2265
2269
  ]);
2270
+ const [approvableRequests, retryAttempts, temporalConfig] = detailData;
2266
2271
 
2267
2272
  // Fetch the workflow DSL from the version
2268
2273
  let dsl: any = null;
@@ -2283,6 +2288,7 @@ export class WorkflowService {
2283
2288
  ? listSameRunRetryableNodeIds(dsl, instance.nodeExecutions)
2284
2289
  : [],
2285
2290
  retryAttempts,
2291
+ temporalNamespace: temporalConfig?.namespace ?? null,
2286
2292
  dsl,
2287
2293
  };
2288
2294
  }
@@ -6,6 +6,16 @@ import { useTranslation } from 'react-i18next';
6
6
 
7
7
  export type TransferEntity = 'user' | 'role' | 'page' | 'resource';
8
8
 
9
+ // 后端按转发后的 Host 拼 URL(正式环境会是内网地址),统一改写为当前浏览器 origin
10
+ const toCurrentOrigin = (u: string) => {
11
+ try {
12
+ const parsed = new URL(u);
13
+ return `${window.location.origin}${parsed.pathname}${parsed.search}`;
14
+ } catch {
15
+ return u;
16
+ }
17
+ };
18
+
9
19
  interface TransferExportModalProps {
10
20
  open: boolean;
11
21
  onClose: () => void;
@@ -37,7 +47,7 @@ export function TransferExportModal({
37
47
  setUrl('');
38
48
  setLoading(true);
39
49
  customRequest<{ url: string }>(exportTokenEndpoint, 'POST')
40
- .then((res) => setUrl(res.url))
50
+ .then((res) => setUrl(toCurrentOrigin(res.url)))
41
51
  .catch((e: any) =>
42
52
  message.error(e?.message ?? t('transfer.export.failed')),
43
53
  )
@@ -85,7 +85,9 @@ export function TransferImportModal({
85
85
  setLoading(true);
86
86
  try {
87
87
  // Step 1: Fetch from source URL (cross-origin)
88
- const response = await fetch(sourceUrl.trim());
88
+ const response = await fetch(sourceUrl.trim(), {
89
+ credentials: 'include',
90
+ });
89
91
  if (!response.ok) {
90
92
  const errorData = await response.json().catch(() => null);
91
93
  throw new Error(
@@ -9,6 +9,16 @@ interface ExportModalProps {
9
9
  onClose: () => void;
10
10
  }
11
11
 
12
+ // 后端按转发后的 Host 拼 URL(正式环境会是内网地址),统一改写为当前浏览器 origin
13
+ const toCurrentOrigin = (u: string) => {
14
+ try {
15
+ const parsed = new URL(u);
16
+ return `${window.location.origin}${parsed.pathname}${parsed.search}`;
17
+ } catch {
18
+ return u;
19
+ }
20
+ };
21
+
12
22
  export function ExportModal({ open, canvasId, onClose }: ExportModalProps) {
13
23
  const [url, setUrl] = useState<string>('');
14
24
  const [loading, setLoading] = useState(false);
@@ -18,7 +28,7 @@ export function ExportModal({ open, canvasId, onClose }: ExportModalProps) {
18
28
  setUrl('');
19
29
  setLoading(true);
20
30
  generateExportToken(canvasId)
21
- .then((res) => setUrl(res.url))
31
+ .then((res) => setUrl(toCurrentOrigin(res.url)))
22
32
  .catch((e: any) =>
23
33
  message.error(e?.message ?? 'Failed to generate export URL'),
24
34
  )
@@ -43,7 +43,9 @@ export function ImportModal({ open, onClose, onSuccess }: ImportModalProps) {
43
43
  setLoading(true);
44
44
  try {
45
45
  // Step 1: Fetch from source URL (cross-origin)
46
- const response = await fetch(sourceUrl.trim());
46
+ const response = await fetch(sourceUrl.trim(), {
47
+ credentials: 'include',
48
+ });
47
49
  if (!response.ok) {
48
50
  const errorData = await response.json().catch(() => null);
49
51
  throw new Error(
@@ -10,6 +10,16 @@ interface ExportModalProps {
10
10
  onClose: () => void;
11
11
  }
12
12
 
13
+ // 后端按转发后的 Host 拼 URL(正式环境会是内网地址),统一改写为当前浏览器 origin
14
+ const toCurrentOrigin = (u: string) => {
15
+ try {
16
+ const parsed = new URL(u);
17
+ return `${window.location.origin}${parsed.pathname}${parsed.search}`;
18
+ } catch {
19
+ return u;
20
+ }
21
+ };
22
+
13
23
  export function ExportModal({ open, workflowId, onClose }: ExportModalProps) {
14
24
  const { t } = useTranslation('workflow');
15
25
  const [url, setUrl] = useState<string>('');
@@ -23,7 +33,7 @@ export function ExportModal({ open, workflowId, onClose }: ExportModalProps) {
23
33
  `workflow/${workflowId}/export-token`,
24
34
  'POST',
25
35
  )
26
- .then((res) => setUrl(res.url))
36
+ .then((res) => setUrl(toCurrentOrigin(res.url)))
27
37
  .catch((e: any) => message.error(e?.message ?? t('export.failed')))
28
38
  .finally(() => setLoading(false));
29
39
  }
@@ -45,7 +45,9 @@ export function ImportModal({ open, onClose, onSuccess }: ImportModalProps) {
45
45
  setLoading(true);
46
46
  try {
47
47
  // Step 1: Fetch from source URL (cross-origin)
48
- const response = await fetch(sourceUrl.trim());
48
+ const response = await fetch(sourceUrl.trim(), {
49
+ credentials: 'include',
50
+ });
49
51
  if (!response.ok) {
50
52
  const errorData = await response.json().catch(() => null);
51
53
  throw new Error(
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import {
3
+ buildTemporalWorkflowUrl,
3
4
  getDisplayNodeExecutions,
4
5
  getErrorSummary,
5
6
  getStepStatus,
@@ -7,6 +8,20 @@ import {
7
8
  shouldShowNodeError,
8
9
  } from './instance-detail';
9
10
 
11
+ describe('Temporal workflow link', () => {
12
+ it('uses the namespace returned by service registration', () => {
13
+ expect(
14
+ buildTemporalWorkflowUrl(
15
+ 'https://temporal.oit-gadmin.woa.com/',
16
+ 'registered-namespace',
17
+ 'wf-outbox-19-w15',
18
+ ),
19
+ ).toBe(
20
+ 'https://temporal.oit-gadmin.woa.com/namespaces/registered-namespace/workflows/wf-outbox-19-w15',
21
+ );
22
+ });
23
+ });
24
+
10
25
  describe('Workflow instance cancellation display', () => {
11
26
  it('does not present a cancelled node as an error', () => {
12
27
  const cancelledError = {
@@ -80,6 +80,7 @@ interface InstanceDetail {
80
80
  context: any;
81
81
  result: any;
82
82
  temporalRunId: string | null;
83
+ temporalNamespace: string | null;
83
84
  cancellationRequestedAt: string | null;
84
85
  cancellationRequestedBy: string | null;
85
86
  cancellationReason: string | null;
@@ -96,6 +97,16 @@ interface InstanceDetail {
96
97
  dsl?: WorkflowDSL;
97
98
  }
98
99
 
100
+ export function buildTemporalWorkflowUrl(
101
+ baseUrl: string,
102
+ namespace: string,
103
+ workflowId: string,
104
+ ): string {
105
+ return `${baseUrl.replace(/\/$/, '')}/namespaces/${encodeURIComponent(
106
+ namespace,
107
+ )}/workflows/${encodeURIComponent(workflowId)}`;
108
+ }
109
+
99
110
  export function getDisplayNodeExecutions(
100
111
  instance: Pick<
101
112
  InstanceDetail,
@@ -355,25 +366,27 @@ export default function WorkflowInstanceDetailPage() {
355
366
  <Space wrap={false}>
356
367
  <Tooltip
357
368
  title={
358
- data.temporalRunId
369
+ data.temporalRunId && data.temporalNamespace
359
370
  ? '在 Temporal Web UI 中查看完整执行历史、Pending Activities 和 Worker 状态'
360
- : '此实例无 Temporal Run ID(可能由 outbox-poller 创建或 Temporal 当时不可用)'
371
+ : !data.temporalRunId
372
+ ? '此实例无 Temporal Workflow ID(可能由 outbox-poller 创建或 Temporal 当时不可用)'
373
+ : '服务注册中未配置 Temporal Namespace'
361
374
  }
362
375
  >
363
376
  <Button
364
377
  icon={<LinkOutlined />}
365
- disabled={!data.temporalRunId}
378
+ disabled={!data.temporalRunId || !data.temporalNamespace}
366
379
  onClick={() => {
367
- if (!data.temporalRunId) return;
380
+ if (!data.temporalRunId || !data.temporalNamespace) return;
368
381
  const base =
369
382
  import.meta.env.VITE_TEMPORAL_UI_URL ||
370
383
  'http://localhost:8080';
371
- const ns =
372
- import.meta.env.VITE_TEMPORAL_NAMESPACE || 'default';
373
384
  window.open(
374
- `${base}/namespaces/${ns}/workflows/${encodeURIComponent(
385
+ buildTemporalWorkflowUrl(
386
+ base,
387
+ data.temporalNamespace,
375
388
  data.temporalRunId,
376
- )}`,
389
+ ),
377
390
  '_blank',
378
391
  'noopener,noreferrer',
379
392
  );
@@ -110,6 +110,10 @@
110
110
  margin: -2px 0 12px;
111
111
  }
112
112
 
113
+ .manual-trigger-config {
114
+ container-type: inline-size;
115
+ }
116
+
113
117
  .manual-trigger-config__empty {
114
118
  margin-bottom: 10px;
115
119
  padding: 16px 12px;
@@ -167,6 +171,36 @@
167
171
  font-size: 12px;
168
172
  }
169
173
 
174
+ @container (max-width: 420px) {
175
+ .manual-trigger-config__primary {
176
+ grid-template-columns: minmax(0, 1fr) max-content;
177
+ }
178
+
179
+ .manual-trigger-config__primary > :nth-child(1) {
180
+ grid-column: 1;
181
+ grid-row: 1;
182
+ }
183
+
184
+ .manual-trigger-config__primary > :nth-child(2) {
185
+ grid-column: 1;
186
+ grid-row: 2;
187
+ }
188
+
189
+ .manual-trigger-config__required {
190
+ grid-column: 2;
191
+ grid-row: 2;
192
+ }
193
+
194
+ .manual-trigger-config__primary > .ant-btn {
195
+ grid-column: 2;
196
+ grid-row: 1;
197
+ }
198
+
199
+ .manual-trigger-config__secondary {
200
+ grid-template-columns: minmax(0, 1fr);
201
+ }
202
+ }
203
+
170
204
  .cron-trigger-config {
171
205
  display: flex;
172
206
  flex-direction: column;
@@ -118,14 +118,7 @@ export function HttpRequestConfigEditor({
118
118
  <Typography.Paragraph type="secondary">
119
119
  {t('httpRequest.connectionDescription')}
120
120
  </Typography.Paragraph>
121
- <div
122
- style={{
123
- display: 'grid',
124
- gridTemplateColumns: 'minmax(260px, 1fr) auto',
125
- gap: 10,
126
- alignItems: 'end',
127
- }}
128
- >
121
+ <div className="http-request-config__connection-row">
129
122
  <Form.Item
130
123
  label={t('httpRequest.connection')}
131
124
  required={requiredFields.has('connectionRef')}
@@ -155,14 +148,7 @@ export function HttpRequestConfigEditor({
155
148
  {t('httpRequest.newConnection')}
156
149
  </Button>
157
150
  </div>
158
- <div
159
- style={{
160
- display: 'grid',
161
- gridTemplateColumns: '130px 1fr',
162
- gap: 10,
163
- marginTop: 14,
164
- }}
165
- >
151
+ <div className="http-request-config__request-row">
166
152
  <Form.Item
167
153
  label={t('httpRequest.method')}
168
154
  required={requiredFields.has('method')}
@@ -3,6 +3,7 @@ import { Button, Input, Space, Typography } from 'antd';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { ParameterBindingEditor } from '../../components/ParameterBindingEditor';
5
5
  import type { ParameterBinding } from '../../types/parameter-binding';
6
+ import './parameter-binding-list-editor.css';
6
7
 
7
8
  export function ParameterBindingListEditor({
8
9
  value = [],
@@ -20,17 +21,14 @@ export function ParameterBindingListEditor({
20
21
  const { t } = useTranslation('workflow');
21
22
 
22
23
  return (
23
- <Space direction="vertical" size={10} style={{ width: '100%' }}>
24
+ <Space
25
+ className="workflow-parameter-list"
26
+ direction="vertical"
27
+ size={10}
28
+ style={{ width: '100%' }}
29
+ >
24
30
  {value.length > 0 && (
25
- <div
26
- style={{
27
- display: 'grid',
28
- gridTemplateColumns:
29
- 'minmax(140px, 0.65fr) minmax(280px, 1.6fr) 32px',
30
- gap: 8,
31
- marginBottom: -2,
32
- }}
33
- >
31
+ <div className="workflow-parameter-list__head">
34
32
  <Typography.Text type="secondary">
35
33
  {t('nodeEditors.database.parameter')}
36
34
  </Typography.Text>
@@ -43,32 +41,27 @@ export function ParameterBindingListEditor({
43
41
  {value.map((binding, index) => {
44
42
  const placeholder = `$${startIndex + index}`;
45
43
  return (
46
- <div
47
- key={index}
48
- style={{
49
- display: 'grid',
50
- gridTemplateColumns:
51
- 'minmax(140px, 0.65fr) minmax(280px, 1.6fr) 32px',
52
- gap: 8,
53
- alignItems: 'start',
54
- }}
55
- >
44
+ <div key={index} className="workflow-parameter-list__row">
56
45
  <Input
46
+ className="workflow-parameter-list__parameter"
57
47
  aria-label={`参数 ${placeholder}`}
58
48
  value={placeholder}
59
49
  readOnly
60
50
  disabled
61
51
  />
62
- <ParameterBindingEditor
63
- name={placeholder}
64
- defaultPath=""
65
- value={binding}
66
- disabled={disabled}
67
- onChange={(next) =>
68
- onChange(value.map((item, i) => (i === index ? next : item)))
69
- }
70
- />
52
+ <div className="workflow-parameter-list__binding">
53
+ <ParameterBindingEditor
54
+ name={placeholder}
55
+ defaultPath=""
56
+ value={binding}
57
+ disabled={disabled}
58
+ onChange={(next) =>
59
+ onChange(value.map((item, i) => (i === index ? next : item)))
60
+ }
61
+ />
62
+ </div>
71
63
  <Button
64
+ className="workflow-parameter-list__delete"
72
65
  type="text"
73
66
  danger
74
67
  aria-label={`删除参数 ${placeholder}`}
@@ -1,7 +1,43 @@
1
+ .http-request-config {
2
+ container-type: inline-size;
3
+ }
4
+
1
5
  .http-request-config--embedded.ant-space {
2
6
  gap: 18px !important;
3
7
  }
4
8
 
9
+ .http-request-config__connection-row,
10
+ .http-request-config__request-row {
11
+ display: grid;
12
+ gap: 10px;
13
+ }
14
+
15
+ .http-request-config__connection-row {
16
+ grid-template-columns: minmax(260px, 1fr) auto;
17
+ align-items: end;
18
+ }
19
+
20
+ .http-request-config__request-row {
21
+ grid-template-columns: 130px minmax(0, 1fr);
22
+ margin-top: 14px;
23
+ }
24
+
25
+ .http-request-config__connection-row > *,
26
+ .http-request-config__request-row > * {
27
+ min-width: 0;
28
+ }
29
+
30
+ @container (max-width: 400px) {
31
+ .http-request-config__connection-row,
32
+ .http-request-config__request-row {
33
+ grid-template-columns: minmax(0, 1fr);
34
+ }
35
+
36
+ .http-request-config__connection-row > .workflow-panel-action {
37
+ width: 100%;
38
+ }
39
+ }
40
+
5
41
  .http-request-config__timeout {
6
42
  display: flex;
7
43
  align-items: center;
@@ -0,0 +1,48 @@
1
+ .workflow-parameter-list {
2
+ container-type: inline-size;
3
+ }
4
+
5
+ .workflow-parameter-list__head,
6
+ .workflow-parameter-list__row {
7
+ display: grid;
8
+ grid-template-columns: minmax(140px, 0.65fr) minmax(280px, 1.6fr) 32px;
9
+ gap: 8px;
10
+ }
11
+
12
+ .workflow-parameter-list__head {
13
+ margin-bottom: -2px;
14
+ }
15
+
16
+ .workflow-parameter-list__row {
17
+ align-items: start;
18
+ }
19
+
20
+ .workflow-parameter-list__parameter,
21
+ .workflow-parameter-list__binding {
22
+ min-width: 0;
23
+ }
24
+
25
+ @container (max-width: 520px) {
26
+ .workflow-parameter-list__head {
27
+ display: none;
28
+ }
29
+
30
+ .workflow-parameter-list__row {
31
+ grid-template-columns: minmax(0, 1fr) 32px;
32
+ }
33
+
34
+ .workflow-parameter-list__parameter {
35
+ grid-column: 1;
36
+ grid-row: 1;
37
+ }
38
+
39
+ .workflow-parameter-list__delete {
40
+ grid-column: 2;
41
+ grid-row: 1;
42
+ }
43
+
44
+ .workflow-parameter-list__binding {
45
+ grid-column: 1 / -1;
46
+ grid-row: 2;
47
+ }
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gadmin2n/schematics",
3
- "version": "0.0.155",
3
+ "version": "0.0.157",
4
4
  "description": "Gadmin - modern, fast, powerful node.js web framework (@schematics)",
5
5
  "main": "dist/index.js",
6
6
  "files": [