orcheo-studio 0.21.3 → 0.23.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 (99) hide show
  1. package/.bumpversion.cfg +1 -1
  2. package/dist/assets/{arc-CI8iBP22.js → arc-4lqv6Kjm.js} +1 -1
  3. package/dist/assets/{architectureDiagram-VXUJARFQ-9lccnHEL.js → architectureDiagram-VXUJARFQ-DjhiFMDA.js} +1 -1
  4. package/dist/assets/{blockDiagram-VD42YOAC-CJPKWPSp.js → blockDiagram-VD42YOAC-AZ1Rspxy.js} +1 -1
  5. package/dist/assets/{c4Diagram-YG6GDRKO-CuW7nex5.js → c4Diagram-YG6GDRKO-eH7LJ-pB.js} +1 -1
  6. package/dist/assets/channel-BbjHmdR-.js +1 -0
  7. package/dist/assets/{chunk-4BX2VUAB-BVQ3rR7o.js → chunk-4BX2VUAB-DJPBOwLj.js} +1 -1
  8. package/dist/assets/{chunk-55IACEB6-BTpqJ_MA.js → chunk-55IACEB6-BtkroFP3.js} +1 -1
  9. package/dist/assets/{chunk-B4BG7PRW-Duatgdyr.js → chunk-B4BG7PRW-9UuoCVI1.js} +1 -1
  10. package/dist/assets/{chunk-DI55MBZ5-CxEvlbgl.js → chunk-DI55MBZ5-DFossSVg.js} +1 -1
  11. package/dist/assets/{chunk-FMBD7UC4-C07lS0oH.js → chunk-FMBD7UC4-DZoCKsqW.js} +1 -1
  12. package/dist/assets/{chunk-QN33PNHL-C09PhK0w.js → chunk-QN33PNHL-CGFY-glY.js} +1 -1
  13. package/dist/assets/{chunk-QZHKN3VN-Dy3GgrIN.js → chunk-QZHKN3VN-CBx9MjYx.js} +1 -1
  14. package/dist/assets/{chunk-TZMSLE5B-9HPWR_AG.js → chunk-TZMSLE5B-BQ1P9b2p.js} +1 -1
  15. package/dist/assets/classDiagram-2ON5EDUG-B3gA5-gj.js +1 -0
  16. package/dist/assets/classDiagram-v2-WZHVMYZB-B3gA5-gj.js +1 -0
  17. package/dist/assets/clone-Ctw1pu1z.js +1 -0
  18. package/dist/assets/{cose-bilkent-S5V4N54A-B9TWyynz.js → cose-bilkent-S5V4N54A-Dy5ctOeT.js} +1 -1
  19. package/dist/assets/{dagre-6UL2VRFP-DFuJOuNQ.js → dagre-6UL2VRFP-CFPE5-yw.js} +1 -1
  20. package/dist/assets/{diagram-PSM6KHXK-3Jj-OXdd.js → diagram-PSM6KHXK-DmESDfR2.js} +1 -1
  21. package/dist/assets/{diagram-QEK2KX5R-DtAwZnbJ.js → diagram-QEK2KX5R-CumSxfGH.js} +1 -1
  22. package/dist/assets/{diagram-S2PKOQOG-atZg3Snz.js → diagram-S2PKOQOG-C1AaIowU.js} +1 -1
  23. package/dist/assets/{erDiagram-Q2GNP2WA-BoqqRkgy.js → erDiagram-Q2GNP2WA-BKk1ijaf.js} +1 -1
  24. package/dist/assets/{flowDiagram-NV44I4VS-Bmg68IjU.js → flowDiagram-NV44I4VS-BFMJTeTZ.js} +1 -1
  25. package/dist/assets/{ganttDiagram-JELNMOA3-53TZ4oeR.js → ganttDiagram-JELNMOA3-CYobSBkw.js} +1 -1
  26. package/dist/assets/{gitGraphDiagram-V2S2FVAM-D1DddraJ.js → gitGraphDiagram-V2S2FVAM-DYNLBSGu.js} +1 -1
  27. package/dist/assets/{graph-Cb10Gpkt.js → graph-CCH_5x2X.js} +1 -1
  28. package/dist/assets/index-BVY_PYJU.css +1 -0
  29. package/dist/assets/index-CD3NusgQ.js +790 -0
  30. package/dist/assets/{infoDiagram-HS3SLOUP-DxsHJs_0.js → infoDiagram-HS3SLOUP-C8K2nOgc.js} +1 -1
  31. package/dist/assets/{isUndefined-D1qU3_L5.js → isUndefined-Cg-i6rYz.js} +1 -1
  32. package/dist/assets/{journeyDiagram-XKPGCS4Q-CZDE4S5c.js → journeyDiagram-XKPGCS4Q-UjysRuQi.js} +1 -1
  33. package/dist/assets/{kanban-definition-3W4ZIXB7-pRhzMZ6H.js → kanban-definition-3W4ZIXB7-C0mLJbkK.js} +1 -1
  34. package/dist/assets/{layout-C0AW3yEL.js → layout-Ctq7ed-p.js} +1 -1
  35. package/dist/assets/{linear-DobjN4tM.js → linear-MdTfjVMd.js} +1 -1
  36. package/dist/assets/{min-B3Oh8jmi.js → min-CkeT6BIA.js} +1 -1
  37. package/dist/assets/{mindmap-definition-VGOIOE7T-g-SEeftZ.js → mindmap-definition-VGOIOE7T-B1KVkl4d.js} +1 -1
  38. package/dist/assets/{pieDiagram-ADFJNKIX-DiwhMRkx.js → pieDiagram-ADFJNKIX-Cw6TMSJj.js} +1 -1
  39. package/dist/assets/{quadrantDiagram-AYHSOK5B-BIZMdGTC.js → quadrantDiagram-AYHSOK5B-8Lq_anf6.js} +1 -1
  40. package/dist/assets/{requirementDiagram-UZGBJVZJ-D5nzjn-8.js → requirementDiagram-UZGBJVZJ-CXBuMo24.js} +1 -1
  41. package/dist/assets/{sankeyDiagram-TZEHDZUN-CnCESEvf.js → sankeyDiagram-TZEHDZUN-DggxeLrr.js} +1 -1
  42. package/dist/assets/{sequenceDiagram-WL72ISMW-DKBzac_O.js → sequenceDiagram-WL72ISMW-DZqrjzbw.js} +1 -1
  43. package/dist/assets/{stateDiagram-FKZM4ZOC-CcT1-5cM.js → stateDiagram-FKZM4ZOC-D4rhKl_S.js} +1 -1
  44. package/dist/assets/stateDiagram-v2-4FDKWEC3-C81OVjQ3.js +1 -0
  45. package/dist/assets/{timeline-definition-IT6M3QCI-DhFmWcr3.js → timeline-definition-IT6M3QCI-vp3knils.js} +1 -1
  46. package/dist/assets/{treemap-GDKQZRPO-CtMKB2mb.js → treemap-GDKQZRPO-RByzG6o8.js} +1 -1
  47. package/dist/assets/{xychartDiagram-PRI3JC2R-OhMt1B4k.js → xychartDiagram-PRI3JC2R-DOLBdPza.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 +27 -2
  52. package/src/features/account/pages/invitation-accept.tsx +92 -0
  53. package/src/features/account/pages/workspace-members.tsx +153 -42
  54. package/src/features/auth/components/require-auth.tsx +13 -18
  55. package/src/features/auth/lib/auth-api.ts +196 -0
  56. package/src/features/auth/lib/auth-session.ts +0 -1
  57. package/src/features/auth/pages/login.tsx +169 -16
  58. package/src/features/auth/pages/verify.tsx +89 -0
  59. package/src/features/chatkit/components/public-chat-widget.tsx +89 -8
  60. package/src/features/chatkit/lib/chatkit-client.ts +66 -0
  61. package/src/features/chatkit/pages/public-chat.tsx +24 -33
  62. package/src/features/shared/components/top-navigation/account-menu.tsx +20 -7
  63. package/src/features/shared/components/top-navigation/studio-brand.tsx +14 -3
  64. package/src/features/workflow/components/dialogs/publish-workflow-dialog.tsx +121 -0
  65. package/src/features/workflow/data/templates/candidate-badges.ts +5 -0
  66. package/src/features/workflow/data/workflow-types.ts +1 -0
  67. package/src/features/workflow/lib/workflow-storage-api.ts +35 -4
  68. package/src/features/workflow/lib/workflow-storage-helpers.ts +15 -6
  69. package/src/features/workflow/lib/workflow-storage.ts +2 -1
  70. package/src/features/workflow/lib/workflow-storage.types.ts +10 -0
  71. package/src/features/workflow/pages/workflow/components/workflow-tab-content.tsx +122 -37
  72. package/src/features/workflow/pages/workflow/hooks/controller/build-layout-props.ts +2 -0
  73. package/src/features/workflow/pages/workflow/hooks/controller/use-workflow-lifecycle.ts +1 -0
  74. package/src/features/workflow/pages/workflow/hooks/use-workflow-loader.ts +5 -0
  75. package/src/features/workflow/pages/workflow/hooks/use-workflow-metadata-state.ts +3 -0
  76. package/src/features/workflow/pages/workflow-gallery/create-team-dialog.tsx +137 -0
  77. package/src/features/workflow/pages/workflow-gallery/onboard-team-dialog.tsx +57 -0
  78. package/src/features/workflow/pages/workflow-gallery/team-section.tsx +58 -0
  79. package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-actions.ts +124 -21
  80. package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-state.ts +22 -2
  81. package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery.ts +2 -0
  82. package/src/features/workflow/pages/workflow-gallery/workflow-card.tsx +26 -5
  83. package/src/features/workflow/pages/workflow-gallery/workflow-gallery-tabs.tsx +92 -17
  84. package/src/features/workflow/pages/workflow-gallery.tsx +45 -1
  85. package/src/lib/api.ts +91 -2
  86. package/src/lib/workspace-routing.ts +15 -0
  87. package/src/main.tsx +7 -0
  88. package/src/vite-env.d.ts +1 -0
  89. package/dist/assets/channel-C-DaAO-p.js +0 -1
  90. package/dist/assets/classDiagram-2ON5EDUG-B2LNkTFb.js +0 -1
  91. package/dist/assets/classDiagram-v2-WZHVMYZB-B2LNkTFb.js +0 -1
  92. package/dist/assets/clone-DcGVOYZT.js +0 -1
  93. package/dist/assets/index-B0_h2wCA.css +0 -1
  94. package/dist/assets/index-DmIcay1T.js +0 -775
  95. package/dist/assets/stateDiagram-v2-4FDKWEC3-Cle1H2JX.js +0 -1
  96. package/src/features/MODULES.md +0 -199
  97. package/src/features/auth/components/auto-login.tsx +0 -51
  98. package/src/features/auth/lib/oidc-client.ts +0 -565
  99. package/src/features/auth/pages/oauth-callback.tsx +0 -63
@@ -21,6 +21,7 @@ export function useWorkflowMetadataState() {
21
21
  null,
22
22
  );
23
23
  const [isWorkflowPublic, setIsWorkflowPublic] = useState(false);
24
+ const [workflowRequireLogin, setWorkflowRequireLogin] = useState(false);
24
25
  const [workflowShareUrl, setWorkflowShareUrl] = useState<string | null>(null);
25
26
  const [chatkitStartScreenPrompts, setChatkitStartScreenPrompts] = useState<
26
27
  ChatKitStartScreenPrompt[] | null
@@ -46,6 +47,8 @@ export function useWorkflowMetadataState() {
46
47
  setWorkflowLoadError,
47
48
  isWorkflowPublic,
48
49
  setIsWorkflowPublic,
50
+ workflowRequireLogin,
51
+ setWorkflowRequireLogin,
49
52
  workflowShareUrl,
50
53
  setWorkflowShareUrl,
51
54
  chatkitStartScreenPrompts,
@@ -0,0 +1,137 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Button } from "@/design-system/ui/button";
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogFooter,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from "@/design-system/ui/dialog";
11
+ import { Input } from "@/design-system/ui/input";
12
+ import { Label } from "@/design-system/ui/label";
13
+
14
+ interface CreateTeamDialogProps {
15
+ open: boolean;
16
+ onOpenChange: (open: boolean) => void;
17
+ onCreate: (name: string, slug: string) => Promise<void>;
18
+ }
19
+
20
+ const toSlug = (name: string): string =>
21
+ name
22
+ .trim()
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9_-]+/g, "-")
25
+ .replace(/^-+|-+$/g, "");
26
+
27
+ export const CreateTeamDialog = ({
28
+ open,
29
+ onOpenChange,
30
+ onCreate,
31
+ }: CreateTeamDialogProps) => {
32
+ const [name, setName] = useState("");
33
+ const [slug, setSlug] = useState("");
34
+ const [slugDirty, setSlugDirty] = useState(false);
35
+ const [error, setError] = useState<string | null>(null);
36
+ const [isSubmitting, setIsSubmitting] = useState(false);
37
+
38
+ useEffect(() => {
39
+ if (!open) {
40
+ setName("");
41
+ setSlug("");
42
+ setSlugDirty(false);
43
+ setError(null);
44
+ setIsSubmitting(false);
45
+ }
46
+ }, [open]);
47
+
48
+ const handleNameChange = (value: string) => {
49
+ setName(value);
50
+ if (!slugDirty) {
51
+ setSlug(toSlug(value));
52
+ }
53
+ };
54
+
55
+ const handleSlugChange = (value: string) => {
56
+ setSlugDirty(true);
57
+ setSlug(value.toLowerCase().replace(/[^a-z0-9_-]/g, ""));
58
+ };
59
+
60
+ const handleSubmit = async () => {
61
+ const trimmedName = name.trim();
62
+ const trimmedSlug = slug.trim();
63
+ if (!trimmedName || !trimmedSlug) {
64
+ setError("Both name and slug are required.");
65
+ return;
66
+ }
67
+ setError(null);
68
+ setIsSubmitting(true);
69
+ try {
70
+ await onCreate(trimmedName, trimmedSlug);
71
+ onOpenChange(false);
72
+ } catch (err) {
73
+ setError(
74
+ err instanceof Error ? err.message : "Failed to create team.",
75
+ );
76
+ } finally {
77
+ setIsSubmitting(false);
78
+ }
79
+ };
80
+
81
+ return (
82
+ <Dialog open={open} onOpenChange={onOpenChange}>
83
+ <DialogContent>
84
+ <DialogHeader>
85
+ <DialogTitle>New team</DialogTitle>
86
+ <DialogDescription>
87
+ Create a team to group colleagues under a shared label.
88
+ </DialogDescription>
89
+ </DialogHeader>
90
+
91
+ <div className="flex flex-col gap-4 py-2">
92
+ <div className="flex flex-col gap-1.5">
93
+ <Label htmlFor="team-name">Name</Label>
94
+ <Input
95
+ id="team-name"
96
+ placeholder="e.g. Sales"
97
+ value={name}
98
+ onChange={(e) => handleNameChange(e.target.value)}
99
+ onKeyDown={(e) => {
100
+ if (e.key === "Enter") void handleSubmit();
101
+ }}
102
+ autoFocus
103
+ />
104
+ </div>
105
+ <div className="flex flex-col gap-1.5">
106
+ <Label htmlFor="team-slug">Slug</Label>
107
+ <Input
108
+ id="team-slug"
109
+ placeholder="e.g. sales"
110
+ value={slug}
111
+ onChange={(e) => handleSlugChange(e.target.value)}
112
+ onKeyDown={(e) => {
113
+ if (e.key === "Enter") void handleSubmit();
114
+ }}
115
+ />
116
+ <p className="text-xs text-muted-foreground">
117
+ Used in API references. Lowercase letters, numbers, hyphens, and
118
+ underscores only.
119
+ </p>
120
+ </div>
121
+ {error ? (
122
+ <p className="text-sm text-destructive">{error}</p>
123
+ ) : null}
124
+ </div>
125
+
126
+ <DialogFooter>
127
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
128
+ Cancel
129
+ </Button>
130
+ <Button onClick={() => void handleSubmit()} disabled={isSubmitting}>
131
+ {isSubmitting ? "Creating…" : "Create team"}
132
+ </Button>
133
+ </DialogFooter>
134
+ </DialogContent>
135
+ </Dialog>
136
+ );
137
+ };
@@ -0,0 +1,57 @@
1
+ import { Button } from "@/design-system/ui/button";
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogHeader,
7
+ DialogTitle,
8
+ } from "@/design-system/ui/dialog";
9
+ import { type ApiTeam } from "@features/workflow/lib/workflow-storage-api";
10
+
11
+ interface OnboardTeamDialogProps {
12
+ open: boolean;
13
+ candidateName: string;
14
+ teams: ApiTeam[];
15
+ onSelect: (teamId: string) => void;
16
+ onOpenChange: (open: boolean) => void;
17
+ }
18
+
19
+ /**
20
+ * Prompts which team to onboard a candidate into when the workspace has more
21
+ * than one team.
22
+ */
23
+ export const OnboardTeamDialog = ({
24
+ open,
25
+ candidateName,
26
+ teams,
27
+ onSelect,
28
+ onOpenChange,
29
+ }: OnboardTeamDialogProps) => {
30
+ return (
31
+ <Dialog open={open} onOpenChange={onOpenChange}>
32
+ <DialogContent>
33
+ <DialogHeader>
34
+ <DialogTitle>Choose a team</DialogTitle>
35
+ <DialogDescription>
36
+ Select the team to onboard "{candidateName}" into.
37
+ </DialogDescription>
38
+ </DialogHeader>
39
+ <div className="flex flex-col gap-2">
40
+ {teams.map((team) => (
41
+ <Button
42
+ key={team.id}
43
+ variant="outline"
44
+ className="justify-between"
45
+ onClick={() => onSelect(team.id)}
46
+ >
47
+ <span>{team.name}</span>
48
+ {team.is_default ? (
49
+ <span className="text-xs text-muted-foreground">Default</span>
50
+ ) : null}
51
+ </Button>
52
+ ))}
53
+ </div>
54
+ </DialogContent>
55
+ </Dialog>
56
+ );
57
+ };
@@ -0,0 +1,58 @@
1
+ import { type ReactNode, useState } from "react";
2
+ import { ChevronDown, ChevronRight, Trash2 } from "lucide-react";
3
+ import { Button } from "@/design-system/ui/button";
4
+
5
+ interface TeamSectionProps {
6
+ name: string;
7
+ count: number;
8
+ defaultOpen?: boolean;
9
+ onRemove?: () => void;
10
+ children: ReactNode;
11
+ }
12
+
13
+ /**
14
+ * A vertically-stacked, individually collapsible section grouping the AI
15
+ * colleagues that belong to a single team.
16
+ */
17
+ export const TeamSection = ({
18
+ name,
19
+ count,
20
+ defaultOpen = true,
21
+ onRemove,
22
+ children,
23
+ }: TeamSectionProps) => {
24
+ const [isOpen, setIsOpen] = useState(defaultOpen);
25
+
26
+ return (
27
+ <section className="border-b border-border/60 pb-4 last:border-b-0">
28
+ <div className="group flex items-center py-3">
29
+ <button
30
+ type="button"
31
+ aria-expanded={isOpen}
32
+ onClick={() => setIsOpen((open) => !open)}
33
+ className="flex items-center gap-2 text-left"
34
+ >
35
+ {isOpen ? (
36
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
37
+ ) : (
38
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
39
+ )}
40
+ <span className="text-sm font-semibold">{name}</span>
41
+ <span className="text-xs text-muted-foreground">{count}</span>
42
+ </button>
43
+ {onRemove ? (
44
+ <Button
45
+ variant="ghost"
46
+ size="icon"
47
+ className="ml-1 h-7 w-7 opacity-0 group-hover:opacity-100"
48
+ onClick={onRemove}
49
+ aria-label={`Remove team ${name}`}
50
+ >
51
+ <Trash2 className="h-3.5 w-3.5 text-muted-foreground" />
52
+ </Button>
53
+ ) : null}
54
+ </div>
55
+ {isOpen ? children : null}
56
+ </section>
57
+ );
58
+ };
@@ -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