orcheo-studio 0.21.3 → 0.22.2

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 (77) hide show
  1. package/.bumpversion.cfg +1 -1
  2. package/dist/assets/{arc-CI8iBP22.js → arc-De9dLqyl.js} +1 -1
  3. package/dist/assets/{architectureDiagram-VXUJARFQ-9lccnHEL.js → architectureDiagram-VXUJARFQ-hEslfmYm.js} +1 -1
  4. package/dist/assets/{blockDiagram-VD42YOAC-CJPKWPSp.js → blockDiagram-VD42YOAC-CyWSMWJa.js} +1 -1
  5. package/dist/assets/{c4Diagram-YG6GDRKO-CuW7nex5.js → c4Diagram-YG6GDRKO-CxoTVJ47.js} +1 -1
  6. package/dist/assets/channel-CzCzGn0f.js +1 -0
  7. package/dist/assets/{chunk-4BX2VUAB-BVQ3rR7o.js → chunk-4BX2VUAB-twftUw9o.js} +1 -1
  8. package/dist/assets/{chunk-55IACEB6-BTpqJ_MA.js → chunk-55IACEB6-DQZDNQUo.js} +1 -1
  9. package/dist/assets/{chunk-B4BG7PRW-Duatgdyr.js → chunk-B4BG7PRW-BYGOoNB5.js} +1 -1
  10. package/dist/assets/{chunk-DI55MBZ5-CxEvlbgl.js → chunk-DI55MBZ5-qyPhcZF3.js} +1 -1
  11. package/dist/assets/{chunk-FMBD7UC4-C07lS0oH.js → chunk-FMBD7UC4-BgUDHGEG.js} +1 -1
  12. package/dist/assets/{chunk-QN33PNHL-C09PhK0w.js → chunk-QN33PNHL-DHlMh8NC.js} +1 -1
  13. package/dist/assets/{chunk-QZHKN3VN-Dy3GgrIN.js → chunk-QZHKN3VN-Bfc8IV0O.js} +1 -1
  14. package/dist/assets/{chunk-TZMSLE5B-9HPWR_AG.js → chunk-TZMSLE5B-BqwdftTm.js} +1 -1
  15. package/dist/assets/classDiagram-2ON5EDUG-ekNI5d_4.js +1 -0
  16. package/dist/assets/classDiagram-v2-WZHVMYZB-ekNI5d_4.js +1 -0
  17. package/dist/assets/clone-DowBu1Kh.js +1 -0
  18. package/dist/assets/{cose-bilkent-S5V4N54A-B9TWyynz.js → cose-bilkent-S5V4N54A-g5agH7fS.js} +1 -1
  19. package/dist/assets/{dagre-6UL2VRFP-DFuJOuNQ.js → dagre-6UL2VRFP-ajLMaXPP.js} +1 -1
  20. package/dist/assets/{diagram-PSM6KHXK-3Jj-OXdd.js → diagram-PSM6KHXK-BndVcBhB.js} +1 -1
  21. package/dist/assets/{diagram-QEK2KX5R-DtAwZnbJ.js → diagram-QEK2KX5R-BFiIYeu7.js} +1 -1
  22. package/dist/assets/{diagram-S2PKOQOG-atZg3Snz.js → diagram-S2PKOQOG-DXy-A9pn.js} +1 -1
  23. package/dist/assets/{erDiagram-Q2GNP2WA-BoqqRkgy.js → erDiagram-Q2GNP2WA-D_Bac_58.js} +1 -1
  24. package/dist/assets/{flowDiagram-NV44I4VS-Bmg68IjU.js → flowDiagram-NV44I4VS-DhEi8qtz.js} +1 -1
  25. package/dist/assets/{ganttDiagram-JELNMOA3-53TZ4oeR.js → ganttDiagram-JELNMOA3-BtpUvOuc.js} +1 -1
  26. package/dist/assets/{gitGraphDiagram-V2S2FVAM-D1DddraJ.js → gitGraphDiagram-V2S2FVAM-MBES60cx.js} +1 -1
  27. package/dist/assets/{graph-Cb10Gpkt.js → graph-BcwvIN-m.js} +1 -1
  28. package/dist/assets/index-DLYzf0K-.js +775 -0
  29. package/dist/assets/index-DebsXgfj.css +1 -0
  30. package/dist/assets/{infoDiagram-HS3SLOUP-DxsHJs_0.js → infoDiagram-HS3SLOUP-vaIFYyC9.js} +1 -1
  31. package/dist/assets/{isUndefined-D1qU3_L5.js → isUndefined-DVO6xakl.js} +1 -1
  32. package/dist/assets/{journeyDiagram-XKPGCS4Q-CZDE4S5c.js → journeyDiagram-XKPGCS4Q-D-_q99If.js} +1 -1
  33. package/dist/assets/{kanban-definition-3W4ZIXB7-pRhzMZ6H.js → kanban-definition-3W4ZIXB7-i_AZcs_J.js} +1 -1
  34. package/dist/assets/{layout-C0AW3yEL.js → layout-CPJ9nR_z.js} +1 -1
  35. package/dist/assets/{linear-DobjN4tM.js → linear-KBGpPBiH.js} +1 -1
  36. package/dist/assets/{min-B3Oh8jmi.js → min-By5f7YdD.js} +1 -1
  37. package/dist/assets/{mindmap-definition-VGOIOE7T-g-SEeftZ.js → mindmap-definition-VGOIOE7T-DRjtRxFS.js} +1 -1
  38. package/dist/assets/{pieDiagram-ADFJNKIX-DiwhMRkx.js → pieDiagram-ADFJNKIX-BkkSglWM.js} +1 -1
  39. package/dist/assets/{quadrantDiagram-AYHSOK5B-BIZMdGTC.js → quadrantDiagram-AYHSOK5B-DY-Ng2qt.js} +1 -1
  40. package/dist/assets/{requirementDiagram-UZGBJVZJ-D5nzjn-8.js → requirementDiagram-UZGBJVZJ-B5w0etXG.js} +1 -1
  41. package/dist/assets/{sankeyDiagram-TZEHDZUN-CnCESEvf.js → sankeyDiagram-TZEHDZUN-BWntUp8b.js} +1 -1
  42. package/dist/assets/{sequenceDiagram-WL72ISMW-DKBzac_O.js → sequenceDiagram-WL72ISMW-Cl77fd7y.js} +1 -1
  43. package/dist/assets/{stateDiagram-FKZM4ZOC-CcT1-5cM.js → stateDiagram-FKZM4ZOC-B8_DoPW6.js} +1 -1
  44. package/dist/assets/stateDiagram-v2-4FDKWEC3-DHL7p-md.js +1 -0
  45. package/dist/assets/{timeline-definition-IT6M3QCI-DhFmWcr3.js → timeline-definition-IT6M3QCI-udvNLTe_.js} +1 -1
  46. package/dist/assets/{treemap-GDKQZRPO-CtMKB2mb.js → treemap-GDKQZRPO-BSvoMl74.js} +1 -1
  47. package/dist/assets/{xychartDiagram-PRI3JC2R-OhMt1B4k.js → xychartDiagram-PRI3JC2R-CTdy-FCX.js} +1 -1
  48. package/dist/index.html +2 -7
  49. package/index.html +0 -5
  50. package/package.json +1 -1
  51. package/src/App.tsx +20 -0
  52. package/src/features/chatkit/pages/public-chat.tsx +7 -3
  53. package/src/features/workflow/data/templates/candidate-badges.ts +5 -0
  54. package/src/features/workflow/data/workflow-types.ts +1 -0
  55. package/src/features/workflow/lib/workflow-storage-api.ts +35 -4
  56. package/src/features/workflow/lib/workflow-storage-helpers.ts +10 -3
  57. package/src/features/workflow/lib/workflow-storage.ts +2 -1
  58. package/src/features/workflow/lib/workflow-storage.types.ts +9 -0
  59. package/src/features/workflow/pages/workflow-gallery/create-team-dialog.tsx +137 -0
  60. package/src/features/workflow/pages/workflow-gallery/onboard-team-dialog.tsx +57 -0
  61. package/src/features/workflow/pages/workflow-gallery/team-section.tsx +58 -0
  62. package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-actions.ts +124 -21
  63. package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-state.ts +22 -2
  64. package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery.ts +2 -0
  65. package/src/features/workflow/pages/workflow-gallery/workflow-card.tsx +26 -5
  66. package/src/features/workflow/pages/workflow-gallery/workflow-gallery-tabs.tsx +91 -16
  67. package/src/features/workflow/pages/workflow-gallery.tsx +45 -1
  68. package/src/lib/api.ts +13 -1
  69. package/src/lib/workspace-routing.ts +15 -0
  70. package/src/main.tsx +7 -0
  71. package/dist/assets/channel-C-DaAO-p.js +0 -1
  72. package/dist/assets/classDiagram-2ON5EDUG-B2LNkTFb.js +0 -1
  73. package/dist/assets/classDiagram-v2-WZHVMYZB-B2LNkTFb.js +0 -1
  74. package/dist/assets/clone-DcGVOYZT.js +0 -1
  75. package/dist/assets/index-B0_h2wCA.css +0 -1
  76. package/dist/assets/index-DmIcay1T.js +0 -775
  77. package/dist/assets/stateDiagram-v2-4FDKWEC3-Cle1H2JX.js +0 -1
@@ -1,4 +1,4 @@
1
- import { useCallback } from "react";
1
+ import { useCallback, useState } from "react";
2
2
  import { useNavigate } from "react-router-dom";
3
3
  import { toast } from "@/hooks/use-toast";
4
4
  import {
@@ -10,14 +10,29 @@ import {
10
10
  deleteWorkflow,
11
11
  onboardCandidateAsWorkflow,
12
12
  } from "@features/workflow/lib/workflow-storage";
13
- import { fetchWorkflowVersions } from "@features/workflow/lib/workflow-storage-api";
13
+ import {
14
+ type ApiTeam,
15
+ createTeam,
16
+ deleteTeam,
17
+ fetchWorkflowVersions,
18
+ } from "@features/workflow/lib/workflow-storage-api";
14
19
  import { getWorkflowRouteRef } from "@features/workflow/lib/workflow-storage-helpers";
15
20
  import { getSelectedWorkspaceSlug } from "@/lib/workspace-session";
16
- import { getWorkspaceWorkflowPath } from "@/lib/workspace-routing";
21
+ import {
22
+ getWorkspaceTeamWorkflowPath,
23
+ getWorkspaceWorkflowPath,
24
+ } from "@/lib/workspace-routing";
17
25
  import { type WorkflowGalleryTab } from "./types";
18
26
 
19
27
  interface WorkflowGalleryActionsArgs {
20
28
  setSelectedTab: (value: WorkflowGalleryTab) => void;
29
+ teams: ApiTeam[];
30
+ refreshTeams: () => Promise<void>;
31
+ }
32
+
33
+ interface OnboardTarget {
34
+ candidateId: string;
35
+ candidateName: string;
21
36
  }
22
37
 
23
38
  const STARTER_TEMPLATE_IDS = ["template-python-agent"];
@@ -81,30 +96,27 @@ export const useWorkflowGalleryActions = (
81
96
  state: WorkflowGalleryActionsArgs,
82
97
  ) => {
83
98
  const navigate = useNavigate();
99
+ const [onboardTarget, setOnboardTarget] = useState<OnboardTarget | null>(
100
+ null,
101
+ );
102
+ const [isCreateTeamOpen, setIsCreateTeamOpen] = useState(false);
84
103
 
85
104
  const handleOpenWorkflow = useCallback(
86
- (workflowId: string) => {
87
- navigate(
88
- getWorkspaceWorkflowPath(getSelectedWorkspaceSlug(), workflowId),
89
- );
105
+ (workflowId: string, teamSlug?: string) => {
106
+ const slug = getSelectedWorkspaceSlug();
107
+ const path =
108
+ teamSlug
109
+ ? getWorkspaceTeamWorkflowPath(slug, teamSlug, workflowId)
110
+ : getWorkspaceWorkflowPath(slug, workflowId);
111
+ navigate(path);
90
112
  },
91
113
  [navigate],
92
114
  );
93
115
 
94
- const handleUseTemplate = useCallback(
95
- async (templateId: string) => {
116
+ const performOnboard = useCallback(
117
+ async (candidateId: string, teamId?: string) => {
96
118
  try {
97
- const badge = getCandidateBadgeDefinition(templateId);
98
- if (!badge?.candidateId) {
99
- toast({
100
- title: "Candidate unavailable",
101
- description: "We couldn't find that candidate. Please try another.",
102
- variant: "destructive",
103
- });
104
- return;
105
- }
106
-
107
- const workflow = await onboardCandidateAsWorkflow(badge.candidateId);
119
+ const workflow = await onboardCandidateAsWorkflow(candidateId, teamId);
108
120
 
109
121
  state.setSelectedTab("all");
110
122
 
@@ -113,7 +125,8 @@ export const useWorkflowGalleryActions = (
113
125
  description: `"${workflow.name}" has been onboarded to your workspace.`,
114
126
  });
115
127
 
116
- handleOpenWorkflow(getWorkflowRouteRef(workflow));
128
+ const team = teamId ? state.teams.find((t) => t.id === teamId) : null;
129
+ handleOpenWorkflow(getWorkflowRouteRef(workflow), team?.slug);
117
130
  } catch (error) {
118
131
  toast({
119
132
  title: "Failed to onboard candidate",
@@ -126,6 +139,48 @@ export const useWorkflowGalleryActions = (
126
139
  [handleOpenWorkflow, state],
127
140
  );
128
141
 
142
+ const handleUseTemplate = useCallback(
143
+ async (templateId: string) => {
144
+ const badge = getCandidateBadgeDefinition(templateId);
145
+ if (!badge?.candidateId) {
146
+ toast({
147
+ title: "Candidate unavailable",
148
+ description: "We couldn't find that candidate. Please try another.",
149
+ variant: "destructive",
150
+ });
151
+ return;
152
+ }
153
+
154
+ // With more than one team, ask which team to onboard into. Otherwise the
155
+ // server places the colleague in the workspace's default team.
156
+ if (state.teams.length > 1) {
157
+ setOnboardTarget({
158
+ candidateId: badge.candidateId,
159
+ candidateName: badge.name ?? badge.candidateId,
160
+ });
161
+ return;
162
+ }
163
+
164
+ await performOnboard(badge.candidateId);
165
+ },
166
+ [performOnboard, state.teams],
167
+ );
168
+
169
+ const confirmOnboardTeam = useCallback(
170
+ async (teamId: string) => {
171
+ const target = onboardTarget;
172
+ setOnboardTarget(null);
173
+ if (target) {
174
+ await performOnboard(target.candidateId, teamId);
175
+ }
176
+ },
177
+ [onboardTarget, performOnboard],
178
+ );
179
+
180
+ const cancelOnboardTeam = useCallback(() => {
181
+ setOnboardTarget(null);
182
+ }, []);
183
+
129
184
  const handleImportStarterPack = useCallback(async () => {
130
185
  try {
131
186
  const results = await Promise.allSettled(
@@ -193,6 +248,40 @@ export const useWorkflowGalleryActions = (
193
248
  }
194
249
  }, []);
195
250
 
251
+ const handleCreateTeam = useCallback(
252
+ async (name: string, slug: string) => {
253
+ await createTeam({ name, slug });
254
+ await state.refreshTeams();
255
+ toast({
256
+ title: "Team created",
257
+ description: `"${name}" is ready to group colleagues.`,
258
+ });
259
+ },
260
+ [state],
261
+ );
262
+
263
+ const handleDeleteTeam = useCallback(
264
+ async (teamId: string) => {
265
+ const team = state.teams.find((t) => t.id === teamId);
266
+ try {
267
+ await deleteTeam(teamId);
268
+ await state.refreshTeams();
269
+ toast({
270
+ title: "Team removed",
271
+ description: team ? `"${team.name}" has been removed.` : undefined,
272
+ });
273
+ } catch (error) {
274
+ toast({
275
+ title: "Cannot remove team",
276
+ description:
277
+ error instanceof Error ? error.message : "Unknown error occurred",
278
+ variant: "destructive",
279
+ });
280
+ }
281
+ },
282
+ [state],
283
+ );
284
+
196
285
  const handleDeleteWorkflow = useCallback(
197
286
  async (workflowId: string, workflowName: string) => {
198
287
  try {
@@ -213,11 +302,25 @@ export const useWorkflowGalleryActions = (
213
302
  [],
214
303
  );
215
304
 
305
+ const openCreateTeamDialog = useCallback(() => setIsCreateTeamOpen(true), []);
306
+ const closeCreateTeamDialog = useCallback(
307
+ () => setIsCreateTeamOpen(false),
308
+ [],
309
+ );
310
+
216
311
  return {
217
312
  handleOpenWorkflow,
218
313
  handleUseTemplate,
219
314
  handleImportStarterPack,
220
315
  handleExportWorkflow,
221
316
  handleDeleteWorkflow,
317
+ onboardTarget,
318
+ confirmOnboardTeam,
319
+ cancelOnboardTeam,
320
+ isCreateTeamOpen,
321
+ openCreateTeamDialog,
322
+ closeCreateTeamDialog,
323
+ handleCreateTeam,
324
+ handleDeleteTeam,
222
325
  };
223
326
  };
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { toast } from "@/hooks/use-toast";
3
3
  import {
4
4
  GALLERY_TEMPLATE_WORKFLOWS,
@@ -16,7 +16,9 @@ import {
16
16
  } from "@features/workflow/lib/workflow-storage";
17
17
  import {
18
18
  type ApiCandidate,
19
+ type ApiTeam,
19
20
  fetchCandidates,
21
+ fetchTeams,
20
22
  } from "@features/workflow/lib/workflow-storage-api";
21
23
  import {
22
24
  type WorkflowGalleryTab,
@@ -33,6 +35,8 @@ interface WorkflowGalleryStateSlice {
33
35
  tabCounts: WorkflowGalleryTabCounts;
34
36
  isTemplateView: boolean;
35
37
  templates: Workflow[];
38
+ teams: ApiTeam[];
39
+ refreshTeams: () => Promise<void>;
36
40
  }
37
41
 
38
42
  const toCandidateSpec = (candidate: ApiCandidate): CandidateBadgeSpec => ({
@@ -63,6 +67,7 @@ const matchesWorkflowSearch = (
63
67
  export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
64
68
  const [workflows, setWorkflows] = useState<StoredWorkflow[]>([]);
65
69
  const [candidateWorkflows, setCandidateWorkflows] = useState<Workflow[]>([]);
70
+ const [teams, setTeams] = useState<ApiTeam[]>([]);
66
71
  const [isLoadingWorkflows, setIsLoadingWorkflows] = useState(true);
67
72
  const [searchQuery, setSearchQuery] = useState("");
68
73
  const [selectedTab, setSelectedTab] = useState<WorkflowGalleryTab>("all");
@@ -96,6 +101,15 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
96
101
  };
97
102
  }, []);
98
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
+
99
113
  useEffect(() => {
100
114
  let isMounted = true;
101
115
 
@@ -104,9 +118,13 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
104
118
  setIsLoadingWorkflows(true);
105
119
  }
106
120
  try {
107
- const items = await listWorkflows({ forceRefresh });
121
+ const [items, teamList] = await Promise.all([
122
+ listWorkflows({ forceRefresh }),
123
+ fetchTeams().catch(() => [] as ApiTeam[]),
124
+ ]);
108
125
  if (isMounted) {
109
126
  setWorkflows(items);
127
+ setTeams(teamList);
110
128
  }
111
129
  } catch (error) {
112
130
  if (!isMounted) {
@@ -202,5 +220,7 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
202
220
  tabCounts,
203
221
  isTemplateView,
204
222
  templates,
223
+ teams,
224
+ refreshTeams,
205
225
  };
206
226
  };
@@ -5,6 +5,8 @@ export const useWorkflowGallery = () => {
5
5
  const state = useWorkflowGalleryState();
6
6
  const actions = useWorkflowGalleryActions({
7
7
  setSelectedTab: state.setSelectedTab,
8
+ teams: state.teams,
9
+ refreshTeams: state.refreshTeams,
8
10
  });
9
11
 
10
12
  return {
@@ -19,7 +19,10 @@ import {
19
19
  import { cn } from "@/lib/utils";
20
20
  import { resolveAvatarUrl } from "@/assets/avatars";
21
21
  import { ConfirmDeleteWorkflowDialog } from "@features/workflow/components/dialogs/confirm-delete-workflow-dialog";
22
- import { getCandidateBadgeDefinition } from "@features/workflow/data/templates/candidate-badges";
22
+ import {
23
+ getCandidateBadgeByHandle,
24
+ getCandidateBadgeDefinition,
25
+ } from "@features/workflow/data/templates/candidate-badges";
23
26
  import { type Workflow } from "@features/workflow/data/workflow-data";
24
27
  import { getWorkflowRouteRef } from "@features/workflow/lib/workflow-storage-helpers";
25
28
  import { WORKFLOW_GALLERY_CARD_ASPECT_CLASSNAME } from "./workflow-card-size";
@@ -37,6 +40,7 @@ const getWorkflowTemplateAvatar = (workflow: Workflow): string | undefined => {
37
40
  interface WorkflowCardProps {
38
41
  workflow: Workflow;
39
42
  isTemplate: boolean;
43
+ teamSlug?: string;
40
44
  workspaceLabel: string;
41
45
  onOpenWorkflow: (workflowId: string) => void;
42
46
  onUseTemplate: (workflowId: string) => void;
@@ -50,6 +54,7 @@ interface WorkflowCardProps {
50
54
  export const WorkflowCard = ({
51
55
  workflow,
52
56
  isTemplate,
57
+ teamSlug,
53
58
  workspaceLabel,
54
59
  onOpenWorkflow,
55
60
  onUseTemplate,
@@ -62,8 +67,24 @@ export const WorkflowCard = ({
62
67
  const candidateBadge = isTemplate
63
68
  ? getCandidateBadgeDefinition(workflow.id)
64
69
  : undefined;
70
+ const templateId = !isTemplate
71
+ ? workflow.versions?.at(-1)?.templateId
72
+ : undefined;
73
+ const templateBadge = !isTemplate
74
+ ? (templateId
75
+ ? getCandidateBadgeDefinition(templateId)
76
+ : workflow.handle
77
+ ? getCandidateBadgeByHandle(workflow.handle)
78
+ : undefined)
79
+ : undefined;
80
+ const displayBadge = candidateBadge ?? templateBadge;
65
81
  const headerLabel = isTemplate ? "Candidate" : workspaceLabel;
66
- const workflowSlug = workflow.handle ?? workflow.id;
82
+ const rawHandle = workflow.handle ?? workflow.id;
83
+ const workflowSlug = isTemplate
84
+ ? rawHandle.replace(/^template-/, "").replace(/_/g, "-")
85
+ : rawHandle;
86
+ const displayHandle =
87
+ !isTemplate && teamSlug ? `${teamSlug}/${workflowSlug}` : workflowSlug;
67
88
  const avatarUrl = resolveAvatarUrl(
68
89
  workflow.avatarEmoji ?? getWorkflowTemplateAvatar(workflow),
69
90
  workflow.id,
@@ -271,15 +292,15 @@ export const WorkflowCard = ({
271
292
  </div>
272
293
 
273
294
  <div className="mt-1 shrink-0 font-mono text-[10px] tracking-[0.04em] text-muted-foreground">
274
- @{workflowSlug}
295
+ @{displayHandle}
275
296
  </div>
276
297
 
277
298
  <div className="mt-3 flex min-h-0 w-full flex-1 flex-col items-center justify-start">
278
299
  <div className="h-px w-8 rounded-full bg-border/80" />
279
300
 
280
- {candidateBadge?.subtitle ? (
301
+ {displayBadge?.subtitle ? (
281
302
  <div className="mt-1 shrink-0 font-mono text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
282
- {candidateBadge.subtitle}
303
+ {displayBadge.subtitle}
283
304
  </div>
284
305
  ) : null}
285
306
 
@@ -9,9 +9,11 @@ import {
9
9
  } from "@/design-system/ui/tabs";
10
10
  import { Loader2, Search, Upload, Zap } from "lucide-react";
11
11
  import { type Workflow } from "@features/workflow/data/workflow-data";
12
+ import { type ApiTeam } from "@features/workflow/lib/workflow-storage-api";
12
13
  import { UploadWorkflowDialog } from "@features/workflow/components/dialogs/upload-workflow-dialog";
13
14
  import { useUploadsAllowed } from "@/hooks/use-uploads-allowed";
14
15
  import { WorkflowCard } from "./workflow-card";
16
+ import { TeamSection } from "./team-section";
15
17
  import {
16
18
  type WorkflowGalleryTab,
17
19
  type WorkflowGalleryTabCounts,
@@ -24,17 +26,19 @@ interface WorkflowGalleryTabsProps {
24
26
  sortedWorkflows: Workflow[];
25
27
  tabCounts: WorkflowGalleryTabCounts;
26
28
  isTemplateView: boolean;
29
+ teams?: ApiTeam[];
27
30
  workspaceLabel: string;
28
31
  searchQuery: string;
29
32
  onSearchQueryChange: (value: string) => void;
30
33
  onImportStarterPack: () => void;
31
- onOpenWorkflow: (workflowId: string) => void;
34
+ onOpenWorkflow: (workflowId: string, teamSlug?: string) => void;
32
35
  onUseTemplate: (workflowId: string) => void;
33
36
  onExportWorkflow: (workflow: Workflow) => void;
34
37
  onDeleteWorkflow: (
35
38
  workflowId: string,
36
39
  workflowName: string,
37
40
  ) => Promise<void> | void;
41
+ onDeleteTeam?: (teamId: string) => void;
38
42
  }
39
43
 
40
44
  export const WorkflowGalleryTabs = ({
@@ -44,6 +48,7 @@ export const WorkflowGalleryTabs = ({
44
48
  sortedWorkflows,
45
49
  tabCounts,
46
50
  isTemplateView,
51
+ teams = [],
47
52
  workspaceLabel,
48
53
  searchQuery,
49
54
  onSearchQueryChange,
@@ -52,6 +57,7 @@ export const WorkflowGalleryTabs = ({
52
57
  onUseTemplate,
53
58
  onExportWorkflow,
54
59
  onDeleteWorkflow,
60
+ onDeleteTeam,
55
61
  }: WorkflowGalleryTabsProps) => {
56
62
  const [isUploadOpen, setIsUploadOpen] = useState(false);
57
63
  const uploadsAllowed = useUploadsAllowed();
@@ -60,6 +66,87 @@ export const WorkflowGalleryTabs = ({
60
66
  onSearchQueryChange(event.target.value);
61
67
  };
62
68
 
69
+ const renderGrid = (items: Workflow[], teamSlug?: string) => (
70
+ <div className="grid grid-cols-1 gap-3 pb-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
71
+ {items.map((workflow) => (
72
+ <WorkflowCard
73
+ key={workflow.id}
74
+ workflow={workflow}
75
+ isTemplate={isTemplateView}
76
+ teamSlug={teamSlug}
77
+ workspaceLabel={workspaceLabel}
78
+ onOpenWorkflow={(id) => onOpenWorkflow(id, teamSlug)}
79
+ onUseTemplate={onUseTemplate}
80
+ onExportWorkflow={onExportWorkflow}
81
+ onDeleteWorkflow={onDeleteWorkflow}
82
+ />
83
+ ))}
84
+ </div>
85
+ );
86
+
87
+ // Group colleagues into vertical, collapsible team sections. Candidate
88
+ // (template) view stays flat since candidates are not yet assigned a team.
89
+ const renderColleagues = () => {
90
+ if (isTemplateView) {
91
+ return renderGrid(sortedWorkflows);
92
+ }
93
+
94
+ // No teams yet — fall back to flat grid (e.g. during first load).
95
+ if (teams.length === 0) {
96
+ return renderGrid(sortedWorkflows);
97
+ }
98
+
99
+ const byTeam = new Map<string, Workflow[]>();
100
+ for (const workflow of sortedWorkflows) {
101
+ const key = workflow.teamId ?? "__none__";
102
+ const bucket = byTeam.get(key);
103
+ if (bucket) {
104
+ bucket.push(workflow);
105
+ } else {
106
+ byTeam.set(key, [workflow]);
107
+ }
108
+ }
109
+
110
+ // All known teams, in default-first order from the server, then any
111
+ // orphaned workflows whose team no longer appears in the list.
112
+ const leftovers = [...byTeam.keys()].filter(
113
+ (key) => key !== "__none__" && !teams.some((t) => t.id === key),
114
+ );
115
+ const sections = [
116
+ ...teams.map((team) => ({
117
+ key: team.id,
118
+ name: team.name,
119
+ slug: team.slug,
120
+ })),
121
+ ...leftovers.map((key) => ({ key, name: "Other", slug: undefined })),
122
+ ...(byTeam.has("__none__")
123
+ ? [{ key: "__none__", name: "Ungrouped", slug: undefined }]
124
+ : []),
125
+ ];
126
+
127
+ return (
128
+ <div className="flex flex-col gap-1 pb-6">
129
+ {sections.map((section) => {
130
+ const items = byTeam.get(section.key) ?? [];
131
+ return (
132
+ <TeamSection
133
+ key={section.key}
134
+ name={section.name}
135
+ count={items.length}
136
+ onRemove={
137
+ onDeleteTeam && section.slug !== undefined
138
+ ? () => onDeleteTeam(section.key)
139
+ : undefined
140
+ }
141
+ >
142
+ {items.length > 0 ? renderGrid(items, section.slug) : null}
143
+ </TeamSection>
144
+ );
145
+ })}
146
+ </div>
147
+ );
148
+ };
149
+
63
150
  return (
64
151
  <Tabs
65
152
  value={selectedTab}
@@ -132,7 +219,8 @@ export const WorkflowGalleryTabs = ({
132
219
  </p>
133
220
  </div>
134
221
  </div>
135
- ) : sortedWorkflows.length === 0 ? (
222
+ ) : sortedWorkflows.length === 0 &&
223
+ (isTemplateView || teams.length === 0) ? (
136
224
  <div className="flex flex-col items-center justify-center py-12 text-center">
137
225
  <div className="mb-4 rounded-full bg-muted p-4">
138
226
  <Zap className="h-8 w-8 text-muted-foreground" />
@@ -152,20 +240,7 @@ export const WorkflowGalleryTabs = ({
152
240
  </div>
153
241
  </div>
154
242
  ) : (
155
- <div className="grid grid-cols-1 gap-3 pb-6 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
156
- {sortedWorkflows.map((workflow) => (
157
- <WorkflowCard
158
- key={workflow.id}
159
- workflow={workflow}
160
- isTemplate={isTemplateView}
161
- workspaceLabel={workspaceLabel}
162
- onOpenWorkflow={onOpenWorkflow}
163
- onUseTemplate={onUseTemplate}
164
- onExportWorkflow={onExportWorkflow}
165
- onDeleteWorkflow={onDeleteWorkflow}
166
- />
167
- ))}
168
- </div>
243
+ renderColleagues()
169
244
  )}
170
245
  </TabsContent>
171
246
  </Tabs>
@@ -3,7 +3,11 @@ import TopNavigation from "@features/shared/components/top-navigation";
3
3
  import { getActiveWorkspace } from "@/lib/api";
4
4
  import useCredentialVault from "@/hooks/use-credential-vault";
5
5
  import { usePageContext } from "@/hooks/use-page-context";
6
+ import { Button } from "@/design-system/ui/button";
7
+ import { Plus } from "lucide-react";
6
8
  import { WorkflowGalleryTabs } from "@/features/workflow/pages/workflow-gallery/workflow-gallery-tabs";
9
+ import { OnboardTeamDialog } from "@/features/workflow/pages/workflow-gallery/onboard-team-dialog";
10
+ import { CreateTeamDialog } from "@/features/workflow/pages/workflow-gallery/create-team-dialog";
7
11
  import { useWorkflowGallery } from "@/features/workflow/pages/workflow-gallery/use-workflow-gallery";
8
12
 
9
13
  export default function WorkflowGallery() {
@@ -53,11 +57,20 @@ export default function WorkflowGallery() {
53
57
  sortedWorkflows,
54
58
  tabCounts,
55
59
  isTemplateView,
60
+ teams,
56
61
  handleUseTemplate,
57
62
  handleImportStarterPack,
58
63
  handleExportWorkflow,
59
64
  handleDeleteWorkflow,
60
65
  handleOpenWorkflow,
66
+ onboardTarget,
67
+ confirmOnboardTeam,
68
+ cancelOnboardTeam,
69
+ isCreateTeamOpen,
70
+ openCreateTeamDialog,
71
+ closeCreateTeamDialog,
72
+ handleCreateTeam,
73
+ handleDeleteTeam,
61
74
  } = useWorkflowGallery();
62
75
 
63
76
  return (
@@ -71,7 +84,7 @@ export default function WorkflowGallery() {
71
84
  onRevealCredentialSecret={onRevealCredentialSecret}
72
85
  />
73
86
 
74
- <main className="flex flex-1 min-h-0 flex-col overflow-hidden">
87
+ <main className="relative flex flex-1 min-h-0 flex-col overflow-hidden">
75
88
  <div className="flex-1 overflow-auto">
76
89
  <WorkflowGalleryTabs
77
90
  selectedTab={selectedTab}
@@ -80,6 +93,7 @@ export default function WorkflowGallery() {
80
93
  sortedWorkflows={sortedWorkflows}
81
94
  tabCounts={tabCounts}
82
95
  isTemplateView={isTemplateView}
96
+ teams={teams}
83
97
  workspaceLabel={workspaceLabel}
84
98
  searchQuery={searchQuery}
85
99
  onSearchQueryChange={setSearchQuery}
@@ -88,9 +102,39 @@ export default function WorkflowGallery() {
88
102
  onUseTemplate={handleUseTemplate}
89
103
  onExportWorkflow={handleExportWorkflow}
90
104
  onDeleteWorkflow={handleDeleteWorkflow}
105
+ onDeleteTeam={handleDeleteTeam}
91
106
  />
92
107
  </div>
108
+
109
+ {!isTemplateView && (
110
+ <div className="absolute bottom-4 left-4">
111
+ <Button variant="outline" size="sm" onClick={openCreateTeamDialog}>
112
+ <Plus className="mr-2 h-4 w-4" />
113
+ New team
114
+ </Button>
115
+ </div>
116
+ )}
93
117
  </main>
118
+
119
+ <CreateTeamDialog
120
+ open={isCreateTeamOpen}
121
+ onOpenChange={(open) => {
122
+ if (!open) closeCreateTeamDialog();
123
+ }}
124
+ onCreate={handleCreateTeam}
125
+ />
126
+
127
+ <OnboardTeamDialog
128
+ open={onboardTarget !== null}
129
+ candidateName={onboardTarget?.candidateName ?? ""}
130
+ teams={teams}
131
+ onSelect={confirmOnboardTeam}
132
+ onOpenChange={(open) => {
133
+ if (!open) {
134
+ cancelOnboardTeam();
135
+ }
136
+ }}
137
+ />
94
138
  </div>
95
139
  );
96
140
  }
package/src/lib/api.ts CHANGED
@@ -244,7 +244,19 @@ async function requestSystemJson<T>(
244
244
  const errorData = await response.json().catch(() => ({
245
245
  detail: "Failed to complete request",
246
246
  }));
247
- throw new Error(errorData.detail || `HTTP ${response.status}`);
247
+ const detail = errorData.detail;
248
+ let message: string;
249
+ if (typeof detail === "string") {
250
+ message = detail;
251
+ } else if (detail && typeof detail === "object" && "error" in detail) {
252
+ const err = detail.error as Record<string, unknown>;
253
+ message = typeof err.message === "string" ? err.message : `HTTP ${response.status}`;
254
+ } else if (Array.isArray(detail)) {
255
+ message = detail.map((e: Record<string, unknown>) => e.msg ?? JSON.stringify(e)).join("; ");
256
+ } else {
257
+ message = `HTTP ${response.status}`;
258
+ }
259
+ throw new Error(message);
248
260
  }
249
261
 
250
262
  return response.json();
@@ -53,6 +53,21 @@ export const getWorkspaceWorkflowPath = (
53
53
  return slug ? `/${slug}/${ref}` : `/${ref}`;
54
54
  };
55
55
 
56
+ export const getWorkspaceTeamWorkflowPath = (
57
+ workspaceSlug: string | null | undefined,
58
+ teamSlug: string,
59
+ workflowRef: string,
60
+ ): string => {
61
+ const slug = trimPathSegment(workspaceSlug);
62
+ const ref = workflowRef.trim();
63
+ if (!ref) {
64
+ return getWorkspaceGalleryPath(slug);
65
+ }
66
+ return slug
67
+ ? `/${slug}/team/${teamSlug}/${ref}`
68
+ : `/team/${teamSlug}/${ref}`;
69
+ };
70
+
56
71
  export const getWorkspaceSlugFromPathname = (
57
72
  pathname: string,
58
73
  ): string | null => {
package/src/main.tsx CHANGED
@@ -3,8 +3,15 @@ import * as ReactDOM from "react-dom";
3
3
  import { createRoot } from "react-dom/client";
4
4
  import App from "./App";
5
5
  import "./index.css";
6
+ import { buildBackendHttpUrl } from "./lib/config";
6
7
  import { initializeTheme, watchSystemTheme } from "./lib/theme";
7
8
 
9
+ const chatkitScript = document.createElement("script");
10
+ chatkitScript.src = buildBackendHttpUrl("/api/chatkit/assets/chatkit.js");
11
+ chatkitScript.async = true;
12
+ chatkitScript.crossOrigin = "anonymous";
13
+ document.head.appendChild(chatkitScript);
14
+
8
15
  // Initialize theme before React renders to prevent flash of wrong theme
9
16
  initializeTheme();
10
17
 
@@ -1 +0,0 @@
1
- import{ap as o,aq as n}from"./index-DmIcay1T.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-Duatgdyr.js";import{_ as i}from"./index-DmIcay1T.js";import"./chunk-FMBD7UC4-C07lS0oH.js";import"./chunk-55IACEB6-BTpqJ_MA.js";import"./chunk-QN33PNHL-C09PhK0w.js";var p={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{p as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-Duatgdyr.js";import{_ as i}from"./index-DmIcay1T.js";import"./chunk-FMBD7UC4-C07lS0oH.js";import"./chunk-55IACEB6-BTpqJ_MA.js";import"./chunk-QN33PNHL-C09PhK0w.js";var p={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{p as diagram};
@@ -1 +0,0 @@
1
- import{ay as r}from"./index-DmIcay1T.js";var a=4;function n(o){return r(o,a)}export{n as c};