Compare commits

..
18 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
54 changed files with 1059 additions and 496 deletions
+4
View File
@@ -97,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) {
+4
View File
@@ -95,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),
}));
}
+16 -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,10 +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 | "ALL", filter: ItemFilter): Promise<Item[]> {
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[]}>("GET", `${scopePrefix}/items?filter=${encodeURIComponent(JSON.stringify(filter))}`).then(r => r.items);
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> {
@@ -99,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> {
@@ -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;
@@ -29,6 +29,7 @@
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> = {};
@@ -42,7 +43,7 @@
loading = {...loading, [key]: true};
try {
const items = await sl3(fetch).listItems($scope.id, filter)
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
@@ -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(_) {}
@@ -31,15 +31,19 @@
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 groups: PartOptionGroup[] = [];
@@ -47,18 +51,20 @@
let loading = false;
let onlyActive = true;
let loaded: SprintKind = SprintKind.Invalid
let hiddenSelections: SprintInputPart[] = [];
let actualScope: Scope;
function forceReload() {
loaded = SprintKind.Invalid;
}
async function load(kind: SprintKind) {
async function load(kind: SprintKind, scope: Scope) {
loading = true;
try {
switch (kind) {
case SprintKind.Items: {
const items = await sl3(fetch).listItems($scope.id, {unAcquired: onlyActive});
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);
@@ -90,6 +96,9 @@
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;
}
@@ -102,7 +111,7 @@
id: -1,
label: "",
subs: [],
options: $scope.stats.map(s => ({partId: s.id, name: s.name, enabled: false, required: 0})),
options: scope.stats.map(s => ({partId: s.id, name: s.name, enabled: false, required: 0})),
}];
showRequired = true;
break;
@@ -148,11 +157,16 @@
}
$: if (loaded === kind && error === "") {
value = groups.flatMap(g => [...g.subs.flatMap(g => g.options), ...g.options]).filter(o => o.enabled).map(o => ({partId: o.partId, required: o.required}))
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>
@@ -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);
}
@@ -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;
@@ -10,6 +10,7 @@
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";
@@ -54,6 +55,9 @@
<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}
@@ -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>
@@ -10,10 +10,12 @@
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} />
@@ -6,14 +6,15 @@
import SprintBody from "./SprintBody.svelte";
import { projectPrettyId } from "$lib/utils/prettyIds";
import Section from "../layout/Section.svelte";
import Icon from "../layout/Icon.svelte";
export let sprint: Sprint;
</script>
<Section anchor={projectPrettyId(sprint)} noProgress title={sprint.name}>
<OptionsRow slot="right">
<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, lastHistoryRange} = getScopeContext();
const {scopes} = getScopeListContext();
function gotoSprint() {
goto(`/${scopePrettyId($scope)}/sprints/${$lastHistoryRange}#${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 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>
+17 -6
View File
@@ -3,12 +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 { getItemMultiListContext } from "$lib/components/contexts/ItemMultiListContext.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";
@@ -20,6 +21,7 @@ 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();
@@ -27,6 +29,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
let item: ItemInput
let itemId: number
let scopeId: number
let currentStats: ItemInput["stats"]
let openedDate: Date
let requirementName: string
@@ -71,6 +74,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
}
op = "Create"
scopeId = scope.id;
openedDate = new Date();
requirementName = requirement?.name;
show = true;
@@ -80,7 +84,12 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
const req = $project.requirements?.find(r => r.id === current.requirementId);
let ctxStats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
if (ctxStats == null || ctxStats.length === 0) {
ctxStats = scope.stats.map(s => ({statId: s.id, required: 0, acquired: 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}));
@@ -93,6 +102,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
scheduledDate: current.scheduledDate,
};
itemId = current.id;
scopeId = current.scopeId;
currentStats = [...item.stats];
op = "Edit"
@@ -124,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) {
@@ -135,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;
}
@@ -180,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;
@@ -161,7 +164,7 @@ import { partsDiff } from "$lib/utils/sprint";
<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} />
<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} />
+8
View File
@@ -68,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;
}
}
+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[]
@@ -4,10 +4,10 @@
function generateItemFilters(now: Date): {scheduledFilter: ItemFilter, acquiredFilter: ItemFilter, looseFilter: ItemFilter} {
return {
scheduledFilter: {
scheduledDate: datesOf(parseInterval("next:7d", new Date)),
scheduledDate: datesOf(parseInterval("next:7d", now)),
},
acquiredFilter: {
acquiredTime: parseInterval("today", new Date),
acquiredTime: morningInterval(parseInterval("today", now)),
},
looseFilter: {
loose: true,
@@ -35,6 +35,7 @@
</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";
@@ -44,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";
@@ -62,7 +61,8 @@
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 ItemListRow from "$lib/components/scope/ItemListRow.svelte";
import Icon from "$lib/components/layout/Icon.svelte";
export let acquiredItems: Item[];
export let scheduledItems: Item[];
@@ -107,13 +107,13 @@ import ItemListRow from "$lib/components/scope/ItemListRow.svelte";
<Column>
<Row title="Sprints">
<OptionsRow slot="right">
<Option open={{name: "sprint.create"}}>Create</Option>
<Option open={{name: "sprint.create"}}><Icon name="plus" /></Option>
</OptionsRow>
<SprintList sub />
</Row>
<Row title="Stats">
<OptionsRow slot="right">
<Option open={{name: "stat.create"}}>Create</Option>
<Option open={{name: "stat.create"}}><Icon name="plus" /></Option>
</OptionsRow>
{#each $scope.stats as stat (stat.id)}
<StatSubSection stat={stat} />
+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)
+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
}
+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
}
+7 -4
View File
@@ -194,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)
}
}
+83 -2
View File
@@ -57,13 +57,75 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
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
@@ -123,7 +185,26 @@ func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]R
res := make([]Result, 0, len(items))
for _, item := range items {
res = append(res, generateResult(item, sc.Scope, progresses, requirements, projects))
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
+4 -4
View File
@@ -195,12 +195,12 @@ 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
}
}
+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
}