From 179e39726d834492736dcf9d57e8dae7f8d5544d Mon Sep 17 00:00:00 2001 From: Arjita Mitra Date: Wed, 3 Jun 2026 12:58:12 +0200 Subject: [PATCH 1/2] fix: pass fire-and-forget invoker to cells share modals(WPB-26087) - Thread `fireAndForgetInvoker` through the global and conversation cells share modal props. - Updated public link hooks to receive the invoker explicitly instead of relying on `RootContext`. - Added regression coverage for rendering both share modal paths outside `RootProvider`. --- .../useCellPublicLink.test.ts | 44 +++++++--- .../useCellPublicLink/useCellPublicLink.ts | 6 +- .../CellsShareModal/CellsShareModal.test.tsx | 83 ++++++++++++++++++ .../CellsShareModal/CellsShareModal.tsx | 17 +++- .../useCellGlobalPublicLink.ts | 10 ++- .../CellsNodeShareModal.test.tsx | 85 +++++++++++++++++++ .../CellsNodeShareModal.tsx | 7 +- .../useCellConversationPublicLink.ts | 5 ++ 8 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.test.tsx create mode 100644 apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.test.tsx diff --git a/apps/webapp/src/script/components/Cells/common/useCellPublicLink/useCellPublicLink.test.ts b/apps/webapp/src/script/components/Cells/common/useCellPublicLink/useCellPublicLink.test.ts index f2c358ed814..2c684d91e14 100644 --- a/apps/webapp/src/script/components/Cells/common/useCellPublicLink/useCellPublicLink.test.ts +++ b/apps/webapp/src/script/components/Cells/common/useCellPublicLink/useCellPublicLink.test.ts @@ -20,11 +20,7 @@ import {act, renderHook, waitFor} from '@testing-library/react'; import {CellsRepository} from 'Repositories/cells/cellsRepository'; -import { - createExecutingFireAndForgetInvokerForTest, - createRootContextValueForTest, - createRootProviderWrapperForTest, -} from 'src/script/page/testSupport/rootContextTestSupport'; +import {createExecutingFireAndForgetInvokerForTest} from 'src/script/page/testSupport/rootContextTestSupport'; import {CellNode, CellNodeType} from 'src/script/types/cellNode'; import {useCellPublicLink} from './useCellPublicLink'; @@ -41,10 +37,7 @@ describe('useCellPublicLink', () => { let mockCellsRepository: jest.Mocked; let mockNode: CellNode | undefined; const mockSetPublicLink = jest.fn(); - const rootContextValue = createRootContextValueForTest({ - fireAndForgetInvoker: createExecutingFireAndForgetInvokerForTest(), - }); - const rootProviderWrapper = createRootProviderWrapperForTest(rootContextValue); + const defaultFireAndForgetInvoker = createExecutingFireAndForgetInvokerForTest(); const createMockNode = (overrides: Partial = {}): CellNode => ({ id: 'test-uuid', @@ -67,15 +60,17 @@ describe('useCellPublicLink', () => { node?: CellNode; refreshLinkDataAfterUpdate?: boolean; setStatusOnPublicLinkUrl?: boolean; + fireAndForgetInvoker?: typeof defaultFireAndForgetInvoker; }) => { const initialProps = { node: options?.node ?? mockNode, refreshLinkDataAfterUpdate: options?.refreshLinkDataAfterUpdate ?? false, setStatusOnPublicLinkUrl: options?.setStatusOnPublicLinkUrl ?? false, + fireAndForgetInvoker: options?.fireAndForgetInvoker ?? defaultFireAndForgetInvoker, }; const hook = renderHook( - ({node, refreshLinkDataAfterUpdate, setStatusOnPublicLinkUrl}) => + ({node, refreshLinkDataAfterUpdate, setStatusOnPublicLinkUrl, fireAndForgetInvoker}) => useCellPublicLink({ uuid: 'test-uuid', node, @@ -83,8 +78,9 @@ describe('useCellPublicLink', () => { setPublicLink: mockSetPublicLink, refreshLinkDataAfterUpdate, setStatusOnPublicLinkUrl, + fireAndForgetInvoker, }), - {initialProps, wrapper: rootProviderWrapper}, + {initialProps}, ); const rerenderWith = (props: Partial) => @@ -92,6 +88,7 @@ describe('useCellPublicLink', () => { node: props.node ?? initialProps.node, refreshLinkDataAfterUpdate: props.refreshLinkDataAfterUpdate ?? initialProps.refreshLinkDataAfterUpdate, setStatusOnPublicLinkUrl: props.setStatusOnPublicLinkUrl ?? initialProps.setStatusOnPublicLinkUrl, + fireAndForgetInvoker: props.fireAndForgetInvoker ?? initialProps.fireAndForgetInvoker, }); return {...hook, rerenderWith}; @@ -146,6 +143,31 @@ describe('useCellPublicLink', () => { alreadyShared: true, }); }); + + it('creates a public link with an explicit fire-and-forget invoker outside RootProvider', async () => { + mockCellsRepository.createPublicLink.mockResolvedValue({ + Uuid: 'new-link-uuid', + LinkUrl: '/public/test-link', + }); + + const {result} = renderPublicLinkHook({ + fireAndForgetInvoker: createExecutingFireAndForgetInvokerForTest(), + }); + + act(() => { + result.current.togglePublicLink(); + }); + + await waitFor(() => { + expect(result.current.status).toBe('success'); + }); + + expect(mockSetPublicLink).toHaveBeenCalledWith({ + uuid: 'new-link-uuid', + url: 'https://cells.example.com/public/test-link', + alreadyShared: true, + }); + }); }); describe('should delete a public link when toggle is disabled', () => { diff --git a/apps/webapp/src/script/components/Cells/common/useCellPublicLink/useCellPublicLink.ts b/apps/webapp/src/script/components/Cells/common/useCellPublicLink/useCellPublicLink.ts index f9eb9970af0..4ed433b96d9 100644 --- a/apps/webapp/src/script/components/Cells/common/useCellPublicLink/useCellPublicLink.ts +++ b/apps/webapp/src/script/components/Cells/common/useCellPublicLink/useCellPublicLink.ts @@ -22,9 +22,10 @@ import {useCallback, useEffect, useRef, useState} from 'react'; import is from '@sindresorhus/is'; import type {RestShareLink} from '@wireapp/api-client/lib/cells'; +import type {FireAndForgetInvoker} from '@wireapp/core'; + import {CellsRepository} from 'Repositories/cells/cellsRepository'; import {Config} from 'src/script/Config'; -import {useApplicationContext} from 'src/script/page/RootProvider'; import type {CellNode} from 'src/script/types/cellNode'; type PublicLinkStatus = 'idle' | 'loading' | 'error' | 'success'; @@ -37,6 +38,7 @@ interface UseCellPublicLinkParams { refreshLinkDataAfterUpdate?: boolean; setStatusOnPublicLinkUrl?: boolean; includeNodePublicLinkInCallbacks?: boolean; + fireAndForgetInvoker: FireAndForgetInvoker; } export const useCellPublicLink = ({ @@ -47,8 +49,8 @@ export const useCellPublicLink = ({ refreshLinkDataAfterUpdate = false, setStatusOnPublicLinkUrl = false, includeNodePublicLinkInCallbacks = false, + fireAndForgetInvoker, }: UseCellPublicLinkParams) => { - const {fireAndForgetInvoker} = useApplicationContext(); const [isEnabled, setIsEnabled] = useState(node?.publicLink?.alreadyShared === true); const [status, setStatus] = useState(node?.publicLink !== undefined ? 'success' : 'idle'); const [linkData, setLinkData] = useState(null); diff --git a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.test.tsx b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.test.tsx new file mode 100644 index 00000000000..d368d898a21 --- /dev/null +++ b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.test.tsx @@ -0,0 +1,83 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {ReactNode} from 'react'; + +import {render} from '@testing-library/react'; + +import {StyledApp, THEME_ID} from '@wireapp/react-ui-kit'; + +import {CellsRepository} from 'Repositories/cells/cellsRepository'; +import {createFireAndForgetInvokerForTest} from 'src/script/page/testSupport/rootContextTestSupport'; +import {CellNode, CellNodeType} from 'src/script/types/cellNode'; + +import {CellsShareModal} from './CellsShareModal'; + +import {useCellsStore} from '../../../common/useCellsStore/useCellsStore'; + +const withTheme = (component: ReactNode) => {component}; + +describe('CellsShareModal', () => { + const nodeId = 'node-id'; + + const createNode = (): CellNode => ({ + id: nodeId, + name: 'file.pdf', + path: '/file.pdf', + mimeType: 'application/pdf', + sizeMb: '1', + extension: 'pdf', + uploadedAtTimestamp: Date.now(), + owner: 'owner', + conversationName: 'Conversation', + tags: [], + presignedUrlExpiresAt: null, + user: null, + type: CellNodeType.FILE, + }); + + const createCellsRepository = (): CellsRepository => + ({ + createPublicLink: jest.fn(), + getPublicLink: jest.fn(), + deletePublicLink: jest.fn(), + updatePublicLink: jest.fn(), + }) as unknown as CellsRepository; + + beforeEach(() => { + useCellsStore.getState().clearAll(); + useCellsStore.getState().setNodes([createNode()]); + }); + + it('renders global share modal outside RootProvider when fireAndForgetInvoker is provided', () => { + expect(() => + render( + withTheme( + , + ), + ), + ).not.toThrow('RootContext has not been set'); + }); +}); diff --git a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx index b4a7d0d6a84..6825a6c20c4 100644 --- a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx +++ b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx @@ -62,7 +62,7 @@ interface ShareModalParams { fireAndForgetInvoker: FireAndForgetInvoker; } -type CellsShareModalProps = Omit & { +type CellsShareModalProps = ShareModalParams & { modalId: string; }; @@ -89,7 +89,15 @@ export const showShareModal = (properties: ShareModalParams): void => { text: t('cells.shareModal.primaryAction'), }, text: { - message: , + message: ( + + ), title: t('cells.shareModal.heading'), }, }, @@ -97,11 +105,12 @@ export const showShareModal = (properties: ShareModalParams): void => { ); }; -const CellsShareModal = (properties: CellsShareModalProps): ReactElement => { - const {type, uuid, cellsRepository, modalId} = properties; +export const CellsShareModal = (properties: CellsShareModalProps): ReactElement => { + const {type, uuid, cellsRepository, fireAndForgetInvoker, modalId} = properties; const {status, link, linkData, isEnabled, togglePublicLink, updatePublicLink} = useCellGlobalPublicLink({ uuid, cellsRepository, + fireAndForgetInvoker, }); const node = useCellsStore(state => state.nodes.find(cellNode => cellNode.id === uuid)); const { diff --git a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/useCellGlobalPublicLink.ts b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/useCellGlobalPublicLink.ts index fb910f5623c..9dbb7c0cccf 100644 --- a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/useCellGlobalPublicLink.ts +++ b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/useCellGlobalPublicLink.ts @@ -17,6 +17,8 @@ * */ +import type {FireAndForgetInvoker} from '@wireapp/core'; + import {CellsRepository} from 'Repositories/cells/cellsRepository'; import {useCellPublicLink} from 'src/script/components/Cells/common/useCellPublicLink/useCellPublicLink'; @@ -25,9 +27,14 @@ import {useCellsStore} from '../../../common/useCellsStore/useCellsStore'; interface UseCellGlobalPublicLinkParams { uuid: string; cellsRepository: CellsRepository; + fireAndForgetInvoker: FireAndForgetInvoker; } -export const useCellGlobalPublicLink = ({uuid, cellsRepository}: UseCellGlobalPublicLinkParams) => { +export const useCellGlobalPublicLink = ({ + uuid, + cellsRepository, + fireAndForgetInvoker, +}: UseCellGlobalPublicLinkParams) => { const {nodes, setPublicLink} = useCellsStore(); const node = nodes.find(n => n.id === uuid); return useCellPublicLink({ @@ -37,5 +44,6 @@ export const useCellGlobalPublicLink = ({uuid, cellsRepository}: UseCellGlobalPu setPublicLink: data => setPublicLink(uuid, data), setStatusOnPublicLinkUrl: true, includeNodePublicLinkInCallbacks: true, + fireAndForgetInvoker, }); }; diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.test.tsx b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.test.tsx new file mode 100644 index 00000000000..f795d5b1266 --- /dev/null +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.test.tsx @@ -0,0 +1,85 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {ReactNode} from 'react'; + +import {render} from '@testing-library/react'; + +import {StyledApp, THEME_ID} from '@wireapp/react-ui-kit'; + +import {CellsRepository} from 'Repositories/cells/cellsRepository'; +import {createFireAndForgetInvokerForTest} from 'src/script/page/testSupport/rootContextTestSupport'; +import {CellNode, CellNodeType} from 'src/script/types/cellNode'; + +import {CellShareModalContent} from './CellsNodeShareModal'; + +import {useCellsStore} from '../../../common/useCellsStore/useCellsStore'; + +const withTheme = (component: ReactNode) => {component}; + +describe('CellShareModalContent', () => { + const conversationId = 'conversation-id'; + const nodeId = 'node-id'; + + const createNode = (): CellNode => ({ + id: nodeId, + name: 'file.pdf', + path: '/file.pdf', + mimeType: 'application/pdf', + sizeMb: '1', + extension: 'pdf', + uploadedAtTimestamp: Date.now(), + owner: 'owner', + conversationName: 'Conversation', + tags: [], + presignedUrlExpiresAt: null, + user: null, + type: CellNodeType.FILE, + }); + + const createCellsRepository = (): CellsRepository => + ({ + createPublicLink: jest.fn(), + getPublicLink: jest.fn(), + deletePublicLink: jest.fn(), + updatePublicLink: jest.fn(), + }) as unknown as CellsRepository; + + beforeEach(() => { + useCellsStore.getState().clearAll({conversationId}); + useCellsStore.getState().setNodes({conversationId, nodes: [createNode()]}); + }); + + it('renders conversation share modal outside RootProvider when fireAndForgetInvoker is provided', () => { + expect(() => + render( + withTheme( + , + ), + ), + ).not.toThrow('RootContext has not been set'); + }); +}); diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx index de3fed1bf10..dee9b4ee0d6 100644 --- a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx @@ -96,6 +96,7 @@ export const showShareModal = ({ uuid={uuid} conversationId={conversationId} cellsRepository={cellsRepository} + fireAndForgetInvoker={fireAndForgetInvoker} modalId={modalId} /> ), @@ -106,17 +107,19 @@ export const showShareModal = ({ ); }; -const CellShareModalContent = ({ +export const CellShareModalContent = ({ type, uuid, conversationId, cellsRepository, + fireAndForgetInvoker, modalId, -}: Omit & {modalId: string}) => { +}: ShareModalParams & {modalId: string}) => { const {status, link, linkData, isEnabled, togglePublicLink, updatePublicLink} = useCellConversationPublicLink({ uuid, conversationId, cellsRepository, + fireAndForgetInvoker, }); const node = useCellsStore(state => state.nodesByConversation[conversationId]?.find(cellNode => cellNode.id === uuid), diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/useCellConversationPublicLink.ts b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/useCellConversationPublicLink.ts index 66f5790b5f1..f78fd5dd095 100644 --- a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/useCellConversationPublicLink.ts +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/useCellConversationPublicLink.ts @@ -17,6 +17,8 @@ * */ +import type {FireAndForgetInvoker} from '@wireapp/core'; + import {CellsRepository} from 'Repositories/cells/cellsRepository'; import {useCellPublicLink} from 'src/script/components/Cells/common/useCellPublicLink/useCellPublicLink'; @@ -26,12 +28,14 @@ interface UseCellConversationPublicLinkParams { uuid: string; conversationId: string; cellsRepository: CellsRepository; + fireAndForgetInvoker: FireAndForgetInvoker; } export const useCellConversationPublicLink = ({ uuid, conversationId, cellsRepository, + fireAndForgetInvoker, }: UseCellConversationPublicLinkParams) => { const {getNodes, setPublicLink} = useCellsStore(); const nodes = getNodes({conversationId}); @@ -42,5 +46,6 @@ export const useCellConversationPublicLink = ({ cellsRepository, setPublicLink: data => setPublicLink({conversationId, nodeId: uuid, data}), refreshLinkDataAfterUpdate: true, + fireAndForgetInvoker, }); }; From 1fb0539c9aaf8012adf02723711c80dd5c382e5e Mon Sep 17 00:00:00 2001 From: zskhan Date: Wed, 3 Jun 2026 13:07:03 +0200 Subject: [PATCH 2/2] chore: bump avs version to 10.3.40 --- apps/webapp/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/webapp/package.json b/apps/webapp/package.json index f48c1bbb9c7..892c763c249 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -35,7 +35,7 @@ "@sindresorhus/is": "4.6.0", "@tanstack/react-table": "8.21.3", "@tanstack/react-virtual": "3.13.24", - "@wireapp/avs": "10.3.28", + "@wireapp/avs": "10.3.40", "@wireapp/avs-debugger": "0.0.7", "@wireapp/config": "workspace:^", "@wireapp/core": "workspace:^", diff --git a/yarn.lock b/yarn.lock index 47e8b8a955f..d9dc74d7c1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8887,10 +8887,10 @@ __metadata: languageName: node linkType: hard -"@wireapp/avs@npm:10.3.28": - version: 10.3.28 - resolution: "@wireapp/avs@npm:10.3.28" - checksum: 10/8a06870e30aa41e464670c0922fa85ff1fc1cb15924cff6c627c882d8dea6e35411267e36745642379affff0ab54ba4380707146afac6d5160cab6d4cf829634 +"@wireapp/avs@npm:10.3.40": + version: 10.3.40 + resolution: "@wireapp/avs@npm:10.3.40" + checksum: 10/d8b3367d8be05467f155c9257365e6fce0c2ff51bd2567c6988946e60cf5def6bcf539bc257e808a6674d0b6c005d26386adc4aefab00a41356d26dc660ac79f languageName: node linkType: hard @@ -9251,7 +9251,7 @@ __metadata: "@types/webpack-bundle-analyzer": "npm:4.7.0" "@types/webpack-env": "npm:1.18.8" "@types/wicg-file-system-access": "npm:2023.10.7" - "@wireapp/avs": "npm:10.3.28" + "@wireapp/avs": "npm:10.3.40" "@wireapp/avs-debugger": "npm:0.0.7" "@wireapp/config": "workspace:^" "@wireapp/core": "workspace:^"