orcheo-studio 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bumpversion.cfg +1 -1
- package/dist/assets/{arc-CqnxfCHp.js → arc-DAv23sxp.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-UGPIhA1j.js → architectureDiagram-VXUJARFQ-D_gUzQPQ.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-Cn6wka40.js → blockDiagram-VD42YOAC-2_hx47qT.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-BvPRNdur.js → c4Diagram-YG6GDRKO-dehdDKxT.js} +1 -1
- package/dist/assets/channel-C3cH7p5A.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-F_o1AmTw.js → chunk-4BX2VUAB-CtLOuP9F.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Cl075es2.js → chunk-55IACEB6-CwinXV4-.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-DcErQhy9.js → chunk-B4BG7PRW-BI7iPFRv.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-CFxCndOp.js → chunk-DI55MBZ5-CNYNrYjW.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-XOcF0fgy.js → chunk-FMBD7UC4-BFVTHaU-.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-CjJH_Bvj.js → chunk-QN33PNHL-xWy7sSv1.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-nLb-Go8u.js → chunk-QZHKN3VN-Ds_R1QaP.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-L07ovmT0.js → chunk-TZMSLE5B-D8g2OOzb.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-o6x5AEsV.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-o6x5AEsV.js +1 -0
- package/dist/assets/clone-CnJR7M_G.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-B6IcYcMk.js → cose-bilkent-S5V4N54A-DnkMvL-f.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-CN7ADhef.js → dagre-6UL2VRFP-dpKYJ1gN.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-3chqLyzk.js → diagram-PSM6KHXK-BnFoR8YQ.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-MrbKZWaG.js → diagram-QEK2KX5R-Brg1AGVu.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-Dr7BijIx.js → diagram-S2PKOQOG-Dhmom7Q4.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-Ck-v9a6N.js → erDiagram-Q2GNP2WA-BTez8fDL.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-D662eMD1.js → flowDiagram-NV44I4VS-CVXmtc0D.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-c2mXK47M.js → ganttDiagram-JELNMOA3-CHbw3AtF.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-DUMhfhsS.js → gitGraphDiagram-V2S2FVAM-CaPZ1SUE.js} +1 -1
- package/dist/assets/{graph-DkWlmVun.js → graph-DjLsaDIL.js} +1 -1
- package/dist/assets/index-DSthAh3a.css +1 -0
- package/dist/assets/{index-kH6EkBsZ.js → index-DlptXU2u.js} +192 -187
- package/dist/assets/{infoDiagram-HS3SLOUP-OTL7qvkP.js → infoDiagram-HS3SLOUP-D_9DW9VU.js} +1 -1
- package/dist/assets/{isUndefined-BmZvH1PX.js → isUndefined-BkijP54r.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-BYzqE5D8.js → journeyDiagram-XKPGCS4Q-xxRWc5wn.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-NtzQ4y9p.js → kanban-definition-3W4ZIXB7-BCI2NbpB.js} +1 -1
- package/dist/assets/{layout-CwhyLMkP.js → layout-M840MAqW.js} +1 -1
- package/dist/assets/{linear-Cirf2V1x.js → linear--_gMRK53.js} +1 -1
- package/dist/assets/{min-C81vLJYh.js → min-D06s3WKk.js} +1 -1
- package/dist/assets/{mindmap-definition-VGOIOE7T-TDnLQIKZ.js → mindmap-definition-VGOIOE7T-B1-ALKFj.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-d0-JhjNZ.js → pieDiagram-ADFJNKIX-76kjoNcf.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-wCoihp8a.js → quadrantDiagram-AYHSOK5B-O_d3GTQl.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-BqiCIjcI.js → requirementDiagram-UZGBJVZJ-CasSIIPr.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-BPH0Pd1D.js → sankeyDiagram-TZEHDZUN-CL_kG7jD.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-B8FfJraH.js → sequenceDiagram-WL72ISMW-RYnztN0k.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-ZQR3Q9_8.js → stateDiagram-FKZM4ZOC-NHFLaLiz.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-3y5AUrwb.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-D-arYxX5.js → timeline-definition-IT6M3QCI-BH1cZoua.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-Dw5BHp_B.js → treemap-GDKQZRPO-QAQDPzuO.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-Dt1zH6V2.js → xychartDiagram-PRI3JC2R-C3M4ZzCP.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +2 -74
- package/src/features/auth/lib/auth-api.ts +21 -3
- package/src/features/workflow/data/templates/candidate-badges.ts +11 -1
- package/src/features/workflow/data/workflow-types.ts +3 -0
- package/src/features/workflow/lib/workflow-storage-api.ts +15 -0
- package/src/features/workflow/lib/workflow-storage-helpers.ts +46 -0
- package/src/features/workflow/lib/workflow-storage.ts +17 -0
- package/src/features/workflow/lib/workflow-storage.types.ts +16 -0
- package/src/features/workflow/pages/workflow/hooks/use-workflow-saver.ts +3 -2
- package/src/features/workflow/pages/workflow-gallery/candidate-updates.ts +107 -0
- package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-actions.ts +29 -4
- package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-state.ts +29 -25
- package/src/features/workflow/pages/workflow-gallery/workflow-card.tsx +187 -6
- package/src/features/workflow/pages/workflow-gallery/workflow-gallery-tabs.tsx +17 -9
- package/src/features/workflow/pages/workflow-gallery.tsx +2 -0
- package/src/lib/auth-fetch.ts +81 -15
- package/vite.config.ts +0 -7
- package/dist/assets/channel-FjxqoZWk.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-8JYNY9Ic.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-8JYNY9Ic.js +0 -1
- package/dist/assets/clone-NPVQC0Uc.js +0 -1
- package/dist/assets/index-BVY_PYJU.css +0 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-uDYn1wPF.js +0 -1
- package/src/features/workflow/components/layouts/workflow-page-layout.tsx +0 -69
- package/src/features/workflow/data/templates/template-owner.ts +0 -7
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { seededAvatarId } from "@/assets/avatars";
|
|
2
2
|
import type { Workflow } from "../workflow-types";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
CandidateUpdateNote,
|
|
5
|
+
StoredWorkflow,
|
|
6
|
+
} from "../../lib/workflow-storage.types";
|
|
4
7
|
import type {
|
|
5
8
|
WorkflowTemplateDefinition,
|
|
6
9
|
WorkflowTemplateMetadata,
|
|
@@ -18,6 +21,8 @@ export interface CandidateBadgeSpec {
|
|
|
18
21
|
avatar?: string;
|
|
19
22
|
notes?: string | null;
|
|
20
23
|
mermaid?: string | null;
|
|
24
|
+
version?: string | null;
|
|
25
|
+
updates?: CandidateUpdateNote[];
|
|
21
26
|
/** Raw snake_case metadata dict from the colleague-candidates frontmatter. */
|
|
22
27
|
rawMetadata?: Record<string, unknown> | null;
|
|
23
28
|
}
|
|
@@ -165,6 +170,11 @@ export const getCandidateBadgeByHandle = (
|
|
|
165
170
|
): CandidateBadgeDefinition | undefined =>
|
|
166
171
|
candidateBadges.find((badge) => badge.handle === handle);
|
|
167
172
|
|
|
173
|
+
export const getCandidateBadgeByCandidateId = (
|
|
174
|
+
candidateId: string,
|
|
175
|
+
): CandidateBadgeDefinition | undefined =>
|
|
176
|
+
candidateBadges.find((badge) => badge.candidateId === candidateId);
|
|
177
|
+
|
|
168
178
|
export const getCandidateTemplateDefinition = (
|
|
169
179
|
templateId: string,
|
|
170
180
|
): WorkflowTemplateDefinition | undefined =>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { WorkflowCandidateSource } from "@features/workflow/lib/workflow-storage.types";
|
|
2
|
+
|
|
1
3
|
export interface WorkflowNode {
|
|
2
4
|
id: string;
|
|
3
5
|
type: string;
|
|
@@ -28,6 +30,7 @@ export interface WorkflowMermaidPreviewVersion {
|
|
|
28
30
|
id: string;
|
|
29
31
|
mermaid?: string | null;
|
|
30
32
|
templateId?: string;
|
|
33
|
+
candidateSource?: WorkflowCandidateSource;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export interface Workflow {
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
WorkflowListenerMetricsResponse,
|
|
15
15
|
WorkflowCredentialReadinessResponse,
|
|
16
16
|
WorkflowPublishResponse,
|
|
17
|
+
CandidateUpdateNote,
|
|
17
18
|
} from "./workflow-storage.types";
|
|
18
19
|
|
|
19
20
|
export const API_BASE = "/api/workflows";
|
|
@@ -179,6 +180,8 @@ export interface ApiCandidate {
|
|
|
179
180
|
notes: string | null;
|
|
180
181
|
metadata: Record<string, unknown> | null;
|
|
181
182
|
mermaid: string | null;
|
|
183
|
+
version: string | null;
|
|
184
|
+
updates: CandidateUpdateNote[];
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
export const fetchCandidates = async (): Promise<ApiCandidate[]> =>
|
|
@@ -214,6 +217,18 @@ export const onboardCandidate = async (
|
|
|
214
217
|
}),
|
|
215
218
|
});
|
|
216
219
|
|
|
220
|
+
export const updateCandidateWorkflow = async (
|
|
221
|
+
workflowId: string,
|
|
222
|
+
candidateId: string,
|
|
223
|
+
): Promise<ApiWorkflow> =>
|
|
224
|
+
request<ApiWorkflow>("/api/candidates/update", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
workflow_id: workflowId,
|
|
228
|
+
candidate_id: candidateId,
|
|
229
|
+
}),
|
|
230
|
+
});
|
|
231
|
+
|
|
217
232
|
export const fetchWorkflowListeners = async (
|
|
218
233
|
workflowId: string,
|
|
219
234
|
): Promise<WorkflowListenerHealth[]> => {
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
ApiWorkflow,
|
|
21
21
|
ApiWorkflowVersion,
|
|
22
22
|
ApiWorkflowVersionSummary,
|
|
23
|
+
WorkflowCandidateSource,
|
|
23
24
|
WorkflowVersionMetadata,
|
|
24
25
|
StoredWorkflow,
|
|
25
26
|
WorkflowVersionRecord,
|
|
@@ -97,6 +98,45 @@ const extractAvatarEmoji = (metadata: unknown): string | undefined => {
|
|
|
97
98
|
return undefined;
|
|
98
99
|
};
|
|
99
100
|
|
|
101
|
+
const extractCandidateSource = (
|
|
102
|
+
metadata: unknown,
|
|
103
|
+
): WorkflowCandidateSource | undefined => {
|
|
104
|
+
if (!isRecord(metadata)) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const source =
|
|
109
|
+
typeof metadata.source === "string" ? metadata.source : undefined;
|
|
110
|
+
const candidateId =
|
|
111
|
+
typeof metadata.candidate_id === "string"
|
|
112
|
+
? metadata.candidate_id
|
|
113
|
+
: undefined;
|
|
114
|
+
const candidateHandle =
|
|
115
|
+
typeof metadata.candidate_handle === "string"
|
|
116
|
+
? metadata.candidate_handle
|
|
117
|
+
: undefined;
|
|
118
|
+
const candidateVersion =
|
|
119
|
+
typeof metadata.candidate_version === "string"
|
|
120
|
+
? metadata.candidate_version
|
|
121
|
+
: undefined;
|
|
122
|
+
const candidateSourceRef =
|
|
123
|
+
typeof metadata.candidate_source_ref === "string"
|
|
124
|
+
? metadata.candidate_source_ref
|
|
125
|
+
: undefined;
|
|
126
|
+
|
|
127
|
+
if (!source && !candidateId && !candidateHandle && !candidateVersion) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
source,
|
|
133
|
+
candidateId,
|
|
134
|
+
candidateHandle,
|
|
135
|
+
candidateVersion,
|
|
136
|
+
candidateSourceRef,
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
100
140
|
const toAuthor = (id: string | undefined): Workflow["owner"] => {
|
|
101
141
|
if (!id) {
|
|
102
142
|
return { ...DEFAULT_OWNER };
|
|
@@ -178,6 +218,7 @@ const parseWorkflowMetadata = (
|
|
|
178
218
|
isRecord(metadata) ? metadata.configurable_schema : undefined,
|
|
179
219
|
);
|
|
180
220
|
const avatarEmoji = extractAvatarEmoji(metadata);
|
|
221
|
+
const candidateSource = extractCandidateSource(metadata);
|
|
181
222
|
const resolveTemplateFallback = (): WorkflowVersionMetadata | undefined => {
|
|
182
223
|
if (!metadata || typeof metadata !== "object") {
|
|
183
224
|
return undefined;
|
|
@@ -204,6 +245,7 @@ const parseWorkflowMetadata = (
|
|
|
204
245
|
templateId,
|
|
205
246
|
configurableSchemas,
|
|
206
247
|
avatarEmoji,
|
|
248
|
+
candidateSource,
|
|
207
249
|
};
|
|
208
250
|
};
|
|
209
251
|
|
|
@@ -214,6 +256,7 @@ const parseWorkflowMetadata = (
|
|
|
214
256
|
templateId: undefined,
|
|
215
257
|
configurableSchemas: undefined,
|
|
216
258
|
avatarEmoji: undefined,
|
|
259
|
+
candidateSource,
|
|
217
260
|
};
|
|
218
261
|
}
|
|
219
262
|
|
|
@@ -226,6 +269,7 @@ const parseWorkflowMetadata = (
|
|
|
226
269
|
summary: { ...DEFAULT_SUMMARY },
|
|
227
270
|
configurableSchemas,
|
|
228
271
|
avatarEmoji,
|
|
272
|
+
candidateSource,
|
|
229
273
|
}
|
|
230
274
|
);
|
|
231
275
|
}
|
|
@@ -280,6 +324,7 @@ const parseWorkflowMetadata = (
|
|
|
280
324
|
templateId,
|
|
281
325
|
configurableSchemas,
|
|
282
326
|
avatarEmoji,
|
|
327
|
+
candidateSource,
|
|
283
328
|
};
|
|
284
329
|
};
|
|
285
330
|
|
|
@@ -337,6 +382,7 @@ const toVersionRecord = (
|
|
|
337
382
|
graphToWorkflow: metadata.graphToWorkflow,
|
|
338
383
|
templateId: metadata.templateId,
|
|
339
384
|
avatarEmoji: metadata.avatarEmoji,
|
|
385
|
+
candidateSource: metadata.candidateSource,
|
|
340
386
|
};
|
|
341
387
|
};
|
|
342
388
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
fetchWorkflowVersions,
|
|
17
17
|
onboardCandidate,
|
|
18
18
|
request,
|
|
19
|
+
updateCandidateWorkflow,
|
|
19
20
|
upsertWorkflow,
|
|
20
21
|
} from "./workflow-storage-api";
|
|
21
22
|
import {
|
|
@@ -472,6 +473,22 @@ export const onboardCandidateAsWorkflow = async (
|
|
|
472
473
|
return stored;
|
|
473
474
|
};
|
|
474
475
|
|
|
476
|
+
export const updateCandidateWorkflowVersion = async (
|
|
477
|
+
workflowId: string,
|
|
478
|
+
candidateId: string,
|
|
479
|
+
): Promise<StoredWorkflow> => {
|
|
480
|
+
const apiWorkflow = await updateCandidateWorkflow(workflowId, candidateId);
|
|
481
|
+
invalidateWorkflowCache(apiWorkflow.id);
|
|
482
|
+
const stored = await ensureWorkflow(apiWorkflow.id);
|
|
483
|
+
if (!stored) {
|
|
484
|
+
throw new Error("Failed to load updated candidate workflow");
|
|
485
|
+
}
|
|
486
|
+
invalidateWorkflowListCache();
|
|
487
|
+
primeWorkflowCache(stored);
|
|
488
|
+
emitUpdate();
|
|
489
|
+
return stored;
|
|
490
|
+
};
|
|
491
|
+
|
|
475
492
|
export type {
|
|
476
493
|
StoredWorkflow,
|
|
477
494
|
WorkflowVersionRecord,
|
|
@@ -68,6 +68,20 @@ export interface ApiWorkflowVersionSummary {
|
|
|
68
68
|
updated_at: string;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export interface CandidateUpdateNote {
|
|
72
|
+
version: string;
|
|
73
|
+
summary: string;
|
|
74
|
+
migration?: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface WorkflowCandidateSource {
|
|
78
|
+
source?: string;
|
|
79
|
+
candidateId?: string;
|
|
80
|
+
candidateHandle?: string;
|
|
81
|
+
candidateVersion?: string;
|
|
82
|
+
candidateSourceRef?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
export interface ApiWorkflowPageData {
|
|
72
86
|
workflow: ApiWorkflow;
|
|
73
87
|
versions?: ApiWorkflowVersionSummary[];
|
|
@@ -219,6 +233,7 @@ export interface WorkflowVersionMetadata {
|
|
|
219
233
|
templateId?: string;
|
|
220
234
|
configurableSchemas?: Record<string, RJSFSchema>;
|
|
221
235
|
avatarEmoji?: string;
|
|
236
|
+
candidateSource?: WorkflowCandidateSource;
|
|
222
237
|
}
|
|
223
238
|
|
|
224
239
|
export interface RequestOptions extends RequestInit {
|
|
@@ -241,6 +256,7 @@ export interface WorkflowVersionRecord {
|
|
|
241
256
|
configurableSchemas?: Record<string, RJSFSchema>;
|
|
242
257
|
graphToWorkflow?: Record<string, string>;
|
|
243
258
|
templateId?: string;
|
|
259
|
+
candidateSource?: WorkflowCandidateSource;
|
|
244
260
|
}
|
|
245
261
|
|
|
246
262
|
export interface StoredWorkflow extends Workflow {
|
|
@@ -63,8 +63,9 @@ export function useWorkflowSaver(
|
|
|
63
63
|
"studio",
|
|
64
64
|
runnableConfig,
|
|
65
65
|
);
|
|
66
|
-
// Keep the in-memory
|
|
67
|
-
// shows the saved values without a page refresh.
|
|
66
|
+
// Keep the in-memory runnable config in sync so reopening the config
|
|
67
|
+
// sheet shows the saved values without a page refresh. Other persisted
|
|
68
|
+
// fields will refresh on the next full workflow fetch.
|
|
68
69
|
setWorkflowVersions((versions) =>
|
|
69
70
|
versions.map((version) =>
|
|
70
71
|
version.versionNumber === targetVersion
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Workflow } from "@features/workflow/data/workflow-data";
|
|
2
|
+
import type { CandidateBadgeDefinition } from "@features/workflow/data/templates/candidate-badges";
|
|
3
|
+
import type { CandidateUpdateNote } from "@features/workflow/lib/workflow-storage.types";
|
|
4
|
+
|
|
5
|
+
interface SemVer {
|
|
6
|
+
major: number;
|
|
7
|
+
minor: number;
|
|
8
|
+
patch: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CandidateUpdateAvailability {
|
|
12
|
+
candidateId: string;
|
|
13
|
+
candidateName: string;
|
|
14
|
+
currentVersion: string;
|
|
15
|
+
latestVersion: string;
|
|
16
|
+
latestSummary?: string;
|
|
17
|
+
firstMigration?: string;
|
|
18
|
+
crossedUpdates: CandidateUpdateNote[];
|
|
19
|
+
isMajor: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
23
|
+
|
|
24
|
+
export const parseSemVer = (value: string): SemVer | null => {
|
|
25
|
+
const match = SEMVER_PATTERN.exec(value);
|
|
26
|
+
if (!match) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
major: Number.parseInt(match[1] ?? "0", 10),
|
|
32
|
+
minor: Number.parseInt(match[2] ?? "0", 10),
|
|
33
|
+
patch: Number.parseInt(match[3] ?? "0", 10),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const compareSemVer = (left: string, right: string): number | null => {
|
|
38
|
+
const leftVersion = parseSemVer(left);
|
|
39
|
+
const rightVersion = parseSemVer(right);
|
|
40
|
+
if (!leftVersion || !rightVersion) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const key of ["major", "minor", "patch"] as const) {
|
|
45
|
+
const delta = leftVersion[key] - rightVersion[key];
|
|
46
|
+
if (delta !== 0) {
|
|
47
|
+
return delta > 0 ? 1 : -1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return 0;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const getCandidateUpdateAvailability = (
|
|
55
|
+
workflow: Workflow,
|
|
56
|
+
candidate: CandidateBadgeDefinition | undefined,
|
|
57
|
+
): CandidateUpdateAvailability | undefined => {
|
|
58
|
+
if (!candidate?.version) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const candidateSource = workflow.versions?.at(-1)?.candidateSource;
|
|
63
|
+
if (candidateSource?.source !== "candidate-onboard") {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const currentVersion = candidateSource.candidateVersion;
|
|
68
|
+
if (!currentVersion) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const latestVersion = candidate.version;
|
|
73
|
+
if (compareSemVer(latestVersion, currentVersion) !== 1) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const currentParsed = parseSemVer(currentVersion);
|
|
78
|
+
const latestParsed = parseSemVer(latestVersion);
|
|
79
|
+
if (!currentParsed || !latestParsed) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const crossedUpdates = (candidate.updates ?? []).filter((note) => {
|
|
84
|
+
const newerThanCurrent = compareSemVer(note.version, currentVersion);
|
|
85
|
+
const notNewerThanLatest = compareSemVer(note.version, latestVersion);
|
|
86
|
+
return (
|
|
87
|
+
newerThanCurrent === 1 &&
|
|
88
|
+
notNewerThanLatest !== null &&
|
|
89
|
+
notNewerThanLatest <= 0
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const firstMigration = crossedUpdates.find(
|
|
94
|
+
(note) => typeof note.migration === "string" && note.migration.trim(),
|
|
95
|
+
)?.migration;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
candidateId: candidate.candidateId,
|
|
99
|
+
candidateName: candidate.name,
|
|
100
|
+
currentVersion,
|
|
101
|
+
latestVersion,
|
|
102
|
+
latestSummary: crossedUpdates[0]?.summary,
|
|
103
|
+
firstMigration,
|
|
104
|
+
crossedUpdates,
|
|
105
|
+
isMajor: latestParsed.major > currentParsed.major,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
@@ -9,6 +9,7 @@ import { getCandidateBadgeDefinition } from "@features/workflow/data/templates/c
|
|
|
9
9
|
import {
|
|
10
10
|
deleteWorkflow,
|
|
11
11
|
onboardCandidateAsWorkflow,
|
|
12
|
+
updateCandidateWorkflowVersion,
|
|
12
13
|
} from "@features/workflow/lib/workflow-storage";
|
|
13
14
|
import {
|
|
14
15
|
type ApiTeam,
|
|
@@ -104,10 +105,9 @@ export const useWorkflowGalleryActions = (
|
|
|
104
105
|
const handleOpenWorkflow = useCallback(
|
|
105
106
|
(workflowId: string, teamSlug?: string) => {
|
|
106
107
|
const slug = getSelectedWorkspaceSlug();
|
|
107
|
-
const path =
|
|
108
|
-
teamSlug
|
|
109
|
-
|
|
110
|
-
: getWorkspaceWorkflowPath(slug, workflowId);
|
|
108
|
+
const path = teamSlug
|
|
109
|
+
? getWorkspaceTeamWorkflowPath(slug, teamSlug, workflowId)
|
|
110
|
+
: getWorkspaceWorkflowPath(slug, workflowId);
|
|
111
111
|
navigate(path);
|
|
112
112
|
},
|
|
113
113
|
[navigate],
|
|
@@ -302,6 +302,30 @@ export const useWorkflowGalleryActions = (
|
|
|
302
302
|
[],
|
|
303
303
|
);
|
|
304
304
|
|
|
305
|
+
const handleUpdateCandidateWorkflow = useCallback(
|
|
306
|
+
async (workflowId: string, candidateId: string) => {
|
|
307
|
+
try {
|
|
308
|
+
const workflow = await updateCandidateWorkflowVersion(
|
|
309
|
+
workflowId,
|
|
310
|
+
candidateId,
|
|
311
|
+
);
|
|
312
|
+
toast({
|
|
313
|
+
title: "Candidate updated",
|
|
314
|
+
description: `"${workflow.name}" is now on the latest candidate version.`,
|
|
315
|
+
});
|
|
316
|
+
} catch (error) {
|
|
317
|
+
toast({
|
|
318
|
+
title: "Failed to update candidate",
|
|
319
|
+
description:
|
|
320
|
+
error instanceof Error ? error.message : "Unknown error occurred",
|
|
321
|
+
variant: "destructive",
|
|
322
|
+
});
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
[],
|
|
327
|
+
);
|
|
328
|
+
|
|
305
329
|
const openCreateTeamDialog = useCallback(() => setIsCreateTeamOpen(true), []);
|
|
306
330
|
const closeCreateTeamDialog = useCallback(
|
|
307
331
|
() => setIsCreateTeamOpen(false),
|
|
@@ -314,6 +338,7 @@ export const useWorkflowGalleryActions = (
|
|
|
314
338
|
handleImportStarterPack,
|
|
315
339
|
handleExportWorkflow,
|
|
316
340
|
handleDeleteWorkflow,
|
|
341
|
+
handleUpdateCandidateWorkflow,
|
|
317
342
|
onboardTarget,
|
|
318
343
|
confirmOnboardTeam,
|
|
319
344
|
cancelOnboardTeam,
|
|
@@ -49,6 +49,8 @@ const toCandidateSpec = (candidate: ApiCandidate): CandidateBadgeSpec => ({
|
|
|
49
49
|
avatar: candidate.avatar ?? undefined,
|
|
50
50
|
notes: candidate.notes,
|
|
51
51
|
mermaid: candidate.mermaid,
|
|
52
|
+
version: candidate.version,
|
|
53
|
+
updates: candidate.updates,
|
|
52
54
|
rawMetadata: candidate.metadata,
|
|
53
55
|
});
|
|
54
56
|
|
|
@@ -75,17 +77,22 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
|
|
|
75
77
|
useEffect(() => {
|
|
76
78
|
let isMounted = true;
|
|
77
79
|
|
|
78
|
-
const loadCandidates = async () => {
|
|
80
|
+
const loadCandidates = async (existingWorkflows?: StoredWorkflow[]) => {
|
|
79
81
|
try {
|
|
80
82
|
// Load both candidates and existing workflows to check for handle conflicts
|
|
81
|
-
const [candidates,
|
|
83
|
+
const [candidates, workflowsForConflicts] = await Promise.all([
|
|
82
84
|
fetchCandidates(),
|
|
83
|
-
|
|
85
|
+
existingWorkflows
|
|
86
|
+
? Promise.resolve(existingWorkflows)
|
|
87
|
+
: listWorkflows(),
|
|
84
88
|
]);
|
|
85
89
|
if (!isMounted) {
|
|
86
90
|
return;
|
|
87
91
|
}
|
|
88
|
-
setCandidateBadges(
|
|
92
|
+
setCandidateBadges(
|
|
93
|
+
candidates.map(toCandidateSpec),
|
|
94
|
+
workflowsForConflicts,
|
|
95
|
+
);
|
|
89
96
|
setCandidateWorkflows(getCandidateWorkflows());
|
|
90
97
|
} catch (error) {
|
|
91
98
|
if (isMounted) {
|
|
@@ -94,26 +101,8 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
|
|
|
94
101
|
}
|
|
95
102
|
};
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return () => {
|
|
100
|
-
isMounted = false;
|
|
101
|
-
};
|
|
102
|
-
}, []);
|
|
103
|
-
|
|
104
|
-
const refreshTeams = useCallback(async () => {
|
|
105
|
-
try {
|
|
106
|
-
const teamList = await fetchTeams();
|
|
107
|
-
setTeams(teamList);
|
|
108
|
-
} catch {
|
|
109
|
-
// non-fatal — teams list will stay stale
|
|
110
|
-
}
|
|
111
|
-
}, []);
|
|
112
|
-
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
let isMounted = true;
|
|
115
|
-
|
|
116
|
-
const load = async (forceRefresh = false) => {
|
|
104
|
+
const load = async (forceRefresh = false, refreshCandidates = false) => {
|
|
105
|
+
let loadedWorkflows: StoredWorkflow[] | undefined;
|
|
117
106
|
if (isMounted) {
|
|
118
107
|
setIsLoadingWorkflows(true);
|
|
119
108
|
}
|
|
@@ -122,6 +111,7 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
|
|
|
122
111
|
listWorkflows({ forceRefresh }),
|
|
123
112
|
fetchTeams().catch(() => [] as ApiTeam[]),
|
|
124
113
|
]);
|
|
114
|
+
loadedWorkflows = items;
|
|
125
115
|
if (isMounted) {
|
|
126
116
|
setWorkflows(items);
|
|
127
117
|
setTeams(teamList);
|
|
@@ -143,14 +133,19 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
|
|
|
143
133
|
setIsLoadingWorkflows(false);
|
|
144
134
|
}
|
|
145
135
|
}
|
|
136
|
+
|
|
137
|
+
if (refreshCandidates && loadedWorkflows) {
|
|
138
|
+
await loadCandidates(loadedWorkflows);
|
|
139
|
+
}
|
|
146
140
|
};
|
|
147
141
|
|
|
142
|
+
void loadCandidates();
|
|
148
143
|
void load();
|
|
149
144
|
|
|
150
145
|
const targetWindow = typeof window !== "undefined" ? window : undefined;
|
|
151
146
|
if (targetWindow) {
|
|
152
147
|
const handler = () => {
|
|
153
|
-
void load(true);
|
|
148
|
+
void load(true, true);
|
|
154
149
|
};
|
|
155
150
|
targetWindow.addEventListener(WORKFLOW_STORAGE_EVENT, handler);
|
|
156
151
|
|
|
@@ -165,6 +160,15 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
|
|
|
165
160
|
};
|
|
166
161
|
}, []);
|
|
167
162
|
|
|
163
|
+
const refreshTeams = useCallback(async () => {
|
|
164
|
+
try {
|
|
165
|
+
const teamList = await fetchTeams();
|
|
166
|
+
setTeams(teamList);
|
|
167
|
+
} catch {
|
|
168
|
+
// non-fatal — teams list will stay stale
|
|
169
|
+
}
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
168
172
|
const templates = useMemo(
|
|
169
173
|
() => [...GALLERY_TEMPLATE_WORKFLOWS, ...candidateWorkflows],
|
|
170
174
|
[candidateWorkflows],
|