This commit is contained in:
poanse 2026-02-02 13:07:55 +03:00
parent 457e946288
commit 275ea93d21
9 changed files with 101 additions and 45 deletions

View file

@ -78,14 +78,35 @@ export class Context {
}
public isTaskBlocked(taskId: TaskId) {
return this.projectData.blockerPairs.some((p) => p.blocked === taskId);
if (this.projectData.getTask(taskId).status === StatusCode.DONE) {
return false;
}
return this.projectData.blockerPairs
.filter((p) => p.blocked === taskId)
.some(
(p) =>
this.projectData.getTask(p.blocker).status !==
StatusCode.DONE,
);
}
public isTaskBlocking(taskId: TaskId) {
return this.projectData.blockerPairs.some((p) => p.blocker === taskId);
if (this.projectData.getTask(taskId).status === StatusCode.DONE) {
return false;
}
return this.projectData.blockerPairs
.filter((p) => p.blocker === taskId)
.some(
(p) =>
this.projectData.getTask(p.blocked).status !==
StatusCode.DONE,
);
}
public isBlockerHighlighted = (taskId: TaskId) => {
if (this.projectData.getTask(taskId).status === StatusCode.DONE) {
return false;
}
if (this.chosenBlockedId !== NoTaskId) {
return (
this.chosenBlockedId === taskId ||
@ -195,7 +216,6 @@ export class Context {
public startReparenting(taskId: TaskId) {
this.reparentingTaskId = taskId;
this.selectedTaskId = NoTaskId;
}
public cancelReparenting() {
@ -221,7 +241,6 @@ export class Context {
}
this.projectData.changeParent(this.reparentingTaskId, newParentId);
this.updateTaskPositions();
this.cancelReparenting();
}
public changeFocusedTask(taskId: TaskId): void {

View file

@ -61,6 +61,7 @@ export class ProjectData {
depth: this.getTask(parentId).depth + 1,
});
this.curTaskId++;
this.recalculateStatusRecursive(parentId);
return id;
}
@ -73,14 +74,17 @@ export class ProjectData {
t.parentId = parentTask.taskId;
t.depth = parentTask.depth + 1;
});
this.recalcPriorities(this.getTask(id).parentId);
this.recalcPriorities(task.parentId);
this.recalculateStatusRecursive(task.parentId);
}
public removeTaskBranch(id: number) {
this.getDescendantIds(id).forEach(
(taskId) => (this.getTask(taskId).deleted = true),
);
this.recalcPriorities(this.getTask(id).parentId);
const parentId = this.getTask(id).parentId;
this.recalcPriorities(parentId);
this.recalculateStatusRecursive(parentId);
}
public getDescendantIds(taskId: number, includeDeleted: boolean = false) {
@ -161,10 +165,12 @@ export class ProjectData {
this.recalculateStatusRecursive(task.parentId);
}
public changeParent(taskId: TaskId, parentId: TaskId) {
public changeParent(taskId: TaskId, newParentId: TaskId) {
const taskData = this.getTask(taskId);
taskData.parentId = parentId;
this.recalculateStatusRecursive(parentId);
const oldParentId = taskData.parentId;
taskData.parentId = newParentId;
this.recalculateStatusRecursive(oldParentId);
this.recalculateStatusRecursive(newParentId);
[taskId, ...this.getDescendantIds(taskId)].forEach((taskId) => {
const task = this.getTask(taskId);
task.depth = this.getTask(task.parentId).depth + 1;
@ -185,6 +191,9 @@ export class ProjectData {
public calculateStatus(taskId: TaskId) {
const children = this.getChildren(taskId).map((x) => this.getTask(x));
if (children.length === 0) {
return this.getTask(taskId).status;
}
const counts = [0, 0, 0, 0];
children.forEach((t) => (counts[t.status] += 1));
if (counts[StatusCode.DONE] == children.length) {

View file

@ -34,13 +34,13 @@ export function getTooltipText(code: IconCode) {
return "Add blocker task";
case IconCode.LOCK:
return "Block another task";
case IconCode.REMOVE:
case IconCode.REMOVE_SUBMENU:
return "Remove";
case IconCode.REMOVE_SINGLE:
return "Remove single task";
case IconCode.REMOVE_MULTIPLE:
return "Remove task branch";
case IconCode.STATUS:
case IconCode.STATUS_SUBMENU:
return "Status";
case IconCode.STATUS_DRAFT:
return "Draft";

View file

@ -25,7 +25,13 @@
let isPressedDown = $state(false);
let isPressed = $derived(context.pressedButtonCode == iconCode);
const stateful = [IconCode.REMOVE, IconCode.STATUS, IconCode.KEY, IconCode.LOCK].contains(iconCode);
const stateful = [
IconCode.REMOVE_SUBMENU,
IconCode.STATUS_SUBMENU,
IconCode.KEY,
IconCode.LOCK,
IconCode.REPARENT
].contains(iconCode);
function onpointerdown(event: MouseEvent) {
isPressedDown = true;
@ -41,6 +47,7 @@
(context.isReparentingOn() && [IconCode.LOCK, IconCode.KEY].contains(iconCode))
|| (context.chosenBlockerId !== NoTaskId && [IconCode.REPARENT].contains(iconCode))
|| (context.chosenBlockedId !== NoTaskId && [IconCode.REPARENT].contains(iconCode))
|| (iconCode === IconCode.STATUS_DONE && context.isTaskBlocked(context.selectedTaskId))
);
function onpointerup(event: MouseEvent) {
@ -88,7 +95,7 @@
let classString = $derived(`
${(isPressed && !isButtonDisabled) ? 'is-pressed-up ': ''}
${(isPressedDown && !isButtonDisabled) ? 'is-pressed-down ': '' }
${iconCode === IconCode.STATUS ? classStringFromStatusCode(context.toolbarStatus) + ' ': ""}
${iconCode === IconCode.STATUS_SUBMENU ? classStringFromStatusCode(context.toolbarStatus) + ' ': ""}
`);
</script>
@ -116,10 +123,10 @@
<path d="M2.56174 6.15378V3.84616C2.56174 3.07296 3.18893 2.44577 3.96213 2.44577H4.80881V3.24655H3.96213C3.63076 3.24655 3.36252 3.51479 3.36252 3.84616V6.15378C3.36252 6.48515 3.63076 6.75436 3.96213 6.75436H4.80881V7.55417H3.96213C3.18893 7.55417 2.56174 6.92698 2.56174 6.15378Z"/>
<path d="M2.96191 4.59954V5.40033H0.5V4.59954H2.96191Z"/>
</svg>
{:else if iconCode === IconCode.REMOVE}
{:else if iconCode === IconCode.REMOVE_SUBMENU}
<Trash2 class={classString}/>
{:else if iconCode === IconCode.KEY}
<KeyRound class={classString}/>
<KeyRound class={classString} style="transform: rotate(-90deg) scale(-1, 1);"/>
{:else if iconCode === IconCode.LOCK}
<LockKeyhole class={classString}/>
{:else if iconCode === IconCode.REPARENT}
@ -128,7 +135,7 @@
<LocateFixed class={classString + " focus"}/>
{:else if iconCode === IconCode.CREATE_LINKED_NOTE}
<SquareArrowOutUpRight class={classString}/>
{:else if iconCode === IconCode.STATUS}
{:else if iconCode === IconCode.STATUS_SUBMENU}
<Circle class={classString}/>
{:else if iconCode === IconCode.STATUS_DRAFT}
<Circle class={classString + " draft"}/>
@ -202,6 +209,9 @@
:global(svg) {
stroke: grey;
}
:global(svg.done) {
stroke: color-mix(in srgb, #30623E 100%, #000000 50%);
}
}
.button:hover:not(.disabled){

View file

@ -32,7 +32,7 @@
let midX = $derived(
isBlockerConnection
? (context.chosenBlockerId !== NoTaskId
? endPoint.x + (isStartTaskDepthLE(startTaskId, endTaskId) ? -1 : 1) * (ParentToChildHorizontalShift - TASK_SIZE.width) / 2
? endPoint.x + (isStartTaskDepthLE(endTaskId, startTaskId) ? 1 : -1) * (ParentToChildHorizontalShift - TASK_SIZE.width) / 2
: startPoint.x + (isStartTaskDepthLE(startTaskId, endTaskId) ? 1 : -1) * (ParentToChildHorizontalShift - TASK_SIZE.width) / 2
)
: (startPoint.x + endPoint.x) / 2

View file

@ -156,21 +156,24 @@
&& !(context.chosenBlockerId !== NoTaskId)}
<HideBranchButton {context} {taskId} />
{/if}
{#if context.isTaskBlocking(taskId) && taskData.status !== StatusCode.DONE}
{#if context.isTaskBlocking(taskId)}
<div
class="icon-container"
style="
top: 4px;
"
>
<KeyRound class={
classStringFromStatusCode(taskData.status)
+ (isBlockerHighlight ? ' blocker-highlight' : '')
+ (isUnselected ? ' unselect' : '')
}/>
<KeyRound
class={
classStringFromStatusCode(taskData.status)
+ (isBlockerHighlight ? ' blocker-highlight' : '')
+ (isUnselected ? ' unselect' : '')
}
style="transform: rotate(-90deg) scale(-1, 1);"
/>
</div>
{/if}
{#if context.isTaskBlocked(taskId) && taskData.status !== StatusCode.DONE}
{#if context.isTaskBlocked(taskId)}
<div
class="icon-container"
style="

View file

@ -4,7 +4,7 @@
import Task from "./Task.svelte";
import Panzoom, {type PanzoomObject} from '@panzoom/panzoom'
import type {Context} from "../Context.svelte.js";
import {MouseDown, type Vector2} from "../types";
import {MouseDown, StatusCode, type Vector2} from "../types";
import Connection from "./Connection.svelte";
import {NoTaskId, RootTaskId} from "../NodePositionsCalculator";
import {DraggingManager} from "../DraggingManager.svelte";
@ -160,10 +160,34 @@
isBlockerConnection={false}
/>
{/each}
</g>
</svg>
<div
class="task-layer"
tabindex="-1"
role="presentation"
>
{#each context.projectData.tasks.filter(t => !context.isTaskHidden(t.taskId)) as task (task.taskId)}
<Task taskId={task.taskId} {context} coords={context.getCurrentTaskPosition(task.taskId)}/>
{/each}
</div>
<svg class="svg-layer" overflow="visible">
<defs>
<defs>
<marker id="arrow" markerWidth="5" markerHeight="10" refX="3" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M-1,0 L-1,6 L4.5,3 z" fill="context-stroke" />
</marker>
</defs>
</defs>
<g class="svg-group" bind:this={svgGroupEl}>
{#if context.chosenBlockerId !== NoTaskId || context.chosenBlockedId !== NoTaskId}
{#each (context.projectData.blockerPairs.filter(
p => p.blocker === context.chosenBlockerId || p.blocked === context.chosenBlockedId
).filter(
p => context.projectData.getTask(p.blocked).status !== StatusCode.DONE
&& context.projectData.getTask(p.blocker).status !== StatusCode.DONE
)) as pair}
<Connection
startTaskId={pair.blocker}
@ -175,16 +199,7 @@
{/if}
</g>
</svg>
<div
class="task-layer"
tabindex="-1"
role="presentation"
>
{#each context.projectData.tasks.filter(t => !context.isTaskHidden(t.taskId)) as task (task.taskId)}
<Task taskId={task.taskId} {context} coords={context.getCurrentTaskPosition(task.taskId)}/>
{/each}
</div>
{#if context.selectedTaskId !== -1}
{#key context.selectedTaskId}
<Toolbar context={context} taskId={context.selectedTaskId}/>

View file

@ -33,16 +33,16 @@
let toolbarButtons = $derived(taskId == RootTaskId ? [
IconCode.CREATE_LINKED_NOTE,
IconCode.FOCUS,
IconCode.STATUS
IconCode.STATUS_SUBMENU
] : [
IconCode.CREATE_LINKED_NOTE,
IconCode.REMOVE,
IconCode.REMOVE_SUBMENU,
IconCode.REPARENT,
IconCode.KEY,
IconCode.LOCK,
IconCode.FOCUS,
IconCode.STATUS
IconCode.STATUS_SUBMENU
]);
let removeButtons = [
@ -86,13 +86,13 @@
{/each}
{/key}
{#if context.pressedButtonCode === IconCode.REMOVE}
{#if context.pressedButtonCode === IconCode.REMOVE_SUBMENU}
<div
class="subtoolbar"
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
style="
top: {subtoolbarTopShift(removeButtons)}px;
left: {toolbarButtons.indexOf(IconCode.REMOVE) * (BUTTON_SIZE + TOOLBAR_GAP) - 2}px;
left: {toolbarButtons.indexOf(IconCode.REMOVE_SUBMENU) * (BUTTON_SIZE + TOOLBAR_GAP) - 2}px;
"
>
{#key context.updateOnZoomCounter}
@ -102,13 +102,13 @@
</div>
{/if}
{#if context.pressedButtonCode === IconCode.STATUS}
{#if context.pressedButtonCode === IconCode.STATUS_SUBMENU}
<div
class="subtoolbar"
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
style="
top: {subtoolbarTopShift(statusButtons)}px;
left: {toolbarButtons.indexOf(IconCode.STATUS) * (BUTTON_SIZE + TOOLBAR_GAP) - 2}px;
left: {toolbarButtons.indexOf(IconCode.STATUS_SUBMENU) * (BUTTON_SIZE + TOOLBAR_GAP) - 2}px;
"
>
{#key context.updateOnZoomCounter}

View file

@ -1,13 +1,13 @@
import type { EasingFunction } from "svelte/transition";
export enum IconCode {
REMOVE,
REMOVE_SUBMENU,
KEY,
LOCK,
FOCUS,
REPARENT,
CREATE_LINKED_NOTE,
STATUS,
STATUS_SUBMENU,
REMOVE_SINGLE,
REMOVE_MULTIPLE,
STATUS_DRAFT,