mattermost-mobile/app/components/autocomplete_selector/index.tsx
Javier Aguirre ac8a18bbfb
[MM-43844][MM-42809] Integration Selector (#6716)
* Activating screens

* Registering the screen

* Adding default themes for components

* Porting items rows, WIP

* WIP Custom List

* Pasting old code to integration_selector, WIP

* No TS errors on components

* Adding selector options

* fix types

* Adding state with hooks

* Page loading with no results

* fix search timeout

* Getting channels, not painting them yet

* searching for profiles

* tuning user and channel remote calls

* Fix radioButton error

* channels being loaded

* rendering options

* Rendering users

* Preparing search results

* Added onPress events for everybody!

* single select working for all selectors

* Remove dirty empty data fix

* remove unused data on custom list row

* fic touchableOpacity styling

* Adding extra info to userlistRow

* Search results (channels and users)

* filter options!

* Adding i18n

* Adding username as name

* move code to effects

* fix typing onRow

* multiselect selection working, missing a "Done" button

* commenting out the selector icons, moving selected options to func

* Added button for multiselect submit

* Fixing data types on selector

* 💄 data sources check

* cleaning custom_list_row

* Fix onLoadMore bug

* ordering setLoading

* eslinting all the things

* more eslint

* multiselect

* fix autocomplete format

* FIx eslint

* fix renderIcon

* fix section type

* actions not being used

* now we have user avatars

* Fix icon checks on multiselect

* handling select for multiple selections

* Moving to its respective folders

* components should render

* Added some test cases

* Multiple fixes from @mickmister feedback

* changing lock icon to padlock on channel row

* Fix children lint errors

* fix useEffect function eslint error

* Adding useCallback to profiles, channels and multiselections

* Fixing @larkox suggestions

* type checking fixes

* Fix onLoadMore

* Multiple hook and functionality fixes

* 🔥 extraData and setting loading channels better

* fix teammate display

* Fix multiselect button selection

* Fix returning selection to autocomplete selector

* Using typography

* Updating snapshots due to typography changes

* removing UserListRow, modifying the existing one

* Extract key for data sources

* Multiselect selection refactor

* fix setNext loop

* refactoring searchprofiles and channels

* Using refs for next and page

* Callback and other fixes

* Multiple fixes

* Add callback to multiselect selected items

* integration selector fixes

* Filter option search

* fix useCallback, timeout

* Remove initial page, fix selection data type

Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2022-11-23 11:17:47 +01:00

235 lines
8.5 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import React, {useCallback, useEffect, useState} from 'react';
import {IntlShape, useIntl} from 'react-intl';
import {Text, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import Footer from '@components/settings/footer';
import Label from '@components/settings/label';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {Screens, View as ViewConstants} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
import {getChannelById} from '@queries/servers/channel';
import {getUserById, observeTeammateNameDisplay} from '@queries/servers/user';
import {goToScreen} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {displayUsername} from '@utils/user';
import type {WithDatabaseArgs} from '@typings/database/database';
type Selection = DialogOption | Channel | UserProfile | DialogOption[] | Channel[] | UserProfile[];
type AutoCompleteSelectorProps = {
dataSource?: string;
disabled?: boolean;
errorText?: string;
getDynamicOptions?: (userInput?: string) => Promise<DialogOption[]>;
helpText?: string;
label?: string;
onSelected?: (value: string | string[]) => void;
optional?: boolean;
options?: PostActionOption[];
placeholder?: string;
roundedBorders?: boolean;
selected?: string | string[];
showRequiredAsterisk?: boolean;
teammateNameDisplay: string;
isMultiselect?: boolean;
testID: string;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const input = {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.1),
backgroundColor: changeOpacity(theme.centerChannelBg, 0.9),
paddingLeft: 10,
paddingRight: 30,
paddingVertical: 7,
height: 40,
};
return {
container: {
width: '100%',
marginBottom: 2,
marginRight: 8,
marginTop: 10,
},
roundedInput: {
...input,
borderRadius: 5,
},
input,
dropdownPlaceholder: {
top: 3,
marginLeft: 5,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
dropdownSelected: {
top: 3,
marginLeft: 5,
color: theme.centerChannelColor,
},
icon: {
position: 'absolute',
top: 13,
right: 12,
},
disabled: {
opacity: 0.5,
},
};
});
async function getItemName(serverUrl: string, selected: string, teammateNameDisplay: string, intl: IntlShape, dataSource?: string, options?: PostActionOption[]) {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
switch (dataSource) {
case ViewConstants.DATA_SOURCE_USERS: {
if (!database) {
return intl.formatMessage({id: 'channel_loader.someone', defaultMessage: 'Someone'});
}
const user = await getUserById(database, selected);
return displayUsername(user, intl.locale, teammateNameDisplay, true);
}
case ViewConstants.DATA_SOURCE_CHANNELS: {
if (!database) {
return intl.formatMessage({id: 'autocomplete_selector.unknown_channel', defaultMessage: 'Unknown channel'});
}
const channel = await getChannelById(database, selected);
return channel?.displayName || intl.formatMessage({id: 'autocomplete_selector.unknown_channel', defaultMessage: 'Unknown channel'});
}
default:
return options?.find((o) => o.value === selected)?.text || selected;
}
}
function getTextAndValueFromSelectedItem(item: DialogOption | Channel | UserProfile, teammateNameDisplay: string, locale: string, dataSource?: string) {
if (dataSource === ViewConstants.DATA_SOURCE_USERS) {
const user = item as UserProfile;
return {text: displayUsername(user, locale, teammateNameDisplay), value: user.id};
} else if (dataSource === ViewConstants.DATA_SOURCE_CHANNELS) {
const channel = item as Channel;
return {text: channel.display_name, value: channel.id};
}
const option = item as DialogOption;
return option;
}
function AutoCompleteSelector({
dataSource, disabled = false, errorText, getDynamicOptions, helpText, label, onSelected, optional = false,
options, placeholder, roundedBorders = true, selected, teammateNameDisplay, isMultiselect = false, testID,
}: AutoCompleteSelectorProps) {
const intl = useIntl();
const theme = useTheme();
const [itemText, setItemText] = useState('');
const style = getStyleSheet(theme);
const title = placeholder || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
const serverUrl = useServerUrl();
const goToSelectorScreen = useCallback(preventDoubleTap(() => {
const screen = Screens.INTEGRATION_SELECTOR;
goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect, teammateNameDisplay});
}), [dataSource, options, getDynamicOptions]);
const handleSelect = useCallback((item?: Selection) => {
if (!item) {
return;
}
if (!Array.isArray(item)) {
const {text: selectedText, value: selectedValue} = getTextAndValueFromSelectedItem(item, teammateNameDisplay, intl.locale, dataSource);
setItemText(selectedText);
if (onSelected) {
onSelected(selectedValue);
}
return;
}
const allSelectedTexts = [];
const allSelectedValues = [];
for (const i of item) {
const {text: selectedText, value: selectedValue} = getTextAndValueFromSelectedItem(i, teammateNameDisplay, intl.locale, dataSource);
allSelectedTexts.push(selectedText);
allSelectedValues.push(selectedValue);
}
setItemText(allSelectedTexts.join(', '));
if (onSelected) {
onSelected(allSelectedValues);
}
}, [teammateNameDisplay, intl, dataSource]);
// Handle the text for the default value.
useEffect(() => {
if (!selected) {
return;
}
if (!Array.isArray(selected)) {
getItemName(serverUrl, selected, teammateNameDisplay, intl, dataSource, options).then((res) => setItemText(res));
return;
}
const namePromises = [];
for (const item of selected) {
namePromises.push(getItemName(serverUrl, item, teammateNameDisplay, intl, dataSource, options));
}
Promise.all(namePromises).then((names) => {
setItemText(names.join(', '));
});
}, []);
return (
<View style={style.container}>
{Boolean(label) && (
<Label
label={label!}
optional={optional}
testID={testID}
/>
)}
<TouchableWithFeedback
disabled={disabled}
onPress={goToSelectorScreen}
style={disabled ? style.disabled : null}
type='opacity'
>
<View style={roundedBorders ? style.roundedInput : style.input}>
<Text
numberOfLines={1}
style={itemText ? style.dropdownSelected : style.dropdownPlaceholder}
>
{itemText || title}
</Text>
<CompassIcon
name='chevron-down'
color={changeOpacity(theme.centerChannelColor, 0.5)}
style={style.icon}
/>
</View>
</TouchableWithFeedback>
<Footer
disabled={disabled}
helpText={helpText}
errorText={errorText}
/>
</View>
);
}
const withTeammateNameDisplay = withObservables([], ({database}: WithDatabaseArgs) => {
return {
teammateNameDisplay: observeTeammateNameDisplay(database),
};
});
export default withDatabase(withTeammateNameDisplay(AutoCompleteSelector));