@starlove/orbit 1.3.4 → 1.4.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/Dockerfile +2 -1
- package/README.md +10 -9
- package/SKILL-ORBIT.md +41 -17
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/dist/full/fonts/Chakra_Petch/OFL.txt +93 -0
- package/dist/full/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/dist/full/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/dist/full/fonts/Inter/OFL.txt +93 -0
- package/dist/full/fonts/Inter/README.txt +118 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/JetBrains_Mono/OFL.txt +93 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +79 -0
- package/dist/full/fonts/OpenRing-Regular.ttf +0 -0
- package/dist/full/fonts/Open_Sans/OFL.txt +93 -0
- package/dist/full/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/dist/full/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/dist/full/fonts/Open_Sans/README.txt +100 -0
- package/dist/full/fonts/Orbitron/OFL.txt +93 -0
- package/dist/full/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/dist/full/fonts/Orbitron/README.txt +68 -0
- package/dist/full/fonts/Rajdhani/OFL.txt +93 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/dist/full/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/dist/full/fonts/runescape-uf.ttf +0 -0
- package/dist/full/index.html +137 -27
- package/dist/full/js/app.62905fcb.js +952 -0
- package/dist/full/js/app.62905fcb.js.map +7 -0
- package/dist/full/styles.4e60d824.css +1 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +20 -13
- package/docs/ORBIT_DISPATCH.md +73 -38
- package/docs/UPDATING.md +114 -0
- package/package.json +5 -7
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +93 -0
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +93 -0
- package/public/fonts/Inter/README.txt +118 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/OFL.txt +93 -0
- package/public/fonts/JetBrains_Mono/README.txt +79 -0
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OFL.txt +93 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +100 -0
- package/public/fonts/Orbitron/OFL.txt +93 -0
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +68 -0
- package/public/fonts/Rajdhani/OFL.txt +93 -0
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +135 -25
- package/public/js/artifact-drawer.js +79 -0
- package/public/js/board-export.js +161 -0
- package/public/js/board-menu.js +219 -17
- package/public/js/card-actions.js +1 -1
- package/public/js/create-card.js +60 -23
- package/public/js/drawer.js +164 -4
- package/public/js/font-preference.js +229 -0
- package/public/js/format.js +0 -10
- package/public/js/icons.js +16 -0
- package/public/js/kanban.js +123 -1
- package/public/js/local-backend.js +31 -4
- package/public/js/main.js +7 -1
- package/public/js/modal.js +3 -1
- package/public/js/panel.js +58 -0
- package/public/js/settings-tabs.js +1 -1
- package/public/js/settings.js +606 -275
- package/public/js/sse.js +3 -0
- package/public/js/state.js +1 -1
- package/public/js/text-editing.js +128 -0
- package/public/js/ticket-detail.js +377 -148
- package/public/js/ticket-typeahead.js +49 -0
- package/public/js/toast.js +19 -4
- package/public/js/topbar-notes.js +3 -3
- package/public/js/typeahead.js +206 -0
- package/public/js/update-check.js +58 -0
- package/public/styles.css +5455 -3538
- package/src/cli/orbit.js +9 -5
- package/src/core/agent.js +12 -4
- package/src/core/agents-md.js +3 -3
- package/src/core/artifacts.js +149 -0
- package/src/core/board.js +3 -2
- package/src/core/boards.js +0 -1
- package/src/core/css-bundle.js +1 -0
- package/src/core/db.js +6 -0
- package/src/core/dispatch.js +344 -34
- package/src/core/http.js +9 -2
- package/src/core/paths.js +26 -2
- package/src/core/router.js +33 -0
- package/src/core/search.js +3 -2
- package/src/core/snapshots.js +10 -5
- package/src/core/tickets.js +34 -3
- package/src/core/util.js +3 -2
- package/src/core/version-check.js +154 -0
- package/src/mcp/orbit-client.js +18 -2
- package/src/mcp-server.js +19 -14
- package/src/server.js +28 -3
- package/dist/full/js/app.fb7767dc.js +0 -742
- package/dist/full/js/app.fb7767dc.js.map +0 -7
- package/dist/full/styles.bbc542f5.css +0 -1
|
@@ -39,6 +39,12 @@ import { toast } from "./toast.js";
|
|
|
39
39
|
import { renderProjectDetail } from "./settings.js";
|
|
40
40
|
import { unreadCount, markRead } from "./unread.js";
|
|
41
41
|
import { formatActorLabel, formatCommentAuthor } from "./actor-labels.js";
|
|
42
|
+
import { openArtifactDrawer } from "./artifact-drawer.js";
|
|
43
|
+
import { closeIconSvg } from "./icons.js";
|
|
44
|
+
import { handleTextareaIndentationKeydown } from "./text-editing.js";
|
|
45
|
+
import { renderPanel, renderTogglePanel } from "./panel.js";
|
|
46
|
+
import { attachTypeahead } from "./typeahead.js";
|
|
47
|
+
import { attachTicketTypeahead } from "./ticket-typeahead.js";
|
|
42
48
|
|
|
43
49
|
/**
|
|
44
50
|
* Refresh the open drawer from the focused ticket context endpoint. When the
|
|
@@ -273,6 +279,7 @@ export function startInlineEdit(node, opts) {
|
|
|
273
279
|
|
|
274
280
|
editor.addEventListener("blur", commit);
|
|
275
281
|
editor.addEventListener("keydown", (event) => {
|
|
282
|
+
if (multiline && handleTextareaIndentationKeydown(event, editor)) return;
|
|
276
283
|
if (event.key === "Escape") {
|
|
277
284
|
event.preventDefault();
|
|
278
285
|
void cancel();
|
|
@@ -290,8 +297,27 @@ export function startInlineEdit(node, opts) {
|
|
|
290
297
|
});
|
|
291
298
|
}
|
|
292
299
|
|
|
300
|
+
function promptArtifactPath({ currentPath = "", label = "Artifact" } = {}) {
|
|
301
|
+
const next = window.prompt(
|
|
302
|
+
`Local markdown/text path for ${label} (example: docs/test.md or ./docs/test.md)`,
|
|
303
|
+
currentPath || "docs/",
|
|
304
|
+
);
|
|
305
|
+
if (next === null) return null;
|
|
306
|
+
return cleanText(next).trim();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function promptAndSaveArtifactPath({ ticketId, fieldName, currentPath, label }) {
|
|
310
|
+
if (!ticketId || !fieldName) return null;
|
|
311
|
+
const next = promptArtifactPath({ currentPath, label });
|
|
312
|
+
if (next === null) return null;
|
|
313
|
+
await patchTicket(ticketId, { [fieldName]: next });
|
|
314
|
+
await refreshTicketDetail(ticketId);
|
|
315
|
+
toast.success(next ? "Artifact path updated" : "Artifact path cleared");
|
|
316
|
+
return next;
|
|
317
|
+
}
|
|
318
|
+
|
|
293
319
|
/** Wire inline editors on the ticket detail header (meta fields, labels). */
|
|
294
|
-
function wireTicketDetailEditors(ticket) {
|
|
320
|
+
function wireTicketDetailEditors(ticket, context) {
|
|
295
321
|
const ticketId = ticket.id;
|
|
296
322
|
|
|
297
323
|
const editTitle = () =>
|
|
@@ -347,12 +373,12 @@ function wireTicketDetailEditors(ticket) {
|
|
|
347
373
|
// "implementation_updates" data-edit-field selectors are emitted by the
|
|
348
374
|
// detail body template below.
|
|
349
375
|
const AI_FIELDS = [
|
|
350
|
-
{ key: "ai_plan", label: "AI
|
|
351
|
-
{ key: "implementation_summary", label: "
|
|
376
|
+
{ key: "ai_plan", label: "AI Plan" },
|
|
377
|
+
{ key: "implementation_summary", label: "Work Summary" },
|
|
352
378
|
{
|
|
353
379
|
key: "implementation_updates",
|
|
354
|
-
label: "
|
|
355
|
-
}
|
|
380
|
+
label: "Revisions",
|
|
381
|
+
}
|
|
356
382
|
];
|
|
357
383
|
for (const { key, label } of AI_FIELDS) {
|
|
358
384
|
const fieldEl = drawerInner.querySelector(`[data-edit-field="${key}"]`);
|
|
@@ -434,6 +460,10 @@ function wireTicketDetailEditors(ticket) {
|
|
|
434
460
|
?.addEventListener("click", () => {
|
|
435
461
|
void addLabel();
|
|
436
462
|
});
|
|
463
|
+
// Attach the typeahead first so its Enter handler runs before the add-on-Enter
|
|
464
|
+
// handler below; when a suggestion is highlighted it stops propagation so the
|
|
465
|
+
// partial typed text isn't added instead.
|
|
466
|
+
attachLabelTypeahead(drawerInner.querySelector("[data-label-input]"), ticket);
|
|
437
467
|
drawerInner
|
|
438
468
|
.querySelector("[data-label-input]")
|
|
439
469
|
?.addEventListener("keydown", (event) => {
|
|
@@ -514,7 +544,7 @@ export async function renderDetail(options = {}) {
|
|
|
514
544
|
)
|
|
515
545
|
.join("");
|
|
516
546
|
|
|
517
|
-
const priorityOptions = [
|
|
547
|
+
const priorityOptions = [4, 3, 2, 1, 0]
|
|
518
548
|
.map(
|
|
519
549
|
(p) =>
|
|
520
550
|
`<option value="${p}" ${Number(ticket.priority) === p ? "selected" : ""}>${escapeHtml(priorityLabel(p))}</option>`,
|
|
@@ -526,19 +556,11 @@ export async function renderDetail(options = {}) {
|
|
|
526
556
|
(label) => `
|
|
527
557
|
<span class="label-pill-removable" style="--label-color: ${escapeHtml(label.color)}">
|
|
528
558
|
${escapeHtml(label.name)}
|
|
529
|
-
<button type="button" class="label-pill-remove" data-remove-label="${escapeHtml(label.name)}" title="Remove label"
|
|
559
|
+
<button type="button" class="label-pill-remove" data-remove-label="${escapeHtml(label.name)}" title="Remove label" aria-label="Remove label">${closeIconSvg}</button>
|
|
530
560
|
</span>`,
|
|
531
561
|
)
|
|
532
562
|
.join("");
|
|
533
563
|
|
|
534
|
-
const catalogNames = boardLabelCatalog()
|
|
535
|
-
.map((l) => l.name)
|
|
536
|
-
.filter((name) => !ticket.labels?.some((tl) => tl.name === name));
|
|
537
|
-
|
|
538
|
-
const datalistOptions = catalogNames
|
|
539
|
-
.map((name) => `<option value="${escapeHtml(name)}"></option>`)
|
|
540
|
-
.join("");
|
|
541
|
-
|
|
542
564
|
const detailCanonicalType = canonicalTicketType(ticket.type);
|
|
543
565
|
const priorityKey = priorityKeyFor(ticket.priority);
|
|
544
566
|
const currentState = states.find((s) => s.id === ticket.state_id);
|
|
@@ -547,8 +569,8 @@ export async function renderDetail(options = {}) {
|
|
|
547
569
|
? renderPreservedText(ticket.description)
|
|
548
570
|
: escapeHtml("No description yet.");
|
|
549
571
|
const descriptionClass = ticket.description
|
|
550
|
-
? "description preserved-text-body editable-field"
|
|
551
|
-
: "description editable-field is-placeholder";
|
|
572
|
+
? "description preserved-text-body editable-field detail-description-body"
|
|
573
|
+
: "description editable-field detail-description-body is-placeholder";
|
|
552
574
|
const detailSubtitleHtml = `
|
|
553
575
|
<span class="ticket-number">${escapeHtml(ticketLabel(ticket))}</span>
|
|
554
576
|
<div class="detail-meta-badge-row" aria-label="Ticket metadata controls">
|
|
@@ -574,80 +596,103 @@ export async function renderDetail(options = {}) {
|
|
|
574
596
|
<div class="detail-head">
|
|
575
597
|
<p class="detail-updated" title="Last saved change">Updated ${escapeHtml(formatDateDetail(ticket.updated_at))}</p>
|
|
576
598
|
<div class="${descriptionClass}" data-edit-field="description" tabindex="0" title="Click to edit">${descriptionHtml}</div>
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
599
|
+
</div>
|
|
600
|
+
|
|
601
|
+
<div class="settings-stack detail-section-stack">
|
|
602
|
+
${renderPanel({
|
|
603
|
+
title: "Labels",
|
|
604
|
+
body: `
|
|
605
|
+
<div class="settings-row">
|
|
581
606
|
<div class="label-pills-row" data-other-labels>${labelPills}</div>
|
|
582
607
|
<div class="label-add-row">
|
|
583
|
-
<
|
|
584
|
-
|
|
585
|
-
|
|
608
|
+
<div class="label-add-input-wrap">
|
|
609
|
+
<input type="text" class="label-add-input meta-inline-input" data-label-input autocomplete="off" placeholder="Add label…" aria-label="Add label" />
|
|
610
|
+
</div>
|
|
611
|
+
<button type="button" class="meta-add-btn detail-action-btn" data-variant="ghost" data-label-add>Add</button>
|
|
586
612
|
</div>
|
|
587
|
-
</
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
613
|
+
</div>
|
|
614
|
+
`,
|
|
615
|
+
})}
|
|
616
|
+
|
|
617
|
+
${renderHierarchySection(ticket, context)}
|
|
618
|
+
|
|
619
|
+
${features.attachments ? renderAttachmentSection(ticket, attachmentList.attachments || []) : ""}
|
|
620
|
+
|
|
621
|
+
${renderParentEpicSection(ticket, context)}
|
|
622
|
+
|
|
623
|
+
${renderPanel({
|
|
624
|
+
title: "Related",
|
|
625
|
+
body: `<div class="settings-row">${renderRelated(context.relations, ticket)}</div>`,
|
|
626
|
+
})}
|
|
627
|
+
|
|
628
|
+
${
|
|
629
|
+
currentBoard()?.ai_enabled !== 0
|
|
630
|
+
? renderTogglePanel({
|
|
631
|
+
title: "AI Plan / Implementation Record",
|
|
632
|
+
open: aiPlanOpen(ticket.id),
|
|
633
|
+
attrs: `data-ai-plan-toggle data-ticket-id="${escapeHtml(ticket.id)}"`,
|
|
634
|
+
body: `
|
|
635
|
+
<div class="ai-fields-grid">
|
|
636
|
+
${renderInlinePreservedTextField({
|
|
637
|
+
fieldName: "ai_plan",
|
|
638
|
+
label: "AI Plan",
|
|
639
|
+
value: ticket.ai_plan,
|
|
640
|
+
placeholder: "What to do: ",
|
|
641
|
+
ticketId: ticket.id,
|
|
642
|
+
artifactPath: ticket.plan_artifact_path,
|
|
643
|
+
artifactFieldName: "plan_artifact_path",
|
|
644
|
+
artifactLabel: "Open full handoff",
|
|
645
|
+
})}
|
|
646
|
+
${renderInlinePreservedTextField({
|
|
647
|
+
fieldName: "implementation_summary",
|
|
648
|
+
label: "Work Summary",
|
|
649
|
+
value: ticket.implementation_summary,
|
|
650
|
+
placeholder: "What was done: ",
|
|
651
|
+
ticketId: ticket.id,
|
|
652
|
+
artifactPath: ticket.implementation_artifact_path,
|
|
653
|
+
artifactFieldName: "implementation_artifact_path",
|
|
654
|
+
artifactLabel: "Open full report",
|
|
655
|
+
})}
|
|
656
|
+
${renderInlinePreservedTextField({
|
|
657
|
+
fieldName: "implementation_updates",
|
|
658
|
+
label: "Revisions",
|
|
659
|
+
value: ticket.implementation_updates,
|
|
660
|
+
placeholder: "Changes since initial implementation: ",
|
|
661
|
+
})}
|
|
662
|
+
</div>`,
|
|
663
|
+
})
|
|
664
|
+
: ""
|
|
665
|
+
}
|
|
636
666
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
667
|
+
${renderPanel({
|
|
668
|
+
title: "Status History",
|
|
669
|
+
body: `<div class="settings-row">${renderStatusHistory(statusHistory)}</div>`,
|
|
670
|
+
})}
|
|
671
|
+
|
|
672
|
+
${renderPanel({
|
|
673
|
+
title: "Comments",
|
|
674
|
+
body: `
|
|
675
|
+
<div class="settings-row">
|
|
676
|
+
${comments.map(renderComment).join("") || `<p class="description">No comments yet.</p>`}
|
|
677
|
+
<form class="comment-form" id="detailCommentForm">
|
|
678
|
+
<textarea name="body" placeholder="Add a comment, decision, or instruction..." required></textarea>
|
|
679
|
+
<button type="submit" class="detail-action-btn" data-variant="ghost">Comment</button>
|
|
680
|
+
</form>
|
|
681
|
+
</div>
|
|
682
|
+
`,
|
|
683
|
+
})}
|
|
644
684
|
</div>
|
|
645
685
|
`,
|
|
646
686
|
});
|
|
647
687
|
|
|
648
|
-
wireTicketDetailEditors(ticket);
|
|
688
|
+
wireTicketDetailEditors(ticket, context);
|
|
649
689
|
if (features.attachments) wireAttachmentControls(ticket);
|
|
650
690
|
|
|
691
|
+
const commentTextarea = $("#detailCommentForm textarea");
|
|
692
|
+
commentTextarea?.addEventListener("keydown", (event) => {
|
|
693
|
+
handleTextareaIndentationKeydown(event, commentTextarea);
|
|
694
|
+
});
|
|
695
|
+
|
|
651
696
|
$("#detailCommentForm").addEventListener("submit", async (event) => {
|
|
652
697
|
event.preventDefault();
|
|
653
698
|
const body = cleanText(
|
|
@@ -661,6 +706,20 @@ export async function renderDetail(options = {}) {
|
|
|
661
706
|
await refreshTicketDetail(ticket.id);
|
|
662
707
|
});
|
|
663
708
|
|
|
709
|
+
drawerInner.querySelectorAll("[data-comment-action='toggle-ai-omission']").forEach((button) => {
|
|
710
|
+
button.addEventListener("click", async () => {
|
|
711
|
+
const commentId = button.dataset.commentId;
|
|
712
|
+
if (!commentId) return;
|
|
713
|
+
const omittedForAi = button.dataset.omittedForAi !== "true";
|
|
714
|
+
await api(withBoardQuery(`/api/tickets/${ticket.id}/comments/${commentId}`), {
|
|
715
|
+
method: "PATCH",
|
|
716
|
+
body: { omitted_for_ai: omittedForAi },
|
|
717
|
+
});
|
|
718
|
+
toast.info(omittedForAi ? "Comment omitted from AI context" : "Comment included in AI context");
|
|
719
|
+
await refreshTicketDetail(ticket.id);
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
664
723
|
drawerInner.querySelectorAll("[data-open-ticket]").forEach((card) => {
|
|
665
724
|
card.addEventListener("click", async (event) => {
|
|
666
725
|
if (
|
|
@@ -738,12 +797,45 @@ export async function renderDetail(options = {}) {
|
|
|
738
797
|
});
|
|
739
798
|
}
|
|
740
799
|
|
|
800
|
+
drawerInner.querySelectorAll("[data-open-artifact]").forEach((btn) => {
|
|
801
|
+
btn.addEventListener("click", async (event) => {
|
|
802
|
+
event.stopPropagation();
|
|
803
|
+
const artifactFieldName = btn.dataset.artifactField;
|
|
804
|
+
const artifactTitle = btn.dataset.artifactTitle || "Linked artifact";
|
|
805
|
+
const artifactTicketId = btn.dataset.artifactTicketId || ticket.id;
|
|
806
|
+
const currentPath = String(btn.dataset.openArtifact || "").trim();
|
|
807
|
+
if (!currentPath) {
|
|
808
|
+
await promptAndSaveArtifactPath({
|
|
809
|
+
ticketId: artifactTicketId,
|
|
810
|
+
fieldName: artifactFieldName,
|
|
811
|
+
currentPath,
|
|
812
|
+
label: artifactTitle,
|
|
813
|
+
});
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const opened = await openArtifactDrawer({
|
|
817
|
+
ticketId: artifactTicketId,
|
|
818
|
+
path: currentPath,
|
|
819
|
+
title: artifactTitle,
|
|
820
|
+
});
|
|
821
|
+
if (opened === false && artifactFieldName) {
|
|
822
|
+
await promptAndSaveArtifactPath({
|
|
823
|
+
ticketId: artifactTicketId,
|
|
824
|
+
fieldName: artifactFieldName,
|
|
825
|
+
currentPath,
|
|
826
|
+
label: artifactTitle,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
|
|
741
832
|
const parentEpicForm = drawerInner.querySelector("#parentEpicForm");
|
|
742
833
|
if (parentEpicForm) {
|
|
743
834
|
const parentInput = parentEpicForm.querySelector("[name=target]");
|
|
744
835
|
const parentClear = parentEpicForm.querySelector(
|
|
745
836
|
"[data-parent-epic-clear]",
|
|
746
837
|
);
|
|
838
|
+
attachTicketTypeahead(parentInput, () => epicCandidates(ticket));
|
|
747
839
|
const syncParentClear = () => {
|
|
748
840
|
parentEpicForm.classList.toggle(
|
|
749
841
|
"has-value",
|
|
@@ -791,6 +883,9 @@ export async function renderDetail(options = {}) {
|
|
|
791
883
|
if (relatedAddForm) {
|
|
792
884
|
const relatedInput = relatedAddForm.querySelector("[name=target]");
|
|
793
885
|
const relatedClear = relatedAddForm.querySelector("[data-related-clear]");
|
|
886
|
+
attachTicketTypeahead(relatedInput, () =>
|
|
887
|
+
relatedTicketCandidates(context.relations, ticket),
|
|
888
|
+
);
|
|
794
889
|
const syncRelatedClear = () => {
|
|
795
890
|
relatedAddForm.classList.toggle(
|
|
796
891
|
"has-value",
|
|
@@ -832,6 +927,53 @@ export async function renderDetail(options = {}) {
|
|
|
832
927
|
});
|
|
833
928
|
}
|
|
834
929
|
|
|
930
|
+
// Reverse of "Set parent epic": from an epic, pull a feature/task in as a
|
|
931
|
+
// child by patching that ticket's parent_ticket_id to this epic.
|
|
932
|
+
const epicChildForm = drawerInner.querySelector("#epicChildForm");
|
|
933
|
+
if (epicChildForm) {
|
|
934
|
+
const childInput = epicChildForm.querySelector("[name=target]");
|
|
935
|
+
const childClear = epicChildForm.querySelector("[data-epic-child-clear]");
|
|
936
|
+
const syncChildClear = () => {
|
|
937
|
+
epicChildForm.classList.toggle("has-value", childInput.value.length > 0);
|
|
938
|
+
};
|
|
939
|
+
childInput.addEventListener("input", syncChildClear);
|
|
940
|
+
syncChildClear();
|
|
941
|
+
childClear.addEventListener("click", () => {
|
|
942
|
+
childInput.value = "";
|
|
943
|
+
syncChildClear();
|
|
944
|
+
childInput.focus();
|
|
945
|
+
});
|
|
946
|
+
attachTicketTypeahead(childInput, () => childCandidates(ticket));
|
|
947
|
+
epicChildForm.addEventListener("submit", async (event) => {
|
|
948
|
+
event.preventDefault();
|
|
949
|
+
const targetLabel = String(
|
|
950
|
+
new FormData(event.currentTarget).get("target") || "",
|
|
951
|
+
).trim();
|
|
952
|
+
if (!targetLabel) return;
|
|
953
|
+
const targetId = resolveTicketIdFromLabel(targetLabel, ticket.id);
|
|
954
|
+
if (!targetId) {
|
|
955
|
+
toast.warning("Pick a ticket from the suggestions");
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const target = ticketsForProject().find((t) => t.id === targetId);
|
|
959
|
+
if (!target || canonicalTicketType(target.type) === "epic") {
|
|
960
|
+
toast.warning("Epics can't be nested inside an epic");
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
try {
|
|
964
|
+
await api(withBoardQuery(`/api/tickets/${targetId}`), {
|
|
965
|
+
method: "PATCH",
|
|
966
|
+
body: { parent_ticket_id: ticket.id },
|
|
967
|
+
});
|
|
968
|
+
} catch (err) {
|
|
969
|
+
toast.error(err.message || "Failed to add feature");
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
await refreshTicketDetail(ticket.id, { renderBoardAfter: true });
|
|
973
|
+
toast.success("Feature added to epic");
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
835
977
|
openDrawer();
|
|
836
978
|
}
|
|
837
979
|
|
|
@@ -842,23 +984,25 @@ function aiPlanOpen(ticketId) {
|
|
|
842
984
|
function renderAttachmentSection(ticket, attachments = []) {
|
|
843
985
|
const cards = attachments.length
|
|
844
986
|
? `<div class="attachment-grid">${attachments.map((attachment) => renderAttachmentCard(ticket, attachment)).join("")}</div>`
|
|
845
|
-
: `<p class="description">No images attached yet.</p>`;
|
|
846
|
-
return
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
987
|
+
: `<p class="description attachment-empty-state">No images attached yet.</p>`;
|
|
988
|
+
return renderPanel({
|
|
989
|
+
title: "Images",
|
|
990
|
+
className: "attachments-panel",
|
|
991
|
+
attrs: `data-attachment-section data-ticket-id="${escapeHtml(ticket.id)}"`,
|
|
992
|
+
body: `
|
|
993
|
+
<div class="settings-row">
|
|
994
|
+
<div class="attachment-dropzone" data-attachment-dropzone tabindex="0">
|
|
995
|
+
<strong>Paste, drag, or upload images</strong>
|
|
996
|
+
<span>PNG, JPG, GIF, WebP, SVG, BMP, or AVIF up to 10 MB each.</span>
|
|
997
|
+
</div>
|
|
998
|
+
${cards}
|
|
999
|
+
<label class="attachment-upload-button detail-action-btn" role="button" data-variant="ghost" tabindex="0">
|
|
851
1000
|
Upload
|
|
852
1001
|
<input type="file" accept="image/*" multiple data-attachment-input />
|
|
853
1002
|
</label>
|
|
854
1003
|
</div>
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
<span>PNG, JPG, GIF, WebP, SVG, BMP, or AVIF up to 10 MB each.</span>
|
|
858
|
-
</div>
|
|
859
|
-
${cards}
|
|
860
|
-
</div>
|
|
861
|
-
`;
|
|
1004
|
+
`,
|
|
1005
|
+
});
|
|
862
1006
|
}
|
|
863
1007
|
|
|
864
1008
|
function renderAttachmentCard(ticket, attachment) {
|
|
@@ -869,7 +1013,7 @@ function renderAttachmentCard(ticket, attachment) {
|
|
|
869
1013
|
<article class="attachment-card is-missing">
|
|
870
1014
|
<div class="attachment-missing-thumb">Missing</div>
|
|
871
1015
|
<div class="attachment-card-meta"><strong>${name}</strong><span>${meta}</span></div>
|
|
872
|
-
<button type="button" class="attachment-delete" data-delete-attachment="${escapeHtml(attachment.id)}" aria-label="Remove missing image"
|
|
1016
|
+
<button type="button" class="attachment-delete" data-delete-attachment="${escapeHtml(attachment.id)}" aria-label="Remove missing image">${closeIconSvg}</button>
|
|
873
1017
|
</article>
|
|
874
1018
|
`;
|
|
875
1019
|
}
|
|
@@ -879,11 +1023,11 @@ function renderAttachmentCard(ticket, attachment) {
|
|
|
879
1023
|
);
|
|
880
1024
|
return `
|
|
881
1025
|
<article class="attachment-card">
|
|
882
|
-
<button type="button" class="attachment-thumb-button" data-open-lightbox="${escapeHtml(src)}" data-lightbox-title="${name}">
|
|
1026
|
+
<button type="button" class="attachment-thumb-button" data-no-arc data-open-lightbox="${escapeHtml(src)}" data-lightbox-title="${name}">
|
|
883
1027
|
<img src="${escapeHtml(src)}" alt="${name}" loading="lazy" />
|
|
884
1028
|
</button>
|
|
885
1029
|
<div class="attachment-card-meta"><strong>${name}</strong><span>${meta}</span></div>
|
|
886
|
-
<button type="button" class="attachment-delete" data-delete-attachment="${escapeHtml(attachment.id)}" aria-label="Remove image"
|
|
1030
|
+
<button type="button" class="attachment-delete" data-delete-attachment="${escapeHtml(attachment.id)}" aria-label="Remove image">${closeIconSvg}</button>
|
|
887
1031
|
</article>
|
|
888
1032
|
`;
|
|
889
1033
|
}
|
|
@@ -1015,7 +1159,7 @@ function openAttachmentLightbox(src, title) {
|
|
|
1015
1159
|
overlay.className = "attachment-lightbox";
|
|
1016
1160
|
overlay.innerHTML = `
|
|
1017
1161
|
<div class="attachment-lightbox-panel" role="dialog" aria-modal="true" aria-label="${escapeHtml(title)}">
|
|
1018
|
-
<button type="button" class="attachment-lightbox-close" aria-label="Close image"
|
|
1162
|
+
<button type="button" class="attachment-lightbox-close" aria-label="Close image">${closeIconSvg}</button>
|
|
1019
1163
|
<img src="${escapeHtml(src)}" alt="${escapeHtml(title)}" />
|
|
1020
1164
|
<p>${escapeHtml(title)}</p>
|
|
1021
1165
|
</div>
|
|
@@ -1078,13 +1222,36 @@ function renderStatusHistory(history) {
|
|
|
1078
1222
|
}
|
|
1079
1223
|
|
|
1080
1224
|
function renderComment(comment) {
|
|
1225
|
+
const aiEnabled = currentBoard()?.ai_enabled !== 0;
|
|
1226
|
+
const omittedForAi = Boolean(comment.omitted_for_ai);
|
|
1227
|
+
const kindLabel =
|
|
1228
|
+
comment.kind === "human_comment" ? "" : escapeHtml(comment.kind.replace(/_/g, " "));
|
|
1081
1229
|
return `
|
|
1082
|
-
<div class="comment ${comment.kind === "checkpoint" ? "checkpoint" : ""}">
|
|
1083
|
-
<div class="comment-
|
|
1084
|
-
<
|
|
1085
|
-
|
|
1230
|
+
<div class="comment ${comment.kind === "checkpoint" ? "checkpoint" : ""} ${aiEnabled && omittedForAi ? "is-ai-omitted" : ""}">
|
|
1231
|
+
<div class="comment-head">
|
|
1232
|
+
<div class="comment-head-main">
|
|
1233
|
+
<div class="comment-head-title">
|
|
1234
|
+
<strong>${escapeHtml(formatCommentAuthor(comment))}</strong>
|
|
1235
|
+
${kindLabel ? `<span class="comment-kind-pill">${kindLabel}</span>` : ""}
|
|
1236
|
+
</div>
|
|
1237
|
+
</div>
|
|
1086
1238
|
</div>
|
|
1087
1239
|
<div class="comment-body preserved-text-body">${renderPreservedText(comment.body)}</div>
|
|
1240
|
+
<div class="comment-footer">
|
|
1241
|
+
<span class="comment-footer-meta">${formatDate(comment.created_at)}</span>
|
|
1242
|
+
${
|
|
1243
|
+
aiEnabled
|
|
1244
|
+
? `<button
|
|
1245
|
+
type="button"
|
|
1246
|
+
class="comment-ai-toggle detail-action-btn"
|
|
1247
|
+
data-variant="ghost"
|
|
1248
|
+
data-comment-action="toggle-ai-omission"
|
|
1249
|
+
data-comment-id="${escapeHtml(comment.id)}"
|
|
1250
|
+
data-omitted-for-ai="${omittedForAi ? "true" : "false"}"
|
|
1251
|
+
>${omittedForAi ? "AI Include" : "AI Ignore"}</button>`
|
|
1252
|
+
: ""
|
|
1253
|
+
}
|
|
1254
|
+
</div>
|
|
1088
1255
|
</div>
|
|
1089
1256
|
`;
|
|
1090
1257
|
}
|
|
@@ -1100,16 +1267,36 @@ export function renderInlinePreservedTextField({
|
|
|
1100
1267
|
label,
|
|
1101
1268
|
value,
|
|
1102
1269
|
placeholder,
|
|
1270
|
+
ticketId,
|
|
1271
|
+
artifactPath,
|
|
1272
|
+
artifactFieldName,
|
|
1273
|
+
artifactLabel,
|
|
1103
1274
|
}) {
|
|
1104
1275
|
const text = value || "";
|
|
1105
1276
|
const hasValue = Boolean(text.trim());
|
|
1277
|
+
const artifactPathText = String(artifactPath || "").trim();
|
|
1278
|
+
const hasArtifactControl = Boolean(artifactLabel || artifactFieldName);
|
|
1106
1279
|
const inner = hasValue
|
|
1107
1280
|
? renderPreservedText(text)
|
|
1108
1281
|
: escapeHtml(placeholder || "");
|
|
1109
1282
|
const placeholderClass = hasValue ? "" : "is-placeholder";
|
|
1283
|
+
const artifactButton = hasArtifactControl
|
|
1284
|
+
? `<button
|
|
1285
|
+
type="button"
|
|
1286
|
+
class="detail-action-btn"
|
|
1287
|
+
data-variant="ghost"
|
|
1288
|
+
data-open-artifact="${escapeHtml(artifactPathText)}"
|
|
1289
|
+
data-artifact-field="${escapeHtml(artifactFieldName || "")}"
|
|
1290
|
+
data-artifact-ticket-id="${escapeHtml(ticketId || "")}"
|
|
1291
|
+
data-artifact-title="${escapeHtml(label)}"
|
|
1292
|
+
>${escapeHtml(artifactLabel || "Open full artifact")}</button>`
|
|
1293
|
+
: "";
|
|
1110
1294
|
return `
|
|
1111
1295
|
<div class="inline-md-field">
|
|
1112
|
-
<
|
|
1296
|
+
<div class="inline-md-field-head">
|
|
1297
|
+
<span class="inline-md-field-label">${escapeHtml(label)}</span>
|
|
1298
|
+
${artifactButton}
|
|
1299
|
+
</div>
|
|
1113
1300
|
<div
|
|
1114
1301
|
class="inline-md-field-body preserved-text-body editable-field ${placeholderClass}"
|
|
1115
1302
|
data-edit-field="${escapeHtml(fieldName)}"
|
|
@@ -1122,7 +1309,9 @@ export function renderInlinePreservedTextField({
|
|
|
1122
1309
|
`;
|
|
1123
1310
|
}
|
|
1124
1311
|
|
|
1125
|
-
|
|
1312
|
+
// Tickets eligible to be linked as "related": everything in the project except
|
|
1313
|
+
// the ticket itself, its existing relations, and its parent/child hierarchy.
|
|
1314
|
+
function relatedTicketCandidates(relations = [], ticket) {
|
|
1126
1315
|
const relatedIds = new Set(relations.map((r) => r.other_ticket.id));
|
|
1127
1316
|
const hierarchyIds = new Set();
|
|
1128
1317
|
if (ticket?.parent_ticket_id) hierarchyIds.add(ticket.parent_ticket_id);
|
|
@@ -1131,16 +1320,13 @@ function renderRelated(relations = [], ticket) {
|
|
|
1131
1320
|
if (t.parent_ticket_id === ticket.id) hierarchyIds.add(t.id);
|
|
1132
1321
|
}
|
|
1133
1322
|
}
|
|
1134
|
-
|
|
1323
|
+
return ticketsForProject().filter(
|
|
1135
1324
|
(t) =>
|
|
1136
1325
|
t.id !== ticket?.id && !relatedIds.has(t.id) && !hierarchyIds.has(t.id),
|
|
1137
1326
|
);
|
|
1138
|
-
|
|
1139
|
-
.map(
|
|
1140
|
-
(t) => `<option value="#${t.number} — ${escapeHtml(t.title)}"></option>`,
|
|
1141
|
-
)
|
|
1142
|
-
.join("");
|
|
1327
|
+
}
|
|
1143
1328
|
|
|
1329
|
+
function renderRelated(relations = [], ticket) {
|
|
1144
1330
|
const list = relations.length
|
|
1145
1331
|
? `<div class="detail-card-grid">${relations.map((r) => renderRelatedCard(r)).join("")}</div>`
|
|
1146
1332
|
: "";
|
|
@@ -1148,16 +1334,15 @@ function renderRelated(relations = [], ticket) {
|
|
|
1148
1334
|
const form = `
|
|
1149
1335
|
<form id="relatedAddForm" class="related-add-row">
|
|
1150
1336
|
<div class="related-add-input-wrap">
|
|
1151
|
-
<input type="text" name="target" class="related-add-input meta-inline-input"
|
|
1152
|
-
<button type="button" class="related-add-clear" data-related-clear aria-label="Clear" tabindex="-1"
|
|
1153
|
-
<datalist id="related-ticket-suggestions">${datalistOptions}</datalist>
|
|
1337
|
+
<input type="text" name="target" class="related-add-input meta-inline-input" placeholder="Add related ticket…" aria-label="Add related ticket" autocomplete="off" />
|
|
1338
|
+
<button type="button" class="related-add-clear" data-related-clear aria-label="Clear" tabindex="-1">${closeIconSvg}</button>
|
|
1154
1339
|
</div>
|
|
1155
1340
|
<select name="type" class="related-add-type meta-inline" aria-label="Relation type">
|
|
1156
1341
|
<option value="relates_to">relates to</option>
|
|
1157
1342
|
<option value="blocks">blocks</option>
|
|
1158
1343
|
<option value="blocked_by">blocked by</option>
|
|
1159
1344
|
</select>
|
|
1160
|
-
<button type="submit" class="
|
|
1345
|
+
<button type="submit" class="meta-add-btn detail-action-btn" data-variant="ghost">Add</button>
|
|
1161
1346
|
</form>
|
|
1162
1347
|
`;
|
|
1163
1348
|
|
|
@@ -1166,52 +1351,96 @@ function renderRelated(relations = [], ticket) {
|
|
|
1166
1351
|
|
|
1167
1352
|
function renderHierarchySection(ticket, context) {
|
|
1168
1353
|
const isEpic = canonicalTicketType(ticket.type) === "epic";
|
|
1169
|
-
if (isEpic)
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1354
|
+
if (!isEpic) return "";
|
|
1355
|
+
|
|
1356
|
+
const cards = context.child_tickets || [];
|
|
1357
|
+
const grid = cards.length
|
|
1358
|
+
? `<div class="detail-card-grid">${cards.map((c) => renderDetailCard(c, { detach: { mode: "child", id: c.id } })).join("")}</div>`
|
|
1359
|
+
: "";
|
|
1360
|
+
|
|
1361
|
+
const form = `
|
|
1362
|
+
<form id="epicChildForm" class="related-add-row epic-child-form">
|
|
1363
|
+
<div class="related-add-input-wrap">
|
|
1364
|
+
<input type="text" name="target" class="related-add-input meta-inline-input" placeholder="Add feature to epic…" aria-label="Add feature to epic" autocomplete="off" />
|
|
1365
|
+
<button type="button" class="related-add-clear" data-epic-child-clear aria-label="Clear" tabindex="-1">${closeIconSvg}</button>
|
|
1178
1366
|
</div>
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1367
|
+
<button type="submit" class="meta-add-btn detail-action-btn" data-variant="ghost">Add</button>
|
|
1368
|
+
</form>
|
|
1369
|
+
`;
|
|
1183
1370
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1371
|
+
return renderPanel({
|
|
1372
|
+
title: "Features inside this Epic",
|
|
1373
|
+
body: `<div class="settings-row">${grid}${form}</div>`,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1186
1376
|
|
|
1187
|
-
|
|
1377
|
+
// Epics eligible to become this ticket's parent (any epic but itself).
|
|
1378
|
+
function epicCandidates(ticket) {
|
|
1379
|
+
return ticketsForProject().filter(
|
|
1188
1380
|
(t) => t.type === "epic" && t.id !== ticket.id,
|
|
1189
1381
|
);
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Tickets eligible to be pulled into this epic as children: any non-epic ticket
|
|
1385
|
+
// not already parented to it. (A ticket with another parent is reparented.)
|
|
1386
|
+
function childCandidates(epic) {
|
|
1387
|
+
return ticketsForProject().filter(
|
|
1388
|
+
(t) =>
|
|
1389
|
+
t.id !== epic.id &&
|
|
1390
|
+
canonicalTicketType(t.type) !== "epic" &&
|
|
1391
|
+
t.parent_ticket_id !== epic.id,
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
// Board labels not yet applied to this ticket, as {name, color} items.
|
|
1397
|
+
function labelCandidates(ticket) {
|
|
1398
|
+
const applied = new Set((ticket.labels || []).map((l) => l.name));
|
|
1399
|
+
return boardLabelCatalog().filter((l) => !applied.has(l.name));
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// A typeahead row for a label: colour swatch + bold name. Mirrors the topbar
|
|
1403
|
+
// hit layout (bold lead + trailing badge), adapted for a label's lack of a
|
|
1404
|
+
// number / state.
|
|
1405
|
+
function renderLabelHitRow(label) {
|
|
1406
|
+
return `
|
|
1407
|
+
<span class="search-hit-main">
|
|
1408
|
+
<span class="typeahead-swatch" style="--label-color: ${escapeHtml(label.color || "var(--muted)")}"></span>
|
|
1409
|
+
<strong>${escapeHtml(label.name)}</strong>
|
|
1410
|
+
</span>
|
|
1411
|
+
`;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function attachLabelTypeahead(input, ticket) {
|
|
1415
|
+
if (!input) return;
|
|
1416
|
+
attachTypeahead(input, {
|
|
1417
|
+
getItems: () => labelCandidates(ticket),
|
|
1418
|
+
match: (label, query) => label.name.toLowerCase().includes(query),
|
|
1419
|
+
renderItem: renderLabelHitRow,
|
|
1420
|
+
valueOf: (label) => label.name,
|
|
1421
|
+
emptyText: "No matching labels",
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function renderParentEpicSection(ticket, context) {
|
|
1426
|
+
if (canonicalTicketType(ticket.type) === "epic") return "";
|
|
1195
1427
|
|
|
1196
1428
|
const body = context.parent_ticket
|
|
1197
1429
|
? `<div class="detail-card-grid detail-card-grid--single">${renderDetailCard(context.parent_ticket, { detach: { mode: "parent" } })}</div>`
|
|
1198
1430
|
: `
|
|
1199
|
-
<form id="parentEpicForm" class="related-add-row">
|
|
1431
|
+
<form id="parentEpicForm" class="related-add-row parent-epic-form">
|
|
1200
1432
|
<div class="related-add-input-wrap">
|
|
1201
|
-
<input type="text" name="target" class="related-add-input meta-inline-input"
|
|
1202
|
-
<button type="button" class="related-add-clear" data-parent-epic-clear aria-label="Clear" tabindex="-1"
|
|
1203
|
-
<datalist id="parent-epic-suggestions">${datalistOptions}</datalist>
|
|
1433
|
+
<input type="text" name="target" class="related-add-input meta-inline-input" placeholder="Set parent epic…" aria-label="Set parent epic" autocomplete="off" />
|
|
1434
|
+
<button type="button" class="related-add-clear" data-parent-epic-clear aria-label="Clear" tabindex="-1">${closeIconSvg}</button>
|
|
1204
1435
|
</div>
|
|
1205
|
-
<button type="submit" class="
|
|
1436
|
+
<button type="submit" class="meta-add-btn detail-action-btn" data-variant="ghost">Set</button>
|
|
1206
1437
|
</form>
|
|
1207
1438
|
`;
|
|
1208
1439
|
|
|
1209
|
-
return
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
</div>
|
|
1214
|
-
`;
|
|
1440
|
+
return renderPanel({
|
|
1441
|
+
title: "Epic",
|
|
1442
|
+
body: `<div class="settings-row">${body}</div>`,
|
|
1443
|
+
});
|
|
1215
1444
|
}
|
|
1216
1445
|
|
|
1217
1446
|
export function renderDetailCard(ticket, options = {}) {
|
|
@@ -1237,7 +1466,7 @@ export function renderDetailCard(ticket, options = {}) {
|
|
|
1237
1466
|
attr = `data-detach-relation="${escapeHtml(id)}"`;
|
|
1238
1467
|
label = "Remove relation";
|
|
1239
1468
|
}
|
|
1240
|
-
detachBtn = `<button type="button" class="card-detach-btn" ${attr} aria-label="${label}" title="${label}"
|
|
1469
|
+
detachBtn = `<button type="button" class="card-detach-btn" ${attr} aria-label="${label}" title="${label}">${closeIconSvg}</button>`;
|
|
1241
1470
|
} else if (options.archive) {
|
|
1242
1471
|
const tid = escapeHtml(options.archive.id);
|
|
1243
1472
|
const restoreSvg = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>`;
|
|
@@ -1259,7 +1488,6 @@ export function renderDetailCard(ticket, options = {}) {
|
|
|
1259
1488
|
return `
|
|
1260
1489
|
<article class="card card--detail type-${escapeHtml(canonicalType)} priority-${escapeHtml(priorityKey)}" data-variant="${escapeHtml(canonicalType)}"${options.disableOpen ? "" : ` data-open-ticket="${escapeHtml(ticket.id)}"`}>
|
|
1261
1490
|
${unreadDot}
|
|
1262
|
-
${detachBtn}
|
|
1263
1491
|
${eyebrow}
|
|
1264
1492
|
<h3>${escapeHtml(ticket.title)}</h3>
|
|
1265
1493
|
<div class="card-meta">
|
|
@@ -1271,6 +1499,7 @@ export function renderDetailCard(ticket, options = {}) {
|
|
|
1271
1499
|
</div>
|
|
1272
1500
|
<span class="detail-card-state" data-variant="${escapeHtml(stateClass)}">${escapeHtml(ticket.state_name || "State")}</span>
|
|
1273
1501
|
${state.showPriority ? renderPriorityPill(ticket.priority) : ""}
|
|
1502
|
+
${detachBtn}
|
|
1274
1503
|
</div>
|
|
1275
1504
|
</div>
|
|
1276
1505
|
</article>
|