@plandesk/api 0.6.0 → 0.8.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/dist/events.d.ts +15 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/routes/documents.js +2 -0
- package/dist/routes/folders.d.ts +4 -0
- package/dist/routes/folders.js +69 -0
- package/dist/routes/projects.js +12 -3
- package/dist/routes/tags.d.ts +4 -0
- package/dist/routes/tags.js +62 -0
- package/dist/routes/tasks.d.ts +1 -0
- package/dist/routes/tasks.js +9 -1
- package/dist/serialize.d.ts +30 -2
- package/dist/serialize.js +66 -1
- package/dist/server.js +5 -1
- package/dist/services/canvas.d.ts +2 -0
- package/dist/services/canvas.js +1 -1
- package/dist/services/documents.d.ts +5 -1
- package/dist/services/documents.js +31 -2
- package/dist/services/folders.d.ts +27 -0
- package/dist/services/folders.js +107 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +6 -0
- package/dist/services/projects.js +5 -2
- package/dist/services/tags.d.ts +27 -0
- package/dist/services/tags.js +79 -0
- package/dist/services/tasks.d.ts +23 -1
- package/dist/services/tasks.js +76 -25
- package/package.json +3 -3
- package/web/assets/{index-cyyf9-Uw.js → index-DSmNB4RA.js} +61 -60
- package/web/index.html +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createFolder as dbCreateFolder, deleteFolder as dbDeleteFolder, getFolder as dbGetFolder, getProject, listFolders as dbListFolders, moveDocumentsToFolder, reparentChildFolders, updateFolder as dbUpdateFolder, } from '@plandesk/db';
|
|
2
|
+
import { serializeFolder } from '../serialize.js';
|
|
3
|
+
export class InvalidFolderError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'InvalidFolderError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function assertNonEmptyName(name) {
|
|
10
|
+
if (name.trim() === '') {
|
|
11
|
+
throw new InvalidFolderError('Folder name must not be empty');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function assertParentInProject(db, projectId, parentFolderId) {
|
|
15
|
+
const parent = dbGetFolder(db, parentFolderId);
|
|
16
|
+
if (!parent || parent.projectId !== projectId) {
|
|
17
|
+
throw new InvalidFolderError('Parent folder does not belong to project');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function assertNoCycle(db, folderId, newParentFolderId) {
|
|
21
|
+
const visited = new Set();
|
|
22
|
+
let current = newParentFolderId;
|
|
23
|
+
while (current !== null) {
|
|
24
|
+
if (current === folderId) {
|
|
25
|
+
throw new InvalidFolderError('Re-parenting would create a folder cycle');
|
|
26
|
+
}
|
|
27
|
+
if (visited.has(current)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
visited.add(current);
|
|
31
|
+
current = dbGetFolder(db, current)?.parentFolderId ?? null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function createFolderService(deps) {
|
|
35
|
+
const { db, eventBus } = deps;
|
|
36
|
+
return {
|
|
37
|
+
list(projectId) {
|
|
38
|
+
const project = getProject(db, projectId);
|
|
39
|
+
if (!project) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return dbListFolders(db, projectId).map(serializeFolder);
|
|
43
|
+
},
|
|
44
|
+
create(projectId, input) {
|
|
45
|
+
const project = getProject(db, projectId);
|
|
46
|
+
if (!project) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
assertNonEmptyName(input.name);
|
|
50
|
+
if (input.parentFolderId !== undefined && input.parentFolderId !== null) {
|
|
51
|
+
assertParentInProject(db, projectId, input.parentFolderId);
|
|
52
|
+
}
|
|
53
|
+
const folder = dbCreateFolder(db, {
|
|
54
|
+
projectId,
|
|
55
|
+
name: input.name,
|
|
56
|
+
parentFolderId: input.parentFolderId,
|
|
57
|
+
});
|
|
58
|
+
eventBus.emit({ type: 'folder_created', folderId: folder.id, projectId });
|
|
59
|
+
return serializeFolder(folder);
|
|
60
|
+
},
|
|
61
|
+
get(id) {
|
|
62
|
+
const folder = dbGetFolder(db, id);
|
|
63
|
+
if (!folder) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return serializeFolder(folder);
|
|
67
|
+
},
|
|
68
|
+
update(id, input) {
|
|
69
|
+
const existing = dbGetFolder(db, id);
|
|
70
|
+
if (!existing) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if (input.name !== undefined) {
|
|
74
|
+
assertNonEmptyName(input.name);
|
|
75
|
+
}
|
|
76
|
+
if (input.parentFolderId !== undefined && input.parentFolderId !== null) {
|
|
77
|
+
if (input.parentFolderId === id) {
|
|
78
|
+
throw new InvalidFolderError('Folder cannot be its own parent');
|
|
79
|
+
}
|
|
80
|
+
assertParentInProject(db, existing.projectId, input.parentFolderId);
|
|
81
|
+
assertNoCycle(db, id, input.parentFolderId);
|
|
82
|
+
}
|
|
83
|
+
const folder = dbUpdateFolder(db, id, input);
|
|
84
|
+
if (!folder) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
eventBus.emit({ type: 'folder_updated', folderId: folder.id, projectId: folder.projectId });
|
|
88
|
+
return serializeFolder(folder);
|
|
89
|
+
},
|
|
90
|
+
delete(id) {
|
|
91
|
+
const existing = dbGetFolder(db, id);
|
|
92
|
+
if (!existing) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
// Never orphan: children folders and contained documents move to the
|
|
96
|
+
// deleted folder's parent (or root when the folder was at root).
|
|
97
|
+
db.transaction((tx) => {
|
|
98
|
+
reparentChildFolders(tx, id, existing.parentFolderId);
|
|
99
|
+
moveDocumentsToFolder(tx, id, existing.parentFolderId);
|
|
100
|
+
dbDeleteFolder(tx, id);
|
|
101
|
+
});
|
|
102
|
+
eventBus.emit({ type: 'folder_updated', folderId: id, projectId: existing.projectId });
|
|
103
|
+
return true;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=folders.js.map
|
package/dist/services/index.d.ts
CHANGED
|
@@ -3,9 +3,11 @@ import { type EventBus } from '../events.js';
|
|
|
3
3
|
import { type CanvasService } from './canvas.js';
|
|
4
4
|
import { type CommentService } from './comments.js';
|
|
5
5
|
import { type DocumentService } from './documents.js';
|
|
6
|
+
import { type FolderService } from './folders.js';
|
|
6
7
|
import { type NoteService } from './notes.js';
|
|
7
8
|
import { type ProjectService } from './projects.js';
|
|
8
9
|
import { type AgentRunService } from './agent-runs.js';
|
|
10
|
+
import { type TagService } from './tags.js';
|
|
9
11
|
import { type TaskService } from './tasks.js';
|
|
10
12
|
import { type TokenService } from './tokens.js';
|
|
11
13
|
import { type ShareService } from './share.js';
|
|
@@ -18,8 +20,10 @@ export type Services = {
|
|
|
18
20
|
eventBus: EventBus;
|
|
19
21
|
projectService: ProjectService;
|
|
20
22
|
taskService: TaskService;
|
|
23
|
+
tagService: TagService;
|
|
21
24
|
canvasService: CanvasService;
|
|
22
25
|
documentService: DocumentService;
|
|
26
|
+
folderService: FolderService;
|
|
23
27
|
noteService: NoteService;
|
|
24
28
|
commentService: CommentService;
|
|
25
29
|
agentRunService: AgentRunService;
|
package/dist/services/index.js
CHANGED
|
@@ -2,9 +2,11 @@ import { createEventBus } from '../events.js';
|
|
|
2
2
|
import { createCanvasService } from './canvas.js';
|
|
3
3
|
import { createCommentService } from './comments.js';
|
|
4
4
|
import { createDocumentService } from './documents.js';
|
|
5
|
+
import { createFolderService } from './folders.js';
|
|
5
6
|
import { createNoteService } from './notes.js';
|
|
6
7
|
import { createProjectService } from './projects.js';
|
|
7
8
|
import { createAgentRunService } from './agent-runs.js';
|
|
9
|
+
import { createTagService } from './tags.js';
|
|
8
10
|
import { createTaskService } from './tasks.js';
|
|
9
11
|
import { createTokenService } from './tokens.js';
|
|
10
12
|
import { createShareService } from './share.js';
|
|
@@ -13,8 +15,10 @@ export function createServices(deps) {
|
|
|
13
15
|
const eventBus = deps.eventBus ?? createEventBus();
|
|
14
16
|
const projectService = createProjectService({ db: deps.db, eventBus });
|
|
15
17
|
const taskService = createTaskService({ db: deps.db, eventBus });
|
|
18
|
+
const tagService = createTagService({ db: deps.db, eventBus });
|
|
16
19
|
const canvasService = createCanvasService({ db: deps.db, eventBus });
|
|
17
20
|
const documentService = createDocumentService({ db: deps.db, eventBus });
|
|
21
|
+
const folderService = createFolderService({ db: deps.db, eventBus });
|
|
18
22
|
const noteService = createNoteService({ db: deps.db, eventBus });
|
|
19
23
|
const commentService = createCommentService({ db: deps.db, eventBus });
|
|
20
24
|
const agentRunService = createAgentRunService({ db: deps.db, eventBus });
|
|
@@ -25,8 +29,10 @@ export function createServices(deps) {
|
|
|
25
29
|
eventBus,
|
|
26
30
|
projectService,
|
|
27
31
|
taskService,
|
|
32
|
+
tagService,
|
|
28
33
|
canvasService,
|
|
29
34
|
documentService,
|
|
35
|
+
folderService,
|
|
30
36
|
noteService,
|
|
31
37
|
commentService,
|
|
32
38
|
agentRunService,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clearDocumentParentRefsByProject, createDocument, createEdge, createProject as dbCreateProject, createTask, deleteAgentRun, deleteAgentRunEventsByRunId, deleteCommentsByProjectId, deleteDocumentsByProjectId, deleteNotesByProjectId, deleteEdgesByProjectId, deleteShareSubmissionsByProjectId, deleteSharesByProjectId, deleteSyncRemoteByProjectId, deleteSyncStateByProjectId, deleteProject as dbDeleteProject, deleteTasksByProjectId, getProject as dbGetProject, InvalidTaskStatusError, isTaskStatus, listAgentRuns, listProjects as dbListProjects, listTasks, updateProject as dbUpdateProject, } from '@plandesk/db';
|
|
1
|
+
import { clearDocumentParentRefsByProject, clearFolderParentRefsByProject, createDocument, createEdge, createProject as dbCreateProject, createTask, deleteAgentRun, deleteAgentRunEventsByRunId, deleteCommentsByProjectId, deleteDocumentsByProjectId, deleteFoldersByProjectId, deleteNotesByProjectId, deleteEdgesByProjectId, deleteShareSubmissionsByProjectId, deleteSharesByProjectId, deleteTagsByProjectId, deleteSyncRemoteByProjectId, deleteSyncStateByProjectId, deleteProject as dbDeleteProject, deleteTasksByProjectId, getProject as dbGetProject, InvalidTaskStatusError, isTaskStatus, listAgentRuns, listProjects as dbListProjects, listTasks, updateProject as dbUpdateProject, } from '@plandesk/db';
|
|
2
2
|
import { emptyTaskStatusSummary, serializeDocument, serializeEdge, serializeProject, serializeProjectDetail, serializeTask, } from '../serialize.js';
|
|
3
3
|
export class InvalidScaffoldError extends Error {
|
|
4
4
|
constructor(message) {
|
|
@@ -89,7 +89,10 @@ export function createProjectService(deps) {
|
|
|
89
89
|
clearDocumentParentRefsByProject(tx, id);
|
|
90
90
|
deleteCommentsByProjectId(tx, id);
|
|
91
91
|
deleteDocumentsByProjectId(tx, id);
|
|
92
|
+
clearFolderParentRefsByProject(tx, id);
|
|
93
|
+
deleteFoldersByProjectId(tx, id);
|
|
92
94
|
deleteNotesByProjectId(tx, id);
|
|
95
|
+
deleteTagsByProjectId(tx, id);
|
|
93
96
|
deleteTasksByProjectId(tx, id);
|
|
94
97
|
deleteShareSubmissionsByProjectId(tx, id);
|
|
95
98
|
deleteSyncStateByProjectId(tx, id);
|
|
@@ -164,7 +167,7 @@ export function createProjectService(deps) {
|
|
|
164
167
|
}
|
|
165
168
|
return {
|
|
166
169
|
project: serializeProject(project),
|
|
167
|
-
tasks: taskRows.map(serializeTask),
|
|
170
|
+
tasks: taskRows.map((task) => serializeTask(task)),
|
|
168
171
|
edges: edgeRows.map(serializeEdge),
|
|
169
172
|
documents: documentRows.map(serializeDocument),
|
|
170
173
|
key_to_id: Object.fromEntries(keyToId),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Db } from '@plandesk/db';
|
|
2
|
+
import type { EventBus } from '../events.js';
|
|
3
|
+
import { type SerializedTag } from '../serialize.js';
|
|
4
|
+
export type TagServiceDeps = {
|
|
5
|
+
db: Db;
|
|
6
|
+
eventBus: EventBus;
|
|
7
|
+
};
|
|
8
|
+
export type CreateTagInput = {
|
|
9
|
+
name: string;
|
|
10
|
+
color?: string | null;
|
|
11
|
+
};
|
|
12
|
+
export type UpdateTagInput = {
|
|
13
|
+
name?: string;
|
|
14
|
+
color?: string | null;
|
|
15
|
+
};
|
|
16
|
+
export declare class InvalidTagError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
export declare function normalizeTagName(name: string): string;
|
|
20
|
+
export declare function createTagService(deps: TagServiceDeps): {
|
|
21
|
+
list(projectId: string): SerializedTag[] | undefined;
|
|
22
|
+
create(projectId: string, input: CreateTagInput): SerializedTag | undefined;
|
|
23
|
+
update(id: string, input: UpdateTagInput): SerializedTag | undefined;
|
|
24
|
+
delete(id: string): boolean;
|
|
25
|
+
};
|
|
26
|
+
export type TagService = ReturnType<typeof createTagService>;
|
|
27
|
+
//# sourceMappingURL=tags.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createTag as dbCreateTag, deleteTag as dbDeleteTag, getProject, getTag as dbGetTag, getTagByName, listTags as dbListTags, updateTag as dbUpdateTag, } from '@plandesk/db';
|
|
2
|
+
import { serializeTag } from '../serialize.js';
|
|
3
|
+
export class InvalidTagError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'InvalidTagError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function normalizeTagName(name) {
|
|
10
|
+
const trimmed = name.trim();
|
|
11
|
+
if (trimmed === '') {
|
|
12
|
+
throw new InvalidTagError('Tag name must not be empty');
|
|
13
|
+
}
|
|
14
|
+
return trimmed;
|
|
15
|
+
}
|
|
16
|
+
export function createTagService(deps) {
|
|
17
|
+
const { db, eventBus } = deps;
|
|
18
|
+
return {
|
|
19
|
+
list(projectId) {
|
|
20
|
+
const project = getProject(db, projectId);
|
|
21
|
+
if (!project) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return dbListTags(db, projectId).map(serializeTag);
|
|
25
|
+
},
|
|
26
|
+
create(projectId, input) {
|
|
27
|
+
const project = getProject(db, projectId);
|
|
28
|
+
if (!project) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const name = normalizeTagName(input.name);
|
|
32
|
+
if (getTagByName(db, projectId, name)) {
|
|
33
|
+
throw new InvalidTagError(`Tag already exists: ${name}`);
|
|
34
|
+
}
|
|
35
|
+
const tag = dbCreateTag(db, { projectId, name, color: input.color });
|
|
36
|
+
eventBus.emit({ type: 'tag_updated', projectId });
|
|
37
|
+
return serializeTag(tag);
|
|
38
|
+
},
|
|
39
|
+
// Renaming propagates everywhere automatically: tasks reference the single
|
|
40
|
+
// tag row through the join table.
|
|
41
|
+
update(id, input) {
|
|
42
|
+
const existing = dbGetTag(db, id);
|
|
43
|
+
if (!existing) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
let name;
|
|
47
|
+
if (input.name !== undefined) {
|
|
48
|
+
name = normalizeTagName(input.name);
|
|
49
|
+
const conflict = getTagByName(db, existing.projectId, name);
|
|
50
|
+
if (conflict && conflict.id !== id) {
|
|
51
|
+
throw new InvalidTagError(`Tag already exists: ${name}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const tag = dbUpdateTag(db, id, {
|
|
55
|
+
...(name !== undefined ? { name } : {}),
|
|
56
|
+
...(input.color !== undefined ? { color: input.color } : {}),
|
|
57
|
+
});
|
|
58
|
+
if (!tag) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
eventBus.emit({ type: 'tag_updated', projectId: tag.projectId });
|
|
62
|
+
return serializeTag(tag);
|
|
63
|
+
},
|
|
64
|
+
// Deleting a tag removes it from all its tasks (cascade on the join table).
|
|
65
|
+
delete(id) {
|
|
66
|
+
const existing = dbGetTag(db, id);
|
|
67
|
+
if (!existing) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const deleted = dbDeleteTag(db, id);
|
|
71
|
+
if (!deleted) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
eventBus.emit({ type: 'tag_updated', projectId: existing.projectId });
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=tags.js.map
|
package/dist/services/tasks.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type CreateTaskInput = {
|
|
|
23
23
|
y?: number;
|
|
24
24
|
assignee?: string | null;
|
|
25
25
|
dueDate?: Date | null;
|
|
26
|
+
tags?: string[];
|
|
26
27
|
};
|
|
27
28
|
export type UpdateTaskInput = {
|
|
28
29
|
label?: string;
|
|
@@ -30,12 +31,29 @@ export type UpdateTaskInput = {
|
|
|
30
31
|
description?: string | null;
|
|
31
32
|
x?: number;
|
|
32
33
|
y?: number;
|
|
34
|
+
tags?: string[];
|
|
33
35
|
};
|
|
34
36
|
export type ListTasksFilter = {
|
|
35
37
|
status?: string;
|
|
38
|
+
tags?: string[];
|
|
36
39
|
};
|
|
37
40
|
export declare function createTaskService(deps: TaskServiceDeps): {
|
|
41
|
+
get(id: string): {
|
|
42
|
+
tags?: import("../serialize.js").SerializedTag[] | undefined;
|
|
43
|
+
id: string;
|
|
44
|
+
project_id: string;
|
|
45
|
+
label: string;
|
|
46
|
+
status: "scope" | "todo" | "in_progress" | "done" | "backlog";
|
|
47
|
+
description: string | null;
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
assignee: string | null;
|
|
51
|
+
due_date: string | null;
|
|
52
|
+
created_at: string;
|
|
53
|
+
updated_at: string;
|
|
54
|
+
} | undefined;
|
|
38
55
|
listByProject(projectId: string, filter?: ListTasksFilter, pagination?: PaginationParams): {
|
|
56
|
+
tags?: import("../serialize.js").SerializedTag[] | undefined;
|
|
39
57
|
id: string;
|
|
40
58
|
project_id: string;
|
|
41
59
|
label: string;
|
|
@@ -49,6 +67,7 @@ export declare function createTaskService(deps: TaskServiceDeps): {
|
|
|
49
67
|
updated_at: string;
|
|
50
68
|
}[] | undefined;
|
|
51
69
|
create(projectId: string, input: CreateTaskInput): {
|
|
70
|
+
tags?: import("../serialize.js").SerializedTag[] | undefined;
|
|
52
71
|
id: string;
|
|
53
72
|
project_id: string;
|
|
54
73
|
label: string;
|
|
@@ -62,6 +81,7 @@ export declare function createTaskService(deps: TaskServiceDeps): {
|
|
|
62
81
|
updated_at: string;
|
|
63
82
|
} | undefined;
|
|
64
83
|
update(id: string, input: UpdateTaskInput): {
|
|
84
|
+
tags?: import("../serialize.js").SerializedTag[] | undefined;
|
|
65
85
|
id: string;
|
|
66
86
|
project_id: string;
|
|
67
87
|
label: string;
|
|
@@ -75,7 +95,9 @@ export declare function createTaskService(deps: TaskServiceDeps): {
|
|
|
75
95
|
updated_at: string;
|
|
76
96
|
} | undefined;
|
|
77
97
|
delete(id: string): boolean;
|
|
78
|
-
nextActionable(projectId: string
|
|
98
|
+
nextActionable(projectId: string, filter?: {
|
|
99
|
+
tags?: string[];
|
|
100
|
+
}): NextActionableResult | undefined;
|
|
79
101
|
};
|
|
80
102
|
export type TaskService = ReturnType<typeof createTaskService>;
|
|
81
103
|
export {};
|
package/dist/services/tasks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createTask, deleteEdgesByTaskId, deleteTask as dbDeleteTask, getProject, getTask, InvalidTaskStatusError, isTaskStatus, listEdges, listTasks, nullDocumentsLinkedTask, updateTask, } from '@plandesk/db';
|
|
1
|
+
import { createTag, createTask, deleteEdgesByTaskId, deleteTask as dbDeleteTask, deleteTaskTagsByTaskId, getProject, getTagByName, getTask, InvalidTaskStatusError, isTaskStatus, listEdges, listTagsByTaskForProject, listTagsForTask, listTasks, nullDocumentsLinkedTask, setTaskTags, taskIdsWithAnyTagName, updateTask, } from '@plandesk/db';
|
|
2
2
|
import { serializeTask } from '../serialize.js';
|
|
3
|
+
import { normalizeTagName } from './tags.js';
|
|
3
4
|
// depends_on: prerequisite = to, dependent = from. All other labels: prerequisite = from, dependent = to.
|
|
4
5
|
function prerequisiteAndDependent(edge) {
|
|
5
6
|
if (edge.fromTaskId === edge.toTaskId) {
|
|
@@ -10,9 +11,31 @@ function prerequisiteAndDependent(edge) {
|
|
|
10
11
|
}
|
|
11
12
|
return { prerequisite: edge.fromTaskId, dependent: edge.toTaskId };
|
|
12
13
|
}
|
|
14
|
+
// Resolves tag names to ids, auto-creating tags that do not exist yet.
|
|
15
|
+
function resolveTagIdsByName(db, projectId, names) {
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
const ids = [];
|
|
18
|
+
for (const raw of names) {
|
|
19
|
+
const name = normalizeTagName(raw);
|
|
20
|
+
if (seen.has(name)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
seen.add(name);
|
|
24
|
+
const tag = getTagByName(db, projectId, name) ?? createTag(db, { projectId, name });
|
|
25
|
+
ids.push(tag.id);
|
|
26
|
+
}
|
|
27
|
+
return ids;
|
|
28
|
+
}
|
|
13
29
|
export function createTaskService(deps) {
|
|
14
30
|
const { db, eventBus } = deps;
|
|
15
31
|
return {
|
|
32
|
+
get(id) {
|
|
33
|
+
const task = getTask(db, id);
|
|
34
|
+
if (!task) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return serializeTask(task, listTagsForTask(db, id));
|
|
38
|
+
},
|
|
16
39
|
listByProject(projectId, filter = {}, pagination = {}) {
|
|
17
40
|
if (filter.status !== undefined && !isTaskStatus(filter.status)) {
|
|
18
41
|
throw new InvalidTaskStatusError(filter.status);
|
|
@@ -22,10 +45,13 @@ export function createTaskService(deps) {
|
|
|
22
45
|
return undefined;
|
|
23
46
|
}
|
|
24
47
|
const statusFilter = filter.status;
|
|
25
|
-
const tasks =
|
|
26
|
-
|
|
27
|
-
:
|
|
28
|
-
|
|
48
|
+
const tasks = listTasks(db, projectId, {
|
|
49
|
+
...(statusFilter !== undefined ? { status: statusFilter } : {}),
|
|
50
|
+
...(filter.tags !== undefined ? { tagNames: filter.tags.map(normalizeTagName) } : {}),
|
|
51
|
+
...pagination,
|
|
52
|
+
});
|
|
53
|
+
const tagsByTask = listTagsByTaskForProject(db, projectId);
|
|
54
|
+
return tasks.map((task) => serializeTask(task, tagsByTask.get(task.id) ?? []));
|
|
29
55
|
},
|
|
30
56
|
create(projectId, input) {
|
|
31
57
|
if (input.status !== undefined && !isTaskStatus(input.status)) {
|
|
@@ -35,22 +61,28 @@ export function createTaskService(deps) {
|
|
|
35
61
|
if (!project) {
|
|
36
62
|
return undefined;
|
|
37
63
|
}
|
|
38
|
-
const task =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
64
|
+
const { task, tags } = db.transaction((tx) => {
|
|
65
|
+
const row = createTask(tx, {
|
|
66
|
+
projectId,
|
|
67
|
+
label: input.label,
|
|
68
|
+
status: input.status,
|
|
69
|
+
description: input.description,
|
|
70
|
+
x: input.x,
|
|
71
|
+
y: input.y,
|
|
72
|
+
assignee: input.assignee,
|
|
73
|
+
dueDate: input.dueDate,
|
|
74
|
+
});
|
|
75
|
+
if (input.tags !== undefined) {
|
|
76
|
+
setTaskTags(tx, row.id, resolveTagIdsByName(tx, projectId, input.tags));
|
|
77
|
+
}
|
|
78
|
+
return { task: row, tags: listTagsForTask(tx, row.id) };
|
|
47
79
|
});
|
|
48
80
|
eventBus.emit({
|
|
49
81
|
type: 'task_updated',
|
|
50
82
|
taskId: task.id,
|
|
51
83
|
projectId,
|
|
52
84
|
});
|
|
53
|
-
return serializeTask(task);
|
|
85
|
+
return serializeTask(task, tags);
|
|
54
86
|
},
|
|
55
87
|
update(id, input) {
|
|
56
88
|
if (input.status !== undefined && !isTaskStatus(input.status)) {
|
|
@@ -60,16 +92,26 @@ export function createTaskService(deps) {
|
|
|
60
92
|
if (!existing) {
|
|
61
93
|
return undefined;
|
|
62
94
|
}
|
|
63
|
-
const
|
|
64
|
-
|
|
95
|
+
const { tags: tagNames, ...columns } = input;
|
|
96
|
+
const result = db.transaction((tx) => {
|
|
97
|
+
const row = updateTask(tx, id, columns);
|
|
98
|
+
if (!row) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (tagNames !== undefined) {
|
|
102
|
+
setTaskTags(tx, id, resolveTagIdsByName(tx, existing.projectId, tagNames));
|
|
103
|
+
}
|
|
104
|
+
return { task: row, tags: listTagsForTask(tx, id) };
|
|
105
|
+
});
|
|
106
|
+
if (!result) {
|
|
65
107
|
return undefined;
|
|
66
108
|
}
|
|
67
109
|
eventBus.emit({
|
|
68
110
|
type: 'task_updated',
|
|
69
|
-
taskId: task.id,
|
|
70
|
-
projectId: task.projectId,
|
|
111
|
+
taskId: result.task.id,
|
|
112
|
+
projectId: result.task.projectId,
|
|
71
113
|
});
|
|
72
|
-
return serializeTask(task);
|
|
114
|
+
return serializeTask(result.task, result.tags);
|
|
73
115
|
},
|
|
74
116
|
delete(id) {
|
|
75
117
|
const task = getTask(db, id);
|
|
@@ -80,12 +122,16 @@ export function createTaskService(deps) {
|
|
|
80
122
|
db.transaction((tx) => {
|
|
81
123
|
deleteEdgesByTaskId(tx, id);
|
|
82
124
|
nullDocumentsLinkedTask(tx, id);
|
|
125
|
+
deleteTaskTagsByTaskId(tx, id);
|
|
83
126
|
dbDeleteTask(tx, id);
|
|
84
127
|
});
|
|
85
128
|
eventBus.emit({ type: 'canvas_updated', projectId });
|
|
86
129
|
return true;
|
|
87
130
|
},
|
|
88
|
-
|
|
131
|
+
// filter.tags (OR semantics): only tasks carrying ANY of the given tag names
|
|
132
|
+
// are candidates for next_task / blocked; prerequisite completion is still
|
|
133
|
+
// evaluated against all tasks in the project.
|
|
134
|
+
nextActionable(projectId, filter = {}) {
|
|
89
135
|
const project = getProject(db, projectId);
|
|
90
136
|
if (!project) {
|
|
91
137
|
return undefined;
|
|
@@ -93,6 +139,11 @@ export function createTaskService(deps) {
|
|
|
93
139
|
const tasks = listTasks(db, projectId).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
94
140
|
const edges = listEdges(db, projectId);
|
|
95
141
|
const taskById = new Map(tasks.map((task) => [task.id, task]));
|
|
142
|
+
const tagsByTask = listTagsByTaskForProject(db, projectId);
|
|
143
|
+
const tagMatches = filter.tags !== undefined && filter.tags.length > 0
|
|
144
|
+
? taskIdsWithAnyTagName(db, projectId, filter.tags.map(normalizeTagName))
|
|
145
|
+
: undefined;
|
|
146
|
+
const serialize = (task) => serializeTask(task, tagsByTask.get(task.id) ?? []);
|
|
96
147
|
const prerequisites = new Map();
|
|
97
148
|
for (const edge of edges) {
|
|
98
149
|
const pair = prerequisiteAndDependent(edge);
|
|
@@ -109,7 +160,7 @@ export function createTaskService(deps) {
|
|
|
109
160
|
if (tasks.length === 0) {
|
|
110
161
|
return { next_task: null, reason: 'no_tasks', blocked: [] };
|
|
111
162
|
}
|
|
112
|
-
const todoTasks = tasks.filter((task) => task.status === 'todo');
|
|
163
|
+
const todoTasks = tasks.filter((task) => task.status === 'todo' && (tagMatches === undefined || tagMatches.has(task.id)));
|
|
113
164
|
if (todoTasks.length === 0) {
|
|
114
165
|
return { next_task: null, reason: 'no_todo_tasks', blocked: [] };
|
|
115
166
|
}
|
|
@@ -134,19 +185,19 @@ export function createTaskService(deps) {
|
|
|
134
185
|
return [...prereqs]
|
|
135
186
|
.map((id) => taskById.get(id))
|
|
136
187
|
.filter((task) => task !== undefined && task.status !== 'done')
|
|
137
|
-
.map(
|
|
188
|
+
.map(serialize);
|
|
138
189
|
};
|
|
139
190
|
const blocked = [];
|
|
140
191
|
let nextTask = null;
|
|
141
192
|
for (const task of todoTasks) {
|
|
142
193
|
if (isActionable(task.id)) {
|
|
143
194
|
if (nextTask === null) {
|
|
144
|
-
nextTask =
|
|
195
|
+
nextTask = serialize(task);
|
|
145
196
|
}
|
|
146
197
|
}
|
|
147
198
|
else {
|
|
148
199
|
blocked.push({
|
|
149
|
-
task:
|
|
200
|
+
task: serialize(task),
|
|
150
201
|
waiting_on: unfinishedPrereqs(task.id),
|
|
151
202
|
});
|
|
152
203
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plandesk/api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"typescript": "^6.0.3",
|
|
20
20
|
"vitest": "^3.2.6",
|
|
21
|
-
"@plandesk/db": "0.
|
|
22
|
-
"@plandesk/sync-server": "0.
|
|
21
|
+
"@plandesk/db": "0.6.0",
|
|
22
|
+
"@plandesk/sync-server": "0.5.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@hono/node-server": "^2.0.4",
|