Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -41,10 +37,7 @@ describe('useCellPublicLink', () => {
let mockCellsRepository: jest.Mocked<CellsRepository>;
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> = {}): CellNode => ({
id: 'test-uuid',
Expand All @@ -67,31 +60,35 @@ 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,
cellsRepository: mockCellsRepository,
setPublicLink: mockSetPublicLink,
refreshLinkDataAfterUpdate,
setStatusOnPublicLinkUrl,
fireAndForgetInvoker,
}),
{initialProps, wrapper: rootProviderWrapper},
{initialProps},
);

const rerenderWith = (props: Partial<typeof initialProps>) =>
hook.rerender({
node: props.node ?? initialProps.node,
refreshLinkDataAfterUpdate: props.refreshLinkDataAfterUpdate ?? initialProps.refreshLinkDataAfterUpdate,
setStatusOnPublicLinkUrl: props.setStatusOnPublicLinkUrl ?? initialProps.setStatusOnPublicLinkUrl,
fireAndForgetInvoker: props.fireAndForgetInvoker ?? initialProps.fireAndForgetInvoker,
});

return {...hook, rerenderWith};
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -37,6 +38,7 @@ interface UseCellPublicLinkParams {
refreshLinkDataAfterUpdate?: boolean;
setStatusOnPublicLinkUrl?: boolean;
includeNodePublicLinkInCallbacks?: boolean;
fireAndForgetInvoker: FireAndForgetInvoker;
}

export const useCellPublicLink = ({
Expand All @@ -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<PublicLinkStatus>(node?.publicLink !== undefined ? 'success' : 'idle');
const [linkData, setLinkData] = useState<RestShareLink | null>(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -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) => <StyledApp themeId={THEME_ID.DEFAULT}>{component}</StyledApp>;

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(
<CellsShareModal
type="file"
uuid={nodeId}
cellsRepository={createCellsRepository()}
fireAndForgetInvoker={createFireAndForgetInvokerForTest()}
modalId="modal-id"
/>,
),
),
).not.toThrow('RootContext has not been set');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ interface ShareModalParams {
fireAndForgetInvoker: FireAndForgetInvoker;
}

type CellsShareModalProps = Omit<ShareModalParams, 'fireAndForgetInvoker'> & {
type CellsShareModalProps = ShareModalParams & {
modalId: string;
};

Expand All @@ -89,19 +89,28 @@ export const showShareModal = (properties: ShareModalParams): void => {
text: t('cells.shareModal.primaryAction'),
},
text: {
message: <CellsShareModal type={type} uuid={uuid} cellsRepository={cellsRepository} modalId={modalId} />,
message: (
<CellsShareModal
type={type}
uuid={uuid}
cellsRepository={cellsRepository}
fireAndForgetInvoker={fireAndForgetInvoker}
modalId={modalId}
/>
),
title: t('cells.shareModal.heading'),
},
},
modalId,
);
};

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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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({
Expand All @@ -37,5 +44,6 @@ export const useCellGlobalPublicLink = ({uuid, cellsRepository}: UseCellGlobalPu
setPublicLink: data => setPublicLink(uuid, data),
setStatusOnPublicLinkUrl: true,
includeNodePublicLinkInCallbacks: true,
fireAndForgetInvoker,
});
};
Original file line number Diff line number Diff line change
@@ -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) => <StyledApp themeId={THEME_ID.DEFAULT}>{component}</StyledApp>;

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(
<CellShareModalContent
type="file"
uuid={nodeId}
conversationId={conversationId}
cellsRepository={createCellsRepository()}
fireAndForgetInvoker={createFireAndForgetInvokerForTest()}
modalId="modal-id"
/>,
),
),
).not.toThrow('RootContext has not been set');
});
});
Loading
Loading