Skip to content

Commit f135043

Browse files
author
Jens Wollert Ehlers
committed
Adding clone project feature
1 parent f48ef4c commit f135043

5 files changed

Lines changed: 813 additions & 443 deletions

File tree

service.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
833956
func (s *service) GetProjects() []*Project {
834957
pp, err := s.r.FindProjectsByWorkspace(s.Member.WorkspaceID)
835958
if err != nil {

webapp/build/index.html

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,23 @@
1-
<!doctype html>
1+
<!DOCTYPE html>
22
<html lang="en">
3-
<head>
4-
<meta charset="utf-8" />
5-
<link rel="shortcut icon" href="/favicon.ico" />
63

7-
<meta
8-
name="viewport"
9-
content="width=device-width, initial-scale=1, shrink-to-fit=no"
10-
/>
11-
<meta name="theme-color" content="#000000" />
4+
<head>
5+
<meta charset="utf-8" />
6+
<link rel="shortcut icon" href="/favicon.ico" />
127

13-
<link
14-
href="https://fonts.googleapis.com/css?family=Roboto:400,500,700"
15-
rel="stylesheet"
16-
/>
17-
<link
18-
href="https://fonts.googleapis.com/icon?family=Material+Icons"
19-
rel="stylesheet"
20-
/>
21-
<title>Featmap</title>
22-
<script
23-
type="module"
24-
crossorigin
25-
src="/assets/index-E60fFEgv.js"
26-
></script>
27-
<link rel="stylesheet" crossorigin href="/assets/index-CKpe4fXE.css" />
28-
</head>
8+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
9+
<meta name="theme-color" content="#000000" />
10+
11+
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700" rel="stylesheet">
12+
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
13+
<title>Featmap</title>
14+
<script type="module" crossorigin src="/assets/index-DBq2dFpu.js"></script>
15+
<link rel="stylesheet" crossorigin href="/assets/index-CKpe4fXE.css">
16+
</head>
17+
18+
<body class="bg-gray-50">
19+
<noscript>You need to enable JavaScript to run this app.</noscript>
20+
<div id="root"></div>
21+
</body>
2922

30-
<body class="bg-gray-50">
31-
<noscript>You need to enable JavaScript to run this app.</noscript>
32-
<div id="root"></div>
33-
</body>
3423
</html>

webapp/src/api/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,18 @@ export const API_DELETE_PROJECT = async (workspaceId: string, id: string) => {
430430
});
431431
}
432432

433+
export const API_CLONE_PROJECT = async (workspaceId: string, projectId: string) => {
434+
return await fetch(endpoint + "/projects/" + projectId + "/clone", {
435+
method: 'POST',
436+
headers: {
437+
'Accept': 'application/json',
438+
'Content-Type': 'application/json',
439+
"Workspace": workspaceId
440+
},
441+
credentials: 'include'
442+
});
443+
}
444+
433445
export const API_UPDATE_PROJECT_DESCRIPTION = async (workspaceId: string, id: string, description: string) => {
434446
return await fetch(endpoint + "/projects/" + id + "/description", {
435447
method: 'POST',

0 commit comments

Comments
 (0)