Skip to content
Open
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"nodemon": "^3.1.9",
"open": "^10.1.0",
"prettier": "^3.1.1",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^3.3.0",
"text-table": "^0.2.0",
"tsx": "^4.16.5",
Expand Down
35 changes: 23 additions & 12 deletions src/app/api/tasks/tasks.logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,33 @@ export class TasksActivityLogger extends BaseService {
}

if (Array.isArray(this.task.associations) && Array.isArray(prevTask.associations)) {
const currentViewers = AssociationsSchema.parse(this.task.associations) || []
const prevViewers = AssociationsSchema.parse(prevTask.associations) || []
const currentAssociations = AssociationsSchema.parse(this.task.associations) || []
const prevAssociations = AssociationsSchema.parse(prevTask.associations) || []
const currentShared = this.task.isShared
const prevShared = prevTask.isShared

// handles the case to show activity log when a task is shared with association
if (
(!!currentViewers.length || !!prevViewers.length) &&
(currentViewers[0]?.clientId !== prevViewers[0]?.clientId ||
currentViewers[0]?.companyId !== prevViewers[0]?.companyId)
(!!currentAssociations.length || !!prevAssociations.length) &&
(currentAssociations[0]?.clientId !== prevAssociations[0]?.clientId ||
currentAssociations[0]?.companyId !== prevAssociations[0]?.companyId ||
currentShared !== prevShared) &&
(currentShared || prevShared)
) {
const currentViewerId = currentViewers[0]?.clientId || currentViewers[0]?.companyId || null
const previousViewerId = prevViewers[0]?.clientId || prevViewers[0]?.companyId || null
if (currentViewerId) {
if (previousViewerId) await this.logTaskViewerRemoved(previousViewerId) // if previous viewer exists, log removed event
await this.logTaskViewerUpdated(previousViewerId, currentViewerId)
const currentAssociationId = currentAssociations[0]?.clientId || currentAssociations[0]?.companyId || null
const prevAssociationId = prevAssociations[0]?.clientId || prevAssociations[0]?.companyId || null

if (currentAssociationId) {
if (prevAssociationId && currentAssociationId !== prevAssociationId && prevShared)
await this.logTaskViewerRemoved(prevAssociationId) // if previous viewer exists, log removed event
if (currentShared) {
await this.logTaskViewerUpdated(prevAssociationId, currentAssociationId)
} else {
await this.logTaskViewerRemoved(currentAssociationId)
}
setUpdate()
} else {
await this.logTaskViewerRemoved(previousViewerId)
} else if (prevAssociationId && prevShared) {
await this.logTaskViewerRemoved(prevAssociationId)
setUpdate()
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/app/api/tasks/tasks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,13 @@ export class TasksService extends TasksSharedService {
}
}

private validateTaskShare(prevTask: Task, isTaskShared?: boolean) {
if (prevTask.associations.length) {
return !!isTaskShared
}
throw new APIError(httpStatus.BAD_REQUEST, 'Cannot share task when it has no association')
}

async updateOneTask(id: string, data: UpdateTaskRequest) {
const policyGate = new PoliciesService(this.user)
policyGate.authorize(UserAction.Update, Resource.Tasks)
Expand Down Expand Up @@ -405,6 +412,7 @@ export class TasksService extends TasksSharedService {
completedBy,
completedByUserType,
associations,
isShared: data.isShared !== undefined ? this.validateTaskShare(prevTask, data.isShared) : false,
...userAssignmentFields,
...(await getTaskTimestamps('update', this.user, data, prevTask)),
},
Expand Down
2 changes: 2 additions & 0 deletions src/app/detail/[task_id]/[user_type]/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const updateAssignee = async (
clientId: string | null,
companyId: string | null,
associations?: Associations,
isShared?: boolean,
) => {
await fetch(`${apiUrl}/api/tasks/${task_id}?token=${token}`, {
method: 'PATCH',
Expand All @@ -57,6 +58,7 @@ export const updateAssignee = async (
clientId,
companyId,
...(associations && { associations: !internalUserId ? [] : associations }), // if assignee is not internal user, remove associations. Only include associations if viewer are changed. Not including viewer means not chaning the current state of associations in DB.
...(isShared && { isShared }),
}),
})
}
Expand Down
12 changes: 9 additions & 3 deletions src/app/detail/[task_id]/[user_type]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { RealTimeTemplates } from '@/hoc/RealtimeTemplates'
import { WorkspaceResponse } from '@/types/common'
import { AncestorTaskResponse, SubTaskStatusResponse, TaskResponse } from '@/types/dto/tasks.dto'
import { UserType } from '@/types/interfaces'
import { getAssigneeCacheLookupKey, UserIdsWithViewersType } from '@/utils/assignee'
import { getAssigneeCacheLookupKey, UserIdsWithAssociationSharedType } from '@/utils/assignee'
import { CopilotAPI } from '@/utils/CopilotAPI'
import EscapeHandler from '@/utils/escapeHandler'
import { getPreviewMode } from '@/utils/previewMode'
Expand Down Expand Up @@ -257,9 +257,15 @@ export default async function TaskDetailPage(props: {
? await clientUpdateTask(token, task_id, workflowState.id)
: await updateWorkflowStateIdOfTask(token, task_id, workflowState?.id)
}}
updateAssignee={async ({ internalUserId, clientId, companyId, viewers }: UserIdsWithViewersType) => {
updateAssignee={async ({
internalUserId,
clientId,
companyId,
associations,
isShared,
}: UserIdsWithAssociationSharedType) => {
'use server'
await updateAssignee(token, task_id, internalUserId, clientId, companyId, viewers)
await updateAssignee(token, task_id, internalUserId, clientId, companyId, associations, isShared)
}}
updateTask={async (payload) => {
'use server'
Expand Down
6 changes: 2 additions & 4 deletions src/app/detail/ui/ActivityLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@
return []
}
},
[assignee, workflowStates, getAssignedToName],

Check warning on line 79 in src/app/detail/ui/ActivityLog.tsx

View workflow job for this annotation

GitHub Actions / Run linters

React Hook useCallback has missing dependencies: 'getViewerName' and 'log.details'. Either include them or remove the dependency array
)

const logEntities = useMemo(() => {
return getLogEntities(log.type)
}, [log.type, assignee, workflowStates])

Check warning on line 84 in src/app/detail/ui/ActivityLog.tsx

View workflow job for this annotation

GitHub Actions / Run linters

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

const activityDescription: { [key in ActivityType]: (...args: string[]) => React.ReactNode } = {
[ActivityType.TASK_CREATED]: () => (
Expand Down Expand Up @@ -146,17 +146,15 @@
[ActivityType.COMMENT_ADDED]: () => null,
[ActivityType.VIEWER_ADDED]: (_from: string, to: string) => (
<>
<StyledTypography> added </StyledTypography>
<StyledTypography> shared the task with </StyledTypography>
<BoldTypography>{to}</BoldTypography>
<StyledTypography> as a viewer </StyledTypography>
<DotSeparator />
</>
),
[ActivityType.VIEWER_REMOVED]: (from: string) => (
<>
<StyledTypography> removed </StyledTypography>
<StyledTypography> stopped sharing the task with </StyledTypography>
<BoldTypography>{from}</BoldTypography>
<StyledTypography> as a viewer </StyledTypography>
<DotSeparator />
</>
),
Expand Down
95 changes: 62 additions & 33 deletions src/app/detail/ui/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ import {
getUserIds,
isEmptyAssignee,
UserIdsType,
UserIdsWithViewersType,
UserIdsWithAssociationSharedType,
} from '@/utils/assignee'
import { createDateFromFormattedDateString, formatDate } from '@/utils/dateHelper'
import { NoAssignee } from '@/utils/noAssignee'
import { Box, Divider, Skeleton, Stack, styled, SxProps, Typography } from '@mui/material'
import {
getSelectedUserIds,
getSelectedViewerIds,
Expand All @@ -42,10 +43,10 @@ import {
shouldConfirmBeforeReassignment,
shouldConfirmViewershipBeforeReassignment,
} from '@/utils/shouldConfirmBeforeReassign'
import { Box, Skeleton, Stack, styled, Typography } from '@mui/material'
import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { z } from 'zod'
import { CopilotToggle } from '@/components/inputs/CopilotToggle'

type StyledTypographyProps = {
display?: string
Expand Down Expand Up @@ -73,7 +74,7 @@ export const Sidebar = ({
selectedWorkflowState: WorkflowStateResponse
selectedAssigneeId: string | undefined
updateWorkflowState: (workflowState: WorkflowStateResponse) => void
updateAssignee: (userIds: UserIdsWithViewersType) => void
updateAssignee: (userIds: UserIdsWithAssociationSharedType) => void
updateTask: (payload: UpdateTaskRequest) => void
disabled: boolean
workflowDisabled?: boolean
Expand Down Expand Up @@ -101,7 +102,12 @@ export const Sidebar = ({
const [assigneeValue, setAssigneeValue] = useState<IAssigneeCombined | undefined>()
const [selectedAssignee, setSelectedAssignee] = useState<UserIdsType | undefined>(undefined)

const [taskViewerValue, setTaskViewerValue] = useState<IAssigneeCombined | null>(null)
const [taskAssociationValue, setTaskAssociationValue] = useState<IAssigneeCombined | null>(null)
const [isTaskShared, setIsTaskShared] = useState(false)

const baseAssociationCondition = assigneeValue && assigneeValue.type === FilterByOptions.IUS
const showShareToggle = baseAssociationCondition && taskAssociationValue
const showAssociation = !assigneeValue || baseAssociationCondition

const { renderingItem: _statusValue, updateRenderingItem: updateStatusValue } = useHandleSelectorComponent({
// item: selectedWorkflowState,
Expand Down Expand Up @@ -129,18 +135,21 @@ export const Sidebar = ({
if (activeTask && assignee.length > 0) {
const currentAssignee = getSelectorAssigneeFromTask(assignee, activeTask)
setAssigneeValue(currentAssignee)
setTaskViewerValue(getSelectorViewerFromTask(assignee, activeTask) || null)
const currentAssociations = getSelectorViewerFromTask(assignee, activeTask) || null
setTaskAssociationValue(currentAssociations)
setIsTaskShared(!!activeTask.isShared)
}
}, [assignee, activeTask])

const windowWidth = useWindowWidth()
const isMobile = windowWidth < 800 && windowWidth !== 0

const checkViewersCompatibility = (userIds: UserIdsType): UserIdsWithViewersType => {
const checkViewersCompatibility = (userIds: UserIdsType): UserIdsWithAssociationSharedType => {
// remove task viewers if assignee is cleared or changed to client or company
if (!userIds.internalUserId) {
setTaskViewerValue(null)
return { ...userIds, viewers: [] } // remove viewers if assignee is cleared or changed to client or company
setTaskAssociationValue(null)
setIsTaskShared(false)
return { ...userIds, associations: [], isShared: false } // remove viewers if assignee is cleared or changed to client or company
}
return userIds // no viewers change. keep viewers as is.
}
Expand Down Expand Up @@ -176,7 +185,7 @@ export const Sidebar = ({
const previousAssignee = assignee.find((assignee) => assignee.id == getAssigneeId(getUserIds(activeTask)))
const nextAssignee = getSelectorAssignee(assignee, inputValue)
const shouldShowConfirmModal = shouldConfirmBeforeReassignment(previousAssignee, nextAssignee)
const shouldShowConfirmViewershipModal = shouldConfirmViewershipBeforeReassignment(taskViewerValue, nextAssignee)
const shouldShowConfirmViewershipModal = shouldConfirmViewershipBeforeReassignment(taskAssociationValue, nextAssignee)
if (shouldShowConfirmModal) {
setSelectedAssignee(newUserIds)
store.dispatch(toggleShowConfirmAssignModal())
Expand All @@ -189,21 +198,35 @@ export const Sidebar = ({
}
}

const handleTaskViewerChange = (inputValue: InputValue[]) => {
if (assigneeValue && assigneeValue.type === FilterByOptions.IUS) {
const handleTaskAssociationChange = (inputValue: InputValue[]) => {
if (showAssociation) {
const newTaskViewerIds = getSelectedViewerIds(inputValue)
setTaskViewerValue(getSelectorAssignee(assignee, inputValue) || null)
setTaskAssociationValue(getSelectorAssignee(assignee, inputValue) || null)

newTaskViewerIds &&
updateAssignee({
internalUserId: assigneeValue.id,
internalUserId: assigneeValue ? assigneeValue.id : null,
clientId: null,
companyId: null,
viewers: newTaskViewerIds,
associations: newTaskViewerIds,
isShared: isTaskShared,
})
}
}

const handleTaskShared = () => {
if (showShareToggle) {
setIsTaskShared((prev) => !prev)

updateAssignee({
internalUserId: assigneeValue.id,
clientId: null,
companyId: null,
isShared: !isTaskShared,
})
}
}

if (!showSidebar || fromNotificationCenter) {
return (
<Stack
Expand Down Expand Up @@ -307,28 +330,28 @@ export const Sidebar = ({
>
<CopilotPopSelector
hideIusList
name="Set client visibility"
onChange={handleTaskViewerChange}
name="Set related to"
onChange={handleTaskAssociationChange}
disabled={(disabled && !previewMode) || fromNotificationCenter}
initialValue={taskViewerValue || undefined}
initialValue={taskAssociationValue || undefined}
buttonContent={
<SelectorButton
disabled={(disabled && !previewMode) || fromNotificationCenter}
height={'30px'}
startIcon={<CopilotAvatar size="xs" currentAssignee={taskViewerValue || undefined} />}
startIcon={<CopilotAvatar size="xs" currentAssignee={taskAssociationValue || undefined} />}
buttonContent={
<Typography
variant="md"
lineHeight="22px"
sx={{
color: (theme) => (taskViewerValue ? theme.color.gray[600] : theme.color.gray[400]),
color: (theme) => (taskAssociationValue ? theme.color.gray[600] : theme.color.gray[400]),
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
maxWidth: '135px',
}}
>
{getAssigneeName(taskViewerValue || undefined, 'Set client visibility')}
{getAssigneeName(taskAssociationValue || undefined, 'Set related to')}
</Typography>
}
/>
Expand Down Expand Up @@ -366,8 +389,8 @@ export const Sidebar = ({
</>
) : (
<>
<strong>{getAssigneeName(getAssigneeValueFromViewers(taskViewerValue, assignee))}</strong> will also lose
visibility to the task.
<strong>{getAssigneeName(getAssigneeValueFromViewers(taskAssociationValue, assignee))}</strong> will also
lose visibility to the task.
</>
)
}
Expand Down Expand Up @@ -517,10 +540,10 @@ export const Sidebar = ({
)}
</Stack>

{assigneeValue && assigneeValue.type === FilterByOptions.IUS && (
<Stack direction="row" m="8px 0px" alignItems="center" columnGap="8px">
{showAssociation && (
<Stack direction="row" m="8px 0px 16px" alignItems="center" columnGap="8px">
<StyledText variant="md" minWidth="100px" fontWeight={400} lineHeight={'22px'}>
Client visibility
Related to
</StyledText>
{assignee.length > 0 ? ( // show skeleton if assignee list is empty
<Box
Expand All @@ -535,30 +558,30 @@ export const Sidebar = ({
>
<CopilotPopSelector
hideIusList
name="Set client visibility"
onChange={handleTaskViewerChange}
name="Set related to"
onChange={handleTaskAssociationChange}
disabled={(disabled && !previewMode) || fromNotificationCenter} // allow visibility change in preview mode
initialValue={taskViewerValue || undefined}
initialValue={taskAssociationValue || undefined}
buttonContent={
<SelectorButton
disabled={(disabled && !previewMode) || fromNotificationCenter}
padding="0px"
startIcon={<CopilotAvatar size="xs" currentAssignee={taskViewerValue || undefined} />}
startIcon={<CopilotAvatar size="xs" currentAssignee={taskAssociationValue || undefined} />}
outlined={true}
buttonContent={
<Typography
variant="md"
lineHeight="22px"
sx={{
color: (theme) => (taskViewerValue ? theme.color.gray[600] : theme.color.gray[400]),
color: (theme) => (taskAssociationValue ? theme.color.gray[600] : theme.color.gray[400]),
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
maxWidth: '135px',
fontWeight: 400,
}}
>
{getAssigneeName(taskViewerValue || undefined, 'Set client visibility')}
{getAssigneeName(taskAssociationValue || undefined, 'Set related to')}
</Typography>
}
/>
Expand All @@ -570,6 +593,12 @@ export const Sidebar = ({
)}
</Stack>
)}
{showShareToggle && (
<>
<Divider sx={{ borderColor: (theme) => theme.color.borders.border, height: '1px' }} />
<CopilotToggle label="Share with client" onChange={handleTaskShared} checked={isTaskShared} className="pt-4" />
</>
)}
</AppMargin>
<StyledModal
open={showConfirmAssignModal || showConfirmViewershipModal}
Expand Down Expand Up @@ -601,8 +630,8 @@ export const Sidebar = ({
</>
) : (
<>
<strong>{getAssigneeName(getAssigneeValueFromViewers(taskViewerValue, assignee))}</strong> will also lose
visibility to the task.
<strong>{getAssigneeName(getAssigneeValueFromViewers(taskAssociationValue, assignee))}</strong> will also
lose visibility to the task.
</>
)
}
Expand Down
1 change: 0 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ a {

/* Custom toggle wrapper to override design system styles */
.copilot-toggle-wrapper {
padding: 8px 6px 8px;
& .cop-font-medium {
font-weight: 400;
color: var(--text-secondary);
Expand Down
1 change: 1 addition & 0 deletions src/app/ui/NewTaskForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ export const NewTaskForm = ({ handleCreate, handleClose }: NewTaskFormProps) =>
)
}
checked={store.getState().createTask.isShared}
className="p-1.5 py-2" // px-1.5 is not working
/>
</Stack>
)}
Expand Down
Loading
Loading