orcheo-studio 0.24.4 → 0.24.6

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 (103) hide show
  1. package/.bumpversion.cfg +1 -1
  2. package/dist/assets/{arc-99TH-Koz.js → arc-CJePtrV4.js} +1 -1
  3. package/dist/assets/{architectureDiagram-VXUJARFQ-DugaTSY2.js → architectureDiagram-VXUJARFQ-BavAz0jp.js} +1 -1
  4. package/dist/assets/{blockDiagram-VD42YOAC-BH68wVtD.js → blockDiagram-VD42YOAC-Bx1XAydH.js} +1 -1
  5. package/dist/assets/{c4Diagram-YG6GDRKO-V6Vqtp7B.js → c4Diagram-YG6GDRKO-B2LhByin.js} +1 -1
  6. package/dist/assets/channel-BuDj-lWJ.js +1 -0
  7. package/dist/assets/{chunk-4BX2VUAB-CvbCAMNv.js → chunk-4BX2VUAB-UqiH5dLp.js} +1 -1
  8. package/dist/assets/{chunk-55IACEB6-h_T3kQh7.js → chunk-55IACEB6-ChxTr_6v.js} +1 -1
  9. package/dist/assets/{chunk-B4BG7PRW-Dm6dgbGh.js → chunk-B4BG7PRW-2ETw-eA1.js} +1 -1
  10. package/dist/assets/{chunk-DI55MBZ5-DSXFA4Fi.js → chunk-DI55MBZ5-BdxVSKej.js} +1 -1
  11. package/dist/assets/{chunk-FMBD7UC4-DJDlfjt1.js → chunk-FMBD7UC4-BJF2M5iQ.js} +1 -1
  12. package/dist/assets/{chunk-QN33PNHL-BnjUGeBL.js → chunk-QN33PNHL-DI9t3nKL.js} +1 -1
  13. package/dist/assets/{chunk-QZHKN3VN-BRudOghZ.js → chunk-QZHKN3VN-DsbUIBgB.js} +1 -1
  14. package/dist/assets/{chunk-TZMSLE5B-CpxSpvqK.js → chunk-TZMSLE5B-DzMhWfBV.js} +1 -1
  15. package/dist/assets/classDiagram-2ON5EDUG-Bc5AzWzU.js +1 -0
  16. package/dist/assets/classDiagram-v2-WZHVMYZB-Bc5AzWzU.js +1 -0
  17. package/dist/assets/clone-CtxIENQC.js +1 -0
  18. package/dist/assets/{cose-bilkent-S5V4N54A-Dl80WZ_a.js → cose-bilkent-S5V4N54A-BcKKbLWz.js} +1 -1
  19. package/dist/assets/{dagre-6UL2VRFP-vw-vkw78.js → dagre-6UL2VRFP-CERJ9KSL.js} +1 -1
  20. package/dist/assets/{diagram-PSM6KHXK-B4MT5Nvf.js → diagram-PSM6KHXK-DGl--Cyq.js} +1 -1
  21. package/dist/assets/{diagram-QEK2KX5R-CMioZnRI.js → diagram-QEK2KX5R-DbfdELCd.js} +1 -1
  22. package/dist/assets/{diagram-S2PKOQOG-PJAE8YS0.js → diagram-S2PKOQOG-CsR4YAm5.js} +1 -1
  23. package/dist/assets/{erDiagram-Q2GNP2WA-DCd-WouG.js → erDiagram-Q2GNP2WA-BlOr_gpI.js} +1 -1
  24. package/dist/assets/{flowDiagram-NV44I4VS-DS__-WT4.js → flowDiagram-NV44I4VS-BAyDXjxF.js} +1 -1
  25. package/dist/assets/{ganttDiagram-JELNMOA3-Btifh-31.js → ganttDiagram-JELNMOA3-b2ntam6s.js} +1 -1
  26. package/dist/assets/{gitGraphDiagram-V2S2FVAM-BCyjopey.js → gitGraphDiagram-V2S2FVAM-BvdeZrga.js} +1 -1
  27. package/dist/assets/{graph-6YBx8iQ0.js → graph-DMrtDwYQ.js} +1 -1
  28. package/dist/assets/{index-lkycEDHM.js → index-CaWVBOSf.js} +204 -189
  29. package/dist/assets/index-CbLxL6va.css +1 -0
  30. package/dist/assets/{infoDiagram-HS3SLOUP-CAsJ2eU1.js → infoDiagram-HS3SLOUP-CFO6LsEb.js} +1 -1
  31. package/dist/assets/{isUndefined-CcAlyXn3.js → isUndefined-DE2VOYrW.js} +1 -1
  32. package/dist/assets/{journeyDiagram-XKPGCS4Q-Bi8XsX15.js → journeyDiagram-XKPGCS4Q-Svi8cltR.js} +1 -1
  33. package/dist/assets/{kanban-definition-3W4ZIXB7-PTAFVsDn.js → kanban-definition-3W4ZIXB7-C7uds03_.js} +1 -1
  34. package/dist/assets/{layout-Do-GOFdW.js → layout-D5N_DnHw.js} +1 -1
  35. package/dist/assets/{linear-CZlgolw0.js → linear-h9-K1RBq.js} +1 -1
  36. package/dist/assets/{min-N15IYW1Y.js → min-CjtZYucR.js} +1 -1
  37. package/dist/assets/{mindmap-definition-VGOIOE7T-Dlgxv6Jo.js → mindmap-definition-VGOIOE7T-Bw80_6TW.js} +1 -1
  38. package/dist/assets/{pieDiagram-ADFJNKIX-Dr2AfIw5.js → pieDiagram-ADFJNKIX-DGukFJSP.js} +1 -1
  39. package/dist/assets/{quadrantDiagram-AYHSOK5B-B8TPuvW6.js → quadrantDiagram-AYHSOK5B-CU7GPi0P.js} +1 -1
  40. package/dist/assets/{requirementDiagram-UZGBJVZJ-CdD2oP1N.js → requirementDiagram-UZGBJVZJ-D_aCZrji.js} +1 -1
  41. package/dist/assets/{sankeyDiagram-TZEHDZUN-XEMX6l5k.js → sankeyDiagram-TZEHDZUN-Wgtprjih.js} +1 -1
  42. package/dist/assets/{sequenceDiagram-WL72ISMW-BlIKpBev.js → sequenceDiagram-WL72ISMW-BCOCCVxS.js} +1 -1
  43. package/dist/assets/{stateDiagram-FKZM4ZOC-D3KGQY6v.js → stateDiagram-FKZM4ZOC-DHVe22f9.js} +1 -1
  44. package/dist/assets/stateDiagram-v2-4FDKWEC3-Bq9eQr8D.js +1 -0
  45. package/dist/assets/{timeline-definition-IT6M3QCI-TkL4mz_f.js → timeline-definition-IT6M3QCI-Cm4CAEQk.js} +1 -1
  46. package/dist/assets/{treemap-GDKQZRPO-BDp_c1Vc.js → treemap-GDKQZRPO-DvLhDp9r.js} +1 -1
  47. package/dist/assets/{xychartDiagram-PRI3JC2R-D0dNQImL.js → xychartDiagram-PRI3JC2R-BtcJYIPe.js} +1 -1
  48. package/dist/index.html +8 -2
  49. package/index.html +6 -0
  50. package/package.json +1 -1
  51. package/src/App.tsx +48 -44
  52. package/src/design-system/ui/button.tsx +1 -1
  53. package/src/features/account/components/settings/appearance-settings-tab.tsx +1 -1
  54. package/src/features/account/pages/profile/components/profile-general-tab.tsx +1 -1
  55. package/src/features/account/pages/workspace-members.tsx +58 -62
  56. package/src/features/auth/pages/login.tsx +4 -2
  57. package/src/features/auth/pages/verify.tsx +1 -1
  58. package/src/features/chatkit/components/public-chat-error-boundary.tsx +1 -1
  59. package/src/features/chatkit/components/public-chat-widget.tsx +2 -3
  60. package/src/features/chatkit/lib/chatkit-theme.ts +3 -2
  61. package/src/features/chatkit/lib/public-chat-access.ts +13 -0
  62. package/src/features/chatkit/pages/public-chat.tsx +18 -27
  63. package/src/features/shared/components/chat-interface.types.ts +1 -5
  64. package/src/features/shared/components/top-navigation/studio-brand.tsx +1 -1
  65. package/src/features/shared/components/top-navigation/version-status.tsx +27 -31
  66. package/src/features/shared/components/workspace-bootstrap-gate.tsx +4 -4
  67. package/src/features/workflow/components/dialogs/credential-access-badge.tsx +2 -2
  68. package/src/features/workflow/components/dialogs/credential-status-badge.tsx +2 -2
  69. package/src/features/workflow/components/dialogs/credentials-table.tsx +1 -1
  70. package/src/features/workflow/components/dialogs/update-workflow-dialog.tsx +34 -78
  71. package/src/features/workflow/components/dialogs/upload-workflow-dialog.tsx +40 -136
  72. package/src/features/workflow/components/dialogs/workflow-folder-picker.tsx +178 -0
  73. package/src/features/workflow/components/panels/rjsf-cron-widget.tsx +35 -10
  74. package/src/features/workflow/components/panels/workflow-diff-dialog.tsx +5 -5
  75. package/src/features/workflow/components/panels/workflow-history-table.tsx +3 -4
  76. package/src/features/workflow/components/trace/agent-prism/SpanCard/SpanCardConnector.tsx +1 -5
  77. package/src/features/workflow/components/trace/agent-prism/shared.ts +1 -10
  78. package/src/features/workflow/components/trace/agent-prism/theme/theme.css +74 -74
  79. package/src/features/workflow/lib/workflow-execution-builders.ts +4 -4
  80. package/src/features/workflow/lib/workflow-execution.types.ts +2 -9
  81. package/src/features/workflow/lib/workflow-folder-selection.ts +239 -0
  82. package/src/features/workflow/lib/workflow-frontmatter.ts +3 -1
  83. package/src/features/workflow/lib/workflow-storage-api.ts +5 -9
  84. package/src/features/workflow/lib/workflow-storage-helpers.ts +2 -4
  85. package/src/features/workflow/lib/workflow-storage.ts +6 -2
  86. package/src/features/workflow/lib/workflow-storage.types.ts +1 -6
  87. package/src/features/workflow/lib/workflow-upload-errors.ts +6 -2
  88. package/src/features/workflow/pages/workflow/components/settings-tab-content.tsx +24 -9
  89. package/src/features/workflow/pages/workflow/components/workflow-tab-content.tsx +6 -4
  90. package/src/features/workflow/pages/workflow/hooks/controller/use-workflow-execution-controller.ts +4 -1
  91. package/src/features/workflow/pages/workflow/hooks/use-execution-updates.ts +4 -2
  92. package/src/features/workflow/pages/workflow-gallery/create-team-dialog.tsx +2 -6
  93. package/src/features/workflow/pages/workflow-gallery/workflow-card.tsx +16 -16
  94. package/src/index.css +101 -59
  95. package/src/lib/api.ts +7 -2
  96. package/src/lib/workspace-routing.ts +1 -3
  97. package/tailwind.config.js +47 -2
  98. package/dist/assets/channel-D562bZxL.js +0 -1
  99. package/dist/assets/classDiagram-2ON5EDUG-Dix_KnuM.js +0 -1
  100. package/dist/assets/classDiagram-v2-WZHVMYZB-Dix_KnuM.js +0 -1
  101. package/dist/assets/clone-0c2Tu-SH.js +0 -1
  102. package/dist/assets/index-Df4jFRhd.css +0 -1
  103. package/dist/assets/stateDiagram-v2-4FDKWEC3-Di3Nkv2n.js +0 -1
@@ -44,17 +44,6 @@ const parseCache = (raw: string | null): SystemInfoCachePayload | null => {
44
44
  }
45
45
  };
46
46
 
47
- const shouldRefresh = (cache: SystemInfoCachePayload | null): boolean => {
48
- if (!cache) {
49
- return true;
50
- }
51
- const lastCheckedAt = Date.parse(cache.checkedAt);
52
- if (Number.isNaN(lastCheckedAt)) {
53
- return true;
54
- }
55
- return Date.now() - lastCheckedAt >= UPDATE_CHECK_TTL_MS;
56
- };
57
-
58
47
  const parseSemver = (value: string | null | undefined): ParsedSemver | null => {
59
48
  if (!value) {
60
49
  return null;
@@ -152,7 +141,10 @@ const isDismissed = (): boolean => {
152
141
  };
153
142
 
154
143
  export default function VersionStatus() {
155
- const [systemInfo, setSystemInfo] = useState<SystemInfoResponse | null>(null);
144
+ const [cachedInfo, setCachedInfo] = useState<SystemInfoResponse | null>(null);
145
+ const [liveBackendVersion, setLiveBackendVersion] = useState<string | null>(
146
+ null,
147
+ );
156
148
  const [dismissedReminder, setDismissedReminder] = useState<boolean>(() =>
157
149
  isDismissed(),
158
150
  );
@@ -163,11 +155,12 @@ export default function VersionStatus() {
163
155
  window.localStorage.getItem(UPDATE_CHECK_CACHE_KEY),
164
156
  );
165
157
  if (cache) {
166
- setSystemInfo(cache.payload);
167
- }
168
-
169
- if (!shouldRefresh(cache)) {
170
- return;
158
+ setCachedInfo(cache.payload);
159
+ setLiveBackendVersion(cache.payload.backend.current_version);
160
+ const cacheAge = Date.now() - Date.parse(cache.checkedAt);
161
+ if (cacheAge < UPDATE_CHECK_TTL_MS) {
162
+ return;
163
+ }
171
164
  }
172
165
 
173
166
  let active = true;
@@ -176,7 +169,8 @@ export default function VersionStatus() {
176
169
  if (!active) {
177
170
  return;
178
171
  }
179
- setSystemInfo(payload);
172
+ setLiveBackendVersion(payload.backend.current_version);
173
+ setCachedInfo(payload);
180
174
  const cachePayload: SystemInfoCachePayload = {
181
175
  checkedAt: new Date().toISOString(),
182
176
  payload,
@@ -196,39 +190,41 @@ export default function VersionStatus() {
196
190
  }, []);
197
191
 
198
192
  const studioUpdateAvailable = useMemo(() => {
199
- if (!systemInfo) {
193
+ if (!cachedInfo) {
200
194
  return false;
201
195
  }
202
- if (systemInfo.studio.update_available) {
196
+ if (cachedInfo.studio.update_available) {
203
197
  return true;
204
198
  }
205
- return compareSemver(studioVersion, systemInfo.studio.latest_version) < 0;
206
- }, [studioVersion, systemInfo]);
199
+ return compareSemver(studioVersion, cachedInfo.studio.latest_version) < 0;
200
+ }, [studioVersion, cachedInfo]);
207
201
 
208
202
  const showReminder =
209
- ((systemInfo?.backend.update_available ?? false) ||
203
+ ((cachedInfo?.backend.update_available ?? false) ||
210
204
  studioUpdateAvailable) &&
211
205
  !dismissedReminder;
212
206
 
213
207
  const versionSummary = useMemo(() => {
214
- const backend = systemInfo?.backend.current_version ?? "unknown";
208
+ const backend = liveBackendVersion ?? "unknown";
215
209
  return `Orcheo ${studioVersion} · Backend ${backend}`;
216
- }, [studioVersion, systemInfo]);
210
+ }, [studioVersion, liveBackendVersion]);
217
211
 
218
212
  const updateLines = useMemo(() => {
219
- if (!systemInfo) return [];
213
+ if (!cachedInfo) return [];
220
214
  const lines: string[] = [];
221
- if (systemInfo.backend.update_available) {
215
+ if (cachedInfo.backend.update_available) {
222
216
  lines.push(
223
- `Backend: ${systemInfo.backend.current_version} → ${systemInfo.backend.latest_version}`,
217
+ `Backend: ${cachedInfo.backend.current_version} → ${cachedInfo.backend.latest_version}`,
224
218
  );
225
219
  }
226
220
  if (studioUpdateAvailable) {
227
- const studioCurrent = systemInfo.studio.current_version ?? studioVersion;
228
- lines.push(`Orcheo: ${studioCurrent} → ${systemInfo.studio.latest_version}`);
221
+ const studioCurrent = cachedInfo.studio.current_version ?? studioVersion;
222
+ lines.push(
223
+ `Orcheo: ${studioCurrent} → ${cachedInfo.studio.latest_version}`,
224
+ );
229
225
  }
230
226
  return lines;
231
- }, [studioUpdateAvailable, studioVersion, systemInfo]);
227
+ }, [studioUpdateAvailable, studioVersion, cachedInfo]);
232
228
 
233
229
  const dismissUpdateReminder = () => {
234
230
  if (typeof window === "undefined") {
@@ -155,7 +155,7 @@ export function WorkspaceBootstrapGate({
155
155
 
156
156
  if (isLoading) {
157
157
  return (
158
- <div className="flex min-h-screen items-center justify-center bg-slate-100 text-foreground dark:bg-slate-950">
158
+ <div className="flex min-h-screen items-center justify-center bg-background text-foreground">
159
159
  <Card className="w-full max-w-md border-border bg-card/80 backdrop-blur-xl dark:border-primary/25 dark:bg-primary/5">
160
160
  <CardContent className="flex items-center gap-3 p-6 text-sm text-muted-foreground">
161
161
  <Loader2 className="h-4 w-4 animate-spin" />
@@ -171,16 +171,16 @@ export function WorkspaceBootstrapGate({
171
171
  }
172
172
 
173
173
  return (
174
- <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-slate-100 text-foreground dark:bg-slate-950">
174
+ <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background text-foreground">
175
175
  <div
176
- className="absolute inset-0 bg-gradient-to-br from-slate-100 via-slate-200 to-slate-100 dark:from-slate-950 dark:via-slate-900/80 dark:to-black"
176
+ className="absolute inset-0 bg-gradient-to-br from-background via-secondary to-background dark:via-card/80"
177
177
  aria-hidden="true"
178
178
  />
179
179
  <div
180
180
  className="absolute inset-0 opacity-40 mix-blend-soft-light dark:opacity-60"
181
181
  style={{
182
182
  backgroundImage:
183
- "radial-gradient(circle at 20% 20%, rgba(148, 163, 184, 0.2), transparent 45%), radial-gradient(circle at 80% 30%, rgba(56, 189, 248, 0.25), transparent 50%), radial-gradient(circle at 50% 80%, rgba(45, 212, 191, 0.2), transparent 55%)",
183
+ "radial-gradient(circle at 20% 20%, rgba(224, 192, 144, 0.25), transparent 45%), radial-gradient(circle at 80% 30%, rgba(248, 120, 37, 0.2), transparent 50%), radial-gradient(circle at 50% 80%, rgba(246, 245, 212, 0.35), transparent 55%)",
184
184
  }}
185
185
  aria-hidden="true"
186
186
  />
@@ -12,7 +12,7 @@ export function CredentialAccessBadge({ access }: CredentialAccessBadgeProps) {
12
12
  return (
13
13
  <Badge
14
14
  variant="outline"
15
- className="bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800"
15
+ className="border-info/30 bg-info-muted text-info-muted-foreground"
16
16
  >
17
17
  <Lock className="h-3 w-3 mr-1" />
18
18
  Scoped
@@ -22,7 +22,7 @@ export function CredentialAccessBadge({ access }: CredentialAccessBadgeProps) {
22
22
  return (
23
23
  <Badge
24
24
  variant="outline"
25
- className="bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800"
25
+ className="border-primary/30 bg-primary/10 text-primary-active"
26
26
  >
27
27
  <Users className="h-3 w-3 mr-1" />
28
28
  Shared
@@ -15,7 +15,7 @@ export function CredentialStatusBadge({ status }: CredentialStatusBadgeProps) {
15
15
  return (
16
16
  <Badge
17
17
  variant="outline"
18
- className="flex items-center gap-1 bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:border-emerald-800"
18
+ className="flex items-center gap-1 border-success/30 bg-success-muted text-success-muted-foreground"
19
19
  >
20
20
  <CheckCircle2 className="h-3 w-3" />
21
21
  Healthy
@@ -25,7 +25,7 @@ export function CredentialStatusBadge({ status }: CredentialStatusBadgeProps) {
25
25
  return (
26
26
  <Badge
27
27
  variant="outline"
28
- className="flex items-center gap-1 bg-red-100 text-red-800 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800"
28
+ className="flex items-center gap-1 border-destructive/30 bg-destructive-muted text-destructive-muted-foreground"
29
29
  >
30
30
  <AlertTriangle className="h-3 w-3" />
31
31
  Unhealthy
@@ -353,7 +353,7 @@ export function CredentialsTable({
353
353
  className={cn(
354
354
  "h-6 w-6 transition-colors",
355
355
  currentCopyState === "success" &&
356
- "text-emerald-600 hover:text-emerald-700",
356
+ "text-success hover:text-success/80",
357
357
  currentCopyState === "error" &&
358
358
  "text-destructive hover:text-destructive",
359
359
  )}
@@ -1,4 +1,4 @@
1
- import { useCallback, useRef, useState } from "react";
1
+ import { useCallback, useState } from "react";
2
2
  import { Loader2, RefreshCw } from "lucide-react";
3
3
  import { Button } from "@/design-system/ui/button";
4
4
  import {
@@ -9,10 +9,11 @@ import {
9
9
  DialogHeader,
10
10
  DialogTitle,
11
11
  } from "@/design-system/ui/dialog";
12
- import { Input } from "@/design-system/ui/input";
13
12
  import { Label } from "@/design-system/ui/label";
14
13
  import { toast } from "@/hooks/use-toast";
15
14
  import { updateWorkflowFromFiles } from "@features/workflow/lib/workflow-storage";
15
+ import { WorkflowFolderPicker } from "./workflow-folder-picker";
16
+ import type { WorkflowFolderSelection } from "@features/workflow/lib/workflow-folder-selection";
16
17
 
17
18
  interface UpdateWorkflowDialogProps {
18
19
  open: boolean;
@@ -27,11 +28,9 @@ export function UpdateWorkflowDialog({
27
28
  workflowId,
28
29
  workflowName,
29
30
  }: UpdateWorkflowDialogProps) {
30
- const scriptInputRef = useRef<HTMLInputElement>(null);
31
- const configInputRef = useRef<HTMLInputElement>(null);
32
-
33
31
  const [scriptContent, setScriptContent] = useState<string | null>(null);
34
32
  const [scriptFileName, setScriptFileName] = useState("");
33
+ const [configFileName, setConfigFileName] = useState<string | null>(null);
35
34
  const [configContent, setConfigContent] = useState<Record<
36
35
  string,
37
36
  unknown
@@ -42,15 +41,10 @@ export function UpdateWorkflowDialog({
42
41
  const reset = useCallback(() => {
43
42
  setScriptContent(null);
44
43
  setScriptFileName("");
44
+ setConfigFileName(null);
45
45
  setConfigContent(null);
46
46
  setIsUpdating(false);
47
47
  setError(null);
48
- if (scriptInputRef.current) {
49
- scriptInputRef.current.value = "";
50
- }
51
- if (configInputRef.current) {
52
- configInputRef.current.value = "";
53
- }
54
48
  }, []);
55
49
 
56
50
  const handleOpenChange = useCallback(
@@ -63,50 +57,26 @@ export function UpdateWorkflowDialog({
63
57
  [onOpenChange, reset],
64
58
  );
65
59
 
66
- const handleScriptFileChange = useCallback(
67
- (event: React.ChangeEvent<HTMLInputElement>) => {
68
- const file = event.target.files?.[0];
69
- if (!file) {
70
- return;
71
- }
72
- setScriptFileName(file.name);
73
- const reader = new FileReader();
74
- reader.onload = (e) => {
75
- const text = e.target?.result;
76
- if (typeof text === "string") {
77
- setScriptContent(text);
78
- }
79
- };
80
- reader.readAsText(file);
81
- },
82
- [],
83
- );
60
+ const handleSelect = useCallback((selection: WorkflowFolderSelection) => {
61
+ setError(null);
62
+ setScriptContent(selection.scriptContent);
63
+ setScriptFileName(selection.scriptName);
64
+ setConfigFileName(selection.configName);
65
+ setConfigContent(selection.configContent);
66
+ }, []);
84
67
 
85
- const handleConfigFileChange = useCallback(
86
- (event: React.ChangeEvent<HTMLInputElement>) => {
87
- const file = event.target.files?.[0];
88
- if (!file) {
89
- return;
90
- }
91
- const reader = new FileReader();
92
- reader.onload = (e) => {
93
- const text = e.target?.result;
94
- if (typeof text !== "string") {
95
- return;
96
- }
97
- try {
98
- const parsed = JSON.parse(text) as Record<string, unknown>;
99
- setConfigContent(parsed);
100
- setError(null);
101
- } catch {
102
- setError("config.json is not valid JSON.");
103
- setConfigContent(null);
104
- }
105
- };
106
- reader.readAsText(file);
107
- },
108
- [],
109
- );
68
+ const handleSelectionError = useCallback((message: string) => {
69
+ setScriptContent(null);
70
+ setScriptFileName("");
71
+ setConfigFileName(null);
72
+ setConfigContent(null);
73
+ setError(message);
74
+ toast({
75
+ title: "Couldn't use that folder",
76
+ description: message,
77
+ variant: "destructive",
78
+ });
79
+ }, []);
110
80
 
111
81
  const handleUpdate = useCallback(async () => {
112
82
  if (!scriptContent) {
@@ -149,35 +119,21 @@ export function UpdateWorkflowDialog({
149
119
  <DialogHeader>
150
120
  <DialogTitle>Update Workflow</DialogTitle>
151
121
  <DialogDescription>
152
- Upload a new Python script and optional JSON config to create a new
153
- version of &ldquo;{workflowName}&rdquo;.
122
+ Select a folder with a new Python script and optional JSON config to
123
+ create a new version of &ldquo;{workflowName}&rdquo;.
154
124
  </DialogDescription>
155
125
  </DialogHeader>
156
126
  <div className="grid gap-4 py-4">
157
127
  <div className="space-y-1.5">
158
- <Label htmlFor="update-script">
159
- Workflow Script <span className="text-destructive">*</span>
128
+ <Label htmlFor="update-folder">
129
+ Workflow Folder <span className="text-destructive">*</span>
160
130
  </Label>
161
- <Input
162
- id="update-script"
163
- ref={scriptInputRef}
164
- type="file"
165
- accept=".py"
166
- onChange={handleScriptFileChange}
167
- disabled={isUpdating}
168
- />
169
- {scriptFileName && (
170
- <p className="text-xs text-muted-foreground">{scriptFileName}</p>
171
- )}
172
- </div>
173
- <div className="space-y-1.5">
174
- <Label htmlFor="update-config">Config (optional)</Label>
175
- <Input
176
- id="update-config"
177
- ref={configInputRef}
178
- type="file"
179
- accept=".json"
180
- onChange={handleConfigFileChange}
131
+ <WorkflowFolderPicker
132
+ idPrefix="update"
133
+ scriptName={scriptFileName}
134
+ configName={configFileName}
135
+ onSelect={handleSelect}
136
+ onError={handleSelectionError}
181
137
  disabled={isUpdating}
182
138
  />
183
139
  </div>
@@ -1,4 +1,4 @@
1
- import { useCallback, useRef, useState } from "react";
1
+ import { useCallback, useState } from "react";
2
2
  import { useNavigate } from "react-router-dom";
3
3
  import { Loader2, Upload } from "lucide-react";
4
4
  import { Button } from "@/design-system/ui/button";
@@ -21,51 +21,24 @@ import { parseWorkflowFrontmatter } from "@features/workflow/lib/workflow-frontm
21
21
  import { getWorkflowRouteRef } from "@features/workflow/lib/workflow-storage-helpers";
22
22
  import { getSelectedWorkspaceSlug } from "@/lib/workspace-session";
23
23
  import { getWorkspaceWorkflowPath } from "@/lib/workspace-routing";
24
+ import { WorkflowFolderPicker } from "./workflow-folder-picker";
25
+ import type { WorkflowFolderSelection } from "@features/workflow/lib/workflow-folder-selection";
24
26
 
25
27
  interface UploadWorkflowDialogProps {
26
28
  open: boolean;
27
29
  onOpenChange: (open: boolean) => void;
28
30
  }
29
31
 
30
- const MAX_SCRIPT_UPLOAD_BYTES = 1024 * 1024;
31
- const MAX_CONFIG_UPLOAD_BYTES = 256 * 1024;
32
- const JSON_CONFIG_MIME_TYPES = new Set(["", "application/json", "text/json"]);
33
-
34
- const formatUploadLimit = (bytes: number): string => {
35
- if (bytes >= 1024 * 1024) {
36
- return `${bytes / (1024 * 1024)} MB`;
37
- }
38
- return `${bytes / 1024} KB`;
39
- };
40
-
41
- const validateUploadFile = (
42
- file: File,
43
- options: {
44
- label: string;
45
- extension: string;
46
- maxBytes: number;
47
- },
48
- ): string | null => {
49
- if (!file.name.toLowerCase().endsWith(options.extension)) {
50
- return `${options.label} must use the ${options.extension} extension.`;
51
- }
52
- if (file.size > options.maxBytes) {
53
- return `${options.label} must be ${formatUploadLimit(options.maxBytes)} or smaller.`;
54
- }
55
- return null;
56
- };
57
-
58
32
  export function UploadWorkflowDialog({
59
33
  open,
60
34
  onOpenChange,
61
35
  }: UploadWorkflowDialogProps) {
62
36
  const navigate = useNavigate();
63
- const scriptInputRef = useRef<HTMLInputElement>(null);
64
- const configInputRef = useRef<HTMLInputElement>(null);
65
37
 
66
38
  const [workflowName, setWorkflowName] = useState("");
67
39
  const [scriptContent, setScriptContent] = useState<string | null>(null);
68
40
  const [scriptFileName, setScriptFileName] = useState("");
41
+ const [configFileName, setConfigFileName] = useState<string | null>(null);
69
42
  const [configContent, setConfigContent] = useState<Record<
70
43
  string,
71
44
  unknown
@@ -77,15 +50,10 @@ export function UploadWorkflowDialog({
77
50
  setWorkflowName("");
78
51
  setScriptContent(null);
79
52
  setScriptFileName("");
53
+ setConfigFileName(null);
80
54
  setConfigContent(null);
81
55
  setIsUploading(false);
82
56
  setError(null);
83
- if (scriptInputRef.current) {
84
- scriptInputRef.current.value = "";
85
- }
86
- if (configInputRef.current) {
87
- configInputRef.current.value = "";
88
- }
89
57
  }, []);
90
58
 
91
59
  const handleOpenChange = useCallback(
@@ -98,83 +66,33 @@ export function UploadWorkflowDialog({
98
66
  [onOpenChange, reset],
99
67
  );
100
68
 
101
- const handleScriptFileChange = useCallback(
102
- (event: React.ChangeEvent<HTMLInputElement>) => {
103
- const file = event.target.files?.[0];
104
- if (!file) {
105
- return;
106
- }
107
- const validationError = validateUploadFile(file, {
108
- label: "Workflow script",
109
- extension: ".py",
110
- maxBytes: MAX_SCRIPT_UPLOAD_BYTES,
111
- });
112
- if (validationError) {
113
- setError(validationError);
114
- setScriptContent(null);
115
- setScriptFileName("");
116
- event.target.value = "";
117
- return;
118
- }
119
- setError(null);
120
- setScriptFileName(file.name);
121
- if (!workflowName) {
122
- const nameWithoutExt = file.name.replace(/\.py$/i, "");
123
- setWorkflowName(nameWithoutExt);
69
+ const handleSelect = useCallback((selection: WorkflowFolderSelection) => {
70
+ setError(null);
71
+ setScriptContent(selection.scriptContent);
72
+ setScriptFileName(selection.scriptName);
73
+ setConfigFileName(selection.configName);
74
+ setConfigContent(selection.configContent);
75
+ setWorkflowName((current) => {
76
+ if (current) {
77
+ return current;
124
78
  }
125
- const reader = new FileReader();
126
- reader.onload = (e) => {
127
- const text = e.target?.result;
128
- if (typeof text === "string") {
129
- setScriptContent(text);
130
- const frontmatter = parseWorkflowFrontmatter(text);
131
- if (!workflowName && frontmatter.name) {
132
- setWorkflowName(frontmatter.name);
133
- }
134
- }
135
- };
136
- reader.readAsText(file);
137
- },
138
- [workflowName],
139
- );
79
+ const frontmatter = parseWorkflowFrontmatter(selection.scriptContent);
80
+ return frontmatter.name ?? selection.scriptName.replace(/\.py$/i, "");
81
+ });
82
+ }, []);
140
83
 
141
- const handleConfigFileChange = useCallback(
142
- (event: React.ChangeEvent<HTMLInputElement>) => {
143
- const file = event.target.files?.[0];
144
- if (!file) {
145
- return;
146
- }
147
- const validationError = validateUploadFile(file, {
148
- label: "Config file",
149
- extension: ".json",
150
- maxBytes: MAX_CONFIG_UPLOAD_BYTES,
151
- acceptedMimeTypes: JSON_CONFIG_MIME_TYPES,
152
- });
153
- if (validationError) {
154
- setError(validationError);
155
- setConfigContent(null);
156
- event.target.value = "";
157
- return;
158
- }
159
- const reader = new FileReader();
160
- reader.onload = (e) => {
161
- const text = e.target?.result;
162
- if (typeof text !== "string") {
163
- return;
164
- }
165
- try {
166
- const parsed = JSON.parse(text) as Record<string, unknown>;
167
- setConfigContent(parsed);
168
- setError(null);
169
- } catch {
170
- setError("config.json is not valid JSON.");
171
- setConfigContent(null);
172
- }
173
- };
174
- reader.readAsText(file);
175
- },
176
- [],
177
- );
84
+ const handleSelectionError = useCallback((message: string) => {
85
+ setScriptContent(null);
86
+ setScriptFileName("");
87
+ setConfigFileName(null);
88
+ setConfigContent(null);
89
+ setError(message);
90
+ toast({
91
+ title: "Couldn't use that folder",
92
+ description: message,
93
+ variant: "destructive",
94
+ });
95
+ }, []);
178
96
 
179
97
  const handleUpload = useCallback(async () => {
180
98
  if (!scriptContent || !workflowName.trim()) {
@@ -230,35 +148,21 @@ export function UploadWorkflowDialog({
230
148
  <DialogHeader>
231
149
  <DialogTitle>Upload Workflow</DialogTitle>
232
150
  <DialogDescription>
233
- Upload a Python workflow script and an optional JSON config to
234
- create a new workflow.
151
+ Select a folder containing a Python workflow script and an optional
152
+ JSON config to create a new workflow.
235
153
  </DialogDescription>
236
154
  </DialogHeader>
237
155
  <div className="grid gap-4 py-4">
238
156
  <div className="space-y-1.5">
239
- <Label htmlFor="upload-script">
240
- Workflow Script <span className="text-destructive">*</span>
157
+ <Label htmlFor="upload-folder">
158
+ Workflow Folder <span className="text-destructive">*</span>
241
159
  </Label>
242
- <Input
243
- id="upload-script"
244
- ref={scriptInputRef}
245
- type="file"
246
- accept=".py"
247
- onChange={handleScriptFileChange}
248
- disabled={isUploading}
249
- />
250
- {scriptFileName && (
251
- <p className="text-xs text-muted-foreground">{scriptFileName}</p>
252
- )}
253
- </div>
254
- <div className="space-y-1.5">
255
- <Label htmlFor="upload-config">Config (optional)</Label>
256
- <Input
257
- id="upload-config"
258
- ref={configInputRef}
259
- type="file"
260
- accept=".json"
261
- onChange={handleConfigFileChange}
160
+ <WorkflowFolderPicker
161
+ idPrefix="upload"
162
+ scriptName={scriptFileName}
163
+ configName={configFileName}
164
+ onSelect={handleSelect}
165
+ onError={handleSelectionError}
262
166
  disabled={isUploading}
263
167
  />
264
168
  </div>