@@ -81,6 +81,7 @@ type Service interface {
8181 GetProjectExtendedByExternalLink (link string ) (* projectResponse , error )
8282 GetProject (id string ) * Project
8383 CreateProjectWithID (id string , title string ) (* Project , error )
84+ CloneProject (sourceProjectID string ) (* Project , error )
8485 RenameProject (id string , title string ) (* Project , error )
8586 DeleteProject (id string ) error
8687 GetProjects () []* Project
@@ -830,6 +831,128 @@ func (s *service) DeleteProject(id string) error {
830831 return nil
831832}
832833
834+ func (s * service ) CloneProject (sourceProjectID string ) (* Project , error ) {
835+ wsID := s .Member .WorkspaceID
836+ t := time .Now ().UTC ()
837+
838+ src , err := s .r .GetProject (wsID , sourceProjectID )
839+ if err != nil {
840+ return nil , err
841+ }
842+
843+ clone := & Project {
844+ WorkspaceID : wsID ,
845+ ID : uuid .Must (uuid .NewV7 ()).String (),
846+ Title : src .Title + " (copy)" ,
847+ Description : src .Description ,
848+ CreatedByName : s .Acc .Name ,
849+ CreatedAt : t ,
850+ LastModified : t ,
851+ LastModifiedByName : s .Acc .Name ,
852+ ExternalLink : uuid .Must (uuid .NewV7 ()).String (),
853+ Annotations : src .Annotations ,
854+ }
855+ s .r .StoreProject (clone )
856+
857+ // Build ID maps: old -> new
858+ personaMap := make (map [string ]string )
859+ milestoneMap := make (map [string ]string )
860+ workflowMap := make (map [string ]string )
861+ subworkflowMap := make (map [string ]string )
862+
863+ // Clone personas
864+ personas , _ := s .r .FindPersonasByProject (wsID , sourceProjectID )
865+ for _ , p := range personas {
866+ newID := uuid .Must (uuid .NewV7 ()).String ()
867+ personaMap [p .ID ] = newID
868+ clone := & Persona {
869+ WorkspaceID : wsID , ProjectID : clone .ID , ID : newID ,
870+ Name : p .Name , Role : p .Role , Avatar : p .Avatar ,
871+ Description : p .Description , CreatedAt : t ,
872+ }
873+ s .r .StorePersona (clone )
874+ }
875+
876+ // Clone milestones
877+ milestones , _ := s .r .FindMilestonesByProject (wsID , sourceProjectID )
878+ for _ , m := range milestones {
879+ newID := uuid .Must (uuid .NewV7 ()).String ()
880+ milestoneMap [m .ID ] = newID
881+ clone := & Milestone {
882+ WorkspaceID : wsID , ProjectID : clone .ID , ID : newID ,
883+ Title : m .Title , Description : m .Description ,
884+ Status : m .Status , Rank : m .Rank , Color : m .Color ,
885+ Annotations : m .Annotations ,
886+ CreatedByName : s .Acc .Name , CreatedAt : t ,
887+ LastModified : t , LastModifiedByName : s .Acc .Name ,
888+ }
889+ s .r .StoreMilestone (clone )
890+ }
891+
892+ // Clone workflows
893+ workflows , _ := s .r .FindWorkflowsByProject (wsID , sourceProjectID )
894+ for _ , w := range workflows {
895+ newID := uuid .Must (uuid .NewV7 ()).String ()
896+ workflowMap [w .ID ] = newID
897+ clone := & Workflow {
898+ WorkspaceID : wsID , ProjectID : clone .ID , ID : newID ,
899+ Title : w .Title , Description : w .Description ,
900+ Status : w .Status , Rank : w .Rank , Color : w .Color ,
901+ Annotations : w .Annotations ,
902+ CreatedByName : s .Acc .Name , CreatedAt : t ,
903+ LastModified : t , LastModifiedByName : s .Acc .Name ,
904+ }
905+ s .r .StoreWorkflow (clone )
906+ }
907+
908+ // Clone subworkflows
909+ subworkflows , _ := s .r .FindSubWorkflowsByProject (wsID , sourceProjectID )
910+ for _ , sw := range subworkflows {
911+ newID := uuid .Must (uuid .NewV7 ()).String ()
912+ subworkflowMap [sw .ID ] = newID
913+ newWorkflowID := workflowMap [sw .WorkflowID ]
914+ clone := & SubWorkflow {
915+ WorkspaceID : wsID , WorkflowID : newWorkflowID , ID : newID ,
916+ Title : sw .Title , Description : sw .Description ,
917+ Status : sw .Status , Rank : sw .Rank , Color : sw .Color ,
918+ Annotations : sw .Annotations ,
919+ CreatedByName : s .Acc .Name , CreatedAt : t ,
920+ LastModified : t , LastModifiedByName : s .Acc .Name ,
921+ }
922+ s .r .StoreSubWorkflow (clone )
923+ }
924+
925+ // Clone features
926+ features , _ := s .r .FindFeaturesByProject (wsID , sourceProjectID )
927+ for _ , f := range features {
928+ newID := uuid .Must (uuid .NewV7 ()).String ()
929+ clone := & Feature {
930+ WorkspaceID : wsID , SubWorkflowID : subworkflowMap [f .SubWorkflowID ],
931+ MilestoneID : milestoneMap [f .MilestoneID ], ID : newID ,
932+ Title : f .Title , Description : f .Description ,
933+ Status : f .Status , Rank : f .Rank , Color : f .Color ,
934+ Annotations : f .Annotations , Estimate : f .Estimate ,
935+ CreatedByName : s .Acc .Name , CreatedAt : t ,
936+ LastModified : t , LastModifiedByName : s .Acc .Name ,
937+ }
938+ s .r .StoreFeature (clone )
939+ }
940+
941+ // Clone workflow personas
942+ wps , _ := s .r .FindWorkflowPersonasByProject (wsID , sourceProjectID )
943+ for _ , wp := range wps {
944+ newID := uuid .Must (uuid .NewV7 ()).String ()
945+ clone := & WorkflowPersona {
946+ WorkspaceID : wsID , ProjectID : clone .ID , ID : newID ,
947+ WorkflowID : workflowMap [wp .WorkflowID ],
948+ PersonaID : personaMap [wp .PersonaID ],
949+ }
950+ s .r .StoreWorkflowPersona (clone )
951+ }
952+
953+ return clone , nil
954+ }
955+
833956func (s * service ) GetProjects () []* Project {
834957 pp , err := s .r .FindProjectsByWorkspace (s .Member .WorkspaceID )
835958 if err != nil {
0 commit comments