diff --git a/e2e-tests/cypress/tests/integration/boards/channel_header_spec.ts b/e2e-tests/cypress/tests/integration/boards/channel_header_spec.ts new file mode 100644 index 0000000000..68d71d5529 --- /dev/null +++ b/e2e-tests/cypress/tests/integration/boards/channel_header_spec.ts @@ -0,0 +1,90 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// *************************************************************** +// - [#] indicates a test step (e.g. # Go to a page) +// - [*] indicates an assertion (e.g. * Check the title) +// *************************************************************** + +// Stage: @prod +// Group: @boards + +describe('channels > channel header', {testIsolation: true}, () => { + let testTeam: {name: string}; + let testUser: {username: string}; + + before(() => { + cy.apiInitSetup().then(({team, user}) => { + testTeam = team; + testUser = user; + + // # Login as testUser + cy.apiLogin(testUser); + }); + }); + + describe('App Bar enabled', () => { + it('webapp should hide the Boards channel header button', () => { + cy.apiAdminLogin(); + cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: true}}); + + // # Login as testUser + cy.apiLogin(testUser); + + // # Navigate directly to a channel + cy.visit(`/${testTeam.name}/channels/town-square`); + + // * Verify channel header button is not showing + cy.get('#channel-header').within(() => { + cy.get('[data-testid="boardsIcon"]').should('not.exist'); + }); + }); + }); + + describe('App Bar disabled', () => { + beforeEach(() => { + cy.apiAdminLogin(); + cy.apiUpdateConfig({ExperimentalSettings: {EnableAppBar: false}}); + + // # Login as testUser + cy.apiLogin(testUser); + }); + + it('webapp should show the Boards channel header button', () => { + // # Navigate directly to a channel + cy.visit(`/${testTeam.name}/channels/town-square`); + + // * Verify channel header button is showing + cy.get('#channel-header').within(() => { + cy.get('#incidentIcon').should('exist'); + }); + }); + + it('tooltip text should show "Boards" for Boards channel header button', () => { + // # Navigate directly to a channel + cy.visit(`/${testTeam.name}/channels/town-square`); + + // # Hover over the channel header icon + cy.get('#channel-header').within(() => { + cy.get('[data-testid="boardsIcon"]').trigger('mouseover'); + }); + + // * Verify tooltip text + cy.get('#pluginTooltip').contains('Boards'); + }); + + it('webapp should make the Boards channel header button active when opened', () => { + // # Navigate directly to a channel + cy.visit(`/${testTeam.name}/channels/town-square`); + + cy.get('#channel-header').within(() => { + // # Click the channel header button + cy.get('[data-testid="boardsIcon"]').as('icon').click(); + + // * Verify channel header button is showing active className + cy.get('@icon').parent(). + should('have.class', 'channel-header__icon--active-inverted'); + }); + }); + }); +}); diff --git a/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js b/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js index 4b3abd6556..5176c821a3 100644 --- a/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js +++ b/e2e-tests/cypress/tests/integration/playbooks/channels/channel_header_spec.js @@ -93,6 +93,20 @@ describe('channels > channel header', {testIsolation: true}, () => { // * Verify tooltip text cy.get('#pluginTooltip').contains('Playbooks'); }); + + it('webapp should make the Playbook channel header button active when opened', () => { + // # Navigate directly to a non-playbook run channel + cy.visit(`/${testTeam.name}/channels/town-square`); + + cy.get('#channel-header').within(() => { + // # Click the channel header button + cy.get('#incidentIcon').click(); + + // * Verify channel header button is showing active className + cy.get('#incidentIcon').parent(). + should('have.class', 'channel-header__icon--active-inverted'); + }); + }); }); describe('description text', () => { diff --git a/webapp/boards/src/components/rhsChannelBoardsToggleIcon.tsx b/webapp/boards/src/components/rhsChannelBoardsToggleIcon.tsx new file mode 100644 index 0000000000..76bafeb1c4 --- /dev/null +++ b/webapp/boards/src/components/rhsChannelBoardsToggleIcon.tsx @@ -0,0 +1,36 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {ElementRef, useRef} from 'react' + +import {useSelector} from 'react-redux' +import {GlobalState} from '@mattermost/types/store' + +import FocalboardIcon from 'src/widgets/icons/logo' + +type Props = { + boardsRhsId: string +} + +type ViewsState = {views: {rhs: {isSidebarOpen: boolean, pluggableId: string}, rhsSuppressed: boolean}} + +const RhsChannelBoardsToggleIcon = ({boardsRhsId}: Props) => { + const iconRef = useRef>(null) + const isOpen = useSelector(({views: {rhs, rhsSuppressed}}: GlobalState & ViewsState) => ( + rhs.isSidebarOpen && + !rhsSuppressed && + rhs.pluggableId === boardsRhsId + )) + + // If it has been mounted, we know our parent is always a button. + const parent = iconRef?.current ? iconRef?.current?.parentNode as HTMLButtonElement : null + parent?.classList.toggle('channel-header__icon--active-inverted', isOpen) + + return ( + + ) +} + +export default RhsChannelBoardsToggleIcon diff --git a/webapp/boards/src/index.tsx b/webapp/boards/src/index.tsx index 70ca1fb6f1..6a1ba97a96 100644 --- a/webapp/boards/src/index.tsx +++ b/webapp/boards/src/index.tsx @@ -74,6 +74,7 @@ import './plugin.scss' import CreateBoardFromTemplate from 'src/components/createBoardFromTemplate' import CloudUpgradeNudge from './components/cloudUpgradeNudge/cloudUpgradeNudge' +import RhsChannelBoardsToggle from './components/rhsChannelBoardsToggleIcon' function getSubpath(siteURL: string): string { const url = new URL(siteURL) @@ -365,10 +366,15 @@ export default class Plugin { ) - const {rhsId, toggleRHSPlugin} = this.registry.registerRightHandSidebarComponent(component, title) - this.rhsId = rhsId + const {id, toggleRHSPlugin} = this.registry.registerRightHandSidebarComponent(component, title) + this.rhsId = id - this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(, () => mmStore.dispatch(toggleRHSPlugin), 'Boards', 'Boards') + this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction( + () => , + () => mmStore.dispatch(toggleRHSPlugin), + 'Boards', + 'Boards' + ) this.registry.registerProduct( '/boards', diff --git a/webapp/boards/src/widgets/icons/compassIcon.tsx b/webapp/boards/src/widgets/icons/compassIcon.tsx index fb8ed7d53a..10f430b782 100644 --- a/webapp/boards/src/widgets/icons/compassIcon.tsx +++ b/webapp/boards/src/widgets/icons/compassIcon.tsx @@ -8,10 +8,14 @@ type Props = { className?: string } -export default function CompassIcon(props: Props): JSX.Element { +export default React.forwardRef(({icon, className, ...props}, ref) => { // All compass icon classes start with icon, // so not expecting that prefix in props. return ( - + ) -} +}) diff --git a/webapp/boards/src/widgets/icons/logo.tsx b/webapp/boards/src/widgets/icons/logo.tsx index 741386c323..7ee8cc8abd 100644 --- a/webapp/boards/src/widgets/icons/logo.tsx +++ b/webapp/boards/src/widgets/icons/logo.tsx @@ -6,11 +6,13 @@ import React from 'react' import './logo.scss' import CompassIcon from './compassIcon' -export default function LogoIcon(): JSX.Element { +export default React.forwardRef((_, ref) => { return ( ) -} +}) diff --git a/webapp/playbooks/src/components/assets/icons/playbooks_product_icon.tsx b/webapp/playbooks/src/components/assets/icons/playbooks_product_icon.tsx index cd1f538510..c5866d61d6 100644 --- a/webapp/playbooks/src/components/assets/icons/playbooks_product_icon.tsx +++ b/webapp/playbooks/src/components/assets/icons/playbooks_product_icon.tsx @@ -4,8 +4,6 @@ import React from 'react'; import styled from 'styled-components'; -export type Ref = HTMLElement; - interface Props { id?: string; } @@ -14,7 +12,7 @@ const Icon = styled.i` font-size: 22px; `; -const PlaybooksProductIcon = React.forwardRef((props: Props, forwardedRef) => ( +const PlaybooksProductIcon = React.forwardRef((props: Props, forwardedRef) => ( { - const myRef = useRef(null); + const myRef = useRef>(null); const isRHSOpen = useSelector(isPlaybookRunRHSOpen); // If it has been mounted, we know our parent is always a button. const parent = myRef?.current ? myRef?.current?.parentNode as HTMLButtonElement : null; - if (parent) { - if (isRHSOpen) { - parent.classList.add('channel-header__icon--active'); - } else { - parent.classList.remove('channel-header__icon--active'); - } - } + parent?.classList.toggle('channel-header__icon--active-inverted', isRHSOpen); return (