Compare commits

..
37 Commits
Author SHA1 Message Date
gisle 5987b7c8e1 fix sprint sorting.
continuous-integration/drone/push Build is passing
2022-07-13 22:13:47 +02:00
gisle e268c1fd97 add plus for scopes too.
continuous-integration/drone/push Build is passing
2022-07-13 20:16:12 +02:00
gisle 7b7a46eae9 fix scope view button not working in scope overview page.
continuous-integration/drone/push Build is passing
2022-07-13 20:13:58 +02:00
gisle 6825584b8d switch out remaining text option labels with icons.
continuous-integration/drone/push Build is passing
2022-07-13 18:28:03 +02:00
gisle a530bed697 fix front page scope stat ordering.
continuous-integration/drone/push Build is passing
2022-07-13 18:15:24 +02:00
gisle e65349662a fix cookies expiring when the browser is closed.
continuous-integration/drone/push Build is passing
2022-07-13 17:44:00 +02:00
gisle fd5d732851 fix acquire on front page not allowing stat inputs.
continuous-integration/drone/push Build is passing
2022-07-10 18:10:31 +02:00
gisle 9259609efe fix stat input deleting 0-required stats inrequirements.
continuous-integration/drone/push Build is passing
2022-07-10 17:48:15 +02:00
gisle a5b9f85eda fix req progress only counting acquired items.
continuous-integration/drone/push Build is passing
2022-07-10 17:20:56 +02:00
gisle c503b7a30e add front page sprints.
continuous-integration/drone/push Build is passing
2022-07-10 16:30:35 +02:00
gisle b089933188 fix hidden items being unselected in part input. 2022-07-09 17:42:09 +02:00
gisle 2bd1e0a9e3 fix delete on front page, fix multi-list context not updating on acquire/delete 2022-07-09 16:00:06 +02:00
gisle 43b75f77c6 fix item sorting in sprints.
continuous-integration/drone/push Build is passing
2022-07-08 17:44:53 +02:00
gisle c8ca2d4344 add all-scopes item endpoint.
continuous-integration/drone/push Build is passing
2022-07-08 17:34:43 +02:00
gisle 6641d99d41 add project and requirement link to items outside of projects.
continuous-integration/drone/push Build is passing
2022-07-07 19:19:05 +02:00
gisle 396b87eea1 add option to show only active items.
continuous-integration/drone/push Build is passing
2022-07-06 22:13:21 +02:00
gisle 1d59182d43 make overview item sprints even more compact by hiding stats.
continuous-integration/drone/push Build is passing
2022-07-06 22:04:56 +02:00
gisle 951be980c0 change item sprint picker to sort projects and reqs by id instead of name. 2022-07-06 22:01:44 +02:00
gisle 3a8bb9709f improve sprint item picker.
continuous-integration/drone/push Build is passing
2022-07-06 19:56:10 +02:00
gisle 015c0c18b9 add item list on sprint page.
continuous-integration/drone/push Build is passing
2022-07-05 18:36:00 +02:00
gisle ed828b0227 add item-based progress bar on requirements with only 0-required stats.
continuous-integration/drone/push Build is passing
2022-07-04 19:59:38 +02:00
gisle d4ab0a8555 add loose items and better item list handling.
continuous-integration/drone/push Build is passing
2022-07-04 19:30:34 +02:00
gisle 22885734c7 fix project progress being rounded down. 2022-07-04 18:11:51 +02:00
gisle 795d49a942 change mobile/desktop cutoff from 800 to 750px.
continuous-integration/drone/push Build is passing
2022-07-04 18:03:32 +02:00
gisle d083c9b1e8 fix sprint viewing.
continuous-integration/drone/push Build is passing
2022-07-03 19:16:10 +02:00
gisle 0e9b1a4930 tweaked header icons and such to better accomadate japanese text.
continuous-integration/drone/push Build is passing
2022-07-03 19:07:09 +02:00
gisle 6bbbf33c83 make weighted totals render better. 2022-07-03 18:57:02 +02:00
gisle ad53ec5c7e add total weighted for items. 2022-07-03 18:50:44 +02:00
gisle 1195f6ab03 add sorting to project requirements.
continuous-integration/drone/push Build is passing
2022-07-03 17:36:46 +02:00
gisle 2bd68930fe ...and make it somewhat persistent. 2022-07-03 17:28:45 +02:00
gisle 0ef4e38b8a add filtering on history item groups, though it is non-persistent.
continuous-integration/drone/push Build is passing
2022-07-03 17:24:13 +02:00
gisle 48b5c17166 change default stat behavior on item create to check all >0 req stats in requirement.
continuous-integration/drone/push Build is passing
2022-07-03 15:51:28 +02:00
gisle 2fcb6a5d1a allow checking scope/requirement stats on item edits. 2022-07-03 15:45:23 +02:00
gisle 0dfda26c09 fix project progress bars.
continuous-integration/drone/push Build is passing
2022-07-03 15:04:30 +02:00
gisle 701932546d geesed the utf8.
continuous-integration/drone/push Build is passing
2022-07-02 18:57:18 +02:00
gisle 287b18e2e1 blürgh.
continuous-integration/drone/push Build is passing
2022-07-02 16:48:11 +02:00
gisle 75dd79127e blurgh.
continuous-integration/drone/push Build is passing
2022-07-02 16:38:57 +02:00
74 changed files with 1737 additions and 574 deletions
+5
View File
@@ -64,6 +64,7 @@ func main() {
Stats: statsService, Stats: statsService,
Repository: db.Items(), Repository: db.Items(),
RequirementFetcher: db.Projects(), RequirementFetcher: db.Projects(),
ProjectFetcher: db.Projects(),
} }
projectsService := &projects.Service{ projectsService := &projects.Service{
Auth: authService, Auth: authService,
@@ -96,6 +97,10 @@ func main() {
httpapi.Items(apiV1ScopesSub.Group("/items"), itemsService) httpapi.Items(apiV1ScopesSub.Group("/items"), itemsService)
httpapi.Sprints(apiV1ScopesSub.Group("/sprints"), sprintsService) httpapi.Sprints(apiV1ScopesSub.Group("/sprints"), sprintsService)
httpapi.Stats(apiV1ScopesSub.Group("/stats"), statsService) httpapi.Stats(apiV1ScopesSub.Group("/stats"), statsService)
apiV1AllScopes := apiV1.Group("/all-scopes")
apiV1AllScopes.Use(httpapi.AllScopeMiddleware(scopesService, authService))
httpapi.ItemsAllScopes(apiV1AllScopes, itemsService)
httpapi.SprintsAllScopes(apiV1AllScopes.Group("/sprints"), sprintsService)
ginLambda := ginadapter.New(server) ginLambda := ginadapter.New(server)
lambda.Start(func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { lambda.Start(func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
+5
View File
@@ -62,6 +62,7 @@ func main() {
Stats: statsService, Stats: statsService,
Repository: db.Items(), Repository: db.Items(),
RequirementFetcher: db.Projects(), RequirementFetcher: db.Projects(),
ProjectFetcher: db.Projects(),
} }
projectsService := &projects.Service{ projectsService := &projects.Service{
Auth: authService, Auth: authService,
@@ -94,6 +95,10 @@ func main() {
httpapi.Items(apiV1ScopesSub.Group("/items"), itemsService) httpapi.Items(apiV1ScopesSub.Group("/items"), itemsService)
httpapi.Sprints(apiV1ScopesSub.Group("/sprints"), sprintsService) httpapi.Sprints(apiV1ScopesSub.Group("/sprints"), sprintsService)
httpapi.Stats(apiV1ScopesSub.Group("/stats"), statsService) httpapi.Stats(apiV1ScopesSub.Group("/stats"), statsService)
apiV1AllScopes := apiV1.Group("/all-scopes")
apiV1AllScopes.Use(httpapi.AllScopeMiddleware(scopesService, authService))
httpapi.ItemsAllScopes(apiV1AllScopes.Group("/items"), itemsService)
httpapi.SprintsAllScopes(apiV1AllScopes.Group("/sprints"), sprintsService)
exitSignal := make(chan os.Signal) exitSignal := make(chan os.Signal)
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM) signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM)
+1
View File
@@ -13,6 +13,7 @@ declare namespace App {
user?: import("$lib/models/user").default user?: import("$lib/models/user").default
idToken?: string idToken?: string
req?: any req?: any
groupOptions?: import("$lib/utils/items").GroupItemsOptions
amplifyConfig: any amplifyConfig: any
} }
+2
View File
@@ -58,6 +58,8 @@ export const handle: Handle = async({ event, resolve }) => {
for (const key in newCookies) { for (const key in newCookies) {
response.headers.append("Set-Cookie", cookie.serialize(key, newCookies[key], { response.headers.append("Set-Cookie", cookie.serialize(key, newCookies[key], {
path: "/", path: "/",
maxAge: 8640000,
expires: new Date(Date.now() + 8640000000),
})); }));
} }
+8 -4
View File
@@ -73,8 +73,10 @@ export default class SL3APIClient {
return this.fetch<{project: Project}>("PUT", `scopes/${scopeId}/projects/${projectId}`, input).then(r => r.project); return this.fetch<{project: Project}>("PUT", `scopes/${scopeId}/projects/${projectId}`, input).then(r => r.project);
} }
async listItems(scopeId: number, filter: ItemFilter): Promise<Item[]> { async listItems(scopeId: number | "ALL", filter: ItemFilter): Promise<Item[]> {
return this.fetch<{items: Item[]}>("GET", `scopes/${scopeId}/items?filter=${encodeURIComponent(JSON.stringify(filter))}`).then(r => r.items); const scopePrefix = (scopeId !== "ALL") ? `scopes/${scopeId}` : "all-scopes";
return this.fetch<{items: Item[]}>("GET", `${scopePrefix}/items?filter=${encodeURIComponent(JSON.stringify(filter))}`).then(r => r.items);
} }
async createItem(scopeId: number, input: ItemInput): Promise<Item> { async createItem(scopeId: number, input: ItemInput): Promise<Item> {
@@ -97,15 +99,17 @@ export default class SL3APIClient {
return this.fetch<{sprint: Sprint}>("GET", `scopes/${scopeId}/sprints/${sprintId}`).then(r => r.sprint); return this.fetch<{sprint: Sprint}>("GET", `scopes/${scopeId}/sprints/${sprintId}`).then(r => r.sprint);
} }
async listSprints(scopeId: number, interval?: TimeInterval<Date | string>): Promise<Sprint[]> { async listSprints(scopeId: number | "ALL", interval?: TimeInterval<Date | string>): Promise<Sprint[]> {
let qs = ""; let qs = "";
if (interval != null) { if (interval != null) {
const minStr = (interval.min instanceof Date) ? interval.min.toISOString() : interval.min; const minStr = (interval.min instanceof Date) ? interval.min.toISOString() : interval.min;
const maxStr = (interval.max instanceof Date) ? interval.max.toISOString() : interval.max; const maxStr = (interval.max instanceof Date) ? interval.max.toISOString() : interval.max;
qs = `?from=${encodeURIComponent(minStr)}&to=${encodeURIComponent(maxStr)}` qs = `?from=${encodeURIComponent(minStr)}&to=${encodeURIComponent(maxStr)}`
} }
let prefix = (scopeId === "ALL") ? `all-scopes` : `scopes/${scopeId}`
return this.fetch<{sprints: Sprint[]}>("GET", `scopes/${scopeId}/sprints${qs}`).then(r => r.sprints); return this.fetch<{sprints: Sprint[]}>("GET", `${prefix}/sprints${qs}`).then(r => r.sprints);
} }
async createSprint(scopeId: number, input: SprintInput): Promise<Sprint> { async createSprint(scopeId: number, input: SprintInput): Promise<Sprint> {
@@ -1,16 +1,32 @@
<script lang="ts"> <script lang="ts">
import PlusMinus from "../layout/PlusMinus.svelte" import shortestDecimal from "$lib/utils/numbers";
import PlusMinus from "../layout/PlusMinus.svelte"
export let label: string export let label: string
export let value: number export let value: number
export let target: number = null export let target: number = null
export let right: boolean = false;
export let dark: boolean = false;
let valueRounded: number;
let targetRounded: number;
let valueString: string;
let targetString: string;
$: {
valueRounded = Math.round(value * 1000) / 1000;
if (target != null) {
targetRounded = Math.round(target * 1000) / 1000;
}
}
</script> </script>
<div class="amount"> <div class="amount" class:right>
<span class="value">{value}&times;</span> <span class:dark class="value">{shortestDecimal(valueRounded)}&times;</span>
<span class="label">{label}</span> <span class="label">{label}</span>
{ #if target != null && target != value } { #if target != null && targetRounded != valueRounded }
<span class="target">(<PlusMinus offset={value - target} />)</span> <span class="target">(<PlusMinus offset={valueRounded - targetRounded} />)</span>
{ /if } { /if }
</div> </div>
@@ -19,9 +35,14 @@ import PlusMinus from "../layout/PlusMinus.svelte"
div.amount { div.amount {
padding-right: 1ch; padding-right: 1ch;
&.right {
padding-right: 0;
margin-left: auto;
}
} }
span.label { span.label, span.dark {
opacity: $opacity-entry4; opacity: $opacity-entry4;
} }
@@ -8,24 +8,26 @@
<div class="par">(</div> <div class="par">(</div>
<div class="selector"> <div class="selector">
<select value={value} on:change> <select value={value} on:change>
<option value="last:7d">Last 7 days</option> <option selected={value === "last:7d" } value="last:7d">Last 7 days</option>
<option value="last:14d">Last 14 days</option> <option selected={value === "last:14d" } value="last:14d">Last 14 days</option>
<option value="last:30d">Last 30 days</option> <option selected={value === "last:30d" } value="last:30d">Last 30 days</option>
<option value="last:90d">Last 90 days</option> <option selected={value === "last:90d" } value="last:90d">Last 90 days</option>
<option value="last:1m">Last 1 month</option> <option selected={value === "last:1m" } value="last:1m">Last 1 month</option>
<option value="last:3m">Last 3 months</option> <option selected={value === "last:3m" } value="last:3m">Last 3 months</option>
<option value="last:6m">Last 6 months</option> <option selected={value === "last:6m" } value="last:6m">Last 6 months</option>
<option value="last:1y">Last 1 year</option> <option selected={value === "last:1y" } value="last:1y">Last 1 year</option>
<option value="this_week">This week</option> <option selected={value === "this_week" } value="this_week">This week</option>
<option value="next_week">Next week</option> <option selected={value === "next_week" } value="next_week">Next week</option>
<option value="last_week">Last week</option> <option selected={value === "last_week" } value="last_week">Last week</option>
<option value="this_month">This month</option> <option selected={value === "this_month" } value="this_month">This month</option>
<option value="next_month">Next month</option> <option selected={value === "next_month" } value="next_month">Next month</option>
<option value="last_month">Last month</option> <option selected={value === "last_month" } value="last_month">Last month</option>
<option value="all_time">All time</option> <option selected={value === "all_time" } value="all_time">All time</option>
</select> </select>
</div> </div>
<div class="par">)</div> <div class="par">)
<slot></slot>
</div>
</div> </div>
<style lang="sass"> <style lang="sass">
@@ -42,7 +42,7 @@
display: inline; display: inline;
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 749px) {
> div { > div {
display: block; display: block;
} }
@@ -0,0 +1,72 @@
<script lang="ts" context="module">
const contextKey = {ctx: "itemMultiListContext"};
interface ItemMultiListContextData {
lists: Readable<Record<string, Item[]>>,
reloadItemLists(): void,
};
const fallback: ItemMultiListContextData = {
lists: readable({}),
reloadItemLists() {},
}
export function getItemMultiListContext() {
return getContext(contextKey) as ItemMultiListContextData || fallback
}
</script>
<script lang="ts">
import { readable, writable, type Readable } from "svelte/store";
import { getContext, setContext } from "svelte";
import type Item from "$lib/models/item";
import type { ItemFilter } from "$lib/models/item";
import { getScopeContext } from "./ScopeContext.svelte";
import deepEqual from "$lib/utils/object";
import { sl3 } from "$lib/clients/sl3";
const {scope} = getScopeContext();
export let lists: Record<string, Item[]>
export let filters: Record<string, ItemFilter>
export let global: boolean = false;
let loaded: Record<string, ItemFilter> = {...filters};
let loading: Record<string, boolean> = {};
const listsWritable = writable<Record<string, Item[]>>({...lists});
function reloadItemLists() {
loaded = {};
}
async function runLoad(key: string, filter: ItemFilter) {
loading = {...loading, [key]: true};
try {
const items = await sl3(fetch).listItems(global ? "ALL" : $scope.id, filter)
listsWritable.update(l => ({...l, [key.replace("Filter", "Items")]: items}));
} catch(_err) {
// TODO: Use err
}
loading = {...loading, [key]: false};
loaded = {...loaded, [key]: filter};
}
setContext<ItemMultiListContextData>(contextKey, {
lists: {subscribe: listsWritable.subscribe},
reloadItemLists,
});
$: for (const key in filters) {
if (loading[key]) {
continue;
}
if (loaded[key] == null || !deepEqual(filters[key], loaded[key])) {
runLoad(key, {...filters[key]})
}
}
</script>
<slot></slot>
@@ -7,7 +7,7 @@
}; };
const fallback: ProjectContextData = { const fallback: ProjectContextData = {
project: writable({} as Project), project: readable({requirements: []} as Project),
reloadProject: () => Promise.resolve() reloadProject: () => Promise.resolve()
}; };
@@ -18,7 +18,7 @@
<script lang="ts"> <script lang="ts">
import type Project from "$lib/models/project"; import type Project from "$lib/models/project";
import { writable, type Readable } from "svelte/store"; import { readable, writable, type Readable } from "svelte/store";
import { getContext, setContext } from "svelte"; import { getContext, setContext } from "svelte";
import { sl3 } from "$lib/clients/sl3"; import { sl3 } from "$lib/clients/sl3";
import { getScopeContext } from "./ScopeContext.svelte"; import { getScopeContext } from "./ScopeContext.svelte";
@@ -10,8 +10,8 @@
}; };
const fallback: ScopeContextData = { const fallback: ScopeContextData = {
scope: readable({} as Scope), scope: readable({stats: []} as Scope),
lastHistoryRange: writable(""), lastHistoryRange: writable("last:14d"),
upsertStat: () => {}, upsertStat: () => {},
deleteStat: () => {}, deleteStat: () => {},
updateScope: () => {}, updateScope: () => {},
@@ -27,6 +27,7 @@
export let sprints: Sprint[]; export let sprints: Sprint[];
export let intervalString: string; export let intervalString: string;
export let global: boolean = false;
const {scope} = getScopeContext(); const {scope} = getScopeContext();
const {now} = getTimeContext(); const {now} = getTimeContext();
@@ -46,7 +47,7 @@
} }
try { try {
const newSprints = await sl3(fetch).listSprints($scope.id, parseInterval(intervalString, $now)); const newSprints = await sl3(fetch).listSprints(global ? "ALL" : $scope.id, parseInterval(intervalString, $now));
sprintsWritable.set(newSprints); sprintsWritable.set(newSprints);
} catch(_) {} } catch(_) {}
@@ -1,69 +1,146 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
export interface PartOptionGroup {
id: number
label: string
subs: PartOptionGroup[]
options: PartOption[]
}
export interface PartOption { export interface PartOption {
partId: number partId: number
name: string name: string
enabled: boolean enabled: boolean
required: number required: number
} }
const SO = [
Status.Active, Status.Background, Status.Available, Status.Blocked,
Status.Completed, Status.Failed, Status.Dropped,
]
function compareStatus<T extends {status: Status, id: number}>(a: T, b: T): number {
if (a.status === b.status) {
return a.id - b.id;
}
return SO.indexOf(a.status) - SO.indexOf(b.status)
}
</script> </script>
<script lang="ts"> <script lang="ts">
import { browser } from "$app/env"; import { browser } from "$app/env";
import { sl3 } from "$lib/clients/sl3"; import { sl3 } from "$lib/clients/sl3";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import type Scope from "$lib/models/scope";
import { SprintKind, type SprintInputPart } from "$lib/models/sprint"; import { SprintKind, type SprintInputPart } from "$lib/models/sprint";
import Status from "$lib/models/status";
import shortestDecimal from "$lib/utils/numbers";
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
import Checkbox from "../layout/Checkbox.svelte"; import Checkbox from "../layout/Checkbox.svelte";
export let value: SprintInputPart[] = []; export let value: SprintInputPart[] = [];
export let kind: SprintKind; export let kind: SprintKind;
export let scopeId: number;
const {scope} = getScopeContext(); const {scope} = getScopeContext();
const {scopes} = getScopeListContext();
let error = ""; let error = "";
let options: PartOption[] = []; let groups: PartOptionGroup[] = [];
let showRequired = false; let showRequired = false;
let loading = false; let loading = false;
let onlyActive = true;
let loaded: SprintKind = SprintKind.Invalid let loaded: SprintKind = SprintKind.Invalid
let hiddenSelections: SprintInputPart[] = [];
let actualScope: Scope;
async function load(kind: SprintKind) { function forceReload() {
loaded = SprintKind.Invalid;
}
async function load(kind: SprintKind, scope: Scope) {
loading = true; loading = true;
try { try {
switch (kind) { switch (kind) {
case SprintKind.Items: { case SprintKind.Items: {
const items = await sl3(fetch).listItems($scope.id, {}); const items = await sl3(fetch).listItems(scope.id, {unAcquired: onlyActive});
options = items.map(i => ({ const projects = items.filter(i => i.project != null).map(i => i.project).sort(compareStatus).filter((p, i, a) => p.id != a[i-1]?.id);
enabled: false, const requirements = items.filter(i => i.requirement != null).map(i => i.requirement).sort(compareStatus).filter((r, i, a) => r.id != a[i-1]?.id);
name: `${i.name}${i.acquiredTime?" ✓":""}`, const loose = items.filter(i => i.requirementId == null);
partId: i.id, const reqProjectIdMap = items.filter(i => i.projectId != null && i.requirementId != null).reduce((p, c) => ({
required: 0, ...p,
[c.requirementId]: c.projectId,
}), {} as Record<number, string>)
groups = projects.filter(p => !onlyActive || p.status === Status.Active || p.status === Status.Background).map(p => ({
id: p.id,
label: `${p.name} (${p.statusName})`,
options: [],
subs: requirements.filter(r => !onlyActive || r.status === Status.Active || r.status === Status.Background).filter(r => reqProjectIdMap[r.id] === p.id).map(r => ({
id: r.id,
label: `${r.name} (${r.statusName})`,
options: items.filter(i => i.requirementId === r.id).map(i => ({
enabled: false,
partId: i.id,
name: `${i.name} (${shortestDecimal(i.weightedRequired)})${!!i.acquiredTime?" ✓":""}`,
required: 0,
})).sort((a,b) => a.partId - b.partId),
subs: [],
}))
})) }))
groups.push({id: -1, label: "Loose items", subs: [], options: loose.map(i => ({
enabled: false,
partId: i.id,
name: `${i.name} (${shortestDecimal(i.weightedRequired)})${!!i.acquiredTime?" ✓":""}`,
required: 0,
})).sort((a,b) => a.partId - b.partId)})
const allParts = groups.flatMap(g => [...g.subs.flatMap(g => g.options), ...g.options]);
hiddenSelections = value.filter(v => !allParts.find(p => p.partId === v.partId));
showRequired = false; showRequired = false;
break; break;
} }
case SprintKind.Requirements: { case SprintKind.Requirements: {
options = []; groups = [];
throw "Requirement selection is not yet supported the UI." throw "Requirement selection is not yet supported the UI."
} }
case SprintKind.Stats: { case SprintKind.Stats: {
options = $scope.stats.map(s => ({partId: s.id, name: s.name, enabled: false, required: 0})); groups = [{
id: -1,
label: "",
subs: [],
options: scope.stats.map(s => ({partId: s.id, name: s.name, enabled: false, required: 0})),
}];
showRequired = true; showRequired = true;
break; break;
} }
case SprintKind.Scope: { case SprintKind.Scope: {
options = []; groups = [];
showRequired = false; showRequired = false;
break; break;
} }
} }
error = ""; error = "";
for (const opt of options) { for (const group of groups) {
const val = value.find(v => v.partId === opt.partId); for (const opt of group.options) {
if (val != null) { const val = value.find(v => v.partId === opt.partId);
opt.enabled = true; if (val != null) {
opt.required = val.required; opt.enabled = true;
opt.required = val.required;
}
}
for (const subGroup of group.subs) {
for (const opt of subGroup.options) {
const val = value.find(v => v.partId === opt.partId);
if (val != null) {
opt.enabled = true;
opt.required = val.required;
}
}
} }
} }
} catch(err) { } catch(err) {
@@ -74,26 +151,55 @@
loading = false; loading = false;
} }
$: if (loaded === kind && error === "") { // Hax to make it reload if onlyActive changed.
value = options.filter(o => o.enabled).map(o => ({partId: o.partId, required: o.required})) $: if (onlyActive != null) {
forceReload();
} }
$: if (loaded === kind && error === "") {
value = [
...hiddenSelections,
...groups.flatMap(g => [...g.subs.flatMap(g => g.options), ...g.options]).filter(o => o.enabled).map(o => ({partId: o.partId, required: o.required})),
]
}
$: actualScope = ($scope.id === scopeId || scopeId == null) ? $scope : $scopes.find(s => s.id === scopeId);
$: if (browser && !loading && loaded !== kind) { $: if (browser && !loading && loaded !== kind) {
load(kind); load(kind, actualScope);
} }
</script> </script>
<div class="part-input"> <div class="part-input">
<div class="error">{error}</div> <div class="error">{error}</div>
{#each options as option (option.partId)} {#each groups as group (group.id)}
<div class="part"> <div class="label">{group.label}</div>
<Checkbox noLabel bind:checked={option.enabled} /> {#each group.subs as group (group.id)}
<div class="name" class:disabled={!option.enabled}>{option.name}</div> <div class="label label-2">{group.label}</div>
{#if showRequired && option.enabled} {#each group.options as option (option.partId)}
<input type="number" class="required" bind:value={option.required} /> <div class="part">
{/if} <Checkbox noLabel bind:checked={option.enabled} />
</div> <div class="name" class:disabled={!option.enabled}>{option.name}</div>
{#if showRequired && option.enabled}
<input type="number" class="required" bind:value={option.required} />
{/if}
</div>
{/each}
{/each}
{#each group.options as option (option.partId)}
<div class="part">
<Checkbox noLabel bind:checked={option.enabled} />
<div class="name" class:disabled={!option.enabled}>{option.name}</div>
{#if showRequired && option.enabled}
<input type="number" class="required" bind:value={option.required} />
{/if}
</div>
{/each}
{/each} {/each}
{#if kind === SprintKind.Items}
<hr />
<Checkbox label="Show only active items" bind:checked={onlyActive} />
{/if}
</div> </div>
<style lang="scss"> <style lang="scss">
@@ -103,7 +209,6 @@
display: flex; display: flex;
margin: 0; margin: 0;
padding: 0; padding: 0;
border-top: 1px solid $color-entry1;
&:first-of-type { &:first-of-type {
border-top: none; border-top: none;
@@ -123,7 +228,7 @@
} }
div { div {
padding: 0.3em 0.5ch; padding: 0.25em 0.5ch;
&.disabled { &.disabled {
color: $color-entry2; color: $color-entry2;
@@ -131,11 +236,37 @@
} }
} }
hr {
margin-top: 2em;
border: 0;
border-bottom: 1px solid $color-entry2;
}
div.label {
margin-top: 0.5em;
border-bottom: 1px solid $color-entry2;
&:empty {
margin-top: 0;
border-bottom: none;
}
&.label-2 {
margin-top: 1em;
font-size: 0.75em;
border-bottom: none;
}
&+div.label-2 {
margin-top: 0.5em;
}
}
div.part-input { div.part-input {
width: calc(100% - 2ch); width: calc(100% - 2ch);
margin-bottom: 1em; margin-bottom: 1em;
margin-top: 0.20em; margin-top: 0.20em;
height: 12em; height: 26em;
overflow-y: auto; overflow-y: auto;
background: $color-entryhalf; background: $color-entryhalf;
@@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
import type { StatValueInput } from "$lib/models/stat"; import type { StatValueInput } from "$lib/models/stat";
import Checkbox from "../layout/Checkbox.svelte"; import Checkbox from "../layout/Checkbox.svelte";
import type Scope from "$lib/models/scope";
export let value: StatValueInput[] = []; export let value: StatValueInput[] = [];
export let showAcquired = false; export let showAcquired = false;
@@ -9,50 +11,58 @@
export let hideUnseen = false; export let hideUnseen = false;
export let zeroDeletes = false; export let zeroDeletes = false;
export let noToggle = false; export let noToggle = false;
export let initOnList: number[] = [];
export let overrideScopeId: number = null;
const {scope} = getScopeContext();
const {scopes} = getScopeListContext();
let acquiredMap: Record<number, number>; let acquiredMap: Record<number, number>;
let requiredMap: Record<number, number>; let requiredMap: Record<number, number>;
let enabledMap: Record<number, boolean>; let enabledMap: Record<number, boolean>;
let actualScope: Scope;
function initialize() { function initialize(actualScope: Scope) {
acquiredMap = {}; acquiredMap = {};
requiredMap = {}; requiredMap = {};
enabledMap = {}; enabledMap = {};
if (hideUnseen) { if (hideUnseen) {
for (const v of value) { for (const v of value) {
enabledMap[v.statId] = zeroDeletes ? v.required > 0 : v.required >= 0; enabledMap[v.statId] = initOnList.includes(v.statId) || (zeroDeletes ? v.required > 0 : v.required >= 0);
requiredMap[v.statId] = v.required; requiredMap[v.statId] = v.required;
acquiredMap[v.statId] = v.acquired; acquiredMap[v.statId] = v.acquired;
} }
} else { } else {
for (const stat of $scope.stats) { for (const stat of actualScope.stats) {
const found = value.find(v => v.statId == stat.id); const found = value.find(v => v.statId == stat.id);
enabledMap[stat.id] = found != null; enabledMap[stat.id] = found != null && (found?.required > (zeroDeletes ? 0 : -1));
requiredMap[stat.id] = found?.required || 0; requiredMap[stat.id] = found?.required || 0;
acquiredMap[stat.id] = found?.acquired || 0; acquiredMap[stat.id] = found?.acquired || 0;
} }
} }
} }
$: actualScope = overrideScopeId !== null && $scopes.length > 0 ? $scopes.find(s => s.id === overrideScopeId) : $scope;
$: { $: {
if (acquiredMap == null) { if (acquiredMap == null) {
initialize(); initialize(actualScope);
} }
} }
$: value = $scope.stats.filter(s => enabledMap[s.id]).map(s => ({ $: value = actualScope.stats.filter(s => enabledMap[s.id]).map(s => ({
statId: s.id, statId: s.id,
required: requiredMap[s.id], required: requiredMap[s.id],
acquired: acquiredMap[s.id], acquired: acquiredMap[s.id],
})); }));
const {scope} = getScopeContext(); $: console.log(value);
</script> </script>
<div class="stat-input"> <div class="stat-input">
{#each $scope.stats as stat (stat.id)} {#each actualScope.stats as stat (stat.id)}
{#if enabledMap[stat.id] != null} {#if enabledMap[stat.id] != null}
<div class="stat"> <div class="stat">
{#if !noToggle} {#if !noToggle}
@@ -19,10 +19,10 @@
<style lang="sass"> <style lang="sass">
div.desktop div.desktop
@media screen and (max-width: 800px) @media screen and (max-width: 749px)
display: none display: none
div.mobile div.mobile
@media screen and (min-width: 800px) @media screen and (min-width: 750px)
display: none display: none
</style> </style>
@@ -0,0 +1,33 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import Icon, { type IconName } from "../layout/Icon.svelte";
export let name: IconName;
export let value: boolean;
export let inverse: boolean = false;
const dispatcher = createEventDispatcher();
function onClick() {
value = !value;
dispatcher("click");
}
</script>
<span on:click={onClick} class="toggle-icon" class:checked={value !== inverse}>
<Icon name={name} />
</span>
<style lang="sass">
@import "../../css/colors"
.toggle-icon
font-size: 0.666em
color: $color-entry1
user-select: none
-moz-user-select: none
-webkit-user-select: none
&.checked
color: $color-entry7
</style>
@@ -14,6 +14,6 @@
flex-basis: 50 flex-basis: 50
&.hideMobile &.hideMobile
@media screen and (max-width: 800px) @media screen and (max-width: 749px)
display: none display: none
</style> </style>
@@ -25,7 +25,7 @@
&.fullwidth &.fullwidth
width: 100% width: 100%
@media screen and (max-width: 800px) @media screen and (max-width: 749px)
width: 100% width: 100%
flex-direction: column flex-direction: column
@@ -42,6 +42,7 @@
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown"; import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash"; import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
import { faCheckToSlot } from "@fortawesome/free-solid-svg-icons/faCheckToSlot"; import { faCheckToSlot } from "@fortawesome/free-solid-svg-icons/faCheckToSlot";
import { faEye } from "@fortawesome/free-solid-svg-icons/faEye";
const icons = { const icons = {
"clock": faClock, "clock": faClock,
@@ -66,6 +67,7 @@
"chevron_down": faChevronDown, "chevron_down": faChevronDown,
"trash": faTrash, "trash": faTrash,
"check_slot": faCheckToSlot, "check_slot": faCheckToSlot,
"eye": faEye,
}; };
export type IconName = keyof typeof icons; export type IconName = keyof typeof icons;
@@ -1,9 +1,12 @@
<script lang="ts"> <script lang="ts">
import shortestDecimal from "$lib/utils/numbers";
export let offset: number; export let offset: number;
let value: string; let value: string;
let negative: boolean let negative: boolean
$: value = (offset > 0) ? `+${offset.toFixed()}` : offset.toFixed(); $: value = (offset > 0) ? `+${shortestDecimal(offset)}` : shortestDecimal(offset);
$: negative = offset < 0 $: negative = offset < 0
</script> </script>
@@ -11,9 +11,10 @@
export let noProgress = false; export let noProgress = false;
export let icon: IconName = null; export let icon: IconName = null;
export let status: Status = null; export let status: Status = null;
export let anchor: string = "";
</script> </script>
<div class="sl3-entry"> <div id={anchor} class="sl3-entry">
<div class="header"> <div class="header">
<div class="entry-icon"> <div class="entry-icon">
{#if icon != null} {#if icon != null}
@@ -44,33 +45,20 @@
opacity: 0.6 opacity: 0.6
margin-right: 0.5ch margin-right: 0.5ch
font-size: 1em font-size: 1em
margin-top: 0.225em margin-top: 0.15em
&:empty &:empty
display: none display: none
div.header div.header
display: flex display: flex
line-height: 1.25em
h2
font-size: 1.25em
margin: 0
color: $color-entry10
span.sub
color: $color-entry5
&.big
margin-top: 1em
font-size: 2em
&.small
font-size: 1em
div.right div.right
color: $color-entry2-transparent color: $color-entry2-transparent
margin-left: auto margin-left: auto
margin-top: auto margin-top: auto
margin-bottom: 0
&.noProgress &.noProgress
margin-top: -0.25em margin-top: -0.25em
@@ -43,6 +43,7 @@
div.sub-section div.sub-section
margin: 0.5em 0.4ch margin: 0.5em 0.4ch
padding: 0.33em 0.75ch padding: 0.33em 0.75ch
padding-bottom: 0.25em
background: $color-entry1-transparent background: $color-entry1-transparent
border-radius: 0.125em border-radius: 0.125em
border-bottom-right-radius: 0.5em border-bottom-right-radius: 0.5em
@@ -9,22 +9,27 @@
import AmountRow from "../common/AmountRow.svelte"; import AmountRow from "../common/AmountRow.svelte";
import type { IconName } from "../layout/Icon.svelte"; import type { IconName } from "../layout/Icon.svelte";
import Icon from "../layout/Icon.svelte"; import Icon from "../layout/Icon.svelte";
import { formatTime } from "$lib/utils/date"; import { formatTime } from "$lib/utils/date";
import RequirementReference from "./RequirementReference.svelte";
export let item: Item; export let item: Item;
export let event: "created" | "scheduled" | "acquired" | "none" = "none"; export let event: "created" | "scheduled" | "acquired" | "none" = "none";
export let compact = false; export let compact = false;
export let showAcquiredTime = false;
export let hideStats = false;
let icon: IconName let icon: IconName
let status: Status let status: Status
let eventText: string let eventText: string
$: { $: {
icon = void(0);
if (event === "none" || event === "acquired") { if (event === "none" || event === "acquired") {
if (item.acquiredTime) { if (item.acquiredTime) {
icon = "check"; icon = "check";
status = Status.Completed; status = Status.Completed;
eventText = event === "acquired" ? formatTime(item.acquiredTime) : void(0) eventText = (showAcquiredTime || event === "acquired") ? formatTime(item.acquiredTime) : void(0)
} }
} else if (event === "scheduled") { } else if (event === "scheduled") {
if (item.acquiredTime) { if (item.acquiredTime) {
@@ -33,9 +38,9 @@ import { formatTime } from "$lib/utils/date";
eventText = "scheduled" eventText = "scheduled"
} }
} else if (event === "created") { } else if (event === "created") {
icon = "lightbulb"; icon = "lightbulb";
status = Status.Available; status = Status.Available;
eventText = formatTime(item.acquiredTime) eventText = formatTime(item.createdTime)
} }
} }
</script> </script>
@@ -50,22 +55,33 @@ import { formatTime } from "$lib/utils/date";
<Option open={{name: "item.delete", item}} color="red"><Icon name="trash" /></Option> <Option open={{name: "item.delete", item}} color="red"><Icon name="trash" /></Option>
{/if} {/if}
</OptionsRow> </OptionsRow>
{#if item.project != null}
<RequirementReference project={item.project} requirement={item.requirement} scopeId={item.scopeId} />
{/if}
{#if !compact} {#if !compact}
<Markdown source={item.description} /> <Markdown source={item.description} />
{/if} {/if}
<AmountRow> <AmountRow>
{#if item.acquiredTime != null} {#if !hideStats}
{#each item.stats as stat (stat.id)} {#if item.acquiredTime != null}
{#if stat.required > 0} {#each item.stats as stat (stat.id)}
<Amount label={stat.name} value={stat.acquired} target={stat.required} /> {#if stat.required > 0}
<Amount label={stat.name} value={stat.acquired} target={stat.required} />
{/if}
{/each}
{#if item.weightedRequired}
<Amount dark right label="" value={item.weightedAcquired} target={item.weightedRequired} />
{/if} {/if}
{/each} {:else}
{:else} {#each item.stats as stat (stat.id)}
{#each item.stats as stat (stat.id)} {#if stat.required > 0}
{#if stat.required > 0} <Amount label={stat.name} value={stat.required} />
<Amount label={stat.name} value={stat.required} /> {/if}
{/each}
{#if item.weightedRequired}
<Amount dark right label="" value={item.weightedRequired} />
{/if} {/if}
{/each} {/if}
{/if} {/if}
</AmountRow> </AmountRow>
</SubSection> </SubSection>
@@ -5,6 +5,7 @@
import Option from "$lib/components/layout/Option.svelte"; import Option from "$lib/components/layout/Option.svelte";
import OptionsRow from "$lib/components/layout/OptionsRow.svelte"; import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
import { getProjectContext } from "../contexts/ProjectContext.svelte"; import { getProjectContext } from "../contexts/ProjectContext.svelte";
import Icon from "../layout/Icon.svelte";
import RequirementEntry from "./RequirementSection.svelte"; import RequirementEntry from "./RequirementSection.svelte";
const {project} = getProjectContext(); const {project} = getProjectContext();
@@ -15,9 +16,9 @@
<Progress alwaysSmooth titlePercentageOnly thinner gray count={$project.totalPlanned} target={$project.totalRequired} /> <Progress alwaysSmooth titlePercentageOnly thinner gray count={$project.totalPlanned} target={$project.totalRequired} />
<Markdown source={$project.description} /> <Markdown source={$project.description} />
<OptionsRow slot="right"> <OptionsRow slot="right">
<Option open={{name: "requirement.create", project: $project}}>Add Requirement</Option> <Option open={{name: "requirement.create", project: $project}}><Icon name="plus" /></Option>
<Option open={{name: "project.edit", project: $project}}>Edit</Option> <Option open={{name: "project.edit", project: $project}}><Icon name="pen" /></Option>
<Option open={{name: "project.delete", project: $project}} color="red">Delete</Option> <Option open={{name: "project.delete", project: $project}} color="red"><Icon name="trash" /></Option>
</OptionsRow> </OptionsRow>
{#each $project.requirements as requirement (requirement.id)} {#each $project.requirements as requirement (requirement.id)}
<RequirementEntry requirement={requirement} /> <RequirementEntry requirement={requirement} />
@@ -0,0 +1,57 @@
<script lang="ts" context="module">
export interface IDAndName {
id: number
name: string
}
</script>
<script lang="ts">
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds";
import { getScopeContext } from "../contexts/ScopeContext.svelte";
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
export let project: IDAndName
export let requirement: IDAndName
export let scopeId: number
const {scope} = getScopeContext();
const {scopes} = getScopeListContext();
let projectLink: string
let requirementLink: string
$: {
let actualScope = $scope;
if (actualScope == null || actualScope.id !== scopeId) {
actualScope = $scopes.find(s => s.id === scopeId);
}
projectLink = `/${scopePrettyId(actualScope)}/projects/${projectPrettyId(project)}`
requirementLink = `${projectLink}#${projectPrettyId(requirement)}`
}
</script>
<div class="reference">
<a href={projectLink}>{project.name}</a>
<span>&gt;</span>
<a href={requirementLink}>{requirement.name}</a>
</div>
<style lang="sass">
@import "../../css/colors"
div.reference
font-size: 0.8em
> span
color: $color-entry3
> a
color: $color-entry5
text-decoration: none
line-height: 1em
transition: 250ms
&:hover
color: $color-entry8
</style>
@@ -9,18 +9,21 @@
import LabeledProgressRow from "../common/LabeledProgressRow.svelte"; import LabeledProgressRow from "../common/LabeledProgressRow.svelte";
import { STATUS_ICONS } from "../common/StatusIcon.svelte"; import { STATUS_ICONS } from "../common/StatusIcon.svelte";
import ItemEntry from "./ItemSubSection.svelte"; import ItemEntry from "./ItemSubSection.svelte";
import Icon from "../layout/Icon.svelte";
import { projectPrettyId } from "$lib/utils/prettyIds";
export let requirement: Requirement; export let requirement: Requirement;
</script> </script>
<div id={projectPrettyId(requirement)} />
<Section title={requirement.name} icon={STATUS_ICONS[requirement.status]} status={requirement.status}> <Section title={requirement.name} icon={STATUS_ICONS[requirement.status]} status={requirement.status}>
<Progress alwaysSmooth titlePercentageOnly thin green status={requirement.status} count={requirement.totalAcquired} target={requirement.totalRequired} /> <Progress alwaysSmooth titlePercentageOnly thin green status={requirement.status} count={requirement.totalAcquired} target={requirement.totalRequired} />
<Progress alwaysSmooth titlePercentageOnly thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} /> <Progress alwaysSmooth titlePercentageOnly thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} />
<Markdown source={requirement.description} /> <Markdown source={requirement.description} />
<OptionsRow slot="right"> <OptionsRow slot="right">
<Option open={{name: "item.create", requirement}}>Add Item</Option> <Option open={{name: "item.create", requirement}}><Icon name="plus" /></Option>
<Option open={{name: "requirement.edit", requirement}}>Edit</Option> <Option open={{name: "requirement.edit", requirement}}><Icon name="pen" /></Option>
<Option open={{name: "requirement.delete", requirement}} color="red">Delete</Option> <Option open={{name: "requirement.delete", requirement}} color="red"><Icon name="trash" /></Option>
</OptionsRow> </OptionsRow>
<LabeledProgressRow> <LabeledProgressRow>
{#each requirement.stats as stat (stat.id)} {#each requirement.stats as stat (stat.id)}
@@ -0,0 +1,20 @@
<script lang="ts">
import { getItemMultiListContext } from "../contexts/ItemMultiListContext.svelte";
import Row from "../layout/Row.svelte";
import ItemSubSection from "../project/ItemSubSection.svelte";
export let key: string;
export let title: string;
export let showAcquiredTime: boolean = false;
const {lists} = getItemMultiListContext();
</script>
{#if ($lists[key]||[]).length > 0}
<Row title={title}>
{#each $lists[key] as item (item.id)}
<ItemSubSection showAcquiredTime={showAcquiredTime} item={item} />
{/each}
</Row>
{/if}
@@ -9,6 +9,7 @@
import ItemSubSection from "../project/ItemSubSection.svelte"; import ItemSubSection from "../project/ItemSubSection.svelte";
export let sprint: Sprint export let sprint: Sprint
export let compact: boolean = false;
let timeRange: [Date, Date]; let timeRange: [Date, Date];
@@ -34,12 +35,18 @@
{/if} {/if}
{#if sprint.kind === SprintKind.Items} {#if sprint.kind === SprintKind.Items}
{#each (sprint.items||[]) as item (item.id)} {#each (sprint.items||[]) as item (item.id)}
{#if !item.acquiredTime} {#if !compact || !item.acquiredTime}
<ItemSubSection compact item={item} /> <ItemSubSection compact={compact} hideStats={compact} item={item} />
{/if} {/if}
{/each} {/each}
{:else} {:else}
<AmountRow> {#if compact}
<Amount label="Acquired Items" value={(sprint.items||[]).length} /> <AmountRow>
</AmountRow> <Amount label="Acquired Items" value={(sprint.items||[]).length} />
</AmountRow>
{:else}
{#each (sprint.items||[]) as item (item.id)}
<ItemSubSection compact item={item} />
{/each}
{/if}
{/if} {/if}
@@ -1,27 +1,20 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation";
import type Sprint from "$lib/models/sprint"; import type Sprint from "$lib/models/sprint";
import Option from "../layout/Option.svelte"; import Option from "../layout/Option.svelte";
import OptionsRow from "../layout/OptionsRow.svelte"; import OptionsRow from "../layout/OptionsRow.svelte";
import SprintBody from "./SprintBody.svelte"; import SprintBody from "./SprintBody.svelte";
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds"; import { projectPrettyId } from "$lib/utils/prettyIds";
import { getScopeContext } from "../contexts/ScopeContext.svelte";
import Section from "../layout/Section.svelte"; import Section from "../layout/Section.svelte";
import Icon from "../layout/Icon.svelte";
export let sprint: Sprint; export let sprint: Sprint;
const {scope} = getScopeContext();
function gotoSprint() {
goto(`/${scopePrettyId($scope)}/sprints#${projectPrettyId(sprint)}`)
}
</script> </script>
<Section noProgress title={sprint.name}> <Section anchor={projectPrettyId(sprint)} noProgress title={sprint.name}>
<OptionsRow slot="right"> <OptionsRow slot="right">
<Option on:click={gotoSprint}>View</Option> <Option open={{name: "sprint.edit", sprint}}><Icon name="pen" /></Option>
<Option open={{name: "sprint.edit", sprint}}>Edit</Option> <Option open={{name: "sprint.delete", sprint}} color="red"><Icon name="trash" /></Option>
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option>
</OptionsRow> </OptionsRow>
<SprintBody sprint={sprint} /> <SprintBody sprint={sprint} />
</Section> </Section>
@@ -8,20 +8,28 @@
import SprintBody from "./SprintBody.svelte"; import SprintBody from "./SprintBody.svelte";
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds"; import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds";
import { getScopeContext } from "../contexts/ScopeContext.svelte"; import { getScopeContext } from "../contexts/ScopeContext.svelte";
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
import Icon from "../layout/Icon.svelte";
export let sprint: Sprint; export let sprint: Sprint;
const {scope} = getScopeContext(); const {scope, lastHistoryRange} = getScopeContext();
const {scopes} = getScopeListContext();
function gotoSprint() { function gotoSprint() {
goto(`/${scopePrettyId($scope)}/sprints#${projectPrettyId(sprint)}`) let sc = $scope;
if (sc.id !== sprint.scopeId) {
sc = $scopes.find(s => s.id === sprint.scopeId);
}
goto(`/${scopePrettyId(sc)}/sprints/${$lastHistoryRange}#${projectPrettyId(sprint)}`)
} }
</script> </script>
<SubSection noProgress title={sprint.name}> <SubSection noProgress title={sprint.name}>
<OptionsRow slot="right"> <OptionsRow slot="right">
<Option on:click={gotoSprint}>View</Option> <Option on:click={gotoSprint}><Icon name="eye" /></Option>
<Option open={{name: "sprint.edit", sprint}}>Edit</Option> <Option open={{name: "sprint.edit", sprint}}><Icon name="pen" /></Option>
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option> <Option open={{name: "sprint.delete", sprint}} color="red"><Icon name="trash" /></Option>
</OptionsRow> </OptionsRow>
<SprintBody sprint={sprint} /> <SprintBody compact sprint={sprint} />
</SubSection> </SubSection>
@@ -3,6 +3,7 @@
import Amount from "../common/Amount.svelte"; import Amount from "../common/Amount.svelte";
import AmountRow from "../common/AmountRow.svelte"; import AmountRow from "../common/AmountRow.svelte";
import Markdown from "../common/Markdown.svelte"; import Markdown from "../common/Markdown.svelte";
import Icon from "../layout/Icon.svelte";
import Option from "../layout/Option.svelte"; import Option from "../layout/Option.svelte";
import OptionsRow from "../layout/OptionsRow.svelte"; import OptionsRow from "../layout/OptionsRow.svelte";
import SubSection from "../layout/SubSection.svelte"; import SubSection from "../layout/SubSection.svelte";
@@ -12,8 +13,8 @@
<SubSection noProgress title={stat.name}> <SubSection noProgress title={stat.name}>
<OptionsRow slot="right"> <OptionsRow slot="right">
<Option open={{name: "stat.edit", stat}}>Edit</Option> <Option open={{name: "stat.edit", stat}}><Icon name="pen" /></Option>
<Option open={{name: "stat.delete", stat}} color="red">Delete</Option> <Option open={{name: "stat.delete", stat}} color="red"><Icon name="trash" /></Option>
</OptionsRow> </OptionsRow>
<Markdown source={stat.description} /> <Markdown source={stat.description} />
{#if stat.weight !== 1} {#if stat.weight !== 1}
+15 -6
View File
@@ -11,7 +11,8 @@
import { sl3 } from "$lib/clients/sl3"; import { sl3 } from "$lib/clients/sl3";
import Modal from "$lib/components/common/Modal.svelte"; import Modal from "$lib/components/common/Modal.svelte";
import ModalBody from "$lib/components/common/ModalBody.svelte"; import ModalBody from "$lib/components/common/ModalBody.svelte";
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte"; import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
import { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.svelte";
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte"; import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte"; import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte";
import { getProjectListContext } from "$lib/components/contexts/ProjectListContext.svelte"; import { getProjectListContext } from "$lib/components/contexts/ProjectListContext.svelte";
@@ -26,6 +27,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
const {reloadProjectList} = getProjectListContext(); const {reloadProjectList} = getProjectListContext();
const {reloadSprintList} = getSprintListContext(); const {reloadSprintList} = getSprintListContext();
const {reloadItemList} = getItemListContext(); const {reloadItemList} = getItemListContext();
const {reloadItemLists} = getItemMultiListContext();
let endpoint: string; let endpoint: string;
@@ -49,7 +51,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
break; break;
case "item.delete": case "item.delete":
init(`items/${$currentModal.item.id}`) init(`items/${$currentModal.item.id}`, [], $currentModal.item.scopeId)
noun = "Item"; noun = "Item";
name = $currentModal.item.name; name = $currentModal.item.name;
break; break;
@@ -62,7 +64,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
break; break;
case "sprint.delete": case "sprint.delete":
init(`sprints/${$currentModal.sprint.id}`) init(`sprints/${$currentModal.sprint.id}`, [], $currentModal.sprint.scopeId)
noun = "Sprint"; noun = "Sprint";
name = $currentModal.sprint.name; name = $currentModal.sprint.name;
break; break;
@@ -72,7 +74,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
$currentModal.requirement.items.map(i => ({ $currentModal.requirement.items.map(i => ({
op: "Detach item", op: "Detach item",
name: i.name, name: i.name,
})) })),
); );
noun = "Requirement"; noun = "Requirement";
name = $currentModal.requirement.name; name = $currentModal.requirement.name;
@@ -100,8 +102,13 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
show = false; show = false;
} }
function init(newEndpoint: string, newSideEffects: DeletionSideEffect[] = []) { function init(newEndpoint: string, newSideEffects: DeletionSideEffect[] = [], overrideScopeId?: number) {
endpoint = `scopes/${$scope.id}/${newEndpoint}`; let scopeId = $scope.id;
if (overrideScopeId != null) {
scopeId = overrideScopeId;
}
endpoint = `scopes/${scopeId}/${newEndpoint}`;
if (endpoint.endsWith("/")) { if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1); endpoint = endpoint.slice(0, -1);
} }
@@ -131,6 +138,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
await reloadProject(); await reloadProject();
if (noun === "Stat") { if (noun === "Stat") {
reloadItemLists();
deleteStat((res as {stat: Stat}).stat); deleteStat((res as {stat: Stat}).stat);
} }
@@ -139,6 +147,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
} }
if (noun === "Item") { if (noun === "Item") {
reloadItemLists();
await reloadItemList(); await reloadItemList();
} }
@@ -4,24 +4,24 @@
import Modal from "$lib/components/common/Modal.svelte"; import Modal from "$lib/components/common/Modal.svelte";
import ModalBody from "$lib/components/common/ModalBody.svelte"; import ModalBody from "$lib/components/common/ModalBody.svelte";
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte"; import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
import { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.svelte";
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte"; import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte"; import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
import AcquiredTimeInput from "$lib/components/controls/AcquiredTimeInput.svelte"; import AcquiredTimeInput from "$lib/components/controls/AcquiredTimeInput.svelte";
import StatInput from "$lib/components/controls/StatInput.svelte"; import StatInput from "$lib/components/controls/StatInput.svelte";
import type Item from "$lib/models/item"; import type Item from "$lib/models/item";
import type { ItemInput } from "$lib/models/item"; import type { ItemInput } from "$lib/models/item";
import type Scope from "$lib/models/scope";
const {currentModal, closeModal} = getModalContext(); const {currentModal, closeModal} = getModalContext();
const {scope} = getScopeContext();
const {reloadProject} = getProjectContext(); const {reloadProject} = getProjectContext();
const {reloadItemList} = getItemListContext(); const {reloadItemList} = getItemListContext();
const {reloadSprintList} = getSprintListContext(); const {reloadSprintList} = getSprintListContext();
const {reloadItemLists} = getItemMultiListContext();
let item: Partial<ItemInput> let item: Partial<ItemInput>
let itemId: number let itemId: number
let scopeId: number
let requirementId: number let requirementId: number
let openedDate: Date let openedDate: Date
@@ -31,7 +31,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
$: switch ($currentModal.name) { $: switch ($currentModal.name) {
case "item.acquire": case "item.acquire":
init($scope, $currentModal.item) init($currentModal.item)
break; break;
default: default:
@@ -40,7 +40,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
show = false; show = false;
} }
function init(scope: Scope, unaquiredItem: Item) { function init(unaquiredItem: Item) {
openedDate = new Date(); openedDate = new Date();
item = { item = {
@@ -50,6 +50,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
} }
show = true; show = true;
scopeId = unaquiredItem.scopeId;
itemId = unaquiredItem.id; itemId = unaquiredItem.id;
requirementId = unaquiredItem.requirementId; requirementId = unaquiredItem.requirementId;
} }
@@ -59,7 +60,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
loading = true; loading = true;
try { try {
await sl3(fetch).updateItem($scope.id, itemId, { await sl3(fetch).updateItem(scopeId, itemId, {
...item, ...item,
acquiredTime: new Date(item.acquiredTime).toISOString(), acquiredTime: new Date(item.acquiredTime).toISOString(),
}); });
@@ -69,6 +70,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
await reloadProject(); await reloadProject();
} }
reloadItemLists();
await reloadItemList(); await reloadItemList();
await reloadSprintList(); await reloadSprintList();
@@ -94,7 +96,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
<label for="description">Description</label> <label for="description">Description</label>
<textarea name="description" bind:value={item.description} /> <textarea name="description" bind:value={item.description} />
<label for="stats">Stats</label> <label for="stats">Stats</label>
<StatInput showAcquired hideUnseen noToggle bind:value={item.stats} /> <StatInput overrideScopeId={scopeId} showAcquired hideUnseen noToggle bind:value={item.stats} />
</ModalBody> </ModalBody>
</Modal> </Modal>
</form> </form>
+39 -9
View File
@@ -3,11 +3,13 @@
import Modal from "$lib/components/common/Modal.svelte"; import Modal from "$lib/components/common/Modal.svelte";
import ModalBody from "$lib/components/common/ModalBody.svelte"; import ModalBody from "$lib/components/common/ModalBody.svelte";
import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte"; import { getItemListContext } from "$lib/components/contexts/ItemListContext.svelte";
import { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.svelte";
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte"; import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte"; import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte"; import { getScopeListContext } from "$lib/components/contexts/ScopeListContext.svelte";
import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte";
import AcquiredTimeInput from "$lib/components/controls/AcquiredTimeInput.svelte"; import AcquiredTimeInput from "$lib/components/controls/AcquiredTimeInput.svelte";
import StatInput from "$lib/components/controls/StatInput.svelte"; import StatInput from "$lib/components/controls/StatInput.svelte";
import type Item from "$lib/models/item"; import type Item from "$lib/models/item";
@@ -19,16 +21,20 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
const {currentModal, closeModal} = getModalContext(); const {currentModal, closeModal} = getModalContext();
const {scope} = getScopeContext(); const {scope} = getScopeContext();
const {scopes} = getScopeListContext();
const {project, reloadProject} = getProjectContext(); const {project, reloadProject} = getProjectContext();
const {reloadItemList} = getItemListContext(); const {reloadItemList} = getItemListContext();
const {reloadSprintList} = getSprintListContext(); const {reloadSprintList} = getSprintListContext();
const {reloadItemLists} = getItemMultiListContext();
let item: ItemInput let item: ItemInput
let itemId: number let itemId: number
let scopeId: number
let currentStats: ItemInput["stats"] let currentStats: ItemInput["stats"]
let openedDate: Date let openedDate: Date
let requirementName: string let requirementName: string
let op: string let op: string
let initOnList: number[] = [];
let error: string let error: string
let loading: boolean let loading: boolean
@@ -38,9 +44,11 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
case "item.create": case "item.create":
initCreate($scope, $currentModal.requirement) initCreate($scope, $currentModal.requirement)
break; break;
case "item.edit": case "item.edit": {
initEdit($currentModal.item) const reqId = $currentModal.item.requirementId;
initEdit($currentModal.item, $project.requirements.find(r => r.id === reqId), $scope)
break; break;
}
default: default:
loading = false; loading = false;
@@ -52,6 +60,8 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
let stats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0})); let stats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
if (stats == null) { if (stats == null) {
stats = scope.stats.map(s => ({statId: s.id, required: 0, acquired: 0})) stats = scope.stats.map(s => ({statId: s.id, required: 0, acquired: 0}))
} else {
initOnList = requirement.stats.filter(s => s.required > 0).map(s => s.id);
} }
item = { item = {
@@ -64,23 +74,35 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
} }
op = "Create" op = "Create"
scopeId = scope.id;
openedDate = new Date(); openedDate = new Date();
requirementName = requirement?.name; requirementName = requirement?.name;
show = true; show = true;
} }
function initEdit(current: Item) { function initEdit(current: Item, requirement?: Requirement, scope?: Scope) {
const req = $project.requirements?.find(r => r.id === current.requirementId); const req = $project.requirements?.find(r => r.id === current.requirementId);
let ctxStats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
if (ctxStats == null || ctxStats.length === 0) {
let actualScope = scope;
if (actualScope.id !== current.scopeId && $scopes?.length > 0) {
actualScope = $scopes.find(s => s.id === current.scopeId)
}
ctxStats = actualScope.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
}
const inputStats = current.stats.map(s => ({statId: s.id, acquired: s.acquired, required: s.required}));
item = { item = {
name: current.name, name: current.name,
description: current.description, description: current.description,
requirementId: current.requirementId, requirementId: current.requirementId,
stats: current.stats.map(s => ({statId: s.id, acquired: s.acquired, required: s.required})), stats: ctxStats.map(s => inputStats.find(s2 => s2.statId === s.statId) || s),
acquiredTime: formatFormTime(current.acquiredTime), acquiredTime: formatFormTime(current.acquiredTime),
scheduledDate: current.scheduledDate, scheduledDate: current.scheduledDate,
}; };
itemId = current.id; itemId = current.id;
scopeId = current.scopeId;
currentStats = [...item.stats]; currentStats = [...item.stats];
op = "Edit" op = "Edit"
@@ -112,7 +134,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
} }
} }
await sl3(fetch).createItem($scope.id, submission); await sl3(fetch).createItem(scopeId, submission);
break; break;
case "Edit": case "Edit":
if (!submission.acquiredTime) { if (!submission.acquiredTime) {
@@ -123,7 +145,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
} }
submission.stats = statDiff(currentStats, submission.stats) submission.stats = statDiff(currentStats, submission.stats)
await sl3(fetch).updateItem($scope.id, itemId, submission); await sl3(fetch).updateItem(scopeId, itemId, submission);
break; break;
} }
@@ -134,6 +156,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
await reloadItemList(); await reloadItemList();
await reloadSprintList(); await reloadSprintList();
reloadItemLists();
closeModal(); closeModal();
} catch(err) { } catch(err) {
@@ -165,7 +188,14 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
<input disabled name="req" type="text" value={requirementName} /> <input disabled name="req" type="text" value={requirementName} />
{/if} {/if}
<label for="stats">Stats</label> <label for="stats">Stats</label>
<StatInput showAcquired={op === "Edit" && !!item.acquiredTime} showRequired hideUnseen={!!item.requirementId} bind:value={item.stats} /> <StatInput
zeroDeletes showRequired
overrideScopeId={scopeId}
initOnList={initOnList}
showAcquired={op === "Edit" && !!item.acquiredTime}
hideUnseen={!!item.requirementId}
bind:value={item.stats}
/>
</ModalBody> </ModalBody>
</Modal> </Modal>
</form> </form>
@@ -22,6 +22,7 @@ import { partsDiff } from "$lib/utils/sprint";
let sprint: SprintInput let sprint: SprintInput
let sprintId: number let sprintId: number
let scopeId: number
let oldParts: SprintInputPart[] let oldParts: SprintInputPart[]
let intervalName: string; let intervalName: string;
@@ -60,6 +61,7 @@ import { partsDiff } from "$lib/utils/sprint";
parts: [], parts: [],
} }
intervalName = "this_month"; intervalName = "this_month";
scopeId = scope.id;
op = "Create" op = "Create"
openedDate = new Date(); openedDate = new Date();
@@ -80,6 +82,7 @@ import { partsDiff } from "$lib/utils/sprint";
isUnweighted: current.isUnweighted, isUnweighted: current.isUnweighted,
}; };
sprintId = current.id; sprintId = current.id;
scopeId = current.scopeId;
intervalName = "specific_dates"; intervalName = "specific_dates";
if (sprint.kind === SprintKind.Stats) { if (sprint.kind === SprintKind.Stats) {
@@ -107,16 +110,16 @@ import { partsDiff } from "$lib/utils/sprint";
try { try {
switch (op) { switch (op) {
case "Create": case "Create":
await sl3(fetch).createSprint($scope.id, submission); await sl3(fetch).createSprint(scopeId, submission);
break; break;
case "Edit": case "Edit":
await sl3(fetch).updateSprint($scope.id, sprintId, submission); await sl3(fetch).updateSprint(scopeId, sprintId, submission);
const {added, removed} = partsDiff(oldParts, submission.parts) const {added, removed} = partsDiff(oldParts, submission.parts)
for (const part of added) { for (const part of added) {
await sl3(fetch).upsertSprintPart($scope.id, sprintId, part).catch(() => {}); await sl3(fetch).upsertSprintPart(scopeId, sprintId, part).catch(() => {});
} }
for (const part of removed) { for (const part of removed) {
await sl3(fetch).deleteSprintPart($scope.id, sprintId, part).catch(() => {}); await sl3(fetch).deleteSprintPart(scopeId, sprintId, part).catch(() => {});
} }
break; break;
@@ -153,15 +156,15 @@ import { partsDiff } from "$lib/utils/sprint";
<label for="aggregateValue">Aggregate Goal</label> <label for="aggregateValue">Aggregate Goal</label>
<input name="aggregateValue" type="number" bind:value={sprint.aggregateRequired} /> <input name="aggregateValue" type="number" bind:value={sprint.aggregateRequired} />
{/if} {/if}
<Checkbox bind:checked={sprint.isTimed} label="Sprint is timed" />
<Checkbox bind:checked={sprint.isCoarse} label="Show only aggregate" />
<Checkbox bind:checked={sprint.isUnweighted} label="Aggregate is unweighted" />
</ModalBody> </ModalBody>
<ModalBody> <ModalBody>
<label for="kind">Kind</label> <label for="kind">Kind</label>
<SprintKindSelect disabled={op === "Edit"} bind:kind={sprint.kind} /> <SprintKindSelect disabled={op === "Edit"} bind:kind={sprint.kind} />
<label for="parts">Parts</label> <label for="parts">Parts</label>
<PartInput bind:value={sprint.parts} kind={sprint.kind} /> <PartInput bind:value={sprint.parts} kind={sprint.kind} scopeId={scopeId} />
<Checkbox bind:checked={sprint.isTimed} label="Sprint is timed" />
<Checkbox bind:checked={sprint.isCoarse} label="Show only aggregate" />
<Checkbox bind:checked={sprint.isUnweighted} label="Aggregate is unweighted" />
</ModalBody> </ModalBody>
</Modal> </Modal>
</form> </form>
@@ -103,7 +103,7 @@ import Checkbox from "$lib/components/layout/Checkbox.svelte";
</script> </script>
<form on:submit|preventDefault={submit}> <form on:submit|preventDefault={submit}>
<Modal closable show={show} verb={op} noun="Item" disabled={loading} error={error}> <Modal closable show={show} verb={op} noun="Stat" disabled={loading} error={error}>
<ModalBody> <ModalBody>
<label for="name">Name</label> <label for="name">Name</label>
<input name="name" type="text" bind:value={stat.name} /> <input name="name" type="text" bind:value={stat.name} />
+22
View File
@@ -1,5 +1,6 @@
import type { TimeInterval } from "./common"; import type { TimeInterval } from "./common";
import type { StatValueInput, StatProgress } from "./stat"; import type { StatValueInput, StatProgress } from "./stat";
import type Status from "./status";
export default interface Item { export default interface Item {
id: number id: number
@@ -13,6 +14,25 @@ export default interface Item {
acquiredTime?: string acquiredTime?: string
scheduledDate?: string scheduledDate?: string
stats: StatProgress[] stats: StatProgress[]
weightedAcquired: number
weightedRequired: number
project: ItemProject
requirement: ItemRequirement
}
interface ItemProject {
id: number
name: string
status: Status
statusName: string
createdTime: string
}
interface ItemRequirement {
id: number
name: string
status: Status
statusName: string
} }
export interface ItemInput { export interface ItemInput {
@@ -36,6 +56,8 @@ export interface ItemFilter {
createdTime?: TimeInterval<Date | string> createdTime?: TimeInterval<Date | string>
acquiredTime?: TimeInterval<Date | string> acquiredTime?: TimeInterval<Date | string>
loose?: boolean loose?: boolean
unAcquired?: boolean
unScheduled?: boolean
} }
export interface ItemGroup { export interface ItemGroup {
+23 -7
View File
@@ -2,7 +2,17 @@ import type { ItemGroup, ItemGroupReference } from "$lib/models/item";
import type Item from "$lib/models/item"; import type Item from "$lib/models/item";
import { formatDate, formatDateTime, formatTime, formatWeekdayDate } from "./date"; import { formatDate, formatDateTime, formatTime, formatWeekdayDate } from "./date";
export function groupItems(items: Item[], minDate?: string): ItemGroup[] { export interface GroupItemsOptions {
hideCreated?: boolean
hideScheduled?: boolean
hideAcquired?: boolean
}
export function groupItemOptionsQuery(opts: GroupItemsOptions): string {
return `hideCreated=${opts.hideCreated},hideScheduled=${opts.hideScheduled},hideAcquired=${opts.hideAcquired}`;
}
export function groupItems(items: Item[], minDate?: string, opts: GroupItemsOptions = {}): ItemGroup[] {
let groups: Record<string, ItemGroup> = {}; let groups: Record<string, ItemGroup> = {};
for (let i = 0; i < items.length; ++i) { for (let i = 0; i < items.length; ++i) {
@@ -10,16 +20,22 @@ export function groupItems(items: Item[], minDate?: string): ItemGroup[] {
const createKey = formatDate(item.createdTime) const createKey = formatDate(item.createdTime)
const createSortKey = `${createKey} ${formatTime(item.createdTime)}` const createSortKey = `${createKey} ${formatTime(item.createdTime)}`
if (!item.acquiredTime || item.acquiredTime >= item.createdTime) { if (!opts.hideCreated) {
addItem(groups, createKey, i, createSortKey, item, "created"); if (!item.acquiredTime || item.acquiredTime >= item.createdTime) {
addItem(groups, createKey, i, createSortKey, item, "created");
}
} }
if (item.scheduledDate) { if (!opts.hideScheduled) {
addItem(groups, item.scheduledDate, i, `${item.scheduledDate} 23:59:59`, item, "scheduled"); if (item.scheduledDate) {
addItem(groups, item.scheduledDate, i, `${item.scheduledDate} 23:59:59`, item, "scheduled");
}
} }
if (item.acquiredTime) { if (!opts.hideAcquired) {
addItem(groups, formatDate(item.acquiredTime), i, formatDateTime(item.acquiredTime), item, "acquired"); if (item.acquiredTime) {
addItem(groups, formatDate(item.acquiredTime), i, formatDateTime(item.acquiredTime), item, "acquired");
}
} }
} }
+11
View File
@@ -0,0 +1,11 @@
export default function shortestDecimal(n: number) {
let s = n.toFixed(3);
while (s.endsWith("0")) {
s = s.slice(0, -1)
}
if (s.endsWith(".")) {
s = s.slice(0, -1)
}
return s;
}
+56
View File
@@ -0,0 +1,56 @@
export default function deepEqual(a: Object, b: Object) {
if (a === void(0)) {
if (b !== void(0)) {
return false;
}
return true;
}
if (a === null) {
if (b !== null) {
return false;
}
return true;
}
if (a instanceof Date) {
if (b instanceof Date) {
b = b.toISOString();
}
a = a.toISOString();
return a === b;
}
if (Array.isArray(a)) {
if (!Array.isArray(b) || b.length !== a.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
return true;
}
if (typeof(a) === "object") {
if (typeof(b) !== "object") {
return false;
}
for (const key in a) {
if (!a.hasOwnProperty(key)) {
continue
}
if (!deepEqual(a[key], b[key])) {
return false
}
}
return true;
}
return a === b;
}
+1 -1
View File
@@ -11,7 +11,7 @@ export function statDiff(before: StatValueInput[], after: StatValueInput[], dele
for (const s of after) { for (const s of after) {
const old = before.find(s2 => s2.statId === s.statId); const old = before.find(s2 => s2.statId === s.statId);
if (old == null || old.required != s.required || old.acquired != s.required) { if (old == null || old.required != s.required || old.acquired != s.acquired) {
res.push(s); res.push(s);
} }
} }
+1 -1
View File
@@ -2,7 +2,7 @@ import type { TimeInterval } from "../models/common";
import { addDays, addMonths, addYears, formatDate, startOfDay, startOfMonth, startOfWeek, startOfYear } from "./date"; import { addDays, addMonths, addYears, formatDate, startOfDay, startOfMonth, startOfWeek, startOfYear } from "./date";
export default function parseInterval(s: string, date: Date): TimeInterval<Date> { export default function parseInterval(s: string, date: Date): TimeInterval<Date> {
const [verb, args] = s.split(":") const [verb, args] = s.split(";")[0].split(":")
switch (verb) { switch (verb) {
case "": case "":
@@ -26,12 +26,12 @@
import ProjectListContext from "$lib/components/contexts/ProjectListContext.svelte"; import ProjectListContext from "$lib/components/contexts/ProjectListContext.svelte";
import ScopeMenu from "$lib/components/scope/ScopeMenu.svelte"; import ScopeMenu from "$lib/components/scope/ScopeMenu.svelte";
import { scopePrettyId } from "$lib/utils/prettyIds"; import { scopePrettyId } from "$lib/utils/prettyIds";
import ScopeHeader from "$lib/components/scope/ScopeHeader.svelte"; import ScopeHeader from "$lib/components/scope/ScopeHeader.svelte";
export let scope: Scope; export let scope: Scope;
export let projects: ProjectEntry[]; export let projects: ProjectEntry[];
let hideMobile; let hideMobile: boolean;
$: hideMobile = $page.url.pathname !== `/${scopePrettyId(scope)}` $: hideMobile = $page.url.pathname !== `/${scopePrettyId(scope)}`
</script> </script>
@@ -3,11 +3,11 @@
import { sl3 } from "$lib/clients/sl3"; import { sl3 } from "$lib/clients/sl3";
import parseInterval, { datesOf, intervalLabel } from "$lib/utils/timeinterval"; import parseInterval, { datesOf, intervalLabel } from "$lib/utils/timeinterval";
import { groupItems } from "$lib/utils/items"; import { groupItemOptionsQuery, groupItems, type GroupItemsOptions } from "$lib/utils/items";
import type Item from "$lib/models/item"; import type Item from "$lib/models/item";
import type { ItemFilter, ItemGroup } from "$lib/models/item"; import type { ItemFilter, ItemGroup } from "$lib/models/item";
export const load: Load = async({ fetch, params }) => { export const load: Load = async({ fetch, params, session }) => {
const scopeId = parseInt(params.scope.split("-")[0]); const scopeId = parseInt(params.scope.split("-")[0]);
const interval = parseInterval(params.interval, new Date()); const interval = parseInterval(params.interval, new Date());
@@ -22,22 +22,26 @@
props: { props: {
items, filter, items, filter,
intervalString: params.interval, intervalString: params.interval,
groups: groupItems(items, datesOf(interval)?.min), groups: groupItems(items, datesOf(interval)?.min, session.groupOptions || {}),
}, },
}; };
} }
</script> </script>
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation";
import { session } from "$app/stores";
import { browser } from "$app/env";
import Main from "$lib/components/layout/Main.svelte"; import Main from "$lib/components/layout/Main.svelte";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import DeletionModal from "$lib/modals/DeletionModal.svelte"; import DeletionModal from "$lib/modals/DeletionModal.svelte";
import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte"; import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte";
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte"; import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
import { goto } from "$app/navigation";
import HistorySelector from "$lib/components/common/HistorySelector.svelte"; import HistorySelector from "$lib/components/common/HistorySelector.svelte";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import ItemListContext from "$lib/components/contexts/ItemListContext.svelte"; import ItemListContext from "$lib/components/contexts/ItemListContext.svelte";
import HistoryGroupList from "$lib/components/history/HistoryGroupList.svelte"; import HistoryGroupList from "$lib/components/history/HistoryGroupList.svelte";
import ToggleIcon from "$lib/components/history/ToggleIcon.svelte";
import Status from "$lib/models/status";
export let intervalString: string export let intervalString: string
export let items: Item[] export let items: Item[]
@@ -51,13 +55,38 @@
lastHistoryRange.set(target.value); lastHistoryRange.set(target.value);
goto(`./${target.value}`); goto(`./${target.value}`, {replaceState: true});
}
function triggerReload() {
goto(`./${intervalString}`, {replaceState: true});
}
$: {
if (browser) {
if ($session.groupOptions == null) {
try {
$session.groupOptions = JSON.parse(localStorage.getItem("sl3.history.group_options")||"{}");
triggerReload();
} catch(_) {
$session.groupOptions = {};
}
} else {
localStorage.setItem("sl3.history.group_options", JSON.stringify($session.groupOptions));
}
}
} }
</script> </script>
<ItemListContext filter={filter} groups={groups} items={items}> <ItemListContext filter={filter} groups={groups} items={items}>
<Main big> <Main big>
<HistorySelector value={intervalString} on:change={changeInterval} /> <HistorySelector value={intervalString} on:change={changeInterval} >
{#if $session.groupOptions != null}
<ToggleIcon inverse name="lightbulb" bind:value={$session.groupOptions.hideCreated} on:click={triggerReload} />
<ToggleIcon inverse name="check_slot" bind:value={$session.groupOptions.hideAcquired} on:click={triggerReload} />
<ToggleIcon inverse name="calendar" bind:value={$session.groupOptions.hideScheduled} on:click={triggerReload} />
{/if}
</HistorySelector>
<HistoryGroupList /> <HistoryGroupList />
</Main> </Main>
@@ -1,19 +1,35 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type { Load } from "@sveltejs/kit/types/internal"; import type { Load } from "@sveltejs/kit/types/internal";
function generateItemFilters(now: Date): {scheduledFilter: ItemFilter, acquiredFilter: ItemFilter, looseFilter: ItemFilter} {
return {
scheduledFilter: {
scheduledDate: datesOf(parseInterval("next:7d", now)),
},
acquiredFilter: {
acquiredTime: parseInterval("today", now),
},
looseFilter: {
loose: true,
unAcquired: true,
unScheduled: true,
},
}
}
export const load: Load = async({params, url, fetch}) => { export const load: Load = async({params, url, fetch}) => {
const scopeId = parseInt(params.scope.split("-")[0]); const scopeId = parseInt(params.scope.split("-")[0]);
const scheduledItems = (await sl3(fetch).listItems(scopeId, { const {acquiredFilter, looseFilter, scheduledFilter} = generateItemFilters(new Date());
scheduledDate: datesOf(parseInterval("next:7d", new Date)),
})) const scheduledItems = (await sl3(fetch).listItems(scopeId, scheduledFilter));
const acquiredItems = (await sl3(fetch).listItems(scopeId, { const acquiredItems = (await sl3(fetch).listItems(scopeId, acquiredFilter));
acquiredTime: parseInterval("today", new Date), const looseItems = (await sl3(fetch).listItems(scopeId, looseFilter));
}))
const sprints = await sl3(fetch).listSprints(scopeId); const sprints = await sl3(fetch).listSprints(scopeId);
return { return {
props: {scheduledItems, acquiredItems, sprints} props: {scheduledItems, acquiredItems, looseItems, sprints}
}; };
} }
</script> </script>
@@ -31,7 +47,6 @@
import { sl3 } from "$lib/clients/sl3"; import { sl3 } from "$lib/clients/sl3";
import parseInterval, { datesOf } from "$lib/utils/timeinterval"; import parseInterval, { datesOf } from "$lib/utils/timeinterval";
import type Item from "$lib/models/item"; import type Item from "$lib/models/item";
import ItemSubSection from "$lib/components/project/ItemSubSection.svelte";
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte"; import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
import Card from "$lib/components/common/Card.svelte"; import Card from "$lib/components/common/Card.svelte";
import CardHeader from "$lib/components/common/CardHeader.svelte"; import CardHeader from "$lib/components/common/CardHeader.svelte";
@@ -43,13 +58,20 @@
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte"; import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
import ProjectCreateEditModal from "$lib/modals/ProjectCreateEditModal.svelte"; import ProjectCreateEditModal from "$lib/modals/ProjectCreateEditModal.svelte";
import ScopeCreateUpdateModal from "$lib/modals/ScopeCreateUpdateModal.svelte"; import ScopeCreateUpdateModal from "$lib/modals/ScopeCreateUpdateModal.svelte";
import type { ItemFilter } from "$lib/models/item";
import ItemMultiListContext from "$lib/components/contexts/ItemMultiListContext.svelte";
import { getTimeContext } from "$lib/components/contexts/TimeContext.svelte";
import ItemListRow from "$lib/components/scope/ItemListRow.svelte";
import Icon from "$lib/components/layout/Icon.svelte";
export let acquiredItems: Item[]; export let acquiredItems: Item[];
export let scheduledItems: Item[]; export let scheduledItems: Item[];
export let looseItems: Item[];
export let sprints: Sprint[]; export let sprints: Sprint[];
const {scope} = getScopeContext(); const {scope} = getScopeContext();
const {openModal} = getModalContext(); const {openModal} = getModalContext();
const {now} = getTimeContext();
function openCreateProject() { function openCreateProject() {
openModal({name: "project.create"}); openModal({name: "project.create"});
@@ -68,53 +90,44 @@
} }
</script> </script>
<SprintListContext sprints={sprints} intervalString=""> <ItemMultiListContext lists={{acquiredItems, scheduledItems, looseItems}} filters={generateItemFilters($now)}>
<Columns fullwidth> <SprintListContext sprints={sprints} intervalString="">
<Column> <Columns fullwidth>
<Row title="Options"> <Column>
<Card on:click={openCreateProject} pointerCursor><CardHeader>Create Project</CardHeader></Card> <Row title="Options">
<Card on:click={openPostItem} pointerCursor><CardHeader>Post Item</CardHeader></Card> <Card on:click={openCreateProject} pointerCursor><CardHeader>Create Project</CardHeader></Card>
<Card on:click={openEditScope} pointerCursor><CardHeader>Edit Scope</CardHeader></Card> <Card on:click={openPostItem} pointerCursor><CardHeader>Create Item</CardHeader></Card>
<Card on:click={openDeleteScope} pointerCursor><CardHeader>Delete Scope</CardHeader></Card> <Card on:click={openEditScope} pointerCursor><CardHeader>Edit Scope</CardHeader></Card>
</Row> <Card on:click={openDeleteScope} pointerCursor><CardHeader>Delete Scope</CardHeader></Card>
{#if scheduledItems.length > 0} </Row>
<Row title="Schedule"> <ItemListRow title="Scheduled" key="scheduleItems" />
{#each scheduledItems as item (item.id)} <ItemListRow title="Today" key="acquiredItems" showAcquiredTime />
<ItemSubSection item={item} /> <ItemListRow title="Loose" key="looseItems" />
</Column>
<Column>
<Row title="Sprints">
<OptionsRow slot="right">
<Option open={{name: "sprint.create"}}><Icon name="plus" /></Option>
</OptionsRow>
<SprintList sub />
</Row>
<Row title="Stats">
<OptionsRow slot="right">
<Option open={{name: "stat.create"}}><Icon name="plus" /></Option>
</OptionsRow>
{#each $scope.stats as stat (stat.id)}
<StatSubSection stat={stat} />
{/each} {/each}
</Row> </Row>
{/if} </Column>
{#if acquiredItems.length > 0} </Columns>
<Row title="Today">
{#each acquiredItems as item (item.id)} <ItemCreateModal />
<ItemSubSection item={item} /> <ItemAcquireModal />
{/each} <StatCreateEditModal />
</Row> <DeletionModal />
{/if} <SprintCreateUpdateModal />
</Column> <ProjectCreateEditModal />
<Column> <ScopeCreateUpdateModal />
<Row title="Sprints"> </SprintListContext>
<OptionsRow slot="right"> </ItemMultiListContext>
<Option open={{name: "sprint.create"}}>Create</Option>
</OptionsRow>
<SprintList sub />
</Row>
<Row title="Stats">
<OptionsRow slot="right">
<Option open={{name: "stat.create"}}>Create</Option>
</OptionsRow>
{#each $scope.stats as stat (stat.id)}
<StatSubSection stat={stat} />
{/each}
</Row>
</Column>
</Columns>
<ItemCreateModal />
<ItemAcquireModal />
<StatCreateEditModal />
<DeletionModal />
<SprintCreateUpdateModal />
<ProjectCreateEditModal />
<ScopeCreateUpdateModal />
</SprintListContext>
+75 -18
View File
@@ -2,11 +2,34 @@
import type { Load } from "@sveltejs/kit/types/internal"; import type { Load } from "@sveltejs/kit/types/internal";
import { sl3 } from "$lib/clients/sl3"; import { sl3 } from "$lib/clients/sl3";
function generateItemFilters(now: Date): {scheduledFilter: ItemFilter, acquiredFilter: ItemFilter, looseFilter: ItemFilter} {
return {
scheduledFilter: {
scheduledDate: datesOf(parseInterval("next:7d", now)),
},
acquiredFilter: {
acquiredTime: parseInterval("today", now),
},
looseFilter: {
loose: true,
unAcquired: true,
unScheduled: true,
},
}
}
export const load: Load = async({ fetch, session }) => { export const load: Load = async({ fetch, session }) => {
const scopes = await sl3(fetch).listScopes(); const scopes = await sl3(fetch).listScopes();
const {acquiredFilter, looseFilter, scheduledFilter} = generateItemFilters(new Date());
const scheduledItems = (await sl3(fetch).listItems("ALL", scheduledFilter));
const acquiredItems = (await sl3(fetch).listItems("ALL", acquiredFilter));
const looseItems = (await sl3(fetch).listItems("ALL", looseFilter));
const sprints = await sl3(fetch).listSprints("ALL");
return { return {
props: { scopes } props: { scopes, scheduledItems, acquiredItems, looseItems, sprints }
}; };
} }
</script> </script>
@@ -21,26 +44,60 @@
import ScopeCreateUpdateModal from "$lib/modals/ScopeCreateUpdateModal.svelte"; import ScopeCreateUpdateModal from "$lib/modals/ScopeCreateUpdateModal.svelte";
import OptionsRow from "$lib/components/layout/OptionsRow.svelte"; import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
import Option from "$lib/components/layout/Option.svelte"; import Option from "$lib/components/layout/Option.svelte";
import ScopeListContext from "$lib/components/contexts/ScopeListContext.svelte"; import ScopeListContext from "$lib/components/contexts/ScopeListContext.svelte";
import parseInterval, { datesOf } from "$lib/utils/timeinterval";
import ItemMultiListContext from "$lib/components/contexts/ItemMultiListContext.svelte";
import type Item from "$lib/models/item";
import DeletionModal from "$lib/modals/DeletionModal.svelte";
import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte";
import ItemListRow from "$lib/components/scope/ItemListRow.svelte";
import { getTimeContext } from "$lib/components/contexts/TimeContext.svelte";
import type { ItemFilter } from "$lib/models/item";
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
import SprintList from "$lib/components/scope/SprintList.svelte";
import SprintCreateUpdateModal from "$lib/modals/SprintCreateUpdateModal.svelte";
import SprintListContext from "$lib/components/contexts/SprintListContext.svelte";
import type Sprint from "$lib/models/sprint";
import Icon from "$lib/components/layout/Icon.svelte";
export let scopes: Scope[] = []; export let scopes: Scope[] = [];
export let acquiredItems: Item[];
export let scheduledItems: Item[];
export let looseItems: Item[];
export let sprints: Sprint[];
const {now} = getTimeContext();
</script> </script>
<Header fullwidth wide subtitle="Logging your stuff">Stufflog</Header> <Header fullwidth wide subtitle="Logging your stuff">Stufflog</Header>
<ScopeListContext scopes={scopes}> <ItemMultiListContext global lists={{acquiredItems, scheduledItems, looseItems}} filters={generateItemFilters($now)}>
<Columns wide> <ScopeListContext scopes={scopes}>
<Column> <SprintListContext global sprints={sprints} intervalString="">
<Row title="Scopes"> <Columns wide>
<OptionsRow slot="right"> <Column>
<Option open={{name: "scope.create"}}>Create</Option> <Row title="Scopes">
</OptionsRow> <OptionsRow slot="right">
<ScopeLinkList /> <Option open={{name: "scope.create"}}><Icon name="plus" /></Option>
</Row> </OptionsRow>
</Column> <ScopeLinkList />
<Column> </Row>
</Column> <ItemListRow title="Scheduled" key="scheduleItems" />
</Columns> <ItemListRow title="Today" key="acquiredItems" showAcquiredTime />
<ItemListRow title="Loose" key="looseItems" />
<ScopeCreateUpdateModal /> </Column>
</ScopeListContext> <Column>
<Row title="Sprints">
<SprintList sub />
</Row>
</Column>
</Columns>
<ItemCreateModal />
<ItemAcquireModal />
<DeletionModal />
<ScopeCreateUpdateModal />
<SprintCreateUpdateModal />
</SprintListContext>
</ScopeListContext>
</ItemMultiListContext>
+2 -3
View File
@@ -4,7 +4,9 @@ go 1.18
require ( require (
github.com/Masterminds/squirrel v1.5.2 github.com/Masterminds/squirrel v1.5.2
github.com/aws/aws-lambda-go v1.19.1
github.com/aws/aws-sdk-go v1.44.27 github.com/aws/aws-sdk-go v1.44.27
github.com/awslabs/aws-lambda-go-api-proxy v0.13.3
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
github.com/gin-gonic/gin v1.7.7 github.com/gin-gonic/gin v1.7.7
github.com/go-sql-driver/mysql v1.6.0 github.com/go-sql-driver/mysql v1.6.0
@@ -13,8 +15,6 @@ require (
) )
require ( require (
github.com/aws/aws-lambda-go v1.19.1 // indirect
github.com/awslabs/aws-lambda-go-api-proxy v0.13.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.13.0 // indirect github.com/go-playground/locales v0.13.0 // indirect
@@ -36,7 +36,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/snabb/isoweek v1.0.1 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
+11 -12
View File
@@ -116,6 +116,7 @@ github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -163,7 +164,6 @@ github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3K
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
@@ -195,6 +195,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -258,7 +259,6 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -285,8 +285,10 @@ github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ= github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
@@ -320,7 +322,6 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@@ -337,11 +338,9 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
@@ -356,16 +355,19 @@ github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJE
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -399,8 +401,6 @@ github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYl
github.com/smartystreets/assertions v1.2.1/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8= github.com/smartystreets/assertions v1.2.1/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/snabb/isoweek v1.0.1 h1:B4IsN2GU8lCNVkaUUgOzaVpPkKC2DdY9zcnxz5yc0qg=
github.com/snabb/isoweek v1.0.1/go.mod h1:CAijAxH7NMgjqGc9baHMDE4sTHMt4B/f6X/XLiEE1iA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
@@ -489,7 +489,6 @@ golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -573,6 +572,7 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 h1:Yqz/iviulwKwAREEeUd3nbBFn0XuyJqkoft2IlrvOhc=
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -663,7 +663,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -681,6 +680,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -744,6 +744,7 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@@ -850,7 +851,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -862,16 +862,15 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+10
View File
@@ -0,0 +1,10 @@
package genutils
func Flatten2[T any](arrs [][]T) []T {
res := make([]T, 0, 128)
for _, arr := range arrs {
res = append(res, arr...)
}
return res
}
+2
View File
@@ -25,4 +25,6 @@ type ItemFilter struct {
RequirementIDs []int `json:"requirementIds,omitempty"` RequirementIDs []int `json:"requirementIds,omitempty"`
StatIDs []int `json:"statIds,omitempty"` StatIDs []int `json:"statIds,omitempty"`
Loose bool `json:"loose,omitempty"` Loose bool `json:"loose,omitempty"`
UnScheduled bool `json:"unScheduled,omitempty"`
UnAcquired bool `json:"unAcquired,omitempty"`
} }
+29
View File
@@ -14,6 +14,35 @@ func (s Status) Name() string {
return StatusLabels[s] return StatusLabels[s]
} }
func (s Status) Less(s2 Status) bool {
if s == Active && s2 != Active {
return true
}
if s != Active && s2 == Active {
return false
}
if s == Background && s2 != Background {
return true
}
if s != Background && s2 == Background {
return false
}
if s == Available && s2 != Available {
return true
}
if s != Available && s2 == Available {
return false
}
if s == Blocked && s2 != Blocked {
return true
}
if s != Blocked && s2 == Blocked {
return false
}
return s < s2
}
const ( const (
Blocked Status = iota Blocked Status = iota
Available Available
+209 -189
View File
@@ -12,6 +12,17 @@ import (
"time" "time"
) )
func ItemsAllScopes(g *gin.RouterGroup, items *items.Service) {
g.GET("", handler("items", func(c *gin.Context) (interface{}, error) {
filter, err := getItemFilterFromQuery(c)
if err != nil {
return nil, err
}
return items.ListAllScopes(c.Request.Context(), filter)
}))
}
func Items(g *gin.RouterGroup, items *items.Service) { func Items(g *gin.RouterGroup, items *items.Service) {
g.GET("/:item_id", handler("project", func(c *gin.Context) (interface{}, error) { g.GET("/:item_id", handler("project", func(c *gin.Context) (interface{}, error) {
id, err := reqInt(c, "item_id") id, err := reqInt(c, "item_id")
@@ -23,195 +34,9 @@ func Items(g *gin.RouterGroup, items *items.Service) {
})) }))
g.GET("", handler("items", func(c *gin.Context) (interface{}, error) { g.GET("", handler("items", func(c *gin.Context) (interface{}, error) {
filter := models.ItemFilter{} filter, err := getItemFilterFromQuery(c)
if err != nil {
if rawFilter := c.Query("filter"); rawFilter != "" { return nil, err
err := json.Unmarshal([]byte(rawFilter), &filter)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "filter",
Problem: "Invalid raw filter: " + err.Error(),
}
}
}
if queryOwnerId := c.Query("ownerId"); queryOwnerId != "" {
filter.OwnerID = &queryOwnerId
}
queryScheduledMin := c.Query("scheduledMin")
queryScheduledMax := c.Query("scheduledMax")
if queryScheduledMin != "" && queryScheduledMax != "" {
min, err := models.ParseDate(queryScheduledMin)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "scheduledMin",
Problem: "Invalid from date: " + err.Error(),
}
}
max, err := models.ParseDate(queryScheduledMax)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "scheduledMax",
Problem: "Invalid to date: " + err.Error(),
}
}
filter.ScheduledDate = &models.TimeInterval[models.Date]{Min: min, Max: max}
}
queryCreatedMin := c.Query("createdMin")
queryCreatedMax := c.Query("createdMax")
if queryCreatedMin != "" && queryCreatedMax != "" {
min, err := time.Parse(time.RFC3339, queryCreatedMin)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "createdMin",
Problem: "Invalid from date: " + err.Error(),
}
}
max, err := time.Parse(time.RFC3339, queryCreatedMax)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "createdMax",
Problem: "Invalid to date: " + err.Error(),
}
}
filter.CreatedTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
}
queryAcquiredMin := c.Query("acquiredMin")
queryAcquiredMax := c.Query("acquiredMax")
if queryAcquiredMin != "" && queryAcquiredMax != "" {
min, err := time.Parse(time.RFC3339, queryAcquiredMin)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "acquiredMin",
Problem: "Invalid from date: " + err.Error(),
}
}
max, err := time.Parse(time.RFC3339, queryAcquiredMax)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "acquiredMax",
Problem: "Invalid to date: " + err.Error(),
}
}
filter.AcquiredTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
}
anyDateMin := c.Query("anyDateMin")
anyDateMax := c.Query("anyDateMax")
if anyDateMin != "" && anyDateMax != "" {
min, err := time.Parse(time.RFC3339, anyDateMin)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "anyDateMin",
Problem: "Invalid from date: " + err.Error(),
}
}
max, err := time.Parse(time.RFC3339, anyDateMax)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "anyDateMax",
Problem: "Invalid to date: " + err.Error(),
}
}
minY, minM, minD := min.Date()
maxY, maxM, maxD := max.Date()
filter.AcquiredTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
filter.CreatedTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
filter.ScheduledDate = &models.TimeInterval[models.Date]{
Min: models.Date{minY, int(minM), minD},
Max: models.Date{maxY, int(maxM), maxD},
}
}
if queryProjectID := c.Query("projectId"); queryProjectID != "" {
ids := strings.Split(queryProjectID, ",")
for _, id := range ids {
parsed, err := strconv.Atoi(id)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: fmt.Sprintf("projectId[%d]", len(filter.ProjectIDs)),
Problem: "Invalid number",
}
}
filter.ProjectIDs = append(filter.ProjectIDs, parsed)
}
}
if queryRequirementID := c.Query("requirementId"); queryRequirementID != "" {
if queryRequirementID != "null" {
ids := strings.Split(queryRequirementID, ",")
for _, id := range ids {
parsed, err := strconv.Atoi(id)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: fmt.Sprintf("requirementId[%d]", len(filter.RequirementIDs)),
Problem: "Invalid number",
}
}
filter.RequirementIDs = append(filter.RequirementIDs, parsed)
}
} else {
filter.Loose = true
}
}
if statID := c.Query("statId"); statID != "" {
ids := strings.Split(statID, ",")
for _, id := range ids {
parsed, err := strconv.Atoi(id)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: fmt.Sprintf("statId[%d]", len(filter.StatIDs)),
Problem: "Invalid number",
}
}
filter.StatIDs = append(filter.StatIDs, parsed)
}
}
if filter.ScheduledDate != nil && !filter.ScheduledDate.Valid() {
return nil, models.BadInputError{
Object: "Filter",
Field: "scheduledDate",
Problem: "Invalid scheduled date range",
}
}
if filter.CreatedTime != nil && !filter.CreatedTime.Valid() {
return nil, models.BadInputError{
Object: "Filter",
Field: "createdTime",
Problem: "Invalid created time range",
}
}
if filter.AcquiredTime != nil && !filter.AcquiredTime.Valid() {
return nil, models.BadInputError{
Object: "Filter",
Field: "acquiredTime",
Problem: "Invalid created time range",
}
} }
return items.ListScoped(c.Request.Context(), filter) return items.ListScoped(c.Request.Context(), filter)
@@ -300,3 +125,198 @@ func Items(g *gin.RouterGroup, items *items.Service) {
return items.Delete(c.Request.Context(), id) return items.Delete(c.Request.Context(), id)
})) }))
} }
func getItemFilterFromQuery(c *gin.Context) (models.ItemFilter, error) {
filter := models.ItemFilter{}
if rawFilter := c.Query("filter"); rawFilter != "" {
err := json.Unmarshal([]byte(rawFilter), &filter)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: "filter",
Problem: "Invalid raw filter: " + err.Error(),
}
}
}
if queryOwnerId := c.Query("ownerId"); queryOwnerId != "" {
filter.OwnerID = &queryOwnerId
}
queryScheduledMin := c.Query("scheduledMin")
queryScheduledMax := c.Query("scheduledMax")
if queryScheduledMin != "" && queryScheduledMax != "" {
min, err := models.ParseDate(queryScheduledMin)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: "scheduledMin",
Problem: "Invalid from date: " + err.Error(),
}
}
max, err := models.ParseDate(queryScheduledMax)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: "scheduledMax",
Problem: "Invalid to date: " + err.Error(),
}
}
filter.ScheduledDate = &models.TimeInterval[models.Date]{Min: min, Max: max}
}
queryCreatedMin := c.Query("createdMin")
queryCreatedMax := c.Query("createdMax")
if queryCreatedMin != "" && queryCreatedMax != "" {
min, err := time.Parse(time.RFC3339, queryCreatedMin)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: "createdMin",
Problem: "Invalid from date: " + err.Error(),
}
}
max, err := time.Parse(time.RFC3339, queryCreatedMax)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: "createdMax",
Problem: "Invalid to date: " + err.Error(),
}
}
filter.CreatedTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
}
queryAcquiredMin := c.Query("acquiredMin")
queryAcquiredMax := c.Query("acquiredMax")
if queryAcquiredMin != "" && queryAcquiredMax != "" {
min, err := time.Parse(time.RFC3339, queryAcquiredMin)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: "acquiredMin",
Problem: "Invalid from date: " + err.Error(),
}
}
max, err := time.Parse(time.RFC3339, queryAcquiredMax)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: "acquiredMax",
Problem: "Invalid to date: " + err.Error(),
}
}
filter.AcquiredTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
}
anyDateMin := c.Query("anyDateMin")
anyDateMax := c.Query("anyDateMax")
if anyDateMin != "" && anyDateMax != "" {
min, err := time.Parse(time.RFC3339, anyDateMin)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: "anyDateMin",
Problem: "Invalid from date: " + err.Error(),
}
}
max, err := time.Parse(time.RFC3339, anyDateMax)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: "anyDateMax",
Problem: "Invalid to date: " + err.Error(),
}
}
minY, minM, minD := min.Date()
maxY, maxM, maxD := max.Date()
filter.AcquiredTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
filter.CreatedTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
filter.ScheduledDate = &models.TimeInterval[models.Date]{
Min: models.Date{minY, int(minM), minD},
Max: models.Date{maxY, int(maxM), maxD},
}
}
if queryProjectID := c.Query("projectId"); queryProjectID != "" {
ids := strings.Split(queryProjectID, ",")
for _, id := range ids {
parsed, err := strconv.Atoi(id)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: fmt.Sprintf("projectId[%d]", len(filter.ProjectIDs)),
Problem: "Invalid number",
}
}
filter.ProjectIDs = append(filter.ProjectIDs, parsed)
}
}
if queryRequirementID := c.Query("requirementId"); queryRequirementID != "" {
if queryRequirementID != "null" {
ids := strings.Split(queryRequirementID, ",")
for _, id := range ids {
parsed, err := strconv.Atoi(id)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: fmt.Sprintf("requirementId[%d]", len(filter.RequirementIDs)),
Problem: "Invalid number",
}
}
filter.RequirementIDs = append(filter.RequirementIDs, parsed)
}
} else {
filter.Loose = true
}
}
if statID := c.Query("statId"); statID != "" {
ids := strings.Split(statID, ",")
for _, id := range ids {
parsed, err := strconv.Atoi(id)
if err != nil {
return models.ItemFilter{}, models.BadInputError{
Object: "Query",
Field: fmt.Sprintf("statId[%d]", len(filter.StatIDs)),
Problem: "Invalid number",
}
}
filter.StatIDs = append(filter.StatIDs, parsed)
}
}
if filter.ScheduledDate != nil && !filter.ScheduledDate.Valid() {
return models.ItemFilter{}, models.BadInputError{
Object: "Filter",
Field: "scheduledDate",
Problem: "Invalid scheduled date range",
}
}
if filter.CreatedTime != nil && !filter.CreatedTime.Valid() {
return models.ItemFilter{}, models.BadInputError{
Object: "Filter",
Field: "createdTime",
Problem: "Invalid created time range",
}
}
if filter.AcquiredTime != nil && !filter.AcquiredTime.Valid() {
return models.ItemFilter{}, models.BadInputError{
Object: "Filter",
Field: "acquiredTime",
Problem: "Invalid created time range",
}
}
return filter, nil
}
+28 -24
View File
@@ -9,20 +9,40 @@ import (
"net/http" "net/http"
) )
type scopeResult struct { func AllScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc {
entities.Scope return func(c *gin.Context) {
user := auth.GetUser(c.Request.Context())
if user == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, Error{
Code: http.StatusNotFound,
Message: "Scopes not found or inaccessible",
})
return
}
Members []scopeResultMember `json:"members"` userScopes, err := scopes.List(c.Request.Context())
} if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, Error{
Code: http.StatusNotFound,
Message: "Scopes not found or inaccessible",
})
return
}
type scopeResultMember struct { c.Request = c.Request.WithContext(scopes.CreateListContext(c.Request.Context(), user.ID, userScopes))
ID string `json:"id"` }
Name string `json:"name"`
Owner bool `json:"owner"`
} }
func ScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc { func ScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
user := auth.GetUser(c.Request.Context())
if user == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, Error{
Code: http.StatusNotFound,
Message: "Scope not found or inaccessible",
})
}
id, err := reqInt(c, "scope_id") id, err := reqInt(c, "scope_id")
if err != nil { if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, Error{ c.AbortWithStatusJSON(http.StatusUnauthorized, Error{
@@ -45,22 +65,6 @@ func ScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc
return return
} }
found := false
user := auth.GetUser(c.Request.Context())
for id, _ := range scope.Members {
if id == user.ID {
found = true
break
}
}
if !found {
c.AbortWithStatusJSON(http.StatusUnauthorized, Error{
Code: http.StatusNotFound,
Message: "Scope not found or inaccessible",
})
return
}
c.Request = c.Request.WithContext(scopes.CreateContext(c.Request.Context(), user.ID, *scope)) c.Request = c.Request.WithContext(scopes.CreateContext(c.Request.Context(), user.ID, *scope))
} }
} }
+44 -24
View File
@@ -9,34 +9,22 @@ import (
"time" "time"
) )
func Sprints(g *gin.RouterGroup, sprintsService *sprints.Service) { func SprintsAllScopes(g *gin.RouterGroup, sprintsService *sprints.Service) {
g.GET("", handler("sprints", func(c *gin.Context) (interface{}, error) { g.GET("", handler("sprints", func(c *gin.Context) (interface{}, error) {
from := time.Now().Add(-time.Hour * 24 * 7) from, to, err := getFromToFromQuery(c)
if fromQuery := c.Query("from"); fromQuery != "" { if err != nil {
parsed, err := time.Parse(time.RFC3339Nano, fromQuery) return nil, err
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "from",
Problem: "Incorrect date format: " + err.Error(),
}
}
from = parsed
} }
to := time.Now().Add(time.Hour * 24 * 7) return sprintsService.ListBetween(c.Request.Context(), from, to)
if toQuery := c.Query("to"); toQuery != "" { }))
parsed, err := time.Parse(time.RFC3339Nano, toQuery) }
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "to",
Problem: "Incorrect date format: " + err.Error(),
}
}
to = parsed func Sprints(g *gin.RouterGroup, sprintsService *sprints.Service) {
g.GET("", handler("sprints", func(c *gin.Context) (interface{}, error) {
from, to, err := getFromToFromQuery(c)
if err != nil {
return nil, err
} }
return sprintsService.ListScopedBetween(c.Request.Context(), from, to) return sprintsService.ListScopedBetween(c.Request.Context(), from, to)
@@ -126,3 +114,35 @@ func Sprints(g *gin.RouterGroup, sprintsService *sprints.Service) {
return sprintsService.Delete(c.Request.Context(), sprintID) return sprintsService.Delete(c.Request.Context(), sprintID)
})) }))
} }
func getFromToFromQuery(c *gin.Context) (time.Time, time.Time, error) {
from := time.Now().Add(-time.Hour * 24 * 7)
if fromQuery := c.Query("from"); fromQuery != "" {
parsed, err := time.Parse(time.RFC3339Nano, fromQuery)
if err != nil {
return time.Time{}, time.Time{}, models.BadInputError{
Object: "Query",
Field: "from",
Problem: "Incorrect date format: " + err.Error(),
}
}
from = parsed
}
to := time.Now().Add(time.Hour * 24 * 7)
if toQuery := c.Query("to"); toQuery != "" {
parsed, err := time.Parse(time.RFC3339Nano, toQuery)
if err != nil {
return time.Time{}, time.Time{}, models.BadInputError{
Object: "Query",
Field: "to",
Problem: "Incorrect date format: " + err.Error(),
}
}
to = parsed
}
return from, to, nil
}
+2 -6
View File
@@ -1,7 +1,6 @@
package mysql package mysql
import ( import (
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -75,11 +74,8 @@ func Connect(host string, port int, username, password, database string) (*Datab
return nil, err return nil, err
} }
q, err := mysqlcore.Prepare(context.Background(), db) q := mysqlcore.New(db)
if err != nil {
return nil, err
}
return &Database{db: db, q: q}, nil return &Database{db: db, q: q}, nil
} }
+6 -2
View File
@@ -67,13 +67,17 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([
squirrel.Lt{"i.created_time": filter.CreatedTime.Max}, squirrel.Lt{"i.created_time": filter.CreatedTime.Max},
}) })
} }
if filter.AcquiredTime != nil { if filter.UnAcquired {
sq = sq.Where("i.acquired_time IS NULL")
} else if filter.AcquiredTime != nil {
dateOr = append(dateOr, squirrel.And{ dateOr = append(dateOr, squirrel.And{
squirrel.GtOrEq{"i.acquired_time": filter.AcquiredTime.Min}, squirrel.GtOrEq{"i.acquired_time": filter.AcquiredTime.Min},
squirrel.Lt{"i.acquired_time": filter.AcquiredTime.Max}, squirrel.Lt{"i.acquired_time": filter.AcquiredTime.Max},
}) })
} }
if filter.ScheduledDate != nil { if filter.UnScheduled {
sq = sq.Where("i.scheduled_date IS NULL")
} else if filter.ScheduledDate != nil {
dateOr = append(dateOr, squirrel.And{ dateOr = append(dateOr, squirrel.And{
squirrel.GtOrEq{"i.scheduled_date": filter.ScheduledDate.Min.AsTime()}, squirrel.GtOrEq{"i.scheduled_date": filter.ScheduledDate.Min.AsTime()},
squirrel.Lt{"i.scheduled_date": filter.ScheduledDate.Max.AsTime()}, squirrel.Lt{"i.scheduled_date": filter.ScheduledDate.Max.AsTime()},
+62 -4
View File
@@ -35,6 +35,61 @@ func (r *projectRepository) Find(ctx context.Context, scopeID, projectID int) (*
}, nil }, nil
} }
func (r *projectRepository) FetchProjects(ctx context.Context, scopeID int, ids ...int) ([]entities.Project, error) {
if len(ids) == 0 {
return []entities.Project{}, nil
} else if len(ids) == 1 && scopeID != -1 {
project, err := r.Find(ctx, scopeID, ids[0])
if err != nil {
return nil, err
}
return []entities.Project{*project}, nil
}
sq := squirrel.Select("id,scope_id,owner_id,name,status,description,created_time").
From("project").
Where(squirrel.Eq{"id": ids})
if scopeID != -1 {
sq = sq.Where(squirrel.Eq{"scope_id": scopeID})
}
query, args, err := sq.ToSql()
if err != nil {
return nil, err
}
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
projects := make([]entities.Project, 0, len(ids))
for rows.Next() {
project := entities.Project{}
if err := rows.Scan(
&project.ID,
&project.ScopeID,
&project.OwnerID,
&project.Name,
&project.Status,
&project.Description,
&project.CreatedTime,
); err != nil {
return nil, err
}
projects = append(projects, project)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return projects, nil
}
func (r *projectRepository) List(ctx context.Context, scopeID int) ([]entities.Project, error) { func (r *projectRepository) List(ctx context.Context, scopeID int) ([]entities.Project, error) {
rows, err := r.q.ListProjects(ctx, scopeID) rows, err := r.q.ListProjects(ctx, scopeID)
if err != nil { if err != nil {
@@ -139,11 +194,14 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int,
return []entities.Requirement{}, []entities.RequirementStat{}, nil return []entities.Requirement{}, []entities.RequirementStat{}, nil
} }
query, args, err := squirrel.Select("id, scope_id, project_id, name, status, description, is_coarse"). sq := squirrel.Select("id, scope_id, project_id, name, status, description, is_coarse").
From("project_requirement"). From("project_requirement").
Where(squirrel.Eq{"scope_id": scopeID}). Where(squirrel.Eq{"id": requirementIDs})
Where(squirrel.Eq{"id": requirementIDs}). if scopeID != -1 {
ToSql() sq = sq.Where(squirrel.Eq{"scope_id": scopeID})
}
query, args, err := sq.ToSql()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
+2 -2
View File
@@ -10,8 +10,8 @@ AND (
OR (from_time <= ? AND to_time > ?) OR (from_time <= ? AND to_time > ?)
-- or from and to time are on each their side of from..to -- or from and to time are on each their side of from..to
OR (from_time >= ? AND to_time < ?) OR (from_time >= ? AND to_time < ?)
); )
ORDER BY from_time, name;
-- name: GetSprint :one -- name: GetSprint :one
SELECT * FROM sprint WHERE id = ? AND scope_id = ?; SELECT * FROM sprint WHERE id = ? AND scope_id = ?;
+1
View File
@@ -88,6 +88,7 @@ func (r *statsRepository) List(ctx context.Context, scopeIDs ...int) ([]entities
Select("id, scope_id, name, description, weight, allowed_amounts, is_primary"). Select("id, scope_id, name, description, weight, allowed_amounts, is_primary").
From("stat"). From("stat").
Where(squirrel.Eq{"scope_id": scopeIDs}). Where(squirrel.Eq{"scope_id": scopeIDs}).
OrderBy("is_primary DESC", "name").
RunWith(r.db). RunWith(r.db).
Query() Query()
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE item CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE project CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE project_requirement CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE scope CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE sprint CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE stat CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
+4
View File
@@ -16,6 +16,10 @@ type Repository interface {
UpdateStat(ctx context.Context, progress entities.ItemStat) error UpdateStat(ctx context.Context, progress entities.ItemStat) error
} }
type ProjectFetcher interface {
FetchProjects(ctx context.Context, scopeID int, ids ...int) ([]entities.Project, error)
}
type RequirementFetcher interface { type RequirementFetcher interface {
FetchRequirements(ctx context.Context, scopeID int, requirementIDs ...int) ([]entities.Requirement, []entities.RequirementStat, error) FetchRequirements(ctx context.Context, scopeID int, requirementIDs ...int) ([]entities.Requirement, []entities.RequirementStat, error)
} }
+59 -4
View File
@@ -2,14 +2,34 @@ package items
import ( import (
"git.aiterp.net/stufflog3/stufflog3/entities" "git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes" "git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"time"
) )
type Result struct { type Result struct {
entities.Item entities.Item
Requirement *entities.Requirement `json:"requirement,omitempty"` Requirement *ResultRequirement `json:"requirement,omitempty"`
Stats []ResultStat `json:"stats"` Project *ResultProject `json:"project,omitempty"`
Stats []ResultStat `json:"stats"`
WeightedAcquired float64 `json:"weightedAcquired"`
WeightedRequired float64 `json:"weightedRequired"`
}
type ResultRequirement struct {
ID int `json:"id"`
Name string `json:"name"`
Status models.Status `json:"status"`
StatusName string `json:"statusName"`
}
type ResultProject struct {
ID int `json:"id"`
CreatedTime time.Time `json:"createdTime"`
Name string `json:"name"`
Status models.Status `json:"status"`
StatusName string `json:"statusName"`
} }
func (r *Result) Stat(statID int) *ResultStat { func (r *Result) Stat(statID int) *ResultStat {
@@ -53,6 +73,7 @@ func (r *Result) AddStat(scope scopes.Result, stat entities.ItemStat) {
Required: stat.Required, Required: stat.Required,
}) })
r.SortStats(scope) r.SortStats(scope)
r.RecalculateWeighted()
} }
func (r *Result) SortStats(scope scopes.Result) { func (r *Result) SortStats(scope scopes.Result) {
@@ -76,6 +97,18 @@ func (r *Result) RemoveStat(id int) {
r.Stats = append(r.Stats[:i], r.Stats[i+1:]...) r.Stats = append(r.Stats[:i], r.Stats[i+1:]...)
} }
} }
r.RecalculateWeighted()
}
func (r *Result) RecalculateWeighted() {
r.WeightedAcquired = 0
r.WeightedRequired = 0
for _, stat := range r.Stats {
r.WeightedAcquired += float64(stat.Acquired) * stat.Weight
r.WeightedRequired += float64(stat.Required) * stat.Weight
}
} }
type ResultStat struct { type ResultStat struct {
@@ -86,7 +119,7 @@ type ResultStat struct {
Required int `json:"required"` Required int `json:"required"`
} }
func generateResult(item entities.Item, scope scopes.Result, progresses []entities.ItemStat, requirements []entities.Requirement) Result { func generateResult(item entities.Item, scope scopes.Result, progresses []entities.ItemStat, requirements []entities.Requirement, projects []entities.Project) Result {
res := Result{ res := Result{
Item: item, Item: item,
Stats: make([]ResultStat, 0, 8), Stats: make([]ResultStat, 0, 8),
@@ -95,7 +128,28 @@ func generateResult(item entities.Item, scope scopes.Result, progresses []entiti
if item.RequirementID != nil { if item.RequirementID != nil {
for _, req := range requirements { for _, req := range requirements {
if req.ID == *item.RequirementID { if req.ID == *item.RequirementID {
res.Requirement = &req res.Requirement = &ResultRequirement{
ID: req.ID,
Name: req.Name,
Status: req.Status,
StatusName: scope.StatusName(req.Status),
}
break
}
}
}
if item.ProjectID != nil {
for _, project := range projects {
if project.ID == *item.ProjectID {
res.Project = &ResultProject{
ID: project.ID,
CreatedTime: project.CreatedTime,
Name: project.Name,
Status: project.Status,
StatusName: scope.StatusName(project.Status),
}
break
} }
} }
} }
@@ -121,6 +175,7 @@ func generateResult(item entities.Item, scope scopes.Result, progresses []entiti
} }
res.SortStats(scope) res.SortStats(scope)
res.RecalculateWeighted()
return res return res
} }
+129 -6
View File
@@ -17,6 +17,7 @@ type Service struct {
Scopes *scopes.Service Scopes *scopes.Service
Stats *stats.Service Stats *stats.Service
Repository Repository Repository Repository
ProjectFetcher ProjectFetcher
RequirementFetcher RequirementFetcher RequirementFetcher RequirementFetcher
} }
@@ -44,17 +45,87 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
} }
} }
result := generateResult(*item, sc.Scope, itemStats, requirements) var projects []entities.Project
if item.ProjectID != nil {
projects, err = s.ProjectFetcher.FetchProjects(ctx, item.ScopeID, *item.ProjectID)
if err != nil {
return nil, err
}
}
result := generateResult(*item, sc.Scope, itemStats, requirements, projects)
return &result, nil return &result, nil
} }
func (s *Service) ListAllScopes(ctx context.Context, filter models.ItemFilter) ([]Result, error) {
userScopes, err := s.Scopes.Context(ctx).Scopes(ctx)
if err != nil {
return nil, err
}
if len(userScopes) == 0 {
return []Result{}, nil
}
filter.ScopeIDs = make([]int, 0, len(userScopes))
for _, scope := range userScopes {
filter.ScopeIDs = append(filter.ScopeIDs, scope.ID)
}
items, err := s.Repository.Fetch(ctx, filter)
if err != nil {
return nil, err
}
progresses, err := s.Repository.ListStat(ctx, items...)
if err != nil {
return nil, err
}
reqIds := genutils.Set[int]{}
projIds := genutils.Set[int]{}
for _, item := range items {
if item.RequirementID != nil {
reqIds.Add(*item.RequirementID)
}
if item.ProjectID != nil {
projIds.Add(*item.ProjectID)
}
}
var requirements []entities.Requirement
if reqIds.Len() > 0 {
requirements, _, err = s.RequirementFetcher.FetchRequirements(ctx, -1, reqIds.Values()...)
if err != nil {
return nil, err
}
}
var projects []entities.Project
if projIds.Len() > 0 {
projects, err = s.ProjectFetcher.FetchProjects(ctx, -1, projIds.Values()...)
if err != nil {
return nil, err
}
}
res := make([]Result, 0, len(items))
for _, item := range items {
for _, scope := range userScopes {
if item.ScopeID == scope.ID {
res = append(res, generateResult(item, scope, progresses, requirements, projects))
break
}
}
}
return res, nil
}
func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]Result, error) { func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]Result, error) {
sc := s.Scopes.Context(ctx) sc := s.Scopes.Context(ctx)
if sc == nil { if sc == nil {
return nil, models.PermissionDeniedError{} return nil, models.PermissionDeniedError{}
} }
if filter.ScopeIDs != nil { if len(filter.ScopeIDs) > 0 {
userScopes, err := sc.Scopes(ctx) userScopes, err := sc.Scopes(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -88,10 +159,14 @@ func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]R
} }
reqIds := genutils.Set[int]{} reqIds := genutils.Set[int]{}
projIds := genutils.Set[int]{}
for _, item := range items { for _, item := range items {
if item.RequirementID != nil { if item.RequirementID != nil {
reqIds.Add(*item.RequirementID) reqIds.Add(*item.RequirementID)
} }
if item.ProjectID != nil {
projIds.Add(*item.ProjectID)
}
} }
var requirements []entities.Requirement var requirements []entities.Requirement
if reqIds.Len() > 0 { if reqIds.Len() > 0 {
@@ -100,10 +175,36 @@ func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]R
return nil, err return nil, err
} }
} }
var projects []entities.Project
if projIds.Len() > 0 {
projects, err = s.ProjectFetcher.FetchProjects(ctx, sc.ID, projIds.Values()...)
if err != nil {
return nil, err
}
}
res := make([]Result, 0, len(items)) res := make([]Result, 0, len(items))
for _, item := range items { for _, item := range items {
res = append(res, generateResult(item, sc.Scope, progresses, requirements)) scope := &sc.Scope
if scope.ID != item.ScopeID {
userScopes, err := sc.Scopes(ctx)
if err != nil {
return nil, err
}
for _, userScope := range userScopes {
if userScope.ID == item.ScopeID {
scope = &userScope
break
}
}
if scope.ID != item.ScopeID {
return nil, models.NotFoundError("Scope")
}
}
res = append(res, generateResult(item, *scope, progresses, requirements, projects))
} }
return res, nil return res, nil
@@ -122,6 +223,7 @@ func (s *Service) Create(ctx context.Context, item entities.Item, stats []entiti
} }
} }
var requirements []entities.Requirement var requirements []entities.Requirement
var projects []entities.Project
if item.RequirementID != nil { if item.RequirementID != nil {
reqs, _, err := s.RequirementFetcher.FetchRequirements(ctx, scope.ID, *item.RequirementID) reqs, _, err := s.RequirementFetcher.FetchRequirements(ctx, scope.ID, *item.RequirementID)
if err != nil { if err != nil {
@@ -133,6 +235,8 @@ func (s *Service) Create(ctx context.Context, item entities.Item, stats []entiti
item.ProjectID = &reqs[0].ProjectID item.ProjectID = &reqs[0].ProjectID
requirements = reqs requirements = reqs
projects, _ = s.ProjectFetcher.FetchProjects(ctx, scope.ID, *item.ProjectID)
} }
item.ScopeID = scope.ID item.ScopeID = scope.ID
item.OwnerID = user.ID item.OwnerID = user.ID
@@ -161,7 +265,7 @@ func (s *Service) Create(ctx context.Context, item entities.Item, stats []entiti
goodStats = append(goodStats, stat) goodStats = append(goodStats, stat)
} }
result := generateResult(*newItem, scope, goodStats, requirements) result := generateResult(*newItem, scope, goodStats, requirements, projects)
return &result, nil return &result, nil
} }
@@ -197,7 +301,26 @@ func (s *Service) Update(ctx context.Context, id int, update models.ItemUpdate,
} }
item.ProjectID = genutils.Ptr(req[0].ProjectID) item.ProjectID = genutils.Ptr(req[0].ProjectID)
item.Requirement = &req[0] item.Requirement = &ResultRequirement{
ID: req[0].ID,
Name: req[0].Name,
Status: req[0].Status,
StatusName: scope.StatusName(req[0].Status),
}
projects, err := s.ProjectFetcher.FetchProjects(ctx, scope.ID, *item.ProjectID)
if err != nil {
return nil, err
}
if len(projects) == 1 {
item.Project = &ResultProject{
ID: projects[0].ID,
CreatedTime: projects[0].CreatedTime,
Name: projects[0].Name,
Status: projects[0].Status,
StatusName: scope.StatusName(projects[0].Status),
}
}
} else { } else {
item.ProjectID = nil item.ProjectID = nil
} }
@@ -211,7 +334,7 @@ func (s *Service) Update(ctx context.Context, id int, update models.ItemUpdate,
for _, stat := range stats { for _, stat := range stats {
stat.ItemID = item.ID stat.ItemID = item.ID
err = s.Repository.UpdateStat(ctx, stat) err = s.Repository.UpdateStat(ctx, stat)
if err == nil { if err == nil {
item.AddStat(scope, stat) item.AddStat(scope, stat)
+1
View File
@@ -9,6 +9,7 @@ import (
type Repository interface { type Repository interface {
Find(ctx context.Context, scopeID, projectID int) (*entities.Project, error) Find(ctx context.Context, scopeID, projectID int) (*entities.Project, error)
List(ctx context.Context, scopeID int) ([]entities.Project, error) List(ctx context.Context, scopeID int) ([]entities.Project, error)
FetchProjects(ctx context.Context, scopeID int, ids ...int) ([]entities.Project, error)
Insert(ctx context.Context, project entities.Project) (*entities.Project, error) Insert(ctx context.Context, project entities.Project) (*entities.Project, error)
Update(ctx context.Context, project entities.Project, update models.ProjectUpdate) error Update(ctx context.Context, project entities.Project, update models.ProjectUpdate) error
Delete(ctx context.Context, project entities.Project) error Delete(ctx context.Context, project entities.Project) error
+51 -19
View File
@@ -36,9 +36,9 @@ type Result struct {
entities.Project entities.Project
OwnerName string `json:"ownerName"` OwnerName string `json:"ownerName"`
StatusName string `json:"statusName"` StatusName string `json:"statusName"`
TotalAcquired int `json:"totalAcquired"` TotalAcquired float64 `json:"totalAcquired"`
TotalRequired int `json:"totalRequired"` TotalRequired float64 `json:"totalRequired"`
TotalPlanned int `json:"totalPlanned"` TotalPlanned float64 `json:"totalPlanned"`
Requirements []RequirementResult `json:"requirements"` Requirements []RequirementResult `json:"requirements"`
} }
@@ -58,9 +58,9 @@ type RequirementResult struct {
Description string `json:"description"` Description string `json:"description"`
Status models.Status `json:"status"` Status models.Status `json:"status"`
StatusName string `json:"statusName"` StatusName string `json:"statusName"`
TotalAcquired int `json:"totalAcquired"` TotalAcquired float64 `json:"totalAcquired"`
TotalRequired int `json:"totalRequired"` TotalRequired float64 `json:"totalRequired"`
TotalPlanned int `json:"totalPlanned"` TotalPlanned float64 `json:"totalPlanned"`
IsCoarse bool `json:"isCoarse"` IsCoarse bool `json:"isCoarse"`
Stats []RequirementResultStat `json:"stats"` Stats []RequirementResultStat `json:"stats"`
Items []items.Result `json:"items"` Items []items.Result `json:"items"`
@@ -142,6 +142,16 @@ func generateResult(
res.Requirements = append(res.Requirements, resReq) res.Requirements = append(res.Requirements, resReq)
} }
sort.Slice(res.Requirements, func(i, j int) bool {
ri := res.Requirements[i]
rj := res.Requirements[j]
if ri.Status != rj.Status {
return ri.Status.Less(rj.Status)
}
return ri.ID < rj.ID
})
return &res return &res
} }
@@ -185,15 +195,18 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
continue continue
} }
if item.AcquiredTime != nil { for _, stat := range item.Stats {
for _, stat := range item.Stats { if resStats[stat.ID] != nil {
if resStats[stat.ID] != nil { if item.AcquiredTime != nil {
resStats[stat.ID].Acquired += stat.Acquired resStats[stat.ID].Acquired += stat.Acquired
resStats[stat.ID].Planned += stat.Required
} }
resStats[stat.ID].Planned += stat.Required
} }
} }
item.Project = nil
item.Requirement = nil
resReq.Items = append(resReq.Items, item) resReq.Items = append(resReq.Items, item)
} }
@@ -223,19 +236,38 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
} }
} }
totalAcquired := 0.0 hasRequiredStats := false
totalRequired := 0.0
totalPlanned := 0.0
for _, stat := range resReq.Stats { for _, stat := range resReq.Stats {
if stat.Required > 0 { if stat.Required > 0 {
totalAcquired += float64(stat.Acquired) * stat.Weight hasRequiredStats = true
totalRequired += float64(stat.Required) * stat.Weight break
totalPlanned += float64(stat.Planned) * stat.Weight
} }
} }
resReq.TotalRequired += int(totalRequired)
resReq.TotalAcquired += int(math.Min(totalAcquired, totalRequired)) if hasRequiredStats {
resReq.TotalPlanned += int(math.Min(totalPlanned, totalRequired)) totalAcquired := 0.0
totalRequired := 0.0
totalPlanned := 0.0
for _, stat := range resReq.Stats {
if stat.Required > 0 {
totalAcquired += math.Min(float64(stat.Acquired), float64(stat.Required)) * stat.Weight
totalRequired += float64(stat.Required) * stat.Weight
totalPlanned += math.Min(float64(stat.Planned), float64(stat.Required)) * stat.Weight
}
}
resReq.TotalRequired += totalRequired
resReq.TotalAcquired += math.Min(totalAcquired, totalRequired)
resReq.TotalPlanned += math.Min(totalPlanned, totalRequired)
} else {
for _, item := range resReq.Items {
if item.AcquiredTime != nil {
resReq.TotalAcquired += item.WeightedRequired
}
resReq.TotalRequired += item.WeightedRequired
}
resReq.TotalPlanned = resReq.TotalRequired
}
return resReq return resReq
} }
+5 -9
View File
@@ -2,7 +2,6 @@ package scopes
import ( import (
"context" "context"
"git.aiterp.net/stufflog3/stufflog3/entities"
"sync" "sync"
) )
@@ -14,19 +13,16 @@ type Context struct {
ID int ID int
Scope Result Scope Result
statsOnce sync.Once
stats []entities.Stat
statsError error
scopesOnce sync.Once scopesOnce sync.Once
scopes []entities.Scope scopes []Result
scopesError error scopesError error
service *Service
} }
// Scopes lazy-loads the scopes in the context of the first caller. // Scopes lazy-loads the scopes in the context of the first caller.
func (c *Context) Scopes(ctx context.Context) ([]entities.Scope, error) { func (c *Context) Scopes(ctx context.Context) ([]Result, error) {
c.statsOnce.Do(func() { c.scopesOnce.Do(func() {
c.scopes, c.scopesError = c.scopesRepo.ListUser(ctx, c.userID) c.scopes, c.scopesError = c.service.List(ctx)
}) })
return c.scopes, c.scopesError return c.scopes, c.scopesError
+1
View File
@@ -68,6 +68,7 @@ func generateResult(scope entities.Scope, members []entities.ScopeMember, stats
Owner: member.Owner, Owner: member.Owner,
} }
} }
if stats != nil { if stats != nil {
res.Stats = make([]ResultStat, 0, len(stats)) res.Stats = make([]ResultStat, 0, len(stats))
for _, stat := range stats { for _, stat := range stats {
+27 -2
View File
@@ -14,7 +14,7 @@ type Service struct {
Repository Repository Repository Repository
StatsLister StatsLister StatsLister StatsLister
contextKey struct{} contextKey struct{ B int64 }
} }
func (s *Service) CreateContext(ctx context.Context, userID string, scope Result) context.Context { func (s *Service) CreateContext(ctx context.Context, userID string, scope Result) context.Context {
@@ -25,9 +25,34 @@ func (s *Service) CreateContext(ctx context.Context, userID string, scope Result
ID: scope.ID, ID: scope.ID,
Scope: scope, Scope: scope,
service: s,
}) })
} }
func (s *Service) CreateListContext(ctx context.Context, userID string, scopes []Result) context.Context {
contextData := &Context{
scopesRepo: s.Repository,
statsRepo: s.StatsLister,
userID: userID,
scopes: scopes,
scopesError: nil,
ID: -1,
Scope: Result{
Scope: entities.Scope{ID: -1},
Members: map[string]ResultMember{},
Stats: []ResultStat{},
},
service: s,
}
contextData.scopesOnce.Do(func() { /* a lot of nothing */ })
return context.WithValue(ctx, &s.contextKey, contextData)
}
func (s *Service) Context(ctx context.Context) *Context { func (s *Service) Context(ctx context.Context) *Context {
v := ctx.Value(&s.contextKey) v := ctx.Value(&s.contextKey)
if v == nil { if v == nil {
@@ -216,4 +241,4 @@ func (s *Service) Delete(ctx context.Context, scopeId int) (*Result, error) {
} }
return scope, nil return scope, nil
} }
+100 -5
View File
@@ -10,6 +10,7 @@ import (
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes" "git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"math" "math"
"sort"
"strings" "strings"
"time" "time"
) )
@@ -36,25 +37,82 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
return s.fill(ctx, *sprint, parts) return s.fill(ctx, *sprint, parts)
} }
func (s *Service) ListBetween(ctx context.Context, from, to time.Time) ([]Result, error) {
allScopes, err := s.Scopes.Context(ctx).Scopes(ctx)
if err != nil {
return nil, err
}
allSprints := make([][]entities.Sprint, len(allScopes))
eg, ctx2 := errgroup.WithContext(ctx)
for i, scope := range allScopes {
list := &allSprints[i]
scopeID := scope.ID
eg.Go(func() (err error) {
*list, err = s.Repository.ListBetween(ctx2, scopeID, from, to)
return
})
}
err = eg.Wait()
if err != nil {
return nil, err
}
sprints := genutils.Flatten2(allSprints)
sort.Slice(sprints, func(i, j int) bool {
si := sprints[i]
sj := sprints[j]
if si.FromTime.Equal(sj.FromTime) {
return si.Name < sj.Name
}
return si.FromTime.Before(sj.FromTime)
})
return s.fillMany(ctx, sprints)
}
func (s *Service) ListScopedBetween(ctx context.Context, from, to time.Time) ([]Result, error) { func (s *Service) ListScopedBetween(ctx context.Context, from, to time.Time) ([]Result, error) {
sprints, err := s.Repository.ListBetween(ctx, s.Scopes.Context(ctx).ID, from, to) sprints, err := s.Repository.ListBetween(ctx, s.Scopes.Context(ctx).ID, from, to)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return s.fillMany(ctx, sprints)
}
func (s *Service) fillMany(ctx context.Context, sprints []entities.Sprint) ([]Result, error) {
sort.Slice(sprints, func(i, j int) bool {
si := sprints[i]
sj := sprints[j]
if si.IsTimed != sj.IsTimed {
return si.IsTimed
}
if !si.FromTime.Equal(sj.FromTime) {
return si.FromTime.Before(sj.FromTime)
}
return si.ID < sj.ID
})
parts, err := s.Repository.ListParts(ctx, sprints...) parts, err := s.Repository.ListParts(ctx, sprints...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
eg, ctx := errgroup.WithContext(ctx) eg, ctx2 := errgroup.WithContext(ctx)
results := make([]Result, len(sprints)) results := make([]Result, len(sprints))
for i := range sprints { for i := range sprints {
iCopy := i iCopy := i
eg.Go(func() error { eg.Go(func() error {
res, err := s.fill(ctx, sprints[iCopy], parts) res, err := s.fill(ctx2, sprints[iCopy], parts)
if err != nil { if err != nil {
return err return err
} }
@@ -308,7 +366,28 @@ func (s *Service) Delete(ctx context.Context, id int) (*Result, error) {
func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []entities.SprintPart) (*Result, error) { func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []entities.SprintPart) (*Result, error) {
res := Result{Sprint: sprint} res := Result{Sprint: sprint}
var allStats []scopes.ResultStat
sc := s.Scopes.Context(ctx) sc := s.Scopes.Context(ctx)
if sc.ID != sprint.ScopeID {
allScopes, err := sc.Scopes(ctx)
if err != nil {
return nil, err
}
for _, scope := range allScopes {
if scope.ID == sprint.ScopeID {
allStats = scope.Stats
break
}
}
if allStats == nil {
return nil, models.NotFoundError("Scope")
}
} else {
allStats = sc.Scope.Stats
}
partIDs := make([]int, 0, len(parts)) partIDs := make([]int, 0, len(parts))
for _, part := range parts { for _, part := range parts {
if part.SprintID == sprint.ID { if part.SprintID == sprint.ID {
@@ -316,8 +395,6 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
} }
} }
allStats := sc.Scope.Stats
progressMap := make(map[int]int, len(allStats)) progressMap := make(map[int]int, len(allStats))
res.Progress = make([]ResultProgress, 0, len(allStats)) res.Progress = make([]ResultProgress, 0, len(allStats))
res.PartIDs = partIDs res.PartIDs = partIDs
@@ -325,7 +402,8 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
switch res.Kind { switch res.Kind {
case models.SprintKindItems: case models.SprintKindItems:
pickedItems, err := s.Items.ListScoped(ctx, models.ItemFilter{ pickedItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
IDs: partIDs, IDs: partIDs,
ScopeIDs: []int{sprint.ScopeID},
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -424,6 +502,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
acquiredItems, err := s.Items.ListScoped(ctx, models.ItemFilter{ acquiredItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime}, AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime},
StatIDs: partIDs, StatIDs: partIDs,
ScopeIDs: []int{sprint.ScopeID},
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -433,6 +512,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
case models.SprintKindScope: case models.SprintKindScope:
acquiredItems, err := s.Items.ListScoped(ctx, models.ItemFilter{ acquiredItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime}, AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime},
ScopeIDs: []int{sprint.ScopeID},
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -449,6 +529,21 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
} }
} }
sort.Slice(res.Items, func(i, j int) bool {
ii := res.Items[i]
ij := res.Items[j]
if ii.AcquiredTime == nil && ij.AcquiredTime == nil {
return res.Items[i].CreatedTime.Before(res.Items[j].CreatedTime)
} else if ii.AcquiredTime == nil && ij.AcquiredTime != nil {
return true
} else if ii.AcquiredTime != nil && ij.AcquiredTime == nil {
return false
} else {
return ii.AcquiredTime.Before(*ij.AcquiredTime)
}
})
// Measure progress // Measure progress
for _, item := range res.Items { for _, item := range res.Items {
if item.AcquiredTime == nil || item.AcquiredTime.Before(sprint.FromTime) || !item.AcquiredTime.Before(sprint.ToTime) { if item.AcquiredTime == nil || item.AcquiredTime.Before(sprint.FromTime) || !item.AcquiredTime.Before(sprint.ToTime) {