Compare commits

...
31 Commits
Author SHA1 Message Date
gisle 0bbfe3689e fix scope sprints progress showing as progress bars, and fix sprint progress counting to avoid counting items acquired outside of the sprint.
continuous-integration/drone/push Build is passing
2022-07-17 20:02:17 +02:00
gisle a93ff5b292 Move group to backend and change intervals and such to use 05:00 as start of day.
continuous-integration/drone/push Build is passing
2022-07-16 12:44:52 +02:00
gisle ae408390a5 optimize sprint search query. 2022-07-16 00:01:59 +02:00
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
73 changed files with 1753 additions and 667 deletions
+5
View File
@@ -64,6 +64,7 @@ func main() {
Stats: statsService,
Repository: db.Items(),
RequirementFetcher: db.Projects(),
ProjectFetcher: db.Projects(),
}
projectsService := &projects.Service{
Auth: authService,
@@ -96,6 +97,10 @@ func main() {
httpapi.Items(apiV1ScopesSub.Group("/items"), itemsService)
httpapi.Sprints(apiV1ScopesSub.Group("/sprints"), sprintsService)
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)
lambda.Start(func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
+5
View File
@@ -62,6 +62,7 @@ func main() {
Stats: statsService,
Repository: db.Items(),
RequirementFetcher: db.Projects(),
ProjectFetcher: db.Projects(),
}
projectsService := &projects.Service{
Auth: authService,
@@ -94,6 +95,10 @@ func main() {
httpapi.Items(apiV1ScopesSub.Group("/items"), itemsService)
httpapi.Sprints(apiV1ScopesSub.Group("/sprints"), sprintsService)
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)
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM)
+8
View File
@@ -25,6 +25,14 @@ type ItemStat struct {
Required int `json:"required"`
}
func (item *Item) AcquiredOutside(fromTime, toTime time.Time) bool {
return item.AcquiredTime != nil && (item.AcquiredTime.Before(fromTime) || !item.AcquiredTime.Before(toTime))
}
func (item *Item) AcquiredBetween(fromTime, toTime time.Time) bool {
return item.AcquiredTime != nil && !item.AcquiredTime.Before(fromTime) && item.AcquiredTime.Before(toTime)
}
func (item *Item) ApplyUpdate(update models.ItemUpdate) {
if update.RequirementID != nil {
if *update.RequirementID <= 0 {
+1 -1
View File
@@ -13,7 +13,7 @@ declare namespace App {
user?: import("$lib/models/user").default
idToken?: string
req?: any
groupOptions?: import("$lib/utils/items").GroupItemsOptions
groupOptions?: import("$lib/models/items").GroupItemsOptions
amplifyConfig: any
}
+2
View File
@@ -58,6 +58,8 @@ export const handle: Handle = async({ event, resolve }) => {
for (const key in newCookies) {
response.headers.append("Set-Cookie", cookie.serialize(key, newCookies[key], {
path: "/",
maxAge: 8640000,
expires: new Date(Date.now() + 8640000000),
}));
}
+18 -5
View File
@@ -1,6 +1,6 @@
import type { TimeInterval } from "$lib/models/common";
import type Item from "$lib/models/item";
import type { ItemFilter, ItemInput } from "$lib/models/item";
import type { GroupItemsOptions, ItemFilter, ItemGroup, ItemInput } from "$lib/models/item";
import type Project from "$lib/models/project";
import type { ProjectEntry, ProjectInput, Requirement, RequirementInput } from "$lib/models/project";
import type { ScopeInput } from "$lib/models/scope";
@@ -73,8 +73,19 @@ export default class SL3APIClient {
return this.fetch<{project: Project}>("PUT", `scopes/${scopeId}/projects/${projectId}`, input).then(r => r.project);
}
async listItems(scopeId: number, filter: ItemFilter): Promise<Item[]> {
return this.fetch<{items: Item[]}>("GET", `scopes/${scopeId}/items?filter=${encodeURIComponent(JSON.stringify(filter))}`).then(r => r.items);
async listItems(scopeId: number | "ALL", filter: ItemFilter, grouped: true, groupOptions: GroupItemsOptions): Promise<{items: Item[], groups: ItemGroup[]}>
async listItems(scopeId: number | "ALL", filter: ItemFilter, grouped?: false): Promise<Item[]>
async listItems(scopeId: number | "ALL", filter: ItemFilter, grouped?: boolean, groupOptions?: GroupItemsOptions): Promise<Item[] | {items: Item[], groups: ItemGroup[]}> {
const scopePrefix = (scopeId !== "ALL") ? `scopes/${scopeId}` : "all-scopes";
const groupedSuffix = !!grouped ? `&grouped=true&hideCreated=${groupOptions?.hideCreated}&hideAcquired=${groupOptions?.hideAcquired}&hideScheduled=${groupOptions?.hideScheduled}` : "";
return this.fetch<{items: Item[], groups: ItemGroup[]}>("GET", `${scopePrefix}/items?filter=${encodeURIComponent(JSON.stringify(filter))}${groupedSuffix}`).then(r => {
if (!grouped) {
return r.items
}
return {groups: r.groups, items: r.items}
});
}
async createItem(scopeId: number, input: ItemInput): Promise<Item> {
@@ -97,15 +108,17 @@ export default class SL3APIClient {
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 = "";
if (interval != null) {
const minStr = (interval.min instanceof Date) ? interval.min.toISOString() : interval.min;
const maxStr = (interval.max instanceof Date) ? interval.max.toISOString() : interval.max;
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> {
@@ -1,16 +1,32 @@
<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 value: number
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>
<div class="amount">
<span class="value">{value}&times;</span>
<div class="amount" class:right>
<span class:dark class="value">{shortestDecimal(valueRounded)}&times;</span>
<span class="label">{label}</span>
{ #if target != null && target != value }
<span class="target">(<PlusMinus offset={value - target} />)</span>
{ #if target != null && targetRounded != valueRounded }
<span class="target">(<PlusMinus offset={valueRounded - targetRounded} />)</span>
{ /if }
</div>
@@ -19,9 +35,14 @@ import PlusMinus from "../layout/PlusMinus.svelte"
div.amount {
padding-right: 1ch;
&.right {
padding-right: 0;
margin-left: auto;
}
}
span.label {
span.label, span.dark {
opacity: $opacity-entry4;
}
@@ -42,7 +42,7 @@
display: inline;
}
@media screen and (max-width: 800px) {
@media screen and (max-width: 749px) {
> div {
display: block;
}
@@ -25,7 +25,6 @@
import { getScopeContext } from "./ScopeContext.svelte";
import type Item from "$lib/models/item";
import type { ItemFilter, ItemGroup } from "$lib/models/item";
import { groupItems } from "$lib/utils/items";
export let items: Item[];
export let groups: ItemGroup[];
@@ -50,9 +49,9 @@
}
try {
const newItems = await sl3(fetch).listItems($scope.id, filter)
itemsWritable.set(newItems);
groupsWritable.set(groupItems(newItems, filter.scheduledDate.min))
const res = await sl3(fetch).listItems($scope.id, filter, true)
itemsWritable.set(res.items);
groupsWritable.set(res.groups);
} catch(_) {}
loading = false;
@@ -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>
@@ -10,8 +10,8 @@
};
const fallback: ScopeContextData = {
scope: readable({} as Scope),
lastHistoryRange: writable(""),
scope: readable({stats: []} as Scope),
lastHistoryRange: writable("last:14d"),
upsertStat: () => {},
deleteStat: () => {},
updateScope: () => {},
@@ -27,6 +27,7 @@
export let sprints: Sprint[];
export let intervalString: string;
export let global: boolean = false;
const {scope} = getScopeContext();
const {now} = getTimeContext();
@@ -46,7 +47,7 @@
}
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);
} catch(_) {}
@@ -1,69 +1,146 @@
<script lang="ts" context="module">
export interface PartOptionGroup {
id: number
label: string
subs: PartOptionGroup[]
options: PartOption[]
}
export interface PartOption {
partId: number
name: string
enabled: boolean
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 lang="ts">
import { browser } from "$app/env";
import { sl3 } from "$lib/clients/sl3";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import type Scope from "$lib/models/scope";
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";
export let value: SprintInputPart[] = [];
export let kind: SprintKind;
export let scopeId: number;
const {scope} = getScopeContext();
const {scopes} = getScopeListContext();
let error = "";
let options: PartOption[] = [];
let groups: PartOptionGroup[] = [];
let showRequired = false;
let loading = false;
let onlyActive = true;
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;
try {
switch (kind) {
case SprintKind.Items: {
const items = await sl3(fetch).listItems($scope.id, {});
options = items.map(i => ({
enabled: false,
name: `${i.name}${i.acquiredTime?" ✓":""}`,
partId: i.id,
required: 0,
const items = await sl3(fetch).listItems(scope.id, {unAcquired: onlyActive});
const projects = items.filter(i => i.project != null).map(i => i.project).sort(compareStatus).filter((p, i, a) => p.id != a[i-1]?.id);
const requirements = items.filter(i => i.requirement != null).map(i => i.requirement).sort(compareStatus).filter((r, i, a) => r.id != a[i-1]?.id);
const loose = items.filter(i => i.requirementId == null);
const reqProjectIdMap = items.filter(i => i.projectId != null && i.requirementId != null).reduce((p, c) => ({
...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;
break;
}
case SprintKind.Requirements: {
options = [];
groups = [];
throw "Requirement selection is not yet supported the UI."
}
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;
break;
}
case SprintKind.Scope: {
options = [];
groups = [];
showRequired = false;
break;
}
}
error = "";
for (const opt of options) {
const val = value.find(v => v.partId === opt.partId);
if (val != null) {
opt.enabled = true;
opt.required = val.required;
for (const group of groups) {
for (const opt of group.options) {
const val = value.find(v => v.partId === opt.partId);
if (val != null) {
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) {
@@ -74,26 +151,55 @@
loading = false;
}
$: if (loaded === kind && error === "") {
value = options.filter(o => o.enabled).map(o => ({partId: o.partId, required: o.required}))
// Hax to make it reload if onlyActive changed.
$: 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) {
load(kind);
load(kind, actualScope);
}
</script>
<div class="part-input">
<div class="error">{error}</div>
{#each 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 groups as group (group.id)}
<div class="label">{group.label}</div>
{#each group.subs as group (group.id)}
<div class="label label-2">{group.label}</div>
{#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 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}
{#if kind === SprintKind.Items}
<hr />
<Checkbox label="Show only active items" bind:checked={onlyActive} />
{/if}
</div>
<style lang="scss">
@@ -103,7 +209,6 @@
display: flex;
margin: 0;
padding: 0;
border-top: 1px solid $color-entry1;
&:first-of-type {
border-top: none;
@@ -123,7 +228,7 @@
}
div {
padding: 0.3em 0.5ch;
padding: 0.25em 0.5ch;
&.disabled {
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 {
width: calc(100% - 2ch);
margin-bottom: 1em;
margin-top: 0.20em;
height: 12em;
height: 26em;
overflow-y: auto;
background: $color-entryhalf;
@@ -1,7 +1,9 @@
<script lang="ts">
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
import type { StatValueInput } from "$lib/models/stat";
import Checkbox from "../layout/Checkbox.svelte";
import type Scope from "$lib/models/scope";
export let value: StatValueInput[] = [];
export let showAcquired = false;
@@ -10,12 +12,17 @@
export let zeroDeletes = 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 requiredMap: Record<number, number>;
let enabledMap: Record<number, boolean>;
let actualScope: Scope;
function initialize() {
function initialize(actualScope: Scope) {
acquiredMap = {};
requiredMap = {};
enabledMap = {};
@@ -27,33 +34,35 @@
acquiredMap[v.statId] = v.acquired;
}
} else {
for (const stat of $scope.stats) {
for (const stat of actualScope.stats) {
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;
acquiredMap[stat.id] = found?.acquired || 0;
}
}
}
$: actualScope = overrideScopeId !== null && $scopes.length > 0 ? $scopes.find(s => s.id === overrideScopeId) : $scope;
$: {
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,
required: requiredMap[s.id],
acquired: acquiredMap[s.id],
}));
const {scope} = getScopeContext();
$: console.log(value);
</script>
<div class="stat-input">
{#each $scope.stats as stat (stat.id)}
{#each actualScope.stats as stat (stat.id)}
{#if enabledMap[stat.id] != null}
<div class="stat">
{#if !noToggle}
@@ -1,6 +1,6 @@
<script lang="ts">
import { formatFormTime } from "$lib/utils/date";
import parseInterval from "$lib/utils/timeinterval";
import parseInterval, { morningInterval } from "$lib/utils/timeinterval";
export let from: string;
export let to: string;
@@ -9,7 +9,7 @@
$: {
if (intervalName !== "specific_dates") {
const int = parseInterval(intervalName, openDate);
const int = morningInterval(parseInterval(intervalName, openDate));
from = formatFormTime(int.min);
to = formatFormTime(int.max);
}
@@ -19,10 +19,10 @@
<style lang="sass">
div.desktop
@media screen and (max-width: 800px)
@media screen and (max-width: 749px)
display: none
div.mobile
@media screen and (min-width: 800px)
@media screen and (min-width: 750px)
display: none
</style>
@@ -14,6 +14,6 @@
flex-basis: 50
&.hideMobile
@media screen and (max-width: 800px)
@media screen and (max-width: 749px)
display: none
</style>
@@ -25,7 +25,7 @@
&.fullwidth
width: 100%
@media screen and (max-width: 800px)
@media screen and (max-width: 749px)
width: 100%
flex-direction: column
@@ -42,6 +42,7 @@
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
import { faCheckToSlot } from "@fortawesome/free-solid-svg-icons/faCheckToSlot";
import { faEye } from "@fortawesome/free-solid-svg-icons/faEye";
const icons = {
"clock": faClock,
@@ -66,6 +67,7 @@
"chevron_down": faChevronDown,
"trash": faTrash,
"check_slot": faCheckToSlot,
"eye": faEye,
};
export type IconName = keyof typeof icons;
@@ -1,9 +1,12 @@
<script lang="ts">
import shortestDecimal from "$lib/utils/numbers";
export let offset: number;
let value: string;
let negative: boolean
$: value = (offset > 0) ? `+${offset.toFixed()}` : offset.toFixed();
$: value = (offset > 0) ? `+${shortestDecimal(offset)}` : shortestDecimal(offset);
$: negative = offset < 0
</script>
@@ -11,9 +11,10 @@
export let noProgress = false;
export let icon: IconName = null;
export let status: Status = null;
export let anchor: string = "";
</script>
<div class="sl3-entry">
<div id={anchor} class="sl3-entry">
<div class="header">
<div class="entry-icon">
{#if icon != null}
@@ -44,33 +45,20 @@
opacity: 0.6
margin-right: 0.5ch
font-size: 1em
margin-top: 0.225em
margin-top: 0.15em
&:empty
display: none
div.header
display: flex
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
line-height: 1.25em
div.right
color: $color-entry2-transparent
margin-left: auto
margin-top: auto
margin-bottom: 0
&.noProgress
margin-top: -0.25em
@@ -43,6 +43,7 @@
div.sub-section
margin: 0.5em 0.4ch
padding: 0.33em 0.75ch
padding-bottom: 0.25em
background: $color-entry1-transparent
border-radius: 0.125em
border-bottom-right-radius: 0.5em
@@ -10,17 +10,21 @@
import type { IconName } from "../layout/Icon.svelte";
import Icon from "../layout/Icon.svelte";
import { formatTime } from "$lib/utils/date";
import RequirementReference from "./RequirementReference.svelte";
export let item: Item;
export let event: "created" | "scheduled" | "acquired" | "none" = "none";
export let compact = false;
export let showAcquiredTime = false;
export let hideStats = false;
let icon: IconName
let status: Status
let eventText: string
$: {
icon = void(0);
if (event === "none" || event === "acquired") {
if (item.acquiredTime) {
icon = "check";
@@ -51,22 +55,33 @@
<Option open={{name: "item.delete", item}} color="red"><Icon name="trash" /></Option>
{/if}
</OptionsRow>
{#if item.project != null}
<RequirementReference project={item.project} requirement={item.requirement} scopeId={item.scopeId} />
{/if}
{#if !compact}
<Markdown source={item.description} />
{/if}
<AmountRow>
{#if item.acquiredTime != null}
{#each item.stats as stat (stat.id)}
{#if stat.required > 0}
<Amount label={stat.name} value={stat.acquired} target={stat.required} />
{#if !hideStats}
{#if item.acquiredTime != null}
{#each item.stats as stat (stat.id)}
{#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}
{/each}
{:else}
{#each item.stats as stat (stat.id)}
{#if stat.required > 0}
<Amount label={stat.name} value={stat.required} />
{:else}
{#each item.stats as stat (stat.id)}
{#if stat.required > 0}
<Amount label={stat.name} value={stat.required} />
{/if}
{/each}
{#if item.weightedRequired}
<Amount dark right label="" value={item.weightedRequired} />
{/if}
{/each}
{/if}
{/if}
</AmountRow>
</SubSection>
@@ -5,6 +5,7 @@
import Option from "$lib/components/layout/Option.svelte";
import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
import { getProjectContext } from "../contexts/ProjectContext.svelte";
import Icon from "../layout/Icon.svelte";
import RequirementEntry from "./RequirementSection.svelte";
const {project} = getProjectContext();
@@ -15,9 +16,9 @@
<Progress alwaysSmooth titlePercentageOnly thinner gray count={$project.totalPlanned} target={$project.totalRequired} />
<Markdown source={$project.description} />
<OptionsRow slot="right">
<Option open={{name: "requirement.create", project: $project}}>Add Requirement</Option>
<Option open={{name: "project.edit", project: $project}}>Edit</Option>
<Option open={{name: "project.delete", project: $project}} color="red">Delete</Option>
<Option open={{name: "requirement.create", project: $project}}><Icon name="plus" /></Option>
<Option open={{name: "project.edit", project: $project}}><Icon name="pen" /></Option>
<Option open={{name: "project.delete", project: $project}} color="red"><Icon name="trash" /></Option>
</OptionsRow>
{#each $project.requirements as requirement (requirement.id)}
<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 { STATUS_ICONS } from "../common/StatusIcon.svelte";
import ItemEntry from "./ItemSubSection.svelte";
import Icon from "../layout/Icon.svelte";
import { projectPrettyId } from "$lib/utils/prettyIds";
export let requirement: Requirement;
</script>
<div id={projectPrettyId(requirement)} />
<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 thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} />
<Markdown source={requirement.description} />
<OptionsRow slot="right">
<Option open={{name: "item.create", requirement}}>Add Item</Option>
<Option open={{name: "requirement.edit", requirement}}>Edit</Option>
<Option open={{name: "requirement.delete", requirement}} color="red">Delete</Option>
<Option open={{name: "item.create", requirement}}><Icon name="plus" /></Option>
<Option open={{name: "requirement.edit", requirement}}><Icon name="pen" /></Option>
<Option open={{name: "requirement.delete", requirement}} color="red"><Icon name="trash" /></Option>
</OptionsRow>
<LabeledProgressRow>
{#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";
export let sprint: Sprint
export let compact: boolean = false;
let timeRange: [Date, Date];
@@ -34,12 +35,18 @@
{/if}
{#if sprint.kind === SprintKind.Items}
{#each (sprint.items||[]) as item (item.id)}
{#if !item.acquiredTime}
<ItemSubSection compact item={item} />
{#if !compact || !item.acquiredTime}
<ItemSubSection compact={compact} hideStats={compact} item={item} />
{/if}
{/each}
{:else}
<AmountRow>
<Amount label="Acquired Items" value={(sprint.items||[]).length} />
</AmountRow>
{#if compact}
<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}
@@ -1,27 +1,20 @@
<script lang="ts">
import { goto } from "$app/navigation";
import type Sprint from "$lib/models/sprint";
import Option from "../layout/Option.svelte";
import OptionsRow from "../layout/OptionsRow.svelte";
import SprintBody from "./SprintBody.svelte";
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds";
import { getScopeContext } from "../contexts/ScopeContext.svelte";
import { projectPrettyId } from "$lib/utils/prettyIds";
import Section from "../layout/Section.svelte";
import Icon from "../layout/Icon.svelte";
export let sprint: Sprint;
const {scope} = getScopeContext();
function gotoSprint() {
goto(`/${scopePrettyId($scope)}/sprints#${projectPrettyId(sprint)}`)
}
</script>
<Section noProgress title={sprint.name}>
<Section anchor={projectPrettyId(sprint)} noProgress title={sprint.name}>
<OptionsRow slot="right">
<Option on:click={gotoSprint}>View</Option>
<Option open={{name: "sprint.edit", sprint}}>Edit</Option>
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option>
<Option open={{name: "sprint.edit", sprint}}><Icon name="pen" /></Option>
<Option open={{name: "sprint.delete", sprint}} color="red"><Icon name="trash" /></Option>
</OptionsRow>
<SprintBody sprint={sprint} />
</Section>
@@ -8,20 +8,28 @@
import SprintBody from "./SprintBody.svelte";
import { projectPrettyId, scopePrettyId } from "$lib/utils/prettyIds";
import { getScopeContext } from "../contexts/ScopeContext.svelte";
import { getScopeListContext } from "../contexts/ScopeListContext.svelte";
import Icon from "../layout/Icon.svelte";
export let sprint: Sprint;
const {scope} = getScopeContext();
const {scope, lastHistoryRange} = getScopeContext();
const {scopes} = getScopeListContext();
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>
<SubSection noProgress title={sprint.name}>
<OptionsRow slot="right">
<Option on:click={gotoSprint}>View</Option>
<Option open={{name: "sprint.edit", sprint}}>Edit</Option>
<Option open={{name: "sprint.delete", sprint}} color="red">Delete</Option>
<Option on:click={gotoSprint}><Icon name="eye" /></Option>
<Option open={{name: "sprint.edit", sprint}}><Icon name="pen" /></Option>
<Option open={{name: "sprint.delete", sprint}} color="red"><Icon name="trash" /></Option>
</OptionsRow>
<SprintBody sprint={sprint} />
<SprintBody compact sprint={sprint} />
</SubSection>
@@ -3,6 +3,7 @@
import Amount from "../common/Amount.svelte";
import AmountRow from "../common/AmountRow.svelte";
import Markdown from "../common/Markdown.svelte";
import Icon from "../layout/Icon.svelte";
import Option from "../layout/Option.svelte";
import OptionsRow from "../layout/OptionsRow.svelte";
import SubSection from "../layout/SubSection.svelte";
@@ -12,8 +13,8 @@
<SubSection noProgress title={stat.name}>
<OptionsRow slot="right">
<Option open={{name: "stat.edit", stat}}>Edit</Option>
<Option open={{name: "stat.delete", stat}} color="red">Delete</Option>
<Option open={{name: "stat.edit", stat}}><Icon name="pen" /></Option>
<Option open={{name: "stat.delete", stat}} color="red"><Icon name="trash" /></Option>
</OptionsRow>
<Markdown source={stat.description} />
{#if stat.weight !== 1}
+15 -6
View File
@@ -11,7 +11,8 @@
import { sl3 } from "$lib/clients/sl3";
import Modal from "$lib/components/common/Modal.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 { getProjectContext } from "$lib/components/contexts/ProjectContext.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 {reloadSprintList} = getSprintListContext();
const {reloadItemList} = getItemListContext();
const {reloadItemLists} = getItemMultiListContext();
let endpoint: string;
@@ -49,7 +51,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
break;
case "item.delete":
init(`items/${$currentModal.item.id}`)
init(`items/${$currentModal.item.id}`, [], $currentModal.item.scopeId)
noun = "Item";
name = $currentModal.item.name;
break;
@@ -62,7 +64,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
break;
case "sprint.delete":
init(`sprints/${$currentModal.sprint.id}`)
init(`sprints/${$currentModal.sprint.id}`, [], $currentModal.sprint.scopeId)
noun = "Sprint";
name = $currentModal.sprint.name;
break;
@@ -72,7 +74,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
$currentModal.requirement.items.map(i => ({
op: "Detach item",
name: i.name,
}))
})),
);
noun = "Requirement";
name = $currentModal.requirement.name;
@@ -100,8 +102,13 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
show = false;
}
function init(newEndpoint: string, newSideEffects: DeletionSideEffect[] = []) {
endpoint = `scopes/${$scope.id}/${newEndpoint}`;
function init(newEndpoint: string, newSideEffects: DeletionSideEffect[] = [], overrideScopeId?: number) {
let scopeId = $scope.id;
if (overrideScopeId != null) {
scopeId = overrideScopeId;
}
endpoint = `scopes/${scopeId}/${newEndpoint}`;
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
@@ -131,6 +138,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
await reloadProject();
if (noun === "Stat") {
reloadItemLists();
deleteStat((res as {stat: Stat}).stat);
}
@@ -139,6 +147,7 @@ import { getItemListContext } from "$lib/components/contexts/ItemListContext.sve
}
if (noun === "Item") {
reloadItemLists();
await reloadItemList();
}
@@ -4,24 +4,24 @@
import Modal from "$lib/components/common/Modal.svelte";
import ModalBody from "$lib/components/common/ModalBody.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 { 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 StatInput from "$lib/components/controls/StatInput.svelte";
import type Item from "$lib/models/item";
import type { ItemInput } from "$lib/models/item";
import type Scope from "$lib/models/scope";
const {currentModal, closeModal} = getModalContext();
const {scope} = getScopeContext();
const {reloadProject} = getProjectContext();
const {reloadItemList} = getItemListContext();
const {reloadSprintList} = getSprintListContext();
const {reloadItemLists} = getItemMultiListContext();
let item: Partial<ItemInput>
let itemId: number
let scopeId: number
let requirementId: number
let openedDate: Date
@@ -31,7 +31,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
$: switch ($currentModal.name) {
case "item.acquire":
init($scope, $currentModal.item)
init($currentModal.item)
break;
default:
@@ -40,7 +40,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
show = false;
}
function init(scope: Scope, unaquiredItem: Item) {
function init(unaquiredItem: Item) {
openedDate = new Date();
item = {
@@ -50,6 +50,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
}
show = true;
scopeId = unaquiredItem.scopeId;
itemId = unaquiredItem.id;
requirementId = unaquiredItem.requirementId;
}
@@ -59,7 +60,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
loading = true;
try {
await sl3(fetch).updateItem($scope.id, itemId, {
await sl3(fetch).updateItem(scopeId, itemId, {
...item,
acquiredTime: new Date(item.acquiredTime).toISOString(),
});
@@ -69,6 +70,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
await reloadProject();
}
reloadItemLists();
await reloadItemList();
await reloadSprintList();
@@ -94,7 +96,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
<label for="description">Description</label>
<textarea name="description" bind:value={item.description} />
<label for="stats">Stats</label>
<StatInput showAcquired hideUnseen noToggle bind:value={item.stats} />
<StatInput overrideScopeId={scopeId} showAcquired hideUnseen noToggle bind:value={item.stats} />
</ModalBody>
</Modal>
</form>
+20 -6
View File
@@ -3,11 +3,13 @@
import Modal from "$lib/components/common/Modal.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 { getProjectContext } from "$lib/components/contexts/ProjectContext.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 StatInput from "$lib/components/controls/StatInput.svelte";
import type Item from "$lib/models/item";
@@ -19,12 +21,15 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
const {currentModal, closeModal} = getModalContext();
const {scope} = getScopeContext();
const {scopes} = getScopeListContext();
const {project, reloadProject} = getProjectContext();
const {reloadItemList} = getItemListContext();
const {reloadSprintList} = getSprintListContext();
const {reloadItemLists} = getItemMultiListContext();
let item: ItemInput
let itemId: number
let scopeId: number
let currentStats: ItemInput["stats"]
let openedDate: Date
let requirementName: string
@@ -69,6 +74,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
}
op = "Create"
scopeId = scope.id;
openedDate = new Date();
requirementName = requirement?.name;
show = true;
@@ -77,8 +83,13 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
function initEdit(current: Item, requirement?: Requirement, scope?: Scope) {
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 = scope.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}));
@@ -91,6 +102,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
scheduledDate: current.scheduledDate,
};
itemId = current.id;
scopeId = current.scopeId;
currentStats = [...item.stats];
op = "Edit"
@@ -122,7 +134,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
}
}
await sl3(fetch).createItem($scope.id, submission);
await sl3(fetch).createItem(scopeId, submission);
break;
case "Edit":
if (!submission.acquiredTime) {
@@ -133,7 +145,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
}
submission.stats = statDiff(currentStats, submission.stats)
await sl3(fetch).updateItem($scope.id, itemId, submission);
await sl3(fetch).updateItem(scopeId, itemId, submission);
break;
}
@@ -144,6 +156,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
await reloadItemList();
await reloadSprintList();
reloadItemLists();
closeModal();
} catch(err) {
@@ -177,6 +190,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
<label for="stats">Stats</label>
<StatInput
zeroDeletes showRequired
overrideScopeId={scopeId}
initOnList={initOnList}
showAcquired={op === "Edit" && !!item.acquiredTime}
hideUnseen={!!item.requirementId}
@@ -22,6 +22,7 @@ import { partsDiff } from "$lib/utils/sprint";
let sprint: SprintInput
let sprintId: number
let scopeId: number
let oldParts: SprintInputPart[]
let intervalName: string;
@@ -60,6 +61,7 @@ import { partsDiff } from "$lib/utils/sprint";
parts: [],
}
intervalName = "this_month";
scopeId = scope.id;
op = "Create"
openedDate = new Date();
@@ -80,6 +82,7 @@ import { partsDiff } from "$lib/utils/sprint";
isUnweighted: current.isUnweighted,
};
sprintId = current.id;
scopeId = current.scopeId;
intervalName = "specific_dates";
if (sprint.kind === SprintKind.Stats) {
@@ -107,16 +110,16 @@ import { partsDiff } from "$lib/utils/sprint";
try {
switch (op) {
case "Create":
await sl3(fetch).createSprint($scope.id, submission);
await sl3(fetch).createSprint(scopeId, submission);
break;
case "Edit":
await sl3(fetch).updateSprint($scope.id, sprintId, submission);
await sl3(fetch).updateSprint(scopeId, sprintId, submission);
const {added, removed} = partsDiff(oldParts, submission.parts)
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) {
await sl3(fetch).deleteSprintPart($scope.id, sprintId, part).catch(() => {});
await sl3(fetch).deleteSprintPart(scopeId, sprintId, part).catch(() => {});
}
break;
@@ -153,15 +156,15 @@ import { partsDiff } from "$lib/utils/sprint";
<label for="aggregateValue">Aggregate Goal</label>
<input name="aggregateValue" type="number" bind:value={sprint.aggregateRequired} />
{/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>
<label for="kind">Kind</label>
<SprintKindSelect disabled={op === "Edit"} bind:kind={sprint.kind} />
<label for="parts">Parts</label>
<PartInput bind:value={sprint.parts} kind={sprint.kind} />
<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" />
<PartInput bind:value={sprint.parts} kind={sprint.kind} scopeId={scopeId} />
</ModalBody>
</Modal>
</form>
@@ -103,7 +103,7 @@ import Checkbox from "$lib/components/layout/Checkbox.svelte";
</script>
<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>
<label for="name">Name</label>
<input name="name" type="text" bind:value={stat.name} />
+30
View File
@@ -1,5 +1,6 @@
import type { TimeInterval } from "./common";
import type { StatValueInput, StatProgress } from "./stat";
import type Status from "./status";
export default interface Item {
id: number
@@ -13,6 +14,25 @@ export default interface Item {
acquiredTime?: string
scheduledDate?: string
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 {
@@ -36,6 +56,8 @@ export interface ItemFilter {
createdTime?: TimeInterval<Date | string>
acquiredTime?: TimeInterval<Date | string>
loose?: boolean
unAcquired?: boolean
unScheduled?: boolean
}
export interface ItemGroup {
@@ -46,9 +68,17 @@ export interface ItemGroup {
export interface ItemGroupReference {
idx: number
event: "created" | "acquired" | "scheduled"
time: string
sortKey?: string
}
export interface GroupItemsOptions {
hideCreated?: boolean
hideAcquired?: boolean
hideScheduled?: boolean
}
// this_week
// this_month
// all_time
-71
View File
@@ -1,71 +0,0 @@
import type { ItemGroup, ItemGroupReference } from "$lib/models/item";
import type Item from "$lib/models/item";
import { formatDate, formatDateTime, formatTime, formatWeekdayDate } from "./date";
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> = {};
for (let i = 0; i < items.length; ++i) {
const item = items[i];
const createKey = formatDate(item.createdTime)
const createSortKey = `${createKey} ${formatTime(item.createdTime)}`
if (!opts.hideCreated) {
if (!item.acquiredTime || item.acquiredTime >= item.createdTime) {
addItem(groups, createKey, i, createSortKey, item, "created");
}
}
if (!opts.hideScheduled) {
if (item.scheduledDate) {
addItem(groups, item.scheduledDate, i, `${item.scheduledDate} 23:59:59`, item, "scheduled");
}
}
if (!opts.hideAcquired) {
if (item.acquiredTime) {
addItem(groups, formatDate(item.acquiredTime), i, formatDateTime(item.acquiredTime), item, "acquired");
}
}
}
return Object.keys(groups).sort((a,b) => (
b.localeCompare(a)
)).map(k => ({
...groups[k],
list: groups[k].list.sort((a, b) => {
return b.sortKey.localeCompare(a.sortKey);
}).map(r => ({...r, sortKey: void(0)})),
})).filter(g => minDate == null || g.label > minDate);
}
function addItem(groups: Record<string, ItemGroup>, key: string, index: number, sortKey: string, item: Item, event: ItemGroupReference["event"]) {
if (groups[key] == null) {
groups[key] = {
label: formatWeekdayDate(sortKey),
list: [],
}
}
const i = groups[key].list.findIndex(r => r.idx === index)
if (i == -1) {
groups[key].list.push({
idx: index,
event: event,
sortKey: sortKey,
})
} else {
groups[key].list[i].event = event;
groups[key].list[i].sortKey = sortKey;
}
}
+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) {
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);
}
}
+13
View File
@@ -55,6 +55,19 @@ export default function parseInterval(s: string, date: Date): TimeInterval<Date>
case "last_year":
return {display: "year", min: startOfYear(addYears(date, -1)), max: startOfYear(date)}
}
return null
}
export function morningInterval(interval: TimeInterval<Date>) {
if (!interval) {
return null
}
return {
min: new Date(interval.min.getTime() + 3600000 * 5),
max: new Date(interval.max.getTime() + 3600000 * 5),
}
}
export function intervalLabel(s: string): string {
@@ -26,12 +26,12 @@
import ProjectListContext from "$lib/components/contexts/ProjectListContext.svelte";
import ScopeMenu from "$lib/components/scope/ScopeMenu.svelte";
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 projects: ProjectEntry[];
let hideMobile;
let hideMobile: boolean;
$: hideMobile = $page.url.pathname !== `/${scopePrettyId(scope)}`
</script>
@@ -3,7 +3,6 @@
import { sl3 } from "$lib/clients/sl3";
import parseInterval, { datesOf, intervalLabel } from "$lib/utils/timeinterval";
import { groupItemOptionsQuery, groupItems, type GroupItemsOptions } from "$lib/utils/items";
import type Item from "$lib/models/item";
import type { ItemFilter, ItemGroup } from "$lib/models/item";
@@ -16,13 +15,12 @@
acquiredTime: interval,
scheduledDate: datesOf(interval),
};
const items = await sl3(fetch).listItems(scopeId, filter);
const {items, groups} = await sl3(fetch).listItems(scopeId, filter, true, session.groupOptions);
return {
props: {
items, filter,
items, groups, filter,
intervalString: params.interval,
groups: groupItems(items, datesOf(interval)?.min, session.groupOptions || {}),
},
};
}
@@ -41,7 +39,6 @@
import ItemListContext from "$lib/components/contexts/ItemListContext.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 items: Item[]
@@ -1,24 +1,41 @@
<script lang="ts" context="module">
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: morningInterval(parseInterval("today", now)),
},
looseFilter: {
loose: true,
unAcquired: true,
unScheduled: true,
},
}
}
export const load: Load = async({params, url, fetch}) => {
const scopeId = parseInt(params.scope.split("-")[0]);
const scheduledItems = (await sl3(fetch).listItems(scopeId, {
scheduledDate: datesOf(parseInterval("next:7d", new Date)),
}))
const acquiredItems = (await sl3(fetch).listItems(scopeId, {
acquiredTime: parseInterval("today", new Date),
}))
const {acquiredFilter, looseFilter, scheduledFilter} = generateItemFilters(new Date());
const scheduledItems = (await sl3(fetch).listItems(scopeId, scheduledFilter));
const acquiredItems = (await sl3(fetch).listItems(scopeId, acquiredFilter));
const looseItems = (await sl3(fetch).listItems(scopeId, looseFilter));
const sprints = await sl3(fetch).listSprints(scopeId);
return {
props: {scheduledItems, acquiredItems, sprints}
props: {scheduledItems, acquiredItems, looseItems, sprints}
};
}
</script>
<script lang="ts">
import { sl3 } from "$lib/clients/sl3";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import Column from "$lib/components/layout/Column.svelte";
import Columns from "$lib/components/layout/Columns.svelte";
@@ -28,10 +45,8 @@
import StatSubSection from "$lib/components/scope/StatSubSection.svelte";
import DeletionModal from "$lib/modals/DeletionModal.svelte";
import StatCreateEditModal from "$lib/modals/StatCreateEditModal.svelte";
import { sl3 } from "$lib/clients/sl3";
import parseInterval, { datesOf } from "$lib/utils/timeinterval";
import parseInterval, { datesOf, morningInterval } from "$lib/utils/timeinterval";
import type Item from "$lib/models/item";
import ItemSubSection from "$lib/components/project/ItemSubSection.svelte";
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
import Card from "$lib/components/common/Card.svelte";
import CardHeader from "$lib/components/common/CardHeader.svelte";
@@ -43,13 +58,20 @@
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
import ProjectCreateEditModal from "$lib/modals/ProjectCreateEditModal.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 scheduledItems: Item[];
export let looseItems: Item[];
export let sprints: Sprint[];
const {scope} = getScopeContext();
const {openModal} = getModalContext();
const {now} = getTimeContext();
function openCreateProject() {
openModal({name: "project.create"});
@@ -68,53 +90,44 @@
}
</script>
<SprintListContext sprints={sprints} intervalString="">
<Columns fullwidth>
<Column>
<Row title="Options">
<Card on:click={openCreateProject} pointerCursor><CardHeader>Create Project</CardHeader></Card>
<Card on:click={openPostItem} pointerCursor><CardHeader>Post Item</CardHeader></Card>
<Card on:click={openEditScope} pointerCursor><CardHeader>Edit Scope</CardHeader></Card>
<Card on:click={openDeleteScope} pointerCursor><CardHeader>Delete Scope</CardHeader></Card>
</Row>
{#if scheduledItems.length > 0}
<Row title="Schedule">
{#each scheduledItems as item (item.id)}
<ItemSubSection item={item} />
<ItemMultiListContext lists={{acquiredItems, scheduledItems, looseItems}} filters={generateItemFilters($now)}>
<SprintListContext sprints={sprints} intervalString="">
<Columns fullwidth>
<Column>
<Row title="Options">
<Card on:click={openCreateProject} pointerCursor><CardHeader>Create Project</CardHeader></Card>
<Card on:click={openPostItem} pointerCursor><CardHeader>Create Item</CardHeader></Card>
<Card on:click={openEditScope} pointerCursor><CardHeader>Edit Scope</CardHeader></Card>
<Card on:click={openDeleteScope} pointerCursor><CardHeader>Delete Scope</CardHeader></Card>
</Row>
<ItemListRow title="Scheduled" key="scheduleItems" />
<ItemListRow title="Today" key="acquiredItems" showAcquiredTime />
<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}
</Row>
{/if}
{#if acquiredItems.length > 0}
<Row title="Today">
{#each acquiredItems as item (item.id)}
<ItemSubSection showAcquiredTime item={item} />
{/each}
</Row>
{/if}
</Column>
<Column>
<Row title="Sprints">
<OptionsRow slot="right">
<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>
</Column>
</Columns>
<ItemCreateModal />
<ItemAcquireModal />
<StatCreateEditModal />
<DeletionModal />
<SprintCreateUpdateModal />
<ProjectCreateEditModal />
<ScopeCreateUpdateModal />
</SprintListContext>
</ItemMultiListContext>
+76 -19
View File
@@ -2,11 +2,34 @@
import type { Load } from "@sveltejs/kit/types/internal";
import { sl3 } from "$lib/clients/sl3";
export const load: Load = async({ fetch, session }) => {
function generateItemFilters(now: Date): {scheduledFilter: ItemFilter, acquiredFilter: ItemFilter, looseFilter: ItemFilter} {
return {
scheduledFilter: {
scheduledDate: datesOf(parseInterval("next:7d", now)),
},
acquiredFilter: {
acquiredTime: morningInterval(parseInterval("today", now)),
},
looseFilter: {
loose: true,
unAcquired: true,
unScheduled: true,
},
}
}
export const load: Load = async({ fetch }) => {
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 {
props: { scopes }
props: { scopes, scheduledItems, acquiredItems, looseItems, sprints }
};
}
</script>
@@ -21,26 +44,60 @@
import ScopeCreateUpdateModal from "$lib/modals/ScopeCreateUpdateModal.svelte";
import OptionsRow from "$lib/components/layout/OptionsRow.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, morningInterval } 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 acquiredItems: Item[];
export let scheduledItems: Item[];
export let looseItems: Item[];
export let sprints: Sprint[];
const {now} = getTimeContext();
</script>
<Header fullwidth wide subtitle="Logging your stuff">Stufflog</Header>
<ScopeListContext scopes={scopes}>
<Columns wide>
<Column>
<Row title="Scopes">
<OptionsRow slot="right">
<Option open={{name: "scope.create"}}>Create</Option>
</OptionsRow>
<ScopeLinkList />
</Row>
</Column>
<Column>
</Column>
</Columns>
<ScopeCreateUpdateModal />
</ScopeListContext>
<ItemMultiListContext global lists={{acquiredItems, scheduledItems, looseItems}} filters={generateItemFilters($now)}>
<ScopeListContext scopes={scopes}>
<SprintListContext global sprints={sprints} intervalString="">
<Columns wide>
<Column>
<Row title="Scopes">
<OptionsRow slot="right">
<Option open={{name: "scope.create"}}><Icon name="plus" /></Option>
</OptionsRow>
<ScopeLinkList />
</Row>
<ItemListRow title="Scheduled" key="scheduleItems" />
<ItemListRow title="Today" key="acquiredItems" showAcquiredTime />
<ItemListRow title="Loose" key="looseItems" />
</Column>
<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 (
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/awslabs/aws-lambda-go-api-proxy v0.13.3
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
github.com/gin-gonic/gin v1.7.7
github.com/go-sql-driver/mysql v1.6.0
@@ -13,8 +15,6 @@ 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/gin-contrib/sse v0.1.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/reflect2 v1.0.2 // 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
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // 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.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.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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.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.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.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
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.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.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
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/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/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
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.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
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/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.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
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/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.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.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
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/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 v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
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 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
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/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.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
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.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.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
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/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.16.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/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
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/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/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 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
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-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-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-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
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-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-20220617184016-355a448f1bc9 h1:Yqz/iviulwKwAREEeUd3nbBFn0XuyJqkoft2IlrvOhc=
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-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-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-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-20220111092808-5a964db01320/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.5/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/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=
@@ -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-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-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
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.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.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
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 v1.0.0-20180628173108-788fd7840127/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.66.4/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/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.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.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/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-20200615113413-eeeca48fe776/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
}
+9
View File
@@ -8,3 +8,12 @@ func Map[I any, O any](arr []I, cb func(a I) O) []O {
return res
}
func MapValues[K comparable, V any](m map[K]V) []V {
res := make([]V, 0, len(m))
for _, v := range m {
res = append(res, v)
}
return res
}
+8
View File
@@ -46,6 +46,14 @@ func (d Date) ToTime() time.Time {
return time.Date(d[0], time.Month(d[1]), d[2], 0, 0, 0, 0, time.UTC)
}
func (d Date) WithTime(hh, mm, ss int) time.Time {
return d.WithTimeAndLocation(hh, mm, ss, time.UTC)
}
func (d Date) WithTimeAndLocation(hh, mm, ss int, loc *time.Location) time.Time {
return time.Date(d[0], time.Month(d[1]), d[2], hh, mm, ss, 0, loc)
}
func (d *Date) UnmarshalJSON(b []byte) error {
var str string
err := json.Unmarshal(b, &str)
+2
View File
@@ -25,4 +25,6 @@ type ItemFilter struct {
RequirementIDs []int `json:"requirementIds,omitempty"`
StatIDs []int `json:"statIds,omitempty"`
Loose bool `json:"loose,omitempty"`
UnScheduled bool `json:"unScheduled,omitempty"`
UnAcquired bool `json:"unAcquired,omitempty"`
}
+9
View File
@@ -55,6 +55,10 @@ func getterHandler[O any](key, idKey string, callback func(ctx context.Context,
}
func handler(key string, callback func(c *gin.Context) (interface{}, error)) gin.HandlerFunc {
type overrider interface {
OverrideJSONResponse() bool
}
return func(c *gin.Context) {
res, err := callback(c)
if err != nil {
@@ -82,6 +86,11 @@ func handler(key string, callback func(c *gin.Context) (interface{}, error)) gin
return
}
if o, ok := res.(overrider); ok && o.OverrideJSONResponse() {
c.JSON(200, res)
return
}
resJson := make(map[string]interface{}, 1)
resJson[key] = res
+254 -193
View File
@@ -12,209 +12,75 @@ import (
"time"
)
func Items(g *gin.RouterGroup, items *items.Service) {
type groupedRes struct {
Items []items.Result `json:"items"`
Groups []items.Group `json:"groups"`
}
func (g groupedRes) OverrideJSONResponse() bool {
return true
}
func ItemsAllScopes(g *gin.RouterGroup, itemService *items.Service) {
g.GET("", handler("items", func(c *gin.Context) (interface{}, error) {
filter, err := getItemFilterFromQuery(c)
if err != nil {
return nil, err
}
res, err := itemService.ListAllScopes(c.Request.Context(), filter)
if err != nil {
return nil, err
}
if c.Query("grouped") == "true" {
return groupedRes{
Items: res,
Groups: items.GenerateGroups(res, items.GroupOptions{
HideCreated: c.Query("hideCreated") == "true",
HideAcquired: c.Query("hideAcquired") == "true",
HideScheduled: c.Query("hideScheduled") == "true",
}),
}, nil
}
return res, nil
}))
}
func Items(g *gin.RouterGroup, itemService *items.Service) {
g.GET("/:item_id", handler("project", func(c *gin.Context) (interface{}, error) {
id, err := reqInt(c, "item_id")
if err != nil {
return nil, err
}
return items.Find(c.Request.Context(), id)
return itemService.Find(c.Request.Context(), id)
}))
g.GET("", handler("items", func(c *gin.Context) (interface{}, error) {
filter := models.ItemFilter{}
if rawFilter := c.Query("filter"); rawFilter != "" {
err := json.Unmarshal([]byte(rawFilter), &filter)
if err != nil {
return nil, models.BadInputError{
Object: "Query",
Field: "filter",
Problem: "Invalid raw filter: " + err.Error(),
}
}
filter, err := getItemFilterFromQuery(c)
if err != nil {
return nil, err
}
if queryOwnerId := c.Query("ownerId"); queryOwnerId != "" {
filter.OwnerID = &queryOwnerId
res, err := itemService.ListScoped(c.Request.Context(), filter)
if err != nil {
return nil, err
}
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}
if c.Query("grouped") == "true" {
return groupedRes{
Items: res,
Groups: items.GenerateGroups(res, items.GroupOptions{
HideCreated: c.Query("hideCreated") == "true",
HideAcquired: c.Query("hideAcquired") == "true",
HideScheduled: c.Query("hideScheduled") == "true",
}),
}, nil
}
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 res, nil
}))
g.POST("", handler("item", func(c *gin.Context) (interface{}, error) {
@@ -230,7 +96,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
}
}
return items.Create(c.Request.Context(), input.Item, input.Stats)
return itemService.Create(c.Request.Context(), input.Item, input.Stats)
}))
g.PUT("/:item_id", handler("item", func(c *gin.Context) (interface{}, error) {
@@ -251,7 +117,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
return nil, err
}
return items.Update(c.Request.Context(), id, input.ItemUpdate, input.Stats)
return itemService.Update(c.Request.Context(), id, input.ItemUpdate, input.Stats)
}))
g.PUT("/:item_id/stats/:stat_id", handler("item", func(c *gin.Context) (interface{}, error) {
@@ -275,7 +141,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
input.StatID = statID
return items.UpdateStat(c.Request.Context(), itemID, input)
return itemService.UpdateStat(c.Request.Context(), itemID, input)
}))
g.DELETE("/:item_id/stats/:stat_id", handler("item", func(c *gin.Context) (interface{}, error) {
@@ -288,7 +154,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
return nil, err
}
return items.DeleteStat(c.Request.Context(), itemID, statID)
return itemService.DeleteStat(c.Request.Context(), itemID, statID)
}))
g.DELETE("/:item_id", handler("item", func(c *gin.Context) (interface{}, error) {
@@ -297,6 +163,201 @@ func Items(g *gin.RouterGroup, items *items.Service) {
return nil, err
}
return items.Delete(c.Request.Context(), id)
return itemService.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"
)
type scopeResult struct {
entities.Scope
func AllScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc {
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 {
ID string `json:"id"`
Name string `json:"name"`
Owner bool `json:"owner"`
c.Request = c.Request.WithContext(scopes.CreateListContext(c.Request.Context(), user.ID, userScopes))
}
}
func ScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc {
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")
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, Error{
@@ -45,22 +65,6 @@ func ScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc
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))
}
}
+44 -24
View File
@@ -9,34 +9,22 @@ import (
"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) {
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 nil, models.BadInputError{
Object: "Query",
Field: "from",
Problem: "Incorrect date format: " + err.Error(),
}
}
from = parsed
from, to, err := getFromToFromQuery(c)
if err != nil {
return nil, err
}
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 nil, models.BadInputError{
Object: "Query",
Field: "to",
Problem: "Incorrect date format: " + err.Error(),
}
}
return sprintsService.ListBetween(c.Request.Context(), from, to)
}))
}
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)
@@ -126,3 +114,35 @@ func Sprints(g *gin.RouterGroup, sprintsService *sprints.Service) {
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
import (
"context"
"database/sql"
"encoding/json"
"fmt"
@@ -75,11 +74,8 @@ func Connect(host string, port int, username, password, database string) (*Datab
return nil, err
}
q, err := mysqlcore.Prepare(context.Background(), db)
if err != nil {
return nil, err
}
q := mysqlcore.New(db)
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},
})
}
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{
squirrel.GtOrEq{"i.acquired_time": filter.AcquiredTime.Min},
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{
squirrel.GtOrEq{"i.scheduled_date": filter.ScheduledDate.Min.AsTime()},
squirrel.Lt{"i.scheduled_date": filter.ScheduledDate.Max.AsTime()},
+9 -25
View File
@@ -191,37 +191,21 @@ func (q *Queries) ListSprintsAt(ctx context.Context, arg ListSprintsAtParams) ([
}
const listSprintsBetween = `-- name: ListSprintsBetween :many
SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required FROM sprint WHERE scope_id = ?
AND (
-- from time is within from..to
(from_time <= ? AND to_time > ?)
-- or to time is within from..to
OR (from_time <= ? AND to_time > ?)
-- or from and to time are on each their side of from..to
OR (from_time >= ? AND to_time < ?)
)
SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required FROM sprint
WHERE scope_id = ?
AND from_time < ?
AND to_time >= ?
ORDER BY from_time, name
`
type ListSprintsBetweenParams struct {
ScopeID int
FromTime time.Time
ToTime time.Time
FromTime_2 time.Time
ToTime_2 time.Time
FromTime_3 time.Time
ToTime_3 time.Time
ScopeID int
ToTime time.Time
FromTime time.Time
}
func (q *Queries) ListSprintsBetween(ctx context.Context, arg ListSprintsBetweenParams) ([]Sprint, error) {
rows, err := q.query(ctx, q.listSprintsBetweenStmt, listSprintsBetween,
arg.ScopeID,
arg.FromTime,
arg.ToTime,
arg.FromTime_2,
arg.ToTime_2,
arg.FromTime_3,
arg.ToTime_3,
)
rows, err := q.query(ctx, q.listSprintsBetweenStmt, listSprintsBetween, arg.ScopeID, arg.ToTime, arg.FromTime)
if err != nil {
return nil, err
}
+62 -4
View File
@@ -35,6 +35,61 @@ func (r *projectRepository) Find(ctx context.Context, scopeID, projectID int) (*
}, 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) {
rows, err := r.q.ListProjects(ctx, scopeID)
if err != nil {
@@ -139,11 +194,14 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int,
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").
Where(squirrel.Eq{"scope_id": scopeID}).
Where(squirrel.Eq{"id": requirementIDs}).
ToSql()
Where(squirrel.Eq{"id": requirementIDs})
if scopeID != -1 {
sq = sq.Where(squirrel.Eq{"scope_id": scopeID})
}
query, args, err := sq.ToSql()
if err != nil {
return nil, nil, err
}
+5 -10
View File
@@ -2,16 +2,11 @@
SELECT * FROM sprint WHERE scope_id = ? AND from_time <= sqlc.arg(time) AND to_time > sqlc.arg(time);
-- name: ListSprintsBetween :many
SELECT * FROM sprint WHERE scope_id = ?
AND (
-- from time is within from..to
(from_time <= ? AND to_time > ?)
-- or to time is within from..to
OR (from_time <= ? AND to_time > ?)
-- or from and to time are on each their side of from..to
OR (from_time >= ? AND to_time < ?)
);
SELECT * FROM sprint
WHERE scope_id = sqlc.arg(scope_id)
AND from_time < sqlc.arg(to_time)
AND to_time >= sqlc.arg(from_time)
ORDER BY from_time, name;
-- name: GetSprint :one
SELECT * FROM sprint WHERE id = ? AND scope_id = ?;
+3 -7
View File
@@ -75,13 +75,9 @@ func (r *sprintRepository) ListAt(ctx context.Context, scopeID int, at time.Time
func (r *sprintRepository) ListBetween(ctx context.Context, scopeID int, from, to time.Time) ([]entities.Sprint, error) {
rows, err := r.q.ListSprintsBetween(ctx, mysqlcore.ListSprintsBetweenParams{
ScopeID: scopeID,
FromTime: from,
ToTime: from,
FromTime_2: to,
ToTime_2: to,
FromTime_3: from,
ToTime_3: to,
ScopeID: scopeID,
FromTime: from,
ToTime: to,
})
if err != nil {
if err == sql.ErrNoRows {
+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").
From("stat").
Where(squirrel.Eq{"scope_id": scopeIDs}).
OrderBy("is_primary DESC", "name").
RunWith(r.db).
Query()
if err == sql.ErrNoRows {
+105
View File
@@ -0,0 +1,105 @@
package items
import (
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"sort"
"time"
)
type Group struct {
Label string `json:"label,omitempty"`
List []GroupReference `json:"list,omitempty"`
}
type GroupOptions struct {
HideCreated bool `json:"hideCreated,omitempty"`
HideScheduled bool `json:"hideScheduled,omitempty"`
HideAcquired bool `json:"hideAcquired,omitempty"`
}
type GroupReference struct {
Index int `json:"idx"`
Event string `json:"event"`
Time string `json:"time"`
sortKey string
}
func GenerateGroups(items []Result, opts GroupOptions) []Group {
groups := make(map[string]Group, 16)
// TODO: Get the scope from somewhere else, if necessary.
for i, item := range items {
if !opts.HideCreated && (item.AcquiredTime == nil || item.AcquiredTime.After(item.CreatedTime)) {
addItem(groups, i, "created", item.CreatedTime, tz)
}
if !opts.HideScheduled && item.ScheduledDate != nil {
addItem(groups, i, "scheduled", item.ScheduledDate.WithTimeAndLocation(23, 59, 59, tz), tz)
}
if !opts.HideAcquired && item.AcquiredTime != nil {
addItem(groups, i, "acquired", *item.AcquiredTime, tz)
}
}
for _, group := range groups {
sort.Slice(group.List, func(i, j int) bool {
return group.List[j].sortKey < group.List[i].sortKey
})
}
groupSlice := genutils.MapValues(groups)
sort.Slice(groupSlice, func(i, j int) bool {
return groupSlice[j].Label < groupSlice[i].Label
})
return groupSlice
}
func addItem(groups map[string]Group, index int, event string, eventDate time.Time, tz *time.Location) {
groupDate := eventDate.In(tz).Add(-time.Minute * 300)
eventDate = eventDate.In(tz)
groupKey := groupDate.Format("2006-01-02")
var sortKey string
if event == "scheduled" {
sortKey = eventDate.Format("2006-01-02") + " 23:59:59.999"
} else {
sortKey = eventDate.Format("2006-01-02 15:04:05")
}
// Why, Go, must you be so.
group, ok := groups[groupKey]
if !ok {
group = Group{
Label: groupDate.Format("2006-01-02 (Monday)"),
List: make([]GroupReference, 0, 16),
}
}
for i, ref := range group.List {
if ref.Index == index {
group.List = append(group.List[:i], group.List[i+1:]...)
break
}
}
group.List = append(group.List, GroupReference{
Index: index,
Event: event,
Time: eventDate.Format("15:04"),
sortKey: sortKey,
})
groups[groupKey] = group
}
var tz *time.Location
func init() {
var err error
tz, err = time.LoadLocation("Europe/Oslo")
if err != nil {
panic(err)
}
}
+4
View File
@@ -16,6 +16,10 @@ type Repository interface {
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 {
FetchRequirements(ctx context.Context, scopeID int, requirementIDs ...int) ([]entities.Requirement, []entities.RequirementStat, error)
}
+59 -4
View File
@@ -2,14 +2,34 @@ package items
import (
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"time"
)
type Result struct {
entities.Item
Requirement *entities.Requirement `json:"requirement,omitempty"`
Stats []ResultStat `json:"stats"`
Requirement *ResultRequirement `json:"requirement,omitempty"`
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 {
@@ -53,6 +73,7 @@ func (r *Result) AddStat(scope scopes.Result, stat entities.ItemStat) {
Required: stat.Required,
})
r.SortStats(scope)
r.RecalculateWeighted()
}
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.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 {
@@ -86,7 +119,7 @@ type ResultStat struct {
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{
Item: item,
Stats: make([]ResultStat, 0, 8),
@@ -95,7 +128,28 @@ func generateResult(item entities.Item, scope scopes.Result, progresses []entiti
if item.RequirementID != nil {
for _, req := range requirements {
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.RecalculateWeighted()
return res
}
+129 -6
View File
@@ -17,6 +17,7 @@ type Service struct {
Scopes *scopes.Service
Stats *stats.Service
Repository Repository
ProjectFetcher ProjectFetcher
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
}
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) {
sc := s.Scopes.Context(ctx)
if sc == nil {
return nil, models.PermissionDeniedError{}
}
if filter.ScopeIDs != nil {
if len(filter.ScopeIDs) > 0 {
userScopes, err := sc.Scopes(ctx)
if err != nil {
return nil, err
@@ -88,10 +159,14 @@ func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]R
}
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 {
@@ -100,10 +175,36 @@ func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]R
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))
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
@@ -122,6 +223,7 @@ func (s *Service) Create(ctx context.Context, item entities.Item, stats []entiti
}
}
var requirements []entities.Requirement
var projects []entities.Project
if item.RequirementID != nil {
reqs, _, err := s.RequirementFetcher.FetchRequirements(ctx, scope.ID, *item.RequirementID)
if err != nil {
@@ -133,6 +235,8 @@ func (s *Service) Create(ctx context.Context, item entities.Item, stats []entiti
item.ProjectID = &reqs[0].ProjectID
requirements = reqs
projects, _ = s.ProjectFetcher.FetchProjects(ctx, scope.ID, *item.ProjectID)
}
item.ScopeID = scope.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)
}
result := generateResult(*newItem, scope, goodStats, requirements)
result := generateResult(*newItem, scope, goodStats, requirements, projects)
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.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 {
item.ProjectID = nil
}
@@ -211,7 +334,7 @@ func (s *Service) Update(ctx context.Context, id int, update models.ItemUpdate,
for _, stat := range stats {
stat.ItemID = item.ID
err = s.Repository.UpdateStat(ctx, stat)
if err == nil {
item.AddStat(scope, stat)
+1
View File
@@ -9,6 +9,7 @@ import (
type Repository interface {
Find(ctx context.Context, scopeID, projectID int) (*entities.Project, error)
List(ctx context.Context, scopeID int) ([]entities.Project, error)
FetchProjects(ctx context.Context, scopeID int, ids ...int) ([]entities.Project, error)
Insert(ctx context.Context, project entities.Project) (*entities.Project, error)
Update(ctx context.Context, project entities.Project, update models.ProjectUpdate) error
Delete(ctx context.Context, project entities.Project) error
+41 -19
View File
@@ -36,9 +36,9 @@ type Result struct {
entities.Project
OwnerName string `json:"ownerName"`
StatusName string `json:"statusName"`
TotalAcquired int `json:"totalAcquired"`
TotalRequired int `json:"totalRequired"`
TotalPlanned int `json:"totalPlanned"`
TotalAcquired float64 `json:"totalAcquired"`
TotalRequired float64 `json:"totalRequired"`
TotalPlanned float64 `json:"totalPlanned"`
Requirements []RequirementResult `json:"requirements"`
}
@@ -58,9 +58,9 @@ type RequirementResult struct {
Description string `json:"description"`
Status models.Status `json:"status"`
StatusName string `json:"statusName"`
TotalAcquired int `json:"totalAcquired"`
TotalRequired int `json:"totalRequired"`
TotalPlanned int `json:"totalPlanned"`
TotalAcquired float64 `json:"totalAcquired"`
TotalRequired float64 `json:"totalRequired"`
TotalPlanned float64 `json:"totalPlanned"`
IsCoarse bool `json:"isCoarse"`
Stats []RequirementResultStat `json:"stats"`
Items []items.Result `json:"items"`
@@ -195,15 +195,18 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
continue
}
if item.AcquiredTime != nil {
for _, stat := range item.Stats {
if resStats[stat.ID] != nil {
for _, stat := range item.Stats {
if resStats[stat.ID] != nil {
if item.AcquiredTime != nil {
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)
}
@@ -233,19 +236,38 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
}
}
totalAcquired := 0.0
totalRequired := 0.0
totalPlanned := 0.0
hasRequiredStats := false
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
hasRequiredStats = true
break
}
}
resReq.TotalRequired += int(totalRequired)
resReq.TotalAcquired += int(math.Min(totalAcquired, totalRequired))
resReq.TotalPlanned += int(math.Min(totalPlanned, totalRequired))
if hasRequiredStats {
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
}
+5 -9
View File
@@ -2,7 +2,6 @@ package scopes
import (
"context"
"git.aiterp.net/stufflog3/stufflog3/entities"
"sync"
)
@@ -14,19 +13,16 @@ type Context struct {
ID int
Scope Result
statsOnce sync.Once
stats []entities.Stat
statsError error
scopesOnce sync.Once
scopes []entities.Scope
scopes []Result
scopesError error
service *Service
}
// Scopes lazy-loads the scopes in the context of the first caller.
func (c *Context) Scopes(ctx context.Context) ([]entities.Scope, error) {
c.statsOnce.Do(func() {
c.scopes, c.scopesError = c.scopesRepo.ListUser(ctx, c.userID)
func (c *Context) Scopes(ctx context.Context) ([]Result, error) {
c.scopesOnce.Do(func() {
c.scopes, c.scopesError = c.service.List(ctx)
})
return c.scopes, c.scopesError
+1
View File
@@ -68,6 +68,7 @@ func generateResult(scope entities.Scope, members []entities.ScopeMember, stats
Owner: member.Owner,
}
}
if stats != nil {
res.Stats = make([]ResultStat, 0, len(stats))
for _, stat := range stats {
+27 -2
View File
@@ -14,7 +14,7 @@ type Service struct {
Repository Repository
StatsLister StatsLister
contextKey struct{}
contextKey struct{ B int64 }
}
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,
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 {
v := ctx.Value(&s.contextKey)
if v == nil {
@@ -216,4 +241,4 @@ func (s *Service) Delete(ctx context.Context, scopeId int) (*Result, error) {
}
return scope, nil
}
}
+109 -9
View File
@@ -10,6 +10,7 @@ import (
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"golang.org/x/sync/errgroup"
"math"
"sort"
"strings"
"time"
)
@@ -36,25 +37,82 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
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) {
sprints, err := s.Repository.ListBetween(ctx, s.Scopes.Context(ctx).ID, from, to)
if err != nil {
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...)
if err != nil {
return nil, err
}
eg, ctx := errgroup.WithContext(ctx)
eg, ctx2 := errgroup.WithContext(ctx)
results := make([]Result, len(sprints))
for i := range sprints {
iCopy := i
eg.Go(func() error {
res, err := s.fill(ctx, sprints[iCopy], parts)
res, err := s.fill(ctx2, sprints[iCopy], parts)
if err != nil {
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) {
res := Result{Sprint: sprint}
var allStats []scopes.ResultStat
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))
for _, part := range parts {
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))
res.Progress = make([]ResultProgress, 0, len(allStats))
res.PartIDs = partIDs
@@ -325,7 +402,8 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
switch res.Kind {
case models.SprintKindItems:
pickedItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
IDs: partIDs,
IDs: partIDs,
ScopeIDs: []int{sprint.ScopeID},
})
if err != nil {
return nil, err
@@ -343,6 +421,10 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
for _, stat := range allStats {
totalRequired := 0
for _, item := range res.Items {
if item.AcquiredOutside(sprint.FromTime, sprint.ToTime) {
continue
}
if itemStat := item.Stat(stat.ID); itemStat != nil {
totalRequired += itemStat.Required
}
@@ -424,6 +506,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
acquiredItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime},
StatIDs: partIDs,
ScopeIDs: []int{sprint.ScopeID},
})
if err != nil {
return nil, err
@@ -433,6 +516,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
case models.SprintKindScope:
acquiredItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime},
ScopeIDs: []int{sprint.ScopeID},
})
if err != nil {
return nil, err
@@ -442,16 +526,32 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
for _, stat := range allStats {
progressMap[stat.ID] = len(res.Progress)
res.Progress = append(res.Progress, ResultProgress{
ID: stat.ID,
Name: stat.Name,
Weight: stat.Weight,
ID: stat.ID,
Name: stat.Name,
Weight: stat.Weight,
Required: genutils.Ptr(0),
})
}
}
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
for _, item := range res.Items {
if item.AcquiredTime == nil || item.AcquiredTime.Before(sprint.FromTime) || !item.AcquiredTime.Before(sprint.ToTime) {
if !item.AcquiredBetween(sprint.FromTime, sprint.ToTime) {
continue
}