@starlove/orbit 1.1.1 → 1.1.11
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/.dockerignore +0 -0
- package/Dockerfile +0 -0
- package/LICENSE +0 -0
- package/README.md +2 -2
- package/SKILL-ORBIT.md +2 -0
- package/build.mjs +0 -0
- package/dist/full/apple-touch-icon.png +0 -0
- package/dist/full/cursors/live_arrow.cur +0 -0
- package/dist/full/cursors/live_link.cur +0 -0
- package/dist/full/cursors/live_move.cur +0 -0
- package/dist/full/cursors/live_select.cur +0 -0
- package/dist/full/cursors/live_unavail.cur +0 -0
- package/dist/full/favicon-32x32.png +0 -0
- package/dist/full/index.html +160 -15
- package/dist/full/js/app.55b38e12.js +698 -0
- package/dist/full/js/app.55b38e12.js.map +7 -0
- package/dist/full/orbits-favicon.png +0 -0
- package/dist/full/orbits-logo-bg.png +0 -0
- package/dist/full/orbits-logo.png +0 -0
- package/dist/full/{styles.836872e7.css → styles.273fc37a.css} +79 -39
- package/dist/full/version.json +2 -2
- package/docs/AGENT_PROTOCOL.md +22 -6
- package/docs/MCP_SETUP.md +0 -0
- package/docs/ORBIT_DISPATCH.md +68 -8
- package/docs/SS-Orbit-Dark.png +0 -0
- package/docs/SS-Orbit-Light.png +0 -0
- package/docs/mcp-client.example.json +0 -0
- package/docs/setup/orbit-init-wizard.md +0 -0
- package/package.json +1 -1
- package/public/apple-touch-icon.png +0 -0
- package/public/cursors/live_arrow.cur +0 -0
- package/public/cursors/live_link.cur +0 -0
- package/public/cursors/live_move.cur +0 -0
- package/public/cursors/live_select.cur +0 -0
- package/public/cursors/live_unavail.cur +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/index.html +158 -13
- package/public/js/actor-labels.js +0 -0
- package/public/js/api.js +0 -0
- package/public/js/app.js +0 -0
- package/public/js/board-menu.js +0 -0
- package/public/js/card-actions.js +19 -0
- package/public/js/config.js +0 -0
- package/public/js/create-card.js +0 -0
- package/public/js/dom.js +0 -0
- package/public/js/drawer.js +17 -43
- package/public/js/format.js +5 -5
- package/public/js/kanban.js +157 -92
- package/public/js/local-backend.js +15 -4
- package/public/js/main.js +2 -1
- package/public/js/modal.js +0 -0
- package/public/js/motion-preference.js +14 -3
- package/public/js/router.js +0 -0
- package/public/js/search.js +0 -0
- package/public/js/settings-tabs.js +0 -0
- package/public/js/settings.js +131 -27
- package/public/js/sse.js +0 -0
- package/public/js/starscape.js +0 -0
- package/public/js/state.js +0 -0
- package/public/js/theme-preference.js +0 -0
- package/public/js/ticket-detail.js +88 -20
- package/public/js/toast.js +0 -0
- package/public/js/transport.js +0 -0
- package/public/js/unread.js +0 -0
- package/public/js/url-routes.js +0 -0
- package/public/orbits-favicon.png +0 -0
- package/public/orbits-logo-bg.png +0 -0
- package/public/orbits-logo.png +0 -0
- package/public/styles.css +79 -39
- package/src/cli/orbit.js +22 -0
- package/src/core/agent.js +25 -4
- package/src/core/agents-md.js +0 -0
- package/src/core/attachments.js +0 -0
- package/src/core/auth.js +0 -0
- package/src/core/backups.js +0 -0
- package/src/core/board.js +2 -1
- package/src/core/boards.js +0 -0
- package/src/core/bootstrap.js +0 -0
- package/src/core/db.js +19 -0
- package/src/core/delete-board.js +0 -0
- package/src/core/dispatch.js +178 -13
- package/src/core/events.js +0 -0
- package/src/core/http.js +0 -0
- package/src/core/migrate.js +0 -0
- package/src/core/paths.js +0 -0
- package/src/core/provision-repo-board.js +0 -0
- package/src/core/queries.js +0 -0
- package/src/core/registry.js +2 -2
- package/src/core/relations.js +0 -0
- package/src/core/review-verdicts.js +138 -0
- package/src/core/router.js +38 -3
- package/src/core/search.js +0 -0
- package/src/core/seed.js +0 -0
- package/src/core/snapshots.js +429 -9
- package/src/core/states.js +0 -0
- package/src/core/system-picker.js +0 -0
- package/src/core/tickets.js +0 -0
- package/src/core/util.js +0 -0
- package/src/mcp/orbit-client.js +47 -19
- package/src/mcp-server.js +122 -26
- package/src/server.js +0 -0
- package/dist/full/js/app.a4f045fc.js +0 -676
- package/dist/full/js/app.a4f045fc.js.map +0 -7
package/public/js/settings.js
CHANGED
|
@@ -165,8 +165,12 @@ function renderProjectAiTab(context) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
function renderAgentContextSection(project) {
|
|
168
|
-
const
|
|
168
|
+
const instructionsPlaceholder =
|
|
169
169
|
"This project is about…\n\nHow any AI agent should use this board and work in this repo.";
|
|
170
|
+
const instructions = project.agent_instructions || "";
|
|
171
|
+
const hasInstructions = Boolean(instructions.trim());
|
|
172
|
+
const placeholderClass = hasInstructions ? "" : "is-placeholder";
|
|
173
|
+
const inner = hasInstructions ? renderPreservedText(instructions) : escapeHtml(instructionsPlaceholder);
|
|
170
174
|
return `
|
|
171
175
|
<div class="section ai-subsection ai-context-setup">
|
|
172
176
|
<div class="ai-context-title-row">
|
|
@@ -174,10 +178,16 @@ function renderAgentContextSection(project) {
|
|
|
174
178
|
<h3>Agent Instructions</h3>
|
|
175
179
|
</div>
|
|
176
180
|
<p class="description">This project-level context is provided to agents when they work on tickets.</p>
|
|
177
|
-
<
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
<div class="inline-md-field">
|
|
182
|
+
<div
|
|
183
|
+
class="inline-md-field-body preserved-text-body editable-field settings-agent-instructions-body ${placeholderClass}"
|
|
184
|
+
data-edit-field="agent_instructions"
|
|
185
|
+
tabindex="0"
|
|
186
|
+
title="Click to edit"
|
|
187
|
+
role="button"
|
|
188
|
+
aria-label="Edit Agent Instructions"
|
|
189
|
+
>${inner}</div>
|
|
190
|
+
</div>
|
|
181
191
|
</div>
|
|
182
192
|
`;
|
|
183
193
|
}
|
|
@@ -381,6 +391,7 @@ function renderProjectRepositoryTab(context) {
|
|
|
381
391
|
|
|
382
392
|
<div class="section">
|
|
383
393
|
<h3>Board Snapshot</h3>
|
|
394
|
+
<p class="description">Import accepts Orbit snapshots or Trello board JSON exports. The default import creates a separate board and leaves this board untouched.</p>
|
|
384
395
|
${features.attachments ? `
|
|
385
396
|
<label class="orbit-check">
|
|
386
397
|
<input type="checkbox" id="exportProjectImages" />
|
|
@@ -394,8 +405,23 @@ function renderProjectRepositoryTab(context) {
|
|
|
394
405
|
<div class="deployment-actions">
|
|
395
406
|
<button type="button" class="ghost" id="exportProject">Export</button>
|
|
396
407
|
<label class="import-button">
|
|
397
|
-
<input type="file" id="
|
|
398
|
-
<span>Import</span>
|
|
408
|
+
<input type="file" id="importSnapshotAsNewFile" accept=".orbit.json,.json,application/json" />
|
|
409
|
+
<span>Import as New Board</span>
|
|
410
|
+
</label>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<div class="section repository-danger-zone">
|
|
415
|
+
<h3>Replace Current Board</h3>
|
|
416
|
+
<p class="description">Advanced: replace this board from an Orbit snapshot or Trello export after making the normal pre-import backup. Type <strong>${escapeHtml(project.slug || "")}</strong> to enable replacement.</p>
|
|
417
|
+
<label class="repository-confirm-field">
|
|
418
|
+
<span>Board slug</span>
|
|
419
|
+
<input id="replaceImportConfirmInput" autocomplete="off" spellcheck="false" placeholder="${escapeHtml(project.slug || "")}" />
|
|
420
|
+
</label>
|
|
421
|
+
<div class="deployment-actions">
|
|
422
|
+
<label class="import-button danger-button">
|
|
423
|
+
<input type="file" id="replaceCurrentBoardImportFile" accept=".orbit.json,.json,application/json" disabled />
|
|
424
|
+
<span>Replace Current Board</span>
|
|
399
425
|
</label>
|
|
400
426
|
</div>
|
|
401
427
|
</div>
|
|
@@ -599,6 +625,18 @@ function renderProjectJournalTab(context) {
|
|
|
599
625
|
`;
|
|
600
626
|
}
|
|
601
627
|
|
|
628
|
+
function parseImportSnapshotText(text) {
|
|
629
|
+
const trimmed = String(text || "").trimStart();
|
|
630
|
+
if (trimmed.startsWith("<")) {
|
|
631
|
+
throw new Error("That file looks like an HTML page instead of JSON. In Trello, use Export JSON/download the board JSON, not Save Page As.");
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
return JSON.parse(text);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
throw new Error(error?.message || "invalid JSON");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
602
640
|
function bindProjectTabHandlers(context, tab) {
|
|
603
641
|
const project = context.board;
|
|
604
642
|
|
|
@@ -628,19 +666,63 @@ function bindProjectTabHandlers(context, tab) {
|
|
|
628
666
|
toast.success(`Renamed board to ${nextName}. Canonical slug unchanged: ${project.slug}`);
|
|
629
667
|
});
|
|
630
668
|
|
|
631
|
-
|
|
632
|
-
const file = event.currentTarget.files?.[0];
|
|
633
|
-
if (!file) return;
|
|
669
|
+
const importSnapshotFile = async (file, bodyForSnapshot) => {
|
|
634
670
|
const text = await file.text();
|
|
635
|
-
const snapshot =
|
|
636
|
-
|
|
671
|
+
const snapshot = parseImportSnapshotText(text);
|
|
672
|
+
return api("/api/admin/import", {
|
|
637
673
|
method: "POST",
|
|
638
|
-
body:
|
|
674
|
+
body: bodyForSnapshot(snapshot)
|
|
639
675
|
});
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
event.currentTarget
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
$("#importSnapshotAsNewFile")?.addEventListener("change", async (event) => {
|
|
679
|
+
const input = event.currentTarget;
|
|
680
|
+
const file = input.files?.[0];
|
|
681
|
+
if (!file) return;
|
|
682
|
+
try {
|
|
683
|
+
const result = await importSnapshotFile(file, (snapshot) => ({ snapshot, create_new: true, board_id: project.id }));
|
|
684
|
+
state.boardId = result.imported_board_id;
|
|
685
|
+
state.boardSlug = result.imported_board_slug;
|
|
686
|
+
state.detailMode = "empty";
|
|
687
|
+
await load();
|
|
688
|
+
await navigate({ boardId: result.imported_board_id, view: "board" });
|
|
689
|
+
const counts = result.imported_counts || {};
|
|
690
|
+
const cardCount = Number.isFinite(Number(counts.tickets)) ? ` (${counts.tickets} cards)` : "";
|
|
691
|
+
toast.success(result.source_format === "trello-board-export" ? `Trello board imported as new board${cardCount}` : "Board imported as new board");
|
|
692
|
+
} catch (err) {
|
|
693
|
+
toast.error(`Import failed: ${err?.message || "invalid JSON"}`);
|
|
694
|
+
} finally {
|
|
695
|
+
input.value = "";
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const replaceImportConfirmInput = $("#replaceImportConfirmInput");
|
|
700
|
+
const replaceCurrentBoardImportFile = $("#replaceCurrentBoardImportFile");
|
|
701
|
+
replaceImportConfirmInput?.addEventListener("input", () => {
|
|
702
|
+
if (replaceCurrentBoardImportFile) replaceCurrentBoardImportFile.disabled = replaceImportConfirmInput.value.trim() !== project.slug;
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
replaceCurrentBoardImportFile?.addEventListener("change", async (event) => {
|
|
706
|
+
const input = event.currentTarget;
|
|
707
|
+
const file = input.files?.[0];
|
|
708
|
+
if (!file) return;
|
|
709
|
+
if (replaceImportConfirmInput.value.trim() !== project.slug) {
|
|
710
|
+
toast.error("Type the board slug before replacing this board.");
|
|
711
|
+
input.value = "";
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
const result = await importSnapshotFile(file, (snapshot) => ({ snapshot, replace_existing: true, board_id: project.id }));
|
|
716
|
+
await load();
|
|
717
|
+
state.detailMode = "settings";
|
|
718
|
+
const counts = result.imported_counts || {};
|
|
719
|
+
const cardCount = Number.isFinite(Number(counts.tickets)) ? ` (${counts.tickets} cards)` : "";
|
|
720
|
+
toast.success(result.source_format === "trello-board-export" ? `Trello board replaced from Trello import${cardCount}` : "Board replaced from snapshot");
|
|
721
|
+
} catch (err) {
|
|
722
|
+
toast.error(`Import failed: ${err?.message || "invalid JSON"}`);
|
|
723
|
+
} finally {
|
|
724
|
+
input.value = "";
|
|
725
|
+
}
|
|
644
726
|
});
|
|
645
727
|
|
|
646
728
|
$("#deleteBoardStart")?.addEventListener("click", async () => {
|
|
@@ -817,17 +899,39 @@ function bindProjectTabHandlers(context, tab) {
|
|
|
817
899
|
});
|
|
818
900
|
});
|
|
819
901
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
902
|
+
const instructionsEl = drawerInner.querySelector('[data-edit-field="agent_instructions"]');
|
|
903
|
+
if (instructionsEl) {
|
|
904
|
+
const edit = () =>
|
|
905
|
+
startInlineEdit(instructionsEl, {
|
|
906
|
+
fieldName: "agent_instructions",
|
|
907
|
+
multiline: true,
|
|
908
|
+
initialValue: project.agent_instructions || "",
|
|
909
|
+
ariaLabel: "Edit Agent Instructions",
|
|
910
|
+
rerender: async () => {
|
|
911
|
+
state.detailMode = "settings";
|
|
912
|
+
await renderProjectDetail();
|
|
913
|
+
},
|
|
914
|
+
commit: async (next) => {
|
|
915
|
+
await api(`/api/boards/${project.id}`, {
|
|
916
|
+
method: "PATCH",
|
|
917
|
+
body: { agent_instructions: cleanText(next) }
|
|
918
|
+
});
|
|
919
|
+
await load();
|
|
920
|
+
state.detailMode = "settings";
|
|
921
|
+
toast.success("Saved");
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
instructionsEl.addEventListener("click", (event) => {
|
|
925
|
+
event.preventDefault();
|
|
926
|
+
edit();
|
|
826
927
|
});
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
928
|
+
instructionsEl.addEventListener("keydown", (event) => {
|
|
929
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
930
|
+
event.preventDefault();
|
|
931
|
+
edit();
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
}
|
|
831
935
|
|
|
832
936
|
$("#aiEnabledToggle")?.addEventListener("change", async (event) => {
|
|
833
937
|
const enabled = !!event.currentTarget.checked;
|
package/public/js/sse.js
CHANGED
|
File without changes
|
package/public/js/starscape.js
CHANGED
|
File without changes
|
package/public/js/state.js
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -39,15 +39,15 @@ import { formatActorLabel, formatCommentAuthor } from "./actor-labels.js";
|
|
|
39
39
|
* cache; otherwise leave the board DOM alone.
|
|
40
40
|
*/
|
|
41
41
|
async function refreshTicketDetail(ticketId, { renderBoardAfter = false } = {}) {
|
|
42
|
-
state.selectedTicketId = ticketId;
|
|
43
|
-
state.detailMode = "ticket";
|
|
44
42
|
const [context, statusHistory, commentsResult] = await Promise.all([
|
|
45
43
|
api(withBoardQuery(`/api/tickets/${ticketId}/context?depth=1`)),
|
|
46
44
|
api(withBoardQuery(`/api/tickets/${ticketId}/history`)),
|
|
47
45
|
api(withBoardQuery(`/api/tickets/${ticketId}/comments`))
|
|
48
46
|
]);
|
|
47
|
+
if (context.ticket?.id !== ticketId) return;
|
|
49
48
|
upsertTicket(context.ticket);
|
|
50
49
|
if (renderBoardAfter) renderBoard();
|
|
50
|
+
if (state.detailMode !== "ticket" || state.selectedTicketId !== ticketId) return;
|
|
51
51
|
await renderDetail({ context, statusHistory, comments: commentsResult.comments || [] });
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -71,11 +71,62 @@ async function patchTicket(ticketId, body) {
|
|
|
71
71
|
* - initialValue: text shown in the editor on open.
|
|
72
72
|
* - ariaLabel: full label for screen readers.
|
|
73
73
|
* - emptyMessage: if set, an empty trimmed value triggers a warning toast and cancels.
|
|
74
|
+
* - sourceEvent: optional pointer/click event used to seed the textarea caret near the clicked text.
|
|
74
75
|
* - rerender: async fn called on cancel / no-change to put the static node back.
|
|
75
76
|
* - commit: async fn called with the new value when the user accepts a real change.
|
|
76
77
|
* Receives the *raw* editor value; the caller is responsible for any
|
|
77
78
|
* trimming, cleanText, or PATCH wiring.
|
|
78
79
|
*/
|
|
80
|
+
function inlineTextOffsetFromPoint(node, sourceEvent, maxLength) {
|
|
81
|
+
if (!node || !sourceEvent || typeof sourceEvent.clientX !== "number" || typeof sourceEvent.clientY !== "number") return null;
|
|
82
|
+
|
|
83
|
+
let range = null;
|
|
84
|
+
if (document.caretPositionFromPoint) {
|
|
85
|
+
const position = document.caretPositionFromPoint(sourceEvent.clientX, sourceEvent.clientY);
|
|
86
|
+
if (position?.offsetNode && (position.offsetNode === node || node.contains(position.offsetNode))) {
|
|
87
|
+
range = document.createRange();
|
|
88
|
+
range.setStart(node, 0);
|
|
89
|
+
range.setEnd(position.offsetNode, position.offset);
|
|
90
|
+
}
|
|
91
|
+
} else if (document.caretRangeFromPoint) {
|
|
92
|
+
const caretRange = document.caretRangeFromPoint(sourceEvent.clientX, sourceEvent.clientY);
|
|
93
|
+
if (caretRange?.startContainer && (caretRange.startContainer === node || node.contains(caretRange.startContainer))) {
|
|
94
|
+
range = document.createRange();
|
|
95
|
+
range.setStart(node, 0);
|
|
96
|
+
range.setEnd(caretRange.startContainer, caretRange.startOffset);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!range) return null;
|
|
101
|
+
return Math.max(0, Math.min(maxLength, range.toString().length));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function captureInlineEditScrollState(node) {
|
|
105
|
+
const scrollContainer = node?.closest?.(".drawer-inner");
|
|
106
|
+
if (!scrollContainer) return null;
|
|
107
|
+
return {
|
|
108
|
+
scrollContainer,
|
|
109
|
+
nodeRect: node.getBoundingClientRect(),
|
|
110
|
+
scrollTop: scrollContainer.scrollTop
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function restoreInlineEditScroll(scrollState, editor) {
|
|
115
|
+
if (!scrollState?.scrollContainer || !editor?.isConnected) return;
|
|
116
|
+
const editorRect = editor.getBoundingClientRect();
|
|
117
|
+
const nextScrollTop = Math.max(0, scrollState.scrollContainer.scrollTop + (editorRect.top - scrollState.nodeRect.top));
|
|
118
|
+
if (Number.isFinite(nextScrollTop)) scrollState.scrollContainer.scrollTop = nextScrollTop;
|
|
119
|
+
else scrollState.scrollContainer.scrollTop = scrollState.scrollTop;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function focusWithoutScrolling(editor) {
|
|
123
|
+
try {
|
|
124
|
+
editor.focus({ preventScroll: true });
|
|
125
|
+
} catch {
|
|
126
|
+
editor.focus();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
79
130
|
export function startInlineEdit(node, opts) {
|
|
80
131
|
if (!node || node.dataset.editing === "true") return;
|
|
81
132
|
const {
|
|
@@ -84,15 +135,20 @@ export function startInlineEdit(node, opts) {
|
|
|
84
135
|
initialValue = "",
|
|
85
136
|
ariaLabel,
|
|
86
137
|
emptyMessage,
|
|
138
|
+
sourceEvent,
|
|
87
139
|
rerender,
|
|
88
140
|
commit: commitHandler
|
|
89
141
|
} = opts;
|
|
90
142
|
|
|
143
|
+
const scrollState = multiline ? captureInlineEditScrollState(node) : null;
|
|
144
|
+
const initialString = String(initialValue);
|
|
145
|
+
const clickedCaretOffset = multiline ? inlineTextOffsetFromPoint(node, sourceEvent, initialString.length) : null;
|
|
146
|
+
|
|
91
147
|
node.dataset.editing = "true";
|
|
92
148
|
|
|
93
149
|
const editor = document.createElement(multiline ? "textarea" : "input");
|
|
94
150
|
editor.className = multiline ? "inline-desc-editor" : "inline-title-editor";
|
|
95
|
-
editor.value =
|
|
151
|
+
editor.value = initialString;
|
|
96
152
|
if (fieldName) editor.setAttribute("data-editing-field", fieldName);
|
|
97
153
|
if (ariaLabel) editor.setAttribute("aria-label", ariaLabel);
|
|
98
154
|
if (!multiline) {
|
|
@@ -101,13 +157,22 @@ export function startInlineEdit(node, opts) {
|
|
|
101
157
|
editor.autocomplete = "off";
|
|
102
158
|
editor.spellcheck = true;
|
|
103
159
|
} else {
|
|
104
|
-
editor.rows = Math.max(3,
|
|
160
|
+
editor.rows = Math.max(3, initialString.split("\n").length + 1);
|
|
161
|
+
const sourceHeight = node.getBoundingClientRect().height;
|
|
162
|
+
if (Number.isFinite(sourceHeight) && sourceHeight > 0) {
|
|
163
|
+
editor.style.minHeight = `${Math.ceil(sourceHeight)}px`;
|
|
164
|
+
}
|
|
105
165
|
}
|
|
106
166
|
|
|
107
167
|
node.replaceWith(editor);
|
|
108
|
-
editor
|
|
109
|
-
if (!multiline)
|
|
110
|
-
|
|
168
|
+
focusWithoutScrolling(editor);
|
|
169
|
+
if (!multiline) {
|
|
170
|
+
editor.select();
|
|
171
|
+
} else {
|
|
172
|
+
const selectionStart = clickedCaretOffset ?? 0;
|
|
173
|
+
editor.setSelectionRange(selectionStart, selectionStart);
|
|
174
|
+
restoreInlineEditScroll(scrollState, editor);
|
|
175
|
+
}
|
|
111
176
|
|
|
112
177
|
let done = false;
|
|
113
178
|
|
|
@@ -115,11 +180,9 @@ export function startInlineEdit(node, opts) {
|
|
|
115
180
|
document.removeEventListener("pointerdown", handleOutsidePointerDown, true);
|
|
116
181
|
};
|
|
117
182
|
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// stranded on screen until a full reload. Put the original node back before
|
|
122
|
-
// any downstream render runs.
|
|
183
|
+
// If a downstream render rebuilds the title block while the editor is open,
|
|
184
|
+
// the editor is dropped along with everything else; only restore the saved
|
|
185
|
+
// node when our editor is still connected to the DOM.
|
|
123
186
|
const restoreNode = () => {
|
|
124
187
|
if (editor.isConnected) editor.replaceWith(node);
|
|
125
188
|
delete node.dataset.editing;
|
|
@@ -209,12 +272,13 @@ function wireTicketDetailEditors(ticket) {
|
|
|
209
272
|
}
|
|
210
273
|
|
|
211
274
|
const descEl = drawerInner.querySelector('[data-edit-field="description"]');
|
|
212
|
-
const editDescription = () =>
|
|
275
|
+
const editDescription = (sourceEvent) =>
|
|
213
276
|
startInlineEdit(descEl, {
|
|
214
277
|
fieldName: "description",
|
|
215
278
|
multiline: true,
|
|
216
279
|
initialValue: ticket.description || "",
|
|
217
280
|
ariaLabel: "Edit ticket description",
|
|
281
|
+
sourceEvent,
|
|
218
282
|
rerender: () => renderDetail(),
|
|
219
283
|
commit: (next) => patchTicket(ticketId, { description: cleanText(next) })
|
|
220
284
|
});
|
|
@@ -222,7 +286,7 @@ function wireTicketDetailEditors(ticket) {
|
|
|
222
286
|
if (descEl) {
|
|
223
287
|
descEl.addEventListener("click", (event) => {
|
|
224
288
|
event.preventDefault();
|
|
225
|
-
editDescription();
|
|
289
|
+
editDescription(event);
|
|
226
290
|
});
|
|
227
291
|
descEl.addEventListener("keydown", (event) => {
|
|
228
292
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -245,18 +309,19 @@ function wireTicketDetailEditors(ticket) {
|
|
|
245
309
|
for (const { key, label } of AI_FIELDS) {
|
|
246
310
|
const fieldEl = drawerInner.querySelector(`[data-edit-field="${key}"]`);
|
|
247
311
|
if (!fieldEl) continue;
|
|
248
|
-
const edit = () =>
|
|
312
|
+
const edit = (sourceEvent) =>
|
|
249
313
|
startInlineEdit(fieldEl, {
|
|
250
314
|
fieldName: key,
|
|
251
315
|
multiline: true,
|
|
252
316
|
initialValue: ticket[key] || "",
|
|
253
317
|
ariaLabel: `Edit ${label}`,
|
|
318
|
+
sourceEvent,
|
|
254
319
|
rerender: () => renderDetail(),
|
|
255
320
|
commit: (next) => patchTicket(ticketId, { [key]: cleanText(next) })
|
|
256
321
|
});
|
|
257
322
|
fieldEl.addEventListener("click", (event) => {
|
|
258
323
|
event.preventDefault();
|
|
259
|
-
edit();
|
|
324
|
+
edit(event);
|
|
260
325
|
});
|
|
261
326
|
fieldEl.addEventListener("keydown", (event) => {
|
|
262
327
|
if (event.key === "Enter" || event.key === " ") {
|
|
@@ -337,23 +402,26 @@ export async function renderDetail(options = {}) {
|
|
|
337
402
|
return;
|
|
338
403
|
}
|
|
339
404
|
|
|
405
|
+
const requestedTicketId = state.selectedTicketId;
|
|
340
406
|
const hasOption = (key) => Object.prototype.hasOwnProperty.call(options, key);
|
|
341
407
|
const [context, statusHistory, comments, attachmentList] = await Promise.all([
|
|
342
408
|
hasOption("context")
|
|
343
409
|
? Promise.resolve(options.context)
|
|
344
|
-
: api(withBoardQuery(`/api/tickets/${
|
|
410
|
+
: api(withBoardQuery(`/api/tickets/${requestedTicketId}/context?depth=1`)),
|
|
345
411
|
hasOption("statusHistory")
|
|
346
412
|
? Promise.resolve(options.statusHistory)
|
|
347
|
-
: api(withBoardQuery(`/api/tickets/${
|
|
413
|
+
: api(withBoardQuery(`/api/tickets/${requestedTicketId}/history`)),
|
|
348
414
|
hasOption("comments")
|
|
349
415
|
? Promise.resolve(options.comments)
|
|
350
|
-
: api(withBoardQuery(`/api/tickets/${
|
|
416
|
+
: api(withBoardQuery(`/api/tickets/${requestedTicketId}/comments`)).then((result) => result.comments || []),
|
|
351
417
|
hasOption("attachmentList")
|
|
352
418
|
? Promise.resolve(options.attachmentList)
|
|
353
419
|
: features.attachments
|
|
354
|
-
? api(withBoardQuery(`/api/tickets/${
|
|
420
|
+
? api(withBoardQuery(`/api/tickets/${requestedTicketId}/attachments`)).catch(() => ({ attachments: [] }))
|
|
355
421
|
: Promise.resolve({ attachments: [] })
|
|
356
422
|
]);
|
|
423
|
+
if (state.detailMode !== "ticket" || state.selectedTicketId !== requestedTicketId) return;
|
|
424
|
+
if (context.ticket?.id !== requestedTicketId) return;
|
|
357
425
|
const ticket = context.ticket;
|
|
358
426
|
// Acknowledge the read receipt: clear the unread dot on this card.
|
|
359
427
|
// Also wipe the dot from the already-rendered card in the board so the
|
package/public/js/toast.js
CHANGED
|
File without changes
|
package/public/js/transport.js
CHANGED
|
File without changes
|
package/public/js/unread.js
CHANGED
|
File without changes
|
package/public/js/url-routes.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/public/orbits-logo.png
CHANGED
|
File without changes
|