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:
Daniel Espino García 2024-11-11 11:06:08 +01:00 committed by GitHub
parent ead2a7c018
commit 33005922c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 439 additions and 0 deletions

View File

@ -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);
}

View 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];
};

View 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;

View File

@ -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;

View 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;
}

View File

@ -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),
};
}

View File

@ -121,6 +121,7 @@ describe('components/Root', () => {
push: jest.fn(),
} as unknown as RouteComponentProps['history'],
} as RouteComponentProps,
isDevModeEnabled: false,
};
let originalMatchMedia: (query: string) => MediaQueryList;

View File

@ -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'}
>