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",
|
"@types/d3": "^5.7.2",
|
||||||
"d3": "^6.2.0",
|
"d3": "^6.2.0",
|
||||||
"sigma": "1.2.1",
|
"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 TasksLoader from "./TasksLoader.svelte";
|
||||||
import TaskPanel from "./TaskPanel.svelte";
|
import TaskPanel from "./TaskPanel.svelte";
|
||||||
import Editor from "./Editor.svelte";
|
import Editor from "./Editor.svelte";
|
||||||
import GraphEdge from "./GraphEdge.svelte";
|
import Modal from "svelte-simple-modal";
|
||||||
import type { detach } from "svelte/internal";
|
|
||||||
|
|
||||||
const tasksPromise: Promise<TasksFile> = loadTasks();
|
const tasksPromise: Promise<TasksFile> = loadTasks();
|
||||||
|
|
||||||
let taskPanel: TaskPanel
|
let taskPanel: TaskPanel;
|
||||||
|
|
||||||
// react to hash changes
|
// react to hash changes
|
||||||
let hash = window.location.hash.substr(1);
|
let hash = window.location.hash.substr(1);
|
||||||
window.onhashchange = () => {
|
window.onhashchange = () => {
|
||||||
hash = window.location.hash.substr(1);
|
hash = window.location.hash.substr(1);
|
||||||
}
|
};
|
||||||
$: selectedTaskId = (/^task\/([^\/]*)/.exec(hash) || [null, null])[1]
|
$: selectedTaskId = (/^task\/([^\/]*)/.exec(hash) || [null, null])[1];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -29,19 +28,22 @@ import type { detach } from "svelte/internal";
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
{#if hash == 'editor'}
|
<Modal>
|
||||||
<TasksLoader promise={tasksPromise} let:data={t}>
|
{#if hash == 'editor'}
|
||||||
<Editor tasks={t} />
|
<TasksLoader promise={tasksPromise} let:data={t}>
|
||||||
</TasksLoader>
|
<Editor tasks={t} />
|
||||||
{:else}
|
</TasksLoader>
|
||||||
<TasksLoader promise={tasksPromise} let:data={t}>
|
{:else}
|
||||||
<TaskPanel tasks={t} bind:this={taskPanel} {selectedTaskId} />
|
<TasksLoader promise={tasksPromise} let:data={t}>
|
||||||
<div style="height: 100%">
|
<TaskPanel tasks={t} bind:this={taskPanel} {selectedTaskId} />
|
||||||
<Graph tasks={t}
|
<div style="height: 100%">
|
||||||
on:selectTask={e => location.hash=`#task/${e.detail.id}`}
|
<Graph
|
||||||
on:preSelectTask={e => taskPanel.preSelect(e.detail)}
|
tasks={t}
|
||||||
on:unPreSelectTask={e => taskPanel.unPreselect(e.detail)} />
|
on:selectTask={(e) => (location.hash = `#task/${e.detail.id}`)}
|
||||||
</div>
|
on:preSelectTask={(e) => taskPanel.preSelect(e.detail)}
|
||||||
</TasksLoader>
|
on:unPreSelectTask={(e) => taskPanel.unPreselect(e.detail)} />
|
||||||
{/if}
|
</div>
|
||||||
|
</TasksLoader>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
<script type="ts">
|
<script type="ts">
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
import Graph from "./Graph.svelte";
|
import Graph from "./Graph.svelte";
|
||||||
import { nonNull } from "./helpers";
|
import { nonNull } from "./helpers";
|
||||||
import type { TaskDescriptor, TasksFile } from "./task-loader";
|
import type { TaskDescriptor, TasksFile } from "./task-loader";
|
||||||
import { saveTasks, getCategories } from "./task-loader";
|
import { saveTasks, getCategories } from "./task-loader";
|
||||||
import TaskDisplay from "./TaskDisplay.svelte";
|
import TaskDisplay from "./TaskDisplay.svelte";
|
||||||
|
import TaskDetailEditor from "./TaskDetailEditor.svelte";
|
||||||
|
|
||||||
export let tasks: TasksFile;
|
export let tasks: TasksFile;
|
||||||
|
|
||||||
|
@ -12,6 +15,7 @@
|
||||||
let graph: Graph;
|
let graph: Graph;
|
||||||
let currentTask: TaskDescriptor | null = null;
|
let currentTask: TaskDescriptor | null = null;
|
||||||
let nodeDraggingEnabled: boolean = false;
|
let nodeDraggingEnabled: boolean = false;
|
||||||
|
const { open } = getContext("simple-modal");
|
||||||
|
|
||||||
function clickTask(e: CustomEvent<TaskDescriptor>) {
|
function clickTask(e: CustomEvent<TaskDescriptor>) {
|
||||||
// ukladani seznamu poslednich kliknuti
|
// ukladani seznamu poslednich kliknuti
|
||||||
|
@ -48,6 +52,15 @@
|
||||||
async function saveCurrentState() {
|
async function saveCurrentState() {
|
||||||
await saveTasks(tasks);
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -112,8 +125,9 @@
|
||||||
{repulsionForce}
|
{repulsionForce}
|
||||||
on:selectTask={clickTask}
|
on:selectTask={clickTask}
|
||||||
on:preSelectTask={startHovering}
|
on:preSelectTask={startHovering}
|
||||||
bind:this={graph}
|
bind:this={graph}
|
||||||
{nodeDraggingEnabled} />
|
{nodeDraggingEnabled}
|
||||||
|
on:openTask={openTaskDetailEditor} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
import { taskForce } from "./task-force";
|
import { taskForce } from "./task-force";
|
||||||
import { grabTaskStates, isLoggedIn } from "./ksp-task-grabber";
|
import { grabTaskStates, isLoggedIn } from "./ksp-task-grabber";
|
||||||
import type { TaskStatus } from "./ksp-task-grabber"
|
import type { TaskStatus } from "./ksp-task-grabber"
|
||||||
import { json } from "d3";
|
|
||||||
|
|
||||||
export let tasks: TasksFile;
|
export let tasks: TasksFile;
|
||||||
export let repulsionForce: number = -1000;
|
export let repulsionForce: number = -1000;
|
||||||
|
@ -41,6 +40,10 @@ import { json } from "d3";
|
||||||
eventDispatcher("selectTask", task);
|
eventDispatcher("selectTask", task);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nodeDoubleClick = (task: TaskDescriptor) => (e: CustomEvent<MouseEvent>) => {
|
||||||
|
eventDispatcher("openTask", task);
|
||||||
|
};
|
||||||
|
|
||||||
const nodeHover = (task: TaskDescriptor) => (
|
const nodeHover = (task: TaskDescriptor) => (
|
||||||
hovering: CustomEvent<boolean>
|
hovering: CustomEvent<boolean>
|
||||||
) => {
|
) => {
|
||||||
|
@ -153,7 +156,8 @@ import { json } from "d3";
|
||||||
on:hoveringChange={nodeHover(task.task)}
|
on:hoveringChange={nodeHover(task.task)}
|
||||||
on:positionChange={() => { tasks = tasks; }}
|
on:positionChange={() => { tasks = tasks; }}
|
||||||
status={taskStatuses.get(task.id)}
|
status={taskStatuses.get(task.id)}
|
||||||
draggingEnabled={nodeDraggingEnabled} />
|
draggingEnabled={nodeDraggingEnabled}
|
||||||
|
on:dblclick={nodeDoubleClick(task.task)} />
|
||||||
{/each}
|
{/each}
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|
|
@ -64,6 +64,12 @@
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dblclick(e: MouseEvent) {
|
||||||
|
eventDispatcher("dblclick", e);
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -81,7 +87,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</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} />
|
<ellipse rx={ellipse_rx} ry={20} {cx} {cy} />
|
||||||
<text
|
<text
|
||||||
bind:this={text_element}
|
bind:this={text_element}
|
||||||
|
@ -89,6 +95,6 @@
|
||||||
y={cy + 5}
|
y={cy + 5}
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
alignment-baseline="middle">
|
alignment-baseline="middle">
|
||||||
{task.id}
|
{task.task.title == null ? task.id : task.task.title}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</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 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 = {
|
export type TaskDescriptor = {
|
||||||
id: string
|
id: string
|
||||||
|
title?: string
|
||||||
requires: string[]
|
requires: string[]
|
||||||
comment?: string
|
comment?: string
|
||||||
}
|
} & (
|
||||||
|
{
|
||||||
|
type: "open-data",
|
||||||
|
taskReference: string
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
htmlContent: string
|
||||||
|
}
|
||||||
|
|
|
||||||
|
{
|
||||||
|
type: "label"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export type TasksFile = {
|
export type TasksFile = {
|
||||||
tasks: TaskDescriptor[]
|
tasks: TaskDescriptor[]
|
||||||
|
@ -28,7 +43,7 @@ export async function saveTasks(tasks: TasksFile) {
|
||||||
let p: any = {}
|
let p: any = {}
|
||||||
for (let [key, val] of tasks.positions.entries())
|
for (let [key, val] of tasks.positions.entries())
|
||||||
p[key] = val;
|
p[key] = val;
|
||||||
const data = {...tasks, positions: p}
|
const data = { ...tasks, positions: p }
|
||||||
|
|
||||||
// request options
|
// request options
|
||||||
const options = {
|
const options = {
|
||||||
|
|
|
@ -1328,6 +1328,11 @@ svelte-preprocess@^4.0.0, svelte-preprocess@~4.3.0:
|
||||||
detect-indent "^6.0.0"
|
detect-indent "^6.0.0"
|
||||||
strip-indent "^3.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@*:
|
svelte2tsx@*:
|
||||||
version "0.1.118"
|
version "0.1.118"
|
||||||
resolved "https://registry.yarnpkg.com/svelte2tsx/-/svelte2tsx-0.1.118.tgz#0f408fcd74a69295d6601a99228bc8ca1637b39c"
|
resolved "https://registry.yarnpkg.com/svelte2tsx/-/svelte2tsx-0.1.118.tgz#0f408fcd74a69295d6601a99228bc8ca1637b39c"
|
||||||
|
|
|
@ -12,7 +12,9 @@
|
||||||
"requires": [
|
"requires": [
|
||||||
"start"
|
"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",
|
"id": "31-Z1-1",
|
||||||
|
|
Reference in a new issue