From b8fb8cd6db4123ffb8923b94752844d3cf12a105 Mon Sep 17 00:00:00 2001 From: bahmez Date: Thu, 15 Jan 2026 23:38:18 +0100 Subject: [PATCH 01/11] feat: add board background functionality - Introduced a new BoardBackground type to support color, gradient, and image backgrounds for boards. - Updated the Board model and related services to handle the new background structure. - Implemented background selection in the UI, allowing users to choose from colors, gradients, and images sourced from Unsplash. - Enhanced the board controller and client components to manage background updates and display. - Added tests to ensure proper functionality and validation of background inputs. --- apps/api/package.json | 3 +- .../board/board-backgrounds.service.spec.ts | 84 ++++++++++ .../src/board/board-backgrounds.service.ts | 156 ++++++++++++++++++ apps/api/src/board/board.controller.ts | 55 +++++- apps/api/src/board/board.module.ts | 3 +- apps/api/src/main.ts | 42 ++++- .../boards/[boardId]/board-client.tsx | 133 ++++++++++++++- .../app/(app)/dashboard/dashboard-client.tsx | 10 +- .../[workspaceId]/workspace-boards-client.tsx | 10 +- .../src/components/board/board-background.tsx | 41 +++++ packages/database/src/boards/board.model.ts | 20 ++- .../database/src/boards/boards.store.spec.ts | 26 +++ packages/database/src/boards/boards.store.ts | 69 +++++++- packages/database/src/index.ts | 2 + packages/trpc/src/activity-log.spec.ts | 3 + packages/trpc/src/index.ts | 118 ++++++++++++- pnpm-lock.yaml | 9 + 17 files changed, 750 insertions(+), 34 deletions(-) create mode 100644 apps/api/src/board/board-backgrounds.service.spec.ts create mode 100644 apps/api/src/board/board-backgrounds.service.ts create mode 100644 apps/web/src/components/board/board-background.tsx diff --git a/apps/api/package.json b/apps/api/package.json index 104574c..8b79cbc 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -27,7 +27,8 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "unsplash-js": "^7.0.20" }, "devDependencies": { "@nestjs/testing": "^10.4.22", diff --git a/apps/api/src/board/board-backgrounds.service.spec.ts b/apps/api/src/board/board-backgrounds.service.spec.ts new file mode 100644 index 0000000..c80fbd0 --- /dev/null +++ b/apps/api/src/board/board-backgrounds.service.spec.ts @@ -0,0 +1,84 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { BoardBackgroundsService } from './board-backgrounds.service.js'; +import { createApi } from 'unsplash-js'; + +vi.mock('unsplash-js', () => ({ createApi: vi.fn() })); + +describe('BoardBackgroundsService', () => { + const createApiMock = createApi as unknown as ReturnType; + + beforeEach(() => { + createApiMock.mockReset(); + delete process.env.UNSPLASH_ACCESS_KEY; + }); + + it('paginates colors list', async () => { + const service = new BoardBackgroundsService(); + + const first = await service.list({ type: 'color', limit: 3 }); + const second = await service.list({ type: 'color', limit: 3, cursor: first.nextCursor }); + + expect(first.items).toHaveLength(3); + expect(first.items[0]).toMatchObject({ type: 'color' }); + expect(first.nextCursor).toBe('3'); + expect(second.items[0]).toMatchObject({ type: 'color' }); + expect(second.items[0]).not.toEqual(first.items[0]); + }); + + it('paginates gradients list', async () => { + const service = new BoardBackgroundsService(); + + const first = await service.list({ type: 'gradient', limit: 2 }); + const second = await service.list({ type: 'gradient', limit: 2, cursor: first.nextCursor }); + + expect(first.items).toHaveLength(2); + expect(first.items[0]).toMatchObject({ type: 'gradient' }); + expect(first.nextCursor).toBe('2'); + expect(second.items[0]).toMatchObject({ type: 'gradient' }); + expect(second.items[0]).not.toEqual(first.items[0]); + }); + + it('throws if Unsplash access key is missing', async () => { + const service = new BoardBackgroundsService(); + await expect(service.list({ type: 'image', limit: 2 })).rejects.toThrow(/Unsplash access key/i); + }); + + it('lists Unsplash images', async () => { + process.env.UNSPLASH_ACCESS_KEY = 'test-key'; + const list = vi.fn().mockResolvedValue({ + type: 'success', + response: [ + { + id: 'p1', + urls: { regular: 'https://img/regular', small: 'https://img/small' }, + user: { name: 'Alice', links: { html: 'https://unsplash.com/@alice' } }, + color: '#ffffff', + blur_hash: 'hash', + }, + ], + }); + + createApiMock.mockReturnValueOnce({ + photos: { list }, + search: { getPhotos: vi.fn() }, + }); + + const service = new BoardBackgroundsService(); + const res = await service.list({ type: 'image', limit: 1 }); + + expect(list).toHaveBeenCalledWith({ page: 1, perPage: 1, orderBy: 'popular' }); + expect(res.items).toHaveLength(1); + expect(res.items[0]).toMatchObject({ + type: 'image', + value: { + source: 'unsplash', + id: 'p1', + url: 'https://img/regular', + thumbUrl: 'https://img/small', + authorName: 'Alice', + authorUrl: 'https://unsplash.com/@alice', + }, + }); + }); +}); + diff --git a/apps/api/src/board/board-backgrounds.service.ts b/apps/api/src/board/board-backgrounds.service.ts new file mode 100644 index 0000000..29dafdd --- /dev/null +++ b/apps/api/src/board/board-backgrounds.service.ts @@ -0,0 +1,156 @@ +import { Injectable } from '@nestjs/common'; +import { createApi } from 'unsplash-js'; +import type { BoardBackground } from '@taskly/database'; + +type BackgroundType = 'color' | 'gradient' | 'image'; + +export type ListBackgroundsInput = { + type: BackgroundType; + limit: number; + cursor?: string | null; + query?: string | null; +}; + +export type ListBackgroundsResult = { + items: BoardBackground[]; + nextCursor: string | null; +}; + +const COLOR_VALUES: string[] = [ + '#0F172A', + '#1E293B', + '#334155', + '#0F766E', + '#0EA5E9', + '#2563EB', + '#4338CA', + '#7C3AED', + '#C026D3', + '#DB2777', + '#E11D48', + '#F97316', + '#F59E0B', + '#84CC16', + '#22C55E', + '#14B8A6', +]; + +const GRADIENT_VALUES: string[] = [ + 'linear-gradient(135deg, #0EA5E9 0%, #6366F1 100%)', + 'linear-gradient(135deg, #22C55E 0%, #14B8A6 100%)', + 'linear-gradient(135deg, #F97316 0%, #F59E0B 100%)', + 'linear-gradient(135deg, #DB2777 0%, #7C3AED 100%)', + 'linear-gradient(135deg, #0F766E 0%, #22C55E 100%)', + 'linear-gradient(135deg, #1E293B 0%, #0F172A 100%)', + 'linear-gradient(135deg, #8B5CF6 0%, #EC4899 100%)', + 'linear-gradient(135deg, #38BDF8 0%, #6366F1 100%)', + 'linear-gradient(135deg, #F43F5E 0%, #F97316 100%)', + 'linear-gradient(135deg, #A855F7 0%, #3B82F6 100%)', +]; + +function clampLimit(value: number): number { + const v = Math.floor(Number.isFinite(value) ? value : 20); + return Math.max(1, Math.min(50, v)); +} + +function parseOffset(cursor?: string | null, fallback = 0): number { + if (!cursor) return fallback; + const parsed = Number.parseInt(cursor, 10); + if (!Number.isFinite(parsed) || parsed < 0) return fallback; + return parsed; +} + +function mapColor(value: string): BoardBackground { + return { type: 'color', value }; +} + +function mapGradient(value: string): BoardBackground { + return { type: 'gradient', value }; +} + +function mapUnsplashPhoto(photo: Record): BoardBackground | null { + const id = typeof photo.id === 'string' ? photo.id : ''; + const urls = (photo.urls as Record | undefined) ?? {}; + const url = + (typeof urls.full === 'string' && urls.full) || + (typeof urls.regular === 'string' && urls.regular) || + (typeof urls.raw === 'string' && urls.raw) || + ''; + const thumbUrl = + (typeof urls.small === 'string' && urls.small) || + (typeof urls.thumb === 'string' && urls.thumb) || + (typeof urls.regular === 'string' && urls.regular) || + url || + ''; + if (!id || !url || !thumbUrl) return null; + + const user = (photo.user as Record | undefined) ?? {}; + const links = (user.links as Record | undefined) ?? {}; + + return { + type: 'image', + value: { + source: 'unsplash', + id, + url, + thumbUrl, + blurHash: typeof photo.blur_hash === 'string' ? photo.blur_hash : null, + color: typeof photo.color === 'string' ? photo.color : null, + authorName: typeof user.name === 'string' ? user.name : null, + authorUrl: typeof links.html === 'string' ? links.html : null, + }, + }; +} + +@Injectable() +export class BoardBackgroundsService { + private readonly unsplash = process.env.UNSPLASH_ACCESS_KEY + ? createApi({ accessKey: process.env.UNSPLASH_ACCESS_KEY }) + : null; + + async list(input: ListBackgroundsInput): Promise { + const limit = clampLimit(input.limit); + + if (input.type === 'color') { + const offset = parseOffset(input.cursor, 0); + const slice = COLOR_VALUES.slice(offset, offset + limit); + const nextCursor = offset + limit < COLOR_VALUES.length ? String(offset + limit) : null; + return { items: slice.map(mapColor), nextCursor }; + } + + if (input.type === 'gradient') { + const offset = parseOffset(input.cursor, 0); + const slice = GRADIENT_VALUES.slice(offset, offset + limit); + const nextCursor = offset + limit < GRADIENT_VALUES.length ? String(offset + limit) : null; + return { items: slice.map(mapGradient), nextCursor }; + } + + if (!this.unsplash) { + throw new Error('Unsplash access key not configured'); + } + + const page = Math.max(1, parseOffset(input.cursor, 1)); + const query = (input.query ?? '').trim(); + if (query) { + const res = await this.unsplash.search.getPhotos({ + query, + page, + perPage: limit, + orientation: 'landscape', + }); + if (res.type !== 'success') throw new Error('Unsplash search failed'); + const items = res.response.results.map(mapUnsplashPhoto).filter(Boolean) as BoardBackground[]; + const hasMore = page < res.response.total_pages; + return { items, nextCursor: hasMore ? String(page + 1) : null }; + } + + const res = await this.unsplash.photos.list({ page, perPage: limit, orderBy: 'popular' }); + if (res.type !== 'success') throw new Error('Unsplash list failed'); + const raw = + Array.isArray(res.response) ? res.response : (res.response as { results?: unknown[] } | null)?.results ?? []; + const items = raw.map(mapUnsplashPhoto).filter(Boolean) as BoardBackground[]; + const hasMore = items.length === limit; + return { items, nextCursor: hasMore ? String(page + 1) : null }; + } +} + diff --git a/apps/api/src/board/board.controller.ts b/apps/api/src/board/board.controller.ts index b4be734..eae2b4e 100644 --- a/apps/api/src/board/board.controller.ts +++ b/apps/api/src/board/board.controller.ts @@ -11,11 +11,11 @@ import { UseGuards, } from '@nestjs/common'; import { CurrentUser, FirebaseAuthGuard } from '@taskly/auth'; -import type { UserModel } from '@taskly/database'; +import type { BoardBackground, UserModel } from '@taskly/database'; import { BoardsService } from '@taskly/database'; import { BoardAccessService } from './board-access.service.js'; -type PatchBoardDto = { title?: string; description?: string; background?: string | null }; +type PatchBoardDto = { title?: string; description?: string; background?: BoardBackground | string | null }; type CreateColumnDto = { title?: string; key?: string }; type PatchColumnDto = { title?: string; key?: string }; type ReorderColumnsDto = { columnIds?: string[] }; @@ -24,6 +24,45 @@ type CreateLabelDto = { name?: string; color?: string | null }; type PatchLabelDto = { name?: string; color?: string | null }; type ReorderLabelsDto = { labelIds?: string[] }; +function parseBoardBackgroundInput(input: unknown): BoardBackground | null { + if (input === null || input === undefined) return null; + if (typeof input === 'string') { + const value = input.trim(); + if (!value) return null; + return { type: 'color', value }; + } + if (typeof input !== 'object') return null; + const data = input as { type?: unknown; value?: unknown }; + if (data.type === 'color' || data.type === 'gradient') { + if (typeof data.value !== 'string') return null; + const value = data.value.trim(); + if (!value) return null; + return { type: data.type, value }; + } + if (data.type === 'image') { + if (typeof data.value !== 'object' || !data.value) return null; + const value = data.value as Record; + const id = typeof value.id === 'string' ? value.id : ''; + const url = typeof value.url === 'string' ? value.url : ''; + const thumbUrl = typeof value.thumbUrl === 'string' ? value.thumbUrl : ''; + if (!id || !url || !thumbUrl) return null; + return { + type: 'image', + value: { + source: value.source === 'unsplash' ? 'unsplash' : 'unsplash', + id, + url, + thumbUrl, + blurHash: typeof value.blurHash === 'string' ? value.blurHash : null, + color: typeof value.color === 'string' ? value.color : null, + authorName: typeof value.authorName === 'string' ? value.authorName : null, + authorUrl: typeof value.authorUrl === 'string' ? value.authorUrl : null, + }, + }; + } + return null; +} + @UseGuards(FirebaseAuthGuard) @Controller('/api/boards') export class BoardController { @@ -72,14 +111,22 @@ export class BoardController { const role = await this.access.getWorkspaceRoleOrThrow(user.id, board.workspaceId); this.access.requirePermission(role, 'board.meta.write'); - const patch: { title?: string; description?: string; background?: string | null } = {}; + const patch: { title?: string; description?: string; background?: BoardBackground | null } = {}; if (typeof body.title === 'string') { const t = body.title.trim(); if (!t) throw new BadRequestException('title cannot be empty'); patch.title = t; } if (typeof body.description === 'string') patch.description = body.description.trim(); - if (body.background === null || typeof body.background === 'string') patch.background = body.background; + if (Object.prototype.hasOwnProperty.call(body, 'background')) { + if (body.background === null) { + patch.background = null; + } else { + const parsed = parseBoardBackgroundInput(body.background); + if (!parsed) throw new BadRequestException('background is invalid'); + patch.background = parsed; + } + } return await this.boards.updateBoard(boardId, patch); } diff --git a/apps/api/src/board/board.module.ts b/apps/api/src/board/board.module.ts index b26e337..e98c6d8 100644 --- a/apps/api/src/board/board.module.ts +++ b/apps/api/src/board/board.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; import { BoardController } from './board.controller.js'; import { BoardAccessService } from './board-access.service.js'; +import { BoardBackgroundsService } from './board-backgrounds.service.js'; @Module({ controllers: [BoardController], - providers: [BoardAccessService], + providers: [BoardAccessService, BoardBackgroundsService], }) export class BoardModule {} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index d391466..b481544 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -19,6 +19,7 @@ import { } from '@taskly/database'; import type { AppRouter, Context } from '@taskly/trpc'; import { GcsService } from './gcs/gcs.service.js'; +import { BoardBackgroundsService } from './board/board-backgrounds.service.js'; import crypto from 'node:crypto'; function extractBearerToken(header: string | undefined): string | null { @@ -77,6 +78,7 @@ async function bootstrap() { const usersService = app.get(UsersService); const workspacesService = app.get(WorkspacesService); const boardsService = app.get(BoardsService); + const boardBackgroundsService = app.get(BoardBackgroundsService); const ticketsService = app.get(TicketsService); const ticketRemindersService = app.get(TicketRemindersService); const notificationsService = app.get(NotificationsService); @@ -187,6 +189,10 @@ async function bootstrap() { archiveTicket: boardsService.archiveTicket.bind(boardsService), }; + const boardBackgrounds = { + list: boardBackgroundsService.list.bind(boardBackgroundsService), + }; + const makeTickets = (actorId: string | null) => ({ getById: ticketsService.getById.bind(ticketsService), update: ticketsService.update.bind(ticketsService), @@ -284,20 +290,50 @@ async function bootstrap() { }; if (!token) { - return { user: null, users, workspaces, boards, tickets: makeTickets(null), ticketReminders, notifications, activityLogs }; + return { + user: null, + users, + workspaces, + boards, + boardBackgrounds, + tickets: makeTickets(null), + ticketReminders, + notifications, + activityLogs, + }; } try { const decoded = await firebaseAuth.verifyIdToken(token); const user = await usersService.ensureUserExists(decoded); - return { user, users, workspaces, boards, tickets: makeTickets(user.id), ticketReminders, notifications, activityLogs }; + return { + user, + users, + workspaces, + boards, + boardBackgrounds, + tickets: makeTickets(user.id), + ticketReminders, + notifications, + activityLogs, + }; } catch (e) { if (process.env.NODE_ENV !== 'production') { const err = e as { message?: string; code?: string }; // eslint-disable-next-line no-console console.warn('[trpc] verifyIdToken failed', { code: err?.code, message: err?.message }); } - return { user: null, users, workspaces, boards, tickets: makeTickets(null), ticketReminders, notifications, activityLogs }; + return { + user: null, + users, + workspaces, + boards, + boardBackgrounds, + tickets: makeTickets(null), + ticketReminders, + notifications, + activityLogs, + }; } }, }), diff --git a/apps/web/src/app/(app)/dashboard/boards/[boardId]/board-client.tsx b/apps/web/src/app/(app)/dashboard/boards/[boardId]/board-client.tsx index eae4b9c..efc78e0 100644 --- a/apps/web/src/app/(app)/dashboard/boards/[boardId]/board-client.tsx +++ b/apps/web/src/app/(app)/dashboard/boards/[boardId]/board-client.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { api } from '@/app/trpc'; +import type { BoardBackground } from '@taskly/trpc'; +import { getBoardBackgroundStyle } from '@/components/board/board-background'; import { DndContext, DragEndEvent, @@ -37,7 +39,7 @@ import { useToast, } from '@taskly/ui'; import { useWorkspaceUI } from '@/components/workspace/workspace-ui-provider'; -import { MoreHorizontal, Plus, CheckSquare, Paperclip, MessageSquare, GripVertical, Calendar, ScrollText, Clock } from 'lucide-react'; +import { MoreHorizontal, Plus, CheckSquare, Paperclip, MessageSquare, GripVertical, Calendar, ScrollText, Clock, Paintbrush } from 'lucide-react'; import TicketDialogV2 from '@/components/ticket/ticket-dialog-v2'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; @@ -140,6 +142,17 @@ function formatBoardActivityType(input: { type: string; ticketTitle?: string | n } } +function boardBackgroundKey(bg: BoardBackground): string { + if (bg.type === 'image') return `image:${bg.value.id}`; + return `${bg.type}:${bg.value}`; +} + +function isSameBackground(a: BoardBackground, b: BoardBackground | null): boolean { + if (!b || a.type !== b.type) return false; + if (a.type === 'image' && b.type === 'image') return a.value.id === b.value.id; + return a.value === b.value; +} + type AnyLabel = { id: string; name: string; color: string }; type TicketLike = { id: string; @@ -516,6 +529,23 @@ export default function BoardClient({ boardId }: { boardId: string }) { { enabled: boardActivityOpen }, ); + const [backgroundPickerOpen, setBackgroundPickerOpen] = React.useState(false); + const [backgroundTab, setBackgroundTab] = React.useState<'color' | 'gradient' | 'image'>('color'); + const [imageSearch, setImageSearch] = React.useState(''); + const imageSearchValue = imageSearch.trim(); + + const backgroundsQuery = api.boards.listBackgrounds.useInfiniteQuery( + { + type: backgroundTab, + limit: 12, + query: backgroundTab === 'image' && imageSearchValue ? imageSearchValue : undefined, + }, + { + enabled: backgroundPickerOpen, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ); + React.useEffect(() => { if (viewQuery.data?.board?.workspaceId) { setSelectedWorkspaceId(viewQuery.data.board.workspaceId); @@ -593,6 +623,7 @@ export default function BoardClient({ boardId }: { boardId: string }) { }); const board = viewQuery.data?.board; + const canEditBackground = viewQuery.data?.permissions?.canBackgroundWrite ?? false; const serverColumns = React.useMemo(() => viewQuery.data?.columns ?? [], [viewQuery.data?.columns]); const serverTickets = React.useMemo(() => viewQuery.data?.tickets ?? [], [viewQuery.data?.tickets]); const boardLabels = labelsQuery.data ?? []; @@ -853,9 +884,12 @@ export default function BoardClient({ boardId }: { boardId: string }) { return
Board not found.
; } + const currentBackground = board.background ?? null; + const backgroundItems = backgroundsQuery.data?.pages.flatMap((p) => p.items) ?? []; + return ( -
-
+
+
{isEditingBoardTitle ? ( @@ -900,6 +934,99 @@ export default function BoardClient({ boardId }: { boardId: string }) {
+ + + + + + + Board background + +
+ {(['color', 'gradient', 'image'] as const).map((tab) => ( + + ))} + +
+ + {backgroundTab === 'image' && ( + setImageSearch(e.target.value)} + placeholder="Search Unsplash" + /> + )} + + {backgroundsQuery.isLoading ? ( +
Loading backgrounds…
+ ) : backgroundItems.length === 0 ? ( +
No background found.
+ ) : ( +
+ {backgroundItems.map((bg) => { + const selected = isSameBackground(bg, currentBackground); + return ( +
+ )} + + {backgroundsQuery.hasNextPage && ( +
+ +
+ )} +
+
+ @@ -405,14 +481,126 @@ export function Navbar() {
ID: {firebaseUser?.uid ?? "-"}
+
+ +
+
Profile photo
+ + + + + + + Update avatar + + +
+ {(["color", "gradient", "image", "upload"] as const).map((tab) => ( + + ))} + +
+ + {avatarTab === "image" && ( + setAvatarSearch(e.target.value)} + placeholder="Search Unsplash" + /> + )} + + {avatarTab === "upload" ? ( +
+
+ Upload a square image (we’ll store it in GCS). +
+ +
+ ) : avatarBackgroundsQuery.isLoading ? ( +
Loading backgrounds…
+ ) : avatarBackgroundItems.length === 0 ? ( +
No background found.
+ ) : ( +
+ {avatarBackgroundItems.map((bg) => ( +
+ )} + + {avatarTab !== "upload" && avatarBackgroundsQuery.hasNextPage && ( +
+ +
+ )} +
+
+
+
+
Display name
setDisplayName(e.target.value)} placeholder="Display name" />
-
-
Photo URL
- setPhotoURL(e.target.value)} placeholder="https://..." /> -
Username
@@ -446,7 +634,6 @@ export function Navbar() { setDescription(me.description ?? "") } setDisplayName(firebaseUser?.displayName ?? "") - setPhotoURL(firebaseUser?.photoURL ?? "") }} > Reset @@ -470,7 +657,6 @@ export function Navbar() { try { await updateProfile(u, { displayName: displayName.trim() || null, - photoURL: photoURL.trim() || null, }) } catch { // ignore firebase update errors for now diff --git a/apps/web/src/components/layout/sidebar.tsx b/apps/web/src/components/layout/sidebar.tsx index fa0df60..f93b2e2 100644 --- a/apps/web/src/components/layout/sidebar.tsx +++ b/apps/web/src/components/layout/sidebar.tsx @@ -54,7 +54,6 @@ export function Sidebar({ className, isCollapsed: controlledCollapsed, onCollaps isCollapsed ? "w-12" : "w-64", className, )} - style={{ zIndex: 1000 }} {...props} > {/* Collapse Toggle */} diff --git a/apps/web/src/components/ticket/ticket-dialog-v2.tsx b/apps/web/src/components/ticket/ticket-dialog-v2.tsx index 83b4f4a..2033ec1 100644 --- a/apps/web/src/components/ticket/ticket-dialog-v2.tsx +++ b/apps/web/src/components/ticket/ticket-dialog-v2.tsx @@ -11,8 +11,6 @@ import { DialogContent, Input, Textarea, - Avatar, - AvatarFallback, Separator, useToast, cn, @@ -37,6 +35,7 @@ import { ConfirmDialog } from '../ui/confirm-dialog'; import { PromptDialog } from '../ui/prompt-dialog'; import { MemberSelectDialog } from '../ui/member-select-dialog'; import { LabelManagerDialog } from '../ui/label-manager-dialog'; +import { UserAvatar } from '@/components/user/user-avatar'; import { MentionDropdown, filterMentionUsers, @@ -127,6 +126,7 @@ export default function TicketDialogV2({ open, onOpenChange, ticketId, boardId } const checklistsQuery = api.tickets.checklists.list.useQuery({ ticketId }, { enabled: open }); const attachmentsQuery = api.tickets.attachments.list.useQuery({ ticketId }, { enabled: open }); const assigneesQuery = api.tickets.assignees.list.useQuery({ ticketId }, { enabled: open }); + const meQuery = api.users.me.useQuery(undefined, { enabled: open }); const labelsQuery = api.tickets.labels.list.useQuery({ ticketId }, { enabled: open }); const boardLabelsQuery = api.boards.labels.list.useQuery({ boardId }, { enabled: open }); const remindersQuery = api.tickets.reminders.list.useQuery({ ticketId }, { enabled: open }); @@ -203,6 +203,7 @@ export default function TicketDialogV2({ open, onOpenChange, ticketId, boardId } username: u.username, first_name: u.first_name, last_name: u.last_name, + avatar: u.avatar ?? null, })); }, [workspaceMembersDetailsQuery.data]); @@ -1092,11 +1093,7 @@ export default function TicketDialogV2({ open, onOpenChange, ticketId, boardId }
{ticketFeedView === 'comments' && canComment && (
- - - {getUserInitials('You')} - - +