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.
- package/.bumpversion.cfg +1 -1
- package/dist/assets/{arc-CI8iBP22.js → arc-4lqv6Kjm.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-9lccnHEL.js → architectureDiagram-VXUJARFQ-DjhiFMDA.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-CJPKWPSp.js → blockDiagram-VD42YOAC-AZ1Rspxy.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-CuW7nex5.js → c4Diagram-YG6GDRKO-eH7LJ-pB.js} +1 -1
- package/dist/assets/channel-BbjHmdR-.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-BVQ3rR7o.js → chunk-4BX2VUAB-DJPBOwLj.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-BTpqJ_MA.js → chunk-55IACEB6-BtkroFP3.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-Duatgdyr.js → chunk-B4BG7PRW-9UuoCVI1.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-CxEvlbgl.js → chunk-DI55MBZ5-DFossSVg.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C07lS0oH.js → chunk-FMBD7UC4-DZoCKsqW.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-C09PhK0w.js → chunk-QN33PNHL-CGFY-glY.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Dy3GgrIN.js → chunk-QZHKN3VN-CBx9MjYx.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-9HPWR_AG.js → chunk-TZMSLE5B-BQ1P9b2p.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-B3gA5-gj.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-B3gA5-gj.js +1 -0
- package/dist/assets/clone-Ctw1pu1z.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-B9TWyynz.js → cose-bilkent-S5V4N54A-Dy5ctOeT.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-DFuJOuNQ.js → dagre-6UL2VRFP-CFPE5-yw.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-3Jj-OXdd.js → diagram-PSM6KHXK-DmESDfR2.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-DtAwZnbJ.js → diagram-QEK2KX5R-CumSxfGH.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-atZg3Snz.js → diagram-S2PKOQOG-C1AaIowU.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-BoqqRkgy.js → erDiagram-Q2GNP2WA-BKk1ijaf.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-Bmg68IjU.js → flowDiagram-NV44I4VS-BFMJTeTZ.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-53TZ4oeR.js → ganttDiagram-JELNMOA3-CYobSBkw.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-D1DddraJ.js → gitGraphDiagram-V2S2FVAM-DYNLBSGu.js} +1 -1
- package/dist/assets/{graph-Cb10Gpkt.js → graph-CCH_5x2X.js} +1 -1
- package/dist/assets/index-BVY_PYJU.css +1 -0
- package/dist/assets/index-CD3NusgQ.js +790 -0
- package/dist/assets/{infoDiagram-HS3SLOUP-DxsHJs_0.js → infoDiagram-HS3SLOUP-C8K2nOgc.js} +1 -1
- package/dist/assets/{isUndefined-D1qU3_L5.js → isUndefined-Cg-i6rYz.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-CZDE4S5c.js → journeyDiagram-XKPGCS4Q-UjysRuQi.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-pRhzMZ6H.js → kanban-definition-3W4ZIXB7-C0mLJbkK.js} +1 -1
- package/dist/assets/{layout-C0AW3yEL.js → layout-Ctq7ed-p.js} +1 -1
- package/dist/assets/{linear-DobjN4tM.js → linear-MdTfjVMd.js} +1 -1
- package/dist/assets/{min-B3Oh8jmi.js → min-CkeT6BIA.js} +1 -1
- package/dist/assets/{mindmap-definition-VGOIOE7T-g-SEeftZ.js → mindmap-definition-VGOIOE7T-B1KVkl4d.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-DiwhMRkx.js → pieDiagram-ADFJNKIX-Cw6TMSJj.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-BIZMdGTC.js → quadrantDiagram-AYHSOK5B-8Lq_anf6.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-D5nzjn-8.js → requirementDiagram-UZGBJVZJ-CXBuMo24.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-CnCESEvf.js → sankeyDiagram-TZEHDZUN-DggxeLrr.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-DKBzac_O.js → sequenceDiagram-WL72ISMW-DZqrjzbw.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-CcT1-5cM.js → stateDiagram-FKZM4ZOC-D4rhKl_S.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-C81OVjQ3.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-DhFmWcr3.js → timeline-definition-IT6M3QCI-vp3knils.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-CtMKB2mb.js → treemap-GDKQZRPO-RByzG6o8.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-OhMt1B4k.js → xychartDiagram-PRI3JC2R-DOLBdPza.js} +1 -1
- package/dist/index.html +2 -7
- package/index.html +0 -5
- package/package.json +1 -1
- package/src/App.tsx +27 -2
- package/src/features/account/pages/invitation-accept.tsx +92 -0
- package/src/features/account/pages/workspace-members.tsx +153 -42
- package/src/features/auth/components/require-auth.tsx +13 -18
- package/src/features/auth/lib/auth-api.ts +196 -0
- package/src/features/auth/lib/auth-session.ts +0 -1
- package/src/features/auth/pages/login.tsx +169 -16
- package/src/features/auth/pages/verify.tsx +89 -0
- package/src/features/chatkit/components/public-chat-widget.tsx +89 -8
- package/src/features/chatkit/lib/chatkit-client.ts +66 -0
- package/src/features/chatkit/pages/public-chat.tsx +24 -33
- package/src/features/shared/components/top-navigation/account-menu.tsx +20 -7
- package/src/features/shared/components/top-navigation/studio-brand.tsx +14 -3
- package/src/features/workflow/components/dialogs/publish-workflow-dialog.tsx +121 -0
- package/src/features/workflow/data/templates/candidate-badges.ts +5 -0
- package/src/features/workflow/data/workflow-types.ts +1 -0
- package/src/features/workflow/lib/workflow-storage-api.ts +35 -4
- package/src/features/workflow/lib/workflow-storage-helpers.ts +15 -6
- package/src/features/workflow/lib/workflow-storage.ts +2 -1
- package/src/features/workflow/lib/workflow-storage.types.ts +10 -0
- package/src/features/workflow/pages/workflow/components/workflow-tab-content.tsx +122 -37
- package/src/features/workflow/pages/workflow/hooks/controller/build-layout-props.ts +2 -0
- package/src/features/workflow/pages/workflow/hooks/controller/use-workflow-lifecycle.ts +1 -0
- package/src/features/workflow/pages/workflow/hooks/use-workflow-loader.ts +5 -0
- package/src/features/workflow/pages/workflow/hooks/use-workflow-metadata-state.ts +3 -0
- package/src/features/workflow/pages/workflow-gallery/create-team-dialog.tsx +137 -0
- package/src/features/workflow/pages/workflow-gallery/onboard-team-dialog.tsx +57 -0
- package/src/features/workflow/pages/workflow-gallery/team-section.tsx +58 -0
- package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-actions.ts +124 -21
- package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-state.ts +22 -2
- package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery.ts +2 -0
- package/src/features/workflow/pages/workflow-gallery/workflow-card.tsx +26 -5
- package/src/features/workflow/pages/workflow-gallery/workflow-gallery-tabs.tsx +92 -17
- package/src/features/workflow/pages/workflow-gallery.tsx +45 -1
- package/src/lib/api.ts +91 -2
- package/src/lib/workspace-routing.ts +15 -0
- package/src/main.tsx +7 -0
- package/src/vite-env.d.ts +1 -0
- package/dist/assets/channel-C-DaAO-p.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-B2LNkTFb.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-B2LNkTFb.js +0 -1
- package/dist/assets/clone-DcGVOYZT.js +0 -1
- package/dist/assets/index-B0_h2wCA.css +0 -1
- package/dist/assets/index-DmIcay1T.js +0 -775
- package/dist/assets/stateDiagram-v2-4FDKWEC3-Cle1H2JX.js +0 -1
- package/src/features/MODULES.md +0 -199
- package/src/features/auth/components/auto-login.tsx +0 -51
- package/src/features/auth/lib/oidc-client.ts +0 -565
- 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 {
|
|
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 {
|
|
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
|
-
|
|
88
|
-
|
|
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
|
|
95
|
-
async (
|
|
116
|
+
const performOnboard = useCallback(
|
|
117
|
+
async (candidateId: string, teamId?: string) => {
|
|
96
118
|
try {
|
|
97
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
};
|
|
@@ -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 {
|
|
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
|
|
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
|
-
@{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
303
|
+
{displayBadge.subtitle}
|
|
283
304
|
</div>
|
|
284
305
|
) : null}
|
|
285
306
|
|