@goodandready/dsh-kanban 0.2.1 → 0.2.7
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 +10 -0
- package/{docs/README.ru.md → README.ru.md} +10 -0
- package/{docs/README.zh.md → README.zh.md} +10 -0
- package/lib/board-tool.js +151 -0
- package/lib/client.js +862 -739
- package/lib/gitea.js +15 -0
- package/lib/index.js +112 -93
- package/lib/routes.js +357 -1
- package/lib/store.js +42 -4
- package/lib/sync.js +24 -1
- package/package.json +3 -2
- package/docs/design/DESIGN.md +0 -93
- 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/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
|
* Соседи для переноса карточки. Хранилище работает не с числовым индексом,
|
|
@@ -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,9 +626,95 @@ 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 (_) {}
|
|
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}' +
|
|
@@ -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}' +
|
|
@@ -797,13 +855,9 @@ 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
861
|
'.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}' +
|
|
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}}' +
|
|
@@ -819,23 +873,21 @@ window.__ModuleLoader__.load({
|
|
|
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:#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:#0d1117;color:#58a6ff;padding:12px;border-radius:8px;font-family:monospace;font-size:11px;max-height:260px;overflow-y:auto;white-space:pre-wrap;border:1px solid #30363d}'
|
|
827
888
|
|
|
828
889
|
|
|
829
|
-
// ---------------------------------------------------------------- строки
|
|
830
890
|
|
|
831
|
-
// Правило регистра, чтобы подписи не расползались:
|
|
832
|
-
//
|
|
833
|
-
// подпись УПРАВЛЕНИЯ — с прописной: «Архив», «Поиск», «Обновить». Это
|
|
834
|
-
// имя кнопки, а не продолжение фразы;
|
|
835
|
-
// встроенный текст и состояние — со строчной: «ждёт ответа», «здесь
|
|
836
|
-
// 3 дн», «показано 12 из 81». Это часть предложения, а не ярлык.
|
|
837
|
-
//
|
|
838
|
-
// Смешение того и другого в одном ряду и выглядит халтурой.
|
|
839
891
|
const en = {
|
|
840
892
|
'title': 'Kanban',
|
|
841
893
|
'subtitle': 'Task board: Gitea issues and a session per task',
|
|
@@ -1176,15 +1228,33 @@ window.__ModuleLoader__.load({
|
|
|
1176
1228
|
'comments.placeholder': 'Add a note or feedback...',
|
|
1177
1229
|
'comments.authorUser': 'User',
|
|
1178
1230
|
'comments.authorAgent': 'Agent',
|
|
1231
|
+
'epic.badge': 'Epic',
|
|
1232
|
+
'epic.progress': '{done}/{total} subtasks',
|
|
1233
|
+
'epic.triage': 'Triage / Decompose',
|
|
1234
|
+
'epic.triageTitle': 'Decompose Task into Subtasks',
|
|
1235
|
+
'epic.triageHelp': 'Enter one subtask title per line. Child tasks will be linked to this Epic.',
|
|
1236
|
+
'epic.triageSubmit': 'Create Subtasks',
|
|
1237
|
+
'epic.createGiteaIssues': 'Also create linked Gitea issues',
|
|
1238
|
+
'epic.subtasks': 'Subtasks',
|
|
1239
|
+
'epic.addSubtask': 'Add subtask',
|
|
1240
|
+
'epic.noSubtasks': 'No subtasks yet.',
|
|
1241
|
+
'console.tab': 'Live Console',
|
|
1242
|
+
'console.empty': 'No agent output recorded yet.',
|
|
1243
|
+
'filter.all': 'All',
|
|
1244
|
+
'filter.active': 'Active',
|
|
1245
|
+
'filter.review': 'Review',
|
|
1246
|
+
'filter.epics': 'Epics',
|
|
1247
|
+
'filter.stale': 'Silent',
|
|
1248
|
+
'pr.create': 'Create Gitea PR',
|
|
1249
|
+
'pr.creating': 'Creating PR...',
|
|
1250
|
+
'pr.created': 'PR #{n} created',
|
|
1251
|
+
'pr.view': 'View PR',
|
|
1179
1252
|
'error.issueNotFound': 'Issue not found.',
|
|
1180
1253
|
'error.titleRequired': 'A title is required.',
|
|
1181
1254
|
'error.crossSite': 'Request rejected as cross-site.',
|
|
1182
1255
|
'error.unknown': 'Request failed.',
|
|
1183
1256
|
}
|
|
1184
1257
|
|
|
1185
|
-
// Китайский — вторая встроенная локаль ядра. Русского здесь нет:
|
|
1186
|
-
// он приезжает языковым пакетом `dsh-russian-lang`, который
|
|
1187
|
-
// регистрирует `ru` для этого пространства извне.
|
|
1188
1258
|
const zh = {
|
|
1189
1259
|
'title': '看板',
|
|
1190
1260
|
'subtitle': '任务看板:Gitea issue,每个任务一个会话',
|
|
@@ -1525,6 +1595,27 @@ window.__ModuleLoader__.load({
|
|
|
1525
1595
|
'comments.placeholder': '添加说明或反馈...',
|
|
1526
1596
|
'comments.authorUser': '用户',
|
|
1527
1597
|
'comments.authorAgent': 'Agent',
|
|
1598
|
+
'epic.badge': '史诗',
|
|
1599
|
+
'epic.progress': '{done}/{total} 子任务',
|
|
1600
|
+
'epic.triage': '分解子任务',
|
|
1601
|
+
'epic.triageTitle': '将任务分解为子任务',
|
|
1602
|
+
'epic.triageHelp': '每行输入一个子任务标题。子任务将关联到此史诗。',
|
|
1603
|
+
'epic.triageSubmit': '创建子任务',
|
|
1604
|
+
'epic.createGiteaIssues': '同时在 Gitea 中创建关联 issue',
|
|
1605
|
+
'epic.subtasks': '子任务',
|
|
1606
|
+
'epic.addSubtask': '添加子任务',
|
|
1607
|
+
'epic.noSubtasks': '暂无子任务。',
|
|
1608
|
+
'console.tab': '实时终端',
|
|
1609
|
+
'console.empty': '暂无 Agent 活动输出。',
|
|
1610
|
+
'filter.all': '全部',
|
|
1611
|
+
'filter.active': '进行中',
|
|
1612
|
+
'filter.review': '待复核',
|
|
1613
|
+
'filter.epics': '史诗',
|
|
1614
|
+
'filter.stale': '沉默',
|
|
1615
|
+
'pr.create': '创建 Gitea PR',
|
|
1616
|
+
'pr.creating': '正在创建 PR...',
|
|
1617
|
+
'pr.created': '已创建 PR #{n}',
|
|
1618
|
+
'pr.view': '查看 PR',
|
|
1528
1619
|
'error.issueNotFound': '没有找到 issue。',
|
|
1529
1620
|
'error.titleRequired': '标题不能为空。',
|
|
1530
1621
|
'error.crossSite': '请求被判定为跨站而拒绝。',
|
|
@@ -1532,8 +1623,6 @@ window.__ModuleLoader__.load({
|
|
|
1532
1623
|
}
|
|
1533
1624
|
|
|
1534
1625
|
function fallbackT(key, vars) {
|
|
1535
|
-
// Запасной перевод — английский: русский приезжает языковым пакетом,
|
|
1536
|
-
// а до службы локализации доска обязана говорить хоть на чём-то.
|
|
1537
1626
|
let text = en[key] !== undefined ? en[key] : key
|
|
1538
1627
|
if (vars) {
|
|
1539
1628
|
for (const name of Object.keys(vars)) {
|
|
@@ -1543,7 +1632,6 @@ window.__ModuleLoader__.load({
|
|
|
1543
1632
|
return text
|
|
1544
1633
|
}
|
|
1545
1634
|
|
|
1546
|
-
// ------------------------------------------------------- карточка настроек
|
|
1547
1635
|
|
|
1548
1636
|
const FIELDS = [
|
|
1549
1637
|
{ field: 'giteaUrl', kind: 'text', group: 'gitea' },
|
|
@@ -1584,18 +1672,16 @@ window.__ModuleLoader__.load({
|
|
|
1584
1672
|
}
|
|
1585
1673
|
|
|
1586
1674
|
function Chevron(props) {
|
|
1587
|
-
return
|
|
1675
|
+
return h('svg', {
|
|
1588
1676
|
className: 'dkb-chevron' + (props.open ? ' dkb-chevronOpen' : ''),
|
|
1589
1677
|
width: 16, height: 16, viewBox: '0 0 16 16', 'aria-hidden': 'true',
|
|
1590
|
-
},
|
|
1678
|
+
}, h('path', {
|
|
1591
1679
|
d: 'M4 6l4 4 4-4', fill: 'none', stroke: 'currentColor', strokeWidth: '1.5',
|
|
1592
1680
|
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
1593
1681
|
}))
|
|
1594
1682
|
}
|
|
1595
1683
|
|
|
1596
1684
|
function KanbanSettingsCard(props) {
|
|
1597
|
-
// Все хуки объявлены выше любого возврата: ранний возврат над хуком даёт
|
|
1598
|
-
// React error 310 при первом же изменении состояния.
|
|
1599
1685
|
const ctx = props.ctx
|
|
1600
1686
|
const t = props.t || fallbackT
|
|
1601
1687
|
const [open, setOpen] = React.useState(false)
|
|
@@ -1605,7 +1691,10 @@ window.__ModuleLoader__.load({
|
|
|
1605
1691
|
const [saved, setSaved] = React.useState(false)
|
|
1606
1692
|
|
|
1607
1693
|
const scope = React.useMemo(
|
|
1608
|
-
() =>
|
|
1694
|
+
() => {
|
|
1695
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope
|
|
1696
|
+
return s ? s.bind({ namespace: NS }) : undefined
|
|
1697
|
+
},
|
|
1609
1698
|
[ctx],
|
|
1610
1699
|
)
|
|
1611
1700
|
const snapshot = React.useSyncExternalStore(
|
|
@@ -1633,9 +1722,6 @@ window.__ModuleLoader__.load({
|
|
|
1633
1722
|
const save = React.useCallback(async () => {
|
|
1634
1723
|
if (!scope || !draft) return
|
|
1635
1724
|
setSaving(true); setFailed(''); setSaved(false)
|
|
1636
|
-
// Пишем ВСЕ поля, а не до первой ошибки: обрыв на первой неудаче
|
|
1637
|
-
// оставил бы остальные поля незаписанными, а снаружи это выглядит как
|
|
1638
|
-
// «кнопка не работает».
|
|
1639
1725
|
const broken = []
|
|
1640
1726
|
for (const spec of FIELDS) {
|
|
1641
1727
|
if (!draft || !Object.prototype.hasOwnProperty.call(draft, spec.field)) continue
|
|
@@ -1657,82 +1743,108 @@ window.__ModuleLoader__.load({
|
|
|
1657
1743
|
setDraft(null); setSaved(true)
|
|
1658
1744
|
}, [scope, draft, t])
|
|
1659
1745
|
|
|
1660
|
-
const head =
|
|
1746
|
+
const head = h('button', {
|
|
1661
1747
|
type: 'button', className: 'dkb-head', onClick: () => setOpen((v) => !v),
|
|
1662
1748
|
},
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1749
|
+
h('span', { className: 'dkb-headText' },
|
|
1750
|
+
h('span', { className: 'dkb-title' }, t('title')),
|
|
1751
|
+
h('span', { className: 'dkb-sub' }, t('subtitle')),
|
|
1666
1752
|
),
|
|
1667
|
-
|
|
1753
|
+
h(Chevron, { open }),
|
|
1668
1754
|
)
|
|
1669
|
-
|
|
1755
|
+
ensureCss()
|
|
1756
|
+
const style = null
|
|
1670
1757
|
|
|
1671
|
-
if (!open) return
|
|
1758
|
+
if (!open) return h('li', { className: 'dkb-card' }, head)
|
|
1672
1759
|
|
|
1673
1760
|
let body
|
|
1674
1761
|
if (status === 'loading') {
|
|
1675
|
-
body =
|
|
1762
|
+
body = h('p', { className: 'dkb-note', role: 'status' }, t('settings.loading'))
|
|
1676
1763
|
} else if (status !== 'ready') {
|
|
1677
|
-
|
|
1678
|
-
// остаётся истинным, и карточка нарисовала бы пустую, но с виду
|
|
1679
|
-
// рабочую форму.
|
|
1680
|
-
body = React.createElement('p', { className: 'dkb-failed', role: 'status' }, t('settings.unavailable'))
|
|
1764
|
+
body = h('p', { className: 'dkb-failed', role: 'status' }, t('settings.unavailable'))
|
|
1681
1765
|
} else {
|
|
1682
1766
|
const writable = !snapshot || snapshot.writable !== false
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1767
|
+
const groups = []
|
|
1768
|
+
const groupMap = {}
|
|
1685
1769
|
for (const spec of FIELDS) {
|
|
1686
|
-
if (spec.group
|
|
1687
|
-
|
|
1688
|
-
|
|
1770
|
+
if (!groupMap[spec.group]) {
|
|
1771
|
+
groupMap[spec.group] = []
|
|
1772
|
+
groups.push(spec.group)
|
|
1689
1773
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1774
|
+
groupMap[spec.group].push(spec)
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
const rows = []
|
|
1778
|
+
for (const grp of groups) {
|
|
1779
|
+
const fieldsInGroup = groupMap[grp]
|
|
1780
|
+
const fieldElements = []
|
|
1781
|
+
for (const spec of fieldsInGroup) {
|
|
1782
|
+
if (spec.kind === 'bool') {
|
|
1783
|
+
fieldElements.push(h('div', { className: 'dkb-field', key: spec.field },
|
|
1784
|
+
h('label', { className: 'dkb-row' },
|
|
1785
|
+
h('input', {
|
|
1786
|
+
type: 'checkbox',
|
|
1787
|
+
checked: String(valueOf(spec.field)) === 'true',
|
|
1788
|
+
disabled: !writable || saving,
|
|
1789
|
+
onChange: (e) => edit(spec.field, e.target.checked ? 'true' : 'false'),
|
|
1790
|
+
}),
|
|
1791
|
+
h('span', { className: 'dkb-label' }, t('field.' + spec.field)),
|
|
1792
|
+
),
|
|
1793
|
+
h('p', { className: 'dkb-hint' }, t(hintKey(spec))),
|
|
1794
|
+
))
|
|
1795
|
+
continue
|
|
1796
|
+
}
|
|
1797
|
+
const common = {
|
|
1798
|
+
className: spec.kind === 'area' ? 'dkb-area' : 'dkb-input',
|
|
1799
|
+
value: valueOf(spec.field),
|
|
1800
|
+
disabled: !writable || saving,
|
|
1801
|
+
onChange: (e) => edit(spec.field, e.target.value),
|
|
1802
|
+
}
|
|
1803
|
+
if (spec.kind === 'number') common.type = 'number'
|
|
1804
|
+
fieldElements.push(h('div', { className: 'dkb-field', key: spec.field },
|
|
1805
|
+
h('span', { className: 'dkb-label' }, t('field.' + spec.field)),
|
|
1806
|
+
h(spec.kind === 'area' ? 'textarea' : 'input', common),
|
|
1807
|
+
h('p', { className: 'dkb-hint' }, t(hintKey(spec))),
|
|
1704
1808
|
))
|
|
1705
|
-
continue
|
|
1706
1809
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1810
|
+
|
|
1811
|
+
let badge = null
|
|
1812
|
+
if (grp === 'gitea') {
|
|
1813
|
+
const hasUrl = Boolean(valueOf('giteaUrl'))
|
|
1814
|
+
badge = h('span', {
|
|
1815
|
+
className: 'dkb-section-badge' + (hasUrl ? ' dkb-section-badge-ok' : ''),
|
|
1816
|
+
}, hasUrl ? 'Gitea Configured' : 'No URL')
|
|
1817
|
+
} else if (grp === 'sync') {
|
|
1818
|
+
const interval = Number(valueOf('syncIntervalSec')) || 0
|
|
1819
|
+
badge = h('span', {
|
|
1820
|
+
className: 'dkb-section-badge' + (interval > 0 ? ' dkb-section-badge-ok' : ''),
|
|
1821
|
+
}, interval > 0 ? `${interval}s` : 'Manual')
|
|
1712
1822
|
}
|
|
1713
|
-
|
|
1714
|
-
rows.push(
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1823
|
+
|
|
1824
|
+
rows.push(h('div', { className: 'dkb-section-card', key: 'g-' + grp },
|
|
1825
|
+
h('div', { className: 'dkb-section-title' },
|
|
1826
|
+
h('span', null, t('group.' + grp)),
|
|
1827
|
+
badge,
|
|
1828
|
+
),
|
|
1829
|
+
...fieldElements,
|
|
1718
1830
|
))
|
|
1719
1831
|
}
|
|
1720
1832
|
const dirty = draft !== null && Object.keys(draft).length > 0
|
|
1721
1833
|
let note
|
|
1722
|
-
if (failed) note =
|
|
1723
|
-
else if (!writable) note =
|
|
1724
|
-
else if (saved) note =
|
|
1725
|
-
else note =
|
|
1726
|
-
rows.push(
|
|
1834
|
+
if (failed) note = h('p', { className: 'dkb-failed', role: 'status' }, failed)
|
|
1835
|
+
else if (!writable) note = h('p', { className: 'dkb-note', role: 'status' }, t('settings.readOnly'))
|
|
1836
|
+
else if (saved) note = h('p', { className: 'dkb-note', role: 'status' }, t('settings.saved'))
|
|
1837
|
+
else note = h('p', { className: 'dkb-note' })
|
|
1838
|
+
rows.push(h('div', { className: 'dkb-foot', key: 'foot' },
|
|
1727
1839
|
note,
|
|
1728
|
-
|
|
1729
|
-
|
|
1840
|
+
h('button', { type: 'button', className: 'dkb-discard', disabled: !dirty || saving, onClick: discard }, t('discard')),
|
|
1841
|
+
h('button', { type: 'button', className: 'dkb-save', disabled: !dirty || saving || !writable, onClick: save }, t('save')),
|
|
1730
1842
|
))
|
|
1731
1843
|
body = rows
|
|
1732
1844
|
}
|
|
1733
1845
|
|
|
1734
|
-
return
|
|
1735
|
-
|
|
1846
|
+
return h('li', { className: 'dkb-card dkb-cardOpen' },
|
|
1847
|
+
head, h('div', { className: 'dkb-body' }, body))
|
|
1736
1848
|
}
|
|
1737
1849
|
|
|
1738
1850
|
/** Стрелка влево для кнопки возврата: 14x14, обводка цветом текста. */
|
|
@@ -1740,9 +1852,8 @@ window.__ModuleLoader__.load({
|
|
|
1740
1852
|
+ ' stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
|
|
1741
1853
|
+ '<path d="M10 3.5 5.5 8l4.5 4.5"/></svg>'
|
|
1742
1854
|
|
|
1743
|
-
// ------------------------------------------------------------ экран доски
|
|
1744
1855
|
|
|
1745
|
-
function TaskCard(props) {
|
|
1856
|
+
const TaskCard = React.memo(function TaskCard(props) {
|
|
1746
1857
|
const { task, t, now } = props
|
|
1747
1858
|
const ref = cardRef(task)
|
|
1748
1859
|
const status = cardStatus(task)
|
|
@@ -1752,26 +1863,19 @@ window.__ModuleLoader__.load({
|
|
|
1752
1863
|
? t('state.queued', { n: task.queuePos || 1 })
|
|
1753
1864
|
: stateLabel(task, t)
|
|
1754
1865
|
const pack = props.pack
|
|
1755
|
-
// Дата заведения, а не время в колонке. «Здесь 34 минуты» у задачи,
|
|
1756
|
-
// приехавшей подхватом полчаса назад, не значит ничего: это возраст
|
|
1757
|
-
// карточки на доске, а спрашивают про возраст самой задачи.
|
|
1758
1866
|
const born = shortDate(task.createdAt)
|
|
1759
1867
|
const touched = task.sessionId ? agoText(now, task.updatedAt, t) : ''
|
|
1760
1868
|
const menu = props.menuOpen
|
|
1761
1869
|
const urgent = priorityColor(task)
|
|
1762
1870
|
const overdue = task.overdue === true
|
|
1763
|
-
return
|
|
1871
|
+
return h('div', {
|
|
1764
1872
|
className: 'dkb-taskCard' + (urgent ? ' dkb-urgent' : '')
|
|
1765
1873
|
+ (overdue ? ' dkb-overdue' : '')
|
|
1766
1874
|
+ (props.sameSession ? ' dkb-sameSession' : ''),
|
|
1767
1875
|
onMouseEnter: () => { if (props.onHover) props.onHover(task.sessionId || '') },
|
|
1768
1876
|
onMouseLeave: () => { if (props.onHover) props.onHover('') },
|
|
1769
1877
|
style: urgent ? { borderLeftColor: urgent } : undefined,
|
|
1770
|
-
// Полный заголовок в подсказке: на карточке он обрезан двумя строками,
|
|
1771
|
-
// и прочитать его иначе можно было только открыв её.
|
|
1772
1878
|
title: task.title,
|
|
1773
|
-
// Карточка становится точкой табуляции: доска была мышиной целиком, а
|
|
1774
|
-
// перенос теперь ещё и команда агенту — действие с последствиями.
|
|
1775
1879
|
tabIndex: 0,
|
|
1776
1880
|
draggable: true,
|
|
1777
1881
|
onDragStart: (e) => {
|
|
@@ -1780,10 +1884,8 @@ window.__ModuleLoader__.load({
|
|
|
1780
1884
|
},
|
|
1781
1885
|
onClick: () => props.onOpen(task),
|
|
1782
1886
|
},
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
React.createElement('div', { className: 'dkb-taskHead' },
|
|
1786
|
-
React.createElement('input', {
|
|
1887
|
+
h('div', { className: 'dkb-taskHead' },
|
|
1888
|
+
h('input', {
|
|
1787
1889
|
type: 'checkbox',
|
|
1788
1890
|
className: 'dkb-pick' + (props.picked ? ' dkb-pickOn' : ''),
|
|
1789
1891
|
checked: props.picked === true,
|
|
@@ -1791,113 +1893,114 @@ window.__ModuleLoader__.load({
|
|
|
1791
1893
|
onClick: (e) => e.stopPropagation(),
|
|
1792
1894
|
onChange: () => props.onPick(task),
|
|
1793
1895
|
}),
|
|
1794
|
-
waiting ?
|
|
1795
|
-
waiting ?
|
|
1896
|
+
waiting ? h('span', { className: 'dkb-waitDot' }) : null,
|
|
1897
|
+
waiting ? h('span', { className: 'dkb-waiting' }, t('board.waiting')) : null,
|
|
1796
1898
|
taskRef(task, t),
|
|
1797
|
-
task.
|
|
1798
|
-
task.
|
|
1799
|
-
task.
|
|
1899
|
+
task.isEpic ? h('span', { className: 'dkb-tag dkb-epicTag', title: t('epic.badge') }, '👑 ' + t('epic.badge')) : null,
|
|
1900
|
+
task.parentId ? h('span', { className: 'dkb-tag', style: { opacity: 0.75 } }, '↳') : null,
|
|
1901
|
+
task.subtasksCount && task.subtasksCount.total > 0
|
|
1902
|
+
? h('span', { className: 'dkb-tag', title: 'Subtasks' }, t('epic.progress', { done: task.subtasksCount.completed, total: task.subtasksCount.total }))
|
|
1903
|
+
: null,
|
|
1904
|
+
task.cron ? h('span', { className: 'dkb-tag', title: task.cron }, '⏰ cron') : null,
|
|
1905
|
+
task.progressDump ? h('span', { className: 'dkb-tag', title: 'PROGRESSDUMP' }, '📋 dump') : null,
|
|
1906
|
+
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
1907
|
Array.isArray(task.checklist) && task.checklist.length > 0
|
|
1801
|
-
?
|
|
1908
|
+
? h('span', { className: 'dkb-tag', title: 'DoD Checklist' }, `☑ ${task.checklist.filter((i) => i.completed).length}/${task.checklist.length}`)
|
|
1802
1909
|
: null,
|
|
1803
1910
|
task.report && task.report.summary
|
|
1804
|
-
?
|
|
1911
|
+
? h('span', { className: 'dkb-tag', title: 'Execution Report' }, '📄 report')
|
|
1805
1912
|
: null,
|
|
1806
1913
|
Array.isArray(task.comments) && task.comments.length > 0
|
|
1807
|
-
?
|
|
1914
|
+
? h('span', { className: 'dkb-tag', title: 'Comments' }, `💬 ${task.comments.length}`)
|
|
1808
1915
|
: null,
|
|
1809
|
-
(task.labels || []).map((l) =>
|
|
1916
|
+
(task.labels || []).map((l) => h('span', {
|
|
1810
1917
|
className: 'dkb-tag', key: l, style: tagStyle(task, l),
|
|
1811
1918
|
}, l))),
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1919
|
+
h('div', { className: 'dkb-taskTitle' }, task.title),
|
|
1920
|
+
task.subtasksCount && task.subtasksCount.total > 0
|
|
1921
|
+
? h('div', { className: 'dkb-progressBar', title: `${task.subtasksCount.completed}/${task.subtasksCount.total}` },
|
|
1922
|
+
h('div', {
|
|
1923
|
+
className: 'dkb-progressFill',
|
|
1924
|
+
style: { width: Math.round((task.subtasksCount.completed / task.subtasksCount.total) * 100) + '%' },
|
|
1925
|
+
}))
|
|
1926
|
+
: null,
|
|
1927
|
+
plan ? h('div', { className: 'dkb-planLine' }, plan) : null,
|
|
1928
|
+
h('div', { className: 'dkb-cardMenu' },
|
|
1929
|
+
h('button', {
|
|
1819
1930
|
type: 'button', className: 'dkb-moveBtn',
|
|
1820
|
-
// Скрыто до наведения — но появляется и при фокусе с клавиатуры:
|
|
1821
|
-
// прятать ради красоты то, что чинили ради доступности, нельзя.
|
|
1822
1931
|
'aria-expanded': menu === task.id,
|
|
1823
1932
|
onClick: (e) => { e.stopPropagation(); props.onMenu(menu === task.id ? '' : task.id) },
|
|
1824
1933
|
}, t('card.move')),
|
|
1825
1934
|
menu === task.id
|
|
1826
|
-
?
|
|
1827
|
-
(props.columns || []).filter((c) => c !== task.column).map((c) =>
|
|
1935
|
+
? h('div', { className: 'dkb-facetList' },
|
|
1936
|
+
(props.columns || []).filter((c) => c !== task.column).map((c) => h('button', {
|
|
1828
1937
|
type: 'button', className: 'dkb-facetRow', key: c,
|
|
1829
1938
|
onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onMove(task, c) },
|
|
1830
1939
|
}, t('column.' + c))),
|
|
1831
|
-
// Очередь стоит рядом с переносом: и то и другое — «отдать
|
|
1832
|
-
// работу», разница лишь в том, кому и когда.
|
|
1833
|
-
// Останавливать нечего у того, кто не идёт: пункт, который
|
|
1834
|
-
// ничего не делает, хуже отсутствующего.
|
|
1835
1940
|
task.state === 'stopped'
|
|
1836
|
-
?
|
|
1941
|
+
? h('button', {
|
|
1837
1942
|
type: 'button', className: 'dkb-facetRow', key: 'revive',
|
|
1838
1943
|
onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onRevive(task) },
|
|
1839
1944
|
}, t('card.revive'))
|
|
1840
1945
|
: null,
|
|
1841
1946
|
task.state === 'running' || task.state === 'waiting'
|
|
1842
|
-
?
|
|
1947
|
+
? h('button', {
|
|
1843
1948
|
type: 'button', className: 'dkb-facetRow', key: 'stop',
|
|
1844
1949
|
onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onStop(task) },
|
|
1845
1950
|
}, t('card.stop'))
|
|
1846
1951
|
: null,
|
|
1847
1952
|
task.state === 'queued'
|
|
1848
|
-
?
|
|
1953
|
+
? h('button', {
|
|
1849
1954
|
type: 'button', className: 'dkb-facetRow', key: 'unqueue',
|
|
1850
1955
|
onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onUnqueue(task) },
|
|
1851
1956
|
}, t('card.unqueue'))
|
|
1852
|
-
:
|
|
1957
|
+
: h('button', {
|
|
1853
1958
|
type: 'button', className: 'dkb-facetRow', key: 'queue',
|
|
1854
1959
|
onClick: (e) => { e.stopPropagation(); props.onMenu(''); props.onQueue(task) },
|
|
1855
1960
|
}, t('card.queue')))
|
|
1856
1961
|
: null,
|
|
1857
1962
|
),
|
|
1858
1963
|
status || state || born || touched
|
|
1859
|
-
?
|
|
1964
|
+
? h('div', { className: 'dkb-taskRef' },
|
|
1860
1965
|
state
|
|
1861
|
-
?
|
|
1966
|
+
? h('span', {
|
|
1862
1967
|
className: 'dkb-state' + (task.state === 'running' ? ' dkb-stateLive' : ''),
|
|
1863
1968
|
}, state)
|
|
1864
1969
|
: null,
|
|
1865
|
-
// Ссылка в чат стоит рядом с отметкой работы: «идёт» без пути к
|
|
1866
|
-
// тому, что идёт, — сведение, которым нельзя воспользоваться.
|
|
1867
1970
|
task.sessionId
|
|
1868
|
-
?
|
|
1971
|
+
? h('button', {
|
|
1869
1972
|
type: 'button', className: 'dkb-toChat',
|
|
1870
1973
|
onClick: (e) => { e.stopPropagation(); props.onOpenChat(task) },
|
|
1871
1974
|
}, t('card.toChat'))
|
|
1872
1975
|
: null,
|
|
1873
1976
|
touched
|
|
1874
|
-
?
|
|
1977
|
+
? h('span', {
|
|
1875
1978
|
className: task.stale ? 'dkb-countOver' : '',
|
|
1876
1979
|
title: exactAt(task.updatedAt),
|
|
1877
1980
|
}, touched)
|
|
1878
1981
|
: null,
|
|
1879
1982
|
born
|
|
1880
|
-
?
|
|
1983
|
+
? h('span', { title: exactAt(task.createdAt) }, born)
|
|
1881
1984
|
: null,
|
|
1882
1985
|
task.dueAt > 0
|
|
1883
|
-
?
|
|
1986
|
+
? h('span', {
|
|
1884
1987
|
className: overdue ? 'dkb-dueOver' : '',
|
|
1885
1988
|
title: exactAt(task.dueAt),
|
|
1886
1989
|
}, t('card.due', { date: shortDate(task.dueAt) }))
|
|
1887
1990
|
: null,
|
|
1888
1991
|
task.assignee
|
|
1889
|
-
?
|
|
1992
|
+
? h('span', { className: 'dkb-who', title: t('card.assigneeHint') },
|
|
1890
1993
|
'@' + task.assignee)
|
|
1891
1994
|
: null,
|
|
1892
1995
|
pack
|
|
1893
|
-
?
|
|
1996
|
+
? h('span', {
|
|
1894
1997
|
className: 'dkb-pack', title: t('card.packHint'),
|
|
1895
1998
|
}, t('card.pack', { at: pack.at, total: pack.total }))
|
|
1896
1999
|
: null,
|
|
1897
|
-
status ?
|
|
2000
|
+
status ? h('span', null, status) : null)
|
|
1898
2001
|
: null,
|
|
1899
2002
|
)
|
|
1900
|
-
}
|
|
2003
|
+
})
|
|
1901
2004
|
|
|
1902
2005
|
/**
|
|
1903
2006
|
* Чем запускать: профиль агента и уровень доступа.
|
|
@@ -1913,36 +2016,34 @@ window.__ModuleLoader__.load({
|
|
|
1913
2016
|
const t = props.t
|
|
1914
2017
|
const rows = []
|
|
1915
2018
|
if (!props.hidePreset && (props.presets.agentPresets || []).length > 0) {
|
|
1916
|
-
rows.push(
|
|
1917
|
-
|
|
1918
|
-
|
|
2019
|
+
rows.push(h('div', { className: 'dkb-field', key: 'preset' },
|
|
2020
|
+
h('span', { className: 'dkb-label' }, t('panel.agentPreset')),
|
|
2021
|
+
h('select', {
|
|
1919
2022
|
className: 'dkb-input', value: props.agentPreset, disabled: props.disabled,
|
|
1920
2023
|
onChange: (e) => props.onPreset(e.target.value),
|
|
1921
2024
|
},
|
|
1922
|
-
|
|
1923
|
-
props.presets.agentPresets.map((one) =>
|
|
2025
|
+
h('option', { value: '' }, t('panel.presetDefault')),
|
|
2026
|
+
props.presets.agentPresets.map((one) => h('option', {
|
|
1924
2027
|
value: one.id, key: one.id, title: one.description || '',
|
|
1925
2028
|
}, one.name || one.id))),
|
|
1926
2029
|
))
|
|
1927
2030
|
}
|
|
1928
2031
|
if ((props.presets.access || []).length > 0) {
|
|
1929
2032
|
const picked = (props.presets.access || []).find((one) => one.value === props.permission)
|
|
1930
|
-
rows.push(
|
|
1931
|
-
|
|
1932
|
-
|
|
2033
|
+
rows.push(h('div', { className: 'dkb-field', key: 'access' },
|
|
2034
|
+
h('span', { className: 'dkb-label' }, t('panel.permission')),
|
|
2035
|
+
h('select', {
|
|
1933
2036
|
className: 'dkb-input', value: props.permission, disabled: props.disabled,
|
|
1934
2037
|
onChange: (e) => props.onPermission(e.target.value),
|
|
1935
|
-
}, props.presets.access.map((one) =>
|
|
2038
|
+
}, props.presets.access.map((one) => h('option', {
|
|
1936
2039
|
value: one.value, key: one.value,
|
|
1937
2040
|
}, one.name || one.value))),
|
|
1938
|
-
// Пояснение к уровню доступа пишет ядро: свои слова разошлись бы с
|
|
1939
|
-
// тем, что этот уровень значит на самом деле.
|
|
1940
2041
|
picked && picked.description
|
|
1941
|
-
?
|
|
2042
|
+
? h('p', { className: 'dkb-hint' }, picked.description)
|
|
1942
2043
|
: null,
|
|
1943
2044
|
))
|
|
1944
2045
|
}
|
|
1945
|
-
return rows.length === 0 ? null :
|
|
2046
|
+
return rows.length === 0 ? null : h('div', { className: 'dkb-start' }, rows)
|
|
1946
2047
|
}
|
|
1947
2048
|
|
|
1948
2049
|
function BoardScreen(props) {
|
|
@@ -1954,11 +2055,17 @@ window.__ModuleLoader__.load({
|
|
|
1954
2055
|
const [rejectReasonText, setRejectReasonText] = React.useState('')
|
|
1955
2056
|
const [newChecklistText, setNewChecklistText] = React.useState('')
|
|
1956
2057
|
const [newCommentText, setNewCommentText] = React.useState('')
|
|
2058
|
+
const [quickFilter, setQuickFilter] = React.useState('all')
|
|
2059
|
+
const [subtasks, setSubtasks] = React.useState([])
|
|
2060
|
+
const [triageOpen, setTriageOpen] = React.useState(false)
|
|
2061
|
+
const [triageText, setTriageText] = React.useState('')
|
|
2062
|
+
const [triageGitea, setTriageGitea] = React.useState(false)
|
|
2063
|
+
const [triageSubmitting, setTriageSubmitting] = React.useState(false)
|
|
2064
|
+
const [newSubtaskTitle, setNewSubtaskTitle] = React.useState('')
|
|
2065
|
+
const [prCreating, setPrCreating] = React.useState(false)
|
|
2066
|
+
const [createdPr, setCreatedPr] = React.useState(null)
|
|
1957
2067
|
const [log, setLog] = React.useState([])
|
|
1958
2068
|
const [dialog, setDialog] = React.useState(null)
|
|
1959
|
-
// Черновик правки приоритета и меток в окне задачи. Живёт отдельно от
|
|
1960
|
-
// карточки: пока человек не нажал «Сохранить», доска не должна видеть
|
|
1961
|
-
// полувведённое.
|
|
1962
2069
|
const [editPriority, setEditPriority] = React.useState('')
|
|
1963
2070
|
const [editLabels, setEditLabels] = React.useState('')
|
|
1964
2071
|
const [editDue, setEditDue] = React.useState('')
|
|
@@ -1971,41 +2078,30 @@ window.__ModuleLoader__.load({
|
|
|
1971
2078
|
const [starting, setStarting] = React.useState(false)
|
|
1972
2079
|
const [draftText, setDraftText] = React.useState('')
|
|
1973
2080
|
const [query, setQuery] = React.useState('')
|
|
2081
|
+
const [debouncedQuery, setDebouncedQuery] = React.useState('')
|
|
2082
|
+
React.useEffect(() => {
|
|
2083
|
+
const timer = setTimeout(() => setDebouncedQuery(query), 150)
|
|
2084
|
+
return () => clearTimeout(timer)
|
|
2085
|
+
}, [query])
|
|
1974
2086
|
const [dropAt, setDropAt] = React.useState(null)
|
|
1975
2087
|
const [board, setBoard] = React.useState('main')
|
|
1976
|
-
// Решения о сворачивании держим в состоянии экрана, а не в хранилище:
|
|
1977
|
-
// это взгляд одного человека на одну доску прямо сейчас, а не свойство
|
|
1978
|
-
// задач. Обновление доски их переживает, перезагрузка страницы — нет.
|
|
1979
2088
|
const [collapsed, setCollapsed] = React.useState({})
|
|
1980
|
-
// Отборы живут в состоянии экрана: он смонтирован всё время работы
|
|
1981
|
-
// харнесса, поэтому выбор переживает и закрытие доски, и обновление.
|
|
1982
2089
|
const [filters, setFilters] = React.useState({})
|
|
1983
2090
|
const [openFacet, setOpenFacet] = React.useState('')
|
|
1984
2091
|
const [cardMenu, setCardMenu] = React.useState('')
|
|
1985
2092
|
const [body, setBody] = React.useState(null)
|
|
1986
2093
|
const [groups, setGroups] = React.useState({})
|
|
1987
2094
|
const [syncing, setSyncing] = React.useState(false)
|
|
1988
|
-
// Отмеченные для группового запуска. Живут отдельно от открытой карточки:
|
|
1989
|
-
// выбор переживает и открытие окна, и обновление доски.
|
|
1990
2095
|
const [picked, setPicked] = React.useState({})
|
|
1991
2096
|
const [archive, setArchive] = React.useState(null)
|
|
1992
2097
|
const [metrics, setMetrics] = React.useState(null)
|
|
1993
|
-
// «Только ждущие» держим отдельно от отборов: это не значение измерения,
|
|
1994
|
-
// а взгляд «покажи, где стоит работа», и складывается он с любым отбором.
|
|
1995
2098
|
const [onlyWaiting, setOnlyWaiting] = React.useState(false)
|
|
1996
2099
|
const [onlyOverdue, setOnlyOverdue] = React.useState(false)
|
|
1997
2100
|
const [hoverSession, setHoverSession] = React.useState('')
|
|
1998
2101
|
const [order, setOrder] = React.useState('manual')
|
|
1999
|
-
// «По проектам» — как приходит с сервера; «по ответственному» — своя
|
|
2000
|
-
// раскладка поверх того же порядка.
|
|
2001
2102
|
const [grouping, setGrouping] = React.useState('repo')
|
|
2002
|
-
// Кто «я»: спрашивается один раз у сервера. Пусто — Gitea не ответил, и
|
|
2003
|
-
// кнопку «Мои» рисовать нельзя: соврать числом хуже, чем не показать.
|
|
2004
2103
|
const [me, setMe] = React.useState('')
|
|
2005
2104
|
const [onlyMine, setOnlyMine] = React.useState(false)
|
|
2006
|
-
// Чем запускать: профиль агента и уровень доступа. Обе службы у ядра
|
|
2007
|
-
// необязательны, поэтому пустой список — не поломка, а «выбирать не из
|
|
2008
|
-
// чего»: поле тогда не рисуется вовсе.
|
|
2009
2105
|
const [presets, setPresets] = React.useState({ agentPresets: [], access: [] })
|
|
2010
2106
|
const [agentPreset, setAgentPreset] = React.useState('')
|
|
2011
2107
|
const [permission, setPermission] = React.useState('')
|
|
@@ -2013,9 +2109,6 @@ window.__ModuleLoader__.load({
|
|
|
2013
2109
|
|
|
2014
2110
|
const load = React.useCallback(async (nextBoard) => {
|
|
2015
2111
|
try {
|
|
2016
|
-
// Отбор по репозиторию делает браузер вместе с остальными отборами:
|
|
2017
|
-
// серверный отбор спорил бы с ним и прятал бы часть значений из
|
|
2018
|
-
// списка, потому что их не было бы в ответе.
|
|
2019
2112
|
const query = '?board=' + encodeURIComponent(nextBoard || 'main')
|
|
2020
2113
|
setState(normalizeBoard(await api('/board' + query)))
|
|
2021
2114
|
setError('')
|
|
@@ -2026,7 +2119,6 @@ window.__ModuleLoader__.load({
|
|
|
2026
2119
|
|
|
2027
2120
|
React.useEffect(() => { load(board) }, [load, board])
|
|
2028
2121
|
|
|
2029
|
-
// Подписка на Server-Sent Events (SSE) для мгновенного обновления доски (#222).
|
|
2030
2122
|
React.useEffect(() => {
|
|
2031
2123
|
if (typeof window === 'undefined' || typeof window.EventSource !== 'function') return undefined
|
|
2032
2124
|
let es = null
|
|
@@ -2035,12 +2127,16 @@ window.__ModuleLoader__.load({
|
|
|
2035
2127
|
const connect = () => {
|
|
2036
2128
|
try {
|
|
2037
2129
|
es = new window.EventSource('/dsh-kanban/events')
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2130
|
+
let reloadTimer = null
|
|
2131
|
+
const scheduleReload = () => {
|
|
2132
|
+
if (reloadTimer) return
|
|
2133
|
+
reloadTimer = setTimeout(() => {
|
|
2134
|
+
reloadTimer = null
|
|
2135
|
+
load(board)
|
|
2136
|
+
}, 60)
|
|
2137
|
+
}
|
|
2138
|
+
es.addEventListener('board:change', scheduleReload)
|
|
2139
|
+
es.addEventListener('task:change', scheduleReload)
|
|
2044
2140
|
es.onerror = () => {
|
|
2045
2141
|
if (es) {
|
|
2046
2142
|
try { es.close() } catch {}
|
|
@@ -2067,8 +2163,6 @@ window.__ModuleLoader__.load({
|
|
|
2067
2163
|
}
|
|
2068
2164
|
}, [load, board])
|
|
2069
2165
|
|
|
2070
|
-
// Просьба чипа «покажи вот эту». Забираем её один раз: повтор открывал
|
|
2071
|
-
// бы окно снова после каждого закрытия.
|
|
2072
2166
|
const [wanted, setWanted] = React.useState('')
|
|
2073
2167
|
React.useEffect(() => {
|
|
2074
2168
|
const toggle = props.toggle
|
|
@@ -2083,8 +2177,6 @@ window.__ModuleLoader__.load({
|
|
|
2083
2177
|
return toggle.subscribe(pull)
|
|
2084
2178
|
}, [props.toggle])
|
|
2085
2179
|
|
|
2086
|
-
// Открываем, когда задача приехала: доска могла переключаться, и в
|
|
2087
|
-
// момент просьбы её ещё не было в списке.
|
|
2088
2180
|
React.useEffect(() => {
|
|
2089
2181
|
if (wanted === '' || state === null) return
|
|
2090
2182
|
const task = state.tasks.find((x) => x.id === wanted)
|
|
@@ -2093,9 +2185,6 @@ window.__ModuleLoader__.load({
|
|
|
2093
2185
|
openPanelRef.current(task)
|
|
2094
2186
|
}, [wanted, state])
|
|
2095
2187
|
|
|
2096
|
-
// Escape снимает по одному слою за нажатие: диалог, затем панель, затем
|
|
2097
|
-
// саму доску. Закрывать всё разом — терять контекст, которого человек не
|
|
2098
|
-
// просил лишаться.
|
|
2099
2188
|
React.useEffect(() => {
|
|
2100
2189
|
const onKey = (e) => {
|
|
2101
2190
|
if (e.key !== 'Escape') return
|
|
@@ -2107,7 +2196,6 @@ window.__ModuleLoader__.load({
|
|
|
2107
2196
|
return () => document.removeEventListener('keydown', onKey)
|
|
2108
2197
|
}, [dialog, openTask, props])
|
|
2109
2198
|
|
|
2110
|
-
// Сначала провайдеры и текущий выбор, затем модели выбранного провайдера.
|
|
2111
2199
|
React.useEffect(() => {
|
|
2112
2200
|
let alive = true
|
|
2113
2201
|
api('/models')
|
|
@@ -2119,8 +2207,6 @@ window.__ModuleLoader__.load({
|
|
|
2119
2207
|
const provider = stillOffered(saved.provider, list.map((p) => p.id), '')
|
|
2120
2208
|
if (provider !== '') {
|
|
2121
2209
|
setProvider(provider)
|
|
2122
|
-
// Модель проверяется своим списком, когда он приедет: здесь её
|
|
2123
|
-
// ещё не с чем сверить, а подставить чужую нельзя.
|
|
2124
2210
|
setModel(typeof saved.model === 'string' ? saved.model : '')
|
|
2125
2211
|
} else if (out.current) {
|
|
2126
2212
|
setProvider(out.current.provider || '')
|
|
@@ -2142,8 +2228,6 @@ window.__ModuleLoader__.load({
|
|
|
2142
2228
|
const rows = out.agentPresets || []
|
|
2143
2229
|
const access = out.access || []
|
|
2144
2230
|
setPresets({ agentPresets: rows, access })
|
|
2145
|
-
// Пустое значение — «как у харнесса по умолчанию»; подставляем его
|
|
2146
|
-
// же явно, чтобы человек видел, с чем задача пойдёт.
|
|
2147
2231
|
const saved = recallLaunch(board)
|
|
2148
2232
|
setAgentPreset(stillOffered(saved.agentPreset, rows.map((x) => x.id), out.agentPresetDefault || ''))
|
|
2149
2233
|
setPermission(stillOffered(saved.permission, access.map((x) => x.value), out.accessDefault || ''))
|
|
@@ -2160,8 +2244,6 @@ window.__ModuleLoader__.load({
|
|
|
2160
2244
|
if (!alive || !out) return
|
|
2161
2245
|
const list = out.models || []
|
|
2162
2246
|
setModels(list)
|
|
2163
|
-
// Модель могла исчезнуть у провайдера: подставленная из памяти, она
|
|
2164
|
-
// осталась бы в поле, а запуск отказал бы уже после нажатия.
|
|
2165
2247
|
setModel((cur) => stillOffered(cur, list.map((m) => m.id), ''))
|
|
2166
2248
|
})
|
|
2167
2249
|
.catch(() => { if (alive) setModels([]) })
|
|
@@ -2169,8 +2251,6 @@ window.__ModuleLoader__.load({
|
|
|
2169
2251
|
}, [provider])
|
|
2170
2252
|
|
|
2171
2253
|
const commitMove = React.useCallback(async (task, column, where) => {
|
|
2172
|
-
// Карточку не перерисовываем заранее: до подтверждения она обязана
|
|
2173
|
-
// оставаться там, где лежала, иначе отмена выглядит откатом.
|
|
2174
2254
|
try {
|
|
2175
2255
|
await api('/task/' + encodeURIComponent(task.id) + '/move', {
|
|
2176
2256
|
method: 'POST',
|
|
@@ -2192,7 +2272,6 @@ window.__ModuleLoader__.load({
|
|
|
2192
2272
|
const askMove = React.useCallback((task, column, where) => {
|
|
2193
2273
|
if (!task) return
|
|
2194
2274
|
const place = where ?? neighboursFor(tasksOf((state && state.tasks) || [], column), task.id, Infinity)
|
|
2195
|
-
// Перенос внутри колонки — это порядок, а не команда: спрашивать не о чем.
|
|
2196
2275
|
if (task.column === column) { commitMove(task, column, place); return }
|
|
2197
2276
|
setDialog({ mode: 'move', task, column, where: place })
|
|
2198
2277
|
}, [state, commitMove])
|
|
@@ -2203,7 +2282,6 @@ window.__ModuleLoader__.load({
|
|
|
2203
2282
|
setOverColumn('')
|
|
2204
2283
|
setDropAt(null)
|
|
2205
2284
|
if (!task || !state) return
|
|
2206
|
-
// Мышь слишком легко задевает карточку, чтобы перенос уходил без спроса.
|
|
2207
2285
|
askMove(task, column, neighboursFor(tasksOf(state.tasks, column), task.id, index))
|
|
2208
2286
|
}, [state, askMove])
|
|
2209
2287
|
|
|
@@ -2218,6 +2296,15 @@ window.__ModuleLoader__.load({
|
|
|
2218
2296
|
? new Date(task.dueAt).toISOString().slice(0, 10)
|
|
2219
2297
|
: '')
|
|
2220
2298
|
setEditCron(typeof task?.cron === 'string' ? task.cron : '')
|
|
2299
|
+
setTriageOpen(false)
|
|
2300
|
+
setTriageText('')
|
|
2301
|
+
setTriageGitea(false)
|
|
2302
|
+
setCreatedPr(null)
|
|
2303
|
+
setNewSubtaskTitle('')
|
|
2304
|
+
setSubtasks([])
|
|
2305
|
+
api('/task/' + encodeURIComponent(task.id) + '/subtasks')
|
|
2306
|
+
.then((out) => setSubtasks((out && out.subtasks) || []))
|
|
2307
|
+
.catch(() => setSubtasks([]))
|
|
2221
2308
|
api('/task/' + encodeURIComponent(task.id) + '/body')
|
|
2222
2309
|
.then((out) => setBody((out && out.blocks) || []))
|
|
2223
2310
|
.catch(() => setBody([]))
|
|
@@ -2231,75 +2318,65 @@ window.__ModuleLoader__.load({
|
|
|
2231
2318
|
}, [])
|
|
2232
2319
|
openPanelRef.current = openPanel
|
|
2233
2320
|
|
|
2234
|
-
|
|
2321
|
+
ensureCss()
|
|
2322
|
+
const style = null
|
|
2235
2323
|
|
|
2236
2324
|
if (state === null) {
|
|
2237
|
-
return
|
|
2238
|
-
|
|
2325
|
+
return h('div', { className: 'dkb-screen' },
|
|
2326
|
+
h('p', { className: 'dkb-note' }, error || t('board.loading')))
|
|
2239
2327
|
}
|
|
2240
2328
|
|
|
2241
2329
|
const boards = (state && state.boards) || []
|
|
2242
|
-
const bar =
|
|
2243
|
-
|
|
2244
|
-
// Своего механизма закрытия не заводит: их уже три (Escape, повторный
|
|
2245
|
-
// щелчок по боковой строке, переход в чужую сессию), и четвёртый
|
|
2246
|
-
// разошёлся бы с ними при первой же правке.
|
|
2247
|
-
React.createElement('button', {
|
|
2330
|
+
const bar = h('div', { className: 'dkb-bar' },
|
|
2331
|
+
h('button', {
|
|
2248
2332
|
type: 'button', className: 'dkb-back', title: t('board.backHint'),
|
|
2249
2333
|
onClick: () => { if (props.onClose) props.onClose() },
|
|
2250
2334
|
},
|
|
2251
|
-
|
|
2335
|
+
h('span', {
|
|
2252
2336
|
className: 'dkb-backIcon',
|
|
2253
2337
|
dangerouslySetInnerHTML: { __html: BACK_ICON },
|
|
2254
2338
|
}),
|
|
2255
|
-
|
|
2339
|
+
h('span', null, t('board.back')),
|
|
2256
2340
|
),
|
|
2257
|
-
|
|
2341
|
+
h('span', { className: 'dkb-barTitle' }, t('section.label')),
|
|
2258
2342
|
boards.length > 1
|
|
2259
|
-
?
|
|
2343
|
+
? h('select', {
|
|
2260
2344
|
className: 'dkb-input', style: { width: 'auto' }, value: board,
|
|
2261
2345
|
onChange: (e) => {
|
|
2262
|
-
// Отборы собраны из меток ЭТОЙ доски: на другой те же значения
|
|
2263
|
-
// могут не встречаться вовсе, и выбор превратился бы в пустоту.
|
|
2264
2346
|
setOpenTask(null); setCollapsed({}); setFilters({}); setBoard(e.target.value)
|
|
2265
2347
|
},
|
|
2266
|
-
}, boards.map((b) =>
|
|
2348
|
+
}, boards.map((b) => h('option', { value: b.id, key: b.id }, boardTitle(b, t))))
|
|
2267
2349
|
: null,
|
|
2268
2350
|
(state.facets || []).map((facet) => {
|
|
2269
2351
|
const picked = filters[facet.ns] || []
|
|
2270
|
-
// Имена пространств приходят из Gitea сырыми. Известные переводим,
|
|
2271
|
-
// незнакомое оставляем как есть: выдумывать перевод метке, которую
|
|
2272
|
-
// завели вчера, доска не вправе.
|
|
2273
2352
|
const label = facetLabel(facet.ns, t)
|
|
2274
|
-
return
|
|
2275
|
-
|
|
2353
|
+
return h('div', { className: 'dkb-facet', key: facet.ns },
|
|
2354
|
+
h('button', {
|
|
2276
2355
|
type: 'button',
|
|
2277
2356
|
className: 'dkb-facetHead' + (picked.length ? ' dkb-facetOn' : ''),
|
|
2278
2357
|
'aria-expanded': openFacet === facet.ns,
|
|
2279
2358
|
onClick: () => setOpenFacet((cur) => (cur === facet.ns ? '' : facet.ns)),
|
|
2280
2359
|
}, picked.length ? label + ' · ' + picked.length : label),
|
|
2281
2360
|
openFacet === facet.ns
|
|
2282
|
-
?
|
|
2283
|
-
facet.values.map((v) =>
|
|
2361
|
+
? h('div', { className: 'dkb-facetList' },
|
|
2362
|
+
facet.values.map((v) => h('label', {
|
|
2284
2363
|
className: 'dkb-facetRow', key: v.value,
|
|
2285
2364
|
},
|
|
2286
|
-
|
|
2365
|
+
h('input', {
|
|
2287
2366
|
type: 'checkbox',
|
|
2288
2367
|
checked: picked.indexOf(v.value) >= 0,
|
|
2289
2368
|
onChange: () => setFilters((cur) => toggleValue(cur, facet.ns, v.value)),
|
|
2290
2369
|
}),
|
|
2291
|
-
|
|
2292
|
-
|
|
2370
|
+
h('span', { className: 'dkb-facetName' }, v.value),
|
|
2371
|
+
h('span', { className: 'dkb-count' }, String(v.count)),
|
|
2293
2372
|
)))
|
|
2294
2373
|
: null,
|
|
2295
2374
|
)
|
|
2296
2375
|
}),
|
|
2297
2376
|
(() => {
|
|
2298
|
-
// Ноль в шапке — мебель, за которой перестают следить, поэтому
|
|
2299
|
-
// счётчик появляется только когда кого-то действительно ждут.
|
|
2300
2377
|
const waiting = state.tasks.filter((x) => x.state === 'waiting').length
|
|
2301
2378
|
if (waiting === 0) return null
|
|
2302
|
-
return
|
|
2379
|
+
return h('button', {
|
|
2303
2380
|
type: 'button',
|
|
2304
2381
|
className: 'dkb-waitCount' + (onlyWaiting ? ' dkb-save' : ''),
|
|
2305
2382
|
title: t('board.waitCountHint'),
|
|
@@ -2307,11 +2384,9 @@ window.__ModuleLoader__.load({
|
|
|
2307
2384
|
}, t('board.waitCount', { n: waiting }))
|
|
2308
2385
|
})(),
|
|
2309
2386
|
(() => {
|
|
2310
|
-
// Просроченные — те, чей дедлайн уже прошёл. Счётчик появляется
|
|
2311
|
-
// только когда такие есть: ноль в шапке — мебель.
|
|
2312
2387
|
const overdue = state.tasks.filter((x) => x.overdue === true).length
|
|
2313
2388
|
if (overdue === 0) return null
|
|
2314
|
-
return
|
|
2389
|
+
return h('button', {
|
|
2315
2390
|
type: 'button',
|
|
2316
2391
|
className: 'dkb-waitCount' + (onlyOverdue ? ' dkb-save' : ''),
|
|
2317
2392
|
title: t('board.overdueHint'),
|
|
@@ -2319,34 +2394,32 @@ window.__ModuleLoader__.load({
|
|
|
2319
2394
|
}, t('board.overdueCount', { n: overdue }))
|
|
2320
2395
|
})(),
|
|
2321
2396
|
(() => {
|
|
2322
|
-
// Ноль своих задач — кнопки нет: пустой счётчик в шапке перестают
|
|
2323
|
-
// замечать вместе с непустым.
|
|
2324
2397
|
if (me === '') return null
|
|
2325
2398
|
const mine = state.tasks.filter((x) => x.assignee === me).length
|
|
2326
2399
|
if (mine === 0) return null
|
|
2327
|
-
return
|
|
2400
|
+
return h('button', {
|
|
2328
2401
|
type: 'button',
|
|
2329
2402
|
className: 'dkb-waitCount' + (onlyMine ? ' dkb-save' : ''),
|
|
2330
2403
|
title: t('board.mineHint', { who: me }),
|
|
2331
2404
|
onClick: () => setOnlyMine((cur) => !cur),
|
|
2332
2405
|
}, t('board.mine', { n: mine }))
|
|
2333
2406
|
})(),
|
|
2334
|
-
|
|
2407
|
+
h('select', {
|
|
2335
2408
|
className: 'dkb-input', style: { width: 'auto' }, value: grouping,
|
|
2336
2409
|
onChange: (e) => setGrouping(e.target.value),
|
|
2337
2410
|
},
|
|
2338
|
-
|
|
2339
|
-
|
|
2411
|
+
h('option', { value: 'repo' }, t('group.repo')),
|
|
2412
|
+
h('option', { value: 'assignee' }, t('group.assignee')),
|
|
2340
2413
|
),
|
|
2341
|
-
|
|
2414
|
+
h('select', {
|
|
2342
2415
|
className: 'dkb-input', style: { width: 'auto' }, value: order,
|
|
2343
2416
|
onChange: (e) => setOrder(e.target.value),
|
|
2344
2417
|
},
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2418
|
+
h('option', { value: 'manual' }, t('order.manual')),
|
|
2419
|
+
h('option', { value: 'old' }, t('order.old')),
|
|
2420
|
+
h('option', { value: 'new' }, t('order.new')),
|
|
2348
2421
|
),
|
|
2349
|
-
|
|
2422
|
+
h('button', {
|
|
2350
2423
|
type: 'button', className: metrics ? 'dkb-save' : 'dkb-discard',
|
|
2351
2424
|
onClick: async () => {
|
|
2352
2425
|
if (metrics) { setMetrics(null); return }
|
|
@@ -2356,7 +2429,7 @@ window.__ModuleLoader__.load({
|
|
|
2356
2429
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
2357
2430
|
},
|
|
2358
2431
|
}, t('board.metrics')),
|
|
2359
|
-
|
|
2432
|
+
h('button', {
|
|
2360
2433
|
type: 'button', className: archive ? 'dkb-save' : 'dkb-discard',
|
|
2361
2434
|
onClick: async () => {
|
|
2362
2435
|
if (archive) { setArchive(null); return }
|
|
@@ -2367,8 +2440,7 @@ window.__ModuleLoader__.load({
|
|
|
2367
2440
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
2368
2441
|
},
|
|
2369
2442
|
}, t('board.archive')),
|
|
2370
|
-
|
|
2371
|
-
React.createElement('button', {
|
|
2443
|
+
h('button', {
|
|
2372
2444
|
type: 'button', className: 'dkb-discard',
|
|
2373
2445
|
title: t('board.exportHint'),
|
|
2374
2446
|
onClick: async () => {
|
|
@@ -2384,9 +2456,9 @@ window.__ModuleLoader__.load({
|
|
|
2384
2456
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
2385
2457
|
},
|
|
2386
2458
|
}, t('board.export')),
|
|
2387
|
-
|
|
2459
|
+
h('label', { className: 'dkb-discard dkb-import' },
|
|
2388
2460
|
t('board.import'),
|
|
2389
|
-
|
|
2461
|
+
h('input', {
|
|
2390
2462
|
type: 'file', accept: 'application/json,.json', style: { display: 'none' },
|
|
2391
2463
|
onChange: async (e) => {
|
|
2392
2464
|
const file = e.target.files && e.target.files[0]
|
|
@@ -2400,42 +2472,34 @@ window.__ModuleLoader__.load({
|
|
|
2400
2472
|
} catch { setError(t('error.badImport')) }
|
|
2401
2473
|
},
|
|
2402
2474
|
})),
|
|
2403
|
-
// «Показано 12 из 81» появляется, только когда что-то отобрано или
|
|
2404
|
-
// введён поиск: на нетронутой доске эта строка была бы шумом.
|
|
2405
2475
|
anySelected(filters) || query.trim() !== ''
|
|
2406
|
-
?
|
|
2476
|
+
? h('span', { className: 'dkb-shown' }, t('board.shown', {
|
|
2407
2477
|
shown: state.tasks.filter((x) => matchesFilters(x, filters) && matchesQuery(x, query)).length,
|
|
2408
2478
|
total: state.tasks.length,
|
|
2409
2479
|
}))
|
|
2410
2480
|
: null,
|
|
2411
2481
|
anySelected(filters)
|
|
2412
|
-
?
|
|
2482
|
+
? h('button', {
|
|
2413
2483
|
type: 'button', className: 'dkb-discard',
|
|
2414
2484
|
onClick: () => { setFilters({}); setOpenFacet('') },
|
|
2415
2485
|
}, t('board.clearFilters'))
|
|
2416
2486
|
: null,
|
|
2417
|
-
|
|
2487
|
+
h('input', {
|
|
2418
2488
|
className: 'dkb-search', value: query, placeholder: t('board.search'),
|
|
2419
2489
|
onChange: (e) => setQuery(e.target.value),
|
|
2420
2490
|
}),
|
|
2421
2491
|
(() => {
|
|
2422
|
-
// Пока запрос в пути, состояние на доске ещё вчерашнее: оно приедет
|
|
2423
|
-
// только со следующим ответом. Поэтому ход показываем сами, а не
|
|
2424
|
-
// ждём, когда сервер расскажет о нём задним числом.
|
|
2425
2492
|
const line = syncing
|
|
2426
2493
|
? { tone: 'idle', text: t('sync.running'), title: '' }
|
|
2427
2494
|
: syncLine(state.sync, state.now, t)
|
|
2428
|
-
return
|
|
2495
|
+
return h('span', {
|
|
2429
2496
|
className: 'dkb-sync' + (line.tone === 'bad' ? ' dkb-syncBad' : ''),
|
|
2430
2497
|
title: line.title,
|
|
2431
2498
|
}, line.text)
|
|
2432
2499
|
})(),
|
|
2433
|
-
|
|
2500
|
+
h('button', {
|
|
2434
2501
|
type: 'button', className: 'dkb-discard', disabled: syncing,
|
|
2435
2502
|
onClick: async () => {
|
|
2436
|
-
// Кнопка гаснет на время прохода: нажать её второй раз значило бы
|
|
2437
|
-
// попросить сверку, которая и так идёт, а сервер ответил бы
|
|
2438
|
-
// «пропущено» — и человек решил бы, что ничего не случилось.
|
|
2439
2503
|
setSyncing(true)
|
|
2440
2504
|
try {
|
|
2441
2505
|
await api('/sync', { method: 'POST', body: '{}' })
|
|
@@ -2447,21 +2511,32 @@ window.__ModuleLoader__.load({
|
|
|
2447
2511
|
}
|
|
2448
2512
|
},
|
|
2449
2513
|
}, syncing ? t('board.syncing') : t('board.sync')),
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2514
|
+
h('button', { type: 'button', className: 'dkb-discard', onClick: () => load(board) }, t('board.refresh')),
|
|
2515
|
+
h('div', { className: 'dkb-quickFilterBar' },
|
|
2516
|
+
[
|
|
2517
|
+
{ id: 'all', label: t('filter.all') },
|
|
2518
|
+
{ id: 'active', label: t('filter.active') },
|
|
2519
|
+
{ id: 'review', label: t('filter.review') },
|
|
2520
|
+
{ id: 'epics', label: t('filter.epics') },
|
|
2521
|
+
{ id: 'stale', label: t('filter.stale') },
|
|
2522
|
+
].map((chip) => h('button', {
|
|
2523
|
+
type: 'button',
|
|
2524
|
+
key: chip.id,
|
|
2525
|
+
className: 'dkb-filterChip' + (quickFilter === chip.id ? ' dkb-filterChipActive' : ''),
|
|
2526
|
+
onClick: () => setQuickFilter(chip.id),
|
|
2527
|
+
}, chip.label))),
|
|
2453
2528
|
Object.keys(picked).length > 0
|
|
2454
|
-
?
|
|
2529
|
+
? h('span', { className: 'dkb-shown' },
|
|
2455
2530
|
t('board.pickedCount', { n: Object.keys(picked).length }))
|
|
2456
2531
|
: null,
|
|
2457
2532
|
Object.keys(picked).length > 0
|
|
2458
|
-
?
|
|
2533
|
+
? h('button', {
|
|
2459
2534
|
type: 'button', className: 'dkb-discard',
|
|
2460
2535
|
onClick: () => setPicked({}),
|
|
2461
2536
|
}, t('board.pickNone'))
|
|
2462
2537
|
: null,
|
|
2463
2538
|
Object.keys(picked).length > 0
|
|
2464
|
-
?
|
|
2539
|
+
? h('button', {
|
|
2465
2540
|
type: 'button', className: 'dkb-save',
|
|
2466
2541
|
onClick: () => setDialog({
|
|
2467
2542
|
mode: 'batch',
|
|
@@ -2471,15 +2546,13 @@ window.__ModuleLoader__.load({
|
|
|
2471
2546
|
}, t('board.pickStart'))
|
|
2472
2547
|
: null,
|
|
2473
2548
|
Object.keys(picked).length > 0
|
|
2474
|
-
?
|
|
2549
|
+
? h('button', {
|
|
2475
2550
|
type: 'button', className: 'dkb-discard',
|
|
2476
2551
|
onClick: async () => {
|
|
2477
2552
|
const ids = Object.keys(picked)
|
|
2478
2553
|
const out = await applyToEach(ids, (id) => api(
|
|
2479
2554
|
'/task/' + encodeURIComponent(id) + '/archive', { method: 'POST', body: '{}' },
|
|
2480
2555
|
))
|
|
2481
|
-
// Часть задач архивировать нельзя (архив только из «Выполнено»),
|
|
2482
|
-
// и число отказов называется вслух.
|
|
2483
2556
|
if (out.failed > 0) setError(t('board.someFailed', { done: out.done, total: ids.length }))
|
|
2484
2557
|
else setPicked({})
|
|
2485
2558
|
await load(board)
|
|
@@ -2487,7 +2560,7 @@ window.__ModuleLoader__.load({
|
|
|
2487
2560
|
}, t('board.pickArchive'))
|
|
2488
2561
|
: null,
|
|
2489
2562
|
Object.keys(picked).length > 0
|
|
2490
|
-
?
|
|
2563
|
+
? h('button', {
|
|
2491
2564
|
type: 'button', className: 'dkb-discard',
|
|
2492
2565
|
onClick: async () => {
|
|
2493
2566
|
const ids = Object.keys(picked)
|
|
@@ -2501,14 +2574,12 @@ window.__ModuleLoader__.load({
|
|
|
2501
2574
|
}, t('board.pickUnqueue'))
|
|
2502
2575
|
: null,
|
|
2503
2576
|
Object.keys(picked).length > 0
|
|
2504
|
-
?
|
|
2505
|
-
className: 'dkb-input',
|
|
2577
|
+
? h('select', {
|
|
2578
|
+
className: 'dkb-input',
|
|
2506
2579
|
onChange: async (e) => {
|
|
2507
2580
|
const column = e.target.value
|
|
2508
|
-
if (column
|
|
2581
|
+
if (!column) return
|
|
2509
2582
|
const ids = Object.keys(picked)
|
|
2510
|
-
// Групповой перенос — та же команда агенту, что и одиночный:
|
|
2511
|
-
// маршрут один, и последствия у него те же.
|
|
2512
2583
|
const out = await applyToEach(ids, (id) => api(
|
|
2513
2584
|
'/task/' + encodeURIComponent(id) + '/move',
|
|
2514
2585
|
{ method: 'POST', body: JSON.stringify({ column }) },
|
|
@@ -2518,36 +2589,33 @@ window.__ModuleLoader__.load({
|
|
|
2518
2589
|
await load(board)
|
|
2519
2590
|
},
|
|
2520
2591
|
},
|
|
2521
|
-
|
|
2522
|
-
(state.columns || []).map((c) =>
|
|
2592
|
+
h('option', { value: '' }, t('board.pickMove')),
|
|
2593
|
+
(state.columns || []).map((c) => h('option', {
|
|
2523
2594
|
value: c.id, key: c.id,
|
|
2524
2595
|
}, columnTitle(c, t))),
|
|
2525
2596
|
)
|
|
2526
2597
|
: null,
|
|
2527
|
-
|
|
2598
|
+
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
2599
|
)
|
|
2529
2600
|
|
|
2530
|
-
// Счётчик колонки считает ВСЕ карточки, а не найденные: иначе поиск
|
|
2531
|
-
// сделал бы вид, что предел колонки соблюдён.
|
|
2532
2601
|
const packs = packInfo(state.tasks)
|
|
2533
|
-
const columns =
|
|
2602
|
+
const columns = h('div', { className: 'dkb-cols' },
|
|
2534
2603
|
state.columns.map((col) => {
|
|
2535
|
-
// Счётчик колонки считает ОТОБРАННОЕ, а поиск — нет.
|
|
2536
|
-
//
|
|
2537
|
-
// Разница осмысленная, а не недосмотр: поиск временный, и предел
|
|
2538
|
-
// «3 из 3» при найденной одной карточке врал бы. Отбор постоянный, и
|
|
2539
|
-
// на отборе по одному проекту «в работе 3 из 3» должно значить «по
|
|
2540
|
-
// этому проекту» — иначе предел ни о чём.
|
|
2541
2604
|
const all = tasksOf(state.tasks, col.id).filter((task) => matchesFilters(task, filters))
|
|
2542
2605
|
const items = sortTasks(
|
|
2543
|
-
all.filter((task) => matchesQuery(task,
|
|
2606
|
+
all.filter((task) => matchesQuery(task, debouncedQuery))
|
|
2544
2607
|
.filter((task) => !onlyWaiting || task.state === 'waiting')
|
|
2545
2608
|
.filter((task) => !onlyOverdue || task.overdue === true)
|
|
2546
|
-
.filter((task) => !onlyMine || task.assignee === me)
|
|
2609
|
+
.filter((task) => !onlyMine || task.assignee === me)
|
|
2610
|
+
.filter((task) => {
|
|
2611
|
+
if (quickFilter === 'active') return task.column === 'in-progress'
|
|
2612
|
+
if (quickFilter === 'review') return task.column === 'review'
|
|
2613
|
+
if (quickFilter === 'epics') return task.isEpic === true
|
|
2614
|
+
if (quickFilter === 'stale') return task.stale === true
|
|
2615
|
+
return true
|
|
2616
|
+
}),
|
|
2547
2617
|
order,
|
|
2548
2618
|
)
|
|
2549
|
-
// Ключ группы решает переключатель: заголовки, сворачивание и
|
|
2550
|
-
// счётчики остаются те же, меняется только по чему делим.
|
|
2551
2619
|
const groupKeyOf = (task) => (grouping === 'assignee'
|
|
2552
2620
|
? (task.assignee || '')
|
|
2553
2621
|
: (task.repo || ''))
|
|
@@ -2556,7 +2624,7 @@ window.__ModuleLoader__.load({
|
|
|
2556
2624
|
: items
|
|
2557
2625
|
const limit = col.limit
|
|
2558
2626
|
const overLimit = limit !== undefined && limit !== null && all.length > limit
|
|
2559
|
-
const slot = (index) =>
|
|
2627
|
+
const slot = (index) => h('div', {
|
|
2560
2628
|
className: 'dkb-slot' + (dropAt && dropAt.column === col.id && dropAt.index === index ? ' dkb-slotOn' : ''),
|
|
2561
2629
|
key: 's' + index,
|
|
2562
2630
|
onDragOver: (e) => {
|
|
@@ -2569,8 +2637,6 @@ window.__ModuleLoader__.load({
|
|
|
2569
2637
|
|
|
2570
2638
|
const rows = []
|
|
2571
2639
|
rows.push(slot(0))
|
|
2572
|
-
// Заголовок рисуем при смене проекта. Задачи приходят уже
|
|
2573
|
-
// разложенными, поэтому группы идут подряд и разрываться не могут.
|
|
2574
2640
|
let group = null
|
|
2575
2641
|
const sizes = new Map()
|
|
2576
2642
|
for (const x of ordered) sizes.set(groupKeyOf(x), (sizes.get(groupKeyOf(x)) || 0) + 1)
|
|
@@ -2581,20 +2647,18 @@ window.__ModuleLoader__.load({
|
|
|
2581
2647
|
if (manyGroups && repo !== group) {
|
|
2582
2648
|
group = repo
|
|
2583
2649
|
const shut = groups[col.id + '/' + repo] === true
|
|
2584
|
-
rows.push(
|
|
2650
|
+
rows.push(h('button', {
|
|
2585
2651
|
type: 'button', className: 'dkb-groupHead', key: 'g' + repo,
|
|
2586
2652
|
'aria-expanded': !shut,
|
|
2587
2653
|
onClick: () => setGroups((cur) => Object.assign({}, cur, { [col.id + '/' + repo]: !shut })),
|
|
2588
2654
|
},
|
|
2589
|
-
|
|
2655
|
+
h('span', null,
|
|
2590
2656
|
repo || t(grouping === 'assignee' ? 'group.nobody' : 'board.noRepo')),
|
|
2591
|
-
|
|
2592
|
-
// пустой, — то же зло, что и свёрнутая колонка без числа.
|
|
2593
|
-
React.createElement('span', { className: 'dkb-groupCount' }, String(sizes.get(repo))),
|
|
2657
|
+
h('span', { className: 'dkb-groupCount' }, String(sizes.get(repo))),
|
|
2594
2658
|
))
|
|
2595
2659
|
}
|
|
2596
2660
|
if (manyGroups && groups[col.id + '/' + (task.repo || '')] === true) return
|
|
2597
|
-
rows.push(
|
|
2661
|
+
rows.push(h(TaskCard, {
|
|
2598
2662
|
key: task.id, task, t, now: state.now,
|
|
2599
2663
|
columns: (state.columns || []).map((c) => c.id),
|
|
2600
2664
|
menuOpen: cardMenu,
|
|
@@ -2610,9 +2674,6 @@ window.__ModuleLoader__.load({
|
|
|
2610
2674
|
sameSession: hoverSession !== '' && task.sessionId === hoverSession,
|
|
2611
2675
|
onHover: setHoverSession,
|
|
2612
2676
|
onRevive: async (x) => {
|
|
2613
|
-
// Переиспользуем обычный запуск: он сам пробует возобновить
|
|
2614
|
-
// прежнюю сессию и только потом заводит новую. Здесь важно
|
|
2615
|
-
// сказать, что именно вышло.
|
|
2616
2677
|
try {
|
|
2617
2678
|
const out = await api('/task/' + encodeURIComponent(x.id) + '/start', {
|
|
2618
2679
|
method: 'POST', body: JSON.stringify({ revive: true }),
|
|
@@ -2624,8 +2685,6 @@ window.__ModuleLoader__.load({
|
|
|
2624
2685
|
onStop: async (x) => {
|
|
2625
2686
|
try {
|
|
2626
2687
|
const out = await api('/task/' + encodeURIComponent(x.id) + '/stop', { method: 'POST', body: '{}' })
|
|
2627
|
-
// «Остановлено» и «он и так стоял» — разные ответы, и второй
|
|
2628
|
-
// нельзя выдавать за первый.
|
|
2629
2688
|
if (out && out.acted !== 'stopped') setError(t('stop.' + out.acted))
|
|
2630
2689
|
await load(board)
|
|
2631
2690
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
@@ -2635,8 +2694,6 @@ window.__ModuleLoader__.load({
|
|
|
2635
2694
|
try {
|
|
2636
2695
|
const out = await api('/sessions')
|
|
2637
2696
|
const live = (out && out.sessions) || []
|
|
2638
|
-
// Живых сессий нет — ставить некуда, и сказать об этом надо
|
|
2639
|
-
// сразу, а не показывать пустой список.
|
|
2640
2697
|
if (live.length === 0) { setError(t('error.no-live-sessions')); return }
|
|
2641
2698
|
setDialog({ mode: 'queue', task: x, sessions: live, sessionId: live[0].sessionId, error: '' })
|
|
2642
2699
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
@@ -2666,37 +2723,33 @@ window.__ModuleLoader__.load({
|
|
|
2666
2723
|
? all.length + ' / ' + t('board.limit', { n: limit })
|
|
2667
2724
|
: String(all.length)
|
|
2668
2725
|
|
|
2669
|
-
return
|
|
2726
|
+
return h('div', {
|
|
2670
2727
|
className: 'dkb-col'
|
|
2671
2728
|
+ (overColumn === col.id ? ' dkb-colOver' : '')
|
|
2672
2729
|
+ (shut ? ' dkb-colShut' : ''),
|
|
2673
2730
|
key: col.id,
|
|
2674
|
-
// Свёрнутая колонка остаётся целью переноса: иначе карточку было бы
|
|
2675
|
-
// некуда положить, не развернув её сперва.
|
|
2676
2731
|
onDragOver: (e) => { e.preventDefault(); setOverColumn(col.id) },
|
|
2677
2732
|
onDragLeave: () => { setOverColumn(''); setDropAt(null) },
|
|
2678
2733
|
onDrop: (e) => { e.preventDefault(); drop(col.id, items.length) },
|
|
2679
2734
|
},
|
|
2680
|
-
|
|
2735
|
+
h('button', {
|
|
2681
2736
|
type: 'button', className: 'dkb-colHead',
|
|
2682
2737
|
'aria-expanded': !shut,
|
|
2683
2738
|
title: t(shut ? 'board.expand' : 'board.collapse'),
|
|
2684
2739
|
onClick: toggle,
|
|
2685
2740
|
},
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
// выглядящая пустой, — худшее, что может сделать сворачивание.
|
|
2690
|
-
React.createElement('span', {
|
|
2741
|
+
h('span', { className: 'dkb-dot', 'data-col': col.id }),
|
|
2742
|
+
h('span', { className: 'dkb-colName' }, columnTitle(col, t)),
|
|
2743
|
+
h('span', {
|
|
2691
2744
|
className: 'dkb-count' + (overLimit ? ' dkb-countOver' : ''),
|
|
2692
2745
|
'data-col': col.id,
|
|
2693
2746
|
}, count),
|
|
2694
2747
|
),
|
|
2695
|
-
shut ? null :
|
|
2748
|
+
shut ? null : h('div', { className: 'dkb-list' },
|
|
2696
2749
|
all.length === 0
|
|
2697
|
-
?
|
|
2750
|
+
? h('div', { className: 'dkb-empty' }, t('board.empty'))
|
|
2698
2751
|
: items.length === 0
|
|
2699
|
-
?
|
|
2752
|
+
? h('div', { className: 'dkb-empty' }, t('board.noMatch'))
|
|
2700
2753
|
: null,
|
|
2701
2754
|
rows,
|
|
2702
2755
|
),
|
|
@@ -2704,24 +2757,18 @@ window.__ModuleLoader__.load({
|
|
|
2704
2757
|
}),
|
|
2705
2758
|
)
|
|
2706
2759
|
|
|
2707
|
-
// Окно, а не панель справа: в 420 пикселей помещались описание и журнал,
|
|
2708
|
-
// но не план с отметками. Живёт внутри экрана доски — сама доска лежит в
|
|
2709
|
-
// колонке разговора, и выход за её пределы наложился бы на чужие части
|
|
2710
|
-
// оболочки.
|
|
2711
2760
|
const panel = openTask
|
|
2712
|
-
?
|
|
2761
|
+
? h('div', {
|
|
2713
2762
|
className: 'dkb-modal',
|
|
2714
2763
|
onClick: () => setOpenTask(null),
|
|
2715
|
-
},
|
|
2764
|
+
}, h('div', {
|
|
2716
2765
|
className: 'dkb-panel dkb-panelWide',
|
|
2717
2766
|
role: 'dialog', 'aria-modal': 'true',
|
|
2718
2767
|
onClick: (e) => e.stopPropagation(),
|
|
2719
2768
|
},
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
React.createElement('label', { className: 'dkb-label' }, t('reasoning.label')),
|
|
2724
|
-
React.createElement('select', {
|
|
2769
|
+
h('div', { className: 'dkb-row' },
|
|
2770
|
+
h('label', { className: 'dkb-label' }, t('reasoning.label')),
|
|
2771
|
+
h('select', {
|
|
2725
2772
|
className: 'dkb-select',
|
|
2726
2773
|
value: openTask.reasoningEffort || '',
|
|
2727
2774
|
onChange: async (e) => {
|
|
@@ -2735,50 +2782,48 @@ window.__ModuleLoader__.load({
|
|
|
2735
2782
|
} catch {}
|
|
2736
2783
|
},
|
|
2737
2784
|
},
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2785
|
+
h('option', { value: '' }, t('reasoning.default')),
|
|
2786
|
+
h('option', { value: 'low' }, t('reasoning.low')),
|
|
2787
|
+
h('option', { value: 'medium' }, t('reasoning.medium')),
|
|
2788
|
+
h('option', { value: 'high' }, t('reasoning.high')),
|
|
2742
2789
|
),
|
|
2743
2790
|
),
|
|
2744
|
-
|
|
2745
|
-
|
|
2791
|
+
h('div', { className: 'dkb-row' },
|
|
2792
|
+
h('div', { className: 'dkb-taskHead', style: { flex: 1 } },
|
|
2746
2793
|
taskRef(openTask, t),
|
|
2747
|
-
(openTask.labels || []).map((l) =>
|
|
2794
|
+
(openTask.labels || []).map((l) => h('span', {
|
|
2748
2795
|
className: 'dkb-tag', key: l, style: tagStyle(openTask, l),
|
|
2749
2796
|
}, l))),
|
|
2750
|
-
|
|
2797
|
+
h('button', {
|
|
2751
2798
|
type: 'button', className: 'dkb-discard', onClick: () => setOpenTask(null),
|
|
2752
2799
|
}, t('panel.close')),
|
|
2753
2800
|
),
|
|
2754
|
-
|
|
2755
|
-
|
|
2801
|
+
h('div', { className: 'dkb-title' }, openTask.title),
|
|
2802
|
+
h('div', { className: 'dkb-note' },
|
|
2756
2803
|
t('panel.created', { ago: agoText(state.now, openTask.createdAt, t) })
|
|
2757
2804
|
+ (openTask.assignee ? ' · ' + t('panel.assignee', { who: openTask.assignee }) : '')
|
|
2758
|
-
// Автор стоит рядом с датой заведения: это одно событие — кто и
|
|
2759
|
-
// когда завёл, — и разносить его по разным углам незачем.
|
|
2760
2805
|
+ (openTask.author ? ' · ' + t('panel.author', { who: openTask.author }) : ''),
|
|
2761
2806
|
' · ',
|
|
2762
2807
|
t('panel.updated', { ago: agoText(state.now, openTask.updatedAt, t) }),
|
|
2763
2808
|
openTask.issueUrl
|
|
2764
|
-
?
|
|
2809
|
+
? h('a', {
|
|
2765
2810
|
className: 'dkb-issueLink', href: openTask.issueUrl,
|
|
2766
2811
|
target: '_blank', rel: 'noreferrer noopener',
|
|
2767
2812
|
}, ' · ' + t('panel.issue') + ' ↗')
|
|
2768
2813
|
: null),
|
|
2769
2814
|
openTask.worktree
|
|
2770
|
-
?
|
|
2815
|
+
? h('div', { className: 'dkb-note', style: { wordBreak: 'break-all' } },
|
|
2771
2816
|
t('panel.worktree', { path: openTask.worktree })
|
|
2772
2817
|
+ (openTask.branch ? ' (' + openTask.branch + ')' : ''))
|
|
2773
2818
|
: null,
|
|
2774
2819
|
openTask.permission && !openTask.permissionConfirmedAt
|
|
2775
|
-
?
|
|
2820
|
+
? h('div', {
|
|
2776
2821
|
className: 'dkb-warnBox',
|
|
2777
2822
|
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
2823
|
},
|
|
2779
|
-
|
|
2824
|
+
h('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-state-warn-primary, #856404)' } },
|
|
2780
2825
|
t('task.permissionGateWarning', { permission: openTask.permission })),
|
|
2781
|
-
|
|
2826
|
+
h('button', {
|
|
2782
2827
|
type: 'button', className: 'dkb-save',
|
|
2783
2828
|
onClick: async () => {
|
|
2784
2829
|
try {
|
|
@@ -2790,59 +2835,53 @@ window.__ModuleLoader__.load({
|
|
|
2790
2835
|
}, t('task.confirmPermission')),
|
|
2791
2836
|
)
|
|
2792
2837
|
: null,
|
|
2793
|
-
// Пустое тело — отсутствие раздела, а не пустой раздел с заголовком.
|
|
2794
2838
|
body === null
|
|
2795
|
-
?
|
|
2839
|
+
? h('p', { className: 'dkb-note' }, t('panel.bodyLoading'))
|
|
2796
2840
|
: body.length === 0
|
|
2797
2841
|
? 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', {
|
|
2842
|
+
: h('div', { className: 'dkb-mdBody' }, renderBlocks(body)),
|
|
2843
|
+
h('div', { className: 'dkb-edit' },
|
|
2844
|
+
h('div', { className: 'dkb-field' },
|
|
2845
|
+
h('span', { className: 'dkb-label' }, t('panel.priority')),
|
|
2846
|
+
h('select', {
|
|
2806
2847
|
className: 'dkb-input', value: editPriority,
|
|
2807
2848
|
onChange: (e) => setEditPriority(e.target.value),
|
|
2808
2849
|
},
|
|
2809
|
-
|
|
2810
|
-
['high', 'medium', 'low'].map((p) =>
|
|
2850
|
+
h('option', { value: '' }, t('panel.priorityNone')),
|
|
2851
|
+
['high', 'medium', 'low'].map((p) => h('option', { value: p, key: p }, t('priority.' + p)))),
|
|
2811
2852
|
),
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2853
|
+
h('div', { className: 'dkb-field' },
|
|
2854
|
+
h('span', { className: 'dkb-label' }, t('panel.labels')),
|
|
2855
|
+
h('input', {
|
|
2815
2856
|
className: 'dkb-input', value: editLabels,
|
|
2816
2857
|
placeholder: t('dialog.labelsHint'),
|
|
2817
2858
|
onChange: (e) => setEditLabels(e.target.value),
|
|
2818
2859
|
}),
|
|
2819
2860
|
),
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2861
|
+
h('div', { className: 'dkb-field' },
|
|
2862
|
+
h('span', { className: 'dkb-label' }, t('panel.due')),
|
|
2863
|
+
h('input', {
|
|
2823
2864
|
type: 'date', className: 'dkb-input', value: editDue,
|
|
2824
2865
|
onChange: (e) => setEditDue(e.target.value),
|
|
2825
2866
|
}),
|
|
2826
2867
|
),
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2868
|
+
h('div', { className: 'dkb-field' },
|
|
2869
|
+
h('span', { className: 'dkb-label' }, t('task.cron')),
|
|
2870
|
+
h('input', {
|
|
2830
2871
|
className: 'dkb-input', value: editCron,
|
|
2831
2872
|
placeholder: '* * * * *',
|
|
2832
2873
|
onChange: (e) => setEditCron(e.target.value),
|
|
2833
2874
|
}),
|
|
2834
2875
|
openTask.nextRunAt
|
|
2835
|
-
?
|
|
2876
|
+
? h('p', { className: 'dkb-hint' },
|
|
2836
2877
|
t('task.nextRun', { date: new Date(openTask.nextRunAt).toLocaleString() }))
|
|
2837
2878
|
: null,
|
|
2838
2879
|
),
|
|
2839
|
-
|
|
2880
|
+
h('button', {
|
|
2840
2881
|
type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
|
|
2841
2882
|
onClick: async () => {
|
|
2842
2883
|
try {
|
|
2843
2884
|
const labels = editLabels.split(',').map((s) => s.trim()).filter(Boolean)
|
|
2844
|
-
// Пустая дата снимает дедлайн (0); заполненная — полночь
|
|
2845
|
-
// этого дня по местному времени.
|
|
2846
2885
|
const dueAt = editDue === ''
|
|
2847
2886
|
? 0
|
|
2848
2887
|
: new Date(editDue + 'T00:00:00').getTime()
|
|
@@ -2857,9 +2896,9 @@ window.__ModuleLoader__.load({
|
|
|
2857
2896
|
}, t('panel.saveEdit')),
|
|
2858
2897
|
),
|
|
2859
2898
|
openTask.progressDump
|
|
2860
|
-
?
|
|
2861
|
-
|
|
2862
|
-
|
|
2899
|
+
? h('div', null,
|
|
2900
|
+
h('div', { className: 'dkb-group' }, t('task.progressDump')),
|
|
2901
|
+
h('pre', {
|
|
2863
2902
|
style: {
|
|
2864
2903
|
background: 'var(--dsw-alias-bg-layer-1, #f6f8fa)', padding: 8, borderRadius: 6, fontSize: 12,
|
|
2865
2904
|
whiteSpace: 'pre-wrap', maxHeight: 150, overflowY: 'auto', margin: 0,
|
|
@@ -2867,21 +2906,155 @@ window.__ModuleLoader__.load({
|
|
|
2867
2906
|
}, openTask.progressDump),
|
|
2868
2907
|
)
|
|
2869
2908
|
: null,
|
|
2909
|
+
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 } },
|
|
2910
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
|
|
2911
|
+
h('span', { className: 'dkb-group', style: { margin: 0 } },
|
|
2912
|
+
'👑 ' + t('epic.subtasks') + (subtasks.length > 0 ? ` (${subtasks.filter(s => s.column === 'done').length}/${subtasks.length})` : '')
|
|
2913
|
+
),
|
|
2914
|
+
h('button', {
|
|
2915
|
+
type: 'button', className: 'dkb-discard',
|
|
2916
|
+
style: { padding: '2px 8px', fontSize: 11 },
|
|
2917
|
+
onClick: () => setTriageOpen(!triageOpen),
|
|
2918
|
+
}, triageOpen ? t('dialog.cancel') : '⚡ ' + t('epic.triage')),
|
|
2919
|
+
),
|
|
2920
|
+
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)' } },
|
|
2921
|
+
h('span', { style: { fontSize: 12, fontWeight: 600 } }, t('epic.triageTitle')),
|
|
2922
|
+
h('span', { className: 'dkb-hint', style: { margin: 0 } }, t('epic.triageHelp')),
|
|
2923
|
+
h('textarea', {
|
|
2924
|
+
className: 'dkb-area',
|
|
2925
|
+
rows: 4,
|
|
2926
|
+
placeholder: 'Subtask 1\nSubtask 2\nSubtask 3...',
|
|
2927
|
+
value: triageText,
|
|
2928
|
+
onChange: (e) => setTriageText(e.target.value),
|
|
2929
|
+
}),
|
|
2930
|
+
openTask.owner && openTask.repo ? h('label', { style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, cursor: 'pointer' } },
|
|
2931
|
+
h('input', {
|
|
2932
|
+
type: 'checkbox',
|
|
2933
|
+
checked: triageGitea,
|
|
2934
|
+
onChange: (e) => setTriageGitea(e.target.checked),
|
|
2935
|
+
}),
|
|
2936
|
+
h('span', null, t('epic.createGiteaIssues'))
|
|
2937
|
+
) : null,
|
|
2938
|
+
h('div', { style: { display: 'flex', gap: 6 } },
|
|
2939
|
+
h('button', {
|
|
2940
|
+
type: 'button', className: 'dkb-save',
|
|
2941
|
+
disabled: triageSubmitting || !triageText.trim(),
|
|
2942
|
+
onClick: async () => {
|
|
2943
|
+
const lines = triageText.split('\n').map(s => s.trim()).filter(Boolean)
|
|
2944
|
+
if (lines.length === 0) return
|
|
2945
|
+
setTriageSubmitting(true)
|
|
2946
|
+
try {
|
|
2947
|
+
const out = await api('/task/' + encodeURIComponent(openTask.id) + '/triage', {
|
|
2948
|
+
method: 'POST',
|
|
2949
|
+
body: JSON.stringify({
|
|
2950
|
+
subtasks: lines.map(title => ({ title })),
|
|
2951
|
+
createGiteaIssues: triageGitea,
|
|
2952
|
+
}),
|
|
2953
|
+
})
|
|
2954
|
+
if (out && out.subtasks) setSubtasks(out.subtasks)
|
|
2955
|
+
if (out && out.epic) setOpenTask(out.epic)
|
|
2956
|
+
setTriageText('')
|
|
2957
|
+
setTriageOpen(false)
|
|
2958
|
+
await load(board)
|
|
2959
|
+
} catch (err) { setError(t(err.key || 'error.unknown')) }
|
|
2960
|
+
finally { setTriageSubmitting(false) }
|
|
2961
|
+
},
|
|
2962
|
+
}, triageSubmitting ? '...' : t('epic.triageSubmit')),
|
|
2963
|
+
h('button', {
|
|
2964
|
+
type: 'button', className: 'dkb-discard',
|
|
2965
|
+
onClick: () => setTriageOpen(false),
|
|
2966
|
+
}, t('dialog.cancel')),
|
|
2967
|
+
)
|
|
2968
|
+
) : null,
|
|
2969
|
+
subtasks.length > 0 ? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
2970
|
+
subtasks.map((st) => h('li', {
|
|
2971
|
+
key: st.id,
|
|
2972
|
+
style: {
|
|
2973
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
2974
|
+
padding: '4px 8px', background: 'var(--dsw-alias-bg-layer-2)',
|
|
2975
|
+
borderRadius: 4, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
|
|
2976
|
+
},
|
|
2977
|
+
},
|
|
2978
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, flex: 1 } },
|
|
2979
|
+
h('span', { style: { color: st.column === 'done' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-tertiary)' } },
|
|
2980
|
+
st.column === 'done' ? '✓' : '•'
|
|
2981
|
+
),
|
|
2982
|
+
h('span', { style: { textDecoration: st.column === 'done' ? 'line-through' : 'none', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, st.title),
|
|
2983
|
+
st.issueNumber ? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, '#' + st.issueNumber) : null,
|
|
2984
|
+
),
|
|
2985
|
+
h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('column.' + st.column)),
|
|
2986
|
+
))
|
|
2987
|
+
) : h('p', { className: 'dkb-hint', style: { margin: '4px 0' } }, t('epic.noSubtasks')),
|
|
2988
|
+
h('div', { style: { display: 'flex', gap: 6, marginTop: 6 } },
|
|
2989
|
+
h('input', {
|
|
2990
|
+
className: 'dkb-input',
|
|
2991
|
+
style: { flex: 1, fontSize: 12 },
|
|
2992
|
+
placeholder: t('epic.addSubtask'),
|
|
2993
|
+
value: newSubtaskTitle,
|
|
2994
|
+
onChange: (e) => setNewSubtaskTitle(e.target.value),
|
|
2995
|
+
onKeyDown: async (e) => {
|
|
2996
|
+
if (e.key === 'Enter' && newSubtaskTitle.trim()) {
|
|
2997
|
+
e.preventDefault()
|
|
2998
|
+
const title = newSubtaskTitle.trim()
|
|
2999
|
+
setNewSubtaskTitle('')
|
|
3000
|
+
try {
|
|
3001
|
+
const out = await api('/task/' + encodeURIComponent(openTask.id) + '/subtask', {
|
|
3002
|
+
method: 'POST',
|
|
3003
|
+
body: JSON.stringify({ title }),
|
|
3004
|
+
})
|
|
3005
|
+
if (out && out.subtask) {
|
|
3006
|
+
setSubtasks(prev => [...prev, out.subtask])
|
|
3007
|
+
await load(board)
|
|
3008
|
+
}
|
|
3009
|
+
} catch (err) { setError(t(err.key || 'error.unknown')) }
|
|
3010
|
+
}
|
|
3011
|
+
},
|
|
3012
|
+
}),
|
|
3013
|
+
h('button', {
|
|
3014
|
+
type: 'button', className: 'dkb-save',
|
|
3015
|
+
style: { padding: '3px 10px', fontSize: 12 },
|
|
3016
|
+
onClick: async () => {
|
|
3017
|
+
const title = newSubtaskTitle.trim()
|
|
3018
|
+
if (!title) return
|
|
3019
|
+
setNewSubtaskTitle('')
|
|
3020
|
+
try {
|
|
3021
|
+
const out = await api('/task/' + encodeURIComponent(openTask.id) + '/subtask', {
|
|
3022
|
+
method: 'POST',
|
|
3023
|
+
body: JSON.stringify({ title }),
|
|
3024
|
+
})
|
|
3025
|
+
if (out && out.subtask) {
|
|
3026
|
+
setSubtasks(prev => [...prev, out.subtask])
|
|
3027
|
+
await load(board)
|
|
3028
|
+
}
|
|
3029
|
+
} catch (err) { setError(t(err.key || 'error.unknown')) }
|
|
3030
|
+
},
|
|
3031
|
+
}, '+'),
|
|
3032
|
+
)
|
|
3033
|
+
),
|
|
3034
|
+
openTask.sessionId ? h('div', { style: { margin: '6px 0' } },
|
|
3035
|
+
h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3036
|
+
h('span', null, '🖥️ ' + t('console.tab')),
|
|
3037
|
+
h('span', { className: 'dkb-tag', style: { fontSize: 10, fontFamily: 'monospace' } }, openTask.sessionId.slice(0, 16) + '...'),
|
|
3038
|
+
),
|
|
3039
|
+
h('pre', { className: 'dkb-consolePre' },
|
|
3040
|
+
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'))
|
|
3041
|
+
)
|
|
3042
|
+
) : null,
|
|
2870
3043
|
openTask.runs && openTask.runs.length
|
|
2871
|
-
?
|
|
2872
|
-
|
|
2873
|
-
|
|
3044
|
+
? h('div', null,
|
|
3045
|
+
h('div', { className: 'dkb-group' }, t('task.runs')),
|
|
3046
|
+
h('ul', { className: 'dkb-log' }, openTask.runs.map((r) => h('li', {
|
|
2874
3047
|
className: 'dkb-logRow', key: r.id,
|
|
2875
3048
|
style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
|
|
2876
3049
|
},
|
|
2877
|
-
|
|
3050
|
+
h('span', null,
|
|
2878
3051
|
(r.result === 'succeeded' ? '✓ ' : r.result === 'failed' ? '✕ ' : '▸ ')
|
|
2879
3052
|
+ new Date(r.startedAt).toLocaleTimeString()
|
|
2880
3053
|
+ (r.model ? ' · ' + r.model : '')
|
|
2881
3054
|
+ (r.error ? ' · ' + r.error : '')
|
|
2882
3055
|
),
|
|
2883
3056
|
r.sessionId
|
|
2884
|
-
?
|
|
3057
|
+
? h('button', {
|
|
2885
3058
|
type: 'button',
|
|
2886
3059
|
className: 'dkb-discard',
|
|
2887
3060
|
style: { padding: '2px 6px', fontSize: 11 },
|
|
@@ -2897,9 +3070,7 @@ window.__ModuleLoader__.load({
|
|
|
2897
3070
|
: null,
|
|
2898
3071
|
))))
|
|
2899
3072
|
: null,
|
|
2900
|
-
|
|
2901
|
-
// «взять» и «отказаться» значит предлагать бессмысленное действие.
|
|
2902
|
-
React.createElement('button', {
|
|
3073
|
+
h('button', {
|
|
2903
3074
|
type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
|
|
2904
3075
|
onClick: async () => {
|
|
2905
3076
|
const mine = openTask.assignee !== '' && openTask.assignee !== undefined
|
|
@@ -2914,12 +3085,9 @@ window.__ModuleLoader__.load({
|
|
|
2914
3085
|
},
|
|
2915
3086
|
}, openTask.assignee ? t('panel.assignDrop') : t('panel.assignMe')),
|
|
2916
3087
|
openTask.sessionId
|
|
2917
|
-
?
|
|
3088
|
+
? h('button', {
|
|
2918
3089
|
type: 'button', className: 'dkb-discard', style: { alignSelf: 'flex-start' },
|
|
2919
3090
|
onClick: () => {
|
|
2920
|
-
// Тот же путь, что и сразу после запуска: задача, идущая
|
|
2921
|
-
// вторые сутки, иначе ищется в общем списке по имени вида
|
|
2922
|
-
// kanban-9e84cae6-…
|
|
2923
3091
|
if (!openSession(props.ctx, openTask.sessionId)) {
|
|
2924
3092
|
setError(t('error.sessionNotOpened', { id: openTask.sessionId }))
|
|
2925
3093
|
return
|
|
@@ -2930,31 +3098,31 @@ window.__ModuleLoader__.load({
|
|
|
2930
3098
|
}, t('panel.openChat'))
|
|
2931
3099
|
: null,
|
|
2932
3100
|
log.length
|
|
2933
|
-
?
|
|
2934
|
-
|
|
2935
|
-
|
|
3101
|
+
? h('div', null,
|
|
3102
|
+
h('div', { className: 'dkb-group' }, t('panel.log')),
|
|
3103
|
+
h('ul', { className: 'dkb-log' }, log.map((row) => h('li', {
|
|
2936
3104
|
className: 'dkb-logRow', key: row.id,
|
|
2937
3105
|
}, `${t('column.' + row.toCol)} ← ${row.source}${row.detail ? ' · ' + row.detail : ''}`))))
|
|
2938
3106
|
: null,
|
|
2939
3107
|
openTask.plan && openTask.plan.items && openTask.plan.items.length
|
|
2940
|
-
?
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
openTask.plan.items.map((item, i) =>
|
|
3108
|
+
? h('div', null,
|
|
3109
|
+
h('div', { className: 'dkb-group' }, t('plan.title')),
|
|
3110
|
+
h('p', { className: 'dkb-hint' }, planLine(openTask, t)),
|
|
3111
|
+
h('ul', { className: 'dkb-planList' },
|
|
3112
|
+
openTask.plan.items.map((item, i) => h('li', {
|
|
2945
3113
|
key: i,
|
|
2946
3114
|
className: 'dkb-planItem'
|
|
2947
3115
|
+ (item.done ? ' dkb-planDone' : '')
|
|
2948
3116
|
+ (item.active ? ' dkb-planActive' : ''),
|
|
2949
3117
|
},
|
|
2950
|
-
|
|
3118
|
+
h('span', { className: 'dkb-planMark' },
|
|
2951
3119
|
item.done ? '✓' : item.active ? '▸' : '·'),
|
|
2952
|
-
|
|
3120
|
+
h('span', null, item.text),
|
|
2953
3121
|
))),
|
|
2954
3122
|
)
|
|
2955
3123
|
: null,
|
|
2956
3124
|
openTask.rejectReason
|
|
2957
|
-
?
|
|
3125
|
+
? h('div', {
|
|
2958
3126
|
className: 'dkb-warnBox',
|
|
2959
3127
|
style: {
|
|
2960
3128
|
margin: '4px 0', padding: 10,
|
|
@@ -2963,13 +3131,13 @@ window.__ModuleLoader__.load({
|
|
|
2963
3131
|
borderRadius: 6,
|
|
2964
3132
|
},
|
|
2965
3133
|
},
|
|
2966
|
-
|
|
3134
|
+
h('div', {
|
|
2967
3135
|
style: { marginBottom: 4, fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-state-danger-primary, #cf222e)' },
|
|
2968
3136
|
}, '⚠️ ' + t('acceptance.rejectedAlert', { reason: openTask.rejectReason })),
|
|
2969
3137
|
)
|
|
2970
3138
|
: null,
|
|
2971
3139
|
openTask.worktree
|
|
2972
|
-
?
|
|
3140
|
+
? h('div', {
|
|
2973
3141
|
style: {
|
|
2974
3142
|
margin: '8px 0', padding: 12,
|
|
2975
3143
|
background: 'var(--dsw-alias-bg-layer-3)',
|
|
@@ -2977,13 +3145,13 @@ window.__ModuleLoader__.load({
|
|
|
2977
3145
|
borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
|
|
2978
3146
|
},
|
|
2979
3147
|
},
|
|
2980
|
-
|
|
2981
|
-
|
|
3148
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3149
|
+
h('div', { className: 'dkb-group', style: { margin: 0 } },
|
|
2982
3150
|
t('diff.title'),
|
|
2983
3151
|
commitsData.length > 0 ? ` · ${t('diff.commits', { n: commitsData.length })}` : '',
|
|
2984
3152
|
diffData?.filesChanged ? ` · ${t('diff.filesChanged', { n: diffData.filesChanged })}` : '',
|
|
2985
3153
|
),
|
|
2986
|
-
|
|
3154
|
+
h('button', {
|
|
2987
3155
|
type: 'button', className: 'dkb-discard',
|
|
2988
3156
|
style: { padding: '2px 8px', fontSize: 11 },
|
|
2989
3157
|
onClick: () => {
|
|
@@ -2992,23 +3160,23 @@ window.__ModuleLoader__.load({
|
|
|
2992
3160
|
},
|
|
2993
3161
|
}, diffExpanded ? t('diff.hide') : t('diff.show')),
|
|
2994
3162
|
),
|
|
2995
|
-
diffExpanded ?
|
|
3163
|
+
diffExpanded ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
2996
3164
|
commitsData.length > 0
|
|
2997
|
-
?
|
|
2998
|
-
commitsData.map((cm) =>
|
|
3165
|
+
? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 4 } },
|
|
3166
|
+
commitsData.map((cm) => h('div', {
|
|
2999
3167
|
key: cm.hash,
|
|
3000
3168
|
style: { fontSize: 11, display: 'flex', gap: 6, opacity: 0.85, fontFamily: 'monospace' },
|
|
3001
3169
|
},
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3170
|
+
h('span', { style: { color: 'var(--dsw-alias-label-link, #3b82f6)' } }, cm.hash),
|
|
3171
|
+
h('span', { style: { flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, cm.subject),
|
|
3172
|
+
h('span', { style: { opacity: 0.6 } }, cm.date),
|
|
3005
3173
|
))
|
|
3006
3174
|
)
|
|
3007
3175
|
: null,
|
|
3008
3176
|
diffLoading
|
|
3009
|
-
?
|
|
3177
|
+
? h('div', { style: { fontSize: 12, opacity: 0.7 } }, 'Loading diff...')
|
|
3010
3178
|
: diffData?.diff
|
|
3011
|
-
?
|
|
3179
|
+
? h('pre', {
|
|
3012
3180
|
style: {
|
|
3013
3181
|
margin: 0, padding: 8, maxHeight: 280, overflowY: 'auto',
|
|
3014
3182
|
background: 'var(--dsw-alias-bg-layer-2, #1e1e1e)',
|
|
@@ -3017,12 +3185,12 @@ window.__ModuleLoader__.load({
|
|
|
3017
3185
|
border: '1px solid var(--dsw-alias-border-l1)',
|
|
3018
3186
|
},
|
|
3019
3187
|
}, diffData.diff)
|
|
3020
|
-
:
|
|
3188
|
+
: h('div', { style: { fontSize: 12, opacity: 0.7 } }, t('diff.empty'))
|
|
3021
3189
|
) : null
|
|
3022
3190
|
)
|
|
3023
3191
|
: null,
|
|
3024
3192
|
openTask.column === 'review'
|
|
3025
|
-
?
|
|
3193
|
+
? h('div', {
|
|
3026
3194
|
style: {
|
|
3027
3195
|
margin: '8px 0', padding: 12,
|
|
3028
3196
|
background: 'var(--dsw-alias-bg-layer-3)',
|
|
@@ -3030,10 +3198,10 @@ window.__ModuleLoader__.load({
|
|
|
3030
3198
|
borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 8,
|
|
3031
3199
|
},
|
|
3032
3200
|
},
|
|
3033
|
-
|
|
3034
|
-
|
|
3201
|
+
h('div', { className: 'dkb-group', style: { margin: 0 } }, t('acceptance.title')),
|
|
3202
|
+
h('div', { style: { display: 'flex', gap: 8 } },
|
|
3035
3203
|
openTask.worktree
|
|
3036
|
-
?
|
|
3204
|
+
? h('button', {
|
|
3037
3205
|
type: 'button', className: 'dkb-save',
|
|
3038
3206
|
disabled: merging,
|
|
3039
3207
|
style: { background: 'var(--dsw-alias-bg-success, #16a34a)', color: '#fff' },
|
|
@@ -3048,7 +3216,7 @@ window.__ModuleLoader__.load({
|
|
|
3048
3216
|
},
|
|
3049
3217
|
}, merging ? t('acceptance.merging') : '🔀 ' + t('acceptance.acceptAndMerge'))
|
|
3050
3218
|
: null,
|
|
3051
|
-
|
|
3219
|
+
h('button', {
|
|
3052
3220
|
type: 'button', className: 'dkb-save',
|
|
3053
3221
|
onClick: async () => {
|
|
3054
3222
|
try {
|
|
@@ -3058,23 +3226,50 @@ window.__ModuleLoader__.load({
|
|
|
3058
3226
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
3059
3227
|
},
|
|
3060
3228
|
}, '✓ ' + t('acceptance.accept')),
|
|
3061
|
-
|
|
3229
|
+
h('button', {
|
|
3062
3230
|
type: 'button', className: 'dkb-discard',
|
|
3063
3231
|
onClick: () => { setRejectModalOpen(true); setRejectReasonText('') },
|
|
3064
3232
|
}, '✗ ' + t('acceptance.reject')),
|
|
3233
|
+
openTask.owner && openTask.repo
|
|
3234
|
+
? h('button', {
|
|
3235
|
+
type: 'button', className: 'dkb-save',
|
|
3236
|
+
disabled: prCreating,
|
|
3237
|
+
style: { background: 'var(--dsw-alias-state-info-primary, #0969da)', color: '#fff' },
|
|
3238
|
+
onClick: async () => {
|
|
3239
|
+
setPrCreating(true)
|
|
3240
|
+
try {
|
|
3241
|
+
const out = await api('/task/' + encodeURIComponent(openTask.id) + '/pr', {
|
|
3242
|
+
method: 'POST',
|
|
3243
|
+
body: JSON.stringify({ base: 'main' }),
|
|
3244
|
+
})
|
|
3245
|
+
if (out && out.pr) setCreatedPr(out.pr)
|
|
3246
|
+
await load(board)
|
|
3247
|
+
} catch (e) { setError(t(e.key || e.error || 'error.unknown')) }
|
|
3248
|
+
finally { setPrCreating(false) }
|
|
3249
|
+
},
|
|
3250
|
+
}, prCreating ? t('pr.creating') : '🐙 ' + t('pr.create'))
|
|
3251
|
+
: null,
|
|
3252
|
+
createdPr
|
|
3253
|
+
? h('a', {
|
|
3254
|
+
className: 'dkb-save',
|
|
3255
|
+
style: { background: 'var(--dsw-alias-state-success-primary, #16a34a)', color: '#fff', textDecoration: 'none', display: 'inline-flex', alignItems: 'center' },
|
|
3256
|
+
href: createdPr.html_url,
|
|
3257
|
+
target: '_blank', rel: 'noopener noreferrer',
|
|
3258
|
+
}, '↗ ' + t('pr.view') + ` #${createdPr.number}`)
|
|
3259
|
+
: null,
|
|
3065
3260
|
),
|
|
3066
3261
|
rejectModalOpen
|
|
3067
|
-
?
|
|
3068
|
-
|
|
3069
|
-
|
|
3262
|
+
? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6 } },
|
|
3263
|
+
h('span', { className: 'dkb-label' }, t('acceptance.rejectTitle')),
|
|
3264
|
+
h('textarea', {
|
|
3070
3265
|
className: 'dkb-area',
|
|
3071
3266
|
value: rejectReasonText,
|
|
3072
3267
|
placeholder: t('acceptance.rejectReasonPlaceholder'),
|
|
3073
3268
|
onChange: (e) => setRejectReasonText(e.target.value),
|
|
3074
3269
|
rows: 3,
|
|
3075
3270
|
}),
|
|
3076
|
-
|
|
3077
|
-
|
|
3271
|
+
h('div', { style: { display: 'flex', gap: 6 } },
|
|
3272
|
+
h('button', {
|
|
3078
3273
|
type: 'button', className: 'dkb-save',
|
|
3079
3274
|
onClick: async () => {
|
|
3080
3275
|
if (!rejectReasonText.trim()) return
|
|
@@ -3089,7 +3284,7 @@ window.__ModuleLoader__.load({
|
|
|
3089
3284
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
3090
3285
|
},
|
|
3091
3286
|
}, t('acceptance.rejectSubmit')),
|
|
3092
|
-
|
|
3287
|
+
h('button', {
|
|
3093
3288
|
type: 'button', className: 'dkb-discard',
|
|
3094
3289
|
onClick: () => setRejectModalOpen(false),
|
|
3095
3290
|
}, t('dialog.cancel')),
|
|
@@ -3098,11 +3293,11 @@ window.__ModuleLoader__.load({
|
|
|
3098
3293
|
: null,
|
|
3099
3294
|
)
|
|
3100
3295
|
: null,
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3296
|
+
h('div', { style: { margin: '4px 0' } },
|
|
3297
|
+
h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3298
|
+
h('span', null, t('checklist.title')),
|
|
3104
3299
|
Array.isArray(openTask.checklist) && openTask.checklist.length > 0
|
|
3105
|
-
?
|
|
3300
|
+
? h('span', { className: 'dkb-hint', style: { fontWeight: 'normal' } },
|
|
3106
3301
|
t('checklist.progress', {
|
|
3107
3302
|
done: openTask.checklist.filter((i) => i.completed).length,
|
|
3108
3303
|
total: openTask.checklist.length,
|
|
@@ -3110,8 +3305,8 @@ window.__ModuleLoader__.load({
|
|
|
3110
3305
|
: null,
|
|
3111
3306
|
),
|
|
3112
3307
|
Array.isArray(openTask.checklist) && openTask.checklist.length > 0
|
|
3113
|
-
?
|
|
3114
|
-
openTask.checklist.map((item, idx) =>
|
|
3308
|
+
? h('ul', { style: { listStyle: 'none', padding: 0, margin: '4px 0', display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
3309
|
+
openTask.checklist.map((item, idx) => h('li', {
|
|
3115
3310
|
key: idx,
|
|
3116
3311
|
style: {
|
|
3117
3312
|
display: 'flex', flexDirection: 'column', gap: 2, padding: '4px 8px',
|
|
@@ -3119,8 +3314,8 @@ window.__ModuleLoader__.load({
|
|
|
3119
3314
|
borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)',
|
|
3120
3315
|
},
|
|
3121
3316
|
},
|
|
3122
|
-
|
|
3123
|
-
|
|
3317
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
3318
|
+
h('input', {
|
|
3124
3319
|
type: 'checkbox',
|
|
3125
3320
|
checked: Boolean(item.completed),
|
|
3126
3321
|
onChange: async (e) => {
|
|
@@ -3136,7 +3331,7 @@ window.__ModuleLoader__.load({
|
|
|
3136
3331
|
} catch (err) { setError(t(err.key || 'error.unknown')) }
|
|
3137
3332
|
},
|
|
3138
3333
|
}),
|
|
3139
|
-
|
|
3334
|
+
h('span', {
|
|
3140
3335
|
style: {
|
|
3141
3336
|
textDecoration: item.completed ? 'line-through' : 'none',
|
|
3142
3337
|
color: item.completed ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-primary)',
|
|
@@ -3144,11 +3339,11 @@ window.__ModuleLoader__.load({
|
|
|
3144
3339
|
},
|
|
3145
3340
|
}, item.text),
|
|
3146
3341
|
item.required !== false
|
|
3147
|
-
?
|
|
3342
|
+
? h('span', { className: 'dkb-tag', style: { fontSize: 10 } }, t('checklist.required'))
|
|
3148
3343
|
: null,
|
|
3149
3344
|
),
|
|
3150
3345
|
item.evidence
|
|
3151
|
-
?
|
|
3346
|
+
? h('div', {
|
|
3152
3347
|
style: {
|
|
3153
3348
|
fontSize: 11, color: 'var(--dsw-alias-label-secondary)', marginLeft: 24,
|
|
3154
3349
|
fontFamily: 'var(--dsw-font-family-mono, monospace)', whiteSpace: 'pre-wrap',
|
|
@@ -3156,9 +3351,9 @@ window.__ModuleLoader__.load({
|
|
|
3156
3351
|
}, t('checklist.evidence', { text: item.evidence }))
|
|
3157
3352
|
: null,
|
|
3158
3353
|
)))
|
|
3159
|
-
:
|
|
3160
|
-
|
|
3161
|
-
|
|
3354
|
+
: h('p', { className: 'dkb-hint' }, t('checklist.empty')),
|
|
3355
|
+
h('div', { style: { display: 'flex', gap: 6, marginTop: 4 } },
|
|
3356
|
+
h('input', {
|
|
3162
3357
|
className: 'dkb-input',
|
|
3163
3358
|
style: { flex: 1 },
|
|
3164
3359
|
placeholder: t('checklist.placeholder'),
|
|
@@ -3180,7 +3375,7 @@ window.__ModuleLoader__.load({
|
|
|
3180
3375
|
}
|
|
3181
3376
|
},
|
|
3182
3377
|
}),
|
|
3183
|
-
|
|
3378
|
+
h('button', {
|
|
3184
3379
|
type: 'button', className: 'dkb-save',
|
|
3185
3380
|
onClick: async () => {
|
|
3186
3381
|
if (!newChecklistText.trim()) return
|
|
@@ -3199,7 +3394,7 @@ window.__ModuleLoader__.load({
|
|
|
3199
3394
|
),
|
|
3200
3395
|
),
|
|
3201
3396
|
openTask.report && openTask.report.summary
|
|
3202
|
-
?
|
|
3397
|
+
? h('div', {
|
|
3203
3398
|
style: {
|
|
3204
3399
|
margin: '6px 0', padding: 10,
|
|
3205
3400
|
background: 'var(--dsw-alias-bg-layer-1)',
|
|
@@ -3207,79 +3402,79 @@ window.__ModuleLoader__.load({
|
|
|
3207
3402
|
borderRadius: 6,
|
|
3208
3403
|
},
|
|
3209
3404
|
},
|
|
3210
|
-
|
|
3211
|
-
|
|
3405
|
+
h('div', { className: 'dkb-group', style: { margin: '0 0 6px 0' } }, t('report.title')),
|
|
3406
|
+
h('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)', marginBottom: 6, fontWeight: 500 } },
|
|
3212
3407
|
openTask.report.summary),
|
|
3213
3408
|
openTask.report.changedFiles && openTask.report.changedFiles.length
|
|
3214
|
-
?
|
|
3215
|
-
|
|
3216
|
-
openTask.report.changedFiles.map((f) =>
|
|
3409
|
+
? h('div', { style: { fontSize: 12, marginBottom: 4 } },
|
|
3410
|
+
h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.changedFiles') + ': '),
|
|
3411
|
+
openTask.report.changedFiles.map((f) => h('span', { className: 'dkb-tag', key: f, style: { margin: '1px 2px', fontFamily: 'monospace' } }, f)),
|
|
3217
3412
|
)
|
|
3218
3413
|
: null,
|
|
3219
3414
|
openTask.report.checksRun && openTask.report.checksRun.length
|
|
3220
|
-
?
|
|
3221
|
-
|
|
3222
|
-
openTask.report.checksRun.map((chk, i) =>
|
|
3415
|
+
? h('div', { style: { fontSize: 12, marginBottom: 4 } },
|
|
3416
|
+
h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.checksRun') + ': '),
|
|
3417
|
+
openTask.report.checksRun.map((chk, i) => h('div', { key: i, style: { fontFamily: 'monospace', fontSize: 11, marginLeft: 8 } }, '• ' + chk)),
|
|
3223
3418
|
)
|
|
3224
3419
|
: null,
|
|
3225
3420
|
openTask.report.artifacts && openTask.report.artifacts.length
|
|
3226
|
-
?
|
|
3227
|
-
|
|
3228
|
-
openTask.report.artifacts.map((a, i) =>
|
|
3421
|
+
? h('div', { style: { fontSize: 12, marginBottom: 4 } },
|
|
3422
|
+
h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.artifacts') + ': '),
|
|
3423
|
+
openTask.report.artifacts.map((a, i) => h('span', { className: 'dkb-tag', key: i, style: { margin: '1px 2px' } }, a)),
|
|
3229
3424
|
)
|
|
3230
3425
|
: null,
|
|
3231
3426
|
openTask.report.risks && openTask.report.risks.length
|
|
3232
|
-
?
|
|
3233
|
-
|
|
3234
|
-
openTask.report.risks.map((r, i) =>
|
|
3427
|
+
? h('div', { style: { fontSize: 12 } },
|
|
3428
|
+
h('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, t('report.risks') + ': '),
|
|
3429
|
+
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
3430
|
)
|
|
3236
3431
|
: null,
|
|
3237
3432
|
)
|
|
3238
3433
|
: null,
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3434
|
+
h('div', { style: { margin: '6px 0' } },
|
|
3435
|
+
h('div', { className: 'dkb-group', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
3436
|
+
h('span', null, t('comments.title')),
|
|
3242
3437
|
Array.isArray(openTask.comments) && openTask.comments.length > 0
|
|
3243
|
-
?
|
|
3438
|
+
? h('span', { className: 'dkb-hint' }, `(${openTask.comments.length})`)
|
|
3244
3439
|
: null,
|
|
3245
3440
|
),
|
|
3246
3441
|
Array.isArray(openTask.comments) && openTask.comments.length > 0
|
|
3247
|
-
?
|
|
3442
|
+
? h('ul', {
|
|
3248
3443
|
style: {
|
|
3249
3444
|
listStyle: 'none', padding: 0, margin: '4px 0', maxHeight: 200, overflowY: 'auto',
|
|
3250
3445
|
display: 'flex', flexDirection: 'column', gap: 6,
|
|
3251
3446
|
},
|
|
3252
3447
|
},
|
|
3253
|
-
openTask.comments.map((c, i) =>
|
|
3448
|
+
openTask.comments.map((c, i) => h('li', {
|
|
3254
3449
|
key: c.id || i,
|
|
3255
3450
|
style: {
|
|
3256
3451
|
padding: 8, background: 'var(--dsw-alias-bg-layer-3)',
|
|
3257
3452
|
borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12,
|
|
3258
3453
|
},
|
|
3259
3454
|
},
|
|
3260
|
-
|
|
3261
|
-
|
|
3455
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 4 } },
|
|
3456
|
+
h('span', {
|
|
3262
3457
|
className: 'dkb-tag',
|
|
3263
3458
|
style: {
|
|
3264
3459
|
fontWeight: 600,
|
|
3265
3460
|
background: c.role === 'agent' ? 'var(--dsw-alias-bg-layer-1)' : 'var(--dsw-alias-bg-layer-2)',
|
|
3266
3461
|
},
|
|
3267
3462
|
}, c.role === 'agent' ? t('comments.authorAgent') : (c.author || t('comments.authorUser'))),
|
|
3268
|
-
|
|
3463
|
+
h('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 } },
|
|
3269
3464
|
new Date(c.createdAt || Date.now()).toLocaleTimeString()),
|
|
3270
3465
|
),
|
|
3271
|
-
|
|
3466
|
+
h('div', { style: { whiteSpace: 'pre-wrap', color: 'var(--dsw-alias-label-primary)' } }, c.text),
|
|
3272
3467
|
)))
|
|
3273
|
-
:
|
|
3274
|
-
|
|
3275
|
-
|
|
3468
|
+
: h('p', { className: 'dkb-hint' }, t('comments.empty')),
|
|
3469
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 } },
|
|
3470
|
+
h('textarea', {
|
|
3276
3471
|
className: 'dkb-area',
|
|
3277
3472
|
placeholder: t('comments.placeholder'),
|
|
3278
3473
|
value: newCommentText,
|
|
3279
3474
|
rows: 2,
|
|
3280
3475
|
onChange: (e) => setNewCommentText(e.target.value),
|
|
3281
3476
|
}),
|
|
3282
|
-
|
|
3477
|
+
h('button', {
|
|
3283
3478
|
type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
|
|
3284
3479
|
onClick: async () => {
|
|
3285
3480
|
if (!newCommentText.trim()) return
|
|
@@ -3297,10 +3492,8 @@ window.__ModuleLoader__.load({
|
|
|
3297
3492
|
),
|
|
3298
3493
|
),
|
|
3299
3494
|
openTask.archivedAt
|
|
3300
|
-
?
|
|
3495
|
+
? h('button', {
|
|
3301
3496
|
type: 'button', className: 'dkb-save', style: { alignSelf: 'flex-start' },
|
|
3302
|
-
// Запускать работу по архивной задаче нечего: сперва она
|
|
3303
|
-
// возвращается на доску, и это одно нажатие.
|
|
3304
3497
|
onClick: async () => {
|
|
3305
3498
|
try {
|
|
3306
3499
|
await api('/task/' + encodeURIComponent(openTask.id) + '/restore', { method: 'POST', body: '{}' })
|
|
@@ -3310,70 +3503,59 @@ window.__ModuleLoader__.load({
|
|
|
3310
3503
|
} catch (e) { setError(t(e.key || 'error.unknown')) }
|
|
3311
3504
|
},
|
|
3312
3505
|
}, t('archive.restore'))
|
|
3313
|
-
:
|
|
3314
|
-
|
|
3506
|
+
: h('div', { className: 'dkb-start' },
|
|
3507
|
+
h('div', { className: 'dkb-group' },
|
|
3315
3508
|
openTask.sessionId ? t('panel.continue') : t('panel.start')),
|
|
3316
|
-
|
|
3509
|
+
h('p', { className: 'dkb-hint' },
|
|
3317
3510
|
openTask.sessionId ? t('panel.continueHint') : t('panel.startHint')),
|
|
3318
|
-
// Корень не задан настройкой — сессия пойдёт в рабочей папке
|
|
3319
|
-
// харнесса. Молчать об этом нельзя: человек иначе узнает, где
|
|
3320
|
-
// оказался агент, только из его первых слов. Показываем фактический
|
|
3321
|
-
// корень, который приехал с доской.
|
|
3322
3511
|
!openTask.sessionId && state && !state.projectRoot.set && state.projectRoot.path !== ''
|
|
3323
|
-
?
|
|
3512
|
+
? h('p', { className: 'dkb-hint dkb-workdir' },
|
|
3324
3513
|
t('panel.workDir', { path: state.projectRoot.path }),
|
|
3325
|
-
|
|
3514
|
+
h('br'),
|
|
3326
3515
|
t('panel.workDirHint'))
|
|
3327
3516
|
: null,
|
|
3328
3517
|
React.createElement(LaunchFields, {
|
|
3329
3518
|
t, presets, agentPreset, permission, disabled: starting,
|
|
3330
3519
|
onPreset: setAgentPreset, onPermission: setPermission,
|
|
3331
|
-
// У идущей сессии профиль уже выбран: сменить его нельзя, а
|
|
3332
|
-
// показывать поле, которое ничего не меняет, — врать.
|
|
3333
3520
|
hidePreset: Boolean(openTask.sessionId),
|
|
3334
3521
|
}),
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3522
|
+
h('div', { className: 'dkb-field' },
|
|
3523
|
+
h('span', { className: 'dkb-label' }, t('panel.provider')),
|
|
3524
|
+
h('select', {
|
|
3338
3525
|
className: 'dkb-input', value: provider, disabled: starting || providers.length === 0,
|
|
3339
3526
|
onChange: (e) => { setProvider(e.target.value); setModel('') },
|
|
3340
3527
|
},
|
|
3341
|
-
|
|
3528
|
+
h('option', { value: '' },
|
|
3342
3529
|
providers.length === 0 ? t('panel.noProviders') : t('panel.pickProvider')),
|
|
3343
|
-
providers.map((p) =>
|
|
3530
|
+
providers.map((p) => h('option', { value: p.id, key: p.id }, p.name)),
|
|
3344
3531
|
),
|
|
3345
3532
|
),
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3533
|
+
h('div', { className: 'dkb-field' },
|
|
3534
|
+
h('span', { className: 'dkb-label' }, t('panel.model')),
|
|
3535
|
+
h('select', {
|
|
3349
3536
|
className: 'dkb-input', value: model,
|
|
3350
3537
|
disabled: starting || provider === '' || models.length === 0,
|
|
3351
3538
|
onChange: (e) => setModel(e.target.value),
|
|
3352
3539
|
},
|
|
3353
|
-
|
|
3540
|
+
h('option', { value: '' },
|
|
3354
3541
|
provider === '' ? t('panel.pickProviderFirst')
|
|
3355
3542
|
: models.length === 0 ? t('panel.noModels') : t('panel.pickModel')),
|
|
3356
|
-
models.map((m) =>
|
|
3543
|
+
models.map((m) => h('option', { value: m.id, key: m.id }, m.name)),
|
|
3357
3544
|
),
|
|
3358
3545
|
),
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3546
|
+
h('div', { className: 'dkb-field' },
|
|
3547
|
+
h('span', { className: 'dkb-label' }, t('panel.message')),
|
|
3548
|
+
h('textarea', {
|
|
3362
3549
|
className: 'dkb-area', value: draftText, disabled: starting,
|
|
3363
3550
|
onChange: (e) => setDraftText(e.target.value),
|
|
3364
3551
|
}),
|
|
3365
|
-
|
|
3552
|
+
h('p', { className: 'dkb-hint' }, t('panel.messageHint')),
|
|
3366
3553
|
),
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
React.createElement('div', { className: 'dkb-foot dkb-footSplit' },
|
|
3371
|
-
React.createElement('div', { className: 'dkb-footLeft' },
|
|
3372
|
-
React.createElement('button', {
|
|
3554
|
+
h('div', { className: 'dkb-foot dkb-footSplit' },
|
|
3555
|
+
h('div', { className: 'dkb-footLeft' },
|
|
3556
|
+
h('button', {
|
|
3373
3557
|
type: 'button', className: 'dkb-danger',
|
|
3374
3558
|
onClick: async () => {
|
|
3375
|
-
// Подтверждение обязательно: удаление карточки необратимо, а
|
|
3376
|
-
// сама кнопка стоит рядом с безобидными.
|
|
3377
3559
|
if (!window.confirm(t('panel.deleteConfirm', { title: openTask.title }))) return
|
|
3378
3560
|
try {
|
|
3379
3561
|
await api('/task/' + encodeURIComponent(openTask.id), { method: 'DELETE' })
|
|
@@ -3385,7 +3567,7 @@ window.__ModuleLoader__.load({
|
|
|
3385
3567
|
},
|
|
3386
3568
|
}, t('panel.delete')),
|
|
3387
3569
|
openTask.column === 'done'
|
|
3388
|
-
?
|
|
3570
|
+
? h('button', {
|
|
3389
3571
|
type: 'button', className: 'dkb-discard',
|
|
3390
3572
|
onClick: async () => {
|
|
3391
3573
|
try {
|
|
@@ -3397,7 +3579,7 @@ window.__ModuleLoader__.load({
|
|
|
3397
3579
|
}, t('panel.archive'))
|
|
3398
3580
|
: null,
|
|
3399
3581
|
openTask.column === 'cleanup' && openTask.worktree
|
|
3400
|
-
?
|
|
3582
|
+
? h('button', {
|
|
3401
3583
|
type: 'button', className: 'dkb-discard',
|
|
3402
3584
|
onClick: async () => {
|
|
3403
3585
|
try {
|
|
@@ -3415,7 +3597,7 @@ window.__ModuleLoader__.load({
|
|
|
3415
3597
|
}, t('panel.cleanupWorktree'))
|
|
3416
3598
|
: null,
|
|
3417
3599
|
),
|
|
3418
|
-
(openTask.worktree || openTask.rejectReason || (Array.isArray(openTask.runs) && openTask.runs.length > 0)) ?
|
|
3600
|
+
(openTask.worktree || openTask.rejectReason || (Array.isArray(openTask.runs) && openTask.runs.length > 0)) ? h('button', {
|
|
3419
3601
|
type: 'button',
|
|
3420
3602
|
className: 'dkb-secondary',
|
|
3421
3603
|
style: { marginRight: '8px', border: '1px solid var(--dsw-alias-border-color-primary, #444)' },
|
|
@@ -3442,11 +3624,9 @@ window.__ModuleLoader__.load({
|
|
|
3442
3624
|
}
|
|
3443
3625
|
},
|
|
3444
3626
|
}, t('action.resume')) : null,
|
|
3445
|
-
|
|
3627
|
+
h('button', {
|
|
3446
3628
|
type: 'button', className: 'dkb-save', disabled: starting || model === '',
|
|
3447
3629
|
onClick: async () => {
|
|
3448
|
-
// Кнопка блокируется на время запроса: двойное нажатие подняло
|
|
3449
|
-
// бы две сессии по одной задаче.
|
|
3450
3630
|
setStarting(true)
|
|
3451
3631
|
try {
|
|
3452
3632
|
rememberLaunch(board, { provider, model, agentPreset, permission })
|
|
@@ -3456,9 +3636,6 @@ window.__ModuleLoader__.load({
|
|
|
3456
3636
|
})
|
|
3457
3637
|
setOpenTask(null)
|
|
3458
3638
|
await load(board)
|
|
3459
|
-
// Сессия поднята — но пока её не открыли, для человека
|
|
3460
|
-
// «ничего не произошло»: он остаётся на доске и не видит
|
|
3461
|
-
// чата, ради которого нажимал.
|
|
3462
3639
|
if (!openSession(props.ctx, out && out.sessionId)) {
|
|
3463
3640
|
setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
|
|
3464
3641
|
return
|
|
@@ -3478,18 +3655,18 @@ window.__ModuleLoader__.load({
|
|
|
3478
3655
|
: null
|
|
3479
3656
|
|
|
3480
3657
|
const moveDialogEl = dialog && dialog.mode === 'move'
|
|
3481
|
-
?
|
|
3482
|
-
|
|
3483
|
-
|
|
3658
|
+
? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
|
|
3659
|
+
h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
|
|
3660
|
+
h('span', { className: 'dkb-title' },
|
|
3484
3661
|
t('move.title', { column: t('column.' + dialog.column) })),
|
|
3485
|
-
|
|
3662
|
+
h('p', { className: 'dkb-note' },
|
|
3486
3663
|
cardRef(dialog.task) ? cardRef(dialog.task) + ' · ' + dialog.task.title : dialog.task.title),
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3664
|
+
h('p', { className: 'dkb-hint' }, t('move.' + dialog.column)),
|
|
3665
|
+
h('div', { className: 'dkb-row' },
|
|
3666
|
+
h('button', {
|
|
3490
3667
|
type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
|
|
3491
3668
|
}, t('dialog.cancel')),
|
|
3492
|
-
|
|
3669
|
+
h('button', {
|
|
3493
3670
|
type: 'button', className: 'dkb-save',
|
|
3494
3671
|
onClick: () => {
|
|
3495
3672
|
const d = dialog
|
|
@@ -3501,51 +3678,49 @@ window.__ModuleLoader__.load({
|
|
|
3501
3678
|
))
|
|
3502
3679
|
: null
|
|
3503
3680
|
|
|
3504
|
-
// Групповой запуск. Провайдер и модель спрашиваем один раз на всю пачку:
|
|
3505
|
-
// сессия одна, значит и модель одна.
|
|
3506
3681
|
const batchEl = dialog && dialog.mode === 'batch'
|
|
3507
|
-
?
|
|
3508
|
-
|
|
3509
|
-
|
|
3682
|
+
? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
|
|
3683
|
+
h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
|
|
3684
|
+
h('span', { className: 'dkb-title' },
|
|
3510
3685
|
t('batch.title', { n: dialog.ids.length })),
|
|
3511
|
-
|
|
3512
|
-
dialog.error ?
|
|
3513
|
-
|
|
3686
|
+
h('p', { className: 'dkb-hint' }, t('batch.hint')),
|
|
3687
|
+
dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
|
|
3688
|
+
h('ul', { className: 'dkb-batchList' },
|
|
3514
3689
|
dialog.ids.map((id, at) => {
|
|
3515
3690
|
const task = state.tasks.find((x) => x.id === id)
|
|
3516
|
-
return
|
|
3517
|
-
|
|
3518
|
-
|
|
3691
|
+
return h('li', { className: 'dkb-batchRow', key: id },
|
|
3692
|
+
h('span', { className: 'dkb-refNum' }, String(at + 1)),
|
|
3693
|
+
h('span', null, task ? task.title : id))
|
|
3519
3694
|
})),
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3695
|
+
h('div', { className: 'dkb-field' },
|
|
3696
|
+
h('span', { className: 'dkb-label' }, t('panel.provider')),
|
|
3697
|
+
h('select', {
|
|
3523
3698
|
className: 'dkb-input', value: provider,
|
|
3524
3699
|
onChange: (e) => { setProvider(e.target.value); setModel('') },
|
|
3525
3700
|
},
|
|
3526
|
-
|
|
3527
|
-
providers.map((p) =>
|
|
3701
|
+
h('option', { value: '' }, t('panel.pickProvider')),
|
|
3702
|
+
providers.map((p) => h('option', { value: p.id, key: p.id }, p.name)),
|
|
3528
3703
|
),
|
|
3529
3704
|
),
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3705
|
+
h('div', { className: 'dkb-field' },
|
|
3706
|
+
h('span', { className: 'dkb-label' }, t('panel.model')),
|
|
3707
|
+
h('select', {
|
|
3533
3708
|
className: 'dkb-input', value: model, disabled: provider === '',
|
|
3534
3709
|
onChange: (e) => setModel(e.target.value),
|
|
3535
3710
|
},
|
|
3536
|
-
|
|
3537
|
-
models.map((m) =>
|
|
3711
|
+
h('option', { value: '' }, t('panel.pickModel')),
|
|
3712
|
+
models.map((m) => h('option', { value: m.id, key: m.id }, m.name)),
|
|
3538
3713
|
),
|
|
3539
3714
|
),
|
|
3540
3715
|
React.createElement(LaunchFields, {
|
|
3541
3716
|
t, presets, agentPreset, permission,
|
|
3542
3717
|
onPreset: setAgentPreset, onPermission: setPermission,
|
|
3543
3718
|
}),
|
|
3544
|
-
|
|
3545
|
-
|
|
3719
|
+
h('div', { className: 'dkb-row' },
|
|
3720
|
+
h('button', {
|
|
3546
3721
|
type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
|
|
3547
3722
|
}, t('dialog.cancel')),
|
|
3548
|
-
|
|
3723
|
+
h('button', {
|
|
3549
3724
|
type: 'button', className: 'dkb-save', disabled: model === '',
|
|
3550
3725
|
onClick: async () => {
|
|
3551
3726
|
try {
|
|
@@ -3557,8 +3732,6 @@ window.__ModuleLoader__.load({
|
|
|
3557
3732
|
setDialog(null)
|
|
3558
3733
|
setPicked({})
|
|
3559
3734
|
await load(board)
|
|
3560
|
-
// Сессия поднята — но пока её не открыли, для человека
|
|
3561
|
-
// «ничего не произошло».
|
|
3562
3735
|
if (!openSession(props.ctx, out && out.sessionId)) {
|
|
3563
3736
|
setError(t('error.sessionNotOpened', { id: (out && out.sessionId) || '' }))
|
|
3564
3737
|
return
|
|
@@ -3574,28 +3747,28 @@ window.__ModuleLoader__.load({
|
|
|
3574
3747
|
: null
|
|
3575
3748
|
|
|
3576
3749
|
const queueEl = dialog && dialog.mode === 'queue'
|
|
3577
|
-
?
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
dialog.error ?
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3750
|
+
? h('div', { className: 'dkb-dialog', onClick: () => setDialog(null) },
|
|
3751
|
+
h('div', { className: 'dkb-dialogBox', onClick: (e) => e.stopPropagation() },
|
|
3752
|
+
h('span', { className: 'dkb-title' }, t('queue.title')),
|
|
3753
|
+
h('p', { className: 'dkb-hint' }, t('queue.hint')),
|
|
3754
|
+
dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
|
|
3755
|
+
h('div', { className: 'dkb-field' },
|
|
3756
|
+
h('span', { className: 'dkb-label' }, t('queue.session')),
|
|
3757
|
+
h('select', {
|
|
3585
3758
|
className: 'dkb-input', value: dialog.sessionId,
|
|
3586
3759
|
onChange: (e) => setDialog(Object.assign({}, dialog, { sessionId: e.target.value })),
|
|
3587
|
-
}, dialog.sessions.map((one) =>
|
|
3760
|
+
}, dialog.sessions.map((one) => h('option', {
|
|
3588
3761
|
value: one.sessionId, key: one.sessionId,
|
|
3589
3762
|
}, t('queue.option', {
|
|
3590
3763
|
n: one.tasks.length,
|
|
3591
3764
|
title: (one.tasks[0] && one.tasks[0].title) || one.sessionId,
|
|
3592
3765
|
})))),
|
|
3593
3766
|
),
|
|
3594
|
-
|
|
3595
|
-
|
|
3767
|
+
h('div', { className: 'dkb-row' },
|
|
3768
|
+
h('button', {
|
|
3596
3769
|
type: 'button', className: 'dkb-discard', onClick: () => setDialog(null),
|
|
3597
3770
|
}, t('dialog.cancel')),
|
|
3598
|
-
|
|
3771
|
+
h('button', {
|
|
3599
3772
|
type: 'button', className: 'dkb-save',
|
|
3600
3773
|
onClick: async () => {
|
|
3601
3774
|
try {
|
|
@@ -3615,17 +3788,15 @@ window.__ModuleLoader__.load({
|
|
|
3615
3788
|
: null
|
|
3616
3789
|
|
|
3617
3790
|
const dialogEl = dialog && dialog.mode !== 'move' && dialog.mode !== 'batch' && dialog.mode !== 'queue'
|
|
3618
|
-
?
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
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' }, t('dialog.title')),
|
|
3794
|
+
h('div', { className: 'dkb-row' },
|
|
3795
|
+
h('button', {
|
|
3623
3796
|
type: 'button', className: dialog.mode === 'own' ? 'dkb-save' : 'dkb-discard',
|
|
3624
3797
|
onClick: () => setDialog(Object.assign({}, dialog, { mode: 'own', error: '' })),
|
|
3625
3798
|
}, t('dialog.own')),
|
|
3626
|
-
|
|
3627
|
-
// значит звать туда, откуда сервер вернёт отказ.
|
|
3628
|
-
state.kind === 'simple' ? null : React.createElement('button', {
|
|
3799
|
+
state.kind === 'simple' ? null : h('button', {
|
|
3629
3800
|
type: 'button', className: dialog.mode === 'gitea' ? 'dkb-save' : 'dkb-discard',
|
|
3630
3801
|
onClick: async () => {
|
|
3631
3802
|
setDialog(Object.assign({}, dialog, { mode: 'gitea', error: '' }))
|
|
@@ -3639,93 +3810,86 @@ window.__ModuleLoader__.load({
|
|
|
3639
3810
|
},
|
|
3640
3811
|
}, t('dialog.fromGitea')),
|
|
3641
3812
|
),
|
|
3642
|
-
dialog.error ?
|
|
3813
|
+
dialog.error ? h('p', { className: 'dkb-failed' }, dialog.error) : null,
|
|
3643
3814
|
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', {
|
|
3815
|
+
? h('div', null,
|
|
3816
|
+
h('div', { className: 'dkb-field' },
|
|
3817
|
+
h('span', { className: 'dkb-label' }, t('dialog.kind')),
|
|
3818
|
+
h('div', { className: 'dkb-row' },
|
|
3819
|
+
h('button', {
|
|
3652
3820
|
type: 'button', className: dialog.project ? 'dkb-save' : 'dkb-discard',
|
|
3653
3821
|
onClick: () => setDialog(Object.assign({}, dialog, { project: true, error: '' })),
|
|
3654
3822
|
}, t('dialog.kindProject')),
|
|
3655
|
-
|
|
3823
|
+
h('button', {
|
|
3656
3824
|
type: 'button', className: dialog.project ? 'dkb-discard' : 'dkb-save',
|
|
3657
3825
|
onClick: () => setDialog(Object.assign({}, dialog, { project: false, error: '' })),
|
|
3658
3826
|
}, t('dialog.kindPlain')),
|
|
3659
3827
|
),
|
|
3660
|
-
|
|
3828
|
+
h('p', { className: 'dkb-hint' },
|
|
3661
3829
|
t(dialog.project ? 'dialog.kindProjectHint' : 'dialog.kindPlainHint')),
|
|
3662
3830
|
),
|
|
3663
3831
|
dialog.project
|
|
3664
|
-
?
|
|
3665
|
-
|
|
3666
|
-
|
|
3832
|
+
? h('div', { className: 'dkb-field' },
|
|
3833
|
+
h('span', { className: 'dkb-label' }, t('dialog.repo')),
|
|
3834
|
+
h('select', {
|
|
3667
3835
|
className: 'dkb-input', value: dialog.repo || '',
|
|
3668
3836
|
onChange: (e) => setDialog(Object.assign({}, dialog, { repo: e.target.value })),
|
|
3669
3837
|
},
|
|
3670
|
-
|
|
3838
|
+
h('option', { value: '' }, t('dialog.repoNew')),
|
|
3671
3839
|
(state.facets || [])
|
|
3672
3840
|
.filter((f) => f.ns === REPO_FACET)
|
|
3673
3841
|
.flatMap((f) => f.values)
|
|
3674
|
-
.map((v) =>
|
|
3842
|
+
.map((v) => h('option', { value: v.value, key: v.value }, v.value)),
|
|
3675
3843
|
),
|
|
3676
3844
|
(dialog.repo || '') === ''
|
|
3677
|
-
?
|
|
3678
|
-
|
|
3845
|
+
? h('div', null,
|
|
3846
|
+
h('input', {
|
|
3679
3847
|
className: 'dkb-input', value: dialog.newRepo || '',
|
|
3680
3848
|
placeholder: t('dialog.repoNamePlaceholder'),
|
|
3681
3849
|
onChange: (e) => setDialog(Object.assign({}, dialog, { newRepo: e.target.value })),
|
|
3682
3850
|
}),
|
|
3683
|
-
|
|
3684
|
-
// человек должен видеть это ДО нажатия, а не после.
|
|
3685
|
-
React.createElement('p', { className: 'dkb-hint' }, t('dialog.repoNewHint')),
|
|
3851
|
+
h('p', { className: 'dkb-hint' }, t('dialog.repoNewHint')),
|
|
3686
3852
|
)
|
|
3687
3853
|
: null,
|
|
3688
3854
|
)
|
|
3689
3855
|
: null,
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3856
|
+
h('div', { className: 'dkb-field' },
|
|
3857
|
+
h('span', { className: 'dkb-label' }, t('dialog.taskTitle')),
|
|
3858
|
+
h('input', {
|
|
3693
3859
|
className: 'dkb-input', value: dialog.title, autoFocus: true,
|
|
3694
3860
|
onChange: (e) => setDialog(Object.assign({}, dialog, { title: e.target.value })),
|
|
3695
3861
|
}),
|
|
3696
3862
|
),
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3863
|
+
h('div', { className: 'dkb-field' },
|
|
3864
|
+
h('span', { className: 'dkb-label' }, t('dialog.taskBody')),
|
|
3865
|
+
h('textarea', {
|
|
3700
3866
|
className: 'dkb-area', value: dialog.body || '',
|
|
3701
3867
|
onChange: (e) => setDialog(Object.assign({}, dialog, { body: e.target.value })),
|
|
3702
3868
|
}),
|
|
3703
|
-
|
|
3869
|
+
h('p', { className: 'dkb-hint' }, t('dialog.bodyHint')),
|
|
3704
3870
|
),
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3871
|
+
h('div', { className: 'dkb-field' },
|
|
3872
|
+
h('span', { className: 'dkb-label' }, t('dialog.taskLabels')),
|
|
3873
|
+
h('input', {
|
|
3708
3874
|
className: 'dkb-input', value: dialog.labels || '',
|
|
3709
3875
|
placeholder: t('dialog.labelsHint'),
|
|
3710
3876
|
onChange: (e) => setDialog(Object.assign({}, dialog, { labels: e.target.value })),
|
|
3711
3877
|
}),
|
|
3712
3878
|
),
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3879
|
+
h('div', { className: 'dkb-field' },
|
|
3880
|
+
h('span', { className: 'dkb-label' }, t('dialog.taskColumn')),
|
|
3881
|
+
h('select', {
|
|
3716
3882
|
className: 'dkb-input', value: dialog.column || 'backlog',
|
|
3717
3883
|
onChange: (e) => setDialog(Object.assign({}, dialog, { column: e.target.value })),
|
|
3718
|
-
}, (state.columns || []).map((c) =>
|
|
3884
|
+
}, (state.columns || []).map((c) => h('option', { value: c.id, key: c.id }, t('column.' + c.id)))),
|
|
3719
3885
|
),
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3886
|
+
h('div', { className: 'dkb-foot' },
|
|
3887
|
+
h('button', { type: 'button', className: 'dkb-discard', onClick: () => setDialog(null) }, t('dialog.cancel')),
|
|
3888
|
+
h('button', {
|
|
3723
3889
|
type: 'button', className: 'dkb-save', disabled: String(dialog.title || '').trim() === '',
|
|
3724
3890
|
onClick: async () => {
|
|
3725
3891
|
try {
|
|
3726
3892
|
if (dialog.project) {
|
|
3727
|
-
// Проектная задача заводит issue и привязывается к
|
|
3728
|
-
// нему; метки ей ставит Gitea, а не мы.
|
|
3729
3893
|
await api('/project-task', {
|
|
3730
3894
|
method: 'POST',
|
|
3731
3895
|
body: JSON.stringify({
|
|
@@ -3742,12 +3906,8 @@ window.__ModuleLoader__.load({
|
|
|
3742
3906
|
body: JSON.stringify({
|
|
3743
3907
|
title: dialog.title,
|
|
3744
3908
|
body: dialog.body || '',
|
|
3745
|
-
// Метки вводятся через запятую: отдельный редактор
|
|
3746
|
-
// меток ради своей задачи — лишняя мебель.
|
|
3747
3909
|
labels: String(dialog.labels || '').split(',').map((x) => x.trim()).filter(Boolean),
|
|
3748
3910
|
column: dialog.column || 'backlog',
|
|
3749
|
-
// Задача без issue живёт на простой доске: правило
|
|
3750
|
-
// без исключений, и помнить, где её завели, не надо.
|
|
3751
3911
|
board: 'simple',
|
|
3752
3912
|
}),
|
|
3753
3913
|
})
|
|
@@ -3761,9 +3921,9 @@ window.__ModuleLoader__.load({
|
|
|
3761
3921
|
}, t('dialog.create')),
|
|
3762
3922
|
),
|
|
3763
3923
|
)
|
|
3764
|
-
:
|
|
3765
|
-
|
|
3766
|
-
|
|
3924
|
+
: h('div', null,
|
|
3925
|
+
h('div', { className: 'dkb-row' },
|
|
3926
|
+
h('select', {
|
|
3767
3927
|
className: 'dkb-input', value: dialog.full,
|
|
3768
3928
|
disabled: dialog.repos === null || dialog.repos.length === 0,
|
|
3769
3929
|
onChange: async (e) => {
|
|
@@ -3780,20 +3940,20 @@ window.__ModuleLoader__.load({
|
|
|
3780
3940
|
}
|
|
3781
3941
|
},
|
|
3782
3942
|
},
|
|
3783
|
-
|
|
3943
|
+
h('option', { value: '' },
|
|
3784
3944
|
dialog.repos === null ? t('dialog.loadingRepos')
|
|
3785
3945
|
: dialog.repos.length === 0 ? t('dialog.noRepos') : t('dialog.pickRepo')),
|
|
3786
3946
|
(dialog.repos || []).map((r) => {
|
|
3787
3947
|
const full = r.fullName || (r.owner + '/' + r.repo)
|
|
3788
|
-
return
|
|
3948
|
+
return h('option', { value: full, key: full }, repoOption(r, t))
|
|
3789
3949
|
}),
|
|
3790
3950
|
),
|
|
3791
3951
|
),
|
|
3792
3952
|
dialog.issues === null ? null
|
|
3793
3953
|
: dialog.issues.length === 0
|
|
3794
|
-
?
|
|
3795
|
-
:
|
|
3796
|
-
|
|
3954
|
+
? h('p', { className: 'dkb-note' }, t('dialog.noIssues'))
|
|
3955
|
+
: h('div', { className: 'dkb-list' }, dialog.issues.map((issue) =>
|
|
3956
|
+
h('button', {
|
|
3797
3957
|
type: 'button',
|
|
3798
3958
|
className: 'dkb-issue' + (issue.imported ? ' dkb-issueUsed' : ''),
|
|
3799
3959
|
key: issue.number,
|
|
@@ -3816,25 +3976,21 @@ window.__ModuleLoader__.load({
|
|
|
3816
3976
|
))
|
|
3817
3977
|
: null
|
|
3818
3978
|
|
|
3819
|
-
// Архив — свой экран, а не колонка: колонка это стадия работы, а архив
|
|
3820
|
-
// стадией не является. Возврат ставит карточку ровно туда, где она была.
|
|
3821
3979
|
const archiveEl = archive
|
|
3822
|
-
?
|
|
3823
|
-
|
|
3980
|
+
? h('div', { className: 'dkb-archive' },
|
|
3981
|
+
h('p', { className: 'dkb-hint' }, t('archive.hint')),
|
|
3824
3982
|
archive.length === 0
|
|
3825
|
-
?
|
|
3826
|
-
:
|
|
3827
|
-
archive.map((task) =>
|
|
3828
|
-
|
|
3983
|
+
? h('p', { className: 'dkb-note' }, t('archive.empty'))
|
|
3984
|
+
: h('ul', { className: 'dkb-archiveList' },
|
|
3985
|
+
archive.map((task) => h('li', { className: 'dkb-archiveRow', key: task.id },
|
|
3986
|
+
h('button', {
|
|
3829
3987
|
type: 'button', className: 'dkb-archiveTitle dkb-archiveOpen',
|
|
3830
|
-
// Архив — не братская могила: задачу туда убрали, а не
|
|
3831
|
-
// стёрли, и прочитать её должно быть можно, не возвращая.
|
|
3832
3988
|
onClick: () => setOpenTask(task),
|
|
3833
3989
|
},
|
|
3834
3990
|
[cardRef(task), task.title].filter(Boolean).join(' · ')),
|
|
3835
|
-
|
|
3991
|
+
h('span', { className: 'dkb-taskRef', title: exactAt(task.archivedAt) },
|
|
3836
3992
|
t('archive.since', { ago: agoText(state.now, task.archivedAt, t) })),
|
|
3837
|
-
|
|
3993
|
+
h('button', {
|
|
3838
3994
|
type: 'button', className: 'dkb-discard',
|
|
3839
3995
|
onClick: async () => {
|
|
3840
3996
|
try {
|
|
@@ -3848,45 +4004,43 @@ window.__ModuleLoader__.load({
|
|
|
3848
4004
|
)
|
|
3849
4005
|
: null
|
|
3850
4006
|
|
|
3851
|
-
// Метрика — это взгляд на журнал, а не новые данные: экран ничего не
|
|
3852
|
-
// меняет, поэтому и кнопок действий на нём нет.
|
|
3853
4007
|
const metricsEl = metrics
|
|
3854
|
-
?
|
|
3855
|
-
|
|
4008
|
+
? h('div', { className: 'dkb-archive' },
|
|
4009
|
+
h('p', { className: 'dkb-hint' },
|
|
3856
4010
|
t('metrics.hint', { n: metrics.tasks, week: metrics.done.week, month: metrics.done.month })),
|
|
3857
|
-
|
|
4011
|
+
h('div', { className: 'dkb-group' }, t('metrics.columns')),
|
|
3858
4012
|
metrics.columns.length === 0
|
|
3859
|
-
?
|
|
3860
|
-
:
|
|
3861
|
-
metrics.columns.map((row) =>
|
|
4013
|
+
? h('p', { className: 'dkb-note' }, t('metrics.empty'))
|
|
4014
|
+
: h('ul', { className: 'dkb-archiveList' },
|
|
4015
|
+
metrics.columns.map((row) => h('li', {
|
|
3862
4016
|
className: 'dkb-archiveRow', key: row.column,
|
|
3863
4017
|
},
|
|
3864
|
-
|
|
3865
|
-
|
|
4018
|
+
h('span', { className: 'dkb-archiveTitle' }, columnTitle({ id: row.column }, t)),
|
|
4019
|
+
h('span', { className: 'dkb-taskRef' },
|
|
3866
4020
|
t('metrics.median', { time: agoText(0, -row.median, t) })),
|
|
3867
|
-
|
|
4021
|
+
h('span', { className: 'dkb-taskRef' },
|
|
3868
4022
|
t('metrics.mean', { time: agoText(0, -row.mean, t) })),
|
|
3869
|
-
|
|
4023
|
+
h('span', { className: 'dkb-count' }, String(row.tasks)),
|
|
3870
4024
|
))),
|
|
3871
|
-
|
|
4025
|
+
h('div', { className: 'dkb-group' },
|
|
3872
4026
|
t('metrics.stale', { days: metrics.staleDays })),
|
|
3873
4027
|
metrics.stale.length === 0
|
|
3874
|
-
?
|
|
3875
|
-
:
|
|
3876
|
-
metrics.stale.map((row) =>
|
|
4028
|
+
? h('p', { className: 'dkb-note' }, t('metrics.noStale'))
|
|
4029
|
+
: h('ul', { className: 'dkb-archiveList' },
|
|
4030
|
+
metrics.stale.map((row) => h('li', {
|
|
3877
4031
|
className: 'dkb-archiveRow', key: row.id,
|
|
3878
4032
|
},
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
4033
|
+
h('span', { className: 'dkb-archiveTitle' }, row.title),
|
|
4034
|
+
h('span', { className: 'dkb-taskRef' }, columnTitle({ id: row.column }, t)),
|
|
4035
|
+
h('span', { className: 'dkb-taskRef', title: exactAt(row.since) },
|
|
3882
4036
|
agoText(state.now, row.since, t)),
|
|
3883
4037
|
))),
|
|
3884
4038
|
)
|
|
3885
4039
|
: null
|
|
3886
4040
|
|
|
3887
|
-
return
|
|
4041
|
+
return h('div', { className: 'dkb-screen' },
|
|
3888
4042
|
style, bar,
|
|
3889
|
-
error ?
|
|
4043
|
+
error ? h('p', { className: 'dkb-failed' }, error) : null,
|
|
3890
4044
|
metrics ? metricsEl : archive ? archiveEl : columns,
|
|
3891
4045
|
panel, dialogEl, moveDialogEl, batchEl, queueEl,
|
|
3892
4046
|
)
|
|
@@ -3934,35 +4088,32 @@ window.__ModuleLoader__.load({
|
|
|
3934
4088
|
if (!task) return null
|
|
3935
4089
|
|
|
3936
4090
|
const label = [cardRef(task) || task.title, t('column.' + task.column)].filter(Boolean).join(' · ')
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
4091
|
+
ensureCss()
|
|
4092
|
+
return h('span', { className: 'dkb-chipWrap' },
|
|
4093
|
+
h('button', {
|
|
3940
4094
|
type: 'button', className: 'dkb-chip', title: task.title,
|
|
3941
4095
|
onClick: () => setOpen((v) => !v),
|
|
3942
4096
|
}, '▦ ' + label),
|
|
3943
|
-
open ?
|
|
3944
|
-
|
|
3945
|
-
columns.map((id) =>
|
|
4097
|
+
open ? h('div', { className: 'dkb-chipPanel' },
|
|
4098
|
+
h('span', { className: 'dkb-hint' }, t('chip.moveTo')),
|
|
4099
|
+
columns.map((id) => h('button', {
|
|
3946
4100
|
type: 'button', key: id,
|
|
3947
4101
|
className: id === task.column ? 'dkb-save' : 'dkb-discard',
|
|
3948
4102
|
onClick: () => move(id),
|
|
3949
4103
|
}, t('column.' + id))),
|
|
3950
4104
|
props.toggle
|
|
3951
|
-
?
|
|
4105
|
+
? h('button', {
|
|
3952
4106
|
type: 'button', className: 'dkb-discard',
|
|
3953
4107
|
onClick: () => { setOpen(false); props.toggle.show(task.id, task.board) },
|
|
3954
4108
|
}, t('chip.openCard'))
|
|
3955
4109
|
: null,
|
|
3956
|
-
|
|
3957
|
-
|
|
4110
|
+
h('span', { className: 'dkb-hint' }, t('chip.note')),
|
|
4111
|
+
h('textarea', {
|
|
3958
4112
|
className: 'dkb-area', value: note, disabled: saving,
|
|
3959
4113
|
onChange: (e) => setNote(e.target.value),
|
|
3960
4114
|
}),
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
// сделать поведение случайным на вид.
|
|
3964
|
-
React.createElement('span', { className: 'dkb-hint' }, t('chip.noteHint')),
|
|
3965
|
-
React.createElement('button', {
|
|
4115
|
+
h('span', { className: 'dkb-hint' }, t('chip.noteHint')),
|
|
4116
|
+
h('button', {
|
|
3966
4117
|
type: 'button', className: 'dkb-save',
|
|
3967
4118
|
disabled: saving || note.trim() === '',
|
|
3968
4119
|
onClick: async () => {
|
|
@@ -3984,17 +4135,6 @@ window.__ModuleLoader__.load({
|
|
|
3984
4135
|
}
|
|
3985
4136
|
|
|
3986
4137
|
|
|
3987
|
-
// ------------------------------------------------- встраивание в оболочку
|
|
3988
|
-
//
|
|
3989
|
-
// Раздела верхнего уровня в этой сборке нет: слоты `app.section` и
|
|
3990
|
-
// `sidebar.section` отсутствуют, а `settings.section` уводит доску в
|
|
3991
|
-
// настройки, где ей не место. Поэтому строка и экран вставляются прямо в
|
|
3992
|
-
// вёрстку оболочки — тем же приёмом, каким это делает штатный task board.
|
|
3993
|
-
//
|
|
3994
|
-
// Приём хрупкий по своей природе: он опирается на чужую вёрстку. Смягчение
|
|
3995
|
-
// — селекторы по подстроке класса, наблюдатели за перерисовкой React и
|
|
3996
|
-
// мягкий отказ: не нашли корень — просто ничего не показали, ничего не
|
|
3997
|
-
// сломали.
|
|
3998
4138
|
|
|
3999
4139
|
const PANEL = 'kanban'
|
|
4000
4140
|
const ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
@@ -4012,8 +4152,6 @@ window.__ModuleLoader__.load({
|
|
|
4012
4152
|
/** @see helpers.createToggle */
|
|
4013
4153
|
function createToggle() {
|
|
4014
4154
|
let open = false
|
|
4015
|
-
// Задача, которую просили показать при открытии. Состояние окна живёт
|
|
4016
|
-
// внутри экрана доски, и снаружи до него иначе не дотянуться.
|
|
4017
4155
|
let wanted = undefined
|
|
4018
4156
|
const listeners = new Set()
|
|
4019
4157
|
return {
|
|
@@ -4026,8 +4164,6 @@ window.__ModuleLoader__.load({
|
|
|
4026
4164
|
toggle() { this.set(!open) },
|
|
4027
4165
|
/** Открыть доску на конкретной задаче. */
|
|
4028
4166
|
show(taskId, boardId) {
|
|
4029
|
-
// Доска едет вместе с задачей: карточка может лежать на простой, а
|
|
4030
|
-
// открытой быть проектная, и тогда искать её было бы негде.
|
|
4031
4167
|
wanted = { id: String(taskId || ''), board: String(boardId || 'main') }
|
|
4032
4168
|
if (open) { for (const fn of listeners) fn() } else this.set(true)
|
|
4033
4169
|
},
|
|
@@ -4124,8 +4260,6 @@ window.__ModuleLoader__.load({
|
|
|
4124
4260
|
else delete entry.dataset.active
|
|
4125
4261
|
}
|
|
4126
4262
|
|
|
4127
|
-
// Оболочка перерисовывается сама по себе: без наблюдателя кнопка исчезнет
|
|
4128
|
-
// при первом же обновлении списка сессий.
|
|
4129
4263
|
const observer = new MutationObserver(tryPlace)
|
|
4130
4264
|
observer.observe(document.body, { childList: true, subtree: true })
|
|
4131
4265
|
const unsubscribe = toggle.subscribe(syncActive)
|
|
@@ -4154,7 +4288,7 @@ window.__ModuleLoader__.load({
|
|
|
4154
4288
|
container.setAttribute('data-dsh-plugin', 'kanban')
|
|
4155
4289
|
column.appendChild(container)
|
|
4156
4290
|
root = ReactDOM.createRoot(container)
|
|
4157
|
-
root.render(
|
|
4291
|
+
root.render(h(BoardScreen, { ctx, toggle, onClose: () => toggle.set(false) }))
|
|
4158
4292
|
}
|
|
4159
4293
|
|
|
4160
4294
|
const applyActive = () => {
|
|
@@ -4166,8 +4300,6 @@ window.__ModuleLoader__.load({
|
|
|
4166
4300
|
}
|
|
4167
4301
|
}
|
|
4168
4302
|
|
|
4169
|
-
// Соседние панели объявляют себя тем же событием: две панели поверх одной
|
|
4170
|
-
// колонки одновременно — это каша, поэтому уступаем.
|
|
4171
4303
|
const onOtherActivate = (event) => {
|
|
4172
4304
|
if (event.detail !== PANEL && toggle.isOpen()) toggle.set(false)
|
|
4173
4305
|
}
|
|
@@ -4198,7 +4330,6 @@ window.__ModuleLoader__.load({
|
|
|
4198
4330
|
}
|
|
4199
4331
|
}
|
|
4200
4332
|
|
|
4201
|
-
// ------------------------------------------------------------ регистрация
|
|
4202
4333
|
|
|
4203
4334
|
/**
|
|
4204
4335
|
* Зарегистрировать компонент в первый слот, который его примет.
|
|
@@ -4235,31 +4366,23 @@ window.__ModuleLoader__.load({
|
|
|
4235
4366
|
function apply(ctx) {
|
|
4236
4367
|
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-kanban: словари')
|
|
4237
4368
|
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
// равным имени пространства.
|
|
4242
|
-
// Карточка настроек. В сборке с вкладкой «Настройки плагинов» регистрируется
|
|
4243
|
-
// в settings.plugin.item, иначе откатывается в отдельный раздел settings.section.
|
|
4369
|
+
refreshMirrorUntilVisible(ctx)
|
|
4370
|
+
const SettingsErrorBoundary = createErrorBoundary()
|
|
4371
|
+
const SafeSettingsCard = (props) => h(SettingsErrorBoundary, null, h(KanbanSettingsCard, props))
|
|
4244
4372
|
const cardSlot = registerFirst(ctx, [
|
|
4245
4373
|
{ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) },
|
|
4246
|
-
|
|
4247
|
-
], KanbanSettingsCard)
|
|
4374
|
+
], SafeSettingsCard)
|
|
4248
4375
|
|
|
4249
|
-
// Экран доски встраивается в оболочку, а не в слот: раздела верхнего
|
|
4250
|
-
// уровня в сборке нет, а настройки — не место для доски.
|
|
4251
4376
|
const toggle = createToggle()
|
|
4252
4377
|
ctx.effect(() => {
|
|
4253
4378
|
const off = [mountSidebarEntry(toggle, fallbackT('section.label')), mountBoard(ctx, toggle)]
|
|
4254
4379
|
return () => { for (const dispose of off) dispose() }
|
|
4255
4380
|
}, 'dsh-kanban: доска в оболочке')
|
|
4256
4381
|
|
|
4257
|
-
// Чип задачи в шапке чата. Слот подтверждён живым плагином dsh-gitea.
|
|
4258
4382
|
const chipSlot = registerFirst(ctx, [
|
|
4259
4383
|
{ name: 'conversation.session.header.utilities', id: '@goodandready/dsh-kanban.chip', order: 26, locale: NS, inject: () => ({ ctx, toggle }) },
|
|
4260
4384
|
], TaskChip)
|
|
4261
4385
|
|
|
4262
|
-
// Куда встали — видно снаружи: живая проверка сверяет это с интерфейсом.
|
|
4263
4386
|
exports.slots = { card: cardSlot, chip: chipSlot }
|
|
4264
4387
|
}
|
|
4265
4388
|
|