diff --git a/frontend/src/lib/components/contexts/ModalContext.svelte b/frontend/src/lib/components/contexts/ModalContext.svelte index 756fb63..bb30814 100644 --- a/frontend/src/lib/components/contexts/ModalContext.svelte +++ b/frontend/src/lib/components/contexts/ModalContext.svelte @@ -4,9 +4,14 @@ export type ModalData = | { name: "closed" } | { name: "item.create", requirement?: Requirement } + | { name: "item.edit", item: Item } | { name: "item.acquire", item: Item } + | { name: "item.delete", item: Item } | { name: "requirement.create", project: ProjectEntry } | { name: "requirement.edit", requirement: Requirement } + | { name: "requirement.delete", requirement: Requirement } + | { name: "project.delete", project: Project } + | { name: "stat.delete", stat: Stat } interface ModalContextData { currentModal: Readable @@ -23,7 +28,9 @@ import { writable, type Readable } from "svelte/store"; import { getContext, onMount, setContext } from "svelte"; import type { ProjectEntry, Requirement } from "$lib/models/project"; -import type Item from "$lib/models/item"; + import type Item from "$lib/models/item"; +import type Project from "$lib/models/project"; +import type Stat from "$lib/models/stat"; let store = writable({name: "closed"}); diff --git a/frontend/src/lib/components/contexts/ProjectContext.svelte b/frontend/src/lib/components/contexts/ProjectContext.svelte index cdd0d1f..17496f6 100644 --- a/frontend/src/lib/components/contexts/ProjectContext.svelte +++ b/frontend/src/lib/components/contexts/ProjectContext.svelte @@ -49,8 +49,6 @@ } $: projectWritable.set(project); - - $: console.log($projectWritable); \ No newline at end of file diff --git a/frontend/src/lib/components/contexts/ProjectListContext.svelte b/frontend/src/lib/components/contexts/ProjectListContext.svelte new file mode 100644 index 0000000..2c30e5f --- /dev/null +++ b/frontend/src/lib/components/contexts/ProjectListContext.svelte @@ -0,0 +1,60 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/lib/components/project/ItemEntry.svelte b/frontend/src/lib/components/project/ItemEntry.svelte index ad61aa8..4507e0e 100644 --- a/frontend/src/lib/components/project/ItemEntry.svelte +++ b/frontend/src/lib/components/project/ItemEntry.svelte @@ -17,8 +17,8 @@ {#if item.acquiredTime == null} {/if} - - + + {#if item.acquiredTime != null} diff --git a/frontend/src/lib/components/project/ProjectEntry.svelte b/frontend/src/lib/components/project/ProjectEntry.svelte index 350d016..701db22 100644 --- a/frontend/src/lib/components/project/ProjectEntry.svelte +++ b/frontend/src/lib/components/project/ProjectEntry.svelte @@ -5,7 +5,7 @@ import Option from "$lib/components/layout/Option.svelte"; import OptionsRow from "$lib/components/layout/OptionsRow.svelte"; import { getProjectContext } from "../contexts/ProjectContext.svelte"; -import RequirementEntry from "./RequirementEntry.svelte"; + import RequirementEntry from "./RequirementEntry.svelte"; const {project} = getProjectContext(); @@ -17,7 +17,7 @@ import RequirementEntry from "./RequirementEntry.svelte"; - + {#each $project.requirements as requirement (requirement.id)} diff --git a/frontend/src/lib/components/project/RequirementEntry.svelte b/frontend/src/lib/components/project/RequirementEntry.svelte index 9daf73d..1960a7d 100644 --- a/frontend/src/lib/components/project/RequirementEntry.svelte +++ b/frontend/src/lib/components/project/RequirementEntry.svelte @@ -21,7 +21,7 @@ import ItemEntry from "./ItemEntry.svelte"; - + {#each requirement.stats as stat (stat.id)} diff --git a/frontend/src/lib/components/scope/ProjectMenu.svelte b/frontend/src/lib/components/scope/ProjectMenu.svelte index f3a7f5c..2dd66e1 100644 --- a/frontend/src/lib/components/scope/ProjectMenu.svelte +++ b/frontend/src/lib/components/scope/ProjectMenu.svelte @@ -18,15 +18,15 @@ import MenuItem from "../common/MenuItem.svelte"; import { getScopeContext } from "../contexts/ScopeContext.svelte"; import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds"; + import { getProjectListContext } from "../contexts/ProjectListContext.svelte"; const {scope} = getScopeContext(); - - export let projects: ProjectEntry[]; + const {projects} = getProjectListContext(); let projectsByStatus: Record; let scopeUrlPrefix: string; - $: projectsByStatus = projects.reduce((p, c) => ( + $: projectsByStatus = $projects.reduce((p, c) => ( {...p, [c.status]: [...p[c.status], c]} ), {0: [], 1: [], 2: [], 3: [], 4: [], 5: [], 6: []}); diff --git a/frontend/src/lib/modals/DeletionModal.svelte b/frontend/src/lib/modals/DeletionModal.svelte new file mode 100644 index 0000000..0a697cc --- /dev/null +++ b/frontend/src/lib/modals/DeletionModal.svelte @@ -0,0 +1,151 @@ + + + + +
+ + +

Are you sure you want to delete this {noun.toLocaleLowerCase()}?

+ {#if sideEffects.length > 0} +

There are specific side-effects to this deletion.

+
    + {#each sideEffects as sideEffect} +
  • {sideEffect.op} {sideEffect.name}
  • + {/each} +
+ {/if} + + + {#if dangerZone} + + + {/if} +
+
+
\ No newline at end of file diff --git a/frontend/src/lib/modals/ItemCreateModal.svelte b/frontend/src/lib/modals/ItemCreateModal.svelte index 2e82637..b23b12b 100644 --- a/frontend/src/lib/modals/ItemCreateModal.svelte +++ b/frontend/src/lib/modals/ItemCreateModal.svelte @@ -8,17 +8,23 @@ import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; import AcquiredTimeInput from "$lib/components/controls/AcquiredTimeInput.svelte"; import StatInput from "$lib/components/controls/StatInput.svelte"; + import type Item from "$lib/models/item"; import type { ItemInput } from "$lib/models/item"; import type { Requirement } from "$lib/models/project"; import type Scope from "$lib/models/scope"; + import { formatFormTime } from "$lib/utils/date"; +import { statDiff } from "$lib/utils/stat"; const {currentModal, closeModal} = getModalContext(); const {scope} = getScopeContext(); - const projectCtx = getProjectContext(); + const {project, reloadProject} = getProjectContext(); let item: ItemInput + let itemId: number + let currentStats: ItemInput["stats"] let openedDate: Date let requirementName: string + let op: string let error: string let loading: boolean @@ -26,7 +32,10 @@ $: switch ($currentModal.name) { case "item.create": - init($scope, $currentModal.requirement) + initCreate($scope, $currentModal.requirement) + break; + case "item.edit": + initEdit($currentModal.item) break; default: @@ -35,7 +44,7 @@ show = false; } - function init(scope: Scope, requirement?: Requirement) { + function initCreate(scope: Scope, requirement?: Requirement) { let stats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0})); if (stats == null) { stats = scope.stats.map(s => ({statId: s.id, required: 0, acquired: 0})) @@ -50,22 +59,42 @@ scheduledDate: null, } + op = "Create" openedDate = new Date(); requirementName = requirement?.name; show = true; } + function initEdit(current: Item) { + const req = $project.requirements?.find(r => r.id === current.requirementId); + + item = { + name: current.name, + description: current.description, + requirementId: current.requirementId, + stats: current.stats.map(s => ({statId: s.id, acquired: s.acquired, required: s.required})), + acquiredTime: formatFormTime(current.acquiredTime), + scheduledDate: current.scheduledDate, + }; + itemId = current.id; + currentStats = [...item.stats]; + + op = "Edit" + openedDate = new Date(); + requirementName = req?.name; + show = true; + } + async function submit() { error = null; loading = true; const submission: ItemInput = { ...item, - scheduledDate: item.scheduledDate || void(0), acquiredTime: item.acquiredTime ? new Date(item.acquiredTime).toISOString() : void(0), stats: item.stats.filter(s => s.required > 0).map(s => { if (!!item.acquiredTime) { - return {...s, acquired: s.required} + return s } else { return {...s, acquired: 0} } @@ -73,11 +102,19 @@ } try { - await sl3(fetch).createItem($scope.id, submission); + switch (op) { + case "Create": + await sl3(fetch).createItem($scope.id, submission); + break; + case "Edit": + submission.stats = statDiff(currentStats, submission.stats) + await sl3(fetch).updateItem($scope.id, itemId, submission); + break; + } // Wait for project reload if it's updating a project - if (projectCtx != null && item.requirementId != null) { - await projectCtx.reloadProject(); + if (item.requirementId != null) { + await reloadProject(); } // TODO: History context upsert @@ -97,7 +134,7 @@
- + @@ -112,7 +149,7 @@ {/if} - + \ No newline at end of file diff --git a/frontend/src/lib/utils/date.ts b/frontend/src/lib/utils/date.ts index ef56ec3..42cd901 100644 --- a/frontend/src/lib/utils/date.ts +++ b/frontend/src/lib/utils/date.ts @@ -1,5 +1,5 @@ export function formatFormTime(time: Date | string): string { - if (time === "") { + if (!time) { return ""; } if (!(time instanceof Date)) { diff --git a/frontend/src/routes/[scope=prettyid]/__layout.svelte b/frontend/src/routes/[scope=prettyid]/__layout.svelte index 98616ef..2b23e14 100644 --- a/frontend/src/routes/[scope=prettyid]/__layout.svelte +++ b/frontend/src/routes/[scope=prettyid]/__layout.svelte @@ -26,25 +26,28 @@ import MenuCategory from "$lib/components/common/MenuCategory.svelte"; import MenuItem from "$lib/components/common/MenuItem.svelte"; import { scopePrettyId } from "$lib/utils/prettyIds"; +import ProjectListContext from "$lib/components/contexts/ProjectListContext.svelte"; export let scope: Scope; export let projects: ProjectEntry[]; - - -
{scope.abbreviation}
- - Home - Overview - Sprints - History - - -
- - - -
+ + + +
{scope.abbreviation}
+ + Home + Overview + Sprints + History + + +
+ + + +
+
diff --git a/frontend/src/routes/[scope=prettyid]/projects/[project=prettyid].svelte b/frontend/src/routes/[scope=prettyid]/projects/[project=prettyid].svelte index ff2fa0b..7c5d587 100644 --- a/frontend/src/routes/[scope=prettyid]/projects/[project=prettyid].svelte +++ b/frontend/src/routes/[scope=prettyid]/projects/[project=prettyid].svelte @@ -21,6 +21,7 @@ import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte"; import RequirementCreateModal from "$lib/modals/RequirementCreateModal.svelte"; import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte"; +import DeletionModal from "$lib/modals/DeletionModal.svelte"; export let project: Project; @@ -30,4 +31,5 @@ import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte"; + \ No newline at end of file diff --git a/ports/httpapi/items.go b/ports/httpapi/items.go index fab4d1b..5111b72 100644 --- a/ports/httpapi/items.go +++ b/ports/httpapi/items.go @@ -200,7 +200,10 @@ func Items(g *gin.RouterGroup, items *items.Service) { })) g.PUT("/:item_id", handler("item", func(c *gin.Context) (interface{}, error) { - input := models.ItemUpdate{} + input := struct { + models.ItemUpdate + Stats []entities.ItemStat `json:"stats"` + }{} err := c.BindJSON(&input) if err != nil { return nil, models.BadInputError{ @@ -214,7 +217,7 @@ func Items(g *gin.RouterGroup, items *items.Service) { return nil, err } - return items.Update(c.Request.Context(), id, input) + return items.Update(c.Request.Context(), id, input.ItemUpdate, input.Stats) })) g.PUT("/:item_id/stats/:stat_id", handler("item", func(c *gin.Context) (interface{}, error) { diff --git a/usecases/items/result.go b/usecases/items/result.go index 198877c..76fe372 100644 --- a/usecases/items/result.go +++ b/usecases/items/result.go @@ -26,12 +26,21 @@ func (r *Result) Stat(statID int) *ResultStat { func (r *Result) AddStat(scope scopes.Result, stat entities.ItemStat) { for i, stat2 := range r.Stats { if stat2.ID == stat.StatID { - r.Stats[i].Acquired = stat.Acquired - r.Stats[i].Required = stat.Required + if stat.Required <= 0 { + r.Stats = append(r.Stats[:i], r.Stats[i+1:]...) + } else { + r.Stats[i].Acquired = stat.Acquired + r.Stats[i].Required = stat.Required + } + return } } + if stat.Required <= 0 { + return + } + scopeStat := scope.Stat(stat.StatID) if scopeStat == nil { return diff --git a/usecases/items/service.go b/usecases/items/service.go index 4dd9fdd..c75bcd8 100644 --- a/usecases/items/service.go +++ b/usecases/items/service.go @@ -165,7 +165,7 @@ func (s *Service) Create(ctx context.Context, item entities.Item, stats []entiti return &result, nil } -func (s *Service) Update(ctx context.Context, id int, update models.ItemUpdate) (*Result, error) { +func (s *Service) Update(ctx context.Context, id int, update models.ItemUpdate, stats []entities.ItemStat) (*Result, error) { scope := s.Scopes.Context(ctx).Scope if update.OwnerID != nil { if _, ok := scope.Members[*update.OwnerID]; !ok { @@ -173,7 +173,7 @@ func (s *Service) Update(ctx context.Context, id int, update models.ItemUpdate) } } - if update.Name != nil { + if update.Name != nil && *update.Name == "" { return nil, models.BadInputError{ Object: "ItemUpdate", Field: "name", @@ -209,6 +209,15 @@ func (s *Service) Update(ctx context.Context, id int, update models.ItemUpdate) } item.Item.ApplyUpdate(update) + for _, stat := range stats { + stat.ItemID = item.ID + + err = s.Repository.UpdateStat(ctx, stat) + if err == nil { + item.AddStat(scope, stat) + } + } + return item, nil }