diff --git a/sentry.client.config.ts b/sentry.client.config.ts index 5260484fc..8e0fc39e3 100644 --- a/sentry.client.config.ts +++ b/sentry.client.config.ts @@ -2,55 +2,55 @@ // The config you add here will be used whenever a users loads a page in their browser. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ -import * as Sentry from "@sentry/nextjs"; +import * as Sentry from '@sentry/nextjs' -const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN; -const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV; -const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; +const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN +const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV +const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' if (dsn) { - Sentry.init({ - dsn, - - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: isProd ? 0.2 : 1, - profilesSampleRate: 0.1, - // NOTE: reducing sample only 10% of transactions in prod to get general trends instead of detailed and overfitted data - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, - - // You can remove this option if you're not planning to use the Sentry Session Replay feature: - // NOTE: Since session replay barely helps us anyways, getting rid of it to reduce some bundle size at least - // replaysOnErrorSampleRate: 1.0, - // replaysSessionSampleRate: 0, - integrations: [ - Sentry.browserTracingIntegration({ - beforeStartSpan: (e) => { - console.info('SentryBrowserTracingSpan', e.name) - return e - }, - }), - // Sentry.replayIntegration({ - // Additional Replay configuration goes in here, for example: - // maskAllText: true, - // blockAllMedia: true, - // }), - ], - - // ignoreErrors: [/fetch failed/i], - ignoreErrors: [/fetch failed/i], - - beforeSend(event) { - if (!isProd && event.type === undefined) { - return null - } - event.tags = { - ...event.tags, - // Adding additional app_env tag for cross-checking - app_env: isProd ? 'production' : vercelEnv || 'development', - } - return event - }, - }) + Sentry.init({ + dsn, + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: isProd ? 0.2 : 1, + profilesSampleRate: 0.1, + // NOTE: reducing sample only 10% of transactions in prod to get general trends instead of detailed and overfitted data + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + + // You can remove this option if you're not planning to use the Sentry Session Replay feature: + // NOTE: Since session replay barely helps us anyways, getting rid of it to reduce some bundle size at least + // replaysOnErrorSampleRate: 1.0, + // replaysSessionSampleRate: 0, + integrations: [ + Sentry.browserTracingIntegration({ + beforeStartSpan: (e) => { + console.info('SentryBrowserTracingSpan', e.name) + return e + }, + }), + // Sentry.replayIntegration({ + // Additional Replay configuration goes in here, for example: + // maskAllText: true, + // blockAllMedia: true, + // }), + ], + + // ignoreErrors: [/fetch failed/i], + ignoreErrors: [/fetch failed/i], + + beforeSend(event) { + if (!isProd && event.type === undefined) { + return null + } + event.tags = { + ...event.tags, + // Adding additional app_env tag for cross-checking + app_env: isProd ? 'production' : vercelEnv || 'development', + } + return event + }, + }) } diff --git a/sentry.server.config.ts b/sentry.server.config.ts index 8c6accff6..517962e0e 100644 --- a/sentry.server.config.ts +++ b/sentry.server.config.ts @@ -2,31 +2,31 @@ // The config you add here will be used whenever the server handles a request. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ -import * as Sentry from "@sentry/nextjs"; +import * as Sentry from '@sentry/nextjs' const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' if (dsn) { - Sentry.init({ - dsn, + Sentry.init({ + dsn, - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: 1, + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1, - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, - // Uncomment the line below to enable Spotlight (https://spotlightjs.com) - // spotlight: process.env.NODE_ENV === 'development', - ignoreErrors: [/fetch failed/i], + // Uncomment the line below to enable Spotlight (https://spotlightjs.com) + // spotlight: process.env.NODE_ENV === 'development', + ignoreErrors: [/fetch failed/i], - beforeSend(event) { - if (!isProd && event.type === undefined) { - return null - } - return event - }, - }) + beforeSend(event) { + if (!isProd && event.type === undefined) { + return null + } + return event + }, + }) } diff --git a/src/app/api/tasks/tasks.service.ts b/src/app/api/tasks/tasks.service.ts index aff570c7b..a5b08b51f 100644 --- a/src/app/api/tasks/tasks.service.ts +++ b/src/app/api/tasks/tasks.service.ts @@ -313,11 +313,22 @@ export class TasksService extends TasksSharedService { } } - private validateTaskShare(prevTask: Task, isTaskShared?: boolean) { - if (prevTask.associations.length) { - return !!isTaskShared + private validateTaskShare(prevTask: Task, data: UpdateTaskRequest): boolean | undefined { + const isTaskShared = data.isShared + + if (isTaskShared === undefined) return undefined + + if (isTaskShared) { + const isEligibleForShare = !!( + (prevTask.associations.length && prevTask.internalUserId) || + (data.associations?.length && data.internalUserId) + ) + if (!isEligibleForShare) { + throw new APIError(httpStatus.BAD_REQUEST, 'Cannot share task with assocations') + } + return true } - throw new APIError(httpStatus.BAD_REQUEST, 'Cannot share task when it has no association') + return false } async updateOneTask(id: string, data: UpdateTaskRequest) { @@ -355,7 +366,9 @@ export class TasksService extends TasksSharedService { }) let associations: Associations = AssociationsSchema.parse(prevTask.associations) - const viewersResetCondition = shouldUpdateUserIds ? !!clientId || !!companyId : !prevTask.internalUserId + + // check if current or previous assignee is a client or company + const viewersResetCondition = shouldUpdateUserIds ? !!clientId || !!companyId : prevTask.clientId || prevTask.companyId if (data.associations) { // only update of associations attribute is available. No associations in payload attribute means the data remains as it is in DB. if (viewersResetCondition || !data.associations?.length) { @@ -412,7 +425,7 @@ export class TasksService extends TasksSharedService { completedBy, completedByUserType, associations, - isShared: data.isShared !== undefined ? this.validateTaskShare(prevTask, data.isShared) : false, + isShared: this.validateTaskShare(prevTask, data), ...userAssignmentFields, ...(await getTaskTimestamps('update', this.user, data, prevTask)), }, diff --git a/src/app/detail/[task_id]/[user_type]/actions.ts b/src/app/detail/[task_id]/[user_type]/actions.ts index e3b814e20..f18e2daaf 100644 --- a/src/app/detail/[task_id]/[user_type]/actions.ts +++ b/src/app/detail/[task_id]/[user_type]/actions.ts @@ -57,8 +57,8 @@ export const updateAssignee = async ( internalUserId, 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 }), + ...(associations && { associations: clientId || companyId ? [] : 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 ?? undefined, }), }) } diff --git a/src/app/detail/ui/Sidebar.tsx b/src/app/detail/ui/Sidebar.tsx index 15b79db0c..6391db8db 100644 --- a/src/app/detail/ui/Sidebar.tsx +++ b/src/app/detail/ui/Sidebar.tsx @@ -23,7 +23,7 @@ import { FilterByOptions, IAssigneeCombined, InputValue, Sizes, UserType } from import { getAssigneeId, getAssigneeName, - getAssigneeValueFromViewers, + getAssigneeValueFromAssociations, getUserIds, isEmptyAssignee, UserIdsType, @@ -37,16 +37,17 @@ import { getSelectedViewerIds, getSelectorAssignee, getSelectorAssigneeFromTask, - getSelectorViewerFromTask, + getSelectorAssociationFromTask, } from '@/utils/selector' import { shouldConfirmBeforeReassignment, - shouldConfirmViewershipBeforeReassignment, + shouldConfirmTaskSharedBeforeReassignment, } from '@/utils/shouldConfirmBeforeReassign' import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { z } from 'zod' import { CopilotToggle } from '@/components/inputs/CopilotToggle' +import { SelectorFieldType } from '@/types/common' type StyledTypographyProps = { display?: string @@ -97,7 +98,8 @@ export const Sidebar = ({ (activeTask?.assigneeId === tokenPayload?.clientId || activeTask?.assigneeId === tokenPayload?.companyId) const [dueDate, setDueDate] = useState() - const [showConfirmViewershipModal, setShowConfirmViewershipModal] = useState(false) //this is used only in sidebar. + const [showAssociationConfirmationModal, setAssociationConfirmationModal] = useState(false) //this is used only in sidebar. + const [selectorFieldType, setSelectorFieldType] = useState(null) const [assigneeValue, setAssigneeValue] = useState() const [selectedAssignee, setSelectedAssignee] = useState(undefined) @@ -135,7 +137,7 @@ export const Sidebar = ({ if (activeTask && assignee.length > 0) { const currentAssignee = getSelectorAssigneeFromTask(assignee, activeTask) setAssigneeValue(currentAssignee) - const currentAssociations = getSelectorViewerFromTask(assignee, activeTask) || null + const currentAssociations = getSelectorAssociationFromTask(assignee, activeTask) || null setTaskAssociationValue(currentAssociations) setIsTaskShared(!!activeTask.isShared) } @@ -144,20 +146,45 @@ export const Sidebar = ({ const windowWidth = useWindowWidth() const isMobile = windowWidth < 800 && windowWidth !== 0 - const checkViewersCompatibility = (userIds: UserIdsType): UserIdsWithAssociationSharedType => { - // remove task viewers if assignee is cleared or changed to client or company - if (!userIds.internalUserId) { - setTaskAssociationValue(null) + const checkForAssociationAndShared = (userIds: UserIdsType): UserIdsWithAssociationSharedType => { + const { internalUserId, clientId, companyId } = userIds + + if (internalUserId) return userIds + + const noAssignee = !internalUserId && !clientId && !companyId + const temp: Partial = {} + + if (isTaskShared) { + temp.isShared = false 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. + + if (!noAssignee) { + temp.associations = [] // remove association only if assignee is non empty and not IU + setTaskAssociationValue(null) + } + return { ...userIds, ...temp } // remove task shared if assignee is cleared or changed to client or company } const handleConfirmAssigneeChange = (userIds: UserIdsType) => { - updateAssignee(checkViewersCompatibility(userIds)) + updateAssignee(checkForAssociationAndShared(userIds)) setAssigneeValue(getAssigneeValue(userIds) as IAssigneeCombined) - showConfirmAssignModal ? store.dispatch(toggleShowConfirmAssignModal()) : setShowConfirmViewershipModal(false) + showConfirmAssignModal ? store.dispatch(toggleShowConfirmAssignModal()) : setAssociationConfirmationModal(false) + setSelectorFieldType(null) + } + + const handleConfirmAssociationChange = () => { + updateAssignee({ + internalUserId: assigneeValue?.id || null, + clientId: null, + companyId: null, + isShared: false, + associations: [], + }) + setIsTaskShared(false) + setTaskAssociationValue(null) + setSelectorFieldType(null) + setAssociationConfirmationModal(false) } useEffect(() => { @@ -180,53 +207,65 @@ export const Sidebar = ({ } if (!activeTask || !isHydrated) return + const handleAssigneeChange = (inputValue: InputValue[]) => { + setSelectorFieldType(SelectorFieldType.ASSIGNEE) const newUserIds = getSelectedUserIds(inputValue) const previousAssignee = assignee.find((assignee) => assignee.id == getAssigneeId(getUserIds(activeTask))) const nextAssignee = getSelectorAssignee(assignee, inputValue) const shouldShowConfirmModal = shouldConfirmBeforeReassignment(previousAssignee, nextAssignee) - const shouldShowConfirmViewershipModal = shouldConfirmViewershipBeforeReassignment(taskAssociationValue, nextAssignee) + const showAssociationConfirmModal = shouldConfirmTaskSharedBeforeReassignment( + taskAssociationValue, + isTaskShared, + nextAssignee, + ) if (shouldShowConfirmModal) { setSelectedAssignee(newUserIds) store.dispatch(toggleShowConfirmAssignModal()) - } else if (shouldShowConfirmViewershipModal) { + } else if (showAssociationConfirmModal) { setSelectedAssignee(newUserIds) - setShowConfirmViewershipModal(true) + setAssociationConfirmationModal(true) } else { setAssigneeValue(getAssigneeValue(newUserIds) as IAssigneeCombined) - updateAssignee(checkViewersCompatibility(newUserIds)) + updateAssignee(checkForAssociationAndShared(newUserIds)) + if (newUserIds.clientId || newUserIds.companyId) { + setTaskAssociationValue(null) + } + setSelectorFieldType(null) } } const handleTaskAssociationChange = (inputValue: InputValue[]) => { - if (showAssociation) { - const newTaskViewerIds = getSelectedViewerIds(inputValue) - setTaskAssociationValue(getSelectorAssignee(assignee, inputValue) || null) - - newTaskViewerIds && - updateAssignee({ - internalUserId: assigneeValue ? assigneeValue.id : null, - clientId: null, - companyId: null, - associations: newTaskViewerIds, - isShared: isTaskShared, - }) - } - } - - const handleTaskShared = () => { - if (showShareToggle) { - setIsTaskShared((prev) => !prev) + setSelectorFieldType(SelectorFieldType.ASSOCIATION) + const newTaskAssociationIds = getSelectedViewerIds(inputValue) + const showModal = shouldConfirmTaskSharedBeforeReassignment(taskAssociationValue, isTaskShared) + if (showModal) { + setAssociationConfirmationModal(true) + } else if (newTaskAssociationIds) { + setTaskAssociationValue(getSelectorAssignee(assignee, inputValue) || null) updateAssignee({ - internalUserId: assigneeValue.id, + internalUserId: assigneeValue ? assigneeValue.id : null, clientId: null, companyId: null, - isShared: !isTaskShared, + associations: newTaskAssociationIds, + isShared: newTaskAssociationIds.length ? isTaskShared : false, }) + setSelectorFieldType(null) } } + const handleTaskShared = () => { + setIsTaskShared((prev) => !prev) + + updateAssignee({ + internalUserId: assigneeValue?.id || null, + clientId: null, + companyId: null, + isShared: !isTaskShared, + }) + } + if (!showSidebar || fromNotificationCenter) { return ( )} - showConfirmAssignModal ? store.dispatch(toggleShowConfirmAssignModal()) : setShowConfirmViewershipModal(false) + showConfirmAssignModal ? store.dispatch(toggleShowConfirmAssignModal()) : setAssociationConfirmationModal(false) } aria-labelledby="confirm-reassignment-modal" aria-describedby="confirm-reassignment" @@ -370,14 +409,19 @@ export const Sidebar = ({ { setSelectedAssignee(undefined) - showConfirmAssignModal ? store.dispatch(toggleShowConfirmAssignModal()) : setShowConfirmViewershipModal(false) + setSelectorFieldType(null) + showConfirmAssignModal + ? store.dispatch(toggleShowConfirmAssignModal()) + : setAssociationConfirmationModal(false) }} handleConfirm={() => { - if (selectedAssignee) { + if (selectorFieldType === SelectorFieldType.ASSIGNEE && selectedAssignee) { handleConfirmAssigneeChange(selectedAssignee) + } else if (selectorFieldType === SelectorFieldType.ASSOCIATION) { + handleConfirmAssociationChange() } }} - buttonText={showConfirmViewershipModal ? 'Remove' : 'Reassign'} + buttonText={showAssociationConfirmationModal ? 'Remove' : 'Reassign'} description={ showConfirmAssignModal ? ( <> @@ -389,13 +433,17 @@ export const Sidebar = ({ ) : ( <> - {getAssigneeName(getAssigneeValueFromViewers(taskAssociationValue, assignee))} will also - lose visibility to the task. + {getAssigneeName(getAssigneeValueFromAssociations(taskAssociationValue, assignee))} will + also lose visibility to the task. ) } - title={showConfirmViewershipModal && isEmptyAssignee(selectedAssignee) ? 'Remove assignee?' : 'Reassign task?'} - variant={showConfirmViewershipModal ? 'danger' : 'default'} + title={ + showAssociationConfirmationModal && isEmptyAssignee(selectedAssignee) && selectorFieldType + ? `Remove ${selectorFieldType}?` + : 'Reassign task?' + } + variant={showAssociationConfirmationModal ? 'danger' : 'default'} /> @@ -601,9 +649,9 @@ export const Sidebar = ({ )} - showConfirmAssignModal ? store.dispatch(toggleShowConfirmAssignModal()) : setShowConfirmViewershipModal(false) + showConfirmAssignModal ? store.dispatch(toggleShowConfirmAssignModal()) : setAssociationConfirmationModal(false) } aria-labelledby="confirm-reassignment-modal" aria-describedby="confirm-reassignment" @@ -611,14 +659,17 @@ export const Sidebar = ({ { setSelectedAssignee(undefined) - showConfirmAssignModal ? store.dispatch(toggleShowConfirmAssignModal()) : setShowConfirmViewershipModal(false) + setSelectorFieldType(null) + showConfirmAssignModal ? store.dispatch(toggleShowConfirmAssignModal()) : setAssociationConfirmationModal(false) }} handleConfirm={() => { - if (selectedAssignee) { + if (selectorFieldType === SelectorFieldType.ASSOCIATION) { + handleConfirmAssociationChange() + } else if (selectorFieldType === SelectorFieldType.ASSIGNEE && selectedAssignee) { handleConfirmAssigneeChange(selectedAssignee) } }} - buttonText={showConfirmViewershipModal ? 'Remove' : 'Reassign'} + buttonText={showAssociationConfirmationModal ? 'Remove' : 'Reassign'} description={ showConfirmAssignModal ? ( <> @@ -630,13 +681,17 @@ export const Sidebar = ({ ) : ( <> - {getAssigneeName(getAssigneeValueFromViewers(taskAssociationValue, assignee))} will also - lose visibility to the task. + The task will be stopped sharing with{' '} + {getAssigneeName(getAssigneeValueFromAssociations(taskAssociationValue, assignee))}. ) } - title={showConfirmViewershipModal && isEmptyAssignee(selectedAssignee) ? 'Remove assignee?' : 'Reassign task?'} - variant={showConfirmViewershipModal ? 'danger' : 'default'} + title={ + showAssociationConfirmationModal && selectorFieldType && isEmptyAssignee(selectedAssignee) + ? `Remove ${selectorFieldType}?` + : 'Reassign task?' + } + variant={showAssociationConfirmationModal ? 'danger' : 'default'} /> {isAssignedToCU && userType == UserType.CLIENT_USER && !previewMode && ( diff --git a/src/app/detail/ui/TaskCardList.tsx b/src/app/detail/ui/TaskCardList.tsx index 27298fe4f..2b392074c 100644 --- a/src/app/detail/ui/TaskCardList.tsx +++ b/src/app/detail/ui/TaskCardList.tsx @@ -20,7 +20,7 @@ import { selectTaskBoard, setAssigneeCache, setConfirmAssigneeModalId, - setConfirmViewershipModalId, + setConfirmAssociationModalId, updateWorkflowStateIdByTaskId, } from '@/redux/features/taskBoardSlice' import store from '@/redux/store' @@ -31,7 +31,7 @@ import { IAssigneeCombined, InputValue, Sizes } from '@/types/interfaces' import { getAssigneeId, getAssigneeName, - getAssigneeValueFromViewers, + getAssigneeValueFromAssociations, getUserIds, isEmptyAssignee, UserIdsType, @@ -44,11 +44,11 @@ import { getSelectedUserIds, getSelectorAssignee, getSelectorAssigneeFromTask, - getSelectorViewerFromTask, + getSelectorAssociationFromTask, } from '@/utils/selector' import { shouldConfirmBeforeReassignment, - shouldConfirmViewershipBeforeReassignment, + shouldConfirmTaskSharedBeforeReassignment, } from '@/utils/shouldConfirmBeforeReassign' import { checkIfTaskViewer } from '@/utils/taskViewer' @@ -79,7 +79,7 @@ export const TaskCardList = ({ sx, disableNavigation = false, }: TaskCardListProps) => { - const { assignee, workflowStates, previewMode, token, confirmAssignModalId, assigneeCache, confirmViewershipModalId } = + const { assignee, workflowStates, previewMode, token, confirmAssignModalId, assigneeCache, confirmAssociationModalId } = useSelector(selectTaskBoard) const { tokenPayload } = useSelector(selectAuthDetails) @@ -116,17 +116,20 @@ export const TaskCardList = ({ const statusValue = _statusValue as WorkflowStateResponse const handleConfirmAssigneeChange = (userIds: UserIdsType) => { - const viewers = !userIds.internalUserId ? [] : undefined const { internalUserId, clientId, companyId } = userIds + const isAssigneeClient = !!(clientId || companyId) + const hasNoAssignee = !internalUserId && !isAssigneeClient + const associations = isAssigneeClient ? [] : undefined + const isShared = hasNoAssignee || isAssigneeClient ? false : undefined store.dispatch(setConfirmAssigneeModalId(undefined)) - store.dispatch(setConfirmViewershipModalId(undefined)) + store.dispatch(setConfirmAssociationModalId(undefined)) if (handleUpdate) { token && handleUpdate(task.id, { internalUserId, clientId, companyId }, () => - updateAssignee(token, task.id, internalUserId, clientId, companyId, viewers), + updateAssignee(token, task.id, internalUserId, clientId, companyId, associations, isShared), ) } else { - token && updateAssignee(token, task.id, internalUserId, clientId, companyId, viewers) + token && updateAssignee(token, task.id, internalUserId, clientId, companyId, associations, isShared) } } @@ -135,26 +138,30 @@ export const TaskCardList = ({ const previousAssignee = assignee.find((assignee) => assignee.id == getAssigneeId(getUserIds(task))) const nextAssignee = getSelectorAssignee(assignee, inputValue) const shouldShowConfirmModal = shouldConfirmBeforeReassignment(previousAssignee, nextAssignee) - const shouldShowConfirmViewershipModal = shouldConfirmViewershipBeforeReassignment( - getSelectorViewerFromTask(assignee, task) ?? null, + const showAssociationConfirmModal = shouldConfirmTaskSharedBeforeReassignment( + getSelectorAssociationFromTask(assignee, task) ?? null, + !!task.isShared, nextAssignee, ) if (shouldShowConfirmModal) { setSelectedAssignee(newUserIds) store.dispatch(setConfirmAssigneeModalId(task.id)) - } else if (shouldShowConfirmViewershipModal) { + } else if (showAssociationConfirmModal) { setSelectedAssignee(newUserIds) - store.dispatch(setConfirmViewershipModalId(task.id)) + store.dispatch(setConfirmAssociationModalId(task.id)) } else { - const viewers = !newUserIds.internalUserId ? [] : undefined const { internalUserId, clientId, companyId } = newUserIds + const isAssigneeClient = !!(clientId || companyId) + const hasNoAssignee = !internalUserId && !isAssigneeClient + const associations = isAssigneeClient ? [] : undefined + const isShared = hasNoAssignee || isAssigneeClient ? false : undefined if (handleUpdate) { token && handleUpdate(task.id, { assigneeId: nextAssignee?.id }, () => - updateAssignee(token, task.id, internalUserId, clientId, companyId, viewers), + updateAssignee(token, task.id, internalUserId, clientId, companyId, associations, isShared), ) } else { - token && updateAssignee(token, task.id, internalUserId, clientId, companyId, viewers) + token && updateAssignee(token, task.id, internalUserId, clientId, companyId, associations, isShared) } setAssigneeValue(nextAssignee ?? NoAssignee) } @@ -394,11 +401,11 @@ export const TaskCardList = ({ )} { e.stopPropagation() store.dispatch(setConfirmAssigneeModalId(undefined)) - store.dispatch(setConfirmViewershipModalId(undefined)) + store.dispatch(setConfirmAssociationModalId(undefined)) }} aria-labelledby="confirm-reassignment-modal" aria-describedby="confirm-reassignment" @@ -407,14 +414,14 @@ export const TaskCardList = ({ handleCancel={() => { setSelectedAssignee(undefined) store.dispatch(setConfirmAssigneeModalId(undefined)) - store.dispatch(setConfirmViewershipModalId(undefined)) + store.dispatch(setConfirmAssociationModalId(undefined)) }} handleConfirm={() => { if (selectedAssignee) { handleConfirmAssigneeChange(selectedAssignee) } }} - buttonText={confirmViewershipModalId === task.id ? 'Remove' : 'Reassign'} + buttonText={confirmAssociationModalId === task.id ? 'Remove' : 'Reassign'} description={ confirmAssignModalId === task.id ? ( <> @@ -426,17 +433,21 @@ export const TaskCardList = ({ ) : ( <> + The task will be stopped sharing with{' '} - {getAssigneeName(getAssigneeValueFromViewers(getSelectorViewerFromTask(assignee, task) ?? null, assignee))} - {' '} - will also lose visibility to the task. + {getAssigneeName( + getAssigneeValueFromAssociations(getSelectorAssociationFromTask(assignee, task) ?? null, assignee), + )} + ) } title={ - confirmViewershipModalId === task.id && isEmptyAssignee(selectedAssignee) ? 'Remove assignee?' : 'Reassign task?' + confirmAssociationModalId === task.id && isEmptyAssignee(selectedAssignee) + ? 'Remove assignee?' + : 'Reassign task?' } - variant={confirmViewershipModalId === task.id ? 'danger' : 'default'} + variant={confirmAssociationModalId === task.id ? 'danger' : 'default'} /> diff --git a/src/components/cards/TaskCard.tsx b/src/components/cards/TaskCard.tsx index fe67b0b87..fe5aab619 100644 --- a/src/components/cards/TaskCard.tsx +++ b/src/components/cards/TaskCard.tsx @@ -10,7 +10,7 @@ import { selectTaskBoard, setAssigneeCache, setConfirmAssigneeModalId, - setConfirmViewershipModalId, + setConfirmAssociationModalId, updateWorkflowStateIdByTaskId, } from '@/redux/features/taskBoardSlice' import store from '@/redux/store' @@ -20,7 +20,7 @@ import { IAssigneeCombined, InputValue, Sizes } from '@/types/interfaces' import { getAssigneeId, getAssigneeName, - getAssigneeValueFromViewers, + getAssigneeValueFromAssociations, getUserIds, isEmptyAssignee, UserIdsType, @@ -47,11 +47,11 @@ import { getSelectedUserIds, getSelectorAssignee, getSelectorAssigneeFromTask, - getSelectorViewerFromTask, + getSelectorAssociationFromTask, } from '@/utils/selector' import { shouldConfirmBeforeReassignment, - shouldConfirmViewershipBeforeReassignment, + shouldConfirmTaskSharedBeforeReassignment, } from '@/utils/shouldConfirmBeforeReassign' import z from 'zod' import { StyledModal } from '@/app/detail/ui/styledComponent' @@ -96,7 +96,7 @@ export const TaskCard = ({ task, href, workflowState, mode, subtasks, workflowDi accessibleTasks, showSubtasks, confirmAssignModalId, - confirmViewershipModalId, + confirmAssociationModalId, } = useSelector(selectTaskBoard) const subtaskCount = useSubtaskCount(task.id) @@ -132,11 +132,14 @@ export const TaskCard = ({ task, href, workflowState, mode, subtasks, workflowDi }, [task.dueDate]) const handleConfirmAssigneeChange = (userIds: UserIdsType) => { + const isAssigneeClient = !!(userIds.clientId || userIds.companyId) + const hasNoAssignee = !userIds.internalUserId && !isAssigneeClient + const associations = isAssigneeClient ? [] : undefined + const isShared = hasNoAssignee || isAssigneeClient ? false : undefined const { internalUserId, clientId, companyId } = userIds - const viewers = !internalUserId ? [] : undefined store.dispatch(setConfirmAssigneeModalId(undefined)) - store.dispatch(setConfirmViewershipModalId(undefined)) - token && updateAssignee(token, task.id, internalUserId, clientId, companyId, viewers) + store.dispatch(setConfirmAssociationModalId(undefined)) + token && updateAssignee(token, task.id, internalUserId, clientId, companyId, associations, isShared) } const handleAssigneeChange = (inputValue: InputValue[]) => { @@ -144,23 +147,28 @@ export const TaskCard = ({ task, href, workflowState, mode, subtasks, workflowDi const previousAssignee = assignee.find((assignee) => assignee.id == getAssigneeId(getUserIds(task))) const nextAssignee = getSelectorAssignee(assignee, inputValue) const shouldShowConfirmModal = shouldConfirmBeforeReassignment(previousAssignee, nextAssignee) - const shouldShowConfirmViewershipModal = shouldConfirmViewershipBeforeReassignment( - getSelectorViewerFromTask(assignee, task) ?? null, + const showAssociationConfirmModal = shouldConfirmTaskSharedBeforeReassignment( + getSelectorAssociationFromTask(assignee, task) ?? null, + !!task.isShared, nextAssignee, ) if (shouldShowConfirmModal) { setSelectedAssignee(newUserIds) store.dispatch(setConfirmAssigneeModalId(task.id)) - } else if (shouldShowConfirmViewershipModal) { + } else if (showAssociationConfirmModal) { setSelectedAssignee(newUserIds) - store.dispatch(setConfirmViewershipModalId(task.id)) + store.dispatch(setConfirmAssociationModalId(task.id)) } else { const { internalUserId, clientId, companyId } = newUserIds - const viewers = !internalUserId ? [] : undefined - token && updateAssignee(token, task.id, internalUserId, clientId, companyId, viewers) + const isAssigneeClient = !!(clientId || companyId) + const hasNoAssignee = !internalUserId && !isAssigneeClient + const associations = isAssigneeClient ? [] : undefined + const isShared = hasNoAssignee || isAssigneeClient ? false : undefined + token && updateAssignee(token, task.id, internalUserId, clientId, companyId, associations, isShared) setAssigneeValue(nextAssignee ?? NoAssignee) } } + const getAssigneeValue = (userIds?: UserIdsType) => { if (!userIds) { return NoAssignee @@ -330,11 +338,11 @@ export const TaskCard = ({ task, href, workflowState, mode, subtasks, workflowDi )} { e.stopPropagation() store.dispatch(setConfirmAssigneeModalId(undefined)) - store.dispatch(setConfirmViewershipModalId(undefined)) + store.dispatch(setConfirmAssociationModalId(undefined)) }} aria-labelledby="confirm-reassignment-modal" aria-describedby="confirm-reassignment" @@ -343,14 +351,14 @@ export const TaskCard = ({ task, href, workflowState, mode, subtasks, workflowDi handleCancel={() => { setSelectedAssignee(undefined) store.dispatch(setConfirmAssigneeModalId(undefined)) - store.dispatch(setConfirmViewershipModalId(undefined)) + store.dispatch(setConfirmAssociationModalId(undefined)) }} handleConfirm={() => { if (selectedAssignee) { handleConfirmAssigneeChange(selectedAssignee) } }} - buttonText={confirmViewershipModalId === task.id ? 'Remove' : 'Reassign'} + buttonText={confirmAssociationModalId === task.id ? 'Remove' : 'Reassign'} description={ confirmAssignModalId === task.id ? ( <> @@ -362,17 +370,21 @@ export const TaskCard = ({ task, href, workflowState, mode, subtasks, workflowDi ) : ( <> + The task will be stopped sharing with{' '} - {getAssigneeName(getAssigneeValueFromViewers(getSelectorViewerFromTask(assignee, task) ?? null, assignee))} - {' '} - will also lose visibility to the task. + {getAssigneeName( + getAssigneeValueFromAssociations(getSelectorAssociationFromTask(assignee, task) ?? null, assignee), + )} + ) } title={ - confirmViewershipModalId === task.id && isEmptyAssignee(selectedAssignee) ? 'Remove assignee?' : 'Reassign task?' + confirmAssociationModalId === task.id && isEmptyAssignee(selectedAssignee) + ? 'Remove assignee?' + : 'Reassign task?' } - variant={confirmViewershipModalId === task.id ? 'danger' : 'default'} + variant={confirmAssociationModalId === task.id ? 'danger' : 'default'} /> diff --git a/src/redux/features/taskBoardSlice.tsx b/src/redux/features/taskBoardSlice.tsx index 28e288ead..204c66c71 100644 --- a/src/redux/features/taskBoardSlice.tsx +++ b/src/redux/features/taskBoardSlice.tsx @@ -27,7 +27,7 @@ interface IInitialState { accesibleTaskIds: string[] accessibleTasks: TaskResponse[] confirmAssignModalId: string | undefined - confirmViewershipModalId: string | undefined + confirmAssociationModalId: string | undefined assigneeCache: Record previewClientCompany: PreviewClientCompanyType urlActionParams: UrlActionParamsType @@ -63,7 +63,7 @@ const initialState: IInitialState = { accesibleTaskIds: [], accessibleTasks: [], confirmAssignModalId: '', - confirmViewershipModalId: '', + confirmAssociationModalId: '', assigneeCache: {}, urlActionParams: { action: '', @@ -221,8 +221,8 @@ const taskBoardSlice = createSlice({ state.confirmAssignModalId = action.payload }, - setConfirmViewershipModalId: (state, action: { payload: string | undefined }) => { - state.confirmViewershipModalId = action.payload + setConfirmAssociationModalId: (state, action: { payload: string | undefined }) => { + state.confirmAssociationModalId = action.payload }, setAssigneeCache: (state, action: { payload: { key: string; value: IAssigneeCombined } }) => { @@ -255,7 +255,7 @@ export const { setAccesibleTaskIds, setAccessibleTasks, setConfirmAssigneeModalId, - setConfirmViewershipModalId, + setConfirmAssociationModalId, setAssigneeCache, setPreviewClientCompany, setUrlActionParams, diff --git a/src/types/common.ts b/src/types/common.ts index eb18881fc..327e761a2 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -328,3 +328,8 @@ export enum FilterType { Visibility = 'Client Visibility', Creator = 'Creator', } + +export enum SelectorFieldType { + ASSIGNEE = 'assignee', + ASSOCIATION = 'association', +} diff --git a/src/utils/assignee.ts b/src/utils/assignee.ts index 6e5edeb0e..442b98d43 100644 --- a/src/utils/assignee.ts +++ b/src/utils/assignee.ts @@ -106,7 +106,7 @@ export const isEmptyAssignee = (userIds?: UserIdsType) => { return Object.values(userIds).every((value) => value === null) } -export const getAssigneeValueFromViewers = (viewer: IAssigneeCombined | null, assignee: IAssigneeCombined[]) => { +export const getAssigneeValueFromAssociations = (viewer: IAssigneeCombined | null, assignee: IAssigneeCombined[]) => { if (!viewer) { return NoAssignee } diff --git a/src/utils/selector.ts b/src/utils/selector.ts index 3dadda3a7..16ad03602 100644 --- a/src/utils/selector.ts +++ b/src/utils/selector.ts @@ -58,7 +58,7 @@ export const getSelectorAssigneeFromTask = (assignee: IAssigneeCombined[], task: ) } //util to get initial assignee from task for selector. -export const getSelectorViewerFromTask = (assignee: IAssigneeCombined[], task: TaskResponse) => { +export const getSelectorAssociationFromTask = (assignee: IAssigneeCombined[], task: TaskResponse) => { if (!task) return undefined return assignee.find( (assignee) => diff --git a/src/utils/shouldConfirmBeforeReassign.ts b/src/utils/shouldConfirmBeforeReassign.ts index c3bf8e2c4..7d68cc1f6 100644 --- a/src/utils/shouldConfirmBeforeReassign.ts +++ b/src/utils/shouldConfirmBeforeReassign.ts @@ -34,13 +34,14 @@ export const shouldConfirmBeforeReassignment = ( } } -export const shouldConfirmViewershipBeforeReassignment = ( - viewer: IAssigneeCombined | null, +export const shouldConfirmTaskSharedBeforeReassignment = ( + association: IAssigneeCombined | null, + isTaskShared: boolean, currentAssignee?: IAssigneeCombined, ) => { - if (viewer) { + if (association && isTaskShared) { const assigneeType = currentAssignee && getAssigneeTypeCorrected(currentAssignee) - if (!assigneeType || (assigneeType !== AssigneeType.internalUser && currentAssignee.id !== viewer.id)) { + if (!assigneeType || (assigneeType !== AssigneeType.internalUser && currentAssignee.id !== association.id)) { //no assignee or assignee is a non-IU return true }