@critical-path/svelte 0.15.8 → 0.16.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/CHANGELOG.md +25 -0
- package/dist/index.js +401 -390
- package/dist/kanban-state.svelte.d.ts +1 -1
- package/dist/task-state.svelte.d.ts +4 -2
- package/dist/task-state.svelte.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.test.ts +61 -0
- package/src/task-state.svelte.ts +94 -69
|
@@ -18,7 +18,7 @@ export declare class KanbanState {
|
|
|
18
18
|
createTask(input: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task>;
|
|
19
19
|
moveTask(taskId: string, targetStatus: TaskStatus): Promise<Task>;
|
|
20
20
|
updateTaskStatus(taskId: string, status: TaskStatus): Promise<Task>;
|
|
21
|
-
deleteTask(taskId: string): Promise<
|
|
21
|
+
deleteTask(taskId: string): Promise<void>;
|
|
22
22
|
}
|
|
23
23
|
export declare function createKanbanState(client: CriticalPathClient, projectId?: string, options?: KanbanStateOptions): KanbanState;
|
|
24
24
|
//# sourceMappingURL=kanban-state.svelte.d.ts.map
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type { CriticalPathClient } from '@critical-path/client';
|
|
2
|
-
import type
|
|
2
|
+
import { type Task, type TaskStatus } from '@critical-path/core';
|
|
3
3
|
export declare class TaskState {
|
|
4
4
|
private client;
|
|
5
5
|
projectId?: string | undefined;
|
|
6
6
|
data: Task[];
|
|
7
7
|
loading: boolean;
|
|
8
8
|
error: Error | null;
|
|
9
|
+
private pendingCreations;
|
|
10
|
+
private tempToRealIdMap;
|
|
9
11
|
constructor(client: CriticalPathClient, projectId?: string | undefined);
|
|
10
12
|
fetch(): Promise<void>;
|
|
11
13
|
createTask(input: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task>;
|
|
12
14
|
updateTask(taskId: string, updates: Partial<Task>): Promise<Task>;
|
|
13
15
|
updateTaskStatus(taskId: string, status: TaskStatus): Promise<Task>;
|
|
14
|
-
deleteTask(taskId: string): Promise<
|
|
16
|
+
deleteTask(taskId: string): Promise<void>;
|
|
15
17
|
}
|
|
16
18
|
export declare function createTaskState(client: CriticalPathClient, projectId?: string): TaskState;
|
|
17
19
|
//# sourceMappingURL=task-state.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-state.svelte.d.ts","sourceRoot":"","sources":["../src/task-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"task-state.svelte.d.ts","sourceRoot":"","sources":["../src/task-state.svelte.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,UAAU,EAAgB,MAAM,qBAAqB,CAAC;AAE/E,qBAAa,SAAS;IASlB,OAAO,CAAC,MAAM;IACP,SAAS,CAAC,EAAE,MAAM;IAT3B,IAAI,SAAsB;IAC1B,OAAO,UAA0B;IACjC,KAAK,eAA8B;IAEnC,OAAO,CAAC,gBAAgB,CAAoC;IAC5D,OAAO,CAAC,eAAe,CAA6B;gBAG1C,MAAM,EAAE,kBAAkB,EAC3B,SAAS,CAAC,EAAE,MAAM,YAAA;IAGrB,KAAK;IAYL,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC;IAiC9D,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC;IAyEjD,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU;IAInD,UAAU,CAAC,MAAM,EAAE,MAAM;CA4ChC;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,kBAAkB,EAC1B,SAAS,CAAC,EAAE,MAAM,GACjB,SAAS,CAEX"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@critical-path/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Svelte 5 Runes state and reactive integrations for Critical Path headless project management framework",
|
|
5
5
|
"author": "Jack James",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@critical-path/
|
|
22
|
-
"@critical-path/
|
|
23
|
-
"@critical-path/mcp": "0.6.
|
|
21
|
+
"@critical-path/client": "0.14.6",
|
|
22
|
+
"@critical-path/core": "0.21.0",
|
|
23
|
+
"@critical-path/mcp": "0.6.9"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"svelte": "^5.0.0"
|
package/src/index.test.ts
CHANGED
|
@@ -241,6 +241,67 @@ describe('@critical-path/svelte Svelte 5 Runes Test Suite', () => {
|
|
|
241
241
|
// Should restore deleted task
|
|
242
242
|
expect(taskState.data).toEqual([mockTask]);
|
|
243
243
|
});
|
|
244
|
+
|
|
245
|
+
it('resolves in-flight temporary task when updated or deleted before creation completes', async () => {
|
|
246
|
+
let resolveCreate: (value: Task) => void;
|
|
247
|
+
const createPromise = new Promise<Task>((resolve) => {
|
|
248
|
+
resolveCreate = resolve;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const mockClient = {
|
|
252
|
+
getTasks: vi.fn().mockResolvedValue([]),
|
|
253
|
+
createTask: vi.fn().mockReturnValue(createPromise),
|
|
254
|
+
updateTask: vi.fn().mockImplementation((taskId: string, updates: Partial<Task>) =>
|
|
255
|
+
Promise.resolve({ id: taskId, projectId: 'proj_1', title: 'Real Task', status: 'todo', priority: 'medium', createdAt: '2026-01-01', updatedAt: '2026-01-01', ...updates } as Task)
|
|
256
|
+
),
|
|
257
|
+
deleteTask: vi.fn().mockResolvedValue(undefined)
|
|
258
|
+
} as unknown as CriticalPathClient;
|
|
259
|
+
|
|
260
|
+
const taskState = createTaskState(mockClient, 'proj_1');
|
|
261
|
+
await taskState.fetch();
|
|
262
|
+
|
|
263
|
+
// Start creation
|
|
264
|
+
const creationAction = taskState.createTask({
|
|
265
|
+
projectId: 'proj_1',
|
|
266
|
+
title: 'Draft Task',
|
|
267
|
+
status: 'todo',
|
|
268
|
+
priority: 'medium'
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// While in flight, optimistic task with temp_ ID is present
|
|
272
|
+
expect(taskState.data.length).toBe(1);
|
|
273
|
+
const tempId = taskState.data[0].id;
|
|
274
|
+
expect(tempId.startsWith('temp_')).toBe(true);
|
|
275
|
+
|
|
276
|
+
// Immediately update using tempId before creation resolves
|
|
277
|
+
const updateAction = taskState.updateTask(tempId, { title: 'Renamed Draft Task' });
|
|
278
|
+
|
|
279
|
+
// Local optimistic state is updated immediately
|
|
280
|
+
expect(taskState.data[0].title).toBe('Renamed Draft Task');
|
|
281
|
+
|
|
282
|
+
// Now resolve the server creation
|
|
283
|
+
const serverTask: Task = {
|
|
284
|
+
id: 'real_task_99',
|
|
285
|
+
projectId: 'proj_1',
|
|
286
|
+
title: 'Draft Task',
|
|
287
|
+
status: 'todo',
|
|
288
|
+
priority: 'medium',
|
|
289
|
+
createdAt: '2026-01-01',
|
|
290
|
+
updatedAt: '2026-01-01'
|
|
291
|
+
};
|
|
292
|
+
resolveCreate!(serverTask);
|
|
293
|
+
|
|
294
|
+
await creationAction;
|
|
295
|
+
await updateAction;
|
|
296
|
+
|
|
297
|
+
// Verify server update was called with the real task ID, not the temp ID
|
|
298
|
+
expect(mockClient.updateTask).toHaveBeenCalledWith('real_task_99', { title: 'Renamed Draft Task' });
|
|
299
|
+
|
|
300
|
+
// And deleting with the tempId resolves to real_task_99
|
|
301
|
+
await taskState.deleteTask(tempId);
|
|
302
|
+
expect(mockClient.deleteTask).toHaveBeenCalledWith('real_task_99');
|
|
303
|
+
expect(taskState.data.length).toBe(0);
|
|
304
|
+
});
|
|
244
305
|
});
|
|
245
306
|
|
|
246
307
|
describe('CommentState', () => {
|
package/src/task-state.svelte.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/// <reference types="svelte" />
|
|
2
2
|
import type { CriticalPathClient } from '@critical-path/client';
|
|
3
|
-
import type
|
|
3
|
+
import { type Task, type TaskStatus, isTempTaskId } from '@critical-path/core';
|
|
4
4
|
|
|
5
5
|
export class TaskState {
|
|
6
6
|
data = $state<Task[]>([]);
|
|
7
7
|
loading = $state<boolean>(false);
|
|
8
8
|
error = $state<Error | null>(null);
|
|
9
9
|
|
|
10
|
+
private pendingCreations = new Map<string, Promise<Task>>();
|
|
11
|
+
private tempToRealIdMap = new Map<string, string>();
|
|
12
|
+
|
|
10
13
|
constructor(
|
|
11
14
|
private client: CriticalPathClient,
|
|
12
15
|
public projectId?: string
|
|
@@ -37,8 +40,12 @@ export class TaskState {
|
|
|
37
40
|
// Optimistically add temp task
|
|
38
41
|
this.data = [...this.data, tempTask];
|
|
39
42
|
|
|
43
|
+
const promise = this.client.createTask(input);
|
|
44
|
+
this.pendingCreations.set(tempId, promise);
|
|
45
|
+
|
|
40
46
|
try {
|
|
41
|
-
const created = await
|
|
47
|
+
const created = await promise;
|
|
48
|
+
this.tempToRealIdMap.set(tempId, created.id);
|
|
42
49
|
// Replace temp task with created task from server
|
|
43
50
|
this.data = this.data.map((t) => (t.id === tempId ? created : t));
|
|
44
51
|
return created;
|
|
@@ -48,71 +55,62 @@ export class TaskState {
|
|
|
48
55
|
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
49
56
|
this.error = errorObj;
|
|
50
57
|
throw errorObj;
|
|
58
|
+
} finally {
|
|
59
|
+
this.pendingCreations.delete(tempId);
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
async updateTask(taskId: string, updates: Partial<Task>) {
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
64
|
+
let targetId = taskId;
|
|
65
|
+
if (this.tempToRealIdMap.has(taskId)) {
|
|
66
|
+
targetId = this.tempToRealIdMap.get(taskId)!;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// 1. Immediately apply optimistic updates to the local task (temp or confirmed)
|
|
70
|
+
const idx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
|
|
71
|
+
let previousTask: Task | undefined;
|
|
72
|
+
if (idx !== -1) {
|
|
73
|
+
previousTask = this.data[idx];
|
|
74
|
+
const optimisticTask: Task = {
|
|
75
|
+
...previousTask,
|
|
76
|
+
...updates,
|
|
77
|
+
updatedAt: new Date().toISOString()
|
|
78
|
+
};
|
|
79
|
+
const newData = [...this.data];
|
|
80
|
+
newData[idx] = optimisticTask;
|
|
81
|
+
this.data = newData;
|
|
82
|
+
}
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
// 2. If it's a temporary task, await pending creation if in flight
|
|
85
|
+
if (isTempTaskId(targetId)) {
|
|
86
|
+
const pendingPromise =
|
|
87
|
+
this.pendingCreations.get(targetId) ||
|
|
88
|
+
(this.pendingCreations.size === 1
|
|
89
|
+
? Array.from(this.pendingCreations.values())[0]
|
|
90
|
+
: undefined);
|
|
91
|
+
|
|
92
|
+
if (pendingPromise) {
|
|
93
|
+
try {
|
|
94
|
+
const created = await pendingPromise;
|
|
95
|
+
if (created && created.id) {
|
|
96
|
+
targetId = created.id;
|
|
97
|
+
this.tempToRealIdMap.set(taskId, targetId);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
throw new Error(`Failed to update task: task creation failed for "${taskId}"`);
|
|
101
|
+
}
|
|
88
102
|
}
|
|
89
|
-
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
90
|
-
this.error = errorObj;
|
|
91
|
-
throw errorObj;
|
|
92
103
|
}
|
|
93
|
-
}
|
|
94
104
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return
|
|
105
|
+
// If still a temp ID, task exists purely in local client state; avoid sending unresolvable temp ID
|
|
106
|
+
if (isTempTaskId(targetId)) {
|
|
107
|
+
const currentTask = this.data.find((t) => t.id === targetId || t.id === taskId);
|
|
108
|
+
return (currentTask || { id: targetId, ...updates }) as Task;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
const previousTask = this.data[idx];
|
|
102
|
-
const optimisticTask: Task = {
|
|
103
|
-
...previousTask,
|
|
104
|
-
status,
|
|
105
|
-
updatedAt: new Date().toISOString()
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// Optimistically update local task status
|
|
109
|
-
const newData = [...this.data];
|
|
110
|
-
newData[idx] = optimisticTask;
|
|
111
|
-
this.data = newData;
|
|
112
|
-
|
|
113
111
|
try {
|
|
114
|
-
const updated = await this.client.updateTask(
|
|
115
|
-
const currentIdx = this.data.findIndex((t) => t.id === taskId);
|
|
112
|
+
const updated = await this.client.updateTask(targetId, updates);
|
|
113
|
+
const currentIdx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
|
|
116
114
|
if (currentIdx !== -1) {
|
|
117
115
|
const confirmedData = [...this.data];
|
|
118
116
|
confirmedData[currentIdx] = updated;
|
|
@@ -121,11 +119,13 @@ export class TaskState {
|
|
|
121
119
|
return updated;
|
|
122
120
|
} catch (err) {
|
|
123
121
|
// Rollback to previous task on error
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
if (previousTask) {
|
|
123
|
+
const rollbackIdx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
|
|
124
|
+
if (rollbackIdx !== -1) {
|
|
125
|
+
const rollbackData = [...this.data];
|
|
126
|
+
rollbackData[rollbackIdx] = previousTask;
|
|
127
|
+
this.data = rollbackData;
|
|
128
|
+
}
|
|
129
129
|
}
|
|
130
130
|
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
131
131
|
this.error = errorObj;
|
|
@@ -133,24 +133,49 @@ export class TaskState {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
async updateTaskStatus(taskId: string, status: TaskStatus) {
|
|
137
|
+
return this.updateTask(taskId, { status });
|
|
138
|
+
}
|
|
139
|
+
|
|
136
140
|
async deleteTask(taskId: string) {
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
141
|
+
let targetId = taskId;
|
|
142
|
+
if (this.tempToRealIdMap.has(taskId)) {
|
|
143
|
+
targetId = this.tempToRealIdMap.get(taskId)!;
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
const
|
|
146
|
+
const idx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
|
|
147
|
+
const previousTask = idx !== -1 ? this.data[idx] : undefined;
|
|
143
148
|
|
|
144
|
-
// Optimistically remove task
|
|
145
|
-
|
|
149
|
+
// Optimistically remove task from local state
|
|
150
|
+
if (idx !== -1) {
|
|
151
|
+
this.data = this.data.filter((t) => t.id !== targetId && t.id !== taskId);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// If temporary task:
|
|
155
|
+
if (isTempTaskId(targetId)) {
|
|
156
|
+
const pendingPromise = this.pendingCreations.get(targetId);
|
|
157
|
+
if (pendingPromise) {
|
|
158
|
+
try {
|
|
159
|
+
const created = await pendingPromise;
|
|
160
|
+
if (created && created.id) {
|
|
161
|
+
await this.client.deleteTask(created.id);
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// If creation failed anyway, task is already gone
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
146
169
|
|
|
147
170
|
try {
|
|
148
|
-
await this.client.deleteTask(
|
|
171
|
+
await this.client.deleteTask(targetId);
|
|
149
172
|
} catch (err) {
|
|
150
173
|
// Rollback on error by re-inserting at previous index
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
174
|
+
if (previousTask && idx !== -1) {
|
|
175
|
+
const restoredData = [...this.data];
|
|
176
|
+
restoredData.splice(idx, 0, previousTask);
|
|
177
|
+
this.data = restoredData;
|
|
178
|
+
}
|
|
154
179
|
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
155
180
|
this.error = errorObj;
|
|
156
181
|
throw errorObj;
|