@takagaki/cortex-decisions-viewer 0.12.200 → 0.12.210
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 +4 -2
- package/dist/build.js +64 -83
- package/dist/canonical-evidence.js +11 -191
- package/dist/change-log.js +1 -1
- package/dist/config-flow.js +462 -0
- package/dist/config-mode.js +57 -0
- package/dist/confirmed.js +2 -2
- package/dist/engine/decision-v2-fields.js +188 -0
- package/dist/engine/ref-tokens.js +276 -0
- package/dist/engine/related-materials-field.js +148 -0
- package/dist/engine/supersedes-index.js +257 -0
- package/dist/google-confirm-page.js +14 -3
- package/dist/ledger-dir.js +1 -0
- package/dist/memory-dir.js +40 -0
- package/dist/rederivation.js +17 -8
- package/dist/render.js +896 -74
- package/dist/story.js +13 -9
- package/dist/supersedes.js +12 -110
- package/dist/view-dir.js +100 -2
- package/package.json +2 -2
package/dist/render.js
CHANGED
|
@@ -85,6 +85,7 @@ exports.connectorReadRows = connectorReadRows;
|
|
|
85
85
|
exports.capabilityPipeRe = capabilityPipeRe;
|
|
86
86
|
exports.capabilityWarnSource = capabilityWarnSource;
|
|
87
87
|
exports.renderConnectionMap = renderConnectionMap;
|
|
88
|
+
const memory_dir_1 = require("./memory-dir");
|
|
88
89
|
const node_crypto_1 = require("node:crypto");
|
|
89
90
|
const google_chat_guide_images_1 = require("./google-chat-guide-images");
|
|
90
91
|
const meet_guide_images_1 = require("./meet-guide-images");
|
|
@@ -104,6 +105,8 @@ const external_id_1 = require("./external-id");
|
|
|
104
105
|
const permissions_data_1 = require("./permissions-data");
|
|
105
106
|
// Drive・Google Chat の追加の確認ページ(#1357)。本体からの開き方と、メッセージの型の綴りだけ引く
|
|
106
107
|
const google_confirm_page_1 = require("./google-confirm-page");
|
|
108
|
+
// 設定の表の案件(mode: "table")の保存の流れ(cortex-engine #1911 便 DS-8a-1)。判定の実体はあちら——ここは流し込みと配線だけ
|
|
109
|
+
const config_flow_1 = require("./config-flow");
|
|
107
110
|
// 操作タブの仮置き(#388)。判定の実体はあちら——ここは流し込みと配線だけ
|
|
108
111
|
const pending_ops_1 = require("./pending-ops");
|
|
109
112
|
/**
|
|
@@ -124,7 +127,11 @@ function renderSite(site, assets) {
|
|
|
124
127
|
// (claudeCodeUrl を直に呼ぶと、引数を1つ落とした側だけが違う先を開く)
|
|
125
128
|
const askUrl = askAiUrl(site);
|
|
126
129
|
// ※ 投入フォーム設定 site.intake は build.ts が --intake-url/--intake-token/--project-key から付与する。
|
|
127
|
-
|
|
130
|
+
// 設定の置き場のヒント(DS-8a-1)は知っている 2 値だけ通す。**与えないときは site をそのまま**(file の出力を変えない)
|
|
131
|
+
const siteOut = "configMode" in site && site.configMode !== "table" && site.configMode !== "file"
|
|
132
|
+
? (({ configMode: _drop, ...rest }) => rest)(site)
|
|
133
|
+
: site;
|
|
134
|
+
const dataJson = JSON.stringify(siteOut).replace(/</g, "\\u003c");
|
|
128
135
|
const safeTitle = escapeHtml(site.title);
|
|
129
136
|
// 接続マップ(操作タブ最上部のSVG図)。ビルド時に生成し<template>で埋め込む。fleet情報が無ければ出さない
|
|
130
137
|
const connMap = renderConnectionMap(site);
|
|
@@ -4501,6 +4508,7 @@ function renderConnectionMap(site) {
|
|
|
4501
4508
|
*/
|
|
4502
4509
|
function buildTutorialData(site, repoSlug) {
|
|
4503
4510
|
const c4 = tutorialChallenge4Prompt(site.decisions);
|
|
4511
|
+
const memDir = site.memoryDir === "Memory" ? "Memory" : "Cortex";
|
|
4504
4512
|
const specs = [
|
|
4505
4513
|
{
|
|
4506
4514
|
title: "全体像を知る",
|
|
@@ -4549,7 +4557,7 @@ function buildTutorialData(site, repoSlug) {
|
|
|
4549
4557
|
specs.push({
|
|
4550
4558
|
title: "自分の名前を変更してみよう",
|
|
4551
4559
|
desc: "関係者一覧の自分のレコードをAIに直してもらう",
|
|
4552
|
-
prompt: tutMemberPrompt(!!site.memoryV2),
|
|
4560
|
+
prompt: (0, memory_dir_1.promptInMemoryDir)(tutMemberPrompt(!!site.memoryV2), memDir),
|
|
4553
4561
|
});
|
|
4554
4562
|
}
|
|
4555
4563
|
// Backlogに記票する体験(課題管理にBacklogを使う案件のみ出す)
|
|
@@ -4559,7 +4567,7 @@ function buildTutorialData(site, repoSlug) {
|
|
|
4559
4567
|
specs.push({
|
|
4560
4568
|
title: "Backlogに記票してみよう",
|
|
4561
4569
|
desc: "AIから自分の名義で課題にコメントする",
|
|
4562
|
-
prompt: TUT_BACKLOG_PROMPT,
|
|
4570
|
+
prompt: (0, memory_dir_1.promptInMemoryDir)(memDir === "Memory" ? TUT_BACKLOG_PROMPT_REBUILT : TUT_BACKLOG_PROMPT, memDir),
|
|
4563
4571
|
});
|
|
4564
4572
|
}
|
|
4565
4573
|
// デザインの指摘を読む体験(デザインを同期している案件のみ出す)。
|
|
@@ -4570,7 +4578,7 @@ function buildTutorialData(site, repoSlug) {
|
|
|
4570
4578
|
specs.push({
|
|
4571
4579
|
title: "デザインを読んでみよう",
|
|
4572
4580
|
desc: "Figmaの指摘から1画面を選んで理解する",
|
|
4573
|
-
prompt: TUT_DESIGN_PROMPT,
|
|
4581
|
+
prompt: memDir === "Memory" ? TUT_DESIGN_PROMPT_REBUILT : TUT_DESIGN_PROMPT,
|
|
4574
4582
|
});
|
|
4575
4583
|
}
|
|
4576
4584
|
// 完了をSlackに知らせる体験(チャットにSlackを宣言している案件のみ出す。最終チャレンジ)
|
|
@@ -4579,7 +4587,7 @@ function buildTutorialData(site, repoSlug) {
|
|
|
4579
4587
|
specs.push({
|
|
4580
4588
|
title: "Slackに完了を知らせよう",
|
|
4581
4589
|
desc: "チュートリアルの完了をプロジェクトのチャンネルに投稿する",
|
|
4582
|
-
prompt: TUT_SLACK_PROMPT,
|
|
4590
|
+
prompt: (0, memory_dir_1.promptInMemoryDir)(memDir === "Memory" ? TUT_SLACK_PROMPT_REBUILT : TUT_SLACK_PROMPT, memDir),
|
|
4583
4591
|
});
|
|
4584
4592
|
}
|
|
4585
4593
|
return {
|
|
@@ -4662,6 +4670,46 @@ const TUT_SLACK_PROMPT = [
|
|
|
4662
4670
|
"",
|
|
4663
4671
|
"投稿できる道具(Slack のコネクタ)は道具の検索で探してください。見つからない・認証で失敗するときは、操作タブの「Claude から原文を読む」でつなぐよう案内して止まってください。同じ働きの道具が未認証で出ていれば、その認証(/mcp)を先に案内してください。",
|
|
4664
4672
|
].join("\n");
|
|
4673
|
+
/*
|
|
4674
|
+
* 組み直した案件(`Memory/`・`memDir === "Memory"`)の練習用の指示文(cortex-engine #1911 便 TL・X6)。
|
|
4675
|
+
* 登録簿の指し先は、雛形の案内文(engine の `scaffold/repo/CLAUDE.md`・便 SC)と同じ: `Memory/Index.md` の「読んでいる場」
|
|
4676
|
+
* (接続の一覧と変更はビューアの設定の画面)。組み直した案件には `sources.json`・`チャット/channels.json`・`課題管理/…`・`デザイン/指摘/` が無いので、
|
|
4677
|
+
* それらを指す今までの文は使わない。🔴 今までの案件(`Cortex/`)の文は 1 バイトも変えない(下の元の定数のまま)。
|
|
4678
|
+
*/
|
|
4679
|
+
const TUT_BACKLOG_PROMPT_REBUILT = [
|
|
4680
|
+
"Backlogの課題に、わたしの名義でコメントを1件投稿してください。Backlogの画面を開かずにAIから記票する練習です。",
|
|
4681
|
+
"課題を取れる道具(Backlog のコネクタ)を道具の検索で探してください。同じ働きの道具が2つ以上あれば、Memory/Index.md の「読んでいる場」の課題管理のスペース(接続の一覧は、ビューアの設定の画面で見られます)と同じスペースを読むものを選びます。",
|
|
4682
|
+
"課題はその道具で一覧して、どの課題に何を書くかをわたしに質問してください。",
|
|
4683
|
+
"",
|
|
4684
|
+
"投稿はその道具で行います(あなた本人の名義)。Backlog上の名義を先に伝えてください。",
|
|
4685
|
+
"道具の検索で探しても見つからない・認証で失敗するときは、つながっていません。操作タブの「Claude から原文を読む」でつなぐよう案内して止まってください。同じ働きの道具が未認証で出ていれば、その認証(/mcp)を先に案内してください。",
|
|
4686
|
+
"",
|
|
4687
|
+
"環境変数のAPIキーで Backlog のAPIを直接呼ぶことはしないでください(キーの持ち主の名義で記録されてしまいます)。",
|
|
4688
|
+
"",
|
|
4689
|
+
"投稿する前に、課題キー・本文・経路・名義を提示して承認を得てください。投稿後は投稿先のURLを報告してください。",
|
|
4690
|
+
"今回はコメントの追加だけです。課題の削除・クローズ・属性変更はしないでください。",
|
|
4691
|
+
].join("\n");
|
|
4692
|
+
const TUT_DESIGN_PROMPT_REBUILT = [
|
|
4693
|
+
"Memory/Index.md の「読んでいる場」に載っている Figma のファイルから、指摘(コメント)を1つ選び、それがどの画面への指摘なのかを説明してください。コメントも画面もリポジトリには取り込んでいません。デザインを開ける道具(Figma のコネクタ)を道具の検索で探して、そこで読んでください。",
|
|
4694
|
+
"見つからない・認証で失敗するときは、操作タブの「Claude から原文を読む」でつなぐよう案内し、Figma のファイルのリンクを示すところで止めてください。同じ働きの道具が未認証で出ていれば、その認証(/mcp)を先に案内してください。",
|
|
4695
|
+
"どの指摘・画面が見たいかは、まずわたしに質問してください(希望が無ければ未解決の指摘をあなたが選んでかまいません)。",
|
|
4696
|
+
"画面の役割・主な要素と、指摘が何を求めているかを説明し、関連する決定・用語・課題があれば安定IDをたどって補足し、最後にFigmaで実物を開くリンクを提示してください。",
|
|
4697
|
+
"読むだけの練習です。Figma にコメントも書き込まないでください。",
|
|
4698
|
+
].join("\n");
|
|
4699
|
+
const TUT_SLACK_PROMPT_REBUILT = [
|
|
4700
|
+
"Cortex のチュートリアルを完了したことを、このプロジェクトの Slack チャンネルに投稿してください。最後のステップです。",
|
|
4701
|
+
"",
|
|
4702
|
+
"1. Memory/Index.md の「読んでいる場」に載っている Slack のチャンネルから投稿先を選ぶ(複数あれば名前から判断し、迷ったらわたしに聞く。接続の一覧は、ビューアの設定の画面で見られます)",
|
|
4703
|
+
"2. 次の雛形で本文を作る({名前} は Memory/Members/records/ にあるわたしのレコードの表記に合わせる)",
|
|
4704
|
+
"",
|
|
4705
|
+
"🎉 {名前} さんが Cortex のチュートリアルを完了しました",
|
|
4706
|
+
"できるようになったこと: Gold層からプロジェクトを把握する・自分の情報を更新する・Backlogに記票する・資料を投入する",
|
|
4707
|
+
"使い方の質問はこのチャンネルへどうぞ",
|
|
4708
|
+
"",
|
|
4709
|
+
"3. 投稿先と本文をプレビューして承認を得てから投稿し、メッセージのリンクを報告する",
|
|
4710
|
+
"",
|
|
4711
|
+
"投稿できる道具(Slack のコネクタ)は道具の検索で探してください。見つからない・認証で失敗するときは、操作タブの「Claude から原文を読む」でつなぐよう案内して止まってください。同じ働きの道具が未認証で出ていれば、その認証(/mcp)を先に案内してください。",
|
|
4712
|
+
].join("\n");
|
|
4665
4713
|
const CSS = `
|
|
4666
4714
|
:root {
|
|
4667
4715
|
--bg: #f6f7f9;
|
|
@@ -4763,10 +4811,12 @@ a:hover { text-decoration: underline; }
|
|
|
4763
4811
|
/* ---- tabs ---- */
|
|
4764
4812
|
/* 🔴 幅が足りないときはタブの列だけが横にスクロールする(ページ全体は横スクロールさせない・分類B 2026-09-30)。
|
|
4765
4813
|
overflow-x を visible 以外にすると overflow-y も auto 相当になり、.tab の margin-bottom: -1px と
|
|
4766
|
-
border-bottom の重なり(下の下線用の1px)が縦のスクロールバーとして見えかねないので、overflow-y は明示的に止める
|
|
4814
|
+
border-bottom の重なり(下の下線用の1px)が縦のスクロールバーとして見えかねないので、overflow-y は明示的に止める
|
|
4815
|
+
🔴 タブは本文の枠の左端から右端まで均等の幅で並べる(flex: 1 0 0・本数は決め打ちしない)。min-width: max-content で
|
|
4816
|
+
文字より狭くはならないので、狭い画面では均等割りをやめて今までどおり横に流れる */
|
|
4767
4817
|
.tabs { display: flex; gap: 6px; margin-bottom: 18px; border-bottom: 1px solid var(--border); overflow-x: auto; overflow-y: hidden; }
|
|
4768
4818
|
.tab {
|
|
4769
|
-
flex: 0 0
|
|
4819
|
+
flex: 1 0 0; min-width: max-content; text-align: center; white-space: nowrap;
|
|
4770
4820
|
padding: 8px 16px; font-size: 14px; font-weight: 600; color: var(--muted);
|
|
4771
4821
|
border: none; background: none; cursor: pointer;
|
|
4772
4822
|
border-bottom: 2px solid transparent; margin-bottom: -1px;
|
|
@@ -6165,6 +6215,8 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
|
|
|
6165
6215
|
const CLIENT_JS = `
|
|
6166
6216
|
(function () {
|
|
6167
6217
|
var SITE = JSON.parse(document.getElementById("site-data").textContent);
|
|
6218
|
+
// 記憶の置き場の名前(組み直した案件だけ site-data に印がある。無ければ今までの Cortex・便 T-12)
|
|
6219
|
+
var MEM_DIR = SITE.memoryDir === "Memory" ? "Memory" : "Cortex";
|
|
6168
6220
|
var ADAPTER_ICONS = ${JSON.stringify(ADAPTER_ICONS)};
|
|
6169
6221
|
|
|
6170
6222
|
// ---- intake(操作の受け口)への共通の呼び出し(#349)----
|
|
@@ -6316,6 +6368,10 @@ const CLIENT_JS = `
|
|
|
6316
6368
|
gcPending.discard();
|
|
6317
6369
|
}
|
|
6318
6370
|
var intake = SITE.intake;
|
|
6371
|
+
// 設定の表の案件の追加(便 DS-8b): 窓が意図の形(baseRev・req つき)で保存する。req は 1 回の確認のあいだ同じ値(押し直しは同じ意図)
|
|
6372
|
+
var tableAdd = opts.mode === "add" && viewMode() === "table";
|
|
6373
|
+
if (tableAdd && !CONFIG) { resolve({ ok: false, message: "設定を読み込めていません。少し待ってからもう一度押してください" }); return; }
|
|
6374
|
+
var intent = tableAdd ? { baseRev: CONFIG.rev, req: configNewReq(Date.now(), configRandomHex()) } : null;
|
|
6319
6375
|
var flow = gcRandomFlow();
|
|
6320
6376
|
var win = window.open(new URL(GC_PAGE, location.href).href, "cortex-google-confirm", gcWindowFeatures());
|
|
6321
6377
|
if (!win) { resolve({ ok: false, message: "ポップアップが開けませんでした。ブラウザのポップアップを許可して、もう一度押してください" }); return; }
|
|
@@ -6348,7 +6404,7 @@ const CLIENT_JS = `
|
|
|
6348
6404
|
}
|
|
6349
6405
|
function sendStart(target) {
|
|
6350
6406
|
// 名乗り券は、Google で時間がかかっても切れないよう、払い出すたびに作り直す
|
|
6351
|
-
|
|
6407
|
+
var start = {
|
|
6352
6408
|
type: GC_MSG.start,
|
|
6353
6409
|
flow: flow,
|
|
6354
6410
|
intake: { url: intake.url, token: intake.token },
|
|
@@ -6357,7 +6413,9 @@ const CLIENT_JS = `
|
|
|
6357
6413
|
mode: opts.mode,
|
|
6358
6414
|
url: opts.url || "",
|
|
6359
6415
|
id: opts.id || "",
|
|
6360
|
-
}
|
|
6416
|
+
};
|
|
6417
|
+
if (intent) start.intent = intent;
|
|
6418
|
+
target(start);
|
|
6361
6419
|
}
|
|
6362
6420
|
function handle(data, replyTo) {
|
|
6363
6421
|
if (done || !data || typeof data !== "object" || data.flow !== flow) return;
|
|
@@ -6633,6 +6691,30 @@ ${pending_ops_1.pendingCapsOf.toString()}
|
|
|
6633
6691
|
${pending_ops_1.pendingRowNote.toString()}
|
|
6634
6692
|
${pending_ops_1.pendingCountDelta.toString()}
|
|
6635
6693
|
${pending_ops_1.pendingIssuesRef.toString()}
|
|
6694
|
+
// 設定の表の案件の保存(DS-8a-1)。**判定の実体は config-flow.ts**——仮置きとは部品を共有しない
|
|
6695
|
+
${config_flow_1.configModeOf.toString()}
|
|
6696
|
+
${config_flow_1.configViewOk.toString()}
|
|
6697
|
+
${config_flow_1.configViewOfState.toString()}
|
|
6698
|
+
${config_flow_1.configRevAccepts.toString()}
|
|
6699
|
+
${config_flow_1.configSameWorkspace.toString()}
|
|
6700
|
+
${config_flow_1.configNightReadOf.toString()}
|
|
6701
|
+
${config_flow_1.configCapRowCount.toString()}
|
|
6702
|
+
${config_flow_1.configRowKey.toString()}
|
|
6703
|
+
${config_flow_1.configSourceRows.toString()}
|
|
6704
|
+
${config_flow_1.configFindSource.toString()}
|
|
6705
|
+
${config_flow_1.configNotifyOf.toString()}
|
|
6706
|
+
${config_flow_1.configIntentOf.toString()}
|
|
6707
|
+
${config_flow_1.configIntentSig.toString()}
|
|
6708
|
+
${config_flow_1.configReqOk.toString()}
|
|
6709
|
+
${config_flow_1.configNewReq.toString()}
|
|
6710
|
+
${config_flow_1.configClassify.toString()}
|
|
6711
|
+
${config_flow_1.configUnlinkedNotify.toString()}
|
|
6712
|
+
${config_flow_1.configDriveFolderId.toString()}
|
|
6713
|
+
${config_flow_1.configAddTarget.toString()}
|
|
6714
|
+
${config_flow_1.configSourceUrl.toString()}
|
|
6715
|
+
${config_flow_1.configBacklogLabel.toString()}
|
|
6716
|
+
${config_flow_1.configRemoveConfirmLines.toString()}
|
|
6717
|
+
${config_flow_1.configDuplicateLines.toString()}
|
|
6636
6718
|
var STATE_STATUSES = ${JSON.stringify(STATE_STATUSES)};
|
|
6637
6719
|
var STATE_ROW_FIELDS = ${JSON.stringify(STATE_ROW_FIELDS)};
|
|
6638
6720
|
// 行の runUrl の形と、出どころの語(#560)。**正規表現は JSON にできない**ので
|
|
@@ -7284,7 +7366,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
7284
7366
|
|
|
7285
7367
|
// 受け口が受け付けるパスの形。**サーバ側にも同じ門がある**(logic.mjs の isPromotablePath)。
|
|
7286
7368
|
// ここは「押せないボタンを出さない」ための出し分けで、**守りはサーバ側**
|
|
7287
|
-
var PROMOTABLE_PATH = /^Cortex\\/(Decisions|Glossary|Members|Rules)\\/records\\/[^/]+\\.md$/;
|
|
7369
|
+
var PROMOTABLE_PATH = /^(?:Cortex|Memory)\\/(Decisions|Glossary|Members|Rules)\\/records\\/[^/]+\\.md$/;
|
|
7288
7370
|
function promotableRecordPath(rec) {
|
|
7289
7371
|
if (!SITE.intake) return "";
|
|
7290
7372
|
var p = repoPathOf(rec);
|
|
@@ -7901,8 +7983,8 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
7901
7983
|
function confirmPathOf(rec) {
|
|
7902
7984
|
var p = repoPathOf(rec);
|
|
7903
7985
|
if (p) return p;
|
|
7904
|
-
if (rec && rec._kind === "decision") return "
|
|
7905
|
-
if (rec && rec._kind === "generic" && rec._section) return "
|
|
7986
|
+
if (rec && rec._kind === "decision") return MEM_DIR + "/Decisions/";
|
|
7987
|
+
if (rec && rec._kind === "generic" && rec._section) return MEM_DIR + "/" + rec._section + "/";
|
|
7906
7988
|
return null;
|
|
7907
7989
|
}
|
|
7908
7990
|
/**
|
|
@@ -7940,7 +8022,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
7940
8022
|
// Home(案件の入口)のAI編集。Gold層の4区画と違い規約READMEが無いので、Home自身を読ませて直させる。
|
|
7941
8023
|
// 「このプロジェクトについて」「ゴール」はAIが判断の方向を決める材料なので、未記入なら埋めることを促す。
|
|
7942
8024
|
var HOME_EDIT_PROMPT = [
|
|
7943
|
-
"
|
|
8025
|
+
MEM_DIR + "/Home.md を読んでから、このプロジェクトの入口ページを修正してください。",
|
|
7944
8026
|
"何を直すかは、まずわたしに質問してください。「このプロジェクトについて」「ゴール」が未記入なら、そこを埋めるところから始めてください。",
|
|
7945
8027
|
"先頭の frontmatter(識別カードと設定)は触らないでください。日付・進捗・実績値は書かないでください(変わるものの正本は課題管理ツール等の外部にあります)。",
|
|
7946
8028
|
"このサイトの使い方の説明は書かないでください(説明は画面の側が持っています)。",
|
|
@@ -7955,7 +8037,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
7955
8037
|
* 「何をする案件か・誰のためか・どうなったら成功か」を必ず聞かせる。
|
|
7956
8038
|
*/
|
|
7957
8039
|
var HOME_WRITE_PROMPT = [
|
|
7958
|
-
"
|
|
8040
|
+
MEM_DIR + "/Home.md を読んでから、「このプロジェクトについて」「ゴール」の2つの節を埋めてください。",
|
|
7959
8041
|
"まず、わたしに質問して内容を聞き出してください。少なくとも次の3つを確かめてください: 何をするプロジェクトか/誰のためのプロジェクトか(顧客・利用者は誰か)/どうなったら成功と言えるか。",
|
|
7960
8042
|
"聞き出した内容をもとに、2つの節を具体的な散文で書いてください。雛形の {{…}} は残さず、実際の内容に置き換えてください。",
|
|
7961
8043
|
"この2つの節は、毎晩のAI生成(決定事項・用語集・関係者・ルールの抽出)がプロジェクトを理解するために読む場所です。抽象的な言葉ではなく、このプロジェクトに固有の具体で書いてください。",
|
|
@@ -7971,9 +8053,9 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
7971
8053
|
var GOLD_SKILL = {
|
|
7972
8054
|
"Decisions": {
|
|
7973
8055
|
noun: "決定",
|
|
7974
|
-
dir: "
|
|
8056
|
+
dir: MEM_DIR + "/Decisions",
|
|
7975
8057
|
add: [
|
|
7976
|
-
"
|
|
8058
|
+
MEM_DIR + "/Decisions/README.md と " + MEM_DIR + "/Decisions/template.md を読んで記録の規約を把握してから、決定事項を1件記録してください。",
|
|
7977
8059
|
"何を記録するかは、まずわたしに質問してください。",
|
|
7978
8060
|
"書き込む前に、作成するファイルの内容を提示して承認を得てください。",
|
|
7979
8061
|
].join("\\n"),
|
|
@@ -7989,9 +8071,9 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
7989
8071
|
},
|
|
7990
8072
|
"Glossary": {
|
|
7991
8073
|
noun: "用語",
|
|
7992
|
-
dir: "
|
|
8074
|
+
dir: MEM_DIR + "/Glossary",
|
|
7993
8075
|
add: [
|
|
7994
|
-
"
|
|
8076
|
+
MEM_DIR + "/Glossary/README.md と " + MEM_DIR + "/Glossary/template.md を読んで登録の規約を把握してから、用語を1件登録してください。",
|
|
7995
8077
|
"何を登録するかは、まずわたしに質問してください。",
|
|
7996
8078
|
"書き込む前に、作成するファイルの内容を提示して承認を得てください。",
|
|
7997
8079
|
].join("\\n"),
|
|
@@ -8006,9 +8088,9 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
8006
8088
|
},
|
|
8007
8089
|
"Members": {
|
|
8008
8090
|
noun: "関係者",
|
|
8009
|
-
dir: "
|
|
8091
|
+
dir: MEM_DIR + "/Members",
|
|
8010
8092
|
add: [
|
|
8011
|
-
"
|
|
8093
|
+
MEM_DIR + "/Members/README.md と " + MEM_DIR + "/Members/template.md を読んで登録の規約を把握してから、関係者を1人登録してください。",
|
|
8012
8094
|
"誰を登録するかは、まずわたしに質問してください。",
|
|
8013
8095
|
"書き込む前に、作成するファイルの内容を提示して承認を得てください。",
|
|
8014
8096
|
].join("\\n"),
|
|
@@ -8023,9 +8105,9 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
8023
8105
|
},
|
|
8024
8106
|
"Rules": {
|
|
8025
8107
|
noun: "ルール",
|
|
8026
|
-
dir: "
|
|
8108
|
+
dir: MEM_DIR + "/Rules",
|
|
8027
8109
|
add: [
|
|
8028
|
-
"
|
|
8110
|
+
MEM_DIR + "/Rules/README.md と " + MEM_DIR + "/Rules/template.md を読んで登録の規約を把握してから、ルール(プロジェクトで継続的に守る制約)を1件登録してください。",
|
|
8029
8111
|
"何を登録するかは、まずわたしに質問してください。",
|
|
8030
8112
|
"書き込む前に、作成するファイルの内容を提示して承認を得てください。",
|
|
8031
8113
|
].join("\\n"),
|
|
@@ -8998,9 +9080,9 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
8998
9080
|
|
|
8999
9081
|
// ストーリーの日・週・月のカードの直し口(仕様 7-5)。「AIで編集」と「GitHubで編集」を並べる。人の追記の節は置かない。
|
|
9000
9082
|
// 🔴 AI の起動文に入れるパスは、build が日付の形(YYYY-MM-DD・YYYY-MM)だけ通した鍵から作る(aiFenceScrub も通す)。frontmatter の date・relations は触らせない
|
|
9001
|
-
var STORY_DAYS_BASE = "
|
|
9002
|
-
var STORY_WEEKS_BASE = "
|
|
9003
|
-
var STORY_MONTHS_BASE = "
|
|
9083
|
+
var STORY_DAYS_BASE = MEM_DIR + "/Story/Days/";
|
|
9084
|
+
var STORY_WEEKS_BASE = MEM_DIR + "/Story/Weeks/";
|
|
9085
|
+
var STORY_MONTHS_BASE = MEM_DIR + "/Story/Months/";
|
|
9004
9086
|
function storyEditPrompt(path) {
|
|
9005
9087
|
return [
|
|
9006
9088
|
aiFenceScrub(path) + " を読んでから、このストーリーのページを直してください。",
|
|
@@ -9327,7 +9409,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
9327
9409
|
titleEl.appendChild(el("time", {
|
|
9328
9410
|
class: "home-updated",
|
|
9329
9411
|
datetime: SITE.homeUpdatedAt,
|
|
9330
|
-
title: "
|
|
9412
|
+
title: MEM_DIR + "/Home.md への最後の実質的な変更",
|
|
9331
9413
|
text: "最終更新: " + SITE.homeUpdatedAt,
|
|
9332
9414
|
}));
|
|
9333
9415
|
}
|
|
@@ -11552,6 +11634,8 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
11552
11634
|
return pendingOpsKey(SITE.intake ? SITE.intake.projectKey : "");
|
|
11553
11635
|
}
|
|
11554
11636
|
function readPendingOps() {
|
|
11637
|
+
// 錠 1(便 DS-8b): 設定の表の案件は仮置きを読まない(置き場の境目で閉じる=直接呼ぶ 4 か所すべてに効く)
|
|
11638
|
+
if (viewMode() === "table") return [];
|
|
11555
11639
|
var key = pendingOpsStoreKey();
|
|
11556
11640
|
if (!key) return [];
|
|
11557
11641
|
try {
|
|
@@ -11560,6 +11644,8 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
11560
11644
|
} catch (e) { return []; }
|
|
11561
11645
|
}
|
|
11562
11646
|
function writePendingOps(list) {
|
|
11647
|
+
// 錠 1: table では書かない(残骸の削除は dropPendingLeftovers だけ)
|
|
11648
|
+
if (viewMode() === "table") return;
|
|
11563
11649
|
var key = pendingOpsStoreKey();
|
|
11564
11650
|
if (!key) return;
|
|
11565
11651
|
// 書けなくても操作は成功したまま進める(次の取り直し・次のビルドで本物が出る)
|
|
@@ -11568,6 +11654,18 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
11568
11654
|
else localStorage.removeItem(key);
|
|
11569
11655
|
} catch (e) {}
|
|
11570
11656
|
}
|
|
11657
|
+
/**
|
|
11658
|
+
* 錠 2(便 DS-8b): 設定の表の案件になったとき、仮置きの残骸を**読まずに消す**。
|
|
11659
|
+
* ①24 時間以内の残骸が、戻し(木の復元)で file に戻ったとき幽霊の行として復活するのを防ぐ ②ブラウザに古い値を残さない。
|
|
11660
|
+
* 読み込み済みの控え(PENDING_OPS)と合成のキャッシュ(MERGED)も捨てる。mode が切り替わるたびに呼ぶ
|
|
11661
|
+
*/
|
|
11662
|
+
function dropPendingLeftovers() {
|
|
11663
|
+
PENDING_OPS = null;
|
|
11664
|
+
MERGED = null;
|
|
11665
|
+
var key = pendingOpsStoreKey();
|
|
11666
|
+
if (!key) return;
|
|
11667
|
+
try { localStorage.removeItem(key); } catch (e) {}
|
|
11668
|
+
}
|
|
11571
11669
|
/**
|
|
11572
11670
|
* いま効いている仮置き。**呼ぶたびに失効を評価する**(安いので)。減ったら書き戻し、
|
|
11573
11671
|
* 合成のキャッシュ(MERGED)も捨てる——捨てないと、失効した仮置きが描かれ続ける
|
|
@@ -12269,6 +12367,10 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
12269
12367
|
* 「無い欄を指す案内だけが出る」のどれかが起きる(3つとも実機で出た形)。
|
|
12270
12368
|
*/
|
|
12271
12369
|
function keyTargetCount(cap) {
|
|
12370
|
+
// 設定の表の案件は、接続先の正本が ConfigView(夜の表の件数では、足した直後・外した直後に欄の出し入れが遅れる)
|
|
12371
|
+
if (viewMode() === "table" && CONFIG && (cap === "課題管理" || cap === "デザイン")) {
|
|
12372
|
+
return configSourceRows(CONFIG, cap === "課題管理" ? "backlog" : "figma").length;
|
|
12373
|
+
}
|
|
12272
12374
|
if (cap === "課題管理") {
|
|
12273
12375
|
// 応答で採った一覧のほうが新しい(まとめ表が追いつくのは次の取り直しから)
|
|
12274
12376
|
if (ADOPTED_TARGET_COUNT != null) return ADOPTED_TARGET_COUNT;
|
|
@@ -12645,11 +12747,23 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
12645
12747
|
* 先に使っており、どちらも truthy 判定なので位置を取り違えると静かに壊れる
|
|
12646
12748
|
*/
|
|
12647
12749
|
function openCapabilityModal(cap, items, note, noteIsUnset, adapter) {
|
|
12750
|
+
// 設定の表の案件(DS-8a-1): チャットを開くたびに設定を読み直す(file では何もしない)
|
|
12751
|
+
if (cap === "チャット" && viewMode() === "table") reloadLiveState();
|
|
12752
|
+
// 成功の 1 行(✓)は開き直したら消す(DS-8a-1)
|
|
12753
|
+
if (cap === "チャット") {
|
|
12754
|
+
for (var nk in CONFIG_NOTES) {
|
|
12755
|
+
if (Object.prototype.hasOwnProperty.call(CONFIG_NOTES, nk) && CONFIG_NOTES[nk].cls === "is-ok") delete CONFIG_NOTES[nk];
|
|
12756
|
+
}
|
|
12757
|
+
}
|
|
12648
12758
|
// **開いた能力を覚えて、その登録ファイルを読みに行く(段階1)。** 全能力を毎回
|
|
12649
12759
|
// 読ませない代わりに、見ている能力だけを取りに行く
|
|
12650
12760
|
noteOpenCap(cap);
|
|
12651
12761
|
// 見出しは押したタイルのツール名(Backlog・Google ドライブ…)。capToolLabel の説明を参照
|
|
12652
12762
|
var m = makeModal(capToolLabel(cap, adapter), true);
|
|
12763
|
+
// どの mode の部品で開いたかを覚える(応答の mode が変わったら組み直す・DS-8a-1)
|
|
12764
|
+
CONFIG_OPEN_CHAT = cap === "チャット" && adapter !== "google-chat"
|
|
12765
|
+
? { overlay: m.overlay, mode: viewMode(), tileIds: adapter ? ["ds-" + cap + "-" + adapter, "ds-" + cap] : ["ds-" + cap] }
|
|
12766
|
+
: null;
|
|
12653
12767
|
// [権限](#841)。ツールが決まらない・写しに無いときは出さない(permissionToolsFor の項)
|
|
12654
12768
|
var permTools = permissionToolsFor(cap, adapter);
|
|
12655
12769
|
if (permTools.length) attachPermissionButton(m, permTools);
|
|
@@ -12777,7 +12891,12 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
12777
12891
|
// 戻すと、Google Chat のモーダルの説明が Slack の状態・detail から引き直される。
|
|
12778
12892
|
// アダプタの分からない呼び出し(能力ではない見出し・混載の1枚タイル)だけ代表値。
|
|
12779
12893
|
var noteText = note;
|
|
12780
|
-
|
|
12894
|
+
// 設定の表の案件は、「未接続」を**いまの登録の有無**から言う(便 DS-8d・#1361)。行があるなら言わない。
|
|
12895
|
+
// 夜の表(まとめ表)の結果は、上の一文とは別に「夜の更新の結果(時刻)」として添える
|
|
12896
|
+
var cfgNow = viewMode() === "table" && CONFIG ? CONFIG : null;
|
|
12897
|
+
if (noteIsUnset && cfgNow && configCapRowCount(cfgNow, cap, adapter) !== null) {
|
|
12898
|
+
noteText = configCapRowCount(cfgNow, cap, adapter) ? "" : note;
|
|
12899
|
+
} else if (noteIsUnset) {
|
|
12781
12900
|
// 足した行があるなら、この一文(接続先が設定されていません)はもう事実ではない
|
|
12782
12901
|
var capsNow = capsView();
|
|
12783
12902
|
var dNow = adapter ? adapterDisplayState(cap, adapter, capsNow) : capabilityDisplayState(cap, capsNow);
|
|
@@ -12791,6 +12910,10 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
12791
12910
|
? ""
|
|
12792
12911
|
: (CAP_STATE_TEXT[dNow] || "状態が判定できません") + "(" + detailNow + ")";
|
|
12793
12912
|
}
|
|
12913
|
+
if (!noteIsUnset && cfgNow && noteText) {
|
|
12914
|
+
var nightAt = dsFmtTime(fleetVal("generatedAt") || "");
|
|
12915
|
+
noteText = "夜の更新の結果" + (nightAt ? "(" + nightAt + ")" : "") + ": " + noteText;
|
|
12916
|
+
}
|
|
12794
12917
|
// 🔴 **Figma・Drive・Backlog の「未接続(…が登録されていません)」は作らない(c-07)。** 0 件のとき、
|
|
12795
12918
|
// 同じ窓に一覧の ds-empty「…はまだ登録されていません。」が出ていて二重だった。しかも一覧は
|
|
12796
12919
|
// liveList で追加に追随するが、この一文は開いた時点の値なので、1 件足すと「未接続」が足した行の
|
|
@@ -13077,7 +13200,9 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
13077
13200
|
// 未使用(enabled: false)はグレー行。アダプターの存在は見せつつ、この案件では設定されていないことを示す
|
|
13078
13201
|
// 🔴 **足した行がある能力では「未使用」と言わない(段階1)。** enabled は夜間の値で、
|
|
13079
13202
|
// いま登録した行が一覧に出ているのに「この案件では設定されていません」と並ぶ
|
|
13080
|
-
var inactive = s.enabled === false && !capPending(s.kind)
|
|
13203
|
+
var inactive = s.enabled === false && !capPending(s.kind)
|
|
13204
|
+
// 設定の表の案件は、登録された行があるなら「未使用」と言わない(便 DS-8d・#1361)
|
|
13205
|
+
&& !(viewMode() === "table" && CONFIG && configCapRowCount(CONFIG, s.kind, "") > 0);
|
|
13081
13206
|
if (inactive) {
|
|
13082
13207
|
// **灰色(.ds-inactive)だけにしない。** 注記が無いと、なぜ灰色なのかが読めない
|
|
13083
13208
|
col.appendChild(el("span", { class: "ds-inactive-note", text: "未使用(このプロジェクトでは設定されていません)" }));
|
|
@@ -13223,7 +13348,9 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
13223
13348
|
function liveListWanted(kind) { return !!SITE.intake || liveExternals(kind).length > 0; }
|
|
13224
13349
|
// **箱にはアダプタのタグを付ける(#153)。** pushCap の無タグは「どのタイルにも出す」
|
|
13225
13350
|
// 既定なので、タグを忘れると Slack の一覧が Google Chat モーダルに丸ごと出る
|
|
13226
|
-
|
|
13351
|
+
// 設定の表の案件(DS-8a-1)は、Slack の一覧を ConfigView から描く別の関数に差し替える(file は今のまま)
|
|
13352
|
+
if (viewMode() === "table") pushCap("チャット", tableSlackList(), "slack");
|
|
13353
|
+
else if (liveListWanted("slack")) pushCap("チャット", liveList(
|
|
13227
13354
|
"チャット",
|
|
13228
13355
|
// 通知先の栓(#1370)も入れる。行が同じでも、栓が開いた(ライブが届いた)ら行の出し分けを描き直す
|
|
13229
13356
|
function () { return externalsSig("slack", [slackWorkspaces(), LIVE_NOTIFY_ANY_WS]); },
|
|
@@ -13239,7 +13366,8 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
13239
13366
|
), "slack");
|
|
13240
13367
|
// Google Chat の箱(#153)。行は平置き(組織情報が無いので組ごとの並べ替え・囲いは
|
|
13241
13368
|
// 作れない。判断③)。4件以上でも畳まない——畳みは汎用経路の仕様で、ここは通らない
|
|
13242
|
-
if (
|
|
13369
|
+
if (viewMode() === "table") pushCap("チャット", tableKindList("google-chat"), "google-chat");
|
|
13370
|
+
else if (liveListWanted("google-chat")) pushCap("チャット", liveList(
|
|
13243
13371
|
"チャット",
|
|
13244
13372
|
function () { return externalsSig("google-chat", googleInfoSig()); },
|
|
13245
13373
|
function (box) {
|
|
@@ -13258,7 +13386,8 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
13258
13386
|
.forEach(function (n) { box.appendChild(n); });
|
|
13259
13387
|
},
|
|
13260
13388
|
), "google-chat");
|
|
13261
|
-
if (
|
|
13389
|
+
if (viewMode() === "table") { if (capabilityUsable("開発", "github")) pushCap("開発", tableKindList("github")); }
|
|
13390
|
+
else if (liveListWanted("github")) pushCap("開発", liveList(
|
|
13262
13391
|
"開発",
|
|
13263
13392
|
function () { return externalsSig("github", githubOwnersView() || null); },
|
|
13264
13393
|
function (box) {
|
|
@@ -13287,7 +13416,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
13287
13416
|
// **新しい形が別の解き方をしている**——チャンネルを追加すれば
|
|
13288
13417
|
// 読み取れていない組として現れ、そこに接続ボタンが出る。
|
|
13289
13418
|
// 0件なら繋ぐ相手もまだ無いので、入口が要る場面にならない。
|
|
13290
|
-
pushCap("チャット", adders.querySelector('[data-adder="slack"]'), "slack");
|
|
13419
|
+
pushCap("チャット", viewMode() === "table" ? tableSlackAdder() : adders.querySelector('[data-adder="slack"]'), "slack");
|
|
13291
13420
|
/* -------------------------------------------------------------------------
|
|
13292
13421
|
* 🔴 接続の入口は、必要になった行の一手の中にだけある(c-07)
|
|
13293
13422
|
* -------------------------------------------------------------------------
|
|
@@ -13308,7 +13437,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
13308
13437
|
// Workspace 管理者による Admin install が承認そのもので、画面に押す先が無い。
|
|
13309
13438
|
// アプリ追加の操作列(画像つき)と管理者の承認は、追加の欄の手順ではなく、**そのスペースの
|
|
13310
13439
|
// 行の一手**(DS_GATE.not_in_space の details)が持つ(c-07)
|
|
13311
|
-
pushCap("チャット", adders.querySelector('[data-adder="google-chat"]'), "google-chat");
|
|
13440
|
+
pushCap("チャット", viewMode() === "table" ? tableKindAdder("google-chat") : adders.querySelector('[data-adder="google-chat"]'), "google-chat");
|
|
13312
13441
|
// **組織ごとの接続状況は、行と一体にした(2026-08-20・Slack からの横展開)。**
|
|
13313
13442
|
//
|
|
13314
13443
|
// それまでは「リポジトリの一覧」と「組織ごとの接続状況」が**別々に並んで**おり、
|
|
@@ -13325,7 +13454,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
13325
13454
|
// 立ち上げ直後(githubOwners が空)でも、追加フォームは必ず出る
|
|
13326
13455
|
// (adders はグループの外に置いてある)。
|
|
13327
13456
|
|
|
13328
|
-
pushCap("開発", adders.querySelector('[data-adder="github"]'));
|
|
13457
|
+
pushCap("開発", viewMode() === "table" ? (capabilityUsable("開発", "github") ? tableKindAdder("github") : null) : adders.querySelector('[data-adder="github"]'));
|
|
13329
13458
|
|
|
13330
13459
|
// **0件の案件にも1行だけ残す(#138 のオーナー判断・置き場所だけ移した)。**
|
|
13331
13460
|
// 元は GitHub の読み取りトークンの鍵カードを畳んだ跡地に出していたが、
|
|
@@ -13352,7 +13481,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
13352
13481
|
//
|
|
13353
13482
|
// **箱を置いてよいのは、そこへ足せる案件だけ**(liveListWanted の項)。投入の口が
|
|
13354
13483
|
// 無い構成に「上のフォームで登録すると」と書いても、指す先のフォームが無い
|
|
13355
|
-
if (liveListWanted("github")) pushCap("開発", liveList("開発", function () {
|
|
13484
|
+
if (viewMode() !== "table" && liveListWanted("github")) pushCap("開発", liveList("開発", function () {
|
|
13356
13485
|
// 判定に使う値そのものを署名にする(行の増減と、夜間の組の一覧の両方)
|
|
13357
13486
|
return JSON.stringify([liveExternals("github").length, githubOwnersView() || null]);
|
|
13358
13487
|
}, function (box) {
|
|
@@ -14414,19 +14543,21 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
14414
14543
|
return "chat";
|
|
14415
14544
|
}
|
|
14416
14545
|
|
|
14546
|
+
/** Slack のチャンネルの登録解除の確認文(file の行と table の行で同じ文を使う) */
|
|
14547
|
+
function slackRemoveConfirmLines(label) {
|
|
14548
|
+
return [
|
|
14549
|
+
"「" + label + "」を、読み取り対象から外します。",
|
|
14550
|
+
"",
|
|
14551
|
+
// 外したときの約束の 1 文(intake の REMOVED_PROMISE と同じ・cortex-engine #1094 c-30 G7)
|
|
14552
|
+
"外すと、次の晩から読まなくなります。すでに記憶に入った内容は残ります。",
|
|
14553
|
+
"",
|
|
14554
|
+
"解除しますか?",
|
|
14555
|
+
];
|
|
14556
|
+
}
|
|
14417
14557
|
function removeChannelButton(s, card) {
|
|
14418
14558
|
return removeButton({
|
|
14419
14559
|
label: "登録を解除",
|
|
14420
|
-
confirmLines: function () {
|
|
14421
|
-
return [
|
|
14422
|
-
"「" + (s.name || s.ref) + "」を、読み取り対象から外します。",
|
|
14423
|
-
"",
|
|
14424
|
-
// 外したときの約束の 1 文(intake の REMOVED_PROMISE と同じ・cortex-engine #1094 c-30 G7)
|
|
14425
|
-
"外すと、次の晩から読まなくなります。すでに記憶に入った内容は残ります。",
|
|
14426
|
-
"",
|
|
14427
|
-
"解除しますか?",
|
|
14428
|
-
];
|
|
14429
|
-
},
|
|
14560
|
+
confirmLines: function () { return slackRemoveConfirmLines(s.name || s.ref); },
|
|
14430
14561
|
build: function () {
|
|
14431
14562
|
// **どのワークスペースのチャンネルかを必ず送る(#104)。** 落とすと、サーバ側は
|
|
14432
14563
|
// 既定ワークスペースの同じIDのエントリを探しに行き、「対象チャンネルが
|
|
@@ -14560,6 +14691,614 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
14560
14691
|
if (act) wrap.appendChild(act);
|
|
14561
14692
|
}
|
|
14562
14693
|
|
|
14694
|
+
/* ---------------------------------------------------------------------------
|
|
14695
|
+
* 設定の表の案件(mode: "table")の保存「保存 → 本当の結果」(cortex-engine #1911 便 DS-8a-1)
|
|
14696
|
+
* ---------------------------------------------------------------------------
|
|
14697
|
+
* 対象は Slack の足す・外す・直す(表示名の取り直し)と通知先の入り切りだけ。
|
|
14698
|
+
* - 画面が持つ設定は CONFIG(サーバが返した ConfigView)1 つだけ。**メモリにだけ置く**
|
|
14699
|
+
* (localStorage・sessionStorage・IndexedDB に設定を置かない)
|
|
14700
|
+
* - **仮置き(行を先に出す・消す)をしない。** 押したらその行(追加は追加の口)を「保存中…」にして止め、
|
|
14701
|
+
* 応答の view で CONFIG を丸ごと置き換えて描き直す。保存のあとに設定の取り直しはしない
|
|
14702
|
+
* - file の道(sendConfig・仮置き)とは部品を共有しない。全案件の組み直しが終わった日に file の道をまるごと消す
|
|
14703
|
+
* ------------------------------------------------------------------------- */
|
|
14704
|
+
/** サーバが最後に返した ConfigView(null はまだ無い・file の案件) */
|
|
14705
|
+
var CONFIG = null;
|
|
14706
|
+
/** サーバが最後に答えた mode。null はまだ答えが無い(焼いたヒントで描く) */
|
|
14707
|
+
var CONFIG_SERVER_MODE = null;
|
|
14708
|
+
/** 設定の表の案件で、設定をまだ一度も読めずに失敗した・見切った */
|
|
14709
|
+
var CONFIG_READ_FAILED = false;
|
|
14710
|
+
/** 保存中の対象(行の鍵・追加の口の鍵 → true)。CONFIG と別に持つ(置き換えで描き直しても消えない) */
|
|
14711
|
+
var CONFIG_SAVING = {};
|
|
14712
|
+
/** 対象ごとの 1 行({ cls, text, act })。act は reread(最新の状態を読み直す)か reload(再読み込み) */
|
|
14713
|
+
var CONFIG_NOTES = {};
|
|
14714
|
+
/** 意図の署名 → 冪等キー。**確定するまで**同じ意図の押し直しは同じ値で送る */
|
|
14715
|
+
var CONFIG_REQS = {};
|
|
14716
|
+
/** 追加の口の入力(描き直しで消さないためのメモリの控え) */
|
|
14717
|
+
var CONFIG_ADD_DRAFT = { url: "", name: "" };
|
|
14718
|
+
var CONFIG_ADD_KEY = "add|slack";
|
|
14719
|
+
var CONFIG_SAVE_TIMEOUT_MS = LIVE_CONFIRM_TIMEOUT_MS;
|
|
14720
|
+
|
|
14721
|
+
/** 分岐点は 1 か所。応答の mode が常に勝ち、まだ答えが無いあいだだけ焼いたヒント(無ければ file) */
|
|
14722
|
+
function viewMode() {
|
|
14723
|
+
if (CONFIG) return CONFIG.mode;
|
|
14724
|
+
if (CONFIG_SERVER_MODE) return CONFIG_SERVER_MODE;
|
|
14725
|
+
return SITE.configMode === "table" ? "table" : "file";
|
|
14726
|
+
}
|
|
14727
|
+
/** ConfigView を受ける。**持っている rev より小さければ捨てる**。受けて中身が変わったら true */
|
|
14728
|
+
function applyConfigView(v) {
|
|
14729
|
+
if (!configViewOk(v)) return false;
|
|
14730
|
+
if (CONFIG && !configRevAccepts(CONFIG.rev, v.rev)) return false;
|
|
14731
|
+
var changed = !CONFIG || JSON.stringify(CONFIG) !== JSON.stringify(v);
|
|
14732
|
+
CONFIG = v;
|
|
14733
|
+
CONFIG_READ_FAILED = false;
|
|
14734
|
+
return changed;
|
|
14735
|
+
}
|
|
14736
|
+
/** op=state の応答から mode と ConfigView を読む(loadLiveFleet の成功側・fleet より前) */
|
|
14737
|
+
function configFromState(j) {
|
|
14738
|
+
var before = viewMode();
|
|
14739
|
+
configReadState(j);
|
|
14740
|
+
// 開いているチャットのモーダルが別の mode の部品のままなら組み直す(ヒントが無いまま table が届いた回)
|
|
14741
|
+
if (before !== viewMode()) { configModeSwitched(); configReopenChat(); }
|
|
14742
|
+
}
|
|
14743
|
+
/** mode が切り替わった。table になったなら仮置きを捨てる(錠 2)。file に戻るなら控えを読み直させる */
|
|
14744
|
+
function configModeSwitched() {
|
|
14745
|
+
if (viewMode() === "table") dropPendingLeftovers();
|
|
14746
|
+
else { PENDING_OPS = null; MERGED = null; }
|
|
14747
|
+
}
|
|
14748
|
+
// 焼いたヒントが table なら、最初の応答を待たずに残骸を消す(応答の mode が file なら、そのとき控えを読み直す)
|
|
14749
|
+
if (viewMode() === "table") dropPendingLeftovers();
|
|
14750
|
+
function configReadState(j) {
|
|
14751
|
+
CONFIG_SERVER_MODE = configModeOf(j);
|
|
14752
|
+
if (CONFIG_SERVER_MODE === "table") {
|
|
14753
|
+
var v = configViewOfState(j);
|
|
14754
|
+
if (v) applyConfigView(v);
|
|
14755
|
+
else if (!CONFIG) CONFIG_READ_FAILED = true;
|
|
14756
|
+
} else {
|
|
14757
|
+
CONFIG = null;
|
|
14758
|
+
CONFIG_READ_FAILED = false;
|
|
14759
|
+
}
|
|
14760
|
+
}
|
|
14761
|
+
/** 開いているチャット(Slack)のモーダルと、そのとき描いた mode。mode が変わったら組み直す */
|
|
14762
|
+
var CONFIG_OPEN_CHAT = null;
|
|
14763
|
+
function configReopenChat() {
|
|
14764
|
+
var o = CONFIG_OPEN_CHAT;
|
|
14765
|
+
if (!o || !o.overlay || !app.contains(o.overlay) || o.mode === viewMode()) return;
|
|
14766
|
+
CONFIG_OPEN_CHAT = null;
|
|
14767
|
+
if (o.overlay.__close) o.overlay.__close(true);
|
|
14768
|
+
var old = app.querySelector(".datasources");
|
|
14769
|
+
var next = buildDataSources();
|
|
14770
|
+
if (old && next) old.parentNode.replaceChild(next, old);
|
|
14771
|
+
// タイルの id は先頭のアダプタだけ能力名(2 枚目から能力名-アダプタ)。アダプタ付きの id を先に引く
|
|
14772
|
+
var tile = null;
|
|
14773
|
+
for (var ti = 0; !tile && ti < o.tileIds.length; ti++) tile = document.getElementById(o.tileIds[ti]);
|
|
14774
|
+
if (tile) tile.click();
|
|
14775
|
+
}
|
|
14776
|
+
/**
|
|
14777
|
+
* file の道の旧キーの要求が 400 CLIENT_OUTDATED(mode: table と view つき)で返った。
|
|
14778
|
+
* 最初の読み取りに失敗してヒントも無かった回だけ起きる。応答の view を受けて画面を table に切り替える
|
|
14779
|
+
*/
|
|
14780
|
+
function configAdoptOutdated(status, j) {
|
|
14781
|
+
if (status !== 400 || !j || j.code !== "CLIENT_OUTDATED" || configModeOf(j) !== "table" || !configViewOk(j.view)) return false;
|
|
14782
|
+
var before = viewMode();
|
|
14783
|
+
CONFIG_SERVER_MODE = "table";
|
|
14784
|
+
applyConfigView(j.view);
|
|
14785
|
+
if (before !== viewMode()) { configModeSwitched(); configReopenChat(); }
|
|
14786
|
+
configRepaint();
|
|
14787
|
+
showToast("画面を最新の形に切り替えました。もう一度押してください。", true);
|
|
14788
|
+
return true;
|
|
14789
|
+
}
|
|
14790
|
+
/** 設定が変わったときの描き直し(開いている一覧と、節ごと) */
|
|
14791
|
+
function configRepaint() {
|
|
14792
|
+
repaintOpenLists();
|
|
14793
|
+
refreshDataSources();
|
|
14794
|
+
}
|
|
14795
|
+
function configRandomHex() {
|
|
14796
|
+
try {
|
|
14797
|
+
var a = new Uint8Array(8);
|
|
14798
|
+
crypto.getRandomValues(a);
|
|
14799
|
+
var out = "";
|
|
14800
|
+
for (var i = 0; i < a.length; i++) out += (a[i] < 16 ? "0" : "") + a[i].toString(16);
|
|
14801
|
+
return out;
|
|
14802
|
+
} catch (e) {
|
|
14803
|
+
return Math.random().toString(16).slice(2);
|
|
14804
|
+
}
|
|
14805
|
+
}
|
|
14806
|
+
/** 対象の 1 行を描く(無ければ何も出さない) */
|
|
14807
|
+
function configNoteEl(key) {
|
|
14808
|
+
var n = CONFIG_NOTES[key];
|
|
14809
|
+
if (!n) return null;
|
|
14810
|
+
var box = el("div", { class: "ds-toggle-msg ds-config-note " + (n.cls || ""), "data-config-note": key });
|
|
14811
|
+
box.appendChild(el("span", { text: n.text }));
|
|
14812
|
+
if (n.act === "reread") {
|
|
14813
|
+
var again = el("button", { class: "ds-toggle-btn", type: "button", text: "最新の状態を読み直す" });
|
|
14814
|
+
again.addEventListener("click", function () {
|
|
14815
|
+
delete CONFIG_NOTES[key];
|
|
14816
|
+
configRepaint();
|
|
14817
|
+
reloadLiveState();
|
|
14818
|
+
});
|
|
14819
|
+
box.appendChild(again);
|
|
14820
|
+
} else if (n.act === "reload") {
|
|
14821
|
+
var reload = el("button", { class: "ds-toggle-btn", type: "button", text: "再読み込み" });
|
|
14822
|
+
reload.addEventListener("click", function () { location.reload(); });
|
|
14823
|
+
box.appendChild(reload);
|
|
14824
|
+
}
|
|
14825
|
+
return box;
|
|
14826
|
+
}
|
|
14827
|
+
/**
|
|
14828
|
+
* 結果を 1 行で出す。対象の行が今の CONFIG に無い(消えた)ときはトーストに出す
|
|
14829
|
+
* (追加の口は消えないので必ずその場)
|
|
14830
|
+
*/
|
|
14831
|
+
function configNote(key, cls, text, act, rowGone) {
|
|
14832
|
+
if (rowGone) {
|
|
14833
|
+
delete CONFIG_NOTES[key];
|
|
14834
|
+
showToast(text, cls === "is-error");
|
|
14835
|
+
return;
|
|
14836
|
+
}
|
|
14837
|
+
CONFIG_NOTES[key] = { cls: cls, text: text, act: act || "" };
|
|
14838
|
+
}
|
|
14839
|
+
|
|
14840
|
+
/**
|
|
14841
|
+
* 1 回の保存。o = { key, kind, action, target, expectVer, formKind, duplicateLines, returnTo, onOk, row }
|
|
14842
|
+
* row は対象の行({ kind, ref, workspace })。結果のあとに行が残っているかを見る
|
|
14843
|
+
*/
|
|
14844
|
+
async function configSave(o) {
|
|
14845
|
+
if (!SITE.intake || !CONFIG || CONFIG_SAVING[o.key]) return;
|
|
14846
|
+
var sig = configIntentSig(o.kind, o.action, o.target, o.change);
|
|
14847
|
+
if (!CONFIG_REQS[sig]) CONFIG_REQS[sig] = configNewReq(Date.now(), configRandomHex());
|
|
14848
|
+
var req = CONFIG_REQS[sig];
|
|
14849
|
+
var body = configIntentOf(o.kind, o.action, o.target, o.change, CONFIG.rev, req, o.expectVer);
|
|
14850
|
+
CONFIG_SAVING[o.key] = true;
|
|
14851
|
+
delete CONFIG_NOTES[o.key];
|
|
14852
|
+
configRepaint();
|
|
14853
|
+
// 応答が来ないまま待たせない(10 秒で見切る)。見切ったら「保存できたか分かりません」
|
|
14854
|
+
var ctrl = typeof AbortController === "function" ? new AbortController() : null;
|
|
14855
|
+
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, CONFIG_SAVE_TIMEOUT_MS) : null;
|
|
14856
|
+
var res = null;
|
|
14857
|
+
var j = null;
|
|
14858
|
+
try {
|
|
14859
|
+
res = await callIntake("config", body, { signal: ctrl ? ctrl.signal : undefined });
|
|
14860
|
+
// 応答のヘッダが届いたら見切りのタイマを外す(本文の読み取り中に中断しない)
|
|
14861
|
+
if (timer != null) { clearTimeout(timer); timer = null; }
|
|
14862
|
+
j = await res.json().catch(function () { return {}; });
|
|
14863
|
+
} catch (e) {
|
|
14864
|
+
res = null;
|
|
14865
|
+
}
|
|
14866
|
+
if (timer != null) clearTimeout(timer);
|
|
14867
|
+
delete CONFIG_SAVING[o.key];
|
|
14868
|
+
var rowGoneNow = function () {
|
|
14869
|
+
return !!o.row && !configFindSource(CONFIG, o.row.kind, o.row.ref, o.row.workspace);
|
|
14870
|
+
};
|
|
14871
|
+
if (!res) {
|
|
14872
|
+
// 勝手に再送しない。req は確定するまで残す(押し直しは同じ req・同じ意図)
|
|
14873
|
+
configNote(o.key, "is-error", "保存できたか分かりません", "reread", false);
|
|
14874
|
+
configRepaint();
|
|
14875
|
+
return;
|
|
14876
|
+
}
|
|
14877
|
+
var c = configClassify(res.status, j, o.formKind || "");
|
|
14878
|
+
if (c.final) delete CONFIG_REQS[sig];
|
|
14879
|
+
if (c.type === "ok" || c.type === "noChange") {
|
|
14880
|
+
var text = c.message || (c.type === "ok" ? "保存しました。" : "変更はありません");
|
|
14881
|
+
// もっと新しい設定が先に届いていれば view は捨てて、文だけ出す
|
|
14882
|
+
if (c.view) applyConfigView(c.view);
|
|
14883
|
+
if (c.type === "ok" && typeof o.onOk === "function") o.onOk();
|
|
14884
|
+
configNote(o.key, "is-ok", "✓ " + text, "", rowGoneNow());
|
|
14885
|
+
// 形の読めない 200(view が無い)だけは、設定を読み直して追いつく
|
|
14886
|
+
if (!c.view) reloadLiveState();
|
|
14887
|
+
} else if (c.type === "revConflict") {
|
|
14888
|
+
if (c.view) applyConfigView(c.view);
|
|
14889
|
+
configNote(o.key, "is-error", "他の人が先に変更していました。最新の状態を表示しています", "", rowGoneNow());
|
|
14890
|
+
} else if (c.type === "clientOutdated") {
|
|
14891
|
+
configNote(o.key, "is-error", "この画面が古くなっています。再読み込みしてください", "reload", false);
|
|
14892
|
+
} else if (c.type === "duplicate" && typeof o.duplicateLines === "function" && !(o.target && o.target.confirmDuplicate === true)) {
|
|
14893
|
+
configRepaint();
|
|
14894
|
+
var yes = await confirmDanger(o.duplicateLines(), { okLabel: "追加する", returnTo: o.returnTo });
|
|
14895
|
+
if (yes) {
|
|
14896
|
+
var again = {};
|
|
14897
|
+
for (var k in o.target) if (Object.prototype.hasOwnProperty.call(o.target, k)) again[k] = o.target[k];
|
|
14898
|
+
again.confirmDuplicate = true;
|
|
14899
|
+
var next = {};
|
|
14900
|
+
for (var k2 in o) if (Object.prototype.hasOwnProperty.call(o, k2)) next[k2] = o[k2];
|
|
14901
|
+
next.target = again;
|
|
14902
|
+
// 意図が変わるので新しい req で送る
|
|
14903
|
+
await configSave(next);
|
|
14904
|
+
return;
|
|
14905
|
+
}
|
|
14906
|
+
// [いいえ]は静かに戻す(赤い ✗ を出さない)
|
|
14907
|
+
delete CONFIG_NOTES[o.key];
|
|
14908
|
+
} else {
|
|
14909
|
+
// 形・権限・取り違え・5xx。行は元のまま、その場に 1 行(5xx は req を残す)
|
|
14910
|
+
configNote(o.key, "is-error", "✗ " + (c.message || "保存できませんでした。"), "", false);
|
|
14911
|
+
}
|
|
14912
|
+
configRepaint();
|
|
14913
|
+
}
|
|
14914
|
+
|
|
14915
|
+
/** 設定の表の案件の Slack の一覧(ConfigView の kind slack と notify から描く) */
|
|
14916
|
+
function tableSlackList() {
|
|
14917
|
+
return liveList(
|
|
14918
|
+
"チャット",
|
|
14919
|
+
function () {
|
|
14920
|
+
return JSON.stringify([
|
|
14921
|
+
CONFIG ? [configSourceRows(CONFIG, "slack"), CONFIG.notify] : null,
|
|
14922
|
+
CONFIG_READ_FAILED, CONFIG_SAVING, CONFIG_NOTES, LIVE_NOTIFY_ANY_WS, slackWorkspaces(), nightReadSig("slack"),
|
|
14923
|
+
]);
|
|
14924
|
+
},
|
|
14925
|
+
function (box) {
|
|
14926
|
+
box.setAttribute("data-config-list", "slack");
|
|
14927
|
+
if (tableStateBox(box)) return;
|
|
14928
|
+
configSourceRows(CONFIG, "slack").forEach(function (r) { box.appendChild(tableSlackRow(r)); });
|
|
14929
|
+
configUnlinkedNotify(CONFIG, "slack").forEach(function (n) { box.appendChild(tableUnlinkedRow("slack", n)); });
|
|
14930
|
+
},
|
|
14931
|
+
);
|
|
14932
|
+
}
|
|
14933
|
+
/** 1 行。表示名・リンク・札・[登録を解除]・[表示名を取り直す]・通知先の入り切り */
|
|
14934
|
+
function tableSlackRow(r) {
|
|
14935
|
+
var key = configRowKey("slack", r.ref, r.workspace);
|
|
14936
|
+
var saving = CONFIG_SAVING[key] === true;
|
|
14937
|
+
var meta = DS_TYPE["slack"] || { icon: "🔗", label: "Slack" };
|
|
14938
|
+
var n = configNotifyOf(CONFIG, "slack", r.ref, r.workspace);
|
|
14939
|
+
var on = !!(n && n.on === true);
|
|
14940
|
+
var badges = el("div", { class: "ds-badges" });
|
|
14941
|
+
if (on) badges.appendChild(notifyBadge());
|
|
14942
|
+
badges.appendChild(r.workspace
|
|
14943
|
+
? el("span", { class: "badge ds-workspace", text: workspaceLabel(r.workspace), title: "接続した Slack ワークスペースのチャンネルです。" })
|
|
14944
|
+
: el("span", { class: "badge ds-workspace", text: "クラスメソッド", title: "クラスメソッドの社内 Slack のチャンネルです。" }));
|
|
14945
|
+
var card = el("div", { class: "ds-card" + (saving ? " is-saving" : ""), "data-config-row": key }, [
|
|
14946
|
+
el("div", { class: "ds-card-main" }, [
|
|
14947
|
+
el("span", { class: "ds-icon", text: meta.icon }),
|
|
14948
|
+
el("span", { class: "ds-type", text: meta.label }),
|
|
14949
|
+
dsNameEl(slackDisplayName(r.name || r.ref), r.url || ""),
|
|
14950
|
+
]),
|
|
14951
|
+
badges,
|
|
14952
|
+
]);
|
|
14953
|
+
var nightS = nightReadEl("slack", r);
|
|
14954
|
+
if (nightS) card.appendChild(nightS);
|
|
14955
|
+
if (!SITE.intake) return card;
|
|
14956
|
+
var row = { kind: "slack", ref: r.ref, workspace: r.workspace };
|
|
14957
|
+
var acts = el("div", { class: "ds-toggle" });
|
|
14958
|
+
var remove = el("button", { class: "ds-toggle-btn ds-danger", type: "button", text: "登録を解除" });
|
|
14959
|
+
remove.addEventListener("click", async function () {
|
|
14960
|
+
if (!(await confirmDanger(slackRemoveConfirmLines(r.name || r.ref), { returnTo: remove }))) return;
|
|
14961
|
+
// remove・update の target は行の ref だけ(workspace を付けない・契約 §3)
|
|
14962
|
+
configSave({ key: key, kind: "slack", action: "remove", target: { ref: r.ref }, expectVer: r.ver, row: row });
|
|
14963
|
+
});
|
|
14964
|
+
var rename = el("button", { class: "ds-toggle-btn", type: "button", text: "表示名を取り直す" });
|
|
14965
|
+
rename.addEventListener("click", function () {
|
|
14966
|
+
configSave({ key: key, kind: "slack", action: "update", target: { ref: r.ref }, change: { refreshName: true }, expectVer: r.ver, row: row });
|
|
14967
|
+
});
|
|
14968
|
+
acts.appendChild(remove);
|
|
14969
|
+
acts.appendChild(rename);
|
|
14970
|
+
// 通知先の入り切り。ワークスペースの行は、栓が開くまで通知先にできない(file の行と同じ出し分け)
|
|
14971
|
+
if (r.workspace && !on && !LIVE_NOTIFY_ANY_WS) {
|
|
14972
|
+
acts.appendChild(el("span", { class: "ds-toggle-msg", text: NOTIFY_WS_PENDING_TEXT }));
|
|
14973
|
+
} else {
|
|
14974
|
+
var toggle = el("button", { class: "ds-toggle-btn", type: "button", text: on ? "通知を止める" : "通知先にする" });
|
|
14975
|
+
toggle.addEventListener("click", function () {
|
|
14976
|
+
configSave({ key: key, kind: "notify", action: "update", target: { ref: r.ref }, change: { value: !on }, row: row });
|
|
14977
|
+
});
|
|
14978
|
+
acts.appendChild(toggle);
|
|
14979
|
+
}
|
|
14980
|
+
var buttons = acts.querySelectorAll("button");
|
|
14981
|
+
for (var i = 0; i < buttons.length; i++) buttons[i].disabled = saving;
|
|
14982
|
+
card.appendChild(acts);
|
|
14983
|
+
if (saving) card.appendChild(el("div", { class: "ds-toggle-msg ds-config-saving", text: "保存中…" }));
|
|
14984
|
+
var note = saving ? null : configNoteEl(key);
|
|
14985
|
+
if (note) card.appendChild(note);
|
|
14986
|
+
return card;
|
|
14987
|
+
}
|
|
14988
|
+
/**
|
|
14989
|
+
* 設定の表の案件の Slack の追加の口。URL と任意の表示名 → add の意図(workspace は送らない。サーバが実測で決める)。
|
|
14990
|
+
* 描き直しは状態の塗り直しだけ(入力は CONFIG_ADD_DRAFT が持つ)
|
|
14991
|
+
*/
|
|
14992
|
+
function tableSlackAdder() {
|
|
14993
|
+
if (!SITE.intake) return null;
|
|
14994
|
+
var url = el("input", { class: "sec-input", type: "text", placeholder: "https://…slack.com/archives/C0XXXXXXX", autocomplete: "off" });
|
|
14995
|
+
var name = el("input", { class: "sec-input", type: "text", placeholder: "表示名(省略するとSlackから取得します)", autocomplete: "off" });
|
|
14996
|
+
var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "追加" });
|
|
14997
|
+
var msg = el("div", { class: "ds-toggle-msg" });
|
|
14998
|
+
var box = el("div", { class: "ds-addform", "data-adder": "slack", "data-config-adder": "slack", "data-live-cap": "チャット" }, [
|
|
14999
|
+
el("div", { class: "ds-addform-title", text: "+ Slackチャンネルを追加" }),
|
|
15000
|
+
url,
|
|
15001
|
+
name,
|
|
15002
|
+
el("div", { class: "ds-toggle" }, [btn, msg]),
|
|
15003
|
+
]);
|
|
15004
|
+
url.value = CONFIG_ADD_DRAFT.url;
|
|
15005
|
+
name.value = CONFIG_ADD_DRAFT.name;
|
|
15006
|
+
url.addEventListener("input", function () {
|
|
15007
|
+
CONFIG_ADD_DRAFT.url = url.value;
|
|
15008
|
+
// 「URLを入力してください」は入力を始めたら消す
|
|
15009
|
+
var w = CONFIG_NOTES[CONFIG_ADD_KEY];
|
|
15010
|
+
if (w && w.warn) { delete CONFIG_NOTES[CONFIG_ADD_KEY]; paint(); }
|
|
15011
|
+
});
|
|
15012
|
+
name.addEventListener("input", function () { CONFIG_ADD_DRAFT.name = name.value; });
|
|
15013
|
+
var paint = function () {
|
|
15014
|
+
var saving = CONFIG_SAVING[CONFIG_ADD_KEY] === true;
|
|
15015
|
+
var ready = !!CONFIG;
|
|
15016
|
+
url.value = CONFIG_ADD_DRAFT.url;
|
|
15017
|
+
name.value = CONFIG_ADD_DRAFT.name;
|
|
15018
|
+
url.disabled = saving || !ready;
|
|
15019
|
+
name.disabled = saving || !ready;
|
|
15020
|
+
btn.disabled = saving || !ready;
|
|
15021
|
+
btn.textContent = "追加";
|
|
15022
|
+
box.classList.toggle("is-saving", saving);
|
|
15023
|
+
while (msg.firstChild) msg.removeChild(msg.firstChild);
|
|
15024
|
+
msg.className = "ds-toggle-msg";
|
|
15025
|
+
if (saving) { msg.textContent = "保存中…"; return; }
|
|
15026
|
+
var note = configNoteEl(CONFIG_ADD_KEY);
|
|
15027
|
+
if (note) msg.appendChild(note);
|
|
15028
|
+
};
|
|
15029
|
+
btn.addEventListener("click", function () {
|
|
15030
|
+
var u = url.value.trim();
|
|
15031
|
+
if (!u) {
|
|
15032
|
+
configNote(CONFIG_ADD_KEY, "is-error", "⚠ チャンネルのURLを入力してください。", "", false);
|
|
15033
|
+
CONFIG_NOTES[CONFIG_ADD_KEY].warn = true;
|
|
15034
|
+
paint();
|
|
15035
|
+
return;
|
|
15036
|
+
}
|
|
15037
|
+
var target = { url: u };
|
|
15038
|
+
var nm = name.value.trim();
|
|
15039
|
+
if (nm) target.name = nm;
|
|
15040
|
+
configSave({
|
|
15041
|
+
key: CONFIG_ADD_KEY, kind: "slack", action: "add", target: target, formKind: "slack",
|
|
15042
|
+
duplicateLines: function () { return slackDuplicateConfirmLines(u); },
|
|
15043
|
+
returnTo: btn,
|
|
15044
|
+
onOk: function () { CONFIG_ADD_DRAFT = { url: "", name: "" }; },
|
|
15045
|
+
});
|
|
15046
|
+
});
|
|
15047
|
+
box.__rebuild = paint;
|
|
15048
|
+
paint();
|
|
15049
|
+
return box;
|
|
15050
|
+
}
|
|
15051
|
+
|
|
15052
|
+
/** 設定の読み込み中・読めなかったときの 1 行(出したら true)。Slack と同じ見た目・同じ印 */
|
|
15053
|
+
function tableStateBox(box) {
|
|
15054
|
+
if (CONFIG) return false;
|
|
15055
|
+
if (CONFIG_READ_FAILED) {
|
|
15056
|
+
var again = el("button", { class: "ds-toggle-btn", type: "button", text: "読み直す" });
|
|
15057
|
+
again.addEventListener("click", function () {
|
|
15058
|
+
CONFIG_READ_FAILED = false;
|
|
15059
|
+
configRepaint();
|
|
15060
|
+
reloadLiveState();
|
|
15061
|
+
});
|
|
15062
|
+
box.appendChild(el("div", { class: "ds-config-state is-error", "data-config-state": "failed" }, [
|
|
15063
|
+
el("span", { text: "読めませんでした" }),
|
|
15064
|
+
again,
|
|
15065
|
+
]));
|
|
15066
|
+
} else {
|
|
15067
|
+
box.appendChild(el("div", { class: "ds-config-state", "data-config-state": "loading", text: "読み込み中…" }));
|
|
15068
|
+
}
|
|
15069
|
+
return true;
|
|
15070
|
+
}
|
|
15071
|
+
|
|
15072
|
+
/**
|
|
15073
|
+
* 設定の表の案件で、Slack 以外の接続先の種別ごとの見せ方(DS-8a-2)。
|
|
15074
|
+
* cap は一覧の data-live-cap(開いているモーダルの差し替え先)・icon / label は行のツール名・
|
|
15075
|
+
* notify は通知先の入り切りを持つか・unit は件数の単位語。追加の口の語は adder が持つ
|
|
15076
|
+
*/
|
|
15077
|
+
var TABLE_KINDS = {
|
|
15078
|
+
"google-chat": {
|
|
15079
|
+
cap: "チャット", icon: "💬", label: "Google Chat", notify: true,
|
|
15080
|
+
empty: "読み取り対象のスペースはまだ登録されていません。",
|
|
15081
|
+
adder: { title: "+ Google Chat スペースを追加", placeholder: "https://chat.google.com/app/chat/XXXXXXXXXXX", required: "スペースの URL か spaces/… を入力してください。" },
|
|
15082
|
+
},
|
|
15083
|
+
"github": {
|
|
15084
|
+
cap: "開発", icon: "🐙", label: "GitHub", notify: false,
|
|
15085
|
+
empty: "読み取り対象のリポジトリはまだ登録されていません。",
|
|
15086
|
+
adder: { title: "+ GitHubリポジトリを追加", placeholder: "owner/repo または https://github.com/owner/repo", required: "リポジトリを入力してください。" },
|
|
15087
|
+
},
|
|
15088
|
+
"figma": {
|
|
15089
|
+
cap: "デザイン", icon: "🎨", label: "Figma", notify: false,
|
|
15090
|
+
empty: "同期対象のFigmaファイルはまだ登録されていません。",
|
|
15091
|
+
adder: { title: "+ Figmaファイルを追加", placeholder: "https://www.figma.com/design/…", required: "FigmaのURLを入力してください。" },
|
|
15092
|
+
},
|
|
15093
|
+
"drive": {
|
|
15094
|
+
cap: "共有資料", icon: "📁", label: "Google Drive", notify: false,
|
|
15095
|
+
empty: "取り込み対象のフォルダはまだ登録されていません。",
|
|
15096
|
+
adder: { title: "+ Google Drive のフォルダを追加", placeholder: "https://drive.google.com/drive/folders/…", required: "フォルダのURLを入力してください。" },
|
|
15097
|
+
},
|
|
15098
|
+
"backlog": {
|
|
15099
|
+
cap: "課題管理", icon: "📋", label: "Backlog", notify: false,
|
|
15100
|
+
empty: "同期する接続先はまだ登録されていません(未設定)。",
|
|
15101
|
+
adder: { title: "+ 接続先を追加", placeholder: "https://<スペース>.backlog.jp/projects/<プロジェクトキー>", required: "BacklogのURLを入力してください(プロジェクトキーが入った形。数字だけのURLは受け付けません)。" },
|
|
15102
|
+
},
|
|
15103
|
+
};
|
|
15104
|
+
/** 通知先の宛先が取り込み設定(読み取り対象)に結びついていない行の 1 行(契約の unlinked: true・分類 B の文言) */
|
|
15105
|
+
var UNLINKED_NOTIFY_TEXT = "通知先としてだけ登録されています(読み取り対象には入っていません)。";
|
|
15106
|
+
/** 追加の口の入力の控え(種別 → 文字列)。描き直しで消さない(Slack は CONFIG_ADD_DRAFT) */
|
|
15107
|
+
var CONFIG_ADD_DRAFTS = {};
|
|
15108
|
+
|
|
15109
|
+
/** 取り込み設定に結びついていない通知先の 1 行(操作は付けない。行の根拠は契約の unlinked) */
|
|
15110
|
+
function tableUnlinkedRow(kind, n) {
|
|
15111
|
+
var meta = DS_TYPE[kind] || { icon: "🔗", label: kind };
|
|
15112
|
+
var badges = el("div", { class: "ds-badges" });
|
|
15113
|
+
badges.appendChild(notifyBadge());
|
|
15114
|
+
if (kind === "slack" && n.workspace) {
|
|
15115
|
+
badges.appendChild(el("span", { class: "badge ds-workspace", text: workspaceLabel(n.workspace), title: "接続した Slack ワークスペースのチャンネルです。" }));
|
|
15116
|
+
}
|
|
15117
|
+
return el("div", { class: "ds-card", "data-config-unlinked": configRowKey(kind, n.ref, n.workspace) }, [
|
|
15118
|
+
el("div", { class: "ds-card-main" }, [
|
|
15119
|
+
el("span", { class: "ds-icon", text: meta.icon }),
|
|
15120
|
+
el("span", { class: "ds-type", text: meta.label }),
|
|
15121
|
+
el("span", { class: "ds-name", text: n.ref }),
|
|
15122
|
+
]),
|
|
15123
|
+
badges,
|
|
15124
|
+
el("div", { class: "ds-toggle-msg", text: UNLINKED_NOTIFY_TEXT }),
|
|
15125
|
+
]);
|
|
15126
|
+
}
|
|
15127
|
+
|
|
15128
|
+
/** 一覧の描き直しの鍵に足す、その種別の行の「夜の更新」の札(行が変われば描き直す) */
|
|
15129
|
+
function nightReadSig(kind) {
|
|
15130
|
+
if (!CONFIG) return null;
|
|
15131
|
+
return configSourceRows(CONFIG, kind).map(function (r) {
|
|
15132
|
+
var v = configNightReadOf(LIVE_STATE, kind, r.ref, r.workspace);
|
|
15133
|
+
return v ? v.status + "@" + v.at : "";
|
|
15134
|
+
});
|
|
15135
|
+
}
|
|
15136
|
+
/** 行の「夜の更新: ◯に読めました/読めませんでした」(便 DS-8c)。夜の便りの行が無い・読まなかった行は出さない(null) */
|
|
15137
|
+
function nightReadEl(kind, r) {
|
|
15138
|
+
var v = configNightReadOf(LIVE_STATE, kind, r.ref, r.workspace);
|
|
15139
|
+
if (!v) return null;
|
|
15140
|
+
var when = rtAgo(v.at);
|
|
15141
|
+
var ok = v.status === "ok";
|
|
15142
|
+
var text = "夜の更新: " + (when ? when + (ok ? "に読めました" : "は読めませんでした") : (ok ? "読めました" : "読めませんでした"));
|
|
15143
|
+
return el("span", { class: "ds-ingest" + (ok ? "" : " ds-ingest-failed"), "data-night-read": ok ? "ok" : "failed", text: text });
|
|
15144
|
+
}
|
|
15145
|
+
/** 1 行の名前の見出し(Backlog は スペース / キー・Drive は名前が無ければ ID) */
|
|
15146
|
+
function tableRowLabel(kind, r) {
|
|
15147
|
+
if (kind === "backlog") return configBacklogLabel(r.ref);
|
|
15148
|
+
return r.name || r.ref;
|
|
15149
|
+
}
|
|
15150
|
+
|
|
15151
|
+
/** 設定の表の案件の、Slack 以外の接続先の一覧(ConfigView の kind から描く) */
|
|
15152
|
+
function tableKindList(kind) {
|
|
15153
|
+
var spec = TABLE_KINDS[kind];
|
|
15154
|
+
return liveList(
|
|
15155
|
+
spec.cap,
|
|
15156
|
+
function () {
|
|
15157
|
+
return JSON.stringify([
|
|
15158
|
+
CONFIG ? [configSourceRows(CONFIG, kind), configUnlinkedNotify(CONFIG, kind), spec.notify ? CONFIG.notify : null] : null,
|
|
15159
|
+
CONFIG_READ_FAILED, CONFIG_SAVING, CONFIG_NOTES, googleInfoSig(), nightReadSig(kind),
|
|
15160
|
+
]);
|
|
15161
|
+
},
|
|
15162
|
+
function (box) {
|
|
15163
|
+
box.setAttribute("data-config-list", kind);
|
|
15164
|
+
if (tableStateBox(box)) return;
|
|
15165
|
+
var rows = configSourceRows(CONFIG, kind);
|
|
15166
|
+
var unlinked = spec.notify ? configUnlinkedNotify(CONFIG, kind) : [];
|
|
15167
|
+
if (!rows.length && !unlinked.length) {
|
|
15168
|
+
box.appendChild(el("p", { class: "ds-empty", text: spec.empty }));
|
|
15169
|
+
return;
|
|
15170
|
+
}
|
|
15171
|
+
var list = el("div", { class: "ds-list" });
|
|
15172
|
+
rows.forEach(function (r) { list.appendChild(tableSourceRow(kind, r)); });
|
|
15173
|
+
unlinked.forEach(function (n) { list.appendChild(tableUnlinkedRow(kind, n)); });
|
|
15174
|
+
box.appendChild(list);
|
|
15175
|
+
},
|
|
15176
|
+
);
|
|
15177
|
+
}
|
|
15178
|
+
|
|
15179
|
+
/** 1 行。名前(リンク)・札・[外す]・(Google Chat は)通知先の入り切り。保存中は「保存中…」 */
|
|
15180
|
+
function tableSourceRow(kind, r) {
|
|
15181
|
+
var spec = TABLE_KINDS[kind];
|
|
15182
|
+
var key = configRowKey(kind, r.ref, r.workspace);
|
|
15183
|
+
var saving = CONFIG_SAVING[key] === true;
|
|
15184
|
+
var label = tableRowLabel(kind, r);
|
|
15185
|
+
var n = spec.notify ? configNotifyOf(CONFIG, kind, r.ref, r.workspace) : null;
|
|
15186
|
+
var on = !!(n && n.on === true);
|
|
15187
|
+
var badges = el("div", { class: "ds-badges" });
|
|
15188
|
+
if (on) badges.appendChild(notifyBadge());
|
|
15189
|
+
// 読み取りを止めている行(enabled が false)。操作は出さない(file の道でも止める操作は撤去済み)
|
|
15190
|
+
if (kind === "github" && r.enabled === false) badges.appendChild(el("span", { class: "badge", text: "読み取り停止中" }));
|
|
15191
|
+
var main = [
|
|
15192
|
+
el("span", { class: "ds-icon", text: spec.icon }),
|
|
15193
|
+
el("span", { class: "ds-type", text: spec.label }),
|
|
15194
|
+
dsNameEl(label, configSourceUrl(kind, r)),
|
|
15195
|
+
];
|
|
15196
|
+
var card = el("div", { class: "ds-card" + (saving ? " is-saving" : ""), "data-config-row": key }, [
|
|
15197
|
+
el("div", { class: "ds-card-main" }, main),
|
|
15198
|
+
badges,
|
|
15199
|
+
]);
|
|
15200
|
+
// 名前を出したときは ID も併記する(同名のフォルダを見分け、確認文の ID と画面を食い違わせない)
|
|
15201
|
+
if (kind === "drive" && r.name && r.name !== r.ref) card.appendChild(el("span", { class: "ds-sync", text: r.ref }));
|
|
15202
|
+
var nightK = nightReadEl(kind, r);
|
|
15203
|
+
if (nightK) card.appendChild(nightK);
|
|
15204
|
+
// 追加した人が自分の Google で開けると確かめた記録が無い行(栓が入っているときだけ・#1357)。確認は設定の表の行を確かめる
|
|
15205
|
+
if (SITE.intake && (kind === "drive" || kind === "google-chat")) appendGoogleVerifyUi(card, kind, r.ref);
|
|
15206
|
+
if (!SITE.intake) return card;
|
|
15207
|
+
var row = { kind: kind, ref: r.ref, workspace: r.workspace };
|
|
15208
|
+
var acts = el("div", { class: "ds-toggle" });
|
|
15209
|
+
var removeLabel = { "google-chat": "登録を解除", "github": "登録から削除", "figma": "このファイルを外す", "drive": "このフォルダを外す", "backlog": "この接続先を外す" }[kind];
|
|
15210
|
+
var remove = el("button", { class: "ds-toggle-btn ds-danger", type: "button", text: removeLabel });
|
|
15211
|
+
remove.addEventListener("click", async function () {
|
|
15212
|
+
// 最後の 1 件かどうかは押した時点で数える(描画時の件数で決めると、続けて外したとき警告が出ない)
|
|
15213
|
+
var last = configSourceRows(CONFIG, kind).length === 1;
|
|
15214
|
+
var shown = kind === "drive" ? r.ref : label;
|
|
15215
|
+
if (!(await confirmDanger(configRemoveConfirmLines(kind, shown, last), { returnTo: remove }))) return;
|
|
15216
|
+
configSave({ key: key, kind: kind, action: "remove", target: { ref: r.ref }, expectVer: r.ver, row: row });
|
|
15217
|
+
});
|
|
15218
|
+
acts.appendChild(remove);
|
|
15219
|
+
if (spec.notify) {
|
|
15220
|
+
var toggle = el("button", { class: "ds-toggle-btn", type: "button", text: on ? "通知を止める" : "通知先にする" });
|
|
15221
|
+
toggle.addEventListener("click", function () {
|
|
15222
|
+
configSave({ key: key, kind: "notify", action: "update", target: { ref: r.ref }, change: { value: !on }, row: row });
|
|
15223
|
+
});
|
|
15224
|
+
acts.appendChild(toggle);
|
|
15225
|
+
}
|
|
15226
|
+
var buttons = acts.querySelectorAll("button");
|
|
15227
|
+
for (var i = 0; i < buttons.length; i++) buttons[i].disabled = saving;
|
|
15228
|
+
card.appendChild(acts);
|
|
15229
|
+
if (saving) card.appendChild(el("div", { class: "ds-toggle-msg ds-config-saving", text: "保存中…" }));
|
|
15230
|
+
var note = saving ? null : configNoteEl(key);
|
|
15231
|
+
if (note) card.appendChild(note);
|
|
15232
|
+
return card;
|
|
15233
|
+
}
|
|
15234
|
+
|
|
15235
|
+
/**
|
|
15236
|
+
* 設定の表の案件の、Slack 以外の追加の口。入力は 1 つ(URL など)→ add の意図。
|
|
15237
|
+
* 描き直しは状態の塗り直しだけ(入力は CONFIG_ADD_DRAFTS が持つ)。保存中は欄と[追加]を止める
|
|
15238
|
+
*/
|
|
15239
|
+
function tableKindAdder(kind) {
|
|
15240
|
+
if (!SITE.intake) return null;
|
|
15241
|
+
var spec = TABLE_KINDS[kind];
|
|
15242
|
+
var addKey = "add|" + kind;
|
|
15243
|
+
if (typeof CONFIG_ADD_DRAFTS[kind] !== "string") CONFIG_ADD_DRAFTS[kind] = "";
|
|
15244
|
+
var input = el("input", { class: "sec-input", type: "text", placeholder: spec.adder.placeholder, autocomplete: "off" });
|
|
15245
|
+
var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "追加" });
|
|
15246
|
+
var msg = el("div", { class: "ds-toggle-msg" });
|
|
15247
|
+
var box = el("div", { class: "ds-addform", "data-adder": kind, "data-config-adder": kind, "data-live-cap": spec.cap }, [
|
|
15248
|
+
el("div", { class: "ds-addform-title", text: spec.adder.title }),
|
|
15249
|
+
input,
|
|
15250
|
+
el("div", { class: "ds-toggle" }, [btn, msg]),
|
|
15251
|
+
]);
|
|
15252
|
+
input.addEventListener("input", function () {
|
|
15253
|
+
CONFIG_ADD_DRAFTS[kind] = input.value;
|
|
15254
|
+
// 入力の促しは入力を始めたら消す
|
|
15255
|
+
var w = CONFIG_NOTES[addKey];
|
|
15256
|
+
if (w && w.warn) { delete CONFIG_NOTES[addKey]; paint(); }
|
|
15257
|
+
});
|
|
15258
|
+
var paint = function () {
|
|
15259
|
+
var saving = CONFIG_SAVING[addKey] === true;
|
|
15260
|
+
var ready = !!CONFIG;
|
|
15261
|
+
input.value = CONFIG_ADD_DRAFTS[kind];
|
|
15262
|
+
input.disabled = saving || !ready;
|
|
15263
|
+
btn.disabled = saving || !ready;
|
|
15264
|
+
box.classList.toggle("is-saving", saving);
|
|
15265
|
+
while (msg.firstChild) msg.removeChild(msg.firstChild);
|
|
15266
|
+
msg.className = "ds-toggle-msg";
|
|
15267
|
+
if (saving) { msg.textContent = "保存中…"; return; }
|
|
15268
|
+
var note = configNoteEl(addKey);
|
|
15269
|
+
if (note) msg.appendChild(note);
|
|
15270
|
+
};
|
|
15271
|
+
btn.addEventListener("click", function () {
|
|
15272
|
+
var value = input.value.trim();
|
|
15273
|
+
var target = configAddTarget(kind, value, "");
|
|
15274
|
+
if (!target) {
|
|
15275
|
+
configNote(addKey, "is-error", "⚠ " + spec.adder.required, "", false);
|
|
15276
|
+
CONFIG_NOTES[addKey].warn = true;
|
|
15277
|
+
paint();
|
|
15278
|
+
return;
|
|
15279
|
+
}
|
|
15280
|
+
// Google Chat は栓が入っているとき確認ページ(本人の Google)を通す。窓が意図の形で保存する(便 DS-8b)
|
|
15281
|
+
if (kind === "google-chat" && googleChatAddIntercept(value, msg, btn, function () { CONFIG_ADD_DRAFTS[kind] = ""; })) return;
|
|
15282
|
+
configSave({
|
|
15283
|
+
key: addKey, kind: kind, action: "add", target: target,
|
|
15284
|
+
// 重複の確認(confirmDuplicate)を持つ種別だけ。Drive の add は契約に無い
|
|
15285
|
+
formKind: kind === "drive" ? "" : kind,
|
|
15286
|
+
duplicateLines: kind === "drive" ? null : function () { return configDuplicateLines(kind, value); },
|
|
15287
|
+
returnTo: btn,
|
|
15288
|
+
onOk: function () { CONFIG_ADD_DRAFTS[kind] = ""; },
|
|
15289
|
+
});
|
|
15290
|
+
});
|
|
15291
|
+
box.__rebuild = paint;
|
|
15292
|
+
paint();
|
|
15293
|
+
return box;
|
|
15294
|
+
}
|
|
15295
|
+
|
|
15296
|
+
// タブに戻ったときに設定を読み直す(table だけ。file では何もしない)
|
|
15297
|
+
document.addEventListener("visibilitychange", function () {
|
|
15298
|
+
if (document.visibilityState === "hidden") return;
|
|
15299
|
+
if (viewMode() === "table") reloadLiveState();
|
|
15300
|
+
});
|
|
15301
|
+
|
|
14563
15302
|
/**
|
|
14564
15303
|
* Google Chat スペースの登録解除(#153)。removeChannelButton と同型で、
|
|
14565
15304
|
* 書き換わる先が google-chat-spaces.json・鍵が ref のみ(スペースの ref は
|
|
@@ -15531,6 +16270,9 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
15531
16270
|
var type = dispatchProbeTypeOf(capId);
|
|
15532
16271
|
// **書き込み経路が無ければ出さない**(押しても何も起きないボタンを置かない)
|
|
15533
16272
|
if (!type || !SITE.intake || !ref) return row;
|
|
16273
|
+
// 🔴 表の案件(組み直した案件)には起こす同期が無い(押すと「確認を開始できませんでした」になる)。2 つとも出さない。
|
|
16274
|
+
// 見分けは設定の画面と同じ 1 か所(viewMode)。今までの案件(file)はここを素通り=画面は 1 バイトも変わらない
|
|
16275
|
+
if (viewMode() === "table") return row;
|
|
15534
16276
|
var checkBtn = el("button", { class: "ds-toggle-btn", type: "button", text: "接続を確認" });
|
|
15535
16277
|
var syncBtn = el("button", { class: "ds-toggle-btn", type: "button", text: "今すぐ同期" });
|
|
15536
16278
|
var note = el("p", { class: "ds-probe-wait" });
|
|
@@ -15738,7 +16480,9 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
15738
16480
|
* 押した結果が消える(Gold の先出しバッジで踏んだのと同じ罠)。
|
|
15739
16481
|
*/
|
|
15740
16482
|
function backlogTargetsSection(s) {
|
|
15741
|
-
|
|
16483
|
+
// 設定の表の案件(DS-8a-2)は、接続先の正本が ConfigView(backlog の行)。file は今のまま
|
|
16484
|
+
var tableMode = viewMode() === "table";
|
|
16485
|
+
var targets = tableMode ? tableBacklogTargets() : backlogTargetsNow(s);
|
|
15742
16486
|
var editable = backlogTargetsEditable();
|
|
15743
16487
|
/** 追加・削除の応答で一覧を受け取ったか(受け取っていたら、そちらが正本) */
|
|
15744
16488
|
var adopted = false;
|
|
@@ -15771,10 +16515,11 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
15771
16515
|
]),
|
|
15772
16516
|
// 仮置きの行(#388)の印。無ければ null
|
|
15773
16517
|
pendingBadgeEl(t),
|
|
15774
|
-
// この接続先の取り込みが最後にどうなったか(段C)。測っていなければ null
|
|
15775
|
-
|
|
16518
|
+
// この接続先の取り込みが最後にどうなったか(段C)。測っていなければ null。
|
|
16519
|
+
// 設定の表の案件は、その晩の更新で読めたか(夜の便りの行)だけを出す(DS-8c・取り込み状態は持ち込まない)
|
|
16520
|
+
tableMode ? nightReadEl("backlog", t) : ingestedEl(ingestedStateOf(t.domain, t.projectKey), "課題管理"),
|
|
15776
16521
|
// 「記憶に反映」(cortex-engine #968 条件4・T2)。R が無ければ null(栓offの今の姿)
|
|
15777
|
-
memoryReflectEl(ingestedStateOf(t.domain, t.projectKey), "issues"),
|
|
16522
|
+
tableMode ? null : memoryReflectEl(ingestedStateOf(t.domain, t.projectKey), "issues"),
|
|
15778
16523
|
]);
|
|
15779
16524
|
// この接続先に届くか(#523 S1)。**「取り込み」とは別の問い**なので併記する
|
|
15780
16525
|
// ——鍵はあるが 403 でプロジェクトが見えない、は取り込みの失敗より前の話。
|
|
@@ -15790,6 +16535,25 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
15790
16535
|
return appendBacklogRowHelp(row, t, issuesRef);
|
|
15791
16536
|
}
|
|
15792
16537
|
var btn = el("button", { class: "ds-toggle-btn ds-danger", type: "button", text: "この接続先を外す" });
|
|
16538
|
+
if (tableMode) {
|
|
16539
|
+
// 保存の流れ(DS-8a-2): 確認 → 保存中… → 応答の view で描き直す。仮置きの行の出し入れはしない
|
|
16540
|
+
var tKey = configRowKey("backlog", t.ref, "");
|
|
16541
|
+
var tSaving = CONFIG_SAVING[tKey] === true;
|
|
16542
|
+
row.setAttribute("data-config-row", tKey);
|
|
16543
|
+
if (tSaving) row.classList.add("is-saving");
|
|
16544
|
+
btn.disabled = tSaving;
|
|
16545
|
+
btn.addEventListener("click", async function () {
|
|
16546
|
+
var lastT = configSourceRows(CONFIG, "backlog").length === 1;
|
|
16547
|
+
if (!(await confirmDanger(removeTargetConfirmLines(t, lastT), { returnTo: btn }))) return;
|
|
16548
|
+
configSave({ key: tKey, kind: "backlog", action: "remove", target: { ref: t.ref }, expectVer: t.ver, row: { kind: "backlog", ref: t.ref, workspace: "" } });
|
|
16549
|
+
});
|
|
16550
|
+
row.appendChild(btn);
|
|
16551
|
+
if (tSaving) row.appendChild(el("div", { class: "ds-toggle-msg ds-config-saving", text: "保存中…" }));
|
|
16552
|
+
var tNote = tSaving ? null : configNoteEl(tKey);
|
|
16553
|
+
if (tNote) row.appendChild(tNote);
|
|
16554
|
+
appendDispatchProbe(row, "issues", issuesRef);
|
|
16555
|
+
return appendBacklogRowHelp(row, t, issuesRef);
|
|
16556
|
+
}
|
|
15793
16557
|
btn.addEventListener("click", async function () {
|
|
15794
16558
|
// **最後の1件かどうかは「押した時点」で数える。** 描画時の件数で決めると、
|
|
15795
16559
|
// 2件を1件ずつ外したとき最後の1件で確認が出ない(Figma で踏んだのと同じ)
|
|
@@ -15834,6 +16598,13 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
15834
16598
|
return out;
|
|
15835
16599
|
}
|
|
15836
16600
|
|
|
16601
|
+
/** 描き直しの要否の署名。設定の表の案件は、保存中・結果の 1 行・読み込み状態も混ぜる(それで行が変わる) */
|
|
16602
|
+
function paintSigOf(list) {
|
|
16603
|
+
var base = backlogPaintSigOf(list);
|
|
16604
|
+
if (!tableMode) return base;
|
|
16605
|
+
return base + "|" + JSON.stringify([!!CONFIG, CONFIG_READ_FAILED, CONFIG_SAVING, CONFIG_NOTES, configSourceRows(CONFIG, "backlog"), nightReadSig("backlog")]);
|
|
16606
|
+
}
|
|
16607
|
+
|
|
15837
16608
|
function paint() {
|
|
15838
16609
|
while (listEl.firstChild) listEl.removeChild(listEl.firstChild);
|
|
15839
16610
|
// 一手・札・鍵カードの見え方が引く行の控え(行を描く前に入れ替える)
|
|
@@ -15866,6 +16637,8 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
15866
16637
|
// **0件を黙って見せない。** 表示が無いと「まだ何も無い」のか「出し忘れ」なのか
|
|
15867
16638
|
// 区別できない。**一覧の外**(Figma / Drive と同じ)に、件数に応じて出し入れする
|
|
15868
16639
|
// ——出しっぱなしにすると、設定済みの案件にも「未設定」と書いた行が残る
|
|
16640
|
+
// 設定の表の案件で、設定がまだ無い間は「未設定」と言わない(読み込み中/読めませんでした)
|
|
16641
|
+
if (tableMode) emptyEl.textContent = CONFIG ? EMPTY_TEXT : (CONFIG_READ_FAILED ? "読めませんでした" : "読み込み中…");
|
|
15869
16642
|
var here = emptyEl.parentNode === box;
|
|
15870
16643
|
if (!targets.length && !here) box.insertBefore(emptyEl, listEl.nextSibling);
|
|
15871
16644
|
else if (targets.length && here) box.removeChild(emptyEl);
|
|
@@ -16052,7 +16825,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
16052
16825
|
var box = el("div", { "data-live-cap": REGISTERED_CAP_NAMES.issues }, [listEl]);
|
|
16053
16826
|
paint();
|
|
16054
16827
|
/** いま描いてある取り込みの状態(段C)。接続先の集合とは別に変わるので、別に覚える */
|
|
16055
|
-
var lastIngestSig =
|
|
16828
|
+
var lastIngestSig = paintSigOf(targets);
|
|
16056
16829
|
/**
|
|
16057
16830
|
* 取り直しで新しい値が届いたときの差し替え(段階1)。**一覧だけを描き直す。**
|
|
16058
16831
|
*
|
|
@@ -16065,17 +16838,17 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
16065
16838
|
// ただし行の札・一手が読む外の事実(経路・固定 IP・キーの登録状況・リアルタイムの受信)は
|
|
16066
16839
|
// 集合と別に変わるので、署名が変わったときは**行だけ**描き直す(c-07 の 2 本目・設計レビュー🔴1)
|
|
16067
16840
|
if (adopted) {
|
|
16068
|
-
var sigAdopted =
|
|
16841
|
+
var sigAdopted = paintSigOf(targets);
|
|
16069
16842
|
if (sigAdopted === lastIngestSig) return;
|
|
16070
16843
|
lastIngestSig = sigAdopted;
|
|
16071
16844
|
paint();
|
|
16072
16845
|
return;
|
|
16073
16846
|
}
|
|
16074
|
-
var next = backlogTargetsNow(internalSourceOf(REGISTERED_CAP_NAMES.issues));
|
|
16847
|
+
var next = tableMode ? tableBacklogTargets() : backlogTargetsNow(internalSourceOf(REGISTERED_CAP_NAMES.issues));
|
|
16075
16848
|
// 🔴 **変化が無いと言えるのは、接続先の集合と取り込みの状態が両方同じときだけ(段C)。**
|
|
16076
16849
|
// 世代だけで判断すると、**応答を待たずにモーダルを開いた人には取り込みが永久に出ない**
|
|
16077
16850
|
//(届いた応答で集合は変わらないので、ここで飛ばされる)。ingestSigOf の項に経緯
|
|
16078
|
-
var sig =
|
|
16851
|
+
var sig = paintSigOf(next);
|
|
16079
16852
|
if (targetsGenerationOf(next) === targetsGenerationOf(targets) && sig === lastIngestSig) return;
|
|
16080
16853
|
targets = next;
|
|
16081
16854
|
lastIngestSig = sig;
|
|
@@ -16088,14 +16861,17 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
16088
16861
|
* 応答の一覧を採った後でも、行の札・一手は最新の事実で描かれる(lastIngestSig も揃える)
|
|
16089
16862
|
*/
|
|
16090
16863
|
box.__repaintRows = function () {
|
|
16091
|
-
lastIngestSig =
|
|
16864
|
+
lastIngestSig = paintSigOf(targets);
|
|
16092
16865
|
paint();
|
|
16093
16866
|
};
|
|
16094
16867
|
BACKLOG_UI.repaintRows = box.__repaintRows;
|
|
16095
16868
|
BACKLOG_UI.msg = msg;
|
|
16096
16869
|
// 接続先がまだ無い案件の受け皿は、0件の1行の直後に置く(行が無いので中へ入れられない)
|
|
16097
16870
|
if (KEY_SLOTS.backlog[""]) box.appendChild(KEY_SLOTS.backlog[""]);
|
|
16098
|
-
if (editable) {
|
|
16871
|
+
if (editable && tableMode) {
|
|
16872
|
+
var backlogAdder = tableKindAdder("backlog");
|
|
16873
|
+
if (backlogAdder) box.appendChild(backlogAdder);
|
|
16874
|
+
} else if (editable) {
|
|
16099
16875
|
box.appendChild(addForm({
|
|
16100
16876
|
// **「追加」に戻す(#127)。** 1件しか持てなかった頃は「保存」(置き換え)だったが、
|
|
16101
16877
|
// 接続先は本当に足せるようになった。押した結果は一覧の行として出る
|
|
@@ -16214,6 +16990,17 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
16214
16990
|
}
|
|
16215
16991
|
|
|
16216
16992
|
/** 接続先を外すときの確認。**最後の1件は、止まるものを名指しで伝える** */
|
|
16993
|
+
/** 設定の表の案件の Backlog の接続先(ConfigView の backlog の行。ref は スペース/プロジェクトキー) */
|
|
16994
|
+
function tableBacklogTargets() {
|
|
16995
|
+
var out = [];
|
|
16996
|
+
configSourceRows(CONFIG, "backlog").forEach(function (r) {
|
|
16997
|
+
var ref = String(r.ref || "");
|
|
16998
|
+
var i = ref.indexOf("/");
|
|
16999
|
+
if (i <= 0 || i === ref.length - 1) return;
|
|
17000
|
+
out.push({ domain: ref.slice(0, i), projectKey: ref.slice(i + 1), ref: ref, ver: r.ver });
|
|
17001
|
+
});
|
|
17002
|
+
return out;
|
|
17003
|
+
}
|
|
16217
17004
|
function removeTargetConfirmLines(t, last) {
|
|
16218
17005
|
var lines = [
|
|
16219
17006
|
"「" + t.domain + " / " + t.projectKey + "」を同期の対象から外します。",
|
|
@@ -16338,7 +17125,14 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
16338
17125
|
if (s.kind === "共有資料" && usable("共有資料", "google-drive")) {
|
|
16339
17126
|
// 状態の出どころは資料の取り込み設定(materials-config.json)だが、会議と同じく
|
|
16340
17127
|
// 文面には出さない(2026-08-12 の文言原則)。画面から直せない状態の次の一歩は担当者への連絡。
|
|
16341
|
-
if (
|
|
17128
|
+
if (viewMode() === "table") {
|
|
17129
|
+
// 設定の表の案件(DS-8a-2): 一覧は ConfigView の drive の行・追加は保存の流れで。
|
|
17130
|
+
// driveState は file の設定ファイル由来なので、unset・broken の案内より先に分ける
|
|
17131
|
+
box.appendChild(tableKindList("drive"));
|
|
17132
|
+
// 栓が入っているときは[Google で選ぶ](本人の Google で開けることの確認を通す・便 DS-8b)。切のあいだは URL の追加欄
|
|
17133
|
+
if (SITE.intake) box.appendChild(googleAwareSlot(function () { return tableKindAdder("drive") || el("div", {}); }, googleDriveAddBox));
|
|
17134
|
+
used = true;
|
|
17135
|
+
} else if (s.driveState === "unset") {
|
|
16342
17136
|
box.appendChild(el("p", { class: "sec-how", text: "このプロジェクトでは共有資料の取り込みがまだ用意されていません。cortex の担当者にご連絡ください。" }));
|
|
16343
17137
|
used = true;
|
|
16344
17138
|
} else if (s.driveState === "broken") {
|
|
@@ -16455,7 +17249,20 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
16455
17249
|
}
|
|
16456
17250
|
}
|
|
16457
17251
|
|
|
16458
|
-
if (s.kind === "デザイン" && usable("デザイン", "figma")) {
|
|
17252
|
+
if (s.kind === "デザイン" && usable("デザイン", "figma") && viewMode() === "table") {
|
|
17253
|
+
// 設定の表の案件(DS-8a-2): 一覧は ConfigView の figma の行・追加は保存の流れで。
|
|
17254
|
+
// 鍵の欄(KEY_SLOTS.figma)は案件に 1 本で行の数に依らないので、file の道と同じく一覧の直後に置く
|
|
17255
|
+
var figGroupT = el("div", { class: "ds-scope-group" });
|
|
17256
|
+
box.appendChild(figGroupT);
|
|
17257
|
+
figGroupT.appendChild(tableKindList("figma"));
|
|
17258
|
+
if (!KEY_SLOTS.figma) {
|
|
17259
|
+
KEY_SLOTS.figma = el("div", { class: "ds-keyrow ds-keyrow-list" });
|
|
17260
|
+
figGroupT.appendChild(KEY_SLOTS.figma);
|
|
17261
|
+
}
|
|
17262
|
+
var figAdder = tableKindAdder("figma");
|
|
17263
|
+
if (figAdder) box.appendChild(figAdder);
|
|
17264
|
+
used = true;
|
|
17265
|
+
} else if (s.kind === "デザイン" && usable("デザイン", "figma")) {
|
|
16459
17266
|
// 登録済みファイルを1件ずつ外せるようにする(6ファイル抱えている案件があり、
|
|
16460
17267
|
// 古いスプリントのファイルを外したいのが実際に起きる操作)。
|
|
16461
17268
|
// **一覧は開いたままでも差し替わる箱に入れる(段階1)。** 追加は「モーダルを開いたまま」
|
|
@@ -16654,6 +17461,20 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
16654
17461
|
* カタログはこの製品が実装しているアダプタの定義なので、出したボタンが
|
|
16655
17462
|
* 「押せるのに必ず失敗する」ことはない。
|
|
16656
17463
|
*/
|
|
17464
|
+
/** Slack のチャンネルが別のプロジェクトでも追加されていたときの確認文(file の追加欄と table の追加欄で同じ文) */
|
|
17465
|
+
function slackDuplicateConfirmLines(value) {
|
|
17466
|
+
return [
|
|
17467
|
+
"このチャンネルは、別のプロジェクトでも追加されています。",
|
|
17468
|
+
"",
|
|
17469
|
+
"このまま追加すると、同じ会話が両方のプロジェクトの Decision・用語集に出ます。",
|
|
17470
|
+
"",
|
|
17471
|
+
"全体定例のように複数のプロジェクトに関係するチャンネルなら、これは正しい状態です。",
|
|
17472
|
+
"別のチャンネルと取り違えていないか、貼ったURLを確かめてください:",
|
|
17473
|
+
String(value || ""),
|
|
17474
|
+
"",
|
|
17475
|
+
"追加しますか?",
|
|
17476
|
+
];
|
|
17477
|
+
}
|
|
16657
17478
|
function renderExternalAdders() {
|
|
16658
17479
|
var box = el("div", {});
|
|
16659
17480
|
if (!SITE.intake) return box;
|
|
@@ -16699,19 +17520,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
16699
17520
|
// **他案件の名前は出さない。** この画面の操作はその案件の Basic 認証を通れる人
|
|
16700
17521
|
// (顧客を含む)なら誰でもできるので、案件名の列挙に使わせない。
|
|
16701
17522
|
// **貼ったURLを本文に出す。** 「URLを確かめてください」と言うのに出ていなかった
|
|
16702
|
-
duplicateConfirmLines:
|
|
16703
|
-
return [
|
|
16704
|
-
"このチャンネルは、別のプロジェクトでも追加されています。",
|
|
16705
|
-
"",
|
|
16706
|
-
"このまま追加すると、同じ会話が両方のプロジェクトの Decision・用語集に出ます。",
|
|
16707
|
-
"",
|
|
16708
|
-
"全体定例のように複数のプロジェクトに関係するチャンネルなら、これは正しい状態です。",
|
|
16709
|
-
"別のチャンネルと取り違えていないか、貼ったURLを確かめてください:",
|
|
16710
|
-
String(value || ""),
|
|
16711
|
-
"",
|
|
16712
|
-
"追加しますか?",
|
|
16713
|
-
];
|
|
16714
|
-
},
|
|
17523
|
+
duplicateConfirmLines: slackDuplicateConfirmLines,
|
|
16715
17524
|
duplicateKind: "slack",
|
|
16716
17525
|
adder: "slack",
|
|
16717
17526
|
}));
|
|
@@ -16946,6 +17755,13 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
16946
17755
|
// **送信時の仮置きを巻き戻す(#968 E)。** 先に戻す——重複の確認の画面は入力欄の値を読むし、
|
|
16947
17756
|
// 「既に登録されています」の分岐は下で仮置きを書き直す
|
|
16948
17757
|
revert();
|
|
17758
|
+
// 設定の表に組み直された案件(DS-8a-1): 旧キーが断られたら、応答の view で table の画面に切り替える
|
|
17759
|
+
if (configAdoptOutdated(res.status, j)) {
|
|
17760
|
+
endBusy();
|
|
17761
|
+
msg.className = "ds-toggle-msg";
|
|
17762
|
+
msg.textContent = "";
|
|
17763
|
+
return;
|
|
17764
|
+
}
|
|
16949
17765
|
// **確認すれば覆せる断り方(2026-08-20・Slack の重複だけ)。**
|
|
16950
17766
|
//
|
|
16951
17767
|
// 全体定例のように複数案件に関係するチャンネルは実在するのに、
|
|
@@ -18195,7 +19011,8 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
18195
19011
|
var startedAt = Date.now();
|
|
18196
19012
|
callIntake(
|
|
18197
19013
|
"state",
|
|
18198
|
-
|
|
19014
|
+
// 設定の表の案件(table)では fresh を送らない(設定は据え置かれない・DS-8a-1)
|
|
19015
|
+
useFresh && viewMode() !== "table" ? { caps: ALL_STATE_CAPS, fresh: true } : { caps: ALL_STATE_CAPS },
|
|
18199
19016
|
{ signal: ctrl ? ctrl.signal : undefined },
|
|
18200
19017
|
)
|
|
18201
19018
|
// **非2xxも失敗として扱う。** ビューアとLambdaの版は独立に進むので、
|
|
@@ -18212,6 +19029,9 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
18212
19029
|
// **fleet より先に読む。** fleet-status.json をまだ持たない案件(立ち上げ直後)は
|
|
18213
19030
|
// 下で throw するが、リアルタイム同期の可否は索引由来なので独立に決まる
|
|
18214
19031
|
LIVE_REALTIME = (j && j.realtime) || null;
|
|
19032
|
+
// 設定の表の案件(DS-8a-1): 応答の mode と ConfigView を受ける。fleet より先に読む(まとめ表の無い案件でも描ける)。
|
|
19033
|
+
// mode の無い応答は file(CONFIG を持たない=今の道のまま)
|
|
19034
|
+
configFromState(j);
|
|
18215
19035
|
// 同期経路と固定 IP(#516)。**サーバが知らない値・返さない回は null(読めない)**
|
|
18216
19036
|
// ——undefined のままにすると「確認しています」が永久に残る
|
|
18217
19037
|
LIVE_BACKLOG_ROUTE = backlogRouteOf(j && j.backlogRoute);
|
|
@@ -18281,6 +19101,8 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
18281
19101
|
// 同期経路(#516)。**一度も答えが無いまま失敗したら「読めない」に倒す**(切り替えさせない)。
|
|
18282
19102
|
// 前の答え(または切り替え直後の応答の値)があるなら、それを捨てない
|
|
18283
19103
|
if (LIVE_BACKLOG_ROUTE === undefined) LIVE_BACKLOG_ROUTE = null;
|
|
19104
|
+
// 設定の表の案件(DS-8a-1)で設定をまだ読めていなければ「読めませんでした」(夜の表で代わりに描かない)
|
|
19105
|
+
if (viewMode() === "table" && !CONFIG) CONFIG_READ_FAILED = true;
|
|
18284
19106
|
// 🔴 **確認中に倒した図を焼いた表示へ戻す(#256・設計v2 の3)。** ライブが
|
|
18285
19107
|
// 取れなかったときのフォールバックはここに残る——戻さないと「確認中…」の
|
|
18286
19108
|
// ままの図が残り、オフラインでは**何も分からない画面**になる。
|
|
@@ -20235,7 +21057,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
20235
21057
|
* 🔴 **縮退は 1 つの形。** 帳簿が無い・壊れ・版違い・その記録の行が無い → 同じ節に空の文を出す(今の欄には戻さない)。
|
|
20236
21058
|
* 出す記録は 4 区画の記録だけ(Home・ストーリー・README・雛形には出さない)。
|
|
20237
21059
|
*/
|
|
20238
|
-
var CH_RECORD_PATH = /^Cortex\\/(Decisions|Glossary|Members|Rules)\\/records\\/[^/]+\\.md$/;
|
|
21060
|
+
var CH_RECORD_PATH = /^(?:Cortex|Memory)\\/(Decisions|Glossary|Members|Rules)\\/records\\/[^/]+\\.md$/;
|
|
20239
21061
|
var CH_FIRST = 5;
|
|
20240
21062
|
var CH_MAX = 50;
|
|
20241
21063
|
var CH_EMPTY = "更新履歴はまだありません";
|
|
@@ -20694,7 +21516,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
20694
21516
|
//
|
|
20695
21517
|
// 出し分けはサーバの DELETABLE_SECTIONS と同じ4区画。ここは「押せないボタンを
|
|
20696
21518
|
// 出さない」ためのもので、**守りはサーバ側**(promote と同じ関係)。
|
|
20697
|
-
var DELETABLE_PATH = /^Cortex\\/(Decisions|Glossary|Members|Rules)\\/records\\/[^/]+\\.md$/;
|
|
21519
|
+
var DELETABLE_PATH = /^(?:Cortex|Memory)\\/(Decisions|Glossary|Members|Rules)\\/records\\/[^/]+\\.md$/;
|
|
20698
21520
|
function deletableRecordPath(rec) {
|
|
20699
21521
|
if (!SITE.intake) return "";
|
|
20700
21522
|
var p = repoPathOf(rec);
|
|
@@ -20811,7 +21633,7 @@ ${story_1.splitByNameLinks.toString()}
|
|
|
20811
21633
|
Rules: "このルールを消し、以後同じ題で作り直さないよう「除外ルール」の一覧に足します。",
|
|
20812
21634
|
};
|
|
20813
21635
|
function deleteConfirmLines(rec, path) {
|
|
20814
|
-
var m = /^Cortex\\/([^/]+)\\//.exec(path || "");
|
|
21636
|
+
var m = /^(?:Cortex|Memory)\\/([^/]+)\\//.exec(path || "");
|
|
20815
21637
|
var dir = m ? m[1] : "";
|
|
20816
21638
|
var title = (rec && rec.title) || (rec && rec.fileName) || "(題なし)";
|
|
20817
21639
|
return [
|