trekoon 0.2.8 → 0.2.9
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/package.json +1 -1
- package/src/board/assets/app.js +468 -1377
- package/src/board/assets/components/ClampedText.js +1 -1
- package/src/board/assets/components/Component.js +271 -0
- package/src/board/assets/components/ConfirmDialog.js +81 -0
- package/src/board/assets/components/EpicRow.js +23 -21
- package/src/board/assets/components/EpicsOverview.js +48 -11
- package/src/board/assets/components/Inspector.js +335 -0
- package/src/board/assets/components/Notice.js +80 -0
- package/src/board/assets/components/SubtaskModal.js +100 -0
- package/src/board/assets/components/TaskCard.js +82 -0
- package/src/board/assets/components/TaskModal.js +99 -0
- package/src/board/assets/components/TopBar.js +167 -0
- package/src/board/assets/components/Workspace.js +308 -0
- package/src/board/assets/components/assetMap.js +29 -14
- package/src/board/assets/components/helpers.js +244 -0
- package/src/board/assets/fonts/inter-latin.woff2 +0 -0
- package/src/board/assets/fonts/material-symbols-rounded.woff2 +0 -0
- package/src/board/assets/index.html +20 -57
- package/src/board/assets/main.js +2 -18
- package/src/board/assets/runtime/delegation.js +309 -0
- package/src/board/assets/state/actions.js +136 -16
- package/src/board/assets/state/api.js +201 -46
- package/src/board/assets/state/store.js +417 -117
- package/src/board/assets/state/url.js +184 -0
- package/src/board/assets/state/utils.js +222 -0
- package/src/board/assets/styles/board.css +811 -127
- package/src/board/assets/styles/fonts.css +22 -0
- package/src/board/routes.ts +15 -6
- package/src/board/server.ts +1 -0
- package/src/board/assets/components/AppShell.js +0 -17
- package/src/board/assets/components/BoardTopbar.js +0 -78
- package/src/board/assets/components/WorkspaceHeader.js +0 -70
- package/src/board/assets/utils/dom.js +0 -308
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { orderEpicsNewestFirst } from "./store.js";
|
|
2
|
+
|
|
1
3
|
function cloneSnapshot(snapshot) {
|
|
2
4
|
if (typeof structuredClone === "function") {
|
|
3
5
|
return structuredClone(snapshot);
|
|
@@ -38,6 +40,40 @@ export function updateSubtaskInSnapshot(snapshot, subtaskId, updates, normalizeS
|
|
|
38
40
|
return normalizeSnapshot(nextSnapshot);
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
export function cascadeEpicStatusInSnapshot(snapshot, epicId, status, normalizeSnapshot) {
|
|
44
|
+
const nextSnapshot = cloneSnapshot(snapshot);
|
|
45
|
+
const epic = nextSnapshot.epics.find((candidate) => candidate.id === epicId);
|
|
46
|
+
if (!epic) {
|
|
47
|
+
return snapshot;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const updatedAt = Date.now();
|
|
51
|
+
epic.status = status;
|
|
52
|
+
epic.updatedAt = updatedAt;
|
|
53
|
+
|
|
54
|
+
const taskIds = new Set();
|
|
55
|
+
for (const task of nextSnapshot.tasks) {
|
|
56
|
+
if (task.epicId !== epicId) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
task.status = status;
|
|
61
|
+
task.updatedAt = updatedAt;
|
|
62
|
+
taskIds.add(task.id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const subtask of nextSnapshot.subtasks) {
|
|
66
|
+
if (!taskIds.has(subtask.taskId)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
subtask.status = status;
|
|
71
|
+
subtask.updatedAt = updatedAt;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return normalizeSnapshot(nextSnapshot);
|
|
75
|
+
}
|
|
76
|
+
|
|
41
77
|
export function addDependencyInSnapshot(snapshot, sourceId, dependsOnId, normalizeSnapshot) {
|
|
42
78
|
const nextSnapshot = cloneSnapshot(snapshot);
|
|
43
79
|
const duplicate = normalizeArray(nextSnapshot.dependencies).some(
|
|
@@ -98,6 +134,8 @@ export function createBoardActions(options) {
|
|
|
98
134
|
applyTheme,
|
|
99
135
|
closeTopmostDisclosure,
|
|
100
136
|
dismissSearch,
|
|
137
|
+
hasOpenOverlay,
|
|
138
|
+
closeActiveOverlay,
|
|
101
139
|
focusSearch,
|
|
102
140
|
focusTaskDetail,
|
|
103
141
|
searchFocusKeys,
|
|
@@ -117,6 +155,47 @@ export function createBoardActions(options) {
|
|
|
117
155
|
};
|
|
118
156
|
|
|
119
157
|
let searchTimer = null;
|
|
158
|
+
let pendingSearchValue = null;
|
|
159
|
+
|
|
160
|
+
const syncSearchInputToState = () => {
|
|
161
|
+
const input = document.querySelector("#board-search-input");
|
|
162
|
+
if (input instanceof HTMLInputElement) {
|
|
163
|
+
input.value = store.search;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const cancelPendingSearch = (options = {}) => {
|
|
168
|
+
const { syncInput = true } = options;
|
|
169
|
+
pendingSearchValue = null;
|
|
170
|
+
if (searchTimer !== null) {
|
|
171
|
+
clearTimeout(searchTimer);
|
|
172
|
+
searchTimer = null;
|
|
173
|
+
}
|
|
174
|
+
if (syncInput) {
|
|
175
|
+
syncSearchInputToState();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const focusSearchInput = () => {
|
|
180
|
+
const input = document.querySelector("#board-search-input");
|
|
181
|
+
if (input instanceof HTMLInputElement) {
|
|
182
|
+
input.focus({ preventScroll: true });
|
|
183
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const shouldRefocusSearchInput = () => document.activeElement?.id === "board-search-input";
|
|
188
|
+
|
|
189
|
+
const commitSearch = (nextSearch, options = {}) => {
|
|
190
|
+
const { focusInput = false } = options;
|
|
191
|
+
cancelPendingSearch({ syncInput: false });
|
|
192
|
+
syncState({ search: nextSearch });
|
|
193
|
+
persist();
|
|
194
|
+
rerender({ preserveFocus: false });
|
|
195
|
+
if (focusInput) {
|
|
196
|
+
focusSearchInput();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
120
199
|
|
|
121
200
|
return {
|
|
122
201
|
toggleTheme() {
|
|
@@ -124,23 +203,26 @@ export function createBoardActions(options) {
|
|
|
124
203
|
applyTheme(store.theme);
|
|
125
204
|
rerender();
|
|
126
205
|
},
|
|
206
|
+
toggleNotesPanel() {
|
|
207
|
+
store.notesPanelOpen = !store.notesPanelOpen;
|
|
208
|
+
persist();
|
|
209
|
+
rerender();
|
|
210
|
+
},
|
|
127
211
|
updateSearch(value) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
212
|
+
const nextSearch = typeof value === "string" ? value : "";
|
|
213
|
+
cancelPendingSearch({ syncInput: false });
|
|
214
|
+
pendingSearchValue = nextSearch;
|
|
132
215
|
searchTimer = setTimeout(() => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
persist();
|
|
136
|
-
rerender({ preserveFocus: false });
|
|
137
|
-
const input = document.querySelector("#board-search-input");
|
|
138
|
-
if (input instanceof HTMLInputElement) {
|
|
139
|
-
input.focus({ preventScroll: true });
|
|
140
|
-
input.setSelectionRange(input.value.length, input.value.length);
|
|
216
|
+
if (pendingSearchValue !== nextSearch) {
|
|
217
|
+
return;
|
|
141
218
|
}
|
|
219
|
+
commitSearch(nextSearch, { focusInput: shouldRefocusSearchInput() });
|
|
142
220
|
}, 180);
|
|
143
221
|
},
|
|
222
|
+
clearSearch() {
|
|
223
|
+
commitSearch("");
|
|
224
|
+
},
|
|
225
|
+
cancelPendingSearch,
|
|
144
226
|
openEpic(epicId) {
|
|
145
227
|
transition({
|
|
146
228
|
screen: "tasks",
|
|
@@ -165,7 +247,11 @@ export function createBoardActions(options) {
|
|
|
165
247
|
});
|
|
166
248
|
},
|
|
167
249
|
showBoard() {
|
|
168
|
-
const
|
|
250
|
+
const boardState = getBoardState();
|
|
251
|
+
const fallbackEpicId = boardState.selectedEpicId
|
|
252
|
+
|| boardState.visibleEpics[0]?.id
|
|
253
|
+
|| orderEpicsNewestFirst(store.snapshot.epics)[0]?.id
|
|
254
|
+
|| null;
|
|
169
255
|
if (!fallbackEpicId) {
|
|
170
256
|
return;
|
|
171
257
|
}
|
|
@@ -261,6 +347,20 @@ export function createBoardActions(options) {
|
|
|
261
347
|
transition({ selectedTaskId: taskId }, { rerenderBoard: false });
|
|
262
348
|
api.patchTask(taskId, { status: nextStatus }, (snapshot) => updateTaskInSnapshot(snapshot, taskId, { status: nextStatus }, normalizeSnapshot));
|
|
263
349
|
},
|
|
350
|
+
changeEpicStatus(epicId, newStatus) {
|
|
351
|
+
const normalizedStatus = normalizeStatus(newStatus);
|
|
352
|
+
api.patchEpic(epicId, { status: normalizedStatus }, (snapshot) => {
|
|
353
|
+
const epic = snapshot.epics.find(e => e.id === epicId);
|
|
354
|
+
if (epic) epic.status = normalizedStatus;
|
|
355
|
+
return snapshot;
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
bulkSetStatus(epicId, newStatus) {
|
|
359
|
+
const normalizedStatus = normalizeStatus(newStatus);
|
|
360
|
+
api.cascadeEpicStatus(epicId, normalizedStatus, (snapshot) =>
|
|
361
|
+
cascadeEpicStatusInSnapshot(snapshot, epicId, normalizedStatus, normalizeSnapshot),
|
|
362
|
+
);
|
|
363
|
+
},
|
|
264
364
|
handleKeydown(event) {
|
|
265
365
|
const boardState = getBoardState();
|
|
266
366
|
const activeElement = document.activeElement;
|
|
@@ -276,20 +376,36 @@ export function createBoardActions(options) {
|
|
|
276
376
|
}
|
|
277
377
|
|
|
278
378
|
if (event.key === "Escape") {
|
|
379
|
+
if (activeElement?.id === "board-search-input" && pendingSearchValue !== null) {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
activeElement.value = "";
|
|
382
|
+
this.clearSearch();
|
|
383
|
+
activeElement.blur();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
279
387
|
if (closeTopmostDisclosure?.(boardState, activeElement)) {
|
|
280
388
|
event.preventDefault();
|
|
281
389
|
return;
|
|
282
390
|
}
|
|
283
391
|
|
|
392
|
+
if (dismissSearch?.(boardState, activeElement)) {
|
|
393
|
+
event.preventDefault();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (hasOpenOverlay?.()) {
|
|
398
|
+
event.preventDefault();
|
|
399
|
+
closeActiveOverlay?.();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
284
403
|
if (boardState.selectedSubtaskId) {
|
|
285
404
|
event.preventDefault();
|
|
286
405
|
this.closeSubtask();
|
|
287
406
|
} else if (boardState.selectedTaskId) {
|
|
288
407
|
event.preventDefault();
|
|
289
408
|
this.closeTask();
|
|
290
|
-
} else if (dismissSearch?.(boardState, activeElement)) {
|
|
291
|
-
event.preventDefault();
|
|
292
|
-
return;
|
|
293
409
|
} else if (boardState.screen === "tasks") {
|
|
294
410
|
event.preventDefault();
|
|
295
411
|
this.showEpics();
|
|
@@ -301,6 +417,10 @@ export function createBoardActions(options) {
|
|
|
301
417
|
return;
|
|
302
418
|
}
|
|
303
419
|
|
|
420
|
+
if (hasOpenOverlay?.()) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
304
424
|
if (boardState.screen !== "tasks" || isTypingTarget || visibleTasks.length === 0) {
|
|
305
425
|
return;
|
|
306
426
|
}
|
|
@@ -1,15 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API layer with serial mutation queue.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the boolean isMutating gate with a proper queue that processes
|
|
5
|
+
* mutations sequentially, applies optimistic updates immediately, and
|
|
6
|
+
* reverts on error.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
function cloneSnapshot(snapshot) {
|
|
2
10
|
if (typeof structuredClone === "function") {
|
|
3
11
|
return structuredClone(snapshot);
|
|
4
12
|
}
|
|
5
|
-
|
|
6
13
|
return JSON.parse(JSON.stringify(snapshot));
|
|
7
14
|
}
|
|
8
15
|
|
|
16
|
+
async function readJsonPayload(response) {
|
|
17
|
+
const text = await response.text();
|
|
18
|
+
if (text.length === 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(text);
|
|
24
|
+
} catch {
|
|
25
|
+
const error = new Error(`Board API returned malformed JSON (${response.status} ${response.statusText || "unknown"})`);
|
|
26
|
+
error.code = "invalid_response";
|
|
27
|
+
error.status = response.status;
|
|
28
|
+
error.statusText = response.statusText;
|
|
29
|
+
error.details = {
|
|
30
|
+
responseText: text.slice(0, 240),
|
|
31
|
+
};
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildRequestError(method, path, response, payload) {
|
|
37
|
+
const code = payload?.error?.code;
|
|
38
|
+
const routeMessage = payload?.error?.message;
|
|
39
|
+
const message = routeMessage
|
|
40
|
+
? `${method} ${path} failed (${response.status}${code ? ` ${code}` : ""}): ${routeMessage}`
|
|
41
|
+
: `${method} ${path} failed with ${response.status} ${response.statusText || "unknown error"}`;
|
|
42
|
+
const error = new Error(message);
|
|
43
|
+
error.code = code;
|
|
44
|
+
error.status = response.status;
|
|
45
|
+
error.statusText = response.statusText;
|
|
46
|
+
error.details = payload?.error?.details;
|
|
47
|
+
return error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a serial mutation queue.
|
|
52
|
+
*
|
|
53
|
+
* Mutations are enqueued and processed one at a time in FIFO order.
|
|
54
|
+
* Each mutation can apply an optimistic update, make an async request,
|
|
55
|
+
* and handle success or error.
|
|
56
|
+
*
|
|
57
|
+
* @returns {{
|
|
58
|
+
* enqueue: (mutation: object) => void,
|
|
59
|
+
* isPending: boolean,
|
|
60
|
+
* flush: () => Promise<void>,
|
|
61
|
+
* }}
|
|
62
|
+
*/
|
|
63
|
+
export function createMutationQueue(model, rerender) {
|
|
64
|
+
/** @type {Array<{ optimistic?: function, request: function, onSuccess?: function, onError?: function, successMessage?: string }>} */
|
|
65
|
+
const queue = [];
|
|
66
|
+
let processing = false;
|
|
67
|
+
/** @type {Array<() => void>} */
|
|
68
|
+
let flushResolvers = [];
|
|
69
|
+
|
|
70
|
+
function resolveFlushes() {
|
|
71
|
+
if (processing || queue.length > 0 || flushResolvers.length === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const pendingResolvers = flushResolvers;
|
|
76
|
+
flushResolvers = [];
|
|
77
|
+
pendingResolvers.forEach((resolve) => resolve());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function processNext() {
|
|
81
|
+
if (processing || queue.length === 0) return;
|
|
82
|
+
processing = true;
|
|
83
|
+
model.store.isMutating = true;
|
|
84
|
+
|
|
85
|
+
while (queue.length > 0) {
|
|
86
|
+
const mutation = queue.shift();
|
|
87
|
+
const previousSnapshot = cloneSnapshot(model.store.snapshot);
|
|
88
|
+
model.store.notice = null;
|
|
89
|
+
|
|
90
|
+
// Apply optimistic update
|
|
91
|
+
if (typeof mutation.optimistic === "function") {
|
|
92
|
+
model.store.snapshot = mutation.optimistic(cloneSnapshot(model.store.snapshot));
|
|
93
|
+
rerender();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const data = await mutation.request();
|
|
98
|
+
|
|
99
|
+
if (data?.snapshot) {
|
|
100
|
+
model.replaceSnapshot(data.snapshot);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (typeof mutation.onSuccess === "function") {
|
|
104
|
+
mutation.onSuccess(data);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
model.store.notice = mutation.successMessage
|
|
108
|
+
? { type: "success", message: mutation.successMessage }
|
|
109
|
+
: null;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// Revert to pre-optimistic snapshot
|
|
112
|
+
model.replaceSnapshot(previousSnapshot);
|
|
113
|
+
|
|
114
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
+
model.store.notice = { type: "error", message };
|
|
116
|
+
|
|
117
|
+
if (typeof mutation.onError === "function") {
|
|
118
|
+
mutation.onError(error);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Clear remaining queue on error to prevent cascading failures
|
|
122
|
+
queue.length = 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
processing = false;
|
|
127
|
+
model.store.isMutating = false;
|
|
128
|
+
rerender();
|
|
129
|
+
resolveFlushes();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
enqueue(mutation) {
|
|
134
|
+
queue.push(mutation);
|
|
135
|
+
processNext();
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
get isPending() {
|
|
139
|
+
return processing || queue.length > 0;
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
flush() {
|
|
143
|
+
if (!processing && queue.length === 0) return Promise.resolve();
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
flushResolvers.push(resolve);
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create the API layer with mutation queue.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} model - Store model from createStore
|
|
155
|
+
* @param {object} options
|
|
156
|
+
* @param {string} options.sessionToken - Auth token for API requests
|
|
157
|
+
* @param {function} options.rerender - Trigger a UI rerender
|
|
158
|
+
* @returns {object} API methods: patchTask, patchSubtask, createSubtask, deleteSubtask, addDependency, removeDependency
|
|
159
|
+
*/
|
|
9
160
|
export function createApi(model, options) {
|
|
10
161
|
const { sessionToken, rerender } = options;
|
|
11
162
|
|
|
12
163
|
async function request(path, requestOptions = {}) {
|
|
164
|
+
const method = typeof requestOptions.method === "string" ? requestOptions.method.toUpperCase() : "GET";
|
|
13
165
|
const headers = new Headers(requestOptions.headers || {});
|
|
14
166
|
if (sessionToken.length > 0) {
|
|
15
167
|
headers.set("authorization", `Bearer ${sessionToken}`);
|
|
@@ -18,54 +170,41 @@ export function createApi(model, options) {
|
|
|
18
170
|
headers.set("content-type", "application/json");
|
|
19
171
|
}
|
|
20
172
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return payload.data;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function runMutation({ optimistic, request: mutationRequest, successMessage }) {
|
|
35
|
-
if (model.store.isMutating) {
|
|
36
|
-
return;
|
|
173
|
+
let response;
|
|
174
|
+
try {
|
|
175
|
+
response = await fetch(path, { ...requestOptions, headers });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
178
|
+
const requestError = new Error(`${method} ${path} failed before a response was received: ${message}`);
|
|
179
|
+
requestError.code = "network_error";
|
|
180
|
+
requestError.cause = error;
|
|
181
|
+
throw requestError;
|
|
37
182
|
}
|
|
38
183
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (typeof optimistic === "function") {
|
|
44
|
-
model.store.snapshot = optimistic(cloneSnapshot(model.store.snapshot));
|
|
45
|
-
rerender();
|
|
184
|
+
const payload = await readJsonPayload(response);
|
|
185
|
+
if (!response.ok || !payload?.ok) {
|
|
186
|
+
throw buildRequestError(method, path, response, payload);
|
|
46
187
|
}
|
|
47
188
|
|
|
48
|
-
|
|
49
|
-
const data = await mutationRequest();
|
|
50
|
-
if (data?.snapshot) {
|
|
51
|
-
model.replaceSnapshot(data.snapshot);
|
|
52
|
-
}
|
|
53
|
-
model.store.notice = successMessage ? { type: "success", message: successMessage } : null;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
model.replaceSnapshot(previousSnapshot);
|
|
56
|
-
model.store.notice = {
|
|
57
|
-
type: "error",
|
|
58
|
-
message: error instanceof Error ? error.message : String(error),
|
|
59
|
-
};
|
|
60
|
-
} finally {
|
|
61
|
-
model.store.isMutating = false;
|
|
62
|
-
rerender();
|
|
63
|
-
}
|
|
189
|
+
return payload.data;
|
|
64
190
|
}
|
|
65
191
|
|
|
192
|
+
const queue = createMutationQueue(model, rerender);
|
|
193
|
+
|
|
66
194
|
return {
|
|
195
|
+
patchEpic(epicId, updates, optimistic) {
|
|
196
|
+
queue.enqueue({
|
|
197
|
+
optimistic,
|
|
198
|
+
successMessage: "Epic saved.",
|
|
199
|
+
request: () => request(`/api/epics/${encodeURIComponent(epicId)}`, {
|
|
200
|
+
method: "PATCH",
|
|
201
|
+
body: JSON.stringify(updates),
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
|
|
67
206
|
patchTask(taskId, updates, optimistic) {
|
|
68
|
-
|
|
207
|
+
queue.enqueue({
|
|
69
208
|
optimistic,
|
|
70
209
|
successMessage: "Task saved.",
|
|
71
210
|
request: () => request(`/api/tasks/${encodeURIComponent(taskId)}`, {
|
|
@@ -74,8 +213,9 @@ export function createApi(model, options) {
|
|
|
74
213
|
}),
|
|
75
214
|
});
|
|
76
215
|
},
|
|
216
|
+
|
|
77
217
|
patchSubtask(subtaskId, updates, optimistic) {
|
|
78
|
-
|
|
218
|
+
queue.enqueue({
|
|
79
219
|
optimistic,
|
|
80
220
|
successMessage: "Subtask saved.",
|
|
81
221
|
request: () => request(`/api/subtasks/${encodeURIComponent(subtaskId)}`, {
|
|
@@ -84,8 +224,20 @@ export function createApi(model, options) {
|
|
|
84
224
|
}),
|
|
85
225
|
});
|
|
86
226
|
},
|
|
227
|
+
|
|
228
|
+
cascadeEpicStatus(epicId, status, optimistic) {
|
|
229
|
+
queue.enqueue({
|
|
230
|
+
optimistic,
|
|
231
|
+
successMessage: "Epic cascade status updated.",
|
|
232
|
+
request: () => request(`/api/epics/${encodeURIComponent(epicId)}/cascade`, {
|
|
233
|
+
method: "PATCH",
|
|
234
|
+
body: JSON.stringify({ status }),
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
|
|
87
239
|
createSubtask(input, optimistic) {
|
|
88
|
-
|
|
240
|
+
queue.enqueue({
|
|
89
241
|
optimistic,
|
|
90
242
|
successMessage: "Subtask added.",
|
|
91
243
|
request: () => request("/api/subtasks", {
|
|
@@ -94,8 +246,9 @@ export function createApi(model, options) {
|
|
|
94
246
|
}),
|
|
95
247
|
});
|
|
96
248
|
},
|
|
249
|
+
|
|
97
250
|
deleteSubtask(subtaskId, optimistic) {
|
|
98
|
-
|
|
251
|
+
queue.enqueue({
|
|
99
252
|
optimistic,
|
|
100
253
|
successMessage: "Subtask removed.",
|
|
101
254
|
request: () => request(`/api/subtasks/${encodeURIComponent(subtaskId)}`, {
|
|
@@ -103,8 +256,9 @@ export function createApi(model, options) {
|
|
|
103
256
|
}),
|
|
104
257
|
});
|
|
105
258
|
},
|
|
259
|
+
|
|
106
260
|
addDependency(sourceId, dependsOnId, optimistic) {
|
|
107
|
-
|
|
261
|
+
queue.enqueue({
|
|
108
262
|
optimistic,
|
|
109
263
|
successMessage: "Dependency added.",
|
|
110
264
|
request: () => request("/api/dependencies", {
|
|
@@ -113,8 +267,9 @@ export function createApi(model, options) {
|
|
|
113
267
|
}),
|
|
114
268
|
});
|
|
115
269
|
},
|
|
270
|
+
|
|
116
271
|
removeDependency(sourceId, dependsOnId, optimistic) {
|
|
117
|
-
|
|
272
|
+
queue.enqueue({
|
|
118
273
|
optimistic,
|
|
119
274
|
successMessage: "Dependency removed.",
|
|
120
275
|
request: () => request(`/api/dependencies?sourceId=${encodeURIComponent(sourceId)}&dependsOnId=${encodeURIComponent(dependsOnId)}`, {
|