diff --git a/entities/item.go b/entities/item.go index 6bf49a4..db841f6 100644 --- a/entities/item.go +++ b/entities/item.go @@ -1,6 +1,7 @@ package entities import ( + "git.aiterp.net/stufflog3/stufflog3/internal/genutils" "git.aiterp.net/stufflog3/stufflog3/models" "sort" "strings" @@ -75,23 +76,10 @@ func (item *Item) ApplyUpdate(update models.ItemUpdate) { if update.ClearAcquiredTime { item.AcquiredTime = nil } + if update.RemoveTags != nil || update.AddTags != nil { - item.Tags = append(item.Tags[:0:0], item.Tags...) - for _, tagToRemove := range update.RemoveTags { - for i, tag := range item.Tags { - if strings.EqualFold(tag, tagToRemove) { - { - item.Tags = append(item.Tags[:i], item.Tags[i+1:]...) - break - } - } - } - } - for _, tagToAdd := range update.AddTags { - item.Tags = append(item.Tags, tagToAdd) - } - if len(update.AddTags) > 0 { - sort.Strings(item.Tags) - } + item.Tags = genutils.SliceWithout(item.Tags, update.RemoveTags) + item.Tags = genutils.SliceWithUniques(item.Tags, update.AddTags) + sort.Strings(item.Tags) } } diff --git a/entities/project.go b/entities/project.go index f8e84cc..28029c4 100644 --- a/entities/project.go +++ b/entities/project.go @@ -1,7 +1,9 @@ package entities import ( + "git.aiterp.net/stufflog3/stufflog3/internal/genutils" "git.aiterp.net/stufflog3/stufflog3/models" + "sort" "time" ) @@ -13,6 +15,7 @@ type Project struct { Name string `json:"name"` Description string `json:"description"` Status models.Status `json:"status"` + Tags []string `json:"tags"` } func (project *Project) Update(update models.ProjectUpdate) { @@ -28,6 +31,12 @@ func (project *Project) Update(update models.ProjectUpdate) { if update.Status != nil { project.Status = *update.Status } + + if update.RemoveTags != nil || update.AddTags != nil { + project.Tags = genutils.SliceWithout(project.Tags, update.RemoveTags) + project.Tags = genutils.SliceWithUniques(project.Tags, update.AddTags) + sort.Strings(project.Tags) + } } type Requirement struct { @@ -39,6 +48,7 @@ type Requirement struct { Description string `json:"description"` IsCoarse bool `json:"isCoarse"` Status models.Status `json:"status"` + Tags []string `json:"tags"` } func (requirement *Requirement) Update(update models.RequirementUpdate) { @@ -57,6 +67,12 @@ func (requirement *Requirement) Update(update models.RequirementUpdate) { if update.AggregateRequired != nil { requirement.AggregateRequired = *update.AggregateRequired } + + if update.RemoveTags != nil || update.AddTags != nil { + requirement.Tags = genutils.SliceWithout(requirement.Tags, update.RemoveTags) + requirement.Tags = genutils.SliceWithUniques(requirement.Tags, update.AddTags) + sort.Strings(requirement.Tags) + } } type RequirementStat struct { diff --git a/frontend/src/lib/components/project/ProjectMain.svelte b/frontend/src/lib/components/project/ProjectMain.svelte index ca1717c..8b901f5 100644 --- a/frontend/src/lib/components/project/ProjectMain.svelte +++ b/frontend/src/lib/components/project/ProjectMain.svelte @@ -4,6 +4,7 @@ import Main from "$lib/components/layout/Main.svelte"; import Option from "$lib/components/layout/Option.svelte"; import OptionsRow from "$lib/components/layout/OptionsRow.svelte"; + import TagRow from "../common/TagRow.svelte"; import { getProjectContext } from "../contexts/ProjectContext.svelte"; import Icon from "../layout/Icon.svelte"; import RequirementEntry from "./RequirementSection.svelte"; @@ -14,6 +15,7 @@
+ diff --git a/frontend/src/lib/components/project/RequirementSection.svelte b/frontend/src/lib/components/project/RequirementSection.svelte index c260d1f..c7e3698 100644 --- a/frontend/src/lib/components/project/RequirementSection.svelte +++ b/frontend/src/lib/components/project/RequirementSection.svelte @@ -11,8 +11,8 @@ import ItemEntry from "./ItemSubSection.svelte"; import Icon from "../layout/Icon.svelte"; import { projectPrettyId } from "$lib/utils/prettyIds"; - import AmountRow from "../common/AmountRow.svelte"; -import AggregateAmountRow from "./AggregateAmountRow.svelte"; + import AggregateAmountRow from "./AggregateAmountRow.svelte"; + import TagRow from "../common/TagRow.svelte"; export let requirement: Requirement; @@ -21,6 +21,7 @@ import AggregateAmountRow from "./AggregateAmountRow.svelte";
+ diff --git a/frontend/src/lib/models/project.ts b/frontend/src/lib/models/project.ts index dc134b3..b38afa7 100644 --- a/frontend/src/lib/models/project.ts +++ b/frontend/src/lib/models/project.ts @@ -19,6 +19,7 @@ export interface ProjectEntry { name: string status: number statusName: string + tags: string[] } export interface ProjectInput { @@ -41,6 +42,7 @@ export interface Requirement { aggregateRequired: number stats: StatProgressWithPlanned[] items: Item[] + tags: string[] } export interface RequirementInput { diff --git a/internal/genutils/slice.go b/internal/genutils/slice.go new file mode 100644 index 0000000..a7e5025 --- /dev/null +++ b/internal/genutils/slice.go @@ -0,0 +1,24 @@ +package genutils + +func SliceWithUniques[T comparable](slice []T, elemsToAdd []T) []T { + newSlice := slice[:len(slice):len(slice)] + +addLoop: + for _, elem := range elemsToAdd { + for _, existing := range slice { + if existing == elem { + continue addLoop + } + } + + newSlice = append(newSlice, elem) + } + + return newSlice +} + +func SliceWithout[T comparable](originals []T, elemsToRemove []T) []T { + return Retain(originals, func(v T) bool { + return !Contains(elemsToRemove, v) + }) +} diff --git a/internal/validate/tags.go b/internal/validate/tags.go new file mode 100644 index 0000000..e490442 --- /dev/null +++ b/internal/validate/tags.go @@ -0,0 +1,49 @@ +package validate + +import ( + "fmt" + "git.aiterp.net/stufflog3/stufflog3/internal/genutils" + "git.aiterp.net/stufflog3/stufflog3/models" +) + +func Tags(existingTags, addTags, removeTags []string) error { + for i, tag := range addTags { + if !Tag(tag) { + return models.BadInputError{ + Object: "ItemInput", + Field: "addTags", + Problem: fmt.Sprintf("Invalid tag: %s", tag), + Element: tag, + } + } + + if genutils.Contains(addTags[:i], tag) { + return models.BadInputError{ + Object: "ItemUpdate", + Field: "addTags", + Problem: fmt.Sprintf("The tag %s already exists!", tag), + Element: tag, + } + } + if genutils.Contains(existingTags, tag) { + return models.BadInputError{ + Object: "ItemUpdate", + Field: "addTags", + Problem: fmt.Sprintf("The tag %s already exists!", tag), + Element: tag, + } + } + } + for _, tag := range removeTags { + if !genutils.Contains(existingTags, tag) { + return models.BadInputError{ + Object: "ItemUpdate", + Field: "removeTags", + Problem: fmt.Sprintf("The tag %s does not exist!", tag), + Element: tag, + } + } + } + + return nil +} diff --git a/models/project.go b/models/project.go index 7f90531..59b5288 100644 --- a/models/project.go +++ b/models/project.go @@ -1,16 +1,20 @@ package models type ProjectUpdate struct { - Name *string `json:"name,omitempty"` - OwnerID *string `json:"ownerId,omitempty"` - Status *Status `json:"status,omitempty"` - Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` + OwnerID *string `json:"ownerId,omitempty"` + Status *Status `json:"status,omitempty"` + Description *string `json:"description,omitempty"` + AddTags []string `json:"addTags"` + RemoveTags []string `json:"removeTags"` } type RequirementUpdate struct { - Name *string `json:"name"` - Description *string `json:"description"` - Status *Status `json:"status"` - IsCoarse *bool `json:"isCoarse"` - AggregateRequired *int `json:"aggregateRequired"` + Name *string `json:"name"` + Description *string `json:"description"` + Status *Status `json:"status"` + IsCoarse *bool `json:"isCoarse"` + AggregateRequired *int `json:"aggregateRequired"` + AddTags []string `json:"addTags"` + RemoveTags []string `json:"removeTags"` } diff --git a/ports/httpapi/projects.go b/ports/httpapi/projects.go index 2d30f1f..8079bc7 100644 --- a/ports/httpapi/projects.go +++ b/ports/httpapi/projects.go @@ -5,11 +5,16 @@ import ( "git.aiterp.net/stufflog3/stufflog3/models" "git.aiterp.net/stufflog3/stufflog3/usecases/projects" "github.com/gin-gonic/gin" + "strings" ) func Projects(g *gin.RouterGroup, service *projects.Service) { g.GET("", handler("projects", func(c *gin.Context) (interface{}, error) { - return service.List(c.Request.Context()) + if tagsQuery := c.Query("tags"); tagsQuery != "" { + return service.ListByTags(c.Request.Context(), strings.Split(tagsQuery, ",")) + } else { + return service.List(c.Request.Context()) + } })) g.GET("/:project_id", handler("project", func(c *gin.Context) (interface{}, error) { diff --git a/ports/mysql/db.go b/ports/mysql/db.go index 6c46543..8567fbf 100644 --- a/ports/mysql/db.go +++ b/ports/mysql/db.go @@ -1,6 +1,7 @@ package mysql import ( + "context" "database/sql" "encoding/json" "fmt" @@ -12,6 +13,7 @@ import ( "git.aiterp.net/stufflog3/stufflog3/usecases/scopes" "git.aiterp.net/stufflog3/stufflog3/usecases/sprints" "git.aiterp.net/stufflog3/stufflog3/usecases/stats" + "github.com/Masterminds/squirrel" "time" _ "github.com/go-sql-driver/mysql" @@ -135,3 +137,37 @@ const ( tagObjectKindRequirement tagObjectKindProject ) + +func fetchTags(ctx context.Context, db *sql.DB, kind int, ids []int, cb func(id int, tag string)) error { + query, args, err := squirrel.Select("object_id, tag_name"). + From("tag"). + Where(squirrel.Eq{"object_id": ids, "object_kind": kind}). + OrderBy("tag_name"). + ToSql() + if err != nil { + return err + } + rows, err := db.QueryContext(ctx, query, args...) + if err != nil { + return err + } + for rows.Next() { + var tagStr string + var objectID int + err := rows.Scan(&objectID, &tagStr) + if err != nil { + return err + } + + cb(objectID, tagStr) + } + + if err := rows.Close(); err != nil { + return err + } + if err := rows.Err(); err != nil { + return err + } + + return nil +} diff --git a/ports/mysql/items.go b/ports/mysql/items.go index 50788dd..c806135 100644 --- a/ports/mysql/items.go +++ b/ports/mysql/items.go @@ -221,31 +221,15 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([ ids := genutils.Map(res, func(i entities.Item) int { return i.ID }) - query, args, err := squirrel.Select("object_id, tag_name"). - From("tag"). - Where(squirrel.Eq{"object_id": ids, "object_kind": tagObjectKindItem}). - ToSql() - if err != nil { - return nil, err - } - rows, err = r.db.QueryContext(ctx, query, args...) - if err != nil { - return nil, err - } - for rows.Next() { - var tagStr string - var objectID int - err := rows.Scan(&objectID, &tagStr) - if err != nil { - return nil, err - } - + err = fetchTags(ctx, r.db, tagObjectKindItem, ids, func(id int, tag string) { for i := range res { - if res[i].ID == objectID { - res[i].Tags = append(res[i].Tags, tagStr) - break + if id == res[i].ID { + res[i].Tags = append(res[i].Tags, tag) } } + }) + if err != nil { + return nil, err } sort.Slice(res, func(i, j int) bool { @@ -386,6 +370,14 @@ func (r *itemRepository) Update(ctx context.Context, item entities.Item, update } func (r *itemRepository) Delete(ctx context.Context, item entities.Item) error { + err := r.q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{ + ObjectKind: tagObjectKindItem, + ObjectID: item.ID, + }) + if err != nil { + return err + } + return r.q.DeleteItem(ctx, item.ID) } diff --git a/ports/mysql/projects.go b/ports/mysql/projects.go index 5efb3af..349652d 100644 --- a/ports/mysql/projects.go +++ b/ports/mysql/projects.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "git.aiterp.net/stufflog3/stufflog3/entities" + "git.aiterp.net/stufflog3/stufflog3/internal/genutils" "git.aiterp.net/stufflog3/stufflog3/models" "git.aiterp.net/stufflog3/stufflog3/ports/mysql/mysqlcore" "github.com/Masterminds/squirrel" @@ -24,6 +25,11 @@ func (r *projectRepository) Find(ctx context.Context, scopeID, projectID int) (* return nil, err } + tags, err := r.q.ListTagsByObject(ctx, mysqlcore.ListTagsByObjectParams{ + ObjectKind: tagObjectKindProject, + ObjectID: row.ID, + }) + return &entities.Project{ ID: row.ID, ScopeID: row.ScopeID, @@ -32,6 +38,7 @@ func (r *projectRepository) Find(ctx context.Context, scopeID, projectID int) (* Name: row.Name, Description: row.Description, Status: models.Status(row.Status), + Tags: tags, }, nil } @@ -78,6 +85,8 @@ func (r *projectRepository) FetchProjects(ctx context.Context, scopeID int, ids return nil, err } + project.Tags = []string{} + projects = append(projects, project) } if err := rows.Close(); err != nil { @@ -87,6 +96,18 @@ func (r *projectRepository) FetchProjects(ctx context.Context, scopeID int, ids return nil, err } + // Fill tags + err = fetchTags(ctx, r.db, tagObjectKindProject, ids, func(id int, tag string) { + for i := range projects { + if projects[i].ID == id { + projects[i].Tags = append(projects[i].Tags, tag) + } + } + }) + if err != nil { + return nil, err + } + return projects, nil } @@ -101,6 +122,7 @@ func (r *projectRepository) List(ctx context.Context, scopeID int) ([]entities.P } res := make([]entities.Project, 0, len(rows)) + ids := make([]int, 0, len(rows)) for _, row := range rows { res = append(res, entities.Project{ ID: row.ID, @@ -110,14 +132,74 @@ func (r *projectRepository) List(ctx context.Context, scopeID int) ([]entities.P Name: row.Name, Description: row.Description, Status: models.Status(row.Status), + Tags: []string{}, }) + ids = append(ids, row.ID) + } + + // Fill tags + err = fetchTags(ctx, r.db, tagObjectKindProject, ids, func(id int, tag string) { + for i := range res { + if id == res[i].ID { + res[i].Tags = append(res[i].Tags, tag) + } + } + }) + if err != nil { + return nil, err } return res, nil } +func (r *projectRepository) ListByTags(ctx context.Context, scopeID int, tags []string) ([]entities.Project, error) { + query, args, err := squirrel.Select("object_id, tag_name"). + From("tag"). + Where(squirrel.Eq{"tag_name": tags, "object_kind": tagObjectKindProject}). + ToSql() + if err != nil { + return nil, err + } + rows, err := r.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + ids := make([]int, 0, 16) + matches := make(map[int]int, 64) + for rows.Next() { + var objectID int + var tagName string + err := rows.Scan(&objectID, &tagName) + if err != nil { + return nil, err + } + if matches[objectID] == 0 { + ids = append(ids, objectID) + } + matches[objectID] += 1 + } + + err = rows.Close() + if err != nil { + return nil, err + } + + ids = genutils.RetainInPlace(ids, func(id int) bool { + return matches[id] == len(tags) + }) + + return r.FetchProjects(ctx, scopeID, ids...) +} + func (r *projectRepository) Insert(ctx context.Context, project entities.Project) (*entities.Project, error) { - res, err := r.q.InsertProject(ctx, mysqlcore.InsertProjectParams{ + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + q := r.q.WithTx(tx) + + res, err := q.InsertProject(ctx, mysqlcore.InsertProjectParams{ ScopeID: project.ScopeID, OwnerID: project.OwnerID, Name: project.Name, @@ -134,6 +216,23 @@ func (r *projectRepository) Insert(ctx context.Context, project entities.Project } project.ID = int(id) + + for _, tag := range project.Tags { + err := q.InsertTag(ctx, mysqlcore.InsertTagParams{ + ObjectKind: tagObjectKindProject, + ObjectID: project.ID, + TagName: tag, + }) + if err != nil { + return nil, err + } + } + + err = tx.Commit() + if err != nil { + return nil, err + } + return &project, nil } @@ -174,6 +273,22 @@ func (r *projectRepository) Delete(ctx context.Context, project entities.Project if err != nil { return err } + + err = q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{ + ObjectKind: tagObjectKindRequirement, + ObjectID: req.ID, + }) + if err != nil { + return err + } + } + + err = q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{ + ObjectKind: tagObjectKindProject, + ObjectID: project.ID, + }) + if err != nil { + return err } err = q.DeleteAllProjectRequirements(ctx, project.ID) @@ -226,10 +341,25 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int, return nil, nil, err } + requirement.Tags = []string{} + requirements = append(requirements, requirement) ids = append(ids, requirement.ID) } + // Fill tags + err = fetchTags(ctx, r.db, tagObjectKindRequirement, ids, func(id int, tag string) { + for i := range requirements { + if id == requirements[i].ID { + requirements[i].Tags = append(requirements[i].Tags, tag) + break + } + } + }) + if err != nil { + return nil, nil, err + } + query, args, err = squirrel.Select("project_requirement_id, stat_id, required"). From("project_requirement_stat"). Where(squirrel.Eq{"project_requirement_id": requirementIDs}). @@ -270,6 +400,7 @@ func (r *projectRepository) ListRequirements(ctx context.Context, projectID int) } requirements := make([]entities.Requirement, 0, len(reqRows)) + ids := make([]int, 0, len(reqRows)) for _, row := range reqRows { requirements = append(requirements, entities.Requirement{ ID: row.ID, @@ -280,7 +411,22 @@ func (r *projectRepository) ListRequirements(ctx context.Context, projectID int) IsCoarse: row.IsCoarse, AggregateRequired: row.AggregateRequired, Status: models.Status(row.Status), + Tags: []string{}, }) + ids = append(ids, row.ID) + } + + // Fill tags + err = fetchTags(ctx, r.db, tagObjectKindRequirement, ids, func(id int, tag string) { + for i := range requirements { + if id == requirements[i].ID { + requirements[i].Tags = append(requirements[i].Tags, tag) + break + } + } + }) + if err != nil { + return nil, nil, err } stats := make([]entities.RequirementStat, 0, len(statsRows)) @@ -296,7 +442,14 @@ func (r *projectRepository) ListRequirements(ctx context.Context, projectID int) } func (r *projectRepository) CreateRequirement(ctx context.Context, requirement entities.Requirement) (*entities.Requirement, error) { - res, err := r.q.InsertProjectRequirement(ctx, mysqlcore.InsertProjectRequirementParams{ + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + q := r.q.WithTx(tx) + + res, err := q.InsertProjectRequirement(ctx, mysqlcore.InsertProjectRequirementParams{ ScopeID: requirement.ScopeID, ProjectID: requirement.ProjectID, Name: requirement.Name, @@ -314,6 +467,22 @@ func (r *projectRepository) CreateRequirement(ctx context.Context, requirement e return nil, err } + for _, tag := range requirement.Tags { + err := q.InsertTag(ctx, mysqlcore.InsertTagParams{ + ObjectKind: tagObjectKindRequirement, + ObjectID: int(id), + TagName: tag, + }) + if err != nil { + return nil, err + } + } + + err = tx.Commit() + if err != nil { + return nil, err + } + requirement.ID = int(id) return &requirement, nil } @@ -358,6 +527,14 @@ func (r *projectRepository) DeleteRequirement(ctx context.Context, requirement e return err } + err = q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{ + ObjectKind: tagObjectKindRequirement, + ObjectID: requirement.ID, + }) + if err != nil { + return err + } + return tx.Commit() } diff --git a/usecases/items/service.go b/usecases/items/service.go index 08e3ddd..7787f46 100644 --- a/usecases/items/service.go +++ b/usecases/items/service.go @@ -314,34 +314,9 @@ func (s *Service) Update(ctx context.Context, id int, update models.ItemUpdate, return nil, err } - for _, tag := range update.AddTags { - if !validate.Tag(tag) { - return nil, models.BadInputError{ - Object: "ItemInput", - Field: "addTags", - Problem: fmt.Sprintf("Invalid tag: %s", tag), - Element: tag, - } - } - - if item.HasTag(tag) { - return nil, models.BadInputError{ - Object: "ItemUpdate", - Field: "addTags", - Problem: fmt.Sprintf("The tag %s already exists!", tag), - Element: tag, - } - } - } - for _, tag := range update.RemoveTags { - if !item.HasTag(tag) { - return nil, models.BadInputError{ - Object: "ItemUpdate", - Field: "removeTags", - Problem: fmt.Sprintf("The tag %s does not exist!", tag), - Element: tag, - } - } + err = validate.Tags(item.Tags, update.AddTags, update.RemoveTags) + if err != nil { + return nil, err } if update.RequirementID != nil { diff --git a/usecases/projects/repository.go b/usecases/projects/repository.go index 3d460c8..9edca7f 100644 --- a/usecases/projects/repository.go +++ b/usecases/projects/repository.go @@ -9,6 +9,7 @@ import ( type Repository interface { Find(ctx context.Context, scopeID, projectID int) (*entities.Project, error) List(ctx context.Context, scopeID int) ([]entities.Project, error) + ListByTags(ctx context.Context, scopeID int, tags []string) ([]entities.Project, error) FetchProjects(ctx context.Context, scopeID int, ids ...int) ([]entities.Project, error) Insert(ctx context.Context, project entities.Project) (*entities.Project, error) Update(ctx context.Context, project entities.Project, update models.ProjectUpdate) error diff --git a/usecases/projects/result.go b/usecases/projects/result.go index e8d9676..26a6202 100644 --- a/usecases/projects/result.go +++ b/usecases/projects/result.go @@ -18,6 +18,7 @@ type Entry struct { Status models.Status `json:"status"` StatusName string `json:"statusName"` OwnerName string `json:"ownerName,omitempty"` + Tags []string `json:"tags"` } func generateEntry(project entities.Project, scope scopes.Result) Entry { @@ -27,6 +28,7 @@ func generateEntry(project entities.Project, scope scopes.Result) Entry { CreatedTime: project.CreatedTime, Name: project.Name, Status: project.Status, + Tags: project.Tags, StatusName: scope.StatusName(project.Status), OwnerName: scope.MemberName(project.OwnerID), } @@ -65,6 +67,7 @@ type RequirementResult struct { AggregateRequired int `json:"aggregateRequired"` Stats []RequirementResultStat `json:"stats"` Items []items.Result `json:"items"` + Tags []string `json:"tags"` Requirement entities.Requirement `json:"-"` } @@ -171,6 +174,7 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re AggregateRequired: req.AggregateRequired, Stats: make([]RequirementResultStat, 0, 8), Items: make([]items.Result, 0, 8), + Tags: req.Tags, Requirement: req, } diff --git a/usecases/projects/service.go b/usecases/projects/service.go index d5a7549..e9a7845 100644 --- a/usecases/projects/service.go +++ b/usecases/projects/service.go @@ -4,6 +4,7 @@ import ( "context" "git.aiterp.net/stufflog3/stufflog3/entities" "git.aiterp.net/stufflog3/stufflog3/internal/genutils" + "git.aiterp.net/stufflog3/stufflog3/internal/validate" "git.aiterp.net/stufflog3/stufflog3/models" "git.aiterp.net/stufflog3/stufflog3/usecases/auth" "git.aiterp.net/stufflog3/stufflog3/usecases/items" @@ -66,6 +67,20 @@ func (s *Service) List(ctx context.Context) ([]Entry, error) { return entries, nil } +func (s *Service) ListByTags(ctx context.Context, tags []string) (interface{}, error) { + projects, err := s.Repository.ListByTags(ctx, s.Scopes.Context(ctx).ID, tags) + if err != nil { + return nil, err + } + + entries := make([]Entry, 0, len(projects)) + for _, project := range projects { + entries = append(entries, generateEntry(project, s.Scopes.Context(ctx).Scope)) + } + + return entries, nil +} + func (s *Service) FetchRequirements(ctx context.Context, ids ...int) ([]RequirementResult, error) { sc := s.Scopes.Context(ctx) @@ -111,6 +126,11 @@ func (s *Service) Create(ctx context.Context, project entities.Project) (*Result } } + err := validate.Tags(nil, project.Tags, nil) + if err != nil { + return nil, err + } + // Allow importing and scripts to mess with the created time, so only set it to now if it's not set. if project.CreatedTime.IsZero() { project.CreatedTime = time.Now() @@ -196,6 +216,11 @@ func (s *Service) CreateRequirement(ctx context.Context, id int, requirement ent } } + err := validate.Tags(nil, requirement.Tags, nil) + if err != nil { + return nil, err + } + project, err := s.Find(ctx, id) if err != nil { return nil, err @@ -250,6 +275,11 @@ func (s *Service) UpdateRequirement(ctx context.Context, projectID, requirementI return nil, models.NotFoundError("Requirement") } + err = validate.Tags(req.Tags, update.AddTags, update.RemoveTags) + if err != nil { + return nil, err + } + err = s.Repository.UpdateRequirement(ctx, req.Requirement, update) if err != nil { return nil, err