@takagaki/cortex-decisions-viewer 0.4.76 → 0.4.78
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/build.js +23 -4
- package/dist/render.js +282 -62
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -312,18 +312,37 @@ async function readFleetSources(repoRoot) {
|
|
|
312
312
|
// **画面に現在値を出すために要る。** 「接続先を変更」フォームが入力欄だけだったため、
|
|
313
313
|
// 実機で「何もセットされていない」と読まれ、同じURLが貼り直された(#127)。
|
|
314
314
|
//
|
|
315
|
-
//
|
|
316
|
-
// features
|
|
315
|
+
// **接続先そのものしか通さない。** 生成側は configPath / sections / strays /
|
|
316
|
+
// features も持っているが、画面が使うのは接続先だけ。素通しにすると、engine が
|
|
317
317
|
// このオブジェクトに足したものが顧客の画面データへ黙って流れる(Lambda 側の
|
|
318
318
|
// FLEET_PUBLIC_KEYS と同じ考え方で、こちらも許可リストにする)。
|
|
319
|
+
// 配列の要素も同じ2つに絞る——要素だけ素通しにすると、許可リストを通したつもりで
|
|
320
|
+
// 中身が漏れる(externalSources で実際にやった間違い)。
|
|
321
|
+
//
|
|
322
|
+
// **接続先は複数ありうる(#127)。** 1案件が複数の Backlog プロジェクトを同期できる
|
|
323
|
+
// ようになったので、`targets` があればそれを一覧の正本にする。**単数の2つは残す**
|
|
324
|
+
// (先頭と同じ値)——`targets` を載せない古いスナップショットでも現在値を出せるように、
|
|
325
|
+
// 読み手の両対応と同じ約束を画面データでも守る。
|
|
319
326
|
//
|
|
320
327
|
// **値が無い形は落とす。** 正本もミラーも持たない案件では `{ source: "secrets" }` だけが
|
|
321
328
|
// 来る。空の器を通すと、画面は「接続先の情報がある」と判断して空文字を並べることになる
|
|
322
329
|
const bt = obj.backlogTarget != null && typeof obj.backlogTarget === "object" && !Array.isArray(obj.backlogTarget)
|
|
323
330
|
? obj.backlogTarget
|
|
324
331
|
: null;
|
|
325
|
-
const
|
|
326
|
-
|
|
332
|
+
const oneTarget = (v) => {
|
|
333
|
+
if (v == null || typeof v !== "object")
|
|
334
|
+
return null;
|
|
335
|
+
const o = v;
|
|
336
|
+
return typeof o.domain === "string" && o.domain && typeof o.projectKey === "string" && o.projectKey
|
|
337
|
+
? { domain: o.domain, projectKey: o.projectKey }
|
|
338
|
+
: null;
|
|
339
|
+
};
|
|
340
|
+
const btTargets = Array.isArray(bt?.targets)
|
|
341
|
+
? bt.targets.map(oneTarget).filter((t) => t !== null)
|
|
342
|
+
: [];
|
|
343
|
+
const btHead = oneTarget(bt) ?? btTargets[0] ?? null;
|
|
344
|
+
const backlogTarget = btHead
|
|
345
|
+
? { ...btHead, ...(btTargets.length ? { targets: btTargets } : {}) }
|
|
327
346
|
: undefined;
|
|
328
347
|
const secretSources = obj.secretSources && typeof obj.secretSources === "object"
|
|
329
348
|
? obj.secretSources
|
package/dist/render.js
CHANGED
|
@@ -1304,6 +1304,12 @@ a.ds-name:hover { text-decoration: underline; }
|
|
|
1304
1304
|
.ds-current-label { color: var(--muted); flex: 0 0 auto; }
|
|
1305
1305
|
.ds-current-value { color: var(--text); font-weight: 600; word-break: break-all; }
|
|
1306
1306
|
.ds-current-value.is-unset { color: var(--muted); font-weight: 400; }
|
|
1307
|
+
/* 接続先の一覧(#127)。**1件のときは従来どおりラベルの右に1行**——単数の案件で
|
|
1308
|
+
見た目が変わると、複数対応と関係のない全案件に「何か変わった」と読ませる */
|
|
1309
|
+
.ds-target-list { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
1310
|
+
.ds-target-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
|
1311
|
+
/* 行の中のボタンは行に揃える(.ds-toggle の縦マージンは節と節の間隔なので効かせない) */
|
|
1312
|
+
.ds-target-row .ds-toggle-btn { flex: 0 0 auto; }
|
|
1307
1313
|
/* 入力欄のすぐ下。貼る直前に読める位置に置く */
|
|
1308
1314
|
.sec-note { color: var(--muted); font-size: 12px; line-height: 1.7; margin: 6px 0 0; }
|
|
1309
1315
|
.sec-how p { margin: 0; }
|
|
@@ -1323,6 +1329,8 @@ a.ds-name:hover { text-decoration: underline; }
|
|
|
1323
1329
|
.rt-steps li { margin: 2px 0; }
|
|
1324
1330
|
/* URLは長いので、入力欄を伸ばしてボタンを右に置く(折り返しても崩れないように wrap) */
|
|
1325
1331
|
.rt-url-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
1332
|
+
.rt-copy { margin: 6px 0 10px; }
|
|
1333
|
+
.rt-copy-text { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #f3f4f6; border: 1px solid #e5e7eb; border-radius: 6px; padding: 4px 10px; user-select: all; }
|
|
1326
1334
|
.rt-url { flex: 1 1 260px; margin-bottom: 0; font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; }
|
|
1327
1335
|
/* 稼働中の表示(#123)。緑は「動いている」の合図で、見出しの右に置いて先に目に入れる */
|
|
1328
1336
|
.rt-live-badge {
|
|
@@ -1537,6 +1545,46 @@ ${capabilityState.toString()}
|
|
|
1537
1545
|
footerEl.textContent = "生成: " + SITE.generatedAt +
|
|
1538
1546
|
(SITE.repoBaseUrl ? " / " + SITE.repoBaseUrl.split("://").pop() + " (" + SITE.branch + ")" : "");
|
|
1539
1547
|
|
|
1548
|
+
/**
|
|
1549
|
+
* 「貼り付けて使う文字列」を、選べる形+コピーボタンで出す。
|
|
1550
|
+
*
|
|
1551
|
+
* **手で打たせない。** 「/invite @Cortex」のような短いものでも、打ち間違いは黙って失敗し、
|
|
1552
|
+
* 利用者には「招待したのに読まれない」としか見えない。
|
|
1553
|
+
* clipboard API が使えない環境(http・古いブラウザ・権限拒否)では、**選択までしてやって**
|
|
1554
|
+
* 次の一手を伝える(何も起きないのが一番悪い)。
|
|
1555
|
+
*/
|
|
1556
|
+
function copyRow(text) {
|
|
1557
|
+
// **input と button を使わない。** 追加フォームの中に置くので、input を足すと
|
|
1558
|
+
// 「先頭がURL欄・末尾が名前欄」、button を足すと「最初のボタンが送信」という
|
|
1559
|
+
// フォームの並びが崩れる(実際にテストが壊れて気づいた)。
|
|
1560
|
+
// 表示は code、コピーは role="button" のリンクで代える。
|
|
1561
|
+
var field = el("code", { class: "rt-copy-text", text: text });
|
|
1562
|
+
var btn = el("a", { class: "ds-toggle-btn", role: "button", tabindex: "0", text: "コピー" });
|
|
1563
|
+
var msg = el("div", { class: "ds-toggle-msg" });
|
|
1564
|
+
btn.addEventListener("click", function () {
|
|
1565
|
+
var done = function () { msg.className = "ds-toggle-msg is-ok"; msg.textContent = "✓ コピーしました。"; };
|
|
1566
|
+
var fallback = function () {
|
|
1567
|
+
// clipboard が使えない環境では、せめて**文字列を選択状態にして**次の一手を伝える
|
|
1568
|
+
try {
|
|
1569
|
+
var r = document.createRange();
|
|
1570
|
+
r.selectNodeContents(field);
|
|
1571
|
+
var sel = window.getSelection();
|
|
1572
|
+
sel.removeAllRanges();
|
|
1573
|
+
sel.addRange(r);
|
|
1574
|
+
if (document.execCommand && document.execCommand("copy")) return done();
|
|
1575
|
+
} catch (e) { /* 下の案内へ */ }
|
|
1576
|
+
msg.className = "ds-toggle-msg is-error";
|
|
1577
|
+
msg.textContent = "⚠ 自動コピーできませんでした。選択された文字列を手でコピーしてください。";
|
|
1578
|
+
};
|
|
1579
|
+
try {
|
|
1580
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1581
|
+
navigator.clipboard.writeText(text).then(done, fallback);
|
|
1582
|
+
} else fallback();
|
|
1583
|
+
} catch (e) { fallback(); }
|
|
1584
|
+
});
|
|
1585
|
+
return el("div", { class: "rt-copy" }, [el("div", { class: "rt-url-row" }, [field, btn]), msg]);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1540
1588
|
function el(tag, props, children) {
|
|
1541
1589
|
var e = document.createElement(tag);
|
|
1542
1590
|
if (props) Object.keys(props).forEach(function (k) {
|
|
@@ -1941,7 +1989,9 @@ ${capabilityState.toString()}
|
|
|
1941
1989
|
};
|
|
1942
1990
|
var DS_GATE = {
|
|
1943
1991
|
"ok": { text: "接続OK", cls: "ds-gate-ok" },
|
|
1944
|
-
|
|
1992
|
+
// **状態だけでなく、次にすることを書く。** 「bot未招待」では何をすればよいか分からない
|
|
1993
|
+
// (公開チャンネルでも招待が要ることは Slack の仕様で、知らない人のほうが多い)
|
|
1994
|
+
"not_in_channel": { text: "Slackで招待してください", cls: "ds-gate-warn" },
|
|
1945
1995
|
"unreachable": { text: "アクセス不可", cls: "ds-gate-warn" },
|
|
1946
1996
|
"no_token": { text: "トークン未設定", cls: "ds-gate-muted" },
|
|
1947
1997
|
"unknown": { text: "不明", cls: "ds-gate-muted" },
|
|
@@ -2875,31 +2925,200 @@ ${capabilityState.toString()}
|
|
|
2875
2925
|
}
|
|
2876
2926
|
|
|
2877
2927
|
/**
|
|
2878
|
-
* いま同期している Backlog
|
|
2928
|
+
* いま同期している Backlog の接続先の一覧(#127)。**1件でも配列で返す。**
|
|
2879
2929
|
*
|
|
2880
|
-
* **
|
|
2881
|
-
*
|
|
2882
|
-
*
|
|
2883
|
-
*
|
|
2930
|
+
* **3つの経路から引く。** 正本は fleet の backlogTarget で、接続先が複数ある案件では
|
|
2931
|
+
* targets に全件が入る。targets を載せていないスナップショット(複数対応より前に
|
|
2932
|
+
* 生成されたもの)では単数の2つを1件として読み、それも無ければ内部ソースの url
|
|
2933
|
+
* (同じ値から機械導出されたプロジェクトURL)を解析して補う。
|
|
2884
2934
|
* ここを backlogTarget だけにすると、**設定済みの案件に「未設定」と出す**
|
|
2885
|
-
*
|
|
2886
|
-
* 画面の側からもう一度作ることになる。
|
|
2935
|
+
* ——直したい誤読(何もセットされていないと読んで貼り直す)を画面の側から作ることになる。
|
|
2887
2936
|
*
|
|
2888
|
-
*
|
|
2889
|
-
*
|
|
2937
|
+
* **半端な要素は落とす。** ドメインかキーの片方しか無い接続先は行として読めない
|
|
2938
|
+
* (「cm1.backlog.jp / 」が並ぶ)。空になったら「未設定」に倒す。
|
|
2890
2939
|
*/
|
|
2891
|
-
function
|
|
2940
|
+
function backlogTargetsNow(s) {
|
|
2892
2941
|
var t = fleetVal("backlogTarget");
|
|
2893
|
-
|
|
2894
|
-
var
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2942
|
+
var raw = t && Array.isArray(t.targets) ? t.targets : t ? [t] : [];
|
|
2943
|
+
var out = [];
|
|
2944
|
+
for (var i = 0; i < raw.length; i++) {
|
|
2945
|
+
var x = raw[i];
|
|
2946
|
+
if (x && x.domain && x.projectKey) out.push({ domain: String(x.domain), projectKey: String(x.projectKey) });
|
|
2947
|
+
}
|
|
2948
|
+
if (out.length) return out;
|
|
2949
|
+
var u = (s && s.url && backlogTargetOf(s.url)) || null;
|
|
2950
|
+
return u ? [u] : [];
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
/**
|
|
2954
|
+
* 接続先の一覧を1つの文字列にする(**サーバへ送る「いま画面が見ている世代」**・#127)。
|
|
2955
|
+
*
|
|
2956
|
+
* 追加・削除は一覧に対する操作なので、後勝ちで上書きすると**他の人が足した接続先が
|
|
2957
|
+
* 黙って消える**。サーバは受け取った世代といまの正本を突き合わせ、違えば 409 で断る。
|
|
2958
|
+
*
|
|
2959
|
+
* **並びと大小は問わない**(サーバ側の targetsGeneration と同じ畳み方にすること)。
|
|
2960
|
+
* 片方だけが大文字化していると、正しい操作が毎回 409 になって**再読み込みしても直らない**
|
|
2961
|
+
* ——食い違わないことは backlog-targets-ui.test.mjs が実物どうしで突き合わせている。
|
|
2962
|
+
*
|
|
2963
|
+
* **クロージャを持たない純関数に保つこと**(テストが配信物から切り出して直接呼ぶ)。
|
|
2964
|
+
*/
|
|
2965
|
+
function targetsGenerationOf(list) {
|
|
2966
|
+
return (list || [])
|
|
2967
|
+
.filter(function (t) { return t && t.domain && t.projectKey; })
|
|
2968
|
+
.map(function (t) { return String(t.domain).toLowerCase() + "/" + String(t.projectKey).toUpperCase(); })
|
|
2969
|
+
.sort()
|
|
2970
|
+
.join(",");
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
/**
|
|
2974
|
+
* 他の人が先に一覧を変えていたときの文面(#127)。
|
|
2975
|
+
*
|
|
2976
|
+
* **サーバの文面をそのまま出さない。** サーバは「画面を再読み込みして…」と案内するが、
|
|
2977
|
+
* 画面は応答に添えられた一覧でその場で追いついている。再読み込みを促すと、
|
|
2978
|
+
* 済んでいることをやらせる(そして再読み込みしても同じ表示になる=堂々巡りに見える)。
|
|
2979
|
+
*/
|
|
2980
|
+
var TARGETS_RACE_NOTE =
|
|
2981
|
+
"他の人が先に接続先を変更しました。最新の一覧に更新しましたので、内容を確かめてもう一度お試しください。";
|
|
2982
|
+
|
|
2983
|
+
/**
|
|
2984
|
+
* 「課題管理」カードの接続先(一覧+追加+削除・#127)。
|
|
2985
|
+
*
|
|
2986
|
+
* **一覧は応答で描き直す。** 追加・削除の応答には必ずいまの一覧が添えられている
|
|
2987
|
+
* (成功も、重複で断られたときも、世代がずれていたときも)。夜間スナップショットの
|
|
2988
|
+
* 到着を待つと、押した直後は何も変わらないので「効いていない」と読まれ、もう一度
|
|
2989
|
+
* 押して「すでに登録されています」になる——他の追加フォームで実際に起きたこと。
|
|
2990
|
+
*
|
|
2991
|
+
* **結果の表示は一覧の外に置く。** 行の中に置くと、一覧を描き直した瞬間に
|
|
2992
|
+
* 押した結果が消える(Gold の先出しバッジで踏んだのと同じ罠)。
|
|
2993
|
+
*/
|
|
2994
|
+
function backlogTargetsSection(s) {
|
|
2995
|
+
var targets = backlogTargetsNow(s);
|
|
2996
|
+
// 書き込み先(正本の置き場)が決まらない案件では操作を出さない。判定は呼び出し側と
|
|
2997
|
+
// 同じ s.url(同期ミラーが1つでもあるか)で、フォームを出さない条件と揃える
|
|
2998
|
+
var editable = !!(s && s.url);
|
|
2999
|
+
|
|
3000
|
+
var listEl = el("div", { class: "ds-target-list" });
|
|
3001
|
+
var current = el("div", { class: "ds-current" }, [
|
|
3002
|
+
el("span", { class: "ds-current-label", text: "現在の接続先" }),
|
|
3003
|
+
listEl,
|
|
3004
|
+
]);
|
|
3005
|
+
// 追加・削除の結果はここに1つだけ出す(行を描き直しても消えない位置)
|
|
3006
|
+
var msg = el("div", { class: "ds-toggle-msg" });
|
|
3007
|
+
|
|
3008
|
+
function removeRow(t) {
|
|
3009
|
+
var row = el("div", { class: "ds-target-row" }, [
|
|
3010
|
+
el("span", { class: "ds-current-value", text: t.domain + " / " + t.projectKey }),
|
|
3011
|
+
]);
|
|
3012
|
+
if (!editable) return row;
|
|
3013
|
+
var btn = el("button", { class: "ds-toggle-btn ds-danger", type: "button", text: "この接続先を外す" });
|
|
3014
|
+
btn.addEventListener("click", function () {
|
|
3015
|
+
// **最後の1件かどうかは「押した時点」で数える。** 描画時の件数で決めると、
|
|
3016
|
+
// 2件を1件ずつ外したとき最後の1件で確認が出ない(Figma で踏んだのと同じ)
|
|
3017
|
+
var last = targets.length === 1;
|
|
3018
|
+
if (!confirmDanger(removeTargetConfirmLines(t, last))) { msg.textContent = ""; return; }
|
|
3019
|
+
var payload = {
|
|
3020
|
+
op: "backlog-target-remove",
|
|
3021
|
+
// **projectKey では送らない。** body の projectKey は案件キー(cortex の
|
|
3022
|
+
// 案件識別子)に予約されていて、トークンから解決した案件と食い違えば 403 で断られる
|
|
3023
|
+
backlogProjectKey: t.projectKey,
|
|
3024
|
+
baseGeneration: targetsGenerationOf(targets),
|
|
3025
|
+
};
|
|
3026
|
+
// **常には送らない。** 最後の1件の重さを利用者に確かめてもらった証がこのフラグで、
|
|
3027
|
+
// 無条件に付けると「一覧から1行消すだけ」の見た目で同期が止まる
|
|
3028
|
+
if (last) payload.confirmLast = true;
|
|
3029
|
+
sendConfig(payload, msg, btn, null, targetsHooks());
|
|
3030
|
+
});
|
|
3031
|
+
row.appendChild(btn);
|
|
3032
|
+
return row;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
function paint() {
|
|
3036
|
+
while (listEl.firstChild) listEl.removeChild(listEl.firstChild);
|
|
3037
|
+
if (!targets.length) {
|
|
3038
|
+
// **行ごと消さない。** 表示が無いと「まだ何も無い」のか「出し忘れ」なのか区別できない
|
|
3039
|
+
listEl.appendChild(el("span", { class: "ds-current-value is-unset", text: "未設定" }));
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
for (var i = 0; i < targets.length; i++) listEl.appendChild(removeRow(targets[i]));
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
/**
|
|
3046
|
+
* 応答に添えられた一覧を取り込む。**画面が見ていた集合と違っていたら true**
|
|
3047
|
+
* (=他の人が先に変えていた。409 の文面をこちらの言葉に差し替える判断に使う)。
|
|
3048
|
+
*/
|
|
3049
|
+
function adoptTargets(list) {
|
|
3050
|
+
var before = targetsGenerationOf(targets);
|
|
3051
|
+
var next = [];
|
|
3052
|
+
for (var i = 0; i < list.length; i++) {
|
|
3053
|
+
var x = list[i];
|
|
3054
|
+
if (x && x.domain && x.projectKey) next.push({ domain: String(x.domain), projectKey: String(x.projectKey) });
|
|
3055
|
+
}
|
|
3056
|
+
targets = next;
|
|
3057
|
+
paint();
|
|
3058
|
+
return targetsGenerationOf(targets) !== before;
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
function targetsHooks() {
|
|
3062
|
+
return { onTargets: adoptTargets, staleNote: TARGETS_RACE_NOTE };
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
paint();
|
|
3066
|
+
var box = el("div", {}, [current]);
|
|
3067
|
+
if (editable) {
|
|
3068
|
+
box.appendChild(addForm({
|
|
3069
|
+
// **「追加」に戻す(#127)。** 1件しか持てなかった頃は「保存」(置き換え)だったが、
|
|
3070
|
+
// 接続先は本当に足せるようになった。押した結果は一覧の行として出る
|
|
3071
|
+
title: "+ 接続先を追加",
|
|
3072
|
+
placeholder: "https://<スペース>.backlog.jp/projects/<プロジェクトキー>",
|
|
3073
|
+
requiredNote: "BacklogのURLを入力してください(プロジェクトキーが入った形。数字だけのURLは受け付けません)。",
|
|
3074
|
+
hint: "Backlog でこの案件のプロジェクトを開き、ブラウザのURLをそのまま貼ってください(課題の画面のURLでも構いません)。APIキーは下の「Backlog のAPIキー」から別に登録します。",
|
|
3075
|
+
confirmLines: function (v) {
|
|
3076
|
+
// **解析した結果を見せる。** 貼ったURLをそのまま出すと、どのプロジェクトを
|
|
3077
|
+
// 指すことになるかが確定前に分からない。実際 /projects/PJ-CORTEX は
|
|
3078
|
+
// ハイフンで切れて PJ と解釈される(黙って別プロジェクトを指す)
|
|
3079
|
+
var t = backlogTargetOf(v);
|
|
3080
|
+
return [
|
|
3081
|
+
"この案件が同期する Backlog プロジェクトを追加します。",
|
|
3082
|
+
"",
|
|
3083
|
+
t ? t.domain + " / " + t.projectKey : v,
|
|
3084
|
+
"",
|
|
3085
|
+
"このプロジェクトの課題・ドキュメント・Wiki が、次の同期から取り込まれます。",
|
|
3086
|
+
"貼ったURLがこの案件のものか、もう一度確認してください。",
|
|
3087
|
+
"",
|
|
3088
|
+
"※ 増えるのは同期先だけです。ビューアからの起票先は変わりません。リアルタイム同期を使う場合は、追加したプロジェクトの Backlog 側にも案件専用URLの登録が要ります。",
|
|
3089
|
+
"",
|
|
3090
|
+
"追加しますか?",
|
|
3091
|
+
];
|
|
3092
|
+
},
|
|
3093
|
+
build: function (url) {
|
|
3094
|
+
return { op: "backlog-target-add", url: url, baseGeneration: targetsGenerationOf(targets) };
|
|
3095
|
+
},
|
|
3096
|
+
// 応答の一覧で本物の行が増えるので、フォーム側の控え(「追加済み・反映中」)は出さない
|
|
3097
|
+
hooks: targetsHooks,
|
|
3098
|
+
}));
|
|
3099
|
+
}
|
|
3100
|
+
box.appendChild(msg);
|
|
3101
|
+
return box;
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
/** 接続先を外すときの確認。**最後の1件は、止まるものを名指しで伝える** */
|
|
3105
|
+
function removeTargetConfirmLines(t, last) {
|
|
3106
|
+
var lines = [
|
|
3107
|
+
"「" + t.domain + " / " + t.projectKey + "」を同期の対象から外します。",
|
|
3108
|
+
"",
|
|
3109
|
+
];
|
|
3110
|
+
if (last) {
|
|
3111
|
+
// 一覧から1行消す見た目と、実際に起きることの重さが釣り合わないので必ず言う
|
|
3112
|
+
lines.push("これが最後の接続先です。この接続先を外すと課題の同期が止まります。よろしいですか?");
|
|
3113
|
+
} else {
|
|
3114
|
+
lines.push(
|
|
3115
|
+
"次回の同期から、このプロジェクトの課題・ドキュメント・Wiki は取り込まれなくなります。",
|
|
3116
|
+
"取り込み済みの内容はリポジトリに残ります(不要なら手で削除してください)。",
|
|
3117
|
+
"",
|
|
3118
|
+
"外しますか?",
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
return lines;
|
|
2903
3122
|
}
|
|
2904
3123
|
|
|
2905
3124
|
/**
|
|
@@ -2969,46 +3188,21 @@ ${capabilityState.toString()}
|
|
|
2969
3188
|
// 画面のどこにも出ていなかった。保存直後に出る楽観的なチップはリロードで消えるので、
|
|
2970
3189
|
// **利用者は「何もセットされていない」と読み、同じURLを貼り直した**。
|
|
2971
3190
|
// 設定を変える口の隣には、いまの値が必ず要る。
|
|
2972
|
-
|
|
2973
|
-
//
|
|
2974
|
-
// 貼っても 404 で終わる。三菱電機は tools が backlog なのに
|
|
3191
|
+
// **設定ファイルが無い案件には追加・削除を出さない。** 書き込み先が無いので
|
|
3192
|
+
// 押しても 404 で終わる。三菱電機は tools が backlog なのに
|
|
2975
3193
|
// backlog-settings.json が1つも無い(固定IP経由の同期で、UIからの投入は
|
|
2976
3194
|
// 鍵と同じく対応していない)。会議・共有資料は unset / broken を文言で
|
|
2977
|
-
//
|
|
3195
|
+
// 伝えてフォームを出さない形なので、課題管理だけ不揃いだった。
|
|
3196
|
+
// 一覧そのもの(現在の接続先)は**そういう案件にも出す**——読めているものを隠さない
|
|
2978
3197
|
//
|
|
2979
3198
|
// **接続先の正本は issues-config.json(#111)**だが、その置き場は
|
|
2980
3199
|
// 同期ミラー(backlog-settings.json)の位置から決めている。ミラーが1つも
|
|
2981
3200
|
// 無い案件では正本をどこへ置けばよいかも決められないので、条件は変わらない
|
|
3201
|
+
box.appendChild(backlogTargetsSection(s));
|
|
2982
3202
|
if (!s.url) {
|
|
2983
3203
|
box.appendChild(el("p", { class: "sec-how", text: "この案件はまだ一度も Backlog と同期していないため、この画面からは接続先を設定できません。先に初回の同期を実行するか、cortex の担当者にご連絡ください。" }));
|
|
2984
3204
|
used = true;
|
|
2985
|
-
}
|
|
2986
|
-
title: "接続先を変更",
|
|
2987
|
-
// **「追加」ではなく「保存」。** 接続先は1件しか持てない置き換えで、
|
|
2988
|
-
// 「追加」だと2つ目を足せると読める(実機テストでの指摘・複数対応は #127)
|
|
2989
|
-
submitLabel: "保存",
|
|
2990
|
-
placeholder: "https://<スペース>.backlog.jp/projects/<プロジェクトキー>",
|
|
2991
|
-
requiredNote: "BacklogのURLを入力してください(プロジェクトキーが入った形。数字だけのURLは受け付けません)。",
|
|
2992
|
-
hint: "Backlog でこの案件のプロジェクトを開き、ブラウザのURLをそのまま貼ってください(課題の画面のURLでも構いません)。APIキーは下の「Backlog のAPIキー」から別に登録します。",
|
|
2993
|
-
confirmLines: function (v) {
|
|
2994
|
-
// **解析した結果を見せる。** 貼ったURLをそのまま出すと、どのプロジェクトを
|
|
2995
|
-
// 指すことになるかが確定前に分からない。実際 /projects/PJ-CORTEX は
|
|
2996
|
-
// ハイフンで切れて PJ と解釈される(黙って別プロジェクトを指す)
|
|
2997
|
-
var t = backlogTargetOf(v);
|
|
2998
|
-
return [
|
|
2999
|
-
"この案件が同期する Backlog プロジェクトを設定します。",
|
|
3000
|
-
"",
|
|
3001
|
-
"別のプロジェクトを指すと、次の同期で別案件の課題が入ります。",
|
|
3002
|
-
"",
|
|
3003
|
-
t ? t.domain + " / " + t.projectKey : v,
|
|
3004
|
-
"",
|
|
3005
|
-
"※ 変わるのは同期先だけです。ビューアからの起票先と Webhook の配送先は別途の対応が要ります。",
|
|
3006
|
-
"",
|
|
3007
|
-
"設定しますか?",
|
|
3008
|
-
];
|
|
3009
|
-
},
|
|
3010
|
-
build: function (url) { return { op: "set-backlog-target", url: url }; },
|
|
3011
|
-
}));
|
|
3205
|
+
}
|
|
3012
3206
|
// **接続先の下に置く。** リアルタイム同期は「どのプロジェクトを見るか」が
|
|
3013
3207
|
// 決まった後の話で、順序が逆だと、接続先を設定していない案件が先に
|
|
3014
3208
|
// Webhook を貼りに行く(貼っても届かない)
|
|
@@ -3326,7 +3520,11 @@ ${capabilityState.toString()}
|
|
|
3326
3520
|
// 別名を付けたいときだけ入れてもらう。
|
|
3327
3521
|
namePlaceholder: "表示名(省略するとSlackから取得します)",
|
|
3328
3522
|
requiredNote: "チャンネルのURLを入力してください。",
|
|
3329
|
-
|
|
3523
|
+
// **招待の話を先頭に置く。** Slack の仕様で、ボットは**公開チャンネルでも**招待されるまで
|
|
3524
|
+
// 中身を1件も読めない(conversations.history が not_in_channel を返す・2026-08-16 実測+公式ドキュメント)。
|
|
3525
|
+
// 以前は「名前が取得できない」を主に書いていたが、読めない方が重い話なので順序を入れ替えた。
|
|
3526
|
+
hint: "先に Slack でそのチャンネルを開き、下のコマンドを貼って Cortex を招待してください。公開チャンネルでも招待が必要で、招待していないと中身を読めません。そのあと、チャンネル名を右クリック →「リンクをコピー」で出るURLを下に貼ります。追加しただけでは Gold 昇格には載りません(載せるには各チャンネルの「Gold昇格を始める」を押します)。招待前に登録すると名前は「名前未取得」になり、招待後にGold昇格をONにすると実名に変わります。",
|
|
3527
|
+
copyText: "/invite @Cortex",
|
|
3330
3528
|
// **「Goldに載る」と書かない。** 追加だけでは載らないので嘘になる。
|
|
3331
3529
|
// 確認する理由は「中央Botで読みに行く」「他案件の取り違え」。
|
|
3332
3530
|
confirmLines: function (v) {
|
|
@@ -3337,6 +3535,10 @@ ${capabilityState.toString()}
|
|
|
3337
3535
|
"",
|
|
3338
3536
|
v,
|
|
3339
3537
|
"",
|
|
3538
|
+
// Slack の仕様(公開チャンネルでも招待が要る)を、最後にもう一度置く。
|
|
3539
|
+
// 「追加したのに何も取り込まれない」の原因のほとんどがこれ。
|
|
3540
|
+
"※ Slack でこのチャンネルに Cortex を招待していないと、中身は読めません。",
|
|
3541
|
+
"",
|
|
3340
3542
|
"追加しますか?",
|
|
3341
3543
|
];
|
|
3342
3544
|
},
|
|
@@ -3391,8 +3593,15 @@ ${capabilityState.toString()}
|
|
|
3391
3593
|
* 押した内容を「(反映中)」として先に出す。出してよいのは (1) 保存の成否がサーバで
|
|
3392
3594
|
* 確定していて推測が入らない (2) **確定した表示と見た目で区別できる**、の両方を満たす
|
|
3393
3595
|
* ときだけ。区別しないと、リロードで消えたときに「戻された」と読まれる。
|
|
3596
|
+
*
|
|
3597
|
+
* **もう1つの例外が接続先の一覧(#127)。** 追加・削除の応答にはいまの一覧が必ず
|
|
3598
|
+
* 添えられていて(成功も、重複で断られたときも、世代がずれていたときも)、これは
|
|
3599
|
+
* スナップショットではなく**サーバが書いた直後の確定値**なので、その場で描き直してよい。
|
|
3600
|
+
* @param hooks { onTargets(list) → 画面が見ていた集合と違ったか, staleNote }
|
|
3601
|
+
* onTargets … 応答に添えられた一覧を画面へ取り込む。**成否を問わず先に呼ぶ**——
|
|
3602
|
+
* 断られたときこそ、画面を正しい状態へ追いつかせないと堂々巡りになる
|
|
3394
3603
|
*/
|
|
3395
|
-
async function sendConfig(payload, msg, btn, onDone) {
|
|
3604
|
+
async function sendConfig(payload, msg, btn, onDone, hooks) {
|
|
3396
3605
|
var intake = SITE.intake;
|
|
3397
3606
|
if (!intake) return;
|
|
3398
3607
|
var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
|
|
@@ -3406,10 +3615,14 @@ ${capabilityState.toString()}
|
|
|
3406
3615
|
body: JSON.stringify(payload),
|
|
3407
3616
|
});
|
|
3408
3617
|
var j = await res.json().catch(function () { return {}; });
|
|
3618
|
+
// 一覧が添えられていれば、断られていても先に画面を追いつかせる(#127)
|
|
3619
|
+
var raced = hooks && hooks.onTargets && Array.isArray(j.targets) ? hooks.onTargets(j.targets) : false;
|
|
3409
3620
|
if (!res.ok) {
|
|
3410
|
-
//
|
|
3621
|
+
// サーバの文言をそのまま出す(貼り方の案内・取り違えの警告を含んでいる)。
|
|
3622
|
+
// **例外は「他の人が先に変えていた」だけ**——サーバは再読み込みを案内するが、
|
|
3623
|
+
// 画面は既に追いついているので、済んでいることをやらせる案内になる(#127)
|
|
3411
3624
|
msg.className = "ds-toggle-msg is-error";
|
|
3412
|
-
msg.textContent = "✗ " + (j.message || "保存できませんでした。");
|
|
3625
|
+
msg.textContent = "✗ " + ((res.status === 409 && raced && hooks.staleNote) || j.message || "保存できませんでした。");
|
|
3413
3626
|
btn.disabled = false;
|
|
3414
3627
|
return;
|
|
3415
3628
|
}
|
|
@@ -3520,12 +3733,14 @@ ${capabilityState.toString()}
|
|
|
3520
3733
|
/**
|
|
3521
3734
|
* 「+…を追加」のフォーム。URLをそのまま貼れるようにする(IDだけを要求すると転記ミスが増える)。
|
|
3522
3735
|
*
|
|
3523
|
-
* **ボタンの語は呼び出し側が決める(既定は「追加」)。**
|
|
3524
|
-
*
|
|
3525
|
-
*
|
|
3526
|
-
* 押した結果の表示(+/追加済み)も同じ理由で語を揃える。
|
|
3736
|
+
* **ボタンの語は呼び出し側が決める(既定は「追加」)。** 置き換えの設定に「追加」と書くと
|
|
3737
|
+
* 2つ目を足せると読めるため、そういう口だけ submitLabel で語を変える。押した結果の
|
|
3738
|
+
* 表示(+/追加済み)も同じ理由で語を揃える。
|
|
3527
3739
|
*
|
|
3528
|
-
* @param opts { title, placeholder, hint, needsName, confirmLines, build(values), submitLabel }
|
|
3740
|
+
* @param opts { title, placeholder, hint, needsName, confirmLines, build(values), submitLabel, hooks }
|
|
3741
|
+
* hooks … sendConfig へ渡す追い付き処理を返す関数(接続先の一覧・#127)。
|
|
3742
|
+
* **これがある口では、その場の控え(「追加済み・反映中」)を出さない**——
|
|
3743
|
+
* 応答で本物の一覧が増えるので、控えを併記すると同じものが2回出る
|
|
3529
3744
|
*/
|
|
3530
3745
|
function addForm(opts) {
|
|
3531
3746
|
var main = el("input", { class: "sec-input", type: "text", placeholder: opts.placeholder, autocomplete: "off" });
|
|
@@ -3573,6 +3788,8 @@ ${capabilityState.toString()}
|
|
|
3573
3788
|
// 追加できたら入力欄を空にする(同じものを二度押ししないように)
|
|
3574
3789
|
main.value = "";
|
|
3575
3790
|
if (name) name.value = "";
|
|
3791
|
+
// 応答で本物の一覧が描き直される口では、控えを出さない(同じものが2回並ぶ)
|
|
3792
|
+
if (opts.hooks) return;
|
|
3576
3793
|
// **追加したものをその場に出す。** 上の一覧は fleet-status.json(夜間スナップショット)
|
|
3577
3794
|
// から描かれているので、追加してもその場では変わらない。何も変わらないと
|
|
3578
3795
|
// 「追加できていない」と読まれ、もう一度押して「既に登録されています」になる
|
|
@@ -3584,7 +3801,7 @@ ${capabilityState.toString()}
|
|
|
3584
3801
|
el("span", { class: "ds-pending-note", text: submitLabel + "済み・反映中" }),
|
|
3585
3802
|
]),
|
|
3586
3803
|
);
|
|
3587
|
-
});
|
|
3804
|
+
}, opts.hooks ? opts.hooks() : null);
|
|
3588
3805
|
});
|
|
3589
3806
|
|
|
3590
3807
|
// このセッションで追加したものを並べる場所(リロードすると消える=正式な一覧は上に出る)
|
|
@@ -3593,6 +3810,9 @@ ${capabilityState.toString()}
|
|
|
3593
3810
|
var box = el("div", opts.adder ? { class: "ds-addform", "data-adder": opts.adder } : { class: "ds-addform" }, [
|
|
3594
3811
|
el("div", { class: "ds-addform-title", text: opts.title }),
|
|
3595
3812
|
opts.hint ? el("p", { class: "sec-how", text: opts.hint }) : null,
|
|
3813
|
+
// 手順の中に「どこかへ貼る文字列」があるときは、**手で打たせない**。
|
|
3814
|
+
// 「/invite @Cortex」のような短い文字列でも、打ち間違えると黙って失敗する
|
|
3815
|
+
opts.copyText ? copyRow(opts.copyText) : null,
|
|
3596
3816
|
main,
|
|
3597
3817
|
name,
|
|
3598
3818
|
wsSelect,
|
package/package.json
CHANGED