@goodandready/dsh-kanban 0.2.2 → 0.2.8
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/README.md +24 -0
- package/{docs/README.ru.md → README.ru.md} +10 -0
- package/{docs/README.zh.md → README.zh.md} +21 -0
- package/lib/board-tool.js +151 -0
- package/lib/client.js +1020 -763
- package/lib/commands.js +6 -11
- package/lib/config.js +31 -29
- package/lib/events.js +2 -2
- package/lib/filters.js +2 -2
- package/lib/gitea.js +15 -0
- package/lib/index.js +171 -142
- package/lib/power.js +1 -1
- package/lib/progress-dump.js +3 -3
- package/lib/routes.js +403 -5
- package/lib/store.js +59 -9
- package/lib/sync.js +26 -3
- package/lib/task-service.js +56 -0
- package/lib/updater.js +338 -0
- package/lib/worktree.js +1 -1
- package/package.json +7 -3
- package/docs/design/DESIGN.md +0 -94
- package/docs/plans/2026-08-26-dsh-kanban-0.1.md +0 -169
- package/docs/plans/2026-09-07-quality-acceptance-gateway.md +0 -25
- package/docs/plans/2026-09-08-concurrency-safety.md +0 -8
- package/docs/plans/2026-09-08-diff-viewer-merge.md +0 -7
- package/docs/plans/2026-09-08-session-worktree-stability.md +0 -33
- package/docs/plans/2026-09-10-issue-238-settings-scope.md +0 -13
package/lib/client.js
CHANGED
|
@@ -13,12 +13,12 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
var module = { exports: {} }
|
|
14
14
|
var exports = module.exports
|
|
15
15
|
const React = require('react')
|
|
16
|
+
const h = React.createElement
|
|
16
17
|
const ReactDOM = require('react-dom/client')
|
|
17
18
|
|
|
18
19
|
const NS = 'dsh-kanban'
|
|
19
20
|
const COLUMN_ORDER = ['backlog', 'in-progress', 'review', 'deploy', 'cleanup', 'done']
|
|
20
21
|
|
|
21
|
-
// ---------------------------------------------------------- чистая часть
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Соседи для переноса карточки. Хранилище работает не с числовым индексом,
|
|
@@ -115,7 +115,7 @@ window.__ModuleLoader__.load({
|
|
|
115
115
|
function rememberLaunch(board, choice) {
|
|
116
116
|
try {
|
|
117
117
|
window.localStorage.setItem(RECALL_KEY + (board || 'main'), JSON.stringify(choice))
|
|
118
|
-
} catch { /*
|
|
118
|
+
} catch { /* storage unavailable */ }
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/**
|
|
@@ -141,7 +141,6 @@ window.__ModuleLoader__.load({
|
|
|
141
141
|
}
|
|
142
142
|
const out = {}
|
|
143
143
|
for (const group of bySession.values()) {
|
|
144
|
-
// Одна задача в сессии — не пачка: отметка «1 из 1» ничего не сообщает.
|
|
145
144
|
if (group.length < 2) continue
|
|
146
145
|
const order = group.slice().sort((a, b) => (a.queuedAt || 0) - (b.queuedAt || 0))
|
|
147
146
|
order.forEach((task, at) => { out[task.id] = { at: at + 1, total: order.length } })
|
|
@@ -191,17 +190,15 @@ window.__ModuleLoader__.load({
|
|
|
191
190
|
const href = typeof task.issueUrl === 'string' && /^https?:\/\//i.test(task.issueUrl)
|
|
192
191
|
? task.issueUrl
|
|
193
192
|
: ''
|
|
194
|
-
return
|
|
195
|
-
|
|
193
|
+
return h('span', { className: 'dkb-ref' },
|
|
194
|
+
h('span', { className: 'dkb-refRepo', title: task.repo }, task.repo),
|
|
196
195
|
number === ''
|
|
197
196
|
? null
|
|
198
197
|
: href === ''
|
|
199
|
-
?
|
|
200
|
-
:
|
|
198
|
+
? h('span', { className: 'dkb-refNum' }, number)
|
|
199
|
+
: h('a', {
|
|
201
200
|
className: 'dkb-refNum dkb-refLink', href, target: '_blank', rel: 'noreferrer noopener',
|
|
202
201
|
title: t ? t('panel.issue') : undefined,
|
|
203
|
-
// Щелчок по номеру — «открыть issue», а не «открыть карточку».
|
|
204
|
-
// Два разных намерения, и путать их нельзя.
|
|
205
202
|
onClick: (e) => e.stopPropagation(),
|
|
206
203
|
}, number),
|
|
207
204
|
)
|
|
@@ -224,9 +221,6 @@ window.__ModuleLoader__.load({
|
|
|
224
221
|
function planLine(task, t) {
|
|
225
222
|
const progress = task && task.plan && task.plan.progress
|
|
226
223
|
if (!progress || !progress.total) return ''
|
|
227
|
-
// На идущем пункте показываем ЕГО номер, а не число сделанных: «3 из 7 ·
|
|
228
|
-
// пишу тесты» читается как «идёт третий», и так оно и есть. Когда
|
|
229
|
-
// текущего пункта нет — план не начат или закончен, — показываем сделанные.
|
|
230
224
|
const shown = progress.text ? progress.current : progress.done
|
|
231
225
|
const counter = t('plan.counter', { done: shown, total: progress.total })
|
|
232
226
|
return progress.text ? counter + ' · ' + progress.text : counter
|
|
@@ -312,8 +306,6 @@ window.__ModuleLoader__.load({
|
|
|
312
306
|
if (state === 'failed') {
|
|
313
307
|
const where = (sync.problem && sync.problem.where) || ''
|
|
314
308
|
const why = (sync.problem && sync.problem.message) || ''
|
|
315
|
-
// Время ПОСЛЕДНЕГО УСПЕХА, а не последней попытки: вопрос человека —
|
|
316
|
-
// когда доска в последний раз видела правду.
|
|
317
309
|
const seen = sync.okAt
|
|
318
310
|
? t('sync.lastSeen', { ago: agoText(now, sync.okAt, t) })
|
|
319
311
|
: t('sync.neverSeen')
|
|
@@ -345,7 +337,6 @@ window.__ModuleLoader__.load({
|
|
|
345
337
|
const r = (n >> 16) & 255
|
|
346
338
|
const g = (n >> 8) & 255
|
|
347
339
|
const b = n & 255
|
|
348
|
-
// Коэффициенты воспринимаемой яркости: глаз чувствительнее к зелёному.
|
|
349
340
|
return (r * 299 + g * 587 + b * 114) / 1000 > 140 ? '#111' : '#fff'
|
|
350
341
|
}
|
|
351
342
|
|
|
@@ -383,11 +374,11 @@ window.__ModuleLoader__.load({
|
|
|
383
374
|
function renderSpans(spans, keyBase) {
|
|
384
375
|
return (spans || []).map((span, i) => {
|
|
385
376
|
const key = keyBase + '-' + i
|
|
386
|
-
if (span.kind === 'code') return
|
|
387
|
-
if (span.kind === 'strong') return
|
|
388
|
-
if (span.kind === 'em') return
|
|
377
|
+
if (span.kind === 'code') return h('code', { className: 'dkb-code', key }, span.text)
|
|
378
|
+
if (span.kind === 'strong') return h('strong', { key }, span.text)
|
|
379
|
+
if (span.kind === 'em') return h('em', { key }, span.text)
|
|
389
380
|
if (span.kind === 'link') {
|
|
390
|
-
return
|
|
381
|
+
return h('a', {
|
|
391
382
|
key, href: span.href, target: '_blank', rel: 'noreferrer noopener',
|
|
392
383
|
}, span.text)
|
|
393
384
|
}
|
|
@@ -399,26 +390,24 @@ window.__ModuleLoader__.load({
|
|
|
399
390
|
return (blocks || []).map((block, i) => {
|
|
400
391
|
const key = 'b' + i
|
|
401
392
|
if (block.kind === 'heading') {
|
|
402
|
-
|
|
403
|
-
// классом, а не h1..h6, у которых свой размер в оболочке.
|
|
404
|
-
return React.createElement('div', {
|
|
393
|
+
return h('div', {
|
|
405
394
|
key, className: 'dkb-mdHead dkb-mdHead' + Math.min(block.level, 3),
|
|
406
395
|
}, renderSpans(block.spans, key))
|
|
407
396
|
}
|
|
408
397
|
if (block.kind === 'code') {
|
|
409
|
-
return
|
|
398
|
+
return h('pre', { key, className: 'dkb-mdCode' }, block.text)
|
|
410
399
|
}
|
|
411
|
-
if (block.kind === 'rule') return
|
|
400
|
+
if (block.kind === 'rule') return h('hr', { key, className: 'dkb-mdRule' })
|
|
412
401
|
if (block.kind === 'quote') {
|
|
413
|
-
return
|
|
402
|
+
return h('blockquote', { key, className: 'dkb-mdQuote' },
|
|
414
403
|
renderSpans(block.spans, key))
|
|
415
404
|
}
|
|
416
405
|
if (block.kind === 'list') {
|
|
417
|
-
return
|
|
418
|
-
(block.items || []).map((item, j) =>
|
|
406
|
+
return h(block.ordered ? 'ol' : 'ul', { key, className: 'dkb-mdList' },
|
|
407
|
+
(block.items || []).map((item, j) => h('li', { key: key + '-' + j },
|
|
419
408
|
renderSpans(item, key + '-' + j))))
|
|
420
409
|
}
|
|
421
|
-
return
|
|
410
|
+
return h('p', { key, className: 'dkb-mdPara' }, renderSpans(block.spans, key))
|
|
422
411
|
})
|
|
423
412
|
}
|
|
424
413
|
|
|
@@ -467,17 +456,12 @@ window.__ModuleLoader__.load({
|
|
|
467
456
|
return {
|
|
468
457
|
board: (payload && payload.board) || 'main',
|
|
469
458
|
kind: (payload && payload.kind) || 'project',
|
|
470
|
-
// Своё «сейчас» на случай ответа без него: часы браузера и сервера
|
|
471
|
-
// расходятся, и лучше считать по одному из них, чем по нулю.
|
|
472
459
|
now: (payload && typeof payload.now === 'number') ? payload.now : Date.now(),
|
|
473
460
|
boards: payload && Array.isArray(payload.boards) ? payload.boards : [],
|
|
474
461
|
facets: payload && Array.isArray(payload.facets) ? payload.facets : [],
|
|
475
462
|
sync: (payload && payload.sync) || { state: 'never' },
|
|
476
463
|
columns,
|
|
477
464
|
tasks: payload && Array.isArray(payload.tasks) ? payload.tasks : [],
|
|
478
|
-
// Фактический корень проектов: {path, set} — путь и признак «задан
|
|
479
|
-
// настройкой». Сервер знает и настройку, и рабочую папку харнесса,
|
|
480
|
-
// браузеру ни то ни другое не доступно.
|
|
481
465
|
projectRoot: payload && typeof payload.projectRoot === 'object' && payload.projectRoot !== null
|
|
482
466
|
? { path: String(payload.projectRoot.path || ''), set: payload.projectRoot.set === true }
|
|
483
467
|
: { path: '', set: false },
|
|
@@ -625,11 +609,8 @@ window.__ModuleLoader__.load({
|
|
|
625
609
|
columnTitle, planLine, stateLabel, isCollapsed,
|
|
626
610
|
agoText, exactAt, shortDate, syncLine, readableOn, tagStyle, priorityColor, renderBlocks, renderSpans, matchesFilters, anySelected, toggleValue, normalizeBoard, errorKey,
|
|
627
611
|
chipSessionId, matchesQuery, splitFullName, repoOption, openSession, COLUMN_ORDER,
|
|
628
|
-
// createToggle подставляется ниже, когда объявлен: он нужен тестам, а
|
|
629
|
-
// объявление живёт рядом со встраиванием в оболочку.
|
|
630
612
|
}
|
|
631
613
|
|
|
632
|
-
// ------------------------------------------------------------------ сеть
|
|
633
614
|
|
|
634
615
|
async function api(path, options) {
|
|
635
616
|
const res = await fetch('/dsh-kanban' + path, Object.assign({
|
|
@@ -645,16 +626,102 @@ window.__ModuleLoader__.load({
|
|
|
645
626
|
return payload
|
|
646
627
|
}
|
|
647
628
|
|
|
648
|
-
|
|
629
|
+
|
|
630
|
+
function ensureCss() {
|
|
631
|
+
if (typeof document === 'undefined') return
|
|
632
|
+
if (document.getElementById('dsh-kanban-full-css')) return
|
|
633
|
+
const style = document.createElement('style')
|
|
634
|
+
style.id = 'dsh-kanban-full-css'
|
|
635
|
+
style.dataset.dshPlugin = NS
|
|
636
|
+
style.textContent = css
|
|
637
|
+
document.head.appendChild(style)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function createErrorBoundary() {
|
|
641
|
+
if (!React || typeof React.Component !== 'function') {
|
|
642
|
+
return function NoopBoundary(props) { return props?.children || null }
|
|
643
|
+
}
|
|
644
|
+
return class ErrorBoundary extends React.Component {
|
|
645
|
+
constructor(props) {
|
|
646
|
+
super(props)
|
|
647
|
+
this.state = { hasError: false, error: null }
|
|
648
|
+
}
|
|
649
|
+
static getDerivedStateFromError(error) {
|
|
650
|
+
return { hasError: true, error }
|
|
651
|
+
}
|
|
652
|
+
componentDidCatch(error, errorInfo) {
|
|
653
|
+
console.error('[dsh-kanban] React Error:', error, errorInfo)
|
|
654
|
+
}
|
|
655
|
+
render() {
|
|
656
|
+
if (this.state.hasError) {
|
|
657
|
+
return h(
|
|
658
|
+
'div',
|
|
659
|
+
{
|
|
660
|
+
style: {
|
|
661
|
+
margin: '12px 0', padding: '14px', borderRadius: '8px',
|
|
662
|
+
background: 'var(--dsw-alias-state-danger-bg, #ffebe9)',
|
|
663
|
+
border: '1px solid var(--dsw-alias-state-danger-border, #ffc1c0)',
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
h('div', {
|
|
667
|
+
style: { fontWeight: 600, marginBottom: '6px', color: 'var(--dsw-alias-state-danger-primary, #cf222e)' },
|
|
668
|
+
}, '⚠️ Kanban UI Error:'),
|
|
669
|
+
h('div', {
|
|
670
|
+
style: { fontSize: '12px', wordBreak: 'break-all', color: 'var(--dsw-alias-label-primary)' },
|
|
671
|
+
}, String(this.state.error?.message || this.state.error)),
|
|
672
|
+
h(
|
|
673
|
+
'button',
|
|
674
|
+
{
|
|
675
|
+
type: 'button',
|
|
676
|
+
className: 'dkb-discard',
|
|
677
|
+
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
678
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
679
|
+
},
|
|
680
|
+
'Retry'
|
|
681
|
+
)
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
return this.props?.children || null
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function refreshMirrorUntilVisible(ctx) {
|
|
690
|
+
const setTimer = typeof setInterval === 'function' ? setInterval : (typeof globalThis !== 'undefined' && typeof globalThis.setInterval === 'function' ? globalThis.setInterval : null)
|
|
691
|
+
const clearTimer = typeof clearInterval === 'function' ? clearInterval : (typeof globalThis !== 'undefined' && typeof globalThis.clearInterval === 'function' ? globalThis.clearInterval : null)
|
|
692
|
+
if (!setTimer || !clearTimer) return () => {}
|
|
693
|
+
const visible = () => {
|
|
694
|
+
try {
|
|
695
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
|
|
696
|
+
const view = s?.describe?.()?.getSnapshot?.()?.view
|
|
697
|
+
return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
|
|
698
|
+
} catch (_) {
|
|
699
|
+
return false
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (visible()) return () => {}
|
|
703
|
+
let tries = 0
|
|
704
|
+
const timer = setTimer(() => {
|
|
705
|
+
if (visible() || tries >= 15) { clearTimer(timer); return }
|
|
706
|
+
tries += 1
|
|
707
|
+
try {
|
|
708
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
|
|
709
|
+
s?.describe?.()?.load?.()
|
|
710
|
+
} catch (_) { /* clipboard read best-effort */ }
|
|
711
|
+
}, 1000)
|
|
712
|
+
return () => clearTimer(timer)
|
|
713
|
+
}
|
|
714
|
+
|
|
649
715
|
|
|
650
716
|
const css =
|
|
717
|
+
'.dkb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:14px 16px;display:flex;flex-direction:column;gap:8px;margin-bottom:12px}' +'.dkb-section-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px;padding-bottom:4px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +'.dkb-section-badge{font-size:11px;font-weight:500;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}' +'.dkb-section-badge-ok{border-color:var(--dsw-alias-state-success-border,#34d399);color:var(--dsw-alias-state-success-primary,#059669);background:var(--dsw-alias-state-success-bg,rgba(16,185,129,0.1))}' +
|
|
651
718
|
'.dkb-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
|
|
652
719
|
'.dkb-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}' +
|
|
653
720
|
'.dkb-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
|
|
654
721
|
'.dkb-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
|
|
655
722
|
'.dkb-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
656
723
|
'.dkb-sub{color:var(--dsw-alias-label-secondary);font-size:13px}' +
|
|
657
|
-
'.dkb-chevron{color:var(--dsw-alias-label-secondary);flex:none;transition:transform .16s}' +
|
|
724
|
+
'.dkb-chevron{color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;justify-content:center;flex:none;transition:transform .16s}' +
|
|
658
725
|
'.dkb-chevronOpen{transform:rotate(180deg)}' +
|
|
659
726
|
'.dkb-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}' +
|
|
660
727
|
'.dkb-group{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;padding:14px 0 2px}' +
|
|
@@ -672,27 +739,18 @@ window.__ModuleLoader__.load({
|
|
|
672
739
|
'.dkb-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 14px;font-size:13px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}' +
|
|
673
740
|
'.dkb-discard{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 14px;font-size:13px;color:var(--dsw-alias-label-secondary);background:transparent}' +
|
|
674
741
|
'.dkb-discard:disabled,.dkb-save:disabled{opacity:.4;cursor:default}' +
|
|
675
|
-
// Строка в боковой панели. Метрики и переменные взяты у оболочки, чтобы
|
|
676
|
-
// строка не выделялась среди родных: та же высота, те же состояния.
|
|
677
|
-
// Кнопка — клон родной, поэтому вид берётся у оболочки. Своего здесь
|
|
678
|
-
// ровно столько, чтобы разложить значок и подпись внутри клона.
|
|
679
742
|
'.dkb-entry-clone{align-items:center;gap:8px;display:flex}' +
|
|
680
743
|
'.dkb-entry-clone[data-active]{font-weight:600}' +
|
|
681
744
|
'.dkb-entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}' +
|
|
682
745
|
'.dkb-entryLabel{text-overflow:ellipsis;overflow:hidden}' +
|
|
683
746
|
'[data-dsh-frame][data-sidebar-collapsed] .dkb-entryLabel{display:none}' +
|
|
684
|
-
// Экран поверх колонки разговора. Показывается атрибутом на <html>, а
|
|
685
|
-
// соседи колонки прячутся — иначе доска легла бы поверх живого чата.
|
|
686
747
|
'[data-pane=conversation],[class*=centerCol]{position:relative}' +
|
|
687
748
|
'[data-dsh-kanban-view]{z-index:60;background:var(--dsw-alias-bg-base);display:none;position:absolute;inset:0}' +
|
|
688
749
|
'html[data-dsh-kanban-active]:not([data-dsh-ssh-active]) [data-dsh-kanban-view]{display:block}' +
|
|
689
750
|
'html[data-dsh-kanban-active]:not([data-dsh-ssh-active]) [data-pane=conversation]>:not([data-dsh-kanban-view]),' +
|
|
690
751
|
'html[data-dsh-kanban-active]:not([data-dsh-ssh-active]) [class*=centerCol]>:not([data-dsh-kanban-view]){display:none!important}' +
|
|
691
|
-
// Сама доска
|
|
692
752
|
'.dkb-screen{box-sizing:border-box;background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:12px;padding:14px 16px 56px;display:flex}' +
|
|
693
753
|
'.dkb-bar{flex:none;align-items:center;gap:8px;display:flex;flex-wrap:wrap}' +
|
|
694
|
-
// Всё управление в шапке одной высоты: пять разных высот подряд читаются
|
|
695
|
-
// как ряд, собранный из чужих деталей.
|
|
696
754
|
'.dkb-bar .dkb-input,.dkb-bar .dkb-search,.dkb-bar .dkb-save,.dkb-bar .dkb-discard,.dkb-bar .dkb-facetHead,.dkb-bar .dkb-back{height:30px;box-sizing:border-box;padding-top:0;padding-bottom:0;border-radius:8px;font-size:13px;line-height:28px;display:inline-flex;align-items:center}' +
|
|
697
755
|
'.dkb-bar .dkb-facet{display:inline-flex}' +
|
|
698
756
|
'.dkb-back{appearance:none;font:inherit;cursor:pointer;color:var(--dsw-alias-label-secondary);background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;align-items:center;gap:6px;padding:4px 10px;font-size:13px;display:flex;flex:none}' +
|
|
@@ -733,7 +791,7 @@ window.__ModuleLoader__.load({
|
|
|
733
791
|
'.dkb-cols{overscroll-behavior-inline:contain;scrollbar-color:var(--dsw-alias-border-l3) var(--dsw-alias-interactive-bg-hover);scrollbar-width:thin;flex:1;gap:12px;min-height:0;padding-bottom:6px;display:flex;overflow:auto hidden}' +
|
|
734
792
|
'.dkb-cols::-webkit-scrollbar{height:10px}' +
|
|
735
793
|
'.dkb-cols::-webkit-scrollbar-track{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px}' +
|
|
736
|
-
'.dkb-cols::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l3);background-clip:content-box;border:2px solid
|
|
794
|
+
'.dkb-cols::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l3);background-clip:content-box;border:2px solid transparent;border-radius:999px}' +
|
|
737
795
|
'.dkb-col{flex:1 1 220px;min-width:220px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;flex-direction:column;min-height:0;display:flex;overflow:hidden}' +
|
|
738
796
|
'.dkb-colOver{border-color:var(--dsw-alias-border-l3)}' +
|
|
739
797
|
'.dkb-colHead{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;flex:none;align-items:center;gap:6px;padding:10px 12px;display:flex}' +
|
|
@@ -755,7 +813,7 @@ window.__ModuleLoader__.load({
|
|
|
755
813
|
'.dkb-count[data-col=review]{background:var(--dsw-alias-state-business-primary)}' +
|
|
756
814
|
'.dkb-count[data-col=deploy]{background:var(--dsw-alias-state-business-primary)}' +
|
|
757
815
|
'.dkb-count[data-col=done]{background:var(--dsw-alias-state-success-primary)}' +
|
|
758
|
-
'.dkb-countOver{background:var(--dsw-alias-state-error-primary);color
|
|
816
|
+
'.dkb-countOver{background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-inverse,#fff)}' +
|
|
759
817
|
'.dkb-overdue{border-color:var(--dsw-alias-state-error-primary)}' +
|
|
760
818
|
'.dkb-dueOver{color:var(--dsw-alias-state-error-primary);font-weight:600}' +
|
|
761
819
|
'.dkb-list{flex-direction:column;flex:1;gap:8px;min-height:0;padding:2px 8px 10px;display:flex;overflow-y:auto}' +
|
|
@@ -797,14 +855,10 @@ window.__ModuleLoader__.load({
|
|
|
797
855
|
'.dkb-planItem.dkb-planActive{color:var(--dsw-alias-label-primary);font-weight:600;text-decoration:none}' +
|
|
798
856
|
'.dkb-planMark{flex:none;width:14px;text-align:center}' +
|
|
799
857
|
'.dkb-tags{display:flex;flex-wrap:wrap;gap:4px}' +
|
|
800
|
-
// Ожидание — не колонка, а состояние карточки: агент стоит и ждёт человека.
|
|
801
858
|
'.dkb-waiting{color:var(--dsw-alias-state-warn-primary);font-weight:600}' +
|
|
802
859
|
'.dkb-waitDot{border-radius:50%;flex:none;width:8px;height:8px;background:var(--dsw-alias-state-warn-primary);display:inline-block}' +
|
|
803
860
|
'.dkb-tag{white-space:nowrap;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);border:1px solid transparent;border-radius:999px;padding:1px 7px;font-size:11px;line-height:1.5;font-weight:500}' +
|
|
804
|
-
|
|
805
|
-
// плагинов (например счётчик расходов), и без запаса последняя кнопка
|
|
806
|
-
// оказывается под ними — нажать нельзя, а выглядит как «ничего не работает».
|
|
807
|
-
'.dkb-modal{position:absolute;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;padding:24px 24px 84px;box-sizing:border-box;z-index:70}' +
|
|
861
|
+
'.dkb-modal{position:absolute;inset:0;background:var(--dsw-alias-mask-bg,rgba(0,0,0,.4));display:flex;align-items:center;justify-content:center;padding:24px 24px 84px;box-sizing:border-box;z-index:70}' +
|
|
808
862
|
'.dkb-panel{width:min(720px,100%);max-height:100%;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px;box-sizing:border-box;overflow-y:auto;display:flex;flex-direction:column;gap:12px}' +
|
|
809
863
|
'@media (min-width: 960px){.dkb-panelWide{width:min(1100px,96%)}.dkb-drawerGrid{display:grid;grid-template-columns:300px 1fr;gap:20px;align-items:start}}' +
|
|
810
864
|
'.dkb-drawerSidebar{display:flex;flex-direction:column;gap:12px;border-right:1px solid var(--dsw-alias-border-l2);padding-right:16px}' +
|
|
@@ -812,31 +866,40 @@ window.__ModuleLoader__.load({
|
|
|
812
866
|
'.dkb-panelBody{white-space:pre-wrap;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5;margin:0}' +
|
|
813
867
|
'.dkb-log{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}' +
|
|
814
868
|
'.dkb-logRow{color:var(--dsw-alias-label-secondary);font-size:12px}' +
|
|
815
|
-
'.dkb-dialog{position:absolute;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:80}' +
|
|
869
|
+
'.dkb-dialog{position:absolute;inset:0;background:var(--dsw-alias-mask-bg,rgba(0,0,0,.4));display:flex;align-items:center;justify-content:center;z-index:80}' +
|
|
816
870
|
'.dkb-dialogBox{width:min(560px,94%);max-height:86%;overflow-y:auto;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:10px}' +
|
|
817
871
|
'.dkb-issue{appearance:none;font:inherit;text-align:left;cursor:pointer;background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 10px;color:var(--dsw-alias-label-primary);font-size:13px}' +
|
|
818
872
|
'.dkb-issueUsed{opacity:.5}' +
|
|
819
873
|
'.dkb-row{display:flex;gap:8px;align-items:center}' +
|
|
820
874
|
'.dkb-search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 240px;padding:6px 10px;font-size:13px}' +
|
|
821
875
|
'.dkb-search::placeholder{color:var(--dsw-alias-label-tertiary)}' +
|
|
822
|
-
// Указатель места вставки: без него карточку отпускают вслепую.
|
|
823
876
|
'.dkb-slot{height:2px;border-radius:2px;background:transparent;flex:none}' +
|
|
824
877
|
'.dkb-slotOn{background:var(--dsw-alias-state-business-primary)}' +
|
|
825
878
|
'.dkb-danger{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;padding:5px 14px;font-size:13px;color:var(--dsw-alias-state-error-primary);background:transparent}' +
|
|
826
|
-
'.dkb-danger:hover{background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-bg-base)}'
|
|
879
|
+
'.dkb-danger:hover{background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-bg-base)}' +
|
|
880
|
+
'.dkb-quickFilterBar{display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 0}' +
|
|
881
|
+
'.dkb-filterChip{appearance:none;font:inherit;cursor:pointer;background:var(--dsw-alias-interactive-bg-hover);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:3px 10px;font-size:12px;color:var(--dsw-alias-label-secondary);transition:all .15s}' +
|
|
882
|
+
'.dkb-filterChip:hover{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}' +
|
|
883
|
+
'.dkb-filterChipActive{background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-inverse,#fff);border-color:transparent;font-weight:600}' +
|
|
884
|
+
'.dkb-epicTag{background:var(--dsw-alias-state-info-bg,#ddf4ff);color:var(--dsw-alias-state-info-primary,#0969da);font-weight:600}' +
|
|
885
|
+
'.dkb-progressBar{height:4px;background:var(--dsw-alias-border-l2);border-radius:2px;overflow:hidden;margin:4px 0}' +
|
|
886
|
+
'.dkb-progressFill{height:100%;background:var(--dsw-alias-state-success-primary,#16a34a);transition:width .2s}' +
|
|
887
|
+
'.dkb-consolePre{background:var(--dsw-alias-bg-layer-1,#0d1117);color:var(--dsw-alias-state-info-primary,#58a6ff);border:1px solid var(--dsw-alias-border-l2);padding:12px;border-radius:8px;font-family:monospace;font-size:11px;max-height:260px;overflow-y:auto;white-space:pre-wrap}'
|
|
827
888
|
|
|
828
889
|
|
|
829
|
-
// ---------------------------------------------------------------- строки
|
|
830
890
|
|
|
831
|
-
// Правило регистра, чтобы подписи не расползались:
|
|
832
|
-
//
|
|
833
|
-
// подпись УПРАВЛЕНИЯ — с прописной: «Архив», «Поиск», «Обновить». Это
|
|
834
|
-
// имя кнопки, а не продолжение фразы;
|
|
835
|
-
// встроенный текст и состояние — со строчной: «ждёт ответа», «здесь
|
|
836
|
-
// 3 дн», «показано 12 из 81». Это часть предложения, а не ярлык.
|
|
837
|
-
//
|
|
838
|
-
// Смешение того и другого в одном ряду и выглядит халтурой.
|
|
839
891
|
const en = {
|
|
892
|
+
'updater.title': 'Plugin Updates',
|
|
893
|
+
'updater.check': 'Check updates',
|
|
894
|
+
'updater.checking': 'Checking…',
|
|
895
|
+
'updater.current': 'Current version',
|
|
896
|
+
'updater.latest': 'Latest version',
|
|
897
|
+
'updater.upToDate': 'Up to date',
|
|
898
|
+
'updater.updateNow': 'Update to v{version}',
|
|
899
|
+
'updater.updating': 'Updating…',
|
|
900
|
+
'updater.success': 'Plugin updated successfully! Please restart DSH service.',
|
|
901
|
+
'updater.failed': 'Update check failed: ',
|
|
902
|
+
|
|
840
903
|
'title': 'Kanban',
|
|
841
904
|
'subtitle': 'Task board: Gitea issues and a session per task',
|
|
842
905
|
'section.label': 'Kanban',
|
|
@@ -1176,16 +1239,45 @@ window.__ModuleLoader__.load({
|
|
|
1176
1239
|
'comments.placeholder': 'Add a note or feedback...',
|
|
1177
1240
|
'comments.authorUser': 'User',
|
|
1178
1241
|
'comments.authorAgent': 'Agent',
|
|
1242
|
+
'epic.badge': 'Epic',
|
|
1243
|
+
'epic.progress': '{done}/{total} subtasks',
|
|
1244
|
+
'epic.triage': 'Triage / Decompose',
|
|
1245
|
+
'epic.triageTitle': 'Decompose Task into Subtasks',
|
|
1246
|
+
'epic.triageHelp': 'Enter one subtask title per line. Child tasks will be linked to this Epic.',
|
|
1247
|
+
'epic.triageSubmit': 'Create Subtasks',
|
|
1248
|
+
'epic.createGiteaIssues': 'Also create linked Gitea issues',
|
|
1249
|
+
'epic.subtasks': 'Subtasks',
|
|
1250
|
+
'epic.addSubtask': 'Add subtask',
|
|
1251
|
+
'epic.noSubtasks': 'No subtasks yet.',
|
|
1252
|
+
'console.tab': 'Live Console',
|
|
1253
|
+
'console.empty': 'No agent output recorded yet.',
|
|
1254
|
+
'filter.all': 'All',
|
|
1255
|
+
'filter.active': 'Active',
|
|
1256
|
+
'filter.review': 'Review',
|
|
1257
|
+
'filter.epics': 'Epics',
|
|
1258
|
+
'filter.stale': 'Silent',
|
|
1259
|
+
'pr.create': 'Create Gitea PR',
|
|
1260
|
+
'pr.creating': 'Creating PR...',
|
|
1261
|
+
'pr.created': 'PR #{n} created',
|
|
1262
|
+
'pr.view': 'View PR',
|
|
1179
1263
|
'error.issueNotFound': 'Issue not found.',
|
|
1180
1264
|
'error.titleRequired': 'A title is required.',
|
|
1181
1265
|
'error.crossSite': 'Request rejected as cross-site.',
|
|
1182
1266
|
'error.unknown': 'Request failed.',
|
|
1183
1267
|
}
|
|
1184
1268
|
|
|
1185
|
-
// Китайский — вторая встроенная локаль ядра. Русского здесь нет:
|
|
1186
|
-
// он приезжает языковым пакетом `dsh-russian-lang`, который
|
|
1187
|
-
// регистрирует `ru` для этого пространства извне.
|
|
1188
1269
|
const zh = {
|
|
1270
|
+
'updater.title': '插件更新',
|
|
1271
|
+
'updater.check': '检查更新',
|
|
1272
|
+
'updater.checking': '正在检查…',
|
|
1273
|
+
'updater.current': '当前版本',
|
|
1274
|
+
'updater.latest': '最新版本',
|
|
1275
|
+
'updater.upToDate': '已是最新',
|
|
1276
|
+
'updater.updateNow': '一键更新至 v{version}',
|
|
1277
|
+
'updater.updating': '正在更新…',
|
|
1278
|
+
'updater.success': '插件更新成功!请重启 DSH 服务以生效。',
|
|
1279
|
+
'updater.failed': '更新检查失败:',
|
|
1280
|
+
|
|
1189
1281
|
'title': '看板',
|
|
1190
1282
|
'subtitle': '任务看板:Gitea issue,每个任务一个会话',
|
|
1191
1283
|
'section.label': '看板',
|
|
@@ -1525,6 +1617,27 @@ window.__ModuleLoader__.load({
|
|
|
1525
1617
|
'comments.placeholder': '添加说明或反馈...',
|
|
1526
1618
|
'comments.authorUser': '用户',
|
|
1527
1619
|
'comments.authorAgent': 'Agent',
|
|
1620
|
+
'epic.badge': '史诗',
|
|
1621
|
+
'epic.progress': '{done}/{total} 子任务',
|
|
1622
|
+
'epic.triage': '分解子任务',
|
|
1623
|
+
'epic.triageTitle': '将任务分解为子任务',
|
|
1624
|
+
'epic.triageHelp': '每行输入一个子任务标题。子任务将关联到此史诗。',
|
|
1625
|
+
'epic.triageSubmit': '创建子任务',
|
|
1626
|
+
'epic.createGiteaIssues': '同时在 Gitea 中创建关联 issue',
|
|
1627
|
+
'epic.subtasks': '子任务',
|
|
1628
|
+
'epic.addSubtask': '添加子任务',
|
|
1629
|
+
'epic.noSubtasks': '暂无子任务。',
|
|
1630
|
+
'console.tab': '实时终端',
|
|
1631
|
+
'console.empty': '暂无 Agent 活动输出。',
|
|
1632
|
+
'filter.all': '全部',
|
|
1633
|
+
'filter.active': '进行中',
|
|
1634
|
+
'filter.review': '待复核',
|
|
1635
|
+
'filter.epics': '史诗',
|
|
1636
|
+
'filter.stale': '沉默',
|
|
1637
|
+
'pr.create': '创建 Gitea PR',
|
|
1638
|
+
'pr.creating': '正在创建 PR...',
|
|
1639
|
+
'pr.created': '已创建 PR #{n}',
|
|
1640
|
+
'pr.view': '查看 PR',
|
|
1528
1641
|
'error.issueNotFound': '没有找到 issue。',
|
|
1529
1642
|
'error.titleRequired': '标题不能为空。',
|
|
1530
1643
|
'error.crossSite': '请求被判定为跨站而拒绝。',
|
|
@@ -1532,8 +1645,6 @@ window.__ModuleLoader__.load({
|
|
|
1532
1645
|
}
|
|
1533
1646
|
|
|
1534
1647
|
function fallbackT(key, vars) {
|
|
1535
|
-
// Запасной перевод — английский: русский приезжает языковым пакетом,
|
|
1536
|
-
// а до службы локализации доска обязана говорить хоть на чём-то.
|
|
1537
1648
|
let text = en[key] !== undefined ? en[key] : key
|
|
1538
1649
|
if (vars) {
|
|
1539
1650
|
for (const name of Object.keys(vars)) {
|
|
@@ -1543,7 +1654,6 @@ window.__ModuleLoader__.load({
|
|
|
1543
1654
|
return text
|
|
1544
1655
|
}
|
|
1545
1656
|
|
|
1546
|
-
// ------------------------------------------------------- карточка настроек
|
|
1547
1657
|
|
|
1548
1658
|
const FIELDS = [
|
|
1549
1659
|
{ field: 'giteaUrl', kind: 'text', group: 'gitea' },
|
|
@@ -1583,19 +1693,127 @@ window.__ModuleLoader__.load({
|
|
|
1583
1693
|
return 'hint.' + spec.field
|
|
1584
1694
|
}
|
|
1585
1695
|
|
|
1696
|
+
let ChevronIcon = null
|
|
1697
|
+
try {
|
|
1698
|
+
const primitives = typeof require === 'function' ? require('@deepseek-ai/dsh-client-ui-primitives') : null
|
|
1699
|
+
ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDown)
|
|
1700
|
+
} catch (_) {
|
|
1701
|
+
ChevronIcon = null
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function FallbackChevron(props) {
|
|
1705
|
+
return h('svg', {
|
|
1706
|
+
width: 14,
|
|
1707
|
+
height: 14,
|
|
1708
|
+
viewBox: '0 0 14 14',
|
|
1709
|
+
fill: 'none',
|
|
1710
|
+
'aria-hidden': 'true',
|
|
1711
|
+
}, h('path', {
|
|
1712
|
+
d: 'M3.5 5.25L7 8.75L10.5 5.25',
|
|
1713
|
+
stroke: 'currentColor',
|
|
1714
|
+
strokeWidth: '1.5',
|
|
1715
|
+
strokeLinecap: 'round',
|
|
1716
|
+
strokeLinejoin: 'round',
|
|
1717
|
+
}))
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1586
1720
|
function Chevron(props) {
|
|
1587
|
-
|
|
1721
|
+
const Icon = ChevronIcon || FallbackChevron
|
|
1722
|
+
return h('span', {
|
|
1588
1723
|
className: 'dkb-chevron' + (props.open ? ' dkb-chevronOpen' : ''),
|
|
1589
|
-
|
|
1590
|
-
},
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1724
|
+
'aria-hidden': 'true',
|
|
1725
|
+
}, h(Icon))
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
function PluginUpdaterSection(props) {
|
|
1730
|
+
const t = props.t || fallbackT
|
|
1731
|
+
const [status, setStatus] = React.useState(null)
|
|
1732
|
+
const [loading, setLoading] = React.useState(false)
|
|
1733
|
+
const [updating, setUpdating] = React.useState(false)
|
|
1734
|
+
const [msg, setMsg] = React.useState(null)
|
|
1735
|
+
|
|
1736
|
+
const checkUpdate = React.useCallback(async () => {
|
|
1737
|
+
setLoading(true)
|
|
1738
|
+
setMsg(null)
|
|
1739
|
+
try {
|
|
1740
|
+
const res = await fetch('/api/dsh-kanban/update', {
|
|
1741
|
+
headers: { accept: 'application/json' },
|
|
1742
|
+
cache: 'no-store',
|
|
1743
|
+
})
|
|
1744
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
1745
|
+
const data = await res.json()
|
|
1746
|
+
setStatus(data)
|
|
1747
|
+
} catch (e) {
|
|
1748
|
+
setMsg({ ok: false, text: (t('updater.failed') || 'Update check failed: ') + (e?.message || String(e)) })
|
|
1749
|
+
} finally {
|
|
1750
|
+
setLoading(false)
|
|
1751
|
+
}
|
|
1752
|
+
}, [t])
|
|
1753
|
+
|
|
1754
|
+
const onUpdateNow = async () => {
|
|
1755
|
+
setUpdating(true)
|
|
1756
|
+
setMsg(null)
|
|
1757
|
+
try {
|
|
1758
|
+
const res = await fetch('/api/dsh-kanban/update', {
|
|
1759
|
+
method: 'POST',
|
|
1760
|
+
headers: {
|
|
1761
|
+
'x-dsh-plugin-update': '1',
|
|
1762
|
+
'content-type': 'application/json',
|
|
1763
|
+
},
|
|
1764
|
+
})
|
|
1765
|
+
const data = await res.json()
|
|
1766
|
+
if (!res.ok) throw new Error(data?.error || 'HTTP ' + res.status)
|
|
1767
|
+
setStatus(data)
|
|
1768
|
+
setMsg({ ok: true, text: t('updater.success') || 'Plugin updated successfully! Please restart DSH service.' })
|
|
1769
|
+
} catch (e) {
|
|
1770
|
+
setMsg({ ok: false, text: e?.message || String(e) })
|
|
1771
|
+
} finally {
|
|
1772
|
+
setUpdating(false)
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
React.useEffect(() => {
|
|
1777
|
+
checkUpdate()
|
|
1778
|
+
}, [checkUpdate])
|
|
1779
|
+
|
|
1780
|
+
const currentVer = status?.currentVersion || '0.2.7'
|
|
1781
|
+
const latestVer = status?.latestVersion
|
|
1782
|
+
const updateAvailable = Boolean(status?.updateAvailable && latestVer && latestVer !== currentVer)
|
|
1783
|
+
|
|
1784
|
+
return h('div', { className: 'dkb-section-card', key: 'g-updater' },
|
|
1785
|
+
h('div', { className: 'dkb-section-title' },
|
|
1786
|
+
h('span', null, '🔄 ' + t('updater.title')),
|
|
1787
|
+
h('span', { className: 'dkb-section-badge' + (!updateAvailable ? ' dkb-section-badge-ok' : '') },
|
|
1788
|
+
updateAvailable ? 'v' + latestVer + ' available' : 'v' + currentVer + ' (' + t('updater.upToDate') + ')'
|
|
1789
|
+
),
|
|
1790
|
+
),
|
|
1791
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '8px', marginTop: '6px' } },
|
|
1792
|
+
h('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
1793
|
+
updateAvailable ? (t('updater.latest') + ': v' + latestVer) : (t('updater.current') + ': v' + currentVer)
|
|
1794
|
+
),
|
|
1795
|
+
h('div', { style: { display: 'flex', gap: '8px' } },
|
|
1796
|
+
updateAvailable ? h('button', {
|
|
1797
|
+
type: 'button',
|
|
1798
|
+
className: 'dkb-save',
|
|
1799
|
+
style: { padding: '4px 10px', fontSize: '12px' },
|
|
1800
|
+
onClick: onUpdateNow,
|
|
1801
|
+
disabled: updating || loading,
|
|
1802
|
+
}, updating ? (t('updater.updating') || 'Updating…') : (t('updater.updateNow') || 'Update now').replace('{version}', latestVer)) : null,
|
|
1803
|
+
h('button', {
|
|
1804
|
+
type: 'button',
|
|
1805
|
+
className: 'dkb-discard',
|
|
1806
|
+
style: { padding: '4px 10px', fontSize: '12px' },
|
|
1807
|
+
onClick: checkUpdate,
|
|
1808
|
+
disabled: loading || updating,
|
|
1809
|
+
}, loading ? (t('updater.checking') || 'Checking…') : (t('updater.check') || 'Check updates')),
|
|
1810
|
+
),
|
|
1811
|
+
),
|
|
1812
|
+
msg ? h('p', { className: msg.ok ? 'dkb-note' : 'dkb-failed', style: { margin: '4px 0 0 0', fontSize: '12px' } }, msg.text) : null,
|
|
1813
|
+
)
|
|
1594
1814
|
}
|
|
1595
1815
|
|
|
1596
1816
|
function KanbanSettingsCard(props) {
|
|
1597
|
-
// Все хуки объявлены выше любого возврата: ранний возврат над хуком даёт
|
|
1598
|
-
// React error 310 при первом же изменении состояния.
|
|
1599
1817
|
const ctx = props.ctx
|
|
1600
1818
|
const t = props.t || fallbackT
|
|
1601
1819
|
const [open, setOpen] = React.useState(false)
|
|
@@ -1605,7 +1823,10 @@ window.__ModuleLoader__.load({
|
|
|
1605
1823
|
const [saved, setSaved] = React.useState(false)
|
|
1606
1824
|
|
|
1607
1825
|
const scope = React.useMemo(
|
|
1608
|
-
() =>
|
|
1826
|
+
() => {
|
|
1827
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
|
|
1828
|
+
return s ? s.bind({ namespace: NS }) : undefined
|
|
1829
|
+
},
|
|
1609
1830
|
[ctx],
|
|
1610
1831
|
)
|
|
1611
1832
|
const snapshot = React.useSyncExternalStore(
|
|
@@ -1633,9 +1854,6 @@ window.__ModuleLoader__.load({
|
|
|
1633
1854
|
const save = React.useCallback(async () => {
|
|
1634
1855
|
if (!scope || !draft) return
|
|
1635
1856
|
setSaving(true); setFailed(''); setSaved(false)
|
|
1636
|
-
// Пишем ВСЕ поля, а не до первой ошибки: обрыв на первой неудаче
|
|
1637
|
-
// оставил бы остальные поля незаписанными, а снаружи это выглядит как
|
|
1638
|
-
// «кнопка не работает».
|
|
1639
1857
|
const broken = []
|
|
1640
1858
|
for (const spec of FIELDS) {
|
|
1641
1859
|
if (!draft || !Object.prototype.hasOwnProperty.call(draft, spec.field)) continue
|
|
@@ -1657,82 +1875,109 @@ window.__ModuleLoader__.load({
|
|
|
1657
1875
|
setDraft(null); setSaved(true)
|
|
1658
1876
|
}, [scope, draft, t])
|
|
1659
1877
|
|
|
1660
|
-
const head =
|
|
1878
|
+
const head = h('button', {
|
|
1661
1879
|
type: 'button', className: 'dkb-head', onClick: () => setOpen((v) => !v),
|
|
1662
1880
|
},
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1881
|
+
h('span', { className: 'dkb-headText' },
|
|
1882
|
+
h('span', { className: 'dkb-title' }, t('title')),
|
|
1883
|
+
h('span', { className: 'dkb-sub' }, t('subtitle')),
|
|
1666
1884
|
),
|
|
1667
|
-
|
|
1885
|
+
h(Chevron, { open }),
|
|
1668
1886
|
)
|
|
1669
|
-
|
|
1887
|
+
ensureCss()
|
|
1888
|
+
const style = null
|
|
1670
1889
|
|
|
1671
|
-
if (!open) return
|
|
1890
|
+
if (!open) return h('li', { className: 'dkb-card' }, head)
|
|
1672
1891
|
|
|
1673
1892
|
let body
|
|
1674
1893
|
if (status === 'loading') {
|
|
1675
|
-
body =
|
|
1894
|
+
body = h('p', { className: 'dkb-note', role: 'status' }, t('settings.loading'))
|
|
1676
1895
|
} else if (status !== 'ready') {
|
|
1677
|
-
|
|
1678
|
-
// остаётся истинным, и карточка нарисовала бы пустую, но с виду
|
|
1679
|
-
// рабочую форму.
|
|
1680
|
-
body = React.createElement('p', { className: 'dkb-failed', role: 'status' }, t('settings.unavailable'))
|
|
1896
|
+
body = h('p', { className: 'dkb-failed', role: 'status' }, t('settings.unavailable'))
|
|
1681
1897
|
} else {
|
|
1682
1898
|
const writable = !snapshot || snapshot.writable !== false
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1899
|
+
const groups = []
|
|
1900
|
+
const groupMap = {}
|
|
1685
1901
|
for (const spec of FIELDS) {
|
|
1686
|
-
if (spec.group
|
|
1687
|
-
|
|
1688
|
-
|
|
1902
|
+
if (!groupMap[spec.group]) {
|
|
1903
|
+
groupMap[spec.group] = []
|
|
1904
|
+
groups.push(spec.group)
|
|
1689
1905
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1906
|
+
groupMap[spec.group].push(spec)
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
const rows = []
|
|
1910
|
+
for (const grp of groups) {
|
|
1911
|
+
const fieldsInGroup = groupMap[grp]
|
|
1912
|
+
const fieldElements = []
|
|
1913
|
+
for (const spec of fieldsInGroup) {
|
|
1914
|
+
if (spec.kind === 'bool') {
|
|
1915
|
+
fieldElements.push(h('div', { className: 'dkb-field', key: spec.field },
|
|
1916
|
+
h('label', { className: 'dkb-row' },
|
|
1917
|
+
h('input', {
|
|
1918
|
+
type: 'checkbox',
|
|
1919
|
+
checked: String(valueOf(spec.field)) === 'true',
|
|
1920
|
+
disabled: !writable || saving,
|
|
1921
|
+
onChange: (e) => edit(spec.field, e.target.checked ? 'true' : 'false'),
|
|
1922
|
+
}),
|
|
1923
|
+
h('span', { className: 'dkb-label' }, t('field.' + spec.field)),
|
|
1924
|
+
),
|
|
1925
|
+
h('p', { className: 'dkb-hint' }, t(hintKey(spec))),
|
|
1926
|
+
))
|
|
1927
|
+
continue
|
|
1928
|
+
}
|
|
1929
|
+
const common = {
|
|
1930
|
+
className: spec.kind === 'area' ? 'dkb-area' : 'dkb-input',
|
|
1931
|
+
value: valueOf(spec.field),
|
|
1932
|
+
disabled: !writable || saving,
|
|
1933
|
+
onChange: (e) => edit(spec.field, e.target.value),
|
|
1934
|
+
}
|
|
1935
|
+
if (spec.kind === 'number') common.type = 'number'
|
|
1936
|
+
fieldElements.push(h('div', { className: 'dkb-field', key: spec.field },
|
|
1937
|
+
h('span', { className: 'dkb-label' }, t('field.' + spec.field)),
|
|
1938
|
+
h(spec.kind === 'area' ? 'textarea' : 'input', common),
|
|
1939
|
+
h('p', { className: 'dkb-hint' }, t(hintKey(spec))),
|
|
1704
1940
|
))
|
|
1705
|
-
continue
|
|
1706
1941
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1942
|
+
|
|
1943
|
+
let badge = null
|
|
1944
|
+
if (grp === 'gitea') {
|
|
1945
|
+
const hasUrl = Boolean(valueOf('giteaUrl'))
|
|
1946
|
+
badge = h('span', {
|
|
1947
|
+
className: 'dkb-section-badge' + (hasUrl ? ' dkb-section-badge-ok' : ''),
|
|
1948
|
+
}, hasUrl ? 'Gitea Configured' : 'No URL')
|
|
1949
|
+
} else if (grp === 'sync') {
|
|
1950
|
+
const interval = Number(valueOf('syncIntervalSec')) || 0
|
|
1951
|
+
badge = h('span', {
|
|
1952
|
+
className: 'dkb-section-badge' + (interval > 0 ? ' dkb-section-badge-ok' : ''),
|
|
1953
|
+
}, interval > 0 ? `${interval}s` : 'Manual')
|
|
1712
1954
|
}
|
|
1713
|
-
|
|
1714
|
-
rows.push(
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1955
|
+
|
|
1956
|
+
rows.push(h('div', { className: 'dkb-section-card', key: 'g-' + grp },
|
|
1957
|
+
h('div', { className: 'dkb-section-title' },
|
|
1958
|
+
h('span', null, t('group.' + grp)),
|
|
1959
|
+
badge,
|
|
1960
|
+
),
|
|
1961
|
+
...fieldElements,
|
|
1718
1962
|
))
|
|
1719
1963
|
}
|
|
1720
1964
|
const dirty = draft !== null && Object.keys(draft).length > 0
|
|
1721
1965
|
let note
|
|
1722
|
-
if (failed) note =
|
|
1723
|
-
else if (!writable) note =
|
|
1724
|
-
else if (saved) note =
|
|
1725
|
-
else note =
|
|
1726
|
-
rows.push(
|
|
1966
|
+
if (failed) note = h('p', { className: 'dkb-failed', role: 'status' }, failed)
|
|
1967
|
+
else if (!writable) note = h('p', { className: 'dkb-note', role: 'status' }, t('settings.readOnly'))
|
|
1968
|
+
else if (saved) note = h('p', { className: 'dkb-note', role: 'status' }, t('settings.saved'))
|
|
1969
|
+
else note = h('p', { className: 'dkb-note' })
|
|
1970
|
+
rows.push(h(PluginUpdaterSection, { t, key: 'updater' }))
|
|
1971
|
+
rows.push(h('div', { className: 'dkb-foot', key: 'foot' },
|
|
1727
1972
|
note,
|
|
1728
|
-
|
|
1729
|
-
|
|
1973
|
+
h('button', { type: 'button', className: 'dkb-discard', disabled: !dirty || saving, onClick: discard }, t('discard')),
|
|
1974
|
+
h('button', { type: 'button', className: 'dkb-save', disabled: !dirty || saving || !writable, onClick: save }, t('save')),
|
|
1730
1975
|
))
|
|
1731
1976
|
body = rows
|
|
1732
1977
|
}
|
|
1733
1978
|
|
|
1734
|
-
return
|
|
1735
|
-
|
|
1979
|
+
return h('li', { className: 'dkb-card dkb-cardOpen' },
|
|
1980
|
+
head, h('div', { className: 'dkb-body' }, body))
|
|
1736
1981
|
}
|
|
1737
1982
|
|
|
1738
1983
|
/** Стрелка влево для кнопки возврата: 14x14, обводка цветом текста. */
|
|
@@ -1740,9 +1985,8 @@ window.__ModuleLoader__.load({
|
|
|
1740
1985
|
+ ' stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
|
|
1741
1986
|
+ '<path d="M10 3.5 5.5 8l4.5 4.5"/></svg>'
|
|
1742
1987
|
|
|
1743
|
-
// ------------------------------------------------------------ экран доски
|
|
1744
1988
|
|
|
1745
|
-
function TaskCard(props) {
|
|
1989
|
+
const TaskCard = React.memo(function TaskCard(props) {
|
|
1746
1990
|
const { task, t, now } = props
|
|
1747
1991
|
const ref = cardRef(task)
|
|
1748
1992
|
const status = cardStatus(task)
|
|
@@ -1752,38 +1996,29 @@ window.__ModuleLoader__.load({
|
|
|
1752
1996
|
? t('state.queued', { n: task.queuePos || 1 })
|
|
1753
1997
|
: stateLabel(task, t)
|
|
1754
1998
|
const pack = props.pack
|
|
1755
|
-
// Дата заведения, а не время в колонке. «Здесь 34 минуты» у задачи,
|
|
1756
|
-
// приехавшей подхватом полчаса назад, не значит ничего: это возраст
|
|
1757
|
-
// карточки на доске, а спрашивают про возраст самой задачи.
|
|
1758
1999
|
const born = shortDate(task.createdAt)
|
|
1759
2000
|
const touched = task.sessionId ? agoText(now, task.updatedAt, t) : ''
|
|
1760
2001
|
const menu = props.menuOpen
|
|
1761
2002
|
const urgent = priorityColor(task)
|
|
1762
2003
|
const overdue = task.overdue === true
|
|
1763
|
-
return
|
|
2004
|
+
return h('div', {
|
|
1764
2005
|
className: 'dkb-taskCard' + (urgent ? ' dkb-urgent' : '')
|
|
1765
2006
|
+ (overdue ? ' dkb-overdue' : '')
|
|
1766
2007
|
+ (props.sameSession ? ' dkb-sameSession' : ''),
|
|
1767
2008
|
onMouseEnter: () => { if (props.onHover) props.onHover(task.sessionId || '') },
|
|
1768
2009
|
onMouseLeave: () => { if (props.onHover) props.onHover('') },
|
|
1769
2010
|
style: urgent ? { borderLeftColor: urgent } : undefined,
|
|
1770
|
-
// Полный заголовок в подсказке: на карточке он обрезан двумя строками,
|
|
1771
|
-
// и прочитать его иначе можно было только открыв её.
|
|
1772
2011
|
title: task.title,
|
|
1773
|
-
// Карточка становится точкой табуляции: доска была мышиной целиком, а
|
|
1774
|
-
// перенос теперь ещё и команда агенту — действие с последствиями.
|
|
1775
2012
|
tabIndex: 0,
|
|
1776
2013
|
draggable: true,
|
|
1777
2014
|
onDragStart: (e) => {
|
|
1778
|
-
try { e.dataTransfer.setData('text/plain', task.id) } catch { /*
|
|
2015
|
+
try { e.dataTransfer.setData('text/plain', task.id) } catch { /* dataTransfer unsupported */ }
|
|
1779
2016
|
props.onDragStart(task)
|
|
1780
2017
|
},
|
|
1781
2018
|
onClick: () => props.onOpen(task),
|
|
1782
2019
|
},
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
React.createElement('div', { className: 'dkb-taskHead' },
|
|
1786
|
-
React.createElement('input', {
|
|
2020
|
+
h('div', { className: 'dkb-taskHead' },
|
|
2021
|
+
h('input', {
|
|
1787
2022
|
type: 'checkbox',
|
|
1788
2023
|
className: 'dkb-pick' + (props.picked ? ' dkb-pickOn' : ''),
|
|
1789
2024
|
checked: props.picked === true,
|
|
@@ -1791,113 +2026,114 @@ window.__ModuleLoader__.load({
|
|
|
1791
2026
|
onClick: (e) => e.stopPropagation(),
|
|
1792
2027
|
onChange: () => props.onPick(task),
|
|
1793
2028
|
}),
|
|
1794
|
-
waiting ?
|
|
1795
|
-
waiting ?
|
|
2029
|
+
waiting ? h('span', { className: 'dkb-waitDot' }) : null,
|
|
2030
|
+
waiting ? h('span', { className: 'dkb-waiting' }, t('board.waiting')) : null,
|
|
1796
2031
|
taskRef(task, t),
|
|
1797
|
-
task.
|
|
1798
|
-
task.
|
|
1799
|
-
task.
|
|
2032
|
+
task.isEpic ? h('span', { className: 'dkb-tag dkb-epicTag', title: t('epic.badge') }, '👑 ' + t('epic.badge')) : null,
|
|
2033
|
+
task.parentId ? h('span', { className: 'dkb-tag', style: { opacity: 0.75 } }, '↳') : null,
|
|
2034
|
+
task.subtasksCount && task.subtasksCount.total > 0
|
|
2035
|
+
? h('span', { className: 'dkb-tag', title: 'Subtasks' }, t('epic.progress', { done: task.subtasksCount.completed, total: task.subtasksCount.total }))
|
|
2036
|
+
: null,
|
|
2037
|
+
task.cron ? h('span', { className: 'dkb-tag', title: task.cron }, '⏰ cron') : null,
|
|
2038
|
+
task.progressDump ? h('span', { className: 'dkb-tag', title: 'PROGRESSDUMP' }, '📋 dump') : null,
|
|
2039
|
+
task.rejectReason ? h('span', { className: 'dkb-tag', style: { background: 'var(--dsw-alias-state-danger-bg, #ffebe9)', color: 'var(--dsw-alias-state-danger-primary, #cf222e)', borderColor: 'var(--dsw-alias-state-danger-border, #ffc1c0)' }, title: task.rejectReason }, '↩ ' + t('acceptance.rejectedBadge')) : null,
|
|
1800
2040
|
Array.isArray(task.checklist) && task.checklist.length > 0
|
|
1801
|
-
?
|
|
2041
|
+
? h('span', { className: 'dkb-tag', title: 'DoD Checklist' }, `☑ ${task.checklist.filter((i) => i.completed).length}/${task.checklist.length}`)
|
|
1802
2042
|
: null,
|
|
1803
2043
|
task.report && task.report.summary
|
|
1804
|
-
?
|
|
2044
|
+
? h('span', { className: 'dkb-tag', title: 'Execution Report' }, '📄 report')
|
|
1805
2045
|
: null,
|
|
1806
2046
|
Array.isArray(task.comments) && task.comments.length > 0
|
|
1807
|
-
?
|
|
2047
|
+
? h('span', { className: 'dkb-tag', title: 'Comments' }, `💬 ${task.comments.length}`)
|
|
1808
2048
|
: null,
|
|
1809
|
-
(task.labels || []).map((l) =>
|
|
2049
|
+
(task.labels || []).map((l) => h('span', {
|
|
1810
2050
|
className: 'dkb-tag', key: l, style: tagStyle(task, l),
|
|
1811
2051
|
}, l))),
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
2052
|
+
h('div', { className: 'dkb-taskTitle' }, task.title),
|
|
2053
|
+
task.subtasksCount && task.subtasksCount.total > 0
|
|
2054
|
+
? h('div', { className: 'dkb-progressBar', title: `${task.subtasksCount.completed}/${task.subtasksCount.total}` },
|
|
2055
|
+
h('div', {
|
|
2056
|
+
className: 'dkb-progressFill',
|
|
2057
|
+
style: { width: Math.round((task.subtasksCount.completed / task.subtasksCount.total) * 100) + '%' },
|
|
2058
|
+
}))
|
|
2059
|
+
: null,
|
|
2060
|
+
plan ? h('div', { className: 'dkb-planLine' }, plan) : null,
|
|
2061
|
+
h('div', { className: 'dkb-cardMenu' },
|
|
2062
|
+
h('button', {
|
|
1819
2063
|
type: 'button', className: 'dkb-moveBtn',
|
|
1820
|
-
// Скрыто до наведения — но появляется и при фокусе с клавиатуры:
|
|
1821
|
-
// прятать ради красоты то, что чинили ради доступности, нельзя.
|
|
1822
2064
|
'aria-expanded': menu === task.id,
|
|
1823
2065
|
onClick: (e) => { e.stopPropagation(); props.onMenu(menu === task.id ? '' : task.id) },
|
|
1824
2066
|
}, t('card.move')),
|
|
1825
2067
|
menu === task.id
|
|
1826
|
-
?
|
|
1827
|
-
(props.columns || []).filter((c) => c !== task.column).map((c) =>
|
|
2068
|
+
? h('div', { className: 'dkb-facetList' },
|
|
2069
|
+
(props.columns || []).filter((c) => c !== task.column).map((c) => h('button', {
|
|
1828
2070
|
type: 'button', className: 'dkb-facetRow', key: c,
|
|
1829
2071
|
onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onMove(task, c) },
|
|
1830
2072
|
}, t('column.' + c))),
|
|
1831
|
-
// Очередь стоит рядом с переносом: и то и другое — «отдать
|
|
1832
|
-
// работу», разница лишь в том, кому и когда.
|
|
1833
|
-
// Останавливать нечего у того, кто не идёт: пункт, который
|
|
1834
|
-
// ничего не делает, хуже отсутствующего.
|
|
1835
2073
|
task.state === 'stopped'
|
|
1836
|
-
?
|
|
2074
|
+
? h('button', {
|
|
1837
2075
|
type: 'button', className: 'dkb-facetRow', key: 'revive',
|
|
1838
2076
|
onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onRevive(task) },
|
|
1839
2077
|
}, t('card.revive'))
|
|
1840
2078
|
: null,
|
|
1841
2079
|
task.state === 'running' || task.state === 'waiting'
|
|
1842
|
-
?
|
|
2080
|
+
? h('button', {
|
|
1843
2081
|
type: 'button', className: 'dkb-facetRow', key: 'stop',
|
|
1844
2082
|
onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onStop(task) },
|
|
1845
2083
|
}, t('card.stop'))
|
|
1846
2084
|
: null,
|
|
1847
2085
|
task.state === 'queued'
|
|
1848
|
-
?
|
|
2086
|
+
? h('button', {
|
|
1849
2087
|
type: 'button', className: 'dkb-facetRow', key: 'unqueue',
|
|
1850
2088
|
onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onUnqueue(task) },
|
|
1851
2089
|
}, t('card.unqueue'))
|
|
1852
|
-
:
|
|
2090
|
+
: h('button', {
|
|
1853
2091
|
type: 'button', className: 'dkb-facetRow', key: 'queue',
|
|
1854
2092
|
onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onQueue(task) },
|
|
1855
2093
|
}, t('card.queue')))
|
|
1856
2094
|
: null,
|
|
1857
2095
|
),
|
|
1858
2096
|
status || state || born || touched
|
|
1859
|
-
?
|
|
2097
|
+
? h('div', { className: 'dkb-taskRef' },
|
|
1860
2098
|
state
|
|
1861
|
-
?
|
|
2099
|
+
? h('span', {
|
|
1862
2100
|
className: 'dkb-state' + (task.state === 'running' ? ' dkb-stateLive' : ''),
|
|
1863
2101
|
}, state)
|
|
1864
2102
|
: null,
|
|
1865
|
-
// Ссылка в чат стоит рядом с отметкой работы: «идёт» без пути к
|
|
1866
|
-
// тому, что идёт, — сведение, которым нельзя воспользоваться.
|
|
1867
2103
|
task.sessionId
|
|
1868
|
-
?
|
|
2104
|
+
? h('button', {
|
|
1869
2105
|
type: 'button', className: 'dkb-toChat',
|
|
1870
2106
|
onClick: (e) => { e.stopPropagation(); props.onOpenChat(task) },
|
|
1871
2107
|
}, t('card.toChat'))
|
|
1872
2108
|
: null,
|
|
1873
2109
|
touched
|
|
1874
|
-
?
|
|
2110
|
+
? h('span', {
|
|
1875
2111
|
className: task.stale ? 'dkb-countOver' : '',
|
|
1876
2112
|
title: exactAt(task.updatedAt),
|
|
1877
2113
|
}, touched)
|
|
1878
2114
|
: null,
|
|
1879
2115
|
born
|
|
1880
|
-
?
|
|
2116
|
+
? h('span', { title: exactAt(task.createdAt) }, born)
|
|
1881
2117
|
: null,
|
|
1882
2118
|
task.dueAt > 0
|
|
1883
|
-
?
|
|
2119
|
+
? h('span', {
|
|
1884
2120
|
className: overdue ? 'dkb-dueOver' : '',
|
|
1885
2121
|
title: exactAt(task.dueAt),
|
|
1886
2122
|
}, t('card.due', { date: shortDate(task.dueAt) }))
|
|
1887
2123
|
: null,
|
|
1888
2124
|
task.assignee
|
|
1889
|
-
?
|
|
2125
|
+
? h('span', { className: 'dkb-who', title: t('card.assigneeHint') },
|
|
1890
2126
|
'@' + task.assignee)
|
|
1891
2127
|
: null,
|
|
1892
2128
|
pack
|
|
1893
|
-
?
|
|
2129
|
+
? h('span', {
|
|
1894
2130
|
className: 'dkb-pack', title: t('card.packHint'),
|
|
1895
2131
|
}, t('card.pack', { at: pack.at, total: pack.total }))
|
|
1896
2132
|
: null,
|
|
1897
|
-
status ?
|
|
2133
|
+
status ? h('span', null, status) : null)
|
|
1898
2134
|
: null,
|
|
1899
2135
|
)
|
|
1900
|
-
}
|
|
2136
|
+
})
|
|
1901
2137
|
|
|
1902
2138
|
/**
|
|
1903
2139
|
* Чем запускать: профиль агента и уровень доступа.
|
|
@@ -1913,36 +2149,34 @@ window.__ModuleLoader__.load({
|
|
|
1913
2149
|
const t = props.t
|
|
1914
2150
|
const rows = []
|
|
1915
2151
|
if (!props.hidePreset && (props.presets.agentPresets || []).length > 0) {
|
|
1916
|
-
rows.push(
|
|
1917
|
-
|
|
1918
|
-
|
|
2152
|
+
rows.push(h('div', { className: 'dkb-field', key: 'preset' },
|
|
2153
|
+
h('span', { className: 'dkb-label' }, t('panel.agentPreset')),
|
|
2154
|
+
h('select', {
|
|
1919
2155
|
className: 'dkb-input', value: props.agentPreset, disabled: props.disabled,
|
|
1920
2156
|
onChange: (e) => props.onPreset(e.target.value),
|
|
1921
2157
|
},
|
|
1922
|
-
|
|
1923
|
-
props.presets.agentPresets.map((one) =>
|
|
2158
|
+
h('option', { value: '' }, t('panel.presetDefault')),
|
|
2159
|
+
props.presets.agentPresets.map((one) => h('option', {
|
|
1924
2160
|
value: one.id, key: one.id, title: one.description || '',
|
|
1925
2161
|
}, one.name || one.id))),
|
|
1926
2162
|
))
|
|
1927
2163
|
}
|
|
1928
2164
|
if ((props.presets.access || []).length > 0) {
|
|
1929
2165
|
const picked = (props.presets.access || []).find((one) => one.value === props.permission)
|
|
1930
|
-
rows.push(
|
|
1931
|
-
|
|
1932
|
-
|
|
2166
|
+
rows.push(h('div', { className: 'dkb-field', key: 'access' },
|
|
2167
|
+
h('span', { className: 'dkb-label' }, t('panel.permission')),
|
|
2168
|
+
h('select', {
|
|
1933
2169
|
className: 'dkb-input', value: props.permission, disabled: props.disabled,
|
|
1934
2170
|
onChange: (e) => props.onPermission(e.target.value),
|
|
1935
|
-
}, props.presets.access.map((one) =>
|
|
2171
|
+
}, props.presets.access.map((one) => h('option', {
|
|
1936
2172
|
value: one.value, key: one.value,
|
|
1937
2173
|
}, one.name || one.value))),
|
|
1938
|
-
// Пояснение к уровню доступа пишет ядро: свои слова разошлись бы с
|
|
1939
|
-
// тем, что этот уровень значит на самом деле.
|
|
1940
2174
|
picked && picked.description
|
|
1941
|
-
?
|
|
2175
|
+
? h('p', { className: 'dkb-hint' }, picked.description)
|
|
1942
2176
|
: null,
|
|
1943
2177
|
))
|
|
1944
2178
|
}
|
|
1945
|
-
return rows.length === 0 ? null :
|
|
2179
|
+
return rows.length === 0 ? null : h('div', { className: 'dkb-start' }, rows)
|
|
1946
2180
|
}
|
|
1947
2181
|
|
|
1948
2182
|
function BoardScreen(props) {
|
|
@@ -1954,11 +2188,17 @@ window.__ModuleLoader__.load({
|
|
|
1954
2188
|
const [rejectReasonText, setRejectReasonText] = React.useState('')
|
|
1955
2189
|
const [newChecklistText, setNewChecklistText] = React.useState('')
|
|
1956
2190
|
const [newCommentText, setNewCommentText] = React.useState('')
|
|
2191
|
+
const [quickFilter, setQuickFilter] = React.useState('all')
|
|
2192
|
+
const [subtasks, setSubtasks] = React.useState([])
|
|
2193
|
+
const [triageOpen, setTriageOpen] = React.useState(false)
|
|
2194
|
+
const [triageText, setTriageText] = React.useState('')
|
|
2195
|
+
const [triageGitea, setTriageGitea] = React.useState(false)
|
|
2196
|
+
const [triageSubmitting, setTriageSubmitting] = React.useState(false)
|
|
2197
|
+
const [newSubtaskTitle, setNewSubtaskTitle] = React.useState('')
|
|
2198
|
+
const [prCreating, setPrCreating] = React.useState(false)
|
|
2199
|
+
const [createdPr, setCreatedPr] = React.useState(null)
|
|
1957
2200
|
const [log, setLog] = React.useState([])
|
|
1958
2201
|
const [dialog, setDialog] = React.useState(null)
|
|
1959
|
-
// Черновик правки приоритета и меток в окне задачи. Живёт отдельно от
|
|
1960
|
-
// карточки: пока человек не нажал «Сохранить», доска не должна видеть
|
|
1961
|
-
// полувведённое.
|
|
1962
2202
|
const [editPriority, setEditPriority] = React.useState('')
|
|
1963
2203
|
const [editLabels, setEditLabels] = React.useState('')
|
|
1964
2204
|
const [editDue, setEditDue] = React.useState('')
|
|
@@ -1971,41 +2211,30 @@ window.__ModuleLoader__.load({
|
|
|
1971
2211
|
const [starting, setStarting] = React.useState(false)
|
|
1972
2212
|
const [draftText, setDraftText] = React.useState('')
|
|
1973
2213
|
const [query, setQuery] = React.useState('')
|
|
2214
|
+
const [debouncedQuery, setDebouncedQuery] = React.useState('')
|
|
2215
|
+
React.useEffect(() => {
|
|
2216
|
+
const timer = setTimeout(() => setDebouncedQuery(query), 150)
|
|
2217
|
+
return () => clearTimeout(timer)
|
|
2218
|
+
}, [query])
|
|
1974
2219
|
const [dropAt, setDropAt] = React.useState(null)
|
|
1975
2220
|
const [board, setBoard] = React.useState('main')
|
|
1976
|
-
// Решения о сворачивании держим в состоянии экрана, а не в хранилище:
|
|
1977
|
-
// это взгляд одного человека на одну доску прямо сейчас, а не свойство
|
|
1978
|
-
// задач. Обновление доски их переживает, перезагрузка страницы — нет.
|
|
1979
2221
|
const [collapsed, setCollapsed] = React.useState({})
|
|
1980
|
-
// Отборы живут в состоянии экрана: он смонтирован всё время работы
|
|
1981
|
-
// харнесса, поэтому выбор переживает и закрытие доски, и обновление.
|
|
1982
2222
|
const [filters, setFilters] = React.useState({})
|
|
1983
2223
|
const [openFacet, setOpenFacet] = React.useState('')
|
|
1984
2224
|
const [cardMenu, setCardMenu] = React.useState('')
|
|
1985
2225
|
const [body, setBody] = React.useState(null)
|
|
1986
2226
|
const [groups, setGroups] = React.useState({})
|
|
1987
2227
|
const [syncing, setSyncing] = React.useState(false)
|
|
1988
|
-
// Отмеченные для группового запуска. Живут отдельно от открытой карточки:
|
|
1989
|
-
// выбор переживает и открытие окна, и обновление доски.
|
|
1990
2228
|
const [picked, setPicked] = React.useState({})
|
|
1991
2229
|
const [archive, setArchive] = React.useState(null)
|
|
1992
2230
|
const [metrics, setMetrics] = React.useState(null)
|
|
1993
|
-
// «Только ждущие» держим отдельно от отборов: это не значение измерения,
|
|
1994
|
-
// а взгляд «покажи, где стоит работа», и складывается он с любым отбором.
|
|
1995
2231
|
const [onlyWaiting, setOnlyWaiting] = React.useState(false)
|
|
1996
2232
|
const [onlyOverdue, setOnlyOverdue] = React.useState(false)
|
|
1997
2233
|
const [hoverSession, setHoverSession] = React.useState('')
|
|
1998
2234
|
const [order, setOrder] = React.useState('manual')
|
|
1999
|
-
// «По проектам» — как приходит с сервера; «по ответственному» — своя
|
|
2000
|
-
// раскладка поверх того же порядка.
|
|
2001
2235
|
const [grouping, setGrouping] = React.useState('repo')
|
|
2002
|
-
// Кто «я»: спрашивается один раз у сервера. Пусто — Gitea не ответил, и
|
|
2003
|
-
// кнопку «Мои» рисовать нельзя: соврать числом хуже, чем не показать.
|
|
2004
2236
|
const [me, setMe] = React.useState('')
|
|
2005
2237
|
const [onlyMine, setOnlyMine] = React.useState(false)
|
|
2006
|
-
// Чем запускать: профиль агента и уровень доступа. Обе службы у ядра
|
|
2007
|
-
// необязательны, поэтому пустой список — не поломка, а «выбирать не из
|
|
2008
|
-
// чего»: поле тогда не рисуется вовсе.
|
|
2009
2238
|
const [presets, setPresets] = React.useState({ agentPresets: [], access: [] })
|
|
2010
2239
|
const [agentPreset, setAgentPreset] = React.useState('')
|
|
2011
2240
|
const [permission, setPermission] = React.useState('')
|
|
@@ -2013,9 +2242,6 @@ window.__ModuleLoader__.load({
|
|
|
2013
2242
|
|
|
2014
2243
|
const load = React.useCallback(async (nextBoard) => {
|
|
2015
2244
|
try {
|
|
2016
|
-
// Отбор по репозиторию делает браузер вместе с остальными отборами:
|
|
2017
|
-
// серверный отбор спорил бы с ним и прятал бы часть значений из
|
|
2018
|
-
// списка, потому что их не было бы в ответе.
|
|
2019
2245
|
const query = '?board=' + encodeURIComponent(nextBoard || 'main')
|
|
2020
2246
|
setState(normalizeBoard(await api('/board' + query)))
|
|
2021
2247
|
setError('')
|
|
@@ -2026,7 +2252,6 @@ window.__ModuleLoader__.load({
|
|
|
2026
2252
|
|
|
2027
2253
|
React.useEffect(() => { load(board) }, [load, board])
|
|
2028
2254
|
|
|
2029
|
-
// Подписка на Server-Sent Events (SSE) для мгновенного обновления доски (#222).
|
|
2030
2255
|
React.useEffect(() => {
|
|
2031
2256
|
if (typeof window === 'undefined' || typeof window.EventSource !== 'function') return undefined
|
|
2032
2257
|
let es = null
|
|
@@ -2035,15 +2260,19 @@ window.__ModuleLoader__.load({
|
|
|
2035
2260
|
const connect = () => {
|
|
2036
2261
|
try {
|
|
2037
2262
|
es = new window.EventSource('/dsh-kanban/events')
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2263
|
+
let reloadTimer = null
|
|
2264
|
+
const scheduleReload = () => {
|
|
2265
|
+
if (reloadTimer) return
|
|
2266
|
+
reloadTimer = setTimeout(() => {
|
|
2267
|
+
reloadTimer = null
|
|
2268
|
+
load(board)
|
|
2269
|
+
}, 60)
|
|
2270
|
+
}
|
|
2271
|
+
es.addEventListener('board:change', scheduleReload)
|
|
2272
|
+
es.addEventListener('task:change', scheduleReload)
|
|
2044
2273
|
es.onerror = () => {
|
|
2045
2274
|
if (es) {
|
|
2046
|
-
try { es.close() } catch {}
|
|
2275
|
+
try { es.close() } catch { /* eventsource close best-effort */ }
|
|
2047
2276
|
es = null
|
|
2048
2277
|
}
|
|
2049
2278
|
if (!reconnectTimer) {
|
|
@@ -2053,7 +2282,7 @@ window.__ModuleLoader__.load({
|
|
|
2053
2282
|
}, 5000)
|
|
2054
2283
|
}
|
|
2055
2284
|
}
|
|
2056
|
-
} catch {}
|
|
2285
|
+
} catch { /* eventsource setup best-effort */ }
|
|
2057
2286
|
}
|
|
2058
2287
|
|
|
2059
2288
|
connect()
|
|
@@ -2061,14 +2290,12 @@ window.__ModuleLoader__.load({
|
|
|
2061
2290
|
return () => {
|
|
2062
2291
|
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
2063
2292
|
if (es) {
|
|
2064
|
-
try { es.close() } catch {}
|
|
2293
|
+
try { es.close() } catch { /* eventsource close best-effort */ }
|
|
2065
2294
|
es = null
|
|
2066
2295
|
}
|
|
2067
2296
|
}
|
|
2068
2297
|
}, [load, board])
|
|
2069
2298
|
|
|
2070
|
-
// Просьба чипа «покажи вот эту». Забираем её один раз: повтор открывал
|
|
2071
|
-
// бы окно снова после каждого закрытия.
|
|
2072
2299
|
const [wanted, setWanted] = React.useState('')
|
|
2073
2300
|
React.useEffect(() => {
|
|
2074
2301
|
const toggle = props.toggle
|
|
@@ -2083,8 +2310,6 @@ window.__ModuleLoader__.load({
|
|
|
2083
2310
|
return toggle.subscribe(pull)
|
|
2084
2311
|
}, [props.toggle])
|
|
2085
2312
|
|
|
2086
|
-
// Открываем, когда задача приехала: доска могла переключаться, и в
|
|
2087
|
-
// момент просьбы её ещё не было в списке.
|
|
2088
2313
|
React.useEffect(() => {
|
|
2089
2314
|
if (wanted === '' || state === null) return
|
|
2090
2315
|
const task = state.tasks.find((x) => x.id === wanted)
|
|
@@ -2093,9 +2318,6 @@ window.__ModuleLoader__.load({
|
|
|
2093
2318
|
openPanelRef.current(task)
|
|
2094
2319
|
}, [wanted, state])
|
|
2095
2320
|
|
|
2096
|
-
// Escape снимает по одному слою за нажатие: диалог, затем панель, затем
|
|
2097
|
-
// саму доску. Закрывать всё разом — терять контекст, которого человек не
|
|
2098
|
-
// просил лишаться.
|
|
2099
2321
|
React.useEffect(() => {
|
|
2100
2322
|
const onKey = (e) => {
|
|
2101
2323
|
if (e.key !== 'Escape') return
|
|
@@ -2107,7 +2329,6 @@ window.__ModuleLoader__.load({
|
|
|
2107
2329
|
return () => document.removeEventListener('keydown', onKey)
|
|
2108
2330
|
}, [dialog, openTask, props])
|
|
2109
2331
|
|
|
2110
|
-
// Сначала провайдеры и текущий выбор, затем модели выбранного провайдера.
|
|
2111
2332
|
React.useEffect(() => {
|
|
2112
2333
|
let alive = true
|
|
2113
2334
|
api('/models')
|
|
@@ -2119,8 +2340,6 @@ window.__ModuleLoader__.load({
|
|
|
2119
2340
|
const provider = stillOffered(saved.provider, list.map((p) => p.id), '')
|
|
2120
2341
|
if (provider !== '') {
|
|
2121
2342
|
setProvider(provider)
|
|
2122
|
-
// Модель проверяется своим списком, когда он приедет: здесь её
|
|
2123
|
-
// ещё не с чем сверить, а подставить чужую нельзя.
|
|
2124
2343
|
setModel(typeof saved.model === 'string' ? saved.model : '')
|
|
2125
2344
|
} else if (out.current) {
|
|
2126
2345
|
setProvider(out.current.provider || '')
|
|
@@ -2142,8 +2361,6 @@ window.__ModuleLoader__.load({
|
|
|
2142
2361
|
const rows = out.agentPresets || []
|
|
2143
2362
|
const access = out.access || []
|
|
2144
2363
|
setPresets({ agentPresets: rows, access })
|
|
2145
|
-
// Пустое значение — «как у харнесса по умолчанию»; подставляем его
|
|
2146
|
-
// же явно, чтобы человек видел, с чем задача пойдёт.
|
|
2147
2364
|
const saved = recallLaunch(board)
|
|
2148
2365
|
setAgentPreset(stillOffered(saved.agentPreset, rows.map((x) => x.id), out.agentPresetDefault || ''))
|
|
2149
2366
|
setPermission(stillOffered(saved.permission, access.map((x) => x.value), out.accessDefault || ''))
|
|
@@ -2160,8 +2377,6 @@ window.__ModuleLoader__.load({
|
|
|
2160
2377
|
if (!alive || !out) return
|
|
2161
2378
|
const list = out.models || []
|
|
2162
2379
|
setModels(list)
|
|
2163
|
-
// Модель могла исчезнуть у провайдера: подставленная из памяти, она
|
|
2164
|
-
// осталась бы в поле, а запуск отказал бы уже после нажатия.
|
|
2165
2380
|
setModel((cur) => stillOffered(cur, list.map((m) => m.id), ''))
|
|
2166
2381
|
})
|
|
2167
2382
|
.catch(() => { if (alive) setModels([]) })
|
|
@@ -2169,8 +2384,6 @@ window.__ModuleLoader__.load({
|
|
|
2169
2384
|
}, [provider])
|
|
2170
2385
|
|
|
2171
2386
|
const commitMove = React.useCallback(async (task, column, where) => {
|
|
2172
|
-
// Карточку не перерисовываем заранее: до подтверждения она обязана
|
|
2173
|
-
// оставаться там, где лежала, иначе отмена выглядит откатом.
|
|
2174
2387
|
try {
|
|
2175
2388
|
await api('/task/' + encodeURIComponent(task.id) + '/move', {
|
|
2176
2389
|
method: 'POST',
|
|
@@ -2192,7 +2405,6 @@ window.__ModuleLoader__.load({
|
|
|
2192
2405
|
const askMove = React.useCallback((task, column, where) => {
|
|
2193
2406
|
if (!task) return
|
|
2194
2407
|
const place = where ?? neighboursFor(tasksOf((state && state.tasks) || [], column), task.id, Infinity)
|
|
2195
|
-
// Перенос внутри колонки — это порядок, а не команда: спрашивать не о чем.
|
|
2196
2408
|
if (task.column === column) { commitMove(task, column, place); return }
|
|
2197
2409
|
setDialog({ mode: 'move', task, column, where: place })
|
|
2198
2410
|
}, [state, commitMove])
|
|
@@ -2203,7 +2415,6 @@ window.__ModuleLoader__.load({
|
|
|
2203
2415
|
setOverColumn('')
|
|
2204
2416
|
setDropAt(null)
|
|
2205
2417
|
if (!task || !state) return
|
|
2206
|
-
// Мышь слишком легко задевает карточку, чтобы перенос уходил без спроса.
|
|
2207
2418
|
askMove(task, column, neighboursFor(tasksOf(state.tasks, column), task.id, index))
|
|
2208
2419
|
}, [state, askMove])
|
|
2209
2420
|
|
|
@@ -2218,6 +2429,15 @@ window.__ModuleLoader__.load({
|
|
|
2218
2429
|
? new Date(task.dueAt).toISOString().slice(0, 10)
|
|
2219
2430
|
: '')
|
|
2220
2431
|
setEditCron(typeof task?.cron === 'string' ? task.cron : '')
|
|
2432
|
+
setTriageOpen(false)
|
|
2433
|
+
setTriageText('')
|
|
2434
|
+
setTriageGitea(false)
|
|
2435
|
+
setCreatedPr(null)
|
|
2436
|
+
setNewSubtaskTitle('')
|
|
2437
|
+
setSubtasks([])
|
|
2438
|
+
api('/task/' + encodeURIComponent(task.id) + '/subtasks')
|
|
2439
|
+
.then((out) => setSubtasks((out && out.subtasks) || []))
|
|
2440
|
+
.catch(() => setSubtasks([]))
|
|
2221
2441
|
api('/task/' + encodeURIComponent(task.id) + '/body')
|
|
2222
2442
|
.then((out) => setBody((out && out.blocks) || []))
|
|
2223
2443
|
.catch(() => setBody([]))
|
|
@@ -2231,75 +2451,65 @@ window.__ModuleLoader__.load({
|
|
|
2231
2451
|
}, [])
|
|
2232
2452
|
openPanelRef.current = openPanel
|
|
2233
2453
|
|
|
2234
|
-
|
|
2454
|
+
ensureCss()
|
|
2455
|
+
const style = null
|
|
2235
2456
|
|
|
2236
2457
|
if (state === null) {
|
|
2237
|
-
return
|
|
2238
|
-
|
|
2458
|
+
return h('div', { className: 'dkb-screen' },
|
|
2459
|
+
h('p', { className: 'dkb-note' }, error || t('board.loading')))
|
|
2239
2460
|
}
|
|
2240
2461
|
|
|
2241
2462
|
const boards = (state && state.boards) || []
|
|
2242
|
-
const bar =
|
|
2243
|
-
|
|
2244
|
-
// Своего механизма закрытия не заводит: их уже три (Escape, повторный
|
|
2245
|
-
// щелчок по боковой строке, переход в чужую сессию), и четвёртый
|
|
2246
|
-
// разошёлся бы с ними при первой же правке.
|
|
2247
|
-
React.createElement('button', {
|
|
2463
|
+
const bar = h('div', { className: 'dkb-bar' },
|
|
2464
|
+
h('button', {
|
|
2248
2465
|
type: 'button', className: 'dkb-back', title: t('board.backHint'),
|
|
2249
2466
|
onClick: () => { if (props.onClose) props.onClose() },
|
|
2250
2467
|
},
|
|
2251
|
-
|
|
2468
|
+
h('span', {
|
|
2252
2469
|
className: 'dkb-backIcon',
|
|
2253
2470
|
dangerouslySetInnerHTML: { __html: BACK_ICON },
|
|
2254
2471
|
}),
|
|
2255
|
-
|
|
2472
|
+
h('span', null, t('board.back')),
|
|
2256
2473
|
),
|
|
2257
|
-
|
|
2474
|
+
h('span', { className: 'dkb-barTitle' }, t('section.label')),
|
|
2258
2475
|
boards.length > 1
|
|
2259
|
-
?
|
|
2476
|
+
? h('select', {
|
|
2260
2477
|
className: 'dkb-input', style: { width: 'auto' }, value: board,
|
|
2261
2478
|
onChange: (e) => {
|
|
2262
|
-
// Отборы собраны из меток ЭТОЙ доски: на другой те же значения
|
|
2263
|
-
// могут не встречаться вовсе, и выбор превратился бы в пустоту.
|
|
2264
2479
|
setOpenTask(null); setCollapsed({}); setFilters({}); setBoard(e.target.value)
|
|
2265
2480
|
},
|
|
2266
|
-
}, boards.map((b) =>
|
|
2481
|
+
}, boards.map((b) => h('option', { value: b.id, key: b.id }, boardTitle(b, t))))
|
|
2267
2482
|
: null,
|
|
2268
2483
|
(state.facets || []).map((facet) => {
|
|
2269
2484
|
const picked = filters[facet.ns] || []
|
|
2270
|
-
// Имена пространств приходят из Gitea сырыми. Известные переводим,
|
|
2271
|
-
// незнакомое оставляем как есть: выдумывать перевод метке, которую
|
|
2272
|
-
// завели вчера, доска не вправе.
|
|
2273
2485
|
const label = facetLabel(facet.ns, t)
|
|
2274
|
-
return
|
|
2275
|
-
|
|
2486
|
+
return h('div', { className: 'dkb-facet', key: facet.ns },
|
|
2487
|
+
h('button', {
|
|
2276
2488
|
type: 'button',
|
|
2277
2489
|
className: 'dkb-facetHead' + (picked.length ? ' dkb-facetOn' : ''),
|
|
2278
2490
|
'aria-expanded': openFacet === facet.ns,
|
|
2279
2491
|
onClick: () => setOpenFacet((cur) => (cur === facet.ns ? '' : facet.ns)),
|
|
2280
2492
|
}, picked.length ? label + ' · ' + picked.length : label),
|
|
2281
2493
|
openFacet === facet.ns
|
|
2282
|
-
?
|
|
2283
|
-
facet.values.map((v) =>
|
|
2494
|
+
? h('div', { className: 'dkb-facetList' },
|
|
2495
|
+
facet.values.map((v) => h('label', {
|
|
2284
2496
|
className: 'dkb-facetRow', key: v.value,
|
|
2285
2497
|
},
|
|
2286
|
-
|
|
2498
|
+
h('input', {
|
|
2287
2499
|
type: 'checkbox',
|
|
2288
2500
|
checked: picked.indexOf(v.value) >= 0,
|
|
2289
2501
|
onChange: () => setFilters((cur) => toggleValue(cur, facet.ns, v.value)),
|
|
2290
2502
|
}),
|
|
2291
|
-
|
|
2292
|
-
|
|
2503
|
+
h('span', { className: 'dkb-facetName' }, v.value),
|
|
2504
|
+
h('span', { className: 'dkb-count' }, String(v.count)),
|
|
2293
2505
|
)))
|
|
2294
2506
|
: null,
|
|
2295
2507
|
)
|
|
2296
2508
|
}),
|
|
2297
2509
|
(() => {
|
|
2298
|
-
// Ноль в шапке — мебель, за которой перестают следить, поэтому
|
|
2299
|
-
// счётчик появляется только когда кого-то действительно ждут.
|
|
2300
2510
|
const waiting = state.tasks.filter((x) => x.state === 'waiting').length
|
|
2301
2511
|
if (waiting === 0) return null
|
|
2302
|
-
return
|
|
2512
|
+
return h('button', {
|
|
2303
2513
|
type: 'button',
|
|
2304
2514
|
className: 'dkb-waitCount' + (onlyWaiting ? ' dkb-save' : ''),
|
|
2305
2515
|
title: t('board.waitCountHint'),
|
|
@@ -2307,11 +2517,9 @@ window.__ModuleLoader__.load({
|
|
|
2307
2517
|
}, t('board.waitCount', { n: waiting }))
|
|
2308
2518
|
})(),
|
|
2309
2519
|
(() => {
|
|
2310
|
-
// Просроченные — те, чей дедлайн уже прошёл. Счётчик появляется
|
|
2311
|
-
// только когда такие есть: ноль в шапке — мебель.
|
|
2312
2520
|
const overdue = state.tasks.filter((x) => x.overdue === true).length
|
|
2313
2521
|
if (overdue === 0) return null
|
|
2314
|
-
return
|
|
2522
|
+
return h('button', {
|
|
2315
2523
|
type: 'button',
|
|
2316
2524
|
className: 'dkb-waitCount' + (onlyOverdue ? ' dkb-save' : ''),
|
|
2317
2525
|
title: t('board.overdueHint'),
|
|
@@ -2319,34 +2527,32 @@ window.__ModuleLoader__.load({
|
|
|
2319
2527
|
}, t('board.overdueCount', { n: overdue }))
|
|
2320
2528
|
})(),
|
|
2321
2529
|
(() => {
|
|
2322
|
-
// Ноль своих задач — кнопки нет: пустой счётчик в шапке перестают
|
|
2323
|
-
// замечать вместе с непустым.
|
|
2324
2530
|
if (me === '') return null
|
|
2325
2531
|
const mine = state.tasks.filter((x) => x.assignee === me).length
|
|
2326
2532
|
if (mine === 0) return null
|
|
2327
|
-
return
|
|
2533
|
+
return h('button', {
|
|
2328
2534
|
type: 'button',
|
|
2329
2535
|
className: 'dkb-waitCount' + (onlyMine ? ' dkb-save' : ''),
|
|
2330
2536
|
title: t('board.mineHint', { who: me }),
|
|
2331
2537
|
onClick: () => setOnlyMine((cur) => !cur),
|
|
2332
2538
|
}, t('board.mine', { n: mine }))
|
|
2333
2539
|
})(),
|
|
2334
|
-
|
|
2540
|
+
h('select', {
|
|
2335
2541
|
className: 'dkb-input', style: { width: 'auto' }, value: grouping,
|
|
2336
2542
|
onChange: (e) => setGrouping(e.target.value),
|
|
2337
2543
|
},
|
|
2338
|
-
|
|
2339
|
-
|
|
2544
|
+
h('option', { value: 'repo' }, t('group.repo')),
|
|
2545
|
+
h('option', { value: 'assignee' }, t('group.assignee')),
|
|
2340
2546
|
),
|
|
2341
|
-
|
|
2547
|
+
h('select', {
|
|
2342
2548
|
className: 'dkb-input', style: { width: 'auto' }, value: order,
|
|
2343
2549
|
onChange: (e) => setOrder(e.target.value),
|
|
2344
2550
|
},
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2551
|
+
h('option', { value: 'manual' }, t('order.manual')),
|
|
2552
|
+
h('option', { value: 'old' }, t('order.old')),
|
|
2553
|
+
h('option', { value: 'new' }, t('order.new')),
|
|
2348
2554
|
),
|
|
2349
|
-
|
|
2555
|
+
h('button', {
|
|
2350
2556
|
type: 'button', className: metrics ? 'dkb-save' : 'dkb-discard',
|
|
2351
2557
|
onClick: async () => {
|
|
2352
2558
|
if (metrics) { setMetrics(null); return }
|
|
@@ -2356,7 +2562,7 @@ window.__ModuleLoader__.load({
|
|
|
2356
2562
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
2357
2563
|
},
|
|
2358
2564
|
}, t('board.metrics')),
|
|
2359
|
-
|
|
2565
|
+
h('button', {
|
|
2360
2566
|
type: 'button', className: archive ? 'dkb-save' : 'dkb-discard',
|
|
2361
2567
|
onClick: async () => {
|
|
2362
2568
|
if (archive) { setArchive(null); return }
|
|
@@ -2367,8 +2573,7 @@ window.__ModuleLoader__.load({
|
|
|
2367
2573
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
2368
2574
|
},
|
|
2369
2575
|
}, t('board.archive')),
|
|
2370
|
-
|
|
2371
|
-
React.createElement('button', {
|
|
2576
|
+
h('button', {
|
|
2372
2577
|
type: 'button', className: 'dkb-discard',
|
|
2373
2578
|
title: t('board.exportHint'),
|
|
2374
2579
|
onClick: async () => {
|
|
@@ -2384,9 +2589,9 @@ window.__ModuleLoader__.load({
|
|
|
2384
2589
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
2385
2590
|
},
|
|
2386
2591
|
}, t('board.export')),
|
|
2387
|
-
|
|
2592
|
+
h('label', { className: 'dkb-discard dkb-import' },
|
|
2388
2593
|
t('board.import'),
|
|
2389
|
-
|
|
2594
|
+
h('input', {
|
|
2390
2595
|
type: 'file', accept: 'application/json,.json', style: { display: 'none' },
|
|
2391
2596
|
onChange: async (e) => {
|
|
2392
2597
|
const file = e.target.files && e.target.files[0]
|
|
@@ -2400,42 +2605,34 @@ window.__ModuleLoader__.load({
|
|
|
2400
2605
|
} catch { setError(t('error.badImport')) }
|
|
2401
2606
|
},
|
|
2402
2607
|
})),
|
|
2403
|
-
// «Показано 12 из 81» появляется, только когда что-то отобрано или
|
|
2404
|
-
// введён поиск: на нетронутой доске эта строка была бы шумом.
|
|
2405
2608
|
anySelected(filters) || query.trim() !== ''
|
|
2406
|
-
?
|
|
2609
|
+
? h('span', { className: 'dkb-shown' }, t('board.shown', {
|
|
2407
2610
|
shown: state.tasks.filter((x) => matchesFilters(x, filters) && matchesQuery(x, query)).length,
|
|
2408
2611
|
total: state.tasks.length,
|
|
2409
2612
|
}))
|
|
2410
2613
|
: null,
|
|
2411
2614
|
anySelected(filters)
|
|
2412
|
-
?
|
|
2615
|
+
? h('button', {
|
|
2413
2616
|
type: 'button', className: 'dkb-discard',
|
|
2414
2617
|
onClick: () => { setFilters({}); setOpenFacet('') },
|
|
2415
2618
|
}, t('board.clearFilters'))
|
|
2416
2619
|
: null,
|
|
2417
|
-
|
|
2620
|
+
h('input', {
|
|
2418
2621
|
className: 'dkb-search', value: query, placeholder: t('board.search'),
|
|
2419
2622
|
onChange: (e) => setQuery(e.target.value),
|
|
2420
2623
|
}),
|
|
2421
2624
|
(() => {
|
|
2422
|
-
// Пока запрос в пути, состояние на доске ещё вчерашнее: оно приедет
|
|
2423
|
-
// только со следующим ответом. Поэтому ход показываем сами, а не
|
|
2424
|
-
// ждём, когда сервер расскажет о нём задним числом.
|
|
2425
2625
|
const line = syncing
|
|
2426
2626
|
? { tone: 'idle', text: t('sync.running'), title: '' }
|
|
2427
2627
|
: syncLine(state.sync, state.now, t)
|
|
2428
|
-
return
|
|
2628
|
+
return h('span', {
|
|
2429
2629
|
className: 'dkb-sync' + (line.tone === 'bad' ? ' dkb-syncBad' : ''),
|
|
2430
2630
|
title: line.title,
|
|
2431
2631
|
}, line.text)
|
|
2432
2632
|
})(),
|
|
2433
|
-
|
|
2633
|
+
h('button', {
|
|
2434
2634
|
type: 'button', className: 'dkb-discard', disabled: syncing,
|
|
2435
2635
|
onClick: async () => {
|
|
2436
|
-
// Кнопка гаснет на время прохода: нажать её второй раз значило бы
|
|
2437
|
-
// попросить сверку, которая и так идёт, а сервер ответил бы
|
|
2438
|
-
// «пропущено» — и человек решил бы, что ничего не случилось.
|
|
2439
2636
|
setSyncing(true)
|
|
2440
2637
|
try {
|
|
2441
2638
|
await api('/sync', { method: 'POST', body: '{}' })
|
|
@@ -2447,21 +2644,32 @@ window.__ModuleLoader__.load({
|
|
|
2447
2644
|
}
|
|
2448
2645
|
},
|
|
2449
2646
|
}, syncing ? t('board.syncing') : t('board.sync')),
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2647
|
+
h('button', { type: 'button', className: 'dkb-discard', onClick: () => load(board) }, t('board.refresh')),
|
|
2648
|
+
h('div', { className: 'dkb-quickFilterBar' },
|
|
2649
|
+
[
|
|
2650
|
+
{ id: 'all', label: t('filter.all') },
|
|
2651
|
+
{ id: 'active', label: t('filter.active') },
|
|
2652
|
+
{ id: 'review', label: t('filter.review') },
|
|
2653
|
+
{ id: 'epics', label: t('filter.epics') },
|
|
2654
|
+
{ id: 'stale', label: t('filter.stale') },
|
|
2655
|
+
].map((chip) => h('button', {
|
|
2656
|
+
type: 'button',
|
|
2657
|
+
key: chip.id,
|
|
2658
|
+
className: 'dkb-filterChip' + (quickFilter === chip.id ? ' dkb-filterChipActive' : ''),
|
|
2659
|
+
onClick: () => setQuickFilter(chip.id),
|
|
2660
|
+
}, chip.label))),
|
|
2453
2661
|
Object.keys(picked).length > 0
|
|
2454
|
-
?
|
|
2662
|
+
? h('span', { className: 'dkb-shown' },
|
|
2455
2663
|
t('board.pickedCount', { n: Object.keys(picked).length }))
|
|
2456
2664
|
: null,
|
|
2457
2665
|
Object.keys(picked).length > 0
|
|
2458
|
-
?
|
|
2666
|
+
? h('button', {
|
|
2459
2667
|
type: 'button', className: 'dkb-discard',
|
|
2460
2668
|
onClick: () => setPicked({}),
|
|
2461
2669
|
}, t('board.pickNone'))
|
|
2462
2670
|
: null,
|
|
2463
2671
|
Object.keys(picked).length > 0
|
|
2464
|
-
?
|
|
2672
|
+
? h('button', {
|
|
2465
2673
|
type: 'button', className: 'dkb-save',
|
|
2466
2674
|
onClick: () => setDialog({
|
|
2467
2675
|
mode: 'batch',
|
|
@@ -2471,15 +2679,13 @@ window.__ModuleLoader__.load({
|
|
|
2471
2679
|
}, t('board.pickStart'))
|
|
2472
2680
|
: null,
|
|
2473
2681
|
Object.keys(picked).length > 0
|
|
2474
|
-
?
|
|
2682
|
+
? h('button', {
|
|
2475
2683
|
type: 'button', className: 'dkb-discard',
|
|
2476
2684
|
onClick: async () => {
|
|
2477
2685
|
const ids = Object.keys(picked)
|
|
2478
2686
|
const out = await applyToEach(ids, (id) => api(
|
|
2479
2687
|
'/task/' + encodeURIComponent(id) + '/archive', { method: 'POST', body: '{}' },
|
|
2480
2688
|
))
|
|
2481
|
-
// Часть задач архивировать нельзя (архив только из «Выполнено»),
|
|
2482
|
-
// и число отказов называется вслух.
|
|
2483
2689
|
if (out.failed > 0) setError(t('board.someFailed', { done: out.done, total: ids.length }))
|
|
2484
2690
|
else setPicked({})
|
|
2485
2691
|
await load(board)
|
|
@@ -2487,7 +2693,7 @@ window.__ModuleLoader__.load({
|
|
|
2487
2693
|
}, t('board.pickArchive'))
|
|
2488
2694
|
: null,
|
|
2489
2695
|
Object.keys(picked).length > 0
|
|
2490
|
-
?
|
|
2696
|
+
? h('button', {
|
|
2491
2697
|
type: 'button', className: 'dkb-discard',
|
|
2492
2698
|
onClick: async () => {
|
|
2493
2699
|
const ids = Object.keys(picked)
|
|
@@ -2501,14 +2707,12 @@ window.__ModuleLoader__.load({
|
|
|
2501
2707
|
}, t('board.pickUnqueue'))
|
|
2502
2708
|
: null,
|
|
2503
2709
|
Object.keys(picked).length > 0
|
|
2504
|
-
?
|
|
2505
|
-
className: 'dkb-input',
|
|
2710
|
+
? h('select', {
|
|
2711
|
+
className: 'dkb-input',
|
|
2506
2712
|
onChange: async (e) => {
|
|
2507
2713
|
const column = e.target.value
|
|
2508
|
-
if (column
|
|
2714
|
+
if (!column) return
|
|
2509
2715
|
const ids = Object.keys(picked)
|
|
2510
|
-
// Групповой перенос — та же команда агенту, что и одиночный:
|
|
2511
|
-
// маршрут один, и последствия у него те же.
|
|
2512
2716
|
const out = await applyToEach(ids, (id) => api(
|
|
2513
2717
|
'/task/' + encodeURIComponent(id) + '/move',
|
|
2514
2718
|
{ method: 'POST', body: JSON.stringify({ column }) },
|
|
@@ -2518,36 +2722,33 @@ window.__ModuleLoader__.load({
|
|
|
2518
2722
|
await load(board)
|
|
2519
2723
|
},
|
|
2520
2724
|
},
|
|
2521
|
-
|
|
2522
|
-
(state.columns || []).map((c) =>
|
|
2725
|
+
h('option', { value: '' }, t('board.pickMove')),
|
|
2726
|
+
(state.columns || []).map((c) => h('option', {
|
|
2523
2727
|
value: c.id, key: c.id,
|
|
2524
2728
|
}, columnTitle(c, t))),
|
|
2525
2729
|
)
|
|
2526
2730
|
: null,
|
|
2527
|
-
|
|
2731
|
+
h('button', { type: 'button', className: 'dkb-save', onClick: () => setDialog({ mode: 'own', project: false, repo: '', newRepo: '', title: '', full: '', repos: null, issues: null, error: '' }) }, t('board.newTask')),
|
|
2528
2732
|
)
|
|
2529
2733
|
|
|
2530
|
-
// Счётчик колонки считает ВСЕ карточки, а не найденные: иначе поиск
|
|
2531
|
-
// сделал бы вид, что предел колонки соблюдён.
|
|
2532
2734
|
const packs = packInfo(state.tasks)
|
|
2533
|
-
const columns =
|
|
2735
|
+
const columns = h('div', { className: 'dkb-cols' },
|
|
2534
2736
|
state.columns.map((col) => {
|
|
2535
|
-
// Счётчик колонки считает ОТОБРАННОЕ, а поиск — нет.
|
|
2536
|
-
//
|
|
2537
|
-
// Разница осмысленная, а не недосмотр: поиск временный, и предел
|
|
2538
|
-
// «3 из 3» при найденной одной карточке врал бы. Отбор постоянный, и
|
|
2539
|
-
// на отборе по одному проекту «в работе 3 из 3» должно значить «по
|
|
2540
|
-
// этому проекту» — иначе предел ни о чём.
|
|
2541
2737
|
const all = tasksOf(state.tasks, col.id).filter((task) => matchesFilters(task, filters))
|
|
2542
2738
|
const items = sortTasks(
|
|
2543
|
-
all.filter((task) => matchesQuery(task,
|
|
2739
|
+
all.filter((task) => matchesQuery(task, debouncedQuery))
|
|
2544
2740
|
.filter((task) => !onlyWaiting || task.state === 'waiting')
|
|
2545
2741
|
.filter((task) => !onlyOverdue || task.overdue === true)
|
|
2546
|
-
.filter((task) => !onlyMine || task.assignee === me)
|
|
2742
|
+
.filter((task) => !onlyMine || task.assignee === me)
|
|
2743
|
+
.filter((task) => {
|
|
2744
|
+
if (quickFilter === 'active') return task.column === 'in-progress'
|
|
2745
|
+
if (quickFilter === 'review') return task.column === 'review'
|
|
2746
|
+
if (quickFilter === 'epics') return task.isEpic === true
|
|
2747
|
+
if (quickFilter === 'stale') return task.stale === true
|
|
2748
|
+
return true
|
|
2749
|
+
}),
|
|
2547
2750
|
order,
|
|
2548
2751
|
)
|
|
2549
|
-
// Ключ группы решает переключатель: заголовки, сворачивание и
|
|
2550
|
-
// счётчики остаются те же, меняется только по чему делим.
|
|
2551
2752
|
const groupKeyOf = (task) => (grouping === 'assignee'
|
|
2552
2753
|
? (task.assignee || '')
|
|
2553
2754
|
: (task.repo || ''))
|
|
@@ -2556,7 +2757,7 @@ window.__ModuleLoader__.load({
|
|
|
2556
2757
|
: items
|
|
2557
2758
|
const limit = col.limit
|
|
2558
2759
|
const overLimit = limit !== undefined && limit !== null && all.length > limit
|
|
2559
|
-
const slot = (index) =>
|
|
2760
|
+
const slot = (index) => h('div', {
|
|
2560
2761
|
className: 'dkb-slot' + (dropAt && dropAt.column === col.id && dropAt.index === index ? ' dkb-slotOn' : ''),
|
|
2561
2762
|
key: 's' + index,
|
|
2562
2763
|
onDragOver: (e) => {
|
|
@@ -2569,8 +2770,6 @@ window.__ModuleLoader__.load({
|
|
|
2569
2770
|
|
|
2570
2771
|
const rows = []
|
|
2571
2772
|
rows.push(slot(0))
|
|
2572
|
-
// Заголовок рисуем при смене проекта. Задачи приходят уже
|
|
2573
|
-
// разложенными, поэтому группы идут подряд и разрываться не могут.
|
|
2574
2773
|
let group = null
|
|
2575
2774
|
const sizes = new Map()
|
|
2576
2775
|
for (const x of ordered) sizes.set(groupKeyOf(x), (sizes.get(groupKeyOf(x)) || 0) + 1)
|
|
@@ -2581,20 +2780,18 @@ window.__ModuleLoader__.load({
|
|
|
2581
2780
|
if (manyGroups && repo !== group) {
|
|
2582
2781
|
group = repo
|
|
2583
2782
|
const shut = groups[col.id + '/' + repo] === true
|
|
2584
|
-
rows.push(
|
|
2783
|
+
rows.push(h('button', {
|
|
2585
2784
|
type: 'button', className: 'dkb-groupHead', key: 'g' + repo,
|
|
2586
2785
|
'aria-expanded': !shut,
|
|
2587
2786
|
onClick: () => setGroups((cur) => Object.assign({}, cur, { [col.id + '/' + repo]: !shut })),
|
|
2588
2787
|
},
|
|
2589
|
-
|
|
2788
|
+
h('span', null,
|
|
2590
2789
|
repo || t(grouping === 'assignee' ? 'group.nobody' : 'board.noRepo')),
|
|
2591
|
-
|
|
2592
|
-
// пустой, — то же зло, что и свёрнутая колонка без числа.
|
|
2593
|
-
React.createElement('span', { className: 'dkb-groupCount' }, String(sizes.get(repo))),
|
|
2790
|
+
h('span', { className: 'dkb-groupCount' }, String(sizes.get(repo))),
|
|
2594
2791
|
))
|
|
2595
2792
|
}
|
|
2596
2793
|
if (manyGroups && groups[col.id + '/' + (task.repo || '')] === true) return
|
|
2597
|
-
rows.push(
|
|
2794
|
+
rows.push(h(TaskCard, {
|
|
2598
2795
|
key: task.id, task, t, now: state.now,
|
|
2599
2796
|
columns: (state.columns || []).map((c) => c.id),
|
|
2600
2797
|
menuOpen: cardMenu,
|
|
@@ -2610,9 +2807,6 @@ window.__ModuleLoader__.load({
|
|
|
2610
2807
|
sameSession: hoverSession !== '' && task.sessionId === hoverSession,
|
|
2611
2808
|
onHover: setHoverSession,
|
|
2612
2809
|
onRevive: async (x) => {
|
|
2613
|
-
// Переиспользуем обычный запуск: он сам пробует возобновить
|
|
2614
|
-
// прежнюю сессию и только потом заводит новую. Здесь важно
|
|
2615
|
-
// сказать, что именно вышло.
|
|
2616
2810
|
try {
|
|
2617
2811
|
const out = await api('/task/' + encodeURIComponent(x.id) + '/start', {
|
|
2618
2812
|
method: 'POST', body: JSON.stringify({ revive: true }),
|
|
@@ -2624,8 +2818,6 @@ window.__ModuleLoader__.load({
|
|
|
2624
2818
|
onStop: async (x) => {
|
|
2625
2819
|
try {
|
|
2626
2820
|
const out = await api('/task/' + encodeURIComponent(x.id) + '/stop', { method: 'POST', body: '{}' })
|
|
2627
|
-
// «Остановлено» и «он и так стоял» — разные ответы, и второй
|
|
2628
|
-
// нельзя выдавать за первый.
|
|
2629
2821
|
if (out && out.acted !== 'stopped') setError(t('stop.' + out.acted))
|
|
2630
2822
|
await load(board)
|
|
2631
2823
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
@@ -2635,8 +2827,6 @@ window.__ModuleLoader__.load({
|
|
|
2635
2827
|
try {
|
|
2636
2828
|
const out = await api('/sessions')
|
|
2637
2829
|
const live = (out && out.sessions) || []
|
|
2638
|
-
// Живых сессий нет — ставить некуда, и сказать об этом надо
|
|
2639
|
-
// сразу, а не показывать пустой список.
|
|
2640
2830
|
if (live.length === 0) { setError(t('error.no-live-sessions')); return }
|
|
2641
2831
|
setDialog({ mode: 'queue', task: x, sessions: live, sessionId: live[0].sessionId, error: '' })
|
|
2642
2832
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
@@ -2666,37 +2856,33 @@ window.__ModuleLoader__.load({
|
|
|
2666
2856
|
? all.length + ' / ' + t('board.limit', { n: limit })
|
|
2667
2857
|
: String(all.length)
|
|
2668
2858
|
|
|
2669
|
-
return
|
|
2859
|
+
return h('div', {
|
|
2670
2860
|
className: 'dkb-col'
|
|
2671
2861
|
+ (overColumn === col.id ? ' dkb-colOver' : '')
|
|
2672
2862
|
+ (shut ? ' dkb-colShut' : ''),
|
|
2673
2863
|
key: col.id,
|
|
2674
|
-
// Свёрнутая колонка остаётся целью переноса: иначе карточку было бы
|
|
2675
|
-
// некуда положить, не развернув её сперва.
|
|
2676
2864
|
onDragOver: (e) => { e.preventDefault(); setOverColumn(col.id) },
|
|
2677
2865
|
onDragLeave: () => { setOverColumn(''); setDropAt(null) },
|
|
2678
2866
|
onDrop: (e) => { e.preventDefault(); drop(col.id, items.length) },
|
|
2679
2867
|
},
|
|
2680
|
-
|
|
2868
|
+
h('button', {
|
|
2681
2869
|
type: 'button', className: 'dkb-colHead',
|
|
2682
2870
|
'aria-expanded': !shut,
|
|
2683
2871
|
title: t(shut ? 'board.expand' : 'board.collapse'),
|
|
2684
2872
|
onClick: toggle,
|
|
2685
2873
|
},
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
// выглядящая пустой, — худшее, что может сделать сворачивание.
|
|
2690
|
-
React.createElement('span', {
|
|
2874
|
+
h('span', { className: 'dkb-dot', 'data-col': col.id }),
|
|
2875
|
+
h('span', { className: 'dkb-colName' }, columnTitle(col, t)),
|
|
2876
|
+
h('span', {
|
|
2691
2877
|
className: 'dkb-count' + (overLimit ? ' dkb-countOver' : ''),
|
|
2692
2878
|
'data-col': col.id,
|
|
2693
2879
|
}, count),
|
|
2694
2880
|
),
|
|
2695
|
-
shut ? null :
|
|
2881
|
+
shut ? null : h('div', { className: 'dkb-list' },
|
|
2696
2882
|
all.length === 0
|
|
2697
|
-
?
|
|
2883
|
+
? h('div', { className: 'dkb-empty' }, t('board.empty'))
|
|
2698
2884
|
: items.length === 0
|
|
2699
|
-
?
|
|
2885
|
+
? h('div', { className: 'dkb-empty' }, t('board.noMatch'))
|
|
2700
2886
|
: null,
|
|
2701
2887
|
rows,
|
|
2702
2888
|
),
|
|
@@ -2704,24 +2890,18 @@ window.__ModuleLoader__.load({
|
|
|
2704
2890
|
}),
|
|
2705
2891
|
)
|
|
2706
2892
|
|
|
2707
|
-
// Окно, а не панель справа: в 420 пикселей помещались описание и журнал,
|
|
2708
|
-
// но не план с отметками. Живёт внутри экрана доски — сама доска лежит в
|
|
2709
|
-
// колонке разговора, и выход за её пределы наложился бы на чужие части
|
|
2710
|
-
// оболочки.
|
|
2711
2893
|
const panel = openTask
|
|
2712
|
-
?
|
|
2894
|
+
? h('div', {
|
|
2713
2895
|
className: 'dkb-modal',
|
|
2714
2896
|
onClick: () => setOpenTask(null),
|
|
2715
|
-
},
|
|
2897
|
+
}, h('div', {
|
|
2716
2898
|
className: 'dkb-panel dkb-panelWide',
|
|
2717
2899
|
role: 'dialog', 'aria-modal': 'true',
|
|
2718
2900
|
onClick: (e) => e.stopPropagation(),
|
|
2719
2901
|
},
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
React.createElement('label', { className: 'dkb-label' }, t('reasoning.label')),
|
|
2724
|
-
React.createElement('select', {
|
|
2902
|
+
h('div', { className: 'dkb-row' },
|
|
2903
|
+
h('label', { className: 'dkb-label' }, t('reasoning.label')),
|
|
2904
|
+
h('select', {
|
|
2725
2905
|
className: 'dkb-select',
|
|
2726
2906
|
value: openTask.reasoningEffort || '',
|
|
2727
2907
|
onChange: async (e) => {
|
|
@@ -2732,53 +2912,51 @@ window.__ModuleLoader__.load({
|
|
|
2732
2912
|
method: 'POST',
|
|
2733
2913
|
body: JSON.stringify({ reasoningEffort: val }),
|
|
2734
2914
|
})
|
|
2735
|
-
} catch {}
|
|
2915
|
+
} catch { /* update reasoning effort best-effort */ }
|
|
2736
2916
|
},
|
|
2737
2917
|
},
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2918
|
+
h('option', { value: '' }, t('reasoning.default')),
|
|
2919
|
+
h('option', { value: 'low' }, t('reasoning.low')),
|
|
2920
|
+
h('option', { value: 'medium' }, t('reasoning.medium')),
|
|
2921
|
+
h('option', { value: 'high' }, t('reasoning.high')),
|
|
2742
2922
|
),
|
|
2743
2923
|
),
|
|
2744
|
-
|
|
2745
|
-
|
|
2924
|
+
h('div', { className: 'dkb-row' },
|
|
2925
|
+
h('div', { className: 'dkb-taskHead', style: { flex: 1 } },
|
|
2746
2926
|
taskRef(openTask, t),
|
|
2747
|
-
(openTask.labels || []).map((l) =>
|
|
2927
|
+
(openTask.labels || []).map((l) => h('span', {
|
|
2748
2928
|
className: 'dkb-tag', key: l, style: tagStyle(openTask, l),
|
|
2749
2929
|
}, l))),
|
|
2750
|
-
|
|
2930
|
+
h('button', {
|
|
2751
2931
|
type: 'button', className: 'dkb-discard', onClick: () => setOpenTask(null),
|
|
2752
2932
|
}, t('panel.close')),
|
|
2753
2933
|
),
|
|
2754
|
-
|
|
2755
|
-
|
|
2934
|
+
h('div', { className: 'dkb-title' }, openTask.title),
|
|
2935
|
+
h('div', { className: 'dkb-note' },
|
|
2756
2936
|
t('panel.created', { ago: agoText(state.now, openTask.createdAt, t) })
|
|
2757
2937
|
+ (openTask.assignee ? ' · ' + t('panel.assignee', { who: openTask.assignee }) : '')
|
|
2758
|
-
// Автор стоит рядом с датой заведения: это одно событие — кто и
|
|
2759
|
-
// когда завёл, — и разносить его по разным углам незачем.
|
|
2760
2938
|
+ (openTask.author ? ' · ' + t('panel.author', { who: openTask.author }) : ''),
|
|
2761
2939
|
' · ',
|
|
2762
2940
|
t('panel.updated', { ago: agoText(state.now, openTask.updatedAt, t) }),
|
|
2763
2941
|
openTask.issueUrl
|
|
2764
|
-
?
|
|
2942
|
+
? h('a', {
|
|
2765
2943
|
className: 'dkb-issueLink', href: openTask.issueUrl,
|
|
2766
2944
|
target: '_blank', rel: 'noreferrer noopener',
|
|
2767
2945
|
}, ' · ' + t('panel.issue') + ' ↗')
|
|
2768
2946
|
: null),
|
|
2769
2947
|
openTask.worktree
|
|
2770
|
-
?
|
|
2948
|
+
? h('div', { className: 'dkb-note', style: { wordBreak: 'break-all' } },
|
|
2771
2949
|
t('panel.worktree', { path: openTask.worktree })
|
|
2772
2950
|
+ (openTask.branch ? ' (' + openTask.branch + ')' : ''))
|
|
2773
2951
|
: null,
|
|
2774
2952
|
openTask.permission && !openTask.permissionConfirmedAt
|
|
2775
|
-
?
|
|
2953
|
+
? h('div', {
|
|
2776
2954
|
className: 'dkb-warnBox',
|
|
2777
2955
|
style: { margin: '4px 0', padding: 10, background: 'var(--dsw-alias-state-warn-bg, #fff3cd)', border: '1px solid var(--dsw-alias-state-warn-border, #ffeeba)', borderRadius: 6 },
|
|
2778
2956
|
},
|
|
2779
|
-
|
|
2957
|
+
h('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-state-warn-primary, #856404)' } },
|
|
2780
2958
|
t('task.permissionGateWarning', { permission: openTask.permission })),
|
|
2781
|
-
|
|
2959
|
+
h('button', {
|
|
2782
2960
|
type: 'button', className: 'dkb-save',
|
|
2783
2961
|
onClick: async () => {
|
|
2784
2962
|
try {
|
|
@@ -2790,59 +2968,53 @@ window.__ModuleLoader__.load({
|
|
|
2790
2968
|
}, t('task.confirmPermission')),
|
|
2791
2969
|
)
|
|
2792
2970
|
: null,
|
|
2793
|
-
// Пустое тело — отсутствие раздела, а не пустой раздел с заголовком.
|
|
2794
2971
|
body === null
|
|
2795
|
-
?
|
|
2972
|
+
? h('p', { className: 'dkb-note' }, t('panel.bodyLoading'))
|
|
2796
2973
|
: body.length === 0
|
|
2797
2974
|
? null
|
|
2798
|
-
:
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
React.createElement('div', { className: 'dkb-field' },
|
|
2804
|
-
React.createElement('span', { className: 'dkb-label' }, t('panel.priority')),
|
|
2805
|
-
React.createElement('select', {
|
|
2975
|
+
: h('div', { className: 'dkb-mdBody' }, renderBlocks(body)),
|
|
2976
|
+
h('div', { className: 'dkb-edit' },
|
|
2977
|
+
h('div', { className: 'dkb-field' },
|
|
2978
|
+
h('span', { className: 'dkb-label' }, t('panel.priority')),
|
|
2979
|
+
h('select', {
|
|
2806
2980
|
className: 'dkb-input', value: editPriority,
|
|
2807
2981
|
onChange: (e) => setEditPriority(e.target.value),
|
|
2808
2982
|
},
|
|
2809
|
-
|
|
2810
|
-
['high', 'medium', 'low'].map((p) =>
|
|
2983
|
+
h('option', { value: '' }, t('panel.priorityNone')),
|
|
2984
|
+
['high', 'medium', 'low'].map((p) => h('option', { value: p, key: p }, t('priority.' + p)))),
|
|
2811
2985
|
),
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2986
|
+
h('div', { className: 'dkb-field' },
|
|
2987
|
+
h('span', { className: 'dkb-label' }, t('panel.labels')),
|
|
2988
|
+
h('input', {
|
|
2815
2989
|
className: 'dkb-input', value: editLabels,
|
|
2816
2990
|
placeholder: t('dialog.labelsHint'),
|
|
2817
2991
|
onChange: (e) => setEditLabels(e.target.value),
|
|
2818
2992
|
}),
|
|
2819
2993
|
),
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2994
|
+
h('div', { className: 'dkb-field' },
|
|
2995
|
+
h('span', { className: 'dkb-label' }, t('panel.due')),
|
|
2996
|
+
h('input', {
|
|
2823
2997
|
type: 'date', className: 'dkb-input', value: editDue,
|
|
2824
2998
|
onChange: (e) => setEditDue(e.target.value),
|
|
2825
2999
|
}),
|
|
2826
3000
|
),
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
3001
|
+
h('div', { className: 'dkb-field' },
|
|
3002
|
+
h('span', { className: 'dkb-label' }, t('task.cron')),
|
|
3003
|
+
h('input', {
|
|
2830
3004
|
className: 'dkb-input', value: editCron,
|
|
2831
3005
|
placeholder: '* * * * *',
|
|
2832
3006
|
onChange: (e) => setEditCron(e.target.value),
|
|
2833
3007
|
}),
|
|
2834
3008
|
openTask.nextRunAt
|
|
2835
|
-
?
|
|
3009
|
+
? h('p', { className: 'dkb-hint' },
|
|
2836
3010
|
t('task.nextRun', { date: new Date(openTask.nextRunAt).toLocaleString() }))
|
|
2837
3011
|
: null,
|
|
2838
3012
|
),
|
|
2839
|
-
|
|
3013
|
+
h('button', {
|
|
2840
3014
|
type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
|
|
2841
3015
|
onClick: async () => {
|
|
2842
3016
|
try {
|
|
2843
3017
|
const labels = editLabels.split(',').map((s) => s.trim()).filter(Boolean)
|
|
2844
|
-
// Пустая дата снимает дедлайн (0); заполненная — полночь
|
|
2845
|
-
// этого дня по местному времени.
|
|
2846
3018
|
const dueAt = editDue === ''
|
|
2847
3019
|
? 0
|
|
2848
3020
|
: new Date(editDue + 'T00:00:00').getTime()
|
|
@@ -2857,9 +3029,9 @@ window.__ModuleLoader__.load({
|
|
|
2857
3029
|
}, t('panel.saveEdit')),
|
|
2858
3030
|
),
|
|
2859
3031
|
openTask.progressDump
|
|
2860
|
-
?
|
|
2861
|
-
|
|
2862
|
-
|
|
3032
|
+
? h('div', null,
|
|
3033
|
+
h('div', { className: 'dkb-group' }, t('task.progressDump')),
|
|
3034
|
+
h('pre', {
|
|
2863
3035
|
style: {
|
|
2864
3036
|
background: 'var(--dsw-alias-bg-layer-1, #f6f8fa)', padding: 8, borderRadius: 6, fontSize: 12,
|
|
2865
3037
|
whiteSpace: 'pre-wrap', maxHeight: 150, overflowY: 'auto', margin: 0,
|
|
@@ -2867,21 +3039,155 @@ window.__ModuleLoader__.load({
|
|
|
2867
3039
|
}, openTask.progressDump),
|
|
2868
3040
|
)
|
|
2869
3041
|
: null,
|
|
3042
|
+
h('div', { style: { margin: '6px 0', padding: 10, background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 6 } },
|
|
3043
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
|
|
3044
|
+
h('span', { className: 'dkb-group', style: { margin: 0 } },
|
|
3045
|
+
'👑 ' + t('epic.subtasks') + (subtasks.length > 0 ? ` (${subtasks.filter(s => s.column === 'done').length}/${subtasks.length})` : '')
|
|
3046
|
+
),
|
|
3047
|
+
h('button', {
|
|
3048
|
+
type: 'button', className: 'dkb-discard',
|
|
3049
|
+
style: { padding: '2px 8px', fontSize: 11 },
|
|
3050
|
+
onClick: () => setTriageOpen(!triageOpen),
|
|
3051
|
+
}, triageOpen ? t('dialog.cancel') : '⚡ ' + t('epic.triage')),
|
|
3052
|
+
),
|
|
3053
|
+
triageOpen ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, margin: '8px 0', padding: 8, background: 'var(--dsw-alias-bg-layer-3)', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l1)' } },
|
|
3054
|
+
h('span', { style: { fontSize: 12, fontWeight: 600 } }, t('epic.triageTitle')),
|
|
3055
|
+
h('span', { className: 'dkb-hint', style: { margin: 0 } }, t('epic.triageHelp')),
|
|
3056
|
+
h('textarea', {
|
|
3057
|
+
className: 'dkb-area',
|
|
3058
|
+
rows: 4,
|
|
3059
|
+
placeholder: 'Subtask 1\nSubtask 2\nSubtask 3...',
|
|
3060
|
+
value: triageText,
|
|
3061
|
+
onChange: (e) => setTriageText(e.target.value),
|
|
3062
|
+
}),
|
|
3063
|
+
openTask.owner && openTask.repo ? h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, cursor: 'pointer' } },
|
|
3064
|
+
h('input', {
|
|
3065
|
+
type: 'checkbox',
|
|
3066
|
+
checked: triageGitea,
|
|
3067
|
+
onChange: (e) => setTriageGitea(e.target.checked),
|
|
3068
|
+
}),
|
|
3069
|
+
h('span', null, t('epic.createGiteaIssues'))
|
|
3070
|
+
) : null,
|
|
3071
|
+
h('div', { style: { display: 'flex', gap: 6 } },
|
|
3072
|
+
h('button', {
|
|
3073
|
+
type: 'button', className: 'dkb-save',
|
|
3074
|
+
disabled: triageSubmitting || !triageText.trim(),
|
|
3075
|
+
onClick: async () => {
|
|
3076
|
+
const lines = triageText.split('\n').map(s => s.trim()).filter(Boolean)
|
|
3077
|
+
if (lines.length === 0) return
|
|
3078
|
+
setTriageSubmitting(true)
|
|
3079
|
+
try {
|
|
3080
|
+
const out = await api('/task/' + encodeURIComponent(openTask.id) + '/triage', {
|
|
3081
|
+
method: 'POST',
|
|
3082
|
+
body: JSON.stringify({
|
|
3083
|
+
subtasks: lines.map(title => ({ title })),
|
|
3084
|
+
createGiteaIssues: triageGitea,
|
|
3085
|
+
}),
|
|
3086
|
+
})
|
|
3087
|
+
if (out && out.subtasks) setSubtasks(out.subtasks)
|
|
3088
|
+
if (out && out.epic) setOpenTask(out.epic)
|
|
3089
|
+
setTriageText('')
|
|
3090
|
+
setTriageOpen(false)
|
|
3091
|
+
await load(board)
|
|
3092
|
+
} catch (err) { setError(t(err.key || 'error.unknown')) }
|
|
3093
|
+
finally { setTriageSubmitting(false) }
|
|
3094
|
+
},
|
|
3095
|
+
}, triageSubmitting ? '...' : t('epic.triageSubmit')),
|
|
3096
|
+
h('button', {
|
|
3097
|
+
type: 'button', className: 'dkb-discard',
|
|
3098
|
+
onClick: () => setTriageOpen(false),
|
|
3099
|
+
}, t('dialog.cancel')),
|
|
3100
|
+
)
|
|
3101
|
+
) : null,
|
|
3102
|
+
subtasks.length > 0 ? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
3103
|
+
subtasks.map((st) => h('li', {
|
|
3104
|
+
key: st.id,
|
|
3105
|
+
style: {
|
|
3106
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
3107
|
+
padding: '4px 8px', background: 'var(--dsw-alias-bg-layer-2)',
|
|
3108
|
+
borderRadius: 4, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
|
|
3109
|
+
},
|
|
3110
|
+
},
|
|
3111
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, flex: 1 } },
|
|
3112
|
+
h('span', { style: { color: st.column === 'done' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-tertiary)' } },
|
|
3113
|
+
st.column === 'done' ? '✓' : '•'
|
|
3114
|
+
),
|
|
3115
|
+
h('span', { style: { textDecoration: st.column === 'done' ? 'line-through' : 'none', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, st.title),
|
|
3116
|
+
st.issueNumber ? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, '#' + st.issueNumber) : null,
|
|
3117
|
+
),
|
|
3118
|
+
h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('column.' + st.column)),
|
|
3119
|
+
))
|
|
3120
|
+
) : h('p', { className: 'dkb-hint', style: { margin: '4px 0' } }, t('epic.noSubtasks')),
|
|
3121
|
+
h('div', { style: { display: 'flex', gap: 6, marginTop: 6 } },
|
|
3122
|
+
h('input', {
|
|
3123
|
+
className: 'dkb-input',
|
|
3124
|
+
style: { flex: 1, fontSize: 12 },
|
|
3125
|
+
placeholder: t('epic.addSubtask'),
|
|
3126
|
+
value: newSubtaskTitle,
|
|
3127
|
+
onChange: (e) => setNewSubtaskTitle(e.target.value),
|
|
3128
|
+
onKeyDown: async (e) => {
|
|
3129
|
+
if (e.key === 'Enter' && newSubtaskTitle.trim()) {
|
|
3130
|
+
e.preventDefault()
|
|
3131
|
+
const title = newSubtaskTitle.trim()
|
|
3132
|
+
setNewSubtaskTitle('')
|
|
3133
|
+
try {
|
|
3134
|
+
const out = await api('/task/' + encodeURIComponent(openTask.id) + '/subtask', {
|
|
3135
|
+
method: 'POST',
|
|
3136
|
+
body: JSON.stringify({ title }),
|
|
3137
|
+
})
|
|
3138
|
+
if (out && out.subtask) {
|
|
3139
|
+
setSubtasks(prev => [...prev, out.subtask])
|
|
3140
|
+
await load(board)
|
|
3141
|
+
}
|
|
3142
|
+
} catch (err) { setError(t(err.key || 'error.unknown')) }
|
|
3143
|
+
}
|
|
3144
|
+
},
|
|
3145
|
+
}),
|
|
3146
|
+
h('button', {
|
|
3147
|
+
type: 'button', className: 'dkb-save',
|
|
3148
|
+
style: { padding: '3px 10px', fontSize: 12 },
|
|
3149
|
+
onClick: async () => {
|
|
3150
|
+
const title = newSubtaskTitle.trim()
|
|
3151
|
+
if (!title) return
|
|
3152
|
+
setNewSubtaskTitle('')
|
|
3153
|
+
try {
|
|
3154
|
+
const out = await api('/task/' + encodeURIComponent(openTask.id) + '/subtask', {
|
|
3155
|
+
method: 'POST',
|
|
3156
|
+
body: JSON.stringify({ title }),
|
|
3157
|
+
})
|
|
3158
|
+
if (out && out.subtask) {
|
|
3159
|
+
setSubtasks(prev => [...prev, out.subtask])
|
|
3160
|
+
await load(board)
|
|
3161
|
+
}
|
|
3162
|
+
} catch (err) { setError(t(err.key || 'error.unknown')) }
|
|
3163
|
+
},
|
|
3164
|
+
}, '+'),
|
|
3165
|
+
)
|
|
3166
|
+
),
|
|
3167
|
+
openTask.sessionId ? h('div', { style: { margin: '6px 0' } },
|
|
3168
|
+
h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3169
|
+
h('span', null, '🖥️ ' + t('console.tab')),
|
|
3170
|
+
h('span', { className: 'dkb-tag', style: { fontSize: 10, fontFamily: 'monospace' } }, openTask.sessionId.slice(0, 16) + '...'),
|
|
3171
|
+
),
|
|
3172
|
+
h('pre', { className: 'dkb-consolePre' },
|
|
3173
|
+
openTask.progressDump || (openTask.runs && openTask.runs[0] ? `[${new Date(openTask.runs[0].startedAt).toLocaleTimeString()}] ${openTask.runs[0].model || 'Agent'} active\nResult: ${openTask.runs[0].result || 'running'}` : t('console.empty'))
|
|
3174
|
+
)
|
|
3175
|
+
) : null,
|
|
2870
3176
|
openTask.runs && openTask.runs.length
|
|
2871
|
-
?
|
|
2872
|
-
|
|
2873
|
-
|
|
3177
|
+
? h('div', null,
|
|
3178
|
+
h('div', { className: 'dkb-group' }, t('task.runs')),
|
|
3179
|
+
h('ul', { className: 'dkb-log' }, openTask.runs.map((r) => h('li', {
|
|
2874
3180
|
className: 'dkb-logRow', key: r.id,
|
|
2875
3181
|
style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
|
|
2876
3182
|
},
|
|
2877
|
-
|
|
3183
|
+
h('span', null,
|
|
2878
3184
|
(r.result === 'succeeded' ? '✓ ' : r.result === 'failed' ? '✕ ' : '▸ ')
|
|
2879
3185
|
+ new Date(r.startedAt).toLocaleTimeString()
|
|
2880
3186
|
+ (r.model ? ' · ' + r.model : '')
|
|
2881
3187
|
+ (r.error ? ' · ' + r.error : '')
|
|
2882
3188
|
),
|
|
2883
3189
|
r.sessionId
|
|
2884
|
-
?
|
|
3190
|
+
? h('button', {
|
|
2885
3191
|
type: 'button',
|
|
2886
3192
|
className: 'dkb-discard',
|
|
2887
3193
|
style: { padding: '2px 6px', fontSize: 11 },
|
|
@@ -2897,9 +3203,7 @@ window.__ModuleLoader__.load({
|
|
|
2897
3203
|
: null,
|
|
2898
3204
|
))))
|
|
2899
3205
|
: null,
|
|
2900
|
-
|
|
2901
|
-
// «взять» и «отказаться» значит предлагать бессмысленное действие.
|
|
2902
|
-
React.createElement('button', {
|
|
3206
|
+
h('button', {
|
|
2903
3207
|
type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
|
|
2904
3208
|
onClick: async () => {
|
|
2905
3209
|
const mine = openTask.assignee !== '' && openTask.assignee !== undefined
|
|
@@ -2914,12 +3218,9 @@ window.__ModuleLoader__.load({
|
|
|
2914
3218
|
},
|
|
2915
3219
|
}, openTask.assignee ? t('panel.assignDrop') : t('panel.assignMe')),
|
|
2916
3220
|
openTask.sessionId
|
|
2917
|
-
?
|
|
3221
|
+
? h('button', {
|
|
2918
3222
|
type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
|
|
2919
3223
|
onClick: () => {
|
|
2920
|
-
// Тот же путь, что и сразу после запуска: задача, идущая
|
|
2921
|
-
// вторые сутки, иначе ищется в общем списке по имени вида
|
|
2922
|
-
// kanban-9e84cae6-…
|
|
2923
3224
|
if (!openSession(props.ctx, openTask.sessionId)) {
|
|
2924
3225
|
setError(t('error.sessionNotOpened', { id: openTask.sessionId }))
|
|
2925
3226
|
return
|
|
@@ -2930,31 +3231,31 @@ window.__ModuleLoader__.load({
|
|
|
2930
3231
|
}, t('panel.openChat'))
|
|
2931
3232
|
: null,
|
|
2932
3233
|
log.length
|
|
2933
|
-
?
|
|
2934
|
-
|
|
2935
|
-
|
|
3234
|
+
? h('div', null,
|
|
3235
|
+
h('div', { className: 'dkb-group' }, t('panel.log')),
|
|
3236
|
+
h('ul', { className: 'dkb-log' }, log.map((row) => h('li', {
|
|
2936
3237
|
className: 'dkb-logRow', key: row.id,
|
|
2937
3238
|
}, `${t('column.' + row.toCol)} ← ${row.source}${row.detail ? ' · ' + row.detail : ''}`))))
|
|
2938
3239
|
: null,
|
|
2939
3240
|
openTask.plan && openTask.plan.items && openTask.plan.items.length
|
|
2940
|
-
?
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
openTask.plan.items.map((item, i) =>
|
|
3241
|
+
? h('div', null,
|
|
3242
|
+
h('div', { className: 'dkb-group' }, t('plan.title')),
|
|
3243
|
+
h('p', { className: 'dkb-hint' }, planLine(openTask, t)),
|
|
3244
|
+
h('ul', { className: 'dkb-planList' },
|
|
3245
|
+
openTask.plan.items.map((item, i) => h('li', {
|
|
2945
3246
|
key: i,
|
|
2946
3247
|
className: 'dkb-planItem'
|
|
2947
3248
|
+ (item.done ? ' dkb-planDone' : '')
|
|
2948
3249
|
+ (item.active ? ' dkb-planActive' : ''),
|
|
2949
3250
|
},
|
|
2950
|
-
|
|
3251
|
+
h('span', { className: 'dkb-planMark' },
|
|
2951
3252
|
item.done ? '✓' : item.active ? '▸' : '·'),
|
|
2952
|
-
|
|
3253
|
+
h('span', null, item.text),
|
|
2953
3254
|
))),
|
|
2954
3255
|
)
|
|
2955
3256
|
: null,
|
|
2956
3257
|
openTask.rejectReason
|
|
2957
|
-
?
|
|
3258
|
+
? h('div', {
|
|
2958
3259
|
className: 'dkb-warnBox',
|
|
2959
3260
|
style: {
|
|
2960
3261
|
margin: '4px 0', padding: 10,
|
|
@@ -2963,13 +3264,13 @@ window.__ModuleLoader__.load({
|
|
|
2963
3264
|
borderRadius: 6,
|
|
2964
3265
|
},
|
|
2965
3266
|
},
|
|
2966
|
-
|
|
3267
|
+
h('div', {
|
|
2967
3268
|
style: { marginBottom: 4, fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-state-danger-primary, #cf222e)' },
|
|
2968
3269
|
}, '⚠️ ' + t('acceptance.rejectedAlert', { reason: openTask.rejectReason })),
|
|
2969
3270
|
)
|
|
2970
3271
|
: null,
|
|
2971
3272
|
openTask.worktree
|
|
2972
|
-
?
|
|
3273
|
+
? h('div', {
|
|
2973
3274
|
style: {
|
|
2974
3275
|
margin: '8px 0', padding: 12,
|
|
2975
3276
|
background: 'var(--dsw-alias-bg-layer-3)',
|
|
@@ -2977,13 +3278,13 @@ window.__ModuleLoader__.load({
|
|
|
2977
3278
|
borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
|
|
2978
3279
|
},
|
|
2979
3280
|
},
|
|
2980
|
-
|
|
2981
|
-
|
|
3281
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3282
|
+
h('div', { className: 'dkb-group', style: { margin: 0 } },
|
|
2982
3283
|
t('diff.title'),
|
|
2983
3284
|
commitsData.length > 0 ? ` · ${t('diff.commits', { n: commitsData.length })}` : '',
|
|
2984
3285
|
diffData?.filesChanged ? ` · ${t('diff.filesChanged', { n: diffData.filesChanged })}` : '',
|
|
2985
3286
|
),
|
|
2986
|
-
|
|
3287
|
+
h('button', {
|
|
2987
3288
|
type: 'button', className: 'dkb-discard',
|
|
2988
3289
|
style: { padding: '2px 8px', fontSize: 11 },
|
|
2989
3290
|
onClick: () => {
|
|
@@ -2992,37 +3293,37 @@ window.__ModuleLoader__.load({
|
|
|
2992
3293
|
},
|
|
2993
3294
|
}, diffExpanded ? t('diff.hide') : t('diff.show')),
|
|
2994
3295
|
),
|
|
2995
|
-
diffExpanded ?
|
|
3296
|
+
diffExpanded ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
2996
3297
|
commitsData.length > 0
|
|
2997
|
-
?
|
|
2998
|
-
commitsData.map((cm) =>
|
|
3298
|
+
? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 4 } },
|
|
3299
|
+
commitsData.map((cm) => h('div', {
|
|
2999
3300
|
key: cm.hash,
|
|
3000
3301
|
style: { fontSize: 11, display: 'flex', gap: 6, opacity: 0.85, fontFamily: 'monospace' },
|
|
3001
3302
|
},
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3303
|
+
h('span', { style: { color: 'var(--dsw-alias-label-link, #3b82f6)' } }, cm.hash),
|
|
3304
|
+
h('span', { style: { flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, cm.subject),
|
|
3305
|
+
h('span', { style: { opacity: 0.6 } }, cm.date),
|
|
3005
3306
|
))
|
|
3006
3307
|
)
|
|
3007
3308
|
: null,
|
|
3008
3309
|
diffLoading
|
|
3009
|
-
?
|
|
3310
|
+
? h('div', { style: { fontSize: 12, opacity: 0.7 } }, 'Loading diff...')
|
|
3010
3311
|
: diffData?.diff
|
|
3011
|
-
?
|
|
3312
|
+
? h('pre', {
|
|
3012
3313
|
style: {
|
|
3013
3314
|
margin: 0, padding: 8, maxHeight: 280, overflowY: 'auto',
|
|
3014
|
-
background: 'var(--dsw-alias-bg-layer-2
|
|
3015
|
-
color: 'var(--dsw-alias-label-primary
|
|
3315
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
3316
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
3016
3317
|
fontSize: 11, fontFamily: 'monospace', borderRadius: 4, whiteSpace: 'pre-wrap',
|
|
3017
3318
|
border: '1px solid var(--dsw-alias-border-l1)',
|
|
3018
3319
|
},
|
|
3019
3320
|
}, diffData.diff)
|
|
3020
|
-
:
|
|
3321
|
+
: h('div', { style: { fontSize: 12, opacity: 0.7 } }, t('diff.empty'))
|
|
3021
3322
|
) : null
|
|
3022
3323
|
)
|
|
3023
3324
|
: null,
|
|
3024
3325
|
openTask.column === 'review'
|
|
3025
|
-
?
|
|
3326
|
+
? h('div', {
|
|
3026
3327
|
style: {
|
|
3027
3328
|
margin: '8px 0', padding: 12,
|
|
3028
3329
|
background: 'var(--dsw-alias-bg-layer-3)',
|
|
@@ -3030,13 +3331,13 @@ window.__ModuleLoader__.load({
|
|
|
3030
3331
|
borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
|
|
3031
3332
|
},
|
|
3032
3333
|
},
|
|
3033
|
-
|
|
3034
|
-
|
|
3334
|
+
h('div', { className: 'dkb-group', style: { margin: 0 } }, t('acceptance.title')),
|
|
3335
|
+
h('div', { style: { display: 'flex', gap: 8 } },
|
|
3035
3336
|
openTask.worktree
|
|
3036
|
-
?
|
|
3337
|
+
? h('button', {
|
|
3037
3338
|
type: 'button', className: 'dkb-save',
|
|
3038
3339
|
disabled: merging,
|
|
3039
|
-
style: { background: 'var(--dsw-alias-
|
|
3340
|
+
style: { background: 'var(--dsw-alias-state-success-primary, #16a34a)', color: 'var(--dsw-alias-label-inverse, #fff)' },
|
|
3040
3341
|
onClick: async () => {
|
|
3041
3342
|
try {
|
|
3042
3343
|
setMerging(true)
|
|
@@ -3048,7 +3349,7 @@ window.__ModuleLoader__.load({
|
|
|
3048
3349
|
},
|
|
3049
3350
|
}, merging ? t('acceptance.merging') : '🔀 ' + t('acceptance.acceptAndMerge'))
|
|
3050
3351
|
: null,
|
|
3051
|
-
|
|
3352
|
+
h('button', {
|
|
3052
3353
|
type: 'button', className: 'dkb-save',
|
|
3053
3354
|
onClick: async () => {
|
|
3054
3355
|
try {
|
|
@@ -3058,23 +3359,50 @@ window.__ModuleLoader__.load({
|
|
|
3058
3359
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
3059
3360
|
},
|
|
3060
3361
|
}, '✓ ' + t('acceptance.accept')),
|
|
3061
|
-
|
|
3362
|
+
h('button', {
|
|
3062
3363
|
type: 'button', className: 'dkb-discard',
|
|
3063
3364
|
onClick: () => { setRejectModalOpen(true); setRejectReasonText('') },
|
|
3064
3365
|
}, '✗ ' + t('acceptance.reject')),
|
|
3366
|
+
openTask.owner && openTask.repo
|
|
3367
|
+
? h('button', {
|
|
3368
|
+
type: 'button', className: 'dkb-save',
|
|
3369
|
+
disabled: prCreating,
|
|
3370
|
+
style: { background: 'var(--dsw-alias-state-info-primary, #0969da)', color: 'var(--dsw-alias-label-inverse, #fff)' },
|
|
3371
|
+
onClick: async () => {
|
|
3372
|
+
setPrCreating(true)
|
|
3373
|
+
try {
|
|
3374
|
+
const out = await api('/task/' + encodeURIComponent(openTask.id) + '/pr', {
|
|
3375
|
+
method: 'POST',
|
|
3376
|
+
body: JSON.stringify({ base: 'main' }),
|
|
3377
|
+
})
|
|
3378
|
+
if (out && out.pr) setCreatedPr(out.pr)
|
|
3379
|
+
await load(board)
|
|
3380
|
+
} catch (e) { setError(t(e.key || e.error || 'error.unknown')) }
|
|
3381
|
+
finally { setPrCreating(false) }
|
|
3382
|
+
},
|
|
3383
|
+
}, prCreating ? t('pr.creating') : '🐙 ' + t('pr.create'))
|
|
3384
|
+
: null,
|
|
3385
|
+
createdPr
|
|
3386
|
+
? h('a', {
|
|
3387
|
+
className: 'dkb-save',
|
|
3388
|
+
style: { background: 'var(--dsw-alias-state-success-primary, #16a34a)', color: 'var(--dsw-alias-label-inverse, #fff)', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
|
|
3389
|
+
href: createdPr.html_url,
|
|
3390
|
+
target: '_blank', rel: 'noopener noreferrer',
|
|
3391
|
+
}, '↗ ' + t('pr.view') + ` #${createdPr.number}`)
|
|
3392
|
+
: null,
|
|
3065
3393
|
),
|
|
3066
3394
|
rejectModalOpen
|
|
3067
|
-
?
|
|
3068
|
-
|
|
3069
|
-
|
|
3395
|
+
? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6 } },
|
|
3396
|
+
h('span', { className: 'dkb-label' }, t('acceptance.rejectTitle')),
|
|
3397
|
+
h('textarea', {
|
|
3070
3398
|
className: 'dkb-area',
|
|
3071
3399
|
value: rejectReasonText,
|
|
3072
3400
|
placeholder: t('acceptance.rejectReasonPlaceholder'),
|
|
3073
3401
|
onChange: (e) => setRejectReasonText(e.target.value),
|
|
3074
3402
|
rows: 3,
|
|
3075
3403
|
}),
|
|
3076
|
-
|
|
3077
|
-
|
|
3404
|
+
h('div', { style: { display: 'flex', gap: 6 } },
|
|
3405
|
+
h('button', {
|
|
3078
3406
|
type: 'button', className: 'dkb-save',
|
|
3079
3407
|
onClick: async () => {
|
|
3080
3408
|
if (!rejectReasonText.trim()) return
|
|
@@ -3089,7 +3417,7 @@ window.__ModuleLoader__.load({
|
|
|
3089
3417
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
3090
3418
|
},
|
|
3091
3419
|
}, t('acceptance.rejectSubmit')),
|
|
3092
|
-
|
|
3420
|
+
h('button', {
|
|
3093
3421
|
type: 'button', className: 'dkb-discard',
|
|
3094
3422
|
onClick: () => setRejectModalOpen(false),
|
|
3095
3423
|
}, t('dialog.cancel')),
|
|
@@ -3098,11 +3426,11 @@ window.__ModuleLoader__.load({
|
|
|
3098
3426
|
: null,
|
|
3099
3427
|
)
|
|
3100
3428
|
: null,
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3429
|
+
h('div', { style: { margin: '4px 0' } },
|
|
3430
|
+
h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3431
|
+
h('span', null, t('checklist.title')),
|
|
3104
3432
|
Array.isArray(openTask.checklist) && openTask.checklist.length > 0
|
|
3105
|
-
?
|
|
3433
|
+
? h('span', { className: 'dkb-hint', style: { fontWeight: 'normal' } },
|
|
3106
3434
|
t('checklist.progress', {
|
|
3107
3435
|
done: openTask.checklist.filter((i) => i.completed).length,
|
|
3108
3436
|
total: openTask.checklist.length,
|
|
@@ -3110,8 +3438,8 @@ window.__ModuleLoader__.load({
|
|
|
3110
3438
|
: null,
|
|
3111
3439
|
),
|
|
3112
3440
|
Array.isArray(openTask.checklist) && openTask.checklist.length > 0
|
|
3113
|
-
?
|
|
3114
|
-
openTask.checklist.map((item, idx) =>
|
|
3441
|
+
? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
3442
|
+
openTask.checklist.map((item, idx) => h('li', {
|
|
3115
3443
|
key: idx,
|
|
3116
3444
|
style: {
|
|
3117
3445
|
display: 'flex', flexDirection: 'column', gap: 2, padding: '4px 8px',
|
|
@@ -3119,8 +3447,8 @@ window.__ModuleLoader__.load({
|
|
|
3119
3447
|
borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)',
|
|
3120
3448
|
},
|
|
3121
3449
|
},
|
|
3122
|
-
|
|
3123
|
-
|
|
3450
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
3451
|
+
h('input', {
|
|
3124
3452
|
type: 'checkbox',
|
|
3125
3453
|
checked: Boolean(item.completed),
|
|
3126
3454
|
onChange: async (e) => {
|
|
@@ -3136,7 +3464,7 @@ window.__ModuleLoader__.load({
|
|
|
3136
3464
|
} catch (err) { setError(t(err.key || 'error.unknown')) }
|
|
3137
3465
|
},
|
|
3138
3466
|
}),
|
|
3139
|
-
|
|
3467
|
+
h('span', {
|
|
3140
3468
|
style: {
|
|
3141
3469
|
textDecoration: item.completed ? 'line-through' : 'none',
|
|
3142
3470
|
color: item.completed ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-primary)',
|
|
@@ -3144,11 +3472,11 @@ window.__ModuleLoader__.load({
|
|
|
3144
3472
|
},
|
|
3145
3473
|
}, item.text),
|
|
3146
3474
|
item.required !== false
|
|
3147
|
-
?
|
|
3475
|
+
? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('checklist.required'))
|
|
3148
3476
|
: null,
|
|
3149
3477
|
),
|
|
3150
3478
|
item.evidence
|
|
3151
|
-
?
|
|
3479
|
+
? h('div', {
|
|
3152
3480
|
style: {
|
|
3153
3481
|
fontSize: 11, color: 'var(--dsw-alias-label-secondary)', marginLeft: 24,
|
|
3154
3482
|
fontFamily: 'var(--dsw-font-family-mono, monospace)', whiteSpace: 'pre-wrap',
|
|
@@ -3156,9 +3484,9 @@ window.__ModuleLoader__.load({
|
|
|
3156
3484
|
}, t('checklist.evidence', { text: item.evidence }))
|
|
3157
3485
|
: null,
|
|
3158
3486
|
)))
|
|
3159
|
-
:
|
|
3160
|
-
|
|
3161
|
-
|
|
3487
|
+
: h('p', { className: 'dkb-hint' }, t('checklist.empty')),
|
|
3488
|
+
h('div', { style: { display: 'flex', gap: 6, marginTop: 4 } },
|
|
3489
|
+
h('input', {
|
|
3162
3490
|
className: 'dkb-input',
|
|
3163
3491
|
style: { flex: 1 },
|
|
3164
3492
|
placeholder: t('checklist.placeholder'),
|
|
@@ -3180,7 +3508,7 @@ window.__ModuleLoader__.load({
|
|
|
3180
3508
|
}
|
|
3181
3509
|
},
|
|
3182
3510
|
}),
|
|
3183
|
-
|
|
3511
|
+
h('button', {
|
|
3184
3512
|
type: 'button', className: 'dkb-save',
|
|
3185
3513
|
onClick: async () => {
|
|
3186
3514
|
if (!newChecklistText.trim()) return
|
|
@@ -3199,7 +3527,7 @@ window.__ModuleLoader__.load({
|
|
|
3199
3527
|
),
|
|
3200
3528
|
),
|
|
3201
3529
|
openTask.report && openTask.report.summary
|
|
3202
|
-
?
|
|
3530
|
+
? h('div', {
|
|
3203
3531
|
style: {
|
|
3204
3532
|
margin: '6px 0', padding: 10,
|
|
3205
3533
|
background: 'var(--dsw-alias-bg-layer-1)',
|
|
@@ -3207,79 +3535,79 @@ window.__ModuleLoader__.load({
|
|
|
3207
3535
|
borderRadius: 6,
|
|
3208
3536
|
},
|
|
3209
3537
|
},
|
|
3210
|
-
|
|
3211
|
-
|
|
3538
|
+
h('div', { className: 'dkb-group', style: { margin: '0 0 6px 0' } }, t('report.title')),
|
|
3539
|
+
h('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)', marginBottom: 6, fontWeight: 500 } },
|
|
3212
3540
|
openTask.report.summary),
|
|
3213
3541
|
openTask.report.changedFiles && openTask.report.changedFiles.length
|
|
3214
|
-
?
|
|
3215
|
-
|
|
3216
|
-
openTask.report.changedFiles.map((f) =>
|
|
3542
|
+
? h('div', { style: { fontSize: 12, marginBottom: 4 } },
|
|
3543
|
+
h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.changedFiles') + ': '),
|
|
3544
|
+
openTask.report.changedFiles.map((f) => h('span', { className: 'dkb-tag', key: f, style: { margin: '1px 2px', fontFamily: 'monospace' } }, f)),
|
|
3217
3545
|
)
|
|
3218
3546
|
: null,
|
|
3219
3547
|
openTask.report.checksRun && openTask.report.checksRun.length
|
|
3220
|
-
?
|
|
3221
|
-
|
|
3222
|
-
openTask.report.checksRun.map((chk, i) =>
|
|
3548
|
+
? h('div', { style: { fontSize: 12, marginBottom: 4 } },
|
|
3549
|
+
h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.checksRun') + ': '),
|
|
3550
|
+
openTask.report.checksRun.map((chk, i) => h('div', { key: i, style: { fontFamily: 'monospace', fontSize: 11, marginLeft: 8 } }, '• ' + chk)),
|
|
3223
3551
|
)
|
|
3224
3552
|
: null,
|
|
3225
3553
|
openTask.report.artifacts && openTask.report.artifacts.length
|
|
3226
|
-
?
|
|
3227
|
-
|
|
3228
|
-
openTask.report.artifacts.map((a, i) =>
|
|
3554
|
+
? h('div', { style: { fontSize: 12, marginBottom: 4 } },
|
|
3555
|
+
h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.artifacts') + ': '),
|
|
3556
|
+
openTask.report.artifacts.map((a, i) => h('span', { className: 'dkb-tag', key: i, style: { margin: '1px 2px' } }, a)),
|
|
3229
3557
|
)
|
|
3230
3558
|
: null,
|
|
3231
3559
|
openTask.report.risks && openTask.report.risks.length
|
|
3232
|
-
?
|
|
3233
|
-
|
|
3234
|
-
openTask.report.risks.map((r, i) =>
|
|
3560
|
+
? h('div', { style: { fontSize: 12 } },
|
|
3561
|
+
h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.risks') + ': '),
|
|
3562
|
+
openTask.report.risks.map((r, i) => h('div', { key: i, style: { fontSize: 11, color: 'var(--dsw-alias-state-warn-primary, #b08800)', marginLeft: 8 } }, '⚠ ' + r)),
|
|
3235
3563
|
)
|
|
3236
3564
|
: null,
|
|
3237
3565
|
)
|
|
3238
3566
|
: null,
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3567
|
+
h('div', { style: { margin: '6px 0' } },
|
|
3568
|
+
h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3569
|
+
h('span', null, t('comments.title')),
|
|
3242
3570
|
Array.isArray(openTask.comments) && openTask.comments.length > 0
|
|
3243
|
-
?
|
|
3571
|
+
? h('span', { className: 'dkb-hint' }, `(${openTask.comments.length})`)
|
|
3244
3572
|
: null,
|
|
3245
3573
|
),
|
|
3246
3574
|
Array.isArray(openTask.comments) && openTask.comments.length > 0
|
|
3247
|
-
?
|
|
3575
|
+
? h('ul', {
|
|
3248
3576
|
style: {
|
|
3249
3577
|
listStyle: 'none', padding: 0, margin: '4px 0', maxHeight: 200, overflowY: 'auto',
|
|
3250
3578
|
display: 'flex', flexDirection: 'column', gap: 6,
|
|
3251
3579
|
},
|
|
3252
3580
|
},
|
|
3253
|
-
openTask.comments.map((c, i) =>
|
|
3581
|
+
openTask.comments.map((c, i) => h('li', {
|
|
3254
3582
|
key: c.id || i,
|
|
3255
3583
|
style: {
|
|
3256
3584
|
padding: 8, background: 'var(--dsw-alias-bg-layer-3)',
|
|
3257
3585
|
borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
|
|
3258
3586
|
},
|
|
3259
3587
|
},
|
|
3260
|
-
|
|
3261
|
-
|
|
3588
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 4 } },
|
|
3589
|
+
h('span', {
|
|
3262
3590
|
className: 'dkb-tag',
|
|
3263
3591
|
style: {
|
|
3264
3592
|
fontWeight: 600,
|
|
3265
3593
|
background: c.role === 'agent' ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-2)',
|
|
3266
3594
|
},
|
|
3267
3595
|
}, c.role === 'agent' ? t('comments.authorAgent') : (c.author || t('comments.authorUser'))),
|
|
3268
|
-
|
|
3596
|
+
h('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 } },
|
|
3269
3597
|
new Date(c.createdAt || Date.now()).toLocaleTimeString()),
|
|
3270
3598
|
),
|
|
3271
|
-
|
|
3599
|
+
h('div', { style: { whiteSpace: 'pre-wrap', color: 'var(--dsw-alias-label-primary)' } }, c.text),
|
|
3272
3600
|
)))
|
|
3273
|
-
:
|
|
3274
|
-
|
|
3275
|
-
|
|
3601
|
+
: h('p', { className: 'dkb-hint' }, t('comments.empty')),
|
|
3602
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 } },
|
|
3603
|
+
h('textarea', {
|
|
3276
3604
|
className: 'dkb-area',
|
|
3277
3605
|
placeholder: t('comments.placeholder'),
|
|
3278
3606
|
value: newCommentText,
|
|
3279
3607
|
rows: 2,
|
|
3280
3608
|
onChange: (e) => setNewCommentText(e.target.value),
|
|
3281
3609
|
}),
|
|
3282
|
-
|
|
3610
|
+
h('button', {
|
|
3283
3611
|
type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
|
|
3284
3612
|
onClick: async () => {
|
|
3285
3613
|
if (!newCommentText.trim()) return
|
|
@@ -3297,10 +3625,8 @@ window.__ModuleLoader__.load({
|
|
|
3297
3625
|
),
|
|
3298
3626
|
),
|
|
3299
3627
|
openTask.archivedAt
|
|
3300
|
-
?
|
|
3628
|
+
? h('button', {
|
|
3301
3629
|
type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
|
|
3302
|
-
// Запускать работу по архивной задаче нечего: сперва она
|
|
3303
|
-
// возвращается на доску, и это одно нажатие.
|
|
3304
3630
|
onClick: async () => {
|
|
3305
3631
|
try {
|
|
3306
3632
|
await api('/task/' + encodeURIComponent(openTask.id) + '/restore', { method: 'POST', body: '{}' })
|
|
@@ -3310,70 +3636,59 @@ window.__ModuleLoader__.load({
|
|
|
3310
3636
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
3311
3637
|
},
|
|
3312
3638
|
}, t('archive.restore'))
|
|
3313
|
-
:
|
|
3314
|
-
|
|
3639
|
+
: h('div', { className: 'dkb-start' },
|
|
3640
|
+
h('div', { className: 'dkb-group' },
|
|
3315
3641
|
openTask.sessionId ? t('panel.continue') : t('panel.start')),
|
|
3316
|
-
|
|
3642
|
+
h('p', { className: 'dkb-hint' },
|
|
3317
3643
|
openTask.sessionId ? t('panel.continueHint') : t('panel.startHint')),
|
|
3318
|
-
// Корень не задан настройкой — сессия пойдёт в рабочей папке
|
|
3319
|
-
// харнесса. Молчать об этом нельзя: человек иначе узнает, где
|
|
3320
|
-
// оказался агент, только из его первых слов. Показываем фактический
|
|
3321
|
-
// корень, который приехал с доской.
|
|
3322
3644
|
!openTask.sessionId && state && !state.projectRoot.set && state.projectRoot.path !== ''
|
|
3323
|
-
?
|
|
3645
|
+
? h('p', { className: 'dkb-hint dkb-workdir' },
|
|
3324
3646
|
t('panel.workDir', { path: state.projectRoot.path }),
|
|
3325
|
-
|
|
3647
|
+
h('br'),
|
|
3326
3648
|
t('panel.workDirHint'))
|
|
3327
3649
|
: null,
|
|
3328
3650
|
React.createElement(LaunchFields, {
|
|
3329
3651
|
t, presets, agentPreset, permission, disabled: starting,
|
|
3330
3652
|
onPreset: setAgentPreset, onPermission: setPermission,
|
|
3331
|
-
// У идущей сессии профиль уже выбран: сменить его нельзя, а
|
|
3332
|
-
// показывать поле, которое ничего не меняет, — врать.
|
|
3333
3653
|
hidePreset: Boolean(openTask.sessionId),
|
|
3334
3654
|
}),
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3655
|
+
h('div', { className: 'dkb-field' },
|
|
3656
|
+
h('span', { className: 'dkb-label' }, t('panel.provider')),
|
|
3657
|
+
h('select', {
|
|
3338
3658
|
className: 'dkb-input', value: provider, disabled: starting || providers.length === 0,
|
|
3339
3659
|
onChange: (e) => { setProvider(e.target.value); setModel('') },
|
|
3340
3660
|
},
|
|
3341
|
-
|
|
3661
|
+
h('option', { value: '' },
|
|
3342
3662
|
providers.length === 0 ? t('panel.noProviders') : t('panel.pickProvider')),
|
|
3343
|
-
providers.map((p) =>
|
|
3663
|
+
providers.map((p) => h('option', { value: p.id, key: p.id }, p.name)),
|
|
3344
3664
|
),
|
|
3345
3665
|
),
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3666
|
+
h('div', { className: 'dkb-field' },
|
|
3667
|
+
h('span', { className: 'dkb-label' }, t('panel.model')),
|
|
3668
|
+
h('select', {
|
|
3349
3669
|
className: 'dkb-input', value: model,
|
|
3350
3670
|
disabled: starting || provider === '' || models.length === 0,
|
|
3351
3671
|
onChange: (e) => setModel(e.target.value),
|
|
3352
3672
|
},
|
|
3353
|
-
|
|
3673
|
+
h('option', { value: '' },
|
|
3354
3674
|
provider === '' ? t('panel.pickProviderFirst')
|
|
3355
3675
|
: models.length === 0 ? t('panel.noModels') : t('panel.pickModel')),
|
|
3356
|
-
models.map((m) =>
|
|
3676
|
+
models.map((m) => h('option', { value: m.id, key: m.id }, m.name)),
|
|
3357
3677
|
),
|
|
3358
3678
|
),
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3679
|
+
h('div', { className: 'dkb-field' },
|
|
3680
|
+
h('span', { className: 'dkb-label' }, t('panel.message')),
|
|
3681
|
+
h('textarea', {
|
|
3362
3682
|
className: 'dkb-area', value: draftText, disabled: starting,
|
|
3363
3683
|
onChange: (e) => setDraftText(e.target.value),
|
|
3364
3684
|
}),
|
|
3365
|
-
|
|
3685
|
+
h('p', { className: 'dkb-hint' }, t('panel.messageHint')),
|
|
3366
3686
|
),
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
React.createElement('div', { className: 'dkb-foot dkb-footSplit' },
|
|
3371
|
-
React.createElement('div', { className: 'dkb-footLeft' },
|
|
3372
|
-
React.createElement('button', {
|
|
3687
|
+
h('div', { className: 'dkb-foot dkb-footSplit' },
|
|
3688
|
+
h('div', { className: 'dkb-footLeft' },
|
|
3689
|
+
h('button', {
|
|
3373
3690
|
type: 'button', className: 'dkb-danger',
|
|
3374
3691
|
onClick: async () => {
|
|
3375
|
-
// Подтверждение обязательно: удаление карточки необратимо, а
|
|
3376
|
-
// сама кнопка стоит рядом с безобидными.
|
|
3377
3692
|
if (!window.confirm(t('panel.deleteConfirm', { title: openTask.title }))) return
|
|
3378
3693
|
try {
|
|
3379
3694
|
await api('/task/' + encodeURIComponent(openTask.id), { method: 'DELETE' })
|
|
@@ -3385,7 +3700,7 @@ window.__ModuleLoader__.load({
|
|
|
3385
3700
|
},
|
|
3386
3701
|
}, t('panel.delete')),
|
|
3387
3702
|
openTask.column === 'done'
|
|
3388
|
-
?
|
|
3703
|
+
? h('button', {
|
|
3389
3704
|
type: 'button', className: 'dkb-discard',
|
|
3390
3705
|
onClick: async () => {
|
|
3391
3706
|
try {
|
|
@@ -3397,7 +3712,7 @@ window.__ModuleLoader__.load({
|
|
|
3397
3712
|
}, t('panel.archive'))
|
|
3398
3713
|
: null,
|
|
3399
3714
|
openTask.column === 'cleanup' && openTask.worktree
|
|
3400
|
-
?
|
|
3715
|
+
? h('button', {
|
|
3401
3716
|
type: 'button', className: 'dkb-discard',
|
|
3402
3717
|
onClick: async () => {
|
|
3403
3718
|
try {
|
|
@@ -3415,10 +3730,10 @@ window.__ModuleLoader__.load({
|
|
|
3415
3730
|
}, t('panel.cleanupWorktree'))
|
|
3416
3731
|
: null,
|
|
3417
3732
|
),
|
|
3418
|
-
(openTask.worktree || openTask.rejectReason || (Array.isArray(openTask.runs) && openTask.runs.length > 0)) ?
|
|
3733
|
+
(openTask.worktree || openTask.rejectReason || (Array.isArray(openTask.runs) && openTask.runs.length > 0)) ? h('button', {
|
|
3419
3734
|
type: 'button',
|
|
3420
3735
|
className: 'dkb-secondary',
|
|
3421
|
-
style: { marginRight: '8px', border: '1px solid var(--dsw-alias-border-
|
|
3736
|
+
style: { marginRight: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
|
|
3422
3737
|
disabled: starting || model === '',
|
|
3423
3738
|
onClick: async () => {
|
|
3424
3739
|
setStarting(true)
|
|
@@ -3442,11 +3757,9 @@ window.__ModuleLoader__.load({
|
|
|
3442
3757
|
}
|
|
3443
3758
|
},
|
|
3444
3759
|
}, t('action.resume')) : null,
|
|
3445
|
-
|
|
3760
|
+
h('button', {
|
|
3446
3761
|
type: 'button', className: 'dkb-save', disabled: starting || model === '',
|
|
3447
3762
|
onClick: async () => {
|
|
3448
|
-
// Кнопка блокируется на время запроса: двойное нажатие подняло
|
|
3449
|
-
// бы две сессии по одной задаче.
|
|
3450
3763
|
setStarting(true)
|
|
3451
3764
|
try {
|
|
3452
3765
|
rememberLaunch(board, { provider, model, agentPreset, permission })
|
|
@@ -3456,9 +3769,6 @@ window.__ModuleLoader__.load({
|
|
|
3456
3769
|
})
|
|
3457
3770
|
setOpenTask(null)
|
|
3458
3771
|
await load(board)
|
|
3459
|
-
// Сессия поднята — но пока её не открыли, для человека
|
|
3460
|
-
// «ничего не произошло»: он остаётся на доске и не видит
|
|
3461
|
-
// чата, ради которого нажимал.
|
|
3462
3772
|
if (!openSession(props.ctx, out && out.sessionId)) {
|
|
3463
3773
|
setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
|
|
3464
3774
|
return
|
|
@@ -3478,18 +3788,18 @@ window.__ModuleLoader__.load({
|
|
|
3478
3788
|
: null
|
|
3479
3789
|
|
|
3480
3790
|
const moveDialogEl = dialog && dialog.mode === 'move'
|
|
3481
|
-
?
|
|
3482
|
-
|
|
3483
|
-
|
|
3791
|
+
? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
|
|
3792
|
+
h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
|
|
3793
|
+
h('span', { className: 'dkb-title' },
|
|
3484
3794
|
t('move.title', { column: t('column.' + dialog.column) })),
|
|
3485
|
-
|
|
3795
|
+
h('p', { className: 'dkb-note' },
|
|
3486
3796
|
cardRef(dialog.task) ? cardRef(dialog.task) + ' · ' + dialog.task.title : dialog.task.title),
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3797
|
+
h('p', { className: 'dkb-hint' }, t('move.' + dialog.column)),
|
|
3798
|
+
h('div', { className: 'dkb-row' },
|
|
3799
|
+
h('button', {
|
|
3490
3800
|
type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
|
|
3491
3801
|
}, t('dialog.cancel')),
|
|
3492
|
-
|
|
3802
|
+
h('button', {
|
|
3493
3803
|
type: 'button', className: 'dkb-save',
|
|
3494
3804
|
onClick: () => {
|
|
3495
3805
|
const d = dialog
|
|
@@ -3501,51 +3811,49 @@ window.__ModuleLoader__.load({
|
|
|
3501
3811
|
))
|
|
3502
3812
|
: null
|
|
3503
3813
|
|
|
3504
|
-
// Групповой запуск. Провайдер и модель спрашиваем один раз на всю пачку:
|
|
3505
|
-
// сессия одна, значит и модель одна.
|
|
3506
3814
|
const batchEl = dialog && dialog.mode === 'batch'
|
|
3507
|
-
?
|
|
3508
|
-
|
|
3509
|
-
|
|
3815
|
+
? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
|
|
3816
|
+
h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
|
|
3817
|
+
h('span', { className: 'dkb-title' },
|
|
3510
3818
|
t('batch.title', { n: dialog.ids.length })),
|
|
3511
|
-
|
|
3512
|
-
dialog.error ?
|
|
3513
|
-
|
|
3819
|
+
h('p', { className: 'dkb-hint' }, t('batch.hint')),
|
|
3820
|
+
dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
|
|
3821
|
+
h('ul', { className: 'dkb-batchList' },
|
|
3514
3822
|
dialog.ids.map((id, at) => {
|
|
3515
3823
|
const task = state.tasks.find((x) => x.id === id)
|
|
3516
|
-
return
|
|
3517
|
-
|
|
3518
|
-
|
|
3824
|
+
return h('li', { className: 'dkb-batchRow', key: id },
|
|
3825
|
+
h('span', { className: 'dkb-refNum' }, String(at + 1)),
|
|
3826
|
+
h('span', null, task ? task.title : id))
|
|
3519
3827
|
})),
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3828
|
+
h('div', { className: 'dkb-field' },
|
|
3829
|
+
h('span', { className: 'dkb-label' }, t('panel.provider')),
|
|
3830
|
+
h('select', {
|
|
3523
3831
|
className: 'dkb-input', value: provider,
|
|
3524
3832
|
onChange: (e) => { setProvider(e.target.value); setModel('') },
|
|
3525
3833
|
},
|
|
3526
|
-
|
|
3527
|
-
providers.map((p) =>
|
|
3834
|
+
h('option', { value: '' }, t('panel.pickProvider')),
|
|
3835
|
+
providers.map((p) => h('option', { value: p.id, key: p.id }, p.name)),
|
|
3528
3836
|
),
|
|
3529
3837
|
),
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3838
|
+
h('div', { className: 'dkb-field' },
|
|
3839
|
+
h('span', { className: 'dkb-label' }, t('panel.model')),
|
|
3840
|
+
h('select', {
|
|
3533
3841
|
className: 'dkb-input', value: model, disabled: provider === '',
|
|
3534
3842
|
onChange: (e) => setModel(e.target.value),
|
|
3535
3843
|
},
|
|
3536
|
-
|
|
3537
|
-
models.map((m) =>
|
|
3844
|
+
h('option', { value: '' }, t('panel.pickModel')),
|
|
3845
|
+
models.map((m) => h('option', { value: m.id, key: m.id }, m.name)),
|
|
3538
3846
|
),
|
|
3539
3847
|
),
|
|
3540
3848
|
React.createElement(LaunchFields, {
|
|
3541
3849
|
t, presets, agentPreset, permission,
|
|
3542
3850
|
onPreset: setAgentPreset, onPermission: setPermission,
|
|
3543
3851
|
}),
|
|
3544
|
-
|
|
3545
|
-
|
|
3852
|
+
h('div', { className: 'dkb-row' },
|
|
3853
|
+
h('button', {
|
|
3546
3854
|
type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
|
|
3547
3855
|
}, t('dialog.cancel')),
|
|
3548
|
-
|
|
3856
|
+
h('button', {
|
|
3549
3857
|
type: 'button', className: 'dkb-save', disabled: model === '',
|
|
3550
3858
|
onClick: async () => {
|
|
3551
3859
|
try {
|
|
@@ -3557,8 +3865,6 @@ window.__ModuleLoader__.load({
|
|
|
3557
3865
|
setDialog(null)
|
|
3558
3866
|
setPicked({})
|
|
3559
3867
|
await load(board)
|
|
3560
|
-
// Сессия поднята — но пока её не открыли, для человека
|
|
3561
|
-
// «ничего не произошло».
|
|
3562
3868
|
if (!openSession(props.ctx, out && out.sessionId)) {
|
|
3563
3869
|
setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
|
|
3564
3870
|
return
|
|
@@ -3574,28 +3880,28 @@ window.__ModuleLoader__.load({
|
|
|
3574
3880
|
: null
|
|
3575
3881
|
|
|
3576
3882
|
const queueEl = dialog && dialog.mode === 'queue'
|
|
3577
|
-
?
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
dialog.error ?
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3883
|
+
? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
|
|
3884
|
+
h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
|
|
3885
|
+
h('span', { className: 'dkb-title' }, t('queue.title')),
|
|
3886
|
+
h('p', { className: 'dkb-hint' }, t('queue.hint')),
|
|
3887
|
+
dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
|
|
3888
|
+
h('div', { className: 'dkb-field' },
|
|
3889
|
+
h('span', { className: 'dkb-label' }, t('queue.session')),
|
|
3890
|
+
h('select', {
|
|
3585
3891
|
className: 'dkb-input', value: dialog.sessionId,
|
|
3586
3892
|
onChange: (e) => setDialog(Object.assign({}, dialog, { sessionId: e.target.value })),
|
|
3587
|
-
}, dialog.sessions.map((one) =>
|
|
3893
|
+
}, dialog.sessions.map((one) => h('option', {
|
|
3588
3894
|
value: one.sessionId, key: one.sessionId,
|
|
3589
3895
|
}, t('queue.option', {
|
|
3590
3896
|
n: one.tasks.length,
|
|
3591
3897
|
title: (one.tasks[0] && one.tasks[0].title) || one.sessionId,
|
|
3592
3898
|
})))),
|
|
3593
3899
|
),
|
|
3594
|
-
|
|
3595
|
-
|
|
3900
|
+
h('div', { className: 'dkb-row' },
|
|
3901
|
+
h('button', {
|
|
3596
3902
|
type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
|
|
3597
3903
|
}, t('dialog.cancel')),
|
|
3598
|
-
|
|
3904
|
+
h('button', {
|
|
3599
3905
|
type: 'button', className: 'dkb-save',
|
|
3600
3906
|
onClick: async () => {
|
|
3601
3907
|
try {
|
|
@@ -3615,17 +3921,15 @@ window.__ModuleLoader__.load({
|
|
|
3615
3921
|
: null
|
|
3616
3922
|
|
|
3617
3923
|
const dialogEl = dialog && dialog.mode !== 'move' && dialog.mode !== 'batch' && dialog.mode !== 'queue'
|
|
3618
|
-
?
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3924
|
+
? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
|
|
3925
|
+
h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
|
|
3926
|
+
h('span', { className: 'dkb-title' }, t('dialog.title')),
|
|
3927
|
+
h('div', { className: 'dkb-row' },
|
|
3928
|
+
h('button', {
|
|
3623
3929
|
type: 'button', className: dialog.mode === 'own' ? 'dkb-save' : 'dkb-discard',
|
|
3624
3930
|
onClick: () => setDialog(Object.assign({}, dialog, { mode: 'own', error: '' })),
|
|
3625
3931
|
}, t('dialog.own')),
|
|
3626
|
-
|
|
3627
|
-
// значит звать туда, откуда сервер вернёт отказ.
|
|
3628
|
-
state.kind === 'simple' ? null : React.createElement('button', {
|
|
3932
|
+
state.kind === 'simple' ? null : h('button', {
|
|
3629
3933
|
type: 'button', className: dialog.mode === 'gitea' ? 'dkb-save' : 'dkb-discard',
|
|
3630
3934
|
onClick: async () => {
|
|
3631
3935
|
setDialog(Object.assign({}, dialog, { mode: 'gitea', error: '' }))
|
|
@@ -3639,93 +3943,86 @@ window.__ModuleLoader__.load({
|
|
|
3639
3943
|
},
|
|
3640
3944
|
}, t('dialog.fromGitea')),
|
|
3641
3945
|
),
|
|
3642
|
-
dialog.error ?
|
|
3946
|
+
dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
|
|
3643
3947
|
dialog.mode === 'own'
|
|
3644
|
-
?
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
React.createElement('span', { className: 'dkb-label' }, t('dialog.kind')),
|
|
3650
|
-
React.createElement('div', { className: 'dkb-row' },
|
|
3651
|
-
React.createElement('button', {
|
|
3948
|
+
? h('div', null,
|
|
3949
|
+
h('div', { className: 'dkb-field' },
|
|
3950
|
+
h('span', { className: 'dkb-label' }, t('dialog.kind')),
|
|
3951
|
+
h('div', { className: 'dkb-row' },
|
|
3952
|
+
h('button', {
|
|
3652
3953
|
type: 'button', className: dialog.project ? 'dkb-save' : 'dkb-discard',
|
|
3653
3954
|
onClick: () => setDialog(Object.assign({}, dialog, { project: true, error: '' })),
|
|
3654
3955
|
}, t('dialog.kindProject')),
|
|
3655
|
-
|
|
3956
|
+
h('button', {
|
|
3656
3957
|
type: 'button', className: dialog.project ? 'dkb-discard' : 'dkb-save',
|
|
3657
3958
|
onClick: () => setDialog(Object.assign({}, dialog, { project: false, error: '' })),
|
|
3658
3959
|
}, t('dialog.kindPlain')),
|
|
3659
3960
|
),
|
|
3660
|
-
|
|
3961
|
+
h('p', { className: 'dkb-hint' },
|
|
3661
3962
|
t(dialog.project ? 'dialog.kindProjectHint' : 'dialog.kindPlainHint')),
|
|
3662
3963
|
),
|
|
3663
3964
|
dialog.project
|
|
3664
|
-
?
|
|
3665
|
-
|
|
3666
|
-
|
|
3965
|
+
? h('div', { className: 'dkb-field' },
|
|
3966
|
+
h('span', { className: 'dkb-label' }, t('dialog.repo')),
|
|
3967
|
+
h('select', {
|
|
3667
3968
|
className: 'dkb-input', value: dialog.repo || '',
|
|
3668
3969
|
onChange: (e) => setDialog(Object.assign({}, dialog, { repo: e.target.value })),
|
|
3669
3970
|
},
|
|
3670
|
-
|
|
3971
|
+
h('option', { value: '' }, t('dialog.repoNew')),
|
|
3671
3972
|
(state.facets || [])
|
|
3672
3973
|
.filter((f) => f.ns === REPO_FACET)
|
|
3673
3974
|
.flatMap((f) => f.values)
|
|
3674
|
-
.map((v) =>
|
|
3975
|
+
.map((v) => h('option', { value: v.value, key: v.value }, v.value)),
|
|
3675
3976
|
),
|
|
3676
3977
|
(dialog.repo || '') === ''
|
|
3677
|
-
?
|
|
3678
|
-
|
|
3978
|
+
? h('div', null,
|
|
3979
|
+
h('input', {
|
|
3679
3980
|
className: 'dkb-input', value: dialog.newRepo || '',
|
|
3680
3981
|
placeholder: t('dialog.repoNamePlaceholder'),
|
|
3681
3982
|
onChange: (e) => setDialog(Object.assign({}, dialog, { newRepo: e.target.value })),
|
|
3682
3983
|
}),
|
|
3683
|
-
|
|
3684
|
-
// человек должен видеть это ДО нажатия, а не после.
|
|
3685
|
-
React.createElement('p', { className: 'dkb-hint' }, t('dialog.repoNewHint')),
|
|
3984
|
+
h('p', { className: 'dkb-hint' }, t('dialog.repoNewHint')),
|
|
3686
3985
|
)
|
|
3687
3986
|
: null,
|
|
3688
3987
|
)
|
|
3689
3988
|
: null,
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3989
|
+
h('div', { className: 'dkb-field' },
|
|
3990
|
+
h('span', { className: 'dkb-label' }, t('dialog.taskTitle')),
|
|
3991
|
+
h('input', {
|
|
3693
3992
|
className: 'dkb-input', value: dialog.title, autoFocus: true,
|
|
3694
3993
|
onChange: (e) => setDialog(Object.assign({}, dialog, { title: e.target.value })),
|
|
3695
3994
|
}),
|
|
3696
3995
|
),
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3996
|
+
h('div', { className: 'dkb-field' },
|
|
3997
|
+
h('span', { className: 'dkb-label' }, t('dialog.taskBody')),
|
|
3998
|
+
h('textarea', {
|
|
3700
3999
|
className: 'dkb-area', value: dialog.body || '',
|
|
3701
4000
|
onChange: (e) => setDialog(Object.assign({}, dialog, { body: e.target.value })),
|
|
3702
4001
|
}),
|
|
3703
|
-
|
|
4002
|
+
h('p', { className: 'dkb-hint' }, t('dialog.bodyHint')),
|
|
3704
4003
|
),
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
4004
|
+
h('div', { className: 'dkb-field' },
|
|
4005
|
+
h('span', { className: 'dkb-label' }, t('dialog.taskLabels')),
|
|
4006
|
+
h('input', {
|
|
3708
4007
|
className: 'dkb-input', value: dialog.labels || '',
|
|
3709
4008
|
placeholder: t('dialog.labelsHint'),
|
|
3710
4009
|
onChange: (e) => setDialog(Object.assign({}, dialog, { labels: e.target.value })),
|
|
3711
4010
|
}),
|
|
3712
4011
|
),
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
4012
|
+
h('div', { className: 'dkb-field' },
|
|
4013
|
+
h('span', { className: 'dkb-label' }, t('dialog.taskColumn')),
|
|
4014
|
+
h('select', {
|
|
3716
4015
|
className: 'dkb-input', value: dialog.column || 'backlog',
|
|
3717
4016
|
onChange: (e) => setDialog(Object.assign({}, dialog, { column: e.target.value })),
|
|
3718
|
-
}, (state.columns || []).map((c) =>
|
|
4017
|
+
}, (state.columns || []).map((c) => h('option', { value: c.id, key: c.id }, t('column.' + c.id)))),
|
|
3719
4018
|
),
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
4019
|
+
h('div', { className: 'dkb-foot' },
|
|
4020
|
+
h('button', { type: 'button', className: 'dkb-discard', onClick: () => setDialog(null) }, t('dialog.cancel')),
|
|
4021
|
+
h('button', {
|
|
3723
4022
|
type: 'button', className: 'dkb-save', disabled: String(dialog.title || '').trim() === '',
|
|
3724
4023
|
onClick: async () => {
|
|
3725
4024
|
try {
|
|
3726
4025
|
if (dialog.project) {
|
|
3727
|
-
// Проектная задача заводит issue и привязывается к
|
|
3728
|
-
// нему; метки ей ставит Gitea, а не мы.
|
|
3729
4026
|
await api('/project-task', {
|
|
3730
4027
|
method: 'POST',
|
|
3731
4028
|
body: JSON.stringify({
|
|
@@ -3742,12 +4039,8 @@ window.__ModuleLoader__.load({
|
|
|
3742
4039
|
body: JSON.stringify({
|
|
3743
4040
|
title: dialog.title,
|
|
3744
4041
|
body: dialog.body || '',
|
|
3745
|
-
// Метки вводятся через запятую: отдельный редактор
|
|
3746
|
-
// меток ради своей задачи — лишняя мебель.
|
|
3747
4042
|
labels: String(dialog.labels || '').split(',').map((x) => x.trim()).filter(Boolean),
|
|
3748
4043
|
column: dialog.column || 'backlog',
|
|
3749
|
-
// Задача без issue живёт на простой доске: правило
|
|
3750
|
-
// без исключений, и помнить, где её завели, не надо.
|
|
3751
4044
|
board: 'simple',
|
|
3752
4045
|
}),
|
|
3753
4046
|
})
|
|
@@ -3761,9 +4054,9 @@ window.__ModuleLoader__.load({
|
|
|
3761
4054
|
}, t('dialog.create')),
|
|
3762
4055
|
),
|
|
3763
4056
|
)
|
|
3764
|
-
:
|
|
3765
|
-
|
|
3766
|
-
|
|
4057
|
+
: h('div', null,
|
|
4058
|
+
h('div', { className: 'dkb-row' },
|
|
4059
|
+
h('select', {
|
|
3767
4060
|
className: 'dkb-input', value: dialog.full,
|
|
3768
4061
|
disabled: dialog.repos === null || dialog.repos.length === 0,
|
|
3769
4062
|
onChange: async (e) => {
|
|
@@ -3780,20 +4073,20 @@ window.__ModuleLoader__.load({
|
|
|
3780
4073
|
}
|
|
3781
4074
|
},
|
|
3782
4075
|
},
|
|
3783
|
-
|
|
4076
|
+
h('option', { value: '' },
|
|
3784
4077
|
dialog.repos === null ? t('dialog.loadingRepos')
|
|
3785
4078
|
: dialog.repos.length === 0 ? t('dialog.noRepos') : t('dialog.pickRepo')),
|
|
3786
4079
|
(dialog.repos || []).map((r) => {
|
|
3787
4080
|
const full = r.fullName || (r.owner + '/' + r.repo)
|
|
3788
|
-
return
|
|
4081
|
+
return h('option', { value: full, key: full }, repoOption(r, t))
|
|
3789
4082
|
}),
|
|
3790
4083
|
),
|
|
3791
4084
|
),
|
|
3792
4085
|
dialog.issues === null ? null
|
|
3793
4086
|
: dialog.issues.length === 0
|
|
3794
|
-
?
|
|
3795
|
-
:
|
|
3796
|
-
|
|
4087
|
+
? h('p', { className: 'dkb-note' }, t('dialog.noIssues'))
|
|
4088
|
+
: h('div', { className: 'dkb-list' }, dialog.issues.map((issue) =>
|
|
4089
|
+
h('button', {
|
|
3797
4090
|
type: 'button',
|
|
3798
4091
|
className: 'dkb-issue' + (issue.imported ? ' dkb-issueUsed' : ''),
|
|
3799
4092
|
key: issue.number,
|
|
@@ -3816,25 +4109,21 @@ window.__ModuleLoader__.load({
|
|
|
3816
4109
|
))
|
|
3817
4110
|
: null
|
|
3818
4111
|
|
|
3819
|
-
// Архив — свой экран, а не колонка: колонка это стадия работы, а архив
|
|
3820
|
-
// стадией не является. Возврат ставит карточку ровно туда, где она была.
|
|
3821
4112
|
const archiveEl = archive
|
|
3822
|
-
?
|
|
3823
|
-
|
|
4113
|
+
? h('div', { className: 'dkb-archive' },
|
|
4114
|
+
h('p', { className: 'dkb-hint' }, t('archive.hint')),
|
|
3824
4115
|
archive.length === 0
|
|
3825
|
-
?
|
|
3826
|
-
:
|
|
3827
|
-
archive.map((task) =>
|
|
3828
|
-
|
|
4116
|
+
? h('p', { className: 'dkb-note' }, t('archive.empty'))
|
|
4117
|
+
: h('ul', { className: 'dkb-archiveList' },
|
|
4118
|
+
archive.map((task) => h('li', { className: 'dkb-archiveRow', key: task.id },
|
|
4119
|
+
h('button', {
|
|
3829
4120
|
type: 'button', className: 'dkb-archiveTitle dkb-archiveOpen',
|
|
3830
|
-
// Архив — не братская могила: задачу туда убрали, а не
|
|
3831
|
-
// стёрли, и прочитать её должно быть можно, не возвращая.
|
|
3832
4121
|
onClick: () => setOpenTask(task),
|
|
3833
4122
|
},
|
|
3834
4123
|
[cardRef(task), task.title].filter(Boolean).join(' · ')),
|
|
3835
|
-
|
|
4124
|
+
h('span', { className: 'dkb-taskRef', title: exactAt(task.archivedAt) },
|
|
3836
4125
|
t('archive.since', { ago: agoText(state.now, task.archivedAt, t) })),
|
|
3837
|
-
|
|
4126
|
+
h('button', {
|
|
3838
4127
|
type: 'button', className: 'dkb-discard',
|
|
3839
4128
|
onClick: async () => {
|
|
3840
4129
|
try {
|
|
@@ -3848,45 +4137,43 @@ window.__ModuleLoader__.load({
|
|
|
3848
4137
|
)
|
|
3849
4138
|
: null
|
|
3850
4139
|
|
|
3851
|
-
// Метрика — это взгляд на журнал, а не новые данные: экран ничего не
|
|
3852
|
-
// меняет, поэтому и кнопок действий на нём нет.
|
|
3853
4140
|
const metricsEl = metrics
|
|
3854
|
-
?
|
|
3855
|
-
|
|
4141
|
+
? h('div', { className: 'dkb-archive' },
|
|
4142
|
+
h('p', { className: 'dkb-hint' },
|
|
3856
4143
|
t('metrics.hint', { n: metrics.tasks, week: metrics.done.week, month: metrics.done.month })),
|
|
3857
|
-
|
|
4144
|
+
h('div', { className: 'dkb-group' }, t('metrics.columns')),
|
|
3858
4145
|
metrics.columns.length === 0
|
|
3859
|
-
?
|
|
3860
|
-
:
|
|
3861
|
-
metrics.columns.map((row) =>
|
|
4146
|
+
? h('p', { className: 'dkb-note' }, t('metrics.empty'))
|
|
4147
|
+
: h('ul', { className: 'dkb-archiveList' },
|
|
4148
|
+
metrics.columns.map((row) => h('li', {
|
|
3862
4149
|
className: 'dkb-archiveRow', key: row.column,
|
|
3863
4150
|
},
|
|
3864
|
-
|
|
3865
|
-
|
|
4151
|
+
h('span', { className: 'dkb-archiveTitle' }, columnTitle({ id: row.column }, t)),
|
|
4152
|
+
h('span', { className: 'dkb-taskRef' },
|
|
3866
4153
|
t('metrics.median', { time: agoText(0, -row.median, t) })),
|
|
3867
|
-
|
|
4154
|
+
h('span', { className: 'dkb-taskRef' },
|
|
3868
4155
|
t('metrics.mean', { time: agoText(0, -row.mean, t) })),
|
|
3869
|
-
|
|
4156
|
+
h('span', { className: 'dkb-count' }, String(row.tasks)),
|
|
3870
4157
|
))),
|
|
3871
|
-
|
|
4158
|
+
h('div', { className: 'dkb-group' },
|
|
3872
4159
|
t('metrics.stale', { days: metrics.staleDays })),
|
|
3873
4160
|
metrics.stale.length === 0
|
|
3874
|
-
?
|
|
3875
|
-
:
|
|
3876
|
-
metrics.stale.map((row) =>
|
|
4161
|
+
? h('p', { className: 'dkb-note' }, t('metrics.noStale'))
|
|
4162
|
+
: h('ul', { className: 'dkb-archiveList' },
|
|
4163
|
+
metrics.stale.map((row) => h('li', {
|
|
3877
4164
|
className: 'dkb-archiveRow', key: row.id,
|
|
3878
4165
|
},
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
4166
|
+
h('span', { className: 'dkb-archiveTitle' }, row.title),
|
|
4167
|
+
h('span', { className: 'dkb-taskRef' }, columnTitle({ id: row.column }, t)),
|
|
4168
|
+
h('span', { className: 'dkb-taskRef', title: exactAt(row.since) },
|
|
3882
4169
|
agoText(state.now, row.since, t)),
|
|
3883
4170
|
))),
|
|
3884
4171
|
)
|
|
3885
4172
|
: null
|
|
3886
4173
|
|
|
3887
|
-
return
|
|
4174
|
+
return h('div', { className: 'dkb-screen' },
|
|
3888
4175
|
style, bar,
|
|
3889
|
-
error ?
|
|
4176
|
+
error ? h('p', { className: 'dkb-failed' }, error) : null,
|
|
3890
4177
|
metrics ? metricsEl : archive ? archiveEl : columns,
|
|
3891
4178
|
panel, dialogEl, moveDialogEl, batchEl, queueEl,
|
|
3892
4179
|
)
|
|
@@ -3928,41 +4215,38 @@ window.__ModuleLoader__.load({
|
|
|
3928
4215
|
})
|
|
3929
4216
|
setTask(out.task)
|
|
3930
4217
|
setOpen(false)
|
|
3931
|
-
} catch { /*
|
|
4218
|
+
} catch { /* board handles error; keep header intact */ }
|
|
3932
4219
|
}, [task])
|
|
3933
4220
|
|
|
3934
4221
|
if (!task) return null
|
|
3935
4222
|
|
|
3936
4223
|
const label = [cardRef(task) || task.title, t('column.' + task.column)].filter(Boolean).join(' · ')
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
4224
|
+
ensureCss()
|
|
4225
|
+
return h('span', { className: 'dkb-chipWrap' },
|
|
4226
|
+
h('button', {
|
|
3940
4227
|
type: 'button', className: 'dkb-chip', title: task.title,
|
|
3941
4228
|
onClick: () => setOpen((v) => !v),
|
|
3942
4229
|
}, '▦ ' + label),
|
|
3943
|
-
open ?
|
|
3944
|
-
|
|
3945
|
-
columns.map((id) =>
|
|
4230
|
+
open ? h('div', { className: 'dkb-chipPanel' },
|
|
4231
|
+
h('span', { className: 'dkb-hint' }, t('chip.moveTo')),
|
|
4232
|
+
columns.map((id) => h('button', {
|
|
3946
4233
|
type: 'button', key: id,
|
|
3947
4234
|
className: id === task.column ? 'dkb-save' : 'dkb-discard',
|
|
3948
4235
|
onClick: () => move(id),
|
|
3949
4236
|
}, t('column.' + id))),
|
|
3950
4237
|
props.toggle
|
|
3951
|
-
?
|
|
4238
|
+
? h('button', {
|
|
3952
4239
|
type: 'button', className: 'dkb-discard',
|
|
3953
4240
|
onClick: () => { setOpen(false); props.toggle.show(task.id, task.board) },
|
|
3954
4241
|
}, t('chip.openCard'))
|
|
3955
4242
|
: null,
|
|
3956
|
-
|
|
3957
|
-
|
|
4243
|
+
h('span', { className: 'dkb-hint' }, t('chip.note')),
|
|
4244
|
+
h('textarea', {
|
|
3958
4245
|
className: 'dkb-area', value: note, disabled: saving,
|
|
3959
4246
|
onChange: (e) => setNote(e.target.value),
|
|
3960
4247
|
}),
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
// сделать поведение случайным на вид.
|
|
3964
|
-
React.createElement('span', { className: 'dkb-hint' }, t('chip.noteHint')),
|
|
3965
|
-
React.createElement('button', {
|
|
4248
|
+
h('span', { className: 'dkb-hint' }, t('chip.noteHint')),
|
|
4249
|
+
h('button', {
|
|
3966
4250
|
type: 'button', className: 'dkb-save',
|
|
3967
4251
|
disabled: saving || note.trim() === '',
|
|
3968
4252
|
onClick: async () => {
|
|
@@ -3974,7 +4258,7 @@ window.__ModuleLoader__.load({
|
|
|
3974
4258
|
setTask(out.task)
|
|
3975
4259
|
setNote('')
|
|
3976
4260
|
setOpen(false)
|
|
3977
|
-
} catch { /*
|
|
4261
|
+
} catch { /* board handles error; keep header intact */ } finally {
|
|
3978
4262
|
setSaving(false)
|
|
3979
4263
|
}
|
|
3980
4264
|
},
|
|
@@ -3984,17 +4268,6 @@ window.__ModuleLoader__.load({
|
|
|
3984
4268
|
}
|
|
3985
4269
|
|
|
3986
4270
|
|
|
3987
|
-
// ------------------------------------------------- встраивание в оболочку
|
|
3988
|
-
//
|
|
3989
|
-
// Раздела верхнего уровня в этой сборке нет: слоты `app.section` и
|
|
3990
|
-
// `sidebar.section` отсутствуют, а `settings.section` уводит доску в
|
|
3991
|
-
// настройки, где ей не место. Поэтому строка и экран вставляются прямо в
|
|
3992
|
-
// вёрстку оболочки — тем же приёмом, каким это делает штатный task board.
|
|
3993
|
-
//
|
|
3994
|
-
// Приём хрупкий по своей природе: он опирается на чужую вёрстку. Смягчение
|
|
3995
|
-
// — селекторы по подстроке класса, наблюдатели за перерисовкой React и
|
|
3996
|
-
// мягкий отказ: не нашли корень — просто ничего не показали, ничего не
|
|
3997
|
-
// сломали.
|
|
3998
4271
|
|
|
3999
4272
|
const PANEL = 'kanban'
|
|
4000
4273
|
const ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
@@ -4012,8 +4285,6 @@ window.__ModuleLoader__.load({
|
|
|
4012
4285
|
/** @see helpers.createToggle */
|
|
4013
4286
|
function createToggle() {
|
|
4014
4287
|
let open = false
|
|
4015
|
-
// Задача, которую просили показать при открытии. Состояние окна живёт
|
|
4016
|
-
// внутри экрана доски, и снаружи до него иначе не дотянуться.
|
|
4017
4288
|
let wanted = undefined
|
|
4018
4289
|
const listeners = new Set()
|
|
4019
4290
|
return {
|
|
@@ -4026,8 +4297,6 @@ window.__ModuleLoader__.load({
|
|
|
4026
4297
|
toggle() { this.set(!open) },
|
|
4027
4298
|
/** Открыть доску на конкретной задаче. */
|
|
4028
4299
|
show(taskId, boardId) {
|
|
4029
|
-
// Доска едет вместе с задачей: карточка может лежать на простой, а
|
|
4030
|
-
// открытой быть проектная, и тогда искать её было бы негде.
|
|
4031
4300
|
wanted = { id: String(taskId || ''), board: String(boardId || 'main') }
|
|
4032
4301
|
if (open) { for (const fn of listeners) fn() } else this.set(true)
|
|
4033
4302
|
},
|
|
@@ -4124,8 +4393,6 @@ window.__ModuleLoader__.load({
|
|
|
4124
4393
|
else delete entry.dataset.active
|
|
4125
4394
|
}
|
|
4126
4395
|
|
|
4127
|
-
// Оболочка перерисовывается сама по себе: без наблюдателя кнопка исчезнет
|
|
4128
|
-
// при первом же обновлении списка сессий.
|
|
4129
4396
|
const observer = new MutationObserver(tryPlace)
|
|
4130
4397
|
observer.observe(document.body, { childList: true, subtree: true })
|
|
4131
4398
|
const unsubscribe = toggle.subscribe(syncActive)
|
|
@@ -4154,7 +4421,7 @@ window.__ModuleLoader__.load({
|
|
|
4154
4421
|
container.setAttribute('data-dsh-plugin', 'kanban')
|
|
4155
4422
|
column.appendChild(container)
|
|
4156
4423
|
root = ReactDOM.createRoot(container)
|
|
4157
|
-
root.render(
|
|
4424
|
+
root.render(h(BoardScreen, { ctx, toggle, onClose: () => toggle.set(false) }))
|
|
4158
4425
|
}
|
|
4159
4426
|
|
|
4160
4427
|
const applyActive = () => {
|
|
@@ -4166,8 +4433,6 @@ window.__ModuleLoader__.load({
|
|
|
4166
4433
|
}
|
|
4167
4434
|
}
|
|
4168
4435
|
|
|
4169
|
-
// Соседние панели объявляют себя тем же событием: две панели поверх одной
|
|
4170
|
-
// колонки одновременно — это каша, поэтому уступаем.
|
|
4171
4436
|
const onOtherActivate = (event) => {
|
|
4172
4437
|
if (event.detail !== PANEL && toggle.isOpen()) toggle.set(false)
|
|
4173
4438
|
}
|
|
@@ -4198,7 +4463,6 @@ window.__ModuleLoader__.load({
|
|
|
4198
4463
|
}
|
|
4199
4464
|
}
|
|
4200
4465
|
|
|
4201
|
-
// ------------------------------------------------------------ регистрация
|
|
4202
4466
|
|
|
4203
4467
|
/**
|
|
4204
4468
|
* Зарегистрировать компонент в первый слот, который его примет.
|
|
@@ -4225,7 +4489,7 @@ window.__ModuleLoader__.load({
|
|
|
4225
4489
|
if (registeredInSlot || (res !== null && res !== false && res !== undefined)) {
|
|
4226
4490
|
return entry.name
|
|
4227
4491
|
}
|
|
4228
|
-
} catch { /*
|
|
4492
|
+
} catch { /* slot missing in build — try next */ }
|
|
4229
4493
|
}
|
|
4230
4494
|
return undefined
|
|
4231
4495
|
}
|
|
@@ -4233,32 +4497,25 @@ window.__ModuleLoader__.load({
|
|
|
4233
4497
|
helpers.createToggle = createToggle
|
|
4234
4498
|
|
|
4235
4499
|
function apply(ctx) {
|
|
4236
|
-
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-kanban:
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
// равным имени пространства.
|
|
4242
|
-
// Собственный раздел верхнего уровня (settings.section) не регистрируется,
|
|
4243
|
-
// чтобы не засорять плоский общий список настроек (#238).
|
|
4500
|
+
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-kanban: locales')
|
|
4501
|
+
|
|
4502
|
+
refreshMirrorUntilVisible(ctx)
|
|
4503
|
+
const SettingsErrorBoundary = createErrorBoundary()
|
|
4504
|
+
const SafeSettingsCard = (props) => h(SettingsErrorBoundary, null, h(KanbanSettingsCard, props))
|
|
4244
4505
|
const cardSlot = registerFirst(ctx, [
|
|
4245
4506
|
{ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) },
|
|
4246
|
-
],
|
|
4507
|
+
], SafeSettingsCard)
|
|
4247
4508
|
|
|
4248
|
-
// Экран доски встраивается в оболочку, а не в слот: раздела верхнего
|
|
4249
|
-
// уровня в сборке нет, а настройки — не место для доски.
|
|
4250
4509
|
const toggle = createToggle()
|
|
4251
4510
|
ctx.effect(() => {
|
|
4252
4511
|
const off = [mountSidebarEntry(toggle, fallbackT('section.label')), mountBoard(ctx, toggle)]
|
|
4253
4512
|
return () => { for (const dispose of off) dispose() }
|
|
4254
|
-
}, 'dsh-kanban:
|
|
4513
|
+
}, 'dsh-kanban: shell board')
|
|
4255
4514
|
|
|
4256
|
-
// Чип задачи в шапке чата. Слот подтверждён живым плагином dsh-gitea.
|
|
4257
4515
|
const chipSlot = registerFirst(ctx, [
|
|
4258
4516
|
{ name: 'conversation.session.header.utilities', id: '@goodandready/dsh-kanban.chip', order: 26, locale: NS, inject: () => ({ ctx, toggle }) },
|
|
4259
4517
|
], TaskChip)
|
|
4260
4518
|
|
|
4261
|
-
// Куда встали — видно снаружи: живая проверка сверяет это с интерфейсом.
|
|
4262
4519
|
exports.slots = { card: cardSlot, chip: chipSlot }
|
|
4263
4520
|
}
|
|
4264
4521
|
|