diff --git a/api/internal/handler/issue.go b/api/internal/handler/issue.go index 56ba3e4..d95f05a 100644 --- a/api/internal/handler/issue.go +++ b/api/internal/handler/issue.go @@ -29,6 +29,32 @@ func issueID(c *gin.Context) (uuid.UUID, bool) { return id, true } +// ListWorkspaceDrafts returns draft work items for the workspace (all projects). +// GET /api/workspaces/:slug/draft-issues/ +func (h *IssueHandler) ListWorkspaceDrafts(c *gin.Context) { + user := middleware.GetUser(c) + if user == nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"}) + return + } + slug := c.Param("slug") + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) + offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) + if limit <= 0 || limit > 100 { + limit = 50 + } + list, err := h.Issue.ListDraftsForWorkspace(c.Request.Context(), slug, user.ID, limit, offset) + if err != nil { + if err == service.ErrWorkspaceForbidden { + c.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list draft issues"}) + return + } + c.JSON(http.StatusOK, list) +} + // List returns issues for the project. // GET /api/workspaces/:slug/projects/:projectId/issues/ func (h *IssueHandler) List(c *gin.Context) { @@ -114,6 +140,7 @@ func (h *IssueHandler) Create(c *gin.Context) { TargetDate *string `json:"target_date"` AssigneeIDs []uuid.UUID `json:"assignee_ids"` LabelIDs []uuid.UUID `json:"label_ids"` + IsDraft bool `json:"is_draft"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "detail": err.Error()}) @@ -137,7 +164,7 @@ func (h *IssueHandler) Create(c *gin.Context) { } } - issue, err := h.Issue.Create(c.Request.Context(), slug, projectID, user.ID, body.Name, body.Description, body.Priority, body.StateID, body.AssigneeIDs, body.LabelIDs, startDate, targetDate, body.ParentID) + issue, err := h.Issue.Create(c.Request.Context(), slug, projectID, user.ID, body.Name, body.Description, body.Priority, body.StateID, body.AssigneeIDs, body.LabelIDs, startDate, targetDate, body.ParentID, body.IsDraft) if err != nil { if err == service.ErrProjectForbidden || err == service.ErrProjectNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) @@ -177,6 +204,7 @@ func (h *IssueHandler) Update(c *gin.Context) { TargetDate *string `json:"target_date"` AssigneeIDs []uuid.UUID `json:"assignee_ids"` LabelIDs []uuid.UUID `json:"label_ids"` + IsDraft *bool `json:"is_draft"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "detail": err.Error()}) @@ -222,7 +250,7 @@ func (h *IssueHandler) Update(c *gin.Context) { } } - issue, err := h.Issue.Update(c.Request.Context(), slug, projectID, issueID, user.ID, name, priority, description, body.StateID, assigneeIDs, labelIDs, startDate, targetDate, body.ParentID) + issue, err := h.Issue.Update(c.Request.Context(), slug, projectID, issueID, user.ID, name, priority, description, body.StateID, assigneeIDs, labelIDs, startDate, targetDate, body.ParentID, body.IsDraft) if err != nil { if err == service.ErrIssueNotFound || err == service.ErrProjectForbidden { c.JSON(http.StatusNotFound, gin.H{"error": "Issue not found"}) diff --git a/api/internal/router/router.go b/api/internal/router/router.go index 8991393..df44598 100644 --- a/api/internal/router/router.go +++ b/api/internal/router/router.go @@ -170,6 +170,8 @@ func New(cfg Config) *gin.Engine { api.GET("/users/me/workspaces/:slug/projects/invitations/", projectHandler.ListUserProjectInvitations) api.POST("/workspaces/:slug/projects/join/", projectHandler.JoinByToken) + api.GET("/workspaces/:slug/draft-issues/", issueHandler.ListWorkspaceDrafts) + api.GET("/workspaces/:slug/projects/", projectHandler.List) api.POST("/workspaces/:slug/projects/", projectHandler.Create) api.GET("/workspaces/:slug/projects/:projectId/", projectHandler.Get) diff --git a/api/internal/service/issue.go b/api/internal/service/issue.go index 2bfca0c..cbc6917 100644 --- a/api/internal/service/issue.go +++ b/api/internal/service/issue.go @@ -42,6 +42,46 @@ func (s *IssueService) ensureProjectAccess(ctx context.Context, workspaceSlug st return nil } +func (s *IssueService) ensureWorkspaceAccess(ctx context.Context, workspaceSlug string, userID uuid.UUID) (*model.Workspace, error) { + wrk, err := s.ws.GetBySlug(ctx, workspaceSlug) + if err != nil { + return nil, ErrWorkspaceForbidden + } + ok, _ := s.ws.IsMember(ctx, wrk.ID, userID) + if !ok { + return nil, ErrWorkspaceForbidden + } + return wrk, nil +} + +// ListDraftsForWorkspace returns draft issues for all projects in the workspace the user can access. +func (s *IssueService) ListDraftsForWorkspace(ctx context.Context, workspaceSlug string, userID uuid.UUID, limit, offset int) ([]model.Issue, error) { + wrk, err := s.ensureWorkspaceAccess(ctx, workspaceSlug, userID) + if err != nil { + return nil, err + } + list, err := s.is.ListDraftsByWorkspaceID(ctx, wrk.ID, limit, offset) + if err != nil { + return nil, err + } + for i := range list { + issueID := list[i].ID + if ids, err := s.is.ListAssigneesForIssue(ctx, issueID); err == nil { + list[i].AssigneeIDs = ids + } + if ids, err := s.is.ListLabelsForIssue(ctx, issueID); err == nil { + list[i].LabelIDs = ids + } + if ids, err := s.is.ListCycleIDsForIssue(ctx, issueID); err == nil { + list[i].CycleIDs = ids + } + if ids, err := s.is.ListModuleIDsForIssue(ctx, issueID); err == nil { + list[i].ModuleIDs = ids + } + } + return list, nil +} + func (s *IssueService) List(ctx context.Context, workspaceSlug string, projectID uuid.UUID, userID uuid.UUID, limit, offset int) ([]model.Issue, error) { if err := s.ensureProjectAccess(ctx, workspaceSlug, projectID, userID); err != nil { return nil, err @@ -97,7 +137,7 @@ func (s *IssueService) GetByID(ctx context.Context, workspaceSlug string, projec return issue, nil } -func (s *IssueService) Create(ctx context.Context, workspaceSlug string, projectID uuid.UUID, userID uuid.UUID, name, description, priority string, stateID *uuid.UUID, assigneeIDs []uuid.UUID, labelIDs []uuid.UUID, startDate, targetDate *time.Time, parentID *uuid.UUID) (*model.Issue, error) { +func (s *IssueService) Create(ctx context.Context, workspaceSlug string, projectID uuid.UUID, userID uuid.UUID, name, description, priority string, stateID *uuid.UUID, assigneeIDs []uuid.UUID, labelIDs []uuid.UUID, startDate, targetDate *time.Time, parentID *uuid.UUID, isDraft bool) (*model.Issue, error) { if err := s.ensureProjectAccess(ctx, workspaceSlug, projectID, userID); err != nil { return nil, err } @@ -107,6 +147,7 @@ func (s *IssueService) Create(ctx context.Context, workspaceSlug string, project ProjectID: projectID, WorkspaceID: wrk.ID, CreatedByID: &userID, + IsDraft: isDraft, } if description != "" { issue.DescriptionHTML = description @@ -145,7 +186,7 @@ func (s *IssueService) Create(ctx context.Context, workspaceSlug string, project return issue, nil } -func (s *IssueService) Update(ctx context.Context, workspaceSlug string, projectID, issueID uuid.UUID, userID uuid.UUID, name, priority, description *string, stateID *uuid.UUID, assigneeIDs, labelIDs *[]uuid.UUID, startDate, targetDate *time.Time, parentID *uuid.UUID) (*model.Issue, error) { +func (s *IssueService) Update(ctx context.Context, workspaceSlug string, projectID, issueID uuid.UUID, userID uuid.UUID, name, priority, description *string, stateID *uuid.UUID, assigneeIDs, labelIDs *[]uuid.UUID, startDate, targetDate *time.Time, parentID *uuid.UUID, isDraft *bool) (*model.Issue, error) { issue, err := s.GetByID(ctx, workspaceSlug, projectID, issueID, userID) if err != nil { return nil, err @@ -171,6 +212,9 @@ func (s *IssueService) Update(ctx context.Context, workspaceSlug string, project if parentID != nil { issue.ParentID = parentID } + if isDraft != nil { + issue.IsDraft = *isDraft + } issue.UpdatedByID = &userID if err := s.is.Update(ctx, issue); err != nil { return nil, err diff --git a/api/internal/store/issue.go b/api/internal/store/issue.go index 38cb225..b676042 100644 --- a/api/internal/store/issue.go +++ b/api/internal/store/issue.go @@ -73,6 +73,22 @@ func (s *IssueStore) ListByProjectID(ctx context.Context, projectID uuid.UUID, l return list, err } +func (s *IssueStore) ListDraftsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID, limit, offset int) ([]model.Issue, error) { + var list []model.Issue + q := s.db.WithContext(ctx).Where( + "workspace_id = ? AND is_draft = ? AND deleted_at IS NULL", + workspaceID, true, + ).Order("updated_at DESC") + if limit > 0 { + q = q.Limit(limit) + } + if offset > 0 { + q = q.Offset(offset) + } + err := q.Find(&list).Error + return list, err +} + func (s *IssueStore) Update(ctx context.Context, i *model.Issue) error { return s.db.WithContext(ctx).Save(i).Error } diff --git a/ui/package-lock.json b/ui/package-lock.json index 9d96200..af20175 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1171,9 +1171,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", "cpu": [ "arm" ], @@ -1184,9 +1184,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", "cpu": [ "arm64" ], @@ -1197,9 +1197,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", "cpu": [ "arm64" ], @@ -1210,9 +1210,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", "cpu": [ "x64" ], @@ -1223,9 +1223,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", "cpu": [ "arm64" ], @@ -1236,9 +1236,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", "cpu": [ "x64" ], @@ -1249,9 +1249,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", "cpu": [ "arm" ], @@ -1262,9 +1262,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", "cpu": [ "arm" ], @@ -1275,9 +1275,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", "cpu": [ "arm64" ], @@ -1288,9 +1288,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", "cpu": [ "arm64" ], @@ -1301,9 +1301,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", "cpu": [ "loong64" ], @@ -1314,9 +1314,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", "cpu": [ "loong64" ], @@ -1327,9 +1327,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", "cpu": [ "ppc64" ], @@ -1340,9 +1340,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", "cpu": [ "ppc64" ], @@ -1353,9 +1353,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", "cpu": [ "riscv64" ], @@ -1366,9 +1366,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", "cpu": [ "riscv64" ], @@ -1379,9 +1379,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", "cpu": [ "s390x" ], @@ -1392,9 +1392,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", "cpu": [ "x64" ], @@ -1405,9 +1405,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", "cpu": [ "x64" ], @@ -1418,9 +1418,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", "cpu": [ "x64" ], @@ -1431,9 +1431,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", "cpu": [ "arm64" ], @@ -1444,9 +1444,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", "cpu": [ "arm64" ], @@ -1457,9 +1457,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", "cpu": [ "ia32" ], @@ -1470,9 +1470,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", "cpu": [ "x64" ], @@ -1483,9 +1483,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", "cpu": [ "x64" ], @@ -2858,13 +2858,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -2969,9 +2969,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3858,9 +3858,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -4635,9 +4635,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4782,9 +4782,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -5218,9 +5218,9 @@ } }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -5233,31 +5233,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" } }, diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index 056f27f..29a9385 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -195,6 +195,7 @@ export interface CreateIssueRequest { start_date?: string | null; target_date?: string | null; parent_id?: string | null; + is_draft?: boolean; } /** GET /api/instance/setup-status/ */ @@ -421,6 +422,21 @@ export interface PageApiResponse { updated_at: string; } +export interface CreatePageRequest { + name: string; + description_html?: string; + project_id?: string | null; + /** 0 public, 1 private */ + access?: number; +} + +export interface UpdatePageRequest { + name?: string; + description_html?: string; + /** 0 public, 1 private */ + access?: number; +} + /** Notification as returned by the API */ export interface NotificationApiResponse { id: string; diff --git a/ui/src/components/CreateWorkItemModal.tsx b/ui/src/components/CreateWorkItemModal.tsx index 4f2aa58..9a73ff1 100644 --- a/ui/src/components/CreateWorkItemModal.tsx +++ b/ui/src/components/CreateWorkItemModal.tsx @@ -94,6 +94,21 @@ const IconBuilding = () => ( const PRIORITIES: Priority[] = ['urgent', 'high', 'medium', 'low', 'none']; +export interface WorkItemInitialValues { + title?: string; + description?: string; + projectId?: string; + stateId?: string; + priority?: Priority; + assigneeIds?: string[]; + labelIds?: string[]; + startDate?: string; + dueDate?: string; + cycleId?: string | null; + moduleId?: string | null; + parentId?: string | null; +} + export interface CreateWorkItemModalProps { open: boolean; onClose: () => void; @@ -102,6 +117,16 @@ export interface CreateWorkItemModalProps { defaultProjectId?: string; defaultModuleId?: string | null; createError?: string | null; + /** Pre-fill form fields (used by edit and duplicate flows). */ + initialValues?: WorkItemInitialValues; + /** + * When true, configures the modal for the workspace drafts flow: + * - Draft-specific title copy + * - `onSave` receives `isDraft: true` + * + * Callers are still responsible for mapping `isDraft` to the API payload (e.g. `is_draft`). + */ + draftOnly?: boolean; onSave?: (data: { title: string; description: string; @@ -116,7 +141,8 @@ export interface CreateWorkItemModalProps { cycleId?: string | null; moduleId?: string | null; parentId?: string | null; - }) => void; + isDraft?: boolean; + }) => void | Promise; } export function CreateWorkItemModal({ @@ -127,6 +153,8 @@ export function CreateWorkItemModal({ defaultProjectId, defaultModuleId, createError, + initialValues, + draftOnly = false, onSave, }: CreateWorkItemModalProps) { const [title, setTitle] = useState(''); @@ -273,22 +301,23 @@ export function CreateWorkItemModal({ useEffect(() => { if (open) { - setProjectId(defaultProjectId ?? projects[0]?.id ?? ''); - setTitle(''); - setDescription(''); - setStateId(''); - setPriority('none'); - setAssigneeIds([]); - setLabelIds([]); - setStartDate(''); - setDueDate(''); - setCycleId(null); - setModuleId(defaultModuleId ?? null); - setParentId(null); + const iv = initialValues; + setProjectId(iv?.projectId ?? defaultProjectId ?? projects[0]?.id ?? ''); + setTitle(iv?.title ?? ''); + setDescription(iv?.description ?? ''); + setStateId(iv?.stateId ?? ''); + setPriority(iv?.priority ?? 'none'); + setAssigneeIds(iv?.assigneeIds ?? []); + setLabelIds(iv?.labelIds ?? []); + setStartDate(iv?.startDate ?? ''); + setDueDate(iv?.dueDate ?? ''); + setCycleId(iv?.cycleId ?? null); + setModuleId(iv?.moduleId ?? defaultModuleId ?? null); + setParentId(iv?.parentId ?? null); setOpenDropdown(null); setParentModalOpen(false); } - }, [open, defaultProjectId, defaultModuleId, projects]); + }, [open, defaultProjectId, defaultModuleId, projects, initialValues]); useEffect(() => { if (!open) return; @@ -316,7 +345,7 @@ export function CreateWorkItemModal({ title, description, projectId, - stateId: stateId || undefined, + stateId: draftOnly ? undefined : stateId || undefined, priority: priority !== 'none' ? priority : undefined, assigneeIds: assigneeIds.length ? assigneeIds : undefined, assigneeId: assigneeIds[0] ?? undefined, @@ -326,6 +355,7 @@ export function CreateWorkItemModal({ cycleId: cycleId ?? undefined, moduleId: moduleId ?? undefined, parentId: parentId ?? undefined, + isDraft: draftOnly ? true : undefined, }); if (!createMore) onClose(); else { @@ -407,7 +437,7 @@ export function CreateWorkItemModal({ >

- Create new work item + {draftOnly ? 'Create draft work item' : 'Create new work item'}

string; + isEmpty: () => boolean; + focus: () => void; + setHtml: (html: string) => void; +}; + +export type PageDescriptionEditorProps = { + initialHtml?: string; + placeholder?: string; + autoFocus?: boolean; + readOnly?: boolean; + className?: string; + /** + * Optional keyboard shortcut handler. + * If provided, pressing `Ctrl/Cmd + S` triggers it (default: prevent browser save dialog). + */ + onSaveShortcut?: () => void; +}; + +export const PageDescriptionEditor = forwardRef< + PageDescriptionEditorHandle, + PageDescriptionEditorProps +>( + ( + { + initialHtml, + placeholder, + autoFocus, + readOnly, + className, + onSaveShortcut, + }: PageDescriptionEditorProps, + ref, + ) => { + const editor = useEditor({ + extensions: [ + StarterKit.configure({ + bulletList: { keepMarks: true, keepAttributes: true }, + orderedList: { keepMarks: true, keepAttributes: true }, + codeBlock: {}, + }), + Underline, + Placeholder.configure({ + placeholder: placeholder ?? 'Write something…', + }), + ], + content: initialHtml ?? '', + editable: !readOnly, + autofocus: autoFocus ? 'end' : false, + }); + + useEffect(() => { + if (!editor) return; + // keep editor content in sync when parent loads data + if (initialHtml !== undefined) { + editor.commands.setContent(initialHtml || ''); + } + }, [editor, initialHtml]); + + useImperativeHandle( + ref, + () => ({ + getHtml: () => editor?.getHTML() ?? '', + isEmpty: () => { + const html = (editor?.getHTML() ?? '').trim(); + return html === '' || html === '

'; + }, + focus: () => editor?.commands.focus(), + setHtml: (html: string) => editor?.commands.setContent(html ?? ''), + }), + [editor], + ); + + if (!editor) return null; + + const buttonBase = + 'inline-flex h-8 w-8 items-center justify-center rounded border border-transparent text-(--txt-icon-tertiary) hover:bg-(--bg-layer-1-hover) hover:text-(--txt-icon-secondary) disabled:opacity-40'; + + return ( +
+
+ + + + + + + {onSaveShortcut && ( + + Ctrl/Cmd + S to save + + )} +
+
+ { + if (!onSaveShortcut) return; + const key = event.key?.toLowerCase?.() ?? ''; + if ((event.metaKey || event.ctrlKey) && key === 's') { + event.preventDefault(); + onSaveShortcut(); + } + }} + /> +
+
+ ); + }, +); + +PageDescriptionEditor.displayName = 'PageDescriptionEditor'; diff --git a/ui/src/components/drafts/DraftIssueRowProperties.tsx b/ui/src/components/drafts/DraftIssueRowProperties.tsx new file mode 100644 index 0000000..1976f5b --- /dev/null +++ b/ui/src/components/drafts/DraftIssueRowProperties.tsx @@ -0,0 +1,1007 @@ +import { useMemo, useRef, useState } from 'react'; +import { Dropdown } from '../work-item/Dropdown'; +import { Avatar } from '../ui'; +import type { + IssueApiResponse, + ProjectApiResponse, + StateApiResponse, + LabelApiResponse, + CycleApiResponse, + ModuleApiResponse, + WorkspaceMemberApiResponse, +} from '../../api/types'; +import type { Priority } from '../../types'; +import type { StateGroup } from '../../types/workspaceViewFilters'; +import { PRIORITY_LABELS } from '../workspace-views/WorkspaceViewsFiltersData'; +import { findWorkspaceMemberByUserId, getImageUrl } from '../../lib/utils'; +import { labelService } from '../../services/labelService'; + +const PRIORITIES: Priority[] = ['urgent', 'high', 'medium', 'low', 'none']; +const PRIORITY_TILE: Record = { + urgent: 'border-red-200 bg-red-50 text-red-600', + high: 'border-orange-200 bg-orange-50 text-orange-600', + medium: 'border-yellow-200 bg-yellow-50 text-yellow-700', + low: 'border-blue-200 bg-blue-50 text-blue-600', + none: 'border-(--border-subtle) bg-(--bg-layer-1) text-(--txt-icon-tertiary)', +}; + +function cx(...parts: Array) { + return parts.filter(Boolean).join(' '); +} + +function IconBacklogStatus() { + return ( + + + + ); +} + +function IconTodoStatus() { + return ( + + + + ); +} + +function IconInProgressStatus() { + return ( + + + + + ); +} + +function IconDoneStatus() { + return ( + + + + + + ); +} + +function IconCancelledStatus() { + return ( + + + + + + ); +} + +function stateIconByGroup(group: string | undefined) { + const g = (group ?? 'backlog').toLowerCase() as StateGroup; + switch (g) { + case 'unstarted': + return ; + case 'started': + return ; + case 'completed': + return ; + case 'canceled': + return ; + case 'backlog': + default: + return ; + } +} + +function IconPriorityUrgent() { + return ( + + + + + + ); +} + +function IconPriorityBars({ level }: { level: 1 | 2 | 3 }) { + const bars = level === 3 ? [9.7, 7.9, 6.1] : level === 2 ? [9.7, 7.9] : [9.7]; + return ( + + {bars.map((y, idx) => ( + + ))} + + ); +} + +function IconPriorityNone() { + return ( + + + + + ); +} + +function priorityIcon(p: Priority) { + switch (p) { + case 'urgent': + return ; + case 'high': + return ; + case 'medium': + return ; + case 'low': + return ; + case 'none': + default: + return ; + } +} + +function IconStartDateProperty() { + return ( + + + + + + + ); +} + +function IconDueDateProperty() { + return ( + + + + + + ); +} + +const IconChevronDown = () => ( + + + +); + +const IconTag = () => ( + + + +); + +const IconUser = () => ( + + + + +); + +const IconLayoutGrid = () => ( + + + + + + +); + +const IconCycle = () => ( + + + + +); + +const IconMoreHorizontal = () => ( + + + + + +); + +const IconEdit = () => ( + + + + +); + +const IconCopy = () => ( + + + + +); + +const IconMoveToIssues = () => ( + + + + + +); + +const IconTrash = () => ( + + + + + +); + +function stateGroupIcon(group: string | undefined) { + return stateIconByGroup(group); +} + +const propBtnSquare = + 'relative flex size-7 shrink-0 items-center justify-center rounded border border-(--border-subtle) bg-(--bg-surface-1) text-(--txt-icon-tertiary) hover:bg-(--bg-layer-1-hover) disabled:pointer-events-none disabled:opacity-40'; + +export interface DraftIssueRowPropertiesProps { + workspaceSlug: string; + issue: IssueApiResponse; + project: ProjectApiResponse | undefined; + states: StateApiResponse[]; + labels: LabelApiResponse[]; + modules: ModuleApiResponse[]; + cycles: CycleApiResponse[]; + members: WorkspaceMemberApiResponse[]; + busy: boolean; + openDropdownId: string | null; + setOpenDropdownId: (id: string | null) => void; + onPatch: (issue: IssueApiResponse, payload: Record) => Promise; + onModuleChange: (issue: IssueApiResponse, moduleId: string | null) => Promise; + onCycleChange: (issue: IssueApiResponse, cycleId: string | null) => Promise; + onToggleRowMenu: () => void; + rowMenuOpen: boolean; + onEdit: () => void; + onDuplicate: () => void; + onMoveToIssues: () => void; + onDelete: () => void; +} + +export function DraftIssueRowProperties({ + workspaceSlug, + issue, + project, + states, + labels, + modules, + cycles, + members, + busy, + openDropdownId, + setOpenDropdownId, + onPatch, + onModuleChange, + onCycleChange, + onToggleRowMenu, + rowMenuOpen, + onEdit, + onDuplicate, + onMoveToIssues, + onDelete, +}: DraftIssueRowPropertiesProps) { + const startInputRef = useRef(null); + const dueInputRef = useRef(null); + const [stateSearch, setStateSearch] = useState(''); + const [prioritySearch, setPrioritySearch] = useState(''); + const [labelSearch, setLabelSearch] = useState(''); + const [createLabelLoading, setCreateLabelLoading] = useState(false); + const [createLabelError, setCreateLabelError] = useState(null); + const [localLabels, setLocalLabels] = useState(labels); + if (localLabels !== labels && labels.length >= 0) { + // keep local options in sync with prop updates + setLocalLabels(labels); + } + + const pri = (issue.priority ?? 'none') as Priority; + const currentState = states.find((s) => s.id === issue.state_id); + const stateName = currentState?.name ?? 'Backlog'; + const primaryAssigneeId = + issue.assignee_ids && issue.assignee_ids.length > 0 ? issue.assignee_ids[0] : null; + const assigneeMember = findWorkspaceMemberByUserId(members, primaryAssigneeId); + const assigneeName = + assigneeMember?.member_display_name?.trim() || + assigneeMember?.member_email?.split('@')[0] || + (primaryAssigneeId ? primaryAssigneeId.slice(0, 8) : ''); + const assigneeAvatar = assigneeMember?.member_avatar?.trim(); + const labelNames = (issue.label_ids ?? []) + .map((id) => localLabels.find((l) => l.id === id)?.name) + .filter((n): n is string => Boolean(n)); + const currentModuleId = issue.module_ids?.[0] ?? null; + const moduleCount = issue.module_ids?.length ?? 0; + const currentCycleId = issue.cycle_ids?.[0] ?? null; + const cycleName = currentCycleId ? cycles.find((c) => c.id === currentCycleId)?.name : ''; + const stateOptions = useMemo(() => { + const byGroup = new Map(); + for (const s of states) { + const g = (s.group ?? '').toLowerCase(); + if (!g) continue; + if (!byGroup.has(g)) byGroup.set(g, s); + } + // Plane drafts shows these groups in this order + const ORDER: Array<{ group: string; label: string }> = [ + { group: 'backlog', label: 'Backlog' }, + { group: 'unstarted', label: 'Todo' }, + { group: 'started', label: 'In Progress' }, + { group: 'completed', label: 'Done' }, + { group: 'canceled', label: 'Cancelled' }, + ]; + return ORDER.map(({ group, label }) => { + const st = byGroup.get(group); + return { group, label, id: st?.id ?? null }; + }); + }, [states]); + + const filteredStateOptions = useMemo(() => { + const q = stateSearch.trim().toLowerCase(); + if (!q) return stateOptions; + return stateOptions.filter((o) => o.label.toLowerCase().includes(q)); + }, [stateOptions, stateSearch]); + + const filteredPriorities = useMemo(() => { + const q = prioritySearch.trim().toLowerCase(); + if (!q) return PRIORITIES; + return PRIORITIES.filter((p) => PRIORITY_LABELS[p].toLowerCase().includes(q)); + }, [prioritySearch]); + + const filteredLabels = useMemo(() => { + const q = labelSearch.trim().toLowerCase(); + if (!q) return localLabels; + return localLabels.filter((l) => l.name.toLowerCase().includes(q)); + }, [localLabels, labelSearch]); + + const canCreateLabel = useMemo(() => { + const name = labelSearch.trim(); + if (!name) return false; + return !localLabels.some((l) => l.name.toLowerCase() === name.toLowerCase()); + }, [labelSearch, localLabels]); + + const handleCreateLabel = async () => { + const name = labelSearch.trim(); + if (!name || !workspaceSlug) return; + setCreateLabelError(null); + setCreateLabelLoading(true); + try { + const created = await labelService.create(workspaceSlug, issue.project_id, { name }); + setLocalLabels((prev) => [...prev, created]); + const cur = issue.label_ids ?? []; + if (!cur.includes(created.id)) { + void onPatch(issue, { label_ids: [...cur, created.id] }); + } + setLabelSearch(''); + setOpenDropdownId(null); + } catch (err) { + setCreateLabelError(err instanceof Error ? err.message : 'Failed to create label.'); + } finally { + setCreateLabelLoading(false); + } + }; + + const toggleLabel = (labelId: string) => { + const cur = issue.label_ids ?? []; + const next = cur.includes(labelId) ? cur.filter((x) => x !== labelId) : [...cur, labelId]; + void onPatch(issue, { label_ids: next }); + }; + + const panelClass = + 'max-h-64 min-w-[180px] overflow-auto rounded-md border border-(--border-subtle) bg-(--bg-surface-1) py-1 shadow-(--shadow-raised)'; + + const showModules = Boolean(project?.module_view); + const showCycles = Boolean(project?.cycle_view); + + const moduleLabel = + moduleCount > 1 + ? `${moduleCount} Modules` + : moduleCount === 1 + ? (modules.find((m) => m.id === currentModuleId)?.name ?? '1 Module') + : 'No module'; + + return ( +
e.stopPropagation()} + > + {/* State — StateDropdown border-with-text + dashed */} + } + displayValue="" + align="right" + disabled={busy} + triggerClassName="inline-flex h-7 max-w-[10rem] min-w-0 items-center gap-1 rounded border border-(--border-subtle) bg-(--bg-surface-1) px-2 text-[12px] font-medium text-(--txt-primary) hover:bg-(--bg-layer-1-hover) disabled:opacity-40" + triggerContent={ + <> + + {stateGroupIcon(currentState?.group)} + + {stateName} + + + } + panelClassName={panelClass} + > +
+ setStateSearch(e.target.value)} + className="w-full rounded border border-(--border-subtle) bg-(--bg-surface-1) px-2 py-1 text-xs text-(--txt-primary) placeholder:text-(--txt-placeholder) focus:border-(--border-strong) focus:outline-none" + /> +
+ {filteredStateOptions.length === 0 ? ( +
No states
+ ) : ( + <> + {filteredStateOptions.map((opt) => { + const currentGroup = (currentState?.group ?? 'backlog').toLowerCase(); + const isSelected = + currentGroup === opt.group || + (!!opt.id && issue.state_id === opt.id) || + (!opt.id && !issue.state_id && opt.group === 'backlog'); + return ( + + ); + })} + + )} +
+ + {/* Priority — Plane PriorityDropdown border-without-text */} + } + displayValue="" + align="right" + disabled={busy} + triggerClassName={propBtnSquare} + triggerAriaLabel="Priority" + triggerTitle={`Priority ${PRIORITY_LABELS[pri]}`} + triggerContent={ + + {priorityIcon(pri)} + + } + panelClassName="w-56 overflow-hidden rounded-md border border-(--border-subtle) bg-(--bg-surface-1) shadow-(--shadow-raised)" + > +
+
+ + + + + setPrioritySearch(e.target.value)} + className="w-full bg-transparent text-[13px] text-(--txt-primary) placeholder:text-(--txt-placeholder) focus:outline-none" + /> +
+
+
+ {filteredPriorities.map((p) => { + const selected = pri === p; + return ( + + ); + })} +
+
+ + {/* Labels */} + } + displayValue="" + align="right" + disabled={busy} + triggerClassName={propBtnSquare} + triggerContent={ + + + + } + panelClassName={panelClass} + > +
+ setLabelSearch(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && canCreateLabel && !createLabelLoading) { + e.preventDefault(); + void handleCreateLabel(); + } + }} + className="w-full rounded border border-(--border-subtle) bg-(--bg-surface-1) px-2 py-1 text-xs text-(--txt-primary) placeholder:text-(--txt-placeholder) focus:border-(--border-strong) focus:outline-none" + /> +
+ {filteredLabels.length === 0 ? ( +
+ {canCreateLabel ? 'Press Enter to create label' : 'Type to add a new label'} +
+ ) : ( + filteredLabels.map((l) => { + const on = (issue.label_ids ?? []).includes(l.id); + return ( + + ); + }) + )} + {canCreateLabel ? ( + <> +
+ + {createLabelError ? ( +
+ {createLabelError} +
+ ) : null} + + ) : null} + + + {/* Start date — icon-only + hidden date input (Plane DateDropdown border-without-text) */} +
+ + { + const v = e.target.value; + void onPatch(issue, { start_date: v || null }); + }} + /> +
+ + {/* Due date */} +
+ + { + const v = e.target.value; + void onPatch(issue, { target_date: v || null }); + }} + /> +
+ + {/* Assignee — icon / avatar + chevron in bordered control */} + } + displayValue="" + align="right" + disabled={busy} + triggerClassName="inline-flex h-7 items-center gap-0.5 rounded border border-(--border-subtle) bg-(--bg-surface-1) pl-1 pr-1 hover:bg-(--bg-layer-1-hover) disabled:opacity-40" + triggerContent={ + <> + + {primaryAssigneeId ? ( + + ) : ( + + + + )} + + + + } + panelClassName={panelClass} + > + + {members.map((m) => { + const uid = m.member_id ?? m.id; + const nm = + m.member_display_name?.trim() || m.member_email?.split('@')[0] || uid.slice(0, 8); + return ( + + ); + })} + + + {/* Modules — Plane ModuleDropdown icon */} + {showModules ? ( + } + displayValue="" + align="right" + disabled={busy} + triggerClassName="inline-flex h-7 max-w-[10rem] min-w-0 items-center gap-1 rounded border border-(--border-subtle) bg-(--bg-surface-1) px-2 text-[12px] font-medium text-(--txt-secondary) hover:bg-(--bg-layer-1-hover) disabled:opacity-40" + triggerContent={ + <> + + + + {moduleLabel} + + + } + panelClassName={panelClass} + > + + {modules.map((mod) => ( + + ))} + + ) : null} + + {/* Cycles — Plane CycleDropdown border-with-text */} + {showCycles ? ( + } + displayValue="" + align="right" + disabled={busy} + triggerClassName="inline-flex h-7 max-w-[10rem] min-w-0 items-center gap-1 rounded border border-(--border-subtle) bg-(--bg-surface-1) px-2 text-[12px] font-medium text-(--txt-secondary) hover:bg-(--bg-layer-1-hover) disabled:opacity-40" + triggerContent={ + <> + + + + {cycleName || 'No cycle'} + + + } + panelClassName={panelClass} + > + + {cycles.map((cy) => ( + + ))} + + ) : null} + + {/* Quick actions — ⋯ menu (Plane: Edit, Make a copy, Move to issues, Delete) */} +
+ + {rowMenuOpen ? ( +
+ + + + +
+ ) : null} +
+
+ ); +} diff --git a/ui/src/components/layout/AppShell.tsx b/ui/src/components/layout/AppShell.tsx index 2a277f4..5044135 100644 --- a/ui/src/components/layout/AppShell.tsx +++ b/ui/src/components/layout/AppShell.tsx @@ -9,18 +9,24 @@ export function AppShell() { const { pathname } = useLocation(); const isViewsRoute = pathname.includes('/views'); const isCyclesPage = pathname.endsWith('/cycles'); + const isModulesRoute = pathname.includes('/modules'); + const isDraftsRoute = pathname.includes('/drafts'); return (
-
+
diff --git a/ui/src/components/layout/PageHeader.tsx b/ui/src/components/layout/PageHeader.tsx index a6ac6c9..5993114 100644 --- a/ui/src/components/layout/PageHeader.tsx +++ b/ui/src/components/layout/PageHeader.tsx @@ -165,6 +165,22 @@ const IconChevronDown = () => ( ); +const IconPencil = () => ( + + + +); + const IconChevronUp = () => ( +
+ + + + Drafts +
+
+ + + +
+ + ); +} + function ProjectsHeader({ workspaceSlug }: { workspaceSlug: string }) { const [searchParams, setSearchParams] = useSearchParams(); const searchQuery = searchParams.get('q') ?? ''; @@ -1440,7 +1476,7 @@ function ProjectSectionHeader({ : [...prev, s.key], ); }} - className="rounded border-[var(--border-subtle)]" + className="rounded border-(--border-subtle)" /> {s.label} @@ -1500,7 +1536,7 @@ function ProjectSectionHeader({ : [...prev, p.key], ); }} - className="rounded border-[var(--border-subtle)]" + className="rounded border-(--border-subtle)" /> {p.label} @@ -1561,7 +1597,7 @@ function ProjectSectionHeader({ : [...prev, p.key], ); }} - className="rounded border-[var(--border-subtle)]" + className="rounded border-(--border-subtle)" /> {p.label} @@ -3047,6 +3083,7 @@ export function PageHeader() { const isWorkspaceViewsPage = workspaceSlug && (pathname === `/${workspaceSlug}/views` || pathname.startsWith(`/${workspaceSlug}/views/`)); + const isDraftsPage = workspaceSlug && pathname === `/${workspaceSlug}/drafts`; const projectSection: ProjectSection | null = isIssuesPage ? 'issues' @@ -3075,6 +3112,8 @@ export function PageHeader() { content = ; } else if (isWorkspaceViewsPage && workspaceSlug) { content = ; + } else if (isDraftsPage) { + content = ; } else if (isModuleDetailPage && workspaceSlug && projectId && project && module && moduleId) { content = ( (null); const panelRef = useRef(null); @@ -87,8 +95,11 @@ export function Dropdown({ +
+ ) : ( +
+
    + {drafts.map((issue) => { + const proj = projectById.get(issue.project_id); + const displayId = draftDisplayId(proj, issue); + const busy = rowBusy === issue.id; + const states = statesByProject.get(issue.project_id) ?? []; + const labels = labelsByProject.get(issue.project_id) ?? []; + const modules = modulesByProject.get(issue.project_id) ?? []; + const cycles = cyclesByProject.get(issue.project_id) ?? []; + const issueUrl = `${base}/projects/${issue.project_id}/issues/${issue.id}`; + + return ( +
  • +
    +
    navigate(issueUrl)} + aria-label={`Open draft ${issue.name}`} + > + + {displayId} + + {issue.name} +
    + + + setMenuOpenId((id) => { + const next = id === issue.id ? null : issue.id; + if (next) setPropDropdownId(null); + return next; + }) + } + onEdit={() => handleEdit(issue)} + onDuplicate={() => handleDuplicate(issue)} + onMoveToIssues={() => void handlePublish(issue)} + onDelete={() => void handleDelete(issue)} + /> +
    +
  • + ); + })} +
+ {hasMore ? ( +
+ +
+ ) : null} +
+ )} + + { + setCreateOpen(false); + setCreateError(null); + setEditingIssueId(null); + setModalInitialValues(undefined); + if (searchParams.get('create') === '1') { + searchParams.delete('create'); + setSearchParams(searchParams, { replace: true }); + } + }} + workspaceSlug={workspace.slug} + projects={projects} + defaultProjectId={projects[0]?.id} + initialValues={modalInitialValues} + draftOnly + createError={createError} + onSave={handleCreateSave} + />
); } diff --git a/ui/src/pages/IssueDetailPage.tsx b/ui/src/pages/IssueDetailPage.tsx index a128f41..8bfb69c 100644 --- a/ui/src/pages/IssueDetailPage.tsx +++ b/ui/src/pages/IssueDetailPage.tsx @@ -897,6 +897,7 @@ export function IssueDetailPage() { start_date: data.startDate || undefined, target_date: data.dueDate || undefined, parent_id: issue.id, + is_draft: data.isDraft === true ? true : undefined, }); if (data.cycleId) { await cycleService diff --git a/ui/src/pages/IssueListPage.tsx b/ui/src/pages/IssueListPage.tsx index 21199e2..e43d8a6 100644 --- a/ui/src/pages/IssueListPage.tsx +++ b/ui/src/pages/IssueListPage.tsx @@ -511,6 +511,7 @@ export function IssueListPage() { cycleId?: string | null; moduleId?: string | null; parentId?: string | null; + isDraft?: boolean; }) => { if (!workspaceSlug || !data.title.trim()) return; setCreateError(null); @@ -525,6 +526,7 @@ export function IssueListPage() { start_date: data.startDate || undefined, target_date: data.dueDate || undefined, parent_id: data.parentId || undefined, + is_draft: data.isDraft === true ? true : undefined, }); if (created?.id) { if (data.cycleId) { diff --git a/ui/src/pages/ModuleDetailPage.tsx b/ui/src/pages/ModuleDetailPage.tsx index 2353411..d3b07a7 100644 --- a/ui/src/pages/ModuleDetailPage.tsx +++ b/ui/src/pages/ModuleDetailPage.tsx @@ -344,6 +344,7 @@ export function ModuleDetailPage() { cycleId?: string | null; moduleId?: string | null; parentId?: string | null; + isDraft?: boolean; }) => { if (!workspaceSlug || !data.title.trim() || !resolvedModuleId) return; setCreateError(null); @@ -358,6 +359,7 @@ export function ModuleDetailPage() { start_date: data.startDate || undefined, target_date: data.dueDate || undefined, parent_id: data.parentId || undefined, + is_draft: data.isDraft === true ? true : undefined, }); if (created?.id) { await moduleService.addIssue(workspaceSlug, data.projectId, resolvedModuleId, created.id); diff --git a/ui/src/pages/ViewDetailPage.tsx b/ui/src/pages/ViewDetailPage.tsx index a28a46c..6a514f1 100644 --- a/ui/src/pages/ViewDetailPage.tsx +++ b/ui/src/pages/ViewDetailPage.tsx @@ -750,6 +750,7 @@ export function ViewDetailPage() { cycleId?: string | null; moduleId?: string | null; parentId?: string | null; + isDraft?: boolean; }) => { if (!workspaceSlug || !data.title.trim()) return; setCreateError(null); @@ -764,6 +765,7 @@ export function ViewDetailPage() { start_date: data.startDate || undefined, target_date: data.dueDate || undefined, parent_id: data.parentId || undefined, + is_draft: data.isDraft === true ? true : undefined, }); if (created?.id) { if (data.cycleId) { diff --git a/ui/src/services/issueService.ts b/ui/src/services/issueService.ts index 413c073..f0b70f5 100644 --- a/ui/src/services/issueService.ts +++ b/ui/src/services/issueService.ts @@ -7,6 +7,19 @@ export interface ListIssuesParams { } export const issueService = { + async listWorkspaceDrafts( + workspaceSlug: string, + params?: ListIssuesParams, + ): Promise { + const searchParams = new URLSearchParams(); + if (params?.limit != null) searchParams.set('limit', String(params.limit)); + if (params?.offset != null) searchParams.set('offset', String(params.offset)); + const qs = searchParams.toString(); + const url = `/api/workspaces/${encodeURIComponent(workspaceSlug)}/draft-issues/${qs ? `?${qs}` : ''}`; + const { data } = await apiClient.get(url); + return data; + }, + async list( workspaceSlug: string, projectId: string, @@ -44,7 +57,7 @@ export const issueService = { workspaceSlug: string, projectId: string, issueId: string, - payload: Partial, + payload: Partial, ): Promise { const { data } = await apiClient.patch( `/api/workspaces/${encodeURIComponent(workspaceSlug)}/projects/${encodeURIComponent(projectId)}/issues/${encodeURIComponent(issueId)}/`, diff --git a/ui/src/services/pageService.ts b/ui/src/services/pageService.ts index b2dc8b8..b15862b 100644 --- a/ui/src/services/pageService.ts +++ b/ui/src/services/pageService.ts @@ -1,5 +1,5 @@ import { apiClient } from '../api/client'; -import type { PageApiResponse } from '../api/types'; +import type { CreatePageRequest, PageApiResponse, UpdatePageRequest } from '../api/types'; export const pageService = { async list(workspaceSlug: string, projectId?: string | null): Promise { @@ -9,4 +9,37 @@ export const pageService = { const { data } = await apiClient.get(url); return data; }, + + async create(workspaceSlug: string, payload: CreatePageRequest): Promise { + const { data } = await apiClient.post( + `/api/workspaces/${encodeURIComponent(workspaceSlug)}/pages/`, + payload, + ); + return data; + }, + + async get(workspaceSlug: string, pageId: string): Promise { + const { data } = await apiClient.get( + `/api/workspaces/${encodeURIComponent(workspaceSlug)}/pages/${encodeURIComponent(pageId)}/`, + ); + return data; + }, + + async update( + workspaceSlug: string, + pageId: string, + payload: UpdatePageRequest, + ): Promise { + const { data } = await apiClient.patch( + `/api/workspaces/${encodeURIComponent(workspaceSlug)}/pages/${encodeURIComponent(pageId)}/`, + payload, + ); + return data; + }, + + async delete(workspaceSlug: string, pageId: string): Promise { + await apiClient.delete( + `/api/workspaces/${encodeURIComponent(workspaceSlug)}/pages/${encodeURIComponent(pageId)}/`, + ); + }, };