dsh-worktodo 0.1.0 → 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/CHANGELOG.md +67 -2
- package/README.en.md +13 -3
- package/README.md +18 -3
- package/lib/client.js +138 -14
- package/lib/client.js.map +2 -2
- package/lib/index.js +50 -0
- package/package.json +15 -25
- package/src/client/index.js +165 -12
- package/src/records.mjs +85 -9
- package/src/schema.mjs +3 -0
- package/src/service.mjs +58 -3
- package/src/tools.mjs +105 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
> 本文提到的 `tools/`、`docs/` 等路径都在**仓库**里,不在 npm 发布包内:<https://github.com/olimc2016/dsh-worktodo>
|
|
4
|
+
|
|
5
|
+
## 0.1.3
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- **排序模式**:面板顶部新增「手动 / 优先级 / 创建时间」三档排序(`worktodo_list` 也开放了同样的 `sort` 参数)。
|
|
10
|
+
- `manual`(默认):尊重你拖动过的顺序;**没拖过的条目自动退回**优先级 → 截止日期 → 创建时间,所以一上手看到的顺序依然合理;
|
|
11
|
+
- `priority`:高优先级在前,同优先级按截止日期,无日期垫底;
|
|
12
|
+
- `created`:最近添加的在前。
|
|
13
|
+
三种模式都保持「已完成永远沉底」。
|
|
14
|
+
- **拖动排序**:按住任意一行拖到目标位置即可调整顺序。拖动时会**自动切到手动模式**(否则松手后看起来"没生效");
|
|
15
|
+
落序由宿主在校验后写盘(`POST /worktodo/reorder`,收到完整 id 顺序,只写真正变了的记录);
|
|
16
|
+
传了不存在的 id 会**显式失败**(`not-found`),不会静默改一半。
|
|
17
|
+
|
|
18
|
+
### 测试
|
|
19
|
+
|
|
20
|
+
- `verify-records` 新增 2 例:三种排序模式的顺序、`applyManualOrder` 的纯函数语义(含"没拖过的垫后");
|
|
21
|
+
- `smoke-host` 新增 2 例:`/list?sort=created` 的顺序、`POST /reorder` 落盘并在 manual 模式生效(含坏 id 的显式失败)。
|
|
22
|
+
|
|
23
|
+
## 0.1.2
|
|
24
|
+
|
|
25
|
+
### 新增
|
|
26
|
+
|
|
27
|
+
- **按项目管理待办**:面板顶部新增「**按截止 / 按项目**」视图切换。
|
|
28
|
+
「按项目」视图里每个项目是一个可折叠分组,标题显示「未完成/总数 · 逾期 N」;
|
|
29
|
+
没写项目的记录归入「未归类」并**永远排在最后**。
|
|
30
|
+
排序规则:未完成多的项目在前 → 未完成里最早截止日期更早的在前 → 名字字典序。
|
|
31
|
+
宿主侧 `/worktodo/list` 新增 `projectGroups` 字段(`groupByProject`),且**不受 `project` 参数收窄**——
|
|
32
|
+
这样面板一次就能拿到全部项目用于切换。
|
|
33
|
+
|
|
34
|
+
### AI 友好
|
|
35
|
+
|
|
36
|
+
- **给模型的用法指引**:通过官方 `ctx.systemPrompt.section()` 贡献一段提示(段名 `worktodo:guidance`,
|
|
37
|
+
`order` 取 3050——官方内置工具段是 1000–3000、`MCP_SERVERS` 是 3100,插在两者之间)。
|
|
38
|
+
内容讲清三件事:什么时候该用(用户说出承诺/截止日期,或发现超出本会话、仍需人跟进的待办)、
|
|
39
|
+
什么时候**不该**用(当前任务的步骤拆解属于会话内的 `todo_write`,两者互补)、
|
|
40
|
+
以及「加之前先查重」「做完用 `worktodo_done`」「标题写成祈使句」等约定。
|
|
41
|
+
- **批量登记**:新增 `worktodo_add_many`,一次调用登记多条;某条不合法只跳过那一条并把原因回传,
|
|
42
|
+
不影响其余条目(用户一口气说好几件事时省调用)。
|
|
43
|
+
|
|
44
|
+
### 测试
|
|
45
|
+
|
|
46
|
+
- `verify-records` 新增 4 例覆盖 `groupByProject`(未归类最后、未完成多者在前、桶内 total/open/overdue、空输入);
|
|
47
|
+
- `smoke-host` 新增 1 例断言 `/list` 载荷确实带 `projectGroups` 且形状正确。
|
|
48
|
+
|
|
49
|
+
## 0.1.1
|
|
50
|
+
|
|
51
|
+
### 修复(重要)
|
|
52
|
+
|
|
53
|
+
- **装出来的插件根本无法启动**:0.1.0 把运行期真正 import 的三个官方平台包(`@deepseek-ai/dsh-tools`、
|
|
54
|
+
`@deepseek-ai/dsh-storage-domain`、`@deepseek-ai/dsh-home-paths`)只声明为 `peerDependencies`,
|
|
55
|
+
于是从 npm / GitHub 装出来的包**没有任何可解析的平台包**,DSH 启动时该插件直接加载失败
|
|
56
|
+
(实测:工具全部消失,`worktodo_list` 报 `unknown tool`)。现在改为 `dependencies`——
|
|
57
|
+
实测从 GitHub 安装后,从插件自身位置三个包都能解析。
|
|
58
|
+
- **npm 包页面没有 README**:0.1.0 是经「暂存发布」(staged publishing)上线的,而实测那条路径**不会写入 registry 的 `readme` 字段**
|
|
59
|
+
(同机对照:普通 `publish` 的包 `readme` 有 8193 字符,本包 0 字符),于是包页面没有 README。
|
|
60
|
+
本版改为**直接发布**——OIDC 下同样不需要人类 2FA,所以并没有牺牲自动化。
|
|
61
|
+
|
|
62
|
+
### 文档
|
|
63
|
+
|
|
64
|
+
- 三份文档(README 中英、CHANGELOG)里补了**绝对仓库地址**的说明:提到 `docs/`、`tools/` 等目录时指向仓库
|
|
65
|
+
(这些文件不在 npm 包内)。npm 页面上的读者因此能找到出处,而不会撞上够不着的路径。
|
|
66
|
+
- `publish.yml` 的默认发布方式改为**直接发布**,同时保留手动「暂存 + 网页批准」入口。
|
|
67
|
+
|
|
3
68
|
## 0.1.0
|
|
4
69
|
|
|
5
70
|
首个可用版本。
|
|
@@ -43,7 +108,7 @@
|
|
|
43
108
|
- 新增可观测性:`/worktodo/health` 现在报告运行中的实际 `version` 与
|
|
44
109
|
`tools: { status, registered, names, error }`——工具静默没注册这类问题不用再翻日志猜。
|
|
45
110
|
|
|
46
|
-
### 测试(
|
|
111
|
+
### 测试(172 例,均不需要启动 DSH)
|
|
47
112
|
|
|
48
113
|
- `tools/verify-records.mjs`(22):日期归一化与相对日期、状态/优先级同义词、完成时间维护、
|
|
49
114
|
引用解析(id / 标题 / 前缀 / 歧义)、分组边界(本周日 vs 下周一)、统计口径、筛选、坏记录修复。
|
|
@@ -54,7 +119,7 @@
|
|
|
54
119
|
统计不随筛选变化、提升去重、变更通知。
|
|
55
120
|
- `tools/smoke-host.mjs`(26):假 ctx 里跑通 `apply` → 7 条路由 → 服务 → 存储的完整增删改查,
|
|
56
121
|
方法围栏与 `connection` 准入,失败路径(空标题、坏 JSON、未知 filter),health 自检字段,
|
|
57
|
-
|
|
122
|
+
7 个工具经**官方 `defineTool`** 校验注册并真实执行,以及 5 条「工具返回值必须无损」的断言——
|
|
58
123
|
判定用的是 **DSH 自己的校验器** `snapshotJsonValue`(来自 `@deepseek-ai/dsh-util-values`,
|
|
59
124
|
就是 `@deepseek-ai/dsh-tools` 报 "value is not lossless JSON" 时用的那一个函数),
|
|
60
125
|
并有一条专门断言「旧写法 `due: undefined` 必须被它拒绝」,用来证明这条守卫确实有牙齿。
|
package/README.en.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Work Todo · dsh-worktodo
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
**A work todo list that belongs to you, not to a session.** A dedicated DSH sidebar panel, plus `worktodo_*` tools so the agent reads and writes **the same** list you do.
|
|
4
5
|
|
|
5
|
-
> Works with **DSH 0.2.x** (tested on 0.2.0-rc.2) · **
|
|
6
|
+
> Works with **DSH 0.2.x** (tested on 0.2.0-rc.2) · runtime deps are **3 official platform packages only** (no third-party deps) · community plugin, unofficial
|
|
7
|
+
|
|
8
|
+
> **About the paths in this file**: references to `docs/`, `tools/` and similar point into the **repository** —
|
|
9
|
+
> those files are not part of the npm package. Repository: <https://github.com/olimc2016/dsh-worktodo>
|
|
6
10
|
|
|
7
11
|
## The gap it fills
|
|
8
12
|
|
|
@@ -33,7 +37,8 @@ dsh plugin --profile desktop add github:olimc2016/dsh-worktodo
|
|
|
33
37
|
|
|
34
38
|
This path is verified (see `docs/verification.md`): the browser half's build output `lib/client.js` is committed,
|
|
35
39
|
and the package declares no `prepare`/`install` build script, so a git install needs **no build authorization**
|
|
36
|
-
|
|
40
|
+
measurement (install from GitHub): **1 top-level package** plus the 3 official platform packages, about **12 MB** installed
|
|
41
|
+
(~8 MB of that is zod, pulled in by the official `@deepseek-ai/dsh-tools`), and no development dependencies.
|
|
37
42
|
no nested `node_modules`, and the installed tree matches the npm tarball file for file.
|
|
38
43
|
|
|
39
44
|
**Option 2: from npm** (once the package is published)
|
|
@@ -90,10 +95,15 @@ Pin your own path in `cordis.patch.yml`:
|
|
|
90
95
|
```bash
|
|
91
96
|
pnpm install # esbuild ships a prebuilt binary; no approve-builds needed
|
|
92
97
|
pnpm run build # build lib/client.js (browser half)
|
|
93
|
-
pnpm test #
|
|
98
|
+
pnpm test # 172 cases: data model / storage / service / host wiring and tools / client contract / client bundle / CI contract / privacy gate / catalog gate
|
|
94
99
|
pnpm run watch # incremental client build
|
|
95
100
|
```
|
|
96
101
|
|
|
102
|
+
Release flow (trusted publishing + staged approval) is documented in `docs/releasing.md`:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
```
|
|
106
|
+
|
|
97
107
|
No DSH instance is needed to run the suite: `verify-records` (pure data), `verify-store` (real file IO plus a genuine **official `defineDomain` contract check**), `verify-service` (explicit statuses and boundaries), `smoke-host` (apply → routes → service → storage in a fake context, plus tool registration and execution with schemas validated by the **official `defineTool`**), `smoke-contract` (extracts the paths and field names the client actually sends and checks them against the host's real routes — catching silent failures where a mistyped field means, for instance, that due dates never save), `smoke-client` (bundle format, slot registration, and a first render that must not throw), and `verify-submission` (a local gate for the catalog PR: fields, filename, category and date, plus checks that only matter on the GitHub distribution path, such as whether every file a git install needs is actually tracked).
|
|
98
108
|
|
|
99
109
|
## License
|
package/README.md
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
**归你所有、跨会话持久的工作待办。** DSH 侧栏一个独立面板,agent 通过 `worktodo_*` 工具读写**同一份**列表。
|
|
4
4
|
|
|
5
|
-
> 兼容 **DSH 0.2.x**(0.2.0-rc.2 实测)·
|
|
5
|
+
> 兼容 **DSH 0.2.x**(0.2.0-rc.2 实测)· 运行期只依赖 **3 个官方平台包**(无任何第三方依赖)· 社区插件,非官方
|
|
6
|
+
|
|
7
|
+
> **关于本文里的路径**:提到 `docs/`、`tools/` 等目录时,指的是**仓库**里的文件——它们不在 npm 发布包内。
|
|
8
|
+
> 仓库地址:<https://github.com/olimc2016/dsh-worktodo>
|
|
6
9
|
|
|
7
10
|
---
|
|
8
11
|
|
|
@@ -35,7 +38,14 @@ dsh plugin --profile desktop add github:olimc2016/dsh-worktodo
|
|
|
35
38
|
|
|
36
39
|
这条路是实测过的(见 `docs/verification.md`):仓库里提交了浏览器半侧的构建产物 `lib/client.js`,
|
|
37
40
|
包内没有 `prepare`/`install` 这类构建脚本,所以 **git 安装不需要任何构建授权**,也不会把开发依赖装进来
|
|
38
|
-
|
|
41
|
+
——实测(从 GitHub 安装):顶层解析 **1 个包** + 3 个官方平台包,安装约 **12 MB**(其中约 8 MB 是官方 `@deepseek-ai/dsh-tools` 依赖的 zod),
|
|
42
|
+
包内含已构建的 `lib/client.js`,**无需构建授权**。
|
|
43
|
+
|
|
44
|
+
> **为什么平台包必须是 `dependencies` 而不是 peer**:0.1.0 曾只声明 peer,结果从 npm/GitHub 装出来的包里
|
|
45
|
+
> **没有任何可解析的平台包**,DSH 启动时该插件直接加载失败(工具全部消失)。0.1.1 起改为 `dependencies`——
|
|
46
|
+
> 实测安装后从插件自身位置三个包都能解析。
|
|
47
|
+
(注意:DSH 的插件安装器会额外在包目录里装一次开发依赖,本机观察到 22 MB;那是安装器行为、与包本身无关,
|
|
48
|
+
删掉不影响运行——见 `docs/verification.md`。)
|
|
39
49
|
|
|
40
50
|
**方式二:从 npm 装**(包发布后可用)
|
|
41
51
|
|
|
@@ -97,10 +107,15 @@ dsh plugin --profile desktop add dsh-worktodo
|
|
|
97
107
|
```bash
|
|
98
108
|
pnpm install # esbuild 用预编译二进制,无需 approve-builds
|
|
99
109
|
pnpm run build # 打 lib/client.js(浏览器半侧)
|
|
100
|
-
pnpm test #
|
|
110
|
+
pnpm test # 155 个用例:数据模型 / 存储 / 服务 / 宿主接线与工具 / 客户端↔宿主契约 / 客户端 bundle / CI 契约 / 隐私门禁 / 收录门禁
|
|
101
111
|
pnpm run watch # 改客户端时增量构建
|
|
102
112
|
```
|
|
103
113
|
|
|
114
|
+
发版流程(Trusted Publishing + 暂存批准)见仓库里的 `docs/releasing.md`:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
```
|
|
118
|
+
|
|
104
119
|
测试策略:不用启动 DSH 就能跑完——
|
|
105
120
|
`verify-records`(纯数据)、`verify-store`(真实文件 IO + **官方 `defineDomain` 契约校验**)、
|
|
106
121
|
`verify-service`(显式状态与边界)、`smoke-host`(假 ctx 里跑通 apply → 路由 → 服务 → 存储,
|
package/lib/client.js
CHANGED
|
@@ -71,6 +71,14 @@ var zh = {
|
|
|
71
71
|
"groupWeek": "\u672C\u5468",
|
|
72
72
|
"groupLater": "\u4EE5\u540E",
|
|
73
73
|
"groupCompleted": "\u5DF2\u5B8C\u6210",
|
|
74
|
+
"viewDue": "\u6309\u622A\u6B62",
|
|
75
|
+
"viewProject": "\u6309\u9879\u76EE",
|
|
76
|
+
"noProject": "\u672A\u5F52\u7C7B",
|
|
77
|
+
"sortLabel": "\u6392\u5E8F",
|
|
78
|
+
"sortManual": "\u624B\u52A8",
|
|
79
|
+
"sortPriority": "\u4F18\u5148\u7EA7",
|
|
80
|
+
"sortCreated": "\u521B\u5EFA\u65F6\u95F4",
|
|
81
|
+
"dragHint": "\u62D6\u52A8\u8C03\u6574\u987A\u5E8F\uFF08\u4F1A\u81EA\u52A8\u5207\u5230\u624B\u52A8\u6392\u5E8F\uFF09",
|
|
74
82
|
"expand": "\u5C55\u5F00",
|
|
75
83
|
"collapse": "\u6536\u8D77",
|
|
76
84
|
"empty": "\u8FD8\u6CA1\u6709\u5F85\u529E",
|
|
@@ -129,6 +137,14 @@ var en = {
|
|
|
129
137
|
"groupWeek": "This week",
|
|
130
138
|
"groupLater": "Later",
|
|
131
139
|
"groupCompleted": "Completed",
|
|
140
|
+
"viewDue": "By due",
|
|
141
|
+
"viewProject": "By project",
|
|
142
|
+
"noProject": "Uncategorized",
|
|
143
|
+
"sortLabel": "Sort",
|
|
144
|
+
"sortManual": "Manual",
|
|
145
|
+
"sortPriority": "Priority",
|
|
146
|
+
"sortCreated": "Created",
|
|
147
|
+
"dragHint": "Drag to reorder (switches to manual sort)",
|
|
132
148
|
"expand": "Expand",
|
|
133
149
|
"collapse": "Collapse",
|
|
134
150
|
"empty": "No to-dos yet",
|
|
@@ -226,6 +242,18 @@ var STYLE_TEXT = `
|
|
|
226
242
|
flex-direction: column; align-items: stretch; gap: 6px;
|
|
227
243
|
border: 1px solid ${C.border1}; background: ${C.layer1};
|
|
228
244
|
}
|
|
245
|
+
.wt-seg {
|
|
246
|
+
display: inline-flex; align-items: center; border-radius: 8px; overflow: hidden;
|
|
247
|
+
border: 1px solid ${C.border}; background: ${C.bg2};
|
|
248
|
+
}
|
|
249
|
+
.wt-seg-btn {
|
|
250
|
+
appearance: none; border: 0; background: transparent; color: ${C.label2};
|
|
251
|
+
font: inherit; font-size: 11.5px; line-height: 1; padding: 5px 9px; cursor: pointer;
|
|
252
|
+
}
|
|
253
|
+
.wt-seg-btn.is-on { background: ${C.hover}; color: ${C.label}; font-weight: 600; }
|
|
254
|
+
.wt-seg-btn:hover { color: ${C.label}; }
|
|
255
|
+
.wt-row[draggable="true"] { cursor: grab; }
|
|
256
|
+
.wt-row[draggable="true"]:active { cursor: grabbing; }
|
|
229
257
|
.wt-group-head {
|
|
230
258
|
display: flex; align-items: center; gap: 7px; padding: 3px 8px; border-radius: 8px;
|
|
231
259
|
cursor: pointer; user-select: none; color: ${C.label2};
|
|
@@ -362,12 +390,13 @@ async function postJson(path, body) {
|
|
|
362
390
|
return parsed.data ?? {};
|
|
363
391
|
}
|
|
364
392
|
var lastList = null;
|
|
365
|
-
|
|
393
|
+
var lastSort = "manual";
|
|
394
|
+
function useList(sort) {
|
|
366
395
|
const [state, setState] = import_react.default.useState(() => lastList ? { status: "ready", data: lastList, error: null } : { status: "loading", data: null, error: null });
|
|
367
396
|
const [tick, setTick] = import_react.default.useState(0);
|
|
368
397
|
import_react.default.useEffect(() => {
|
|
369
398
|
let alive = true;
|
|
370
|
-
fetch(`${ROUTE_LIST}?filter=all`, { cache: "no-store", credentials: "same-origin" }).then(async (response) => {
|
|
399
|
+
fetch(`${ROUTE_LIST}?filter=all&sort=${encodeURIComponent(sort || "manual")}`, { cache: "no-store", credentials: "same-origin" }).then(async (response) => {
|
|
371
400
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
372
401
|
const body = await response.json().catch(() => null);
|
|
373
402
|
if (!body?.ok) throw new Error(body?.error ?? body?.reason ?? "\u672A\u77E5\u9519\u8BEF");
|
|
@@ -385,7 +414,7 @@ function useList() {
|
|
|
385
414
|
return () => {
|
|
386
415
|
alive = false;
|
|
387
416
|
};
|
|
388
|
-
}, [tick]);
|
|
417
|
+
}, [tick, sort]);
|
|
389
418
|
const refresh = import_react.default.useCallback(() => setTick((n) => n + 1), []);
|
|
390
419
|
return { ...state, refresh };
|
|
391
420
|
}
|
|
@@ -457,7 +486,7 @@ function QuickAdd({ t, onAdd, busy }) {
|
|
|
457
486
|
}, t("add"))
|
|
458
487
|
]);
|
|
459
488
|
}
|
|
460
|
-
function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
489
|
+
function TodoRow({ todo, today, t, onToggle, onRemove, onSave, drag }) {
|
|
461
490
|
const [editing, setEditing] = import_react.default.useState(false);
|
|
462
491
|
const [draft, setDraft] = import_react.default.useState(null);
|
|
463
492
|
const [notesOpen, setNotesOpen] = import_react.default.useState(false);
|
|
@@ -627,7 +656,29 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
627
656
|
title: row.source === "agent" ? t("sourceAgent") : t("sourceSession")
|
|
628
657
|
}, row.source === "agent" ? t("sourceAgent") : t("sourceSession")));
|
|
629
658
|
}
|
|
630
|
-
|
|
659
|
+
const dragAttrs = drag ? {
|
|
660
|
+
draggable: true,
|
|
661
|
+
onDragStart: (event) => {
|
|
662
|
+
try {
|
|
663
|
+
event.dataTransfer?.setData("text/plain", row.id ?? "");
|
|
664
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
665
|
+
} catch {
|
|
666
|
+
}
|
|
667
|
+
drag.onStart?.(row.id);
|
|
668
|
+
},
|
|
669
|
+
onDragOver: (event) => {
|
|
670
|
+
event.preventDefault();
|
|
671
|
+
drag.onOver?.(row.id);
|
|
672
|
+
},
|
|
673
|
+
onDrop: (event) => {
|
|
674
|
+
event.preventDefault();
|
|
675
|
+
drag.onDrop?.(row.id);
|
|
676
|
+
},
|
|
677
|
+
onDragEnd: () => drag.onEnd?.(),
|
|
678
|
+
title: drag.hint,
|
|
679
|
+
style: drag.dragging ? { opacity: 0.45 } : void 0
|
|
680
|
+
} : {};
|
|
681
|
+
return import_react.default.createElement("div", { className: "wt-row", ...dragAttrs }, [
|
|
631
682
|
import_react.default.createElement("button", {
|
|
632
683
|
key: "check",
|
|
633
684
|
type: "button",
|
|
@@ -674,7 +725,7 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
674
725
|
}, confirming ? t("confirmDelete") : import_react.default.createElement(IconTrash, { key: "i" }))
|
|
675
726
|
]);
|
|
676
727
|
}
|
|
677
|
-
function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onRemove, onSave }) {
|
|
728
|
+
function Group({ label, items, meta, closed, t, today, onToggleOpen, onToggleTodo, onRemove, onSave, drag }) {
|
|
678
729
|
const list = asArray(items);
|
|
679
730
|
if (!list.length) return null;
|
|
680
731
|
return import_react.default.createElement("div", { style: { marginTop: 6 } }, [
|
|
@@ -688,7 +739,7 @@ function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onR
|
|
|
688
739
|
}, [
|
|
689
740
|
import_react.default.createElement(IconChevron, { key: "c", open: !closed }),
|
|
690
741
|
import_react.default.createElement("span", { key: "l", style: { fontWeight: 600, fontSize: 12 } }, label),
|
|
691
|
-
import_react.default.createElement("span", { key: "n", style: { fontSize: 11, color: C.label3 } }, String(list.length)),
|
|
742
|
+
import_react.default.createElement("span", { key: "n", style: { fontSize: 11, color: C.label3 } }, meta || String(list.length)),
|
|
692
743
|
import_react.default.createElement("span", { key: "sp", style: { flex: 1 } }),
|
|
693
744
|
import_react.default.createElement("span", { key: "h", style: { fontSize: 11, color: C.label3 } }, closed ? t("expand") : t("collapse"))
|
|
694
745
|
]),
|
|
@@ -699,7 +750,15 @@ function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onR
|
|
|
699
750
|
t,
|
|
700
751
|
onToggle: onToggleTodo,
|
|
701
752
|
onRemove,
|
|
702
|
-
onSave
|
|
753
|
+
onSave,
|
|
754
|
+
drag: drag ? {
|
|
755
|
+
dragging: drag.dragId === todo?.id,
|
|
756
|
+
hint: drag.hint,
|
|
757
|
+
onStart: drag.onStart,
|
|
758
|
+
onOver: drag.onOver,
|
|
759
|
+
onDrop: drag.onDrop,
|
|
760
|
+
onEnd: drag.onEnd
|
|
761
|
+
} : void 0
|
|
703
762
|
})))
|
|
704
763
|
]);
|
|
705
764
|
}
|
|
@@ -747,7 +806,13 @@ var GROUPS = [
|
|
|
747
806
|
];
|
|
748
807
|
function WorkTodoPanel(props) {
|
|
749
808
|
const t = typeof props?.t === "function" ? props.t : (key) => zh[key] ?? key;
|
|
750
|
-
const
|
|
809
|
+
const [sort, setSortState] = import_react.default.useState(lastSort);
|
|
810
|
+
const setSort = import_react.default.useCallback((next) => {
|
|
811
|
+
lastSort = next;
|
|
812
|
+
setSortState(next);
|
|
813
|
+
}, []);
|
|
814
|
+
const [dragId, setDragId] = import_react.default.useState(null);
|
|
815
|
+
const { status, data, error, refresh } = useList(sort);
|
|
751
816
|
const [barError, setBarError] = import_react.default.useState(null);
|
|
752
817
|
const [dismissedReadError, setDismissedReadError] = import_react.default.useState(null);
|
|
753
818
|
const readError = error && error !== dismissedReadError ? error : null;
|
|
@@ -755,6 +820,7 @@ function WorkTodoPanel(props) {
|
|
|
755
820
|
const [notice, setNotice] = import_react.default.useState(null);
|
|
756
821
|
const [pending, setPending] = import_react.default.useState(0);
|
|
757
822
|
const [closed, setClosed] = import_react.default.useState({ completed: true });
|
|
823
|
+
const [view, setView] = import_react.default.useState("due");
|
|
758
824
|
const runWrite = import_react.default.useCallback(async (job) => {
|
|
759
825
|
setPending((n) => n + 1);
|
|
760
826
|
try {
|
|
@@ -784,6 +850,30 @@ function WorkTodoPanel(props) {
|
|
|
784
850
|
const handleToggle = import_react.default.useCallback((ref) => runWrite(() => postJson("/status", { ref, toggle: true })), [runWrite]);
|
|
785
851
|
const handleRemove = import_react.default.useCallback((ref) => runWrite(() => postJson("/remove", { ref })), [runWrite]);
|
|
786
852
|
const handleSave = import_react.default.useCallback((ref, patch) => runWrite(() => postJson("/update", { ref, ...patch })), [runWrite]);
|
|
853
|
+
const handleDropOn = import_react.default.useCallback((targetId) => {
|
|
854
|
+
const dragging = dragId;
|
|
855
|
+
setDragId(null);
|
|
856
|
+
if (!dragging || !targetId || dragging === targetId) return Promise.resolve(false);
|
|
857
|
+
const ids = asArray(data?.todos).map((todo) => todo?.id).filter(Boolean);
|
|
858
|
+
const from = ids.indexOf(dragging);
|
|
859
|
+
if (from === -1) return Promise.resolve(false);
|
|
860
|
+
ids.splice(from, 1);
|
|
861
|
+
const to = ids.indexOf(targetId);
|
|
862
|
+
ids.splice(to === -1 ? ids.length : to, 0, dragging);
|
|
863
|
+
return runWrite(() => postJson("/reorder", { ids })).then((result) => result.ok);
|
|
864
|
+
}, [dragId, data, runWrite]);
|
|
865
|
+
const dragHandlers = import_react.default.useMemo(() => ({
|
|
866
|
+
dragId,
|
|
867
|
+
hint: t("dragHint"),
|
|
868
|
+
onStart: (id) => {
|
|
869
|
+
setDragId(id);
|
|
870
|
+
if (sort !== "manual") setSort("manual");
|
|
871
|
+
},
|
|
872
|
+
onOver: () => {
|
|
873
|
+
},
|
|
874
|
+
onDrop: (id) => handleDropOn(id),
|
|
875
|
+
onEnd: () => setDragId(null)
|
|
876
|
+
}), [dragId, sort, t, setSort, handleDropOn]);
|
|
787
877
|
const handleAdopt = import_react.default.useCallback((sessionTodos) => runWrite(() => postJson("/adopt", { todos: sessionTodos })).then((result) => {
|
|
788
878
|
if (result.ok) {
|
|
789
879
|
const created = Array.isArray(result.data?.created) ? result.data.created.length : 0;
|
|
@@ -834,7 +924,14 @@ function WorkTodoPanel(props) {
|
|
|
834
924
|
const store = data?.store ?? null;
|
|
835
925
|
const num = (value) => Number.isFinite(Number(value)) ? Number(value) : 0;
|
|
836
926
|
const overdueCount = num(stats.overdue);
|
|
837
|
-
const
|
|
927
|
+
const projectGroups = asArray(data?.projectGroups);
|
|
928
|
+
const viewGroups = view === "project" ? projectGroups.map((bucket) => ({
|
|
929
|
+
key: `p:${bucket?.key ?? ""}`,
|
|
930
|
+
label: bucket?.project ? String(bucket.project) : t("noProject"),
|
|
931
|
+
meta: `${num(bucket?.open)}/${num(bucket?.total)}${num(bucket?.overdue) ? ` \xB7 ${t("statOverdue")} ${num(bucket.overdue)}` : ""}`,
|
|
932
|
+
items: bucket?.todos
|
|
933
|
+
})) : GROUPS.map((group) => ({ key: group.key, label: t(group.label), items: groups[group.key] }));
|
|
934
|
+
const totalVisible = viewGroups.reduce((sum, group) => sum + asArray(group.items).length, 0);
|
|
838
935
|
const chips = [
|
|
839
936
|
{ key: "open", label: t("statOpen"), value: num(stats.open) },
|
|
840
937
|
{ key: "overdue", label: t("statOverdue"), value: overdueCount, alert: overdueCount > 0 },
|
|
@@ -858,6 +955,31 @@ function WorkTodoPanel(props) {
|
|
|
858
955
|
import_react.default.createElement("span", { key: "l", style: { color: C.label3 } }, chip.label),
|
|
859
956
|
import_react.default.createElement("b", { key: "v", style: { fontWeight: 650 } }, String(chip.value))
|
|
860
957
|
])),
|
|
958
|
+
import_react.default.createElement("span", { key: "sort", className: "wt-seg", title: t("sortLabel") }, [
|
|
959
|
+
...["manual", "priority", "created"].map((mode) => import_react.default.createElement("button", {
|
|
960
|
+
key: mode,
|
|
961
|
+
type: "button",
|
|
962
|
+
className: `wt-seg-btn${sort === mode ? " is-on" : ""}`,
|
|
963
|
+
title: mode === "manual" ? t("dragHint") : `${t("sortLabel")}\uFF1A${t(`sort${mode === "manual" ? "Manual" : mode === "priority" ? "Priority" : "Created"}`)}`,
|
|
964
|
+
onClick: () => setSort(mode)
|
|
965
|
+
}, t(`sort${mode === "manual" ? "Manual" : mode === "priority" ? "Priority" : "Created"}`)))
|
|
966
|
+
]),
|
|
967
|
+
import_react.default.createElement("span", { key: "view", className: "wt-seg" }, [
|
|
968
|
+
import_react.default.createElement("button", {
|
|
969
|
+
key: "due",
|
|
970
|
+
type: "button",
|
|
971
|
+
className: `wt-seg-btn${view === "due" ? " is-on" : ""}`,
|
|
972
|
+
title: t("viewDue"),
|
|
973
|
+
onClick: () => setView("due")
|
|
974
|
+
}, t("viewDue")),
|
|
975
|
+
import_react.default.createElement("button", {
|
|
976
|
+
key: "proj",
|
|
977
|
+
type: "button",
|
|
978
|
+
className: `wt-seg-btn${view === "project" ? " is-on" : ""}`,
|
|
979
|
+
title: t("viewProject"),
|
|
980
|
+
onClick: () => setView("project")
|
|
981
|
+
}, t("viewProject"))
|
|
982
|
+
]),
|
|
861
983
|
import_react.default.createElement("span", { key: "sp", style: { flex: 1 } }),
|
|
862
984
|
import_react.default.createElement("button", {
|
|
863
985
|
key: "rf",
|
|
@@ -938,17 +1060,19 @@ function WorkTodoPanel(props) {
|
|
|
938
1060
|
}) : null,
|
|
939
1061
|
import_react.default.createElement(QuickAdd, { key: "quick", t, busy: pending > 0, onAdd: handleAdd }),
|
|
940
1062
|
/* 分组列表 */
|
|
941
|
-
totalVisible ? import_react.default.createElement("div", { key: "groups" },
|
|
1063
|
+
totalVisible ? import_react.default.createElement("div", { key: "groups" }, viewGroups.map((group) => import_react.default.createElement(Group, {
|
|
942
1064
|
key: group.key,
|
|
943
|
-
label:
|
|
944
|
-
|
|
1065
|
+
label: group.label,
|
|
1066
|
+
meta: group.meta,
|
|
1067
|
+
items: group.items,
|
|
945
1068
|
closed: Boolean(closed[group.key]),
|
|
946
1069
|
t,
|
|
947
1070
|
today,
|
|
948
1071
|
onToggleOpen: () => setClosed((prev) => ({ ...prev, [group.key]: !prev[group.key] })),
|
|
949
1072
|
onToggleTodo: handleToggle,
|
|
950
1073
|
onRemove: handleRemove,
|
|
951
|
-
onSave: handleSave
|
|
1074
|
+
onSave: handleSave,
|
|
1075
|
+
drag: dragHandlers
|
|
952
1076
|
}))) : import_react.default.createElement("div", {
|
|
953
1077
|
key: "empty",
|
|
954
1078
|
style: { padding: "22px 12px", textAlign: "center", color: C.label3 }
|