mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Add component library (#28826)
* Add component library * Fix lint and add different enable logic * Fix test * Address feedback and add background context * Fix lint --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
ead2a7c018
commit
33005922c9
@ -0,0 +1,17 @@
|
|||||||
|
.clInput {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clWrapper {
|
||||||
|
width: auto;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clCenterBackground {
|
||||||
|
background-color: var(--center-channel-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clSidebarBackground {
|
||||||
|
background-color: var(--sidebar-bg);
|
||||||
|
}
|
114
webapp/channels/src/components/component_library/hooks.tsx
Normal file
114
webapp/channels/src/components/component_library/hooks.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback, useMemo, useState} from 'react';
|
||||||
|
|
||||||
|
import './component_library.scss';
|
||||||
|
|
||||||
|
type HookResult<T> = [
|
||||||
|
{[x: string]: T},
|
||||||
|
JSX.Element,
|
||||||
|
]
|
||||||
|
export const useStringProp = (
|
||||||
|
propName: string,
|
||||||
|
defaultValue: string,
|
||||||
|
isTextarea: boolean,
|
||||||
|
): HookResult<string> => {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
const onChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => setValue(e.target.value), []);
|
||||||
|
const selector = useMemo(() => {
|
||||||
|
const input = isTextarea ? (
|
||||||
|
<textarea
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<label className='clInput'>
|
||||||
|
{`${propName}: `}
|
||||||
|
{input}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}, [onChange, value, propName, isTextarea]);
|
||||||
|
const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]);
|
||||||
|
|
||||||
|
return [preparedProp, selector];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBooleanProp = (
|
||||||
|
propName: string,
|
||||||
|
defaultValue: boolean,
|
||||||
|
): HookResult<boolean> => {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.checked), []);
|
||||||
|
const selector = useMemo(() => (
|
||||||
|
<label className='clInput'>
|
||||||
|
{`${propName}: `}
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
onChange={onChange}
|
||||||
|
checked={value}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
), [onChange, propName, value]);
|
||||||
|
const preparedProp = useMemo(() => ({[propName]: value}), [propName, value]);
|
||||||
|
|
||||||
|
return [preparedProp, selector];
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALL_OPTION = 'ALL';
|
||||||
|
type DropdownHookResult = [
|
||||||
|
{[x: string]: string} | undefined,
|
||||||
|
{[x: string]: string[]} | undefined,
|
||||||
|
JSX.Element,
|
||||||
|
];
|
||||||
|
export const useDropdownProp = (
|
||||||
|
propName: string,
|
||||||
|
defaultValue: string,
|
||||||
|
options: string[],
|
||||||
|
allowAll: boolean,
|
||||||
|
): DropdownHookResult => {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
const onChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => setValue(e.target.value), []);
|
||||||
|
const renderedOptions = useMemo(() => {
|
||||||
|
const toReturn = options.map((v) => (
|
||||||
|
<option
|
||||||
|
key={v}
|
||||||
|
value={v}
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</option>
|
||||||
|
));
|
||||||
|
if (allowAll) {
|
||||||
|
toReturn.unshift((
|
||||||
|
<option
|
||||||
|
key={ALL_OPTION}
|
||||||
|
value={ALL_OPTION}
|
||||||
|
>
|
||||||
|
{ALL_OPTION}
|
||||||
|
</option>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return toReturn;
|
||||||
|
}, [options, allowAll]);
|
||||||
|
const selector = useMemo(() => (
|
||||||
|
<label className='clInput'>
|
||||||
|
{`${propName}: `}
|
||||||
|
<select
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{renderedOptions}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
), [onChange, propName, renderedOptions, value]);
|
||||||
|
const preparedProp = useMemo(() => (value === ALL_OPTION ? undefined : ({[propName]: value})), [propName, value]);
|
||||||
|
const preparedPossibilities = useMemo(() => (value === ALL_OPTION ? ({[propName]: options}) : undefined), [propName, value, options]);
|
||||||
|
return [preparedProp, preparedPossibilities, selector];
|
||||||
|
};
|
125
webapp/channels/src/components/component_library/index.tsx
Normal file
125
webapp/channels/src/components/component_library/index.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||||
|
|
||||||
|
import {Preferences} from 'mattermost-redux/constants';
|
||||||
|
|
||||||
|
import {applyTheme} from 'utils/utils';
|
||||||
|
|
||||||
|
import SectionNoticeComponentLibrary from './section_notice.cl';
|
||||||
|
|
||||||
|
import './component_library.scss';
|
||||||
|
|
||||||
|
const componentMap = {
|
||||||
|
'Section Notice': SectionNoticeComponentLibrary,
|
||||||
|
};
|
||||||
|
|
||||||
|
type ComponentName = keyof typeof componentMap
|
||||||
|
const defaultComponent = Object.keys(componentMap)[0] as ComponentName;
|
||||||
|
|
||||||
|
type ThemeName = keyof typeof Preferences.THEMES;
|
||||||
|
const defaultTheme = Object.keys(Preferences.THEMES)[0] as ThemeName;
|
||||||
|
|
||||||
|
type BackgroundType = 'center' | 'sidebar';
|
||||||
|
|
||||||
|
const ComponentLibrary = () => {
|
||||||
|
const [selectedComponent, setSelectedComponent] = useState<ComponentName>(defaultComponent);
|
||||||
|
const onSelectComponent = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setSelectedComponent(e.target.value as ComponentName);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [selectedTheme, setSelectedTheme] = useState(defaultTheme);
|
||||||
|
const onSelectTheme = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setSelectedTheme(e.target.value as ThemeName);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [selectedBackground, setSelectedBackground] = useState<BackgroundType>('center');
|
||||||
|
const onSelectBackground = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSelectedBackground(e.currentTarget.value as BackgroundType);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyTheme(Preferences.THEMES[selectedTheme]);
|
||||||
|
}, [selectedTheme]);
|
||||||
|
|
||||||
|
const componentOptions = useMemo(() => {
|
||||||
|
return Object.keys(componentMap).map((v) => (
|
||||||
|
<option
|
||||||
|
key={v}
|
||||||
|
value={v}
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</option>
|
||||||
|
));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const themeOptions = useMemo(() => {
|
||||||
|
return Object.keys(Preferences.THEMES).map((v) => (
|
||||||
|
<option
|
||||||
|
key={v}
|
||||||
|
value={v}
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</option>
|
||||||
|
));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const SelectedComponent = componentMap[selectedComponent];
|
||||||
|
return (
|
||||||
|
<div className={'clWrapper'}>
|
||||||
|
<label className={'clInput'}>
|
||||||
|
{'Component: '}
|
||||||
|
<select
|
||||||
|
onChange={onSelectComponent}
|
||||||
|
value={selectedComponent}
|
||||||
|
>
|
||||||
|
{componentOptions}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className={'clInput'}>
|
||||||
|
{'Theme: '}
|
||||||
|
<select
|
||||||
|
onChange={onSelectTheme}
|
||||||
|
value={selectedTheme}
|
||||||
|
>
|
||||||
|
{themeOptions}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className={'clInput'}>
|
||||||
|
{'Background: '}
|
||||||
|
<label>
|
||||||
|
{'Center channel'}
|
||||||
|
<input
|
||||||
|
onChange={onSelectBackground}
|
||||||
|
name={'background'}
|
||||||
|
value={'center'}
|
||||||
|
type={'radio'}
|
||||||
|
checked={selectedBackground === 'center'}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
{'Sidebar'}
|
||||||
|
<input
|
||||||
|
onChange={onSelectBackground}
|
||||||
|
name={'background'}
|
||||||
|
value={'sidebar'}
|
||||||
|
type={'radio'}
|
||||||
|
checked={selectedBackground === 'sidebar'}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</label>
|
||||||
|
<div className={'clWrapper'}>
|
||||||
|
<SelectedComponent
|
||||||
|
backgroundClass={classNames({
|
||||||
|
clCenterBackground: selectedBackground === 'center',
|
||||||
|
clSidebarBackground: selectedBackground === 'sidebar',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComponentLibrary;
|
@ -0,0 +1,94 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
/* eslint-disable no-alert */
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, {useCallback, useMemo, useState} from 'react';
|
||||||
|
|
||||||
|
import SectionNotice from 'components/section_notice';
|
||||||
|
|
||||||
|
import {useBooleanProp, useDropdownProp, useStringProp} from './hooks';
|
||||||
|
import {buildComponent} from './utils';
|
||||||
|
|
||||||
|
import './component_library.scss';
|
||||||
|
|
||||||
|
const propPossibilities = {};
|
||||||
|
|
||||||
|
const sectionTypeValues = ['danger', 'info', 'success', 'welcome', 'warning'];
|
||||||
|
|
||||||
|
const primaryButton = {primaryButton: {onClick: () => window.alert('primary!'), text: 'Primary'}};
|
||||||
|
const secondaryButton = {secondaryButton: {onClick: () => window.alert('secondary!'), text: 'Secondary'}};
|
||||||
|
const linkButton = {linkButton: {onClick: () => window.alert('link!'), text: 'Link'}};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
backgroundClass: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SectionNoticeComponentLibrary = ({
|
||||||
|
backgroundClass,
|
||||||
|
}: Props) => {
|
||||||
|
const [text, textSelector] = useStringProp('text', 'Some text', true);
|
||||||
|
const [title, titleSelector] = useStringProp('title', 'Some text', false);
|
||||||
|
const [dismissable, dismissableSelector] = useBooleanProp('isDismissable', true);
|
||||||
|
const [sectionType, sectionTypePosibilities, sectionTypeSelector] = useDropdownProp('type', 'danger', sectionTypeValues, true);
|
||||||
|
|
||||||
|
const [showPrimaryButton, setShowPrimaryButton] = useState(false);
|
||||||
|
const onChangePrimaryButton = useCallback((e: React.ChangeEvent<HTMLInputElement>) => setShowPrimaryButton(e.target.checked), []);
|
||||||
|
|
||||||
|
const [showSecondaryButton, setShowSecondaryButton] = useState(false);
|
||||||
|
const onChangeSecondaryButton = useCallback((e: React.ChangeEvent<HTMLInputElement>) => setShowSecondaryButton(e.target.checked), []);
|
||||||
|
|
||||||
|
const [showLinkButton, setShowLinkButton] = useState(false);
|
||||||
|
const onChangeLinkButton = useCallback((e: React.ChangeEvent<HTMLInputElement>) => setShowLinkButton(e.target.checked), []);
|
||||||
|
|
||||||
|
const components = useMemo(
|
||||||
|
() => buildComponent(SectionNotice, propPossibilities, [sectionTypePosibilities], [
|
||||||
|
text,
|
||||||
|
title,
|
||||||
|
dismissable,
|
||||||
|
sectionType,
|
||||||
|
showPrimaryButton ? primaryButton : undefined,
|
||||||
|
showSecondaryButton ? secondaryButton : undefined,
|
||||||
|
showLinkButton ? linkButton : undefined,
|
||||||
|
{onDismissClick: () => window.alert('dismiss!')},
|
||||||
|
]),
|
||||||
|
[dismissable, sectionType, sectionTypePosibilities, showLinkButton, showPrimaryButton, showSecondaryButton, text, title],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{textSelector}
|
||||||
|
{titleSelector}
|
||||||
|
{dismissableSelector}
|
||||||
|
{sectionTypeSelector}
|
||||||
|
<label className='clInput'>
|
||||||
|
{'Show primary button: '}
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
onChange={onChangePrimaryButton}
|
||||||
|
checked={showPrimaryButton}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className='clInput'>
|
||||||
|
{'Show secondary button: '}
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
onChange={onChangeSecondaryButton}
|
||||||
|
checked={showSecondaryButton}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className='clInput'>
|
||||||
|
{'Show link button: '}
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
onChange={onChangeLinkButton}
|
||||||
|
checked={showLinkButton}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className={classNames('clWrapper', backgroundClass)}>{components}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionNoticeComponentLibrary;
|
79
webapp/channels/src/components/component_library/utils.tsx
Normal file
79
webapp/channels/src/components/component_library/utils.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function buildPropsLists(inputPossibilities: {[x: string]: any[]}): Array<{[x: string]: any}> {
|
||||||
|
const keys = Object.keys(inputPossibilities);
|
||||||
|
if (!keys.length) {
|
||||||
|
return [{}];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedKey = keys[0];
|
||||||
|
const restPossibilities = {...inputPossibilities};
|
||||||
|
delete restPossibilities[selectedKey];
|
||||||
|
const subProps = buildPropsLists(restPossibilities);
|
||||||
|
const result: Array<{[x: string]: any}> = [];
|
||||||
|
inputPossibilities[selectedKey].forEach((v) => {
|
||||||
|
subProps.forEach((rest) => {
|
||||||
|
result.push({...rest, [selectedKey]: v});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPropString(inputProps: {[x: string]: any}) {
|
||||||
|
const propKeys = Object.keys(inputProps);
|
||||||
|
if (!propKeys.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [(<>{'PROPS: '}</>)];
|
||||||
|
propKeys.forEach((v) => {
|
||||||
|
result.push((<><b>{v}</b>{`: ${inputProps[v]}, `}</>));
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildComponent(
|
||||||
|
Component: React.ComponentType<any>,
|
||||||
|
propPossibilities: {[x: string]: any[]},
|
||||||
|
dropdownPossibilities: Array<{[x: string]: string[]} | undefined>,
|
||||||
|
setProps: Array<{[x: string]: any} | undefined>,
|
||||||
|
) {
|
||||||
|
const res: React.ReactNode[] = [];
|
||||||
|
let currentPropPossibilities = {...propPossibilities};
|
||||||
|
dropdownPossibilities.forEach((v) => {
|
||||||
|
if (v) {
|
||||||
|
currentPropPossibilities = {
|
||||||
|
...currentPropPossibilities,
|
||||||
|
...v,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const propsVariations = buildPropsLists(currentPropPossibilities);
|
||||||
|
let builtSetProps = {};
|
||||||
|
setProps.forEach((v) => {
|
||||||
|
if (v) {
|
||||||
|
builtSetProps = {
|
||||||
|
...builtSetProps,
|
||||||
|
...v,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
propsVariations.forEach((v) => {
|
||||||
|
const propString = buildPropString(v);
|
||||||
|
res.push(
|
||||||
|
<>
|
||||||
|
{Boolean(propString) && <p>{propString}</p>}
|
||||||
|
<Component
|
||||||
|
{...builtSetProps}
|
||||||
|
{...v}
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
@ -17,6 +17,7 @@ import {getTeam} from 'mattermost-redux/selectors/entities/teams';
|
|||||||
import {shouldShowTermsOfService, getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {shouldShowTermsOfService, getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {loadRecentlyUsedCustomEmojis, migrateRecentEmojis} from 'actions/emoji_actions';
|
import {loadRecentlyUsedCustomEmojis, migrateRecentEmojis} from 'actions/emoji_actions';
|
||||||
|
import {isDevModeEnabled} from 'selectors/general';
|
||||||
import {getShowLaunchingWorkspace} from 'selectors/onboarding';
|
import {getShowLaunchingWorkspace} from 'selectors/onboarding';
|
||||||
import {shouldShowAppBar} from 'selectors/plugins';
|
import {shouldShowAppBar} from 'selectors/plugins';
|
||||||
import {
|
import {
|
||||||
@ -72,6 +73,7 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
rhsState: getRhsState(state),
|
rhsState: getRhsState(state),
|
||||||
shouldShowAppBar: shouldShowAppBar(state),
|
shouldShowAppBar: shouldShowAppBar(state),
|
||||||
isCloud: isCurrentLicenseCloud(state),
|
isCloud: isCurrentLicenseCloud(state),
|
||||||
|
isDevModeEnabled: isDevModeEnabled(state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,6 +121,7 @@ describe('components/Root', () => {
|
|||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
} as unknown as RouteComponentProps['history'],
|
} as unknown as RouteComponentProps['history'],
|
||||||
} as RouteComponentProps,
|
} as RouteComponentProps,
|
||||||
|
isDevModeEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let originalMatchMedia: (query: string) => MediaQueryList;
|
let originalMatchMedia: (query: string) => MediaQueryList;
|
||||||
|
@ -79,6 +79,7 @@ const TeamSidebar = makeAsyncComponent('TeamSidebar', lazy(() => import('compone
|
|||||||
const SidebarRight = makeAsyncComponent('SidebarRight', lazy(() => import('components/sidebar_right')));
|
const SidebarRight = makeAsyncComponent('SidebarRight', lazy(() => import('components/sidebar_right')));
|
||||||
const ModalController = makeAsyncComponent('ModalController', lazy(() => import('components/modal_controller')));
|
const ModalController = makeAsyncComponent('ModalController', lazy(() => import('components/modal_controller')));
|
||||||
const AppBar = makeAsyncComponent('AppBar', lazy(() => import('components/app_bar/app_bar')));
|
const AppBar = makeAsyncComponent('AppBar', lazy(() => import('components/app_bar/app_bar')));
|
||||||
|
const ComponentLibrary = makeAsyncComponent('ComponentLibrary', lazy(() => import('components/component_library')));
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
@ -440,6 +441,12 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
path={'/landing'}
|
path={'/landing'}
|
||||||
component={LinkingLandingPage}
|
component={LinkingLandingPage}
|
||||||
/>
|
/>
|
||||||
|
{this.props.isDevModeEnabled && (
|
||||||
|
<Route
|
||||||
|
path={'/component_library'}
|
||||||
|
component={ComponentLibrary}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Route
|
<Route
|
||||||
path={'/admin_console'}
|
path={'/admin_console'}
|
||||||
>
|
>
|
||||||
|
Loading…
Reference in New Issue
Block a user