@takagaki/cortex-decisions-viewer 0.4.75 → 0.4.76
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 +83 -2
- package/dist/render.js +208 -14
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.readFleetSources = readFleetSources;
|
|
40
|
+
exports.findBacklogDir = findBacklogDir;
|
|
40
41
|
exports.build = build;
|
|
41
42
|
exports.createRefResolver = createRefResolver;
|
|
42
43
|
const node_fs_1 = require("node:fs");
|
|
@@ -146,8 +147,31 @@ async function readFleetSources(repoRoot) {
|
|
|
146
147
|
// 自動導出されたものは登録ファイルから消しても翌日また現れるので、
|
|
147
148
|
// explicit のときだけ削除ボタンを出す。
|
|
148
149
|
origin: s.origin != null ? String(s.origin) : undefined,
|
|
150
|
+
// どの Slack ワークスペースのチャンネルか(#104)。**列挙し忘れると静かに消える**
|
|
151
|
+
// (このマッピングは素通しではなく明示の許可リスト)。落とすと、同じIDの
|
|
152
|
+
// チャンネルを2つのワークスペースに持つ案件で Gold トグルが別の行を撃つ
|
|
153
|
+
workspace: s.workspace != null ? String(s.workspace) : undefined,
|
|
149
154
|
}))
|
|
150
155
|
: undefined;
|
|
156
|
+
// 接続済み Slack ワークスペース(#104)。**空配列と undefined を区別して通す**——
|
|
157
|
+
// 空配列は「まだ1つも繋いでいない」(接続の導線を出してよい)、undefined は
|
|
158
|
+
// 「この項目を知らない古い生成物」(何も言えないので出さない)。
|
|
159
|
+
// ここで `|| undefined` に潰すと、その2つが同じ表示になる
|
|
160
|
+
const slackWorkspaces = Array.isArray(obj.slackWorkspaces)
|
|
161
|
+
? obj.slackWorkspaces
|
|
162
|
+
.filter((w) => w != null && typeof w === "object")
|
|
163
|
+
.map((w) => ({
|
|
164
|
+
teamId: String(w.teamId ?? ""),
|
|
165
|
+
// 表示名が無ければ team_id。**空欄にしない**(どれのことか分からない行になる)
|
|
166
|
+
name: String(w.name ?? "") || String(w.teamId ?? ""),
|
|
167
|
+
domain: w.domain != null ? String(w.domain) : undefined,
|
|
168
|
+
connectedAt: w.connectedAt != null ? String(w.connectedAt) : undefined,
|
|
169
|
+
channels: typeof w.channels === "number" ? w.channels : undefined,
|
|
170
|
+
// ok / missing / revoked / unknown。**潰さない**(次の一手が違う)
|
|
171
|
+
tokenState: w.tokenState != null ? String(w.tokenState) : undefined,
|
|
172
|
+
}))
|
|
173
|
+
.filter((w) => w.teamId !== "")
|
|
174
|
+
: undefined;
|
|
151
175
|
// セットアップの充足チェック。**missing だけを通す。**
|
|
152
176
|
// na(使わないと宣言した能力)を通すと「設定する必要が無いものに設定を促す」偽の警告になり、
|
|
153
177
|
// #101 で潰した穴が復活する。unknown は AWS/gh へ到達できなかった回に出るので、
|
|
@@ -304,7 +328,7 @@ async function readFleetSources(repoRoot) {
|
|
|
304
328
|
const secretSources = obj.secretSources && typeof obj.secretSources === "object"
|
|
305
329
|
? obj.secretSources
|
|
306
330
|
: undefined;
|
|
307
|
-
return { externalSources, internalSources, pipelines, capabilities, fleetGeneratedAt, secretSources, checks, githubOwners, backlogTarget };
|
|
331
|
+
return { externalSources, slackWorkspaces, internalSources, pipelines, capabilities, fleetGeneratedAt, secretSources, checks, githubOwners, backlogTarget };
|
|
308
332
|
}
|
|
309
333
|
/** パスがディレクトリとして存在するか */
|
|
310
334
|
async function isDirectory(dirAbs) {
|
|
@@ -482,9 +506,65 @@ async function findDirByMarker(repoRoot, marker, fallback) {
|
|
|
482
506
|
}
|
|
483
507
|
return fallback;
|
|
484
508
|
}
|
|
509
|
+
/** exporter が作る区画名(engine の `BACKLOG_SECTIONS` と同じ固定の名前) */
|
|
510
|
+
const BACKLOG_SECTIONS = ["issues", "documents", "wiki"];
|
|
511
|
+
/**
|
|
512
|
+
* 課題管理の能力ディレクトリ(`<ここ>/issues` に課題ミラーが入る)を探す。
|
|
513
|
+
*
|
|
514
|
+
* **汎用の `findDirByMarker` では足りない。** `backlog-settings.json` は能力ディレクトリの
|
|
515
|
+
* 直下ではなく**区画の中**(`<課題管理>/issues/` 等)にあり、しかも接続先が複数になると
|
|
516
|
+
* さらに1階層落ちる(`<課題管理>/<キー>/{issues,…}/`・#127)。汎用版は最上位の
|
|
517
|
+
* ディレクトリ名しか返さないので、入れ子形の案件では `<課題管理>/issues` という
|
|
518
|
+
* 実在しないパスを見に行き、**課題キー→Backlog URL の表が空になる**
|
|
519
|
+
* (グラフの課題ノードが全部リポ内パス止まりになり、Backlog へ飛べない)。
|
|
520
|
+
*
|
|
521
|
+
* 規則の出どころは cortex-engine の `plugin/scripts/issues-config.mjs`(`findBacklogDir`)。
|
|
522
|
+
* **あちらを変えたらここも変えること**(`viewer/test/backlog-nested-layout.test.mjs` が見張る)。
|
|
523
|
+
*
|
|
524
|
+
* 浅い順(ルート → 深さ1 → 深さ2)に見て最初に当たったものを採る。深い側は後ろなので、
|
|
525
|
+
* いまフラット形で見つかっている案件の答えは1つも動かない。
|
|
526
|
+
*/
|
|
527
|
+
async function findBacklogDir(repoRoot, fallback) {
|
|
528
|
+
const dirsIn = async (rel) => {
|
|
529
|
+
try {
|
|
530
|
+
return (await node_fs_1.promises.readdir(path.join(repoRoot, rel), { withFileTypes: true }))
|
|
531
|
+
.filter((e) => e.isDirectory() && e.name !== "node_modules" && !e.name.startsWith("."))
|
|
532
|
+
.map((e) => e.name)
|
|
533
|
+
.sort();
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
const hasMarker = async (rel) => {
|
|
540
|
+
for (const s of BACKLOG_SECTIONS) {
|
|
541
|
+
try {
|
|
542
|
+
await node_fs_1.promises.stat(path.join(repoRoot, rel, s, "backlog-settings.json"));
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// 次の区画へ
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
};
|
|
551
|
+
if (await hasMarker(""))
|
|
552
|
+
return fallback; // ルート直下に区画がある形(`""` を返すと絶対パスになる)
|
|
553
|
+
const dirs = await dirsIn("");
|
|
554
|
+
for (const d of dirs)
|
|
555
|
+
if (await hasMarker(d))
|
|
556
|
+
return d;
|
|
557
|
+
for (const d of dirs) {
|
|
558
|
+
for (const sub of await dirsIn(d)) {
|
|
559
|
+
if (await hasMarker(`${d}/${sub}`))
|
|
560
|
+
return `${d}/${sub}`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return fallback;
|
|
564
|
+
}
|
|
485
565
|
async function buildTargetUrlMap(repoRoot, repoBaseUrl, branch) {
|
|
486
566
|
const map = new Map();
|
|
487
|
-
const issuesDir = await
|
|
567
|
+
const issuesDir = await findBacklogDir(repoRoot, "課題管理");
|
|
488
568
|
const designDir = await findDirByMarker(repoRoot, "figma.json", "デザイン");
|
|
489
569
|
const meetingDir = await findDirByMarker(repoRoot, "ingest-config.json", "会議");
|
|
490
570
|
const materialsDir = await findDirByMarker(repoRoot, "materials-config.json", "共有資料");
|
|
@@ -1180,6 +1260,7 @@ async function build(opts) {
|
|
|
1180
1260
|
.map((s) => s.trim())
|
|
1181
1261
|
.filter(Boolean),
|
|
1182
1262
|
externalSources: fleet.externalSources,
|
|
1263
|
+
slackWorkspaces: fleet.slackWorkspaces,
|
|
1183
1264
|
fleetGeneratedAt: fleet.fleetGeneratedAt,
|
|
1184
1265
|
secretSources: fleet.secretSources,
|
|
1185
1266
|
internalSources: fleet.internalSources,
|
package/dist/render.js
CHANGED
|
@@ -1250,6 +1250,16 @@ a.ds-name:hover { text-decoration: underline; }
|
|
|
1250
1250
|
/* 登録の由来(#118)。**薄く出す**——読み取りの可否や Gold 対象かより弱い情報で、
|
|
1251
1251
|
ここが目立つと「除外」「未宣言」の判断のバッジと同じ強さに見えてしまう */
|
|
1252
1252
|
.badge.ds-origin { background: transparent; color: var(--muted); border: 1px dashed var(--border); }
|
|
1253
|
+
/* どの Slack ワークスペースのチャンネルか(#104)。**由来のバッジと同じ強さにする**——
|
|
1254
|
+
「Gold 対象か」より弱い、どれのことかを見分けるための情報。強くすると判断のバッジと競る */
|
|
1255
|
+
.badge.ds-workspace { background: transparent; color: var(--muted); border: 1px solid var(--border); }
|
|
1256
|
+
/* 接続済み Slack ワークスペースの一覧(#104)。追加フォームの中に置く */
|
|
1257
|
+
.ds-ws-list { display: flex; flex-direction: column; gap: 6px; margin: 8px 0 10px; }
|
|
1258
|
+
.ds-ws { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: 12.5px; }
|
|
1259
|
+
.ds-ws-name { color: var(--text); font-weight: 600; word-break: break-all; }
|
|
1260
|
+
.ds-ws-count { color: var(--muted); flex: 0 0 auto; }
|
|
1261
|
+
/* 1つも繋いでいない状態。**空白にしない**——一覧が出ていないのか繋がっていないのかが読めない */
|
|
1262
|
+
.ds-ws-empty { color: var(--muted); font-size: 12.5px; }
|
|
1253
1263
|
/* 全組織が接続済みのときに、トークン入力欄の代わりに出す1行(#118) */
|
|
1254
1264
|
.ds-all-connected { color: var(--muted); }
|
|
1255
1265
|
.ds-gate { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; flex: 0 0 auto; }
|
|
@@ -2326,6 +2336,9 @@ ${capabilityState.toString()}
|
|
|
2326
2336
|
// **追加フォームはグループの外に置く。** グループは0件だと描かれず、4件以上だと
|
|
2327
2337
|
// 畳まれる(既定は閉)。中に入れると**新規案件(0件)=一番追加したい場面**で消える。
|
|
2328
2338
|
var adders = renderExternalAdders();
|
|
2339
|
+
// **接続の入口はチャンネル追加より前に置く。** 顧客ワークスペースを繋いでいない状態で
|
|
2340
|
+
// 先に追加フォームを見せると、貼れるのに読まれないチャンネルを登録することになる
|
|
2341
|
+
pushCap("チャット", slackConnectBox());
|
|
2329
2342
|
pushCap("チャット", adders.querySelector('[data-adder="slack"]'));
|
|
2330
2343
|
// **組織ごとの接続状況を、開発の一番上に置く。** トークンは組織ごとにしか
|
|
2331
2344
|
// 効かない(Fine-grained PAT も GitHub App のインストールトークンも1組織分)ので、
|
|
@@ -2833,7 +2846,12 @@ ${capabilityState.toString()}
|
|
|
2833
2846
|
"有効にしますか?",
|
|
2834
2847
|
],
|
|
2835
2848
|
build: function (next) {
|
|
2836
|
-
|
|
2849
|
+
// **どのワークスペースのチャンネルかを必ず送る(#104)。** 落とすと、サーバ側は
|
|
2850
|
+
// 既定ワークスペースの同じIDのエントリを探しに行き、「対象チャンネルが
|
|
2851
|
+
// 見つかりません」か、最悪**別のチャンネルを Gold に上げる**
|
|
2852
|
+
var payload = { op: "set-channel-gold", ref: s.ref, value: next };
|
|
2853
|
+
if (s.workspace) payload.workspace = s.workspace;
|
|
2854
|
+
return payload;
|
|
2837
2855
|
},
|
|
2838
2856
|
// 保存できた直後に、この行のバッジだけを「反映中」に差し替える。
|
|
2839
2857
|
// 一覧そのものは触らない(結果メッセージや他行の状態を巻き込まないため)。
|
|
@@ -2842,8 +2860,9 @@ ${capabilityState.toString()}
|
|
|
2842
2860
|
// 画面が出している値と一致するので待つものが無い。ここで消さないと、
|
|
2843
2861
|
// 何も保留していないのに (反映中) が残り、リロードで消えて不可解に見える。
|
|
2844
2862
|
var back = next === (s.gold === true);
|
|
2845
|
-
|
|
2846
|
-
|
|
2863
|
+
// 控えの鍵もワークスペース込み(#104。pendingKeyOf の doc)
|
|
2864
|
+
if (back) clearPendingGold(pendingKeyOf(s));
|
|
2865
|
+
else markPendingGold(pendingKeyOf(s), next);
|
|
2847
2866
|
var card = toggleEl && toggleEl.parentNode;
|
|
2848
2867
|
var badges = card && card.querySelector(".ds-badges");
|
|
2849
2868
|
if (!badges) return;
|
|
@@ -3167,6 +3186,115 @@ ${capabilityState.toString()}
|
|
|
3167
3186
|
return wrap;
|
|
3168
3187
|
}
|
|
3169
3188
|
|
|
3189
|
+
/* ---------------------------------------------------------------------------
|
|
3190
|
+
* Slack ワークスペースの接続(#104)
|
|
3191
|
+
*
|
|
3192
|
+
* 顧客のワークスペースを読み始める日の入口。押す → サーバが署名つきの承認リンクを
|
|
3193
|
+
* 作って返す → そこへ遷移 → 顧客が Slack で承認 → 別の Lambda が受け取って棚に鍵を入れる。
|
|
3194
|
+
*
|
|
3195
|
+
* **リンクを静的に焼き込まない。** 承認リンクには「この案件へ書き込んでよい」という
|
|
3196
|
+
* 10分間有効な署名(state)が載っている。HTML に埋めると、配信物を読める人(=その案件の
|
|
3197
|
+
* Basic 認証を通れる人)が期限のないリンクを手に入れることになるし、
|
|
3198
|
+
* ビルド時点の state はどのみち期限切れで使えない。押した瞬間にサーバから貰う。
|
|
3199
|
+
*
|
|
3200
|
+
* **「トークン」という語を出さない。** 顧客に見せる画面なので、内部語ではなく
|
|
3201
|
+
* 「接続する/読み取りたいチャンネルに招待する」で通す(内部語排除の原則の資格情報版)。
|
|
3202
|
+
* ------------------------------------------------------------------------- */
|
|
3203
|
+
function slackConnectBox() {
|
|
3204
|
+
if (!SITE.intake) return null;
|
|
3205
|
+
// **この項目を知らない古い fleet-status では何も出さない。** 「0件」と「知らない」を
|
|
3206
|
+
// 同じ扱いにすると、engine が @v1 で止まっている案件に「まだ接続されていません」と
|
|
3207
|
+
// 嘘を出すことになる(実際には社内ワークスペースで動いている)
|
|
3208
|
+
var list = slackWorkspaces();
|
|
3209
|
+
if (!list) return null;
|
|
3210
|
+
|
|
3211
|
+
var box = el("div", { class: "ds-addform", "data-adder": "slack-workspace" }, [
|
|
3212
|
+
el("div", { class: "ds-addform-title", text: "Slack ワークスペースを接続" }),
|
|
3213
|
+
el("p", {
|
|
3214
|
+
class: "sec-how",
|
|
3215
|
+
text:
|
|
3216
|
+
"お客様の Slack ワークスペースのチャンネルを読み取り対象にするための接続です。" +
|
|
3217
|
+
"ボタンを押すと Slack の承認画面が開きます(承認するのはワークスペースの管理者です)。" +
|
|
3218
|
+
"お願いする権限は読み取りだけで、Cortex から投稿することはありません。" +
|
|
3219
|
+
"接続したあと、読み取りたいチャンネルにボットを招待し、この画面から追加してください。",
|
|
3220
|
+
}),
|
|
3221
|
+
]);
|
|
3222
|
+
|
|
3223
|
+
// 接続済みの一覧。**0件でも「まだありません」と書く**——空白だと、
|
|
3224
|
+
// 一覧が出ていないのか繋がっていないのかが読めない
|
|
3225
|
+
var listBox = el("div", { class: "ds-ws-list" });
|
|
3226
|
+
if (!list.length) {
|
|
3227
|
+
listBox.appendChild(el("div", { class: "ds-ws-empty", text: "接続済みのワークスペースはまだありません。" }));
|
|
3228
|
+
} else {
|
|
3229
|
+
list.forEach(function (w) {
|
|
3230
|
+
var row = el("div", { class: "ds-ws" }, [
|
|
3231
|
+
el("span", { class: "ds-ws-name", text: w.name || w.teamId }),
|
|
3232
|
+
el("span", {
|
|
3233
|
+
class: "ds-ws-count",
|
|
3234
|
+
text: (w.channels || 0) + "チャンネル",
|
|
3235
|
+
}),
|
|
3236
|
+
]);
|
|
3237
|
+
// **revoked と missing を分けて出す。** 前者は「接続し直してください」、
|
|
3238
|
+
// 後者は「まだ接続が完了していない」で、次の一手が違う。
|
|
3239
|
+
// unknown(確かめられなかった回)には何も出さない——繋がっているのに
|
|
3240
|
+
// 「接続し直して」と言う方が、黙っているより悪い。
|
|
3241
|
+
// 注: ここは埋め込みJSのテンプレートリテラル内。**コメントでもバッククォートを使わない**
|
|
3242
|
+
if (w.tokenState === "revoked") {
|
|
3243
|
+
row.appendChild(el("span", {
|
|
3244
|
+
class: "ds-gate ds-gate-warn",
|
|
3245
|
+
text: "⚠️ 接続が切れています",
|
|
3246
|
+
title: "Slack 側で Cortex のアプリが削除されたか、接続が無効になっています。もう一度接続してください。",
|
|
3247
|
+
}));
|
|
3248
|
+
} else if (w.tokenState === "missing") {
|
|
3249
|
+
row.appendChild(el("span", {
|
|
3250
|
+
class: "ds-gate ds-gate-warn",
|
|
3251
|
+
text: "⚠️ 未接続",
|
|
3252
|
+
title: "このワークスペースのチャンネルが登録されていますが、接続が見つかりません。もう一度接続してください。",
|
|
3253
|
+
}));
|
|
3254
|
+
} else if (w.tokenState === "ok") {
|
|
3255
|
+
row.appendChild(el("span", { class: "ds-gate ds-gate-ok", text: "● 接続OK" }));
|
|
3256
|
+
}
|
|
3257
|
+
listBox.appendChild(row);
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
box.appendChild(listBox);
|
|
3261
|
+
|
|
3262
|
+
var btn = el("button", { class: "ds-toggle-btn", type: "button", text: "Slack ワークスペースを接続" });
|
|
3263
|
+
var msg = el("div", { class: "ds-toggle-msg" });
|
|
3264
|
+
btn.addEventListener("click", async function () {
|
|
3265
|
+
var intake = SITE.intake;
|
|
3266
|
+
if (!intake) return;
|
|
3267
|
+
btn.disabled = true;
|
|
3268
|
+
msg.className = "ds-toggle-msg";
|
|
3269
|
+
msg.textContent = "承認画面を準備しています…";
|
|
3270
|
+
try {
|
|
3271
|
+
var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
|
|
3272
|
+
var res = await fetch(pfx + "op=slack-oauth-start&t=" + encodeURIComponent(intake.token), {
|
|
3273
|
+
method: "POST",
|
|
3274
|
+
headers: { "content-type": "application/json" },
|
|
3275
|
+
body: "{}",
|
|
3276
|
+
});
|
|
3277
|
+
var j = await res.json().catch(function () { return {}; });
|
|
3278
|
+
if (!res.ok || !j.url) {
|
|
3279
|
+
// サーバの文言をそのまま出す(未設定のときは「準備中です」が返る)
|
|
3280
|
+
msg.className = "ds-toggle-msg is-error";
|
|
3281
|
+
msg.textContent = "✗ " + (j.message || "接続を開始できませんでした。");
|
|
3282
|
+
btn.disabled = false;
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
// **同じタブで遷移する。** 別タブだと、承認後の「接続しました」を見た人が
|
|
3286
|
+
// 元のタブに戻る動線を自分で見つけることになる
|
|
3287
|
+
location.href = j.url;
|
|
3288
|
+
} catch (e) {
|
|
3289
|
+
msg.className = "ds-toggle-msg is-error";
|
|
3290
|
+
msg.textContent = "✗ 接続を開始できませんでした。時間をおいて試してください。";
|
|
3291
|
+
btn.disabled = false;
|
|
3292
|
+
}
|
|
3293
|
+
});
|
|
3294
|
+
box.appendChild(el("div", { class: "ds-toggle" }, [btn, msg]));
|
|
3295
|
+
return box;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3170
3298
|
/**
|
|
3171
3299
|
* 外部ソースの追加フォーム(Slack・GitHub)。
|
|
3172
3300
|
*
|
|
@@ -3212,7 +3340,14 @@ ${capabilityState.toString()}
|
|
|
3212
3340
|
"追加しますか?",
|
|
3213
3341
|
];
|
|
3214
3342
|
},
|
|
3215
|
-
|
|
3343
|
+
// **選んだワークスペースを送る(#104)。** 空文字=社内ワークスペースなのでキーごと落とす
|
|
3344
|
+
// (サーバ側は「未宣言・null・空」を区別しない約束で、無いことで既定を表す)
|
|
3345
|
+
build: function (url, name, workspace) {
|
|
3346
|
+
var payload = { op: "add-channel", url: url, name: name };
|
|
3347
|
+
if (workspace) payload.workspace = workspace;
|
|
3348
|
+
return payload;
|
|
3349
|
+
},
|
|
3350
|
+
workspacePicker: true,
|
|
3216
3351
|
adder: "slack",
|
|
3217
3352
|
}));
|
|
3218
3353
|
|
|
@@ -3397,6 +3532,24 @@ ${capabilityState.toString()}
|
|
|
3397
3532
|
var name = opts.needsName
|
|
3398
3533
|
? el("input", { class: "sec-input", type: "text", placeholder: opts.namePlaceholder || "表示名", autocomplete: "off" })
|
|
3399
3534
|
: null;
|
|
3535
|
+
// 接続先ワークスペースの選択(#104・Slackの追加だけ)。
|
|
3536
|
+
//
|
|
3537
|
+
// **接続済みのワークスペースが1つも無ければ出さない。** 選択肢が「社内」1つだけの
|
|
3538
|
+
// プルダウンは、選ぶものが無いのに1手増やすだけ。既存の全案件がこの状態から始まる。
|
|
3539
|
+
//
|
|
3540
|
+
// **チャンネルのURLから推測しない。** URL のホスト名(acme.slack.com)は
|
|
3541
|
+
// ワークスペースの改名で変わるし、共有チャンネルでは貼った人のホストになる。
|
|
3542
|
+
// 推測して間違えると「追加できたのに読まれない」チャンネルができ、
|
|
3543
|
+
// 翌朝の接続状況で初めて分かる。人に選ばせる。
|
|
3544
|
+
var wsList = opts.workspacePicker ? slackWorkspaces() || [] : [];
|
|
3545
|
+
var wsSelect = null;
|
|
3546
|
+
if (wsList.length) {
|
|
3547
|
+
wsSelect = el("select", { class: "sec-input" });
|
|
3548
|
+
wsSelect.appendChild(el("option", { value: "", text: "社内ワークスペース(クラスメソッド)" }));
|
|
3549
|
+
wsList.forEach(function (w) {
|
|
3550
|
+
wsSelect.appendChild(el("option", { value: w.teamId, text: w.name || w.teamId }));
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3400
3553
|
var submitLabel = opts.submitLabel || "追加";
|
|
3401
3554
|
var btn = el("button", { class: "ds-toggle-btn", type: "button", text: submitLabel });
|
|
3402
3555
|
var msg = el("div", { class: "ds-toggle-msg" });
|
|
@@ -3415,7 +3568,7 @@ ${capabilityState.toString()}
|
|
|
3415
3568
|
if (opts.confirmLines && !confirmDanger(opts.confirmLines(main.value.trim()))) { msg.textContent = ""; return; }
|
|
3416
3569
|
var added = main.value.trim();
|
|
3417
3570
|
var addedName = name ? name.value.trim() : "";
|
|
3418
|
-
var payload = opts.build(added, addedName);
|
|
3571
|
+
var payload = opts.build(added, addedName, wsSelect ? wsSelect.value : "");
|
|
3419
3572
|
sendConfig(payload, msg, btn, function () {
|
|
3420
3573
|
// 追加できたら入力欄を空にする(同じものを二度押ししないように)
|
|
3421
3574
|
main.value = "";
|
|
@@ -3442,6 +3595,7 @@ ${capabilityState.toString()}
|
|
|
3442
3595
|
opts.hint ? el("p", { class: "sec-how", text: opts.hint }) : null,
|
|
3443
3596
|
main,
|
|
3444
3597
|
name,
|
|
3598
|
+
wsSelect,
|
|
3445
3599
|
el("div", { class: "ds-toggle" }, [btn, msg]),
|
|
3446
3600
|
pending,
|
|
3447
3601
|
]);
|
|
@@ -3519,18 +3673,27 @@ ${capabilityState.toString()}
|
|
|
3519
3673
|
function writePending(map) {
|
|
3520
3674
|
try { sessionStorage.setItem(PENDING_KEY, JSON.stringify(map)); } catch (_) {}
|
|
3521
3675
|
}
|
|
3522
|
-
function clearPendingGold(
|
|
3523
|
-
if (!
|
|
3676
|
+
function clearPendingGold(key) {
|
|
3677
|
+
if (!key) return;
|
|
3524
3678
|
var map = readPending();
|
|
3525
|
-
if (!(
|
|
3526
|
-
delete map[
|
|
3679
|
+
if (!(key in map)) return;
|
|
3680
|
+
delete map[key];
|
|
3527
3681
|
writePending(map);
|
|
3528
3682
|
}
|
|
3683
|
+
/**
|
|
3684
|
+
* 控えの鍵。**チャンネルIDだけでは足りない(#104)。**
|
|
3685
|
+
* IDはワークスペースの中でしか一意でないので、同じIDのチャンネルを2つのワークスペースに
|
|
3686
|
+
* 持つ案件では、片方で押した「反映中」が**もう片方の行にも出る**。
|
|
3687
|
+
*/
|
|
3688
|
+
function pendingKeyOf(s) {
|
|
3689
|
+
if (!s || !s.ref) return "";
|
|
3690
|
+
return s.workspace ? s.workspace + "\t" + s.ref : s.ref;
|
|
3691
|
+
}
|
|
3529
3692
|
/** 押した内容を控える。fleet-status が追いつくまでの間だけ効く */
|
|
3530
|
-
function markPendingGold(
|
|
3531
|
-
if (!
|
|
3693
|
+
function markPendingGold(key, value) {
|
|
3694
|
+
if (!key) return;
|
|
3532
3695
|
var map = readPending();
|
|
3533
|
-
map[
|
|
3696
|
+
map[key] = { value: value === true, at: Date.now() };
|
|
3534
3697
|
writePending(map);
|
|
3535
3698
|
}
|
|
3536
3699
|
/**
|
|
@@ -3538,9 +3701,10 @@ ${capabilityState.toString()}
|
|
|
3538
3701
|
* 捨てないと、正しくなった後も (反映中) が残り続ける。
|
|
3539
3702
|
*/
|
|
3540
3703
|
function pendingGoldFor(s) {
|
|
3541
|
-
|
|
3704
|
+
var key = pendingKeyOf(s);
|
|
3705
|
+
if (!key) return null;
|
|
3542
3706
|
var map = readPending();
|
|
3543
|
-
var e = map[
|
|
3707
|
+
var e = map[key];
|
|
3544
3708
|
if (!e) return null;
|
|
3545
3709
|
// **「値が一致したら捨てる」だけでは足りない。** 別の人が直後に逆を押して、
|
|
3546
3710
|
// その結果で追いついた場合、値が一致しないので控えが残り、TTL切れまで
|
|
@@ -3573,6 +3737,26 @@ ${capabilityState.toString()}
|
|
|
3573
3737
|
* **「登録から削除」が出てはいけない行に出て**、押しても翌日また現れる。
|
|
3574
3738
|
* 全案件の再生成が回りきったら、github 側のこの分岐を落とす。
|
|
3575
3739
|
*/
|
|
3740
|
+
/**
|
|
3741
|
+
* 接続済み Slack ワークスペース(#104)。**空配列と undefined を区別する**——
|
|
3742
|
+
* 空配列は「まだ1つも繋いでいない」(接続の導線を出してよい)、undefined は
|
|
3743
|
+
* 「この項目を知らない古い fleet-status」(何も言えないので導線も一覧も出さない)。
|
|
3744
|
+
* ライブ(op=state)があればそちら、無ければ静的ビルドの値。
|
|
3745
|
+
*/
|
|
3746
|
+
function slackWorkspaces() {
|
|
3747
|
+
var v = fleetVal("slackWorkspaces");
|
|
3748
|
+
return Array.isArray(v) ? v : null;
|
|
3749
|
+
}
|
|
3750
|
+
/** team_id → 表示名。宣言はあるが一覧に無いワークスペースは team_id のまま出す */
|
|
3751
|
+
function workspaceLabel(teamId) {
|
|
3752
|
+
if (!teamId) return "";
|
|
3753
|
+
var list = slackWorkspaces() || [];
|
|
3754
|
+
for (var i = 0; i < list.length; i++) {
|
|
3755
|
+
if (list[i].teamId === teamId) return list[i].name || teamId;
|
|
3756
|
+
}
|
|
3757
|
+
return teamId;
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3576
3760
|
var ORIGIN_LABEL = { explicit: "明示登録", derived: "自動検出", both: "明示登録+自動検出" };
|
|
3577
3761
|
var ORIGIN_HELP = {
|
|
3578
3762
|
explicit: "開発の登録ファイル(dev-sources.json)に登録されているリポジトリです。ここから登録を削除できます。",
|
|
@@ -3593,6 +3777,16 @@ ${capabilityState.toString()}
|
|
|
3593
3777
|
else if (s.goldState === "undeclared") badges.appendChild(el("span", { class: "badge ds-undeclared", text: "未宣言(対象外)" }));
|
|
3594
3778
|
else if (s.gold === false) badges.appendChild(el("span", { class: "badge ds-excluded", text: "除外" }));
|
|
3595
3779
|
if (s.notify === true) badges.appendChild(el("span", { class: "badge ds-notify", text: "通知先" }));
|
|
3780
|
+
// **どのワークスペースのチャンネルかを出す(#104)。** 同じチャンネル名が
|
|
3781
|
+
// 2つのワークスペースに並びうるので、これが無いと「どっちの #general か」が読めない。
|
|
3782
|
+
// 宣言なし(社内ワークスペース)には何も出さない——これまでどおりの見た目を保つ
|
|
3783
|
+
if (s.type === "slack" && s.workspace) {
|
|
3784
|
+
badges.appendChild(el("span", {
|
|
3785
|
+
class: "badge ds-workspace",
|
|
3786
|
+
text: workspaceLabel(s.workspace),
|
|
3787
|
+
title: "接続した Slack ワークスペースのチャンネルです(読み取り専用の接続なので、通知は届きません)。",
|
|
3788
|
+
}));
|
|
3789
|
+
}
|
|
3596
3790
|
// 登録の由来(#118)。**「登録から削除」が出る/出ないの理由がこれ。** 導出されたリポは
|
|
3597
3791
|
// 登録ファイルから消しても翌日また現れるので削除を出していないが、画面には
|
|
3598
3792
|
// 「同じ見た目の行なのに、片方にだけ削除がある」としか映らず、毎回考え直すことになる。
|
package/package.json
CHANGED