From abae56c8b4397869693de5f2fc83483e990e92d8 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 12 Aug 2026 14:44:42 +0545 Subject: [PATCH 1/4] OUT-4029 | Scope task/label deletion by id + workspace, stop deleting Labels rows softDeleteAllSubtasks and deleteAllAssigneeTasks deleted rows keyed on the task label string with no workspaceId filter. Labels has no workspaceId and prefixes derive from brand/company names, so strings like THE10-001 collide across workspaces and deletes wiped other workspaces' tasks. - softDeleteAllSubtasks: select id (not label) and deleteMany by id + workspaceId - deleteAllAssigneeTasks: drop the unscoped label.deleteMany - Remove LabelMappingService.deleteLabel and its call sites; Labels is a next-number registry where orphan rows are harmless and deleting them rolls the counter back, causing duplicate label reissue Co-Authored-By: Claude Opus 4.8 --- .../label-mapping.service.test.ts | 44 ------------------- .../label-mapping/label-mapping.service.ts | 14 ------ src/app/api/tasks/public/public.service.ts | 8 ---- src/app/api/tasks/subtasks.service.ts | 12 ++--- src/app/api/tasks/tasks.service.ts | 10 ----- 5 files changed, 6 insertions(+), 82 deletions(-) delete mode 100644 src/app/api/label-mapping/label-mapping.service.test.ts diff --git a/src/app/api/label-mapping/label-mapping.service.test.ts b/src/app/api/label-mapping/label-mapping.service.test.ts deleted file mode 100644 index 24a195ede..000000000 --- a/src/app/api/label-mapping/label-mapping.service.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -const mockLabelFindFirst = jest.fn() -const mockLabelDelete = jest.fn() - -jest.mock('@/lib/db', () => ({ - __esModule: true, - default: { - getInstance: () => ({ - label: { findFirst: mockLabelFindFirst, delete: mockLabelDelete }, - }), - }, -})) - -jest.mock('@/utils/CopilotAPI', () => ({ CopilotAPI: jest.fn() })) - -import { LabelMappingService } from '@api/label-mapping/label-mapping.service' -import User from '@api/core/models/User.model' -import { UserRole } from '@api/core/types/user' - -const user = { - workspaceId: 'ws-1', - role: UserRole.IU, - internalUserId: 'iu-1', - token: 'token', -} as unknown as User - -describe('LabelMappingService#deleteLabel', () => { - beforeEach(() => jest.clearAllMocks()) - - it('deletes the matching label row', async () => { - mockLabelFindFirst.mockResolvedValue({ id: 'label-1' }) - - await new LabelMappingService(user).deleteLabel('ASS10-009') - - expect(mockLabelDelete).toHaveBeenCalledWith({ where: { id: 'label-1' } }) - }) - - it('no-ops when the label row is already gone', async () => { - mockLabelFindFirst.mockResolvedValue(null) - - await new LabelMappingService(user).deleteLabel('ASS10-009') - - expect(mockLabelDelete).not.toHaveBeenCalled() - }) -}) diff --git a/src/app/api/label-mapping/label-mapping.service.ts b/src/app/api/label-mapping/label-mapping.service.ts index b0d9c2abd..8c44e1991 100644 --- a/src/app/api/label-mapping/label-mapping.service.ts +++ b/src/app/api/label-mapping/label-mapping.service.ts @@ -168,18 +168,4 @@ export class LabelMappingService extends BaseService { }, }) } - - async deleteLabel(label: string) { - const currentLabel = await this.db.label.findFirst({ - where: { - label, - }, - }) - if (!currentLabel) return - await this.db.label.delete({ - where: { - id: currentLabel.id, - }, - }) - } } diff --git a/src/app/api/tasks/public/public.service.ts b/src/app/api/tasks/public/public.service.ts index 6e43f4573..ce0cc8f2a 100644 --- a/src/app/api/tasks/public/public.service.ts +++ b/src/app/api/tasks/public/public.service.ts @@ -369,8 +369,6 @@ export class PublicTasksService extends TasksSharedService { if (!prevTask.assigneeId && assigneeId && assigneeType) { const labelMappingService = new LabelMappingService(this.user) labelMappingService.setTransaction(tx as PrismaClient) - //delete the existing label - await labelMappingService.deleteLabel(prevTask.label) if (validatedIds) { label = z.string().parse(await labelMappingService.getLabel(validatedIds)) } @@ -448,14 +446,8 @@ export class PublicTasksService extends TasksSharedService { } } - //delete the associated label - const labelMappingService = new LabelMappingService(this.user) - // Note: this transaction is timing out in local machine const updatedTask = await this.db.$transaction(async (tx) => { - labelMappingService.setTransaction(tx as PrismaClient) - await labelMappingService.deleteLabel(task?.label) - const deletedTask = await tx.task.update({ where: { id, workspaceId: this.user.workspaceId }, relationLoadStrategy: 'join', diff --git a/src/app/api/tasks/subtasks.service.ts b/src/app/api/tasks/subtasks.service.ts index 8d5a6c218..8ef335a6b 100644 --- a/src/app/api/tasks/subtasks.service.ts +++ b/src/app/api/tasks/subtasks.service.ts @@ -74,10 +74,10 @@ export class SubtaskService extends BaseService { async softDeleteAllSubtasks(id: string) { console.info('SubtasksService#deleteAllSubtasks | Deleting all subtasks for parent with id', id) - const taskLabels = ( - await this.db.$queryRawUnsafe>( + const subtaskIds = ( + await this.db.$queryRawUnsafe>( ` - SELECT "label" + SELECT "id" FROM "Tasks" WHERE "deletedAt" IS NULL AND "workspaceId" = $1 @@ -86,10 +86,10 @@ export class SubtaskService extends BaseService { // even though the rendered SQL works fine when executed in a query console this.user.workspaceId, ) - ).map((row) => row.label) + ).map((row) => row.id) - await this.db.task.deleteMany({ where: { label: { in: taskLabels } } }) - await this.db.label.deleteMany({ where: { label: { in: taskLabels } } }) + // Scope by id + workspaceId: label strings collide across workspaces (Labels has no workspaceId). + await this.db.task.deleteMany({ where: { id: { in: subtaskIds }, workspaceId: this.user.workspaceId } }) } /** diff --git a/src/app/api/tasks/tasks.service.ts b/src/app/api/tasks/tasks.service.ts index 319b9f53b..f078b74dd 100644 --- a/src/app/api/tasks/tasks.service.ts +++ b/src/app/api/tasks/tasks.service.ts @@ -394,8 +394,6 @@ export class TasksService extends TasksSharedService { if (!prevTask.assigneeId && assigneeId && assigneeType) { const labelMappingService = new LabelMappingService(this.user) labelMappingService.setTransaction(tx as PrismaClient) - //delete the existing label - await labelMappingService.deleteLabel(prevTask.label) if (validatedIds) { label = z.string().parse(await labelMappingService.getLabel(validatedIds)) } @@ -488,13 +486,7 @@ export class TasksService extends TasksSharedService { } } - //delete the associated label - const labelMappingService = new LabelMappingService(this.user) - const updatedTask = await this.db.$transaction(async (tx) => { - labelMappingService.setTransaction(tx as PrismaClient) - await labelMappingService.deleteLabel(task?.label) - const deletedTask = await tx.task.update({ where: { id, workspaceId: this.user.workspaceId }, relationLoadStrategy: 'join', @@ -576,12 +568,10 @@ export class TasksService extends TasksSharedService { // If assignee doesn't have an associated task at all, skip logic return [] } - const labels = tasks.map((task) => task.label) await this.db.task.deleteMany({ where: { assigneeId, assigneeType, workspaceId: this.user.workspaceId }, }) - await this.db.label.deleteMany({ where: { label: { in: labels } } }) return tasks } From 5de14025e85d124ea7a1792a8d47fb5c9a3ac614 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 12 Aug 2026 14:45:10 +0545 Subject: [PATCH 2/4] OUT-4029 | Add tests for workspace-scoped task deletion - Unit: softDeleteAllSubtasks and deleteAllAssigneeTasks delete by id/assignee scoped to workspaceId and never touch Labels rows - Integration: two workspaces sharing a label string; deleting one workspace's tree soft-deletes only its own subtree and leaves the other untouched - seedTask: optional label so integration tests can seed colliding labels Co-Authored-By: Claude Opus 4.8 --- .../subtasks.service.integration.test.ts | 53 ++++++++++++++++++ src/app/api/tasks/subtasks.service.test.ts | 49 +++++++++++++++++ ...sks.service.deleteAllAssigneeTasks.test.ts | 54 +++++++++++++++++++ test/integration/db.ts | 3 +- 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/app/api/tasks/subtasks.service.integration.test.ts create mode 100644 src/app/api/tasks/subtasks.service.test.ts create mode 100644 src/app/api/tasks/tasks.service.deleteAllAssigneeTasks.test.ts diff --git a/src/app/api/tasks/subtasks.service.integration.test.ts b/src/app/api/tasks/subtasks.service.integration.test.ts new file mode 100644 index 000000000..ef546f886 --- /dev/null +++ b/src/app/api/tasks/subtasks.service.integration.test.ts @@ -0,0 +1,53 @@ +import { buildLtree } from '@/utils/ltree' +import User from '@api/core/models/User.model' +import { SubtaskService } from '@api/tasks/subtasks.service' + +import { disconnectTestDb, getTestDb, seedTask, truncateAll, uuid } from '../../../../test/integration/db' + +// Real DB; Copilot is doubled only so the service constructs without a live SDK client. +jest.mock('@/utils/CopilotAPI', () => ({ + CopilotAPI: jest.fn().mockImplementation(() => ({})), +})) + +const makeUser = (workspaceId: string) => new User('token', { internalUserId: uuid(), workspaceId } as never) + +// seedTask doesn't set the ltree path; mirror the app's addPathToTask here. +const setPath = async (id: string, ...pathIds: string[]) => { + const path = buildLtree(...pathIds) + await getTestDb().$executeRaw`UPDATE "Tasks" SET path = ${path}::ltree WHERE id::text = ${id}` +} + +describe('SubtaskService.softDeleteAllSubtasks (integration)', () => { + beforeEach(truncateAll) + afterAll(disconnectTestDb) + + // Reproduces OUT-4029: colliding label strings across workspaces must not cause cross-workspace deletes. + it('deletes only the acting workspace subtree when a descendant shares a label string with another workspace', async () => { + const workspaceA = 'ws-a' + const workspaceB = 'ws-b' + const sharedLabel = 'THE10-001' + + // Root is soft-deleted before softDeleteAllSubtasks runs (as in the real call sites), so + // the query returns only live descendants — the collision is on childA. + const rootA = await seedTask({ workspaceId: workspaceA, label: 'THE10-000', deletedAt: new Date() }) + const childA = await seedTask({ workspaceId: workspaceA, parentId: rootA, label: sharedLabel }) + await setPath(rootA, rootA) + await setPath(childA, rootA, childA) + + // Different workspace, different id, but the SAME label string as descendant childA. + const taskB = await seedTask({ workspaceId: workspaceB, label: sharedLabel }) + await setPath(taskB, taskB) + + await new SubtaskService(makeUser(workspaceA)).softDeleteAllSubtasks(rootA) + + const db = getTestDb() + const [ca, b] = await Promise.all([ + db.task.findUnique({ where: { id: childA }, select: { deletedAt: true } }), + db.task.findUnique({ where: { id: taskB }, select: { deletedAt: true } }), + ]) + + expect(ca?.deletedAt).not.toBeNull() + // The colliding-label task in the other workspace must survive. + expect(b?.deletedAt).toBeNull() + }) +}) diff --git a/src/app/api/tasks/subtasks.service.test.ts b/src/app/api/tasks/subtasks.service.test.ts new file mode 100644 index 000000000..48d1d57aa --- /dev/null +++ b/src/app/api/tasks/subtasks.service.test.ts @@ -0,0 +1,49 @@ +const mockQueryRawUnsafe = jest.fn() +const mockTaskDeleteMany = jest.fn() +const mockLabelDeleteMany = jest.fn() + +jest.mock('@/lib/db', () => ({ + __esModule: true, + default: { + getInstance: () => ({ + $queryRawUnsafe: mockQueryRawUnsafe, + task: { deleteMany: mockTaskDeleteMany }, + label: { deleteMany: mockLabelDeleteMany }, + }), + }, +})) + +jest.mock('@/utils/CopilotAPI', () => ({ CopilotAPI: jest.fn() })) + +import { SubtaskService } from '@api/tasks/subtasks.service' +import User from '@api/core/models/User.model' +import { UserRole } from '@api/core/types/user' + +const user = { + workspaceId: 'ws-1', + role: UserRole.IU, + internalUserId: 'iu-1', + token: 'token', +} as unknown as User + +describe('SubtaskService#softDeleteAllSubtasks', () => { + beforeEach(() => jest.clearAllMocks()) + + it('deletes descendant tasks by id scoped to the workspace', async () => { + mockQueryRawUnsafe.mockResolvedValue([{ id: 'task-a' }, { id: 'task-b' }]) + + await new SubtaskService(user).softDeleteAllSubtasks('parent-id') + + expect(mockTaskDeleteMany).toHaveBeenCalledWith({ + where: { id: { in: ['task-a', 'task-b'] }, workspaceId: 'ws-1' }, + }) + }) + + it('never deletes Labels registry rows', async () => { + mockQueryRawUnsafe.mockResolvedValue([{ id: 'task-a' }]) + + await new SubtaskService(user).softDeleteAllSubtasks('parent-id') + + expect(mockLabelDeleteMany).not.toHaveBeenCalled() + }) +}) diff --git a/src/app/api/tasks/tasks.service.deleteAllAssigneeTasks.test.ts b/src/app/api/tasks/tasks.service.deleteAllAssigneeTasks.test.ts new file mode 100644 index 000000000..a86253406 --- /dev/null +++ b/src/app/api/tasks/tasks.service.deleteAllAssigneeTasks.test.ts @@ -0,0 +1,54 @@ +const mockTaskFindMany = jest.fn() +const mockTaskDeleteMany = jest.fn() +const mockLabelDeleteMany = jest.fn() + +jest.mock('@/lib/db', () => ({ + __esModule: true, + default: { + getInstance: () => ({ + task: { findMany: mockTaskFindMany, deleteMany: mockTaskDeleteMany }, + label: { deleteMany: mockLabelDeleteMany }, + }), + }, +})) + +jest.mock('@/utils/CopilotAPI', () => ({ + __esModule: true, + CopilotAPI: jest.fn().mockImplementation(() => ({})), +})) + +import User from '@api/core/models/User.model' +import { TasksService } from '@api/tasks/tasks.service' +import { AssigneeType } from '@prisma/client' + +const makeUser = () => + new User('token', { + internalUserId: 'iu-1', + workspaceId: 'ws-1', + } as never) + +describe('TasksService.deleteAllAssigneeTasks', () => { + beforeEach(() => { + mockTaskFindMany.mockReset() + mockTaskDeleteMany.mockReset() + mockLabelDeleteMany.mockReset() + }) + + it('deletes the assignee tasks scoped to the workspace', async () => { + mockTaskFindMany.mockResolvedValue([{ id: 'task-a', label: 'THE10-001' }]) + + await new TasksService(makeUser()).deleteAllAssigneeTasks('assignee-1', AssigneeType.client) + + expect(mockTaskDeleteMany).toHaveBeenCalledWith({ + where: { assigneeId: 'assignee-1', assigneeType: AssigneeType.client, workspaceId: 'ws-1' }, + }) + }) + + it('never deletes Labels registry rows', async () => { + mockTaskFindMany.mockResolvedValue([{ id: 'task-a', label: 'THE10-001' }]) + + await new TasksService(makeUser()).deleteAllAssigneeTasks('assignee-1', AssigneeType.client) + + expect(mockLabelDeleteMany).not.toHaveBeenCalled() + }) +}) diff --git a/test/integration/db.ts b/test/integration/db.ts index e32da25cb..363b3da86 100644 --- a/test/integration/db.ts +++ b/test/integration/db.ts @@ -75,6 +75,7 @@ export type SeedTaskInput = { parentId?: string | null title?: string createdById?: string + label?: string } // The Tasks table has an `assignee_to_user_id_mapping` CHECK that ties assigneeType to which @@ -101,7 +102,7 @@ export const seedTask = async (input: SeedTaskInput): Promise => { await getTestDb().task.create({ data: { id, - label: `T-${id.slice(0, 8)}`, + label: input.label ?? `T-${id.slice(0, 8)}`, title: input.title ?? 'Reminder task', workspaceId: input.workspaceId, createdById: input.createdById ?? uuid(), From 43832eb42efc01b4573aec1b2fcb663fb5db2279 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 12 Aug 2026 14:45:29 +0545 Subject: [PATCH 3/4] OUT-4029 | Fix authenticate test suite (p-retry ESM) and stale assertion authenticate.test.ts failed to load: p-retry and is-network-error are pure ESM and next/jest only transforms packages listed in transpilePackages. - next.config.js: transpile p-retry / is-network-error (also adds ngrok dev origin) - authenticate.test.ts: update the public-route source fallback assertion from "public" to "platform" to match the intentional change in commit eb76729f (was hidden while the suite couldn't load) Co-Authored-By: Claude Opus 4.8 --- next.config.js | 4 +++- src/app/api/tests/utils/authenticate.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/next.config.js b/next.config.js index cb41fa8c4..5e92d20ee 100644 --- a/next.config.js +++ b/next.config.js @@ -3,6 +3,8 @@ const { withSentryConfig } = require('@sentry/nextjs') /** @type {import('next').NextConfig} */ const nextConfig = { cacheMaxMemorySize: 0, + // Pure-ESM deps; transpile so next/jest can load them from CJS. + transpilePackages: ['p-retry', 'is-network-error'], turbopack: { rules: { '*.svg': { @@ -11,7 +13,7 @@ const nextConfig = { }, }, }, - allowedDevOrigins: ['*.ngrok-free.dev'], + allowedDevOrigins: ['*.ngrok-free.dev', '*.ngrok-free.app'], async redirects() { return [ { diff --git a/src/app/api/tests/utils/authenticate.test.ts b/src/app/api/tests/utils/authenticate.test.ts index 0fc9d8aae..5da156e57 100644 --- a/src/app/api/tests/utils/authenticate.test.ts +++ b/src/app/api/tests/utils/authenticate.test.ts @@ -85,9 +85,9 @@ describe('authenticate util', () => { }) }) - it('uses "public" as source fallback for public routes', async () => { + it('uses "platform" as source fallback for public routes', async () => { const req = new NextRequest(new Request(process.env.VERCEL_URL + '/api/tasks/public/?token=iu-token')) const user = await authenticate(req) - expect(user.assemblyMetadata?.source).toBe('public') + expect(user.assemblyMetadata?.source).toBe('platform') }) }) From 51d44fd933aa703e8e085a4692e069a3ba448e34 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 26 Aug 2026 10:55:02 +0545 Subject: [PATCH 4/4] OUT-4029 | Stop generating task labels; keep label for backward compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stops generating per-task label codes and deletes LabelMappingService and the Labels registry table. Keeps the Tasks.label column (now @default('')) so the public automation API stays backward compatible: pre-existing tasks keep their codes, new tasks return ''. - Public API: label retained in PublicTaskDtoSchema + serializer, with a serializer test locking the contract (empty for new, preserved for old). - Internal app no longer uses label — drag preview shows the workflow-state icon, the breadcrumb shows the task title, the search filter drops the label matcher. - Breadcrumb: desktop truncates the title at 25 chars; below 600px it uses a shorter 12-char title to avoid the platform header overflowing. - Migration (edited in place): set Tasks.label DEFAULT '' and DROP TABLE Labels (the column is kept, not dropped). Co-Authored-By: Claude Opus 4.8 --- .../migration.sql | 2 + prisma/schema/label.prisma | 12 -- prisma/schema/task.prisma | 2 +- .../label-mapping/label-mapping.service.ts | 171 ------------------ .../tasks/public/public.serializer.test.ts | 69 +++++++ src/app/api/tasks/public/public.service.ts | 17 -- .../subtasks.service.integration.test.ts | 18 +- src/app/api/tasks/subtasks.service.test.ts | 10 - src/app/api/tasks/subtasks.service.ts | 2 +- ...sks.service.deleteAllAssigneeTasks.test.ts | 13 +- src/app/api/tasks/tasks.service.ts | 19 -- src/app/detail/[task_id]/[user_type]/page.tsx | 12 +- src/cmd/load-testing/load-testing.service.ts | 10 +- src/components/layouts/HeaderBreadcrumbs.tsx | 4 +- src/hoc/dndKit/TaskDragPreview.tsx | 25 +-- src/hooks/useFilter.tsx | 8 +- src/types/dto/tasks.dto.ts | 3 +- src/utils/optimisticTaskUtils.ts | 1 - test/integration/db.ts | 2 - 19 files changed, 101 insertions(+), 299 deletions(-) create mode 100644 prisma/migrations/20260825000000_drop_task_label_and_labels_table/migration.sql delete mode 100644 prisma/schema/label.prisma delete mode 100644 src/app/api/label-mapping/label-mapping.service.ts create mode 100644 src/app/api/tasks/public/public.serializer.test.ts diff --git a/prisma/migrations/20260825000000_drop_task_label_and_labels_table/migration.sql b/prisma/migrations/20260825000000_drop_task_label_and_labels_table/migration.sql new file mode 100644 index 000000000..ed7b68925 --- /dev/null +++ b/prisma/migrations/20260825000000_drop_task_label_and_labels_table/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "Tasks" ALTER COLUMN "label" SET DEFAULT ''; +DROP TABLE "Labels"; diff --git a/prisma/schema/label.prisma b/prisma/schema/label.prisma deleted file mode 100644 index 64601a482..000000000 --- a/prisma/schema/label.prisma +++ /dev/null @@ -1,12 +0,0 @@ -model Label { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - label String - labelledEntity String - createdAt DateTime @default(now()) @db.Timestamptz() - updatedAt DateTime @updatedAt @db.Timestamptz() - deletedAt DateTime? @db.Timestamptz() - - @@index([labelledEntity, createdAt(sort: Desc)], name: "IX_Labels_labelledEntity_createdAt") - @@index([label, createdAt(sort: Desc)], name: "IX_Labels_label_createdAt") - @@map("Labels") -} diff --git a/prisma/schema/task.prisma b/prisma/schema/task.prisma index 88240d73f..34eedef44 100644 --- a/prisma/schema/task.prisma +++ b/prisma/schema/task.prisma @@ -11,7 +11,7 @@ enum Source { model Task { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - label String + label String @default("") workspaceId String @db.VarChar(32) assigneeId String? @db.Uuid internalUserId String? @db.Uuid diff --git a/src/app/api/label-mapping/label-mapping.service.ts b/src/app/api/label-mapping/label-mapping.service.ts deleted file mode 100644 index 8c44e1991..000000000 --- a/src/app/api/label-mapping/label-mapping.service.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { AssigneeType } from '@prisma/client' -import { BaseService } from '@api/core/services/base.service' -import { CopilotAPI } from '@/utils/CopilotAPI' -import { z } from 'zod' -import { Label as PrismaLabelMapping } from '@prisma/client' -import { ClientResponse, CompanyResponse } from '@/types/common' - -export class LabelMappingService extends BaseService { - /** - * This method returns the computed label - * @param assigneeId - * @param assigneeType - * @returns string | null - */ - async getLabel(userIds: { internalUserId: string | null; clientId: string | null; companyId: string | null }) { - const { internalUserId, clientId, companyId } = userIds - if (!internalUserId && !clientId && !companyId) { - const existingLabel = await this.db.label.findFirst({ - where: { - labelledEntity: 'Unassigned', - }, - orderBy: { - createdAt: 'desc', - }, - }) - if (!existingLabel) { - const label = 'NOA-001' - await this.insertLabelMapping(label, 'Unassigned') - return 'NOA-001' - } - const newLabel = `NOA-${this.getNextLabelCount(existingLabel.label)}` - await this.insertLabelMapping(newLabel, 'Unassigned') - return newLabel - } - - if (internalUserId) { - const workspace = await this.copilot.getWorkspace() - return await this.generateLabel(z.string().parse(workspace.brandName)) - } - if (clientId) { - let client: ClientResponse, company: CompanyResponse - if (companyId) { - ;[client, company] = await Promise.all([this.copilot.getClient(clientId), this.copilot.getCompany(companyId)]) - } else { - client = await this.copilot.getClient(clientId) - company = await this.copilot.getCompany(client.companyId) - } - //client is not assigned in a company - if (company.isPlaceholder) { - return await this.generateLabel(client.givenName) - } - return await this.generateLabel(company.name) - } - if (companyId) { - const company = await this.copilot.getCompany(companyId) - return await this.generateLabel(company.name) - } - } - - /** - * This method computes the label string from the given labelledEntity. - * @param labelledEntity Entity from which label is generated. This should be workspace.brandName for task assigned to IU, - * company.name for task assigned to client or company. If client isn't assigned to any company (i.e. client is assigned to placeholder company), - * then this should be client.givenName - * @returns string - */ - private async generateLabel(labelledEntity: string) { - //baseLabel is the first 3 substring of the labelledEntity - const baseLabel = labelledEntity?.substring(0, 3).toUpperCase() - - //find the latest updated LabelMapping for the given labelledEntity - const existingLabelWithCurrentLabelEntity = await this.db.label.findFirst({ - where: { - labelledEntity, - }, - orderBy: { - createdAt: 'desc', - }, - }) - - //find all the LabelMapping that starts with the baseLabel substring - const existingLabelWithSameBaseLabel = await this.db.label.findMany({ - where: { - label: { - startsWith: baseLabel, - }, - }, - orderBy: { - createdAt: 'desc', - }, - }) - - //case I -> if no items are found with the baseLabel. In this case, existingLabelWithSameBaseLabel is also undefined - if (existingLabelWithSameBaseLabel.length === 0) { - const label = `${baseLabel}-001` - await this.insertLabelMapping(label, labelledEntity) - return label - } - - //case II -> if existingLabelWithCurrentLabelEntity is undefined but there are existingLabelWithSameBaseLabel - if (!existingLabelWithCurrentLabelEntity && existingLabelWithSameBaseLabel.length > 0) { - //suffixNumber is the number assigned to conflicting labels. for ex: OUT2-001 where 2 is suffix number - //The line below finds the highest suffix number from the existingLabelWithCurrentLabelEntity. If no suffixNumber is found then it returns null. - const suffixNumber = this.extractLabelSuffixNumber( - this.findLabelMappingWithHighestSuffix(existingLabelWithSameBaseLabel).label, - ) - const currentBaseLabel = `${baseLabel}${suffixNumber ? Number(suffixNumber) + 1 : '2'}` - const newLabel = `${currentBaseLabel}-001` - await this.insertLabelMapping(newLabel, labelledEntity) - return newLabel - } - - //case III -> if both existingLabelWithSameBaseLabel and existingLabelWithCurrentLabelEntity are truthy - if (existingLabelWithCurrentLabelEntity && existingLabelWithSameBaseLabel.length > 0) { - const newLabel = `${existingLabelWithCurrentLabelEntity.label.split('-')[0]}-${this.getNextLabelCount(existingLabelWithCurrentLabelEntity.label)}` - await this.insertLabelMapping(newLabel, labelledEntity) - return newLabel - } - } - - /** - * This method finds the LabelMapping with the highest suffix. - * ex: [{label: "COP1-001"}, {label: "COP2-002"}] will return {label: "COP2-002"} where 2 is the highest suffix - * @param labels PrismaLabelMapping[] - * @returns PrismaLabelMapping - */ - private findLabelMappingWithHighestSuffix(labels: PrismaLabelMapping[]) { - return labels.reduce((maxLabel, currentLabel) => { - const maxSuffix = this.extractLabelSuffixNumber(maxLabel.label) || 0 - const currentSuffix = this.extractLabelSuffixNumber(currentLabel.label) || 0 - return currentSuffix > maxSuffix ? currentLabel : maxLabel - }, labels[0]) - } - - /** - * This method returns the next count of the label ex: if COP-005 is passed it returns 006 - * @param label - * @returns string - */ - private getNextLabelCount(label: string) { - let str = label.split('-')[1] - - // let num = parseInt(str, 10) - - // num += 1 - const num = +str + 1 - - let incrementedStr = num.toString().padStart(str.length, '0') - - return incrementedStr - } - - /** - * This method extracts the suffix number from the label ex: COP22-010 returns 22 - * @param label - * @returns string - */ - private extractLabelSuffixNumber(label: string) { - const match = label.match(/[A-Z]+(\d+)-\d+/) - return match?.[1] ?? null - } - - private async insertLabelMapping(label: string, labelledEntity: string) { - await this.db.label.create({ - data: { - label, - labelledEntity, - }, - }) - } -} diff --git a/src/app/api/tasks/public/public.serializer.test.ts b/src/app/api/tasks/public/public.serializer.test.ts new file mode 100644 index 000000000..927a01911 --- /dev/null +++ b/src/app/api/tasks/public/public.serializer.test.ts @@ -0,0 +1,69 @@ +jest.mock('@/utils/CopilotAPI', () => ({ CopilotAPI: class {} })) +jest.mock('@/lib/db', () => ({ __esModule: true, default: { getInstance: () => ({}) } })) +jest.mock('@/app/api/attachments/public/public.serializer', () => ({ + PublicAttachmentSerializer: { serializeAttachments: jest.fn(async () => []) }, +})) +jest.mock('@/utils/santizeContents', () => ({ sanitizeHtml: (value: string) => value })) +jest.mock('@/utils/signedUrlReplacer', () => ({ + replaceMediaSources: jest.fn(async (value: string) => value), + replaceImageSrc: jest.fn(async (value: string) => value), +})) +jest.mock('@/utils/signUrl', () => ({ getSignedUrl: jest.fn(async (value: string) => value) })) +jest.mock('@/utils/signedTemplateUrlReplacer', () => ({ + copyTemplateMediaToTask: jest.fn(async (_workspaceId: string, value: string) => value), +})) + +import { PublicTaskSerializer, TaskWithWorkflowStateAndAttachments } from './public.serializer' + +const IU_ID = '22222222-2222-2222-2222-222222222222' + +const makeTask = (overrides: Partial = {}): TaskWithWorkflowStateAndAttachments => + ({ + id: '11111111-1111-1111-1111-111111111111', + title: 'A task', + body: null, + parentId: null, + dueDate: null, + label: '', + templateId: null, + createdById: IU_ID, + completedAt: null, + createdAt: new Date('2026-01-01T00:00:00.000Z'), + isArchived: false, + lastArchivedDate: null, + archivedBy: null, + deletedAt: null, + source: 'api', + deletedBy: null, + completedBy: null, + completedByUserType: null, + internalUserId: IU_ID, + clientId: null, + companyId: null, + associations: [], + isShared: false, + workflowState: { type: 'started' }, + attachments: [], + ...overrides, + }) as unknown as TaskWithWorkflowStateAndAttachments + +// OUT-4029: label generation was removed but the public API keeps the `label` +// field for backward compatibility — empty string for new tasks, the stored +// value for pre-existing ones. These lock that contract so a future change +// can't silently drop the field again. +describe('PublicTaskSerializer — label backward compatibility', () => { + it('returns an empty-string label for tasks created after label generation was removed', async () => { + const result = await PublicTaskSerializer.serializeUnsafe(makeTask({ label: '' })) + expect(result.label).toBe('') + }) + + it('preserves the pre-existing generated label for older tasks', async () => { + const result = await PublicTaskSerializer.serializeUnsafe(makeTask({ label: 'OUT-001' })) + expect(result.label).toBe('OUT-001') + }) + + it('keeps label in the schema-validated public DTO', async () => { + const result = await PublicTaskSerializer.serialize(makeTask({ label: 'OUT-001' })) + expect(result.label).toBe('OUT-001') + }) +}) diff --git a/src/app/api/tasks/public/public.service.ts b/src/app/api/tasks/public/public.service.ts index ce0cc8f2a..0cf9de5c6 100644 --- a/src/app/api/tasks/public/public.service.ts +++ b/src/app/api/tasks/public/public.service.ts @@ -20,7 +20,6 @@ import { AssigneeType, Prisma, PrismaClient, Source, StateType, Task, TaskTempla import httpStatus from 'http-status' import z from 'zod' import APIError from '@api/core/exceptions/api' -import { LabelMappingService } from '@api/label-mapping/label-mapping.service' import { SubtaskService } from '@api/tasks/subtasks.service' import { TasksActivityLogger } from '@api/tasks/tasks.logger' import { TemplatesService } from '@api/tasks/templates/templates.service' @@ -143,10 +142,6 @@ export class PublicTasksService extends TasksSharedService { companyId: validatedIds.companyId, }) - //generate the label - const labelMappingService = new LabelMappingService(this.user) - const label = z.string().parse(await labelMappingService.getLabel(validatedIds)) - console.info('PublicTasksService#createTask | Generated label for task:', label) if (data.parentId) { const canCreateSubTask = await this.canCreateSubTask(data.parentId) if (!canCreateSubTask) { @@ -190,7 +185,6 @@ export class PublicTasksService extends TasksSharedService { ...data, workspaceId: this.user.workspaceId, createdById, - label: label, completedBy, completedByUserType, source: Source.api, @@ -364,16 +358,6 @@ export class PublicTasksService extends TasksSharedService { const subtaskService = new SubtaskService(this.user) let updatedTask = await this.db.$transaction(async (tx) => { - //generate new label if prevTask has no assignee but now assigned to someone - let label: string = prevTask.label - if (!prevTask.assigneeId && assigneeId && assigneeType) { - const labelMappingService = new LabelMappingService(this.user) - labelMappingService.setTransaction(tx as PrismaClient) - if (validatedIds) { - label = z.string().parse(await labelMappingService.getLabel(validatedIds)) - } - } - // Set / reset lastArchivedDate if isArchived has been triggered, else remove it from the update query let lastArchivedDate: Date | undefined | null = undefined let archivedBy: string | null | undefined = undefined @@ -387,7 +371,6 @@ export class PublicTasksService extends TasksSharedService { where: { id }, data: { ...dataWithoutUserIds, - label, lastArchivedDate, archivedBy, completedBy, diff --git a/src/app/api/tasks/subtasks.service.integration.test.ts b/src/app/api/tasks/subtasks.service.integration.test.ts index ef546f886..2a5551c2b 100644 --- a/src/app/api/tasks/subtasks.service.integration.test.ts +++ b/src/app/api/tasks/subtasks.service.integration.test.ts @@ -21,21 +21,20 @@ describe('SubtaskService.softDeleteAllSubtasks (integration)', () => { beforeEach(truncateAll) afterAll(disconnectTestDb) - // Reproduces OUT-4029: colliding label strings across workspaces must not cause cross-workspace deletes. - it('deletes only the acting workspace subtree when a descendant shares a label string with another workspace', async () => { + // OUT-4029: deletion is scoped by id + workspaceId. A task in another workspace must + // never be soft-deleted when we delete the acting workspace's subtree. + it('deletes only the acting workspace subtree and leaves other workspaces untouched', async () => { const workspaceA = 'ws-a' const workspaceB = 'ws-b' - const sharedLabel = 'THE10-001' - // Root is soft-deleted before softDeleteAllSubtasks runs (as in the real call sites), so - // the query returns only live descendants — the collision is on childA. - const rootA = await seedTask({ workspaceId: workspaceA, label: 'THE10-000', deletedAt: new Date() }) - const childA = await seedTask({ workspaceId: workspaceA, parentId: rootA, label: sharedLabel }) + // Root is soft-deleted before softDeleteAllSubtasks runs (as at the real call sites), + // so the descendant query returns only the live child. + const rootA = await seedTask({ workspaceId: workspaceA, deletedAt: new Date() }) + const childA = await seedTask({ workspaceId: workspaceA, parentId: rootA }) await setPath(rootA, rootA) await setPath(childA, rootA, childA) - // Different workspace, different id, but the SAME label string as descendant childA. - const taskB = await seedTask({ workspaceId: workspaceB, label: sharedLabel }) + const taskB = await seedTask({ workspaceId: workspaceB }) await setPath(taskB, taskB) await new SubtaskService(makeUser(workspaceA)).softDeleteAllSubtasks(rootA) @@ -47,7 +46,6 @@ describe('SubtaskService.softDeleteAllSubtasks (integration)', () => { ]) expect(ca?.deletedAt).not.toBeNull() - // The colliding-label task in the other workspace must survive. expect(b?.deletedAt).toBeNull() }) }) diff --git a/src/app/api/tasks/subtasks.service.test.ts b/src/app/api/tasks/subtasks.service.test.ts index 48d1d57aa..6341516e5 100644 --- a/src/app/api/tasks/subtasks.service.test.ts +++ b/src/app/api/tasks/subtasks.service.test.ts @@ -1,6 +1,5 @@ const mockQueryRawUnsafe = jest.fn() const mockTaskDeleteMany = jest.fn() -const mockLabelDeleteMany = jest.fn() jest.mock('@/lib/db', () => ({ __esModule: true, @@ -8,7 +7,6 @@ jest.mock('@/lib/db', () => ({ getInstance: () => ({ $queryRawUnsafe: mockQueryRawUnsafe, task: { deleteMany: mockTaskDeleteMany }, - label: { deleteMany: mockLabelDeleteMany }, }), }, })) @@ -38,12 +36,4 @@ describe('SubtaskService#softDeleteAllSubtasks', () => { where: { id: { in: ['task-a', 'task-b'] }, workspaceId: 'ws-1' }, }) }) - - it('never deletes Labels registry rows', async () => { - mockQueryRawUnsafe.mockResolvedValue([{ id: 'task-a' }]) - - await new SubtaskService(user).softDeleteAllSubtasks('parent-id') - - expect(mockLabelDeleteMany).not.toHaveBeenCalled() - }) }) diff --git a/src/app/api/tasks/subtasks.service.ts b/src/app/api/tasks/subtasks.service.ts index 8ef335a6b..6a928eca8 100644 --- a/src/app/api/tasks/subtasks.service.ts +++ b/src/app/api/tasks/subtasks.service.ts @@ -88,7 +88,7 @@ export class SubtaskService extends BaseService { ) ).map((row) => row.id) - // Scope by id + workspaceId: label strings collide across workspaces (Labels has no workspaceId). + // Scope by id + workspaceId so a delete never crosses into another workspace's tasks. await this.db.task.deleteMany({ where: { id: { in: subtaskIds }, workspaceId: this.user.workspaceId } }) } diff --git a/src/app/api/tasks/tasks.service.deleteAllAssigneeTasks.test.ts b/src/app/api/tasks/tasks.service.deleteAllAssigneeTasks.test.ts index a86253406..987a31a78 100644 --- a/src/app/api/tasks/tasks.service.deleteAllAssigneeTasks.test.ts +++ b/src/app/api/tasks/tasks.service.deleteAllAssigneeTasks.test.ts @@ -1,13 +1,11 @@ const mockTaskFindMany = jest.fn() const mockTaskDeleteMany = jest.fn() -const mockLabelDeleteMany = jest.fn() jest.mock('@/lib/db', () => ({ __esModule: true, default: { getInstance: () => ({ task: { findMany: mockTaskFindMany, deleteMany: mockTaskDeleteMany }, - label: { deleteMany: mockLabelDeleteMany }, }), }, })) @@ -31,11 +29,10 @@ describe('TasksService.deleteAllAssigneeTasks', () => { beforeEach(() => { mockTaskFindMany.mockReset() mockTaskDeleteMany.mockReset() - mockLabelDeleteMany.mockReset() }) it('deletes the assignee tasks scoped to the workspace', async () => { - mockTaskFindMany.mockResolvedValue([{ id: 'task-a', label: 'THE10-001' }]) + mockTaskFindMany.mockResolvedValue([{ id: 'task-a' }]) await new TasksService(makeUser()).deleteAllAssigneeTasks('assignee-1', AssigneeType.client) @@ -43,12 +40,4 @@ describe('TasksService.deleteAllAssigneeTasks', () => { where: { assigneeId: 'assignee-1', assigneeType: AssigneeType.client, workspaceId: 'ws-1' }, }) }) - - it('never deletes Labels registry rows', async () => { - mockTaskFindMany.mockResolvedValue([{ id: 'task-a', label: 'THE10-001' }]) - - await new TasksService(makeUser()).deleteAllAssigneeTasks('assignee-1', AssigneeType.client) - - expect(mockLabelDeleteMany).not.toHaveBeenCalled() - }) }) diff --git a/src/app/api/tasks/tasks.service.ts b/src/app/api/tasks/tasks.service.ts index f078b74dd..6af3d1d23 100644 --- a/src/app/api/tasks/tasks.service.ts +++ b/src/app/api/tasks/tasks.service.ts @@ -14,7 +14,6 @@ import APIError from '@api/core/exceptions/api' import { PoliciesService } from '@api/core/services/policies.service' import { Resource } from '@api/core/types/api' import { UserAction, UserRole } from '@api/core/types/user' -import { LabelMappingService } from '@api/label-mapping/label-mapping.service' import { PublicTaskSerializer } from '@api/tasks/public/public.serializer' import { SubtaskCascadePair, SubtaskService } from '@api/tasks/subtasks.service' import { dispatchUpdatedWebhookEvent, getArchivedStatus, getTaskTimestamps } from '@api/tasks/tasks.helpers' @@ -130,11 +129,6 @@ export class TasksService extends TasksSharedService { companyId: validatedIds.companyId, }) - //generate the label - const labelMappingService = new LabelMappingService(this.user) - const label = z.string().parse(await labelMappingService.getLabel(validatedIds)) - console.info('TasksService#createTask | Generated label for task:', label) - if (data.parentId) { const canCreateSubTask = await this.canCreateSubTask(data.parentId) if (!canCreateSubTask) { @@ -171,7 +165,6 @@ export class TasksService extends TasksSharedService { ...data, workspaceId: this.user.workspaceId, createdById, - label: label, completedBy, completedByUserType, source: Source.web, @@ -389,16 +382,6 @@ export class TasksService extends TasksSharedService { const cascadeAccessWhere = willCascade ? await this.getAccessFilterForTasks() : undefined let updatedTask = await this.db.$transaction(async (tx) => { - //generate new label if prevTask has no assignee but now assigned to someone - let label: string = prevTask.label - if (!prevTask.assigneeId && assigneeId && assigneeType) { - const labelMappingService = new LabelMappingService(this.user) - labelMappingService.setTransaction(tx as PrismaClient) - if (validatedIds) { - label = z.string().parse(await labelMappingService.getLabel(validatedIds)) - } - } - // Set / reset lastArchivedDate if isArchived has been triggered, else remove it from the update query let lastArchivedDate: Date | undefined | null = undefined let archivedBy: string | null | undefined = undefined @@ -412,7 +395,6 @@ export class TasksService extends TasksSharedService { where: { id }, data: { ...dataWithoutUserIds, - label, lastArchivedDate, archivedBy, completedBy, @@ -720,7 +702,6 @@ export class TasksService extends TasksSharedService { select: { id: true, title: true, - label: true, clientId: true, companyId: true, internalUserId: true, diff --git a/src/app/detail/[task_id]/[user_type]/page.tsx b/src/app/detail/[task_id]/[user_type]/page.tsx index 7e2355347..189fb60ba 100644 --- a/src/app/detail/[task_id]/[user_type]/page.tsx +++ b/src/app/detail/[task_id]/[user_type]/page.tsx @@ -87,13 +87,11 @@ export default async function TaskDetailPage(props: { const isPreviewMode = !!getPreviewMode(tokenPayload) - const breadcrumbItems: { label: string; mobileLabel: string; href: string }[] = (taskPath || []).map( - ({ title, label, id }) => ({ - label: truncateText(title, 25), - mobileLabel: label, - href: `/detail/${id}/${user_type}?token=${token}`, - }), - ) + const breadcrumbItems: { label: string; mobileLabel: string; href: string }[] = (taskPath || []).map(({ title, id }) => ({ + label: truncateText(title, 25), + mobileLabel: truncateText(title, 12), + href: `/detail/${id}/${user_type}?token=${token}`, + })) // flag that determines if the current user is the task viewer const isViewer = checkIfTaskViewer(task.associations, tokenPayload) diff --git a/src/cmd/load-testing/load-testing.service.ts b/src/cmd/load-testing/load-testing.service.ts index 80373385e..726bea9af 100644 --- a/src/cmd/load-testing/load-testing.service.ts +++ b/src/cmd/load-testing/load-testing.service.ts @@ -1,5 +1,4 @@ import { authenticateWithToken } from '@/app/api/core/utils/authenticate' -import { LabelMappingService } from '@/app/api/label-mapping/label-mapping.service' import DBClient from '@/lib/db' import { ClientRequest, @@ -92,6 +91,7 @@ class LoadTester { const data: Omit< Task, | 'id' + | 'label' | 'completedAt' | 'deletedAt' | 'lastActivityLogUpdated' @@ -112,7 +112,6 @@ class LoadTester { | 'isShared' >[] = [] const currentUser = await authenticateWithToken(this.token, this.apiKey) - const labelsService = new LabelMappingService(currentUser, this.apiKey) const workflowStates = await this.db.workflowState.findMany({ where: { type: { not: 'completed' }, @@ -130,13 +129,6 @@ class LoadTester { createdById: z.string().parse(currentUser.internalUserId), workflowStateId: workflowStates[Math.floor(Math.random() * workflowStates.length)].id, workspaceId: currentUser.workspaceId, - label: z.string().parse( - await labelsService.getLabel({ - internalUserId: null, - clientId: null, - companyId: null, - }), - ), //passed a static value of userIds null for label. label wont work properly. This load testing service needs to be revamped in the future if we need this to run properly. assigneeId: user.id, assigneeType, assignedAt: getRandomBool() ? getRandomFutureDate() : null, diff --git a/src/components/layouts/HeaderBreadcrumbs.tsx b/src/components/layouts/HeaderBreadcrumbs.tsx index 6ec8c0105..6f6c49ce5 100644 --- a/src/components/layouts/HeaderBreadcrumbs.tsx +++ b/src/components/layouts/HeaderBreadcrumbs.tsx @@ -30,7 +30,7 @@ export const HeaderBreadcrumbs = ({ const router = useRouter() const windowWidth = useWindowWidth() // Below 600px the platform-rendered header overflows with long titles, - // so fall back to the shorter task label that we send via app-bridge. + // so fall back to the shorter mobileLabel (a more aggressively truncated title). const isMobile = windowWidth < 600 && windowWidth !== 0 const displayItems = useMemo( @@ -78,7 +78,7 @@ export const HeaderBreadcrumbs = ({ const isLast = index === displayItems.length - 1 return ( - + {isLast ? ( <> diff --git a/src/hoc/dndKit/TaskDragPreview.tsx b/src/hoc/dndKit/TaskDragPreview.tsx index 349b5ad51..e0581ec1f 100644 --- a/src/hoc/dndKit/TaskDragPreview.tsx +++ b/src/hoc/dndKit/TaskDragPreview.tsx @@ -5,8 +5,9 @@ import { TaskCard } from '@/components/cards/TaskCard' import { ArchiveBoxIcon } from '@/icons' import { selectTaskBoard } from '@/redux/features/taskBoardSlice' import { TaskResponse } from '@/types/dto/tasks.dto' -import { View } from '@/types/interfaces' +import { Sizes, View } from '@/types/interfaces' import { getCardHref } from '@/utils/getCardHref' +import { statusIcons } from '@/utils/iconMatcher' import { Box, Stack, Typography } from '@mui/material' import { useSelector } from 'react-redux' @@ -35,8 +36,8 @@ export function TaskDragPreview({ task, mode }: Props) { return ( - theme.color.gray[500], - flexGrow: 0, - flexShrink: 0, - minWidth: '75px', - lineHeight: '21px', - }} - > - {task.label} - + {workflowState && ( + + {statusIcons[Sizes.SMALL][workflowState.type]} + + )} {task.title} diff --git a/src/hooks/useFilter.tsx b/src/hooks/useFilter.tsx index cc66b215a..a1a34dcda 100644 --- a/src/hooks/useFilter.tsx +++ b/src/hooks/useFilter.tsx @@ -9,7 +9,6 @@ import { useSelector } from 'react-redux' interface KeywordMatchable { title?: string body?: string - label?: string assigneeId?: string internalUserId?: string | null clientId?: string | null @@ -101,12 +100,7 @@ function filterByKeyword( .filter(Boolean) .some((name) => name && name.includes(keyword)) - return ( - task.title?.toLowerCase().includes(keyword) || - task.body?.toLowerCase().includes(keyword) || - task.label?.toLowerCase().includes(keyword) || - assigneeMatches - ) + return task.title?.toLowerCase().includes(keyword) || task.body?.toLowerCase().includes(keyword) || assigneeMatches } const keywordMatchingParentIds = new Set( diff --git a/src/types/dto/tasks.dto.ts b/src/types/dto/tasks.dto.ts index e1ca16cfc..57b058d48 100644 --- a/src/types/dto/tasks.dto.ts +++ b/src/types/dto/tasks.dto.ts @@ -111,7 +111,6 @@ export type UpdateTaskRequest = z.infer export const TaskResponseSchema = z.object({ id: z.string(), - label: z.string(), workspaceId: z.string(), assigneeId: z.string().optional(), assigneeType: AssigneeTypeSchema, @@ -146,7 +145,7 @@ export const SubTaskStatusSchema = z.object({ export type SubTaskStatusResponse = z.infer -export type AncestorTaskResponse = Pick & { +export type AncestorTaskResponse = Pick & { internalUserId: string | null clientId: string | null companyId: string | null diff --git a/src/utils/optimisticTaskUtils.ts b/src/utils/optimisticTaskUtils.ts index cd0e4c0cb..f6e7f89ca 100644 --- a/src/utils/optimisticTaskUtils.ts +++ b/src/utils/optimisticTaskUtils.ts @@ -16,7 +16,6 @@ export const getTempTask = ( ) => { return { id: tempId, - label: 'temp-label', workspaceId: workspaceId, assigneeId: (payload.internalUserId || payload.clientId || payload.companyId) ?? '', internalUserId: payload.internalUserId ?? null, diff --git a/test/integration/db.ts b/test/integration/db.ts index 363b3da86..d5014a799 100644 --- a/test/integration/db.ts +++ b/test/integration/db.ts @@ -75,7 +75,6 @@ export type SeedTaskInput = { parentId?: string | null title?: string createdById?: string - label?: string } // The Tasks table has an `assignee_to_user_id_mapping` CHECK that ties assigneeType to which @@ -102,7 +101,6 @@ export const seedTask = async (input: SeedTaskInput): Promise => { await getTestDb().task.create({ data: { id, - label: input.label ?? `T-${id.slice(0, 8)}`, title: input.title ?? 'Reminder task', workspaceId: input.workspaceId, createdById: input.createdById ?? uuid(),