@critical-path/svelte 0.18.0 → 0.18.1

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/src/index.ts DELETED
@@ -1,23 +0,0 @@
1
- import { CriticalPathClient, type ClientOptions } from '@critical-path/client';
2
-
3
- export function createCriticalPathClient(options: ClientOptions): CriticalPathClient {
4
- return new CriticalPathClient(options);
5
- }
6
-
7
- export * from './project-state.svelte.js';
8
- export * from './task-state.svelte.js';
9
- export * from './workflow-state.svelte.js';
10
- export * from './comment-state.svelte.js';
11
- export * from './attachment-state.svelte.js';
12
- export * from './activity-state.svelte.js';
13
- export * from './deliverable-state.svelte.js';
14
- export * from './deliverable-summary-state.svelte.js';
15
- export * from './kanban-state.svelte.js';
16
- export * from './task-transitions-state.svelte.js';
17
- export * from './webmcp-state.svelte.js';
18
- export * from './critical-path-state.svelte.js';
19
- export * from './timeline-ladder-state.svelte.js';
20
- export * from './task-metrics-state.svelte.js';
21
- export * from './workload-state.svelte.js';
22
-
23
-
@@ -1,113 +0,0 @@
1
- /// <reference types="svelte" />
2
- import type { CreateTaskBody } from '@critical-path/core/schemas';
3
- import type { CriticalPathClient } from '@critical-path/client';
4
- import type { Task, TaskStatus, StatusDefinition, SemanticStatus } from '@critical-path/core';
5
- import { resolveStatusDefinition } from '@critical-path/core';
6
- import { TaskState } from './task-state.svelte.js';
7
-
8
- export interface KanbanStateOptions {
9
- groupBy?: 'workflow' | 'semantic';
10
- customDefinitions?: StatusDefinition[];
11
- }
12
-
13
- export class KanbanState {
14
- private taskState: TaskState;
15
-
16
- get data(): Task[] {
17
- return this.taskState.data;
18
- }
19
-
20
- get tasks(): Task[] {
21
- return this.taskState.data;
22
- }
23
-
24
- get loading(): boolean {
25
- return this.taskState.loading;
26
- }
27
-
28
- get error(): Error | null {
29
- return this.taskState.error;
30
- }
31
-
32
- columns = $derived.by<Record<string, Task[]>>(() => {
33
- const tasks = this.taskState.data;
34
- if (this.options?.groupBy === 'semantic') {
35
- const semanticCols: Record<SemanticStatus, Task[]> = {
36
- not_started: [],
37
- in_progress: [],
38
- completed: [],
39
- canceled: []
40
- };
41
- for (const task of tasks) {
42
- const category =
43
- task.semanticStatus ||
44
- resolveStatusDefinition(task.status, this.options?.customDefinitions).category;
45
- if (semanticCols[category]) {
46
- semanticCols[category].push(task);
47
- } else {
48
- semanticCols.not_started.push(task);
49
- }
50
- }
51
- return semanticCols;
52
- }
53
-
54
- const workflowCols: Record<string, Task[]> = {};
55
- if (this.options?.customDefinitions && this.options.customDefinitions.length > 0) {
56
- for (const def of this.options.customDefinitions) {
57
- workflowCols[def.key] = [];
58
- }
59
- } else {
60
- workflowCols.backlog = [];
61
- workflowCols.todo = [];
62
- workflowCols.in_progress = [];
63
- workflowCols.in_review = [];
64
- workflowCols.done = [];
65
- workflowCols.canceled = [];
66
- }
67
-
68
- for (const task of tasks) {
69
- if (!workflowCols[task.status]) {
70
- workflowCols[task.status] = [];
71
- }
72
- workflowCols[task.status].push(task);
73
- }
74
-
75
- return workflowCols;
76
- });
77
-
78
- constructor(
79
- client: CriticalPathClient,
80
- public projectId?: string,
81
- public options: KanbanStateOptions = {}
82
- ) {
83
- this.taskState = new TaskState(client, projectId);
84
- }
85
-
86
- async fetch() {
87
- return this.taskState.fetch();
88
- }
89
-
90
- async createTask(input: CreateTaskBody) {
91
- return this.taskState.createTask(input);
92
- }
93
-
94
- async moveTask(taskId: string, targetStatus: TaskStatus) {
95
- return this.taskState.updateTaskStatus(taskId, targetStatus);
96
- }
97
-
98
- async updateTaskStatus(taskId: string, status: TaskStatus) {
99
- return this.taskState.updateTaskStatus(taskId, status);
100
- }
101
-
102
- async deleteTask(taskId: string) {
103
- return this.taskState.deleteTask(taskId);
104
- }
105
- }
106
-
107
- export function createKanbanState(
108
- client: CriticalPathClient,
109
- projectId?: string,
110
- options?: KanbanStateOptions
111
- ): KanbanState {
112
- return new KanbanState(client, projectId, options);
113
- }
@@ -1,24 +0,0 @@
1
- import type { CriticalPathClient } from '@critical-path/client';
2
-
3
- /**
4
- * Tracks the latest request for one fetch method. Starting a new request aborts the previous
5
- * one, so a slow response for old inputs can never overwrite newer state.
6
- */
7
- export class LatestRequest {
8
- private controller?: AbortController;
9
-
10
- /** Aborts any in-flight request and returns a client and signal for the new one. */
11
- begin(client: CriticalPathClient): { api: CriticalPathClient; signal: AbortSignal } {
12
- this.controller?.abort();
13
- this.controller = new AbortController();
14
- const { signal } = this.controller;
15
- // Duck-typed clients without with() still work; their requests just are not cancelled.
16
- const api = typeof client.with === 'function' ? client.with({ signal }) : client;
17
- return { api, signal };
18
- }
19
-
20
- cancel(): void {
21
- this.controller?.abort();
22
- this.controller = undefined;
23
- }
24
- }
@@ -1,64 +0,0 @@
1
- /// <reference types="svelte" />
2
- import type { CriticalPathClient } from '@critical-path/client';
3
- import type { Project } from '@critical-path/core';
4
- import { LatestRequest } from './latest-request.js';
5
-
6
- export class ProjectState {
7
- #fetchRequest = new LatestRequest();
8
- data = $state<Project[]>([]);
9
- loading = $state<boolean>(false);
10
- error = $state<Error | null>(null);
11
-
12
- constructor(private client: CriticalPathClient) {}
13
-
14
- async fetch() {
15
- const { api, signal } = this.#fetchRequest.begin(this.client);
16
- this.loading = true;
17
- this.error = null;
18
- try {
19
- this.data = await api.getProjects();
20
- if (signal.aborted) return;
21
- } catch (err) {
22
- if (signal.aborted) return;
23
- this.error = err instanceof Error ? err : new Error(String(err));
24
- } finally {
25
- if (!signal.aborted) this.loading = false;
26
- }
27
- }
28
-
29
- async createProject(input: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>) {
30
- const tempId = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
31
- const now = new Date().toISOString();
32
- const tempProject: Project = {
33
- ...input,
34
- id: tempId,
35
- createdAt: now,
36
- updatedAt: now
37
- };
38
-
39
- // Optimistically add temp project
40
- this.data = [...this.data, tempProject];
41
-
42
- try {
43
- const created = await this.client.createProject(input);
44
- // Replace temp project with server created project
45
- this.data = this.data.map((p) => (p.id === tempId ? created : p));
46
- return created;
47
- } catch (err) {
48
- // Rollback on error
49
- this.data = this.data.filter((p) => p.id !== tempId);
50
- const errorObj = err instanceof Error ? err : new Error(String(err));
51
- this.error = errorObj;
52
- throw errorObj;
53
- }
54
- }
55
-
56
- /** Cancels in-flight requests, e.g. from a component's onDestroy. */
57
- destroy() {
58
- this.#fetchRequest.cancel();
59
- }
60
- }
61
-
62
- export function createProjectState(client: CriticalPathClient): ProjectState {
63
- return new ProjectState(client);
64
- }
@@ -1,103 +0,0 @@
1
- /// <reference types="svelte" />
2
- import type { CriticalPathClient } from '@critical-path/client';
3
- import { LatestRequest } from './latest-request.js';
4
- import type {
5
- TaskMetrics,
6
- TaskProgressHistory,
7
- TaskInferredActuals,
8
- RealityDelta,
9
- TaskProgressInference,
10
- TaskEVM,
11
- ProgressCurveProfile
12
- } from '@critical-path/core';
13
-
14
- export class TaskMetricsState {
15
- #fetchMetricsRequest = new LatestRequest();
16
- #fetchAllRequest = new LatestRequest();
17
- #fetchHistoryRequest = new LatestRequest();
18
- metrics = $state<TaskMetrics | null>(null);
19
- history = $state<TaskProgressHistory | null>(null);
20
- loading = $state<boolean>(false);
21
- error = $state<Error | null>(null);
22
-
23
- inferredActuals = $derived<TaskInferredActuals | null>(this.metrics?.inferredActuals ?? null);
24
- realityDelta = $derived<RealityDelta | null>(this.metrics?.realityDelta ?? null);
25
- progress = $derived<TaskProgressInference | null>(this.metrics?.progress ?? null);
26
- evm = $derived<TaskEVM | null>(this.metrics?.evm ?? null);
27
- curveProfile = $derived<ProgressCurveProfile | null>(this.history?.curveProfile ?? null);
28
-
29
- constructor(
30
- private client: CriticalPathClient,
31
- public taskId: string
32
- ) {}
33
-
34
- async fetchMetrics(): Promise<TaskMetrics | null> {
35
- const { api, signal } = this.#fetchMetricsRequest.begin(this.client);
36
- this.loading = true;
37
- this.error = null;
38
- try {
39
- this.metrics = await api.getTaskMetrics(this.taskId);
40
- if (signal.aborted) return null;
41
- return this.metrics;
42
- } catch (err) {
43
- if (signal.aborted) return null;
44
- this.error = err instanceof Error ? err : new Error(String(err));
45
- return null;
46
- } finally {
47
- if (!signal.aborted) this.loading = false;
48
- }
49
- }
50
-
51
- async fetchHistory(): Promise<TaskProgressHistory | null> {
52
- const { api, signal } = this.#fetchHistoryRequest.begin(this.client);
53
- this.loading = true;
54
- this.error = null;
55
- try {
56
- this.history = await api.getTaskProgressHistory(this.taskId);
57
- if (signal.aborted) return null;
58
- return this.history;
59
- } catch (err) {
60
- if (signal.aborted) return null;
61
- this.error = err instanceof Error ? err : new Error(String(err));
62
- return null;
63
- } finally {
64
- if (!signal.aborted) this.loading = false;
65
- }
66
- }
67
-
68
- async fetchAll(): Promise<{ metrics: TaskMetrics | null; history: TaskProgressHistory | null }> {
69
- const { api, signal } = this.#fetchAllRequest.begin(this.client);
70
- this.loading = true;
71
- this.error = null;
72
- try {
73
- const [m, h] = await Promise.all([
74
- api.getTaskMetrics(this.taskId),
75
- api.getTaskProgressHistory(this.taskId)
76
- ]);
77
- if (signal.aborted) return { metrics: null, history: null };
78
- this.metrics = m;
79
- this.history = h;
80
- return { metrics: m, history: h };
81
- } catch (err) {
82
- if (signal.aborted) return { metrics: null, history: null };
83
- this.error = err instanceof Error ? err : new Error(String(err));
84
- return { metrics: null, history: null };
85
- } finally {
86
- if (!signal.aborted) this.loading = false;
87
- }
88
- }
89
-
90
- /** Cancels in-flight requests, e.g. from a component's onDestroy. */
91
- destroy() {
92
- this.#fetchMetricsRequest.cancel();
93
- this.#fetchAllRequest.cancel();
94
- this.#fetchHistoryRequest.cancel();
95
- }
96
- }
97
-
98
- export function createTaskMetricsState(
99
- client: CriticalPathClient,
100
- taskId: string
101
- ): TaskMetricsState {
102
- return new TaskMetricsState(client, taskId);
103
- }
@@ -1,205 +0,0 @@
1
- /// <reference types="svelte" />
2
- import type { CreateTaskBody, UpdateTaskBody } from '@critical-path/core/schemas';
3
- import type { CriticalPathClient } from '@critical-path/client';
4
- import { type Task, type TaskStatus, isTempTaskId } from '@critical-path/core';
5
- import { LatestRequest } from './latest-request.js';
6
-
7
- export class TaskState {
8
- #fetchRequest = new LatestRequest();
9
- data = $state<Task[]>([]);
10
- loading = $state<boolean>(false);
11
- error = $state<Error | null>(null);
12
-
13
- private pendingCreations = new Map<string, Promise<Task>>();
14
- private tempToRealIdMap = new Map<string, string>();
15
-
16
- constructor(
17
- private client: CriticalPathClient,
18
- public projectId?: string
19
- ) {}
20
-
21
- async fetch() {
22
- const { api, signal } = this.#fetchRequest.begin(this.client);
23
- this.loading = true;
24
- this.error = null;
25
- try {
26
- this.data = await api.getTasks(this.projectId);
27
- if (signal.aborted) return;
28
- } catch (err) {
29
- if (signal.aborted) return;
30
- this.error = err instanceof Error ? err : new Error(String(err));
31
- } finally {
32
- if (!signal.aborted) this.loading = false;
33
- }
34
- }
35
-
36
- async createTask(input: CreateTaskBody) {
37
- const tempId = `temp_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
38
- const now = new Date().toISOString();
39
- const tempTask: Task = {
40
- ...input,
41
- // Placeholders until the server responds with the workflow's real defaults
42
- status: input.status ?? 'todo',
43
- priority: input.priority ?? 'medium',
44
- id: tempId,
45
- createdAt: now,
46
- updatedAt: now
47
- };
48
-
49
- // Optimistically add temp task
50
- this.data = [...this.data, tempTask];
51
-
52
- const promise = this.client.createTask(input);
53
- this.pendingCreations.set(tempId, promise);
54
-
55
- try {
56
- const created = await promise;
57
- this.tempToRealIdMap.set(tempId, created.id);
58
- // Replace temp task with created task from server
59
- this.data = this.data.map((t) => (t.id === tempId ? created : t));
60
- return created;
61
- } catch (err) {
62
- // Rollback on error
63
- this.data = this.data.filter((t) => t.id !== tempId);
64
- const errorObj = err instanceof Error ? err : new Error(String(err));
65
- this.error = errorObj;
66
- throw errorObj;
67
- } finally {
68
- this.pendingCreations.delete(tempId);
69
- }
70
- }
71
-
72
- async updateTask(taskId: string, updates: UpdateTaskBody) {
73
- let targetId = taskId;
74
- if (this.tempToRealIdMap.has(taskId)) {
75
- targetId = this.tempToRealIdMap.get(taskId)!;
76
- }
77
-
78
- // 1. Immediately apply optimistic updates to the local task (temp or confirmed)
79
- const idx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
80
- let previousTask: Task | undefined;
81
- if (idx !== -1) {
82
- previousTask = this.data[idx];
83
- const optimisticTask: Task = {
84
- ...previousTask,
85
- ...updates,
86
- updatedAt: new Date().toISOString()
87
- };
88
- const newData = [...this.data];
89
- newData[idx] = optimisticTask;
90
- this.data = newData;
91
- }
92
-
93
- // 2. If it's a temporary task, await pending creation if in flight
94
- if (isTempTaskId(targetId)) {
95
- const pendingPromise =
96
- this.pendingCreations.get(targetId) ||
97
- (this.pendingCreations.size === 1
98
- ? Array.from(this.pendingCreations.values())[0]
99
- : undefined);
100
-
101
- if (pendingPromise) {
102
- try {
103
- const created = await pendingPromise;
104
- if (created && created.id) {
105
- targetId = created.id;
106
- this.tempToRealIdMap.set(taskId, targetId);
107
- }
108
- } catch {
109
- throw new Error(`Failed to update task: task creation failed for "${taskId}"`);
110
- }
111
- }
112
- }
113
-
114
- // If still a temp ID, task exists purely in local client state; avoid sending unresolvable temp ID
115
- if (isTempTaskId(targetId)) {
116
- const currentTask = this.data.find((t) => t.id === targetId || t.id === taskId);
117
- return (currentTask || { id: targetId, ...updates }) as Task;
118
- }
119
-
120
- try {
121
- const updated = await this.client.updateTask(targetId, updates);
122
- const currentIdx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
123
- if (currentIdx !== -1) {
124
- const confirmedData = [...this.data];
125
- confirmedData[currentIdx] = updated;
126
- this.data = confirmedData;
127
- }
128
- return updated;
129
- } catch (err) {
130
- // Rollback to previous task on error
131
- if (previousTask) {
132
- const rollbackIdx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
133
- if (rollbackIdx !== -1) {
134
- const rollbackData = [...this.data];
135
- rollbackData[rollbackIdx] = previousTask;
136
- this.data = rollbackData;
137
- }
138
- }
139
- const errorObj = err instanceof Error ? err : new Error(String(err));
140
- this.error = errorObj;
141
- throw errorObj;
142
- }
143
- }
144
-
145
- async updateTaskStatus(taskId: string, status: TaskStatus) {
146
- return this.updateTask(taskId, { status });
147
- }
148
-
149
- async deleteTask(taskId: string) {
150
- let targetId = taskId;
151
- if (this.tempToRealIdMap.has(taskId)) {
152
- targetId = this.tempToRealIdMap.get(taskId)!;
153
- }
154
-
155
- const idx = this.data.findIndex((t) => t.id === targetId || t.id === taskId);
156
- const previousTask = idx !== -1 ? this.data[idx] : undefined;
157
-
158
- // Optimistically remove task from local state
159
- if (idx !== -1) {
160
- this.data = this.data.filter((t) => t.id !== targetId && t.id !== taskId);
161
- }
162
-
163
- // If temporary task:
164
- if (isTempTaskId(targetId)) {
165
- const pendingPromise = this.pendingCreations.get(targetId);
166
- if (pendingPromise) {
167
- try {
168
- const created = await pendingPromise;
169
- if (created && created.id) {
170
- await this.client.deleteTask(created.id);
171
- }
172
- } catch {
173
- // If creation failed anyway, task is already gone
174
- }
175
- }
176
- return;
177
- }
178
-
179
- try {
180
- await this.client.deleteTask(targetId);
181
- } catch (err) {
182
- // Rollback on error by re-inserting at previous index
183
- if (previousTask && idx !== -1) {
184
- const restoredData = [...this.data];
185
- restoredData.splice(idx, 0, previousTask);
186
- this.data = restoredData;
187
- }
188
- const errorObj = err instanceof Error ? err : new Error(String(err));
189
- this.error = errorObj;
190
- throw errorObj;
191
- }
192
- }
193
-
194
- /** Cancels in-flight requests, e.g. from a component's onDestroy. */
195
- destroy() {
196
- this.#fetchRequest.cancel();
197
- }
198
- }
199
-
200
- export function createTaskState(
201
- client: CriticalPathClient,
202
- projectId?: string
203
- ): TaskState {
204
- return new TaskState(client, projectId);
205
- }
@@ -1,46 +0,0 @@
1
- /// <reference types="svelte" />
2
- import type { CriticalPathClient } from '@critical-path/client';
3
- import { LatestRequest } from './latest-request.js';
4
-
5
- export class TaskTransitionsState {
6
- #fetchRequest = new LatestRequest();
7
- allowedTransitions = $state<string[]>([]);
8
- loading = $state<boolean>(false);
9
- error = $state<Error | null>(null);
10
-
11
- constructor(private client: CriticalPathClient, public taskId?: string) {}
12
-
13
- async fetch(taskId?: string) {
14
- const { api, signal } = this.#fetchRequest.begin(this.client);
15
- const targetTaskId = taskId || this.taskId;
16
- if (!targetTaskId) {
17
- this.allowedTransitions = [];
18
- this.loading = false;
19
- return;
20
- }
21
- this.taskId = targetTaskId;
22
- this.loading = true;
23
- this.error = null;
24
- try {
25
- this.allowedTransitions = await api.getAllowedTaskTransitions(targetTaskId);
26
- if (signal.aborted) return;
27
- } catch (err) {
28
- if (signal.aborted) return;
29
- this.error = err instanceof Error ? err : new Error(String(err));
30
- } finally {
31
- if (!signal.aborted) this.loading = false;
32
- }
33
- }
34
-
35
- /** Cancels in-flight requests, e.g. from a component's onDestroy. */
36
- destroy() {
37
- this.#fetchRequest.cancel();
38
- }
39
- }
40
-
41
- export function createTaskTransitionsState(
42
- client: CriticalPathClient,
43
- taskId?: string
44
- ): TaskTransitionsState {
45
- return new TaskTransitionsState(client, taskId);
46
- }
@@ -1,68 +0,0 @@
1
- /// <reference types="svelte" />
2
- import type { CriticalPathClient } from '@critical-path/client';
3
- import { LatestRequest } from './latest-request.js';
4
- import type {
5
- TimelineLadder,
6
- TimelineLadderOptions,
7
- AbstractionLevel,
8
- MacroTimelineSummary,
9
- ConcreteTaskEvidence
10
- } from '@critical-path/core';
11
-
12
- export class TimelineLadderState {
13
- #fetchRequest = new LatestRequest();
14
- data = $state<TimelineLadder | null>(null);
15
- level = $state<AbstractionLevel>('all');
16
- loading = $state<boolean>(false);
17
- error = $state<Error | null>(null);
18
-
19
- macro = $derived<MacroTimelineSummary | null>(this.data?.macro ?? null);
20
- standard = $derived<TimelineLadder['standard'] | null>(this.data?.standard ?? null);
21
- concrete = $derived<Record<string, ConcreteTaskEvidence> | null>(this.data?.concrete ?? null);
22
-
23
- constructor(
24
- private client: CriticalPathClient,
25
- public projectId: string,
26
- initialOptions?: TimelineLadderOptions
27
- ) {
28
- if (initialOptions?.level) {
29
- this.level = initialOptions.level;
30
- }
31
- }
32
-
33
- async setLevel(newLevel: AbstractionLevel) {
34
- this.level = newLevel;
35
- await this.fetch();
36
- }
37
-
38
- async fetch(options?: TimelineLadderOptions) {
39
- const { api, signal } = this.#fetchRequest.begin(this.client);
40
- this.loading = true;
41
- this.error = null;
42
- try {
43
- this.data = await api.getTimelineLadder(this.projectId, {
44
- level: this.level,
45
- ...options
46
- });
47
- if (signal.aborted) return;
48
- } catch (err) {
49
- if (signal.aborted) return;
50
- this.error = err instanceof Error ? err : new Error(String(err));
51
- } finally {
52
- if (!signal.aborted) this.loading = false;
53
- }
54
- }
55
-
56
- /** Cancels in-flight requests, e.g. from a component's onDestroy. */
57
- destroy() {
58
- this.#fetchRequest.cancel();
59
- }
60
- }
61
-
62
- export function createTimelineLadderState(
63
- client: CriticalPathClient,
64
- projectId: string,
65
- initialOptions?: TimelineLadderOptions
66
- ): TimelineLadderState {
67
- return new TimelineLadderState(client, projectId, initialOptions);
68
- }