|
|
@@ -87,6 +87,7 @@ export default function WorkflowsPage() {
|
|
|
const [groupToRename, setGroupToRename] = useState<WorkflowGroup | null>(null)
|
|
|
const [groupToDelete, setGroupToDelete] = useState<WorkflowGroup | null>(null)
|
|
|
const [groupToMove, setGroupToMove] = useState<WorkflowGroup | null>(null)
|
|
|
+ const [groupToMoveProject, setGroupToMoveProject] = useState<WorkflowGroup | null>(null)
|
|
|
const [workflowToMove, setWorkflowToMove] = useState<Workflow | null>(null)
|
|
|
|
|
|
// Fetch current path (breadcrumbs)
|
|
|
@@ -194,6 +195,42 @@ export default function WorkflowsPage() {
|
|
|
},
|
|
|
})
|
|
|
|
|
|
+ // What moving groupToMoveProject to another project actually does, worked out
|
|
|
+ // before the user commits to it: the backend patches every workflow directly
|
|
|
+ // in this folder to the new project too (workflow_group_controller.cpp), but
|
|
|
+ // it does not reach into subfolders - a subfolder and its own workflows stay
|
|
|
+ // behind in the old project. Both counts are shown in the confirmation so
|
|
|
+ // neither is a surprise afterwards.
|
|
|
+ const { data: moveProjectImpact } = useQuery({
|
|
|
+ queryKey: ['workflow-group-move-impact', groupToMoveProject?.id],
|
|
|
+ queryFn: async () => {
|
|
|
+ // pageSize 500 and counting the returned array, not the "total" field:
|
|
|
+ // the workflows endpoint's total reflects the page it just returned
|
|
|
+ // rather than the true count once pageSize is small, so a pageSize of 1
|
|
|
+ // would (and did, in testing) report "1 workflow" for a folder holding 2.
|
|
|
+ const [workflowsInFolder, subfolders] = await Promise.all([
|
|
|
+ workflowsApi.list(1, 500, groupToMoveProject!.id),
|
|
|
+ workflowGroupsApi.list(groupToMoveProject!.id),
|
|
|
+ ])
|
|
|
+ return {
|
|
|
+ workflowCount: workflowsInFolder.workflows.length,
|
|
|
+ subfolderCount: subfolders.groups.length,
|
|
|
+ }
|
|
|
+ },
|
|
|
+ enabled: !!groupToMoveProject,
|
|
|
+ })
|
|
|
+
|
|
|
+ const moveGroupToProjectMutation = useMutation({
|
|
|
+ mutationFn: ({ id, projectId }: { id: string; projectId: string }) =>
|
|
|
+ workflowGroupsApi.moveToProject(id, projectId),
|
|
|
+ onSuccess: () => {
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['workflow-groups'] })
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['workflows'] })
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['projects'] })
|
|
|
+ setGroupToMoveProject(null)
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
// Workflow mutations
|
|
|
const createWorkflowMutation = useMutation({
|
|
|
mutationFn: workflowsApi.create,
|
|
|
@@ -345,6 +382,64 @@ export default function WorkflowsPage() {
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
+ {groupToMoveProject && (
|
|
|
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={() => setGroupToMoveProject(null)}>
|
|
|
+ <div className="bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-sm p-5" onClick={(e) => e.stopPropagation()}>
|
|
|
+ <h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
|
|
+ Move "{groupToMoveProject.name}"
|
|
|
+ </h3>
|
|
|
+ <p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
|
|
+ A project decides who can see and change this folder. Moving it may
|
|
|
+ take it away from people who can reach it now, or hand it to people who cannot.
|
|
|
+ </p>
|
|
|
+ <p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
|
|
+ {moveProjectImpact === undefined
|
|
|
+ ? 'Checking what is in this folder...'
|
|
|
+ : moveProjectImpact.workflowCount === 0
|
|
|
+ ? 'This folder has no workflows directly in it.'
|
|
|
+ : `This will also move ${moveProjectImpact.workflowCount} workflow${moveProjectImpact.workflowCount === 1 ? '' : 's'} directly in this folder to the new project.`}
|
|
|
+ </p>
|
|
|
+ {moveProjectImpact !== undefined && moveProjectImpact.subfolderCount > 0 && (
|
|
|
+ <p className="text-xs text-amber-600 dark:text-amber-400 mb-3 flex items-start gap-1.5">
|
|
|
+ <AlertTriangle className="w-3.5 h-3.5 shrink-0 mt-px" />
|
|
|
+ <span>
|
|
|
+ This folder has {moveProjectImpact.subfolderCount} subfolder
|
|
|
+ {moveProjectImpact.subfolderCount === 1 ? '' : 's'}. Only this folder and the
|
|
|
+ workflows directly in it move - subfolders and their workflows stay in the
|
|
|
+ current project.
|
|
|
+ </span>
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+ <div className="space-y-1 max-h-60 overflow-y-auto">
|
|
|
+ {projects.map((p) => (
|
|
|
+ <button
|
|
|
+ key={p._id}
|
|
|
+ onClick={() => moveGroupToProjectMutation.mutate({ id: groupToMoveProject.id, projectId: p._id })}
|
|
|
+ disabled={p._id === groupToMoveProject.projectId || moveGroupToProjectMutation.isPending}
|
|
|
+ className="w-full px-3 py-2 text-left text-sm rounded hover:bg-gray-50 dark:hover:bg-slate-700 disabled:opacity-40 flex items-center justify-between"
|
|
|
+ >
|
|
|
+ <span className="text-gray-900 dark:text-gray-100">{p.name}</span>
|
|
|
+ {p._id === groupToMoveProject.projectId && (
|
|
|
+ <span className="text-xs text-gray-400">where it is now</span>
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ {moveGroupToProjectMutation.isError && (
|
|
|
+ <p className="text-xs text-red-600 dark:text-red-400 mt-3">
|
|
|
+ {(moveGroupToProjectMutation.error as any)?.response?.data?.error ||
|
|
|
+ 'Could not move this folder.'}
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+ <div className="flex justify-end mt-4">
|
|
|
+ <button onClick={() => setGroupToMoveProject(null)} className="px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400">
|
|
|
+ Cancel
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
{isLoading ? (
|
|
|
<div className="flex items-center justify-center py-12">
|
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
|
|
@@ -386,6 +481,7 @@ export default function WorkflowsPage() {
|
|
|
onClick={() => navigateToGroup(group.id)}
|
|
|
onRename={() => setGroupToRename(group)}
|
|
|
onMove={() => setGroupToMove(group)}
|
|
|
+ onMoveToProject={() => setGroupToMoveProject(group)}
|
|
|
onDelete={() => setGroupToDelete(group)}
|
|
|
/>
|
|
|
))}
|