paseo-kanban 0.1.0 → 0.1.2
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/LICENSE +21 -21
- package/README.md +36 -13
- package/README.zh-CN.md +38 -13
- package/client/i18n.tsx +47 -21
- package/client/kanban-board.tsx +92 -50
- package/client/kanban-card.tsx +32 -11
- package/client/kanban-drag.ts +336 -109
- package/client/kanban-lane.tsx +19 -7
- package/client/kanban-overlay.tsx +39 -18
- package/client/kanban-session.ts +67 -29
- package/client/lane-modal.tsx +28 -11
- package/client/task-modal.tsx +63 -20
- package/client/use-projects.ts +18 -7
- package/package.json +17 -3
- package/shared/kanban.ts +43 -22
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 breathi3552
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 breathi3552
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -21,23 +21,25 @@ A lightweight, responsive Kanban board plugin for [Paseo](https://paseo.sh) work
|
|
|
21
21
|
|
|
22
22
|
## Requirements
|
|
23
23
|
|
|
24
|
-
- **Paseo**: `>= 0.8.0`
|
|
25
|
-
- **Node.js
|
|
24
|
+
- **Paseo Host Runtime Commitment**: `>= 0.8.0` (declared in `paseo-plugin.json`). As a Paseo workspace plugin, production runtime execution is managed and hosted by the Paseo application environment. No consumer-side Node version restriction is imposed in `package.json`.
|
|
25
|
+
- **Development & CI Baseline**: Node.js 24 (pinned in `.nvmrc` and `.node-version`). Node 24 is required for local contributors and CI automation workflows running quality checks (`npm run check`), type stripping (`--experimental-strip-types`), and native test suites (`node --test`).
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
## Installation
|
|
29
|
+
## Installation
|
|
30
30
|
|
|
31
31
|
### Quick Install (Recommended)
|
|
32
32
|
|
|
33
33
|
No manual build or git cloning needed. You can install directly via npm or GitHub:
|
|
34
34
|
|
|
35
35
|
**Via NPM (Release package):**
|
|
36
|
+
|
|
36
37
|
```bash
|
|
37
38
|
paseo plugin install npm:paseo-kanban
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
**Via GitHub:**
|
|
42
|
+
|
|
41
43
|
```bash
|
|
42
44
|
paseo plugin add breathi3552/paseo-kanban
|
|
43
45
|
```
|
|
@@ -69,6 +71,7 @@ npm install
|
|
|
69
71
|
# 2. Link this directory into your local Paseo daemon
|
|
70
72
|
paseo plugin link .
|
|
71
73
|
```
|
|
74
|
+
|
|
72
75
|
---
|
|
73
76
|
|
|
74
77
|
## Development & Testing
|
|
@@ -77,23 +80,43 @@ paseo plugin link .
|
|
|
77
80
|
|
|
78
81
|
```bash
|
|
79
82
|
npm install
|
|
83
|
+
# or clean install:
|
|
84
|
+
npm ci
|
|
80
85
|
```
|
|
81
86
|
|
|
82
|
-
###
|
|
87
|
+
### Unified Quality Guardrails (Check)
|
|
83
88
|
|
|
84
|
-
Runs
|
|
89
|
+
Runs typecheck, automated test suites, linting, format verification, and release package checks in sequence:
|
|
85
90
|
|
|
86
91
|
```bash
|
|
87
|
-
npm
|
|
92
|
+
npm run check
|
|
88
93
|
```
|
|
89
94
|
|
|
90
|
-
###
|
|
91
|
-
|
|
92
|
-
Validates
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
### Individual Quality Tasks
|
|
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
|
+
Maintainers can publish a new GitHub Release and npm package in one run of the **Release to GitHub and npm** GitHub Action: select `patch`, `minor`, or `major`. Configure `NPM_TOKEN` and repository write permissions first. For setup, recovery, and manual host acceptance, see the [Development & Acceptance Guide](./docs/development.md#24-一键发布github-actions).
|
|
97
120
|
|
|
98
121
|
---
|
|
99
122
|
|
package/README.zh-CN.md
CHANGED
|
@@ -21,23 +21,25 @@
|
|
|
21
21
|
|
|
22
22
|
## 环境要求
|
|
23
23
|
|
|
24
|
-
- **Paseo
|
|
25
|
-
-
|
|
24
|
+
- **Paseo 宿主版本承诺**:`>= 0.8.0`(声明于 `paseo-plugin.json`)。作为 Paseo 工作区插件,插件的生产运行时环境由 Paseo 宿主应用提供并托管执行,不在 `package.json` 对消费端强加额外的 `engines.node` 限制。
|
|
25
|
+
- **开发与 CI 环境基线**:Node.js 24(显式声明于 `.nvmrc` 与 `.node-version`)。本地参与开发、运行质量检查护栏(`npm run check`)、代码格式化、类型检查与自动化测试必须基于 Node 24,依赖其原生的 `--experimental-strip-types` 类型擦除与原生测试运行器(`node --test`)。
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## 安装
|
|
30
30
|
|
|
31
31
|
### 快速安装(推荐)
|
|
32
32
|
|
|
33
33
|
无需克隆源码或手动编译,推荐直接使用 Paseo CLI 安装:
|
|
34
34
|
|
|
35
35
|
**方式一:通过 NPM 官方包安装(推荐)**
|
|
36
|
+
|
|
36
37
|
```bash
|
|
37
38
|
paseo plugin install npm:paseo-kanban
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
**方式二:直接通过 GitHub 仓库安装**
|
|
42
|
+
|
|
41
43
|
```bash
|
|
42
44
|
paseo plugin add breathi3552/paseo-kanban
|
|
43
45
|
```
|
|
@@ -69,6 +71,7 @@ npm install
|
|
|
69
71
|
# 2. 将当前开发目录软链接至本地 Paseo 守护进程
|
|
70
72
|
paseo plugin link .
|
|
71
73
|
```
|
|
74
|
+
|
|
72
75
|
---
|
|
73
76
|
|
|
74
77
|
## 开发与测试
|
|
@@ -77,23 +80,45 @@ paseo plugin link .
|
|
|
77
80
|
|
|
78
81
|
```bash
|
|
79
82
|
npm install
|
|
83
|
+
# 或干净安装验证:
|
|
84
|
+
npm ci
|
|
80
85
|
```
|
|
81
86
|
|
|
82
|
-
###
|
|
87
|
+
### 统一质量护栏检查
|
|
83
88
|
|
|
84
|
-
|
|
89
|
+
按顺序执行类型检查、自动化测试套件、静态代码检查、代码格式验证及发布包完整性检查:
|
|
85
90
|
|
|
86
91
|
```bash
|
|
87
|
-
npm
|
|
92
|
+
npm run check
|
|
88
93
|
```
|
|
89
94
|
|
|
90
|
-
###
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
维护者可在 GitHub Actions 的 **Release to GitHub and npm** 工作流中选择 `patch` / `minor` / `major` 一键发布 GitHub Release 与 npm 包;首次使用需配置 `NPM_TOKEN` 和仓库写入权限。操作及失败重试说明见 [开发与宿主验收指南](./docs/development.md#24-一键发布github-actions)。
|
|
120
|
+
|
|
121
|
+
详细开发贡献流程、发布前检查机制与真实宿主环境手工验收指南,请参阅 [开发与宿主验收指南](./docs/development.md)。
|
|
97
122
|
|
|
98
123
|
---
|
|
99
124
|
|
package/client/i18n.tsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
1
|
import { useState, useEffect, useMemo } from "react";
|
|
3
2
|
|
|
4
3
|
export type SupportedLanguage = "zh" | "en";
|
|
5
4
|
|
|
6
|
-
export type TranslationParams = Record<
|
|
5
|
+
export type TranslationParams = Record<
|
|
6
|
+
string,
|
|
7
|
+
string | number | boolean | null | undefined
|
|
8
|
+
>;
|
|
7
9
|
|
|
8
10
|
export const translations = {
|
|
9
11
|
zh: {
|
|
@@ -16,7 +18,8 @@ export const translations = {
|
|
|
16
18
|
"kanban.refresh": "刷新最新数据",
|
|
17
19
|
"kanban.loadError": "加载看板配置失败:{error}",
|
|
18
20
|
"kanban.formatError": "看板配置格式异常:{error}",
|
|
19
|
-
"kanban.formatErrorHint":
|
|
21
|
+
"kanban.formatErrorHint":
|
|
22
|
+
"数据未被悄悄覆盖。若需使用默认结构可重置,或重试加载。",
|
|
20
23
|
"kanban.retryLoad": "重试加载",
|
|
21
24
|
"kanban.resetDefault": "重置为默认结构",
|
|
22
25
|
"kanban.saveFailed": "保存失败:{error}",
|
|
@@ -35,7 +38,8 @@ export const translations = {
|
|
|
35
38
|
"laneModal.nameLabel": "泳道名称 *",
|
|
36
39
|
"laneModal.namePlaceholder": "例如:待测试、发布中...",
|
|
37
40
|
"laneModal.cannotDeleteLast": "看板必须至少保留一条泳道,无法删除此泳道。",
|
|
38
|
-
"laneModal.cannotDeleteHasTasks":
|
|
41
|
+
"laneModal.cannotDeleteHasTasks":
|
|
42
|
+
"此泳道当前包含 {count} 个任务,需清空或移走任务后方可删除。",
|
|
39
43
|
"laneModal.deleteLane": "删除此泳道",
|
|
40
44
|
"laneModal.confirmDeletePrompt": "确认删除?",
|
|
41
45
|
"laneModal.confirmDelete": "确认删除",
|
|
@@ -74,7 +78,8 @@ export const translations = {
|
|
|
74
78
|
"kanban.refresh": "Refresh latest data",
|
|
75
79
|
"kanban.loadError": "Failed to load kanban settings: {error}",
|
|
76
80
|
"kanban.formatError": "Kanban settings format error: {error}",
|
|
77
|
-
"kanban.formatErrorHint":
|
|
81
|
+
"kanban.formatErrorHint":
|
|
82
|
+
"Data was not silently overwritten. Reset to default structure if needed, or retry loading.",
|
|
78
83
|
"kanban.retryLoad": "Retry Loading",
|
|
79
84
|
"kanban.resetDefault": "Reset to Default",
|
|
80
85
|
"kanban.saveFailed": "Save failed: {error}",
|
|
@@ -85,15 +90,18 @@ export const translations = {
|
|
|
85
90
|
"kanban.projectsError": "(Project list update restricted)",
|
|
86
91
|
"lane.add": "+ Add",
|
|
87
92
|
"lane.manage": "Manage",
|
|
88
|
-
"lane.emptyHint":
|
|
93
|
+
"lane.emptyHint":
|
|
94
|
+
"No cards yet. Click '+' in the top right or drag cards here.",
|
|
89
95
|
"card.dragHandle": "Drag handle",
|
|
90
96
|
"card.subtasksCount": "Subtasks {completed}/{total}",
|
|
91
97
|
"laneModal.titleEdit": "Manage Lane",
|
|
92
98
|
"laneModal.titleCreate": "Add Lane",
|
|
93
99
|
"laneModal.nameLabel": "Lane Name *",
|
|
94
100
|
"laneModal.namePlaceholder": "e.g., Testing, In Review...",
|
|
95
|
-
"laneModal.cannotDeleteLast":
|
|
96
|
-
|
|
101
|
+
"laneModal.cannotDeleteLast":
|
|
102
|
+
"A kanban board must retain at least one lane. Cannot delete this lane.",
|
|
103
|
+
"laneModal.cannotDeleteHasTasks":
|
|
104
|
+
"This lane currently contains {count} tasks. Clear or move tasks before deleting.",
|
|
97
105
|
"laneModal.deleteLane": "Delete Lane",
|
|
98
106
|
"laneModal.confirmDeletePrompt": "Confirm delete?",
|
|
99
107
|
"laneModal.confirmDelete": "Confirm Delete",
|
|
@@ -166,7 +174,10 @@ interface EnvironmentGlobals {
|
|
|
166
174
|
language?: string;
|
|
167
175
|
};
|
|
168
176
|
window?: {
|
|
169
|
-
addEventListener(
|
|
177
|
+
addEventListener(
|
|
178
|
+
event: string,
|
|
179
|
+
listener: (event: { key?: string }) => void,
|
|
180
|
+
): void;
|
|
170
181
|
};
|
|
171
182
|
i18n?: {
|
|
172
183
|
language?: string;
|
|
@@ -175,7 +186,9 @@ interface EnvironmentGlobals {
|
|
|
175
186
|
|
|
176
187
|
const env = globalThis as typeof globalThis & EnvironmentGlobals;
|
|
177
188
|
|
|
178
|
-
export function detectPaseoLanguage(
|
|
189
|
+
export function detectPaseoLanguage(
|
|
190
|
+
hostProps?: DetectLanguageOptions,
|
|
191
|
+
): SupportedLanguage {
|
|
179
192
|
if (languageOverride) {
|
|
180
193
|
return languageOverride;
|
|
181
194
|
}
|
|
@@ -254,14 +267,18 @@ function ensureGlobalListeners() {
|
|
|
254
267
|
});
|
|
255
268
|
}
|
|
256
269
|
|
|
257
|
-
export function getLanguage(
|
|
270
|
+
export function getLanguage(
|
|
271
|
+
hostProps?: DetectLanguageOptions,
|
|
272
|
+
): SupportedLanguage {
|
|
258
273
|
if (hostProps) {
|
|
259
274
|
return detectPaseoLanguage(hostProps);
|
|
260
275
|
}
|
|
261
276
|
return currentLanguage;
|
|
262
277
|
}
|
|
263
278
|
|
|
264
|
-
export function subscribeLanguage(
|
|
279
|
+
export function subscribeLanguage(
|
|
280
|
+
listener: (lang: SupportedLanguage) => void,
|
|
281
|
+
): () => void {
|
|
265
282
|
listeners.add(listener);
|
|
266
283
|
ensureGlobalListeners();
|
|
267
284
|
return () => {
|
|
@@ -269,7 +286,10 @@ export function subscribeLanguage(listener: (lang: SupportedLanguage) => void):
|
|
|
269
286
|
};
|
|
270
287
|
}
|
|
271
288
|
|
|
272
|
-
export function formatString(
|
|
289
|
+
export function formatString(
|
|
290
|
+
template: string,
|
|
291
|
+
params?: TranslationParams,
|
|
292
|
+
): string {
|
|
273
293
|
if (!params) return template;
|
|
274
294
|
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
275
295
|
return params[key] != null ? String(params[key]) : match;
|
|
@@ -279,7 +299,7 @@ export function formatString(template: string, params?: TranslationParams): stri
|
|
|
279
299
|
export function t(
|
|
280
300
|
key: TranslationKey,
|
|
281
301
|
langOrParams?: SupportedLanguage | TranslationParams,
|
|
282
|
-
params?: TranslationParams
|
|
302
|
+
params?: TranslationParams,
|
|
283
303
|
): string {
|
|
284
304
|
let lang: SupportedLanguage;
|
|
285
305
|
let finalParams: TranslationParams | undefined;
|
|
@@ -303,22 +323,28 @@ export interface I18nContextValue {
|
|
|
303
323
|
}
|
|
304
324
|
|
|
305
325
|
export function useI18n(hostProps?: DetectLanguageOptions): I18nContextValue {
|
|
306
|
-
const
|
|
326
|
+
const hostLocale =
|
|
327
|
+
hostProps?.locale ??
|
|
328
|
+
hostProps?.language ??
|
|
329
|
+
hostProps?.host?.locale ??
|
|
330
|
+
hostProps?.host?.language;
|
|
331
|
+
const [lang, setLang] = useState<SupportedLanguage>(() =>
|
|
332
|
+
detectPaseoLanguage(hostProps),
|
|
333
|
+
);
|
|
307
334
|
|
|
308
335
|
useEffect(() => {
|
|
309
336
|
ensureGlobalListeners();
|
|
310
337
|
const immediate = detectPaseoLanguage(hostProps);
|
|
311
|
-
|
|
312
|
-
setLang(immediate);
|
|
313
|
-
}
|
|
338
|
+
setLang((prev) => (immediate !== prev ? immediate : prev));
|
|
314
339
|
return subscribeLanguage((newLang) => {
|
|
315
340
|
setLang(newLang);
|
|
316
341
|
});
|
|
317
|
-
}, [
|
|
342
|
+
}, [hostLocale, hostProps]);
|
|
318
343
|
|
|
319
344
|
const tBound = useMemo(
|
|
320
|
-
() => (key: TranslationKey, params?: TranslationParams) =>
|
|
321
|
-
|
|
345
|
+
() => (key: TranslationKey, params?: TranslationParams) =>
|
|
346
|
+
t(key, lang, params),
|
|
347
|
+
[lang],
|
|
322
348
|
);
|
|
323
349
|
|
|
324
350
|
return {
|
package/client/kanban-board.tsx
CHANGED
|
@@ -9,12 +9,8 @@ import {
|
|
|
9
9
|
} from "react-native";
|
|
10
10
|
import { useSettings } from "@getpaseo/plugin/client";
|
|
11
11
|
import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
filterTasksByProject,
|
|
15
|
-
type KanbanTask,
|
|
16
|
-
} from "../shared/kanban";
|
|
17
|
-
import { useProjects, getProjectDisplayName } from "./use-projects";
|
|
12
|
+
import { kanbanSettings, filterTasksByProject } from "../shared/kanban";
|
|
13
|
+
import { useProjects } from "./use-projects";
|
|
18
14
|
import { TaskModal } from "./task-modal";
|
|
19
15
|
import { LaneModal } from "./lane-modal";
|
|
20
16
|
import { useKanbanDrag } from "./kanban-drag";
|
|
@@ -33,11 +29,17 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
33
29
|
const { theme, layout } = props;
|
|
34
30
|
const { t, language } = useI18n(props);
|
|
35
31
|
const settings = useSettings(kanbanSettings);
|
|
36
|
-
const {
|
|
32
|
+
const {
|
|
33
|
+
projects,
|
|
34
|
+
resolveProjectName,
|
|
35
|
+
isError: projectsError,
|
|
36
|
+
} = useProjects();
|
|
37
37
|
|
|
38
38
|
const [selectedProjectId, setSelectedProjectId] = useState<string>("all");
|
|
39
|
-
const [activeTaskSession, setActiveTaskSession] =
|
|
40
|
-
|
|
39
|
+
const [activeTaskSession, setActiveTaskSession] =
|
|
40
|
+
useState<TaskEditSession | null>(null);
|
|
41
|
+
const [activeLaneSession, setActiveLaneSession] =
|
|
42
|
+
useState<LaneEditSession | null>(null);
|
|
41
43
|
const [reorderError, setReorderError] = useState<string | null>(null);
|
|
42
44
|
|
|
43
45
|
const isReady = settings.status === "ready";
|
|
@@ -53,7 +55,7 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
53
55
|
const handleReorderTask = async (
|
|
54
56
|
taskId: string,
|
|
55
57
|
targetLaneId: string,
|
|
56
|
-
targetIndex: number
|
|
58
|
+
targetIndex: number,
|
|
57
59
|
) => {
|
|
58
60
|
if (!board) return;
|
|
59
61
|
const result = await executeTaskReorder({
|
|
@@ -102,7 +104,7 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
102
104
|
? null
|
|
103
105
|
: selectedProjectId,
|
|
104
106
|
language,
|
|
105
|
-
})
|
|
107
|
+
}),
|
|
106
108
|
);
|
|
107
109
|
};
|
|
108
110
|
|
|
@@ -116,7 +118,7 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
116
118
|
baseRevision: revision,
|
|
117
119
|
save: (b, r) => settings.save(b, r),
|
|
118
120
|
language,
|
|
119
|
-
})
|
|
121
|
+
}),
|
|
120
122
|
);
|
|
121
123
|
};
|
|
122
124
|
|
|
@@ -129,7 +131,7 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
129
131
|
baseRevision: revision,
|
|
130
132
|
save: (b, r) => settings.save(b, r),
|
|
131
133
|
language,
|
|
132
|
-
})
|
|
134
|
+
}),
|
|
133
135
|
);
|
|
134
136
|
};
|
|
135
137
|
|
|
@@ -143,7 +145,7 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
143
145
|
baseRevision: revision,
|
|
144
146
|
save: (b, r) => settings.save(b, r),
|
|
145
147
|
language,
|
|
146
|
-
})
|
|
148
|
+
}),
|
|
147
149
|
);
|
|
148
150
|
};
|
|
149
151
|
|
|
@@ -294,7 +296,7 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
294
296
|
textAlign: "center",
|
|
295
297
|
},
|
|
296
298
|
}),
|
|
297
|
-
[theme, layout.compact]
|
|
299
|
+
[theme, layout.compact],
|
|
298
300
|
);
|
|
299
301
|
|
|
300
302
|
if (settings.status === "loading") {
|
|
@@ -312,8 +314,13 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
312
314
|
<Text style={[styles.centerText, { color: theme.colors.statusDanger }]}>
|
|
313
315
|
{t("kanban.loadError", { error: settings.error })}
|
|
314
316
|
</Text>
|
|
315
|
-
<Pressable
|
|
316
|
-
|
|
317
|
+
<Pressable
|
|
318
|
+
onPress={() => settings.reload()}
|
|
319
|
+
style={styles.secondaryButton}
|
|
320
|
+
>
|
|
321
|
+
<Text style={styles.secondaryButtonText}>
|
|
322
|
+
{t("kanban.retryLoad")}
|
|
323
|
+
</Text>
|
|
317
324
|
</Pressable>
|
|
318
325
|
</View>
|
|
319
326
|
);
|
|
@@ -322,18 +329,38 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
322
329
|
if (settings.status === "invalid") {
|
|
323
330
|
return (
|
|
324
331
|
<View style={[styles.container, styles.centerBox]}>
|
|
325
|
-
<Text
|
|
332
|
+
<Text
|
|
333
|
+
style={[
|
|
334
|
+
styles.centerText,
|
|
335
|
+
{ color: theme.colors.statusWarning ?? theme.colors.statusDanger },
|
|
336
|
+
]}
|
|
337
|
+
>
|
|
326
338
|
{t("kanban.formatError", { error: settings.error })}
|
|
327
339
|
</Text>
|
|
328
|
-
<Text
|
|
340
|
+
<Text
|
|
341
|
+
style={[
|
|
342
|
+
styles.centerText,
|
|
343
|
+
{ color: theme.colors.foregroundMuted, fontSize: 12 },
|
|
344
|
+
]}
|
|
345
|
+
>
|
|
329
346
|
{t("kanban.formatErrorHint")}
|
|
330
347
|
</Text>
|
|
331
348
|
<View style={{ flexDirection: "row", gap: 10 }}>
|
|
332
|
-
<Pressable
|
|
333
|
-
|
|
349
|
+
<Pressable
|
|
350
|
+
onPress={() => settings.reload()}
|
|
351
|
+
style={styles.secondaryButton}
|
|
352
|
+
>
|
|
353
|
+
<Text style={styles.secondaryButtonText}>
|
|
354
|
+
{t("kanban.retryLoad")}
|
|
355
|
+
</Text>
|
|
334
356
|
</Pressable>
|
|
335
|
-
<Pressable
|
|
336
|
-
|
|
357
|
+
<Pressable
|
|
358
|
+
onPress={() => settings.reset()}
|
|
359
|
+
style={styles.primaryButton}
|
|
360
|
+
>
|
|
361
|
+
<Text style={styles.primaryButtonText}>
|
|
362
|
+
{t("kanban.resetDefault")}
|
|
363
|
+
</Text>
|
|
337
364
|
</Pressable>
|
|
338
365
|
</View>
|
|
339
366
|
</View>
|
|
@@ -349,35 +376,43 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
349
376
|
<View style={styles.headerTopRow}>
|
|
350
377
|
<View style={styles.titleRow}>
|
|
351
378
|
<Text style={styles.titleText}>{t("kanban.title")}</Text>
|
|
352
|
-
<View
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
379
|
+
<View
|
|
380
|
+
style={{
|
|
381
|
+
backgroundColor: theme.colors.surface2,
|
|
382
|
+
paddingHorizontal: 6,
|
|
383
|
+
paddingVertical: 2,
|
|
384
|
+
borderRadius: 10,
|
|
385
|
+
}}
|
|
386
|
+
>
|
|
387
|
+
<Text
|
|
388
|
+
style={{
|
|
389
|
+
fontSize: 11,
|
|
390
|
+
color: theme.colors.foregroundMuted,
|
|
391
|
+
fontWeight: "600",
|
|
392
|
+
}}
|
|
393
|
+
>
|
|
394
|
+
{t("kanban.stats", {
|
|
395
|
+
tasks: board.tasks.length,
|
|
396
|
+
lanes: board.lanes.length,
|
|
397
|
+
})}
|
|
364
398
|
</Text>
|
|
365
399
|
</View>
|
|
366
400
|
</View>
|
|
367
401
|
|
|
368
402
|
<View style={styles.headerActions}>
|
|
369
|
-
<Pressable
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
<Text style={styles.secondaryButtonText}>{t("kanban.addLane")}</Text>
|
|
403
|
+
<Pressable onPress={openNewLane} style={styles.secondaryButton}>
|
|
404
|
+
<Text style={styles.secondaryButtonText}>
|
|
405
|
+
{t("kanban.addLane")}
|
|
406
|
+
</Text>
|
|
374
407
|
</Pressable>
|
|
375
408
|
|
|
376
409
|
<Pressable
|
|
377
410
|
onPress={() => openNewTask()}
|
|
378
411
|
style={styles.primaryButton}
|
|
379
412
|
>
|
|
380
|
-
<Text style={styles.primaryButtonText}>
|
|
413
|
+
<Text style={styles.primaryButtonText}>
|
|
414
|
+
{t("kanban.newTask")}
|
|
415
|
+
</Text>
|
|
381
416
|
</Pressable>
|
|
382
417
|
</View>
|
|
383
418
|
</View>
|
|
@@ -419,7 +454,8 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
419
454
|
<Text
|
|
420
455
|
style={[
|
|
421
456
|
styles.filterChipText,
|
|
422
|
-
selectedProjectId === "unassigned" &&
|
|
457
|
+
selectedProjectId === "unassigned" &&
|
|
458
|
+
styles.filterChipTextSelected,
|
|
423
459
|
]}
|
|
424
460
|
>
|
|
425
461
|
{t("kanban.filterUnassigned")}
|
|
@@ -453,7 +489,13 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
453
489
|
})}
|
|
454
490
|
|
|
455
491
|
{projectsError && (
|
|
456
|
-
<Text
|
|
492
|
+
<Text
|
|
493
|
+
style={{
|
|
494
|
+
fontSize: 11,
|
|
495
|
+
color:
|
|
496
|
+
theme.colors.statusWarning ?? theme.colors.foregroundMuted,
|
|
497
|
+
}}
|
|
498
|
+
>
|
|
457
499
|
{t("kanban.projectsError")}
|
|
458
500
|
</Text>
|
|
459
501
|
)}
|
|
@@ -476,7 +518,9 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
476
518
|
}}
|
|
477
519
|
style={styles.conflictAction}
|
|
478
520
|
>
|
|
479
|
-
<Text style={{ fontSize: 12, color: theme.colors.foreground }}>
|
|
521
|
+
<Text style={{ fontSize: 12, color: theme.colors.foreground }}>
|
|
522
|
+
{t("kanban.refresh")}
|
|
523
|
+
</Text>
|
|
480
524
|
</Pressable>
|
|
481
525
|
</View>
|
|
482
526
|
)}
|
|
@@ -489,7 +533,9 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
489
533
|
style={styles.lanesContainer}
|
|
490
534
|
contentContainerStyle={styles.lanesContent}
|
|
491
535
|
onLayout={(e) => drag.handleContainerLayout(e.nativeEvent.layout)}
|
|
492
|
-
onScroll={(e) =>
|
|
536
|
+
onScroll={(e) =>
|
|
537
|
+
drag.handleContainerScroll(e.nativeEvent.contentOffset.x)
|
|
538
|
+
}
|
|
493
539
|
scrollEventThrottle={16}
|
|
494
540
|
>
|
|
495
541
|
{board.lanes.map((lane) => {
|
|
@@ -512,11 +558,7 @@ export function KanbanBoardView(props: PluginSurfaceProps) {
|
|
|
512
558
|
})}
|
|
513
559
|
</ScrollView>
|
|
514
560
|
|
|
515
|
-
<KanbanDragOverlay
|
|
516
|
-
drag={drag}
|
|
517
|
-
theme={theme}
|
|
518
|
-
layout={layout}
|
|
519
|
-
/>
|
|
561
|
+
<KanbanDragOverlay drag={drag} theme={theme} layout={layout} />
|
|
520
562
|
</View>
|
|
521
563
|
|
|
522
564
|
{/* Modals with Session Baseline Locking */}
|