@unson/brainbase-mcp 0.41.3 → 0.41.6
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/dist/judgment-history-http.js +52 -11
- package/dist/judgment-history.d.ts +70 -2
- package/dist/judgment-history.js +381 -122
- package/package.json +2 -2
- package/ui/brainbase-tokens.css +4 -0
- package/ui/foundation-overview.js +30 -11
- package/ui/judgment-history.css +250 -23
- package/ui/judgment-history.js +497 -115
- package/ui/local-web-shell.js +4 -1
- package/ui/people-and-companies-view.css +143 -0
- package/ui/people-and-companies-view.js +87 -11
- package/ui/workspace-kit.css +43 -0
- package/ui/workspace-kit.js +131 -4
package/ui/judgment-history.js
CHANGED
|
@@ -11,10 +11,14 @@
|
|
|
11
11
|
import {
|
|
12
12
|
makeWorkspaceElement,
|
|
13
13
|
workspaceButton,
|
|
14
|
+
workspaceDialog,
|
|
15
|
+
workspaceFilterPopover,
|
|
16
|
+
workspaceErrorCard,
|
|
17
|
+
workspaceLoadingCard,
|
|
18
|
+
workspaceDismissNotification,
|
|
14
19
|
workspaceDefinition,
|
|
15
20
|
workspaceDetailEmpty,
|
|
16
21
|
workspaceLedger,
|
|
17
|
-
workspaceNotice,
|
|
18
22
|
workspacePageHeader,
|
|
19
23
|
workspaceRailBlock,
|
|
20
24
|
workspaceRailHead,
|
|
@@ -198,7 +202,7 @@ function normalizeNormalReference(value) {
|
|
|
198
202
|
why: text(value.why) || null,
|
|
199
203
|
usage: text(value.usage) || null,
|
|
200
204
|
availability: text(value.availability).toLowerCase() || 'unknown',
|
|
201
|
-
label: text(value.label ?? value.name) ||
|
|
205
|
+
label: text(value.label ?? value.name) || null,
|
|
202
206
|
};
|
|
203
207
|
}
|
|
204
208
|
|
|
@@ -806,23 +810,50 @@ function formatDate(value) {
|
|
|
806
810
|
}
|
|
807
811
|
}
|
|
808
812
|
|
|
813
|
+
function referenceLabel(ref) {
|
|
814
|
+
const label = text(ref?.label);
|
|
815
|
+
const id = text(ref?.id ?? ref?.ref);
|
|
816
|
+
if (!label || (id && label === id)) return '';
|
|
817
|
+
return label;
|
|
818
|
+
}
|
|
819
|
+
|
|
809
820
|
function makeReferenceSummary(doc, references) {
|
|
810
821
|
if (references === null || references === undefined) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照は未記録' });
|
|
811
822
|
if (!references.length) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照はありません' });
|
|
823
|
+
const labeled = [];
|
|
824
|
+
let unlabeled = 0;
|
|
825
|
+
for (const ref of references) {
|
|
826
|
+
const label = referenceLabel(ref);
|
|
827
|
+
if (label) labeled.push(label);
|
|
828
|
+
else unlabeled += 1;
|
|
829
|
+
}
|
|
812
830
|
const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-summary' });
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
831
|
+
if (labeled.length) {
|
|
832
|
+
wrap.append(makeWorkspaceElement(doc, 'strong', { text: labeled.slice(0, 2).join('、') }));
|
|
833
|
+
if (labeled.length > 2) wrap.append(makeWorkspaceElement(doc, 'small', { text: `ほか${labeled.length - 2}件` }));
|
|
834
|
+
}
|
|
835
|
+
if (unlabeled > 0) wrap.append(makeWorkspaceElement(doc, 'strong', { text: `名前のない参照 ${unlabeled}件` }));
|
|
816
836
|
return wrap;
|
|
817
837
|
}
|
|
818
838
|
|
|
839
|
+
function judgmentStatusLabel(status) {
|
|
840
|
+
const labels = {
|
|
841
|
+
resolved: '解決済み',
|
|
842
|
+
needs_clarification: '確認が必要',
|
|
843
|
+
needs_classification: '分類待ち',
|
|
844
|
+
needs_policy_resolution: '方針の確認待ち',
|
|
845
|
+
unknown: '状態は未記録',
|
|
846
|
+
};
|
|
847
|
+
const key = text(status);
|
|
848
|
+
if (!key) return '状態は未記録';
|
|
849
|
+
return labels[key] ?? '状態は未記録';
|
|
850
|
+
}
|
|
851
|
+
|
|
819
852
|
function makeDecisionSummary(doc, row) {
|
|
820
853
|
if (row.mode === 'normal') {
|
|
821
854
|
const summary = text(row.judgment?.summary);
|
|
822
|
-
const status = text(row.judgment?.status);
|
|
823
855
|
const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-decision is-normal' });
|
|
824
856
|
wrap.append(makeWorkspaceElement(doc, 'strong', { text: summary || '判断内容は未記録' }));
|
|
825
|
-
wrap.append(makeWorkspaceElement(doc, 'small', { text: `${NORMAL_ENTRYPOINT_LABELS[row.entrypoint] ?? '入口不明'}${status ? `・${status}` : ''}` }));
|
|
826
857
|
return wrap;
|
|
827
858
|
}
|
|
828
859
|
const summary = text(row.proof?.decision?.summary);
|
|
@@ -833,19 +864,28 @@ function makeDecisionSummary(doc, row) {
|
|
|
833
864
|
return wrap;
|
|
834
865
|
}
|
|
835
866
|
|
|
867
|
+
const REFERENCE_AVAILABILITY_LABELS = Object.freeze({
|
|
868
|
+
recorded: '今の権限で開けます',
|
|
869
|
+
permission_denied: '開く権限がありません',
|
|
870
|
+
unavailable: '本文はもうありません',
|
|
871
|
+
unknown: '未確認',
|
|
872
|
+
});
|
|
873
|
+
|
|
836
874
|
function appendReferenceList(doc, references) {
|
|
837
875
|
if (references === null || references === undefined) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照は記録されていません。' });
|
|
838
876
|
if (!references.length) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照はありません。' });
|
|
839
877
|
const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-reference-list' });
|
|
840
878
|
for (const ref of references) {
|
|
841
879
|
const item = makeWorkspaceElement(doc, 'li');
|
|
842
|
-
|
|
843
|
-
item.append(makeWorkspaceElement(doc, '
|
|
844
|
-
|
|
845
|
-
if (
|
|
846
|
-
|
|
847
|
-
item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-availability', text: availability }));
|
|
880
|
+
const label = referenceLabel(ref);
|
|
881
|
+
item.append(makeWorkspaceElement(doc, 'strong', { text: label || '名前のない参照' }));
|
|
882
|
+
const recordId = text(ref.ref ?? ref.id);
|
|
883
|
+
if (recordId) {
|
|
884
|
+
item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-record-id', text: `記録ID:${recordId}(コピー)` }));
|
|
848
885
|
}
|
|
886
|
+
if (ref.application && ref.application !== ref.label && ref.application !== label) item.append(makeWorkspaceElement(doc, 'span', { text: ref.application }));
|
|
887
|
+
const availability = REFERENCE_AVAILABILITY_LABELS[text(ref.availability)] ?? REFERENCE_AVAILABILITY_LABELS.unknown;
|
|
888
|
+
item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-availability', text: availability }));
|
|
849
889
|
if (ref.why) item.append(makeWorkspaceElement(doc, 'span', { text: `理由: ${ref.why}` }));
|
|
850
890
|
if (ref.usage) item.append(makeWorkspaceElement(doc, 'span', { text: `用途: ${ref.usage}` }));
|
|
851
891
|
const alternateLabels = (ref.labels ?? []).filter((label) => label && label !== ref.label);
|
|
@@ -1147,7 +1187,9 @@ function renderRail(doc, rail, row, detail, onClose, options = {}) {
|
|
|
1147
1187
|
rail.append(workspaceDetailEmpty(doc, {
|
|
1148
1188
|
mark: '↗',
|
|
1149
1189
|
title: '履歴を選択',
|
|
1150
|
-
text:
|
|
1190
|
+
text: options.loading
|
|
1191
|
+
? '行を選ぶと、使った参照と行った判断を確認できます。読み込み中の行も、届いたものから選べます。'
|
|
1192
|
+
: '行を選ぶと、使った参照と行った判断を確認できます。',
|
|
1151
1193
|
}));
|
|
1152
1194
|
return;
|
|
1153
1195
|
}
|
|
@@ -1189,43 +1231,108 @@ function safeDocument(explicit) {
|
|
|
1189
1231
|
return doc;
|
|
1190
1232
|
}
|
|
1191
1233
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
if (
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
if (home?.
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1234
|
+
const RESPONSE_ERROR_CODES = new Set([
|
|
1235
|
+
'response_not_object', 'records_missing', 'sections_missing', 'unsupported_status',
|
|
1236
|
+
'coverage_incomplete', 'record_rejected', 'record_invalid',
|
|
1237
|
+
]);
|
|
1238
|
+
|
|
1239
|
+
function publicErrorText(error) {
|
|
1240
|
+
const code = text(error);
|
|
1241
|
+
if (!code) return '';
|
|
1242
|
+
if (RESPONSE_ERROR_CODES.has(code)) return '応答を読み取れませんでした';
|
|
1243
|
+
return '';
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function prefersReducedMotion() {
|
|
1247
|
+
try {
|
|
1248
|
+
return globalThis.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
|
|
1249
|
+
} catch {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function sourceDetail(doc, home) {
|
|
1255
|
+
const details = makeWorkspaceElement(doc, 'details', { className: 'bb-jh-source-detail' });
|
|
1256
|
+
details.append(makeWorkspaceElement(doc, 'summary', { text: '取得元の詳細' }));
|
|
1257
|
+
const body = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-source-detail-body' });
|
|
1258
|
+
const storage = home?.coverage?.storage;
|
|
1259
|
+
if (storage) body.append(makeWorkspaceElement(doc, 'p', { text: `保存先: ${normalStorageLabel(storage)}` }));
|
|
1260
|
+
const sources = Array.isArray(home?.coverage?.sources) ? home.coverage.sources : [];
|
|
1261
|
+
for (const source of sources) {
|
|
1262
|
+
if (!source?.reason) continue;
|
|
1263
|
+
body.append(makeWorkspaceElement(doc, 'p', { text: `理由コード: ${source.reason}` }));
|
|
1264
|
+
}
|
|
1265
|
+
if (home?.coverage?.reason && !sources.some((source) => source?.reason === home.coverage.reason)) {
|
|
1266
|
+
body.append(makeWorkspaceElement(doc, 'p', { text: `理由コード: ${home.coverage.reason}` }));
|
|
1267
|
+
}
|
|
1268
|
+
const rejected = home?.coverage?.rejected;
|
|
1269
|
+
if (Number.isInteger(rejected) && rejected > 0) {
|
|
1270
|
+
body.append(makeWorkspaceElement(doc, 'p', { text: `読めなかった件数: ${rejected}` }));
|
|
1271
|
+
}
|
|
1272
|
+
if (text(home?.coverage?.reason) === 'journal_scan_capped') {
|
|
1273
|
+
body.append(makeWorkspaceElement(doc, 'p', { text: '記録が多いため、新しい順に上限まで読みました。古い判断が抜けている可能性があります。' }));
|
|
1274
|
+
}
|
|
1275
|
+
details.append(body);
|
|
1276
|
+
return details;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function sourceChips(doc, sources) {
|
|
1280
|
+
const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-banner-sources' });
|
|
1281
|
+
for (const source of sources) wrap.append(makeWorkspaceElement(doc, 'span', { text: sourceChipText(source) }));
|
|
1282
|
+
return wrap;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function historyBanner(doc, { label, tone, role, sources, home, note, reload }) {
|
|
1286
|
+
const banner = workspaceErrorCard(doc, {
|
|
1287
|
+
title: label,
|
|
1288
|
+
tone,
|
|
1289
|
+
message: note || '取得できた記録だけを表示しています。全体の取得状態は通知の詳細で確認できます。',
|
|
1228
1290
|
});
|
|
1291
|
+
banner.className += ` bb-jh-banner is-${tone}`;
|
|
1292
|
+
banner.setAttribute('role', role ?? 'status');
|
|
1293
|
+
const details = findFirst(banner, node => node.tagName === 'DETAILS');
|
|
1294
|
+
if (sources?.length) details.append(sourceChips(doc, sources));
|
|
1295
|
+
if (home) details.append(sourceDetail(doc, home));
|
|
1296
|
+
if (reload) banner.append(reload);
|
|
1297
|
+
return banner;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function statusNotice(doc, phase, error, home, onRetry, { loading = false, slow = false, arrived = null } = {}) {
|
|
1301
|
+
const reload = workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry, attrs: { 'data-action': 'reload' } });
|
|
1302
|
+
if (phase === 'error' || home?.status === 'error' || home?.status === 'invalid') {
|
|
1303
|
+
const detail = publicErrorText(error);
|
|
1304
|
+
return historyBanner(doc, {
|
|
1305
|
+
label: '取得できません',
|
|
1306
|
+
tone: 'danger',
|
|
1307
|
+
role: 'alert',
|
|
1308
|
+
note: `判断履歴を取得できませんでした。データが0件とは確認できていません。${detail ? ` ${detail}` : ''}`,
|
|
1309
|
+
home: error ? { coverage: { reason: text(error) } } : home,
|
|
1310
|
+
reload,
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
if (home?.status === 'unavailable') {
|
|
1314
|
+
return historyBanner(doc, {
|
|
1315
|
+
label: '取得できません',
|
|
1316
|
+
tone: 'danger',
|
|
1317
|
+
role: 'alert',
|
|
1318
|
+
sources: home.coverage?.sources,
|
|
1319
|
+
home,
|
|
1320
|
+
note: '判断の記録に接続できません。データが0件とは確認できていません。',
|
|
1321
|
+
reload,
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
if (loading) {
|
|
1325
|
+
return workspaceLoadingCard(doc, { title: 'ロード中' });
|
|
1326
|
+
}
|
|
1327
|
+
if (home?.status === 'partial') {
|
|
1328
|
+
return historyBanner(doc, {
|
|
1329
|
+
label: '一部の記録だけを表示中',
|
|
1330
|
+
tone: 'warning',
|
|
1331
|
+
role: 'status',
|
|
1332
|
+
sources: home.coverage?.sources,
|
|
1333
|
+
home,
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1229
1336
|
return null;
|
|
1230
1337
|
}
|
|
1231
1338
|
|
|
@@ -1255,38 +1362,51 @@ function createSearch(doc, value, onInput) {
|
|
|
1255
1362
|
}
|
|
1256
1363
|
|
|
1257
1364
|
function normalStorageLabel(storage) {
|
|
1258
|
-
if (storage === 'local') return '
|
|
1259
|
-
if (storage === 'server') return '
|
|
1260
|
-
return
|
|
1365
|
+
if (storage === 'local') return 'この端末';
|
|
1366
|
+
if (storage === 'server') return '組織のサーバー';
|
|
1367
|
+
return '接続先不明';
|
|
1261
1368
|
}
|
|
1262
1369
|
|
|
1263
1370
|
function normalReasonLabel(reason) {
|
|
1264
1371
|
const labels = {
|
|
1372
|
+
journal_scan_capped: '読み込み上限まで',
|
|
1373
|
+
source_not_connected: '未接続',
|
|
1265
1374
|
unconnected: '未接続',
|
|
1266
|
-
mana_not_connected: '
|
|
1267
|
-
company_os_native_source_not_connected: '
|
|
1268
|
-
journal_unavailable: '
|
|
1269
|
-
journal_unreadable: '
|
|
1375
|
+
mana_not_connected: '未接続',
|
|
1376
|
+
company_os_native_source_not_connected: '実行履歴は未接続',
|
|
1377
|
+
journal_unavailable: '判断の記録が見つかりません',
|
|
1378
|
+
journal_unreadable: '判断の記録を読み取れません',
|
|
1379
|
+
some_journal_records_unreadable: '一部の記録を読み取れません',
|
|
1380
|
+
project_scope_unverified: 'プロジェクトの範囲を確かめられていません',
|
|
1381
|
+
project_scope_forbidden: 'このプロジェクトを見る権限がありません',
|
|
1382
|
+
journal_not_directory: '記録の保存場所が正しくありません',
|
|
1383
|
+
journal_symlink_rejected: '記録の保存場所が正しくありません',
|
|
1384
|
+
permission_denied: '見る権限がありません',
|
|
1385
|
+
judgment_history_partial: '一部だけ取得',
|
|
1386
|
+
judgment_history_unavailable: '取得できません',
|
|
1387
|
+
judgment_journal_unavailable: '判断の記録に接続できません',
|
|
1270
1388
|
};
|
|
1271
|
-
|
|
1389
|
+
if (!reason) return '';
|
|
1390
|
+
return labels[reason] ?? '理由を確かめられません';
|
|
1272
1391
|
}
|
|
1273
1392
|
|
|
1274
1393
|
function normalSourceStatusLabel(status) {
|
|
1275
1394
|
const labels = {
|
|
1276
|
-
available: '
|
|
1277
|
-
partial: '
|
|
1395
|
+
available: '取得済み',
|
|
1396
|
+
partial: '一部',
|
|
1278
1397
|
unavailable: '未接続',
|
|
1279
1398
|
};
|
|
1280
1399
|
return labels[status] ?? '未確認';
|
|
1281
1400
|
}
|
|
1282
1401
|
|
|
1283
|
-
function
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
}
|
|
1402
|
+
function sourceChipText(source) {
|
|
1403
|
+
const name = NORMAL_ENTRYPOINT_LABELS[normalEntrypoint(source.entrypoint)] ?? '入口不明';
|
|
1404
|
+
if (source?.status === 'pending' || source?.status === 'loading') return `${name}:確認中…`;
|
|
1405
|
+
if (Number.isInteger(source?.retrieved)) return `${name}:${source.retrieved}件まで届きました`;
|
|
1406
|
+
const statusLabel = normalSourceStatusLabel(source.status);
|
|
1407
|
+
const reasonLabel = source.reason ? normalReasonLabel(source.reason) : '';
|
|
1408
|
+
const reason = reasonLabel && reasonLabel !== statusLabel ? `(${reasonLabel})` : '';
|
|
1409
|
+
return `${name}:${statusLabel}${reason}`;
|
|
1290
1410
|
}
|
|
1291
1411
|
|
|
1292
1412
|
function createFilterSelect(doc, { label, value, options, onChange, className = '' }) {
|
|
@@ -1309,12 +1429,12 @@ function createFilterSelect(doc, { label, value, options, onChange, className =
|
|
|
1309
1429
|
function normalFilterControls(doc, home, state, onChange) {
|
|
1310
1430
|
const filters = home?.filters ?? {};
|
|
1311
1431
|
const projects = [...(filters.projects ?? [])];
|
|
1312
|
-
const knownProjects = (home
|
|
1432
|
+
const knownProjects = (home?.records ?? []).map((record) => record.projectCode).filter(Boolean);
|
|
1313
1433
|
for (const project of knownProjects) {
|
|
1314
1434
|
if (!projects.some((option) => option.value === project)) projects.push({ value: project, label: project });
|
|
1315
1435
|
}
|
|
1316
1436
|
const entrypoints = [...(filters.entrypoints ?? [])];
|
|
1317
|
-
const knownEntrypoints = (home
|
|
1437
|
+
const knownEntrypoints = (home?.records ?? []).map((record) => record.entrypoint).filter(Boolean);
|
|
1318
1438
|
for (const entrypoint of [...NORMAL_ENTRYPOINTS, ...knownEntrypoints]) {
|
|
1319
1439
|
if (!entrypoints.some((option) => option.value === entrypoint)) {
|
|
1320
1440
|
entrypoints.push({ value: entrypoint, label: NORMAL_ENTRYPOINT_LABELS[entrypoint] ?? entrypoint });
|
|
@@ -1350,6 +1470,65 @@ function paginationControls(doc, aggregate, onNext) {
|
|
|
1350
1470
|
return wrap;
|
|
1351
1471
|
}
|
|
1352
1472
|
|
|
1473
|
+
function makeEntrypointStatus(doc, row) {
|
|
1474
|
+
const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-entrypoint' });
|
|
1475
|
+
if (row.mode !== 'normal') {
|
|
1476
|
+
wrap.append(makeWorkspaceElement(doc, 'strong', { text: row.delegated ? 'Brainbase' : '本人' }));
|
|
1477
|
+
return wrap;
|
|
1478
|
+
}
|
|
1479
|
+
wrap.append(makeWorkspaceElement(doc, 'strong', { text: NORMAL_ENTRYPOINT_LABELS[row.entrypoint] ?? '入口不明' }));
|
|
1480
|
+
wrap.append(makeWorkspaceElement(doc, 'small', { text: judgmentStatusLabel(row.judgment?.status) }));
|
|
1481
|
+
return wrap;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function skeletonRows(doc, count = 5) {
|
|
1485
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
1486
|
+
key: `skeleton-${index}`,
|
|
1487
|
+
className: 'bb-jh-skeleton-row',
|
|
1488
|
+
cells: [
|
|
1489
|
+
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-date' }),
|
|
1490
|
+
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-decision' }),
|
|
1491
|
+
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-reference' }),
|
|
1492
|
+
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-status' }),
|
|
1493
|
+
],
|
|
1494
|
+
}));
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function summaryLine(doc, aggregate, { loading = false } = {}) {
|
|
1498
|
+
const line = makeWorkspaceElement(doc, 'p', { className: 'bb-jh-summary' });
|
|
1499
|
+
const retrieved = Number.isInteger(aggregate?.retrievedCount) ? aggregate.retrievedCount : (aggregate?.rows?.length ?? 0);
|
|
1500
|
+
const complete = aggregate?.countComplete === true && Number.isInteger(aggregate?.totalCount);
|
|
1501
|
+
const showCount = complete || retrieved > 0;
|
|
1502
|
+
if (showCount) {
|
|
1503
|
+
line.append(makeWorkspaceElement(doc, 'strong', { text: `${complete ? aggregate.totalCount : retrieved}件` }));
|
|
1504
|
+
line.append(makeWorkspaceElement(doc, 'span', { text: ' 取得済み' }));
|
|
1505
|
+
} else {
|
|
1506
|
+
line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
|
|
1507
|
+
line.append(makeWorkspaceElement(doc, 'span', { text: ' 取得済み' }));
|
|
1508
|
+
}
|
|
1509
|
+
const references = loading ? null : (aggregate?.stats?.referenceCount ?? aggregate?.stats?.references);
|
|
1510
|
+
const confirmed = loading ? null : (aggregate?.stats?.confirmedOutcomes ?? aggregate?.stats?.outcomes);
|
|
1511
|
+
line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 使った参照 ' }));
|
|
1512
|
+
if (Number.isInteger(references)) line.append(makeWorkspaceElement(doc, 'strong', { text: `${references}件` }));
|
|
1513
|
+
else line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
|
|
1514
|
+
line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 結果を確認済み ' }));
|
|
1515
|
+
if (Number.isInteger(confirmed)) line.append(makeWorkspaceElement(doc, 'strong', { text: `${confirmed}件` }));
|
|
1516
|
+
else line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
|
|
1517
|
+
return line;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function formatCheckedAt(value) {
|
|
1521
|
+
const date = new Date(value);
|
|
1522
|
+
if (!Number.isFinite(date.getTime())) return '';
|
|
1523
|
+
try {
|
|
1524
|
+
return new Intl.DateTimeFormat('ja-JP', {
|
|
1525
|
+
timeZone: 'Asia/Tokyo', hour: '2-digit', minute: '2-digit', hour12: false,
|
|
1526
|
+
}).format(date);
|
|
1527
|
+
} catch {
|
|
1528
|
+
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1353
1532
|
function makeRows(doc, aggregate, selectedKey, onSelect) {
|
|
1354
1533
|
return aggregate.rows.map((row) => ({
|
|
1355
1534
|
key: row.key,
|
|
@@ -1359,8 +1538,9 @@ function makeRows(doc, aggregate, selectedKey, onSelect) {
|
|
|
1359
1538
|
onSelect,
|
|
1360
1539
|
cells: [
|
|
1361
1540
|
makeWorkspaceElement(doc, 'span', { className: 'bb-jh-date', text: formatDate(row.recordedAt) }),
|
|
1362
|
-
makeReferenceSummary(doc, row.references),
|
|
1363
1541
|
makeDecisionSummary(doc, row),
|
|
1542
|
+
makeReferenceSummary(doc, row.references),
|
|
1543
|
+
makeEntrypointStatus(doc, row),
|
|
1364
1544
|
],
|
|
1365
1545
|
}));
|
|
1366
1546
|
}
|
|
@@ -1376,7 +1556,10 @@ export function createJudgmentHistoryUI({
|
|
|
1376
1556
|
page,
|
|
1377
1557
|
autoLoad = true,
|
|
1378
1558
|
pageHeader = true,
|
|
1559
|
+
notificationRoot = null,
|
|
1379
1560
|
now = new Date(),
|
|
1561
|
+
skeletonDelayMs = 300,
|
|
1562
|
+
slowDelayMs = 10000,
|
|
1380
1563
|
} = {}) {
|
|
1381
1564
|
if (!root) throw new TypeError('root is required');
|
|
1382
1565
|
const doc = safeDocument(explicitDocument);
|
|
@@ -1409,20 +1592,110 @@ export function createJudgmentHistoryUI({
|
|
|
1409
1592
|
focusSearch: false,
|
|
1410
1593
|
lastFocusedKey: null,
|
|
1411
1594
|
pendingDecisionId: null,
|
|
1595
|
+
showSkeleton: false,
|
|
1596
|
+
slow: false,
|
|
1597
|
+
arrived: null,
|
|
1598
|
+
checkedAt: null,
|
|
1599
|
+
skeletonTimer: null,
|
|
1600
|
+
slowTimer: null,
|
|
1412
1601
|
};
|
|
1413
1602
|
|
|
1603
|
+
let filterPopover = null;
|
|
1604
|
+
let filterExpanded = false;
|
|
1605
|
+
let drawer = null;
|
|
1606
|
+
let drawerContent = null;
|
|
1607
|
+
let drawerKey = null;
|
|
1608
|
+
let suppressDrawerClose = false;
|
|
1609
|
+
function disposeDrawer() {
|
|
1610
|
+
suppressDrawerClose = true;
|
|
1611
|
+
drawer?.dispose();
|
|
1612
|
+
drawer = null; drawerContent = null; drawerKey = null;
|
|
1613
|
+
suppressDrawerClose = false;
|
|
1614
|
+
}
|
|
1615
|
+
const nativeDrawer = Boolean(doc.body && typeof doc.createElement('dialog').showModal === 'function');
|
|
1616
|
+
const previousRailHidden = rail?.hidden;
|
|
1617
|
+
if (nativeDrawer && rail) rail.hidden = true;
|
|
1618
|
+
const notifications = makeWorkspaceElement(doc, 'div', {
|
|
1619
|
+
className: 'bb-ws-error-stack bb-jh-notifications', attrs: { 'aria-label': '判断履歴の通知' },
|
|
1620
|
+
});
|
|
1621
|
+
if (notificationRoot) notificationRoot.append(notifications);
|
|
1622
|
+
let loadingCard = null;
|
|
1623
|
+
let dismissLoading = null;
|
|
1624
|
+
let statusCard = null;
|
|
1625
|
+
let statusKey = null;
|
|
1626
|
+
|
|
1627
|
+
function updateNotifications(loading) {
|
|
1628
|
+
if (loading) {
|
|
1629
|
+
dismissLoading?.(); dismissLoading = null;
|
|
1630
|
+
if (!loadingCard) loadingCard = workspaceLoadingCard(doc, { title: 'ロード中' });
|
|
1631
|
+
if (loadingCard.parentNode !== notifications) notifications.append(loadingCard);
|
|
1632
|
+
} else if (loadingCard && !dismissLoading) {
|
|
1633
|
+
dismissLoading = workspaceDismissNotification(loadingCard);
|
|
1634
|
+
}
|
|
1635
|
+
const home = state.home;
|
|
1636
|
+
const key = loading ? null : JSON.stringify([state.phase, state.error, home?.status, home?.coverage]);
|
|
1637
|
+
if (statusKey !== key) {
|
|
1638
|
+
statusCard?.remove?.();
|
|
1639
|
+
// Small host DOM adapters may not implement Element.remove().
|
|
1640
|
+
if (statusCard?.parentNode === notifications && typeof statusCard.remove !== 'function') {
|
|
1641
|
+
notifications.replaceChildren(...notifications.children.filter(node => node !== statusCard));
|
|
1642
|
+
}
|
|
1643
|
+
statusKey = key;
|
|
1644
|
+
statusCard = loading ? null : statusNotice(doc, state.phase, state.error, home, () => void load());
|
|
1645
|
+
if (statusCard) notifications.append(statusCard);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function clearLoadTimers() {
|
|
1650
|
+
if (state.skeletonTimer !== null) {
|
|
1651
|
+
clearTimeout(state.skeletonTimer);
|
|
1652
|
+
state.skeletonTimer = null;
|
|
1653
|
+
}
|
|
1654
|
+
if (state.slowTimer !== null) {
|
|
1655
|
+
clearTimeout(state.slowTimer);
|
|
1656
|
+
state.slowTimer = null;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function scheduleLoadTimers(generation) {
|
|
1661
|
+
clearLoadTimers();
|
|
1662
|
+
const skeletonDelay = Number.isFinite(skeletonDelayMs) ? Math.max(0, skeletonDelayMs) : 300;
|
|
1663
|
+
const slowDelay = Number.isFinite(slowDelayMs) ? Math.max(0, slowDelayMs) : 10000;
|
|
1664
|
+
const stillLoading = () => generation === state.generation && !state.disposed && state.phase === 'loading';
|
|
1665
|
+
if (skeletonDelay === 0) state.showSkeleton = true;
|
|
1666
|
+
else {
|
|
1667
|
+
state.skeletonTimer = setTimeout(() => {
|
|
1668
|
+
if (!stillLoading()) return;
|
|
1669
|
+
state.showSkeleton = true;
|
|
1670
|
+
render();
|
|
1671
|
+
}, skeletonDelay);
|
|
1672
|
+
}
|
|
1673
|
+
if (slowDelay === 0) {
|
|
1674
|
+
state.slow = true;
|
|
1675
|
+
state.showSkeleton = true;
|
|
1676
|
+
} else {
|
|
1677
|
+
state.slowTimer = setTimeout(() => {
|
|
1678
|
+
if (!stillLoading()) return;
|
|
1679
|
+
state.slow = true;
|
|
1680
|
+
state.showSkeleton = true;
|
|
1681
|
+
render();
|
|
1682
|
+
}, slowDelay);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1414
1686
|
function closeSelected() {
|
|
1415
1687
|
if (!state.selectedKey || state.disposed) return false;
|
|
1416
1688
|
state.lastFocusedKey = state.selectedKey;
|
|
1417
1689
|
state.selectedKey = null;
|
|
1418
1690
|
state.detailPhase = 'idle';
|
|
1419
1691
|
state.detailError = null;
|
|
1692
|
+
if (drawer) drawer.close();
|
|
1420
1693
|
render();
|
|
1421
1694
|
return true;
|
|
1422
1695
|
}
|
|
1423
1696
|
|
|
1424
1697
|
function handleKeyDown(event) {
|
|
1425
|
-
if (event?.key === 'Escape') closeSelected();
|
|
1698
|
+
if (event?.key === 'Escape' && !drawer) closeSelected();
|
|
1426
1699
|
}
|
|
1427
1700
|
|
|
1428
1701
|
root.addEventListener?.('keydown', handleKeyDown);
|
|
@@ -1661,84 +1934,130 @@ export function createJudgmentHistoryUI({
|
|
|
1661
1934
|
|
|
1662
1935
|
function render() {
|
|
1663
1936
|
if (state.disposed) return;
|
|
1937
|
+
updateNotifications(state.phase === 'loading');
|
|
1938
|
+
if (state.phase === 'loading' && !state.showSkeleton && !state.arrived) {
|
|
1939
|
+
if (!notificationRoot && notifications.parentNode !== root) root.append(notifications);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
const focusedFilter = filterExpanded && filterPopover
|
|
1943
|
+
? findFirst(filterPopover.panel, node => node === doc.activeElement) : null;
|
|
1944
|
+
const filterFocus = focusedFilter && {
|
|
1945
|
+
label: attributeValue(focusedFilter, 'aria-label'), period: attributeValue(focusedFilter, 'data-period'),
|
|
1946
|
+
};
|
|
1947
|
+
filterPopover?.destroy();
|
|
1948
|
+
filterPopover = null;
|
|
1664
1949
|
const previousSearchInput = findFirst(root, (node) => node.tagName === 'INPUT');
|
|
1665
1950
|
const previousSearchSelection = previousSearchInput && Number.isInteger(previousSearchInput.selectionStart)
|
|
1666
1951
|
? { start: previousSearchInput.selectionStart, end: previousSearchInput.selectionEnd ?? previousSearchInput.selectionStart }
|
|
1667
1952
|
: null;
|
|
1668
1953
|
const previousSearchFocused = previousSearchInput && doc.activeElement === previousSearchInput;
|
|
1669
|
-
const
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1954
|
+
const loading = state.phase === 'loading';
|
|
1955
|
+
const sourceHome = loading ? state.arrived : state.home;
|
|
1956
|
+
const aggregate = loading
|
|
1957
|
+
? (sourceHome
|
|
1958
|
+
? aggregateJudgmentHistory(sourceHome, { now: readNow(), period: state.period, query: state.query })
|
|
1959
|
+
: { status: 'partial', rows: [], visibleCount: 0, stats: {}, coverage: null, countComplete: false, retrievedCount: 0, mode: 'normal' })
|
|
1960
|
+
: (state.aggregate ?? {
|
|
1961
|
+
status: state.home?.status ?? 'unavailable', rows: [], visibleCount: 0, stats: {}, coverage: state.home?.coverage ?? null,
|
|
1962
|
+
});
|
|
1963
|
+
const wrapper = makeWorkspaceElement(doc, 'div', {
|
|
1964
|
+
className: `bb-jh${prefersReducedMotion() ? ' is-reduced-motion' : ''}`,
|
|
1965
|
+
attrs: { 'data-contract-version': JUDGMENT_HISTORY_UI_CONTRACT_VERSION, 'aria-busy': loading ? 'true' : 'false' },
|
|
1966
|
+
});
|
|
1673
1967
|
if (pageHeader) {
|
|
1674
1968
|
const context = currentPage();
|
|
1675
1969
|
wrapper.append(workspacePageHeader(doc, {
|
|
1676
1970
|
crumbs: context.crumbs,
|
|
1677
1971
|
title: '判断の履歴',
|
|
1678
|
-
lead: 'Brainbase
|
|
1972
|
+
lead: '参照した内容と、Brainbaseが代わりに行った判断を見返す',
|
|
1679
1973
|
source: context.source,
|
|
1680
1974
|
}));
|
|
1681
1975
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
if (
|
|
1685
|
-
wrapper.append(workspaceDetailEmpty(doc, { mark: '…', title:
|
|
1976
|
+
if (!notificationRoot) wrapper.append(notifications);
|
|
1977
|
+
const failed = state.phase === 'error' || state.home?.status === 'unavailable' || state.home?.status === 'error' || state.home?.status === 'invalid';
|
|
1978
|
+
if (failed) {
|
|
1979
|
+
wrapper.append(workspaceDetailEmpty(doc, { mark: '…', title: '判断履歴を確認できません', text: '再試行すると最新の状態を確認します。' }));
|
|
1686
1980
|
root.replaceChildren(wrapper);
|
|
1687
|
-
|
|
1981
|
+
if (drawer) disposeDrawer();
|
|
1982
|
+
if (!nativeDrawer) renderRail(doc, rail, null, null);
|
|
1688
1983
|
return;
|
|
1689
1984
|
}
|
|
1690
1985
|
const controls = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-controls' });
|
|
1691
|
-
|
|
1986
|
+
const filterFields = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-filter-fields' });
|
|
1987
|
+
filterFields.append(periodButtons(doc, state.period, (period) => {
|
|
1692
1988
|
state.period = period;
|
|
1693
1989
|
state.selectedKey = null;
|
|
1694
1990
|
state.cursor = null;
|
|
1695
|
-
if (normalMode()) void load({ append: false });
|
|
1991
|
+
if (loading || normalMode()) void load({ append: false });
|
|
1696
1992
|
else {
|
|
1697
1993
|
state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
|
|
1698
1994
|
render();
|
|
1699
1995
|
}
|
|
1700
1996
|
}));
|
|
1701
|
-
|
|
1997
|
+
const showNormalFilters = loading ? sourceHome?.mode !== 'legacy' : normalMode();
|
|
1998
|
+
if (showNormalFilters) filterFields.append(normalFilterControls(doc, sourceHome ?? state.home, state, handleNormalFilterChange));
|
|
1702
1999
|
controls.append(createSearch(doc, state.query, (query) => {
|
|
1703
2000
|
state.query = String(query);
|
|
1704
2001
|
state.focusSearch = true;
|
|
1705
2002
|
state.selectedKey = null;
|
|
1706
|
-
|
|
2003
|
+
const searchable = loading ? sourceHome : state.home;
|
|
2004
|
+
if (searchable) state.aggregate = aggregateJudgmentHistory(searchable, { now: readNow(), period: state.period, query: state.query });
|
|
1707
2005
|
render();
|
|
1708
2006
|
}));
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
})
|
|
2007
|
+
filterPopover = workspaceFilterPopover(doc, {
|
|
2008
|
+
content: filterFields, open: filterExpanded,
|
|
2009
|
+
onToggle: value => { filterExpanded = value; },
|
|
2010
|
+
});
|
|
2011
|
+
controls.append(filterPopover.root);
|
|
2012
|
+
const toolbarStatus = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-toolbar-status' });
|
|
2013
|
+
if (state.checkedAt) {
|
|
2014
|
+
const stamp = formatCheckedAt(state.checkedAt);
|
|
2015
|
+
if (stamp) toolbarStatus.append(makeWorkspaceElement(doc, 'span', {
|
|
2016
|
+
className: 'bb-jh-checked', text: stamp, attrs: { 'aria-label': `${stamp}に確認` },
|
|
2017
|
+
}));
|
|
2018
|
+
}
|
|
2019
|
+
const reloadButton = workspaceButton(doc, {
|
|
2020
|
+
variant: 'quiet', disabled: loading, onClick: () => void load(),
|
|
2021
|
+
attrs: { 'aria-label': '判断履歴を再読み込み', title: '再読み込み', 'data-action': 'reload' },
|
|
2022
|
+
});
|
|
2023
|
+
reloadButton.className += ' bb-jh-refresh';
|
|
2024
|
+
// Text-independent vector keeps the refresh control available in standalone hosts.
|
|
2025
|
+
const refreshMark = doc.createElementNS?.('http://www.w3.org/2000/svg', 'svg')
|
|
2026
|
+
?? makeWorkspaceElement(doc, 'span', { text: '↻' });
|
|
2027
|
+
refreshMark.setAttribute('class', 'bb-jh-refresh-mark'); refreshMark.setAttribute('aria-hidden', 'true');
|
|
2028
|
+
if (doc.createElementNS) {
|
|
2029
|
+
refreshMark.setAttribute('viewBox', '0 0 24 24'); refreshMark.setAttribute('fill', 'none');
|
|
2030
|
+
refreshMark.setAttribute('stroke', 'currentColor'); refreshMark.setAttribute('stroke-width', '1.7');
|
|
2031
|
+
refreshMark.setAttribute('stroke-linecap', 'round'); refreshMark.setAttribute('stroke-linejoin', 'round');
|
|
2032
|
+
const path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
2033
|
+
path.setAttribute('d', 'M20 7v5h-5M4 17v-5h5M6.1 6.1a8 8 0 0 1 13.2 3.2M4.7 14.7a8 8 0 0 0 13.2 3.2');
|
|
2034
|
+
refreshMark.append(path);
|
|
2035
|
+
}
|
|
2036
|
+
reloadButton.append(refreshMark);
|
|
2037
|
+
toolbarStatus.append(reloadButton);
|
|
2038
|
+
controls.append(toolbarStatus);
|
|
1715
2039
|
wrapper.append(controls);
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
className: 'bb-jh-reference-note',
|
|
1719
|
-
text: normalMode()
|
|
1720
|
-
? `保存された判断の理由、当時の参照、案、実行と結果を表示しています。参照の本文は現在の権限で確認できる場合だけ開きます。${normalSourceSummary ? ` 取得元: ${normalSourceSummary}` : ''}`
|
|
1721
|
-
: '判断に残った参照だけを表示しています。実行結果の証拠は参照件数に含めません。',
|
|
1722
|
-
}));
|
|
1723
|
-
const emptyText = state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
|
|
2040
|
+
wrapper.append(summaryLine(doc, aggregate, { loading }));
|
|
2041
|
+
const emptyText = loading ? '' : state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
|
|
1724
2042
|
? 'この期間に取得できた判断履歴はありません。全件数が未確認のため0件とは確認できません。'
|
|
1725
2043
|
: 'この期間の判断履歴はありません。';
|
|
2044
|
+
const dataRows = makeRows(doc, aggregate, state.selectedKey, (key) => {
|
|
2045
|
+
state.lastFocusedKey = key;
|
|
2046
|
+
state.selectedKey = key;
|
|
2047
|
+
state.detailPhase = 'idle';
|
|
2048
|
+
state.detailError = null;
|
|
2049
|
+
render();
|
|
2050
|
+
const selectedRow = aggregate.rows.find((row) => row.key === key);
|
|
2051
|
+
if (selectedRow?.mode === 'normal') void loadDetail(selectedRow);
|
|
2052
|
+
});
|
|
1726
2053
|
wrapper.append(workspaceLedger(doc, {
|
|
1727
2054
|
className: 'bb-jh-ledger',
|
|
1728
2055
|
ariaLabel: '判断履歴',
|
|
1729
|
-
columns: ['日時', '使った参照', '
|
|
1730
|
-
rows:
|
|
1731
|
-
state.lastFocusedKey = key;
|
|
1732
|
-
state.selectedKey = key;
|
|
1733
|
-
state.detailPhase = 'idle';
|
|
1734
|
-
state.detailError = null;
|
|
1735
|
-
render();
|
|
1736
|
-
const selectedRow = aggregate.rows.find((row) => row.key === key);
|
|
1737
|
-
if (selectedRow?.mode === 'normal') void loadDetail(selectedRow);
|
|
1738
|
-
}),
|
|
2056
|
+
columns: ['日時', '行った判断', '使った参照', '入口・状態'],
|
|
2057
|
+
rows: loading ? [...dataRows, ...skeletonRows(doc)] : dataRows,
|
|
1739
2058
|
empty: emptyText,
|
|
1740
2059
|
}));
|
|
1741
|
-
const pagination = paginationControls(doc, aggregate, () => {
|
|
2060
|
+
const pagination = loading ? null : paginationControls(doc, aggregate, () => {
|
|
1742
2061
|
if (aggregate.pagination?.nextCursor) {
|
|
1743
2062
|
state.cursor = aggregate.pagination.nextCursor;
|
|
1744
2063
|
void load({ cursor: state.cursor, append: true });
|
|
@@ -1751,6 +2070,12 @@ export function createJudgmentHistoryUI({
|
|
|
1751
2070
|
replaceNode(nextSearchInput, previousSearchInput);
|
|
1752
2071
|
}
|
|
1753
2072
|
root.replaceChildren(wrapper);
|
|
2073
|
+
if (filterExpanded && filterFocus) {
|
|
2074
|
+
const field = findFirst(filterPopover.panel, node => filterFocus.label
|
|
2075
|
+
? attributeValue(node, 'aria-label') === filterFocus.label
|
|
2076
|
+
: filterFocus.period && attributeValue(node, 'data-period') === filterFocus.period);
|
|
2077
|
+
field?.focus?.();
|
|
2078
|
+
}
|
|
1754
2079
|
if (state.focusSearch || previousSearchFocused) {
|
|
1755
2080
|
const input = findFirst(root, (node) => node.tagName === 'INPUT');
|
|
1756
2081
|
input?.focus?.();
|
|
@@ -1776,8 +2101,38 @@ export function createJudgmentHistoryUI({
|
|
|
1776
2101
|
onCopyReference: (payload) => void copyReference(payload),
|
|
1777
2102
|
}
|
|
1778
2103
|
: {};
|
|
1779
|
-
|
|
1780
|
-
|
|
2104
|
+
if (nativeDrawer) {
|
|
2105
|
+
if (selected) {
|
|
2106
|
+
if (!drawer || drawerKey !== selected.key) {
|
|
2107
|
+
if (drawer) disposeDrawer();
|
|
2108
|
+
drawerKey = selected.key;
|
|
2109
|
+
drawerContent = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-detail' });
|
|
2110
|
+
const selectedKey = selected.key;
|
|
2111
|
+
drawer = workspaceDialog(doc, {
|
|
2112
|
+
title: '判断の詳細', content: drawerContent, presentation: 'drawer',
|
|
2113
|
+
opener: () => findFirst(root, node => attributeValue(node, 'data-key') === selectedKey),
|
|
2114
|
+
onClose: () => {
|
|
2115
|
+
if (suppressDrawerClose) return;
|
|
2116
|
+
drawer = null; drawerContent = null; drawerKey = null;
|
|
2117
|
+
if (state.disposed) return;
|
|
2118
|
+
state.selectedKey = null; state.detailPhase = 'idle'; state.detailError = null;
|
|
2119
|
+
state.lastFocusedKey = selectedKey; render();
|
|
2120
|
+
},
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
const detailFocus = findFirst(drawerContent, node => node === doc.activeElement);
|
|
2124
|
+
const detailLabel = attributeValue(detailFocus, 'aria-label');
|
|
2125
|
+
const detailSelection = Number.isInteger(detailFocus?.selectionStart)
|
|
2126
|
+
? [detailFocus.selectionStart, detailFocus.selectionEnd] : null;
|
|
2127
|
+
renderRail(doc, drawerContent, selected, selectedDetail, null, { ...railOptions, loading });
|
|
2128
|
+
if (detailLabel) {
|
|
2129
|
+
const field = findFirst(drawerContent, node => attributeValue(node, 'aria-label') === detailLabel);
|
|
2130
|
+
field?.focus?.();
|
|
2131
|
+
if (detailSelection) field?.setSelectionRange?.(...detailSelection);
|
|
2132
|
+
}
|
|
2133
|
+
} else if (drawer) drawer.close();
|
|
2134
|
+
} else renderRail(doc, rail, selected, selectedDetail, selected ? closeSelected : null, { ...railOptions, loading });
|
|
2135
|
+
if (state.lastFocusedKey && !drawer) {
|
|
1781
2136
|
const row = findFirst(root, (node) => attributeValue(node, 'data-key') === state.lastFocusedKey);
|
|
1782
2137
|
row?.focus?.();
|
|
1783
2138
|
state.lastFocusedKey = null;
|
|
@@ -1787,26 +2142,39 @@ export function createJudgmentHistoryUI({
|
|
|
1787
2142
|
async function load({ cursor = null, append = false } = {}) {
|
|
1788
2143
|
if (state.disposed) return null;
|
|
1789
2144
|
const generation = ++state.generation;
|
|
1790
|
-
state.phase = 'loading';
|
|
1791
2145
|
state.error = null;
|
|
1792
2146
|
if (!append) {
|
|
2147
|
+
state.phase = 'loading';
|
|
1793
2148
|
state.home = null;
|
|
1794
2149
|
state.aggregate = null;
|
|
2150
|
+
state.arrived = null;
|
|
2151
|
+
state.showSkeleton = false;
|
|
2152
|
+
state.slow = false;
|
|
2153
|
+
scheduleLoadTimers(generation);
|
|
2154
|
+
render();
|
|
1795
2155
|
}
|
|
1796
|
-
render();
|
|
1797
2156
|
if (!request) {
|
|
1798
2157
|
if (generation !== state.generation || state.disposed) return null;
|
|
2158
|
+
clearLoadTimers();
|
|
2159
|
+
state.showSkeleton = false;
|
|
1799
2160
|
state.phase = 'error';
|
|
1800
2161
|
state.error = 'fetch_unavailable';
|
|
1801
2162
|
render();
|
|
1802
2163
|
return null;
|
|
1803
2164
|
}
|
|
1804
2165
|
try {
|
|
2166
|
+
const report = (payload) => {
|
|
2167
|
+
if (generation !== state.generation || state.disposed || state.phase !== 'loading') return;
|
|
2168
|
+
const normalized = normalizeJudgmentHistoryHome(payload);
|
|
2169
|
+
if (!normalized || normalized.status === 'invalid') return;
|
|
2170
|
+
state.arrived = normalized;
|
|
2171
|
+
render();
|
|
2172
|
+
};
|
|
1805
2173
|
let activePath = normalizedBasePath;
|
|
1806
2174
|
let response = await request(historyPath(normalizedBasePath, cursor), {
|
|
1807
2175
|
method: 'GET',
|
|
1808
2176
|
headers: token ? { 'x-brainbase-review-token': token } : undefined,
|
|
1809
|
-
});
|
|
2177
|
+
}, { report });
|
|
1810
2178
|
if (generation !== state.generation || state.disposed) return null;
|
|
1811
2179
|
if (response?.status === 404 && legacyBasePath && !append && !state.project && !state.entrypoint && !state.cursor) {
|
|
1812
2180
|
activePath = legacyBasePath;
|
|
@@ -1837,6 +2205,11 @@ export function createJudgmentHistoryUI({
|
|
|
1837
2205
|
state.entrypoint = state.entrypoint ?? state.home.filters?.entrypoint ?? null;
|
|
1838
2206
|
state.cursor = state.home.pagination?.nextCursor ?? null;
|
|
1839
2207
|
}
|
|
2208
|
+
clearLoadTimers();
|
|
2209
|
+
state.showSkeleton = false;
|
|
2210
|
+
state.slow = false;
|
|
2211
|
+
state.arrived = null;
|
|
2212
|
+
state.checkedAt = readNow();
|
|
1840
2213
|
state.phase = 'ready';
|
|
1841
2214
|
state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
|
|
1842
2215
|
if (state.pendingDecisionId) {
|
|
@@ -1848,6 +2221,9 @@ export function createJudgmentHistoryUI({
|
|
|
1848
2221
|
return state.aggregate;
|
|
1849
2222
|
} catch (error) {
|
|
1850
2223
|
if (generation !== state.generation || state.disposed) return null;
|
|
2224
|
+
clearLoadTimers();
|
|
2225
|
+
state.showSkeleton = false;
|
|
2226
|
+
state.slow = false;
|
|
1851
2227
|
state.phase = 'error';
|
|
1852
2228
|
state.error = error instanceof Error ? error.message : String(error);
|
|
1853
2229
|
render();
|
|
@@ -1875,10 +2251,16 @@ export function createJudgmentHistoryUI({
|
|
|
1875
2251
|
if (state.disposed) return;
|
|
1876
2252
|
state.disposed = true;
|
|
1877
2253
|
state.generation += 1;
|
|
2254
|
+
clearLoadTimers();
|
|
2255
|
+
filterPopover?.destroy();
|
|
2256
|
+
dismissLoading?.();
|
|
2257
|
+
notifications.remove?.();
|
|
2258
|
+
disposeDrawer();
|
|
1878
2259
|
root.removeEventListener?.('keydown', handleKeyDown);
|
|
1879
2260
|
rail?.removeEventListener?.('keydown', handleKeyDown);
|
|
1880
2261
|
root.replaceChildren();
|
|
1881
2262
|
rail?.replaceChildren();
|
|
2263
|
+
if (nativeDrawer && rail) rail.hidden = previousRailHidden;
|
|
1882
2264
|
}
|
|
1883
2265
|
|
|
1884
2266
|
const controller = { load, render, dispose, openDecision, get state() { return { ...state }; } };
|