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/app/api/tasks/public/public.dto.ts b/src/app/api/tasks/public/public.dto.ts index ff7ef956f..8a8545174 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 @@ -148,9 +173,8 @@ 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) export type PublicTaskUpdateDto = z.infer 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) { 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/app/detail/ui/NewTaskCard.tsx b/src/app/detail/ui/NewTaskCard.tsx index 0473cde81..98cb41f42 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) handleFieldChange('isShared', !isShared) diff --git a/src/app/detail/ui/Sidebar.tsx b/src/app/detail/ui/Sidebar.tsx index f85dc4b08..f71dd5ced 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,11 +107,10 @@ export const Sidebar = ({ const [taskAssociationValue, setTaskAssociationValue] = useState(null) const [selectedAssociationUser, setSelectedAssociationUser] = useState() - const [isTaskShared, setIsTaskShared] = useState(false) const baseAssociationCondition = assigneeValue && assigneeValue.type === FilterByOptions.IUS - const showShareToggle = baseAssociationCondition && taskAssociationValue + const showShareToggle = baseAssociationCondition && taskAssociationValue && (!disabled || !!previewMode) const showAssociation = !assigneeValue || baseAssociationCondition const { renderingItem: _statusValue, updateRenderingItem: updateStatusValue } = useHandleSelectorComponent({ @@ -145,6 +145,8 @@ export const Sidebar = ({ } }, [assignee, activeTask]) + const { associationLabel } = useAssociationLabelForWorkspace({ workspace, associationValue: taskAssociationValue }) + const windowWidth = useWindowWidth() const isMobile = windowWidth < 800 && windowWidth !== 0 @@ -333,11 +335,11 @@ export const Sidebar = ({ } buttonContent={ @@ -359,7 +361,7 @@ export const Sidebar = ({ } /> - {assigneeValue && assigneeValue.type === FilterByOptions.IUS && ( + {showAssociation && ( )} + {showShareToggle && ( + + + + )} @@ -561,11 +581,11 @@ export const Sidebar = ({ } outlined={true} @@ -653,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/components/buttons/FilterChip.tsx b/src/components/buttons/FilterChip.tsx index 160409b07..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, FilterType.IsShared].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 099e43c83..0fd5cce33 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,8 +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) || - (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 5c8b09337..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 || FilterOptionsKeywords.UNASSIGNED ? [FilterType.IsShared] : [] - const removed = type.length > 20 ? [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.includes(filterMode) - const isRemoved = removed.includes(filterMode) + const isDisabled = disabledFilter.has(filterMode) + const isRemoved = removedFilter.has(filterMode) if (isRemoved) return null return ( @@ -67,13 +76,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/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..f802b70c6 --- /dev/null +++ b/src/hooks/app-bridge/useTokenRefresh.ts @@ -0,0 +1,27 @@ +'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) => { + console.info('#onTokenUpdate active', data) + store.dispatch(setToken(data.token)) + }) + return unsubscribe + }, []) +} 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/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/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)) 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/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 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..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', @@ -54,7 +59,6 @@ export enum FileTypes { export enum FilterOptions { ASSIGNEE = 'assignee', ASSOCIATION = 'association', - IS_SHARED = 'isShared', CREATOR = 'creator', KEYWORD = 'keyword', TYPE = 'type', @@ -98,9 +102,7 @@ export type IFilterOptions = { ? UserIdsType : key extends FilterOptions.CREATOR ? UserIdsType - : key extends FilterOptions.IS_SHARED - ? UserIdsType - : string + : string } export interface IAssignee { 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 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"