From b900dd872e72365f1b6ca8af9ba5b12277235907 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Wed, 25 Feb 2026 18:06:45 +0545 Subject: [PATCH 01/15] fix(OUT-3202): made viewers property backward compatible with client association in public api --- src/app/api/tasks/public/public.dto.ts | 30 ++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/app/api/tasks/public/public.dto.ts b/src/app/api/tasks/public/public.dto.ts index ff7ef956f..cb8109aeb 100644 --- a/src/app/api/tasks/public/public.dto.ts +++ b/src/app/api/tasks/public/public.dto.ts @@ -48,6 +48,32 @@ export const PublicTaskDtoSchema = z.object({ }) export type PublicTaskDto = z.infer +const viewersAssociationExclusivitySchema = z + .object({ + viewers: AssociationsSchema.optional(), + association: AssociationsSchema.optional(), + isShared: z.boolean().optional(), + }) + .superRefine((val, ctx) => { + const hasViewers = val.viewers !== undefined + const hasAssociation = val.association !== undefined + const hasIsShared = val.isShared !== undefined + + if (hasViewers && (hasAssociation || hasIsShared)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'viewers cannot be used together with association or isShared. Use either viewers alone, or association/isShared together.', + path: ['viewers'], + }) + } + }) + .transform(({ viewers, association, isShared, ...rest }) => ({ + ...rest, + association: viewers ?? association, + isShared: viewers ? true : isShared, + })) + export const publicTaskCreateDtoSchemaFactory = (token: string) => { return z .object({ @@ -61,9 +87,8 @@ export const publicTaskCreateDtoSchemaFactory = (token: string) => { internalUserId: z.string().uuid().optional(), clientId: z.string().uuid().optional(), companyId: z.string().uuid().optional(), - association: AssociationsSchema, //right now, we only need the feature to have max of 1 viewer per task - isShared: z.boolean().optional(), }) + .and(viewersAssociationExclusivitySchema) .superRefine(async (data, ctx) => { const { name, templateId, internalUserId, clientId, status } = data let { companyId } = data @@ -151,6 +176,7 @@ export const PublicTaskUpdateDtoSchema = z association: AssociationsSchema, isShared: z.boolean().optional(), }) + .and(viewersAssociationExclusivitySchema) .superRefine(validateUserIds) export type PublicTaskUpdateDto = z.infer From a0db99b16de8818446927620231af3e5a7e0966b Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Wed, 25 Feb 2026 18:11:14 +0545 Subject: [PATCH 02/15] fix(OUT-3202): made viewers property backward compatible with client association in public api --- src/app/api/tasks/public/public.dto.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/api/tasks/public/public.dto.ts b/src/app/api/tasks/public/public.dto.ts index cb8109aeb..8a8545174 100644 --- a/src/app/api/tasks/public/public.dto.ts +++ b/src/app/api/tasks/public/public.dto.ts @@ -173,8 +173,6 @@ export const PublicTaskUpdateDtoSchema = z internalUserId: z.string().uuid().nullish(), clientId: z.string().uuid().nullish(), companyId: z.string().uuid().nullish(), - association: AssociationsSchema, - isShared: z.boolean().optional(), }) .and(viewersAssociationExclusivitySchema) .superRefine(validateUserIds) From fa54a303bf5ec352c6aa03cf2104a43c66b0f04c Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Wed, 25 Feb 2026 18:32:41 +0545 Subject: [PATCH 03/15] fix(OUT-3203): added a custom error message when associationSchema is violated --- src/types/dto/tasks.dto.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/types/dto/tasks.dto.ts b/src/types/dto/tasks.dto.ts index 29613a85e..1bceb6534 100644 --- a/src/types/dto/tasks.dto.ts +++ b/src/types/dto/tasks.dto.ts @@ -6,7 +6,11 @@ import { ClientResponseSchema, CompanyResponseSchema, InternalUsersSchema } from export const AssociationSchema = z.object({ clientId: z.string().uuid().optional(), - companyId: z.string().uuid(), + companyId: z + .string({ + required_error: 'companyId is required on association or viewers', + }) + .uuid(), }) export type ViewerType = z.infer From 8ac15c5de3be0a0dd124d30d01cd5c1804496e80 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 26 Feb 2026 13:05:26 +0545 Subject: [PATCH 04/15] fix(OUT-3220): allow assignee reassign in CRM view --- src/app/detail/ui/Sidebar.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/detail/ui/Sidebar.tsx b/src/app/detail/ui/Sidebar.tsx index f85dc4b08..9f522151a 100644 --- a/src/app/detail/ui/Sidebar.tsx +++ b/src/app/detail/ui/Sidebar.tsx @@ -333,11 +333,11 @@ export const Sidebar = ({ } buttonContent={ @@ -561,11 +561,11 @@ export const Sidebar = ({ } outlined={true} From 8371490c4b440f764df71d48aa25fe515173e160 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 26 Feb 2026 16:10:01 +0545 Subject: [PATCH 05/15] fix(OUT-3221): hide 'share with client' toggle on CU task details page --- src/app/detail/ui/Sidebar.tsx | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/app/detail/ui/Sidebar.tsx b/src/app/detail/ui/Sidebar.tsx index 9f522151a..47c9f4937 100644 --- a/src/app/detail/ui/Sidebar.tsx +++ b/src/app/detail/ui/Sidebar.tsx @@ -110,7 +110,7 @@ export const Sidebar = ({ const [isTaskShared, setIsTaskShared] = useState(false) const baseAssociationCondition = assigneeValue && assigneeValue.type === FilterByOptions.IUS - const showShareToggle = baseAssociationCondition && taskAssociationValue + const showShareToggle = baseAssociationCondition && taskAssociationValue && !disabled const showAssociation = !assigneeValue || baseAssociationCondition const { renderingItem: _statusValue, updateRenderingItem: updateStatusValue } = useHandleSelectorComponent({ @@ -359,7 +359,7 @@ export const Sidebar = ({ } /> - {assigneeValue && assigneeValue.type === FilterByOptions.IUS && ( + {showAssociation && ( )} + {showShareToggle && ( + + + + )} From 679d38e97b4e78a2c81554fe71b864e0cb46fc76 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 26 Feb 2026 15:46:26 +0545 Subject: [PATCH 06/15] feat(OUT-3208): remove 'shared with' filter option --- .../api/view-settings/viewSettings.service.ts | 4 --- src/components/buttons/FilterChip.tsx | 2 +- .../FilterSelector/FilterAssigneeSection.tsx | 3 +- .../FilterSelector/FilterTypeSection.tsx | 12 ++++---- .../inputs/FilterSelector/index.tsx | 5 ++-- src/components/layouts/SecondaryFilterBar.tsx | 1 - src/hooks/useFilter.tsx | 29 ++++--------------- src/redux/features/taskBoardSlice.tsx | 1 - src/types/common.ts | 1 - src/types/dto/viewSettings.dto.ts | 1 - src/types/interfaces.ts | 5 +--- 11 files changed, 15 insertions(+), 49 deletions(-) diff --git a/src/app/api/view-settings/viewSettings.service.ts b/src/app/api/view-settings/viewSettings.service.ts index c5eb899de..0ba70438d 100644 --- a/src/app/api/view-settings/viewSettings.service.ts +++ b/src/app/api/view-settings/viewSettings.service.ts @@ -39,9 +39,6 @@ export class ViewSettingsService extends BaseService { if (filterOptions && !filterOptions.association) { viewSettings.filterOptions = { ...filterOptions, [FilterOptions.ASSOCIATION]: emptyAssignee } } - if (filterOptions && !filterOptions.isShared) { - viewSettings.filterOptions = { ...filterOptions, [FilterOptions.IS_SHARED]: emptyAssignee } - } return viewSettings } @@ -84,7 +81,6 @@ export class ViewSettingsService extends BaseService { filterOptions: { [FilterOptions.ASSIGNEE]: emptyAssignee, [FilterOptions.ASSOCIATION]: emptyAssignee, - [FilterOptions.IS_SHARED]: emptyAssignee, [FilterOptions.CREATOR]: emptyAssignee, [FilterOptions.KEYWORD]: '', [FilterOptions.TYPE]: '', diff --git a/src/components/buttons/FilterChip.tsx b/src/components/buttons/FilterChip.tsx index 160409b07..ffd61dc5c 100644 --- a/src/components/buttons/FilterChip.tsx +++ b/src/components/buttons/FilterChip.tsx @@ -45,7 +45,7 @@ export const FilterChip = ({ type, assignee }: FilterChipProps) => { const hideClientsAndCompanies = type === FilterType.Creator || (filterOptions.type === FilterOptionsKeywords.TEAM && type === FilterType.Assignee) const hideIus = - [FilterType.Association, FilterType.IsShared].includes(type) || + [FilterType.Association].includes(type) || (filterOptions.type === FilterOptionsKeywords.CLIENTS && type === FilterType.Assignee) return ( diff --git a/src/components/inputs/FilterSelector/FilterAssigneeSection.tsx b/src/components/inputs/FilterSelector/FilterAssigneeSection.tsx index 099e43c83..07ca7dc44 100644 --- a/src/components/inputs/FilterSelector/FilterAssigneeSection.tsx +++ b/src/components/inputs/FilterSelector/FilterAssigneeSection.tsx @@ -20,7 +20,6 @@ export const filterOptionsMap = { [FilterType.Assignee]: FilterOptions.ASSIGNEE, [FilterType.Creator]: FilterOptions.CREATOR, [FilterType.Association]: FilterOptions.ASSOCIATION, - [FilterType.IsShared]: FilterOptions.IS_SHARED, } export const FilterAssigneeSection = ({ filterMode, setAnchorEl }: FilterAssigneeSectionProps) => { @@ -36,7 +35,7 @@ export const FilterAssigneeSection = ({ filterMode, setAnchorEl }: FilterAssigne const hideClientsAndCompanies = filterMode === FilterType.Creator || (type === FilterOptionsKeywords.TEAM && filterMode === FilterType.Assignee) const hideIus = - [FilterType.Association, FilterType.IsShared].includes(filterMode) || + [FilterType.Association].includes(filterMode) || (type === FilterOptionsKeywords.CLIENTS && filterMode === FilterType.Assignee) const handleChange = (inputValue: InputValue[]) => { diff --git a/src/components/inputs/FilterSelector/FilterTypeSection.tsx b/src/components/inputs/FilterSelector/FilterTypeSection.tsx index 5c8b09337..c5c5be0c8 100644 --- a/src/components/inputs/FilterSelector/FilterTypeSection.tsx +++ b/src/components/inputs/FilterSelector/FilterTypeSection.tsx @@ -16,7 +16,7 @@ export const FilterTypeSection = ({ setFilterMode, filterModes }: FilterTypeSect filterOptions: { type }, } = useSelector(selectTaskBoard) - const disabled = type === FilterOptionsKeywords.CLIENTS || FilterOptionsKeywords.UNASSIGNED ? [FilterType.IsShared] : [] + const disabled = type === FilterOptionsKeywords.CLIENTS ? [FilterType.Association] : [] const removed = type.length > 20 ? [FilterType.Assignee] : [] return ( @@ -67,13 +67,11 @@ export const FilterTypeSection = ({ setFilterMode, filterModes }: FilterTypeSect -
- Shared with is only available -
-
for tasks assigned to internal users.
- + <> + Related to is only available for unassigned tasks or tasks assigned to internal users. + } + allowMaxWidth >
diff --git a/src/components/inputs/FilterSelector/index.tsx b/src/components/inputs/FilterSelector/index.tsx index 0615ef2e4..807934b81 100644 --- a/src/components/inputs/FilterSelector/index.tsx +++ b/src/components/inputs/FilterSelector/index.tsx @@ -13,7 +13,7 @@ type FilterSelectorProps = { disabled?: boolean } -const FILTER_MODES = [FilterType.Assignee, FilterType.Association, FilterType.IsShared, FilterType.Creator] +const FILTER_MODES = [FilterType.Assignee, FilterType.Association, FilterType.Creator] export const FilterSelector = ({ disabled }: FilterSelectorProps) => { const [filterMode, setFilterMode] = useState(null) @@ -22,14 +22,13 @@ export const FilterSelector = ({ disabled }: FilterSelectorProps) => { const id = open ? 'filter-selector-popper' : '' const { - filterOptions: { assignee, creator, association, isShared }, + filterOptions: { assignee, creator, association }, } = useSelector(selectTaskBoard) const filterModes = FILTER_MODES.filter((mode) => { if (mode === FilterType.Assignee && !isEmptyAssignee(assignee)) return false if (mode === FilterType.Creator && !isEmptyAssignee(creator)) return false if (mode === FilterType.Association && !isEmptyAssignee(association)) return false - if (mode === FilterType.IsShared && !isEmptyAssignee(isShared)) return false return true }) diff --git a/src/components/layouts/SecondaryFilterBar.tsx b/src/components/layouts/SecondaryFilterBar.tsx index f86dec440..f3ca21be2 100644 --- a/src/components/layouts/SecondaryFilterBar.tsx +++ b/src/components/layouts/SecondaryFilterBar.tsx @@ -17,7 +17,6 @@ export const SecondaryFilterBar = ({ mode }: SecondaryFilterBarProps) => { - diff --git a/src/hooks/useFilter.tsx b/src/hooks/useFilter.tsx index dbbde26f0..a4b08dd5e 100644 --- a/src/hooks/useFilter.tsx +++ b/src/hooks/useFilter.tsx @@ -20,7 +20,6 @@ const FilterFunctions = { [FilterOptions.ASSIGNEE]: filterByAssignee, [FilterOptions.CREATOR]: filterByCreator, [FilterOptions.ASSOCIATION]: filterByClientAssociation, - [FilterOptions.IS_SHARED]: filterByClientAssociation, [FilterOptions.KEYWORD]: filterByKeyword, [FilterOptions.TYPE]: filterByType, } @@ -51,11 +50,7 @@ function filterByAssignee(filteredTasks: TaskResponse[], filterValue: UserIdsTyp return filteredTasks } -function filterByClientAssociation( - filteredTasks: TaskResponse[], - filterValue: UserIdsType, - includeShared?: boolean, -): TaskResponse[] { +function filterByClientAssociation(filteredTasks: TaskResponse[], filterValue: UserIdsType): TaskResponse[] { const assigneeUserIds = filterValue if (checkEmptyAssignee(assigneeUserIds)) { @@ -65,17 +60,11 @@ function filterByClientAssociation( if (clientId) { filteredTasks = filteredTasks.filter((task) => { - const isAssociated = task.associations?.[0]?.clientId === clientId && task.associations?.[0]?.companyId === companyId - if (includeShared) return isAssociated && task.isShared - - return isAssociated + return task.associations?.[0]?.clientId === clientId && task.associations?.[0]?.companyId === companyId }) } else if (companyId && !clientId) { filteredTasks = filteredTasks.filter((task) => { - const isAssociated = task.associations?.[0]?.companyId === companyId && !task.associations?.[0].clientId - if (includeShared) return isAssociated && task.isShared - - return isAssociated + return task.associations?.[0]?.companyId === companyId && !task.associations?.[0].clientId }) } @@ -169,17 +158,9 @@ export const useFilter = (filterOptions: IFilterOptions, isPreviewMode: boolean) const assigneeFilterValue = UserIdsSchema.parse(filterValue) filteredTasks = FilterFunctions[FilterOptions.ASSIGNEE](filteredTasks, assigneeFilterValue) } - if ( - filterType === FilterOptions.CREATOR || - filterType === FilterOptions.ASSOCIATION || - filterType === FilterOptions.IS_SHARED - ) { - let includeShared = false - if (filterType === FilterOptions.IS_SHARED) { - includeShared = true - } + if (filterType === FilterOptions.CREATOR || filterType === FilterOptions.ASSOCIATION) { const assigneeFilterValue = UserIdsSchema.parse(filterValue) - filteredTasks = FilterFunctions[filterType](filteredTasks, assigneeFilterValue, includeShared) + filteredTasks = FilterFunctions[filterType](filteredTasks, assigneeFilterValue) } if (filterType === FilterOptions.KEYWORD) { filteredTasks = FilterFunctions[FilterOptions.KEYWORD]( diff --git a/src/redux/features/taskBoardSlice.tsx b/src/redux/features/taskBoardSlice.tsx index e3509f114..10a9a17ef 100644 --- a/src/redux/features/taskBoardSlice.tsx +++ b/src/redux/features/taskBoardSlice.tsx @@ -43,7 +43,6 @@ const initialState: IInitialState = { filterOptions: { [FilterOptions.ASSIGNEE]: emptyAssignee, [FilterOptions.ASSOCIATION]: emptyAssignee, - [FilterOptions.IS_SHARED]: emptyAssignee, [FilterOptions.CREATOR]: emptyAssignee, [FilterOptions.KEYWORD]: '', [FilterOptions.TYPE]: '', diff --git a/src/types/common.ts b/src/types/common.ts index 3d0b7526c..213641bf3 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -326,7 +326,6 @@ export type ViewSettingUserIdsType = z.infer export enum FilterType { Assignee = 'Assignee', Association = 'Related to', - IsShared = 'Shared with', Creator = 'Creator', } diff --git a/src/types/dto/viewSettings.dto.ts b/src/types/dto/viewSettings.dto.ts index df5fd4c46..8d531e14f 100644 --- a/src/types/dto/viewSettings.dto.ts +++ b/src/types/dto/viewSettings.dto.ts @@ -5,7 +5,6 @@ import { z } from 'zod' export const FilterOptionsSchema = z.object({ assignee: UserIdsSchema, association: UserIdsSchema, - isShared: UserIdsSchema, creator: UserIdsSchema, keyword: z.string(), type: z.string(), diff --git a/src/types/interfaces.ts b/src/types/interfaces.ts index 3b98e71f3..5c3f5cb36 100644 --- a/src/types/interfaces.ts +++ b/src/types/interfaces.ts @@ -54,7 +54,6 @@ export enum FileTypes { export enum FilterOptions { ASSIGNEE = 'assignee', ASSOCIATION = 'association', - IS_SHARED = 'isShared', CREATOR = 'creator', KEYWORD = 'keyword', TYPE = 'type', @@ -98,9 +97,7 @@ export type IFilterOptions = { ? UserIdsType : key extends FilterOptions.CREATOR ? UserIdsType - : key extends FilterOptions.IS_SHARED - ? UserIdsType - : string + : string } export interface IAssignee { From 3c4d35180c06cb5f81c86a81d9d388d87e90c926 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 26 Feb 2026 16:35:04 +0545 Subject: [PATCH 07/15] refactor(OUT-3208): maintain code quality --- src/components/buttons/FilterChip.tsx | 3 +-- .../inputs/FilterSelector/FilterAssigneeSection.tsx | 3 +-- .../inputs/FilterSelector/FilterTypeSection.tsx | 8 ++++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/buttons/FilterChip.tsx b/src/components/buttons/FilterChip.tsx index ffd61dc5c..12ff8c123 100644 --- a/src/components/buttons/FilterChip.tsx +++ b/src/components/buttons/FilterChip.tsx @@ -45,8 +45,7 @@ export const FilterChip = ({ type, assignee }: FilterChipProps) => { const hideClientsAndCompanies = type === FilterType.Creator || (filterOptions.type === FilterOptionsKeywords.TEAM && type === FilterType.Assignee) const hideIus = - [FilterType.Association].includes(type) || - (filterOptions.type === FilterOptionsKeywords.CLIENTS && type === FilterType.Assignee) + type === FilterType.Association || (filterOptions.type === FilterOptionsKeywords.CLIENTS && type === FilterType.Assignee) return ( <> diff --git a/src/components/inputs/FilterSelector/FilterAssigneeSection.tsx b/src/components/inputs/FilterSelector/FilterAssigneeSection.tsx index 07ca7dc44..0fd5cce33 100644 --- a/src/components/inputs/FilterSelector/FilterAssigneeSection.tsx +++ b/src/components/inputs/FilterSelector/FilterAssigneeSection.tsx @@ -35,8 +35,7 @@ export const FilterAssigneeSection = ({ filterMode, setAnchorEl }: FilterAssigne const hideClientsAndCompanies = filterMode === FilterType.Creator || (type === FilterOptionsKeywords.TEAM && filterMode === FilterType.Assignee) const hideIus = - [FilterType.Association].includes(filterMode) || - (type === FilterOptionsKeywords.CLIENTS && filterMode === FilterType.Assignee) + filterMode === FilterType.Association || (type === FilterOptionsKeywords.CLIENTS && filterMode === FilterType.Assignee) const handleChange = (inputValue: InputValue[]) => { const newUserIds = getSelectedUserIds(inputValue) diff --git a/src/components/inputs/FilterSelector/FilterTypeSection.tsx b/src/components/inputs/FilterSelector/FilterTypeSection.tsx index c5c5be0c8..7c3d7897a 100644 --- a/src/components/inputs/FilterSelector/FilterTypeSection.tsx +++ b/src/components/inputs/FilterSelector/FilterTypeSection.tsx @@ -16,8 +16,8 @@ export const FilterTypeSection = ({ setFilterMode, filterModes }: FilterTypeSect filterOptions: { type }, } = useSelector(selectTaskBoard) - const disabled = type === FilterOptionsKeywords.CLIENTS ? [FilterType.Association] : [] - const removed = type.length > 20 ? [FilterType.Assignee] : [] + const disabled = type === FilterOptionsKeywords.CLIENTS && new Set([FilterType.Association]) + const removed = type.length > 20 && new Set([FilterType.Assignee]) return ( {filterModes.map((filterMode) => { - const isDisabled = disabled.includes(filterMode) - const isRemoved = removed.includes(filterMode) + const isDisabled = disabled && disabled.has(filterMode) + const isRemoved = removed && removed.has(filterMode) if (isRemoved) return null return ( From b8d1b004d9015fa83b2e43f4728b3db4ef69ac9b Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 26 Feb 2026 21:19:45 +0545 Subject: [PATCH 08/15] refactor(OUT-3208): code quality --- .../inputs/FilterSelector/FilterTypeSection.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/inputs/FilterSelector/FilterTypeSection.tsx b/src/components/inputs/FilterSelector/FilterTypeSection.tsx index 7c3d7897a..0cd09abf6 100644 --- a/src/components/inputs/FilterSelector/FilterTypeSection.tsx +++ b/src/components/inputs/FilterSelector/FilterTypeSection.tsx @@ -11,13 +11,22 @@ interface FilterTypeSectionProps { filterModes: FilterType[] } +type FilterSetType = (typeof FilterType)[keyof typeof FilterType] + export const FilterTypeSection = ({ setFilterMode, filterModes }: FilterTypeSectionProps) => { const { filterOptions: { type }, } = useSelector(selectTaskBoard) - const disabled = type === FilterOptionsKeywords.CLIENTS && new Set([FilterType.Association]) - const removed = type.length > 20 && new Set([FilterType.Assignee]) + const disabledFilter = new Set() + if (type === FilterOptionsKeywords.CLIENTS) { + disabledFilter.add(FilterType.Association) + } + + const removedFilter = new Set() + if (type.length > 20) { + removedFilter.add(FilterType.Assignee) + } return ( {filterModes.map((filterMode) => { - const isDisabled = disabled && disabled.has(filterMode) - const isRemoved = removed && removed.has(filterMode) + const isDisabled = disabledFilter.has(filterMode) + const isRemoved = removedFilter.has(filterMode) if (isRemoved) return null return ( From 1785cc0e96502f4d78e8f5f3037a54f94fef6b59 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 26 Feb 2026 21:44:14 +0545 Subject: [PATCH 09/15] fix(OUT-3221): show shared with toggle in CRM view --- src/app/detail/ui/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/detail/ui/Sidebar.tsx b/src/app/detail/ui/Sidebar.tsx index 47c9f4937..e806166ae 100644 --- a/src/app/detail/ui/Sidebar.tsx +++ b/src/app/detail/ui/Sidebar.tsx @@ -110,7 +110,7 @@ export const Sidebar = ({ const [isTaskShared, setIsTaskShared] = useState(false) const baseAssociationCondition = assigneeValue && assigneeValue.type === FilterByOptions.IUS - const showShareToggle = baseAssociationCondition && taskAssociationValue && !disabled + const showShareToggle = baseAssociationCondition && taskAssociationValue && (!disabled || !!previewMode) const showAssociation = !assigneeValue || baseAssociationCondition const { renderingItem: _statusValue, updateRenderingItem: updateStatusValue } = useHandleSelectorComponent({ From ed67441abdff60fd7e43aeee7bf91a7545ba3e09 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Fri, 27 Feb 2026 13:17:31 +0545 Subject: [PATCH 10/15] fix(OUt-3206): update workspce label for client and company in 'Shared with' slider - [x] custom hook that returns the assocation label - [x] use the hook wherever the 'Shared with' slider is used --- src/app/detail/ui/NewTaskCard.tsx | 21 +++++++++++++-------- src/app/detail/ui/Sidebar.tsx | 8 +++++--- src/app/ui/NewTaskForm.tsx | 5 ++++- src/hooks/useWorkspaceLabel.ts | 25 +++++++++++++++++++++++++ src/types/interfaces.ts | 5 +++++ src/utils/getWorkspaceLabels.ts | 2 +- 6 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 src/hooks/useWorkspaceLabel.ts diff --git a/src/app/detail/ui/NewTaskCard.tsx b/src/app/detail/ui/NewTaskCard.tsx index 0473cde81..a0ec8d90e 100644 --- a/src/app/detail/ui/NewTaskCard.tsx +++ b/src/app/detail/ui/NewTaskCard.tsx @@ -37,6 +37,7 @@ import dayjs from 'dayjs' import { useCallback, useEffect, useRef, useState } from 'react' import { useSelector } from 'react-redux' import { Tapwrite } from 'tapwrite' +import { useAssociationLabelForWorkspace } from '@/hooks/useWorkspaceLabel' interface SubTaskFields { title: string @@ -72,7 +73,7 @@ export const NewTaskCard = ({ [UserIds.COMPANY_ID]: null, } - const { tokenPayload } = useSelector(selectAuthDetails) + const { tokenPayload, workspace } = useSelector(selectAuthDetails) const [subTaskFields, setSubTaskFields] = useState({ title: '', description: '', @@ -154,6 +155,8 @@ export const NewTaskCard = ({ const [taskAssociationValue, setTaskAssociationValue] = useState(previewTaskAssociation) const [isShared, setIsShared] = useState(!!previewTaskAssociation) + const { associationLabel } = useAssociationLabelForWorkspace({ workspace, associationValue: taskAssociationValue }) + const applyTemplate = useCallback( (id: string, templateTitle: string) => { const controller = new AbortController() @@ -271,6 +274,13 @@ export const NewTaskCard = ({ handleFieldChange('userIds', newUserIds) } + const handleAssociationChange = (inputValue: InputValue[]) => { + const newUserIds = getSelectedViewerIds(inputValue) + const selectedAssignee = getSelectorAssignee(assignee, inputValue) + setTaskAssociationValue(selectedAssignee || null) + handleFieldChange('associations', newUserIds) + } + const baseAssociationCondition = assigneeValue && assigneeValue.type === FilterByOptions.IUS const showShareToggle = baseAssociationCondition && taskAssociationValue const showAssociation = !assigneeValue || baseAssociationCondition @@ -446,12 +456,7 @@ export const NewTaskCard = ({ hideIusList disabled={!!previewMode} name="Set related to" - onChange={(inputValue) => { - const newUserIds = getSelectedViewerIds(inputValue) - const selectedAssignee = getSelectorAssignee(assignee, inputValue) - setTaskAssociationValue(selectedAssignee || null) - handleFieldChange('associations', newUserIds) - }} + onChange={handleAssociationChange} initialValue={taskAssociationValue || undefined} buttonContent={ {showShareToggle && ( { setIsShared(!isShared) diff --git a/src/app/detail/ui/Sidebar.tsx b/src/app/detail/ui/Sidebar.tsx index e806166ae..c1c2f2e75 100644 --- a/src/app/detail/ui/Sidebar.tsx +++ b/src/app/detail/ui/Sidebar.tsx @@ -48,6 +48,7 @@ import { useSelector } from 'react-redux' import { z } from 'zod' import { CopilotToggle } from '@/components/inputs/CopilotToggle' import { SelectorFieldType } from '@/types/common' +import { useAssociationLabelForWorkspace } from '@/hooks/useWorkspaceLabel' type StyledTypographyProps = { display?: string @@ -84,7 +85,7 @@ export const Sidebar = ({ }) => { const { activeTask, workflowStates, assignee, previewMode } = useSelector(selectTaskBoard) const { showSidebar, showConfirmAssignModal, fromNotificationCenter } = useSelector(selectTaskDetails) - const { tokenPayload } = useSelector(selectAuthDetails) + const { tokenPayload, workspace } = useSelector(selectAuthDetails) const [isHydrated, setIsHydrated] = useState(false) @@ -106,7 +107,6 @@ export const Sidebar = ({ const [taskAssociationValue, setTaskAssociationValue] = useState(null) const [selectedAssociationUser, setSelectedAssociationUser] = useState() - const [isTaskShared, setIsTaskShared] = useState(false) const baseAssociationCondition = assigneeValue && assigneeValue.type === FilterByOptions.IUS @@ -145,6 +145,8 @@ export const Sidebar = ({ } }, [assignee, activeTask]) + const { associationLabel } = useAssociationLabelForWorkspace({ workspace, associationValue: taskAssociationValue }) + const windowWidth = useWindowWidth() const isMobile = windowWidth < 800 && windowWidth !== 0 @@ -671,7 +673,7 @@ export const Sidebar = ({ <> theme.color.borders.border, height: '1px' }} /> const { workflowStates, assignee, previewMode, filterOptions, urlActionParams, token, previewClientCompany } = useSelector(selectTaskBoard) const [actionParamPayload, setActionParamPayload] = useState(null) + const { workspace } = useSelector(selectAuthDetails) const todoWorkflowState = workflowStates.find((el) => el.key === 'todo') || workflowStates[0] const actionParamWorkflowState = actionParamPayload @@ -122,6 +124,7 @@ export const NewTaskForm = ({ handleCreate, handleClose }: NewTaskFormProps) => ) ?? null) : null, ) + const { associationLabel } = useAssociationLabelForWorkspace({ workspace, associationValue: taskAssociationsValue }) // this function handles the action param passed in the url and fill the values in the form const handleUrlActionParam = useCallback(async () => { @@ -434,7 +437,7 @@ export const NewTaskForm = ({ handleCreate, handleClose }: NewTaskFormProps) => }} > { const localSharedState = store.getState().createTask.isShared store.dispatch(setCreateTaskFields({ targetField: 'isShared', value: !localSharedState })) diff --git a/src/hooks/useWorkspaceLabel.ts b/src/hooks/useWorkspaceLabel.ts new file mode 100644 index 000000000..5ed564414 --- /dev/null +++ b/src/hooks/useWorkspaceLabel.ts @@ -0,0 +1,25 @@ +import { WorkspaceResponse } from '@/types/common' +import { DefaultUserLabels, IAssigneeCombined } from '@/types/interfaces' +import { getWorkspaceLabels } from '@/utils/getWorkspaceLabels' +import { useEffect, useMemo, useState } from 'react' + +export const useAssociationLabelForWorkspace = ({ + workspace, + associationValue, +}: { + workspace?: WorkspaceResponse + associationValue: IAssigneeCombined | null +}) => { + const [associationLabel, setAssociationLabel] = useState(DefaultUserLabels.Client) + + const workspaceLabels = useMemo(() => { + return getWorkspaceLabels(workspace) + }, [workspace]) + + useEffect(() => { + const label = associationValue?.type === 'clients' ? workspaceLabels.individualTerm : workspaceLabels.groupTerm + setAssociationLabel(label) + }, [workspaceLabels, associationValue]) + + return { associationLabel } +} diff --git a/src/types/interfaces.ts b/src/types/interfaces.ts index 5c3f5cb36..c9298168f 100644 --- a/src/types/interfaces.ts +++ b/src/types/interfaces.ts @@ -14,6 +14,11 @@ export enum UserType { CLIENT_USER = 'cu', } +export enum DefaultUserLabels { + Client = 'client', + Company = 'company', +} + export enum View { LIST_VIEW = 'list', BOARD_VIEW = 'board', diff --git a/src/utils/getWorkspaceLabels.ts b/src/utils/getWorkspaceLabels.ts index 25a65d4cf..7ce27b6a8 100644 --- a/src/utils/getWorkspaceLabels.ts +++ b/src/utils/getWorkspaceLabels.ts @@ -1,6 +1,6 @@ import { WorkspaceResponse } from '@/types/common' -type WorkspaceLabels = { +export type WorkspaceLabels = { individualTerm: string individualTermPlural: string groupTerm: string From f6b427cd0771f097adbddaec657c707ff4428a16 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Fri, 27 Feb 2026 16:16:37 +0545 Subject: [PATCH 11/15] fix(OUT-3219): show shared tasks in realtime --- src/lib/realtime.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/lib/realtime.ts b/src/lib/realtime.ts index acb1be0da..07e788b76 100644 --- a/src/lib/realtime.ts +++ b/src/lib/realtime.ts @@ -28,13 +28,15 @@ export class RealtimeHandler { } } - private isViewer(newTask: RealTimeTaskResponse): boolean { + private isTaskShared(newTask: RealTimeTaskResponse): boolean { return this.tokenPayload.clientId || !!getPreviewMode(this.tokenPayload) - ? (newTask.associations?.some( - (viewer) => - (viewer.clientId === this.tokenPayload.clientId && viewer.companyId === this.tokenPayload.companyId) || - (!viewer.clientId && viewer.companyId === this.tokenPayload.companyId), - ) ?? false) + ? ((newTask.associations?.some( + (association) => + (association.clientId === this.tokenPayload.clientId && association.companyId === this.tokenPayload.companyId) || + (!association.clientId && association.companyId === this.tokenPayload.companyId), + ) && + newTask.isShared) ?? + false) : false } //check if the task incoming from realtime includes the logged in client as a viewer. @@ -65,7 +67,7 @@ export class RealtimeHandler { // Ignore all tasks that don't belong to client if ( - !this.isViewer(newTask) && + !this.isTaskShared(newTask) && !( (newTask.clientId == this.tokenPayload.clientId && newTask.companyId == this.tokenPayload.companyId) || (newTask.clientId == null && newTask.companyId == this.tokenPayload.companyId) @@ -227,7 +229,7 @@ export class RealtimeHandler { // - task is a client task, assigned to another client // - task's companyId does not match current user's active companyId if ( - !this.isViewer(newTask) && + !this.isTaskShared(newTask) && (!newTask.assigneeId || !!newTask.internalUserId || (newTask.clientId && newTask.clientId !== this.tokenPayload.clientId) || @@ -280,7 +282,7 @@ export class RealtimeHandler { // --- Handle unassignment for clients (board + details page) const isReassignedOutOfClientScope = this.userRole === AssigneeType.client && - !this.isViewer(updatedTask) && + !this.isTaskShared(updatedTask) && (!updatedTask.clientId ? updatedTask.companyId !== this.tokenPayload.companyId : updatedTask.companyId !== this.tokenPayload.companyId || updatedTask.clientId !== this.tokenPayload.clientId) @@ -317,8 +319,8 @@ export class RealtimeHandler { // CASE III: Reassignment into scope const isReassignedIntoClientScope = this.userRole === AssigneeType.client && - updatedTask.assigneeId !== prevTask.assigneeId && - (this.isViewer(updatedTask) || + (updatedTask.assigneeId !== prevTask.assigneeId || + this.isTaskShared(updatedTask) || (!updatedTask.clientId ? updatedTask.companyId === this.tokenPayload.companyId : updatedTask.companyId === this.tokenPayload.companyId && updatedTask.clientId === this.tokenPayload.clientId)) From 43a836d717cea8e3a98b51cfa73ecf612416f966 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Fri, 27 Feb 2026 16:55:35 +0545 Subject: [PATCH 12/15] fix(OUT-3204): disallow association with placeholder company IDs --- src/app/api/tasks/tasksShared.service.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/api/tasks/tasksShared.service.ts b/src/app/api/tasks/tasksShared.service.ts index 105173ffc..cc94ea20f 100644 --- a/src/app/api/tasks/tasksShared.service.ts +++ b/src/app/api/tasks/tasksShared.service.ts @@ -377,7 +377,10 @@ export abstract class TasksSharedService extends BaseService { throw new APIError(httpStatus.BAD_REQUEST, 'Invalid companyId for the provided association.') } } else { - await this.copilot.getCompany(association.companyId) + const company = await this.copilot.getCompany(association.companyId) + if (company.isPlaceholder) { + throw new APIError(httpStatus.BAD_REQUEST, 'Invalid companyId for the provided association.') + } } } catch (err) { if (err instanceof APIError) { From 1aaa6e8fe8c3235297532997962626475a03784e Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Fri, 27 Feb 2026 17:16:01 +0545 Subject: [PATCH 13/15] fix(OUT-3228): enable share with toggle in subtask card --- src/app/detail/ui/NewTaskCard.tsx | 1 - src/app/detail/ui/Sidebar.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/detail/ui/NewTaskCard.tsx b/src/app/detail/ui/NewTaskCard.tsx index a0ec8d90e..98cb41f42 100644 --- a/src/app/detail/ui/NewTaskCard.tsx +++ b/src/app/detail/ui/NewTaskCard.tsx @@ -490,7 +490,6 @@ export const NewTaskCard = ({ {showShareToggle && ( { setIsShared(!isShared) handleFieldChange('isShared', !isShared) diff --git a/src/app/detail/ui/Sidebar.tsx b/src/app/detail/ui/Sidebar.tsx index c1c2f2e75..f71dd5ced 100644 --- a/src/app/detail/ui/Sidebar.tsx +++ b/src/app/detail/ui/Sidebar.tsx @@ -413,7 +413,7 @@ export const Sidebar = ({ }} > Date: Mon, 2 Mar 2026 13:07:39 -0500 Subject: [PATCH 14/15] feat: add token refresh via @assembly-js/app-bridge Subscribe to automatic token refresh events from the parent Copilot dashboard so API calls don't fail when tokens expire mid-session. Co-Authored-By: Claude Opus 4.6 --- package.json | 1 + src/hoc/ClientSideStateUpdate.tsx | 2 ++ src/hooks/app-bridge/useTokenRefresh.ts | 26 +++++++++++++++++++++++++ yarn.lock | 5 +++++ 4 files changed, 34 insertions(+) create mode 100644 src/hooks/app-bridge/useTokenRefresh.ts diff --git a/package.json b/package.json index 594481b66..ef338eea9 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "@assembly-js/app-bridge": "^1.1.0", "@assembly-js/node-sdk": "^3.19.1", "@cyntler/react-doc-viewer": "^1.17.0", "@emotion/react": "^11.11.3", diff --git a/src/hoc/ClientSideStateUpdate.tsx b/src/hoc/ClientSideStateUpdate.tsx index d159c4a25..078b1d51b 100644 --- a/src/hoc/ClientSideStateUpdate.tsx +++ b/src/hoc/ClientSideStateUpdate.tsx @@ -25,6 +25,7 @@ import { CreateViewSettingsDTO } from '@/types/dto/viewSettings.dto' import { WorkflowStateResponse } from '@/types/dto/workflowStates.dto' import { FilterOptionsKeywords, IAssigneeCombined, IAssigneeSuggestions, ITemplate } from '@/types/interfaces' import { filterOptionsMap } from '@/types/objectMaps' +import { useTokenRefresh } from '@/hooks/app-bridge/useTokenRefresh' import { getPreviewMode, handlePreviewMode } from '@/utils/previewMode' import { ReactNode, useEffect } from 'react' import { useSelector } from 'react-redux' @@ -73,6 +74,7 @@ export const ClientSideStateUpdate = ({ }: ClientSideStateUpdateProps) => { const { tasks: tasksInStore, viewSettingsTemp, accessibleTasks: accessibleTaskInStore } = useSelector(selectTaskBoard) const { templates: templatesInStore } = useSelector(selectCreateTemplate) + useTokenRefresh(workspace?.portalUrl) useEffect(() => { if (workflowStates) { diff --git a/src/hooks/app-bridge/useTokenRefresh.ts b/src/hooks/app-bridge/useTokenRefresh.ts new file mode 100644 index 000000000..fdfe758b5 --- /dev/null +++ b/src/hooks/app-bridge/useTokenRefresh.ts @@ -0,0 +1,26 @@ +'use client' + +import { AssemblyBridge } from '@assembly-js/app-bridge' +import { setToken } from '@/redux/features/taskBoardSlice' +import store from '@/redux/store' +import { ensureHttps } from '@/utils/https' +import { useEffect } from 'react' + +/** + * Subscribes to token refresh events from the parent Copilot dashboard + * via @assembly-js/app-bridge and pushes updated tokens into Redux. + */ +export function useTokenRefresh(portalUrl?: string) { + useEffect(() => { + if (portalUrl) { + AssemblyBridge.configure({ additionalOrigins: [ensureHttps(portalUrl)] }) + } + }, [portalUrl]) + + useEffect(() => { + const unsubscribe = AssemblyBridge.sessionToken.onTokenUpdate((data) => { + store.dispatch(setToken(data.token)) + }) + return unsubscribe + }, []) +} diff --git a/yarn.lock b/yarn.lock index 75bba2e55..9980d0e64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18,6 +18,11 @@ "@csstools/css-tokenizer" "^3.0.3" lru-cache "^10.4.3" +"@assembly-js/app-bridge@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@assembly-js/app-bridge/-/app-bridge-1.1.0.tgz#e1c3b4e4ff094ba5dac33709de38f13450c7715f" + integrity sha512-brMSQxpldcwdqCpIxcVUKj6eX3uP18BKH4NhWpgGVaLo/aDVD/G4j/AI98UYeLeLK0unkjKuo4NLQZRNmE9Lag== + "@assembly-js/node-sdk@^3.19.1": version "3.19.1" resolved "https://registry.yarnpkg.com/@assembly-js/node-sdk/-/node-sdk-3.19.1.tgz#3d9ff79782250e9ee7f0afc43d050d205afdb3f8" From bce70e0022943a73127ffa93925ad3a3dfc1f75a Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Tue, 17 Mar 2026 14:34:26 +0545 Subject: [PATCH 15/15] fix(token-refresh): added logs --- src/hooks/app-bridge/useTokenRefresh.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hooks/app-bridge/useTokenRefresh.ts b/src/hooks/app-bridge/useTokenRefresh.ts index fdfe758b5..f802b70c6 100644 --- a/src/hooks/app-bridge/useTokenRefresh.ts +++ b/src/hooks/app-bridge/useTokenRefresh.ts @@ -19,6 +19,7 @@ export function useTokenRefresh(portalUrl?: string) { useEffect(() => { const unsubscribe = AssemblyBridge.sessionToken.onTokenUpdate((data) => { + console.info('#onTokenUpdate active', data) store.dispatch(setToken(data.token)) }) return unsubscribe