Skip to content

Commit 98d8d7d

Browse files
authored
feat: add task manager db calls (#870)
1 parent 50398f8 commit 98d8d7d

3 files changed

Lines changed: 241 additions & 0 deletions

File tree

.changeset/ninety-panthers-kick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@blinkk/root-cms': patch
3+
---
4+
5+
feat: add task manager db calls (#870)

packages/root-cms/ui/hooks/useSiteSettings.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {useFirebase} from './useFirebase.js';
1010
interface SiteSettings {
1111
/** The Google Drive folder where files created by Root CMS are stored. If unspecified, files will be created in your "My Drive". */
1212
googleDriveFolder?: string;
13+
/** The default assignee for new tasks created within the CMS. */
14+
defaultAssignee?: string;
1315
}
1416

1517
interface Setting {
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import {
2+
FieldPath,
3+
Timestamp,
4+
arrayUnion,
5+
collection,
6+
doc,
7+
getDoc,
8+
serverTimestamp,
9+
setDoc,
10+
updateDoc,
11+
} from 'firebase/firestore';
12+
import {logAction} from './actions.js';
13+
14+
export interface Task {
15+
id: string;
16+
title: string;
17+
description?: string;
18+
assignee?: string | null;
19+
status?: string;
20+
createdAt: Timestamp;
21+
createdBy: string;
22+
updatedAt?: Timestamp;
23+
updatedBy?: string;
24+
}
25+
26+
export interface TaskCommentHistoryEntry {
27+
action: 'edit' | 'delete';
28+
content: string;
29+
changedAt: Timestamp;
30+
changedBy: string;
31+
}
32+
33+
export interface TaskComment {
34+
id: string;
35+
taskId: string;
36+
content: string;
37+
createdAt: Timestamp;
38+
createdBy: string;
39+
updatedAt?: Timestamp;
40+
updatedBy?: string;
41+
deletedAt?: Timestamp;
42+
deletedBy?: string;
43+
isDeleted?: boolean;
44+
history?: TaskCommentHistoryEntry[];
45+
}
46+
47+
function projectDocRef() {
48+
const db = window.firebase.db;
49+
const projectId = window.__ROOT_CTX.rootConfig.projectId;
50+
return doc(db, 'Projects', projectId);
51+
}
52+
53+
function taskDocRef(taskId: string) {
54+
const db = window.firebase.db;
55+
const projectId = window.__ROOT_CTX.rootConfig.projectId;
56+
return doc(db, 'Projects', projectId, 'Tasks', taskId);
57+
}
58+
59+
function taskCommentDocRef(taskId: string, commentId: string) {
60+
const db = window.firebase.db;
61+
const projectId = window.__ROOT_CTX.rootConfig.projectId;
62+
return doc(db, 'Projects', projectId, 'Tasks', taskId, 'Comments', commentId);
63+
}
64+
65+
async function getDefaultTaskAssignee(): Promise<string | null> {
66+
const snapshot = await getDoc(projectDocRef());
67+
if (!snapshot.exists()) {
68+
return null;
69+
}
70+
const data: any = snapshot.data() || {};
71+
return data.settings?.defaultAssignee || null;
72+
}
73+
74+
export async function setDefaultTaskAssignee(assignee: string | null) {
75+
await updateDoc(projectDocRef(), new FieldPath('settings', 'defaultAssignee'), assignee);
76+
logAction('tasks.defaultAssignee', {
77+
metadata: {assignee: assignee || null},
78+
});
79+
}
80+
81+
export async function createTask(options: {
82+
title: string;
83+
description?: string;
84+
assignee?: string | null;
85+
}) {
86+
if (!options.title) {
87+
throw new Error('missing task title');
88+
}
89+
90+
const db = window.firebase.db;
91+
const projectId = window.__ROOT_CTX.rootConfig.projectId;
92+
const taskRef = doc(collection(db, 'Projects', projectId, 'Tasks'));
93+
const assignee = options.assignee ?? (await getDefaultTaskAssignee());
94+
const status = 'open';
95+
96+
await setDoc(taskRef, {
97+
id: taskRef.id,
98+
title: options.title,
99+
description: options.description || '',
100+
assignee: assignee ?? null,
101+
status,
102+
createdAt: serverTimestamp(),
103+
createdBy: window.firebase.user.email || '',
104+
updatedAt: serverTimestamp(),
105+
updatedBy: window.firebase.user.email || '',
106+
});
107+
108+
logAction('tasks.create', {metadata: {taskId: taskRef.id}});
109+
}
110+
111+
export async function updateTaskAssignee(taskId: string, assignee: string) {
112+
if (!taskId) {
113+
throw new Error('missing task id');
114+
}
115+
await updateDoc(taskDocRef(taskId), {
116+
assignee,
117+
updatedAt: serverTimestamp(),
118+
updatedBy: window.firebase.user.email || '',
119+
});
120+
logAction('tasks.updateAssignee', {metadata: {taskId, assignee}});
121+
}
122+
123+
export async function updateTaskStatus(taskId: string, status: string) {
124+
if (!taskId) {
125+
throw new Error('missing task id');
126+
}
127+
if (!status) {
128+
throw new Error('missing status');
129+
}
130+
await updateDoc(taskDocRef(taskId), {
131+
status,
132+
updatedAt: serverTimestamp(),
133+
updatedBy: window.firebase.user.email || '',
134+
});
135+
logAction('tasks.updateStatus', {metadata: {taskId, status}});
136+
}
137+
138+
export async function addTaskComment(taskId: string, content: string) {
139+
if (!taskId) {
140+
throw new Error('missing task id');
141+
}
142+
if (!content) {
143+
throw new Error('missing comment content');
144+
}
145+
146+
const db = window.firebase.db;
147+
const projectId = window.__ROOT_CTX.rootConfig.projectId;
148+
const commentRef = doc(
149+
collection(db, 'Projects', projectId, 'Tasks', taskId, 'Comments')
150+
);
151+
const commentId = commentRef.id;
152+
153+
await setDoc(commentRef, {
154+
id: commentId,
155+
taskId,
156+
content,
157+
createdAt: serverTimestamp(),
158+
createdBy: window.firebase.user.email || '',
159+
history: [],
160+
});
161+
162+
logAction('tasks.comment.add', {
163+
metadata: {taskId, commentId},
164+
});
165+
166+
return commentId;
167+
}
168+
169+
export async function editTaskComment(
170+
taskId: string,
171+
commentId: string,
172+
content: string
173+
) {
174+
if (!taskId || !commentId) {
175+
throw new Error('missing task or comment id');
176+
}
177+
if (!content) {
178+
throw new Error('missing comment content');
179+
}
180+
181+
const commentRef = taskCommentDocRef(taskId, commentId);
182+
const snapshot = await getDoc(commentRef);
183+
if (!snapshot.exists()) {
184+
throw new Error('comment not found');
185+
}
186+
const data = snapshot.data() as TaskComment;
187+
188+
await updateDoc(commentRef, {
189+
content,
190+
updatedAt: serverTimestamp(),
191+
updatedBy: window.firebase.user.email || '',
192+
isDeleted: false,
193+
history: arrayUnion({
194+
action: 'edit',
195+
content: data.content,
196+
changedAt: Timestamp.now(),
197+
changedBy: window.firebase.user.email || '',
198+
}),
199+
});
200+
201+
logAction('tasks.comment.edit', {
202+
metadata: {taskId, commentId},
203+
});
204+
}
205+
206+
export async function deleteTaskComment(taskId: string, commentId: string) {
207+
if (!taskId || !commentId) {
208+
throw new Error('missing task or comment id');
209+
}
210+
211+
const commentRef = taskCommentDocRef(taskId, commentId);
212+
const snapshot = await getDoc(commentRef);
213+
if (!snapshot.exists()) {
214+
throw new Error('comment not found');
215+
}
216+
const data = snapshot.data() as TaskComment;
217+
218+
await updateDoc(commentRef, {
219+
content: '',
220+
isDeleted: true,
221+
deletedAt: serverTimestamp(),
222+
deletedBy: window.firebase.user.email || '',
223+
history: arrayUnion({
224+
action: 'delete',
225+
content: data.content,
226+
changedAt: Timestamp.now(),
227+
changedBy: window.firebase.user.email || '',
228+
}),
229+
});
230+
231+
logAction('tasks.comment.delete', {
232+
metadata: {taskId, commentId},
233+
});
234+
}

0 commit comments

Comments
 (0)