Browse Source

add more modals

master
Gisle Aune 2 years ago
parent
commit
0547714420
  1. 2
      entities/project.go
  2. 11
      frontend/src/lib/clients/sl3.ts
  3. 4
      frontend/src/lib/components/common/AmountRow.svelte
  4. 2
      frontend/src/lib/components/contexts/ModalContext.svelte
  5. 23
      frontend/src/lib/components/contexts/ScopeContext.svelte
  6. 4
      frontend/src/lib/components/controls/StatInput.svelte
  7. 0
      frontend/src/lib/components/layout/Main.svelte
  8. 25
      frontend/src/lib/components/layout/Row.svelte
  9. 0
      frontend/src/lib/components/layout/Section.svelte
  10. 4
      frontend/src/lib/components/layout/SubSection.svelte
  11. 6
      frontend/src/lib/components/project/ItemSubSection.svelte
  12. 8
      frontend/src/lib/components/project/ProjectMain.svelte
  13. 8
      frontend/src/lib/components/project/RequirementSection.svelte
  14. 24
      frontend/src/lib/components/scope/StatSubSection.svelte
  15. 9
      frontend/src/lib/modals/DeletionModal.svelte
  16. 113
      frontend/src/lib/modals/StatCreateEditModal.svelte
  17. 4
      frontend/src/lib/models/item.ts
  18. 4
      frontend/src/lib/models/project.ts
  19. 9
      frontend/src/lib/models/stat.ts
  20. 6
      frontend/src/lib/utils/stat.ts
  21. 41
      frontend/src/routes/[scope=prettyid]/index.svelte
  22. 6
      frontend/src/routes/[scope=prettyid]/projects/[project=prettyid].svelte

2
entities/project.go

@ -43,7 +43,7 @@ func (requirement *Requirement) Update(update models.RequirementUpdate) {
if update.Name != nil && *update.Name != "" {
requirement.Name = *update.Name
}
if update.Description != nil && *update.Description != "" {
if update.Description != nil {
requirement.Description = *update.Description
}
if update.Status != nil && update.Status.Valid() {

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

@ -3,6 +3,8 @@ import type { ItemInput } from "$lib/models/item";
import type Project from "$lib/models/project";
import type { ProjectEntry, Requirement, RequirementInput } from "$lib/models/project";
import type Scope from "$lib/models/scope";
import type Stat from "$lib/models/stat";
import type { StatInput } from "$lib/models/stat";
import type User from "$lib/models/user";
import type { AuthResult } from "$lib/models/user";
@ -59,6 +61,15 @@ export default class SL3APIClient {
return this.fetch<{item: Item}>("PUT", `scopes/${scopeId}/items/${itemId}`, input).then(r => r.item);
}
async createStat(scopeId: number, input: StatInput): Promise<Stat> {
return this.fetch<{stat: Stat}>("POST", `scopes/${scopeId}/stats`, input).then(r => r.stat);
}
async updateStat(scopeId: number, statId: number, input: Partial<StatInput>): Promise<Stat> {
return this.fetch<{stat: Stat}>("PUT", `scopes/${scopeId}/stats/${statId}`, input).then(r => r.stat);
}
async autchCheck(): Promise<User> {
return this.fetch<{user: User}>("GET", "auth").then(r => r.user);
}

4
frontend/src/lib/components/common/AmountRow.svelte

@ -7,6 +7,10 @@
flex-wrap: wrap;
margin-top: 0.5em;
font-size: 0.75em;
&:empty {
display: none;
}
}
div.amounts:empty {
margin-top: 0;

2
frontend/src/lib/components/contexts/ModalContext.svelte

@ -11,6 +11,8 @@
| { name: "requirement.edit", requirement: Requirement }
| { name: "requirement.delete", requirement: Requirement }
| { name: "project.delete", project: Project }
| { name: "stat.create" }
| { name: "stat.edit", stat: Stat }
| { name: "stat.delete", stat: Stat }
interface ModalContextData {

23
frontend/src/lib/components/contexts/ScopeContext.svelte

@ -3,6 +3,8 @@
interface ScopeContextData {
scope: Readable<Scope>
upsertStat(stat: Stat): void
deleteStat(stat: Stat): void
};
export function getScopeContext() {
@ -14,15 +16,34 @@
import type Scope from "$lib/models/scope";
import { writable, type Readable } from "svelte/store";
import { getContext, setContext } from "svelte";
import type Stat from "$lib/models/stat";
export let scope: Scope;
let scopeWritable = writable<Scope>(scope);
setContext<ScopeContextData>(contextKey, {
scope: {subscribe: scopeWritable.subscribe}
scope: {subscribe: scopeWritable.subscribe},
upsertStat, deleteStat,
});
function upsertStat(stat: Stat) {
scopeWritable.update(s => ({
...s,
stats: [
...s.stats.filter(s => s.id != stat.id),
{...stat},
].sort((a,b) => a.name.localeCompare(b.name))
}));
}
function deleteStat(stat: Stat) {
scopeWritable.update(s => ({
...s,
stats: s.stats.filter(s => s.id != stat.id),
}));
}
$: $scopeWritable = scope;
</script>

4
frontend/src/lib/components/controls/StatInput.svelte

@ -1,9 +1,9 @@
<script lang="ts">
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import type { StatInput } from "$lib/models/stat";
import type { StatValueInput } from "$lib/models/stat";
import Checkbox from "../layout/Checkbox.svelte";
export let value: StatInput[] = [];
export let value: StatValueInput[] = [];
export let showAcquired = false;
export let showRequired = false;
export let hideUnseen = false;

0
frontend/src/lib/components/layout/BigEntry.svelte → frontend/src/lib/components/layout/Main.svelte

25
frontend/src/lib/components/layout/Row.svelte

@ -3,9 +3,12 @@
</script>
<div class="row">
{#if title != ""}
<h2>{title}</h2>
{/if}
<div class="header-row">
{#if title != ""}
<h2>{title}</h2>
{/if}
<slot name="right"></slot>
</div>
<slot></slot>
</div>
@ -15,8 +18,16 @@
margin: 0.5em 1ch
padding: 0.5em 1ch
> h2
font-size: 1.25em
margin: 0
margin-bottom: 0.25em
div.header-row
display: flex
flex-direction: row
&:empty
display: none
> h2
font-size: 1.25em
margin: 0
margin-bottom: 0.25em
margin-right: auto
</style>

0
frontend/src/lib/components/layout/Entry.svelte → frontend/src/lib/components/layout/Section.svelte

4
frontend/src/lib/components/layout/SmallEntry.svelte → frontend/src/lib/components/layout/SubSection.svelte

@ -5,6 +5,7 @@
import StatusColor from "../common/StatusColor.svelte";
export let title = "";
export let subtitle = "";
export let big = false;
export let small = false;
export let noProgress = false;
@ -35,6 +36,9 @@ import StatusColor from "../common/StatusColor.svelte";
<h2 class:big class:small>
<span class="sub">{titleChunks[0]}</span>
<span>{titleChunks[1]}</span>
{#if subtitle != ""}
<span class="sub">{subtitle}</span>
{/if}
</h2>
<div class="right" class:noProgress>
<slot name="right"></slot>

6
frontend/src/lib/components/project/ItemEntry.svelte → frontend/src/lib/components/project/ItemSubSection.svelte

@ -1,6 +1,6 @@
<script lang="ts">
import Markdown from "$lib/components/common/Markdown.svelte";
import SmallEntry from "$lib/components/layout/SmallEntry.svelte";
import SubSection from "$lib/components/layout/SubSection.svelte";
import Option from "$lib/components/layout/Option.svelte";
import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
import type Item from "$lib/models/item";
@ -11,7 +11,7 @@
export let item: Item;
</script>
<SmallEntry small noProgress title={item.name} icon={item.acquiredTime ? "check" : null} status={Status.Completed}>
<SubSection small noProgress title={item.name} icon={item.acquiredTime ? "check" : null} status={Status.Completed}>
<Markdown source={item.description} />
<OptionsRow slot="right">
{#if item.acquiredTime == null}
@ -35,4 +35,4 @@
{/each}
{/if}
</AmountRow>
</SmallEntry>
</SubSection>

8
frontend/src/lib/components/project/ProjectEntry.svelte → frontend/src/lib/components/project/ProjectMain.svelte

@ -1,16 +1,16 @@
<script lang="ts">
import Markdown from "$lib/components/common/Markdown.svelte";
import Progress from "$lib/components/common/Progress.svelte";
import BigEntry from "$lib/components/layout/BigEntry.svelte";
import Main from "$lib/components/layout/Main.svelte";
import Option from "$lib/components/layout/Option.svelte";
import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
import { getProjectContext } from "../contexts/ProjectContext.svelte";
import RequirementEntry from "./RequirementEntry.svelte";
import RequirementEntry from "./RequirementSection.svelte";
const {project} = getProjectContext();
</script>
<BigEntry big title={$project.name}>
<Main big title={$project.name}>
<Progress alwaysSmooth titlePercentageOnly thin green status={$project.status} count={$project.totalAcquired} target={$project.totalRequired} />
<Progress alwaysSmooth titlePercentageOnly thinner gray count={$project.totalPlanned} target={$project.totalRequired} />
<Markdown source={$project.description} />
@ -22,4 +22,4 @@
{#each $project.requirements as requirement (requirement.id)}
<RequirementEntry requirement={requirement} />
{/each}
</BigEntry>
</Main>

8
frontend/src/lib/components/project/RequirementEntry.svelte → frontend/src/lib/components/project/RequirementSection.svelte

@ -1,7 +1,7 @@
<script lang="ts">
import Markdown from "$lib/components/common/Markdown.svelte";
import Progress from "$lib/components/common/Progress.svelte";
import Entry from "$lib/components/layout/Entry.svelte";
import Section from "$lib/components/layout/Section.svelte";
import Option from "$lib/components/layout/Option.svelte";
import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
import type { Requirement } from "$lib/models/project";
@ -9,12 +9,12 @@
import LabeledProgressRow from "../common/LabeledProgressRow.svelte";
import ProgressRow from "../common/LabeledProgressRow.svelte";
import { STATUS_ICONS } from "../common/StatusIcon.svelte";
import ItemEntry from "./ItemEntry.svelte";
import ItemEntry from "./ItemSubSection.svelte";
export let requirement: Requirement;
</script>
<Entry title={requirement.name} icon={STATUS_ICONS[requirement.status]} status={requirement.status}>
<Section title={requirement.name} icon={STATUS_ICONS[requirement.status]} status={requirement.status}>
<Progress alwaysSmooth titlePercentageOnly thin green status={requirement.status} count={requirement.totalAcquired} target={requirement.totalRequired} />
<Progress alwaysSmooth titlePercentageOnly thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} />
<Markdown source={requirement.description} />
@ -33,4 +33,4 @@ import ItemEntry from "./ItemEntry.svelte";
{#each requirement.items as item (item.id)}
<ItemEntry item={item} />
{/each}
</Entry>
</Section>

24
frontend/src/lib/components/scope/StatSubSection.svelte

@ -0,0 +1,24 @@
<script lang="ts">
import type Stat from "$lib/models/stat";
import Amount from "../common/Amount.svelte";
import AmountRow from "../common/AmountRow.svelte";
import Markdown from "../common/Markdown.svelte";
import Option from "../layout/Option.svelte";
import OptionsRow from "../layout/OptionsRow.svelte";
import SubSection from "../layout/SubSection.svelte";
export let stat: Stat;
</script>
<SubSection noProgress title={stat.name} subtitle="{stat.weight}x">
<OptionsRow slot="right">
<Option open={{name: "stat.edit", stat}}>Edit</Option>
<Option open={{name: "stat.delete", stat}} color="red">Delete</Option>
</OptionsRow>
<Markdown source={stat.description} />
<AmountRow>
{#each (stat.allowedAmounts || []) as amount (amount.value)}
<Amount label={amount.label} value={amount.value} />
{/each}
</AmountRow>
</SubSection>

9
frontend/src/lib/modals/DeletionModal.svelte

@ -15,10 +15,11 @@
import { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte";
import { getProjectListContext } from "$lib/components/contexts/ProjectListContext.svelte";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import type Stat from "$lib/models/stat";
import { scopePrettyId } from "$lib/utils/prettyIds";
const {currentModal, closeModal} = getModalContext();
const {scope} = getScopeContext();
const {scope, deleteStat} = getScopeContext();
const {project, reloadProject} = getProjectContext();
const {reloadProjectList} = getProjectListContext();
@ -96,7 +97,7 @@
loading = true;
try {
await sl3(fetch).fetch("DELETE", endpoint);
const res = await sl3(fetch).fetch("DELETE", endpoint);
if (navigate) {
if (noun === "Project") {
@ -107,6 +108,10 @@
} else {
// Wait for project reload if it's updating a project
await reloadProject();
if (noun === "Stat") {
deleteStat((res as {stat: Stat}).stat);
}
// TODO: History context upsert
}

113
frontend/src/lib/modals/StatCreateEditModal.svelte

@ -0,0 +1,113 @@
<script lang="ts">
import { sl3 } from "$lib/clients/sl3";
import Modal from "$lib/components/common/Modal.svelte";
import ModalBody from "$lib/components/common/ModalBody.svelte";
import { getModalContext } from "$lib/components/contexts/ModalContext.svelte";
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import type { Requirement } from "$lib/models/project";
import type Scope from "$lib/models/scope";
import type { StatInput } from "$lib/models/stat";
import type Stat from "$lib/models/stat";
import { getAllContexts } from "svelte";
const {currentModal, closeModal} = getModalContext();
const {scope, upsertStat} = getScopeContext();
let stat: StatInput
let statId: number
let op: string
let error: string
let loading: boolean
let show: boolean
$: switch ($currentModal.name) {
case "stat.create":
initCreate($scope)
break;
case "stat.edit":
initEdit($currentModal.stat)
break;
default:
loading = false;
error = null;
show = false;
}
function initCreate(scope: Scope, requirement?: Requirement) {
let stats = requirement?.stats.map(s => ({statId: s.id, required: 0, acquired: 0}));
if (stats == null) {
stats = scope.stats.map(s => ({statId: s.id, required: 0, acquired: 0}))
}
stat = {
name: "",
weight: 1,
description: "",
allowedAmounts: null,
}
op = "Create"
show = true;
}
function initEdit(current: Stat) {
stat = {
name: current.name,
weight: current.weight,
description: current.description,
allowedAmounts: current.allowedAmounts ? current.allowedAmounts.slice() : void(0),
};
statId = current.id;
op = "Edit"
show = true;
}
async function submit() {
error = null;
loading = true;
try {
switch (op) {
case "Create":
const createdStat = await sl3(fetch).createStat($scope.id, stat);
upsertStat(createdStat);
break;
case "Edit":
const editedStat = await sl3(fetch).updateStat($scope.id, statId, {
...stat,
allowedAmounts: void(0),
});
upsertStat(editedStat);
break;
}
closeModal();
} catch(err) {
if (err.statusCode != null) {
error = err.statusMessage;
} else {
error = err
}
} finally {
loading = false;
}
}
</script>
<form on:submit|preventDefault={submit}>
<Modal closable show={show} verb={op} noun="Item" disabled={loading} error={error}>
<ModalBody>
<label for="name">Name</label>
<input name="name" type="text" bind:value={stat.name} />
<label for="weight">Weight</label>
<input name="weight" type="number" min={0} step={0.001} bind:value={stat.weight} />
<label for="description">Description</label>
<textarea name="description" bind:value={stat.description} />
</ModalBody>
</Modal>
</form>

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

@ -1,4 +1,4 @@
import type { StatInput, StatProgress } from "./stat";
import type { StatValueInput, StatProgress } from "./stat";
export default interface Item {
id: number
@ -20,5 +20,5 @@ export interface ItemInput {
description: string
acquiredTime?: string
scheduledDate?: string
stats: StatInput[]
stats: StatValueInput[]
}

4
frontend/src/lib/models/project.ts

@ -1,5 +1,5 @@
import type Item from "./item"
import type { StatInput, StatProgress, StatProgressWithPlanned } from "./stat"
import type { StatValueInput, StatProgress, StatProgressWithPlanned } from "./stat"
import type Status from "./status"
export default interface Project extends ProjectEntry {
@ -45,5 +45,5 @@ export interface RequirementInput {
name: string
description: string
status: Status
stats: StatInput[]
stats: StatValueInput[]
}

9
frontend/src/lib/models/stat.ts

@ -23,8 +23,15 @@ export interface StatProgressWithPlanned extends StatProgress {
planned: number
}
export interface StatInput {
export interface StatValueInput {
statId: number
acquired: number
required: number
}
export interface StatInput {
name: string
weight: number
description?: string
allowedAmounts?: StatAllowedAmount[]
}

6
frontend/src/lib/utils/stat.ts

@ -1,7 +1,7 @@
import type { StatInput } from "$lib/models/stat";
import type { StatValueInput } from "$lib/models/stat";
export function statDiff(before: StatInput[], after: StatInput[], deleteValue = 0): StatInput[] {
let res: StatInput[] = [];
export function statDiff(before: StatValueInput[], after: StatValueInput[], deleteValue = 0): StatValueInput[] {
let res: StatValueInput[] = [];
for (const s of before) {
if (!after.find(s2 => s2.statId === s.statId)) {

41
frontend/src/routes/[scope=prettyid]/index.svelte

@ -1,10 +1,49 @@
<script lang="ts" context="module">
import Column from "$lib/components/layout/Column.svelte";
import Columns from "$lib/components/layout/Columns.svelte";
import Row from "$lib/components/layout/Row.svelte";
import type { Load } from "@sveltejs/kit/types/internal";
export const load: Load = async({}) => {
export const load: Load = async({}) => {
return {
props: {}
};
}
</script>
<script lang="ts">
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte";
import Option from "$lib/components/layout/Option.svelte";
import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
import StatSubSection from "$lib/components/scope/StatSubSection.svelte";
import DeletionModal from "$lib/modals/DeletionModal.svelte";
import StatCreateEditModal from "$lib/modals/StatCreateEditModal.svelte";
const {scope} = getScopeContext();
</script>
<Columns wide>
<Column>
<Row title="Schedule">
</Row>
<Row title="Today">
</Row>
</Column>
<Column>
<Row title="Sprints">
</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>
<StatCreateEditModal />
<DeletionModal />

6
frontend/src/routes/[scope=prettyid]/projects/[project=prettyid].svelte

@ -16,12 +16,12 @@
</script>
<script lang="ts">
import ProjectEntry from "$lib/components/project/ProjectEntry.svelte";
import ProjectEntry from "$lib/components/project/ProjectMain.svelte";
import ProjectContext from "$lib/components/contexts/ProjectContext.svelte";
import ItemCreateModal from "$lib/modals/ItemCreateModal.svelte";
import RequirementCreateModal from "$lib/modals/RequirementCreateModal.svelte";
import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte";
import DeletionModal from "$lib/modals/DeletionModal.svelte";
import ItemAcquireModal from "$lib/modals/ItemAcquireModal.svelte";
import DeletionModal from "$lib/modals/DeletionModal.svelte";
export let project: Project;
</script>

Loading…
Cancel
Save