Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a98a31d
feat(OUT-2754): support subTask templates for templates
arpandhakal Dec 8, 2025
859814b
feat(OUT-2752): replace template details modal with a template page r…
arpandhakal Dec 10, 2025
f26ece1
fix(OUT-2752): realtime on subtemplates insert/update when user is on…
arpandhakal Dec 11, 2025
92150ff
fix(OUT-2752): breadcrumbs on template detail page
arpandhakal Dec 11, 2025
de8ecbf
fix(OUT-2752): breadcrumbs on template detail page
arpandhakal Dec 11, 2025
8036426
fix(OUT-2752): breadcrumbs on template detail page
arpandhakal Dec 11, 2025
f8b157d
feat(OUT-2752): app bridge implementation for manage template details…
arpandhakal Dec 11, 2025
7af905e
feat(OUT-2752): app bridge implementation for manage template details…
arpandhakal Dec 11, 2025
fdf5534
fix(OUT-2752): fixed imports
arpandhakal Dec 11, 2025
5229c28
fix(OUT-2752): app bridge not loading functions from react client com…
arpandhakal Dec 11, 2025
2a69fad
fix(OUT-2752): app bridge not loading functions from react client com…
arpandhakal Dec 11, 2025
60b4ab1
fix(OUT-2752): delete logic fixed
arpandhakal Dec 11, 2025
161047b
feat(OUT-2752): mechanism to redirect back from templates details pag…
arpandhakal Dec 11, 2025
2ca4f5a
fix(OUT-2752): everything
arpandhakal Dec 11, 2025
364d729
fix(OUT-2752): applied requested changes
arpandhakal Dec 12, 2025
81e616a
fix(OUT-2752): applied requested changes
arpandhakal Dec 12, 2025
5807149
feat(OUT-2755): integrated sub tasks creation while applying template…
arpandhakal Dec 11, 2025
3219500
fix(OUT-2755): code cleanups, fixes on error handling
arpandhakal Dec 12, 2025
887a964
fix(OUT-2755): fix everything
arpandhakal Dec 15, 2025
3cf3ce8
feat(OUT-2759): subtask indicators in create task form
arpandhakal Dec 15, 2025
4c19af1
fix(OUT-2798): layout fix on template and task create form
arpandhakal Dec 16, 2025
8c570ee
fix(OUT-2799): caching issue on manage templates page and template de…
arpandhakal Dec 16, 2025
63dcc56
fix(OUT-2804): crm subtemplate fixes
arpandhakal Dec 16, 2025
de9cadc
fix(OUT-2804): realtime fixes on template
arpandhakal Dec 18, 2025
8d5e73c
fix(OUT-2807): notification center view template issues
arpandhakal Dec 16, 2025
9613ec3
fix(OUT-2810): fixed templates card rearraging on hover and some desi…
arpandhakal Dec 17, 2025
b36321c
fix(OUT-2810): appended z on realtime templates
arpandhakal Dec 17, 2025
d90e79a
fix(OUT-2810): made generic functions for task and templates instead …
arpandhakal Dec 18, 2025
441e21b
fix(OUT-2810): made generic functions for task and templates instead …
arpandhakal Dec 18, 2025
090d895
fix(OUT-2813): subtasks not being created in proper order from templates
arpandhakal Dec 17, 2025
707be21
fix(OUT-2816): new task form description height
arpandhakal Dec 18, 2025
499be98
fix(OUT-2816): task being created without adding template description
arpandhakal Dec 18, 2025
a98d515
fix(OUT-2827): showing subtasks on CRM view.
arpandhakal Dec 19, 2025
8c9914e
fix(OUT-2827): remove logs
arpandhakal Dec 19, 2025
28223c9
fix(OUT-2827): remove logs
arpandhakal Dec 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "TaskTemplates" ADD COLUMN "parentId" UUID;

-- AddForeignKey
ALTER TABLE "TaskTemplates" ADD CONSTRAINT "TaskTemplates_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "TaskTemplates"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "TaskTemplates_workspaceId_parentId_createdAt_idx" ON "TaskTemplates"("workspaceId", "parentId", "createdAt" DESC);
6 changes: 6 additions & 0 deletions prisma/schema/taskTemplate.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ model TaskTemplate {
workflowState WorkflowState @relation(fields: [workflowStateId], references: [id], onDelete: Cascade)
workflowStateId String @db.Uuid

//subTaskTemplates
parentId String? @db.Uuid
parent TaskTemplate? @relation("TaskTemplateHierarchy", fields: [parentId], references: [id], onDelete: Cascade)
subTaskTemplates TaskTemplate[] @relation("TaskTemplateHierarchy")

createdById String @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Expand All @@ -14,5 +19,6 @@ model TaskTemplate {
scrapMedias ScrapMedia[]

@@unique([title, workspaceId, deletedAt], name: "UQ_TaskTemplates_title_workspaceId_deletedAt")
@@index([workspaceId, parentId, createdAt(sort: Desc)])
@@map("TaskTemplates")
}
17 changes: 12 additions & 5 deletions src/app/(home)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ import { CreateTaskRequest, UpdateTaskRequest } from '@/types/dto/tasks.dto'
import { CreateViewSettingsDTO } from '@/types/dto/viewSettings.dto'
import { AssigneeType } from '@prisma/client'

export const handleCreate = async (token: string, payload: CreateTaskRequest) => {
export const handleCreate = async (
token: string,
payload: CreateTaskRequest,
opts?: { disableSubtaskTemplates?: boolean },
) => {
try {
const response = await fetch(`${apiUrl}/api/tasks?token=${token}`, {
method: 'POST',
body: JSON.stringify(payload),
})
const response = await fetch(
`${apiUrl}/api/tasks?token=${token}&disableSubtaskTemplates=${opts?.disableSubtaskTemplates}`,
{
method: 'POST',
body: JSON.stringify(payload),
},
)
return await response.json()
} catch (e: unknown) {
console.error('Something went wrong while creating task!', e)
Expand Down
21 changes: 12 additions & 9 deletions src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { UserRole } from '@api/core/types/user'
import { Suspense } from 'react'
import { z } from 'zod'
import { fetchWithErrorHandler } from '@/app/_fetchers/fetchWithErrorHandler'
import { RealTimeTemplates } from '@/hoc/RealtimeTemplates'

export async function getAllWorkflowStates(token: string): Promise<WorkflowStateResponse[]> {
const res = await fetch(`${apiUrl}/api/workflow-states?token=${token}`, {
Expand Down Expand Up @@ -133,15 +134,17 @@ export default async function Main({
</Suspense>

<RealTime tokenPayload={tokenPayload}>
<DndWrapper>
<TaskBoard mode={UserRole.IU} workspace={workspace} token={token} />
</DndWrapper>
<ModalNewTaskForm
handleCreateMultipleAttachments={async (attachments: CreateAttachmentRequest[]) => {
'use server'
await createMultipleAttachments(token, attachments)
}}
/>
<RealTimeTemplates tokenPayload={tokenPayload} token={token}>
<DndWrapper>
<TaskBoard mode={UserRole.IU} workspace={workspace} token={token} />
</DndWrapper>
<ModalNewTaskForm
handleCreateMultipleAttachments={async (attachments: CreateAttachmentRequest[]) => {
'use server'
await createMultipleAttachments(token, attachments)
}}
/>
</RealTimeTemplates>
</RealTime>
</ClientSideStateUpdate>
</>
Expand Down
52 changes: 52 additions & 0 deletions src/app/_fetchers/OneTemplateDataFetcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client'

import { setActiveTemplate } from '@/redux/features/templateSlice'
import store from '@/redux/store'
import { ITemplate, PropsWithToken } from '@/types/interfaces'
import { fetcher } from '@/utils/fetcher'
import { extractImgSrcs, replaceImgSrcs } from '@/utils/signedUrlReplacer'
import { useEffect } from 'react'
import useSWR from 'swr'

interface OneTemplateDataFetcherProps extends PropsWithToken {
template_id: string
initialTemplate: ITemplate
}

export const OneTemplateDataFetcher = ({
token,
template_id,
initialTemplate,
}: OneTemplateDataFetcherProps & PropsWithToken) => {
const buildQueryString = (token: string) => {
const queryParams = new URLSearchParams({ token })

return queryParams.toString()
}

const queryString = token ? buildQueryString(token) : null

const { data } = useSWR(queryString ? `/api/tasks/templates/${template_id}?${queryString}` : null, fetcher, {
refreshInterval: 0,
revalidateOnFocus: false,
})

useEffect(() => {
if (data?.data) {
const newTemplate = structuredClone(data.data)
if (initialTemplate?.body && newTemplate.body === undefined) {
newTemplate.body = initialTemplate?.body
}
if (initialTemplate && initialTemplate.body && newTemplate.body) {
const oldImgSrcs = extractImgSrcs(initialTemplate.body)
const newImgSrcs = extractImgSrcs(newTemplate.body)
if (oldImgSrcs.length > 0 && newImgSrcs.length > 0) {
newTemplate.body = replaceImgSrcs(newTemplate.body, newImgSrcs, oldImgSrcs)
}
}
store.dispatch(setActiveTemplate(newTemplate))
}
}, [data])

Check warning on line 49 in src/app/_fetchers/OneTemplateDataFetcher.tsx

View workflow job for this annotation

GitHub Actions / Run linters

React Hook useEffect has a missing dependency: 'initialTemplate'. Either include it or remove the dependency array

Check warning on line 49 in src/app/_fetchers/OneTemplateDataFetcher.tsx

View workflow job for this annotation

GitHub Actions / Run linters

React Hook useEffect has a missing dependency: 'initialTemplate'. Either include it or remove the dependency array

return null
}
6 changes: 4 additions & 2 deletions src/app/api/tasks/tasks.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ export const getTasks = async (req: NextRequest) => {

export const createTask = async (req: NextRequest) => {
const user = await authenticate(req)

const data = CreateTaskRequestSchema.parse(await req.json())
const disableSubtaskTemplates = req.nextUrl.searchParams.get('disableSubtaskTemplates') === 'true'
const tasksService = new TasksService(user)
const newTask = await tasksService.createTask(data)
const newTask = await tasksService.createTask(data, {
disableSubtaskTemplates,
})
return NextResponse.json(newTask, { status: httpStatus.CREATED })
}

Expand Down
80 changes: 75 additions & 5 deletions src/app/api/tasks/tasks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import { deleteTaskNotifications, sendTaskCreateNotifications, sendTaskUpdateNot
import { sendClientUpdateTaskNotifications } from '@/jobs/notifications/send-client-task-update-notifications'
import { ClientResponse, CompanyResponse, InternalUsers, Uuid } from '@/types/common'
import { TaskWithWorkflowState } from '@/types/db'
import { AncestorTaskResponse, CreateTaskRequest, UpdateTaskRequest, Viewers, ViewersSchema } from '@/types/dto/tasks.dto'
import {
AncestorTaskResponse,
CreateTaskRequest,
CreateTaskRequestSchema,
UpdateTaskRequest,
Viewers,
ViewersSchema,
} from '@/types/dto/tasks.dto'
import { DISPATCHABLE_EVENT } from '@/types/webhook'
import { UserIdsType } from '@/utils/assignee'
import { CopilotAPI } from '@/utils/CopilotAPI'
import { isPastDateString } from '@/utils/dateHelper'
import { buildLtree, buildLtreeNodeString, getIdsFromLtreePath } from '@/utils/ltree'
import { getFilePathFromUrl, replaceImageSrc } from '@/utils/signedUrlReplacer'
Expand All @@ -28,10 +34,10 @@ import {
queueBodyUpdatedWebhook,
} from '@api/tasks/tasks.helpers'
import { TasksActivityLogger } from '@api/tasks/tasks.logger'
import { AssigneeType, Prisma, PrismaClient, Source, StateType, Task, WorkflowState } from '@prisma/client'
import dayjs from 'dayjs'
import { AssigneeType, Prisma, PrismaClient, Source, StateType, Task, TaskTemplate, WorkflowState } from '@prisma/client'
import httpStatus from 'http-status'
import { z } from 'zod'
import { TemplatesService } from './templates/templates.service'

export class TasksService extends BaseService {
/**
Expand Down Expand Up @@ -157,7 +163,10 @@ export class TasksService extends BaseService {
return filteredTasks
}

async createTask(data: CreateTaskRequest, opts?: { isPublicApi: boolean }) {
async createTask(
data: CreateTaskRequest,
opts?: { isPublicApi?: boolean; disableSubtaskTemplates?: boolean; manualTimestamp?: Date },
) {
const policyGate = new PoliciesService(this.user)
policyGate.authorize(UserAction.Create, Resource.Tasks)
console.info('TasksService#createTask | Creating task with data:', data)
Expand Down Expand Up @@ -229,6 +238,7 @@ export class TasksService extends BaseService {
assigneeType,
viewers: viewers,
...validatedIds,
...(opts?.manualTimestamp && { createdAt: opts.manualTimestamp }),
...(await getTaskTimestamps('create', this.user, data, undefined, workflowStateStatus)),
},
include: { workflowState: true },
Expand Down Expand Up @@ -289,6 +299,25 @@ export class TasksService extends BaseService {
}),
])

if (data.templateId && !opts?.disableSubtaskTemplates) {
const templateService = new TemplatesService(this.user)
const template = await templateService.getOneTemplate(data.templateId)

if (!template) {
throw new APIError(httpStatus.NOT_FOUND, 'The requested template was not found')
}

if (template.subTaskTemplates.length) {
await Promise.all(
template.subTaskTemplates.map(async (sub, index) => {
const updatedSubTemplate = await templateService.getAppliedTemplateDescription(sub.id)
const manualTimeStamp = new Date(template.createdAt.getTime() + (template.subTaskTemplates.length - index) * 10) //maintain the order of subtasks in tasks with respect to subtasks in templates
await this.createSubtasksFromTemplate(updatedSubTemplate, newTask, manualTimeStamp)
}),
)
}
}

return newTask
}

Expand Down Expand Up @@ -1124,4 +1153,45 @@ export class TasksService extends BaseService {

return viewers
}

private async createSubtasksFromTemplate(data: TaskTemplate, parentTask: Task, manualTimestamp: Date) {
const { workspaceId, title, body, workflowStateId } = data
const previewMode = Boolean(this.user.clientId || this.user.companyId)
const { id: parentId, internalUserId, clientId, companyId, viewers } = parentTask

try {
const createTaskPayload = CreateTaskRequestSchema.parse({
title,
body,
workspaceId,
workflowStateId,
parentId,
templateId: undefined, //just to be safe from circular recursion
...(previewMode && {
internalUserId,
clientId,
companyId,
viewers,
}), //On CRM view, we set assignee and viewers for subtasks same as the parent task.
})

await this.createTask(createTaskPayload, { disableSubtaskTemplates: true, manualTimestamp: manualTimestamp })
} catch (e) {
const deleteTask = this.db.task.delete({ where: { id: parentId } })
const deleteActivityLogs = this.db.activityLog.deleteMany({ where: { taskId: parentId } })

await this.db.$transaction(async (tx) => {
this.setTransaction(tx as PrismaClient)
await deleteTask
await deleteActivityLogs
this.unsetTransaction()
})

console.error('TasksService#createTask | Rolling back task creation', e)
throw new APIError(
httpStatus.INTERNAL_SERVER_ERROR,
'Failed to create subtask from template, new task was not created.',
)
}
}
}
9 changes: 8 additions & 1 deletion src/app/api/tasks/templates/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { withErrorHandler } from '@api/core/utils/withErrorHandler'
import { deleteTaskTemplate, updateTaskTemplate } from '@api/tasks/templates/templates.controller'
import {
createSubTaskTemplate,
deleteTaskTemplate,
getOneTemplate,
updateTaskTemplate,
} from '@api/tasks/templates/templates.controller'

export const PATCH = withErrorHandler(updateTaskTemplate)
export const DELETE = withErrorHandler(deleteTaskTemplate)
export const POST = withErrorHandler(createSubTaskTemplate)
export const GET = withErrorHandler(getOneTemplate)
4 changes: 4 additions & 0 deletions src/app/api/tasks/templates/[id]/sub-templates/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { withErrorHandler } from '@/app/api/core/utils/withErrorHandler'
import { getSubtemplates } from '@/app/api/tasks/templates/templates.controller'

export const GET = withErrorHandler(getSubtemplates)
12 changes: 11 additions & 1 deletion src/app/api/tasks/templates/public/public.dto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { RFC3339DateSchema } from '@/types/common'
import { z } from 'zod'

export const TemplateResponsePublicSchema = z.object({
export interface TemplateResponsePublicType {
id: string
object: 'taskTemplate'
name: string
description: string | null
createdDate: string
subTaskTemplates: TemplateResponsePublicType[]
} //forward declaring the interface

export const TemplateResponsePublicSchema: z.ZodType<TemplateResponsePublicType> = z.object({
id: z.string().uuid(),
object: z.literal('taskTemplate'),
name: z.string(),
description: z.string().nullable(),
createdDate: RFC3339DateSchema,
subTaskTemplates: z.array(z.lazy(() => TemplateResponsePublicSchema)),
})

export type TemplateResponsePublic = z.infer<typeof TemplateResponsePublicSchema>
9 changes: 8 additions & 1 deletion src/app/api/tasks/templates/public/public.serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import { toRFC3339 } from '@/utils/dateHelper'
import { TaskTemplate } from '@prisma/client'
import { z } from 'zod'

type TaskTemplateWithSubtasks = TaskTemplate & {
subTaskTemplates?: TaskTemplateWithSubtasks[]
}
export class PublicTemplateSerializer {
static serialize(template: TaskTemplate | TaskTemplate[]): TemplateResponsePublic | TemplateResponsePublic[] {
static serialize(
template: TaskTemplateWithSubtasks | TaskTemplateWithSubtasks[],
): TemplateResponsePublic | TemplateResponsePublic[] {
if (Array.isArray(template)) {
return z.array(TemplateResponsePublicSchema).parse(
template.map((template) => ({
Expand All @@ -13,6 +18,7 @@ export class PublicTemplateSerializer {
name: template.title,
description: template.body,
createdDate: toRFC3339(template.createdAt),
subTaskTemplates: template.subTaskTemplates?.map((sub) => this.serialize(sub)) ?? [],
})),
)
}
Expand All @@ -23,6 +29,7 @@ export class PublicTemplateSerializer {
name: template.title,
description: template.body,
createdDate: toRFC3339(template.createdAt),
subTaskTemplates: template.subTaskTemplates?.map((sub) => this.serialize(sub)) ?? [],
})
}
}
24 changes: 24 additions & 0 deletions src/app/api/tasks/templates/templates.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ export const updateTaskTemplate = async (req: NextRequest, { params: { id } }: I
return NextResponse.json({ data })
}

export const createSubTaskTemplate = async (req: NextRequest, { params: { id } }: IdParams) => {
const user = await authenticate(req)
const payload = CreateTemplateRequestSchema.parse(await req.json())
const templatesService = new TemplatesService(user)
const data = await templatesService.createSubTaskTemplate(id, payload)
return NextResponse.json({ data })
}

export const deleteTaskTemplate = async (req: NextRequest, { params: { id } }: IdParams) => {
const user = await authenticate(req)

Expand All @@ -50,3 +58,19 @@ export const applyTemplate = async (req: NextRequest, { params: { id } }: IdPara

return NextResponse.json({ data })
}

export const getOneTemplate = async (req: NextRequest, { params: { id } }: IdParams) => {
const user = await authenticate(req)
const templatesService = new TemplatesService(user)
const data = await templatesService.getOneTemplate(id)

return NextResponse.json({ data })
}

export const getSubtemplates = async (req: NextRequest, { params: { id } }: IdParams) => {
const user = await authenticate(req)
const templatesService = new TemplatesService(user)
const data = await templatesService.getSubtemplates(id)

return NextResponse.json({ data })
}
Loading
Loading