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 {loadRecentlyUsedCustomEmojis, migrateRecentEmojis} from 'actions/emoji_actions';
|
||||
import {isDevModeEnabled} from 'selectors/general';
|
||||
import {getShowLaunchingWorkspace} from 'selectors/onboarding';
|
||||
import {shouldShowAppBar} from 'selectors/plugins';
|
||||
import {
|
||||
@ -72,6 +73,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
rhsState: getRhsState(state),
|
||||
shouldShowAppBar: shouldShowAppBar(state),
|
||||
isCloud: isCurrentLicenseCloud(state),
|
||||
isDevModeEnabled: isDevModeEnabled(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -121,6 +121,7 @@ describe('components/Root', () => {
|
||||
push: jest.fn(),
|
||||
} as unknown as RouteComponentProps['history'],
|
||||
} as RouteComponentProps,
|
||||
isDevModeEnabled: false,
|
||||
};
|
||||
|
||||
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 ModalController = makeAsyncComponent('ModalController', lazy(() => import('components/modal_controller')));
|
||||
const AppBar = makeAsyncComponent('AppBar', lazy(() => import('components/app_bar/app_bar')));
|
||||
const ComponentLibrary = makeAsyncComponent('ComponentLibrary', lazy(() => import('components/component_library')));
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
@ -440,6 +441,12 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
path={'/landing'}
|
||||
component={LinkingLandingPage}
|
||||
/>
|
||||
{this.props.isDevModeEnabled && (
|
||||
<Route
|
||||
path={'/component_library'}
|
||||
component={ComponentLibrary}
|
||||
/>
|
||||
)}
|
||||
<Route
|
||||
path={'/admin_console'}
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user