Add the Component Library to the app (#8319)
* Add the Component Library to the app * fix i18n --------- Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
parent
46f1cf3969
commit
c8f42ee67e
10 changed files with 454 additions and 2 deletions
|
|
@ -19,6 +19,7 @@ export const CONVERT_GM_TO_CHANNEL = 'ConvertGMToChannel';
|
|||
export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage';
|
||||
export const CREATE_OR_EDIT_CHANNEL = 'CreateOrEditChannel';
|
||||
export const CREATE_TEAM = 'CreateTeam';
|
||||
export const COMPONENT_LIBRARY = 'ComponentLibrary';
|
||||
export const CUSTOM_STATUS = 'CustomStatus';
|
||||
export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter';
|
||||
export const DRAFT = 'Draft';
|
||||
|
|
@ -98,6 +99,7 @@ export default {
|
|||
CHANNEL_NOTIFICATION_PREFERENCES,
|
||||
CODE,
|
||||
CONVERT_GM_TO_CHANNEL,
|
||||
COMPONENT_LIBRARY,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
CREATE_OR_EDIT_CHANNEL,
|
||||
CREATE_TEAM,
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ const ThemeProvider = ({currentTeamId, children, themes}: Props) => {
|
|||
return (<Provider value={theme}>{children}</Provider>);
|
||||
};
|
||||
|
||||
export const CustomThemeProvider = ({theme, children}: {theme: Theme; children: React.ReactNode}) => {
|
||||
return (<Provider value={theme}>{children}</Provider>);
|
||||
};
|
||||
|
||||
export function withTheme<T extends WithThemeProps>(Component: ComponentType<T>): ComponentType<T> {
|
||||
return function ThemeComponent(props) {
|
||||
return (
|
||||
|
|
|
|||
65
app/screens/component_library/button.cl.tsx
Normal file
65
app/screens/component_library/button.cl.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {Alert, View} from 'react-native';
|
||||
|
||||
import Button from '@components/button';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
import {useBooleanProp, useDropdownProp, useStringProp} from './hooks';
|
||||
import {buildComponent} from './utils';
|
||||
|
||||
const propPossibilities = {};
|
||||
|
||||
const buttonSizeValues = ['xs', 's', 'm', 'lg'];
|
||||
const buttonEmphasisValues = ['primary', 'secondary', 'tertiary', 'link'];
|
||||
const buttonTypeValues = ['default', 'destructive', 'inverted', 'disabled'];
|
||||
const buttonStateValues = ['default', 'hover', 'active', 'focus'];
|
||||
|
||||
const onPress = () => Alert.alert('Button pressed!');
|
||||
|
||||
const ButtonComponentLibrary = () => {
|
||||
const theme = useTheme();
|
||||
const [text, textSelector] = useStringProp('text', 'Some text', false);
|
||||
const [disabled, disabledSelector] = useBooleanProp('disabled', false);
|
||||
const [buttonSize, buttonSizePosibilities, buttonSizeSelector] = useDropdownProp('size', 'm', buttonSizeValues, true);
|
||||
const [buttonEmphasis, buttonEmphasisPosibilities, buttonEmphasisSelector] = useDropdownProp('emphasis', 'primary', buttonEmphasisValues, true);
|
||||
const [buttonType, buttonTypePosibilities, buttonTypeSelector] = useDropdownProp('buttonType', 'default', buttonTypeValues, true);
|
||||
const [buttonState, buttonStatePosibilities, buttonStateSelector] = useDropdownProp('buttonState', 'default', buttonStateValues, true);
|
||||
|
||||
const components = useMemo(
|
||||
() => buildComponent(Button, propPossibilities, [
|
||||
buttonSizePosibilities,
|
||||
buttonEmphasisPosibilities,
|
||||
buttonTypePosibilities,
|
||||
buttonStatePosibilities,
|
||||
], [
|
||||
text,
|
||||
disabled,
|
||||
buttonSize,
|
||||
buttonEmphasis,
|
||||
buttonType,
|
||||
buttonState,
|
||||
{
|
||||
theme,
|
||||
onPress,
|
||||
},
|
||||
]),
|
||||
[buttonEmphasis, buttonEmphasisPosibilities, buttonSize, buttonSizePosibilities, buttonState, buttonStatePosibilities, buttonType, buttonTypePosibilities, disabled, text, theme],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{textSelector}
|
||||
{disabledSelector}
|
||||
{buttonSizeSelector}
|
||||
{buttonEmphasisSelector}
|
||||
{buttonTypeSelector}
|
||||
{buttonStateSelector}
|
||||
<View>{components}</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonComponentLibrary;
|
||||
107
app/screens/component_library/hooks.tsx
Normal file
107
app/screens/component_library/hooks.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
|
||||
import AutocompleteSelector from '@components/autocomplete_selector';
|
||||
import BoolSetting from '@components/settings/bool_setting';
|
||||
import TextSetting from '@components/settings/text_setting';
|
||||
|
||||
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 selector = useMemo(() => (
|
||||
<TextSetting
|
||||
label={propName}
|
||||
multiline={isTextarea}
|
||||
disabled={false}
|
||||
keyboardType='default'
|
||||
onChange={setValue}
|
||||
optional={false}
|
||||
secureTextEntry={false}
|
||||
testID={`${propName}.input`}
|
||||
value={value}
|
||||
/>
|
||||
), [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 selector = useMemo(() => (
|
||||
<BoolSetting
|
||||
onChange={setValue}
|
||||
testID={`${propName}.input`}
|
||||
value={value}
|
||||
label={propName}
|
||||
/>
|
||||
), [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((v: SelectedDialogOption) => {
|
||||
if (!v) {
|
||||
setValue(defaultValue);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(v)) {
|
||||
setValue(v[0].value);
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(v.value);
|
||||
}, [defaultValue]);
|
||||
|
||||
const renderedOptions = useMemo(() => {
|
||||
const toReturn: DialogOption[] = options.map((v) => ({
|
||||
value: v,
|
||||
text: v,
|
||||
}));
|
||||
if (allowAll) {
|
||||
toReturn.unshift({
|
||||
value: ALL_OPTION,
|
||||
text: ALL_OPTION,
|
||||
});
|
||||
}
|
||||
return toReturn;
|
||||
}, [options, allowAll]);
|
||||
const selector = useMemo(() => (
|
||||
<AutocompleteSelector
|
||||
testID={`${propName}.input`}
|
||||
label={propName}
|
||||
onSelected={onChange}
|
||||
options={renderedOptions}
|
||||
selected={value}
|
||||
/>
|
||||
), [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];
|
||||
};
|
||||
145
app/screens/component_library/index.tsx
Normal file
145
app/screens/component_library/index.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
import {ScrollView, View, type StyleProp, type ViewStyle} from 'react-native';
|
||||
|
||||
import AutocompleteSelector from '@components/autocomplete_selector';
|
||||
import {Preferences} from '@constants';
|
||||
import {CustomThemeProvider} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
|
||||
import ButtonComponentLibrary from './button.cl';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
const componentMap = {
|
||||
Button: ButtonComponentLibrary,
|
||||
};
|
||||
|
||||
type ComponentName = keyof typeof componentMap
|
||||
const defaultComponent = Object.keys(componentMap)[0] as ComponentName;
|
||||
const componentOptions = Object.keys(componentMap).map((v) => ({
|
||||
value: v,
|
||||
text: v,
|
||||
}));
|
||||
|
||||
type ThemeName = keyof typeof Preferences.THEMES;
|
||||
const defaultTheme = Object.keys(Preferences.THEMES)[0] as ThemeName;
|
||||
const themeOptions = Object.keys(Preferences.THEMES).map((v) => ({
|
||||
value: v,
|
||||
text: v,
|
||||
}));
|
||||
|
||||
type BackgroundType = 'center' | 'sidebar';
|
||||
const backgroundOptions = [{
|
||||
value: 'center',
|
||||
text: 'Center channel',
|
||||
}, {
|
||||
value: 'sidebar',
|
||||
text: 'Sidebar background',
|
||||
}];
|
||||
|
||||
type Props = {
|
||||
componentId: AvailableScreens;
|
||||
};
|
||||
|
||||
const ComponentLibrary = ({componentId}: Props) => {
|
||||
const [selectedComponent, setSelectedComponent] = useState<ComponentName>(defaultComponent);
|
||||
const onSelectComponent = useCallback((value: SelectedDialogOption) => {
|
||||
if (!value) {
|
||||
setSelectedComponent(defaultComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
setSelectedComponent(value[0].value as ComponentName);
|
||||
return;
|
||||
}
|
||||
setSelectedComponent(value.value as ComponentName);
|
||||
}, []);
|
||||
|
||||
const [selectedTheme, setSelectedTheme] = useState(defaultTheme);
|
||||
const onSelectTheme = useCallback((value: SelectedDialogOption) => {
|
||||
if (!value) {
|
||||
setSelectedTheme(defaultTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
setSelectedTheme(value[0].value as ThemeName);
|
||||
return;
|
||||
}
|
||||
setSelectedTheme(value.value as ThemeName);
|
||||
}, []);
|
||||
|
||||
const [selectedBackground, setSelectedBackground] = useState<BackgroundType>('center');
|
||||
const onSelectBackground = useCallback((value: SelectedDialogOption) => {
|
||||
if (!value) {
|
||||
setSelectedBackground('center');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
setSelectedBackground(value[0].value as BackgroundType);
|
||||
return;
|
||||
}
|
||||
setSelectedBackground(value.value as BackgroundType);
|
||||
}, []);
|
||||
|
||||
const backgroundStyle: StyleProp<ViewStyle> = useMemo(() => {
|
||||
const theme = Preferences.THEMES[selectedTheme];
|
||||
switch (selectedBackground) {
|
||||
case 'center':
|
||||
return {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
};
|
||||
case 'sidebar':
|
||||
default:
|
||||
return {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
};
|
||||
}
|
||||
}, [selectedBackground, selectedTheme]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
popTopScreen(componentId);
|
||||
}, [componentId]);
|
||||
|
||||
useAndroidHardwareBackHandler(componentId, close);
|
||||
|
||||
const SelectedComponent = componentMap[selectedComponent];
|
||||
return (
|
||||
<ScrollView style={{margin: 10}}>
|
||||
<AutocompleteSelector
|
||||
testID='selectedComponent'
|
||||
label='Component'
|
||||
onSelected={onSelectComponent}
|
||||
selected={selectedComponent}
|
||||
options={componentOptions}
|
||||
/>
|
||||
<AutocompleteSelector
|
||||
testID='selectedTheme'
|
||||
label='Theme'
|
||||
onSelected={onSelectTheme}
|
||||
selected={selectedTheme}
|
||||
options={themeOptions}
|
||||
/>
|
||||
<AutocompleteSelector
|
||||
testID='selectedBackground'
|
||||
label='Background'
|
||||
onSelected={onSelectBackground}
|
||||
selected={selectedBackground}
|
||||
options={backgroundOptions}
|
||||
/>
|
||||
<View style={backgroundStyle}>
|
||||
<CustomThemeProvider theme={Preferences.THEMES[selectedTheme]}>
|
||||
<SelectedComponent/>
|
||||
</CustomThemeProvider>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentLibrary;
|
||||
81
app/screens/component_library/utils.tsx
Normal file
81
app/screens/component_library/utils.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
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((<>{v}{`: ${inputProps[v]}, `}</>));
|
||||
});
|
||||
return <Text>{result}</Text>;
|
||||
}
|
||||
|
||||
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) && <Text key={`text.${propString}`}>{propString}</Text>}
|
||||
<Component
|
||||
key={propString}
|
||||
{...builtSetProps}
|
||||
{...v}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
|
@ -99,6 +99,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.CREATE_OR_EDIT_CHANNEL:
|
||||
screen = withServerDatabase(require('@screens/create_or_edit_channel').default);
|
||||
break;
|
||||
case Screens.COMPONENT_LIBRARY:
|
||||
screen = withServerDatabase(require('@screens/component_library').default);
|
||||
break;
|
||||
case Screens.CUSTOM_STATUS:
|
||||
screen = withServerDatabase(require('@screens/custom_status').default);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ import {Alert, TouchableOpacity} from 'react-native';
|
|||
import SettingContainer from '@components/settings/container';
|
||||
import SettingOption from '@components/settings/option';
|
||||
import SettingSeparator from '@components/settings/separator';
|
||||
import {Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {goToScreen, popTopScreen} from '@screens/navigation';
|
||||
import {deleteFileCache, getAllFilesInCachesDirectory, getFormattedFileSize} from '@utils/file';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -32,8 +33,12 @@ const EMPTY_FILES: FileInfo[] = [];
|
|||
|
||||
type AdvancedSettingsProps = {
|
||||
componentId: AvailableScreens;
|
||||
isDevMode: boolean;
|
||||
};
|
||||
const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => {
|
||||
const AdvancedSettings = ({
|
||||
componentId,
|
||||
isDevMode,
|
||||
}: AdvancedSettingsProps) => {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
|
|
@ -77,6 +82,13 @@ const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => {
|
|||
}
|
||||
});
|
||||
|
||||
const onPressComponentLibrary = () => {
|
||||
const screen = Screens.COMPONENT_LIBRARY;
|
||||
const title = intl.formatMessage({id: 'settings.advanced_settings.component_library', defaultMessage: 'Component library'});
|
||||
|
||||
goToScreen(screen, title);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getAllCachedFiles();
|
||||
}, []);
|
||||
|
|
@ -107,6 +119,19 @@ const AdvancedSettings = ({componentId}: AdvancedSettingsProps) => {
|
|||
/>
|
||||
<SettingSeparator/>
|
||||
</TouchableOpacity>
|
||||
{isDevMode && (
|
||||
<TouchableOpacity
|
||||
onPress={onPressComponentLibrary}
|
||||
>
|
||||
<SettingOption
|
||||
containerStyle={styles.itemStyle}
|
||||
label={intl.formatMessage({id: 'settings.advanced.component_library', defaultMessage: 'Component library'})}
|
||||
testID='advanced_settings.component_library.option'
|
||||
type='none'
|
||||
/>
|
||||
<SettingSeparator/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</SettingContainer>
|
||||
);
|
||||
};
|
||||
18
app/screens/settings/advanced/index.ts
Normal file
18
app/screens/settings/advanced/index.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
|
||||
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
|
||||
import AdvancedSettings from './advanced';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
return {
|
||||
isDevMode: observeConfigBooleanValue(database, 'EnableDeveloper', false),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(AdvancedSettings));
|
||||
|
|
@ -1075,7 +1075,9 @@
|
|||
"settings.about.server.version.title": "Server Version:",
|
||||
"settings.about.server.version.value": "{version} (Build {buildNumber})",
|
||||
"settings.advanced_settings": "Advanced Settings",
|
||||
"settings.advanced_settings.component_library": "Component library",
|
||||
"settings.advanced.cancel": "Cancel",
|
||||
"settings.advanced.component_library": "Component library",
|
||||
"settings.advanced.delete": "Delete",
|
||||
"settings.advanced.delete_data": "Delete local files",
|
||||
"settings.advanced.delete_message.confirmation": "\nThis will delete all files downloaded through the app for this server. Please confirm to proceed.\n",
|
||||
|
|
|
|||
Loading…
Reference in a new issue