Browse Source

editor: nastroj pro editaci detailu tasku

mj-deploy
Vašek Šraier 4 years ago
parent
commit
95426fa0d2
  1. 3
      frontend/package.json
  2. 42
      frontend/src/App.svelte
  3. 18
      frontend/src/Editor.svelte
  4. 8
      frontend/src/Graph.svelte
  5. 10
      frontend/src/GraphNode.svelte
  6. 103
      frontend/src/TaskDetailEditor.svelte
  7. 6
      frontend/src/helpers.ts
  8. 19
      frontend/src/task-loader.ts
  9. 5
      frontend/yarn.lock
  10. 4
      tasks.json

3
frontend/package.json

@ -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"
} }
} }

42
frontend/src/App.svelte

@ -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>

18
frontend/src/Editor.svelte

@ -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
frontend/src/Graph.svelte

@ -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>

10
frontend/src/GraphNode.svelte

@ -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

@ -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>

6
frontend/src/helpers.ts

@ -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]
}
}

19
frontend/src/task-loader.ts

@ -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 = {

5
frontend/yarn.lock

@ -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"

4
tasks.json

@ -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",