@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.js
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
el,
|
|
4
4
|
resultRow,
|
|
5
5
|
startableGuide
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-H3CROHPR.js";
|
|
7
|
+
import "./chunk-VJSQM23R.js";
|
|
8
8
|
|
|
9
9
|
// src/messenger.ts
|
|
10
|
-
import { foldPathIds, matchesHelpQuery, pageLabel } from "@guidelytech/shared/runtime";
|
|
10
|
+
import { foldPathIds, matchesHelpQuery, orderedModules, pageLabel } from "@guidelytech/shared/runtime";
|
|
11
11
|
|
|
12
12
|
// src/howto-view.ts
|
|
13
13
|
var pct = (v) => `${(v * 100).toFixed(3)}%`;
|
|
@@ -379,6 +379,874 @@ async function readScreenshot(doc, file) {
|
|
|
379
379
|
return { fileName: scaled ? `${name.replace(/\.[a-z0-9]+$/i, "")}.jpg` : name, data, previewUrl: dataUrl };
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
// src/ideas-view.ts
|
|
383
|
+
import { FEEDBACK_PRIORITIES, FEEDBACK_PRIORITY_LABEL, FEEDBACK_STATUS_LABEL, MAX_FEEDBACK_COMMENT, MAX_FEEDBACK_DESCRIPTION, MAX_FEEDBACK_NOTE, MAX_FEEDBACK_TITLE, feedbackReference } from "@guidelytech/shared";
|
|
384
|
+
|
|
385
|
+
// src/render-safe.ts
|
|
386
|
+
var CITATION = /\s*\[\[article:[^\]]*\]\]/g;
|
|
387
|
+
var INLINE = /\*\*([^*\n]+)\*\*|`([^`\n]+)`|(!?)\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()"']+[^\s<>().,;:!?'"])/g;
|
|
388
|
+
function stripCitationMarkers(text) {
|
|
389
|
+
return text.replace(CITATION, "");
|
|
390
|
+
}
|
|
391
|
+
function safeHttpUrl(raw) {
|
|
392
|
+
try {
|
|
393
|
+
const u = new URL(raw);
|
|
394
|
+
return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function link(doc, label, href) {
|
|
400
|
+
const url = safeHttpUrl(href);
|
|
401
|
+
if (!url) return doc.createTextNode(label);
|
|
402
|
+
const a = doc.createElement("a");
|
|
403
|
+
a.href = url;
|
|
404
|
+
a.target = "_blank";
|
|
405
|
+
a.rel = "noopener noreferrer nofollow";
|
|
406
|
+
a.textContent = label;
|
|
407
|
+
return a;
|
|
408
|
+
}
|
|
409
|
+
function appendInline(doc, parent, text) {
|
|
410
|
+
let last = 0;
|
|
411
|
+
for (const m of text.matchAll(INLINE)) {
|
|
412
|
+
const start = m.index ?? 0;
|
|
413
|
+
if (start > last) parent.appendChild(doc.createTextNode(text.slice(last, start)));
|
|
414
|
+
if (m[1] !== void 0) {
|
|
415
|
+
const b = doc.createElement("strong");
|
|
416
|
+
b.textContent = m[1];
|
|
417
|
+
parent.appendChild(b);
|
|
418
|
+
} else if (m[2] !== void 0) {
|
|
419
|
+
const c = doc.createElement("code");
|
|
420
|
+
c.textContent = m[2];
|
|
421
|
+
parent.appendChild(c);
|
|
422
|
+
} else if (m[4] !== void 0 && m[5] !== void 0) {
|
|
423
|
+
parent.appendChild(m[3] ? doc.createTextNode(m[4]) : link(doc, m[4], m[5]));
|
|
424
|
+
} else if (m[6] !== void 0) {
|
|
425
|
+
parent.appendChild(link(doc, m[6], m[6]));
|
|
426
|
+
}
|
|
427
|
+
last = start + m[0].length;
|
|
428
|
+
}
|
|
429
|
+
if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
|
|
430
|
+
}
|
|
431
|
+
var BULLET = /^\s*[-*•]\s+/;
|
|
432
|
+
var NUMBERED = /^\s*\d{1,3}[.)]\s+/;
|
|
433
|
+
function renderSafeText(doc, text) {
|
|
434
|
+
const frag = doc.createDocumentFragment();
|
|
435
|
+
const blocks = stripCitationMarkers(text).replace(/\r\n?/g, "\n").trim().split(/\n{2,}/);
|
|
436
|
+
for (const block of blocks) {
|
|
437
|
+
if (!block.trim()) continue;
|
|
438
|
+
const lines = block.split("\n");
|
|
439
|
+
if (lines.every((l) => BULLET.test(l)) || lines.every((l) => NUMBERED.test(l))) {
|
|
440
|
+
const ordered = NUMBERED.test(lines[0]);
|
|
441
|
+
const list = doc.createElement(ordered ? "ol" : "ul");
|
|
442
|
+
for (const line of lines) {
|
|
443
|
+
const li = doc.createElement("li");
|
|
444
|
+
appendInline(doc, li, line.replace(ordered ? NUMBERED : BULLET, ""));
|
|
445
|
+
list.appendChild(li);
|
|
446
|
+
}
|
|
447
|
+
frag.appendChild(list);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const p = doc.createElement("p");
|
|
451
|
+
lines.forEach((line, i) => {
|
|
452
|
+
if (i) p.appendChild(doc.createElement("br"));
|
|
453
|
+
appendInline(doc, p, line);
|
|
454
|
+
});
|
|
455
|
+
frag.appendChild(p);
|
|
456
|
+
}
|
|
457
|
+
return frag;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/ideas-view.ts
|
|
461
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
462
|
+
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"];
|
|
463
|
+
var UP_ICON = ["m6 15 6-6 6 6"];
|
|
464
|
+
function svgIcon(doc, paths, cls = "g-m-ico") {
|
|
465
|
+
const svg = doc.createElementNS(SVG_NS, "svg");
|
|
466
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
467
|
+
svg.setAttribute("aria-hidden", "true");
|
|
468
|
+
svg.setAttribute("class", cls);
|
|
469
|
+
for (const d of paths) {
|
|
470
|
+
const p = doc.createElementNS(SVG_NS, "path");
|
|
471
|
+
p.setAttribute("d", d);
|
|
472
|
+
svg.appendChild(p);
|
|
473
|
+
}
|
|
474
|
+
return svg;
|
|
475
|
+
}
|
|
476
|
+
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
477
|
+
function shortDate(iso) {
|
|
478
|
+
const d = new Date(iso);
|
|
479
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
480
|
+
try {
|
|
481
|
+
return d.toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
482
|
+
} catch {
|
|
483
|
+
return d.toISOString().slice(0, 10);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
var errorText = (e, fallback) => (e instanceof Error && e.message ? e.message : fallback).replace(/^[\w.]+: /, "");
|
|
487
|
+
function ideaStatusPill(doc, status) {
|
|
488
|
+
return el(doc, "span", `g-m-pill g-m-idea-pill g-m-idea-${status}`, FEEDBACK_STATUS_LABEL[status] ?? status);
|
|
489
|
+
}
|
|
490
|
+
function voteControl(doc, idea, o) {
|
|
491
|
+
const count = el(doc, "span", "g-m-vote-count", String(idea.votes));
|
|
492
|
+
if (!o.canVote) {
|
|
493
|
+
const box = el(doc, "span", "g-m-vote g-m-vote-static");
|
|
494
|
+
box.setAttribute("aria-label", plural(idea.votes, "vote"));
|
|
495
|
+
box.append(svgIcon(doc, UP_ICON, "g-m-ico g-m-vote-ico"), count);
|
|
496
|
+
return box;
|
|
497
|
+
}
|
|
498
|
+
const b = el(doc, "button", `g-m-vote${idea.myVote ? " g-m-voted" : ""}`);
|
|
499
|
+
b.type = "button";
|
|
500
|
+
b.setAttribute("aria-pressed", String(!!idea.myVote));
|
|
501
|
+
b.setAttribute("aria-label", `${idea.myVote ? "Remove your vote for" : "Vote for"} ${idea.title} (${plural(idea.votes, "vote")})`);
|
|
502
|
+
b.disabled = !!o.busy;
|
|
503
|
+
b.append(svgIcon(doc, UP_ICON, "g-m-ico g-m-vote-ico"), count);
|
|
504
|
+
b.addEventListener("click", (e) => {
|
|
505
|
+
e.stopPropagation();
|
|
506
|
+
o.onToggle?.();
|
|
507
|
+
});
|
|
508
|
+
return b;
|
|
509
|
+
}
|
|
510
|
+
function ideaRow(doc, idea, o) {
|
|
511
|
+
const row = el(doc, "div", "g-m-idea");
|
|
512
|
+
row.setAttribute("role", "listitem");
|
|
513
|
+
row.appendChild(voteControl(doc, idea, { canVote: o.canVote, busy: o.busy, onToggle: o.onVote }));
|
|
514
|
+
const open = el(doc, "button", "g-m-idea-open");
|
|
515
|
+
open.type = "button";
|
|
516
|
+
open.appendChild(el(doc, "span", "g-m-idea-title", idea.title));
|
|
517
|
+
const meta = el(doc, "span", "g-m-idea-meta");
|
|
518
|
+
meta.appendChild(ideaStatusPill(doc, idea.status));
|
|
519
|
+
if (idea.comments) meta.appendChild(el(doc, "span", "g-m-time", plural(idea.comments, "comment")));
|
|
520
|
+
if (idea.productArea) meta.appendChild(el(doc, "span", "g-m-time", idea.productArea));
|
|
521
|
+
if (idea.mine) meta.appendChild(el(doc, "span", "g-m-time", "Yours"));
|
|
522
|
+
open.appendChild(meta);
|
|
523
|
+
open.addEventListener("click", o.onOpen);
|
|
524
|
+
row.appendChild(open);
|
|
525
|
+
return row;
|
|
526
|
+
}
|
|
527
|
+
var FILTERS = [
|
|
528
|
+
["open", "Open"],
|
|
529
|
+
["all", "All"],
|
|
530
|
+
["mine", "Mine"],
|
|
531
|
+
["following", "Following"]
|
|
532
|
+
];
|
|
533
|
+
var SORTS = [
|
|
534
|
+
["top", "Top"],
|
|
535
|
+
["new", "New"],
|
|
536
|
+
["trending", "Trending"]
|
|
537
|
+
];
|
|
538
|
+
function filterChips(doc, current, onPick) {
|
|
539
|
+
const wrap = el(doc, "div", "g-m-filters");
|
|
540
|
+
wrap.setAttribute("role", "group");
|
|
541
|
+
wrap.setAttribute("aria-label", "Show");
|
|
542
|
+
for (const [f, label] of FILTERS) {
|
|
543
|
+
const b = el(doc, "button", "g-m-filter", label);
|
|
544
|
+
b.type = "button";
|
|
545
|
+
b.dataset.filter = f;
|
|
546
|
+
b.setAttribute("aria-pressed", String(f === current));
|
|
547
|
+
b.addEventListener("click", () => {
|
|
548
|
+
wrap.querySelectorAll("button").forEach((x) => x.setAttribute("aria-pressed", String(x === b)));
|
|
549
|
+
onPick(f);
|
|
550
|
+
});
|
|
551
|
+
wrap.appendChild(b);
|
|
552
|
+
}
|
|
553
|
+
return wrap;
|
|
554
|
+
}
|
|
555
|
+
function sortSelect(doc, current, onChange) {
|
|
556
|
+
const select = el(doc, "select", "g-input g-m-sort");
|
|
557
|
+
select.setAttribute("aria-label", "Sort ideas");
|
|
558
|
+
for (const [s, label] of SORTS) {
|
|
559
|
+
const opt = el(doc, "option", "", label);
|
|
560
|
+
opt.value = s;
|
|
561
|
+
select.appendChild(opt);
|
|
562
|
+
}
|
|
563
|
+
select.value = current;
|
|
564
|
+
select.addEventListener("change", () => onChange(select.value));
|
|
565
|
+
return select;
|
|
566
|
+
}
|
|
567
|
+
function priorityChoice(doc, current, onPick, label = "How important is this to you?") {
|
|
568
|
+
const wrap = el(doc, "div", "g-m-field");
|
|
569
|
+
const id = `g-m-prio${++fieldSeq2}`;
|
|
570
|
+
const l = el(doc, "span", "g-m-label", label);
|
|
571
|
+
l.id = id;
|
|
572
|
+
const group = el(doc, "div", "g-m-seg");
|
|
573
|
+
group.setAttribute("role", "radiogroup");
|
|
574
|
+
group.setAttribute("aria-labelledby", id);
|
|
575
|
+
for (const p of FEEDBACK_PRIORITIES) {
|
|
576
|
+
const b = el(doc, "button", "g-m-seg-btn", FEEDBACK_PRIORITY_LABEL[p]);
|
|
577
|
+
b.type = "button";
|
|
578
|
+
b.dataset.priority = p;
|
|
579
|
+
b.setAttribute("role", "radio");
|
|
580
|
+
b.setAttribute("aria-checked", String(p === current));
|
|
581
|
+
b.addEventListener("click", () => {
|
|
582
|
+
group.querySelectorAll("button").forEach((x) => x.setAttribute("aria-checked", String(x === b)));
|
|
583
|
+
onPick(p);
|
|
584
|
+
});
|
|
585
|
+
group.appendChild(b);
|
|
586
|
+
}
|
|
587
|
+
wrap.append(l, group);
|
|
588
|
+
return wrap;
|
|
589
|
+
}
|
|
590
|
+
var fieldSeq2 = 0;
|
|
591
|
+
function field2(doc, label, control, opts = {}) {
|
|
592
|
+
const id = `g-m-i${++fieldSeq2}`;
|
|
593
|
+
control.id = id;
|
|
594
|
+
const wrap = el(doc, "div", "g-m-field");
|
|
595
|
+
const l = el(doc, "label", "g-m-label", label);
|
|
596
|
+
l.htmlFor = id;
|
|
597
|
+
if (opts.optional) l.appendChild(el(doc, "span", "g-m-optional", " (optional)"));
|
|
598
|
+
if (opts.invalid) control.setAttribute("aria-invalid", "true");
|
|
599
|
+
wrap.append(l, control);
|
|
600
|
+
return wrap;
|
|
601
|
+
}
|
|
602
|
+
function errorLine(doc, text) {
|
|
603
|
+
const p = el(doc, "p", "g-m-error", text);
|
|
604
|
+
p.setAttribute("role", "alert");
|
|
605
|
+
return p;
|
|
606
|
+
}
|
|
607
|
+
function button(doc, cls, text, onClick) {
|
|
608
|
+
const b = el(doc, "button", cls, text);
|
|
609
|
+
b.type = "button";
|
|
610
|
+
b.addEventListener("click", onClick);
|
|
611
|
+
return b;
|
|
612
|
+
}
|
|
613
|
+
function writeNotice(doc, reason, onPortal) {
|
|
614
|
+
const box = el(doc, "div", "g-m-idea-notice");
|
|
615
|
+
box.setAttribute("role", "note");
|
|
616
|
+
box.appendChild(el(doc, "span", "", reason));
|
|
617
|
+
if (onPortal) box.appendChild(button(doc, "g-m-text-btn", "Open the feedback portal", onPortal));
|
|
618
|
+
return box;
|
|
619
|
+
}
|
|
620
|
+
function comment(doc, c) {
|
|
621
|
+
const li = el(doc, "li", `g-m-comment${c.authorType === "team" ? " g-m-comment-team" : ""}`);
|
|
622
|
+
const who = el(doc, "div", "g-m-row g-m-center g-m-comment-meta");
|
|
623
|
+
who.appendChild(el(doc, "span", "g-m-comment-author", c.authorName || (c.authorType === "team" ? "Team" : "Someone")));
|
|
624
|
+
if (c.authorType === "team") who.appendChild(el(doc, "span", "g-m-ai", "Team"));
|
|
625
|
+
who.appendChild(el(doc, "span", "g-m-time", shortDate(c.createdAt)));
|
|
626
|
+
const body = el(doc, "div", "g-m-comment-body");
|
|
627
|
+
body.appendChild(renderSafeText(doc, c.body));
|
|
628
|
+
li.append(who, body);
|
|
629
|
+
return li;
|
|
630
|
+
}
|
|
631
|
+
function ideaDetail(doc, o) {
|
|
632
|
+
const { idea } = o;
|
|
633
|
+
const wrap = el(doc, "div", "g-m-form g-m-idea-detail");
|
|
634
|
+
const meta = el(doc, "div", "g-m-row g-m-center g-m-idea-meta");
|
|
635
|
+
meta.append(el(doc, "span", "g-m-time", feedbackReference(idea.number)), ideaStatusPill(doc, idea.status));
|
|
636
|
+
if (idea.productArea) meta.appendChild(el(doc, "span", "g-m-time", idea.productArea));
|
|
637
|
+
wrap.append(meta, el(doc, "h3", "g-m-form-title", idea.title));
|
|
638
|
+
if (idea.description.trim()) {
|
|
639
|
+
const d = el(doc, "div", "g-m-idea-desc");
|
|
640
|
+
d.appendChild(renderSafeText(doc, idea.description));
|
|
641
|
+
wrap.appendChild(d);
|
|
642
|
+
}
|
|
643
|
+
if (idea.statusNote.trim()) {
|
|
644
|
+
const note = el(doc, "div", "g-m-idea-note");
|
|
645
|
+
note.appendChild(el(doc, "span", "g-m-card-label", `${FEEDBACK_STATUS_LABEL[idea.status] ?? idea.status} \xB7 from the team`));
|
|
646
|
+
note.appendChild(renderSafeText(doc, idea.statusNote));
|
|
647
|
+
wrap.appendChild(note);
|
|
648
|
+
}
|
|
649
|
+
const panel = el(doc, "div", "g-m-card g-m-static g-m-idea-vote");
|
|
650
|
+
const top = el(doc, "div", "g-m-row g-m-center");
|
|
651
|
+
top.appendChild(voteControl(doc, idea, { canVote: false }));
|
|
652
|
+
const said = el(doc, "span", "g-m-grow");
|
|
653
|
+
said.appendChild(el(doc, "span", "g-m-card-title", plural(idea.votes, "vote")));
|
|
654
|
+
if (idea.myVote) said.appendChild(el(doc, "span", "g-m-card-sub", `You voted \xB7 ${FEEDBACK_PRIORITY_LABEL[idea.myVote.priority]}`));
|
|
655
|
+
top.appendChild(said);
|
|
656
|
+
if (!o.blocked) {
|
|
657
|
+
const follow = button(doc, `g-m-follow${idea.following ? " g-m-following" : ""}`, idea.following ? "Following" : "Follow", () => o.onFollow(!idea.following));
|
|
658
|
+
follow.setAttribute("aria-pressed", String(idea.following));
|
|
659
|
+
follow.disabled = o.busy;
|
|
660
|
+
follow.title = idea.following ? "Stop getting updates about this idea" : "Get updates here when it changes";
|
|
661
|
+
top.appendChild(follow);
|
|
662
|
+
}
|
|
663
|
+
panel.appendChild(top);
|
|
664
|
+
if (o.error) panel.appendChild(errorLine(doc, o.error));
|
|
665
|
+
if (o.blocked) panel.appendChild(writeNotice(doc, o.blocked, o.onPortal));
|
|
666
|
+
else if (idea.myVote) {
|
|
667
|
+
panel.appendChild(priorityChoice(doc, idea.myVote.priority, (p) => p !== idea.myVote?.priority && o.onVote(p, ""), "How much do you need it?"));
|
|
668
|
+
const remove = button(doc, "g-m-text-btn g-m-unvote", "Remove my vote", o.onUnvote);
|
|
669
|
+
remove.disabled = o.busy;
|
|
670
|
+
panel.appendChild(remove);
|
|
671
|
+
} else {
|
|
672
|
+
const vote = o.vote;
|
|
673
|
+
panel.appendChild(priorityChoice(doc, vote.priority, (p) => vote.priority = p));
|
|
674
|
+
const note = el(doc, "textarea", "g-input g-m-idea-why");
|
|
675
|
+
note.rows = 2;
|
|
676
|
+
note.maxLength = MAX_FEEDBACK_NOTE;
|
|
677
|
+
note.value = vote.note;
|
|
678
|
+
note.placeholder = "What would it let you do?";
|
|
679
|
+
note.addEventListener("input", () => vote.note = note.value);
|
|
680
|
+
panel.appendChild(field2(doc, "Why it matters", note, { optional: true }));
|
|
681
|
+
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()));
|
|
682
|
+
go.disabled = o.busy;
|
|
683
|
+
panel.appendChild(go);
|
|
684
|
+
}
|
|
685
|
+
wrap.appendChild(panel);
|
|
686
|
+
const comments = el(doc, "div", "g-m-comments");
|
|
687
|
+
comments.appendChild(el(doc, "h4", "g-m-comments-title", idea.thread.length ? `Comments (${idea.thread.length})` : "Comments"));
|
|
688
|
+
if (idea.thread.length) {
|
|
689
|
+
const list = el(doc, "ul", "g-m-comment-list");
|
|
690
|
+
idea.thread.forEach((c) => list.appendChild(comment(doc, c)));
|
|
691
|
+
comments.appendChild(list);
|
|
692
|
+
} else comments.appendChild(el(doc, "p", "g-m-hint", "No comments yet."));
|
|
693
|
+
if (!o.blocked) {
|
|
694
|
+
const form = el(doc, "form", "g-m-comment-form");
|
|
695
|
+
form.noValidate = true;
|
|
696
|
+
const box = el(doc, "textarea", "g-input");
|
|
697
|
+
box.rows = 2;
|
|
698
|
+
box.maxLength = MAX_FEEDBACK_COMMENT;
|
|
699
|
+
box.placeholder = "Add a comment\u2026";
|
|
700
|
+
box.setAttribute("aria-label", "Comment");
|
|
701
|
+
box.value = o.commentDraft;
|
|
702
|
+
const post = el(doc, "button", "g-btn g-btn-secondary", "Post");
|
|
703
|
+
post.type = "submit";
|
|
704
|
+
post.disabled = o.busy || !o.commentDraft.trim();
|
|
705
|
+
box.addEventListener("input", () => {
|
|
706
|
+
o.onCommentInput(box.value);
|
|
707
|
+
post.disabled = o.busy || !box.value.trim();
|
|
708
|
+
});
|
|
709
|
+
form.addEventListener("submit", (e) => {
|
|
710
|
+
e.preventDefault();
|
|
711
|
+
if (box.value.trim() && !o.busy) o.onComment();
|
|
712
|
+
});
|
|
713
|
+
form.append(box, post);
|
|
714
|
+
comments.appendChild(form);
|
|
715
|
+
}
|
|
716
|
+
wrap.appendChild(comments);
|
|
717
|
+
return wrap;
|
|
718
|
+
}
|
|
719
|
+
var emptyIdeaDraft = (over = {}) => ({ title: "", description: "", productArea: "", priority: "nice_to_have", conversationId: null, similar: null, similarFor: "", error: null, sending: false, checked: false, ...over });
|
|
720
|
+
function ideaProblem(draft) {
|
|
721
|
+
return draft.title.trim().length < 3 ? "Give your idea a short title." : null;
|
|
722
|
+
}
|
|
723
|
+
function ideaForm(doc, o) {
|
|
724
|
+
const { draft } = o;
|
|
725
|
+
const form = el(doc, "form", "g-m-form g-m-idea-form");
|
|
726
|
+
form.noValidate = true;
|
|
727
|
+
const title = el(doc, "input", "g-input");
|
|
728
|
+
title.type = "text";
|
|
729
|
+
title.maxLength = MAX_FEEDBACK_TITLE;
|
|
730
|
+
title.required = true;
|
|
731
|
+
title.value = draft.title;
|
|
732
|
+
title.placeholder = "Your idea in a few words";
|
|
733
|
+
title.addEventListener("input", () => {
|
|
734
|
+
draft.title = title.value;
|
|
735
|
+
o.onTitleInput();
|
|
736
|
+
});
|
|
737
|
+
form.appendChild(field2(doc, "Idea", title, { invalid: draft.checked && !!ideaProblem(draft) }));
|
|
738
|
+
const similar = el(doc, "div", "g-m-similar");
|
|
739
|
+
similar.setAttribute("aria-live", "polite");
|
|
740
|
+
form.appendChild(similar);
|
|
741
|
+
const desc = el(doc, "textarea", "g-input g-m-textarea");
|
|
742
|
+
desc.rows = 4;
|
|
743
|
+
desc.maxLength = MAX_FEEDBACK_DESCRIPTION;
|
|
744
|
+
desc.value = draft.description;
|
|
745
|
+
desc.placeholder = "What are you trying to do, and what gets in the way?";
|
|
746
|
+
desc.addEventListener("input", () => draft.description = desc.value);
|
|
747
|
+
form.appendChild(field2(doc, "Details", desc, { optional: true }));
|
|
748
|
+
if (o.productAreas.length) {
|
|
749
|
+
const select = el(doc, "select", "g-input");
|
|
750
|
+
const opt = (label, value) => {
|
|
751
|
+
const x = el(doc, "option", "", label);
|
|
752
|
+
x.value = value;
|
|
753
|
+
return x;
|
|
754
|
+
};
|
|
755
|
+
select.appendChild(opt("Choose one (optional)", ""));
|
|
756
|
+
o.productAreas.forEach((a) => select.appendChild(opt(a, a)));
|
|
757
|
+
select.value = o.productAreas.includes(draft.productArea) ? draft.productArea : "";
|
|
758
|
+
select.addEventListener("change", () => draft.productArea = select.value);
|
|
759
|
+
form.appendChild(field2(doc, "Product area", select, { optional: true }));
|
|
760
|
+
}
|
|
761
|
+
form.appendChild(priorityChoice(doc, draft.priority, (p) => draft.priority = p));
|
|
762
|
+
const foot = el(doc, "div", "g-m-form-foot");
|
|
763
|
+
if (draft.error) foot.appendChild(errorLine(doc, draft.error));
|
|
764
|
+
const submit = el(doc, "button", "g-btn g-btn-primary g-m-wide", draft.sending ? "Sending\u2026" : "Submit idea");
|
|
765
|
+
submit.type = "submit";
|
|
766
|
+
submit.disabled = draft.sending;
|
|
767
|
+
foot.appendChild(submit);
|
|
768
|
+
form.appendChild(foot);
|
|
769
|
+
form.addEventListener("submit", (e) => {
|
|
770
|
+
e.preventDefault();
|
|
771
|
+
if (!draft.sending) o.onSubmit();
|
|
772
|
+
});
|
|
773
|
+
return { form, similar };
|
|
774
|
+
}
|
|
775
|
+
function similarIdeas(doc, ideas, row) {
|
|
776
|
+
if (!ideas.length) return null;
|
|
777
|
+
const box = el(doc, "div", "g-m-similar-box");
|
|
778
|
+
box.appendChild(el(doc, "div", "g-m-card-label", "Similar ideas \u2014 vote instead?"));
|
|
779
|
+
const list = el(doc, "div", "g-m-idea-list");
|
|
780
|
+
list.setAttribute("role", "list");
|
|
781
|
+
ideas.slice(0, 5).forEach((i) => list.appendChild(row(i)));
|
|
782
|
+
box.appendChild(list);
|
|
783
|
+
return box;
|
|
784
|
+
}
|
|
785
|
+
function ideaSent(doc, o) {
|
|
786
|
+
const wrap = el(doc, "div", "g-m-form g-m-sent");
|
|
787
|
+
const top = el(doc, "div", "g-m-sent-top");
|
|
788
|
+
const tick = el(doc, "span", "g-m-sent-tick", "\u2713");
|
|
789
|
+
tick.setAttribute("aria-hidden", "true");
|
|
790
|
+
const p = el(doc, "p", "g-m-card-sub");
|
|
791
|
+
p.append(el(doc, "strong", "", `${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.");
|
|
792
|
+
top.append(tick, el(doc, "h3", "g-m-form-title", "Thanks for your idea"), p);
|
|
793
|
+
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));
|
|
794
|
+
return wrap;
|
|
795
|
+
}
|
|
796
|
+
function updatesList(doc, updates, onOpen) {
|
|
797
|
+
const wrap = el(doc, "div", "g-m-idea-updates");
|
|
798
|
+
if (!updates.length) {
|
|
799
|
+
wrap.appendChild(el(doc, "div", "g-m-empty", "No updates yet. Follow or vote for an idea to hear when it changes."));
|
|
800
|
+
return wrap;
|
|
801
|
+
}
|
|
802
|
+
for (const u of updates) {
|
|
803
|
+
const card = el(doc, "button", "g-m-card g-m-update");
|
|
804
|
+
card.type = "button";
|
|
805
|
+
card.disabled = !u.requestId;
|
|
806
|
+
const row = el(doc, "span", "g-m-row");
|
|
807
|
+
if (!u.read) row.appendChild(el(doc, "span", "g-m-dot"));
|
|
808
|
+
const text = el(doc, "span", "g-m-grow");
|
|
809
|
+
text.appendChild(el(doc, "span", "g-m-card-title", u.title));
|
|
810
|
+
if (u.body) text.appendChild(el(doc, "span", "g-m-card-sub g-m-clamp", u.body));
|
|
811
|
+
row.append(text, el(doc, "span", "g-m-time", shortDate(u.createdAt)));
|
|
812
|
+
card.appendChild(row);
|
|
813
|
+
card.setAttribute("aria-label", `${u.title}${u.read ? "" : ", new"}`);
|
|
814
|
+
card.addEventListener("click", () => onOpen(u));
|
|
815
|
+
wrap.appendChild(card);
|
|
816
|
+
}
|
|
817
|
+
return wrap;
|
|
818
|
+
}
|
|
819
|
+
var LIST_TTL_MS = 15e3;
|
|
820
|
+
var asDetail = (d) => ({ ...d, title: d.title ?? "", description: d.description ?? "", statusNote: d.statusNote ?? "", votes: d.votes ?? 0, thread: Array.isArray(d.thread) ? d.thread : [] });
|
|
821
|
+
function createIdeas(o) {
|
|
822
|
+
const { doc, api } = o;
|
|
823
|
+
let query = { sort: "top", filter: "open", page: 1 };
|
|
824
|
+
let list = null;
|
|
825
|
+
let hasMore = false;
|
|
826
|
+
let listAt = 0;
|
|
827
|
+
let listLoading = false;
|
|
828
|
+
let listError = null;
|
|
829
|
+
let listSeq = 0;
|
|
830
|
+
let listActionError = null;
|
|
831
|
+
const details = /* @__PURE__ */ new Map();
|
|
832
|
+
const detailErrors = /* @__PURE__ */ new Map();
|
|
833
|
+
const actionErrors = /* @__PURE__ */ new Map();
|
|
834
|
+
const voteDrafts = /* @__PURE__ */ new Map();
|
|
835
|
+
const commentDrafts = /* @__PURE__ */ new Map();
|
|
836
|
+
const busy = /* @__PURE__ */ new Set();
|
|
837
|
+
const offers = /* @__PURE__ */ new Map();
|
|
838
|
+
let updates = null;
|
|
839
|
+
let updatesAt = 0;
|
|
840
|
+
let updatesLoading = false;
|
|
841
|
+
let updatesSeq = 0;
|
|
842
|
+
let draft = emptyIdeaDraft();
|
|
843
|
+
let similarTimer = null;
|
|
844
|
+
let similarSeq = 0;
|
|
845
|
+
let searchTimer = null;
|
|
846
|
+
let who = o.identified();
|
|
847
|
+
let mounted = null;
|
|
848
|
+
const mount = (root, paint) => {
|
|
849
|
+
mounted = { root, paint };
|
|
850
|
+
paint();
|
|
851
|
+
};
|
|
852
|
+
const repaint = () => {
|
|
853
|
+
if (mounted?.root.isConnected) mounted.paint();
|
|
854
|
+
};
|
|
855
|
+
const showing = (root) => mounted?.root === root && root.isConnected;
|
|
856
|
+
const cfg = () => o.config() ?? { title: "Ideas", anonymous: false, readOnly: true, productAreas: [], portal: false };
|
|
857
|
+
function blocked() {
|
|
858
|
+
const c = cfg();
|
|
859
|
+
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.";
|
|
860
|
+
if (!c.anonymous && !o.identified()) return "Log in to vote, comment or share an idea.";
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
const canPortal = () => cfg().portal && !!api.ideaPortalLink;
|
|
864
|
+
function openPortal() {
|
|
865
|
+
if (!api.ideaPortalLink) return;
|
|
866
|
+
const win = doc.defaultView;
|
|
867
|
+
let tab = null;
|
|
868
|
+
try {
|
|
869
|
+
tab = win?.open("about:blank", "_blank") ?? null;
|
|
870
|
+
if (tab) tab.opener = null;
|
|
871
|
+
} catch {
|
|
872
|
+
tab = null;
|
|
873
|
+
}
|
|
874
|
+
api.ideaPortalLink().then((raw) => {
|
|
875
|
+
const url = safeHttpUrl(raw);
|
|
876
|
+
if (!url) throw new Error("The portal link isn't available.");
|
|
877
|
+
if (tab) tab.location.href = url;
|
|
878
|
+
else win?.open(url, "_blank", "noopener");
|
|
879
|
+
}).catch((e) => {
|
|
880
|
+
tab?.close();
|
|
881
|
+
listActionError = errorText(e, "The feedback portal couldn't be opened.");
|
|
882
|
+
repaint();
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
const portal = () => canPortal() ? openPortal : void 0;
|
|
886
|
+
function patch(idea) {
|
|
887
|
+
if (list) list = list.map((x) => x.id === idea.id ? { ...x, ...idea } : x);
|
|
888
|
+
const d = details.get(idea.id);
|
|
889
|
+
if (d) details.set(idea.id, { ...d, ...idea, thread: "thread" in idea ? idea.thread : d.thread });
|
|
890
|
+
if (draft.similar) draft.similar = draft.similar.map((x) => x.id === idea.id ? { ...x, ...idea } : x);
|
|
891
|
+
}
|
|
892
|
+
const find = (id) => details.get(id) ?? list?.find((x) => x.id === id) ?? draft.similar?.find((x) => x.id === id);
|
|
893
|
+
function toggleVote(idea) {
|
|
894
|
+
if (busy.has(idea.id) || blocked() || !api.voteIdea || idea.myVote && !api.unvoteIdea) return;
|
|
895
|
+
busy.add(idea.id);
|
|
896
|
+
listActionError = null;
|
|
897
|
+
const voted = !!idea.myVote;
|
|
898
|
+
patch({ ...idea, votes: Math.max(0, idea.votes + (voted ? -1 : 1)), myVote: voted ? null : { priority: "nice_to_have" } });
|
|
899
|
+
repaint();
|
|
900
|
+
(voted ? api.unvoteIdea(idea.id) : api.voteIdea(idea.id, { priority: "nice_to_have" })).then(patch).catch((e) => {
|
|
901
|
+
patch(idea);
|
|
902
|
+
listActionError = errorText(e, "Your vote couldn't be saved.");
|
|
903
|
+
}).finally(() => {
|
|
904
|
+
busy.delete(idea.id);
|
|
905
|
+
repaint();
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
function loadList(append = false) {
|
|
909
|
+
if (!api.listIdeas) return;
|
|
910
|
+
const seq = ++listSeq;
|
|
911
|
+
listLoading = true;
|
|
912
|
+
listError = null;
|
|
913
|
+
const q = { ...query };
|
|
914
|
+
api.listIdeas(q).then((res) => {
|
|
915
|
+
if (seq !== listSeq) return;
|
|
916
|
+
const got = Array.isArray(res?.requests) ? res.requests : [];
|
|
917
|
+
list = append && list ? [...list, ...got.filter((r) => !list.some((x) => x.id === r.id))] : got;
|
|
918
|
+
hasMore = !!res?.hasMore;
|
|
919
|
+
listAt = Date.now();
|
|
920
|
+
}).catch((e) => {
|
|
921
|
+
if (seq === listSeq) listError = errorText(e, "Ideas couldn't be loaded.");
|
|
922
|
+
}).finally(() => {
|
|
923
|
+
if (seq !== listSeq) return;
|
|
924
|
+
listLoading = false;
|
|
925
|
+
repaint();
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
const requery = (change) => {
|
|
929
|
+
query = { ...query, ...change, page: 1 };
|
|
930
|
+
loadList();
|
|
931
|
+
repaint();
|
|
932
|
+
};
|
|
933
|
+
function listScreen() {
|
|
934
|
+
const root = el(doc, "div", "g-m-scroll g-m-ideas");
|
|
935
|
+
const top = el(doc, "div", "g-m-ideas-top");
|
|
936
|
+
const why = blocked();
|
|
937
|
+
if (why) top.appendChild(writeNotice(doc, why, portal()));
|
|
938
|
+
else top.appendChild(button(doc, "g-btn g-btn-primary g-m-wide", "Suggest an idea", o.openForm));
|
|
939
|
+
const updatesLink = el(doc, "div");
|
|
940
|
+
top.appendChild(updatesLink);
|
|
941
|
+
const search = el(doc, "input", "g-input g-m-idea-search");
|
|
942
|
+
search.type = "search";
|
|
943
|
+
search.placeholder = "Search ideas";
|
|
944
|
+
search.setAttribute("aria-label", "Search ideas");
|
|
945
|
+
search.maxLength = 200;
|
|
946
|
+
search.value = query.q ?? "";
|
|
947
|
+
search.addEventListener("input", () => {
|
|
948
|
+
if (searchTimer) clearTimeout(searchTimer);
|
|
949
|
+
searchTimer = setTimeout(() => requery({ q: search.value.trim() || void 0 }), 300);
|
|
950
|
+
});
|
|
951
|
+
const tools = el(doc, "div", "g-m-row g-m-center g-m-idea-tools");
|
|
952
|
+
tools.append(filterChips(doc, query.filter, (filter) => requery({ filter })), sortSelect(doc, query.sort, (sort) => requery({ sort })));
|
|
953
|
+
top.append(search, tools);
|
|
954
|
+
const results = el(doc, "div", "g-m-idea-results");
|
|
955
|
+
root.append(top, results);
|
|
956
|
+
mount(root, () => {
|
|
957
|
+
updatesLink.textContent = "";
|
|
958
|
+
const n = updates?.unread ?? 0;
|
|
959
|
+
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));
|
|
960
|
+
results.textContent = "";
|
|
961
|
+
if (listActionError) results.appendChild(errorLine(doc, listActionError));
|
|
962
|
+
if (!list) {
|
|
963
|
+
if (listError) {
|
|
964
|
+
results.append(errorLine(doc, listError), button(doc, "g-m-text-btn", "Try again", () => loadList()));
|
|
965
|
+
} else results.appendChild(el(doc, "div", "g-m-empty", "Loading ideas\u2026"));
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (!list.length) {
|
|
969
|
+
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.";
|
|
970
|
+
results.appendChild(el(doc, "div", "g-m-empty", empty));
|
|
971
|
+
} else {
|
|
972
|
+
const rows = el(doc, "div", "g-m-idea-list");
|
|
973
|
+
rows.setAttribute("role", "list");
|
|
974
|
+
rows.setAttribute("aria-busy", String(listLoading));
|
|
975
|
+
const canVote = !blocked();
|
|
976
|
+
list.forEach((idea) => rows.appendChild(ideaRow(doc, idea, { canVote, busy: busy.has(idea.id), onVote: () => toggleVote(idea), onOpen: () => o.openIdea(idea.id) })));
|
|
977
|
+
results.appendChild(rows);
|
|
978
|
+
}
|
|
979
|
+
if (listError) results.appendChild(errorLine(doc, listError));
|
|
980
|
+
if (hasMore) {
|
|
981
|
+
const more = button(doc, "g-btn g-btn-secondary g-m-wide", listLoading ? "Loading\u2026" : "Show more", () => {
|
|
982
|
+
query = { ...query, page: query.page + 1 };
|
|
983
|
+
loadList(true);
|
|
984
|
+
repaint();
|
|
985
|
+
});
|
|
986
|
+
more.disabled = listLoading;
|
|
987
|
+
results.appendChild(more);
|
|
988
|
+
}
|
|
989
|
+
if (!why && canPortal()) results.appendChild(button(doc, "g-m-text-btn g-m-portal-link", "Open the feedback portal", openPortal));
|
|
990
|
+
});
|
|
991
|
+
if (!list || Date.now() - listAt > LIST_TTL_MS || query.page > 1) {
|
|
992
|
+
query = { ...query, page: 1 };
|
|
993
|
+
loadList();
|
|
994
|
+
}
|
|
995
|
+
return root;
|
|
996
|
+
}
|
|
997
|
+
function act(id, call, fallback, done) {
|
|
998
|
+
if (busy.has(id)) return;
|
|
999
|
+
const p = call();
|
|
1000
|
+
if (!p) return;
|
|
1001
|
+
busy.add(id);
|
|
1002
|
+
actionErrors.delete(id);
|
|
1003
|
+
repaint();
|
|
1004
|
+
p.then((res) => {
|
|
1005
|
+
patch(res);
|
|
1006
|
+
done?.();
|
|
1007
|
+
}).catch((e) => actionErrors.set(id, errorText(e, fallback))).finally(() => {
|
|
1008
|
+
busy.delete(id);
|
|
1009
|
+
repaint();
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
function detailScreen(id) {
|
|
1013
|
+
const root = el(doc, "div", "g-m-scroll");
|
|
1014
|
+
mount(root, () => {
|
|
1015
|
+
root.textContent = "";
|
|
1016
|
+
const idea = details.get(id);
|
|
1017
|
+
if (!idea) {
|
|
1018
|
+
const err = detailErrors.get(id);
|
|
1019
|
+
root.appendChild(err ? errorLine(doc, err) : el(doc, "div", "g-m-empty", "Loading\u2026"));
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (!voteDrafts.has(id)) voteDrafts.set(id, { priority: "nice_to_have", note: "" });
|
|
1023
|
+
root.appendChild(
|
|
1024
|
+
ideaDetail(doc, {
|
|
1025
|
+
idea,
|
|
1026
|
+
blocked: blocked(),
|
|
1027
|
+
onPortal: portal(),
|
|
1028
|
+
busy: busy.has(id),
|
|
1029
|
+
error: actionErrors.get(id) ?? null,
|
|
1030
|
+
vote: voteDrafts.get(id),
|
|
1031
|
+
commentDraft: commentDrafts.get(id) ?? "",
|
|
1032
|
+
onVote: (priority, note) => act(id, () => api.voteIdea?.(id, { priority, ...note ? { note } : {} }), "Your vote couldn't be saved.", () => voteDrafts.delete(id)),
|
|
1033
|
+
onUnvote: () => act(id, () => api.unvoteIdea?.(id), "Your vote couldn't be removed."),
|
|
1034
|
+
onFollow: (follow) => act(id, () => api.followIdea?.(id, follow), "That didn't work. Please try again."),
|
|
1035
|
+
onCommentInput: (text) => commentDrafts.set(id, text),
|
|
1036
|
+
onComment: () => act(id, () => api.commentIdea?.(id, (commentDrafts.get(id) ?? "").trim()), "Your comment couldn't be posted.", () => commentDrafts.delete(id))
|
|
1037
|
+
})
|
|
1038
|
+
);
|
|
1039
|
+
});
|
|
1040
|
+
if (api.getIdea) {
|
|
1041
|
+
detailErrors.delete(id);
|
|
1042
|
+
api.getIdea(id).then((d) => {
|
|
1043
|
+
details.set(id, asDetail(d));
|
|
1044
|
+
patch(d);
|
|
1045
|
+
}).catch((e) => {
|
|
1046
|
+
if (!details.has(id)) detailErrors.set(id, errorText(e, "This idea couldn't be loaded."));
|
|
1047
|
+
}).finally(() => {
|
|
1048
|
+
if (showing(root)) mounted.paint();
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
return root;
|
|
1052
|
+
}
|
|
1053
|
+
function lookUpSimilar(paint) {
|
|
1054
|
+
const title = draft.title.trim();
|
|
1055
|
+
if (title.length < 3 || !api.similarIdeas) {
|
|
1056
|
+
draft.similar = null;
|
|
1057
|
+
draft.similarFor = "";
|
|
1058
|
+
return paint();
|
|
1059
|
+
}
|
|
1060
|
+
if (title === draft.similarFor) return;
|
|
1061
|
+
const seq = ++similarSeq;
|
|
1062
|
+
api.similarIdeas(title).then((res) => {
|
|
1063
|
+
if (seq !== similarSeq) return;
|
|
1064
|
+
draft.similar = res;
|
|
1065
|
+
draft.similarFor = title;
|
|
1066
|
+
paint();
|
|
1067
|
+
}).catch(() => {
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
function formScreen() {
|
|
1071
|
+
const root = el(doc, "div", "g-m-scroll");
|
|
1072
|
+
let similarBox = null;
|
|
1073
|
+
const paintSimilar = () => {
|
|
1074
|
+
if (!similarBox) return;
|
|
1075
|
+
similarBox.textContent = "";
|
|
1076
|
+
const canVote = !blocked();
|
|
1077
|
+
const box = similarIdeas(doc, draft.similar ?? [], (idea) => ideaRow(doc, idea, { canVote, busy: busy.has(idea.id), onVote: () => toggleVote(idea), onOpen: () => o.openIdea(idea.id) }));
|
|
1078
|
+
if (box) similarBox.appendChild(box);
|
|
1079
|
+
if (listActionError) similarBox.appendChild(errorLine(doc, listActionError));
|
|
1080
|
+
};
|
|
1081
|
+
const paintAll = () => {
|
|
1082
|
+
root.textContent = "";
|
|
1083
|
+
const why = blocked();
|
|
1084
|
+
if (why) {
|
|
1085
|
+
root.appendChild(writeNotice(doc, why, portal()));
|
|
1086
|
+
similarBox = null;
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const { form, similar } = ideaForm(doc, {
|
|
1090
|
+
draft,
|
|
1091
|
+
productAreas: cfg().productAreas ?? [],
|
|
1092
|
+
onTitleInput: () => {
|
|
1093
|
+
if (similarTimer) clearTimeout(similarTimer);
|
|
1094
|
+
similarTimer = setTimeout(() => {
|
|
1095
|
+
if (showing(root)) lookUpSimilar(paintSimilar);
|
|
1096
|
+
}, 300);
|
|
1097
|
+
},
|
|
1098
|
+
onSubmit: () => submit(root, paintAll)
|
|
1099
|
+
});
|
|
1100
|
+
root.appendChild(form);
|
|
1101
|
+
similarBox = similar;
|
|
1102
|
+
paintSimilar();
|
|
1103
|
+
};
|
|
1104
|
+
mounted = { root, paint: paintSimilar };
|
|
1105
|
+
paintAll();
|
|
1106
|
+
if (draft.title.trim() && draft.title.trim() !== draft.similarFor) lookUpSimilar(paintSimilar);
|
|
1107
|
+
return root;
|
|
1108
|
+
}
|
|
1109
|
+
function submit(root, paintAll) {
|
|
1110
|
+
const d = draft;
|
|
1111
|
+
d.checked = true;
|
|
1112
|
+
d.error = ideaProblem(d);
|
|
1113
|
+
if (d.error || !api.submitIdea) return paintAll();
|
|
1114
|
+
d.sending = true;
|
|
1115
|
+
paintAll();
|
|
1116
|
+
api.submitIdea({ title: d.title.trim(), description: d.description.trim(), productArea: d.productArea || null, priority: d.priority, conversationId: d.conversationId }).then((idea) => {
|
|
1117
|
+
details.set(idea.id, asDetail(idea));
|
|
1118
|
+
listAt = 0;
|
|
1119
|
+
if (draft === d) draft = emptyIdeaDraft();
|
|
1120
|
+
if (showing(root)) o.openSent(idea);
|
|
1121
|
+
}).catch((e) => {
|
|
1122
|
+
d.sending = false;
|
|
1123
|
+
d.error = errorText(e, "Your idea couldn't be sent.");
|
|
1124
|
+
if (showing(root)) paintAll();
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
function updatesScreen() {
|
|
1128
|
+
const root = el(doc, "div", "g-m-scroll g-m-idea-updates-screen");
|
|
1129
|
+
mount(root, () => {
|
|
1130
|
+
root.textContent = "";
|
|
1131
|
+
if (!updates) {
|
|
1132
|
+
root.appendChild(el(doc, "div", "g-m-empty", updatesLoading || api.ideaUpdates ? "Loading\u2026" : "No updates yet."));
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
root.appendChild(updatesList(doc, updates.updates, (u) => u.requestId && o.openIdea(u.requestId)));
|
|
1136
|
+
});
|
|
1137
|
+
if (!updates) loadUpdates();
|
|
1138
|
+
else markRead();
|
|
1139
|
+
return root;
|
|
1140
|
+
}
|
|
1141
|
+
function markRead() {
|
|
1142
|
+
const ids = updates?.updates.filter((u) => !u.read).map((u) => u.id) ?? [];
|
|
1143
|
+
if (!ids.length || !api.markIdeaUpdatesRead) return;
|
|
1144
|
+
const seq = updatesSeq;
|
|
1145
|
+
api.markIdeaUpdatesRead(ids).then((res) => {
|
|
1146
|
+
if (seq !== updatesSeq || !updates) return;
|
|
1147
|
+
updates = { updates: updates.updates, unread: res.unread };
|
|
1148
|
+
o.onUnreadChange();
|
|
1149
|
+
}).catch(() => {
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
function loadUpdates(maxAgeMs = 0) {
|
|
1153
|
+
if (!api.ideaUpdates || updatesLoading || Date.now() - updatesAt < maxAgeMs) return;
|
|
1154
|
+
updatesLoading = true;
|
|
1155
|
+
const seq = ++updatesSeq;
|
|
1156
|
+
api.ideaUpdates().then((res) => {
|
|
1157
|
+
if (seq !== updatesSeq) return;
|
|
1158
|
+
const before = updates?.unread ?? 0;
|
|
1159
|
+
updates = { updates: Array.isArray(res?.updates) ? res.updates : [], unread: Number(res?.unread) || 0 };
|
|
1160
|
+
updatesAt = Date.now();
|
|
1161
|
+
if (updates.unread !== before) o.onUnreadChange();
|
|
1162
|
+
if (mounted?.root.isConnected) {
|
|
1163
|
+
mounted.paint();
|
|
1164
|
+
if (mounted.root.classList.contains("g-m-idea-updates-screen")) markRead();
|
|
1165
|
+
}
|
|
1166
|
+
}).catch(() => {
|
|
1167
|
+
}).finally(() => {
|
|
1168
|
+
if (seq === updatesSeq) updatesLoading = false;
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
function offerChip(offer, conversationId, lead) {
|
|
1172
|
+
const canWrite = !blocked();
|
|
1173
|
+
const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer g-m-idea-offer");
|
|
1174
|
+
b.type = "button";
|
|
1175
|
+
const label = el(doc, "span");
|
|
1176
|
+
b.append(lead(), label);
|
|
1177
|
+
const id = offer.requestId;
|
|
1178
|
+
if (!id) {
|
|
1179
|
+
if (!canWrite || !api.submitIdea) return null;
|
|
1180
|
+
label.textContent = "Suggest this idea";
|
|
1181
|
+
b.addEventListener("click", () => {
|
|
1182
|
+
startDraft({ title: offer.title, description: offer.description, conversationId });
|
|
1183
|
+
o.openForm();
|
|
1184
|
+
});
|
|
1185
|
+
return b;
|
|
1186
|
+
}
|
|
1187
|
+
const paint = () => {
|
|
1188
|
+
const state = offers.get(id) ?? (find(id)?.myVote ? "voted" : void 0);
|
|
1189
|
+
b.disabled = state === "voting";
|
|
1190
|
+
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`;
|
|
1191
|
+
return state;
|
|
1192
|
+
};
|
|
1193
|
+
paint();
|
|
1194
|
+
b.addEventListener("click", () => {
|
|
1195
|
+
const state = paint();
|
|
1196
|
+
if (!canWrite || state === "voted" || state === "failed" || !api.voteIdea) return o.openIdea(id);
|
|
1197
|
+
if (state === "voting") return;
|
|
1198
|
+
offers.set(id, "voting");
|
|
1199
|
+
paint();
|
|
1200
|
+
api.voteIdea(id, { priority: "nice_to_have", conversationId }).then((idea) => {
|
|
1201
|
+
patch(idea);
|
|
1202
|
+
offers.set(id, "voted");
|
|
1203
|
+
}).catch(() => offers.set(id, "failed")).finally(paint);
|
|
1204
|
+
});
|
|
1205
|
+
return b;
|
|
1206
|
+
}
|
|
1207
|
+
function startDraft(prefill = {}) {
|
|
1208
|
+
draft = emptyIdeaDraft({ title: (prefill.title ?? "").slice(0, MAX_FEEDBACK_TITLE), description: (prefill.description ?? "").slice(0, MAX_FEEDBACK_DESCRIPTION), conversationId: prefill.conversationId ?? null });
|
|
1209
|
+
}
|
|
1210
|
+
return {
|
|
1211
|
+
blocked,
|
|
1212
|
+
unread: () => updates?.unread ?? 0,
|
|
1213
|
+
listScreen,
|
|
1214
|
+
detailScreen,
|
|
1215
|
+
formScreen,
|
|
1216
|
+
sentScreen: (idea) => {
|
|
1217
|
+
mounted = null;
|
|
1218
|
+
const root = el(doc, "div", "g-m-scroll");
|
|
1219
|
+
root.appendChild(ideaSent(doc, { idea, onView: () => o.openIdea(idea.id), onBack: o.openList }));
|
|
1220
|
+
return root;
|
|
1221
|
+
},
|
|
1222
|
+
updatesScreen,
|
|
1223
|
+
startDraft,
|
|
1224
|
+
offerChip,
|
|
1225
|
+
loadUpdates,
|
|
1226
|
+
sync(open) {
|
|
1227
|
+
const now = o.identified();
|
|
1228
|
+
if (now !== who) {
|
|
1229
|
+
who = now;
|
|
1230
|
+
list = null;
|
|
1231
|
+
listAt = 0;
|
|
1232
|
+
details.clear();
|
|
1233
|
+
offers.clear();
|
|
1234
|
+
updates = null;
|
|
1235
|
+
updatesAt = 0;
|
|
1236
|
+
updatesSeq++;
|
|
1237
|
+
updatesLoading = false;
|
|
1238
|
+
o.onUnreadChange();
|
|
1239
|
+
}
|
|
1240
|
+
if (open) loadUpdates(3e4);
|
|
1241
|
+
},
|
|
1242
|
+
destroy() {
|
|
1243
|
+
if (similarTimer) clearTimeout(similarTimer);
|
|
1244
|
+
if (searchTimer) clearTimeout(searchTimer);
|
|
1245
|
+
mounted = null;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
382
1250
|
// src/messenger-styles.ts
|
|
383
1251
|
var MESSENGER_STYLES = `
|
|
384
1252
|
:host([data-g-messenger="bottom-right"]) .g-launcher.g-pos-bottom-right{right:72px}
|
|
@@ -582,90 +1450,81 @@ var MESSENGER_STYLES = `
|
|
|
582
1450
|
.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}
|
|
583
1451
|
.g-m-step-done{color:var(--g-text);font-weight:600}
|
|
584
1452
|
.g-m-step-done .g-m-step-dot{background:var(--g-primary);border-color:var(--g-primary)}
|
|
1453
|
+
.g-m-ideas-top{display:flex;flex-direction:column;gap:10px;margin-bottom:12px}
|
|
1454
|
+
.g-m-ideas-top:empty{display:none}
|
|
1455
|
+
.g-m-idea-tools{justify-content:space-between;gap:8px;flex-wrap:wrap}
|
|
1456
|
+
.g-m-filters{display:flex;gap:6px;flex-wrap:wrap}
|
|
1457
|
+
.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}
|
|
1458
|
+
.g-m-filter:hover{background:var(--g-subtle)}
|
|
1459
|
+
.g-m-filter[aria-pressed="true"]{background:var(--g-primary);border-color:var(--g-primary);color:var(--g-primary-text)}
|
|
1460
|
+
.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}
|
|
1461
|
+
.g-m-sort{width:auto;min-width:0;min-height:30px;padding:4px 8px;font-size:12px}
|
|
1462
|
+
.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}
|
|
1463
|
+
.g-m-idea{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-top:1px solid var(--g-border)}
|
|
1464
|
+
.g-m-idea:first-child{border-top:0}
|
|
1465
|
+
.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}
|
|
1466
|
+
button.g-m-vote:hover{border-color:var(--g-primary);color:var(--g-primary)}
|
|
1467
|
+
.g-m-vote:disabled{opacity:.6;cursor:progress}
|
|
1468
|
+
.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)}
|
|
1469
|
+
.g-m-vote-static{cursor:default;background:var(--g-subtle)}
|
|
1470
|
+
.g-m-vote-ico{width:16px;height:16px;stroke-width:2.4}
|
|
1471
|
+
.g-m-vote-count{font-size:13px;font-weight:700;line-height:1.2}
|
|
1472
|
+
.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}
|
|
1473
|
+
.g-m-idea-open:hover .g-m-idea-title{color:var(--g-primary)}
|
|
1474
|
+
.g-m-idea-title{font-weight:600;overflow-wrap:anywhere}
|
|
1475
|
+
.g-m-idea-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
1476
|
+
.g-m-idea-pill.g-m-idea-new{background:var(--g-subtle);color:var(--g-muted)}
|
|
1477
|
+
.g-m-idea-pill.g-m-idea-under_review{background:#e0e7ff;color:#3730a3}
|
|
1478
|
+
.g-m-idea-pill.g-m-idea-planned{background:#dbeafe;color:#1e40af}
|
|
1479
|
+
.g-m-idea-pill.g-m-idea-in_progress{background:#fef3c7;color:#92400e}
|
|
1480
|
+
.g-m-idea-pill.g-m-idea-released{background:#dcfce7;color:#166534}
|
|
1481
|
+
.g-m-idea-pill.g-m-idea-declined{background:#fee2e2;color:#991b1b}
|
|
1482
|
+
.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)}
|
|
1483
|
+
.g-m-idea-notice .g-m-text-btn{padding:4px 0;min-height:0}
|
|
1484
|
+
.g-m-updates-link,.g-m-portal-link{align-self:flex-start;padding-left:0}
|
|
1485
|
+
.g-m-portal-link{display:block;margin:0 auto}
|
|
1486
|
+
.g-m-idea-detail{gap:12px}
|
|
1487
|
+
.g-m-idea-desc,.g-m-idea-note{font-size:14px;overflow-wrap:anywhere}
|
|
1488
|
+
.g-m-idea-desc p,.g-m-idea-note p,.g-m-comment-body p{margin:0 0 8px}
|
|
1489
|
+
.g-m-idea-desc p:last-child,.g-m-idea-note p:last-child,.g-m-comment-body p:last-child{margin:0}
|
|
1490
|
+
.g-m-idea-note{border-left:3px solid var(--g-primary);background:var(--g-subtle);border-radius:6px;padding:8px 10px}
|
|
1491
|
+
.g-m-idea-vote{display:flex;flex-direction:column;gap:12px;margin:0}
|
|
1492
|
+
.g-m-idea-vote .g-m-vote{min-height:44px}
|
|
1493
|
+
.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}
|
|
1494
|
+
.g-m-follow:hover{background:var(--g-subtle)}
|
|
1495
|
+
.g-m-follow.g-m-following{border-color:var(--g-primary);color:var(--g-primary)}
|
|
1496
|
+
.g-m-follow:disabled{opacity:.6}
|
|
1497
|
+
.g-m-seg{display:flex;border:1px solid var(--g-border);border-radius:10px;overflow:hidden}
|
|
1498
|
+
.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}
|
|
1499
|
+
.g-m-seg-btn:first-child{border-left:0}
|
|
1500
|
+
.g-m-seg-btn:hover{background:var(--g-subtle)}
|
|
1501
|
+
.g-m-seg-btn[aria-checked="true"]{background:var(--g-primary);color:var(--g-primary-text)}
|
|
1502
|
+
.g-m-unvote{align-self:flex-start;padding-left:0;color:var(--g-muted)}
|
|
1503
|
+
.g-m-comments{display:flex;flex-direction:column;gap:8px}
|
|
1504
|
+
.g-m-comments-title{margin:4px 0 0;font-size:14px}
|
|
1505
|
+
.g-m-comment-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
|
|
1506
|
+
.g-m-comment{border:1px solid var(--g-border);border-radius:10px;padding:8px 10px;font-size:13px}
|
|
1507
|
+
.g-m-comment-team{background:var(--g-subtle);border-color:color-mix(in srgb,var(--g-primary) 35%,var(--g-border))}
|
|
1508
|
+
.g-m-comment-meta{gap:6px;margin-bottom:4px}
|
|
1509
|
+
.g-m-comment-author{font-weight:600}
|
|
1510
|
+
.g-m-comment-body{overflow-wrap:anywhere}
|
|
1511
|
+
.g-m-comment-form{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
|
|
1512
|
+
.g-m-comment-form textarea{resize:vertical;min-height:56px}
|
|
1513
|
+
.g-m-similar:empty{display:none}
|
|
1514
|
+
.g-m-similar-box{display:flex;flex-direction:column;gap:6px}
|
|
1515
|
+
.g-m-similar-box .g-m-idea-list{margin:0}
|
|
1516
|
+
.g-m-update{display:block}
|
|
1517
|
+
.g-m-update:disabled{cursor:default}
|
|
1518
|
+
.g-m-update:disabled:hover{background:var(--g-bg)}
|
|
585
1519
|
`;
|
|
586
1520
|
|
|
587
|
-
// src/render-safe.ts
|
|
588
|
-
var CITATION = /\s*\[\[article:[^\]]*\]\]/g;
|
|
589
|
-
var INLINE = /\*\*([^*\n]+)\*\*|`([^`\n]+)`|(!?)\[([^\]\n]{1,200})\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>()"']+[^\s<>().,;:!?'"])/g;
|
|
590
|
-
function stripCitationMarkers(text) {
|
|
591
|
-
return text.replace(CITATION, "");
|
|
592
|
-
}
|
|
593
|
-
function safeHttpUrl(raw) {
|
|
594
|
-
try {
|
|
595
|
-
const u = new URL(raw);
|
|
596
|
-
return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
|
|
597
|
-
} catch {
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
function link(doc, label, href) {
|
|
602
|
-
const url = safeHttpUrl(href);
|
|
603
|
-
if (!url) return doc.createTextNode(label);
|
|
604
|
-
const a = doc.createElement("a");
|
|
605
|
-
a.href = url;
|
|
606
|
-
a.target = "_blank";
|
|
607
|
-
a.rel = "noopener noreferrer nofollow";
|
|
608
|
-
a.textContent = label;
|
|
609
|
-
return a;
|
|
610
|
-
}
|
|
611
|
-
function appendInline(doc, parent, text) {
|
|
612
|
-
let last = 0;
|
|
613
|
-
for (const m of text.matchAll(INLINE)) {
|
|
614
|
-
const start = m.index ?? 0;
|
|
615
|
-
if (start > last) parent.appendChild(doc.createTextNode(text.slice(last, start)));
|
|
616
|
-
if (m[1] !== void 0) {
|
|
617
|
-
const b = doc.createElement("strong");
|
|
618
|
-
b.textContent = m[1];
|
|
619
|
-
parent.appendChild(b);
|
|
620
|
-
} else if (m[2] !== void 0) {
|
|
621
|
-
const c = doc.createElement("code");
|
|
622
|
-
c.textContent = m[2];
|
|
623
|
-
parent.appendChild(c);
|
|
624
|
-
} else if (m[4] !== void 0 && m[5] !== void 0) {
|
|
625
|
-
parent.appendChild(m[3] ? doc.createTextNode(m[4]) : link(doc, m[4], m[5]));
|
|
626
|
-
} else if (m[6] !== void 0) {
|
|
627
|
-
parent.appendChild(link(doc, m[6], m[6]));
|
|
628
|
-
}
|
|
629
|
-
last = start + m[0].length;
|
|
630
|
-
}
|
|
631
|
-
if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
|
|
632
|
-
}
|
|
633
|
-
var BULLET = /^\s*[-*•]\s+/;
|
|
634
|
-
var NUMBERED = /^\s*\d{1,3}[.)]\s+/;
|
|
635
|
-
function renderSafeText(doc, text) {
|
|
636
|
-
const frag = doc.createDocumentFragment();
|
|
637
|
-
const blocks = stripCitationMarkers(text).replace(/\r\n?/g, "\n").trim().split(/\n{2,}/);
|
|
638
|
-
for (const block of blocks) {
|
|
639
|
-
if (!block.trim()) continue;
|
|
640
|
-
const lines = block.split("\n");
|
|
641
|
-
if (lines.every((l) => BULLET.test(l)) || lines.every((l) => NUMBERED.test(l))) {
|
|
642
|
-
const ordered = NUMBERED.test(lines[0]);
|
|
643
|
-
const list = doc.createElement(ordered ? "ol" : "ul");
|
|
644
|
-
for (const line of lines) {
|
|
645
|
-
const li = doc.createElement("li");
|
|
646
|
-
appendInline(doc, li, line.replace(ordered ? NUMBERED : BULLET, ""));
|
|
647
|
-
list.appendChild(li);
|
|
648
|
-
}
|
|
649
|
-
frag.appendChild(list);
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
const p = doc.createElement("p");
|
|
653
|
-
lines.forEach((line, i) => {
|
|
654
|
-
if (i) p.appendChild(doc.createElement("br"));
|
|
655
|
-
appendInline(doc, p, line);
|
|
656
|
-
});
|
|
657
|
-
frag.appendChild(p);
|
|
658
|
-
}
|
|
659
|
-
return frag;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
1521
|
// src/messenger.ts
|
|
663
|
-
var
|
|
1522
|
+
var SVG_NS2 = "http://www.w3.org/2000/svg";
|
|
664
1523
|
function chatIcon(doc) {
|
|
665
|
-
const svg = doc.createElementNS(
|
|
1524
|
+
const svg = doc.createElementNS(SVG_NS2, "svg");
|
|
666
1525
|
svg.setAttribute("viewBox", "0 0 24 24");
|
|
667
1526
|
svg.setAttribute("aria-hidden", "true");
|
|
668
|
-
const path = doc.createElementNS(
|
|
1527
|
+
const path = doc.createElementNS(SVG_NS2, "path");
|
|
669
1528
|
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");
|
|
670
1529
|
svg.appendChild(path);
|
|
671
1530
|
return svg;
|
|
@@ -690,15 +1549,16 @@ var ICONS = {
|
|
|
690
1549
|
circle: ["M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"],
|
|
691
1550
|
expand: ["M15 3h6v6", "M21 3l-7 7", "M9 21H3v-6", "M3 21l7-7"],
|
|
692
1551
|
flag: ["M5 21V4", "M5 4h12l-2.5 4L17 12H5"],
|
|
693
|
-
collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"]
|
|
1552
|
+
collapse: ["M4 14h6v6", "M3 21l7-7", "M20 10h-6V4", "M21 3l-7 7"],
|
|
1553
|
+
idea: IDEA_ICON
|
|
694
1554
|
};
|
|
695
1555
|
function icon(doc, name, cls = "g-m-ico") {
|
|
696
|
-
const svg = doc.createElementNS(
|
|
1556
|
+
const svg = doc.createElementNS(SVG_NS2, "svg");
|
|
697
1557
|
svg.setAttribute("viewBox", "0 0 24 24");
|
|
698
1558
|
svg.setAttribute("aria-hidden", "true");
|
|
699
1559
|
svg.setAttribute("class", cls);
|
|
700
1560
|
for (const d of ICONS[name] ?? []) {
|
|
701
|
-
const path = doc.createElementNS(
|
|
1561
|
+
const path = doc.createElementNS(SVG_NS2, "path");
|
|
702
1562
|
path.setAttribute("d", d);
|
|
703
1563
|
svg.appendChild(path);
|
|
704
1564
|
}
|
|
@@ -725,7 +1585,7 @@ function shortDateTime(iso) {
|
|
|
725
1585
|
return d.toISOString().slice(0, 16).replace("T", " ");
|
|
726
1586
|
}
|
|
727
1587
|
}
|
|
728
|
-
function
|
|
1588
|
+
function shortDate2(iso) {
|
|
729
1589
|
const d = new Date(iso);
|
|
730
1590
|
if (Number.isNaN(d.getTime())) return "";
|
|
731
1591
|
try {
|
|
@@ -820,17 +1680,24 @@ function renderMessenger(options) {
|
|
|
820
1680
|
panel.append(head, main, tabs);
|
|
821
1681
|
root.append(launcher, panel);
|
|
822
1682
|
scope.appendChild(root);
|
|
1683
|
+
function withRcText(items, rc, apply) {
|
|
1684
|
+
if (!rc?.length) return items;
|
|
1685
|
+
const byId = new Map(rc.map((x) => [x.id, x]));
|
|
1686
|
+
return items.map((i) => byId.has(i.id) ? apply(i, byId.get(i.id)) : i);
|
|
1687
|
+
}
|
|
1688
|
+
const helpModules = () => options.resourceCenter?.modules?.length ? 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;
|
|
823
1689
|
const rcExtras = (ids = []) => ids.map((id) => options.guides?.find((g) => g.id === id || g.key === id)).filter((g) => !!g);
|
|
824
1690
|
const help = createHelpView({
|
|
825
1691
|
doc,
|
|
826
1692
|
data: () => ({
|
|
827
|
-
collections: articles?.collections ?? options.resourceCenter?.collections ?? [],
|
|
828
|
-
articles: articles?.articles ?? options.resourceCenter?.articles ?? [],
|
|
1693
|
+
collections: withRcText(articles?.collections ?? options.resourceCenter?.collections ?? [], options.resourceCenter?.collections, (c, t) => ({ ...c, name: t.name, description: t.description })),
|
|
1694
|
+
articles: withRcText(articles?.articles ?? options.resourceCenter?.articles ?? [], options.resourceCenter?.articles, (a, t) => ({ ...a, title: t.title, summary: t.summary })),
|
|
829
1695
|
suggested: helpSuggestions(),
|
|
830
1696
|
recommended: rcExtras(options.resourceCenter?.recommendedGuideIds),
|
|
831
1697
|
checklists: rcExtras(options.resourceCenter?.checklistGuideIds),
|
|
832
1698
|
faqs: options.resourceCenter?.faqs,
|
|
833
|
-
links: options.resourceCenter?.links
|
|
1699
|
+
links: options.resourceCenter?.links,
|
|
1700
|
+
modules: helpModules()
|
|
834
1701
|
}),
|
|
835
1702
|
search: (q) => api.searchArticles(q),
|
|
836
1703
|
audienceIds: options.audienceIds,
|
|
@@ -849,6 +1716,24 @@ function renderMessenger(options) {
|
|
|
849
1716
|
onShowMe: (key) => startWalkthrough(key),
|
|
850
1717
|
onArticleChange: () => syncSize()
|
|
851
1718
|
});
|
|
1719
|
+
const ideasOn = () => !!config.ideas && !!api.listIdeas;
|
|
1720
|
+
const ideasTab = { kind: "tab", tab: "ideas" };
|
|
1721
|
+
const ideas = createIdeas({
|
|
1722
|
+
doc,
|
|
1723
|
+
api,
|
|
1724
|
+
config: () => config.ideas,
|
|
1725
|
+
identified: () => visitor().identified !== false,
|
|
1726
|
+
openIdea: (id) => go({ kind: "idea", id, from: view.kind === "idea" ? view.from : view }),
|
|
1727
|
+
openForm: () => go({ kind: "idea-new", from: view }),
|
|
1728
|
+
openSent: (idea) => go({ kind: "idea-sent", idea }),
|
|
1729
|
+
openList: () => go(ideasTab),
|
|
1730
|
+
openUpdates: () => go({ kind: "idea-updates", from: view }),
|
|
1731
|
+
onUnreadChange: () => {
|
|
1732
|
+
if (!open || destroyed) return;
|
|
1733
|
+
if (view.kind === "tab" && view.tab === "home") render();
|
|
1734
|
+
else renderTabs();
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
852
1737
|
let docCollapsed = false;
|
|
853
1738
|
let sizeButton = null;
|
|
854
1739
|
const readingDoc = () => open && (view.kind === "howto" || view.kind === "tab" && view.tab === "help" && help.isArticleOpen());
|
|
@@ -943,7 +1828,7 @@ function renderMessenger(options) {
|
|
|
943
1828
|
const refreshConversations = () => api.listConversations().then((list) => {
|
|
944
1829
|
conversations = [...list].sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt));
|
|
945
1830
|
updateBadge();
|
|
946
|
-
if (open && view.kind === "tab" && view.tab !== "help") render();
|
|
1831
|
+
if (open && view.kind === "tab" && view.tab !== "help" && view.tab !== "ideas") render();
|
|
947
1832
|
}).catch(() => {
|
|
948
1833
|
});
|
|
949
1834
|
function renderHead(title, back) {
|
|
@@ -977,6 +1862,7 @@ function renderMessenger(options) {
|
|
|
977
1862
|
["messages", "messages", "Messages"],
|
|
978
1863
|
["help", "help", "Help"]
|
|
979
1864
|
];
|
|
1865
|
+
if (ideasOn()) items.push(["ideas", "idea", config.ideas.title || "Ideas"]);
|
|
980
1866
|
const latest = news();
|
|
981
1867
|
if (latest.length) items.push(["news", "news", "News"]);
|
|
982
1868
|
for (const [tab, ico, label] of items) {
|
|
@@ -987,7 +1873,7 @@ function renderMessenger(options) {
|
|
|
987
1873
|
const i = el(doc, "span", "g-m-tab-icon");
|
|
988
1874
|
i.appendChild(icon(doc, ico));
|
|
989
1875
|
b.append(i, el(doc, "span", "", label));
|
|
990
|
-
if (tab === "news" && latest.some((n) => n.unread)) {
|
|
1876
|
+
if (tab === "news" && latest.some((n) => n.unread) || tab === "ideas" && ideas.unread() > 0) {
|
|
991
1877
|
const dot2 = el(doc, "span", "g-m-tab-dot");
|
|
992
1878
|
dot2.setAttribute("aria-label", "New");
|
|
993
1879
|
i.appendChild(dot2);
|
|
@@ -1193,11 +2079,16 @@ function renderMessenger(options) {
|
|
|
1193
2079
|
const body = el(doc, "span", "g-m-news-body");
|
|
1194
2080
|
const meta = el(doc, "span", "g-m-row g-m-center g-m-news-meta");
|
|
1195
2081
|
if (n.unread) meta.appendChild(el(doc, "span", "g-m-tag", "New"));
|
|
1196
|
-
meta.appendChild(el(doc, "span", "g-m-time",
|
|
2082
|
+
meta.appendChild(el(doc, "span", "g-m-time", shortDate2(n.publishedAt)));
|
|
1197
2083
|
body.append(meta, el(doc, "span", "g-m-card-title", n.title));
|
|
1198
2084
|
if (n.body) body.appendChild(el(doc, "span", "g-m-card-sub g-m-clamp", n.body));
|
|
1199
2085
|
card.appendChild(body);
|
|
1200
|
-
card.addEventListener("click", () =>
|
|
2086
|
+
card.addEventListener("click", () => {
|
|
2087
|
+
if (n.key) startGuide(n.key);
|
|
2088
|
+
else if (n.articleId) openArticle(n.articleId);
|
|
2089
|
+
else if (n.url && /^https?:\/\//i.test(n.url)) window.open(n.url, "_blank", "noopener,noreferrer");
|
|
2090
|
+
else startGuide(n.id);
|
|
2091
|
+
});
|
|
1201
2092
|
return card;
|
|
1202
2093
|
}
|
|
1203
2094
|
function renderHome() {
|
|
@@ -1223,6 +2114,8 @@ function renderMessenger(options) {
|
|
|
1223
2114
|
if (filed) inner.appendChild(conversationCard(filed, "Your complaint"));
|
|
1224
2115
|
const recent = conversations.find((c) => c.id !== filed?.id);
|
|
1225
2116
|
if (recent) inner.appendChild(conversationCard(recent, recent.status === "closed" && !recent.unread ? "Recent conversation" : "Continue the conversation"));
|
|
2117
|
+
const ideaUpdates = ideasOn() ? ideas.unread() : 0;
|
|
2118
|
+
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 }) }));
|
|
1226
2119
|
if (config.status) inner.appendChild(statusCard(config.status));
|
|
1227
2120
|
if (cards.progress) progressCards(v).forEach((c) => inner.appendChild(c));
|
|
1228
2121
|
const suggestions = pageSuggestions(v);
|
|
@@ -1242,6 +2135,10 @@ function renderMessenger(options) {
|
|
|
1242
2135
|
}
|
|
1243
2136
|
const latest = news()[0];
|
|
1244
2137
|
if (latest) inner.appendChild(newsCard(latest));
|
|
2138
|
+
if (ideasOn()) {
|
|
2139
|
+
const canWrite = !ideas.blocked();
|
|
2140
|
+
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) }));
|
|
2141
|
+
}
|
|
1245
2142
|
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" }) }));
|
|
1246
2143
|
scroll.append(hero, inner);
|
|
1247
2144
|
main.appendChild(scroll);
|
|
@@ -1342,6 +2239,25 @@ function renderMessenger(options) {
|
|
|
1342
2239
|
scroll.appendChild(complaintSent(doc, { complaint: v.complaint, answers: v.answers, onOpenConversation: () => go({ kind: "thread", conversationId: v.conversationId }), onHome: () => go({ kind: "tab", tab: "home" }) }));
|
|
1343
2240
|
main.appendChild(scroll);
|
|
1344
2241
|
}
|
|
2242
|
+
function renderIdeasView(v) {
|
|
2243
|
+
const title = config.ideas?.title || "Ideas";
|
|
2244
|
+
if (v.kind === "idea") {
|
|
2245
|
+
renderHead(title, () => go(v.from));
|
|
2246
|
+
main.appendChild(ideas.detailScreen(v.id));
|
|
2247
|
+
} else if (v.kind === "idea-new") {
|
|
2248
|
+
renderHead("Suggest an idea", () => go(v.from));
|
|
2249
|
+
main.appendChild(ideas.formScreen());
|
|
2250
|
+
} else if (v.kind === "idea-sent") {
|
|
2251
|
+
renderHead(title, () => go(ideasTab));
|
|
2252
|
+
main.appendChild(ideas.sentScreen(v.idea));
|
|
2253
|
+
} else if (v.kind === "idea-updates") {
|
|
2254
|
+
renderHead("Updates on your ideas", () => go(v.from));
|
|
2255
|
+
main.appendChild(ideas.updatesScreen());
|
|
2256
|
+
} else {
|
|
2257
|
+
renderHead(title);
|
|
2258
|
+
main.appendChild(ideas.listScreen());
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
1345
2261
|
function renderHowToView(id, from) {
|
|
1346
2262
|
const cached = howToCache.get(id);
|
|
1347
2263
|
renderHead(cached?.title ?? "How-to", () => go(from));
|
|
@@ -1455,7 +2371,8 @@ function renderMessenger(options) {
|
|
|
1455
2371
|
bubble.dataset.messageId = m.id;
|
|
1456
2372
|
wrap.appendChild(bubble);
|
|
1457
2373
|
const offer = m.complaintOffer && canComplain() ? complaintCategories().find((c) => c.id === m.complaintOffer.categoryId) : void 0;
|
|
1458
|
-
|
|
2374
|
+
const ideaChip = m.ideaOffer && ideasOn() ? ideas.offerChip(m.ideaOffer, current?.id ?? null, () => icon(doc, "idea", "g-m-ico g-m-chip-ico")) : null;
|
|
2375
|
+
if (m.citations.length || m.suggestedGuides.length || m.howTos?.length || offer || ideaChip) {
|
|
1459
2376
|
const chips = el(doc, "div", "g-m-chips");
|
|
1460
2377
|
if (offer) {
|
|
1461
2378
|
const b = el(doc, "button", "g-m-chip g-m-guide g-m-offer");
|
|
@@ -1468,6 +2385,7 @@ function renderMessenger(options) {
|
|
|
1468
2385
|
});
|
|
1469
2386
|
chips.appendChild(b);
|
|
1470
2387
|
}
|
|
2388
|
+
if (ideaChip) chips.appendChild(ideaChip);
|
|
1471
2389
|
for (const g of m.suggestedGuides) {
|
|
1472
2390
|
const b = el(doc, "button", "g-m-chip g-m-guide", `\u25B6 Show me: ${g.name}`);
|
|
1473
2391
|
b.type = "button";
|
|
@@ -1620,7 +2538,13 @@ function renderMessenger(options) {
|
|
|
1620
2538
|
else if (view.kind === "complaint-category") renderComplaintCategories(view);
|
|
1621
2539
|
else if (view.kind === "complaint-form") renderComplaintForm(view);
|
|
1622
2540
|
else if (view.kind === "complaint-sent") renderComplaintSent(view);
|
|
1623
|
-
else if (view.tab === "
|
|
2541
|
+
else if (view.kind !== "tab" || view.tab === "ideas") {
|
|
2542
|
+
if (ideasOn()) renderIdeasView(view);
|
|
2543
|
+
else {
|
|
2544
|
+
view = { kind: "tab", tab: "home" };
|
|
2545
|
+
renderHome();
|
|
2546
|
+
}
|
|
2547
|
+
} else if (view.tab === "home") renderHome();
|
|
1624
2548
|
else if (view.tab === "messages") renderMessagesTab();
|
|
1625
2549
|
else if (view.tab === "news" && news().length) renderNewsTab();
|
|
1626
2550
|
else if (view.tab === "news") {
|
|
@@ -1771,6 +2695,7 @@ function renderMessenger(options) {
|
|
|
1771
2695
|
render();
|
|
1772
2696
|
void refreshConversations();
|
|
1773
2697
|
loadHowTos();
|
|
2698
|
+
if (ideasOn()) ideas.loadUpdates();
|
|
1774
2699
|
if (view.kind === "thread") scheduleThreadPoll();
|
|
1775
2700
|
options.onOpen?.();
|
|
1776
2701
|
} else {
|
|
@@ -1819,15 +2744,22 @@ function renderMessenger(options) {
|
|
|
1819
2744
|
setOpen(true);
|
|
1820
2745
|
startComplaint(categoryId ?? null);
|
|
1821
2746
|
},
|
|
2747
|
+
openIdeas(requestId) {
|
|
2748
|
+
if (!ideasOn()) return;
|
|
2749
|
+
setOpen(true);
|
|
2750
|
+
go(requestId ? { kind: "idea", id: requestId, from: ideasTab } : ideasTab);
|
|
2751
|
+
},
|
|
1822
2752
|
refresh() {
|
|
1823
2753
|
if (destroyed) return;
|
|
2754
|
+
if (ideasOn()) ideas.sync(open);
|
|
1824
2755
|
if (!open) {
|
|
1825
2756
|
pageKey = null;
|
|
1826
2757
|
return;
|
|
1827
2758
|
}
|
|
1828
2759
|
loadPage();
|
|
1829
|
-
if (view.kind === "tab") render();
|
|
1830
|
-
else if (
|
|
2760
|
+
if (view.kind === "tab" && view.tab !== "ideas") render();
|
|
2761
|
+
else if (view.kind === "tab") renderTabs();
|
|
2762
|
+
else if (view.kind === "thread" && !messages.length) renderLog();
|
|
1831
2763
|
},
|
|
1832
2764
|
destroy() {
|
|
1833
2765
|
destroyed = true;
|
|
@@ -1835,6 +2767,7 @@ function renderMessenger(options) {
|
|
|
1835
2767
|
if (backgroundTimer) clearInterval(backgroundTimer);
|
|
1836
2768
|
panel.removeEventListener("keydown", onKey);
|
|
1837
2769
|
help.destroy();
|
|
2770
|
+
ideas.destroy();
|
|
1838
2771
|
shadowHost?.removeAttribute("data-g-messenger");
|
|
1839
2772
|
root.remove();
|
|
1840
2773
|
}
|