|
1 | 1 | import { addTagBtn, closeTodoBtn, deleteTodoBtn, shareTodoBtn, todoBody, todoBodyPreview, todoBodyPreviewTab, todoBodyToggle, todoBodyWriteTab, todoDialog, todoDialogTitle, todoEstimationField, todoEstimationPoints, todoStatus, todoTags, todoTitle, } from '../dom/elements.js'; |
2 | 2 | import { apiFetch } from '../api.js'; |
| 3 | +import { DIALOG_CLOSE_REQUEST_EVENT } from '../core/modal-outside-click.js'; |
3 | 4 | import { renderMarkdownPreviewInto } from '../markdown-preview.js'; |
4 | 5 | import { getBoard, getBoardMembers, getMarkdownNotesEnabled, getSlug, getTagColors, getUser } from '../state/selectors.js'; |
5 | 6 | import { setAvailableTags, setAvailableTagsMap, setEditingTodo, setTagColors } from '../state/mutations.js'; |
6 | | -import { escapeHTML, isAnonymousBoard, showToast } from '../utils.js'; |
| 7 | +import { escapeHTML, isAnonymousBoard, showConfirmDialog, showToast } from '../utils.js'; |
7 | 8 | import { normalizeSprints } from '../sprints.js'; |
8 | 9 | import { bindShareTodoButton, bindTodoDialogLinkLifecycle, initializeTodoDialogLinks, resetTodoDialogLinks, } from './todo-links.js'; |
9 | 10 | import { computeTodoDialogPermissions, setTodoFormPermissions, } from './todo-permissions.js'; |
10 | | -import { renderTagsChips, resetTodoTagAutocompleteBindings, setupTagAutocomplete, } from './todo-tags.js'; |
| 11 | +import { getTagsFromChips, renderTagsChips, resetTodoTagAutocompleteBindings, setupTagAutocomplete, } from './todo-tags.js'; |
11 | 12 | export { getTodoFormPermissions, } from './todo-permissions.js'; |
12 | 13 | export { getTagsFromChips, normalizeTagName, removeTag, renderTagAutocomplete, renderTagsChips, setupTagAutocomplete, } from './todo-tags.js'; |
13 | 14 | let todoNotesMode = "markdown"; |
14 | 15 | let todoNotesPreviewBound = false; |
| 16 | +let todoDialogCloseGuardsBound = false; |
| 17 | +let todoDialogBaseline = null; |
| 18 | +let todoDialogClosePromptOpen = false; |
15 | 19 | export function resolveColumnKey(raw) { |
16 | 20 | const v = (raw || "").trim(); |
17 | 21 | if (!v) |
@@ -147,9 +151,108 @@ function bindTodoNotesPreviewControls() { |
147 | 151 | }); |
148 | 152 | } |
149 | 153 | } |
| 154 | +function readTodoDialogSnapshot() { |
| 155 | + const assignee = document.getElementById("todoAssignee"); |
| 156 | + const sprint = document.getElementById("todoSprint"); |
| 157 | + return { |
| 158 | + title: todoTitle?.value ?? "", |
| 159 | + body: todoBody?.value ?? "", |
| 160 | + tags: getTagsFromChips(), |
| 161 | + status: todoStatus?.value ?? "", |
| 162 | + estimation: todoEstimationPoints?.value ?? "", |
| 163 | + assignee: assignee?.value ?? "", |
| 164 | + sprint: sprint?.value ?? "", |
| 165 | + }; |
| 166 | +} |
| 167 | +function captureTodoDialogBaseline() { |
| 168 | + todoDialogBaseline = readTodoDialogSnapshot(); |
| 169 | +} |
| 170 | +function isTodoDialogDirty() { |
| 171 | + if (!todoDialogBaseline) { |
| 172 | + return false; |
| 173 | + } |
| 174 | + const current = readTodoDialogSnapshot(); |
| 175 | + return (current.title !== todoDialogBaseline.title || |
| 176 | + current.body !== todoDialogBaseline.body || |
| 177 | + current.status !== todoDialogBaseline.status || |
| 178 | + current.estimation !== todoDialogBaseline.estimation || |
| 179 | + current.assignee !== todoDialogBaseline.assignee || |
| 180 | + current.sprint !== todoDialogBaseline.sprint || |
| 181 | + current.tags.length !== todoDialogBaseline.tags.length || |
| 182 | + current.tags.some((tag, idx) => tag !== todoDialogBaseline?.tags[idx])); |
| 183 | +} |
| 184 | +function resetTodoDialogCloseState() { |
| 185 | + todoDialogBaseline = null; |
| 186 | + todoDialogClosePromptOpen = false; |
| 187 | +} |
| 188 | +async function closeTodoDialogInternal(options = {}) { |
| 189 | + const dialog = todoDialog; |
| 190 | + if (!dialog || !dialog.open) { |
| 191 | + return true; |
| 192 | + } |
| 193 | + if (options.force || !isTodoDialogDirty()) { |
| 194 | + dialog.close(); |
| 195 | + return true; |
| 196 | + } |
| 197 | + if (todoDialogClosePromptOpen) { |
| 198 | + return false; |
| 199 | + } |
| 200 | + todoDialogClosePromptOpen = true; |
| 201 | + try { |
| 202 | + const discard = await showConfirmDialog("You have unsaved changes. Discard them?", "Unsaved changes", "Discard"); |
| 203 | + if (!discard) { |
| 204 | + return false; |
| 205 | + } |
| 206 | + dialog.close(); |
| 207 | + return true; |
| 208 | + } |
| 209 | + finally { |
| 210 | + todoDialogClosePromptOpen = false; |
| 211 | + } |
| 212 | +} |
| 213 | +function bindTodoDialogCloseGuards() { |
| 214 | + if (todoDialogCloseGuardsBound || !todoDialog) { |
| 215 | + return; |
| 216 | + } |
| 217 | + todoDialogCloseGuardsBound = true; |
| 218 | + todoDialog.addEventListener("cancel", (event) => { |
| 219 | + if (todoDialogClosePromptOpen) { |
| 220 | + event.preventDefault(); |
| 221 | + return; |
| 222 | + } |
| 223 | + if (!isTodoDialogDirty()) { |
| 224 | + return; |
| 225 | + } |
| 226 | + event.preventDefault(); |
| 227 | + void closeTodoDialogInternal({ reason: "cancel" }); |
| 228 | + }); |
| 229 | + todoDialog.addEventListener(DIALOG_CLOSE_REQUEST_EVENT, (event) => { |
| 230 | + if (todoDialogClosePromptOpen) { |
| 231 | + event.preventDefault(); |
| 232 | + return; |
| 233 | + } |
| 234 | + if (!isTodoDialogDirty()) { |
| 235 | + return; |
| 236 | + } |
| 237 | + event.preventDefault(); |
| 238 | + const detail = event.detail; |
| 239 | + void closeTodoDialogInternal({ reason: detail?.reason === "outside" ? "outside" : "button" }); |
| 240 | + }); |
| 241 | + todoDialog.addEventListener("close", () => { |
| 242 | + resetTodoDialogCloseState(); |
| 243 | + }); |
| 244 | +} |
| 245 | +export function requestTodoDialogClose(options = {}) { |
| 246 | + bindTodoDialogCloseGuards(); |
| 247 | + return closeTodoDialogInternal(options); |
| 248 | +} |
| 249 | +export function __isTodoDialogDirtyForTest() { |
| 250 | + return isTodoDialogDirty(); |
| 251 | +} |
150 | 252 | export async function openTodoDialog(opts) { |
151 | 253 | const { mode, todo, status, onNavigateToLinkedTodo } = opts; |
152 | 254 | setEditingTodo(mode === "edit" ? todo : null); |
| 255 | + bindTodoDialogCloseGuards(); |
153 | 256 | bindTodoDialogLinkLifecycle(); |
154 | 257 | bindTodoNotesPreviewControls(); |
155 | 258 | const board = getBoard(); |
@@ -412,6 +515,7 @@ export async function openTodoDialog(opts) { |
412 | 515 | setupTagAutocomplete(); |
413 | 516 | } |
414 | 517 | bindShareTodoButton(); |
| 518 | + captureTodoDialogBaseline(); |
415 | 519 | todoDialog.showModal(); |
416 | 520 | let userChoseFocus = false; |
417 | 521 | const ac = new AbortController(); |
|
0 commit comments