paseo-kanban 0.1.1 → 0.1.3
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 +9 -0
- package/README.md +21 -45
- package/README.zh-CN.md +20 -44
- package/client/kanban-board.tsx +46 -6
- package/client/kanban-drag.ts +25 -33
- package/client/kanban-overlay.tsx +2 -1
- package/client/kanban-workspace-panel.tsx +13 -0
- package/client/use-projects.ts +4 -2
- package/index.client.tsx +28 -16
- package/package.json +3 -1
package/OVERVIEW.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Kanban
|
|
2
|
+
|
|
3
|
+
Organize tasks in customizable swimlanes, add descriptions and subtasks, and link tasks to Paseo projects. Drag cards to reorder them within a lane or move them between lanes.
|
|
4
|
+
|
|
5
|
+
Open the board from the sidebar or from a workspace's `+` menu. Workspace tabs initially filter to their project's tasks and can sit beside an agent or terminal using Paseo's native split controls. Each open board has its own project filter; all boards on the same host share tasks and lanes.
|
|
6
|
+
|
|
7
|
+
Board data is stored in Paseo's host-scoped plugin settings. Concurrent edits use revision checks so a stale edit cannot overwrite a newer board. If a project's name cannot be read, its stored ID remains available. If workspace project context is unavailable when the board opens, it initially shows all tasks.
|
|
8
|
+
|
|
9
|
+
Requires Paseo 0.8.0 or later. No accounts, tokens, or additional setup are required.
|
package/README.md
CHANGED
|
@@ -8,11 +8,12 @@ A lightweight, responsive Kanban board plugin for [Paseo](https://paseo.sh) work
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **Flexible Swimlanes**:
|
|
11
|
+
- **Flexible Swimlanes**: Three initial lanes that you can rename, with support for adding and deleting custom lanes.
|
|
12
12
|
- **Rich Task Cards**:
|
|
13
|
-
- Task title and multi-line
|
|
13
|
+
- Task title and multi-line description.
|
|
14
14
|
- Subtasks checklist with completion toggles and live progress tracking.
|
|
15
15
|
- Native Paseo project association.
|
|
16
|
+
- **Workspace Tabs and Splits**: Open the board beside an agent or terminal, initially filtered to the workspace's project. The sidebar remains available.
|
|
16
17
|
- **Project Filtering**: Instantly filter board cards by linked Paseo project to keep context focused.
|
|
17
18
|
- **Smooth Drag-and-Drop**: Real-time hit-testing for reordering cards within lanes and moving cards across lanes.
|
|
18
19
|
- **Versioned State Persistence**: Persists board data through Paseo's settings API with optimistic concurrency control (CAS) to prevent race conditions and accidental overwrites.
|
|
@@ -21,16 +22,16 @@ A lightweight, responsive Kanban board plugin for [Paseo](https://paseo.sh) work
|
|
|
21
22
|
|
|
22
23
|
## Requirements
|
|
23
24
|
|
|
24
|
-
- **Paseo
|
|
25
|
-
- **Development
|
|
25
|
+
- **Paseo host**: `>= 0.8.0` (declared in `paseo-plugin.json`). The host runs the plugin.
|
|
26
|
+
- **Development and CI**: Node.js 24 (pinned in `.nvmrc` and `.node-version`) for checks, TypeScript stripping, and native tests.
|
|
26
27
|
|
|
27
28
|
---
|
|
28
29
|
|
|
29
|
-
## Installation
|
|
30
|
+
## Installation
|
|
30
31
|
|
|
31
32
|
### Quick Install (Recommended)
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
Install directly from npm or GitHub:
|
|
34
35
|
|
|
35
36
|
**Via NPM (Release package):**
|
|
36
37
|
|
|
@@ -56,7 +57,12 @@ Verify that the plugin is running:
|
|
|
56
57
|
paseo plugin ls
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
Once active, open
|
|
60
|
+
Once active, open the board from either entry:
|
|
61
|
+
|
|
62
|
+
- **Sidebar → Kanban**: shows all tasks by default.
|
|
63
|
+
- **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.
|
|
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.
|
|
60
66
|
|
|
61
67
|
### Local Development
|
|
62
68
|
|
|
@@ -66,7 +72,7 @@ To contribute or debug locally:
|
|
|
66
72
|
# 1. Clone repository and install dependencies
|
|
67
73
|
git clone https://github.com/breathi3552/paseo-kanban.git
|
|
68
74
|
cd paseo-kanban
|
|
69
|
-
npm
|
|
75
|
+
npm ci
|
|
70
76
|
|
|
71
77
|
# 2. Link this directory into your local Paseo daemon
|
|
72
78
|
paseo plugin link .
|
|
@@ -76,56 +82,26 @@ paseo plugin link .
|
|
|
76
82
|
|
|
77
83
|
## Development & Testing
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
Install dependencies and run the full quality check:
|
|
80
86
|
|
|
81
87
|
```bash
|
|
82
|
-
npm install
|
|
83
|
-
# or clean install:
|
|
84
88
|
npm ci
|
|
89
|
+
npm run check
|
|
85
90
|
```
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
Individual commands (`npm test`, `npm run typecheck`, `npm run lint`, `npm run format:check`, `npm run check:package`) are defined in [`package.json`](./package.json). For tag-based publishing, release sources of truth, recovery, and host acceptance, see the [development guide](./docs/development.md#21-标签发布github-actions).
|
|
88
93
|
|
|
89
|
-
|
|
94
|
+
---
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
npm run check
|
|
93
|
-
```
|
|
96
|
+
## Releases
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
- **Package Verification**: Validates release package contents, entrypoints, and TypeScript AST relative imports:
|
|
98
|
-
```bash
|
|
99
|
-
npm run check:package
|
|
100
|
-
```
|
|
101
|
-
- **Run Tests**: Cross-platform auto-discovery of all `test/*.test.mjs` suites:
|
|
102
|
-
```bash
|
|
103
|
-
npm test
|
|
104
|
-
```
|
|
105
|
-
- **Type Checking**: Validates TypeScript types across client and server entry points:
|
|
106
|
-
```bash
|
|
107
|
-
npm run typecheck
|
|
108
|
-
```
|
|
109
|
-
- **Linting**: Static code analysis with ESLint:
|
|
110
|
-
```bash
|
|
111
|
-
npm run lint
|
|
112
|
-
```
|
|
113
|
-
- **Formatting**: Check or format code style with Prettier:
|
|
114
|
-
```bash
|
|
115
|
-
npm run format:check
|
|
116
|
-
npm run format
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
For comprehensive contribution guidelines, pre-publish package checks, and manual host acceptance steps, see the [Development & Acceptance Guide](./docs/development.md).
|
|
98
|
+
See the [0.1.3 release notes](./docs/releases/0.1.3.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.
|
|
120
99
|
|
|
121
100
|
---
|
|
122
101
|
|
|
123
102
|
## Architecture & Design
|
|
124
103
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
- **Interactive Architecture Viewer**: Open [`docs/architecture/paseo-kanban.architecture.html`](./docs/architecture/paseo-kanban.architecture.html) in your browser.
|
|
128
|
-
- **Architecture Schema & Model**: [`docs/architecture/paseo-kanban.architecture.json`](./docs/architecture/paseo-kanban.architecture.json).
|
|
104
|
+
Core terminology and design decisions are documented in [`CONTEXT.md`](./CONTEXT.md) and [`docs/adr/`](./docs/adr/).
|
|
129
105
|
|
|
130
106
|
---
|
|
131
107
|
|
package/README.zh-CN.md
CHANGED
|
@@ -13,24 +13,25 @@
|
|
|
13
13
|
- 任务标题与多行描述编辑。
|
|
14
14
|
- 子步骤清单支持独立增删改、完成状态勾选及实时进度展示。
|
|
15
15
|
- 支持关联本地已有的 Paseo 项目。
|
|
16
|
+
- **工作区标签页与分屏**:与 Agent、Terminal 并排打开看板,默认筛选工作区所属项目;保留侧边栏入口。
|
|
16
17
|
- **项目维度筛选**:一键按关联的 Paseo 项目过滤看板卡片,聚焦当前目标。
|
|
17
18
|
- **流畅拖拽交互**:精准的拖拽命中测试(Hit-Testing),支持同泳道排序与跨泳道状态流转。
|
|
18
|
-
-
|
|
19
|
+
- **版本化数据持久化**:通过 Paseo settings API 存储看板,使用乐观并发版本控制(CAS)处理多会话写入冲突。
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
## 环境要求
|
|
23
24
|
|
|
24
|
-
- **Paseo
|
|
25
|
-
- **开发与 CI
|
|
25
|
+
- **Paseo 宿主**:`>= 0.8.0`(声明于 `paseo-plugin.json`),由 Paseo 运行插件。
|
|
26
|
+
- **开发与 CI**:使用 Node.js 24(声明于 `.nvmrc` 与 `.node-version`)执行质量检查、TypeScript 类型擦除和原生测试。
|
|
26
27
|
|
|
27
28
|
---
|
|
28
29
|
|
|
29
|
-
##
|
|
30
|
+
## 安装
|
|
30
31
|
|
|
31
32
|
### 快速安装(推荐)
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
通过 Paseo CLI 从 npm 或 GitHub 安装:
|
|
34
35
|
|
|
35
36
|
**方式一:通过 NPM 官方包安装(推荐)**
|
|
36
37
|
|
|
@@ -56,7 +57,12 @@ paseo plugin add breathi3552/paseo-kanban --ref main
|
|
|
56
57
|
paseo plugin ls
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
状态显示为 `running`
|
|
60
|
+
状态显示为 `running` 后,可从两个入口打开看板:
|
|
61
|
+
|
|
62
|
+
- **侧边栏 → 看板**:默认展示全部任务。
|
|
63
|
+
- **工作区 → `+` → 看板**:默认筛选工作区所属项目,可切换范围;使用 Paseo 原生分屏操作与 Agent、Terminal 等并排显示。
|
|
64
|
+
|
|
65
|
+
两个入口共享任务和泳道,保留完整编辑能力,筛选互不影响。关闭重开或重启后恢复入口默认筛选。无法取得工作区所属项目时静默展示全部任务,本次打开期间不因项目上下文恢复而自动切换。窄分屏保持横向滚动浏览泳道。
|
|
60
66
|
|
|
61
67
|
### 本地开发与调试
|
|
62
68
|
|
|
@@ -66,7 +72,7 @@ paseo plugin ls
|
|
|
66
72
|
# 1. 克隆代码仓库并安装依赖
|
|
67
73
|
git clone https://github.com/breathi3552/paseo-kanban.git
|
|
68
74
|
cd paseo-kanban
|
|
69
|
-
npm
|
|
75
|
+
npm ci
|
|
70
76
|
|
|
71
77
|
# 2. 将当前开发目录软链接至本地 Paseo 守护进程
|
|
72
78
|
paseo plugin link .
|
|
@@ -76,56 +82,26 @@ paseo plugin link .
|
|
|
76
82
|
|
|
77
83
|
## 开发与测试
|
|
78
84
|
|
|
79
|
-
|
|
85
|
+
安装依赖并运行完整检查:
|
|
80
86
|
|
|
81
87
|
```bash
|
|
82
|
-
npm install
|
|
83
|
-
# 或干净安装验证:
|
|
84
88
|
npm ci
|
|
89
|
+
npm run check
|
|
85
90
|
```
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
单项命令(`npm test`、`npm run typecheck`、`npm run lint`、`npm run format:check`、`npm run check:package`)见 [`package.json`](./package.json)。标签发布、事实来源、失败重试与宿主验收见[开发指南](./docs/development.md#21-标签发布github-actions)。
|
|
88
93
|
|
|
89
|
-
|
|
94
|
+
---
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
npm run check
|
|
93
|
-
```
|
|
96
|
+
## 版本发布
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
- **发布包完整性检查**:静态校验发布包产物清单、入口完整性与 TypeScript AST 相对导入闭包:
|
|
98
|
-
```bash
|
|
99
|
-
npm run check:package
|
|
100
|
-
```
|
|
101
|
-
- **运行单元测试**:跨平台自动发现并执行所有 `test/*.test.mjs` 测试套件:
|
|
102
|
-
```bash
|
|
103
|
-
npm test
|
|
104
|
-
```
|
|
105
|
-
- **类型检查**:校验前端组件与服务端扩展的 TypeScript 类型完整性:
|
|
106
|
-
```bash
|
|
107
|
-
npm run typecheck
|
|
108
|
-
```
|
|
109
|
-
- **代码规范检查 (Lint)**:基于 ESLint 开展轻量静态检查:
|
|
110
|
-
```bash
|
|
111
|
-
npm run lint
|
|
112
|
-
```
|
|
113
|
-
- **代码格式检查与格式化 (Format)**:基于 Prettier 校验或自动格式化:
|
|
114
|
-
```bash
|
|
115
|
-
npm run format:check
|
|
116
|
-
npm run format
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
详细开发贡献流程、发布前检查机制与真实宿主环境手工验收指南,请参阅 [开发与宿主验收指南](./docs/development.md)。
|
|
98
|
+
参见 [0.1.3 版本说明](./docs/releases/0.1.3.md)。每版 npm 包与 GitHub Release 对应同一个版本 tag,GitHub 附带经过验证的 npm tarball。`main` 可能包含最新发布版之后的改动。
|
|
120
99
|
|
|
121
100
|
---
|
|
122
101
|
|
|
123
102
|
## 架构设计
|
|
124
103
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
- **交互式架构全景图**:直接使用浏览器打开 [`docs/architecture/paseo-kanban.architecture.html`](./docs/architecture/paseo-kanban.architecture.html) 查看。
|
|
128
|
-
- **架构定义元数据**:[`docs/architecture/paseo-kanban.architecture.json`](./docs/architecture/paseo-kanban.architecture.json)。
|
|
104
|
+
核心术语和架构决策见 [`CONTEXT.md`](./CONTEXT.md) 与 [`docs/adr/`](./docs/adr/)。
|
|
129
105
|
|
|
130
106
|
---
|
|
131
107
|
|
package/client/kanban-board.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Pressable,
|
|
7
7
|
StyleSheet,
|
|
8
8
|
ActivityIndicator,
|
|
9
|
+
type LayoutChangeEvent,
|
|
9
10
|
} from "react-native";
|
|
10
11
|
import { useSettings } from "@getpaseo/plugin/client";
|
|
11
12
|
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
@@ -25,8 +26,23 @@ import {
|
|
|
25
26
|
} from "./kanban-session";
|
|
26
27
|
import { useI18n } from "./i18n";
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
interface KanbanBoardProps extends PluginSurfaceProps {
|
|
30
|
+
initialProjectId?: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function KanbanBoardView(props: KanbanBoardProps) {
|
|
34
|
+
const { theme } = props;
|
|
35
|
+
const [panelWidth, setPanelWidth] = useState<number | null>(null);
|
|
36
|
+
const compact =
|
|
37
|
+
props.layout.compact || (panelWidth !== null && panelWidth < 600);
|
|
38
|
+
const layout = useMemo(
|
|
39
|
+
() => ({ ...props.layout, compact }),
|
|
40
|
+
[props.layout, compact],
|
|
41
|
+
);
|
|
42
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
43
|
+
const { width } = event.nativeEvent.layout;
|
|
44
|
+
if (width > 0) setPanelWidth(width);
|
|
45
|
+
};
|
|
30
46
|
const { t, language } = useI18n(props);
|
|
31
47
|
const settings = useSettings(kanbanSettings);
|
|
32
48
|
const {
|
|
@@ -35,13 +51,29 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
35
51
|
isError: projectsError,
|
|
36
52
|
} = useProjects();
|
|
37
53
|
|
|
38
|
-
const [selectedProjectId, setSelectedProjectId] = useState
|
|
54
|
+
const [selectedProjectId, setSelectedProjectId] = useState(
|
|
55
|
+
props.initialProjectId || "all",
|
|
56
|
+
);
|
|
39
57
|
const [activeTaskSession, setActiveTaskSession] =
|
|
40
58
|
useState<TaskEditSession | null>(null);
|
|
41
59
|
const [activeLaneSession, setActiveLaneSession] =
|
|
42
60
|
useState<LaneEditSession | null>(null);
|
|
43
61
|
const [reorderError, setReorderError] = useState<string | null>(null);
|
|
44
62
|
|
|
63
|
+
const selectableProjects = useMemo(() => {
|
|
64
|
+
const missingIds = new Set([props.initialProjectId, selectedProjectId]);
|
|
65
|
+
const missingProjects = [...missingIds]
|
|
66
|
+
.filter(
|
|
67
|
+
(id): id is string =>
|
|
68
|
+
!!id &&
|
|
69
|
+
id !== "all" &&
|
|
70
|
+
id !== "unassigned" &&
|
|
71
|
+
!projects.some((project) => project.projectId === id),
|
|
72
|
+
)
|
|
73
|
+
.map((id) => ({ projectId: id, projectDisplayName: id }));
|
|
74
|
+
return [...projects, ...missingProjects];
|
|
75
|
+
}, [projects, props.initialProjectId, selectedProjectId]);
|
|
76
|
+
|
|
45
77
|
const isReady = settings.status === "ready";
|
|
46
78
|
const board = isReady ? settings.values : null;
|
|
47
79
|
const revision = isReady ? settings.revision : "";
|
|
@@ -154,6 +186,8 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
154
186
|
StyleSheet.create({
|
|
155
187
|
container: {
|
|
156
188
|
flex: 1,
|
|
189
|
+
minWidth: 0,
|
|
190
|
+
minHeight: 0,
|
|
157
191
|
backgroundColor: theme.colors.surface0,
|
|
158
192
|
userSelect: "none",
|
|
159
193
|
},
|
|
@@ -174,6 +208,9 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
174
208
|
},
|
|
175
209
|
titleRow: {
|
|
176
210
|
flexDirection: "row",
|
|
211
|
+
flexWrap: "wrap",
|
|
212
|
+
flexShrink: 1,
|
|
213
|
+
maxWidth: "100%",
|
|
177
214
|
alignItems: "center",
|
|
178
215
|
gap: 8,
|
|
179
216
|
},
|
|
@@ -184,6 +221,8 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
184
221
|
},
|
|
185
222
|
headerActions: {
|
|
186
223
|
flexDirection: "row",
|
|
224
|
+
flexWrap: "wrap",
|
|
225
|
+
maxWidth: "100%",
|
|
187
226
|
alignItems: "center",
|
|
188
227
|
gap: 8,
|
|
189
228
|
},
|
|
@@ -270,6 +309,7 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
270
309
|
},
|
|
271
310
|
lanesWrapper: {
|
|
272
311
|
flex: 1,
|
|
312
|
+
minHeight: 0,
|
|
273
313
|
position: "relative",
|
|
274
314
|
userSelect: "none",
|
|
275
315
|
},
|
|
@@ -370,7 +410,7 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
370
410
|
if (!board) return null;
|
|
371
411
|
|
|
372
412
|
return (
|
|
373
|
-
<View style={styles.container}>
|
|
413
|
+
<View style={styles.container} onLayout={handleLayout}>
|
|
374
414
|
{/* Top Header */}
|
|
375
415
|
<View style={styles.header}>
|
|
376
416
|
<View style={styles.headerTopRow}>
|
|
@@ -462,7 +502,7 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
462
502
|
</Text>
|
|
463
503
|
</Pressable>
|
|
464
504
|
|
|
465
|
-
{
|
|
505
|
+
{selectableProjects.map((proj) => {
|
|
466
506
|
const isSelected = selectedProjectId === proj.projectId;
|
|
467
507
|
return (
|
|
468
508
|
<Pressable
|
|
@@ -567,7 +607,7 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
567
607
|
open={activeTaskSession !== null}
|
|
568
608
|
onClose={() => setActiveTaskSession(null)}
|
|
569
609
|
session={activeTaskSession}
|
|
570
|
-
projects={
|
|
610
|
+
projects={selectableProjects}
|
|
571
611
|
theme={theme}
|
|
572
612
|
layout={layout}
|
|
573
613
|
/>
|
package/client/kanban-drag.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { useRef, useSyncExternalStore, useEffect } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
GestureResponderHandlers,
|
|
4
|
+
PanResponderCallbacks,
|
|
5
|
+
PanResponderInstance,
|
|
6
|
+
} from "react-native";
|
|
2
7
|
import type { KanbanTask } from "../shared/kanban";
|
|
3
8
|
|
|
4
9
|
export interface Rect {
|
|
@@ -85,7 +90,6 @@ export interface TaskPreviewItem {
|
|
|
85
90
|
|
|
86
91
|
export interface KanbanDragOptions {
|
|
87
92
|
lanes?: ({ id: string } | string)[];
|
|
88
|
-
onMoveTask?: (taskId: string, targetLaneId: string) => void | Promise<void>;
|
|
89
93
|
onReorderTask?: (
|
|
90
94
|
taskId: string,
|
|
91
95
|
targetLaneId: string,
|
|
@@ -98,16 +102,17 @@ export interface ContainerRefTarget {
|
|
|
98
102
|
measureInWindow?: (
|
|
99
103
|
callback: (x: number, y: number, width: number, height: number) => void,
|
|
100
104
|
) => void;
|
|
105
|
+
scrollTo?: (options: { x: number; animated: boolean }) => void;
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
export interface PanResponderFactory {
|
|
104
|
-
create: (config:
|
|
109
|
+
create: (config: PanResponderCallbacks) => PanResponderInstance;
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
export interface CardDragBinding {
|
|
108
113
|
readonly isDragging: boolean;
|
|
109
|
-
readonly cardPanHandlers
|
|
110
|
-
readonly handlePanHandlers
|
|
114
|
+
readonly cardPanHandlers?: GestureResponderHandlers;
|
|
115
|
+
readonly handlePanHandlers?: GestureResponderHandlers;
|
|
111
116
|
readonly onLayout: (rect: Rect) => void;
|
|
112
117
|
readonly onUnmount: () => void;
|
|
113
118
|
}
|
|
@@ -169,10 +174,6 @@ export class KanbanDragController {
|
|
|
169
174
|
private containerRef: ContainerRefTarget | null = null;
|
|
170
175
|
private scrollX: number = 0;
|
|
171
176
|
private activeSession: ActiveDragSession | null = null;
|
|
172
|
-
private onMoveTask?: (
|
|
173
|
-
taskId: string,
|
|
174
|
-
targetLaneId: string,
|
|
175
|
-
) => void | Promise<void>;
|
|
176
177
|
private onReorderTask?: (
|
|
177
178
|
taskId: string,
|
|
178
179
|
targetLaneId: string,
|
|
@@ -193,7 +194,10 @@ export class KanbanDragController {
|
|
|
193
194
|
private cancelHandler?: () => void;
|
|
194
195
|
private panResponderFactory: PanResponderFactory | null = null;
|
|
195
196
|
private cardCallbacks: Map<string, () => void> = new Map();
|
|
196
|
-
private cardResponders: Map<
|
|
197
|
+
private cardResponders: Map<
|
|
198
|
+
string,
|
|
199
|
+
{ body: PanResponderInstance; handle: PanResponderInstance }
|
|
200
|
+
> = new Map();
|
|
197
201
|
private pendingInputKind: "mouse" | "touch" = "mouse";
|
|
198
202
|
private operationSeq: number = 0;
|
|
199
203
|
private currentOperationId: string | null = null;
|
|
@@ -222,9 +226,6 @@ export class KanbanDragController {
|
|
|
222
226
|
if (options?.lanes) {
|
|
223
227
|
this.setLanes(options.lanes);
|
|
224
228
|
}
|
|
225
|
-
if (options?.onMoveTask) {
|
|
226
|
-
this.setOnMoveTask(options.onMoveTask);
|
|
227
|
-
}
|
|
228
229
|
if (options?.onReorderTask) {
|
|
229
230
|
this.setOnReorderTask(options.onReorderTask);
|
|
230
231
|
}
|
|
@@ -244,12 +245,6 @@ export class KanbanDragController {
|
|
|
244
245
|
}
|
|
245
246
|
};
|
|
246
247
|
|
|
247
|
-
setOnMoveTask = (
|
|
248
|
-
fn: (taskId: string, targetLaneId: string) => void | Promise<void>,
|
|
249
|
-
) => {
|
|
250
|
-
this.onMoveTask = fn;
|
|
251
|
-
};
|
|
252
|
-
|
|
253
248
|
setOnReorderTask = (
|
|
254
249
|
fn: (
|
|
255
250
|
taskId: string,
|
|
@@ -428,7 +423,7 @@ export class KanbanDragController {
|
|
|
428
423
|
const body = factory.create({
|
|
429
424
|
onStartShouldSetPanResponder: () => true,
|
|
430
425
|
onMoveShouldSetPanResponder: () => true,
|
|
431
|
-
onPanResponderGrant: (_e
|
|
426
|
+
onPanResponderGrant: (_e, gs) => {
|
|
432
427
|
const cb = this.cardCallbacks.get(key);
|
|
433
428
|
this.handlePointerDown(
|
|
434
429
|
{ taskId, laneId, onPress: cb },
|
|
@@ -436,7 +431,7 @@ export class KanbanDragController {
|
|
|
436
431
|
"body",
|
|
437
432
|
);
|
|
438
433
|
},
|
|
439
|
-
onPanResponderMove: (_e
|
|
434
|
+
onPanResponderMove: (_e, gs) => {
|
|
440
435
|
this.handlePointerMove({ x: gs.moveX, y: gs.moveY });
|
|
441
436
|
},
|
|
442
437
|
onPanResponderRelease: () => this.handlePointerUp(),
|
|
@@ -449,7 +444,7 @@ export class KanbanDragController {
|
|
|
449
444
|
const handle = factory.create({
|
|
450
445
|
onStartShouldSetPanResponder: () => true,
|
|
451
446
|
onMoveShouldSetPanResponder: () => true,
|
|
452
|
-
onPanResponderGrant: (_e
|
|
447
|
+
onPanResponderGrant: (_e, gs) => {
|
|
453
448
|
const cb = this.cardCallbacks.get(key);
|
|
454
449
|
this.handlePointerDown(
|
|
455
450
|
{ taskId, laneId, onPress: cb },
|
|
@@ -457,7 +452,7 @@ export class KanbanDragController {
|
|
|
457
452
|
"handle",
|
|
458
453
|
);
|
|
459
454
|
},
|
|
460
|
-
onPanResponderMove: (_e
|
|
455
|
+
onPanResponderMove: (_e, gs) => {
|
|
461
456
|
this.handlePointerMove({ x: gs.moveX, y: gs.moveY });
|
|
462
457
|
},
|
|
463
458
|
onPanResponderRelease: () => this.handlePointerUp(),
|
|
@@ -899,8 +894,6 @@ export class KanbanDragController {
|
|
|
899
894
|
drop.targetLaneId,
|
|
900
895
|
drop.targetIndex,
|
|
901
896
|
);
|
|
902
|
-
} else if (this.onMoveTask && drop.targetLaneId !== drop.sourceLaneId) {
|
|
903
|
-
await this.onMoveTask(drop.taskId, drop.targetLaneId);
|
|
904
897
|
}
|
|
905
898
|
}
|
|
906
899
|
} finally {
|
|
@@ -1358,7 +1351,7 @@ export interface UseKanbanDragReturn {
|
|
|
1358
1351
|
getTaskPreview?: (taskId: string) => TaskPreviewItem | null;
|
|
1359
1352
|
setGetTaskPreview?: (fn?: (taskId: string) => TaskPreviewItem | null) => void;
|
|
1360
1353
|
bindLane: (laneId: string) => LaneDragBinding;
|
|
1361
|
-
bindContainerRef: (instance:
|
|
1354
|
+
bindContainerRef: (instance: ContainerRefTarget | null) => void;
|
|
1362
1355
|
handleContainerLayout: (layout: Rect) => void;
|
|
1363
1356
|
handleContainerScroll: (scrollX: number) => void;
|
|
1364
1357
|
getContainerBounds: () => Rect | null;
|
|
@@ -1387,21 +1380,17 @@ export interface UseKanbanDragReturn {
|
|
|
1387
1380
|
export function useKanbanDrag(
|
|
1388
1381
|
options: KanbanDragOptions = {},
|
|
1389
1382
|
): UseKanbanDragReturn {
|
|
1390
|
-
const { lanes = [],
|
|
1383
|
+
const { lanes = [], onReorderTask, getTaskPreview } = options;
|
|
1391
1384
|
const controllerRef = useRef<KanbanDragController | null>(null);
|
|
1392
1385
|
if (!controllerRef.current) {
|
|
1393
1386
|
controllerRef.current = new KanbanDragController({
|
|
1394
1387
|
lanes,
|
|
1395
|
-
onMoveTask,
|
|
1396
1388
|
onReorderTask,
|
|
1397
1389
|
});
|
|
1398
1390
|
}
|
|
1399
1391
|
const controller = controllerRef.current;
|
|
1400
1392
|
|
|
1401
1393
|
controller.setLanes(lanes);
|
|
1402
|
-
if (onMoveTask) {
|
|
1403
|
-
controller.setOnMoveTask(onMoveTask);
|
|
1404
|
-
}
|
|
1405
1394
|
if (onReorderTask) {
|
|
1406
1395
|
controller.setOnReorderTask(onReorderTask);
|
|
1407
1396
|
}
|
|
@@ -1427,7 +1416,7 @@ export function useKanbanDrag(
|
|
|
1427
1416
|
controller.getDroppingState,
|
|
1428
1417
|
);
|
|
1429
1418
|
|
|
1430
|
-
const containerScrollRef = useRef<
|
|
1419
|
+
const containerScrollRef = useRef<ContainerRefTarget | null>(null);
|
|
1431
1420
|
const currentScrollX = useRef(0);
|
|
1432
1421
|
|
|
1433
1422
|
useEffect(() => {
|
|
@@ -1448,7 +1437,10 @@ export function useKanbanDrag(
|
|
|
1448
1437
|
if (!curVelocity || !liveFeedback.isDragging) return;
|
|
1449
1438
|
const nextOffset = Math.max(0, currentScrollX.current + curVelocity * 14);
|
|
1450
1439
|
currentScrollX.current = nextOffset;
|
|
1451
|
-
containerScrollRef.current?.scrollTo({
|
|
1440
|
+
containerScrollRef.current?.scrollTo?.({
|
|
1441
|
+
x: nextOffset,
|
|
1442
|
+
animated: false,
|
|
1443
|
+
});
|
|
1452
1444
|
}, 16);
|
|
1453
1445
|
|
|
1454
1446
|
return () => clearInterval(timer);
|
|
@@ -1463,7 +1455,7 @@ export function useKanbanDrag(
|
|
|
1463
1455
|
controller.handleScroll(scrollX);
|
|
1464
1456
|
};
|
|
1465
1457
|
|
|
1466
|
-
const bindContainerRef = (instance:
|
|
1458
|
+
const bindContainerRef = (instance: ContainerRefTarget | null) => {
|
|
1467
1459
|
containerScrollRef.current = instance;
|
|
1468
1460
|
controller.bindContainerRef(instance);
|
|
1469
1461
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useSyncExternalStore, memo } from "react";
|
|
2
2
|
import { Animated, View, StyleSheet } from "react-native";
|
|
3
|
+
import type { ViewStyle } from "react-native";
|
|
3
4
|
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
4
5
|
import type { KanbanTask } from "../shared/kanban";
|
|
5
6
|
import { KanbanCardPreview } from "./kanban-card";
|
|
@@ -151,7 +152,7 @@ function KanbanDragOverlayInner({
|
|
|
151
152
|
const containerY = containerBounds?.y ?? 0;
|
|
152
153
|
const cardHalfWidth = layout.compact ? 125 : 140;
|
|
153
154
|
|
|
154
|
-
let overlayStyle:
|
|
155
|
+
let overlayStyle: Animated.WithAnimatedValue<ViewStyle>;
|
|
155
156
|
if (curDropping && animPos && animRotate && animScale) {
|
|
156
157
|
overlayStyle = {
|
|
157
158
|
left: animPos.x,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useWorkspace,
|
|
3
|
+
type PluginWorkspacePanelProps,
|
|
4
|
+
} from "@getpaseo/plugin/client";
|
|
5
|
+
import { KanbanBoardView } from "./kanban-board";
|
|
6
|
+
|
|
7
|
+
export function KanbanWorkspacePanel(props: PluginWorkspacePanelProps) {
|
|
8
|
+
const projectId = useWorkspace(
|
|
9
|
+
props.workspaceId,
|
|
10
|
+
(workspace) => workspace.projectId,
|
|
11
|
+
);
|
|
12
|
+
return <KanbanBoardView {...props} initialProjectId={projectId} />;
|
|
13
|
+
}
|
package/client/use-projects.ts
CHANGED
|
@@ -48,7 +48,9 @@ export function fetchProjectsWithOrderControl(
|
|
|
48
48
|
let isDestroyed = false;
|
|
49
49
|
|
|
50
50
|
return {
|
|
51
|
-
execute: async (
|
|
51
|
+
execute: async (
|
|
52
|
+
fetchFn: () => Promise<{ projects?: ProjectItem[] | null } | null>,
|
|
53
|
+
): Promise<void> => {
|
|
52
54
|
const requestId = ++latestRequestId;
|
|
53
55
|
try {
|
|
54
56
|
const result = await fetchFn();
|
|
@@ -56,7 +58,7 @@ export function fetchProjectsWithOrderControl(
|
|
|
56
58
|
return;
|
|
57
59
|
}
|
|
58
60
|
onUpdate({
|
|
59
|
-
projects:
|
|
61
|
+
projects: result?.projects ?? [],
|
|
60
62
|
error: null,
|
|
61
63
|
isLoading: false,
|
|
62
64
|
});
|
package/index.client.tsx
CHANGED
|
@@ -1,33 +1,45 @@
|
|
|
1
1
|
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
2
|
import { KanbanBoardView } from "./client/kanban-board";
|
|
3
|
+
import { KanbanWorkspacePanel } from "./client/kanban-workspace-panel";
|
|
3
4
|
import { t, getLanguage, subscribeLanguage } from "./client/i18n";
|
|
4
5
|
|
|
5
6
|
export default function contribute(client: PluginClientContext) {
|
|
6
|
-
client.addSurface("kanban", KanbanBoardView);
|
|
7
|
-
|
|
7
|
+
const removeSurface = client.addSurface("kanban", KanbanBoardView);
|
|
8
8
|
let currentLang = getLanguage();
|
|
9
|
-
let removeSidebar = client.addSidebarItem({
|
|
10
|
-
id: "kanban",
|
|
11
|
-
title: t("kanban.sidebarTitle", currentLang),
|
|
12
|
-
icon: "PanelsTopLeft",
|
|
13
|
-
surface: "kanban",
|
|
14
|
-
});
|
|
15
9
|
|
|
10
|
+
const registerEntries = () => {
|
|
11
|
+
const title = t("kanban.sidebarTitle", currentLang);
|
|
12
|
+
const removeSidebar = client.addSidebarItem({
|
|
13
|
+
id: "kanban",
|
|
14
|
+
title,
|
|
15
|
+
icon: "PanelsTopLeft",
|
|
16
|
+
surface: "kanban",
|
|
17
|
+
});
|
|
18
|
+
const removePanel = client.addWorkspacePanel({
|
|
19
|
+
id: "kanban",
|
|
20
|
+
title,
|
|
21
|
+
icon: "PanelsTopLeft",
|
|
22
|
+
context: "workspace",
|
|
23
|
+
Component: KanbanWorkspacePanel,
|
|
24
|
+
});
|
|
25
|
+
return () => {
|
|
26
|
+
removeSidebar();
|
|
27
|
+
removePanel();
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let removeEntries = registerEntries();
|
|
16
32
|
const unsubscribe = subscribeLanguage((newLang) => {
|
|
17
33
|
if (newLang !== currentLang) {
|
|
18
34
|
currentLang = newLang;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
id: "kanban",
|
|
22
|
-
title: t("kanban.sidebarTitle", newLang),
|
|
23
|
-
icon: "PanelsTopLeft",
|
|
24
|
-
surface: "kanban",
|
|
25
|
-
});
|
|
35
|
+
removeEntries();
|
|
36
|
+
removeEntries = registerEntries();
|
|
26
37
|
}
|
|
27
38
|
});
|
|
28
39
|
|
|
29
40
|
return () => {
|
|
30
41
|
unsubscribe();
|
|
31
|
-
|
|
42
|
+
removeEntries();
|
|
43
|
+
removeSurface();
|
|
32
44
|
};
|
|
33
45
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-kanban",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "A lightweight, responsive Kanban board plugin for Paseo workspaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"board"
|
|
21
21
|
],
|
|
22
22
|
"files": [
|
|
23
|
+
"OVERVIEW.md",
|
|
23
24
|
"paseo-plugin.json",
|
|
24
25
|
"index.client.tsx",
|
|
25
26
|
"index.server.ts",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"format:check": "prettier --check .",
|
|
34
35
|
"format": "prettier --write .",
|
|
35
36
|
"check:package": "node scripts/check-package.mjs",
|
|
37
|
+
"verify:release": "node scripts/verify-release.mjs",
|
|
36
38
|
"check": "npm run typecheck && npm run test && npm run lint && npm run format:check && npm run check:package"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|