dsh-worktodo 0.1.0 → 0.2.0
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 +141 -2
- package/README.en.md +13 -3
- package/README.md +18 -3
- package/lib/client.js +597 -73
- package/lib/client.js.map +4 -4
- package/lib/index.js +107 -1
- package/package.json +15 -25
- package/src/client/index.js +462 -71
- package/src/parse-due.mjs +170 -0
- package/src/records.mjs +131 -16
- package/src/schema.mjs +9 -1
- package/src/service.mjs +58 -3
- package/src/store.mjs +4 -1
- package/src/tools.mjs +137 -4
package/src/client/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
// 与宿主共用同一份解析器:人机一致,且零依赖(会被 esbuild 内联进 bundle)
|
|
3
|
+
import { parseNaturalDue } from '../parse-due.mjs';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* worktodo 浏览器半侧:DSH 侧栏入口 + 中栏「工作待办」面板。
|
|
@@ -63,6 +65,26 @@ const zh = {
|
|
|
63
65
|
'groupWeek': '本周',
|
|
64
66
|
'groupLater': '以后',
|
|
65
67
|
'groupCompleted': '已完成',
|
|
68
|
+
'viewDue': '按截止',
|
|
69
|
+
'viewProject': '按项目',
|
|
70
|
+
'noProject': '未归类',
|
|
71
|
+
'sortLabel': '排序',
|
|
72
|
+
'sortManual': '手动',
|
|
73
|
+
'sortPriority': '优先级',
|
|
74
|
+
'sortCreated': '创建时间',
|
|
75
|
+
'dragHint': '拖动调整顺序(会自动切到手动排序)',
|
|
76
|
+
'versionLabel': '版本',
|
|
77
|
+
'versionHint': '当前安装的 dsh-worktodo 版本',
|
|
78
|
+
'updateAvailable': '有新版本',
|
|
79
|
+
'updateHint': '更新命令(重启 DSH 生效)',
|
|
80
|
+
'addOpen': '新增待办',
|
|
81
|
+
'addCancel': '取消',
|
|
82
|
+
'prioNone': '无',
|
|
83
|
+
'dateUnset': '日期 · 未设',
|
|
84
|
+
'timeUnset': '时间 · 未设',
|
|
85
|
+
'todayWord': '今天',
|
|
86
|
+
'fromParsed': '识别',
|
|
87
|
+
'dueTimeLabel': '时间',
|
|
66
88
|
'expand': '展开',
|
|
67
89
|
'collapse': '收起',
|
|
68
90
|
'empty': '还没有待办',
|
|
@@ -122,6 +144,26 @@ const en = {
|
|
|
122
144
|
'groupWeek': 'This week',
|
|
123
145
|
'groupLater': 'Later',
|
|
124
146
|
'groupCompleted': 'Completed',
|
|
147
|
+
'viewDue': 'By due',
|
|
148
|
+
'viewProject': 'By project',
|
|
149
|
+
'noProject': 'Uncategorized',
|
|
150
|
+
'sortLabel': 'Sort',
|
|
151
|
+
'sortManual': 'Manual',
|
|
152
|
+
'sortPriority': 'Priority',
|
|
153
|
+
'sortCreated': 'Created',
|
|
154
|
+
'dragHint': 'Drag to reorder (switches to manual sort)',
|
|
155
|
+
'versionLabel': 'Version',
|
|
156
|
+
'versionHint': 'Installed dsh-worktodo version',
|
|
157
|
+
'updateAvailable': 'Update available',
|
|
158
|
+
'updateHint': 'Update command (restart DSH to apply)',
|
|
159
|
+
'addOpen': 'New todo',
|
|
160
|
+
'addCancel': 'Cancel',
|
|
161
|
+
'prioNone': 'None',
|
|
162
|
+
'dateUnset': 'Date · unset',
|
|
163
|
+
'timeUnset': 'Time · unset',
|
|
164
|
+
'todayWord': 'Today',
|
|
165
|
+
'fromParsed': 'parsed',
|
|
166
|
+
'dueTimeLabel': 'Time',
|
|
125
167
|
'expand': 'Expand',
|
|
126
168
|
'collapse': 'Collapse',
|
|
127
169
|
'empty': 'No to-dos yet',
|
|
@@ -158,6 +200,13 @@ const en = {
|
|
|
158
200
|
* 不写任何颜色字面量兜底——兜底值等于写死某一种主题下的颜色,另一套主题就花了。
|
|
159
201
|
* 需要「淡淡的底色」时用 color-mix 把语义色混进 transparent,仍然是变量驱动。
|
|
160
202
|
* ------------------------------------------------------------------ */
|
|
203
|
+
/** 本地时区的 YYYY-MM-DD(面板里判断"今天 / 逾期"用;与宿主 records.mjs 的 dayKey 同语义)。 */
|
|
204
|
+
function dayKey(stamp) {
|
|
205
|
+
const d = new Date(stamp);
|
|
206
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
207
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
161
210
|
const C = {
|
|
162
211
|
label: 'var(--dsw-alias-label-primary)',
|
|
163
212
|
label2: 'var(--dsw-alias-label-secondary)',
|
|
@@ -228,6 +277,32 @@ const STYLE_TEXT = `
|
|
|
228
277
|
flex-direction: column; align-items: stretch; gap: 6px;
|
|
229
278
|
border: 1px solid ${C.border1}; background: ${C.layer1};
|
|
230
279
|
}
|
|
280
|
+
.wt-seg {
|
|
281
|
+
display: inline-flex; align-items: center; border-radius: 8px; overflow: hidden;
|
|
282
|
+
border: 1px solid ${C.border}; background: ${C.bg2};
|
|
283
|
+
}
|
|
284
|
+
.wt-seg-btn {
|
|
285
|
+
appearance: none; border: 0; background: transparent; color: ${C.label2};
|
|
286
|
+
font: inherit; font-size: 11.5px; line-height: 1; padding: 5px 9px; cursor: pointer;
|
|
287
|
+
}
|
|
288
|
+
.wt-seg-btn.is-on { background: ${C.hover}; color: ${C.label}; font-weight: 600; }
|
|
289
|
+
.wt-seg-btn:hover { color: ${C.label}; }
|
|
290
|
+
.wt-row[draggable="true"] { cursor: grab; }
|
|
291
|
+
.wt-row[draggable="true"]:active { cursor: grabbing; }
|
|
292
|
+
.wt-plus {
|
|
293
|
+
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
294
|
+
padding: 12px 14px; margin-bottom: 10px; cursor: pointer; font: inherit; font-size: 13.5px; font-weight: 600;
|
|
295
|
+
color: ${C.label2}; background: ${C.layer1};
|
|
296
|
+
border: 1px dashed ${C.border}; border-radius: 12px;
|
|
297
|
+
}
|
|
298
|
+
.wt-plus:hover { color: ${C.label}; border-color: ${C.brand}; background: ${tint(C.brand, 8)}; }
|
|
299
|
+
.wt-plus-mark { font-size: 24px; line-height: 1; font-weight: 400; color: ${C.brand}; }
|
|
300
|
+
.wt-flag-btn {
|
|
301
|
+
appearance: none; display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
|
|
302
|
+
font: inherit; font-size: 11.5px; padding: 5px 9px; border-radius: 9px;
|
|
303
|
+
background: transparent; border: 1px solid ${C.border}; color: ${C.label2};
|
|
304
|
+
}
|
|
305
|
+
.wt-flag-btn.is-on { border-color: currentColor; background: color-mix(in srgb, currentColor 12%, transparent); color: inherit; }
|
|
231
306
|
.wt-group-head {
|
|
232
307
|
display: flex; align-items: center; gap: 7px; padding: 3px 8px; border-radius: 8px;
|
|
233
308
|
cursor: pointer; user-select: none; color: ${C.label2};
|
|
@@ -362,8 +437,10 @@ async function postJson(path, body) {
|
|
|
362
437
|
* (stale-while-revalidate)。侧栏来回点的时候不该每次都转圈。
|
|
363
438
|
*/
|
|
364
439
|
let lastList = null;
|
|
440
|
+
// 排序模式跨「切走再切回」保持;manual = 尊重拖动顺序
|
|
441
|
+
let lastSort = 'manual';
|
|
365
442
|
|
|
366
|
-
function useList() {
|
|
443
|
+
function useList(sort) {
|
|
367
444
|
const [state, setState] = React.useState(() => (lastList
|
|
368
445
|
? { status: 'ready', data: lastList, error: null }
|
|
369
446
|
: { status: 'loading', data: null, error: null }));
|
|
@@ -376,7 +453,7 @@ function useList() {
|
|
|
376
453
|
* 用 open 时 groups.completed 恒为空,「已完成」那一组就永远出不来(也就没法折叠/展开)。
|
|
377
454
|
* 统计 stats 不受 filter 影响(宿主按全量算),所以顶部数字仍然是全局口径。
|
|
378
455
|
*/
|
|
379
|
-
fetch(`${ROUTE_LIST}?filter=all`, { cache: 'no-store', credentials: 'same-origin' })
|
|
456
|
+
fetch(`${ROUTE_LIST}?filter=all&sort=${encodeURIComponent(sort || 'manual')}`, { cache: 'no-store', credentials: 'same-origin' })
|
|
380
457
|
.then(async (response) => {
|
|
381
458
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
382
459
|
const body = await response.json().catch(() => null);
|
|
@@ -395,7 +472,7 @@ function useList() {
|
|
|
395
472
|
}
|
|
396
473
|
});
|
|
397
474
|
return () => { alive = false; };
|
|
398
|
-
}, [tick]);
|
|
475
|
+
}, [tick, sort]);
|
|
399
476
|
|
|
400
477
|
const refresh = React.useCallback(() => setTick((n) => n + 1), []);
|
|
401
478
|
return { ...state, refresh };
|
|
@@ -404,72 +481,220 @@ function useList() {
|
|
|
404
481
|
/* ------------------------------------------------------------------ *
|
|
405
482
|
* 快速添加
|
|
406
483
|
* ------------------------------------------------------------------ */
|
|
484
|
+
/** 小红旗图标:填充与描边都用 currentColor,颜色由按钮的 color 决定。 */
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* 新增待办:平时只是一个**大加号**,点开才展开表单。
|
|
488
|
+
* 日期用原生日历(`type=date`)、时间用原生时钟(`type=time`),
|
|
489
|
+
* 优先级用三个不同颜色的**小红旗**表示(高=红、普通=琥珀、低=灰)。
|
|
490
|
+
*/
|
|
491
|
+
|
|
492
|
+
/** 小红旗图标:颜色由父级 color 决定;filled=选中档位。 */
|
|
493
|
+
function IconFlag({ filled, dashed }) {
|
|
494
|
+
return React.createElement('svg', { width: 13, height: 13, viewBox: '0 0 16 16', 'aria-hidden': 'true' }, [
|
|
495
|
+
React.createElement('path', {
|
|
496
|
+
key: 'pole', d: 'M3.6 1.6v13', stroke: 'currentColor', strokeWidth: 1.6,
|
|
497
|
+
strokeLinecap: 'round', fill: 'none', strokeDasharray: dashed ? '1.5 1.5' : undefined,
|
|
498
|
+
}),
|
|
499
|
+
React.createElement('path', {
|
|
500
|
+
key: 'cloth', d: 'M5.2 2.4h7.4L11 5.2l1.6 2.8H5.2z',
|
|
501
|
+
fill: filled ? 'currentColor' : 'none', stroke: 'currentColor', strokeWidth: 1.3,
|
|
502
|
+
strokeLinejoin: 'round', strokeDasharray: dashed ? '1.5 1.5' : undefined,
|
|
503
|
+
}),
|
|
504
|
+
]);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const IconCal = () => React.createElement('svg', { width: 13, height: 13, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, 'aria-hidden': 'true' }, [
|
|
508
|
+
React.createElement('rect', { key: 'r', x: 2.2, y: 3.4, width: 11.6, height: 10.4, rx: 2 }),
|
|
509
|
+
React.createElement('path', { key: 'p', d: 'M2.2 6.6h11.6M5.4 2.2v2.4M10.6 2.2v2.4' }),
|
|
510
|
+
]);
|
|
511
|
+
const IconClock = () => React.createElement('svg', { width: 13, height: 13, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, 'aria-hidden': 'true' }, [
|
|
512
|
+
React.createElement('circle', { key: 'c', cx: 8, cy: 8, r: 5.8 }),
|
|
513
|
+
React.createElement('path', { key: 'p', d: 'M8 4.9V8l2.2 1.4' }),
|
|
514
|
+
]);
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* 快速添加:两行布局(设计稿 v5)。
|
|
518
|
+
* 第一行:待办内容 + 添加;
|
|
519
|
+
* 第二行(更小):日期 / 时间 / 优先级——显示**当前值**,点开才选。
|
|
520
|
+
* 标题里写了「明天下午3点」这类说法会被实时解析:第二行自动点亮并标"· 识别",
|
|
521
|
+
* 且日期词会从标题里摘掉。**手动改过就不再被自动覆盖**(手动优先)。
|
|
522
|
+
*/
|
|
407
523
|
function QuickAdd({ t, onAdd, busy }) {
|
|
408
524
|
const [title, setTitle] = React.useState('');
|
|
409
525
|
const [due, setDue] = React.useState('');
|
|
410
|
-
const [
|
|
526
|
+
const [dueTime, setDueTime] = React.useState('');
|
|
527
|
+
const [priority, setPriority] = React.useState('none');
|
|
528
|
+
const [parsed, setParsed] = React.useState(null);
|
|
529
|
+
const [manual, setManual] = React.useState(false); // 用户手动改过日期/时间
|
|
530
|
+
const [flagOpen, setFlagOpen] = React.useState(false);
|
|
531
|
+
// 点"整块胶囊"都要弹出选择器:Safari 里带值的 date/time 输入光靠点击只会聚焦,
|
|
532
|
+
// 所以统一主动调 showPicker()(不可用时退回 focus+click)
|
|
533
|
+
const dateRef = React.useRef(null);
|
|
534
|
+
const timeRef = React.useRef(null);
|
|
535
|
+
const openPicker = (ref) => {
|
|
536
|
+
const el = ref.current;
|
|
537
|
+
if (!el) return;
|
|
538
|
+
if (typeof el.showPicker === 'function') {
|
|
539
|
+
try { el.showPicker(); return; } catch { /* 某些环境不允许,走下面的兜底 */ }
|
|
540
|
+
}
|
|
541
|
+
el.focus();
|
|
542
|
+
try { el.click(); } catch { /* 忽略 */ }
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// 输入防抖 300ms 再解析(每按一键就解析会闪烁)
|
|
546
|
+
React.useEffect(() => {
|
|
547
|
+
if (manual) return undefined;
|
|
548
|
+
const text = title;
|
|
549
|
+
if (!text || !text.trim()) { setParsed(null); return undefined; }
|
|
550
|
+
const timer = setTimeout(() => {
|
|
551
|
+
const result = parseNaturalDue(text, Date.now());
|
|
552
|
+
if (result.matched.length) {
|
|
553
|
+
setParsed(result);
|
|
554
|
+
setDue(result.due ?? '');
|
|
555
|
+
setDueTime(result.dueTime ?? '');
|
|
556
|
+
} else {
|
|
557
|
+
setParsed(null);
|
|
558
|
+
}
|
|
559
|
+
}, 300);
|
|
560
|
+
return () => clearTimeout(timer);
|
|
561
|
+
}, [title, manual]);
|
|
411
562
|
|
|
412
563
|
const submit = () => {
|
|
413
564
|
if (busy) return;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
565
|
+
let payloadTitle = title;
|
|
566
|
+
if (!manual) {
|
|
567
|
+
const result = parseNaturalDue(title, Date.now());
|
|
568
|
+
if (result.matched.length) payloadTitle = result.title; // 日期词摘掉
|
|
569
|
+
}
|
|
570
|
+
Promise.resolve(onAdd({
|
|
571
|
+
title: payloadTitle,
|
|
572
|
+
due: due || null,
|
|
573
|
+
dueTime: dueTime || null,
|
|
574
|
+
priority,
|
|
575
|
+
})).then((ok) => {
|
|
576
|
+
if (ok !== false) {
|
|
577
|
+
setTitle('');
|
|
578
|
+
setParsed(null);
|
|
579
|
+
setManual(false);
|
|
580
|
+
setFlagOpen(false);
|
|
581
|
+
// 日期/时间/优先级保留:连着录同一天的几件事不用重设
|
|
582
|
+
}
|
|
583
|
+
}).catch(() => { /* onAdd 内部已显示错误 */ });
|
|
418
584
|
};
|
|
419
585
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
586
|
+
const today = dayKey(Date.now());
|
|
587
|
+
const dateLabel = due
|
|
588
|
+
? (due === today ? t('todayWord') : `${due.slice(5).replace('-', '-')}`)
|
|
589
|
+
: t('dateUnset');
|
|
590
|
+
const timeLabel = dueTime || t('timeUnset');
|
|
591
|
+
const prioLabel = priority === 'high' ? t('prioHigh') : priority === 'normal' ? t('prioNormal') : priority === 'low' ? t('prioLow') : t('prioNone');
|
|
592
|
+
const prioColor = priority === 'high' ? C.error : priority === 'normal' ? C.warn : C.label3;
|
|
593
|
+
|
|
594
|
+
const flags = [
|
|
595
|
+
{ value: 'none', label: t('prioNone'), color: C.label3, dashed: true },
|
|
596
|
+
{ value: 'high', label: t('prioHigh'), color: C.error },
|
|
597
|
+
{ value: 'normal', label: t('prioNormal'), color: C.warn },
|
|
598
|
+
{ value: 'low', label: t('prioLow'), color: C.label3 },
|
|
599
|
+
];
|
|
600
|
+
|
|
601
|
+
const setDate = (value) => {
|
|
602
|
+
setManual(true);
|
|
603
|
+
setDue(value);
|
|
604
|
+
if (!value) setDueTime(''); // 没日期就没时间
|
|
605
|
+
setFlagOpen(false);
|
|
606
|
+
};
|
|
607
|
+
const setTime = (value) => {
|
|
608
|
+
setManual(true);
|
|
609
|
+
if (value && !due) setDue(dayKey(Date.now())); // 只选时间=今天
|
|
610
|
+
setDueTime(value);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
return React.createElement('div', { key: 'qa', style: { marginBottom: 12 } }, [
|
|
614
|
+
// ---- 第一行:内容 + 添加 ----
|
|
615
|
+
React.createElement('div', { key: 'l1', style: { display: 'flex', alignItems: 'center', gap: 8 } }, [
|
|
439
616
|
React.createElement('input', {
|
|
440
|
-
key: '
|
|
617
|
+
key: 'ti',
|
|
441
618
|
className: 'wt-input',
|
|
442
|
-
|
|
443
|
-
value:
|
|
444
|
-
|
|
445
|
-
|
|
619
|
+
style: { flex: '1 1 auto', fontSize: 13, padding: '7px 10px' },
|
|
620
|
+
value: title,
|
|
621
|
+
placeholder: t('addPlaceholder'),
|
|
622
|
+
'aria-label': t('addPlaceholder'),
|
|
623
|
+
onChange: (event) => {
|
|
624
|
+
const next = event.target.value;
|
|
625
|
+
setTitle(next);
|
|
626
|
+
// 手动优先,但把标题清空=重新开始:交回给自动识别
|
|
627
|
+
if (!next.trim()) { setManual(false); setParsed(null); }
|
|
628
|
+
},
|
|
629
|
+
onKeyDown: (event) => { if (event.key === 'Enter') submit(); },
|
|
446
630
|
}),
|
|
631
|
+
React.createElement('button', {
|
|
632
|
+
key: 'add', type: 'button', className: 'wt-btn wt-btn-primary',
|
|
633
|
+
disabled: Boolean(busy), onClick: submit,
|
|
634
|
+
}, t('add')),
|
|
447
635
|
]),
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
'aria-label': t('priorityLabel'),
|
|
459
|
-
onChange: (event) => setPriority(event.target.value),
|
|
636
|
+
|
|
637
|
+
// ---- 第二行(更小):日期 / 时间 / 优先级 ----
|
|
638
|
+
React.createElement('div', { key: 'l2', style: { display: 'flex', alignItems: 'center', gap: 10, marginTop: 8, fontSize: 11.5, flexWrap: 'wrap' } }, [
|
|
639
|
+
// 日期:label 里套一个透明的原生 date 输入,点它即弹系统日历
|
|
640
|
+
React.createElement('label', {
|
|
641
|
+
key: 'due',
|
|
642
|
+
className: `wt-chip${parsed && parsed.due ? ' is-parsed' : ''}`,
|
|
643
|
+
title: t('dueLabel'),
|
|
644
|
+
onClick: (event) => { event.preventDefault(); openPicker(dateRef); },
|
|
645
|
+
style: { position: 'relative', cursor: 'pointer', color: due ? C.label : C.label3, borderColor: parsed && parsed.due ? C.brand : undefined },
|
|
460
646
|
}, [
|
|
461
|
-
React.createElement(
|
|
462
|
-
React.createElement('
|
|
463
|
-
React.createElement('
|
|
647
|
+
React.createElement(IconCal, { key: 'i' }),
|
|
648
|
+
React.createElement('span', { key: 'v' }, due ? `${today === due ? t('todayWord') : due}${parsed && parsed.due ? ` · ${t('fromParsed')}` : ''}` : t('dateUnset')),
|
|
649
|
+
React.createElement('input', {
|
|
650
|
+
key: 'in', ref: dateRef, type: 'date', value: due, 'aria-label': t('dueLabel'),
|
|
651
|
+
style: { position: 'absolute', inset: 0, opacity: 0, border: 0, padding: 0, width: '100%', height: '100%', cursor: 'pointer' },
|
|
652
|
+
onChange: (event) => setDate(event.target.value),
|
|
653
|
+
}),
|
|
654
|
+
]),
|
|
655
|
+
// 时间:同样用原生 time 输入;只选时间时自动补今天
|
|
656
|
+
React.createElement('label', {
|
|
657
|
+
key: 'time',
|
|
658
|
+
className: `wt-chip${parsed && parsed.dueTime ? ' is-parsed' : ''}`,
|
|
659
|
+
title: t('dueTimeLabel'),
|
|
660
|
+
onClick: (event) => { event.preventDefault(); openPicker(timeRef); },
|
|
661
|
+
style: { position: 'relative', cursor: 'pointer', color: dueTime ? C.label : C.label3, borderColor: parsed && parsed.dueTime ? C.brand : undefined },
|
|
662
|
+
}, [
|
|
663
|
+
React.createElement(IconClock, { key: 'i' }),
|
|
664
|
+
React.createElement('span', { key: 'v' }, `${timeLabel}${parsed && parsed.dueTime ? ` · ${t('fromParsed')}` : ''}`),
|
|
665
|
+
React.createElement('input', {
|
|
666
|
+
key: 'in', ref: timeRef, type: 'time', value: dueTime, lang: 'zh-CN', 'aria-label': t('dueTimeLabel'),
|
|
667
|
+
style: { position: 'absolute', inset: 0, opacity: 0, border: 0, padding: 0, width: '100%', height: '100%', cursor: 'pointer' },
|
|
668
|
+
onChange: (event) => setTime(event.target.value),
|
|
669
|
+
}),
|
|
670
|
+
]),
|
|
671
|
+
// 优先级:点开四个小旗
|
|
672
|
+
React.createElement('span', { key: 'prio', style: { position: 'relative' } }, [
|
|
673
|
+
React.createElement('button', {
|
|
674
|
+
key: 'btn', type: 'button', className: 'wt-chip', style: { color: prioColor, cursor: 'pointer', font: 'inherit', fontSize: 11.5 },
|
|
675
|
+
title: t('priorityLabel'), 'aria-expanded': flagOpen,
|
|
676
|
+
onClick: () => setFlagOpen((open) => !open),
|
|
677
|
+
}, [
|
|
678
|
+
React.createElement(IconFlag, { key: 'f', dashed: priority === 'none', filled: priority !== 'none' }),
|
|
679
|
+
React.createElement('span', { key: 'v' }, `${t('priorityLabel')} · ${prioLabel}`),
|
|
680
|
+
]),
|
|
681
|
+
flagOpen ? React.createElement('div', {
|
|
682
|
+
key: 'pop',
|
|
683
|
+
style: {
|
|
684
|
+
position: 'absolute', top: '100%', left: 0, zIndex: 5, marginTop: 4, padding: 6, display: 'flex', gap: 4,
|
|
685
|
+
background: C.layer1, border: `1px solid ${C.border}`, borderRadius: 10, boxShadow: '0 8px 24px rgba(0,0,0,.12)',
|
|
686
|
+
},
|
|
687
|
+
}, flags.map((flag) => React.createElement('button', {
|
|
688
|
+
key: flag.value, type: 'button', className: 'wt-chip',
|
|
689
|
+
style: { color: flag.color, font: 'inherit', fontSize: 11.5, cursor: 'pointer', borderColor: priority === flag.value ? flag.color : undefined },
|
|
690
|
+
title: flag.label,
|
|
691
|
+
onClick: () => { setPriority(flag.value); setFlagOpen(false); },
|
|
692
|
+
}, [
|
|
693
|
+
React.createElement(IconFlag, { key: 'f', filled: priority === flag.value, dashed: flag.value === 'none' }),
|
|
694
|
+
React.createElement('span', { key: 'v' }, flag.label),
|
|
695
|
+
]))) : null,
|
|
464
696
|
]),
|
|
465
697
|
]),
|
|
466
|
-
React.createElement('button', {
|
|
467
|
-
key: 'add',
|
|
468
|
-
type: 'button',
|
|
469
|
-
className: 'wt-btn wt-btn-primary',
|
|
470
|
-
disabled: Boolean(busy),
|
|
471
|
-
onClick: submit,
|
|
472
|
-
}, t('add')),
|
|
473
698
|
]);
|
|
474
699
|
}
|
|
475
700
|
|
|
@@ -477,7 +702,7 @@ function QuickAdd({ t, onAdd, busy }) {
|
|
|
477
702
|
* 单行:查看 / 行内编辑 / 二次确认删除
|
|
478
703
|
* 状态都留在这一行内部,所以在别的行上敲字、或者列表刷新,都不会把正在编辑的内容冲掉。
|
|
479
704
|
* ------------------------------------------------------------------ */
|
|
480
|
-
function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
705
|
+
function TodoRow({ todo, today, t, onToggle, onRemove, onSave, drag }) {
|
|
481
706
|
const [editing, setEditing] = React.useState(false);
|
|
482
707
|
const [draft, setDraft] = React.useState(null);
|
|
483
708
|
const [notesOpen, setNotesOpen] = React.useState(false);
|
|
@@ -501,6 +726,7 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
501
726
|
due,
|
|
502
727
|
priority: row.priority === 'high' || row.priority === 'low' ? row.priority : 'normal',
|
|
503
728
|
project: typeof row.project === 'string' ? row.project : '',
|
|
729
|
+
dueTime: typeof row.dueTime === 'string' ? row.dueTime : '',
|
|
504
730
|
notes: typeof row.notes === 'string' ? row.notes : '',
|
|
505
731
|
});
|
|
506
732
|
setNotesOpen(Boolean(row.notes));
|
|
@@ -519,7 +745,12 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
519
745
|
const title = String(draft.title ?? '').trim();
|
|
520
746
|
if (title !== (row.title ?? '')) patch.title = title;
|
|
521
747
|
const nextDue = draft.due || '';
|
|
522
|
-
if (nextDue !== due)
|
|
748
|
+
if (nextDue !== due) {
|
|
749
|
+
patch.due = nextDue; // 空串 = 清空截止日期
|
|
750
|
+
if (!nextDue) patch.dueTime = 'none'; // 没有日期就没有时间
|
|
751
|
+
}
|
|
752
|
+
const nextDueTime = String(draft.dueTime ?? '').trim();
|
|
753
|
+
if (nextDue && nextDueTime !== (row.dueTime ?? '')) patch.dueTime = nextDueTime === '' ? 'none' : nextDueTime;
|
|
523
754
|
const priority = draft.priority || 'normal';
|
|
524
755
|
if (priority !== (row.priority ?? 'normal')) patch.priority = priority;
|
|
525
756
|
const project = String(draft.project ?? '').trim();
|
|
@@ -571,6 +802,11 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
571
802
|
'aria-label': t('dueLabel'),
|
|
572
803
|
onChange: (event) => setDraft((prev) => ({ ...prev, due: event.target.value })),
|
|
573
804
|
})),
|
|
805
|
+
field('tm', t('dueTimeLabel'), React.createElement('input', {
|
|
806
|
+
className: 'wt-input', style: inputStyle, type: 'time', value: draft.dueTime, disabled: !draft.due,
|
|
807
|
+
'aria-label': t('dueTimeLabel'),
|
|
808
|
+
onChange: (event) => setDraft((prev) => ({ ...prev, dueTime: event.target.value })),
|
|
809
|
+
})),
|
|
574
810
|
field('prio', t('priorityLabel'), React.createElement('select', {
|
|
575
811
|
className: 'wt-input', style: inputStyle, value: draft.priority,
|
|
576
812
|
'aria-label': t('priorityLabel'),
|
|
@@ -644,7 +880,34 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
644
880
|
}, row.source === 'agent' ? t('sourceAgent') : t('sourceSession')));
|
|
645
881
|
}
|
|
646
882
|
|
|
647
|
-
|
|
883
|
+
// 拖动属性:只有父级给了 drag 才可拖(未启用时行为与以前完全一致)
|
|
884
|
+
const dragAttrs = drag
|
|
885
|
+
? {
|
|
886
|
+
draggable: true,
|
|
887
|
+
onDragStart: (event) => {
|
|
888
|
+
try {
|
|
889
|
+
event.dataTransfer?.setData('text/plain', row.id ?? '');
|
|
890
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
|
|
891
|
+
} catch { /* 少数环境不允许写 dataTransfer,忽略即可 */ }
|
|
892
|
+
drag.onStart?.(row.id);
|
|
893
|
+
},
|
|
894
|
+
onDragOver: (event) => { event.preventDefault(); drag.onOver?.(row.id); },
|
|
895
|
+
onDrop: (event) => { event.preventDefault(); drag.onDrop?.(row.id); },
|
|
896
|
+
onDragEnd: () => drag.onEnd?.(),
|
|
897
|
+
title: drag.hint,
|
|
898
|
+
style: drag.dragging ? { opacity: 0.45 } : undefined,
|
|
899
|
+
}
|
|
900
|
+
: {};
|
|
901
|
+
return React.createElement('div', { className: 'wt-row', ...dragAttrs, style: row.priority === 'high' ? { borderLeft: `2px solid ${C.error}`, paddingLeft: 8 } : undefined }, [
|
|
902
|
+
/* 优先级呈现(设计稿 v5):高=红色左缘 + 实心红旗;中=琥珀;低=灰;无=什么都不显示 */
|
|
903
|
+
row.priority && row.priority !== 'none' ? React.createElement('span', {
|
|
904
|
+
key: 'prio-flag',
|
|
905
|
+
title: row.priority === 'high' ? t('prioHigh') : row.priority === 'normal' ? t('prioNormal') : t('prioLow'),
|
|
906
|
+
style: {
|
|
907
|
+
display: 'inline-flex', flex: '0 0 auto',
|
|
908
|
+
color: row.priority === 'high' ? C.error : row.priority === 'normal' ? C.warn : C.label3,
|
|
909
|
+
},
|
|
910
|
+
}, React.createElement(IconFlag, { filled: row.priority === 'high' })) : null,
|
|
648
911
|
React.createElement('button', {
|
|
649
912
|
key: 'check',
|
|
650
913
|
type: 'button',
|
|
@@ -690,7 +953,7 @@ function TodoRow({ todo, today, t, onToggle, onRemove, onSave }) {
|
|
|
690
953
|
/* ------------------------------------------------------------------ *
|
|
691
954
|
* 分组:空组整块不渲染,避免五个空标题堆在面板上
|
|
692
955
|
* ------------------------------------------------------------------ */
|
|
693
|
-
function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onRemove, onSave }) {
|
|
956
|
+
function Group({ label, items, meta, closed, t, today, onToggleOpen, onToggleTodo, onRemove, onSave, drag }) {
|
|
694
957
|
const list = asArray(items);
|
|
695
958
|
if (!list.length) return null;
|
|
696
959
|
return React.createElement('div', { style: { marginTop: 6 } }, [
|
|
@@ -704,7 +967,7 @@ function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onR
|
|
|
704
967
|
}, [
|
|
705
968
|
React.createElement(IconChevron, { key: 'c', open: !closed }),
|
|
706
969
|
React.createElement('span', { key: 'l', style: { fontWeight: 600, fontSize: 12 } }, label),
|
|
707
|
-
React.createElement('span', { key: 'n', style: { fontSize: 11, color: C.label3 } }, String(list.length)),
|
|
970
|
+
React.createElement('span', { key: 'n', style: { fontSize: 11, color: C.label3 } }, meta || String(list.length)),
|
|
708
971
|
React.createElement('span', { key: 'sp', style: { flex: 1 } }),
|
|
709
972
|
React.createElement('span', { key: 'h', style: { fontSize: 11, color: C.label3 } }, closed ? t('expand') : t('collapse')),
|
|
710
973
|
]),
|
|
@@ -716,6 +979,14 @@ function Group({ label, items, closed, t, today, onToggleOpen, onToggleTodo, onR
|
|
|
716
979
|
onToggle: onToggleTodo,
|
|
717
980
|
onRemove,
|
|
718
981
|
onSave,
|
|
982
|
+
drag: drag ? {
|
|
983
|
+
dragging: drag.dragId === todo?.id,
|
|
984
|
+
hint: drag.hint,
|
|
985
|
+
onStart: drag.onStart,
|
|
986
|
+
onOver: drag.onOver,
|
|
987
|
+
onDrop: drag.onDrop,
|
|
988
|
+
onEnd: drag.onEnd,
|
|
989
|
+
} : undefined,
|
|
719
990
|
}))),
|
|
720
991
|
]);
|
|
721
992
|
}
|
|
@@ -779,7 +1050,11 @@ const GROUPS = [
|
|
|
779
1050
|
function WorkTodoPanel(props) {
|
|
780
1051
|
// t 由 locale 服务通过 slot props 注入;拿不到时退回中文,至少不显示裸 key
|
|
781
1052
|
const t = typeof props?.t === 'function' ? props.t : (key) => zh[key] ?? key;
|
|
782
|
-
|
|
1053
|
+
// 排序:manual(默认,尊重拖动)/ priority / created;必须在 useList(sort) 之前声明
|
|
1054
|
+
const [sort, setSortState] = React.useState(lastSort);
|
|
1055
|
+
const setSort = React.useCallback((next) => { lastSort = next; setSortState(next); }, []);
|
|
1056
|
+
const [dragId, setDragId] = React.useState(null);
|
|
1057
|
+
const { status, data, error, refresh } = useList(sort);
|
|
783
1058
|
|
|
784
1059
|
// 写操作的错误条;readError 是拉取错误(用户关掉之后,只有换成新错误才再弹)
|
|
785
1060
|
const [barError, setBarError] = React.useState(null);
|
|
@@ -791,6 +1066,8 @@ function WorkTodoPanel(props) {
|
|
|
791
1066
|
const [pending, setPending] = React.useState(0);
|
|
792
1067
|
// 「已完成」默认折叠(做完的事不用占满屏幕),其余默认展开
|
|
793
1068
|
const [closed, setClosed] = React.useState({ completed: true });
|
|
1069
|
+
// 视图:按截止(默认)或按项目
|
|
1070
|
+
const [view, setView] = React.useState('due');
|
|
794
1071
|
|
|
795
1072
|
/**
|
|
796
1073
|
* 所有写操作的统一外壳:成功清掉错误条,失败把宿主的 error 原文放上去,
|
|
@@ -830,6 +1107,60 @@ function WorkTodoPanel(props) {
|
|
|
830
1107
|
const handleRemove = React.useCallback((ref) => runWrite(() => postJson('/remove', { ref })), [runWrite]);
|
|
831
1108
|
const handleSave = React.useCallback((ref, patch) => runWrite(() => postJson('/update', { ref, ...patch })), [runWrite]);
|
|
832
1109
|
|
|
1110
|
+
/** 把 dragId 挪到目标之前,得到新的完整顺序并落盘(manual 模式生效) */
|
|
1111
|
+
|
|
1112
|
+
const handleDropOn = React.useCallback((targetId) => {
|
|
1113
|
+
|
|
1114
|
+
const dragging = dragId;
|
|
1115
|
+
|
|
1116
|
+
setDragId(null);
|
|
1117
|
+
|
|
1118
|
+
if (!dragging || !targetId || dragging === targetId) return Promise.resolve(false);
|
|
1119
|
+
|
|
1120
|
+
const ids = asArray(data?.todos).map((todo) => todo?.id).filter(Boolean);
|
|
1121
|
+
|
|
1122
|
+
const from = ids.indexOf(dragging);
|
|
1123
|
+
|
|
1124
|
+
if (from === -1) return Promise.resolve(false);
|
|
1125
|
+
|
|
1126
|
+
ids.splice(from, 1);
|
|
1127
|
+
|
|
1128
|
+
const to = ids.indexOf(targetId);
|
|
1129
|
+
|
|
1130
|
+
ids.splice(to === -1 ? ids.length : to, 0, dragging);
|
|
1131
|
+
|
|
1132
|
+
return runWrite(() => postJson('/reorder', { ids })).then((result) => result.ok);
|
|
1133
|
+
|
|
1134
|
+
}, [dragId, data, runWrite]);
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
const dragHandlers = React.useMemo(() => ({
|
|
1139
|
+
|
|
1140
|
+
dragId,
|
|
1141
|
+
|
|
1142
|
+
hint: t('dragHint'),
|
|
1143
|
+
|
|
1144
|
+
onStart: (id) => {
|
|
1145
|
+
|
|
1146
|
+
setDragId(id);
|
|
1147
|
+
|
|
1148
|
+
// 拖了就说明想自己定顺序:自动切到手动,否则松手后看起来「没生效」
|
|
1149
|
+
|
|
1150
|
+
if (sort !== 'manual') setSort('manual');
|
|
1151
|
+
|
|
1152
|
+
},
|
|
1153
|
+
|
|
1154
|
+
onOver: () => {},
|
|
1155
|
+
|
|
1156
|
+
onDrop: (id) => handleDropOn(id),
|
|
1157
|
+
|
|
1158
|
+
onEnd: () => setDragId(null),
|
|
1159
|
+
|
|
1160
|
+
}), [dragId, sort, t, setSort, handleDropOn]);
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
|
|
833
1164
|
const handleAdopt = React.useCallback((sessionTodos) => runWrite(() => postJson('/adopt', { todos: sessionTodos }))
|
|
834
1165
|
.then((result) => {
|
|
835
1166
|
// runWrite 已经刷新过列表,这里补一句「提升了几条、跳过几条」的回执,否则点完像没反应
|
|
@@ -877,9 +1208,21 @@ function WorkTodoPanel(props) {
|
|
|
877
1208
|
const stats = data?.stats ?? {};
|
|
878
1209
|
const today = typeof data?.today === 'string' && data.today ? data.today : localToday();
|
|
879
1210
|
const store = data?.store ?? null;
|
|
1211
|
+
const version = typeof data?.version === 'string' ? data.version : '';
|
|
1212
|
+
const update = data?.update ?? null;
|
|
880
1213
|
const num = (value) => (Number.isFinite(Number(value)) ? Number(value) : 0);
|
|
881
1214
|
const overdueCount = num(stats.overdue);
|
|
882
|
-
|
|
1215
|
+
// 视图分桶:按截止(原有 GROUPS)或按项目(宿主给的 projectGroups,附 未完成/总数/逾期)
|
|
1216
|
+
const projectGroups = asArray(data?.projectGroups);
|
|
1217
|
+
const viewGroups = view === 'project'
|
|
1218
|
+
? projectGroups.map((bucket) => ({
|
|
1219
|
+
key: `p:${bucket?.key ?? ''}`,
|
|
1220
|
+
label: bucket?.project ? String(bucket.project) : t('noProject'),
|
|
1221
|
+
meta: `${num(bucket?.open)}/${num(bucket?.total)}${num(bucket?.overdue) ? ` · ${t('statOverdue')} ${num(bucket.overdue)}` : ''}`,
|
|
1222
|
+
items: bucket?.todos,
|
|
1223
|
+
}))
|
|
1224
|
+
: GROUPS.map((group) => ({ key: group.key, label: t(group.label), items: groups[group.key] }));
|
|
1225
|
+
const totalVisible = viewGroups.reduce((sum, group) => sum + asArray(group.items).length, 0);
|
|
883
1226
|
|
|
884
1227
|
const chips = [
|
|
885
1228
|
{ key: 'open', label: t('statOpen'), value: num(stats.open) },
|
|
@@ -908,6 +1251,31 @@ function WorkTodoPanel(props) {
|
|
|
908
1251
|
React.createElement('span', { key: 'l', style: { color: C.label3 } }, chip.label),
|
|
909
1252
|
React.createElement('b', { key: 'v', style: { fontWeight: 650 } }, String(chip.value)),
|
|
910
1253
|
])),
|
|
1254
|
+
React.createElement('span', { key: 'sort', className: 'wt-seg', title: t('sortLabel') }, [
|
|
1255
|
+
...['manual', 'priority', 'created'].map((mode) => React.createElement('button', {
|
|
1256
|
+
key: mode,
|
|
1257
|
+
type: 'button',
|
|
1258
|
+
className: `wt-seg-btn${sort === mode ? ' is-on' : ''}`,
|
|
1259
|
+
title: mode === 'manual' ? t('dragHint') : `${t('sortLabel')}:${t(`sort${mode === 'manual' ? 'Manual' : mode === 'priority' ? 'Priority' : 'Created'}`)}`,
|
|
1260
|
+
onClick: () => setSort(mode),
|
|
1261
|
+
}, t(`sort${mode === 'manual' ? 'Manual' : mode === 'priority' ? 'Priority' : 'Created'}`))),
|
|
1262
|
+
]),
|
|
1263
|
+
React.createElement('span', { key: 'view', className: 'wt-seg' }, [
|
|
1264
|
+
React.createElement('button', {
|
|
1265
|
+
key: 'due',
|
|
1266
|
+
type: 'button',
|
|
1267
|
+
className: `wt-seg-btn${view === 'due' ? ' is-on' : ''}`,
|
|
1268
|
+
title: t('viewDue'),
|
|
1269
|
+
onClick: () => setView('due'),
|
|
1270
|
+
}, t('viewDue')),
|
|
1271
|
+
React.createElement('button', {
|
|
1272
|
+
key: 'proj',
|
|
1273
|
+
type: 'button',
|
|
1274
|
+
className: `wt-seg-btn${view === 'project' ? ' is-on' : ''}`,
|
|
1275
|
+
title: t('viewProject'),
|
|
1276
|
+
onClick: () => setView('project'),
|
|
1277
|
+
}, t('viewProject')),
|
|
1278
|
+
]),
|
|
911
1279
|
React.createElement('span', { key: 'sp', style: { flex: 1 } }),
|
|
912
1280
|
React.createElement('button', {
|
|
913
1281
|
key: 'rf',
|
|
@@ -980,10 +1348,11 @@ function WorkTodoPanel(props) {
|
|
|
980
1348
|
|
|
981
1349
|
/* 分组列表 */
|
|
982
1350
|
totalVisible
|
|
983
|
-
? React.createElement('div', { key: 'groups' },
|
|
1351
|
+
? React.createElement('div', { key: 'groups' }, viewGroups.map((group) => React.createElement(Group, {
|
|
984
1352
|
key: group.key,
|
|
985
|
-
label:
|
|
986
|
-
|
|
1353
|
+
label: group.label,
|
|
1354
|
+
meta: group.meta,
|
|
1355
|
+
items: group.items,
|
|
987
1356
|
closed: Boolean(closed[group.key]),
|
|
988
1357
|
t,
|
|
989
1358
|
today,
|
|
@@ -991,6 +1360,7 @@ function WorkTodoPanel(props) {
|
|
|
991
1360
|
onToggleTodo: handleToggle,
|
|
992
1361
|
onRemove: handleRemove,
|
|
993
1362
|
onSave: handleSave,
|
|
1363
|
+
drag: dragHandlers,
|
|
994
1364
|
})))
|
|
995
1365
|
: React.createElement('div', {
|
|
996
1366
|
key: 'empty',
|
|
@@ -1004,13 +1374,34 @@ function WorkTodoPanel(props) {
|
|
|
1004
1374
|
React.createElement('div', {
|
|
1005
1375
|
key: 'foot',
|
|
1006
1376
|
style: { marginTop: 14, paddingTop: 8, borderTop: `1px solid ${C.border1}`, fontSize: 10.5, color: C.label3, lineHeight: 1.7 },
|
|
1007
|
-
},
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
style: {
|
|
1012
|
-
},
|
|
1013
|
-
|
|
1377
|
+
}, [
|
|
1378
|
+
/* 角落信息:装了哪一版 + 有没有新版(一眼可见,不占主区) */
|
|
1379
|
+
React.createElement('div', {
|
|
1380
|
+
key: 'ver',
|
|
1381
|
+
style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', marginBottom: 2 },
|
|
1382
|
+
}, [
|
|
1383
|
+
React.createElement('span', {
|
|
1384
|
+
key: 'v',
|
|
1385
|
+
title: t('versionHint'),
|
|
1386
|
+
style: { color: C.label3 },
|
|
1387
|
+
}, `${t('versionLabel')} ${version ? `v${version}` : '—'}`),
|
|
1388
|
+
update?.hasUpdate
|
|
1389
|
+
? React.createElement('span', {
|
|
1390
|
+
key: 'u',
|
|
1391
|
+
className: 'wt-chip',
|
|
1392
|
+
style: { fontSize: 10, color: C.ok, borderColor: tint(C.ok, 50), background: tint(C.ok, 12) },
|
|
1393
|
+
title: `${t('updateHint')}:dsh plugin --profile desktop add dsh-worktodo@${update.latest}`,
|
|
1394
|
+
}, `${t('updateAvailable')} v${update.latest}`)
|
|
1395
|
+
: null,
|
|
1396
|
+
]),
|
|
1397
|
+
store && store.location
|
|
1398
|
+
? React.createElement('div', {
|
|
1399
|
+
key: 'store',
|
|
1400
|
+
title: `${t('storeAt')}:${store.location}`,
|
|
1401
|
+
style: { wordBreak: 'break-all' },
|
|
1402
|
+
}, `${t('storeAt')} · ${store.kind === 'domain' ? t('storeDomain') : t('storeFile')} · ${store.location}`)
|
|
1403
|
+
: null,
|
|
1404
|
+
]),
|
|
1014
1405
|
]);
|
|
1015
1406
|
}
|
|
1016
1407
|
|