editor: nastroj pro editaci detailu tasku
This commit is contained in:
parent
80017529a7
commit
95426fa0d2
10 changed files with 188 additions and 30 deletions
|
@ -27,6 +27,7 @@
|
|||
"@types/d3": "^5.7.2",
|
||||
"d3": "^6.2.0",
|
||||
"sigma": "1.2.1",
|
||||
"sirv-cli": "^1.0.0"
|
||||
"sirv-cli": "^1.0.0",
|
||||
"svelte-simple-modal": "^0.6.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,19 +5,18 @@
|
|||
import TasksLoader from "./TasksLoader.svelte";
|
||||
import TaskPanel from "./TaskPanel.svelte";
|
||||
import Editor from "./Editor.svelte";
|
||||
import GraphEdge from "./GraphEdge.svelte";
|
||||
import type { detach } from "svelte/internal";
|
||||
import Modal from "svelte-simple-modal";
|
||||
|
||||
const tasksPromise: Promise<TasksFile> = loadTasks();
|
||||
|
||||
let taskPanel: TaskPanel
|
||||
let taskPanel: TaskPanel;
|
||||
|
||||
// react to hash changes
|
||||
let hash = window.location.hash.substr(1);
|
||||
window.onhashchange = () => {
|
||||
hash = window.location.hash.substr(1);
|
||||
}
|
||||
$: selectedTaskId = (/^task\/([^\/]*)/.exec(hash) || [null, null])[1]
|
||||
};
|
||||
$: selectedTaskId = (/^task\/([^\/]*)/.exec(hash) || [null, null])[1];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -29,19 +28,22 @@ import type { detach } from "svelte/internal";
|
|||
</style>
|
||||
|
||||
<main>
|
||||
{#if hash == 'editor'}
|
||||
<TasksLoader promise={tasksPromise} let:data={t}>
|
||||
<Editor tasks={t} />
|
||||
</TasksLoader>
|
||||
{:else}
|
||||
<TasksLoader promise={tasksPromise} let:data={t}>
|
||||
<TaskPanel tasks={t} bind:this={taskPanel} {selectedTaskId} />
|
||||
<div style="height: 100%">
|
||||
<Graph tasks={t}
|
||||
on:selectTask={e => location.hash=`#task/${e.detail.id}`}
|
||||
on:preSelectTask={e => taskPanel.preSelect(e.detail)}
|
||||
on:unPreSelectTask={e => taskPanel.unPreselect(e.detail)} />
|
||||
</div>
|
||||
</TasksLoader>
|
||||
{/if}
|
||||
<Modal>
|
||||
{#if hash == 'editor'}
|
||||
<TasksLoader promise={tasksPromise} let:data={t}>
|
||||
<Editor tasks={t} />
|
||||
</TasksLoader>
|
||||
{:else}
|
||||
<TasksLoader promise={tasksPromise} let:data={t}>
|
||||
<TaskPanel tasks={t} bind:this={taskPanel} {selectedTaskId} />
|
||||
<div style="height: 100%">
|
||||
<Graph
|
||||
tasks={t}
|
||||
on:selectTask={(e) => (location.hash = `#task/${e.detail.id}`)}
|
||||
on:preSelectTask={(e) => taskPanel.preSelect(e.detail)}
|
||||
on:unPreSelectTask={(e) => taskPanel.unPreselect(e.detail)} />
|
||||
</div>
|
||||
</TasksLoader>
|
||||
{/if}
|
||||
</Modal>
|
||||
</main>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
<script type="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import Graph from "./Graph.svelte";
|
||||
import { nonNull } from "./helpers";
|
||||
import type { TaskDescriptor, TasksFile } from "./task-loader";
|
||||
import { saveTasks, getCategories } from "./task-loader";
|
||||
import TaskDisplay from "./TaskDisplay.svelte";
|
||||
import TaskDetailEditor from "./TaskDetailEditor.svelte";
|
||||
|
||||
export let tasks: TasksFile;
|
||||
|
||||
|
@ -12,6 +15,7 @@
|
|||
let graph: Graph;
|
||||
let currentTask: TaskDescriptor | null = null;
|
||||
let nodeDraggingEnabled: boolean = false;
|
||||
const { open } = getContext("simple-modal");
|
||||
|
||||
function clickTask(e: CustomEvent<TaskDescriptor>) {
|
||||
// ukladani seznamu poslednich kliknuti
|
||||
|
@ -48,6 +52,15 @@
|
|||
async function saveCurrentState() {
|
||||
await saveTasks(tasks);
|
||||
}
|
||||
|
||||
function openTaskDetailEditor(e: CustomEvent<TaskDescriptor>) {
|
||||
open(
|
||||
TaskDetailEditor,
|
||||
{ task: e.detail, tasks: tasks },
|
||||
{ closeButton: false },
|
||||
{ onClose: () => { console.log("callback invoked", tasks); tasks = tasks; }}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -112,8 +125,9 @@
|
|||
{repulsionForce}
|
||||
on:selectTask={clickTask}
|
||||
on:preSelectTask={startHovering}
|
||||
bind:this={graph}
|
||||
{nodeDraggingEnabled} />
|
||||
bind:this={graph}
|
||||
{nodeDraggingEnabled}
|
||||
on:openTask={openTaskDetailEditor} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { taskForce } from "./task-force";
|
||||
import { grabTaskStates, isLoggedIn } from "./ksp-task-grabber";
|
||||
import type { TaskStatus } from "./ksp-task-grabber"
|
||||
import { json } from "d3";
|
||||
|
||||
export let tasks: TasksFile;
|
||||
export let repulsionForce: number = -1000;
|
||||
|
@ -41,6 +40,10 @@ import { json } from "d3";
|
|||
eventDispatcher("selectTask", task);
|
||||
};
|
||||
|
||||
const nodeDoubleClick = (task: TaskDescriptor) => (e: CustomEvent<MouseEvent>) => {
|
||||
eventDispatcher("openTask", task);
|
||||
};
|
||||
|
||||
const nodeHover = (task: TaskDescriptor) => (
|
||||
hovering: CustomEvent<boolean>
|
||||
) => {
|
||||
|
@ -153,7 +156,8 @@ import { json } from "d3";
|
|||
on:hoveringChange={nodeHover(task.task)}
|
||||
on:positionChange={() => { tasks = tasks; }}
|
||||
status={taskStatuses.get(task.id)}
|
||||
draggingEnabled={nodeDraggingEnabled} />
|
||||
draggingEnabled={nodeDraggingEnabled}
|
||||
on:dblclick={nodeDoubleClick(task.task)} />
|
||||
{/each}
|
||||
</g>
|
||||
</g>
|
||||
|
|
|
@ -64,6 +64,12 @@
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function dblclick(e: MouseEvent) {
|
||||
eventDispatcher("dblclick", e);
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -81,7 +87,7 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<g on:mouseenter={enter} on:mouseleave={leave} on:click={click} on:mousedown={dragStart} on:mouseup={dragStop} on:mousemove={drag} class={status && status.submitted ? "submitted" : ""}>
|
||||
<g on:mouseenter={enter} on:mouseleave={leave} on:click={click} on:mousedown={dragStart} on:mouseup={dragStop} on:mousemove={drag} class={status && status.submitted ? "submitted" : ""} on:dblclick={dblclick}>
|
||||
<ellipse rx={ellipse_rx} ry={20} {cx} {cy} />
|
||||
<text
|
||||
bind:this={text_element}
|
||||
|
@ -89,6 +95,6 @@
|
|||
y={cy + 5}
|
||||
text-anchor="middle"
|
||||
alignment-baseline="middle">
|
||||
{task.id}
|
||||
{task.task.title == null ? task.id : task.task.title}
|
||||
</text>
|
||||
</g>
|
||||
|
|
103
frontend/src/TaskDetailEditor.svelte
Normal file
103
frontend/src/TaskDetailEditor.svelte
Normal file
|
@ -0,0 +1,103 @@
|
|||
<script lang="ts">
|
||||
import { getCategories } from "./task-loader";
|
||||
import type { TaskDescriptor, TasksFile } from "./task-loader";
|
||||
import { getContext } from "svelte";
|
||||
import { copyFieldsThatExist } from "./helpers";
|
||||
|
||||
const { close } = getContext("simple-modal");
|
||||
|
||||
export let task: TaskDescriptor;
|
||||
export let tasks: TasksFile;
|
||||
|
||||
let newCategory: string;
|
||||
|
||||
// copy of task data which we can safely change
|
||||
let editData = {
|
||||
task: {
|
||||
...task,
|
||||
title: task.title == null ? task.id : task.title,
|
||||
},
|
||||
categories: getCategories(tasks, task.id),
|
||||
};
|
||||
|
||||
function removeCategory(catName: string) {
|
||||
return function () {
|
||||
editData.categories = editData.categories.filter((t) => t != catName);
|
||||
};
|
||||
}
|
||||
|
||||
function removeDependency(dep: string) {
|
||||
return function () {
|
||||
editData.task.requires = editData.task.requires.filter((t) => t != dep);
|
||||
};
|
||||
}
|
||||
|
||||
function saveAndExit() {
|
||||
Object.assign(task, editData.task);
|
||||
close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div><i>ID:</i> {editData.task.id}, TYPE: {editData.task.type}</div>
|
||||
<h1>
|
||||
<i>TITLE:</i>
|
||||
<span contenteditable="true" bind:textContent={editData.task.title} />
|
||||
</h1>
|
||||
<div>
|
||||
<i>COMMENT:</i>
|
||||
<span contenteditable="true" bind:textContent={editData.task.comment} />
|
||||
</div>
|
||||
{#if editData.task.type == "text"}
|
||||
<div><i>HTML obsah:</i></div>
|
||||
<div contenteditable="true" bind:textContent={editData.task.htmlContent} />
|
||||
{/if}
|
||||
<hr />
|
||||
<div>
|
||||
<h3>Kategorie</h3>
|
||||
<ul>
|
||||
{#each editData.categories as cat}
|
||||
<li>
|
||||
{cat}<button style="margin-left: 1em;" on:click={removeCategory(cat)}>x</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newCategory}
|
||||
placeholder="Nová kategorie" />
|
||||
<button
|
||||
on:click={() => {
|
||||
editData.categories = [...editData.categories, newCategory];
|
||||
}}>Přidat</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<h3>Závislosti</h3>
|
||||
<ul>
|
||||
{#each editData.task.requires as req}
|
||||
<li>
|
||||
{req}<button style="margin-left: 1em;" on:click={removeDependency(req)}>x</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<span>
|
||||
<i>Jestli chceš vyrobit novou závislost, vyrob novou hranu v grafu.
|
||||
Tady to nejde.</i>
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<button on:click={saveAndExit}>Uložit a zavřít</button>
|
||||
<button on:click={close}>Zrušit</button>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<h3>Vysvětlivky</h3>
|
||||
<i>Kurzívou je psaný text, který nesouvisí s daty. Některý text psaný
|
||||
nekurzívou se dá editovat. Změna typu ale třeba není možná, na to je
|
||||
potřeba upravit přímo soubor <code>tasks.json</code>.</i>
|
||||
</div>
|
||||
</div>
|
|
@ -1 +1,7 @@
|
|||
export function nonNull<T>(a: T | null | undefined): T { return a! }
|
||||
|
||||
export function copyFieldsThatExist(dest: any, source: any) {
|
||||
for (const attr of Object.keys(dest)) {
|
||||
if (attr in source) dest[attr] = source[attr]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,24 @@ import type { SimulationNodeDatum, SimulationLinkDatum } from "d3";
|
|||
|
||||
export type TaskDescriptor = {
|
||||
id: string
|
||||
title?: string
|
||||
requires: string[]
|
||||
comment?: string
|
||||
}
|
||||
} & (
|
||||
{
|
||||
type: "open-data",
|
||||
taskReference: string
|
||||
}
|
||||
|
|
||||
{
|
||||
type: "text",
|
||||
htmlContent: string
|
||||
}
|
||||
|
|
||||
{
|
||||
type: "label"
|
||||
}
|
||||
);
|
||||
|
||||
export type TasksFile = {
|
||||
tasks: TaskDescriptor[]
|
||||
|
@ -28,7 +43,7 @@ export async function saveTasks(tasks: TasksFile) {
|
|||
let p: any = {}
|
||||
for (let [key, val] of tasks.positions.entries())
|
||||
p[key] = val;
|
||||
const data = {...tasks, positions: p}
|
||||
const data = { ...tasks, positions: p }
|
||||
|
||||
// request options
|
||||
const options = {
|
||||
|
|
|
@ -1328,6 +1328,11 @@ svelte-preprocess@^4.0.0, svelte-preprocess@~4.3.0:
|
|||
detect-indent "^6.0.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
svelte-simple-modal@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/svelte-simple-modal/-/svelte-simple-modal-0.6.1.tgz#5e984f384dda16bc50f00846314dc140ad89864b"
|
||||
integrity sha512-GJGYj+jymzuar105fwkZ73dtcSFCordpbHqt53iE1N1GdqhvEmSs24idRzyIcO7TrTD/V/287X1icFXp88RQHQ==
|
||||
|
||||
svelte2tsx@*:
|
||||
version "0.1.118"
|
||||
resolved "https://registry.yarnpkg.com/svelte2tsx/-/svelte2tsx-0.1.118.tgz#0f408fcd74a69295d6601a99228bc8ca1637b39c"
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
"requires": [
|
||||
"start"
|
||||
],
|
||||
"comment": "kecy o tom, jak se může řešit taková úloha"
|
||||
"comment": "kecy o tom, jak se může řešit taková úloha",
|
||||
"title": "Jak řešit úlohy?",
|
||||
"htmlContent": ""
|
||||
},
|
||||
{
|
||||
"id": "31-Z1-1",
|
||||
|
|
Reference in a new issue