Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
MM-37130 Adding basic global header. (#8407)
Browse files Browse the repository at this point in the history
  • Loading branch information
crspeller committed Jul 21, 2021
1 parent 3359ee2 commit 58b6ae1
Show file tree
Hide file tree
Showing 35 changed files with 1,052 additions and 136 deletions.
7 changes: 7 additions & 0 deletions babel.config.js
Expand Up @@ -30,6 +30,13 @@ const config = {
'@babel/plugin-proposal-object-rest-spread',
'react-hot-loader/babel',
'babel-plugin-typescript-to-proptypes',
[
'babel-plugin-styled-components',
{
ssr: false,
fileName: false,
},
],
],
};

Expand Down
16 changes: 16 additions & 0 deletions components/custom_status/custom_status.scss
Expand Up @@ -126,6 +126,22 @@
}
}

.status-dropdown-menu-global-header {
.Menu__content {
left: -200px;
}

.status-wrapper {
height: auto;

.status {
svg {
top: 0;
}
}
}
}

.status-dropdown-menu,
.StatusModal__input {
.input-clear-x {
Expand Down
40 changes: 40 additions & 0 deletions components/global/assets.tsx
@@ -0,0 +1,40 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React from 'react';

export const ChannelsIcon = () => {
return (
<svg
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M9.89999 16.8C14.3735 16.8 18 13.7108 18 9.9C18 6.08924 14.3735 3 9.89999 3C5.42648 3 1.79999 6.08924 1.79999 9.9C1.79999 12.2671 3.19927 14.3558 5.33165 15.5987L5.33165 18.0626C5.33165 18.3023 5.5688 18.466 5.75977 18.358L8.65831 16.7194C9.0631 16.7725 9.47777 16.8 9.89999 16.8ZM16.5 19.5723C13.0728 19.5723 10.2945 17.2056 10.2945 14.2862C10.2945 11.3667 13.0728 9 16.5 9C19.9272 9 22.7055 11.3667 22.7055 14.2862C22.7055 16.0997 21.6334 17.7 19.9996 18.6521V20.4139C19.9996 20.6536 19.7625 20.8173 19.5715 20.7093L17.4511 19.5106C17.141 19.5513 16.8234 19.5723 16.5 19.5723Z'
fill='blue'
/>
</svg>
);
};

export const SwitcherIcon = () => {
return (
<svg
width='14'
height='13'
viewBox='0 0 14 13'
fill='inherit'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M9.98828 12.5618H13.0117V9.53833H9.98828V12.5618ZM9.98828 8.06177H13.0117V5.03833H9.98828V8.06177ZM5.48828 3.56177H8.51172V0.53833H5.48828V3.56177ZM9.98828 3.56177H13.0117V0.53833H9.98828V3.56177ZM5.48828 8.06177H8.51172V5.03833H5.48828V8.06177ZM0.988281 8.06177H4.01172V5.03833H0.988281V8.06177ZM0.988281 12.5618H4.01172V9.53833H0.988281V12.5618ZM5.48828 12.5618H8.51172V9.53833H5.48828V12.5618ZM0.988281 3.56177H4.01172V0.53833H0.988281V3.56177Z'
fill='inherit'
/>
</svg>
);
};
41 changes: 41 additions & 0 deletions components/global/global_header.test.tsx
@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React from 'react';
import {shallow} from 'enzyme';

import * as redux from 'react-redux';

import GlobalHeader from 'components/global/global_header';

import * as hooks from './hooks';

describe('components/global/global_header', () => {
test('should be disabled when global header is disabled', () => {
const spy = jest.spyOn(redux, 'useSelector');
spy.mockReturnValue(false);
const spyProduct = jest.spyOn(hooks, 'useCurrentProductId');
spyProduct.mockReturnValue(null);

const wrapper = shallow(
<GlobalHeader/>,
);

// Global header should render null
expect(wrapper.type()).toEqual(null);
});

test('should be enabled when global header is enabled', () => {
const spy = jest.spyOn(redux, 'useSelector');
spy.mockReturnValue(true);
const spyProduct = jest.spyOn(hooks, 'useCurrentProductId');
spyProduct.mockReturnValue(null);

const wrapper = shallow(
<GlobalHeader/>,
);

// Global header should not be null
expect(wrapper.type()).not.toEqual(null);
});
});
68 changes: 68 additions & 0 deletions components/global/global_header.tsx
@@ -0,0 +1,68 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React from 'react';
import {useSelector} from 'react-redux';

import styled from 'styled-components';

import {getGlobalHeaderEnabled} from 'selectors/global_header';
import StatusDropdown from 'components/status_dropdown';

import Pluggable from 'plugins/pluggable';

import ProductSwitcher from './product_switcher';
import {useCurrentProductId, useProducts} from './hooks';

const HeaderContainer = styled.div`
position: relative;
display: flex;
flex-direction: row;
align-items: center;
height: 40px;
background: var(--sidebar-teambar-bg);
color: var(--sidebar-text);
`;

const AppSpectificContent = styled.div`
flex-grow: 1;
`;

const ProfileWrapper = styled.div`
margin-right: 20px;
`;

const GlobalHeader = () => {
const enabled = useSelector(getGlobalHeaderEnabled);
const products = useProducts();
const currentProductID = useCurrentProductId(products);

if (!enabled) {
return null;
}

return (
<HeaderContainer>
<ProductSwitcher/>
<AppSpectificContent>
{currentProductID !== null &&
<Pluggable
pluggableName={'Product'}
subComponentName={'headerComponent'}
pluggableId={currentProductID}
/>
}
{/*currentProductID === null &&
This is where the header content for the webapp will go
*/}
</AppSpectificContent>
<ProfileWrapper>
<StatusDropdown
globalHeader={true}
/>
</ProfileWrapper>
</HeaderContainer>
);
};

export default GlobalHeader;
54 changes: 54 additions & 0 deletions components/global/hooks.tsx
@@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {MutableRefObject, useEffect} from 'react';
import {useSelector} from 'react-redux';
import {useLocation} from 'react-router';

import {GlobalState} from 'types/store';
import {ProductComponent} from 'types/store/plugins';
import {getBasePath} from 'utils/url';

const selectProducts = (state: GlobalState) => state.plugins.components.Product;

export const useProducts = (): ProductComponent[] | undefined => {
return useSelector<GlobalState, ProductComponent[]>(selectProducts);
};

/**
* Hook that alerts clicks outside of the passed ref.
*/
export function useClickOutsideRef(ref: MutableRefObject<HTMLElement | null>, handler: () => void): void {
useEffect(() => {
function onMouseDown(event: MouseEvent) {
const target = event.target as any;
if (ref.current && target instanceof Node && !ref.current.contains(target)) {
handler();
}
}

// Bind the event listener
document.addEventListener('mousedown', onMouseDown);
return () => {
// Unbind the event listener on clean up
document.removeEventListener('mousedown', onMouseDown);
};
}, [ref, handler]);
}

export const useCurrentProductId = (products?: ProductComponent[]): string | null => {
if (!products) {
return null;
}

const location = useLocation();
for (let i = 0; i < products.length; i++) {
const product = products[i];
if (location.pathname.startsWith(getBasePath() + product.baseURL)) {
return product.id;
}
}

return null;
};

0 comments on commit 58b6ae1

Please sign in to comment.