orcheo-studio 0.23.2 → 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.
Files changed (70) hide show
  1. package/.bumpversion.cfg +1 -1
  2. package/dist/assets/{arc-BokkJPzk.js → arc-DAv23sxp.js} +1 -1
  3. package/dist/assets/{architectureDiagram-VXUJARFQ-CzmPOZBu.js → architectureDiagram-VXUJARFQ-D_gUzQPQ.js} +1 -1
  4. package/dist/assets/{blockDiagram-VD42YOAC-DG89LeH9.js → blockDiagram-VD42YOAC-2_hx47qT.js} +1 -1
  5. package/dist/assets/{c4Diagram-YG6GDRKO-DymWpJZb.js → c4Diagram-YG6GDRKO-dehdDKxT.js} +1 -1
  6. package/dist/assets/channel-C3cH7p5A.js +1 -0
  7. package/dist/assets/{chunk-4BX2VUAB-DulKpeK2.js → chunk-4BX2VUAB-CtLOuP9F.js} +1 -1
  8. package/dist/assets/{chunk-55IACEB6-Bi6MBeyB.js → chunk-55IACEB6-CwinXV4-.js} +1 -1
  9. package/dist/assets/{chunk-B4BG7PRW-teHvBEMz.js → chunk-B4BG7PRW-BI7iPFRv.js} +1 -1
  10. package/dist/assets/{chunk-DI55MBZ5-Clydm5zE.js → chunk-DI55MBZ5-CNYNrYjW.js} +1 -1
  11. package/dist/assets/{chunk-FMBD7UC4-BGh0gUNB.js → chunk-FMBD7UC4-BFVTHaU-.js} +1 -1
  12. package/dist/assets/{chunk-QN33PNHL-D-9fma_8.js → chunk-QN33PNHL-xWy7sSv1.js} +1 -1
  13. package/dist/assets/{chunk-QZHKN3VN-Bux-KM8v.js → chunk-QZHKN3VN-Ds_R1QaP.js} +1 -1
  14. package/dist/assets/{chunk-TZMSLE5B-pgx8j0M-.js → chunk-TZMSLE5B-D8g2OOzb.js} +1 -1
  15. package/dist/assets/classDiagram-2ON5EDUG-o6x5AEsV.js +1 -0
  16. package/dist/assets/classDiagram-v2-WZHVMYZB-o6x5AEsV.js +1 -0
  17. package/dist/assets/clone-CnJR7M_G.js +1 -0
  18. package/dist/assets/{cose-bilkent-S5V4N54A-CszLJ5HU.js → cose-bilkent-S5V4N54A-DnkMvL-f.js} +1 -1
  19. package/dist/assets/{dagre-6UL2VRFP-Cr25CaNe.js → dagre-6UL2VRFP-dpKYJ1gN.js} +1 -1
  20. package/dist/assets/{diagram-PSM6KHXK-CnsZe68H.js → diagram-PSM6KHXK-BnFoR8YQ.js} +1 -1
  21. package/dist/assets/{diagram-QEK2KX5R-CN8ov45N.js → diagram-QEK2KX5R-Brg1AGVu.js} +1 -1
  22. package/dist/assets/{diagram-S2PKOQOG-v2bUyBL7.js → diagram-S2PKOQOG-Dhmom7Q4.js} +1 -1
  23. package/dist/assets/{erDiagram-Q2GNP2WA-BxxatPhi.js → erDiagram-Q2GNP2WA-BTez8fDL.js} +1 -1
  24. package/dist/assets/{flowDiagram-NV44I4VS-DIKLSwgl.js → flowDiagram-NV44I4VS-CVXmtc0D.js} +1 -1
  25. package/dist/assets/{ganttDiagram-JELNMOA3-D_0u3tXB.js → ganttDiagram-JELNMOA3-CHbw3AtF.js} +1 -1
  26. package/dist/assets/{gitGraphDiagram-V2S2FVAM-B_nu7ypr.js → gitGraphDiagram-V2S2FVAM-CaPZ1SUE.js} +1 -1
  27. package/dist/assets/{graph-DmMuvtfO.js → graph-DjLsaDIL.js} +1 -1
  28. package/dist/assets/index-DSthAh3a.css +1 -0
  29. package/dist/assets/{index-DVxGch9w.js → index-DlptXU2u.js} +189 -184
  30. package/dist/assets/{infoDiagram-HS3SLOUP-DLLSS3lr.js → infoDiagram-HS3SLOUP-D_9DW9VU.js} +1 -1
  31. package/dist/assets/{isUndefined-u8cwznUN.js → isUndefined-BkijP54r.js} +1 -1
  32. package/dist/assets/{journeyDiagram-XKPGCS4Q-fF1HZxKQ.js → journeyDiagram-XKPGCS4Q-xxRWc5wn.js} +1 -1
  33. package/dist/assets/{kanban-definition-3W4ZIXB7-CcmlUntf.js → kanban-definition-3W4ZIXB7-BCI2NbpB.js} +1 -1
  34. package/dist/assets/{layout-CuCF7WdL.js → layout-M840MAqW.js} +1 -1
  35. package/dist/assets/{linear-BVXj8lVL.js → linear--_gMRK53.js} +1 -1
  36. package/dist/assets/{min-DEoUCRl0.js → min-D06s3WKk.js} +1 -1
  37. package/dist/assets/{mindmap-definition-VGOIOE7T-DtNTD5Hm.js → mindmap-definition-VGOIOE7T-B1-ALKFj.js} +1 -1
  38. package/dist/assets/{pieDiagram-ADFJNKIX-a07QMF4s.js → pieDiagram-ADFJNKIX-76kjoNcf.js} +1 -1
  39. package/dist/assets/{quadrantDiagram-AYHSOK5B-NxQVfG-F.js → quadrantDiagram-AYHSOK5B-O_d3GTQl.js} +1 -1
  40. package/dist/assets/{requirementDiagram-UZGBJVZJ-DfwcBFwx.js → requirementDiagram-UZGBJVZJ-CasSIIPr.js} +1 -1
  41. package/dist/assets/{sankeyDiagram-TZEHDZUN-BeRNGGtP.js → sankeyDiagram-TZEHDZUN-CL_kG7jD.js} +1 -1
  42. package/dist/assets/{sequenceDiagram-WL72ISMW-dTp9Xfca.js → sequenceDiagram-WL72ISMW-RYnztN0k.js} +1 -1
  43. package/dist/assets/{stateDiagram-FKZM4ZOC-uXrzw1Ia.js → stateDiagram-FKZM4ZOC-NHFLaLiz.js} +1 -1
  44. package/dist/assets/stateDiagram-v2-4FDKWEC3-3y5AUrwb.js +1 -0
  45. package/dist/assets/{timeline-definition-IT6M3QCI-Jx4tDH4r.js → timeline-definition-IT6M3QCI-BH1cZoua.js} +1 -1
  46. package/dist/assets/{treemap-GDKQZRPO-CYRu_RLQ.js → treemap-GDKQZRPO-QAQDPzuO.js} +1 -1
  47. package/dist/assets/{xychartDiagram-PRI3JC2R-DVYMDr6g.js → xychartDiagram-PRI3JC2R-C3M4ZzCP.js} +1 -1
  48. package/dist/index.html +2 -2
  49. package/package.json +2 -74
  50. package/src/features/workflow/data/templates/candidate-badges.ts +11 -1
  51. package/src/features/workflow/data/workflow-types.ts +3 -0
  52. package/src/features/workflow/lib/workflow-storage-api.ts +15 -0
  53. package/src/features/workflow/lib/workflow-storage-helpers.ts +46 -0
  54. package/src/features/workflow/lib/workflow-storage.ts +17 -0
  55. package/src/features/workflow/lib/workflow-storage.types.ts +16 -0
  56. package/src/features/workflow/pages/workflow-gallery/candidate-updates.ts +107 -0
  57. package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-actions.ts +29 -4
  58. package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-state.ts +29 -25
  59. package/src/features/workflow/pages/workflow-gallery/workflow-card.tsx +187 -6
  60. package/src/features/workflow/pages/workflow-gallery/workflow-gallery-tabs.tsx +6 -0
  61. package/src/features/workflow/pages/workflow-gallery.tsx +2 -0
  62. package/vite.config.ts +0 -7
  63. package/dist/assets/channel-DPwJ8Ajm.js +0 -1
  64. package/dist/assets/classDiagram-2ON5EDUG-CYBYFsly.js +0 -1
  65. package/dist/assets/classDiagram-v2-WZHVMYZB-CYBYFsly.js +0 -1
  66. package/dist/assets/clone-tT9-ZTc-.js +0 -1
  67. package/dist/assets/index-BVY_PYJU.css +0 -1
  68. package/dist/assets/stateDiagram-v2-4FDKWEC3-dqZSxN9f.js +0 -1
  69. package/src/features/workflow/components/layouts/workflow-page-layout.tsx +0 -69
  70. package/src/features/workflow/data/templates/template-owner.ts +0 -7
@@ -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 {
@@ -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
- ? getWorkspaceTeamWorkflowPath(slug, teamSlug, workflowId)
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, existingWorkflows] = await Promise.all([
83
+ const [candidates, workflowsForConflicts] = await Promise.all([
82
84
  fetchCandidates(),
83
- listWorkflows(),
85
+ existingWorkflows
86
+ ? Promise.resolve(existingWorkflows)
87
+ : listWorkflows(),
84
88
  ]);
85
89
  if (!isMounted) {
86
90
  return;
87
91
  }
88
- setCandidateBadges(candidates.map(toCandidateSpec), existingWorkflows);
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
- void loadCandidates();
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],