From ae161fec40fe8d924004516430b59ce26328f3b8 Mon Sep 17 00:00:00 2001 From: Nevyana Angelova Date: Mon, 31 Aug 2026 15:03:59 +0700 Subject: [PATCH 1/2] MM-70344: Confirm the copy in the Share modal and Space info --- webapp/i18n/en.json | 4 +- .../share_space_modal.test.tsx | 12 +++++ .../share_space_modal/share_space_modal.tsx | 25 ++++++--- .../space_info/space_info_menu.test.tsx | 54 +++++++++++++++++++ .../components/space_info/space_info_menu.tsx | 20 ++++--- webapp/src/hooks/copy_text.test.ts | 44 +++++++++++++++ webapp/src/hooks/copy_text.ts | 39 ++++++++++++++ 7 files changed, 180 insertions(+), 18 deletions(-) create mode 100644 webapp/src/components/space_info/space_info_menu.test.tsx create mode 100644 webapp/src/hooks/copy_text.test.ts create mode 100644 webapp/src/hooks/copy_text.ts diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index c3932673..3baa5e77 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -145,6 +145,7 @@ "docs.share.access.canView": "Can View", "docs.share.copyLink": "Copy link", "docs.share.handle": "@{username}", + "docs.share.linkCopied": "Copied", "docs.share.noResults": "No people found", "docs.share.remove": "Remove {name}", "docs.share.search": "Add people or groups", @@ -153,8 +154,6 @@ "docs.share.visibility.disabledReason": "Public spaces are coming soon", "docs.share.visibility.private": "Private", "docs.share.visibility.privateHint": "Only invited members", - "docs.share.visibility.public": "Public", - "docs.share.visibility.publicHint": "Anyone in Mattermost", "docs.sidebar.add.browse": "Browse spaces", "docs.sidebar.add.create": "Create a space", "docs.sidebar.add.menu": "Add or browse spaces", @@ -221,6 +220,7 @@ "docs.spaceInfo.editDescription": "Edit description", "docs.spaceInfo.members": "Members", "docs.spaceInfo.menu.copyLink": "Copy link", + "docs.spaceInfo.menu.linkCopied": "Copied", "docs.spaceInfo.menu.members": "Members", "docs.spaceInfo.menu.settings": "Space settings", "docs.spaceInfo.menu.title": "Space info actions", diff --git a/webapp/src/components/share_space_modal/share_space_modal.test.tsx b/webapp/src/components/share_space_modal/share_space_modal.test.tsx index 9c89fd9e..581da294 100644 --- a/webapp/src/components/share_space_modal/share_space_modal.test.tsx +++ b/webapp/src/components/share_space_modal/share_space_modal.test.tsx @@ -3,6 +3,7 @@ import {act, fireEvent, screen, waitFor} from '@testing-library/react'; import React from 'react'; +import {copyToClipboard} from 'utils/clipboard'; import {makeSpace} from 'store/test_fixtures'; @@ -41,6 +42,8 @@ jest.mock('hooks/navigation', () => ({ useDocsNavigation: () => ({paths: {space: (id: string) => `/team/spaces/${id}`}}), })); +jest.mock('utils/clipboard', () => ({copyToClipboard: jest.fn()})); + // AddMembersField renders the real people picker, which pulls in mattermost-redux's // user search actions (published ESM that jest doesn't transform). Stub at the hook // boundary, as people_picker.test.tsx does. @@ -145,4 +148,13 @@ describe('ShareSpaceModal', () => { await waitFor(() => expect(mockLeave).toHaveBeenCalled()); expect(onClose).not.toHaveBeenCalled(); }); + + it('confirms the copy on the button itself', async () => { + renderModal(); + + fireEvent.click(screen.getByRole('button', {name: 'Copy link'})); + + expect(await screen.findByRole('button', {name: 'Copied'})).toBeInTheDocument(); + expect(copyToClipboard).toHaveBeenCalledWith('/team/spaces/space-1'); + }); }); diff --git a/webapp/src/components/share_space_modal/share_space_modal.tsx b/webapp/src/components/share_space_modal/share_space_modal.tsx index fc3fb857..4bfd07e5 100644 --- a/webapp/src/components/share_space_modal/share_space_modal.tsx +++ b/webapp/src/components/share_space_modal/share_space_modal.tsx @@ -1,14 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {useCopyText} from 'hooks/copy_text'; import {useSpaceMemberProfiles} from 'hooks/members'; import {useDocsNavigation} from 'hooks/navigation'; import {useCanManageSpaceMembers} from 'hooks/permissions'; import {useManageSpaceMembers} from 'hooks/space_members'; import React, {useMemo} from 'react'; import {FormattedMessage, useIntl} from 'react-intl'; -import {copyToClipboard} from 'utils/clipboard'; +import CheckIcon from '@mattermost/compass-icons/components/check'; import ChevronDownIcon from '@mattermost/compass-icons/components/chevron-down'; import ContentCopyIcon from '@mattermost/compass-icons/components/content-copy'; import LockOutlineIcon from '@mattermost/compass-icons/components/lock-outline'; @@ -48,7 +49,7 @@ const ShareSpaceModal = ({space, onClose}: Props) => { disabled: busy, }; - const copyLink = () => copyToClipboard(absolutePaths.space(space.id)); + const copyLink = useCopyText(absolutePaths.space(space.id)); const title = ( { - - + {copyLink.copied ? : } + {copyLink.copied ? ( + + ) : ( + + )} ); diff --git a/webapp/src/components/space_info/space_info_menu.test.tsx b/webapp/src/components/space_info/space_info_menu.test.tsx new file mode 100644 index 00000000..8901c2de --- /dev/null +++ b/webapp/src/components/space_info/space_info_menu.test.tsx @@ -0,0 +1,54 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {fireEvent, screen} from '@testing-library/react'; +import React from 'react'; +import {copyToClipboard} from 'utils/clipboard'; + +import {makeSpace} from 'store/test_fixtures'; + +import SpaceInfoMenu from './space_info_menu'; + +import {renderWithContext} from '../../../tests/react_testing_utils'; + +jest.mock('hooks/navigation', () => ({ + useDocsNavigation: () => ({paths: {space: (id: string) => `/team/spaces/${id}`}}), +})); + +jest.mock('hooks/permissions', () => ({ + useCanManageSpaceMembers: () => true, +})); + +jest.mock('utils/clipboard', () => ({copyToClipboard: jest.fn()})); + +const space = makeSpace('space-1', 'Engineering'); + +const renderMenu = () => renderWithContext( + , +); + +describe('SpaceInfoMenu', () => { + beforeEach(() => jest.clearAllMocks()); + + it('copies the space link', () => { + renderMenu(); + + fireEvent.click(screen.getByRole('button', {name: 'Copy link'})); + + expect(copyToClipboard).toHaveBeenCalledWith('/team/spaces/space-1'); + }); + + // MM-70344: the click gave no sign that anything happened. + it('confirms the copy on the item itself', () => { + renderMenu(); + + fireEvent.click(screen.getByRole('button', {name: 'Copy link'})); + + expect(screen.getByRole('button', {name: 'Copied'})).toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Copy link'})).not.toBeInTheDocument(); + }); +}); diff --git a/webapp/src/components/space_info/space_info_menu.tsx b/webapp/src/components/space_info/space_info_menu.tsx index f96e4bf6..6a93b7ab 100644 --- a/webapp/src/components/space_info/space_info_menu.tsx +++ b/webapp/src/components/space_info/space_info_menu.tsx @@ -1,13 +1,14 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {useCopyText} from 'hooks/copy_text'; import {useDocsNavigation} from 'hooks/navigation'; import {useCanManageSpaceMembers} from 'hooks/permissions'; import React, {useCallback} from 'react'; import {useIntl} from 'react-intl'; -import {copyToClipboard} from 'utils/clipboard'; import AccountMultipleOutlineIcon from '@mattermost/compass-icons/components/account-multiple-outline'; +import CheckIcon from '@mattermost/compass-icons/components/check'; import ChevronRightIcon from '@mattermost/compass-icons/components/chevron-right'; import CogOutlineIcon from '@mattermost/compass-icons/components/cog-outline'; import LinkVariantIcon from '@mattermost/compass-icons/components/link-variant'; @@ -29,14 +30,18 @@ type ItemProps = { /** Marks the item as drilling into a sub-panel, adding a chevron. */ opensPanel?: boolean; + + /** Reads out `text` when it changes, for items that confirm in place. */ + announce?: boolean; onClick: () => void; }; -const SpaceInfoMenuItem = ({icon, text, badge, opensPanel, onClick}: ItemProps) => ( +const SpaceInfoMenuItem = ({icon, text, badge, opensPanel, announce, onClick}: ItemProps) => (