paseo-kanban 0.1.3 → 0.1.4
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/OVERVIEW.md +30 -4
- package/README.md +11 -15
- package/README.zh-CN.md +9 -13
- package/client/i18n.tsx +61 -11
- package/client/kanban-board.tsx +57 -23
- package/client/kanban-card.tsx +120 -38
- package/client/kanban-drag.ts +33 -12
- package/client/kanban-lane.tsx +27 -8
- package/client/kanban-session.ts +121 -27
- package/client/lane-modal.tsx +29 -6
- package/client/task-modal.tsx +20 -1
- package/package.json +1 -1
- package/shared/kanban.ts +237 -61
package/OVERVIEW.md
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
# Kanban
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight, responsive Kanban board plugin for Paseo workspaces.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Flexible swimlanes**: Add, delete, and rename lanes.
|
|
8
|
+
- **Rich task cards**: Edit task titles and multi-line descriptions, manage subtask checklists, and link tasks to Paseo projects.
|
|
9
|
+
- **Task completion**: Check a card or drag it into the completion lane to finish; uncheck or drag out to restore.
|
|
10
|
+
- **Workspace tabs and splits**: Open the board beside an agent or terminal with Paseo's native split controls, initially filtered to the workspace's project, or use the sidebar entry.
|
|
11
|
+
- **Smooth drag and drop**: Support intra-lane reordering and cross-lane movement.
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## Storage and requirements
|
|
14
|
+
|
|
15
|
+
- **Data and privacy**: Board data is stored locally in host-scoped plugin settings with revision checks for concurrent edits. No accounts, tokens, or external network requests are required.
|
|
16
|
+
- **Host requirement**: Paseo 0.8.0 or later.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# 看板 (Kanban)
|
|
21
|
+
|
|
22
|
+
专为 Paseo 工作区打造的轻量响应式看板插件。
|
|
23
|
+
|
|
24
|
+
## 核心功能
|
|
25
|
+
|
|
26
|
+
- **灵活的泳道管理**:支持泳道增删与改名。
|
|
27
|
+
- **丰富的任务卡片**:支持任务标题与多行描述编辑、子步骤清单管理,以及关联 Paseo 项目。
|
|
28
|
+
- **任务完成与恢复**:通过卡片勾选或拖入完成泳道标记完成;取消勾选或拖出即可恢复。
|
|
29
|
+
- **工作区标签页与分屏**:支持通过 Paseo 原生分屏控件与 Agent、Terminal 并排打开,默认筛选工作区所属项目,或使用侧边栏入口。
|
|
30
|
+
- **流畅拖拽交互**:支持同泳道排序与跨泳道状态流转。
|
|
31
|
+
|
|
32
|
+
## 存储与要求
|
|
33
|
+
|
|
34
|
+
- **数据与隐私**:看板数据保存在宿主本地插件设置中,具备版本并发冲突保护(CAS)。无需账号、Token 或任何外部网络请求。
|
|
35
|
+
- **宿主要求**:Paseo 0.8.0 或更高版本。
|
package/README.md
CHANGED
|
@@ -2,21 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[简体中文](./README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
A lightweight, responsive Kanban board plugin for [Paseo](https://paseo.sh) workspaces.
|
|
5
|
+
A lightweight, responsive Kanban board plugin for [Paseo](https://paseo.sh) workspaces.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **Flexible Swimlanes**:
|
|
11
|
+
- **Flexible Swimlanes**: Add, delete, and rename lanes.
|
|
12
12
|
- **Rich Task Cards**:
|
|
13
|
-
- Task title and multi-line description.
|
|
14
|
-
- Subtasks checklist
|
|
15
|
-
-
|
|
16
|
-
- **Workspace Tabs and Splits**: Open the board beside an agent or terminal, initially filtered to the workspace's project
|
|
17
|
-
- **
|
|
18
|
-
- **Smooth Drag-and-Drop**: Real-time hit-testing for reordering cards within lanes and moving cards across lanes.
|
|
19
|
-
- **Versioned State Persistence**: Persists board data through Paseo's settings API with optimistic concurrency control (CAS) to prevent race conditions and accidental overwrites.
|
|
13
|
+
- Task title and multi-line description editing.
|
|
14
|
+
- Subtasks checklist.
|
|
15
|
+
- Support linking tasks to projects created in Paseo.
|
|
16
|
+
- **Workspace Tabs and Splits**: Open the board beside an agent or terminal, initially filtered to the workspace's project, or use the sidebar entry.
|
|
17
|
+
- **Smooth Drag-and-Drop**: Support intra-lane reordering and cross-lane movement.
|
|
20
18
|
|
|
21
19
|
---
|
|
22
20
|
|
|
@@ -57,12 +55,10 @@ Verify that the plugin is running:
|
|
|
57
55
|
paseo plugin ls
|
|
58
56
|
```
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
Open the board from either entry:
|
|
61
59
|
|
|
62
60
|
- **Sidebar → Kanban**: shows all tasks by default.
|
|
63
|
-
- **Workspace → `+` → Kanban**: initially filters to the workspace's project, with other filters available
|
|
64
|
-
|
|
65
|
-
Both entries share tasks and lanes with full editing support; their filters are independent. Closing and reopening the board or restarting restores the entry's default filter. If workspace project context is unavailable, the board silently shows all tasks and keeps that filter even if context arrives later. Narrow splits retain horizontal lane scrolling.
|
|
61
|
+
- **Workspace → `+` → Kanban**: initially filters to the workspace's project, with other filters available; use Paseo's native split controls to place it beside an agent or terminal.
|
|
66
62
|
|
|
67
63
|
### Local Development
|
|
68
64
|
|
|
@@ -95,13 +91,13 @@ Individual commands (`npm test`, `npm run typecheck`, `npm run lint`, `npm run f
|
|
|
95
91
|
|
|
96
92
|
## Releases
|
|
97
93
|
|
|
98
|
-
See the [0.1.
|
|
94
|
+
See the [0.1.4 release notes](./docs/releases/0.1.4.md). Each release's npm package and GitHub Release share the same version tag; GitHub includes the verified npm tarball. `main` may contain changes beyond the latest release.
|
|
99
95
|
|
|
100
96
|
---
|
|
101
97
|
|
|
102
98
|
## Architecture & Design
|
|
103
99
|
|
|
104
|
-
Core terminology and design decisions are documented in [`
|
|
100
|
+
Core terminology and design decisions are documented in [`GLOSSARY.md`](./GLOSSARY.md) and [`docs/adr/`](./docs/adr/).
|
|
105
101
|
|
|
106
102
|
---
|
|
107
103
|
|
package/README.zh-CN.md
CHANGED
|
@@ -2,21 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md)
|
|
4
4
|
|
|
5
|
-
专为 [Paseo](https://paseo.sh)
|
|
5
|
+
专为 [Paseo](https://paseo.sh) 工作区打造的轻量响应式看板插件。
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## 核心功能
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- **灵活的泳道管理**:支持泳道增删与改名。
|
|
12
12
|
- **丰富的任务卡片**:
|
|
13
13
|
- 任务标题与多行描述编辑。
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- **工作区标签页与分屏**:与 Agent、Terminal
|
|
17
|
-
-
|
|
18
|
-
- **流畅拖拽交互**:精准的拖拽命中测试(Hit-Testing),支持同泳道排序与跨泳道状态流转。
|
|
19
|
-
- **版本化数据持久化**:通过 Paseo settings API 存储看板,使用乐观并发版本控制(CAS)处理多会话写入冲突。
|
|
14
|
+
- 子步骤清单。
|
|
15
|
+
- 支持关联 Paseo 中创建的项目。
|
|
16
|
+
- **工作区标签页与分屏**:与 Agent、Terminal 并排打开看板,默认筛选工作区所属项目;或使用侧边栏入口。
|
|
17
|
+
- **流畅拖拽交互**:支持同泳道排序与跨泳道状态流转。
|
|
20
18
|
|
|
21
19
|
---
|
|
22
20
|
|
|
@@ -57,13 +55,11 @@ paseo plugin add breathi3552/paseo-kanban --ref main
|
|
|
57
55
|
paseo plugin ls
|
|
58
56
|
```
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
可从两个入口打开看板:
|
|
61
59
|
|
|
62
60
|
- **侧边栏 → 看板**:默认展示全部任务。
|
|
63
61
|
- **工作区 → `+` → 看板**:默认筛选工作区所属项目,可切换范围;使用 Paseo 原生分屏操作与 Agent、Terminal 等并排显示。
|
|
64
62
|
|
|
65
|
-
两个入口共享任务和泳道,保留完整编辑能力,筛选互不影响。关闭重开或重启后恢复入口默认筛选。无法取得工作区所属项目时静默展示全部任务,本次打开期间不因项目上下文恢复而自动切换。窄分屏保持横向滚动浏览泳道。
|
|
66
|
-
|
|
67
63
|
### 本地开发与调试
|
|
68
64
|
|
|
69
65
|
如需进行本地源码调试或二次开发:
|
|
@@ -95,13 +91,13 @@ npm run check
|
|
|
95
91
|
|
|
96
92
|
## 版本发布
|
|
97
93
|
|
|
98
|
-
参见 [0.1.
|
|
94
|
+
参见 [0.1.4 版本说明](./docs/releases/0.1.4.md)。每版 npm 包与 GitHub Release 对应同一个版本 tag,GitHub 附带经过验证的 npm tarball。`main` 可能包含最新发布版之后的改动。
|
|
99
95
|
|
|
100
96
|
---
|
|
101
97
|
|
|
102
98
|
## 架构设计
|
|
103
99
|
|
|
104
|
-
核心术语和架构决策见 [`
|
|
100
|
+
核心术语和架构决策见 [`GLOSSARY.md`](./GLOSSARY.md) 与 [`docs/adr/`](./docs/adr/)。
|
|
105
101
|
|
|
106
102
|
---
|
|
107
103
|
|
package/client/i18n.tsx
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo } from "react";
|
|
1
|
+
import { useState, useEffect, useMemo, createContext, useContext } from "react";
|
|
2
2
|
|
|
3
3
|
export type SupportedLanguage = "zh" | "en";
|
|
4
4
|
|
|
5
|
+
export const I18nLanguageContext = createContext<SupportedLanguage | null>(
|
|
6
|
+
null,
|
|
7
|
+
);
|
|
8
|
+
|
|
5
9
|
export type TranslationParams = Record<
|
|
6
10
|
string,
|
|
7
11
|
string | number | boolean | null | undefined
|
|
@@ -9,6 +13,19 @@ export type TranslationParams = Record<
|
|
|
9
13
|
|
|
10
14
|
export const translations = {
|
|
11
15
|
zh: {
|
|
16
|
+
"notification.taskCreated": "任务已创建",
|
|
17
|
+
"notification.taskUpdated": "任务已更新",
|
|
18
|
+
"notification.taskDeleted": "任务已删除",
|
|
19
|
+
"notification.laneCreated": "泳道已创建",
|
|
20
|
+
"notification.laneRenamed": "泳道已重命名",
|
|
21
|
+
"notification.laneDeleted": "泳道已删除",
|
|
22
|
+
"notification.taskSaveFailed": "任务保存失败,草稿已保留,请核实最新数据。",
|
|
23
|
+
"notification.taskDeleteFailed": "任务删除失败,请核实最新数据。",
|
|
24
|
+
"notification.laneSaveFailed": "泳道保存失败,草稿已保留,请核实最新数据。",
|
|
25
|
+
"notification.laneDeleteFailed": "泳道删除失败,请核实最新数据。",
|
|
26
|
+
"notification.taskReorderFailed": "任务移动失败,请刷新最新数据后重试。",
|
|
27
|
+
"notification.taskCompletionFailed":
|
|
28
|
+
"任务完成状态保存失败,请核实最新状态。",
|
|
12
29
|
"kanban.sidebarTitle": "看板",
|
|
13
30
|
"kanban.title": "任务看板",
|
|
14
31
|
"kanban.loading": "正在加载看板数据...",
|
|
@@ -31,13 +48,18 @@ export const translations = {
|
|
|
31
48
|
"lane.add": "+ 添加",
|
|
32
49
|
"lane.manage": "管理",
|
|
33
50
|
"lane.emptyHint": "暂无卡片,可点击右上角添加或拖动卡片至此",
|
|
51
|
+
"lane.emptyCompletionHint": "暂无已完成任务,可勾选任务或将卡片拖入此泳道",
|
|
52
|
+
"card.completeTask": "完成任务:{title}",
|
|
53
|
+
"card.reopenTask": "取消完成:{title}",
|
|
34
54
|
"card.dragHandle": "拖动手柄",
|
|
35
55
|
"card.subtasksCount": "子步骤 {completed}/{total}",
|
|
36
56
|
"laneModal.titleEdit": "管理泳道",
|
|
37
57
|
"laneModal.titleCreate": "新增泳道",
|
|
38
58
|
"laneModal.nameLabel": "泳道名称 *",
|
|
39
59
|
"laneModal.namePlaceholder": "例如:待测试、发布中...",
|
|
40
|
-
"laneModal.cannotDeleteLast":
|
|
60
|
+
"laneModal.cannotDeleteLast":
|
|
61
|
+
"看板必须至少保留一条普通泳道,无法删除此泳道。",
|
|
62
|
+
"laneModal.cannotDeleteCompletion": "完成泳道不可删除,可以修改名称。",
|
|
41
63
|
"laneModal.cannotDeleteHasTasks":
|
|
42
64
|
"此泳道当前包含 {count} 个任务,需清空或移走任务后方可删除。",
|
|
43
65
|
"laneModal.deleteLane": "删除此泳道",
|
|
@@ -69,6 +91,24 @@ export const translations = {
|
|
|
69
91
|
"taskModal.submitCreate": "创建",
|
|
70
92
|
},
|
|
71
93
|
en: {
|
|
94
|
+
"notification.taskCreated": "Task created",
|
|
95
|
+
"notification.taskUpdated": "Task updated",
|
|
96
|
+
"notification.taskDeleted": "Task deleted",
|
|
97
|
+
"notification.laneCreated": "Lane created",
|
|
98
|
+
"notification.laneRenamed": "Lane renamed",
|
|
99
|
+
"notification.laneDeleted": "Lane deleted",
|
|
100
|
+
"notification.taskSaveFailed":
|
|
101
|
+
"Task save failed. Draft preserved; check the latest data.",
|
|
102
|
+
"notification.taskDeleteFailed":
|
|
103
|
+
"Task deletion failed. Check the latest data.",
|
|
104
|
+
"notification.laneSaveFailed":
|
|
105
|
+
"Lane save failed. Draft preserved; check the latest data.",
|
|
106
|
+
"notification.laneDeleteFailed":
|
|
107
|
+
"Lane deletion failed. Check the latest data.",
|
|
108
|
+
"notification.taskReorderFailed":
|
|
109
|
+
"Task move failed. Refresh the latest data and try again.",
|
|
110
|
+
"notification.taskCompletionFailed":
|
|
111
|
+
"Task completion change failed. Check the latest state.",
|
|
72
112
|
"kanban.sidebarTitle": "Kanban",
|
|
73
113
|
"kanban.title": "Task Kanban",
|
|
74
114
|
"kanban.loading": "Loading kanban board data...",
|
|
@@ -92,6 +132,10 @@ export const translations = {
|
|
|
92
132
|
"lane.manage": "Manage",
|
|
93
133
|
"lane.emptyHint":
|
|
94
134
|
"No cards yet. Click '+' in the top right or drag cards here.",
|
|
135
|
+
"lane.emptyCompletionHint":
|
|
136
|
+
"No completed tasks. Check a task or drag it into this lane.",
|
|
137
|
+
"card.completeTask": "Mark {title} complete",
|
|
138
|
+
"card.reopenTask": "Mark {title} incomplete",
|
|
95
139
|
"card.dragHandle": "Drag handle",
|
|
96
140
|
"card.subtasksCount": "Subtasks {completed}/{total}",
|
|
97
141
|
"laneModal.titleEdit": "Manage Lane",
|
|
@@ -99,7 +143,9 @@ export const translations = {
|
|
|
99
143
|
"laneModal.nameLabel": "Lane Name *",
|
|
100
144
|
"laneModal.namePlaceholder": "e.g., Testing, In Review...",
|
|
101
145
|
"laneModal.cannotDeleteLast":
|
|
102
|
-
"A kanban board must retain at least one lane. Cannot delete this lane.",
|
|
146
|
+
"A kanban board must retain at least one ordinary lane. Cannot delete this lane.",
|
|
147
|
+
"laneModal.cannotDeleteCompletion":
|
|
148
|
+
"The completion lane cannot be deleted. You can rename it.",
|
|
103
149
|
"laneModal.cannotDeleteHasTasks":
|
|
104
150
|
"This lane currently contains {count} tasks. Clear or move tasks before deleting.",
|
|
105
151
|
"laneModal.deleteLane": "Delete Lane",
|
|
@@ -172,6 +218,7 @@ interface EnvironmentGlobals {
|
|
|
172
218
|
};
|
|
173
219
|
navigator?: {
|
|
174
220
|
language?: string;
|
|
221
|
+
languages?: readonly string[];
|
|
175
222
|
};
|
|
176
223
|
window?: {
|
|
177
224
|
addEventListener(
|
|
@@ -214,6 +261,12 @@ export function detectPaseoLanguage(
|
|
|
214
261
|
} catch {}
|
|
215
262
|
}
|
|
216
263
|
|
|
264
|
+
// Match Paseo's web system-language source before page/Intl defaults.
|
|
265
|
+
const navLocale = env.navigator?.languages?.[0] || env.navigator?.language;
|
|
266
|
+
if (navLocale) {
|
|
267
|
+
return normalizeLanguage(navLocale);
|
|
268
|
+
}
|
|
269
|
+
|
|
217
270
|
const docLang = env.document?.documentElement?.lang;
|
|
218
271
|
if (docLang) {
|
|
219
272
|
return normalizeLanguage(docLang);
|
|
@@ -232,11 +285,6 @@ export function detectPaseoLanguage(
|
|
|
232
285
|
return normalizeLanguage(intlLocale);
|
|
233
286
|
}
|
|
234
287
|
|
|
235
|
-
const navLocale = env.navigator?.language;
|
|
236
|
-
if (navLocale) {
|
|
237
|
-
return normalizeLanguage(navLocale);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
288
|
return "en";
|
|
241
289
|
}
|
|
242
290
|
|
|
@@ -323,6 +371,7 @@ export interface I18nContextValue {
|
|
|
323
371
|
}
|
|
324
372
|
|
|
325
373
|
export function useI18n(hostProps?: DetectLanguageOptions): I18nContextValue {
|
|
374
|
+
const inheritedLanguage = useContext(I18nLanguageContext);
|
|
326
375
|
const hostLocale =
|
|
327
376
|
hostProps?.locale ??
|
|
328
377
|
hostProps?.language ??
|
|
@@ -341,14 +390,15 @@ export function useI18n(hostProps?: DetectLanguageOptions): I18nContextValue {
|
|
|
341
390
|
});
|
|
342
391
|
}, [hostLocale, hostProps]);
|
|
343
392
|
|
|
393
|
+
const language = hostLocale ? lang : (inheritedLanguage ?? lang);
|
|
344
394
|
const tBound = useMemo(
|
|
345
395
|
() => (key: TranslationKey, params?: TranslationParams) =>
|
|
346
|
-
t(key,
|
|
347
|
-
[
|
|
396
|
+
t(key, language, params),
|
|
397
|
+
[language],
|
|
348
398
|
);
|
|
349
399
|
|
|
350
400
|
return {
|
|
351
|
-
language
|
|
401
|
+
language,
|
|
352
402
|
t: tBound,
|
|
353
403
|
};
|
|
354
404
|
}
|
package/client/kanban-board.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo } from "react";
|
|
1
|
+
import { useState, useMemo, useRef } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
type LayoutChangeEvent,
|
|
10
10
|
} from "react-native";
|
|
11
11
|
import { useSettings } from "@getpaseo/plugin/client";
|
|
12
|
+
import { useToast } from "@getpaseo/plugin/client/react-native";
|
|
12
13
|
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
13
|
-
import { kanbanSettings,
|
|
14
|
+
import { kanbanSettings, getLaneTasks } from "../shared/kanban";
|
|
14
15
|
import { useProjects } from "./use-projects";
|
|
15
16
|
import { TaskModal } from "./task-modal";
|
|
16
17
|
import { LaneModal } from "./lane-modal";
|
|
@@ -21,16 +22,26 @@ import {
|
|
|
21
22
|
openTaskSession,
|
|
22
23
|
openLaneSession,
|
|
23
24
|
executeTaskReorder,
|
|
25
|
+
executeTaskCompletion,
|
|
24
26
|
type TaskEditSession,
|
|
25
27
|
type LaneEditSession,
|
|
26
28
|
} from "./kanban-session";
|
|
27
|
-
import { useI18n } from "./i18n";
|
|
29
|
+
import { useI18n, I18nLanguageContext } from "./i18n";
|
|
28
30
|
|
|
29
31
|
interface KanbanBoardProps extends PluginSurfaceProps {
|
|
30
32
|
initialProjectId?: string | null;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export function KanbanBoardView(props: KanbanBoardProps) {
|
|
36
|
+
const { language } = useI18n(props);
|
|
37
|
+
return (
|
|
38
|
+
<I18nLanguageContext.Provider value={language}>
|
|
39
|
+
<KanbanBoardContent {...props} />
|
|
40
|
+
</I18nLanguageContext.Provider>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function KanbanBoardContent(props: KanbanBoardProps) {
|
|
34
45
|
const { theme } = props;
|
|
35
46
|
const [panelWidth, setPanelWidth] = useState<number | null>(null);
|
|
36
47
|
const compact =
|
|
@@ -43,8 +54,9 @@ export function KanbanBoardView(props: KanbanBoardProps) {
|
|
|
43
54
|
const { width } = event.nativeEvent.layout;
|
|
44
55
|
if (width > 0) setPanelWidth(width);
|
|
45
56
|
};
|
|
46
|
-
const { t, language } = useI18n(
|
|
57
|
+
const { t, language } = useI18n();
|
|
47
58
|
const settings = useSettings(kanbanSettings);
|
|
59
|
+
const toast = useToast();
|
|
48
60
|
const {
|
|
49
61
|
projects,
|
|
50
62
|
resolveProjectName,
|
|
@@ -58,7 +70,11 @@ export function KanbanBoardView(props: KanbanBoardProps) {
|
|
|
58
70
|
useState<TaskEditSession | null>(null);
|
|
59
71
|
const [activeLaneSession, setActiveLaneSession] =
|
|
60
72
|
useState<LaneEditSession | null>(null);
|
|
61
|
-
const [
|
|
73
|
+
const [taskOperationError, setTaskOperationError] = useState<string | null>(
|
|
74
|
+
null,
|
|
75
|
+
);
|
|
76
|
+
const [completionPending, setCompletionPending] = useState(false);
|
|
77
|
+
const completionPendingRef = useRef(false);
|
|
62
78
|
|
|
63
79
|
const selectableProjects = useMemo(() => {
|
|
64
80
|
const missingIds = new Set([props.initialProjectId, selectedProjectId]);
|
|
@@ -78,18 +94,12 @@ export function KanbanBoardView(props: KanbanBoardProps) {
|
|
|
78
94
|
const board = isReady ? settings.values : null;
|
|
79
95
|
const revision = isReady ? settings.revision : "";
|
|
80
96
|
|
|
81
|
-
// Filter tasks based on selected project
|
|
82
|
-
const filteredTasks = useMemo(() => {
|
|
83
|
-
if (!board) return [];
|
|
84
|
-
return filterTasksByProject(board.tasks, selectedProjectId);
|
|
85
|
-
}, [board, selectedProjectId]);
|
|
86
|
-
|
|
87
97
|
const handleReorderTask = async (
|
|
88
98
|
taskId: string,
|
|
89
99
|
targetLaneId: string,
|
|
90
100
|
targetIndex: number,
|
|
91
101
|
) => {
|
|
92
|
-
if (!board) return;
|
|
102
|
+
if (!board || completionPendingRef.current) return;
|
|
93
103
|
const result = await executeTaskReorder({
|
|
94
104
|
baseBoard: board,
|
|
95
105
|
baseRevision: revision,
|
|
@@ -101,9 +111,10 @@ export function KanbanBoardView(props: KanbanBoardProps) {
|
|
|
101
111
|
language,
|
|
102
112
|
});
|
|
103
113
|
if (!result.success) {
|
|
104
|
-
|
|
114
|
+
setTaskOperationError(result.error);
|
|
115
|
+
toast.error(t("notification.taskReorderFailed"));
|
|
105
116
|
} else {
|
|
106
|
-
|
|
117
|
+
setTaskOperationError(null);
|
|
107
118
|
}
|
|
108
119
|
};
|
|
109
120
|
|
|
@@ -122,8 +133,29 @@ export function KanbanBoardView(props: KanbanBoardProps) {
|
|
|
122
133
|
getTaskPreview,
|
|
123
134
|
});
|
|
124
135
|
|
|
136
|
+
const handleSetCompleted = async (taskId: string, completed: boolean) => {
|
|
137
|
+
if (!board || completionPendingRef.current || drag.isDragLocked()) return;
|
|
138
|
+
completionPendingRef.current = true;
|
|
139
|
+
setCompletionPending(true);
|
|
140
|
+
try {
|
|
141
|
+
const result = await executeTaskCompletion({
|
|
142
|
+
baseBoard: board,
|
|
143
|
+
baseRevision: revision,
|
|
144
|
+
taskId,
|
|
145
|
+
completed,
|
|
146
|
+
save: (next, expectedRevision) => settings.save(next, expectedRevision),
|
|
147
|
+
language,
|
|
148
|
+
});
|
|
149
|
+
setTaskOperationError(result.success ? null : result.error);
|
|
150
|
+
if (!result.success) toast.error(t("notification.taskCompletionFailed"));
|
|
151
|
+
} finally {
|
|
152
|
+
completionPendingRef.current = false;
|
|
153
|
+
setCompletionPending(false);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
125
157
|
const openNewTask = (defaultLaneId?: string) => {
|
|
126
|
-
if (drag.isDragLocked() || !board) return;
|
|
158
|
+
if (completionPendingRef.current || drag.isDragLocked() || !board) return;
|
|
127
159
|
setActiveTaskSession(
|
|
128
160
|
openTaskSession({
|
|
129
161
|
mode: "create",
|
|
@@ -141,7 +173,7 @@ export function KanbanBoardView(props: KanbanBoardProps) {
|
|
|
141
173
|
};
|
|
142
174
|
|
|
143
175
|
const openEditTask = (taskId: string) => {
|
|
144
|
-
if (drag.isDragLocked() || !board) return;
|
|
176
|
+
if (completionPendingRef.current || drag.isDragLocked() || !board) return;
|
|
145
177
|
setActiveTaskSession(
|
|
146
178
|
openTaskSession({
|
|
147
179
|
mode: "edit",
|
|
@@ -155,7 +187,7 @@ export function KanbanBoardView(props: KanbanBoardProps) {
|
|
|
155
187
|
};
|
|
156
188
|
|
|
157
189
|
const openNewLane = () => {
|
|
158
|
-
if (drag.isDragLocked() || !board) return;
|
|
190
|
+
if (completionPendingRef.current || drag.isDragLocked() || !board) return;
|
|
159
191
|
setActiveLaneSession(
|
|
160
192
|
openLaneSession({
|
|
161
193
|
mode: "create",
|
|
@@ -168,7 +200,7 @@ export function KanbanBoardView(props: KanbanBoardProps) {
|
|
|
168
200
|
};
|
|
169
201
|
|
|
170
202
|
const openEditLane = (laneId: string) => {
|
|
171
|
-
if (drag.isDragLocked() || !board) return;
|
|
203
|
+
if (completionPendingRef.current || drag.isDragLocked() || !board) return;
|
|
172
204
|
setActiveLaneSession(
|
|
173
205
|
openLaneSession({
|
|
174
206
|
mode: "edit",
|
|
@@ -544,16 +576,16 @@ export function KanbanBoardView(props: KanbanBoardProps) {
|
|
|
544
576
|
</View>
|
|
545
577
|
|
|
546
578
|
{/* Conflict / Save Error Notification */}
|
|
547
|
-
{(settings.saveError ||
|
|
579
|
+
{(settings.saveError || taskOperationError) && (
|
|
548
580
|
<View style={styles.conflictBanner}>
|
|
549
581
|
<Text style={styles.conflictText}>
|
|
550
|
-
{
|
|
551
|
-
? t("kanban.saveFailed", { error:
|
|
582
|
+
{taskOperationError
|
|
583
|
+
? t("kanban.saveFailed", { error: taskOperationError })
|
|
552
584
|
: t("kanban.conflictError", { error: settings.saveError })}
|
|
553
585
|
</Text>
|
|
554
586
|
<Pressable
|
|
555
587
|
onPress={() => {
|
|
556
|
-
|
|
588
|
+
setTaskOperationError(null);
|
|
557
589
|
settings.reload();
|
|
558
590
|
}}
|
|
559
591
|
style={styles.conflictAction}
|
|
@@ -579,7 +611,7 @@ export function KanbanBoardView(props: KanbanBoardProps) {
|
|
|
579
611
|
scrollEventThrottle={16}
|
|
580
612
|
>
|
|
581
613
|
{board.lanes.map((lane) => {
|
|
582
|
-
const laneTasks =
|
|
614
|
+
const laneTasks = getLaneTasks(board, lane.id, selectedProjectId);
|
|
583
615
|
return (
|
|
584
616
|
<KanbanLaneView
|
|
585
617
|
key={lane.id}
|
|
@@ -593,6 +625,8 @@ export function KanbanBoardView(props: KanbanBoardProps) {
|
|
|
593
625
|
onAddTask={(laneId) => openNewTask(laneId)}
|
|
594
626
|
onManageLane={(laneId) => openEditLane(laneId)}
|
|
595
627
|
onSelectTask={(task) => openEditTask(task.id)}
|
|
628
|
+
onSetCompleted={handleSetCompleted}
|
|
629
|
+
completionPending={completionPending}
|
|
596
630
|
/>
|
|
597
631
|
);
|
|
598
632
|
})}
|