orcheo-studio 0.21.2 → 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.
- package/.bumpversion.cfg +1 -1
- package/dist/assets/{arc-CpXJ41cG.js → arc-De9dLqyl.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-Bnj1Z3kt.js → architectureDiagram-VXUJARFQ-hEslfmYm.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-DSxmIebP.js → blockDiagram-VD42YOAC-CyWSMWJa.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-wk2snQI8.js → c4Diagram-YG6GDRKO-CxoTVJ47.js} +1 -1
- package/dist/assets/channel-CzCzGn0f.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-B0ZkMnbv.js → chunk-4BX2VUAB-twftUw9o.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-BVFWf_lK.js → chunk-55IACEB6-DQZDNQUo.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-DnR7oQ4g.js → chunk-B4BG7PRW-BYGOoNB5.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-CNFww0wO.js → chunk-DI55MBZ5-qyPhcZF3.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-kqvlp-gm.js → chunk-FMBD7UC4-BgUDHGEG.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-CwF-7kEP.js → chunk-QN33PNHL-DHlMh8NC.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-DVLEC_l5.js → chunk-QZHKN3VN-Bfc8IV0O.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-rwpYb83b.js → chunk-TZMSLE5B-BqwdftTm.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-ekNI5d_4.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-ekNI5d_4.js +1 -0
- package/dist/assets/clone-DowBu1Kh.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BG5vo_BU.js → cose-bilkent-S5V4N54A-g5agH7fS.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-DNS8secx.js → dagre-6UL2VRFP-ajLMaXPP.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-DhkTaeY-.js → diagram-PSM6KHXK-BndVcBhB.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-juugDR85.js → diagram-QEK2KX5R-BFiIYeu7.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-BfwDPXhp.js → diagram-S2PKOQOG-DXy-A9pn.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-Dcz1rPHi.js → erDiagram-Q2GNP2WA-D_Bac_58.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-CCfthK8j.js → flowDiagram-NV44I4VS-DhEi8qtz.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-Qm12KTag.js → ganttDiagram-JELNMOA3-BtpUvOuc.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-CjyU6Twb.js → gitGraphDiagram-V2S2FVAM-MBES60cx.js} +1 -1
- package/dist/assets/{graph-DThnWdhX.js → graph-BcwvIN-m.js} +1 -1
- package/dist/assets/index-DLYzf0K-.js +775 -0
- package/dist/assets/index-DebsXgfj.css +1 -0
- package/dist/assets/{infoDiagram-HS3SLOUP-h3Nrkige.js → infoDiagram-HS3SLOUP-vaIFYyC9.js} +1 -1
- package/dist/assets/{isUndefined-yP-9UF2N.js → isUndefined-DVO6xakl.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-BSbwRc84.js → journeyDiagram-XKPGCS4Q-D-_q99If.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-CARNrGlf.js → kanban-definition-3W4ZIXB7-i_AZcs_J.js} +1 -1
- package/dist/assets/{layout-pp8XWeO7.js → layout-CPJ9nR_z.js} +1 -1
- package/dist/assets/{linear-WDZxKeGm.js → linear-KBGpPBiH.js} +1 -1
- package/dist/assets/{min-rK5lTeBA.js → min-By5f7YdD.js} +1 -1
- package/dist/assets/{mindmap-definition-VGOIOE7T-BzF1XeHx.js → mindmap-definition-VGOIOE7T-DRjtRxFS.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-C5j6lI7D.js → pieDiagram-ADFJNKIX-BkkSglWM.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-CMaUda4E.js → quadrantDiagram-AYHSOK5B-DY-Ng2qt.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-BxOFsm2a.js → requirementDiagram-UZGBJVZJ-B5w0etXG.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-CGR0hfi8.js → sankeyDiagram-TZEHDZUN-BWntUp8b.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-QLQiFJnI.js → sequenceDiagram-WL72ISMW-Cl77fd7y.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-CebVOHmS.js → stateDiagram-FKZM4ZOC-B8_DoPW6.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-DHL7p-md.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-CR6Zjm9F.js → timeline-definition-IT6M3QCI-udvNLTe_.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-C0ItD1QK.js → treemap-GDKQZRPO-BSvoMl74.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-T_ak7rrn.js → xychartDiagram-PRI3JC2R-CTdy-FCX.js} +1 -1
- package/dist/index.html +2 -7
- package/index.html +0 -5
- package/package.json +1 -1
- package/src/App.tsx +20 -0
- package/src/features/chatkit/pages/public-chat.tsx +7 -3
- 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 +10 -3
- package/src/features/workflow/lib/workflow-storage.ts +2 -1
- package/src/features/workflow/lib/workflow-storage.types.ts +9 -0
- package/src/features/workflow/pages/workflow/components/workflow-config-sheet.tsx +2 -66
- package/src/features/workflow/pages/workflow/components/workflow-config-sheet.utils.ts +0 -40
- 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 +91 -16
- package/src/features/workflow/pages/workflow-gallery.tsx +45 -1
- package/src/lib/api.ts +13 -1
- package/src/lib/workspace-routing.ts +15 -0
- package/src/main.tsx +7 -0
- package/dist/assets/channel-D5aHOnKn.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-BsnXw08J.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-BsnXw08J.js +0 -1
- package/dist/assets/clone-B8_btpeA.js +0 -1
- package/dist/assets/index-B0_h2wCA.css +0 -1
- package/dist/assets/index-nbmIZFkh.js +0 -775
- package/dist/assets/stateDiagram-v2-4FDKWEC3-Be2We9pM.js +0 -1
|
@@ -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
|
|
|
@@ -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
|
-
|
|
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>
|