Browse Source

add loose items and better item list handling.

master 0.1.11
Gisle Aune 2 years ago
parent
commit
d4ab0a8555
  1. 6
      frontend/src/lib/clients/sl3.ts
  2. 71
      frontend/src/lib/components/contexts/ItemMultiListContext.svelte
  3. 20
      frontend/src/lib/components/scope/ItemListRow.svelte
  4. 3
      frontend/src/lib/modals/ItemCreateModal.svelte
  5. 2
      frontend/src/lib/models/item.ts
  6. 56
      frontend/src/lib/utils/object.ts
  7. 121
      frontend/src/routes/[scope=prettyid]/overview.svelte
  8. 2
      models/item.go
  9. 8
      ports/mysql/items.go

6
frontend/src/lib/clients/sl3.ts

@ -73,8 +73,10 @@ export default class SL3APIClient {
return this.fetch<{project: Project}>("PUT", `scopes/${scopeId}/projects/${projectId}`, input).then(r => r.project);
}
async listItems(scopeId: number, filter: ItemFilter): Promise<Item[]> {
return this.fetch<{items: Item[]}>("GET", `scopes/${scopeId}/items?filter=${encodeURIComponent(JSON.stringify(filter))}`).then(r => r.items);
async listItems(scopeId: number | "ALL", filter: ItemFilter): Promise<Item[]> {
const scopePrefix = (scopeId !== "ALL") ? `scopes/${scopeId}` : "all-scopes";
return this.fetch<{items: Item[]}>("GET", `${scopePrefix}/items?filter=${encodeURIComponent(JSON.stringify(filter))}`).then(r => r.items);
}
async createItem(scopeId: number, input: ItemInput): Promise<Item> {

71
frontend/src/lib/components/contexts/ItemMultiListContext.svelte

@ -0,0 +1,71 @@
<script lang="ts" context="module">
const contextKey = {ctx: "itemMultiListContext"};
interface ItemMultiListContextData {
lists: Readable<Record<string, Item[]>>,
reloadItemLists(): void,
};
const fallback: ItemMultiListContextData = {
lists: readable({}),
reloadItemLists() {},
}
export function getItemMultiListContext() {
return getContext(contextKey) as ItemMultiListContextData || fallback
}
</script>
<script lang="ts">
import { readable, writable, type Readable } from "svelte/store";
import { getContext, setContext } from "svelte";
import type Item from "$lib/models/item";
import type { ItemFilter } from "$lib/models/item";
import { getScopeContext } from "./ScopeContext.svelte";
import deepEqual from "$lib/utils/object";
import { sl3 } from "$lib/clients/sl3";
const {scope} = getScopeContext();
export let lists: Record<string, Item[]>
export let filters: Record<string, ItemFilter>
let loaded: Record<string, ItemFilter> = {...filters};
let loading: Record<string, boolean> = {};
const listsWritable = writable<Record<string, Item[]>>({...lists});
function reloadItemLists() {
loaded = {};
}
async function runLoad(key: string, filter: ItemFilter) {
loading = {...loading, [key]: true};
try {
const items = await sl3(fetch).listItems($scope.id, filter)
listsWritable.update(l => ({...l, [key.replace("Filter", "Items")]: items}));
} catch(_err) {
// TODO: Use err
}
loading = {...loading, [key]: false};
loaded = {...loaded, [key]: filter};
}
setContext<ItemMultiListContextData>(contextKey, {
lists: {subscribe: listsWritable.subscribe},
reloadItemLists,
});
$: for (const key in filters) {
if (loading[key]) {
continue;
}
if (loaded[key] == null || !deepEqual(filters[key], loaded[key])) {
runLoad(key, {...filters[key]})
}
}
</script>
<slot></slot>

20
frontend/src/lib/components/scope/ItemListRow.svelte

@ -0,0 +1,20 @@
<script lang="ts">
import { getItemMultiListContext } from "../contexts/ItemMultiListContext.svelte";
import Row from "../layout/Row.svelte";
import ItemSubSection from "../project/ItemSubSection.svelte";
export let key: string;
export let title: string;
export let showAcquiredTime: boolean = false;
const {lists} = getItemMultiListContext();
</script>
{#if ($lists[key]||[]).length > 0}
<Row title={title}>
{#each $lists[key] as item (item.id)}
<ItemSubSection showAcquiredTime={showAcquiredTime} item={item} />
{/each}
</Row>
{/if}

3
frontend/src/lib/modals/ItemCreateModal.svelte

@ -4,6 +4,7 @@
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";
@ -22,6 +23,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
const {project, reloadProject} = getProjectContext();
const {reloadItemList} = getItemListContext();
const {reloadSprintList} = getSprintListContext();
const {reloadItemLists} = getItemMultiListContext();
let item: ItemInput
let itemId: number
@ -144,6 +146,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext
await reloadItemList();
await reloadSprintList();
reloadItemLists();
closeModal();
} catch(err) {

2
frontend/src/lib/models/item.ts

@ -38,6 +38,8 @@ export interface ItemFilter {
createdTime?: TimeInterval<Date | string>
acquiredTime?: TimeInterval<Date | string>
loose?: boolean
unAcquired?: boolean
unScheduled?: boolean
}
export interface ItemGroup {

56
frontend/src/lib/utils/object.ts

@ -0,0 +1,56 @@
export default function deepEqual(a: Object, b: Object) {
if (a === void(0)) {
if (b !== void(0)) {
return false;
}
return true;
}
if (a === null) {
if (b !== null) {
return false;
}
return true;
}
if (a instanceof Date) {
if (b instanceof Date) {
b = b.toISOString();
}
a = a.toISOString();
return a === b;
}
if (Array.isArray(a)) {
if (!Array.isArray(b) || b.length !== a.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
return true;
}
if (typeof(a) === "object") {
if (typeof(b) !== "object") {
return false;
}
for (const key in a) {
if (!a.hasOwnProperty(key)) {
continue
}
if (!deepEqual(a[key], b[key])) {
return false
}
}
return true;
}
return a === b;
}

121
frontend/src/routes/[scope=prettyid]/overview.svelte

@ -1,19 +1,35 @@
<script lang="ts" context="module">
import type { Load } from "@sveltejs/kit/types/internal";
function generateItemFilters(now: Date): {scheduledFilter: ItemFilter, acquiredFilter: ItemFilter, looseFilter: ItemFilter} {
return {
scheduledFilter: {
scheduledDate: datesOf(parseInterval("next:7d", new Date)),
},
acquiredFilter: {
acquiredTime: parseInterval("today", new Date),
},
looseFilter: {
loose: true,
unAcquired: true,
unScheduled: true,
},
}
}
export const load: Load = async({params, url, fetch}) => {
const scopeId = parseInt(params.scope.split("-")[0]);
const scheduledItems = (await sl3(fetch).listItems(scopeId, {
scheduledDate: datesOf(parseInterval("next:7d", new Date)),
}))
const acquiredItems = (await sl3(fetch).listItems(scopeId, {
acquiredTime: parseInterval("today", new Date),
}))
const {acquiredFilter, looseFilter, scheduledFilter} = generateItemFilters(new Date());
const scheduledItems = (await sl3(fetch).listItems(scopeId, scheduledFilter));
const acquiredItems = (await sl3(fetch).listItems(scopeId, acquiredFilter));
const looseItems = (await sl3(fetch).listItems(scopeId, looseFilter));
const sprints = await sl3(fetch).listSprints(scopeId);
return {
props: {scheduledItems, acquiredItems, sprints}
props: {scheduledItems, acquiredItems, looseItems, sprints}
};
}
</script>
@ -43,13 +59,19 @@
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
import ProjectCreateEditModal from "$lib/modals/ProjectCreateEditModal.svelte";
import ScopeCreateUpdateModal from "$lib/modals/ScopeCreateUpdateModal.svelte";
import type { ItemFilter } from "$lib/models/item";
import ItemMultiListContext from "$lib/components/contexts/ItemMultiListContext.svelte";
import { getTimeContext } from "$lib/components/contexts/TimeContext.svelte";
import ItemListRow from "$lib/components/scope/ItemListRow.svelte";
export let acquiredItems: Item[];
export let scheduledItems: Item[];
export let looseItems: Item[];
export let sprints: Sprint[];
const {scope} = getScopeContext();
const {openModal} = getModalContext();
const {now} = getTimeContext();
function openCreateProject() {
openModal({name: "project.create"});
@ -68,53 +90,44 @@
}
</script>
<SprintListContext sprints={sprints} intervalString="">
<Columns fullwidth>
<Column>
<Row title="Options">
<Card on:click={openCreateProject} pointerCursor><CardHeader>Create Project</CardHeader></Card>
<Card on:click={openPostItem} pointerCursor><CardHeader>Post Item</CardHeader></Card>
<Card on:click={openEditScope} pointerCursor><CardHeader>Edit Scope</CardHeader></Card>
<Card on:click={openDeleteScope} pointerCursor><CardHeader>Delete Scope</CardHeader></Card>
</Row>
{#if scheduledItems.length > 0}
<Row title="Schedule">
{#each scheduledItems as item (item.id)}
<ItemSubSection item={item} />
{/each}
<ItemMultiListContext lists={{acquiredItems, scheduledItems, looseItems}} filters={generateItemFilters($now)}>
<SprintListContext sprints={sprints} intervalString="">
<Columns fullwidth>
<Column>
<Row title="Options">
<Card on:click={openCreateProject} pointerCursor><CardHeader>Create Project</CardHeader></Card>
<Card on:click={openPostItem} pointerCursor><CardHeader>Create Item</CardHeader></Card>
<Card on:click={openEditScope} pointerCursor><CardHeader>Edit Scope</CardHeader></Card>
<Card on:click={openDeleteScope} pointerCursor><CardHeader>Delete Scope</CardHeader></Card>
</Row>
<ItemListRow title="Scheduled" key="scheduleItems" />
<ItemListRow title="Today" key="acquiredItems" showAcquiredTime />
<ItemListRow title="Loose" key="looseItems" />
</Column>
<Column>
<Row title="Sprints">
<OptionsRow slot="right">
<Option open={{name: "sprint.create"}}>Create</Option>
</OptionsRow>
<SprintList sub />
</Row>
{/if}
{#if acquiredItems.length > 0}
<Row title="Today">
{#each acquiredItems as item (item.id)}
<ItemSubSection showAcquiredTime item={item} />
<Row title="Stats">
<OptionsRow slot="right">
<Option open={{name: "stat.create"}}>Create</Option>
</OptionsRow>
{#each $scope.stats as stat (stat.id)}
<StatSubSection stat={stat} />
{/each}
</Row>
{/if}
</Column>
<Column>
<Row title="Sprints">
<OptionsRow slot="right">
<Option open={{name: "sprint.create"}}>Create</Option>
</OptionsRow>
<SprintList sub />
</Row>
<Row title="Stats">
<OptionsRow slot="right">
<Option open={{name: "stat.create"}}>Create</Option>
</OptionsRow>
{#each $scope.stats as stat (stat.id)}
<StatSubSection stat={stat} />
{/each}
</Row>
</Column>
</Columns>
<ItemCreateModal />
<ItemAcquireModal />
<StatCreateEditModal />
<DeletionModal />
<SprintCreateUpdateModal />
<ProjectCreateEditModal />
<ScopeCreateUpdateModal />
</SprintListContext>
</Column>
</Columns>
<ItemCreateModal />
<ItemAcquireModal />
<StatCreateEditModal />
<DeletionModal />
<SprintCreateUpdateModal />
<ProjectCreateEditModal />
<ScopeCreateUpdateModal />
</SprintListContext>
</ItemMultiListContext>

2
models/item.go

@ -25,4 +25,6 @@ type ItemFilter struct {
RequirementIDs []int `json:"requirementIds,omitempty"`
StatIDs []int `json:"statIds,omitempty"`
Loose bool `json:"loose,omitempty"`
UnScheduled bool `json:"unScheduled,omitempty"`
UnAcquired bool `json:"unAcquired,omitempty"`
}

8
ports/mysql/items.go

@ -67,13 +67,17 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([
squirrel.Lt{"i.created_time": filter.CreatedTime.Max},
})
}
if filter.AcquiredTime != nil {
if filter.UnAcquired {
sq = sq.Where("i.acquired_time IS NULL")
} else if filter.AcquiredTime != nil {
dateOr = append(dateOr, squirrel.And{
squirrel.GtOrEq{"i.acquired_time": filter.AcquiredTime.Min},
squirrel.Lt{"i.acquired_time": filter.AcquiredTime.Max},
})
}
if filter.ScheduledDate != nil {
if filter.UnScheduled {
sq = sq.Where("i.scheduled_date IS NULL")
} else if filter.ScheduledDate != nil {
dateOr = append(dateOr, squirrel.And{
squirrel.GtOrEq{"i.scheduled_date": filter.ScheduledDate.Min.AsTime()},
squirrel.Lt{"i.scheduled_date": filter.ScheduledDate.Max.AsTime()},

Loading…
Cancel
Save