Skip to content

Commit d4ab03c

Browse files
committed
Workitems limited by workspace
1 parent d87f19e commit d4ab03c

6 files changed

Lines changed: 145 additions & 0 deletions

File tree

internal/restapi/v1/handlers/items.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,19 @@ func (h *ItemHandler) Create(w http.ResponseWriter, r *http.Request) {
237237
return
238238
}
239239

240+
// Check item type is allowed in workspace config set
241+
if req.ItemTypeID != nil && *req.ItemTypeID != 0 {
242+
allowed, checkErr := services.IsItemTypeAllowedInWorkspace(h.db, req.WorkspaceID, *req.ItemTypeID)
243+
if checkErr != nil {
244+
restapi.RespondError(w, r, restapi.ErrInternalError)
245+
return
246+
}
247+
if !allowed {
248+
restapi.RespondError(w, r, restapi.NewAPIError(http.StatusBadRequest, restapi.ErrCodeValidationFailed, "Item type is not allowed in this workspace"))
249+
return
250+
}
251+
}
252+
240253
// Sanitize user input to prevent XSS
241254
req.Title = utils.StripHTMLTags(req.Title)
242255
req.Description = utils.SanitizeCommentContent(req.Description)

internal/restapi/v1/handlers/workspaces.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,48 @@ func (h *WorkspaceHandler) GetStatuses(w http.ResponseWriter, r *http.Request) {
389389

390390
restapi.RespondOK(w, result)
391391
}
392+
393+
// GetItemTypes handles GET /rest/api/v1/workspaces/{id}/item-types
394+
func (h *WorkspaceHandler) GetItemTypes(w http.ResponseWriter, r *http.Request) {
395+
user, ok := requireAuth(w, r)
396+
if !ok {
397+
return
398+
}
399+
400+
wsID, ok := parsePathID(w, r, "id", "workspace ID")
401+
if !ok {
402+
return
403+
}
404+
405+
canView, _ := h.perms.CanViewWorkspace(user.ID, wsID)
406+
if !canView {
407+
restapi.RespondError(w, r, restapi.ErrWorkspaceNotFound)
408+
return
409+
}
410+
411+
types, err := h.workspaceService.GetItemTypes(wsID)
412+
if err != nil {
413+
restapi.RespondError(w, r, restapi.ErrInternalError)
414+
return
415+
}
416+
417+
var result []ItemTypeResponse
418+
for _, t := range types {
419+
result = append(result, ItemTypeResponse{
420+
ID: t.ID,
421+
Name: t.Name,
422+
Description: t.Description,
423+
Icon: t.Icon,
424+
Color: t.Color,
425+
HierarchyLevel: t.HierarchyLevel,
426+
SortOrder: t.SortOrder,
427+
IsDefault: t.IsDefault,
428+
})
429+
}
430+
431+
if result == nil {
432+
result = []ItemTypeResponse{}
433+
}
434+
435+
restapi.RespondOK(w, result)
436+
}

internal/restapi/v1/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func RegisterRoutes(
7272
v1.HandleWithMiddleware("DELETE /workspaces/{id}", workspaceHandler.Delete, router.RequireNumericID)
7373
v1.HandleWithMiddleware("GET /workspaces/{id}/items", workspaceHandler.GetItems, router.RequireNumericID)
7474
v1.HandleWithMiddleware("GET /workspaces/{id}/statuses", workspaceHandler.GetStatuses, router.RequireNumericID)
75+
v1.HandleWithMiddleware("GET /workspaces/{id}/item-types", workspaceHandler.GetItemTypes, router.RequireNumericID)
7576

7677
// ============================================
7778
// Statuses & Status Categories

internal/services/item_validation.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,34 @@ type ItemValidationResult struct {
2727
Error string
2828
}
2929

30+
// IsItemTypeAllowedInWorkspace checks whether the given item type is allowed
31+
// in the workspace's configuration set. Returns true when:
32+
// - the workspace has no configuration set (all types allowed), or
33+
// - the item type appears in configuration_set_item_types for that config set.
34+
func IsItemTypeAllowedInWorkspace(db database.Database, workspaceID, itemTypeID int) (bool, error) {
35+
var configSetID *int
36+
err := db.QueryRow(
37+
"SELECT configuration_set_id FROM workspace_configuration_sets WHERE workspace_id = ?",
38+
workspaceID,
39+
).Scan(&configSetID)
40+
if err == sql.ErrNoRows || configSetID == nil {
41+
return true, nil // no config set → all types allowed
42+
}
43+
if err != nil {
44+
return false, fmt.Errorf("failed to query workspace config set: %w", err)
45+
}
46+
47+
var exists bool
48+
err = db.QueryRow(
49+
"SELECT EXISTS(SELECT 1 FROM configuration_set_item_types WHERE configuration_set_id = ? AND item_type_id = ?)",
50+
*configSetID, itemTypeID,
51+
).Scan(&exists)
52+
if err != nil {
53+
return false, fmt.Errorf("failed to check item type in config set: %w", err)
54+
}
55+
return exists, nil
56+
}
57+
3058
// ValidateItemCreation validates all parameters for creating an item
3159
// Returns a validation result indicating success or failure with error message
3260
func ValidateItemCreation(db database.Database, params ItemValidationParams) *ItemValidationResult {

internal/services/items.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,17 @@ type ItemCreationParams struct {
9999
// CreateItem creates a new item with proper transaction handling and number generation
100100
// This centralizes the item creation logic used by normal creation, portal submissions, and copying
101101
func CreateItem(db database.Database, params ItemCreationParams) (int64, error) {
102+
// Enforce config set item type restrictions before anything else
103+
if params.ItemTypeID != nil && *params.ItemTypeID != 0 {
104+
allowed, err := IsItemTypeAllowedInWorkspace(db, params.WorkspaceID, *params.ItemTypeID)
105+
if err != nil {
106+
return 0, fmt.Errorf("failed to check item type restriction: %w", err)
107+
}
108+
if !allowed {
109+
return 0, fmt.Errorf("item type is not allowed in this workspace")
110+
}
111+
}
112+
102113
now := time.Now()
103114

104115
// Generate fractional index for manual ordering

internal/services/workspace_service.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,53 @@ func (s *WorkspaceService) GetStatuses(workspaceID int) ([]models.Status, error)
326326
return statuses, nil
327327
}
328328

329+
// GetItemTypes retrieves item types available for a workspace via its configuration set.
330+
// If the workspace has a config set with item types defined, only those are returned.
331+
// If no config set exists, all item types are returned.
332+
func (s *WorkspaceService) GetItemTypes(workspaceID int) ([]ItemTypeResult, error) {
333+
rows, err := s.db.Query(`
334+
SELECT DISTINCT it.id, it.name, it.description, it.icon, it.color,
335+
it.hierarchy_level, it.sort_order, it.is_default
336+
FROM item_types it
337+
WHERE NOT EXISTS (
338+
SELECT 1 FROM workspace_configuration_sets wcs
339+
JOIN configuration_set_item_types csit ON wcs.configuration_set_id = csit.configuration_set_id
340+
WHERE wcs.workspace_id = ?
341+
)
342+
OR EXISTS (
343+
SELECT 1 FROM workspace_configuration_sets wcs
344+
JOIN configuration_set_item_types csit ON wcs.configuration_set_id = csit.configuration_set_id
345+
WHERE wcs.workspace_id = ? AND csit.item_type_id = it.id
346+
)
347+
ORDER BY it.hierarchy_level, it.sort_order, it.name
348+
`, workspaceID, workspaceID)
349+
if err != nil {
350+
return nil, fmt.Errorf("failed to get workspace item types: %w", err)
351+
}
352+
defer rows.Close()
353+
354+
var types []ItemTypeResult
355+
for rows.Next() {
356+
var t ItemTypeResult
357+
var description, icon, color sql.NullString
358+
err := rows.Scan(&t.ID, &t.Name, &description, &icon, &color,
359+
&t.HierarchyLevel, &t.SortOrder, &t.IsDefault)
360+
if err != nil {
361+
continue
362+
}
363+
t.Description = description.String
364+
t.Icon = icon.String
365+
t.Color = color.String
366+
types = append(types, t)
367+
}
368+
369+
if types == nil {
370+
types = []ItemTypeResult{}
371+
}
372+
373+
return types, nil
374+
}
375+
329376
// GetRepository returns the underlying workspace repository for advanced operations.
330377
func (s *WorkspaceService) GetRepository() *repository.WorkspaceRepository {
331378
return s.repo

0 commit comments

Comments
 (0)