@@ -75,6 +75,22 @@ import { useRouter } from "next/router";
7575import { type ReactElement , useMemo , useState } from "react" ;
7676import { toast } from "sonner" ;
7777import superjson from "superjson" ;
78+ import {
79+ Dialog ,
80+ DialogContent ,
81+ DialogDescription ,
82+ DialogFooter ,
83+ DialogHeader ,
84+ DialogTitle ,
85+ DialogTrigger ,
86+ } from "@/components/ui/dialog" ;
87+ import {
88+ Select ,
89+ SelectContent ,
90+ SelectItem ,
91+ SelectTrigger ,
92+ SelectValue ,
93+ } from "@/components/ui/select" ;
7894
7995export type Services = {
8096 appName : string ;
@@ -205,8 +221,13 @@ const Project = (
205221 const { data : auth } = api . user . get . useQuery ( ) ;
206222
207223 const { data, isLoading, refetch } = api . project . one . useQuery ( { projectId } ) ;
224+ const { data : allProjects } = api . project . all . useQuery ( ) ;
208225 const router = useRouter ( ) ;
209226
227+ const [ isMoveDialogOpen , setIsMoveDialogOpen ] = useState ( false ) ;
228+ const [ selectedTargetProject , setSelectedTargetProject ] =
229+ useState < string > ( "" ) ;
230+
210231 const emptyServices =
211232 data ?. mariadb ?. length === 0 &&
212233 data ?. mongo ?. length === 0 &&
@@ -254,6 +275,31 @@ const Project = (
254275 const composeActions = {
255276 start : api . compose . start . useMutation ( ) ,
256277 stop : api . compose . stop . useMutation ( ) ,
278+ move : api . compose . move . useMutation ( ) ,
279+ } ;
280+
281+ const applicationActions = {
282+ move : api . application . move . useMutation ( ) ,
283+ } ;
284+
285+ const postgresActions = {
286+ move : api . postgres . move . useMutation ( ) ,
287+ } ;
288+
289+ const mysqlActions = {
290+ move : api . mysql . move . useMutation ( ) ,
291+ } ;
292+
293+ const mariadbActions = {
294+ move : api . mariadb . move . useMutation ( ) ,
295+ } ;
296+
297+ const redisActions = {
298+ move : api . redis . move . useMutation ( ) ,
299+ } ;
300+
301+ const mongoActions = {
302+ move : api . mongo . move . useMutation ( ) ,
257303 } ;
258304
259305 const handleBulkStart = async ( ) => {
@@ -296,6 +342,80 @@ const Project = (
296342 setIsBulkActionLoading ( false ) ;
297343 } ;
298344
345+ const handleBulkMove = async ( ) => {
346+ if ( ! selectedTargetProject ) {
347+ toast . error ( "Please select a target project" ) ;
348+ return ;
349+ }
350+
351+ let success = 0 ;
352+ setIsBulkActionLoading ( true ) ;
353+ for ( const serviceId of selectedServices ) {
354+ try {
355+ const service = filteredServices . find ( ( s ) => s . id === serviceId ) ;
356+ if ( ! service ) continue ;
357+
358+ switch ( service . type ) {
359+ case "application" :
360+ await applicationActions . move . mutateAsync ( {
361+ applicationId : serviceId ,
362+ targetProjectId : selectedTargetProject ,
363+ } ) ;
364+ break ;
365+ case "compose" :
366+ await composeActions . move . mutateAsync ( {
367+ composeId : serviceId ,
368+ targetProjectId : selectedTargetProject ,
369+ } ) ;
370+ break ;
371+ case "postgres" :
372+ await postgresActions . move . mutateAsync ( {
373+ postgresId : serviceId ,
374+ targetProjectId : selectedTargetProject ,
375+ } ) ;
376+ break ;
377+ case "mysql" :
378+ await mysqlActions . move . mutateAsync ( {
379+ mysqlId : serviceId ,
380+ targetProjectId : selectedTargetProject ,
381+ } ) ;
382+ break ;
383+ case "mariadb" :
384+ await mariadbActions . move . mutateAsync ( {
385+ mariadbId : serviceId ,
386+ targetProjectId : selectedTargetProject ,
387+ } ) ;
388+ break ;
389+ case "redis" :
390+ await redisActions . move . mutateAsync ( {
391+ redisId : serviceId ,
392+ targetProjectId : selectedTargetProject ,
393+ } ) ;
394+ break ;
395+ case "mongo" :
396+ await mongoActions . move . mutateAsync ( {
397+ mongoId : serviceId ,
398+ targetProjectId : selectedTargetProject ,
399+ } ) ;
400+ break ;
401+ }
402+ success ++ ;
403+ } catch ( error ) {
404+ toast . error (
405+ `Error moving service ${ serviceId } : ${ error instanceof Error ? error . message : "Unknown error" } ` ,
406+ ) ;
407+ }
408+ }
409+ if ( success > 0 ) {
410+ toast . success ( `${ success } services moved successfully` ) ;
411+ refetch ( ) ;
412+ }
413+ setSelectedServices ( [ ] ) ;
414+ setIsDropdownOpen ( false ) ;
415+ setIsMoveDialogOpen ( false ) ;
416+ setIsBulkActionLoading ( false ) ;
417+ } ;
418+
299419 const filteredServices = useMemo ( ( ) => {
300420 if ( ! applications ) return [ ] ;
301421 return applications . filter (
@@ -445,6 +565,84 @@ const Project = (
445565 Stop
446566 </ Button >
447567 </ DialogAction >
568+ < Dialog
569+ open = { isMoveDialogOpen }
570+ onOpenChange = { setIsMoveDialogOpen }
571+ >
572+ < DialogTrigger asChild >
573+ < Button
574+ variant = "ghost"
575+ className = "w-full justify-start"
576+ >
577+ < FolderInput className = "mr-2 h-4 w-4" />
578+ Move
579+ </ Button >
580+ </ DialogTrigger >
581+ < DialogContent >
582+ < DialogHeader >
583+ < DialogTitle > Move Services</ DialogTitle >
584+ < DialogDescription >
585+ Select the target project to move{ " " }
586+ { selectedServices . length } services
587+ </ DialogDescription >
588+ </ DialogHeader >
589+ < div className = "flex flex-col gap-4" >
590+ { allProjects ?. filter (
591+ ( p ) => p . projectId !== projectId ,
592+ ) . length === 0 ? (
593+ < div className = "flex flex-col items-center justify-center gap-2 py-4" >
594+ < FolderInput className = "h-8 w-8 text-muted-foreground" />
595+ < p className = "text-sm text-muted-foreground text-center" >
596+ No other projects available. Create a new
597+ project first to move services.
598+ </ p >
599+ </ div >
600+ ) : (
601+ < Select
602+ value = { selectedTargetProject }
603+ onValueChange = { setSelectedTargetProject }
604+ >
605+ < SelectTrigger >
606+ < SelectValue placeholder = "Select target project" />
607+ </ SelectTrigger >
608+ < SelectContent >
609+ { allProjects
610+ ?. filter (
611+ ( p ) => p . projectId !== projectId ,
612+ )
613+ . map ( ( project ) => (
614+ < SelectItem
615+ key = { project . projectId }
616+ value = { project . projectId }
617+ >
618+ { project . name }
619+ </ SelectItem >
620+ ) ) }
621+ </ SelectContent >
622+ </ Select >
623+ ) }
624+ </ div >
625+ < DialogFooter >
626+ < Button
627+ variant = "outline"
628+ onClick = { ( ) => setIsMoveDialogOpen ( false ) }
629+ >
630+ Cancel
631+ </ Button >
632+ < Button
633+ onClick = { handleBulkMove }
634+ isLoading = { isBulkActionLoading }
635+ disabled = {
636+ allProjects ?. filter (
637+ ( p ) => p . projectId !== projectId ,
638+ ) . length === 0
639+ }
640+ >
641+ Move Services
642+ </ Button >
643+ </ DialogFooter >
644+ </ DialogContent >
645+ </ Dialog >
448646 </ DropdownMenuContent >
449647 </ DropdownMenu >
450648 </ div >
0 commit comments