Signed-off-by: Max <ai-agent@topdoglabs.com>
This commit is contained in:
parent
8aaca14e3a
commit
5c6bd134bd
15
README.md
15
README.md
@ -167,6 +167,21 @@ Task and sprint board built with Next.js + Zustand and Supabase-backed API persi
|
|||||||
- Delete any comment or reply from the thread
|
- Delete any comment or reply from the thread
|
||||||
- Thread data is persisted in Supabase through the existing `/api/tasks` sync flow.
|
- Thread data is persisted in Supabase through the existing `/api/tasks` sync flow.
|
||||||
|
|
||||||
|
Current persisted comment shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"text": "string",
|
||||||
|
"createdAt": "2026-02-23T19:00:00.000Z",
|
||||||
|
"commentAuthorId": "user-uuid-or-assistant",
|
||||||
|
"replies": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `commentAuthorId` is required.
|
||||||
|
- Legacy `author` objects are not supported by the UI parser.
|
||||||
|
|
||||||
### Backlog drag and drop
|
### Backlog drag and drop
|
||||||
|
|
||||||
Backlog view supports moving tasks between:
|
Backlog view supports moving tasks between:
|
||||||
|
|||||||
@ -85,6 +85,20 @@ A unified CLI that covers all API operations.
|
|||||||
./scripts/gantt.sh task attach <task-uuid> ./research.pdf
|
./scripts/gantt.sh task attach <task-uuid> ./research.pdf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Comment payload persisted by the API/CLI:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"text": "string",
|
||||||
|
"createdAt": "2026-02-23T19:00:00.000Z",
|
||||||
|
"commentAuthorId": "user-uuid-or-assistant",
|
||||||
|
"replies": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `commentAuthorId` is the only supported author field.
|
||||||
|
|
||||||
### Project Commands
|
### Project Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -468,6 +468,25 @@ export default function Home() {
|
|||||||
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
|
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
|
||||||
}, [users, currentUser])
|
}, [users, currentUser])
|
||||||
|
|
||||||
|
const knownUsersById = useMemo(() => {
|
||||||
|
const byId = new Map<string, AssignableUser>()
|
||||||
|
|
||||||
|
const addUser = (id: string | undefined, name: string | undefined, avatarUrl?: string, email?: string) => {
|
||||||
|
if (!id || !name) return
|
||||||
|
if (id.trim().length === 0 || name.trim().length === 0) return
|
||||||
|
byId.set(id, { id, name, avatarUrl, email })
|
||||||
|
}
|
||||||
|
|
||||||
|
assignableUsers.forEach((user) => byId.set(user.id, user))
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
addUser(task.createdById, task.createdByName, task.createdByAvatarUrl)
|
||||||
|
addUser(task.updatedById, task.updatedByName, task.updatedByAvatarUrl)
|
||||||
|
addUser(task.assigneeId, task.assigneeName, task.assigneeAvatarUrl, task.assigneeEmail)
|
||||||
|
})
|
||||||
|
|
||||||
|
return byId
|
||||||
|
}, [assignableUsers, tasks])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true
|
let isMounted = true
|
||||||
const loadSession = async () => {
|
const loadSession = async () => {
|
||||||
@ -756,7 +775,7 @@ export default function Home() {
|
|||||||
|
|
||||||
const resolveAssignee = (assigneeId: string | undefined) => {
|
const resolveAssignee = (assigneeId: string | undefined) => {
|
||||||
if (!assigneeId) return null
|
if (!assigneeId) return null
|
||||||
return assignableUsers.find((user) => user.id === assigneeId) || null
|
return knownUsersById.get(assigneeId) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
const setNewTaskAssignee = (assigneeId: string) => {
|
const setNewTaskAssignee = (assigneeId: string) => {
|
||||||
|
|||||||
@ -330,6 +330,25 @@ export default function TaskDetailPage() {
|
|||||||
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
|
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name))
|
||||||
}, [users, currentUser])
|
}, [users, currentUser])
|
||||||
|
|
||||||
|
const knownUsersById = useMemo(() => {
|
||||||
|
const byId = new Map<string, AssignableUser>()
|
||||||
|
|
||||||
|
const addUser = (id: string | undefined, name: string | undefined, avatarUrl?: string, email?: string) => {
|
||||||
|
if (!id || !name) return
|
||||||
|
if (id.trim().length === 0 || name.trim().length === 0) return
|
||||||
|
byId.set(id, { id, name, avatarUrl, email })
|
||||||
|
}
|
||||||
|
|
||||||
|
assignableUsers.forEach((user) => byId.set(user.id, user))
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
addUser(task.createdById, task.createdByName, task.createdByAvatarUrl)
|
||||||
|
addUser(task.updatedById, task.updatedByName, task.updatedByAvatarUrl)
|
||||||
|
addUser(task.assigneeId, task.assigneeName, task.assigneeAvatarUrl, task.assigneeEmail)
|
||||||
|
})
|
||||||
|
|
||||||
|
return byId
|
||||||
|
}, [assignableUsers, tasks])
|
||||||
|
|
||||||
const sortedSprints = useMemo(
|
const sortedSprints = useMemo(
|
||||||
() =>
|
() =>
|
||||||
sprints
|
sprints
|
||||||
@ -419,7 +438,7 @@ export default function TaskDetailPage() {
|
|||||||
|
|
||||||
const resolveAssignee = (assigneeId: string | undefined) => {
|
const resolveAssignee = (assigneeId: string | undefined) => {
|
||||||
if (!assigneeId) return null
|
if (!assigneeId) return null
|
||||||
return assignableUsers.find((user) => user.id === assigneeId) || null
|
return knownUsersById.get(assigneeId) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
const setEditedTaskAssignee = (assigneeId: string) => {
|
const setEditedTaskAssignee = (assigneeId: string) => {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user