diff --git a/e2e-tests/cypress/tests/integration/channels/archived_channel/join_archived_channel_spec.ts b/e2e-tests/cypress/tests/integration/channels/archived_channel/join_archived_channel_spec.ts index aa367eaac35..5a841df5a18 100644 --- a/e2e-tests/cypress/tests/integration/channels/archived_channel/join_archived_channel_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/archived_channel/join_archived_channel_spec.ts @@ -103,7 +103,7 @@ describe('Archived channels', () => { function verifyViewingArchivedChannel(channel) { // * Verify that we've switched to the correct channel and that the header contains the archived icon cy.get('#channelHeaderTitle').should('contain', channel.display_name); - cy.get('#channelHeaderInfo .icon__archive').should('be.visible'); + cy.findByTestId('channel-header-archive-icon').should('be.visible'); // * Verify that the channel is visible in the sidebar with the archived icon cy.get(`#sidebarItem_${channel.name}`).should('be.visible'). diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/integrations/incoming_webhook_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/integrations/incoming_webhook_spec.ts index 4d079673dad..2438b67c9f3 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/integrations/incoming_webhook_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/integrations/incoming_webhook_spec.ts @@ -65,8 +65,19 @@ describe('Incoming webhook', () => { cy.visit(`/${testTeam.name}/channels/${testChannel.name}`); + // # Post webhook and wait for attachment to render cy.postIncomingWebhook({url: incomingWebhook.url, data: payload}); + // # Verify the post appears in the channel with attachment + cy.getLastPost().within(() => { + cy.get('.attachment__body').should('be.visible').should('contain', 'Findme.'); + }); + + // # Explicitly wait to give Elasticsearch time to index before searching + // Using a longer wait time since Elasticsearch indexing can be slow in test environments + cy.wait(TIMEOUTS.THREE_SEC); + + // # Search for text in the attachment cy.uiGetSearchContainer().click(); cy.uiGetSearchBox(). wait(TIMEOUTS.HALF_SEC). diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/archived_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/archived_channels_spec.js index 33862500693..4e2645c3a21 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/archived_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/archived_channels_spec.js @@ -14,17 +14,24 @@ import * as TIMEOUTS from '../../../../fixtures/timeouts'; describe('Archived channels', () => { let testChannel; + let testPrivateChannel; before(() => { cy.apiRequireLicense(); cy.apiInitSetup({ channelPrefix: {name: '000-archive', displayName: '000 Archive Test'}, - }).then(({channel}) => { + }).then(({channel, team}) => { testChannel = channel; - // # Archive the channel + // # Archive the public channel cy.apiDeleteChannel(testChannel.id); + + // # Create and archive a private channel with a prefix to ensure proper sorting + cy.apiCreateChannel(team.id, '000-private-archive', '000 Private Archive Test', 'P').then(({channel: privateChannel}) => { + testPrivateChannel = privateChannel; + cy.apiDeleteChannel(privateChannel.id); + }); }); }); @@ -83,4 +90,26 @@ describe('Archived channels', () => { expect(channel.delete_at).to.eq(0); }); }); + + it('display archive icon for public archived channels in channel list', () => { + // # Go to the channels list view + cy.visit('/admin_console/user_management/channels'); + + // * Verify the archived public channel is visible + cy.findByText(testChannel.display_name, {timeout: TIMEOUTS.ONE_MIN}).should('be.visible'); + + // * Verify the archive icon is displayed + cy.findByTestId(`${testChannel.name}-archive-icon`).should('be.visible'); + }); + + it('display archive-lock icon for private archived channels in channel list', () => { + // # Go to the channels list view + cy.visit('/admin_console/user_management/channels'); + + // * Verify the archived private channel is visible + cy.findByText(testPrivateChannel.display_name, {timeout: TIMEOUTS.ONE_MIN}).should('be.visible'); + + // * Verify the archive icon is displayed for private channel + cy.findByTestId(`${testPrivateChannel.name}-archive-icon`).should('be.visible'); + }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/long_draft_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/long_draft_spec.js index 544fccad1ba..30f0ecf2e29 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/long_draft_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/long_draft_spec.js @@ -81,6 +81,13 @@ describe('Messaging', () => { }); function writeLinesToPostTextBox(lines) { + let previousHeight; + + // Get the initial previous height from the alias + cy.get('@previousHeight').then((height) => { + previousHeight = height; + }); + Cypress._.forEach(lines, (line, i) => { // # Add the text cy.uiGetPostTextBox().type(line, {delay: TIMEOUTS.ONE_HUNDRED_MILLIS}).wait(TIMEOUTS.HALF_SEC); @@ -91,11 +98,14 @@ function writeLinesToPostTextBox(lines) { // * Verify new height cy.uiGetPostTextBox().invoke('height').then((height) => { + const currentHeight = parseInt(height, 10); + // * Verify previous height should be lower than the current height - cy.get('@previousHeight').should('be.lessThan', parseInt(height, 10)); + expect(previousHeight).to.be.lessThan(currentHeight); // # Store the current height as the previous height for the next loop - cy.wrap(parseInt(height, 10)).as('previousHeight'); + previousHeight = currentHeight; + cy.wrap(currentHeight).as('previousHeight'); }); } }); diff --git a/e2e-tests/playwright/specs/visual/channels/archived_channel_icons.spec.ts b/e2e-tests/playwright/specs/visual/channels/archived_channel_icons.spec.ts new file mode 100644 index 00000000000..13e17eff948 --- /dev/null +++ b/e2e-tests/playwright/specs/visual/channels/archived_channel_icons.spec.ts @@ -0,0 +1,161 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, test} from '@mattermost/playwright-lib'; + +/** + * @objective Verify archived channel icons display correctly for public and private channels in various UI contexts + */ +test( + 'displays archive icons for public and private channels in sidebar', + {tag: ['@visual', '@archived_channels', '@snapshots']}, + async ({pw, browserName, viewport}, testInfo) => { + // # Initialize setup and create test channels + const {team, user, adminClient} = await pw.initSetup(); + + // # Create public and private channels + const publicChannel = await adminClient.createChannel( + pw.random.channel({ + teamId: team.id, + name: 'public-to-archive', + displayName: 'Public Archive Test', + type: 'O', + }), + ); + + const privateChannel = await adminClient.createChannel( + pw.random.channel({ + teamId: team.id, + name: 'private-to-archive', + displayName: 'Private Archive Test', + type: 'P', + }), + ); + + // # Archive both channels + await adminClient.deleteChannel(publicChannel.id); + await adminClient.deleteChannel(privateChannel.id); + + // # Log in user + const {page, channelsPage} = await pw.testBrowser.login(user); + + // # Visit town square to ensure we're in a stable state + await channelsPage.goto(team.name, 'town-square'); + await channelsPage.toBeVisible(); + + // # Open browse channels modal to show archived channels + await page.keyboard.press('Control+K'); + await page.waitForTimeout(500); + + // # Type to search for archived channels + await page.keyboard.type('archive'); + await page.waitForTimeout(500); + + // # Hide dynamic content + await pw.hideDynamicChannelsContent(page); + + // * Verify channel switcher shows both archived channels with correct icons + const testArgs = {page, browserName, viewport}; + await pw.matchSnapshot(testInfo, testArgs); + }, +); + +/** + * @objective Verify archived channel icons display correctly in admin console channel list + */ +test( + 'displays archive icons in admin console channel list', + {tag: ['@visual', '@archived_channels', '@admin_console', '@snapshots']}, + async ({pw, browserName, viewport}, testInfo) => { + // # Initialize setup with admin user + const {team, adminUser, adminClient} = await pw.initSetup(); + + // # Create public and private channels + const publicChannel = await adminClient.createChannel( + pw.random.channel({ + teamId: team.id, + name: 'admin-public-archive', + displayName: 'Admin Public Archive', + type: 'O', + }), + ); + + const privateChannel = await adminClient.createChannel( + pw.random.channel({ + teamId: team.id, + name: 'admin-private-archive', + displayName: 'Admin Private Archive', + type: 'P', + }), + ); + + // # Archive both channels + await adminClient.deleteChannel(publicChannel.id); + await adminClient.deleteChannel(privateChannel.id); + + // # Log in as admin + const {page} = await pw.testBrowser.login(adminUser); + + // # Navigate to admin console channels list + await page.goto('/admin_console/user_management/channels'); + await page.waitForTimeout(1000); + + // # Wait for channel list to load + await expect(page.locator('.DataGrid')).toBeVisible({timeout: 10000}); + + // # Search for our test channels to bring them into view + await page.fill('[data-testid="searchInput"]', 'Admin'); + await page.waitForTimeout(500); + + // * Verify both archived channels are visible with correct icons + const testArgs = {page, browserName, viewport}; + await pw.matchSnapshot(testInfo, testArgs); + }, +); + +/** + * @objective Verify archived private channel icon displays in channel header when viewing archived channel + */ +test( + 'displays archive icon in channel header for archived private channel', + {tag: ['@visual', '@archived_channels', '@channel_header', '@snapshots']}, + async ({pw, browserName, viewport}, testInfo) => { + // # Initialize setup + const {team, adminUser, adminClient} = await pw.initSetup(); + + // # Create a private channel + const privateChannel = await adminClient.createChannel( + pw.random.channel({ + teamId: team.id, + name: 'private-header-test', + displayName: 'Private Header Test', + type: 'P', + }), + ); + + // # Archive the channel + await adminClient.deleteChannel(privateChannel.id); + + const {page, channelsPage} = await pw.testBrowser.login(adminUser); + + // # Visit the archived channel directly + await channelsPage.goto(team.name, privateChannel.name); + + // # Wait for channel header to load (archived channels don't have post-create) + await expect(page.locator('.channel-header')).toBeVisible(); + + // # Verify archived channel message is visible + await expect(page.locator('#channelArchivedMessage')).toBeVisible(); + + // # Hide dynamic content + await pw.hideDynamicChannelsContent(page); + + // # Focus on channel header area for snapshot + const headerElement = page.locator('.channel-header'); + await expect(headerElement).toBeVisible(); + + // * Verify channel header shows archive-lock icon for private archived channel + const testArgs = {page, browserName, viewport}; + await pw.matchSnapshot(testInfo, testArgs); + }, +); diff --git a/server/channels/api4/post_test.go b/server/channels/api4/post_test.go index 8cb265b8fff..afb2acac375 100644 --- a/server/channels/api4/post_test.go +++ b/server/channels/api4/post_test.go @@ -274,6 +274,8 @@ func TestCreatePost(t *testing.T) { }) t.Run("not logged in", func(t *testing.T) { + defer th.LoginBasic(t) + resp, err := client.Logout(context.Background()) require.NoError(t, err) CheckOKStatus(t, resp) diff --git a/webapp/channels/src/components/admin_console/access_control/modals/job_details/searchable_sync_job_channel_list.tsx b/webapp/channels/src/components/admin_console/access_control/modals/job_details/searchable_sync_job_channel_list.tsx index 3469382aecb..1dd95b49e62 100644 --- a/webapp/channels/src/components/admin_console/access_control/modals/job_details/searchable_sync_job_channel_list.tsx +++ b/webapp/channels/src/components/admin_console/access_control/modals/job_details/searchable_sync_job_channel_list.tsx @@ -4,18 +4,15 @@ import React, {useState, useRef, useEffect} from 'react'; import {FormattedMessage, defineMessages, injectIntl, type WrappedComponentProps} from 'react-intl'; -import {ArchiveOutlineIcon, GlobeIcon, LockOutlineIcon} from '@mattermost/compass-icons/components'; import type {Channel} from '@mattermost/types/channels'; import type {Team} from '@mattermost/types/teams'; import type {IDMappedObjects} from '@mattermost/types/utilities'; -import {isPrivateChannel} from 'mattermost-redux/utils/channel_utils'; - import MagnifyingGlassSVG from 'components/common/svg_images_components/magnifying_glass_svg'; import LoadingScreen from 'components/loading_screen'; import QuickInput from 'components/quick_input'; -import {isArchivedChannel} from 'utils/channel_utils'; +import {getChannelIconComponent} from 'utils/channel_utils'; import Constants from 'utils/constants'; import {isKeyPressed} from 'utils/keyboard'; @@ -82,15 +79,9 @@ const SearchableSyncJobChannelList = (props: Props) => { const createChannelRow = (channel: Channel) => { const ariaLabel = `${channel.display_name}, ${channel.purpose}`.toLowerCase(); - let channelTypeIcon; - if (isArchivedChannel(channel)) { - channelTypeIcon = ; - } else if (isPrivateChannel(channel)) { - channelTypeIcon = ; - } else { - channelTypeIcon = ; - } + const ChannelIcon = getChannelIconComponent(channel); + const channelTypeIcon = ; const team = props.teams[channel.team_id]; diff --git a/webapp/channels/src/components/admin_console/access_control/policy_details/channel_list/channel_list.tsx b/webapp/channels/src/components/admin_console/access_control/policy_details/channel_list/channel_list.tsx index 044c8cae2e6..955f494c5ea 100644 --- a/webapp/channels/src/components/admin_console/access_control/policy_details/channel_list/channel_list.tsx +++ b/webapp/channels/src/components/admin_console/access_control/policy_details/channel_list/channel_list.tsx @@ -15,12 +15,9 @@ import DataGrid from 'components/admin_console/data_grid/data_grid'; import type {Column, Row} from 'components/admin_console/data_grid/data_grid'; import type {FilterOptions} from 'components/admin_console/filter/filter'; import TeamFilterDropdown from 'components/admin_console/filter/team_filter_dropdown'; -import ArchiveIcon from 'components/widgets/icons/archive_icon'; -import GlobeIcon from 'components/widgets/icons/globe_icon'; -import LockIcon from 'components/widgets/icons/lock_icon'; import WithTooltip from 'components/with_tooltip'; -import {isArchivedChannel} from 'utils/channel_utils'; +import {getChannelIconComponent} from 'utils/channel_utils'; import {Constants} from 'utils/constants'; import './channel_list.scss'; @@ -421,19 +418,13 @@ class ChannelList extends React.PureComponent { ].slice(startCount - 1, endCount); return channelsToDisplay.map((channel) => { - // Determine which icon to display based on channel type - let iconToDisplay = ; - if (channel.type === Constants.PRIVATE_CHANNEL) { - iconToDisplay = ; - } - if (isArchivedChannel(channel)) { - iconToDisplay = ( - - ); - } + const ChannelIconComponent = getChannelIconComponent(channel); + const iconToDisplay = ( + + ); // Determine the button text and action based on the channel state const buttonText = ( diff --git a/webapp/channels/src/components/admin_console/data_retention_settings/channel_list/__snapshots__/channel_list.test.tsx.snap b/webapp/channels/src/components/admin_console/data_retention_settings/channel_list/__snapshots__/channel_list.test.tsx.snap index 4cb94e1db87..99ad73b959a 100644 --- a/webapp/channels/src/components/admin_console/data_retention_settings/channel_list/__snapshots__/channel_list.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/data_retention_settings/channel_list/__snapshots__/channel_list.test.tsx.snap @@ -112,6 +112,7 @@ exports[`components/admin_console/data_retention_settings/channel_list should ma >
{ } return channelsToDisplay.map((channel) => { - let iconToDisplay = ; - - if (channel.type === Constants.PRIVATE_CHANNEL) { - iconToDisplay = ; - } - if (isArchivedChannel(channel)) { - iconToDisplay = ( - - ); - } + const ChannelIconComponent = getChannelIconComponent(channel); + const iconToDisplay = ( + + ); return { cells: { id: channel.id, diff --git a/webapp/channels/src/components/admin_console/secure_connections/modals/shared_channels_add_modal.tsx b/webapp/channels/src/components/admin_console/secure_connections/modals/shared_channels_add_modal.tsx index 503727db1cc..c525530355f 100644 --- a/webapp/channels/src/components/admin_console/secure_connections/modals/shared_channels_add_modal.tsx +++ b/webapp/channels/src/components/admin_console/secure_connections/modals/shared_channels_add_modal.tsx @@ -7,7 +7,6 @@ import {FormattedMessage, useIntl} from 'react-intl'; import {useDispatch, useSelector} from 'react-redux'; import styled from 'styled-components'; -import {ArchiveOutlineIcon, GlobeIcon, LockIcon} from '@mattermost/compass-icons/components'; import type IconProps from '@mattermost/compass-icons/components/props'; import {GenericModal} from '@mattermost/components'; import type {Channel, ChannelWithTeamData} from '@mattermost/types/channels'; @@ -19,7 +18,7 @@ import {getChannel} from 'mattermost-redux/selectors/entities/channels'; import SectionNotice from 'components/section_notice'; import ChannelsInput from 'components/widgets/inputs/channels_input'; -import {isArchivedChannel} from 'utils/channel_utils'; +import {getChannelIconComponent} from 'utils/channel_utils'; import Constants from 'utils/constants'; import type {GlobalState} from 'types/store'; @@ -269,15 +268,7 @@ const ChannelLabel = ({channel, bold}: {channel: Channel; bold?: boolean}) => { }; const ChannelIcon = ({channel, size = 16, ...otherProps}: {channel: Channel} & IconProps) => { - let Icon = GlobeIcon; - - if (channel?.type === Constants.PRIVATE_CHANNEL) { - Icon = LockIcon; - } - - if (isArchivedChannel(channel)) { - Icon = ArchiveOutlineIcon; - } + const Icon = getChannelIconComponent(channel); return ( { const channel = useSelector((state: GlobalState) => getChannel(state, channelId)); - let icon = ; - - if (channel?.type === Constants.PRIVATE_CHANNEL) { - icon = ; - } - - if (isArchivedChannel(channel)) { - icon = ; - } + const IconComponent = getChannelIconComponent(channel); return ( - {icon} + ); }; diff --git a/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/__snapshots__/channel_list.test.tsx.snap b/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/__snapshots__/channel_list.test.tsx.snap index 00b799854ff..8f0c4f10f70 100644 --- a/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/__snapshots__/channel_list.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/__snapshots__/channel_list.test.tsx.snap @@ -184,6 +184,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelList should match sn >
`; + +exports[`admin_console/team_channel_settings/channel/ChannelList should render correct icon for archived private channel 1`] = ` +
+ , + "width": 4, + }, + Object { + "field": "team", + "fixed": true, + "name": , + "width": 1.5, + }, + Object { + "field": "management", + "fixed": true, + "name": , + }, + Object { + "field": "edit", + "fixed": true, + "name": "", + "textAlign": "right", + }, + ] + } + endCount={1} + filterProps={ + Object { + "keys": Array [ + "teams", + "channels", + "management", + ], + "onFilter": [Function], + "options": Object { + "channels": Object { + "keys": Array [ + "public", + "private", + "deleted", + ], + "name": "Channels", + "values": Object { + "deleted": Object { + "name": , + "value": false, + }, + "private": Object { + "name": , + "value": false, + }, + "public": Object { + "name": , + "value": false, + }, + }, + }, + "management": Object { + "keys": Array [ + "group_constrained", + "exclude_group_constrained", + "access_control_policy_enforced", + ], + "name": "Management", + "values": Object { + "access_control_policy_enforced": Object { + "name": , + "value": false, + }, + "exclude_group_constrained": Object { + "name": , + "value": false, + }, + "group_constrained": Object { + "name": , + "value": false, + }, + }, + }, + "teams": Object { + "keys": Array [ + "team_ids", + ], + "name": "Teams", + "type": Object { + "$$typeof": Symbol(react.memo), + "WrappedComponent": [Function], + "compare": null, + "type": [Function], + }, + "values": Object { + "team_ids": Object { + "name": , + "value": Array [], + }, + }, + }, + }, + } + } + loading={false} + nextPage={[Function]} + onSearch={[Function]} + page={0} + placeholderEmpty={ + + } + previousPage={[Function]} + rows={ + Array [ + Object { + "cells": Object { + "edit": + + + + , + "id": "archived-private", + "management": + + + + , + "name": + + + Archived Private + + , + "team": + teamDisplayName + , + }, + "onClick": [Function], + }, + ] + } + rowsContainerStyles={ + Object { + "minHeight": "40px", + } + } + startCount={1} + term="" + total={1} + /> +
+`; + +exports[`admin_console/team_channel_settings/channel/ChannelList should render correct icon for archived public channel 1`] = ` +
+ , + "width": 4, + }, + Object { + "field": "team", + "fixed": true, + "name": , + "width": 1.5, + }, + Object { + "field": "management", + "fixed": true, + "name": , + }, + Object { + "field": "edit", + "fixed": true, + "name": "", + "textAlign": "right", + }, + ] + } + endCount={1} + filterProps={ + Object { + "keys": Array [ + "teams", + "channels", + "management", + ], + "onFilter": [Function], + "options": Object { + "channels": Object { + "keys": Array [ + "public", + "private", + "deleted", + ], + "name": "Channels", + "values": Object { + "deleted": Object { + "name": , + "value": false, + }, + "private": Object { + "name": , + "value": false, + }, + "public": Object { + "name": , + "value": false, + }, + }, + }, + "management": Object { + "keys": Array [ + "group_constrained", + "exclude_group_constrained", + "access_control_policy_enforced", + ], + "name": "Management", + "values": Object { + "access_control_policy_enforced": Object { + "name": , + "value": false, + }, + "exclude_group_constrained": Object { + "name": , + "value": false, + }, + "group_constrained": Object { + "name": , + "value": false, + }, + }, + }, + "teams": Object { + "keys": Array [ + "team_ids", + ], + "name": "Teams", + "type": Object { + "$$typeof": Symbol(react.memo), + "WrappedComponent": [Function], + "compare": null, + "type": [Function], + }, + "values": Object { + "team_ids": Object { + "name": , + "value": Array [], + }, + }, + }, + }, + } + } + loading={false} + nextPage={[Function]} + onSearch={[Function]} + page={0} + placeholderEmpty={ + + } + previousPage={[Function]} + rows={ + Array [ + Object { + "cells": Object { + "edit": + + + + , + "id": "archived-public", + "management": + + + + , + "name": + + + Archived Public + + , + "team": + teamDisplayName + , + }, + "onClick": [Function], + }, + ] + } + rowsContainerStyles={ + Object { + "minHeight": "40px", + } + } + startCount={1} + term="" + total={1} + /> +
+`; diff --git a/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/channel_list.test.tsx b/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/channel_list.test.tsx index 8a7752ca56c..66bead1e418 100644 --- a/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/channel_list.test.tsx +++ b/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/channel_list.test.tsx @@ -4,7 +4,9 @@ import {shallow} from 'enzyme'; import React from 'react'; -import type {Channel} from '@mattermost/types/channels'; +import type {Channel, ChannelWithTeamData} from '@mattermost/types/channels'; + +import {General} from 'mattermost-redux/constants'; import {TestHelper} from 'utils/test_helper'; @@ -92,4 +94,60 @@ describe('admin_console/team_channel_settings/channel/ChannelList', () => { wrapper.setState({loading: false}); expect(wrapper).toMatchSnapshot(); }); + + test('should render correct icon for archived public channel', () => { + const archivedPublicChannel: ChannelWithTeamData[] = [{ + ...channel, + id: 'archived-public', + type: General.OPEN_CHANNEL, + display_name: 'Archived Public', + delete_at: 1234567890, + team_display_name: 'teamDisplayName', + team_name: 'teamName', + team_update_at: 1, + }]; + + const actions = { + getData: jest.fn().mockResolvedValue(archivedPublicChannel), + searchAllChannels: jest.fn().mockResolvedValue(archivedPublicChannel), + }; + + const wrapper = shallow( + ); + + wrapper.setState({loading: false}); + expect(wrapper).toMatchSnapshot(); + }); + + test('should render correct icon for archived private channel', () => { + const archivedPrivateChannel: ChannelWithTeamData[] = [{ + ...channel, + id: 'archived-private', + type: General.PRIVATE_CHANNEL, + display_name: 'Archived Private', + delete_at: 1234567890, + team_display_name: 'teamDisplayName', + team_name: 'teamName', + team_update_at: 1, + }]; + + const actions = { + getData: jest.fn().mockResolvedValue(archivedPrivateChannel), + searchAllChannels: jest.fn().mockResolvedValue(archivedPrivateChannel), + }; + + const wrapper = shallow( + ); + + wrapper.setState({loading: false}); + expect(wrapper).toMatchSnapshot(); + }); }); diff --git a/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/channel_list.tsx b/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/channel_list.tsx index 3d690a9ba3f..1bbff658894 100644 --- a/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/channel_list.tsx +++ b/webapp/channels/src/components/admin_console/team_channel_settings/channel/list/channel_list.tsx @@ -16,13 +16,9 @@ import type {FilterOptions} from 'components/admin_console/filter/filter'; import TeamFilterDropdown from 'components/admin_console/filter/team_filter_dropdown'; import {PAGE_SIZE} from 'components/admin_console/team_channel_settings/abstract_list'; import SharedChannelIndicator from 'components/shared_channel_indicator'; -import ArchiveIcon from 'components/widgets/icons/archive_icon'; -import GlobeIcon from 'components/widgets/icons/globe_icon'; -import LockIcon from 'components/widgets/icons/lock_icon'; import {getHistory} from 'utils/browser_history'; -import {isArchivedChannel} from 'utils/channel_utils'; -import {Constants} from 'utils/constants'; +import {getChannelIconComponent} from 'utils/channel_utils'; import './channel_list.scss'; @@ -190,20 +186,13 @@ export default class ChannelList extends React.PureComponent { - let iconToDisplay = ; - - if (channel.type === Constants.PRIVATE_CHANNEL) { - iconToDisplay = ; - } - - if (isArchivedChannel(channel)) { - iconToDisplay = ( - - ); - } + const ChannelIconComponent = getChannelIconComponent(channel); + const iconToDisplay = ( + + ); const sharedChannelIcon = channel.shared ? ( ; + const ArchiveIcon = getArchiveIconComponent(channel.type); + archivedIcon = ( + + ); } let sharedIcon; diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.test.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.test.tsx index 061b6abfb00..dc8a757ac16 100644 --- a/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.test.tsx +++ b/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.test.tsx @@ -100,4 +100,38 @@ describe('components/ChannelHeaderMenu/MenuItems/ArchiveChannel', () => { }, }); }); + + test('renders ArchiveOutlineIcon for public channel', () => { + const publicChannel = TestHelper.getChannelMock({ + id: 'public_channel', + type: 'O', + display_name: 'Public Channel', + }); + + renderWithContext( + + + , initialState, + ); + + // Check that the component renders without error + expect(screen.getByText('Archive Channel')).toBeInTheDocument(); + }); + + test('renders ArchiveLockOutlineIcon for private channel', () => { + const privateChannel = TestHelper.getChannelMock({ + id: 'private_channel', + type: 'P', + display_name: 'Private Channel', + }); + + renderWithContext( + + + , initialState, + ); + + // Check that the component renders without error + expect(screen.getByText('Archive Channel')).toBeInTheDocument(); + }); }); diff --git a/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.tsx b/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.tsx index 89d96a964fb..11f81659abd 100644 --- a/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.tsx +++ b/webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.tsx @@ -5,7 +5,6 @@ import React, {memo} from 'react'; import {FormattedMessage} from 'react-intl'; import {useDispatch} from 'react-redux'; -import {ArchiveOutlineIcon} from '@mattermost/compass-icons/components'; import type {Channel} from '@mattermost/types/channels'; import {openModal} from 'actions/views/modals'; @@ -13,6 +12,7 @@ import {openModal} from 'actions/views/modals'; import DeleteChannelModal from 'components/delete_channel_modal'; import * as Menu from 'components/menu'; +import {getArchiveIconComponent} from 'utils/channel_utils'; import {ModalIdentifiers} from 'utils/constants'; type Props = { @@ -36,10 +36,12 @@ const ArchiveChannel = ({ ); }; + const ArchiveIcon = getArchiveIconComponent(channel.type); + return ( } + leadingElement={} onClick={handleArchiveChannel} labels={ ; + const ArchiveIcon = getArchiveIconComponent(details.type); + icon = ; } else if (details.type === Constants.OPEN_CHANNEL) { icon = ; } else if (details.type === Constants.PRIVATE_CHANNEL) { diff --git a/webapp/channels/src/components/new_channel_modal/new_channel_modal.test.tsx b/webapp/channels/src/components/new_channel_modal/new_channel_modal.test.tsx index c77de51d62d..59eb1bcaeb6 100644 --- a/webapp/channels/src/components/new_channel_modal/new_channel_modal.test.tsx +++ b/webapp/channels/src/components/new_channel_modal/new_channel_modal.test.tsx @@ -399,10 +399,13 @@ describe('components/new_channel_modal', () => { expect(createChannelButton).toBeEnabled(); // Submit - await act(async () => userEvent.click(createChannelButton)); + await userEvent.click(createChannelButton); - const serverError = screen.getByText('Something went wrong. Please try again.'); - expect(serverError).toBeInTheDocument(); + // Wait for async state updates + await waitFor(() => { + const serverError = screen.getByText('Something went wrong. Please try again.'); + expect(serverError).toBeInTheDocument(); + }); expect(createChannelButton).toBeDisabled(); }); diff --git a/webapp/channels/src/components/post/post_component.tsx b/webapp/channels/src/components/post/post_component.tsx index e6157021313..4df3741971b 100644 --- a/webapp/channels/src/components/post/post_component.tsx +++ b/webapp/channels/src/components/post/post_component.tsx @@ -38,12 +38,12 @@ import PostTime from 'components/post_view/post_time'; import ReactionList from 'components/post_view/reaction_list'; import ThreadFooter from 'components/threading/channel_threads/thread_footer'; import type {Props as TimestampProps} from 'components/timestamp/timestamp'; -import ArchiveIcon from 'components/widgets/icons/archive_icon'; import InfoSmallIcon from 'components/widgets/icons/info_small_icon'; import WithTooltip from 'components/with_tooltip'; import {createBurnOnReadDeleteModalHandlers} from 'hooks/useBurnOnReadDeleteModal'; import {getHistory} from 'utils/browser_history'; +import {getArchiveIconComponent} from 'utils/channel_utils'; import Constants, {A11yCustomEventTypes, AppEvents, Locations, PostTypes, ModalIdentifiers} from 'utils/constants'; import type {A11yFocusEventDetail} from 'utils/constants'; import {isKeyPressed} from 'utils/keyboard'; @@ -709,13 +709,20 @@ function PostComponent(props: Props) { } {props.channelIsArchived && - - - - + + + {(() => { + const ArchiveIcon = getArchiveIconComponent(props.channelType); + return ; + })()} + + } {(Boolean(isSearchResultItem) || props.isFlaggedPosts) && Boolean(props.teamDisplayName) && diff --git a/webapp/channels/src/components/searchable_channel_list.test.tsx b/webapp/channels/src/components/searchable_channel_list.test.tsx index a1c9e3635a5..0f70f7f357b 100644 --- a/webapp/channels/src/components/searchable_channel_list.test.tsx +++ b/webapp/channels/src/components/searchable_channel_list.test.tsx @@ -4,6 +4,8 @@ import {shallow} from 'enzyme'; import React from 'react'; +import type {Channel} from '@mattermost/types/channels'; + import {SearchableChannelList} from 'components/searchable_channel_list'; import {type MockIntl} from 'tests/helpers/intl-test-helper'; @@ -50,4 +52,54 @@ describe('components/SearchableChannelList', () => { expect(wrapper.state('page')).toEqual(0); }); + + test('should render ArchiveOutlineIcon for archived public channels', () => { + const channels = [ + { + id: 'channel1', + name: 'archived-public-channel', + display_name: 'Archived Public Channel', + type: 'O', + delete_at: 1234567890, + team_id: 'team1', + purpose: '', + } as Channel, + ]; + + const wrapper = shallow( + , + ); + + const channelRow = wrapper.find('.more-modal__row').first(); + expect(channelRow.find('ArchiveOutlineIcon')).toHaveLength(1); + expect(channelRow.find('ArchiveLockOutlineIcon')).toHaveLength(0); + }); + + test('should render ArchiveLockOutlineIcon for archived private channels', () => { + const channels = [ + { + id: 'channel2', + name: 'archived-private-channel', + display_name: 'Archived Private Channel', + type: 'P', + delete_at: 1234567890, + team_id: 'team1', + purpose: '', + } as Channel, + ]; + + const wrapper = shallow( + , + ); + + const channelRow = wrapper.find('.more-modal__row').first(); + expect(channelRow.find('ArchiveLockOutlineIcon')).toHaveLength(1); + expect(channelRow.find('ArchiveOutlineIcon')).toHaveLength(0); + }); }); diff --git a/webapp/channels/src/components/searchable_channel_list.tsx b/webapp/channels/src/components/searchable_channel_list.tsx index e348af02114..6561337d5c9 100644 --- a/webapp/channels/src/components/searchable_channel_list.tsx +++ b/webapp/channels/src/components/searchable_channel_list.tsx @@ -9,8 +9,6 @@ import {ArchiveOutlineIcon, CheckIcon, ChevronDownIcon, GlobeIcon, LockOutlineIc import type {Channel, ChannelMembership} from '@mattermost/types/channels'; import type {RelationOneToOne} from '@mattermost/types/utilities'; -import {isPrivateChannel} from 'mattermost-redux/utils/channel_utils'; - import MagnifyingGlassSVG from 'components/common/svg_images_components/magnifying_glass_svg'; import LoadingScreen from 'components/loading_screen'; import * as Menu from 'components/menu'; @@ -19,7 +17,7 @@ import SharedChannelIndicator from 'components/shared_channel_indicator'; import CheckboxCheckedIcon from 'components/widgets/icons/checkbox_checked_icon'; import LoadingWrapper from 'components/widgets/loading/loading_wrapper'; -import {isArchivedChannel} from 'utils/channel_utils'; +import {getChannelIconComponent} from 'utils/channel_utils'; import Constants, {ModalIdentifiers} from 'utils/constants'; import {isKeyPressed} from 'utils/keyboard'; import * as UserAgent from 'utils/user_agent'; @@ -123,15 +121,8 @@ export class SearchableChannelList extends React.PureComponent { createChannelRow = (channel: Channel) => { const ariaLabel = `${channel.display_name}, ${channel.purpose}`.toLowerCase(); - let channelTypeIcon; - - if (isArchivedChannel(channel)) { - channelTypeIcon = ; - } else if (isPrivateChannel(channel)) { - channelTypeIcon = ; - } else { - channelTypeIcon = ; - } + const ChannelIcon = getChannelIconComponent(channel); + const channelTypeIcon = ; let memberCount = 0; if (this.props.channelsMemberCount?.[channel.id]) { memberCount = this.props.channelsMemberCount[channel.id]; diff --git a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_icon/sidebar_channel_icon.test.tsx b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_icon/sidebar_channel_icon.test.tsx new file mode 100644 index 00000000000..a285ea95e78 --- /dev/null +++ b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_icon/sidebar_channel_icon.test.tsx @@ -0,0 +1,64 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {shallow} from 'enzyme'; +import React from 'react'; + +import Constants from 'utils/constants'; + +import SidebarChannelIcon from './sidebar_channel_icon'; + +describe('components/sidebar/sidebar_channel/sidebar_channel_icon', () => { + const baseIcon = ; + + test('should render the provided icon when channel is not deleted', () => { + const wrapper = shallow( + , + ); + + expect(wrapper.find('.icon-globe')).toHaveLength(1); + expect(wrapper.find('.icon-archive-outline')).toHaveLength(0); + expect(wrapper.find('.icon-archive-lock-outline')).toHaveLength(0); + }); + + test('should render archive icon for deleted public channel', () => { + const wrapper = shallow( + , + ); + + expect(wrapper.find('.icon-archive-outline')).toHaveLength(1); + expect(wrapper.find('.icon-archive-lock-outline')).toHaveLength(0); + }); + + test('should render archive-lock icon for deleted private channel', () => { + const wrapper = shallow( + , + ); + + expect(wrapper.find('.icon-archive-lock-outline')).toHaveLength(1); + expect(wrapper.find('.icon-archive-outline')).toHaveLength(0); + }); + + test('should render regular archive icon when channelType is not provided', () => { + const wrapper = shallow( + , + ); + + expect(wrapper.find('.icon-archive-outline')).toHaveLength(1); + expect(wrapper.find('.icon-archive-lock-outline')).toHaveLength(0); + }); +}); diff --git a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_icon/sidebar_channel_icon.tsx b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_icon/sidebar_channel_icon.tsx index bc17c006c64..c54cf85aaba 100644 --- a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_icon/sidebar_channel_icon.tsx +++ b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_icon/sidebar_channel_icon.tsx @@ -3,15 +3,18 @@ import React from 'react'; +import {getArchiveIconClassName} from 'utils/channel_utils'; + type Props = { icon: JSX.Element | null; isDeleted: boolean; + channelType?: string; }; -function SidebarChannelIcon({isDeleted, icon}: Props) { +function SidebarChannelIcon({isDeleted, icon, channelType}: Props) { if (isDeleted) { return ( - + ); } return icon; diff --git a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/__snapshots__/sidebar_channel_link.test.tsx.snap b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/__snapshots__/sidebar_channel_link.test.tsx.snap index 98ae00d9068..8ae63b38360 100644 --- a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/__snapshots__/sidebar_channel_link.test.tsx.snap +++ b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/__snapshots__/sidebar_channel_link.test.tsx.snap @@ -10,6 +10,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should fetch sh to="http://a.fake.link" > @@ -97,6 +98,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn to="http://a.fake.link" > @@ -179,6 +181,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn to="http://a.fake.link" > @@ -261,6 +264,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn to="http://a.fake.link" > @@ -347,6 +351,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn to="http://a.fake.link" > @@ -429,6 +434,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should not fetc to="http://a.fake.link" > @@ -521,6 +527,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should not fetc to="http://a.fake.link" > diff --git a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link.tsx b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link.tsx index 6b67f23b74a..886487e7500 100644 --- a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link.tsx +++ b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link.tsx @@ -245,6 +245,7 @@ export class SidebarChannelLink extends React.PureComponent {
({ + ...jest.requireActual('mattermost-redux/selectors/entities/channels'), getMyChannels: jest.fn(() => []), getMyChannelMemberships: jest.fn(() => {}), })); diff --git a/webapp/channels/src/components/suggestion/channel_mention_provider.tsx b/webapp/channels/src/components/suggestion/channel_mention_provider.tsx index 48e1592b161..3c099b8acbd 100644 --- a/webapp/channels/src/components/suggestion/channel_mention_provider.tsx +++ b/webapp/channels/src/components/suggestion/channel_mention_provider.tsx @@ -14,6 +14,7 @@ import store from 'stores/redux_store'; import usePrefixedIds from 'components/common/hooks/usePrefixedIds'; +import {getArchiveIconClassName} from 'utils/channel_utils'; import {Constants} from 'utils/constants'; import Provider from './provider'; @@ -47,7 +48,7 @@ export const ChannelMentionSuggestion = React.forwardRef diff --git a/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.tsx b/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.tsx index 01bc0c302c6..4fbfdcb3fdf 100644 --- a/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.tsx +++ b/webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.tsx @@ -22,6 +22,7 @@ import store from 'stores/redux_store'; import usePrefixedIds from 'components/common/hooks/usePrefixedIds'; +import {getArchiveIconClassName} from 'utils/channel_utils'; import {Constants} from 'utils/constants'; import Provider from './provider'; @@ -50,7 +51,7 @@ const SearchChannelWithPermissionsSuggestion = React.forwardRef(({ defaultMessage: 'Archived channel', })} > - + ); } else if (hasDraft) { diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/create_comment.tsx b/webapp/channels/src/components/threading/virtualized_thread_viewer/create_comment.tsx index 16c4571fa6a..dee6ebc2b71 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/create_comment.tsx +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/create_comment.tsx @@ -5,7 +5,6 @@ import React, {memo, forwardRef, useMemo} from 'react'; import {FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; -import {ArchiveOutlineIcon} from '@mattermost/compass-icons/components'; import type {UserProfile} from '@mattermost/types/users'; import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; @@ -14,6 +13,7 @@ import {getPost, getLimitedViews} from 'mattermost-redux/selectors/entities/post import AdvancedCreateComment from 'components/advanced_create_comment'; import BasicSeparator from 'components/widgets/separator/basic-separator'; +import {getArchiveIconComponent} from 'utils/channel_utils'; import Constants from 'utils/constants'; import type {GlobalState} from 'types/store'; @@ -69,11 +69,12 @@ const CreateComment = forwardRef(({ } if (channelIsArchived) { + const ArchiveIcon = getArchiveIconComponent(channelType); return (
- diff --git a/webapp/channels/src/sass/components/_search.scss b/webapp/channels/src/sass/components/_search.scss index db9ef9e76aa..17f3d72ea1f 100644 --- a/webapp/channels/src/sass/components/_search.scss +++ b/webapp/channels/src/sass/components/_search.scss @@ -304,8 +304,15 @@ .search-channel__archived { flex-shrink: 0; + margin-left: 3px; float: right; - opacity: 0.5; + opacity: 0.64; + + .channel-header-archived-icon { + position: relative; + top: 3px; + margin: 0; + } } .search-team__name { diff --git a/webapp/channels/src/sass/layout/_headers.scss b/webapp/channels/src/sass/layout/_headers.scss index 965ebbeb19c..4c018041a51 100644 --- a/webapp/channels/src/sass/layout/_headers.scss +++ b/webapp/channels/src/sass/layout/_headers.scss @@ -338,7 +338,7 @@ } .channel-header-archived-icon { - opacity: 0.5; + opacity: 0.64; } > a { @@ -943,7 +943,12 @@ .channel-header-archived-icon { position: relative; - top: 2px; margin-right: 5px; fill: var(--center-channel-color); } + +// Specific styling for archive icon in search results context +.search-channel__archived .channel-header-archived-icon { + top: 2px; + margin-left: 4px; +} diff --git a/webapp/channels/src/utils/channel_utils.test.ts b/webapp/channels/src/utils/channel_utils.test.ts index 6cf9518e8cf..a857e3fad6b 100644 --- a/webapp/channels/src/utils/channel_utils.test.ts +++ b/webapp/channels/src/utils/channel_utils.test.ts @@ -1,7 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {ArchiveLockOutlineIcon, ArchiveOutlineIcon, GlobeIcon, LockOutlineIcon} from '@mattermost/compass-icons/components'; +import type {Channel} from '@mattermost/types/channels'; + import * as Utils from 'utils/channel_utils'; +import Constants from 'utils/constants'; describe('Channel Utils', () => { describe('findNextUnreadChannelId', () => { @@ -53,4 +57,119 @@ describe('Channel Utils', () => { expect(Utils.findNextUnreadChannelId(curChannelId, allChannelIds, unreadChannelIds, -1)).toEqual(4); }); }); + + describe('getArchiveIconComponent', () => { + test('should return ArchiveLockOutlineIcon for private channels', () => { + const icon = Utils.getArchiveIconComponent(Constants.PRIVATE_CHANNEL); + expect(icon).toBe(ArchiveLockOutlineIcon); + }); + + test('should return ArchiveOutlineIcon for public channels', () => { + const icon = Utils.getArchiveIconComponent(Constants.OPEN_CHANNEL); + expect(icon).toBe(ArchiveOutlineIcon); + }); + + test('should return ArchiveOutlineIcon for DM channels', () => { + const icon = Utils.getArchiveIconComponent(Constants.DM_CHANNEL); + expect(icon).toBe(ArchiveOutlineIcon); + }); + + test('should return ArchiveOutlineIcon for GM channels', () => { + const icon = Utils.getArchiveIconComponent(Constants.GM_CHANNEL); + expect(icon).toBe(ArchiveOutlineIcon); + }); + + test('should return ArchiveOutlineIcon when channelType is undefined', () => { + const icon = Utils.getArchiveIconComponent(undefined); + expect(icon).toBe(ArchiveOutlineIcon); + }); + }); + + describe('getArchiveIconClassName', () => { + test('should return icon-archive-lock-outline for private channels', () => { + const className = Utils.getArchiveIconClassName(Constants.PRIVATE_CHANNEL); + expect(className).toBe('icon-archive-lock-outline'); + }); + + test('should return icon-archive-outline for public channels', () => { + const className = Utils.getArchiveIconClassName(Constants.OPEN_CHANNEL); + expect(className).toBe('icon-archive-outline'); + }); + + test('should return icon-archive-outline for DM channels', () => { + const className = Utils.getArchiveIconClassName(Constants.DM_CHANNEL); + expect(className).toBe('icon-archive-outline'); + }); + + test('should return icon-archive-outline for GM channels', () => { + const className = Utils.getArchiveIconClassName(Constants.GM_CHANNEL); + expect(className).toBe('icon-archive-outline'); + }); + + test('should return icon-archive-outline when channelType is undefined', () => { + const className = Utils.getArchiveIconClassName(undefined); + expect(className).toBe('icon-archive-outline'); + }); + }); + + describe('getChannelIconComponent', () => { + test('should return ArchiveLockOutlineIcon for archived private channel', () => { + const channel = { + type: Constants.PRIVATE_CHANNEL, + delete_at: 1234567890, + } as Channel; + const icon = Utils.getChannelIconComponent(channel); + expect(icon).toBe(ArchiveLockOutlineIcon); + }); + + test('should return ArchiveOutlineIcon for archived public channel', () => { + const channel = { + type: Constants.OPEN_CHANNEL, + delete_at: 1234567890, + } as Channel; + const icon = Utils.getChannelIconComponent(channel); + expect(icon).toBe(ArchiveOutlineIcon); + }); + + test('should return LockOutlineIcon for active private channel', () => { + const channel = { + type: Constants.PRIVATE_CHANNEL, + delete_at: 0, + } as Channel; + const icon = Utils.getChannelIconComponent(channel); + expect(icon).toBe(LockOutlineIcon); + }); + + test('should return GlobeIcon for active public channel', () => { + const channel = { + type: Constants.OPEN_CHANNEL, + delete_at: 0, + } as Channel; + const icon = Utils.getChannelIconComponent(channel); + expect(icon).toBe(GlobeIcon); + }); + + test('should return GlobeIcon for DM channel', () => { + const channel = { + type: Constants.DM_CHANNEL, + delete_at: 0, + } as Channel; + const icon = Utils.getChannelIconComponent(channel); + expect(icon).toBe(GlobeIcon); + }); + + test('should return GlobeIcon for GM channel', () => { + const channel = { + type: Constants.GM_CHANNEL, + delete_at: 0, + } as Channel; + const icon = Utils.getChannelIconComponent(channel); + expect(icon).toBe(GlobeIcon); + }); + + test('should return GlobeIcon when channel is undefined', () => { + const icon = Utils.getChannelIconComponent(undefined); + expect(icon).toBe(GlobeIcon); + }); + }); }); diff --git a/webapp/channels/src/utils/channel_utils.tsx b/webapp/channels/src/utils/channel_utils.tsx index 9d780f57796..9251c5f6a7c 100644 --- a/webapp/channels/src/utils/channel_utils.tsx +++ b/webapp/channels/src/utils/channel_utils.tsx @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {ArchiveLockOutlineIcon, ArchiveOutlineIcon, GlobeIcon, LockOutlineIcon} from '@mattermost/compass-icons/components'; import type {Channel, ChannelType} from '@mattermost/types/channels'; import type {Team} from '@mattermost/types/teams'; @@ -64,6 +65,47 @@ export function isArchivedChannel(channel?: Channel) { return Boolean(channel && channel.delete_at !== 0); } +/** + * Returns the appropriate archive icon component based on channel type. + * Private archived channels get a lock icon, public archived channels get a standard archive icon. + * + * @param channelType - The type of the channel (e.g., Constants.PRIVATE_CHANNEL, Constants.OPEN_CHANNEL) + * @returns The appropriate icon component + */ +export function getArchiveIconComponent(channelType?: ChannelType | string) { + return channelType === Constants.PRIVATE_CHANNEL ? ArchiveLockOutlineIcon : ArchiveOutlineIcon; +} + +/** + * Returns the appropriate archive icon CSS class name based on channel type. + * Private archived channels get 'icon-archive-lock-outline', public archived channels get 'icon-archive-outline'. + * + * @param channelType - The type of the channel (e.g., Constants.PRIVATE_CHANNEL, Constants.OPEN_CHANNEL) + * @returns The appropriate icon class name + */ +export function getArchiveIconClassName(channelType?: ChannelType | string): string { + return channelType === Constants.PRIVATE_CHANNEL ? 'icon-archive-lock-outline' : 'icon-archive-outline'; +} + +/** + * Returns the appropriate channel icon component based on channel state and type. + * Handles archived channels (with lock for private), private channels, and public channels. + * + * @param channel - The channel object + * @returns The appropriate icon component (ArchiveLockOutlineIcon, ArchiveOutlineIcon, LockOutlineIcon, or GlobeIcon) + */ +export function getChannelIconComponent(channel?: Channel) { + if (isArchivedChannel(channel)) { + return getArchiveIconComponent(channel?.type); + } + + if (channel?.type === Constants.PRIVATE_CHANNEL) { + return LockOutlineIcon; + } + + return GlobeIcon; +} + type JoinPrivateChannelPromptResult = { data: { join: boolean;