Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
16 changes: 10 additions & 6 deletions src/app/api/tasks/tasks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ export class TasksService extends BaseService {
return filteredTasks
}

async createTask(data: CreateTaskRequest, opts?: { isPublicApi?: boolean; disableSubtaskTemplates?: 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 @@ -235,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 @@ -305,9 +309,10 @@ export class TasksService extends BaseService {

if (template.subTaskTemplates.length) {
await Promise.all(
template.subTaskTemplates.map(async (sub) => {
template.subTaskTemplates.map(async (sub, index) => {
const updatedSubTemplate = await templateService.getAppliedTemplateDescription(sub.id)
await this.createSubtasksFromTemplate(updatedSubTemplate, newTask.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.id, manualTimeStamp)
}),
)
}
Expand Down Expand Up @@ -1149,7 +1154,7 @@ export class TasksService extends BaseService {
return viewers
}

private async createSubtasksFromTemplate(data: TaskTemplate, parentId: string) {
private async createSubtasksFromTemplate(data: TaskTemplate, parentId: string, manualTimestamp: Date) {
const { workspaceId, title, body, workflowStateId } = data
try {
const createTaskPayload = CreateTaskRequestSchema.parse({
Expand All @@ -1158,10 +1163,9 @@ export class TasksService extends BaseService {
workspaceId,
workflowStateId,
parentId,

templateId: undefined, //just to be safe from circular recursion
})
await this.createTask(createTaskPayload, { disableSubtaskTemplates: true })
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 } })
Expand Down
246 changes: 127 additions & 119 deletions src/app/detail/[task_id]/[user_type]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AssigneeCacheGetter } from '@/app/_cache/AssigneeCacheGetter'
import { AssigneeFetcher } from '@/app/_fetchers/AssigneeFetcher'
import { fetchWithErrorHandler } from '@/app/_fetchers/fetchWithErrorHandler'
import { OneTaskDataFetcher } from '@/app/_fetchers/OneTaskDataFetcher'
import { TemplatesFetcher } from '@/app/_fetchers/TemplatesFetcher'
import { WorkflowStateFetcher } from '@/app/_fetchers/WorkflowStateFetcher'
import { UserRole } from '@/app/api/core/types/user'
import {
Expand Down Expand Up @@ -32,6 +33,7 @@ import { apiUrl } from '@/config'
import { AppMargin, SizeofAppMargin } from '@/hoc/AppMargin'
import CustomScrollBar from '@/hoc/CustomScrollBar'
import { RealTime } from '@/hoc/RealTime'
import { RealTimeTemplates } from '@/hoc/RealtimeTemplates'
import { WorkspaceResponse } from '@/types/common'
import { AncestorTaskResponse, SubTaskStatusResponse, TaskResponse } from '@/types/dto/tasks.dto'
import { UserType } from '@/types/interfaces'
Expand All @@ -41,6 +43,7 @@ import EscapeHandler from '@/utils/escapeHandler'
import { getPreviewMode } from '@/utils/previewMode'
import { checkIfTaskViewer } from '@/utils/taskViewer'
import { Box, Stack } from '@mui/material'
import { Suspense } from 'react'
import { z } from 'zod'

async function getOneTask(token: string, taskId: string): Promise<TaskResponse | null> {
Expand Down Expand Up @@ -134,132 +137,137 @@ export default async function TaskDetailPage({
workspace={workspace}
>
{token && <OneTaskDataFetcher token={token} task_id={task_id} initialTask={task} />}
<Suspense fallback={null}>
<TemplatesFetcher token={token} />
</Suspense>
<RealTime tokenPayload={tokenPayload}>
<EscapeHandler />
<ResponsiveStack fromNotificationCenter={fromNotificationCenter}>
<Box sx={{ width: '100%', display: 'flex', flex: 1, flexDirection: 'column', overflow: 'hidden' }}>
{isPreviewMode ? (
<StyledBox>
<AppMargin size={SizeofAppMargin.HEADER} py="17.5px">
<Stack direction="row" justifyContent="space-between">
<HeaderBreadcrumbs token={token} items={breadcrumbItems} userType={params.user_type} />
<Stack direction="row" alignItems="center" columnGap="8px">
<MenuBoxContainer role={tokenPayload.internalUserId ? UserRole.IU : UserRole.Client} />
<RealTimeTemplates tokenPayload={tokenPayload} token={token}>
<EscapeHandler />
<ResponsiveStack fromNotificationCenter={fromNotificationCenter}>
<Box sx={{ width: '100%', display: 'flex', flex: 1, flexDirection: 'column', overflow: 'hidden' }}>
{isPreviewMode ? (
<StyledBox>
<AppMargin size={SizeofAppMargin.HEADER} py="17.5px">
<Stack direction="row" justifyContent="space-between">
<HeaderBreadcrumbs token={token} items={breadcrumbItems} userType={params.user_type} />
<Stack direction="row" alignItems="center" columnGap="8px">
<ArchiveWrapper taskId={task_id} userType={user_type} />
<MenuBoxContainer role={tokenPayload.internalUserId ? UserRole.IU : UserRole.Client} />
<Stack direction="row" alignItems="center" columnGap="8px">
<ArchiveWrapper taskId={task_id} userType={user_type} />
</Stack>
</Stack>
</Stack>
</Stack>
</AppMargin>
</StyledBox>
) : (
<>
<HeaderBreadcrumbs
token={token}
items={breadcrumbItems}
userType={params.user_type}
portalUrl={workspace.portalUrl}
/>
<ArchiveWrapper taskId={task_id} userType={user_type} />
</>
)}

<CustomScrollBar>
<TaskDetailsContainer
sx={{
padding: { xs: '20px 16px ', sm: '30px 20px' },
}}
>
<StyledTiptapDescriptionWrapper>
<LastArchivedField />
<TaskEditor
// attachment={attachments}
task_id={task_id}
task={task}
isEditable={params.user_type === UserType.INTERNAL_USER || !!getPreviewMode(tokenPayload)}
updateTaskDetail={async (detail) => {
'use server'
await updateTaskDetail({ token, taskId: task_id, payload: { body: detail } })
}}
updateTaskTitle={async (title) => {
'use server'
title.trim() != '' && (await updateTaskDetail({ token, taskId: task_id, payload: { title } }))
}}
deleteTask={async () => {
'use server'
await deleteTask(token, task_id)
}}
postAttachment={async (postAttachmentPayload) => {
'use server'
await postAttachment(token, postAttachmentPayload)
}}
deleteAttachment={async (id: string) => {
'use server'
await deleteAttachment(token, id)
}}
userType={params.user_type}
token={token}
/>
</StyledTiptapDescriptionWrapper>
{subTaskStatus.canCreateSubtask && (
<Subtasks
task_id={task_id}
</AppMargin>
</StyledBox>
) : (
<>
<HeaderBreadcrumbs
token={token}
userType={tokenPayload.internalUserId ? UserRole.IU : UserRole.Client}
canCreateSubtasks={params.user_type === UserType.INTERNAL_USER || !!getPreviewMode(tokenPayload)}
items={breadcrumbItems}
userType={params.user_type}
portalUrl={workspace.portalUrl}
/>
)}
<ArchiveWrapper taskId={task_id} userType={user_type} />
</>
)}

<CustomScrollBar>
<TaskDetailsContainer
sx={{
padding: { xs: '20px 16px ', sm: '30px 20px' },
}}
>
<StyledTiptapDescriptionWrapper>
<LastArchivedField />
<TaskEditor
// attachment={attachments}
task_id={task_id}
task={task}
isEditable={params.user_type === UserType.INTERNAL_USER || !!getPreviewMode(tokenPayload)}
updateTaskDetail={async (detail) => {
'use server'
await updateTaskDetail({ token, taskId: task_id, payload: { body: detail } })
}}
updateTaskTitle={async (title) => {
'use server'
title.trim() != '' && (await updateTaskDetail({ token, taskId: task_id, payload: { title } }))
}}
deleteTask={async () => {
'use server'
await deleteTask(token, task_id)
}}
postAttachment={async (postAttachmentPayload) => {
'use server'
await postAttachment(token, postAttachmentPayload)
}}
deleteAttachment={async (id: string) => {
'use server'
await deleteAttachment(token, id)
}}
userType={params.user_type}
token={token}
/>
</StyledTiptapDescriptionWrapper>
{subTaskStatus.canCreateSubtask && (
<Subtasks
task_id={task_id}
token={token}
userType={tokenPayload.internalUserId ? UserRole.IU : UserRole.Client}
canCreateSubtasks={params.user_type === UserType.INTERNAL_USER || !!getPreviewMode(tokenPayload)}
/>
)}

<ActivityWrapper task_id={task_id} token={token} tokenPayload={tokenPayload} />
</TaskDetailsContainer>
</CustomScrollBar>
</Box>
<Box
{...(fromNotificationCenter
? {
sx: {
display: 'flex',
overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center',
},
}
: {})}
>
<AssigneeCacheGetter lookupKey={getAssigneeCacheLookupKey(user_type, tokenPayload, isPreviewMode)} />
<AssigneeFetcher
token={token}
userType={params.user_type}
isPreview={!!getPreviewMode(tokenPayload)}
task={task}
tokenPayload={tokenPayload}
/>
<WorkflowStateFetcher token={token} task={task} />
<Sidebar
task_id={task_id}
selectedAssigneeId={task?.assigneeId}
userType={user_type}
portalUrl={workspace.portalUrl}
selectedWorkflowState={task?.workflowState}
updateWorkflowState={async (workflowState) => {
'use server'
params.user_type === UserType.CLIENT_USER && !getPreviewMode(tokenPayload)
? await clientUpdateTask(token, task_id, workflowState.id)
: await updateWorkflowStateIdOfTask(token, task_id, workflowState?.id)
}}
updateAssignee={async ({ internalUserId, clientId, companyId, viewers }: UserIdsWithViewersType) => {
'use server'
await updateAssignee(token, task_id, internalUserId, clientId, companyId, viewers)
}}
updateTask={async (payload) => {
'use server'
await updateTaskDetail({ token, taskId: task_id, payload })
}}
disabled={params.user_type === UserType.CLIENT_USER}
workflowDisabled={isViewer}
/>
</Box>
</ResponsiveStack>
<ActivityWrapper task_id={task_id} token={token} tokenPayload={tokenPayload} />
</TaskDetailsContainer>
</CustomScrollBar>
</Box>
<Box
{...(fromNotificationCenter
? {
sx: {
display: 'flex',
overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center',
},
}
: {})}
>
<AssigneeCacheGetter lookupKey={getAssigneeCacheLookupKey(user_type, tokenPayload, isPreviewMode)} />
<AssigneeFetcher
token={token}
userType={params.user_type}
isPreview={!!getPreviewMode(tokenPayload)}
task={task}
tokenPayload={tokenPayload}
/>
<WorkflowStateFetcher token={token} task={task} />
<Sidebar
task_id={task_id}
selectedAssigneeId={task?.assigneeId}
userType={user_type}
portalUrl={workspace.portalUrl}
selectedWorkflowState={task?.workflowState}
updateWorkflowState={async (workflowState) => {
'use server'
params.user_type === UserType.CLIENT_USER && !getPreviewMode(tokenPayload)
? await clientUpdateTask(token, task_id, workflowState.id)
: await updateWorkflowStateIdOfTask(token, task_id, workflowState?.id)
}}
updateAssignee={async ({ internalUserId, clientId, companyId, viewers }: UserIdsWithViewersType) => {
'use server'
await updateAssignee(token, task_id, internalUserId, clientId, companyId, viewers)
}}
updateTask={async (payload) => {
'use server'
await updateTaskDetail({ token, taskId: task_id, payload })
}}
disabled={params.user_type === UserType.CLIENT_USER}
workflowDisabled={isViewer}
/>
</Box>
</ResponsiveStack>
</RealTimeTemplates>
</RealTime>
</DetailStateUpdate>
)
Expand Down
4 changes: 3 additions & 1 deletion src/app/detail/ui/NewTaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useHandleSelectorComponent } from '@/hooks/useHandleSelectorComponent'
import { PersonIconSmall, TempalteIconMd } from '@/icons'
import { selectAuthDetails } from '@/redux/features/authDetailsSlice'
import { selectTaskBoard } from '@/redux/features/taskBoardSlice'
import { selectTaskDetails } from '@/redux/features/taskDetailsSlice'
import { selectCreateTemplate } from '@/redux/features/templateSlice'
import { DateString } from '@/types/date'
import { CreateTaskRequest, Viewers } from '@/types/dto/tasks.dto'
Expand Down Expand Up @@ -53,6 +54,7 @@ export const NewTaskCard = ({
}) => {
const { workflowStates, assignee, token, activeTask, previewMode, previewClientCompany } = useSelector(selectTaskBoard)
const { templates } = useSelector(selectCreateTemplate)
const { fromNotificationCenter } = useSelector(selectTaskDetails)

const [isEditorReadonly, setIsEditorReadonly] = useState(false)

Expand Down Expand Up @@ -289,7 +291,7 @@ export const NewTaskCard = ({
placeholder="Search..."
value={templateValue}
selectorType={SelectorType.TEMPLATE_SELECTOR}
endOption={<ManageTemplatesEndOption hasTemplates={!!templates?.length} />}
endOption={!fromNotificationCenter && <ManageTemplatesEndOption hasTemplates={!!templates?.length} />}
endOptionHref={`/manage-templates?token=${token}`}
listAutoHeightMax="147px"
variant="normal"
Expand Down
Loading
Loading