Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b089933188 | ||
|
|
2bd1e0a9e3 | ||
|
|
43b75f77c6 | ||
|
|
c8ca2d4344 | ||
|
|
6641d99d41 | ||
|
|
396b87eea1 | ||
|
|
1d59182d43 | ||
|
|
951be980c0 |
@@ -97,7 +97,9 @@ 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)
|
||||
ginLambda := ginadapter.New(server)
|
||||
lambda.Start(func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
|
||||
return ginLambda.ProxyWithContext(ctx, request)
|
||||
|
||||
@@ -95,6 +95,9 @@ 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)
|
||||
|
||||
exitSignal := make(chan os.Signal)
|
||||
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
|
||||
@@ -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,7 +10,7 @@
|
||||
};
|
||||
|
||||
const fallback: ScopeContextData = {
|
||||
scope: readable({} as Scope),
|
||||
scope: readable({stats: []} as Scope),
|
||||
lastHistoryRange: writable(""),
|
||||
upsertStat: () => {},
|
||||
deleteStat: () => {},
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
Status.Completed, Status.Failed, Status.Dropped,
|
||||
]
|
||||
|
||||
function compareStatus<T extends {status: Status, name: string}>(a: T, b: T): number {
|
||||
function compareStatus<T extends {status: Status, id: number}>(a: T, b: T): number {
|
||||
if (a.status === b.status) {
|
||||
return a.name.localeCompare(b.name);
|
||||
return a.id - b.id;
|
||||
}
|
||||
|
||||
return SO.indexOf(a.status) - SO.indexOf(b.status)
|
||||
@@ -45,7 +45,13 @@
|
||||
let groups: PartOptionGroup[] = [];
|
||||
let showRequired = false;
|
||||
let loading = false;
|
||||
let onlyActive = true;
|
||||
let loaded: SprintKind = SprintKind.Invalid
|
||||
let hiddenSelections: SprintInputPart[] = [];
|
||||
|
||||
function forceReload() {
|
||||
loaded = SprintKind.Invalid;
|
||||
}
|
||||
|
||||
async function load(kind: SprintKind) {
|
||||
loading = true;
|
||||
@@ -53,7 +59,7 @@
|
||||
try {
|
||||
switch (kind) {
|
||||
case SprintKind.Items: {
|
||||
const items = await sl3(fetch).listItems($scope.id, {});
|
||||
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);
|
||||
@@ -62,17 +68,17 @@
|
||||
[c.requirementId]: c.projectId,
|
||||
}), {} as Record<number, string>)
|
||||
|
||||
groups = projects.map(p => ({
|
||||
groups = projects.filter(p => !onlyActive || p.status === Status.Active || p.status === Status.Background).map(p => ({
|
||||
id: p.id,
|
||||
label: `${p.name} (${p.statusName})`,
|
||||
options: [],
|
||||
subs: requirements.filter(r => reqProjectIdMap[r.id] === p.id).map(r => ({
|
||||
subs: requirements.filter(r => !onlyActive || r.status === Status.Active || r.status === Status.Background).filter(r => reqProjectIdMap[r.id] === p.id).map(r => ({
|
||||
id: r.id,
|
||||
label: `${r.name} (${r.statusName})`,
|
||||
options: items.filter(i => i.requirementId === r.id).map(i => ({
|
||||
enabled: false,
|
||||
partId: i.id,
|
||||
name: `${i.name} (${shortestDecimal(i.weightedRequired)})`,
|
||||
name: `${i.name} (${shortestDecimal(i.weightedRequired)})${!!i.acquiredTime?" ✓":""}`,
|
||||
required: 0,
|
||||
})).sort((a,b) => a.partId - b.partId),
|
||||
subs: [],
|
||||
@@ -81,11 +87,12 @@
|
||||
groups.push({id: -1, label: "Loose items", subs: [], options: loose.map(i => ({
|
||||
enabled: false,
|
||||
partId: i.id,
|
||||
name: `${i.name} (${shortestDecimal(i.weightedRequired)})`,
|
||||
name: `${i.name} (${shortestDecimal(i.weightedRequired)})${!!i.acquiredTime?" ✓":""}`,
|
||||
required: 0,
|
||||
})).sort((a,b) => a.partId - b.partId)})
|
||||
|
||||
console.log(groups);
|
||||
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;
|
||||
@@ -139,8 +146,16 @@
|
||||
loading = false;
|
||||
}
|
||||
|
||||
// Hax to make it reload if onlyActive changed.
|
||||
$: if (onlyActive != null) {
|
||||
forceReload();
|
||||
}
|
||||
|
||||
$: 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})),
|
||||
]
|
||||
}
|
||||
|
||||
$: if (browser && !loading && loaded !== kind) {
|
||||
@@ -174,6 +189,10 @@
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{#if kind === SprintKind.Items}
|
||||
<hr />
|
||||
<Checkbox label="Show only active items" bind:checked={onlyActive} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -210,6 +229,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 2em;
|
||||
border: 0;
|
||||
border-bottom: 1px solid $color-entry2;
|
||||
}
|
||||
|
||||
div.label {
|
||||
margin-top: 0.5em;
|
||||
border-bottom: 1px solid $color-entry2;
|
||||
|
||||
@@ -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 || !!found?.acquired);
|
||||
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}
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
import type { IconName } from "../layout/Icon.svelte";
|
||||
import Icon from "../layout/Icon.svelte";
|
||||
import { formatTime } from "$lib/utils/date";
|
||||
import RequirementReference from "./RequirementReference.svelte";
|
||||
|
||||
export let item: Item;
|
||||
export let event: "created" | "scheduled" | "acquired" | "none" = "none";
|
||||
export let compact = false;
|
||||
export let showAcquiredTime = false;
|
||||
export let hideStats = false;
|
||||
|
||||
let icon: IconName
|
||||
let status: Status
|
||||
@@ -53,27 +55,32 @@
|
||||
<Option open={{name: "item.delete", item}} color="red"><Icon name="trash" /></Option>
|
||||
{/if}
|
||||
</OptionsRow>
|
||||
{#if item.project != null}
|
||||
<RequirementReference project={item.project} requirement={item.requirement} scopeId={item.scopeId} />
|
||||
{/if}
|
||||
{#if !compact}
|
||||
<Markdown source={item.description} />
|
||||
{/if}
|
||||
<AmountRow>
|
||||
{#if item.acquiredTime != null}
|
||||
{#each item.stats as stat (stat.id)}
|
||||
{#if stat.required > 0}
|
||||
<Amount label={stat.name} value={stat.acquired} target={stat.required} />
|
||||
{#if !hideStats}
|
||||
{#if item.acquiredTime != null}
|
||||
{#each item.stats as stat (stat.id)}
|
||||
{#if stat.required > 0}
|
||||
<Amount label={stat.name} value={stat.acquired} target={stat.required} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if item.weightedRequired}
|
||||
<Amount dark right label="" value={item.weightedAcquired} target={item.weightedRequired} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if item.weightedRequired}
|
||||
<Amount dark right label="" value={item.weightedAcquired} target={item.weightedRequired} />
|
||||
{/if}
|
||||
{:else}
|
||||
{#each item.stats as stat (stat.id)}
|
||||
{#if stat.required > 0}
|
||||
<Amount label={stat.name} value={stat.required} />
|
||||
{:else}
|
||||
{#each item.stats as stat (stat.id)}
|
||||
{#if stat.required > 0}
|
||||
<Amount label={stat.name} value={stat.required} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if item.weightedRequired}
|
||||
<Amount dark right label="" value={item.weightedRequired} />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if item.weightedRequired}
|
||||
<Amount dark right label="" value={item.weightedRequired} />
|
||||
{/if}
|
||||
{/if}
|
||||
</AmountRow>
|
||||
|
||||
@@ -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>></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} />
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{#if sprint.kind === SprintKind.Items}
|
||||
{#each (sprint.items||[]) as item (item.id)}
|
||||
{#if !compact || !item.acquiredTime}
|
||||
<ItemSubSection compact={compact} item={item} />
|
||||
<ItemSubSection compact={compact} hideStats={compact} item={item} />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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: parseInterval("today", now),
|
||||
},
|
||||
looseFilter: {
|
||||
loose: true,
|
||||
@@ -47,7 +47,6 @@
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
import parseInterval, { datesOf } 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,7 @@
|
||||
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";
|
||||
|
||||
export let acquiredItems: Item[];
|
||||
export let scheduledItems: Item[];
|
||||
|
||||
@@ -2,11 +2,33 @@
|
||||
import type { Load } from "@sveltejs/kit/types/internal";
|
||||
import { sl3 } from "$lib/clients/sl3";
|
||||
|
||||
function generateItemFilters(now: Date): {scheduledFilter: ItemFilter, acquiredFilter: ItemFilter, looseFilter: ItemFilter} {
|
||||
return {
|
||||
scheduledFilter: {
|
||||
scheduledDate: datesOf(parseInterval("next:7d", now)),
|
||||
},
|
||||
acquiredFilter: {
|
||||
acquiredTime: parseInterval("today", now),
|
||||
},
|
||||
looseFilter: {
|
||||
loose: true,
|
||||
unAcquired: true,
|
||||
unScheduled: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const load: Load = async({ fetch, session }) => {
|
||||
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));
|
||||
|
||||
return {
|
||||
props: { scopes }
|
||||
props: { scopes, scheduledItems, acquiredItems, looseItems }
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -21,26 +43,48 @@
|
||||
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 } 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";
|
||||
|
||||
export let scopes: Scope[] = [];
|
||||
export let acquiredItems: Item[];
|
||||
export let scheduledItems: Item[];
|
||||
export let looseItems: Item[];
|
||||
|
||||
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}>
|
||||
<Columns wide>
|
||||
<Column>
|
||||
<Row title="Scopes">
|
||||
<OptionsRow slot="right">
|
||||
<Option open={{name: "scope.create"}}>Create</Option>
|
||||
</OptionsRow>
|
||||
<ScopeLinkList />
|
||||
</Row>
|
||||
<ItemListRow title="Scheduled" key="scheduleItems" />
|
||||
<ItemListRow title="Today" key="acquiredItems" showAcquiredTime />
|
||||
<ItemListRow title="Loose" key="looseItems" />
|
||||
</Column>
|
||||
<Column>
|
||||
</Column>
|
||||
</Columns>
|
||||
|
||||
<ItemCreateModal />
|
||||
<ItemAcquireModal />
|
||||
<DeletionModal />
|
||||
<ScopeCreateUpdateModal />
|
||||
</ScopeListContext>
|
||||
</ItemMultiListContext>
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
+209
-189
@@ -12,6 +12,17 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func ItemsAllScopes(g *gin.RouterGroup, items *items.Service) {
|
||||
g.GET("", handler("items", func(c *gin.Context) (interface{}, error) {
|
||||
filter, err := getItemFilterFromQuery(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return items.ListAllScopes(c.Request.Context(), filter)
|
||||
}))
|
||||
}
|
||||
|
||||
func Items(g *gin.RouterGroup, items *items.Service) {
|
||||
g.GET("/:item_id", handler("project", func(c *gin.Context) (interface{}, error) {
|
||||
id, err := reqInt(c, "item_id")
|
||||
@@ -23,195 +34,9 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
||||
}))
|
||||
|
||||
g.GET("", handler("items", func(c *gin.Context) (interface{}, error) {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if queryOwnerId := c.Query("ownerId"); queryOwnerId != "" {
|
||||
filter.OwnerID = &queryOwnerId
|
||||
}
|
||||
|
||||
queryScheduledMin := c.Query("scheduledMin")
|
||||
queryScheduledMax := c.Query("scheduledMax")
|
||||
if queryScheduledMin != "" && queryScheduledMax != "" {
|
||||
min, err := models.ParseDate(queryScheduledMin)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "scheduledMin",
|
||||
Problem: "Invalid from date: " + err.Error(),
|
||||
}
|
||||
}
|
||||
max, err := models.ParseDate(queryScheduledMax)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "scheduledMax",
|
||||
Problem: "Invalid to date: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
filter.ScheduledDate = &models.TimeInterval[models.Date]{Min: min, Max: max}
|
||||
}
|
||||
|
||||
queryCreatedMin := c.Query("createdMin")
|
||||
queryCreatedMax := c.Query("createdMax")
|
||||
if queryCreatedMin != "" && queryCreatedMax != "" {
|
||||
min, err := time.Parse(time.RFC3339, queryCreatedMin)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "createdMin",
|
||||
Problem: "Invalid from date: " + err.Error(),
|
||||
}
|
||||
}
|
||||
max, err := time.Parse(time.RFC3339, queryCreatedMax)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "createdMax",
|
||||
Problem: "Invalid to date: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
filter.CreatedTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
|
||||
}
|
||||
|
||||
queryAcquiredMin := c.Query("acquiredMin")
|
||||
queryAcquiredMax := c.Query("acquiredMax")
|
||||
if queryAcquiredMin != "" && queryAcquiredMax != "" {
|
||||
min, err := time.Parse(time.RFC3339, queryAcquiredMin)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "acquiredMin",
|
||||
Problem: "Invalid from date: " + err.Error(),
|
||||
}
|
||||
}
|
||||
max, err := time.Parse(time.RFC3339, queryAcquiredMax)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "acquiredMax",
|
||||
Problem: "Invalid to date: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
filter.AcquiredTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
|
||||
}
|
||||
|
||||
anyDateMin := c.Query("anyDateMin")
|
||||
anyDateMax := c.Query("anyDateMax")
|
||||
if anyDateMin != "" && anyDateMax != "" {
|
||||
min, err := time.Parse(time.RFC3339, anyDateMin)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "anyDateMin",
|
||||
Problem: "Invalid from date: " + err.Error(),
|
||||
}
|
||||
}
|
||||
max, err := time.Parse(time.RFC3339, anyDateMax)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "anyDateMax",
|
||||
Problem: "Invalid to date: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
minY, minM, minD := min.Date()
|
||||
maxY, maxM, maxD := max.Date()
|
||||
|
||||
filter.AcquiredTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
|
||||
filter.CreatedTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
|
||||
filter.ScheduledDate = &models.TimeInterval[models.Date]{
|
||||
Min: models.Date{minY, int(minM), minD},
|
||||
Max: models.Date{maxY, int(maxM), maxD},
|
||||
}
|
||||
}
|
||||
|
||||
if queryProjectID := c.Query("projectId"); queryProjectID != "" {
|
||||
ids := strings.Split(queryProjectID, ",")
|
||||
for _, id := range ids {
|
||||
parsed, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: fmt.Sprintf("projectId[%d]", len(filter.ProjectIDs)),
|
||||
Problem: "Invalid number",
|
||||
}
|
||||
}
|
||||
|
||||
filter.ProjectIDs = append(filter.ProjectIDs, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
if queryRequirementID := c.Query("requirementId"); queryRequirementID != "" {
|
||||
if queryRequirementID != "null" {
|
||||
ids := strings.Split(queryRequirementID, ",")
|
||||
for _, id := range ids {
|
||||
parsed, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: fmt.Sprintf("requirementId[%d]", len(filter.RequirementIDs)),
|
||||
Problem: "Invalid number",
|
||||
}
|
||||
}
|
||||
|
||||
filter.RequirementIDs = append(filter.RequirementIDs, parsed)
|
||||
}
|
||||
} else {
|
||||
filter.Loose = true
|
||||
}
|
||||
}
|
||||
|
||||
if statID := c.Query("statId"); statID != "" {
|
||||
ids := strings.Split(statID, ",")
|
||||
for _, id := range ids {
|
||||
parsed, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: fmt.Sprintf("statId[%d]", len(filter.StatIDs)),
|
||||
Problem: "Invalid number",
|
||||
}
|
||||
}
|
||||
|
||||
filter.StatIDs = append(filter.StatIDs, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
if filter.ScheduledDate != nil && !filter.ScheduledDate.Valid() {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Filter",
|
||||
Field: "scheduledDate",
|
||||
Problem: "Invalid scheduled date range",
|
||||
}
|
||||
}
|
||||
if filter.CreatedTime != nil && !filter.CreatedTime.Valid() {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Filter",
|
||||
Field: "createdTime",
|
||||
Problem: "Invalid created time range",
|
||||
}
|
||||
}
|
||||
if filter.AcquiredTime != nil && !filter.AcquiredTime.Valid() {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Filter",
|
||||
Field: "acquiredTime",
|
||||
Problem: "Invalid created time range",
|
||||
}
|
||||
filter, err := getItemFilterFromQuery(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return items.ListScoped(c.Request.Context(), filter)
|
||||
@@ -300,3 +125,198 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
||||
return items.Delete(c.Request.Context(), id)
|
||||
}))
|
||||
}
|
||||
|
||||
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
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -57,6 +57,68 @@ 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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -449,6 +450,21 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(res.Items, func(i, j int) bool {
|
||||
ii := res.Items[i]
|
||||
ij := res.Items[j]
|
||||
|
||||
if ii.AcquiredTime == nil && ij.AcquiredTime == nil {
|
||||
return res.Items[i].CreatedTime.Before(res.Items[j].CreatedTime)
|
||||
} else if ii.AcquiredTime == nil && ij.AcquiredTime != nil {
|
||||
return true
|
||||
} else if ii.AcquiredTime != nil && ij.AcquiredTime == nil {
|
||||
return false
|
||||
} else {
|
||||
return ii.AcquiredTime.Before(*ij.AcquiredTime)
|
||||
}
|
||||
})
|
||||
|
||||
// Measure progress
|
||||
for _, item := range res.Items {
|
||||
if item.AcquiredTime == nil || item.AcquiredTime.Before(sprint.FromTime) || !item.AcquiredTime.Before(sprint.ToTime) {
|
||||
|
||||
Reference in New Issue
Block a user