15 changed files with 322 additions and 43 deletions
-
9frontend/src/lib/components/contexts/ModalContext.svelte
-
2frontend/src/lib/components/contexts/ProjectContext.svelte
-
60frontend/src/lib/components/contexts/ProjectListContext.svelte
-
4frontend/src/lib/components/project/ItemEntry.svelte
-
4frontend/src/lib/components/project/ProjectEntry.svelte
-
2frontend/src/lib/components/project/RequirementEntry.svelte
-
6frontend/src/lib/components/scope/ProjectMenu.svelte
-
151frontend/src/lib/modals/DeletionModal.svelte
-
57frontend/src/lib/modals/ItemCreateModal.svelte
-
2frontend/src/lib/utils/date.ts
-
33frontend/src/routes/[scope=prettyid]/__layout.svelte
-
2frontend/src/routes/[scope=prettyid]/projects/[project=prettyid].svelte
-
7ports/httpapi/items.go
-
13usecases/items/result.go
-
13usecases/items/service.go
@ -0,0 +1,60 @@ |
|||
<script lang="ts" context="module"> |
|||
const contextKey = {ctx: "projectListCtx"}; |
|||
|
|||
interface ProjectListContextData { |
|||
projects: Readable<ProjectEntry[]>, |
|||
reloadProjectList(): Promise<void>, |
|||
}; |
|||
|
|||
const fallback: ProjectListContextData = { |
|||
projects: readable([]), |
|||
reloadProjectList: () => Promise.resolve() |
|||
}; |
|||
|
|||
export function getProjectListContext() { |
|||
return getContext(contextKey) as ProjectListContextData || fallback |
|||
} |
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
import { readable, writable, type Readable } from "svelte/store"; |
|||
import { getContext, setContext } from "svelte"; |
|||
import { sl3 } from "$lib/clients/sl3"; |
|||
import { getScopeContext } from "./ScopeContext.svelte"; |
|||
import type { ProjectEntry } from "$lib/models/project"; |
|||
|
|||
export let projects: ProjectEntry[]; |
|||
|
|||
const {scope} = getScopeContext(); |
|||
|
|||
let projectWritable = writable<ProjectEntry[]>(projects); |
|||
let loading = false; |
|||
let lastSet = projects; |
|||
|
|||
setContext<ProjectListContextData>(contextKey, { |
|||
projects: {subscribe: projectWritable.subscribe}, |
|||
reloadProjectList, |
|||
}); |
|||
|
|||
async function reloadProjectList() { |
|||
if (loading) { |
|||
return |
|||
} |
|||
|
|||
try { |
|||
const newProjects = await sl3(fetch).listProjects($scope.id) |
|||
projectWritable.set(newProjects); |
|||
} catch(_) {} |
|||
|
|||
loading = false; |
|||
} |
|||
|
|||
$: { |
|||
if (lastSet !== projects) { |
|||
projectWritable.set(projects); |
|||
lastSet = projects; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<slot></slot> |
@ -0,0 +1,151 @@ |
|||
<script lang="ts" context="module"> |
|||
interface DeletionSideEffect { |
|||
name: string |
|||
op: string |
|||
} |
|||
</script> |
|||
|
|||
<script lang="ts"> |
|||
import { goto } from "$app/navigation"; |
|||
|
|||
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 { getProjectContext } from "$lib/components/contexts/ProjectContext.svelte"; |
|||
import { getProjectListContext } from "$lib/components/contexts/ProjectListContext.svelte"; |
|||
import { getScopeContext } from "$lib/components/contexts/ScopeContext.svelte"; |
|||
import { scopePrettyId } from "$lib/utils/prettyIds"; |
|||
|
|||
const {currentModal, closeModal} = getModalContext(); |
|||
const {scope} = getScopeContext(); |
|||
const {project, reloadProject} = getProjectContext(); |
|||
const {reloadProjectList} = getProjectListContext(); |
|||
|
|||
let endpoint: string; |
|||
|
|||
let error: string |
|||
let loading: boolean; |
|||
let show: boolean; |
|||
let noun: string; |
|||
let sideEffects: DeletionSideEffect[]; |
|||
let name: string; |
|||
let dangerZone: boolean; |
|||
let dangerZoneConfirm: string; |
|||
let navigate: string; |
|||
|
|||
$: switch ($currentModal.name) { |
|||
case "item.delete": |
|||
init(`items/${$currentModal.item.id}`) |
|||
noun = "Item"; |
|||
name = $currentModal.item.name; |
|||
break; |
|||
|
|||
case "stat.delete": |
|||
init(`stats/${$currentModal.stat.id}`) |
|||
noun = "Stat"; |
|||
name = $currentModal.stat.name; |
|||
dangerZone = true; |
|||
break; |
|||
|
|||
case "requirement.delete": |
|||
init(`projects/${$project.id}/requirements/${$currentModal.requirement.id}`, |
|||
$currentModal.requirement.items.map(i => ({ |
|||
op: "Detach item", |
|||
name: i.name, |
|||
})) |
|||
); |
|||
noun = "Requirement"; |
|||
name = $currentModal.requirement.name; |
|||
break; |
|||
|
|||
case "project.delete": |
|||
init(`projects/${$currentModal.project.id}`, |
|||
$currentModal.project.requirements.flatMap(r => ([{ |
|||
op: "Delete requirement", |
|||
name: `${r.name} (${r.statusName})`, |
|||
}, ...r.items.map(i => ({ |
|||
op: "Detach item", |
|||
name: i.name, |
|||
}))])) |
|||
) |
|||
noun = "Project"; |
|||
name = $currentModal.project.name; |
|||
dangerZone = true; |
|||
navigate = `/${scopePrettyId($scope)}`; |
|||
break; |
|||
|
|||
default: |
|||
loading = false; |
|||
error = null; |
|||
show = false; |
|||
} |
|||
|
|||
function init(newEndpoint: string, newSideEffects: DeletionSideEffect[] = []) { |
|||
endpoint = `scopes/${$scope.id}/${newEndpoint}`; |
|||
sideEffects = newSideEffects; |
|||
show = true; |
|||
error = null; |
|||
dangerZone = false; |
|||
dangerZoneConfirm = ""; |
|||
navigate = null; |
|||
} |
|||
|
|||
async function submit() { |
|||
error = null; |
|||
loading = true; |
|||
|
|||
try { |
|||
await sl3(fetch).fetch("DELETE", endpoint); |
|||
|
|||
if (navigate) { |
|||
if (noun === "Project") { |
|||
await reloadProjectList(); |
|||
} |
|||
|
|||
goto(navigate); |
|||
} else { |
|||
// Wait for project reload if it's updating a project |
|||
await reloadProject(); |
|||
|
|||
// TODO: History context upsert |
|||
} |
|||
|
|||
closeModal(); |
|||
} catch(err) { |
|||
if (err.statusCode != null) { |
|||
error = err.statusMessage; |
|||
} else { |
|||
error = err |
|||
} |
|||
|
|||
} finally { |
|||
loading = false; |
|||
} |
|||
} |
|||
|
|||
let disabled; |
|||
$: disabled = loading || (!!dangerZone && name !== dangerZoneConfirm) |
|||
</script> |
|||
|
|||
<form on:submit|preventDefault={submit}> |
|||
<Modal closable show={show} verb="Delete" noun={noun} disabled={disabled} error={error}> |
|||
<ModalBody> |
|||
<p>Are you sure you want to delete this {noun.toLocaleLowerCase()}?</p> |
|||
{#if sideEffects.length > 0} |
|||
<p>There are specific side-effects to this deletion.</p> |
|||
<ul> |
|||
{#each sideEffects as sideEffect} |
|||
<li>{sideEffect.op} {sideEffect.name}</li> |
|||
{/each} |
|||
</ul> |
|||
{/if} |
|||
<label for="name">{noun} name</label> |
|||
<input type="text" disabled name="name" value={name} /> |
|||
{#if dangerZone} |
|||
<label for="confirm">Re-type name to confirm</label> |
|||
<input type="text" name="confirm" bind:value={dangerZoneConfirm} /> |
|||
{/if} |
|||
</ModalBody> |
|||
</Modal> |
|||
</form> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue