@guidelytech/guide-runtime 0.10.0 → 0.11.0
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/capture.cjs +31 -0
- package/dist/capture.cjs.map +1 -1
- package/dist/capture.js +2 -2
- package/dist/chunk-H3CROHPR.js +585 -0
- package/dist/chunk-H3CROHPR.js.map +1 -0
- package/dist/chunk-VJSQM23R.js +359 -0
- package/dist/chunk-VJSQM23R.js.map +1 -0
- package/dist/chunk-VQJ7MZ2K.js +463 -0
- package/dist/chunk-VQJ7MZ2K.js.map +1 -0
- package/dist/index.cjs +282 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +116 -11
- package/dist/index.js.map +1 -1
- package/dist/messenger.cjs +1171 -167
- package/dist/messenger.cjs.map +1 -1
- package/dist/messenger.d.cts +55 -5
- package/dist/messenger.d.ts +55 -5
- package/dist/messenger.js +1029 -96
- package/dist/messenger.js.map +1 -1
- package/package.json +2 -2
package/dist/messenger.cjs
CHANGED
|
@@ -25,15 +25,16 @@ __export(messenger_exports, {
|
|
|
25
25
|
renderMessenger: () => renderMessenger
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(messenger_exports);
|
|
28
|
-
var
|
|
28
|
+
var import_runtime6 = require("@guidelytech/shared/runtime");
|
|
29
29
|
|
|
30
30
|
// src/help-view.ts
|
|
31
|
-
var
|
|
31
|
+
var import_runtime4 = require("@guidelytech/shared/runtime");
|
|
32
32
|
|
|
33
33
|
// src/pdf-viewer.ts
|
|
34
|
-
var
|
|
34
|
+
var import_runtime2 = require("@guidelytech/shared/runtime");
|
|
35
35
|
|
|
36
36
|
// src/host.ts
|
|
37
|
+
var import_runtime = require("@guidelytech/shared/runtime");
|
|
37
38
|
function prefersReducedMotion() {
|
|
38
39
|
try {
|
|
39
40
|
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
@@ -44,9 +45,9 @@ function prefersReducedMotion() {
|
|
|
44
45
|
|
|
45
46
|
// src/pdf-viewer.ts
|
|
46
47
|
function enhancePdfViewers(root, doc = root.ownerDocument) {
|
|
47
|
-
for (const block of Array.from(root.querySelectorAll(`div.${
|
|
48
|
-
const link2 = block.querySelector(`a.${
|
|
49
|
-
const strip = block.querySelector(`.${
|
|
48
|
+
for (const block of Array.from(root.querySelectorAll(`div.${import_runtime2.PDF_BLOCK_CLASS}`))) {
|
|
49
|
+
const link2 = block.querySelector(`a.${import_runtime2.FILE_LINK_CLASS}`);
|
|
50
|
+
const strip = block.querySelector(`.${import_runtime2.PDF_PAGES_CLASS}`);
|
|
50
51
|
if (!link2 || !strip || block.hasAttribute("data-g-ready")) continue;
|
|
51
52
|
for (const img of Array.from(strip.querySelectorAll("img:not([src])"))) img.remove();
|
|
52
53
|
const pages = Array.from(strip.querySelectorAll("img"));
|
|
@@ -172,7 +173,7 @@ function openReader(doc, block, link2, pages, start) {
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
// src/sanitize.ts
|
|
175
|
-
var
|
|
176
|
+
var import_runtime3 = require("@guidelytech/shared/runtime");
|
|
176
177
|
var ALLOWED_TAGS = /* @__PURE__ */ new Set(["p", "br", "strong", "b", "em", "i", "u", "a", "ul", "ol", "li", "img", "h1", "h2", "h3", "h4", "code", "pre", "blockquote", "span", "div", "hr", "button"]);
|
|
177
178
|
var DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "object", "embed", "form", "input", "textarea", "select", "link", "meta", "base", "template", "svg", "math", "frame", "frameset", "noscript"]);
|
|
178
179
|
var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["href", "src", "alt", "title", "target", "rel", "class", "width", "height", "data-g-action", "data-g-value"]);
|
|
@@ -213,18 +214,18 @@ function rebuildMedia(el2) {
|
|
|
213
214
|
const src = el2.getAttribute("src");
|
|
214
215
|
let out;
|
|
215
216
|
if (tag === "iframe") {
|
|
216
|
-
if (!(0,
|
|
217
|
+
if (!(0, import_runtime3.isAllowedEmbedSrc)(src)) return null;
|
|
217
218
|
out = doc.createElement("iframe");
|
|
218
219
|
out.setAttribute("src", src);
|
|
219
220
|
out.setAttribute("title", el2.getAttribute("title") || "Embedded video");
|
|
220
|
-
for (const [k, v] of Object.entries(
|
|
221
|
+
for (const [k, v] of Object.entries(import_runtime3.EMBED_IFRAME_ATTRS)) out.setAttribute(k, v);
|
|
221
222
|
} else {
|
|
222
|
-
if (!(0,
|
|
223
|
+
if (!(0, import_runtime3.isSafeMediaUrl)(src)) return null;
|
|
223
224
|
out = doc.createElement("video");
|
|
224
225
|
out.setAttribute("src", src);
|
|
225
226
|
const poster = el2.getAttribute("poster");
|
|
226
|
-
if ((0,
|
|
227
|
-
for (const [k, v] of Object.entries(
|
|
227
|
+
if ((0, import_runtime3.isSafeMediaUrl)(poster)) out.setAttribute("poster", poster);
|
|
228
|
+
for (const [k, v] of Object.entries(import_runtime3.VIDEO_ATTRS)) out.setAttribute(k, v);
|
|
228
229
|
}
|
|
229
230
|
return out;
|
|
230
231
|
}
|
|
@@ -288,8 +289,8 @@ function resultRow(doc, icon2, title, summary, onOpen, showMe) {
|
|
|
288
289
|
return row;
|
|
289
290
|
}
|
|
290
291
|
function enhanceShowMe(root, doc, canStart, start) {
|
|
291
|
-
for (const a of Array.from(root.querySelectorAll(`a[href^="${
|
|
292
|
-
const key = (0,
|
|
292
|
+
for (const a of Array.from(root.querySelectorAll(`a[href^="${import_runtime4.SHOW_ME_HREF_PREFIX}"]`))) {
|
|
293
|
+
const key = (0, import_runtime4.showMeKeyFromHref)(a.getAttribute("href"));
|
|
293
294
|
const holder = a.closest("p.gd-show-me") ?? a;
|
|
294
295
|
if (!key || !canStart(key)) {
|
|
295
296
|
if (holder !== a) holder.remove();
|
|
@@ -404,10 +405,11 @@ function createHelpView(options) {
|
|
|
404
405
|
if (active) return renderArticle(active);
|
|
405
406
|
const data = options.data();
|
|
406
407
|
const member = options.audienceIds?.() ?? [];
|
|
407
|
-
|
|
408
|
+
searchWrap.hidden = !!data.modules && !data.modules.includes("search");
|
|
409
|
+
const sections = (0, import_runtime4.resourceCenterSections)(data.collections, data.articles, member);
|
|
408
410
|
const q = query.trim().toLowerCase();
|
|
409
411
|
if (q) {
|
|
410
|
-
const walkthroughs = (data.recommended ?? []).filter((g) => (0,
|
|
412
|
+
const walkthroughs = (data.recommended ?? []).filter((g) => (0, import_runtime4.matchesHelpQuery)(g.name, q) && startableGuide(options.guides?.() ?? data.recommended, g.id));
|
|
411
413
|
if (walkthroughs.length) {
|
|
412
414
|
section(`Walkthroughs (${walkthroughs.length})`);
|
|
413
415
|
for (const g of walkthroughs) body.appendChild(resultRow(doc, "\u25B6", g.name, "Show me, step by step", () => options.onStartGuide?.(g.id)));
|
|
@@ -416,10 +418,10 @@ function createHelpView(options) {
|
|
|
416
418
|
body.append(...options.extra?.(q) ?? []);
|
|
417
419
|
if (q) {
|
|
418
420
|
const visible = sections.flatMap((s) => s.articles);
|
|
419
|
-
const local = visible.filter((a) => (0,
|
|
421
|
+
const local = visible.filter((a) => (0, import_runtime4.matchesHelpQuery)(`${a.title} ${a.summary}`, q));
|
|
420
422
|
const seen = new Set(local.map((a) => a.id));
|
|
421
423
|
const hidden = new Set(data.articles.map((a) => a.id).filter((id) => !visible.some((v) => v.id === id)));
|
|
422
|
-
const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0,
|
|
424
|
+
const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0, import_runtime4.isArticleListed)(data.collections, a, member))];
|
|
423
425
|
section(`Results (${merged.length})`);
|
|
424
426
|
if (!merged.length) body.appendChild(el(doc, "div", "g-rc-empty", "No articles match your search."));
|
|
425
427
|
merged.forEach(articleButton);
|
|
@@ -434,55 +436,124 @@ function createHelpView(options) {
|
|
|
434
436
|
body.appendChild(b);
|
|
435
437
|
});
|
|
436
438
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
439
|
+
const renderers = {
|
|
440
|
+
search: () => {
|
|
441
|
+
},
|
|
442
|
+
announcements: () => {
|
|
443
|
+
if (!data.announcements?.length) return;
|
|
444
|
+
section(import_runtime4.RC_MODULE_LABELS.announcements);
|
|
445
|
+
const seen = data.announcementsSeenAt ? Date.parse(data.announcementsSeenAt) : NaN;
|
|
446
|
+
data.announcements.slice(0, 20).forEach((n) => body.appendChild(announcementItem(n, Number.isNaN(seen) || Date.parse(n.publishedAt) > seen)));
|
|
447
|
+
},
|
|
448
|
+
guides: () => {
|
|
449
|
+
if (!data.recommended?.length) return;
|
|
450
|
+
section("Recommended");
|
|
451
|
+
data.recommended.forEach((g) => {
|
|
452
|
+
const b = listItem(doc, "\u25B6", g.name, "Start guide");
|
|
453
|
+
b.addEventListener("click", () => options.onStartGuide?.(g.id));
|
|
454
|
+
body.appendChild(b);
|
|
455
|
+
});
|
|
456
|
+
},
|
|
457
|
+
checklists: () => {
|
|
458
|
+
if (!data.checklists?.length) return;
|
|
459
|
+
section("Checklists");
|
|
460
|
+
data.checklists.forEach((g) => {
|
|
461
|
+
const b = listItem(doc, "\u2611", g.name, "Open checklist");
|
|
462
|
+
b.addEventListener("click", () => options.onOpenChecklist?.(g.id));
|
|
463
|
+
body.appendChild(b);
|
|
464
|
+
});
|
|
465
|
+
},
|
|
466
|
+
articles: () => {
|
|
467
|
+
for (const s of sections) {
|
|
468
|
+
section(s.collection ? s.path.join(" \u203A ") : "Articles", s.depth);
|
|
469
|
+
s.articles.forEach(articleButton);
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
faqs: () => {
|
|
473
|
+
if (!data.faqs?.length) return;
|
|
474
|
+
section("FAQ");
|
|
475
|
+
data.faqs.forEach((f) => {
|
|
476
|
+
const b = listItem(doc, "?", f.question);
|
|
477
|
+
b.setAttribute("aria-expanded", "false");
|
|
478
|
+
const a = el(doc, "div", "g-faq-a");
|
|
479
|
+
a.id = `g-faq-${f.id}`;
|
|
480
|
+
b.setAttribute("aria-controls", a.id);
|
|
481
|
+
setSanitizedHtml(a, f.answer);
|
|
482
|
+
a.hidden = true;
|
|
483
|
+
b.addEventListener("click", () => {
|
|
484
|
+
a.hidden = !a.hidden;
|
|
485
|
+
b.setAttribute("aria-expanded", String(!a.hidden));
|
|
486
|
+
});
|
|
487
|
+
body.append(b, a);
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
links: () => {
|
|
491
|
+
if (!data.links?.length) return;
|
|
492
|
+
section("Links");
|
|
493
|
+
data.links.forEach((l) => {
|
|
494
|
+
const a = el(doc, "a", "g-rc-item");
|
|
495
|
+
a.href = l.url;
|
|
496
|
+
a.target = "_blank";
|
|
497
|
+
a.rel = "noopener noreferrer";
|
|
498
|
+
const i = el(doc, "span", "g-rc-icon", "\u2197");
|
|
499
|
+
i.setAttribute("aria-hidden", "true");
|
|
500
|
+
a.append(i, el(doc, "span", "g-rc-title", l.label));
|
|
501
|
+
a.setAttribute("aria-label", `${l.label} (opens in a new tab)`);
|
|
502
|
+
body.appendChild(a);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
for (const id of data.modules ?? import_runtime4.RC_MODULE_IDS) renderers[id]?.();
|
|
507
|
+
if (!body.childNodes.length) body.appendChild(el(doc, "div", "g-rc-empty", "Nothing here yet."));
|
|
508
|
+
}
|
|
509
|
+
function announcementItem(n, unread) {
|
|
510
|
+
const item = el(doc, "article", `g-rc-news${unread ? " g-rc-news-unread" : ""}`);
|
|
511
|
+
const head = el(doc, "div", "g-rc-news-head");
|
|
512
|
+
const title = el(doc, "h3", "g-rc-news-title", n.title);
|
|
513
|
+
head.appendChild(title);
|
|
514
|
+
if (unread) {
|
|
515
|
+
const dot2 = el(doc, "span", "g-rc-news-new", "New");
|
|
516
|
+
head.appendChild(dot2);
|
|
444
517
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
});
|
|
518
|
+
const date = el(doc, "time", "g-rc-news-date");
|
|
519
|
+
date.dateTime = n.publishedAt;
|
|
520
|
+
try {
|
|
521
|
+
date.textContent = new Date(n.publishedAt).toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
|
|
522
|
+
} catch {
|
|
523
|
+
date.textContent = n.publishedAt.slice(0, 10);
|
|
452
524
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
525
|
+
item.append(head, date);
|
|
526
|
+
if (n.body) {
|
|
527
|
+
const b = el(doc, "div", "g-body g-rc-news-body");
|
|
528
|
+
setSanitizedHtml(b, n.body);
|
|
529
|
+
item.appendChild(b);
|
|
456
530
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
a.hidden = true;
|
|
465
|
-
b.addEventListener("click", () => {
|
|
466
|
-
a.hidden = !a.hidden;
|
|
467
|
-
b.setAttribute("aria-expanded", String(!a.hidden));
|
|
468
|
-
});
|
|
469
|
-
body.append(b, a);
|
|
470
|
-
});
|
|
531
|
+
const actions = el(doc, "div", "g-rc-news-actions");
|
|
532
|
+
const guide = n.guideId ? startableGuide(options.guides?.(), n.guideId) : null;
|
|
533
|
+
if (guide) {
|
|
534
|
+
const b = el(doc, "button", "g-btn g-btn-primary", n.linkLabel || "Show me");
|
|
535
|
+
b.type = "button";
|
|
536
|
+
b.addEventListener("click", () => options.onShowMe ? options.onShowMe(guide.key) : options.onStartGuide?.(guide.id));
|
|
537
|
+
actions.appendChild(b);
|
|
471
538
|
}
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
i.setAttribute("aria-hidden", "true");
|
|
481
|
-
a.append(i, el(doc, "span", "g-rc-title", l.label));
|
|
482
|
-
body.appendChild(a);
|
|
483
|
-
});
|
|
539
|
+
if (n.articleId) {
|
|
540
|
+
const listed = options.data().articles.find((a) => a.id === n.articleId);
|
|
541
|
+
if (listed) {
|
|
542
|
+
const b = el(doc, "button", "g-btn g-btn-secondary", guide ? "Read more" : n.linkLabel || "Read more");
|
|
543
|
+
b.type = "button";
|
|
544
|
+
b.addEventListener("click", () => openArticle(listed));
|
|
545
|
+
actions.appendChild(b);
|
|
546
|
+
}
|
|
484
547
|
}
|
|
485
|
-
if (
|
|
548
|
+
if (n.url && /^https?:\/\//i.test(n.url)) {
|
|
549
|
+
const a = el(doc, "a", "g-btn g-btn-link", !guide && !n.articleId && n.linkLabel ? n.linkLabel : "Learn more");
|
|
550
|
+
a.href = n.url;
|
|
551
|
+
a.target = "_blank";
|
|
552
|
+
a.rel = "noopener noreferrer";
|
|
553
|
+
actions.appendChild(a);
|
|
554
|
+
}
|
|
555
|
+
if (actions.childNodes.length) item.appendChild(actions);
|
|
556
|
+
return item;
|
|
486
557
|
}
|
|
487
558
|
let debounce = null;
|
|
488
559
|
const onInput = () => {
|
|
@@ -636,7 +707,7 @@ function renderHowTo(doc, howTo, onShowMe) {
|
|
|
636
707
|
}
|
|
637
708
|
|
|
638
709
|
// src/complaint-view.ts
|
|
639
|
-
var
|
|
710
|
+
var import_runtime5 = require("@guidelytech/shared/runtime");
|
|
640
711
|
var emptyComplaintDraft = (description = "") => ({ description, answers: {}, email: "", attachment: null, error: null, sending: false, checked: false });
|
|
641
712
|
var COMPLAINT_STEPS = [
|
|
642
713
|
["open", "Received"],
|
|
@@ -655,7 +726,7 @@ function complaintAnswers(category, draft) {
|
|
|
655
726
|
const out = {};
|
|
656
727
|
for (const f of category.fields) {
|
|
657
728
|
const v = (draft.answers[f.id] ?? "").trim();
|
|
658
|
-
if (v) out[f.id] = v.slice(0,
|
|
729
|
+
if (v) out[f.id] = v.slice(0, import_runtime5.MAX_COMPLAINT_ANSWER_LENGTH);
|
|
659
730
|
}
|
|
660
731
|
return out;
|
|
661
732
|
}
|
|
@@ -740,7 +811,7 @@ function complaintForm(doc, o) {
|
|
|
740
811
|
} else {
|
|
741
812
|
const input = el(doc, "input", "g-input");
|
|
742
813
|
input.type = "text";
|
|
743
|
-
input.maxLength =
|
|
814
|
+
input.maxLength = import_runtime5.MAX_COMPLAINT_ANSWER_LENGTH;
|
|
744
815
|
input.value = draft.answers[f.id] ?? "";
|
|
745
816
|
input.addEventListener("input", () => draft.answers[f.id] = input.value);
|
|
746
817
|
control = input;
|
|
@@ -750,7 +821,7 @@ function complaintForm(doc, o) {
|
|
|
750
821
|
}
|
|
751
822
|
const what = el(doc, "textarea", "g-input g-m-textarea");
|
|
752
823
|
what.rows = 5;
|
|
753
|
-
what.maxLength =
|
|
824
|
+
what.maxLength = import_runtime5.MAX_COMPLAINT_LENGTH;
|
|
754
825
|
what.required = true;
|
|
755
826
|
what.value = draft.description;
|
|
756
827
|
what.placeholder = "What went wrong, and when?";
|
|
@@ -891,19 +962,887 @@ async function readScreenshot(doc, file) {
|
|
|
891
962
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
892
963
|
for (const q of [0.85, 0.7, 0.5]) {
|
|
893
964
|
dataUrl = canvas.toDataURL("image/jpeg", q);
|
|
894
|
-
if ((dataUrl.length - dataUrl.indexOf(",") - 1) * 0.75 <=
|
|
965
|
+
if ((dataUrl.length - dataUrl.indexOf(",") - 1) * 0.75 <= import_runtime5.MAX_COMPLAINT_ATTACHMENT_BYTES) break;
|
|
895
966
|
}
|
|
896
967
|
scaled = true;
|
|
897
968
|
} catch {
|
|
898
|
-
dataUrl = file.size <=
|
|
969
|
+
dataUrl = file.size <= import_runtime5.MAX_COMPLAINT_ATTACHMENT_BYTES ? await asDataUrl(file) : null;
|
|
899
970
|
}
|
|
900
971
|
if (!dataUrl) throw new Error("That image is too large. Screenshots can be up to 2 MB.");
|
|
901
972
|
const data = dataUrl.slice(dataUrl.indexOf(",") + 1);
|
|
902
|
-
if (data.length * 0.75 >
|
|
973
|
+
if (data.length * 0.75 > import_runtime5.MAX_COMPLAINT_ATTACHMENT_BYTES) throw new Error("That image is too large. Screenshots can be up to 2 MB.");
|
|
903
974
|
const name = (file.name || "screenshot").slice(0, 180);
|
|
904
975
|
return { fileName: scaled ? `${name.replace(/\.[a-z0-9]+$/i, "")}.jpg` : name, data, previewUrl: dataUrl };
|
|
905
976
|
}
|
|
906
977
|
|
|
978
|
+
// src/ideas-view.ts
|
|
979
|
+
var import_shared = require("@guidelytech/shared");
|
|
980
|
+
|
|
981
|
+
// src/render-safe.ts
|
|
982
|
+
var CITATION = /\s*\[\[article:[^\]]*\]\]/g;
|
|
983
|
+
var INLINE = /\*\*([^*\n]+)\*\*|`([^`\n]+)`|(!?)\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()"']+[^\s<>().,;:!?'"])/g;
|
|
984
|
+
function stripCitationMarkers(text) {
|
|
985
|
+
return text.replace(CITATION, "");
|
|
986
|
+
}
|
|
987
|
+
function safeHttpUrl(raw) {
|
|
988
|
+
try {
|
|
989
|
+
const u = new URL(raw);
|
|
990
|
+
return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
|
|
991
|
+
} catch {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function link(doc, label, href) {
|
|
996
|
+
const url = safeHttpUrl(href);
|
|
997
|
+
if (!url) return doc.createTextNode(label);
|
|
998
|
+
const a = doc.createElement("a");
|
|
999
|
+
a.href = url;
|
|
1000
|
+
a.target = "_blank";
|
|
1001
|
+
a.rel = "noopener noreferrer nofollow";
|
|
1002
|
+
a.textContent = label;
|
|
1003
|
+
return a;
|
|
1004
|
+
}
|
|
1005
|
+
function appendInline(doc, parent, text) {
|
|
1006
|
+
let last = 0;
|
|
1007
|
+
for (const m of text.matchAll(INLINE)) {
|
|
1008
|
+
const start = m.index ?? 0;
|
|
1009
|
+
if (start > last) parent.appendChild(doc.createTextNode(text.slice(last, start)));
|
|
1010
|
+
if (m[1] !== void 0) {
|
|
1011
|
+
const b = doc.createElement("strong");
|
|
1012
|
+
b.textContent = m[1];
|
|
1013
|
+
parent.appendChild(b);
|
|
1014
|
+
} else if (m[2] !== void 0) {
|
|
1015
|
+
const c = doc.createElement("code");
|
|
1016
|
+
c.textContent = m[2];
|
|
1017
|
+
parent.appendChild(c);
|
|
1018
|
+
} else if (m[4] !== void 0 && m[5] !== void 0) {
|
|
1019
|
+
parent.appendChild(m[3] ? doc.createTextNode(m[4]) : link(doc, m[4], m[5]));
|
|
1020
|
+
} else if (m[6] !== void 0) {
|
|
1021
|
+
parent.appendChild(link(doc, m[6], m[6]));
|
|
1022
|
+
}
|
|
1023
|
+
last = start + m[0].length;
|
|
1024
|
+
}
|
|
1025
|
+
if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
|
|
1026
|
+
}
|
|
1027
|
+
var BULLET = /^\s*[-*•]\s+/;
|
|
1028
|
+
var NUMBERED = /^\s*\d{1,3}[.)]\s+/;
|
|
1029
|
+
function renderSafeText(doc, text) {
|
|
1030
|
+
const frag = doc.createDocumentFragment();
|
|
1031
|
+
const blocks = stripCitationMarkers(text).replace(/\r\n?/g, "\n").trim().split(/\n{2,}/);
|
|
1032
|
+
for (const block of blocks) {
|
|
1033
|
+
if (!block.trim()) continue;
|
|
1034
|
+
const lines = block.split("\n");
|
|
1035
|
+
if (lines.every((l) => BULLET.test(l)) || lines.every((l) => NUMBERED.test(l))) {
|
|
1036
|
+
const ordered = NUMBERED.test(lines[0]);
|
|
1037
|
+
const list = doc.createElement(ordered ? "ol" : "ul");
|
|
1038
|
+
for (const line of lines) {
|
|
1039
|
+
const li = doc.createElement("li");
|
|
1040
|
+
appendInline(doc, li, line.replace(ordered ? NUMBERED : BULLET, ""));
|
|
1041
|
+
list.appendChild(li);
|
|
1042
|
+
}
|
|
1043
|
+
frag.appendChild(list);
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
const p = doc.createElement("p");
|
|
1047
|
+
lines.forEach((line, i) => {
|
|
1048
|
+
if (i) p.appendChild(doc.createElement("br"));
|
|
1049
|
+
appendInline(doc, p, line);
|
|
1050
|
+
});
|
|
1051
|
+
frag.appendChild(p);
|
|
1052
|
+
}
|
|
1053
|
+
return frag;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/ideas-view.ts
|
|
1057
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
1058
|
+
var IDEA_ICON = ["M9 18h6", "M10 21h4", "M12 3a6 6 0 0 0-3.6 10.8c.7.5 1.1 1.3 1.1 2.1V16h5v-.1c0-.8.4-1.6 1.1-2.1A6 6 0 0 0 12 3z"];
|
|
1059
|
+
var UP_ICON = ["m6 15 6-6 6 6"];
|
|
1060
|
+
function svgIcon(doc, paths, cls = "g-m-ico") {
|
|
1061
|
+
const svg = doc.createElementNS(SVG_NS, "svg");
|
|
1062
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1063
|
+
svg.setAttribute("aria-hidden", "true");
|
|
1064
|
+
svg.setAttribute("class", cls);
|
|
1065
|
+
for (const d of paths) {
|
|
1066
|
+
const p = doc.createElementNS(SVG_NS, "path");
|
|
1067
|
+
p.setAttribute("d", d);
|
|
1068
|
+
svg.appendChild(p);
|
|
1069
|
+
}
|
|
1070
|
+
return svg;
|
|
1071
|
+
}
|
|
1072
|
+
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
1073
|
+
function shortDate(iso) {
|
|
1074
|
+
const d = new Date(iso);
|
|
1075
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
1076
|
+
try {
|
|
1077
|
+
return d.toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
1078
|
+
} catch {
|
|
1079
|
+
return d.toISOString().slice(0, 10);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
var errorText = (e, fallback) => (e instanceof Error && e.message ? e.message : fallback).replace(/^[\w.]+: /, "");
|
|
1083
|
+
function ideaStatusPill(doc, status) {
|
|
1084
|
+
return el(doc, "span", `g-m-pill g-m-idea-pill g-m-idea-${status}`, import_shared.FEEDBACK_STATUS_LABEL[status] ?? status);
|
|
1085
|
+
}
|
|
1086
|
+
function voteControl(doc, idea, o) {
|
|
1087
|
+
const count = el(doc, "span", "g-m-vote-count", String(idea.votes));
|
|
1088
|
+
if (!o.canVote) {
|
|
1089
|
+
const box = el(doc, "span", "g-m-vote g-m-vote-static");
|
|
1090
|
+
box.setAttribute("aria-label", plural(idea.votes, "vote"));
|
|
1091
|
+
box.append(svgIcon(doc, UP_ICON, "g-m-ico g-m-vote-ico"), count);
|
|
1092
|
+
return box;
|
|
1093
|
+
}
|
|
1094
|
+
const b = el(doc, "button", `g-m-vote${idea.myVote ? " g-m-voted" : ""}`);
|
|
1095
|
+
b.type = "button";
|
|
1096
|
+
b.setAttribute("aria-pressed", String(!!idea.myVote));
|
|
1097
|
+
b.setAttribute("aria-label", `${idea.myVote ? "Remove your vote for" : "Vote for"} ${idea.title} (${plural(idea.votes, "vote")})`);
|
|
1098
|
+
b.disabled = !!o.busy;
|
|
1099
|
+
b.append(svgIcon(doc, UP_ICON, "g-m-ico g-m-vote-ico"), count);
|
|
1100
|
+
b.addEventListener("click", (e) => {
|
|
1101
|
+
e.stopPropagation();
|
|
1102
|
+
o.onToggle?.();
|
|
1103
|
+
});
|
|
1104
|
+
return b;
|
|
1105
|
+
}
|
|
1106
|
+
function ideaRow(doc, idea, o) {
|
|
1107
|
+
const row = el(doc, "div", "g-m-idea");
|
|
1108
|
+
row.setAttribute("role", "listitem");
|
|
1109
|
+
row.appendChild(voteControl(doc, idea, { canVote: o.canVote, busy: o.busy, onToggle: o.onVote }));
|
|
1110
|
+
const open = el(doc, "button", "g-m-idea-open");
|
|
1111
|
+
open.type = "button";
|
|
1112
|
+
open.appendChild(el(doc, "span", "g-m-idea-title", idea.title));
|
|
1113
|
+
const meta = el(doc, "span", "g-m-idea-meta");
|
|
1114
|
+
meta.appendChild(ideaStatusPill(doc, idea.status));
|
|
1115
|
+
if (idea.comments) meta.appendChild(el(doc, "span", "g-m-time", plural(idea.comments, "comment")));
|
|
1116
|
+
if (idea.productArea) meta.appendChild(el(doc, "span", "g-m-time", idea.productArea));
|
|
1117
|
+
if (idea.mine) meta.appendChild(el(doc, "span", "g-m-time", "Yours"));
|
|
1118
|
+
open.appendChild(meta);
|
|
1119
|
+
open.addEventListener("click", o.onOpen);
|
|
1120
|
+
row.appendChild(open);
|
|
1121
|
+
return row;
|
|
1122
|
+
}
|
|
1123
|
+
var FILTERS = [
|
|
1124
|
+
["open", "Open"],
|
|
1125
|
+
["all", "All"],
|
|
1126
|
+
["mine", "Mine"],
|
|
1127
|
+
["following", "Following"]
|
|
1128
|
+
];
|
|
1129
|
+
var SORTS = [
|
|
1130
|
+
["top", "Top"],
|
|
1131
|
+
["new", "New"],
|
|
1132
|
+
["trending", "Trending"]
|
|
1133
|
+
];
|
|
1134
|
+
function filterChips(doc, current, onPick) {
|
|
1135
|
+
const wrap = el(doc, "div", "g-m-filters");
|
|
1136
|
+
wrap.setAttribute("role", "group");
|
|
1137
|
+
wrap.setAttribute("aria-label", "Show");
|
|
1138
|
+
for (const [f, label] of FILTERS) {
|
|
1139
|
+
const b = el(doc, "button", "g-m-filter", label);
|
|
1140
|
+
b.type = "button";
|
|
1141
|
+
b.dataset.filter = f;
|
|
1142
|
+
b.setAttribute("aria-pressed", String(f === current));
|
|
1143
|
+
b.addEventListener("click", () => {
|
|
1144
|
+
wrap.querySelectorAll("button").forEach((x) => x.setAttribute("aria-pressed", String(x === b)));
|
|
1145
|
+
onPick(f);
|
|
1146
|
+
});
|
|
1147
|
+
wrap.appendChild(b);
|
|
1148
|
+
}
|
|
1149
|
+
return wrap;
|
|
1150
|
+
}
|
|
1151
|
+
function sortSelect(doc, current, onChange) {
|
|
1152
|
+
const select = el(doc, "select", "g-input g-m-sort");
|
|
1153
|
+
select.setAttribute("aria-label", "Sort ideas");
|
|
1154
|
+
for (const [s, label] of SORTS) {
|
|
1155
|
+
const opt = el(doc, "option", "", label);
|
|
1156
|
+
opt.value = s;
|
|
1157
|
+
select.appendChild(opt);
|
|
1158
|
+
}
|
|
1159
|
+
select.value = current;
|
|
1160
|
+
select.addEventListener("change", () => onChange(select.value));
|
|
1161
|
+
return select;
|
|
1162
|
+
}
|
|
1163
|
+
function priorityChoice(doc, current, onPick, label = "How important is this to you?") {
|
|
1164
|
+
const wrap = el(doc, "div", "g-m-field");
|
|
1165
|
+
const id = `g-m-prio${++fieldSeq2}`;
|
|
1166
|
+
const l = el(doc, "span", "g-m-label", label);
|
|
1167
|
+
l.id = id;
|
|
1168
|
+
const group = el(doc, "div", "g-m-seg");
|
|
1169
|
+
group.setAttribute("role", "radiogroup");
|
|
1170
|
+
group.setAttribute("aria-labelledby", id);
|
|
1171
|
+
for (const p of import_shared.FEEDBACK_PRIORITIES) {
|
|
1172
|
+
const b = el(doc, "button", "g-m-seg-btn", import_shared.FEEDBACK_PRIORITY_LABEL[p]);
|
|
1173
|
+
b.type = "button";
|
|
1174
|
+
b.dataset.priority = p;
|
|
1175
|
+
b.setAttribute("role", "radio");
|
|
1176
|
+
b.setAttribute("aria-checked", String(p === current));
|
|
1177
|
+
b.addEventListener("click", () => {
|
|
1178
|
+
group.querySelectorAll("button").forEach((x) => x.setAttribute("aria-checked", String(x === b)));
|
|
1179
|
+
onPick(p);
|
|
1180
|
+
});
|
|
1181
|
+
group.appendChild(b);
|
|
1182
|
+
}
|
|
1183
|
+
wrap.append(l, group);
|
|
1184
|
+
return wrap;
|
|
1185
|
+
}
|
|
1186
|
+
var fieldSeq2 = 0;
|
|
1187
|
+
function field2(doc, label, control, opts = {}) {
|
|
1188
|
+
const id = `g-m-i${++fieldSeq2}`;
|
|
1189
|
+
control.id = id;
|
|
1190
|
+
const wrap = el(doc, "div", "g-m-field");
|
|
1191
|
+
const l = el(doc, "label", "g-m-label", label);
|
|
1192
|
+
l.htmlFor = id;
|
|
1193
|
+
if (opts.optional) l.appendChild(el(doc, "span", "g-m-optional", " (optional)"));
|
|
1194
|
+
if (opts.invalid) control.setAttribute("aria-invalid", "true");
|
|
1195
|
+
wrap.append(l, control);
|
|
1196
|
+
return wrap;
|
|
1197
|
+
}
|
|
1198
|
+
function errorLine(doc, text) {
|
|
1199
|
+
const p = el(doc, "p", "g-m-error", text);
|
|
1200
|
+
p.setAttribute("role", "alert");
|
|
1201
|
+
return p;
|
|
1202
|
+
}
|
|
1203
|
+
function button(doc, cls, text, onClick) {
|
|
1204
|
+
const b = el(doc, "button", cls, text);
|
|
1205
|
+
b.type = "button";
|
|
1206
|
+
b.addEventListener("click", onClick);
|
|
1207
|
+
return b;
|
|
1208
|
+
}
|
|
1209
|
+
function writeNotice(doc, reason, onPortal) {
|
|
1210
|
+
const box = el(doc, "div", "g-m-idea-notice");
|
|
1211
|
+
box.setAttribute("role", "note");
|
|
1212
|
+
box.appendChild(el(doc, "span", "", reason));
|
|
1213
|
+
if (onPortal) box.appendChild(button(doc, "g-m-text-btn", "Open the feedback portal", onPortal));
|
|
1214
|
+
return box;
|
|
1215
|
+
}
|
|
1216
|
+
function comment(doc, c) {
|
|
1217
|
+
const li = el(doc, "li", `g-m-comment${c.authorType === "team" ? " g-m-comment-team" : ""}`);
|
|
1218
|
+
const who = el(doc, "div", "g-m-row g-m-center g-m-comment-meta");
|
|
1219
|
+
who.appendChild(el(doc, "span", "g-m-comment-author", c.authorName || (c.authorType === "team" ? "Team" : "Someone")));
|
|
1220
|
+
if (c.authorType === "team") who.appendChild(el(doc, "span", "g-m-ai", "Team"));
|
|
1221
|
+
who.appendChild(el(doc, "span", "g-m-time", shortDate(c.createdAt)));
|
|
1222
|
+
const body = el(doc, "div", "g-m-comment-body");
|
|
1223
|
+
body.appendChild(renderSafeText(doc, c.body));
|
|
1224
|
+
li.append(who, body);
|
|
1225
|
+
return li;
|
|
1226
|
+
}
|
|
1227
|
+
function ideaDetail(doc, o) {
|
|
1228
|
+
const { idea } = o;
|
|
1229
|
+
const wrap = el(doc, "div", "g-m-form g-m-idea-detail");
|
|
1230
|
+
const meta = el(doc, "div", "g-m-row g-m-center g-m-idea-meta");
|
|
1231
|
+
meta.append(el(doc, "span", "g-m-time", (0, import_shared.feedbackReference)(idea.number)), ideaStatusPill(doc, idea.status));
|
|
1232
|
+
if (idea.productArea) meta.appendChild(el(doc, "span", "g-m-time", idea.productArea));
|
|
1233
|
+
wrap.append(meta, el(doc, "h3", "g-m-form-title", idea.title));
|
|
1234
|
+
if (idea.description.trim()) {
|
|
1235
|
+
const d = el(doc, "div", "g-m-idea-desc");
|
|
1236
|
+
d.appendChild(renderSafeText(doc, idea.description));
|
|
1237
|
+
wrap.appendChild(d);
|
|
1238
|
+
}
|
|
1239
|
+
if (idea.statusNote.trim()) {
|
|
1240
|
+
const note = el(doc, "div", "g-m-idea-note");
|
|
1241
|
+
note.appendChild(el(doc, "span", "g-m-card-label", `${import_shared.FEEDBACK_STATUS_LABEL[idea.status] ?? idea.status} \xB7 from the team`));
|
|
1242
|
+
note.appendChild(renderSafeText(doc, idea.statusNote));
|
|
1243
|
+
wrap.appendChild(note);
|
|
1244
|
+
}
|
|
1245
|
+
const panel = el(doc, "div", "g-m-card g-m-static g-m-idea-vote");
|
|
1246
|
+
const top = el(doc, "div", "g-m-row g-m-center");
|
|
1247
|
+
top.appendChild(voteControl(doc, idea, { canVote: false }));
|
|
1248
|
+
const said = el(doc, "span", "g-m-grow");
|
|
1249
|
+
said.appendChild(el(doc, "span", "g-m-card-title", plural(idea.votes, "vote")));
|
|
1250
|
+
if (idea.myVote) said.appendChild(el(doc, "span", "g-m-card-sub", `You voted \xB7 ${import_shared.FEEDBACK_PRIORITY_LABEL[idea.myVote.priority]}`));
|
|
1251
|
+
top.appendChild(said);
|
|
1252
|
+
if (!o.blocked) {
|
|
1253
|
+
const follow = button(doc, `g-m-follow${idea.following ? " g-m-following" : ""}`, idea.following ? "Following" : "Follow", () => o.onFollow(!idea.following));
|
|
1254
|
+
follow.setAttribute("aria-pressed", String(idea.following));
|
|
1255
|
+
follow.disabled = o.busy;
|
|
1256
|
+
follow.title = idea.following ? "Stop getting updates about this idea" : "Get updates here when it changes";
|
|
1257
|
+
top.appendChild(follow);
|
|
1258
|
+
}
|
|
1259
|
+
panel.appendChild(top);
|
|
1260
|
+
if (o.error) panel.appendChild(errorLine(doc, o.error));
|
|
1261
|
+
if (o.blocked) panel.appendChild(writeNotice(doc, o.blocked, o.onPortal));
|
|
1262
|
+
else if (idea.myVote) {
|
|
1263
|
+
panel.appendChild(priorityChoice(doc, idea.myVote.priority, (p) => p !== idea.myVote?.priority && o.onVote(p, ""), "How much do you need it?"));
|
|
1264
|
+
const remove = button(doc, "g-m-text-btn g-m-unvote", "Remove my vote", o.onUnvote);
|
|
1265
|
+
remove.disabled = o.busy;
|
|
1266
|
+
panel.appendChild(remove);
|
|
1267
|
+
} else {
|
|
1268
|
+
const vote = o.vote;
|
|
1269
|
+
panel.appendChild(priorityChoice(doc, vote.priority, (p) => vote.priority = p));
|
|
1270
|
+
const note = el(doc, "textarea", "g-input g-m-idea-why");
|
|
1271
|
+
note.rows = 2;
|
|
1272
|
+
note.maxLength = import_shared.MAX_FEEDBACK_NOTE;
|
|
1273
|
+
note.value = vote.note;
|
|
1274
|
+
note.placeholder = "What would it let you do?";
|
|
1275
|
+
note.addEventListener("input", () => vote.note = note.value);
|
|
1276
|
+
panel.appendChild(field2(doc, "Why it matters", note, { optional: true }));
|
|
1277
|
+
const go = button(doc, "g-btn g-btn-primary g-m-wide g-m-vote-submit", o.busy ? "Voting\u2026" : "Vote for this idea", () => o.onVote(vote.priority, vote.note.trim()));
|
|
1278
|
+
go.disabled = o.busy;
|
|
1279
|
+
panel.appendChild(go);
|
|
1280
|
+
}
|
|
1281
|
+
wrap.appendChild(panel);
|
|
1282
|
+
const comments = el(doc, "div", "g-m-comments");
|
|
1283
|
+
comments.appendChild(el(doc, "h4", "g-m-comments-title", idea.thread.length ? `Comments (${idea.thread.length})` : "Comments"));
|
|
1284
|
+
if (idea.thread.length) {
|
|
1285
|
+
const list = el(doc, "ul", "g-m-comment-list");
|
|
1286
|
+
idea.thread.forEach((c) => list.appendChild(comment(doc, c)));
|
|
1287
|
+
comments.appendChild(list);
|
|
1288
|
+
} else comments.appendChild(el(doc, "p", "g-m-hint", "No comments yet."));
|
|
1289
|
+
if (!o.blocked) {
|
|
1290
|
+
const form = el(doc, "form", "g-m-comment-form");
|
|
1291
|
+
form.noValidate = true;
|
|
1292
|
+
const box = el(doc, "textarea", "g-input");
|
|
1293
|
+
box.rows = 2;
|
|
1294
|
+
box.maxLength = import_shared.MAX_FEEDBACK_COMMENT;
|
|
1295
|
+
box.placeholder = "Add a comment\u2026";
|
|
1296
|
+
box.setAttribute("aria-label", "Comment");
|
|
1297
|
+
box.value = o.commentDraft;
|
|
1298
|
+
const post = el(doc, "button", "g-btn g-btn-secondary", "Post");
|
|
1299
|
+
post.type = "submit";
|
|
1300
|
+
post.disabled = o.busy || !o.commentDraft.trim();
|
|
1301
|
+
box.addEventListener("input", () => {
|
|
1302
|
+
o.onCommentInput(box.value);
|
|
1303
|
+
post.disabled = o.busy || !box.value.trim();
|
|
1304
|
+
});
|
|
1305
|
+
form.addEventListener("submit", (e) => {
|
|
1306
|
+
e.preventDefault();
|
|
1307
|
+
if (box.value.trim() && !o.busy) o.onComment();
|
|
1308
|
+
});
|
|
1309
|
+
form.append(box, post);
|
|
1310
|
+
comments.appendChild(form);
|
|
1311
|
+
}
|
|
1312
|
+
wrap.appendChild(comments);
|
|
1313
|
+
return wrap;
|
|
1314
|
+
}
|
|
1315
|
+
var emptyIdeaDraft = (over = {}) => ({ title: "", description: "", productArea: "", priority: "nice_to_have", conversationId: null, similar: null, similarFor: "", error: null, sending: false, checked: false, ...over });
|
|
1316
|
+
function ideaProblem(draft) {
|
|
1317
|
+
return draft.title.trim().length < 3 ? "Give your idea a short title." : null;
|
|
1318
|
+
}
|
|
1319
|
+
function ideaForm(doc, o) {
|
|
1320
|
+
const { draft } = o;
|
|
1321
|
+
const form = el(doc, "form", "g-m-form g-m-idea-form");
|
|
1322
|
+
form.noValidate = true;
|
|
1323
|
+
const title = el(doc, "input", "g-input");
|
|
1324
|
+
title.type = "text";
|
|
1325
|
+
title.maxLength = import_shared.MAX_FEEDBACK_TITLE;
|
|
1326
|
+
title.required = true;
|
|
1327
|
+
title.value = draft.title;
|
|
1328
|
+
title.placeholder = "Your idea in a few words";
|
|
1329
|
+
title.addEventListener("input", () => {
|
|
1330
|
+
draft.title = title.value;
|
|
1331
|
+
o.onTitleInput();
|
|
1332
|
+
});
|
|
1333
|
+
form.appendChild(field2(doc, "Idea", title, { invalid: draft.checked && !!ideaProblem(draft) }));
|
|
1334
|
+
const similar = el(doc, "div", "g-m-similar");
|
|
1335
|
+
similar.setAttribute("aria-live", "polite");
|
|
1336
|
+
form.appendChild(similar);
|
|
1337
|
+
const desc = el(doc, "textarea", "g-input g-m-textarea");
|
|
1338
|
+
desc.rows = 4;
|
|
1339
|
+
desc.maxLength = import_shared.MAX_FEEDBACK_DESCRIPTION;
|
|
1340
|
+
desc.value = draft.description;
|
|
1341
|
+
desc.placeholder = "What are you trying to do, and what gets in the way?";
|
|
1342
|
+
desc.addEventListener("input", () => draft.description = desc.value);
|
|
1343
|
+
form.appendChild(field2(doc, "Details", desc, { optional: true }));
|
|
1344
|
+
if (o.productAreas.length) {
|
|
1345
|
+
const select = el(doc, "select", "g-input");
|
|
1346
|
+
const opt = (label, value) => {
|
|
1347
|
+
const x = el(doc, "option", "", label);
|
|
1348
|
+
x.value = value;
|
|
1349
|
+
return x;
|
|
1350
|
+
};
|
|
1351
|
+
select.appendChild(opt("Choose one (optional)", ""));
|
|
1352
|
+
o.productAreas.forEach((a) => select.appendChild(opt(a, a)));
|
|
1353
|
+
select.value = o.productAreas.includes(draft.productArea) ? draft.productArea : "";
|
|
1354
|
+
select.addEventListener("change", () => draft.productArea = select.value);
|
|
1355
|
+
form.appendChild(field2(doc, "Product area", select, { optional: true }));
|
|
1356
|
+
}
|
|
1357
|
+
form.appendChild(priorityChoice(doc, draft.priority, (p) => draft.priority = p));
|
|
1358
|
+
const foot = el(doc, "div", "g-m-form-foot");
|
|
1359
|
+
if (draft.error) foot.appendChild(errorLine(doc, draft.error));
|
|
1360
|
+
const submit = el(doc, "button", "g-btn g-btn-primary g-m-wide", draft.sending ? "Sending\u2026" : "Submit idea");
|
|
1361
|
+
submit.type = "submit";
|
|
1362
|
+
submit.disabled = draft.sending;
|
|
1363
|
+
foot.appendChild(submit);
|
|
1364
|
+
form.appendChild(foot);
|
|
1365
|
+
form.addEventListener("submit", (e) => {
|
|
1366
|
+
e.preventDefault();
|
|
1367
|
+
if (!draft.sending) o.onSubmit();
|
|
1368
|
+
});
|
|
1369
|
+
return { form, similar };
|
|
1370
|
+
}
|
|
1371
|
+
function similarIdeas(doc, ideas, row) {
|
|
1372
|
+
if (!ideas.length) return null;
|
|
1373
|
+
const box = el(doc, "div", "g-m-similar-box");
|
|
1374
|
+
box.appendChild(el(doc, "div", "g-m-card-label", "Similar ideas \u2014 vote instead?"));
|
|
1375
|
+
const list = el(doc, "div", "g-m-idea-list");
|
|
1376
|
+
list.setAttribute("role", "list");
|
|
1377
|
+
ideas.slice(0, 5).forEach((i) => list.appendChild(row(i)));
|
|
1378
|
+
box.appendChild(list);
|
|
1379
|
+
return box;
|
|
1380
|
+
}
|
|
1381
|
+
function ideaSent(doc, o) {
|
|
1382
|
+
const wrap = el(doc, "div", "g-m-form g-m-sent");
|
|
1383
|
+
const top = el(doc, "div", "g-m-sent-top");
|
|
1384
|
+
const tick = el(doc, "span", "g-m-sent-tick", "\u2713");
|
|
1385
|
+
tick.setAttribute("aria-hidden", "true");
|
|
1386
|
+
const p = el(doc, "p", "g-m-card-sub");
|
|
1387
|
+
p.append(el(doc, "strong", "", `${(0, import_shared.feedbackReference)(o.idea.number)} \xB7 ${o.idea.title}`), o.idea.following ? ". You're following it: updates show up here when its status changes." : ". Others can now vote for it.");
|
|
1388
|
+
top.append(tick, el(doc, "h3", "g-m-form-title", "Thanks for your idea"), p);
|
|
1389
|
+
wrap.append(top, button(doc, "g-btn g-btn-primary g-m-wide", "View your idea", o.onView), button(doc, "g-btn g-btn-secondary g-m-wide", "Back to ideas", o.onBack));
|
|
1390
|
+
return wrap;
|
|
1391
|
+
}
|
|
1392
|
+
function updatesList(doc, updates, onOpen) {
|
|
1393
|
+
const wrap = el(doc, "div", "g-m-idea-updates");
|
|
1394
|
+
if (!updates.length) {
|
|
1395
|
+
wrap.appendChild(el(doc, "div", "g-m-empty", "No updates yet. Follow or vote for an idea to hear when it changes."));
|
|
1396
|
+
return wrap;
|
|
1397
|
+
}
|
|
1398
|
+
for (const u of updates) {
|
|
1399
|
+
const card = el(doc, "button", "g-m-card g-m-update");
|
|
1400
|
+
card.type = "button";
|
|
1401
|
+
card.disabled = !u.requestId;
|
|
1402
|
+
const row = el(doc, "span", "g-m-row");
|
|
1403
|
+
if (!u.read) row.appendChild(el(doc, "span", "g-m-dot"));
|
|
1404
|
+
const text = el(doc, "span", "g-m-grow");
|
|
1405
|
+
text.appendChild(el(doc, "span", "g-m-card-title", u.title));
|
|
1406
|
+
if (u.body) text.appendChild(el(doc, "span", "g-m-card-sub g-m-clamp", u.body));
|
|
1407
|
+
row.append(text, el(doc, "span", "g-m-time", shortDate(u.createdAt)));
|
|
1408
|
+
card.appendChild(row);
|
|
1409
|
+
card.setAttribute("aria-label", `${u.title}${u.read ? "" : ", new"}`);
|
|
1410
|
+
card.addEventListener("click", () => onOpen(u));
|
|
1411
|
+
wrap.appendChild(card);
|
|
1412
|
+
}
|
|
1413
|
+
return wrap;
|
|
1414
|
+
}
|
|
1415
|
+
var LIST_TTL_MS = 15e3;
|
|
1416
|
+
var asDetail = (d) => ({ ...d, title: d.title ?? "", description: d.description ?? "", statusNote: d.statusNote ?? "", votes: d.votes ?? 0, thread: Array.isArray(d.thread) ? d.thread : [] });
|
|
1417
|
+
function createIdeas(o) {
|
|
1418
|
+
const { doc, api } = o;
|
|
1419
|
+
let query = { sort: "top", filter: "open", page: 1 };
|
|
1420
|
+
let list = null;
|
|
1421
|
+
let hasMore = false;
|
|
1422
|
+
let listAt = 0;
|
|
1423
|
+
let listLoading = false;
|
|
1424
|
+
let listError = null;
|
|
1425
|
+
let listSeq = 0;
|
|
1426
|
+
let listActionError = null;
|
|
1427
|
+
const details = /* @__PURE__ */ new Map();
|
|
1428
|
+
const detailErrors = /* @__PURE__ */ new Map();
|
|
1429
|
+
const actionErrors = /* @__PURE__ */ new Map();
|
|
1430
|
+
const voteDrafts = /* @__PURE__ */ new Map();
|
|
1431
|
+
const commentDrafts = /* @__PURE__ */ new Map();
|
|
1432
|
+
const busy = /* @__PURE__ */ new Set();
|
|
1433
|
+
const offers = /* @__PURE__ */ new Map();
|
|
1434
|
+
let updates = null;
|
|
1435
|
+
let updatesAt = 0;
|
|
1436
|
+
let updatesLoading = false;
|
|
1437
|
+
let updatesSeq = 0;
|
|
1438
|
+
let draft = emptyIdeaDraft();
|
|
1439
|
+
let similarTimer = null;
|
|
1440
|
+
let similarSeq = 0;
|
|
1441
|
+
let searchTimer = null;
|
|
1442
|
+
let who = o.identified();
|
|
1443
|
+
let mounted = null;
|
|
1444
|
+
const mount = (root, paint) => {
|
|
1445
|
+
mounted = { root, paint };
|
|
1446
|
+
paint();
|
|
1447
|
+
};
|
|
1448
|
+
const repaint = () => {
|
|
1449
|
+
if (mounted?.root.isConnected) mounted.paint();
|
|
1450
|
+
};
|
|
1451
|
+
const showing = (root) => mounted?.root === root && root.isConnected;
|
|
1452
|
+
const cfg = () => o.config() ?? { title: "Ideas", anonymous: false, readOnly: true, productAreas: [], portal: false };
|
|
1453
|
+
function blocked() {
|
|
1454
|
+
const c = cfg();
|
|
1455
|
+
if (c.readOnly || !api.voteIdea) return c.portal ? "Voting and new ideas happen in our feedback portal." : "You can browse ideas here. Voting isn't open in the app.";
|
|
1456
|
+
if (!c.anonymous && !o.identified()) return "Log in to vote, comment or share an idea.";
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
const canPortal = () => cfg().portal && !!api.ideaPortalLink;
|
|
1460
|
+
function openPortal() {
|
|
1461
|
+
if (!api.ideaPortalLink) return;
|
|
1462
|
+
const win = doc.defaultView;
|
|
1463
|
+
let tab = null;
|
|
1464
|
+
try {
|
|
1465
|
+
tab = win?.open("about:blank", "_blank") ?? null;
|
|
1466
|
+
if (tab) tab.opener = null;
|
|
1467
|
+
} catch {
|
|
1468
|
+
tab = null;
|
|
1469
|
+
}
|
|
1470
|
+
api.ideaPortalLink().then((raw) => {
|
|
1471
|
+
const url = safeHttpUrl(raw);
|
|
1472
|
+
if (!url) throw new Error("The portal link isn't available.");
|
|
1473
|
+
if (tab) tab.location.href = url;
|
|
1474
|
+
else win?.open(url, "_blank", "noopener");
|
|
1475
|
+
}).catch((e) => {
|
|
1476
|
+
tab?.close();
|
|
1477
|
+
listActionError = errorText(e, "The feedback portal couldn't be opened.");
|
|
1478
|
+
repaint();
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
const portal = () => canPortal() ? openPortal : void 0;
|
|
1482
|
+
function patch(idea) {
|
|
1483
|
+
if (list) list = list.map((x) => x.id === idea.id ? { ...x, ...idea } : x);
|
|
1484
|
+
const d = details.get(idea.id);
|
|
1485
|
+
if (d) details.set(idea.id, { ...d, ...idea, thread: "thread" in idea ? idea.thread : d.thread });
|
|
1486
|
+
if (draft.similar) draft.similar = draft.similar.map((x) => x.id === idea.id ? { ...x, ...idea } : x);
|
|
1487
|
+
}
|
|
1488
|
+
const find = (id) => details.get(id) ?? list?.find((x) => x.id === id) ?? draft.similar?.find((x) => x.id === id);
|
|
1489
|
+
function toggleVote(idea) {
|
|
1490
|
+
if (busy.has(idea.id) || blocked() || !api.voteIdea || idea.myVote && !api.unvoteIdea) return;
|
|
1491
|
+
busy.add(idea.id);
|
|
1492
|
+
listActionError = null;
|
|
1493
|
+
const voted = !!idea.myVote;
|
|
1494
|
+
patch({ ...idea, votes: Math.max(0, idea.votes + (voted ? -1 : 1)), myVote: voted ? null : { priority: "nice_to_have" } });
|
|
1495
|
+
repaint();
|
|
1496
|
+
(voted ? api.unvoteIdea(idea.id) : api.voteIdea(idea.id, { priority: "nice_to_have" })).then(patch).catch((e) => {
|
|
1497
|
+
patch(idea);
|
|
1498
|
+
listActionError = errorText(e, "Your vote couldn't be saved.");
|
|
1499
|
+
}).finally(() => {
|
|
1500
|
+
busy.delete(idea.id);
|
|
1501
|
+
repaint();
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
function loadList(append = false) {
|
|
1505
|
+
if (!api.listIdeas) return;
|
|
1506
|
+
const seq = ++listSeq;
|
|
1507
|
+
listLoading = true;
|
|
1508
|
+
listError = null;
|
|
1509
|
+
const q = { ...query };
|
|
1510
|
+
api.listIdeas(q).then((res) => {
|
|
1511
|
+
if (seq !== listSeq) return;
|
|
1512
|
+
const got = Array.isArray(res?.requests) ? res.requests : [];
|
|
1513
|
+
list = append && list ? [...list, ...got.filter((r) => !list.some((x) => x.id === r.id))] : got;
|
|
1514
|
+
hasMore = !!res?.hasMore;
|
|
1515
|
+
listAt = Date.now();
|
|
1516
|
+
}).catch((e) => {
|
|
1517
|
+
if (seq === listSeq) listError = errorText(e, "Ideas couldn't be loaded.");
|
|
1518
|
+
}).finally(() => {
|
|
1519
|
+
if (seq !== listSeq) return;
|
|
1520
|
+
listLoading = false;
|
|
1521
|
+
repaint();
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
const requery = (change) => {
|
|
1525
|
+
query = { ...query, ...change, page: 1 };
|
|
1526
|
+
loadList();
|
|
1527
|
+
repaint();
|
|
1528
|
+
};
|
|
1529
|
+
function listScreen() {
|
|
1530
|
+
const root = el(doc, "div", "g-m-scroll g-m-ideas");
|
|
1531
|
+
const top = el(doc, "div", "g-m-ideas-top");
|
|
1532
|
+
const why = blocked();
|
|
1533
|
+
if (why) top.appendChild(writeNotice(doc, why, portal()));
|
|
1534
|
+
else top.appendChild(button(doc, "g-btn g-btn-primary g-m-wide", "Suggest an idea", o.openForm));
|
|
1535
|
+
const updatesLink = el(doc, "div");
|
|
1536
|
+
top.appendChild(updatesLink);
|
|
1537
|
+
const search = el(doc, "input", "g-input g-m-idea-search");
|
|
1538
|
+
search.type = "search";
|
|
1539
|
+
search.placeholder = "Search ideas";
|
|
1540
|
+
search.setAttribute("aria-label", "Search ideas");
|
|
1541
|
+
search.maxLength = 200;
|
|
1542
|
+
search.value = query.q ?? "";
|
|
1543
|
+
search.addEventListener("input", () => {
|
|
1544
|
+
if (searchTimer) clearTimeout(searchTimer);
|
|
1545
|
+
searchTimer = setTimeout(() => requery({ q: search.value.trim() || void 0 }), 300);
|
|
1546
|
+
});
|
|
1547
|
+
const tools = el(doc, "div", "g-m-row g-m-center g-m-idea-tools");
|
|
1548
|
+
tools.append(filterChips(doc, query.filter, (filter) => requery({ filter })), sortSelect(doc, query.sort, (sort) => requery({ sort })));
|
|
1549
|
+
top.append(search, tools);
|
|
1550
|
+
const results = el(doc, "div", "g-m-idea-results");
|
|
1551
|
+
root.append(top, results);
|
|
1552
|
+
mount(root, () => {
|
|
1553
|
+
updatesLink.textContent = "";
|
|
1554
|
+
const n = updates?.unread ?? 0;
|
|
1555
|
+
if (updates?.updates.length) updatesLink.appendChild(button(doc, "g-m-text-btn g-m-updates-link", n ? `Updates on your ideas (${n} new)` : "Updates on your ideas", o.openUpdates));
|
|
1556
|
+
results.textContent = "";
|
|
1557
|
+
if (listActionError) results.appendChild(errorLine(doc, listActionError));
|
|
1558
|
+
if (!list) {
|
|
1559
|
+
if (listError) {
|
|
1560
|
+
results.append(errorLine(doc, listError), button(doc, "g-m-text-btn", "Try again", () => loadList()));
|
|
1561
|
+
} else results.appendChild(el(doc, "div", "g-m-empty", "Loading ideas\u2026"));
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
if (!list.length) {
|
|
1565
|
+
const empty = query.q ? `No ideas match \u201C${query.q}\u201D.` : query.filter === "mine" ? "You haven't suggested any ideas yet." : query.filter === "following" ? "You aren't following any ideas yet." : "No ideas yet. Be the first to suggest one.";
|
|
1566
|
+
results.appendChild(el(doc, "div", "g-m-empty", empty));
|
|
1567
|
+
} else {
|
|
1568
|
+
const rows = el(doc, "div", "g-m-idea-list");
|
|
1569
|
+
rows.setAttribute("role", "list");
|
|
1570
|
+
rows.setAttribute("aria-busy", String(listLoading));
|
|
1571
|
+
const canVote = !blocked();
|
|
1572
|
+
list.forEach((idea) => rows.appendChild(ideaRow(doc, idea, { canVote, busy: busy.has(idea.id), onVote: () => toggleVote(idea), onOpen: () => o.openIdea(idea.id) })));
|
|
1573
|
+
results.appendChild(rows);
|
|
1574
|
+
}
|
|
1575
|
+
if (listError) results.appendChild(errorLine(doc, listError));
|
|
1576
|
+
if (hasMore) {
|
|
1577
|
+
const more = button(doc, "g-btn g-btn-secondary g-m-wide", listLoading ? "Loading\u2026" : "Show more", () => {
|
|
1578
|
+
query = { ...query, page: query.page + 1 };
|
|
1579
|
+
loadList(true);
|
|
1580
|
+
repaint();
|
|
1581
|
+
});
|
|
1582
|
+
more.disabled = listLoading;
|
|
1583
|
+
results.appendChild(more);
|
|
1584
|
+
}
|
|
1585
|
+
if (!why && canPortal()) results.appendChild(button(doc, "g-m-text-btn g-m-portal-link", "Open the feedback portal", openPortal));
|
|
1586
|
+
});
|
|
1587
|
+
if (!list || Date.now() - listAt > LIST_TTL_MS || query.page > 1) {
|
|
1588
|
+
query = { ...query, page: 1 };
|
|
1589
|
+
loadList();
|
|
1590
|
+
}
|
|
1591
|
+
return root;
|
|
1592
|
+
}
|
|
1593
|
+
function act(id, call, fallback, done) {
|
|
1594
|
+
if (busy.has(id)) return;
|
|
1595
|
+
const p = call();
|
|
1596
|
+
if (!p) return;
|
|
1597
|
+
busy.add(id);
|
|
1598
|
+
actionErrors.delete(id);
|
|
1599
|
+
repaint();
|
|
1600
|
+
p.then((res) => {
|
|
1601
|
+
patch(res);
|
|
1602
|
+
done?.();
|
|
1603
|
+
}).catch((e) => actionErrors.set(id, errorText(e, fallback))).finally(() => {
|
|
1604
|
+
busy.delete(id);
|
|
1605
|
+
repaint();
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
function detailScreen(id) {
|
|
1609
|
+
const root = el(doc, "div", "g-m-scroll");
|
|
1610
|
+
mount(root, () => {
|
|
1611
|
+
root.textContent = "";
|
|
1612
|
+
const idea = details.get(id);
|
|
1613
|
+
if (!idea) {
|
|
1614
|
+
const err = detailErrors.get(id);
|
|
1615
|
+
root.appendChild(err ? errorLine(doc, err) : el(doc, "div", "g-m-empty", "Loading\u2026"));
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
if (!voteDrafts.has(id)) voteDrafts.set(id, { priority: "nice_to_have", note: "" });
|
|
1619
|
+
root.appendChild(
|
|
1620
|
+
ideaDetail(doc, {
|
|
1621
|
+
idea,
|
|
1622
|
+
blocked: blocked(),
|
|
1623
|
+
onPortal: portal(),
|
|
1624
|
+
busy: busy.has(id),
|
|
1625
|
+
error: actionErrors.get(id) ?? null,
|
|
1626
|
+
vote: voteDrafts.get(id),
|
|
1627
|
+
commentDraft: commentDrafts.get(id) ?? "",
|
|
1628
|
+
onVote: (priority, note) => act(id, () => api.voteIdea?.(id, { priority, ...note ? { note } : {} }), "Your vote couldn't be saved.", () => voteDrafts.delete(id)),
|
|
1629
|
+
onUnvote: () => act(id, () => api.unvoteIdea?.(id), "Your vote couldn't be removed."),
|
|
1630
|
+
onFollow: (follow) => act(id, () => api.followIdea?.(id, follow), "That didn't work. Please try again."),
|
|
1631
|
+
onCommentInput: (text) => commentDrafts.set(id, text),
|
|
1632
|
+
onComment: () => act(id, () => api.commentIdea?.(id, (commentDrafts.get(id) ?? "").trim()), "Your comment couldn't be posted.", () => commentDrafts.delete(id))
|
|
1633
|
+
})
|
|
1634
|
+
);
|
|
1635
|
+
});
|
|
1636
|
+
if (api.getIdea) {
|
|
1637
|
+
detailErrors.delete(id);
|
|
1638
|
+
api.getIdea(id).then((d) => {
|
|
1639
|
+
details.set(id, asDetail(d));
|
|
1640
|
+
patch(d);
|
|
1641
|
+
}).catch((e) => {
|
|
1642
|
+
if (!details.has(id)) detailErrors.set(id, errorText(e, "This idea couldn't be loaded."));
|
|
1643
|
+
}).finally(() => {
|
|
1644
|
+
if (showing(root)) mounted.paint();
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
return root;
|
|
1648
|
+
}
|
|
1649
|
+
function lookUpSimilar(paint) {
|
|
1650
|
+
const title = draft.title.trim();
|
|
1651
|
+
if (title.length < 3 || !api.similarIdeas) {
|
|
1652
|
+
draft.similar = null;
|
|
1653
|
+
draft.similarFor = "";
|
|
1654
|
+
return paint();
|
|
1655
|
+
}
|
|
1656
|
+
if (title === draft.similarFor) return;
|
|
1657
|
+
const seq = ++similarSeq;
|
|
1658
|
+
api.similarIdeas(title).then((res) => {
|
|
1659
|
+
if (seq !== similarSeq) return;
|
|
1660
|
+
draft.similar = res;
|
|
1661
|
+
draft.similarFor = title;
|
|
1662
|
+
paint();
|
|
1663
|
+
}).catch(() => {
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
function formScreen() {
|
|
1667
|
+
const root = el(doc, "div", "g-m-scroll");
|
|
1668
|
+
let similarBox = null;
|
|
1669
|
+
const paintSimilar = () => {
|
|
1670
|
+
if (!similarBox) return;
|
|
1671
|
+
similarBox.textContent = "";
|
|
1672
|
+
const canVote = !blocked();
|
|
1673
|
+
const box = similarIdeas(doc, draft.similar ?? [], (idea) => ideaRow(doc, idea, { canVote, busy: busy.has(idea.id), onVote: () => toggleVote(idea), onOpen: () => o.openIdea(idea.id) }));
|
|
1674
|
+
if (box) similarBox.appendChild(box);
|
|
1675
|
+
if (listActionError) similarBox.appendChild(errorLine(doc, listActionError));
|
|
1676
|
+
};
|
|
1677
|
+
const paintAll = () => {
|
|
1678
|
+
root.textContent = "";
|
|
1679
|
+
const why = blocked();
|
|
1680
|
+
if (why) {
|
|
1681
|
+
root.appendChild(writeNotice(doc, why, portal()));
|
|
1682
|
+
similarBox = null;
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const { form, similar } = ideaForm(doc, {
|
|
1686
|
+
draft,
|
|
1687
|
+
productAreas: cfg().productAreas ?? [],
|
|
1688
|
+
onTitleInput: () => {
|
|
1689
|
+
if (similarTimer) clearTimeout(similarTimer);
|
|
1690
|
+
similarTimer = setTimeout(() => {
|
|
1691
|
+
if (showing(root)) lookUpSimilar(paintSimilar);
|
|
1692
|
+
}, 300);
|
|
1693
|
+
},
|
|
1694
|
+
onSubmit: () => submit(root, paintAll)
|
|
1695
|
+
});
|
|
1696
|
+
root.appendChild(form);
|
|
1697
|
+
similarBox = similar;
|
|
1698
|
+
paintSimilar();
|
|
1699
|
+
};
|
|
1700
|
+
mounted = { root, paint: paintSimilar };
|
|
1701
|
+
paintAll();
|
|
1702
|
+
if (draft.title.trim() && draft.title.trim() !== draft.similarFor) lookUpSimilar(paintSimilar);
|
|
1703
|
+
return root;
|
|
1704
|
+
}
|
|
1705
|
+
function submit(root, paintAll) {
|
|
1706
|
+
const d = draft;
|
|
1707
|
+
d.checked = true;
|
|
1708
|
+
d.error = ideaProblem(d);
|
|
1709
|
+
if (d.error || !api.submitIdea) return paintAll();
|
|
1710
|
+
d.sending = true;
|
|
1711
|
+
paintAll();
|
|
1712
|
+
api.submitIdea({ title: d.title.trim(), description: d.description.trim(), productArea: d.productArea || null, priority: d.priority, conversationId: d.conversationId }).then((idea) => {
|
|
1713
|
+
details.set(idea.id, asDetail(idea));
|
|
1714
|
+
listAt = 0;
|
|
1715
|
+
if (draft === d) draft = emptyIdeaDraft();
|
|
1716
|
+
if (showing(root)) o.openSent(idea);
|
|
1717
|
+
}).catch((e) => {
|
|
1718
|
+
d.sending = false;
|
|
1719
|
+
d.error = errorText(e, "Your idea couldn't be sent.");
|
|
1720
|
+
if (showing(root)) paintAll();
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
function updatesScreen() {
|
|
1724
|
+
const root = el(doc, "div", "g-m-scroll g-m-idea-updates-screen");
|
|
1725
|
+
mount(root, () => {
|
|
1726
|
+
root.textContent = "";
|
|
1727
|
+
if (!updates) {
|
|
1728
|
+
root.appendChild(el(doc, "div", "g-m-empty", updatesLoading || api.ideaUpdates ? "Loading\u2026" : "No updates yet."));
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
root.appendChild(updatesList(doc, updates.updates, (u) => u.requestId && o.openIdea(u.requestId)));
|
|
1732
|
+
});
|
|
1733
|
+
if (!updates) loadUpdates();
|
|
1734
|
+
else markRead();
|
|
1735
|
+
return root;
|
|
1736
|
+
}
|
|
1737
|
+
function markRead() {
|
|
1738
|
+
const ids = updates?.updates.filter((u) => !u.read).map((u) => u.id) ?? [];
|
|
1739
|
+
if (!ids.length || !api.markIdeaUpdatesRead) return;
|
|
1740
|
+
const seq = updatesSeq;
|
|
1741
|
+
api.markIdeaUpdatesRead(ids).then((res) => {
|
|
1742
|
+
if (seq !== updatesSeq || !updates) return;
|
|
1743
|
+
updates = { updates: updates.updates, unread: res.unread };
|
|
1744
|
+
o.onUnreadChange();
|
|
1745
|
+
}).catch(() => {
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
function loadUpdates(maxAgeMs = 0) {
|
|
1749
|
+
if (!api.ideaUpdates || updatesLoading || Date.now() - updatesAt < maxAgeMs) return;
|
|
1750
|
+
updatesLoading = true;
|
|
1751
|
+
const seq = ++updatesSeq;
|
|
1752
|
+
api.ideaUpdates().then((res) => {
|
|
1753
|
+
if (seq !== updatesSeq) return;
|
|
1754
|
+
const before = updates?.unread ?? 0;
|
|
1755
|
+
updates = { updates: Array.isArray(res?.updates) ? res.updates : [], unread: Number(res?.unread) || 0 };
|
|
1756
|
+
updatesAt = Date.now();
|
|
1757
|
+
if (updates.unread !== before) o.onUnreadChange();
|
|
1758
|
+
if (mounted?.root.isConnected) {
|
|
1759
|
+
mounted.paint();
|
|
1760
|
+
if (mounted.root.classList.contains("g-m-idea-updates-screen")) markRead();
|
|
1761
|
+
}
|
|
1762
|
+
}).catch(() => {
|
|
1763
|
+
}).finally(() => {
|
|
1764
|
+
if (seq === updatesSeq) updatesLoading = false;
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
function offerChip(offer, conversationId, lead) {
|
|
1768
|
+
const canWrite = !blocked();
|
|
1769
|
+
const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer g-m-idea-offer");
|
|
1770
|
+
b.type = "button";
|
|
1771
|
+
const label = el(doc, "span");
|
|
1772
|
+
b.append(lead(), label);
|
|
1773
|
+
const id = offer.requestId;
|
|
1774
|
+
if (!id) {
|
|
1775
|
+
if (!canWrite || !api.submitIdea) return null;
|
|
1776
|
+
label.textContent = "Suggest this idea";
|
|
1777
|
+
b.addEventListener("click", () => {
|
|
1778
|
+
startDraft({ title: offer.title, description: offer.description, conversationId });
|
|
1779
|
+
o.openForm();
|
|
1780
|
+
});
|
|
1781
|
+
return b;
|
|
1782
|
+
}
|
|
1783
|
+
const paint = () => {
|
|
1784
|
+
const state = offers.get(id) ?? (find(id)?.myVote ? "voted" : void 0);
|
|
1785
|
+
b.disabled = state === "voting";
|
|
1786
|
+
label.textContent = !canWrite ? `View \u201C${offer.title}\u201D` : state === "voting" ? "Voting\u2026" : state === "voted" ? "Voted \xB7 View the idea" : state === "failed" ? "Couldn't vote \xB7 View the idea" : `Vote for \u201C${offer.title}\u201D`;
|
|
1787
|
+
return state;
|
|
1788
|
+
};
|
|
1789
|
+
paint();
|
|
1790
|
+
b.addEventListener("click", () => {
|
|
1791
|
+
const state = paint();
|
|
1792
|
+
if (!canWrite || state === "voted" || state === "failed" || !api.voteIdea) return o.openIdea(id);
|
|
1793
|
+
if (state === "voting") return;
|
|
1794
|
+
offers.set(id, "voting");
|
|
1795
|
+
paint();
|
|
1796
|
+
api.voteIdea(id, { priority: "nice_to_have", conversationId }).then((idea) => {
|
|
1797
|
+
patch(idea);
|
|
1798
|
+
offers.set(id, "voted");
|
|
1799
|
+
}).catch(() => offers.set(id, "failed")).finally(paint);
|
|
1800
|
+
});
|
|
1801
|
+
return b;
|
|
1802
|
+
}
|
|
1803
|
+
function startDraft(prefill = {}) {
|
|
1804
|
+
draft = emptyIdeaDraft({ title: (prefill.title ?? "").slice(0, import_shared.MAX_FEEDBACK_TITLE), description: (prefill.description ?? "").slice(0, import_shared.MAX_FEEDBACK_DESCRIPTION), conversationId: prefill.conversationId ?? null });
|
|
1805
|
+
}
|
|
1806
|
+
return {
|
|
1807
|
+
blocked,
|
|
1808
|
+
unread: () => updates?.unread ?? 0,
|
|
1809
|
+
listScreen,
|
|
1810
|
+
detailScreen,
|
|
1811
|
+
formScreen,
|
|
1812
|
+
sentScreen: (idea) => {
|
|
1813
|
+
mounted = null;
|
|
1814
|
+
const root = el(doc, "div", "g-m-scroll");
|
|
1815
|
+
root.appendChild(ideaSent(doc, { idea, onView: () => o.openIdea(idea.id), onBack: o.openList }));
|
|
1816
|
+
return root;
|
|
1817
|
+
},
|
|
1818
|
+
updatesScreen,
|
|
1819
|
+
startDraft,
|
|
1820
|
+
offerChip,
|
|
1821
|
+
loadUpdates,
|
|
1822
|
+
sync(open) {
|
|
1823
|
+
const now = o.identified();
|
|
1824
|
+
if (now !== who) {
|
|
1825
|
+
who = now;
|
|
1826
|
+
list = null;
|
|
1827
|
+
listAt = 0;
|
|
1828
|
+
details.clear();
|
|
1829
|
+
offers.clear();
|
|
1830
|
+
updates = null;
|
|
1831
|
+
updatesAt = 0;
|
|
1832
|
+
updatesSeq++;
|
|
1833
|
+
updatesLoading = false;
|
|
1834
|
+
o.onUnreadChange();
|
|
1835
|
+
}
|
|
1836
|
+
if (open) loadUpdates(3e4);
|
|
1837
|
+
},
|
|
1838
|
+
destroy() {
|
|
1839
|
+
if (similarTimer) clearTimeout(similarTimer);
|
|
1840
|
+
if (searchTimer) clearTimeout(searchTimer);
|
|
1841
|
+
mounted = null;
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
|
|
907
1846
|
// src/messenger-styles.ts
|
|
908
1847
|
var MESSENGER_STYLES = `
|
|
909
1848
|
:host([data-g-messenger="bottom-right"]) .g-launcher.g-pos-bottom-right{right:72px}
|
|
@@ -1107,90 +2046,81 @@ var MESSENGER_STYLES = `
|
|
|
1107
2046
|
.g-m-step-dot{width:14px;height:14px;border-radius:999px;border:2px solid var(--g-border);background:var(--g-bg);box-sizing:border-box}
|
|
1108
2047
|
.g-m-step-done{color:var(--g-text);font-weight:600}
|
|
1109
2048
|
.g-m-step-done .g-m-step-dot{background:var(--g-primary);border-color:var(--g-primary)}
|
|
2049
|
+
.g-m-ideas-top{display:flex;flex-direction:column;gap:10px;margin-bottom:12px}
|
|
2050
|
+
.g-m-ideas-top:empty{display:none}
|
|
2051
|
+
.g-m-idea-tools{justify-content:space-between;gap:8px;flex-wrap:wrap}
|
|
2052
|
+
.g-m-filters{display:flex;gap:6px;flex-wrap:wrap}
|
|
2053
|
+
.g-m-filter{border:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);font:inherit;font-size:12px;font-weight:600;border-radius:999px;padding:5px 11px;cursor:pointer;min-height:30px}
|
|
2054
|
+
.g-m-filter:hover{background:var(--g-subtle)}
|
|
2055
|
+
.g-m-filter[aria-pressed="true"]{background:var(--g-primary);border-color:var(--g-primary);color:var(--g-primary-text)}
|
|
2056
|
+
.g-m-filter:focus-visible,.g-m-seg-btn:focus-visible,.g-m-vote:focus-visible,.g-m-idea-open:focus-visible,.g-m-follow:focus-visible{outline:2px solid var(--g-primary);outline-offset:2px}
|
|
2057
|
+
.g-m-sort{width:auto;min-width:0;min-height:30px;padding:4px 8px;font-size:12px}
|
|
2058
|
+
.g-m-idea-list{display:flex;flex-direction:column;border:1px solid var(--g-border);border-radius:calc(var(--g-radius) * .8);background:var(--g-bg);overflow:hidden;margin-bottom:12px}
|
|
2059
|
+
.g-m-idea{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-top:1px solid var(--g-border)}
|
|
2060
|
+
.g-m-idea:first-child{border-top:0}
|
|
2061
|
+
.g-m-vote{flex:none;width:44px;min-height:48px;border:1px solid var(--g-border);border-radius:10px;background:var(--g-bg);color:var(--g-text);font:inherit;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:0;padding:4px 0;cursor:pointer}
|
|
2062
|
+
button.g-m-vote:hover{border-color:var(--g-primary);color:var(--g-primary)}
|
|
2063
|
+
.g-m-vote:disabled{opacity:.6;cursor:progress}
|
|
2064
|
+
.g-m-vote.g-m-voted{background:color-mix(in srgb,var(--g-primary) 12%,var(--g-bg));border-color:var(--g-primary);color:var(--g-primary)}
|
|
2065
|
+
.g-m-vote-static{cursor:default;background:var(--g-subtle)}
|
|
2066
|
+
.g-m-vote-ico{width:16px;height:16px;stroke-width:2.4}
|
|
2067
|
+
.g-m-vote-count{font-size:13px;font-weight:700;line-height:1.2}
|
|
2068
|
+
.g-m-idea-open{flex:1;min-width:0;border:0;background:transparent;color:var(--g-text);font:inherit;text-align:left;padding:2px 0;cursor:pointer;display:flex;flex-direction:column;gap:6px;border-radius:4px}
|
|
2069
|
+
.g-m-idea-open:hover .g-m-idea-title{color:var(--g-primary)}
|
|
2070
|
+
.g-m-idea-title{font-weight:600;overflow-wrap:anywhere}
|
|
2071
|
+
.g-m-idea-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
2072
|
+
.g-m-idea-pill.g-m-idea-new{background:var(--g-subtle);color:var(--g-muted)}
|
|
2073
|
+
.g-m-idea-pill.g-m-idea-under_review{background:#e0e7ff;color:#3730a3}
|
|
2074
|
+
.g-m-idea-pill.g-m-idea-planned{background:#dbeafe;color:#1e40af}
|
|
2075
|
+
.g-m-idea-pill.g-m-idea-in_progress{background:#fef3c7;color:#92400e}
|
|
2076
|
+
.g-m-idea-pill.g-m-idea-released{background:#dcfce7;color:#166534}
|
|
2077
|
+
.g-m-idea-pill.g-m-idea-declined{background:#fee2e2;color:#991b1b}
|
|
2078
|
+
.g-m-idea-notice{display:flex;flex-direction:column;align-items:flex-start;gap:2px;border:1px solid var(--g-border);background:var(--g-subtle);border-radius:10px;padding:10px 12px;font-size:13px;color:var(--g-text)}
|
|
2079
|
+
.g-m-idea-notice .g-m-text-btn{padding:4px 0;min-height:0}
|
|
2080
|
+
.g-m-updates-link,.g-m-portal-link{align-self:flex-start;padding-left:0}
|
|
2081
|
+
.g-m-portal-link{display:block;margin:0 auto}
|
|
2082
|
+
.g-m-idea-detail{gap:12px}
|
|
2083
|
+
.g-m-idea-desc,.g-m-idea-note{font-size:14px;overflow-wrap:anywhere}
|
|
2084
|
+
.g-m-idea-desc p,.g-m-idea-note p,.g-m-comment-body p{margin:0 0 8px}
|
|
2085
|
+
.g-m-idea-desc p:last-child,.g-m-idea-note p:last-child,.g-m-comment-body p:last-child{margin:0}
|
|
2086
|
+
.g-m-idea-note{border-left:3px solid var(--g-primary);background:var(--g-subtle);border-radius:6px;padding:8px 10px}
|
|
2087
|
+
.g-m-idea-vote{display:flex;flex-direction:column;gap:12px;margin:0}
|
|
2088
|
+
.g-m-idea-vote .g-m-vote{min-height:44px}
|
|
2089
|
+
.g-m-follow{flex:none;border:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);font:inherit;font-size:13px;font-weight:600;border-radius:999px;padding:6px 12px;cursor:pointer;min-height:32px}
|
|
2090
|
+
.g-m-follow:hover{background:var(--g-subtle)}
|
|
2091
|
+
.g-m-follow.g-m-following{border-color:var(--g-primary);color:var(--g-primary)}
|
|
2092
|
+
.g-m-follow:disabled{opacity:.6}
|
|
2093
|
+
.g-m-seg{display:flex;border:1px solid var(--g-border);border-radius:10px;overflow:hidden}
|
|
2094
|
+
.g-m-seg-btn{flex:1;border:0;border-left:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);font:inherit;font-size:12px;font-weight:600;padding:8px 4px;cursor:pointer;min-height:36px}
|
|
2095
|
+
.g-m-seg-btn:first-child{border-left:0}
|
|
2096
|
+
.g-m-seg-btn:hover{background:var(--g-subtle)}
|
|
2097
|
+
.g-m-seg-btn[aria-checked="true"]{background:var(--g-primary);color:var(--g-primary-text)}
|
|
2098
|
+
.g-m-unvote{align-self:flex-start;padding-left:0;color:var(--g-muted)}
|
|
2099
|
+
.g-m-comments{display:flex;flex-direction:column;gap:8px}
|
|
2100
|
+
.g-m-comments-title{margin:4px 0 0;font-size:14px}
|
|
2101
|
+
.g-m-comment-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
|
|
2102
|
+
.g-m-comment{border:1px solid var(--g-border);border-radius:10px;padding:8px 10px;font-size:13px}
|
|
2103
|
+
.g-m-comment-team{background:var(--g-subtle);border-color:color-mix(in srgb,var(--g-primary) 35%,var(--g-border))}
|
|
2104
|
+
.g-m-comment-meta{gap:6px;margin-bottom:4px}
|
|
2105
|
+
.g-m-comment-author{font-weight:600}
|
|
2106
|
+
.g-m-comment-body{overflow-wrap:anywhere}
|
|
2107
|
+
.g-m-comment-form{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
|
|
2108
|
+
.g-m-comment-form textarea{resize:vertical;min-height:56px}
|
|
2109
|
+
.g-m-similar:empty{display:none}
|
|
2110
|
+
.g-m-similar-box{display:flex;flex-direction:column;gap:6px}
|
|
2111
|
+
.g-m-similar-box .g-m-idea-list{margin:0}
|
|
2112
|
+
.g-m-update{display:block}
|
|
2113
|
+
.g-m-update:disabled{cursor:default}
|
|
2114
|
+
.g-m-update:disabled:hover{background:var(--g-bg)}
|
|
1110
2115
|
`;
|
|
1111
2116
|
|
|
1112
|
-
// src/render-safe.ts
|
|
1113
|
-
var CITATION = /\s*\[\[article:[^\]]*\]\]/g;
|
|
1114
|
-
var INLINE = /\*\*([^*\n]+)\*\*|`([^`\n]+)`|(!?)\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()"']+[^\s<>().,;:!?'"])/g;
|
|
1115
|
-
function stripCitationMarkers(text) {
|
|
1116
|
-
return text.replace(CITATION, "");
|
|
1117
|
-
}
|
|
1118
|
-
function safeHttpUrl(raw) {
|
|
1119
|
-
try {
|
|
1120
|
-
const u = new URL(raw);
|
|
1121
|
-
return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
|
|
1122
|
-
} catch {
|
|
1123
|
-
return null;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
function link(doc, label, href) {
|
|
1127
|
-
const url = safeHttpUrl(href);
|
|
1128
|
-
if (!url) return doc.createTextNode(label);
|
|
1129
|
-
const a = doc.createElement("a");
|
|
1130
|
-
a.href = url;
|
|
1131
|
-
a.target = "_blank";
|
|
1132
|
-
a.rel = "noopener noreferrer nofollow";
|
|
1133
|
-
a.textContent = label;
|
|
1134
|
-
return a;
|
|
1135
|
-
}
|
|
1136
|
-
function appendInline(doc, parent, text) {
|
|
1137
|
-
let last = 0;
|
|
1138
|
-
for (const m of text.matchAll(INLINE)) {
|
|
1139
|
-
const start = m.index ?? 0;
|
|
1140
|
-
if (start > last) parent.appendChild(doc.createTextNode(text.slice(last, start)));
|
|
1141
|
-
if (m[1] !== void 0) {
|
|
1142
|
-
const b = doc.createElement("strong");
|
|
1143
|
-
b.textContent = m[1];
|
|
1144
|
-
parent.appendChild(b);
|
|
1145
|
-
} else if (m[2] !== void 0) {
|
|
1146
|
-
const c = doc.createElement("code");
|
|
1147
|
-
c.textContent = m[2];
|
|
1148
|
-
parent.appendChild(c);
|
|
1149
|
-
} else if (m[4] !== void 0 && m[5] !== void 0) {
|
|
1150
|
-
parent.appendChild(m[3] ? doc.createTextNode(m[4]) : link(doc, m[4], m[5]));
|
|
1151
|
-
} else if (m[6] !== void 0) {
|
|
1152
|
-
parent.appendChild(link(doc, m[6], m[6]));
|
|
1153
|
-
}
|
|
1154
|
-
last = start + m[0].length;
|
|
1155
|
-
}
|
|
1156
|
-
if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
|
|
1157
|
-
}
|
|
1158
|
-
var BULLET = /^\s*[-*•]\s+/;
|
|
1159
|
-
var NUMBERED = /^\s*\d{1,3}[.)]\s+/;
|
|
1160
|
-
function renderSafeText(doc, text) {
|
|
1161
|
-
const frag = doc.createDocumentFragment();
|
|
1162
|
-
const blocks = stripCitationMarkers(text).replace(/\r\n?/g, "\n").trim().split(/\n{2,}/);
|
|
1163
|
-
for (const block of blocks) {
|
|
1164
|
-
if (!block.trim()) continue;
|
|
1165
|
-
const lines = block.split("\n");
|
|
1166
|
-
if (lines.every((l) => BULLET.test(l)) || lines.every((l) => NUMBERED.test(l))) {
|
|
1167
|
-
const ordered = NUMBERED.test(lines[0]);
|
|
1168
|
-
const list = doc.createElement(ordered ? "ol" : "ul");
|
|
1169
|
-
for (const line of lines) {
|
|
1170
|
-
const li = doc.createElement("li");
|
|
1171
|
-
appendInline(doc, li, line.replace(ordered ? NUMBERED : BULLET, ""));
|
|
1172
|
-
list.appendChild(li);
|
|
1173
|
-
}
|
|
1174
|
-
frag.appendChild(list);
|
|
1175
|
-
continue;
|
|
1176
|
-
}
|
|
1177
|
-
const p = doc.createElement("p");
|
|
1178
|
-
lines.forEach((line, i) => {
|
|
1179
|
-
if (i) p.appendChild(doc.createElement("br"));
|
|
1180
|
-
appendInline(doc, p, line);
|
|
1181
|
-
});
|
|
1182
|
-
frag.appendChild(p);
|
|
1183
|
-
}
|
|
1184
|
-
return frag;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
2117
|
// src/messenger.ts
|
|
1188
|
-
var
|
|
2118
|
+
var SVG_NS2 = "http://www.w3.org/2000/svg";
|
|
1189
2119
|
function chatIcon(doc) {
|
|
1190
|
-
const svg = doc.createElementNS(
|
|
2120
|
+
const svg = doc.createElementNS(SVG_NS2, "svg");
|
|
1191
2121
|
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1192
2122
|
svg.setAttribute("aria-hidden", "true");
|
|
1193
|
-
const path = doc.createElementNS(
|
|
2123
|
+
const path = doc.createElementNS(SVG_NS2, "path");
|
|
1194
2124
|
path.setAttribute("d", "M12 3C6.5 3 2 6.8 2 11.5c0 2.4 1.2 4.6 3.1 6.1L4.5 21l3.9-2c1.1.3 2.3.5 3.6.5 5.5 0 10-3.8 10-8.5S17.5 3 12 3z");
|
|
1195
2125
|
svg.appendChild(path);
|
|
1196
2126
|
return svg;
|
|
@@ -1215,15 +2145,16 @@ var ICONS = {
|
|
|
1215
2145
|
circle: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"],
|
|
1216
2146
|
expand: ["M15 3h6v6", "M21 3l-7 7", "M9 21H3v-6", "M3 21l7-7"],
|
|
1217
2147
|
flag: ["M5 21V4", "M5 4h12l-2.5 4L17 12H5"],
|
|
1218
|
-
collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"]
|
|
2148
|
+
collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"],
|
|
2149
|
+
idea: IDEA_ICON
|
|
1219
2150
|
};
|
|
1220
2151
|
function icon(doc, name, cls = "g-m-ico") {
|
|
1221
|
-
const svg = doc.createElementNS(
|
|
2152
|
+
const svg = doc.createElementNS(SVG_NS2, "svg");
|
|
1222
2153
|
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1223
2154
|
svg.setAttribute("aria-hidden", "true");
|
|
1224
2155
|
svg.setAttribute("class", cls);
|
|
1225
2156
|
for (const d of ICONS[name] ?? []) {
|
|
1226
|
-
const path = doc.createElementNS(
|
|
2157
|
+
const path = doc.createElementNS(SVG_NS2, "path");
|
|
1227
2158
|
path.setAttribute("d", d);
|
|
1228
2159
|
svg.appendChild(path);
|
|
1229
2160
|
}
|
|
@@ -1250,7 +2181,7 @@ function shortDateTime(iso) {
|
|
|
1250
2181
|
return d.toISOString().slice(0, 16).replace("T", " ");
|
|
1251
2182
|
}
|
|
1252
2183
|
}
|
|
1253
|
-
function
|
|
2184
|
+
function shortDate2(iso) {
|
|
1254
2185
|
const d = new Date(iso);
|
|
1255
2186
|
if (Number.isNaN(d.getTime())) return "";
|
|
1256
2187
|
try {
|
|
@@ -1345,17 +2276,24 @@ function renderMessenger(options) {
|
|
|
1345
2276
|
panel.append(head, main, tabs);
|
|
1346
2277
|
root.append(launcher, panel);
|
|
1347
2278
|
scope.appendChild(root);
|
|
2279
|
+
function withRcText(items, rc, apply) {
|
|
2280
|
+
if (!rc?.length) return items;
|
|
2281
|
+
const byId = new Map(rc.map((x) => [x.id, x]));
|
|
2282
|
+
return items.map((i) => byId.has(i.id) ? apply(i, byId.get(i.id)) : i);
|
|
2283
|
+
}
|
|
2284
|
+
const helpModules = () => options.resourceCenter?.modules?.length ? (0, import_runtime6.orderedModules)(options.resourceCenter.modules).filter((m) => m.id !== "announcements" && m.enabled && (!("targeting" in m) || !m.targeting || !options.evaluate || options.evaluate(m.targeting))).map((m) => m.id) : void 0;
|
|
1348
2285
|
const rcExtras = (ids = []) => ids.map((id) => options.guides?.find((g) => g.id === id || g.key === id)).filter((g) => !!g);
|
|
1349
2286
|
const help = createHelpView({
|
|
1350
2287
|
doc,
|
|
1351
2288
|
data: () => ({
|
|
1352
|
-
collections: articles?.collections ?? options.resourceCenter?.collections ?? [],
|
|
1353
|
-
articles: articles?.articles ?? options.resourceCenter?.articles ?? [],
|
|
2289
|
+
collections: withRcText(articles?.collections ?? options.resourceCenter?.collections ?? [], options.resourceCenter?.collections, (c, t) => ({ ...c, name: t.name, description: t.description })),
|
|
2290
|
+
articles: withRcText(articles?.articles ?? options.resourceCenter?.articles ?? [], options.resourceCenter?.articles, (a, t) => ({ ...a, title: t.title, summary: t.summary })),
|
|
1354
2291
|
suggested: helpSuggestions(),
|
|
1355
2292
|
recommended: rcExtras(options.resourceCenter?.recommendedGuideIds),
|
|
1356
2293
|
checklists: rcExtras(options.resourceCenter?.checklistGuideIds),
|
|
1357
2294
|
faqs: options.resourceCenter?.faqs,
|
|
1358
|
-
links: options.resourceCenter?.links
|
|
2295
|
+
links: options.resourceCenter?.links,
|
|
2296
|
+
modules: helpModules()
|
|
1359
2297
|
}),
|
|
1360
2298
|
search: (q) => api.searchArticles(q),
|
|
1361
2299
|
audienceIds: options.audienceIds,
|
|
@@ -1374,6 +2312,24 @@ function renderMessenger(options) {
|
|
|
1374
2312
|
onShowMe: (key) => startWalkthrough(key),
|
|
1375
2313
|
onArticleChange: () => syncSize()
|
|
1376
2314
|
});
|
|
2315
|
+
const ideasOn = () => !!config.ideas && !!api.listIdeas;
|
|
2316
|
+
const ideasTab = { kind: "tab", tab: "ideas" };
|
|
2317
|
+
const ideas = createIdeas({
|
|
2318
|
+
doc,
|
|
2319
|
+
api,
|
|
2320
|
+
config: () => config.ideas,
|
|
2321
|
+
identified: () => visitor().identified !== false,
|
|
2322
|
+
openIdea: (id) => go({ kind: "idea", id, from: view.kind === "idea" ? view.from : view }),
|
|
2323
|
+
openForm: () => go({ kind: "idea-new", from: view }),
|
|
2324
|
+
openSent: (idea) => go({ kind: "idea-sent", idea }),
|
|
2325
|
+
openList: () => go(ideasTab),
|
|
2326
|
+
openUpdates: () => go({ kind: "idea-updates", from: view }),
|
|
2327
|
+
onUnreadChange: () => {
|
|
2328
|
+
if (!open || destroyed) return;
|
|
2329
|
+
if (view.kind === "tab" && view.tab === "home") render();
|
|
2330
|
+
else renderTabs();
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
1377
2333
|
let docCollapsed = false;
|
|
1378
2334
|
let sizeButton = null;
|
|
1379
2335
|
const readingDoc = () => open && (view.kind === "howto" || view.kind === "tab" && view.tab === "help" && help.isArticleOpen());
|
|
@@ -1454,7 +2410,7 @@ function renderMessenger(options) {
|
|
|
1454
2410
|
options.onStartGuide?.(guideKey);
|
|
1455
2411
|
};
|
|
1456
2412
|
function howToItems(q) {
|
|
1457
|
-
const list = q ? (howTos?.howTos ?? []).filter((h) => (0,
|
|
2413
|
+
const list = q ? (howTos?.howTos ?? []).filter((h) => (0, import_runtime6.matchesHelpQuery)(`${h.title} ${h.description}`, q)).slice(0, 10) : [];
|
|
1458
2414
|
if (!list.length) return [];
|
|
1459
2415
|
return [
|
|
1460
2416
|
el(doc, "div", "g-rc-section", `How-tos (${list.length})`),
|
|
@@ -1468,7 +2424,7 @@ function renderMessenger(options) {
|
|
|
1468
2424
|
const refreshConversations = () => api.listConversations().then((list) => {
|
|
1469
2425
|
conversations = [...list].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
|
|
1470
2426
|
updateBadge();
|
|
1471
|
-
if (open && view.kind === "tab" && view.tab !== "help") render();
|
|
2427
|
+
if (open && view.kind === "tab" && view.tab !== "help" && view.tab !== "ideas") render();
|
|
1472
2428
|
}).catch(() => {
|
|
1473
2429
|
});
|
|
1474
2430
|
function renderHead(title, back) {
|
|
@@ -1502,6 +2458,7 @@ function renderMessenger(options) {
|
|
|
1502
2458
|
["messages", "messages", "Messages"],
|
|
1503
2459
|
["help", "help", "Help"]
|
|
1504
2460
|
];
|
|
2461
|
+
if (ideasOn()) items.push(["ideas", "idea", config.ideas.title || "Ideas"]);
|
|
1505
2462
|
const latest = news();
|
|
1506
2463
|
if (latest.length) items.push(["news", "news", "News"]);
|
|
1507
2464
|
for (const [tab, ico, label] of items) {
|
|
@@ -1512,7 +2469,7 @@ function renderMessenger(options) {
|
|
|
1512
2469
|
const i = el(doc, "span", "g-m-tab-icon");
|
|
1513
2470
|
i.appendChild(icon(doc, ico));
|
|
1514
2471
|
b.append(i, el(doc, "span", "", label));
|
|
1515
|
-
if (tab === "news" && latest.some((n) => n.unread)) {
|
|
2472
|
+
if (tab === "news" && latest.some((n) => n.unread) || tab === "ideas" && ideas.unread() > 0) {
|
|
1516
2473
|
const dot2 = el(doc, "span", "g-m-tab-dot");
|
|
1517
2474
|
dot2.setAttribute("aria-label", "New");
|
|
1518
2475
|
i.appendChild(dot2);
|
|
@@ -1718,11 +2675,16 @@ function renderMessenger(options) {
|
|
|
1718
2675
|
const body = el(doc, "span", "g-m-news-body");
|
|
1719
2676
|
const meta = el(doc, "span", "g-m-row g-m-center g-m-news-meta");
|
|
1720
2677
|
if (n.unread) meta.appendChild(el(doc, "span", "g-m-tag", "New"));
|
|
1721
|
-
meta.appendChild(el(doc, "span", "g-m-time",
|
|
2678
|
+
meta.appendChild(el(doc, "span", "g-m-time", shortDate2(n.publishedAt)));
|
|
1722
2679
|
body.append(meta, el(doc, "span", "g-m-card-title", n.title));
|
|
1723
2680
|
if (n.body) body.appendChild(el(doc, "span", "g-m-card-sub g-m-clamp", n.body));
|
|
1724
2681
|
card.appendChild(body);
|
|
1725
|
-
card.addEventListener("click", () =>
|
|
2682
|
+
card.addEventListener("click", () => {
|
|
2683
|
+
if (n.key) startGuide(n.key);
|
|
2684
|
+
else if (n.articleId) openArticle(n.articleId);
|
|
2685
|
+
else if (n.url && /^https?:\/\//i.test(n.url)) window.open(n.url, "_blank", "noopener,noreferrer");
|
|
2686
|
+
else startGuide(n.id);
|
|
2687
|
+
});
|
|
1726
2688
|
return card;
|
|
1727
2689
|
}
|
|
1728
2690
|
function renderHome() {
|
|
@@ -1748,11 +2710,13 @@ function renderMessenger(options) {
|
|
|
1748
2710
|
if (filed) inner.appendChild(conversationCard(filed, "Your complaint"));
|
|
1749
2711
|
const recent = conversations.find((c) => c.id !== filed?.id);
|
|
1750
2712
|
if (recent) inner.appendChild(conversationCard(recent, recent.status === "closed" && !recent.unread ? "Recent conversation" : "Continue the conversation"));
|
|
2713
|
+
const ideaUpdates = ideasOn() ? ideas.unread() : 0;
|
|
2714
|
+
if (ideaUpdates) inner.appendChild(actionCard({ title: "Updates on your ideas", sub: ideaUpdates === 1 ? "1 new update" : `${ideaUpdates} new updates`, lead: iconTile("idea", true), trail: true, onClick: () => go({ kind: "idea-updates", from: view }) }));
|
|
1751
2715
|
if (config.status) inner.appendChild(statusCard(config.status));
|
|
1752
2716
|
if (cards.progress) progressCards(v).forEach((c) => inner.appendChild(c));
|
|
1753
2717
|
const suggestions = pageSuggestions(v);
|
|
1754
2718
|
const questions = cards.suggestions ? (page?.questions ?? []).slice(0, 2) : [];
|
|
1755
|
-
const label = v.path ? (0,
|
|
2719
|
+
const label = v.path ? (0, import_runtime6.pageLabel)(v.path) : null;
|
|
1756
2720
|
const fallback = !suggestions.length && !questions.length ? rcExtras(options.resourceCenter?.recommendedGuideIds).slice(0, 3) : [];
|
|
1757
2721
|
if (suggestions.length || questions.length || fallback.length) {
|
|
1758
2722
|
const box = el(doc, "div", "g-m-card g-m-static g-m-suggest");
|
|
@@ -1767,6 +2731,10 @@ function renderMessenger(options) {
|
|
|
1767
2731
|
}
|
|
1768
2732
|
const latest = news()[0];
|
|
1769
2733
|
if (latest) inner.appendChild(newsCard(latest));
|
|
2734
|
+
if (ideasOn()) {
|
|
2735
|
+
const canWrite = !ideas.blocked();
|
|
2736
|
+
inner.appendChild(actionCard({ title: canWrite ? "Share an idea" : "Browse ideas", sub: canWrite ? "Suggest a feature or vote for what's next." : "See what people are asking for and what's planned.", lead: iconTile("idea"), onClick: () => go(ideasTab) }));
|
|
2737
|
+
}
|
|
1770
2738
|
inner.appendChild(actionCard({ title: "Search for help", sub: "Articles, guides and how-tos", lead: icon(doc, "search", "g-m-ico g-m-lead"), onClick: () => go({ kind: "tab", tab: "help" }) }));
|
|
1771
2739
|
scroll.append(hero, inner);
|
|
1772
2740
|
main.appendChild(scroll);
|
|
@@ -1867,6 +2835,25 @@ function renderMessenger(options) {
|
|
|
1867
2835
|
scroll.appendChild(complaintSent(doc, { complaint: v.complaint, answers: v.answers, onOpenConversation: () => go({ kind: "thread", conversationId: v.conversationId }), onHome: () => go({ kind: "tab", tab: "home" }) }));
|
|
1868
2836
|
main.appendChild(scroll);
|
|
1869
2837
|
}
|
|
2838
|
+
function renderIdeasView(v) {
|
|
2839
|
+
const title = config.ideas?.title || "Ideas";
|
|
2840
|
+
if (v.kind === "idea") {
|
|
2841
|
+
renderHead(title, () => go(v.from));
|
|
2842
|
+
main.appendChild(ideas.detailScreen(v.id));
|
|
2843
|
+
} else if (v.kind === "idea-new") {
|
|
2844
|
+
renderHead("Suggest an idea", () => go(v.from));
|
|
2845
|
+
main.appendChild(ideas.formScreen());
|
|
2846
|
+
} else if (v.kind === "idea-sent") {
|
|
2847
|
+
renderHead(title, () => go(ideasTab));
|
|
2848
|
+
main.appendChild(ideas.sentScreen(v.idea));
|
|
2849
|
+
} else if (v.kind === "idea-updates") {
|
|
2850
|
+
renderHead("Updates on your ideas", () => go(v.from));
|
|
2851
|
+
main.appendChild(ideas.updatesScreen());
|
|
2852
|
+
} else {
|
|
2853
|
+
renderHead(title);
|
|
2854
|
+
main.appendChild(ideas.listScreen());
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
1870
2857
|
function renderHowToView(id, from) {
|
|
1871
2858
|
const cached = howToCache.get(id);
|
|
1872
2859
|
renderHead(cached?.title ?? "How-to", () => go(from));
|
|
@@ -1902,7 +2889,7 @@ function renderMessenger(options) {
|
|
|
1902
2889
|
const v = visitor();
|
|
1903
2890
|
const items = pageSuggestions(v, 5).map((s) => ({ kind: s.icon === "play" ? "guide" : s.icon === "steps" ? "howto" : "article", title: s.title, summary: s.sub, run: s.run }));
|
|
1904
2891
|
if (!items.length) return void 0;
|
|
1905
|
-
const label = v.path ? (0,
|
|
2892
|
+
const label = v.path ? (0, import_runtime6.pageLabel)(v.path) : null;
|
|
1906
2893
|
return { title: label ? `For the ${label} page` : "Suggested for you", items };
|
|
1907
2894
|
}
|
|
1908
2895
|
function renderHelpTab() {
|
|
@@ -1929,7 +2916,7 @@ function renderMessenger(options) {
|
|
|
1929
2916
|
pageKey = null;
|
|
1930
2917
|
return;
|
|
1931
2918
|
}
|
|
1932
|
-
const key = (0,
|
|
2919
|
+
const key = (0, import_runtime6.foldPathIds)(path).toLowerCase();
|
|
1933
2920
|
if (!force && key === pageKey && (pageLoading || Date.now() - pageAt < PAGE_TTL_MS)) return;
|
|
1934
2921
|
if (key !== pageKey) page = null;
|
|
1935
2922
|
pageKey = key;
|
|
@@ -1980,7 +2967,8 @@ function renderMessenger(options) {
|
|
|
1980
2967
|
bubble.dataset.messageId = m.id;
|
|
1981
2968
|
wrap.appendChild(bubble);
|
|
1982
2969
|
const offer = m.complaintOffer && canComplain() ? complaintCategories().find((c) => c.id === m.complaintOffer.categoryId) : void 0;
|
|
1983
|
-
|
|
2970
|
+
const ideaChip = m.ideaOffer && ideasOn() ? ideas.offerChip(m.ideaOffer, current?.id ?? null, () => icon(doc, "idea", "g-m-ico g-m-chip-ico")) : null;
|
|
2971
|
+
if (m.citations.length || m.suggestedGuides.length || m.howTos?.length || offer || ideaChip) {
|
|
1984
2972
|
const chips = el(doc, "div", "g-m-chips");
|
|
1985
2973
|
if (offer) {
|
|
1986
2974
|
const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer");
|
|
@@ -1993,6 +2981,7 @@ function renderMessenger(options) {
|
|
|
1993
2981
|
});
|
|
1994
2982
|
chips.appendChild(b);
|
|
1995
2983
|
}
|
|
2984
|
+
if (ideaChip) chips.appendChild(ideaChip);
|
|
1996
2985
|
for (const g of m.suggestedGuides) {
|
|
1997
2986
|
const b = el(doc, "button", "g-m-chip g-m-guide", `\u25B6 Show me: ${g.name}`);
|
|
1998
2987
|
b.type = "button";
|
|
@@ -2025,7 +3014,7 @@ function renderMessenger(options) {
|
|
|
2025
3014
|
if (current?.complaint) log.appendChild(complaintBanner(current.complaint));
|
|
2026
3015
|
if (!messages.length) {
|
|
2027
3016
|
const v = visitor();
|
|
2028
|
-
const label = v.path ? (0,
|
|
3017
|
+
const label = v.path ? (0, import_runtime6.pageLabel)(v.path) : null;
|
|
2029
3018
|
if (label && config.aiAvailable && !current) log.appendChild(el(doc, "div", "g-m-context", `Asking from the ${label} page`));
|
|
2030
3019
|
const intro = { id: "intro", author: config.aiAvailable ? "scout" : "agent", authorName: "Support team", body: personalGreeting(config.greeting, v.firstName), citations: [], suggestedGuides: [], status: "complete", createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2031
3020
|
log.appendChild(messageNode(intro));
|
|
@@ -2145,7 +3134,13 @@ function renderMessenger(options) {
|
|
|
2145
3134
|
else if (view.kind === "complaint-category") renderComplaintCategories(view);
|
|
2146
3135
|
else if (view.kind === "complaint-form") renderComplaintForm(view);
|
|
2147
3136
|
else if (view.kind === "complaint-sent") renderComplaintSent(view);
|
|
2148
|
-
else if (view.tab === "
|
|
3137
|
+
else if (view.kind !== "tab" || view.tab === "ideas") {
|
|
3138
|
+
if (ideasOn()) renderIdeasView(view);
|
|
3139
|
+
else {
|
|
3140
|
+
view = { kind: "tab", tab: "home" };
|
|
3141
|
+
renderHome();
|
|
3142
|
+
}
|
|
3143
|
+
} else if (view.tab === "home") renderHome();
|
|
2149
3144
|
else if (view.tab === "messages") renderMessagesTab();
|
|
2150
3145
|
else if (view.tab === "news" && news().length) renderNewsTab();
|
|
2151
3146
|
else if (view.tab === "news") {
|
|
@@ -2296,6 +3291,7 @@ function renderMessenger(options) {
|
|
|
2296
3291
|
render();
|
|
2297
3292
|
void refreshConversations();
|
|
2298
3293
|
loadHowTos();
|
|
3294
|
+
if (ideasOn()) ideas.loadUpdates();
|
|
2299
3295
|
if (view.kind === "thread") scheduleThreadPoll();
|
|
2300
3296
|
options.onOpen?.();
|
|
2301
3297
|
} else {
|
|
@@ -2344,15 +3340,22 @@ function renderMessenger(options) {
|
|
|
2344
3340
|
setOpen(true);
|
|
2345
3341
|
startComplaint(categoryId ?? null);
|
|
2346
3342
|
},
|
|
3343
|
+
openIdeas(requestId) {
|
|
3344
|
+
if (!ideasOn()) return;
|
|
3345
|
+
setOpen(true);
|
|
3346
|
+
go(requestId ? { kind: "idea", id: requestId, from: ideasTab } : ideasTab);
|
|
3347
|
+
},
|
|
2347
3348
|
refresh() {
|
|
2348
3349
|
if (destroyed) return;
|
|
3350
|
+
if (ideasOn()) ideas.sync(open);
|
|
2349
3351
|
if (!open) {
|
|
2350
3352
|
pageKey = null;
|
|
2351
3353
|
return;
|
|
2352
3354
|
}
|
|
2353
3355
|
loadPage();
|
|
2354
|
-
if (view.kind === "tab") render();
|
|
2355
|
-
else if (
|
|
3356
|
+
if (view.kind === "tab" && view.tab !== "ideas") render();
|
|
3357
|
+
else if (view.kind === "tab") renderTabs();
|
|
3358
|
+
else if (view.kind === "thread" && !messages.length) renderLog();
|
|
2356
3359
|
},
|
|
2357
3360
|
destroy() {
|
|
2358
3361
|
destroyed = true;
|
|
@@ -2360,6 +3363,7 @@ function renderMessenger(options) {
|
|
|
2360
3363
|
if (backgroundTimer) clearInterval(backgroundTimer);
|
|
2361
3364
|
panel.removeEventListener("keydown", onKey);
|
|
2362
3365
|
help.destroy();
|
|
3366
|
+
ideas.destroy();
|
|
2363
3367
|
shadowHost?.removeAttribute("data-g-messenger");
|
|
2364
3368
|
root.remove();
|
|
2365
3369
|
}
|