@linxin666/dsh-client-ui-task-board 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +34 -15
- package/lib/client.js.map +1 -1
- package/lib/types/client/board/TaskCard.d.ts +0 -1
- package/lib/types/core/controller.d.ts +12 -4
- package/package.json +1 -1
- package/src/client/board/TaskBoard.tsx +21 -2
- package/src/client/board/TaskCard.tsx +7 -1
- package/src/core/controller.ts +18 -18
|
@@ -2,7 +2,6 @@ import type { TaskRecord } from '../../core/tasks.ts';
|
|
|
2
2
|
/** Compact relative/absolute time label. */
|
|
3
3
|
export declare function formatHostTimestamp(ms: number, timeZone?: string): string;
|
|
4
4
|
export declare function formatTime(ms: number, timeZone?: string): string;
|
|
5
|
-
/** One card in a column. */
|
|
6
5
|
declare function TaskCardInner({ task, pending, timeZone, onClick }: {
|
|
7
6
|
task: TaskRecord;
|
|
8
7
|
pending: boolean;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Board controller: the single owner of task-ledger state and view state.
|
|
3
3
|
*
|
|
4
4
|
* In production it projects the Host ledger and submits confirmed actions;
|
|
5
|
-
* the legacy store seam remains for v1 migration tests.
|
|
6
|
-
*
|
|
5
|
+
* the legacy store seam remains for v1 migration tests. The board closes
|
|
6
|
+
* only on explicit user navigation, never implicitly on session-list churn.
|
|
7
7
|
* Framework-free (structural runtime faces) so the whole orchestration is
|
|
8
8
|
* unit-testable with fakes.
|
|
9
9
|
*
|
|
@@ -188,9 +188,17 @@ export declare class BoardController {
|
|
|
188
188
|
runTask(id: string): Promise<boolean>;
|
|
189
189
|
/** Re-run a settled task through the Host (the Host replans and executes). */
|
|
190
190
|
rerunTask(id: string): Promise<void>;
|
|
191
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Session-list notifications fire for all kinds of incidental churn
|
|
193
|
+
* (background navigation, the Host runner creating and selecting a fresh
|
|
194
|
+
* execution session, settlement, other plugins), so closing on `current`
|
|
195
|
+
* changes would evict the board without the user asking. The board closes
|
|
196
|
+
* only on explicit user navigation: a sidebar session/workspace row click
|
|
197
|
+
* (board-mount onClickSidebarRow) or the board's own actions
|
|
198
|
+
* (openSession / close). Keeping the hook preserves the subscription
|
|
199
|
+
* contract for future listeners.
|
|
200
|
+
*/
|
|
192
201
|
private onSessionsChanged;
|
|
193
|
-
private lastCurrent;
|
|
194
202
|
private persistAndNotify;
|
|
195
203
|
private commitRemote;
|
|
196
204
|
private performRemote;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-task-board",
|
|
3
3
|
"description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { memo, useCallback, useEffect, useState } from 'react'
|
|
7
7
|
import { selectedTaskOf, type BoardController } from '../../core/controller.ts'
|
|
8
|
-
import { COLUMNS, type TaskRecord } from '../../core/tasks.ts'
|
|
8
|
+
import { COLUMNS, canMoveManually, type TaskRecord } from '../../core/tasks.ts'
|
|
9
9
|
import { t } from '../locales.ts'
|
|
10
10
|
import css from '../board.module.css'
|
|
11
11
|
import { NewTaskModal } from './NewTaskModal.tsx'
|
|
@@ -124,8 +124,27 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
124
124
|
) : (
|
|
125
125
|
COLUMNS.map(column => {
|
|
126
126
|
const tasks = visible.filter(task => task.status === column.status)
|
|
127
|
+
const isManualDropTarget = column.status === 'backlog' || column.status === 'todo'
|
|
127
128
|
return (
|
|
128
|
-
<section
|
|
129
|
+
<section
|
|
130
|
+
key={column.status}
|
|
131
|
+
className={css.column}
|
|
132
|
+
data-status={column.status}
|
|
133
|
+
data-dsh-part="column"
|
|
134
|
+
onDragOver={isManualDropTarget ? (event) => {
|
|
135
|
+
event.preventDefault()
|
|
136
|
+
event.dataTransfer.dropEffect = 'move'
|
|
137
|
+
} : undefined}
|
|
138
|
+
onDrop={isManualDropTarget ? (event) => {
|
|
139
|
+
event.preventDefault()
|
|
140
|
+
const taskId = event.dataTransfer.getData('text/plain')
|
|
141
|
+
if (!taskId) return
|
|
142
|
+
const dropped = snapshot.tasks.find(t => t.id === taskId)
|
|
143
|
+
if (dropped && canMoveManually(dropped.status, column.status) && dropped.status !== column.status) {
|
|
144
|
+
controller.moveTask(taskId, column.status)
|
|
145
|
+
}
|
|
146
|
+
} : undefined}
|
|
147
|
+
>
|
|
129
148
|
<header className={css.columnHeader}>
|
|
130
149
|
<span className={css.statusDot} data-status={column.status} aria-hidden="true" />
|
|
131
150
|
<h3 className={css.columnTitle}>{t(STATUS_KEY[column.status])}</h3>
|
|
@@ -37,11 +37,12 @@ export function formatTime(ms: number, timeZone?: string): string {
|
|
|
37
37
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** One card in a column. */
|
|
41
40
|
function TaskCardInner({ task, pending, timeZone, onClick }: { task: TaskRecord; pending: boolean; timeZone?: string; onClick: () => void }) {
|
|
42
41
|
const latest = task.executions[task.executions.length - 1]
|
|
43
42
|
const runs = task.executions.length
|
|
44
43
|
const archived = task.archivedAt !== undefined
|
|
44
|
+
const isDraggable = !archived && task.status !== 'running' && !pending
|
|
45
|
+
|
|
45
46
|
return (
|
|
46
47
|
<button
|
|
47
48
|
type="button"
|
|
@@ -49,6 +50,11 @@ function TaskCardInner({ task, pending, timeZone, onClick }: { task: TaskRecord;
|
|
|
49
50
|
data-status={archived ? 'archived' : task.status}
|
|
50
51
|
data-dsh-part="card"
|
|
51
52
|
data-pending={pending || undefined}
|
|
53
|
+
draggable={isDraggable}
|
|
54
|
+
onDragStart={isDraggable ? (event) => {
|
|
55
|
+
event.dataTransfer.setData('text/plain', task.id)
|
|
56
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
57
|
+
} : undefined}
|
|
52
58
|
onClick={onClick}
|
|
53
59
|
title={task.description !== '' ? task.description : task.title}
|
|
54
60
|
>
|
package/src/core/controller.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Board controller: the single owner of task-ledger state and view state.
|
|
3
3
|
*
|
|
4
4
|
* In production it projects the Host ledger and submits confirmed actions;
|
|
5
|
-
* the legacy store seam remains for v1 migration tests.
|
|
6
|
-
*
|
|
5
|
+
* the legacy store seam remains for v1 migration tests. The board closes
|
|
6
|
+
* only on explicit user navigation, never implicitly on session-list churn.
|
|
7
7
|
* Framework-free (structural runtime faces) so the whole orchestration is
|
|
8
8
|
* unit-testable with fakes.
|
|
9
9
|
*
|
|
@@ -108,11 +108,6 @@ function randomUuid(): string {
|
|
|
108
108
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/** Read the current selection off a session-list snapshot (structural). */
|
|
112
|
-
function currentOf(sessions: SessionsControllerFace): string | undefined {
|
|
113
|
-
return sessions.list.getSnapshot().current
|
|
114
|
-
}
|
|
115
|
-
|
|
116
111
|
function messageOf(error: unknown): string {
|
|
117
112
|
return error instanceof Error ? error.message : String(error)
|
|
118
113
|
}
|
|
@@ -205,10 +200,11 @@ export class BoardController {
|
|
|
205
200
|
|
|
206
201
|
openBoard(): void {
|
|
207
202
|
if (this.boardOpen) return
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
203
|
+
// The board stays open until the user explicitly closes it: by clicking
|
|
204
|
+
// the sidebar entry again, a sidebar session/workspace row, or a board
|
|
205
|
+
// action (openSession / close). Session-list churn (background
|
|
206
|
+
// navigation, the Host runner selecting a fresh execution session,
|
|
207
|
+
// settlement, other plugins) never evicts the board.
|
|
212
208
|
this.boardOpen = true
|
|
213
209
|
this.notify()
|
|
214
210
|
}
|
|
@@ -428,16 +424,20 @@ export class BoardController {
|
|
|
428
424
|
|
|
429
425
|
// --- internals ---------------------------------------------------------------
|
|
430
426
|
|
|
431
|
-
/**
|
|
427
|
+
/**
|
|
428
|
+
* Session-list notifications fire for all kinds of incidental churn
|
|
429
|
+
* (background navigation, the Host runner creating and selecting a fresh
|
|
430
|
+
* execution session, settlement, other plugins), so closing on `current`
|
|
431
|
+
* changes would evict the board without the user asking. The board closes
|
|
432
|
+
* only on explicit user navigation: a sidebar session/workspace row click
|
|
433
|
+
* (board-mount onClickSidebarRow) or the board's own actions
|
|
434
|
+
* (openSession / close). Keeping the hook preserves the subscription
|
|
435
|
+
* contract for future listeners.
|
|
436
|
+
*/
|
|
432
437
|
private onSessionsChanged(): void {
|
|
433
|
-
|
|
434
|
-
const current = currentOf(this.deps.sessions)
|
|
435
|
-
if (current !== this.lastCurrent) this.closeBoard()
|
|
436
|
-
this.lastCurrent = current
|
|
438
|
+
// Intentionally empty: never close the board implicitly.
|
|
437
439
|
}
|
|
438
440
|
|
|
439
|
-
private lastCurrent: string | undefined = undefined
|
|
440
|
-
|
|
441
441
|
private persistAndNotify(): void {
|
|
442
442
|
if (this.deps.transport === undefined) this.deps.store.save(this.tasks)
|
|
443
443
|
this.notify()
|