forked from Ivasoft/mattermost-mobile
[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>
This commit is contained in:
@@ -137,7 +137,7 @@ function AutoCompleteSelector({
|
||||
|
||||
const goToSelectorScreen = useCallback(preventDoubleTap(() => {
|
||||
const screen = Screens.INTEGRATION_SELECTOR;
|
||||
goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect});
|
||||
goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect, teammateNameDisplay});
|
||||
}), [dataSource, options, getDynamicOptions]);
|
||||
|
||||
const handleSelect = useCallback((item?: Selection) => {
|
||||
|
||||
@@ -32,6 +32,7 @@ type Props = {
|
||||
onPress?: (user: UserProfile) => void;
|
||||
onLongPress: (user: UserProfile) => void;
|
||||
selectable: boolean;
|
||||
disabled?: boolean;
|
||||
selected: boolean;
|
||||
tutorialWatched?: boolean;
|
||||
}
|
||||
@@ -105,6 +106,7 @@ export default function UserListRow({
|
||||
onLongPress,
|
||||
tutorialWatched = false,
|
||||
selectable,
|
||||
disabled,
|
||||
selected,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
@@ -154,7 +156,11 @@ export default function UserListRow({
|
||||
}, [onLongPress, user]);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
const iconOpacity = DEFAULT_ICON_OPACITY * (selectable ? 1 : DISABLED_OPACITY);
|
||||
if (!selectable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iconOpacity = DEFAULT_ICON_OPACITY * (disabled ? 1 : DISABLED_OPACITY);
|
||||
const color = selected ? theme.buttonBg : changeOpacity(theme.centerChannelColor, iconOpacity);
|
||||
return (
|
||||
<View style={style.selector}>
|
||||
@@ -165,7 +171,7 @@ export default function UserListRow({
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}, [selectable, selected, theme]);
|
||||
}, [selectable, disabled, selected, theme]);
|
||||
|
||||
let usernameDisplay = `@${username}`;
|
||||
if (isMyUser) {
|
||||
@@ -179,7 +185,7 @@ export default function UserListRow({
|
||||
const showTeammateDisplay = teammateDisplay !== username;
|
||||
|
||||
const userItemTestID = `${testID}.${id}`;
|
||||
const opacity = selectable || selected ? 1 : DISABLED_OPACITY;
|
||||
const opacity = selectable || selected || !disabled ? 1 : DISABLED_OPACITY;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -159,6 +159,4 @@ export const NOT_READY = [
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_MENTION,
|
||||
CREATE_TEAM,
|
||||
INTEGRATION_SELECTOR,
|
||||
INTERACTIVE_DIALOG,
|
||||
];
|
||||
|
||||
@@ -124,6 +124,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
case Screens.INTERACTIVE_DIALOG:
|
||||
screen = withServerDatabase(require('@screens/interactive_dialog').default);
|
||||
break;
|
||||
case Screens.INTEGRATION_SELECTOR:
|
||||
screen = withServerDatabase(require('@screens/integration_selector').default);
|
||||
break;
|
||||
case Screens.IN_APP_NOTIFICATION: {
|
||||
const notificationScreen = require('@screens/in_app_notification').default;
|
||||
Navigation.registerComponent(Screens.IN_APP_NOTIFICATION, () =>
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/integration_selector/channel_list_row should match snapshot with delete_at filled in 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 65,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="1234"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.1234"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="archive-outline"
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
"marginLeft": 5,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.display_name"
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/integration_selector/channel_list_row should match snapshot with open channel icon 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 65,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="1234"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.1234"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="circle-multiple-outline"
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
"marginLeft": 5,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.display_name"
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/integration_selector/channel_list_row should match snapshot with private channel icon 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 65,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="1234"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.1234"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="circle-multiple-outline"
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
"marginLeft": 5,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.display_name"
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/integration_selector/channel_list_row should match snapshot with purpose filled in 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 65,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="1234"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.1234"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="circle-multiple-outline"
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
"marginLeft": 5,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.display_name"
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "rgba(63,67,80,0.5)",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 20,
|
||||
"marginTop": 7,
|
||||
}
|
||||
}
|
||||
>
|
||||
My purpose
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/integration_selector/channel_list_row should match snapshot with shared filled in 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 65,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="1234"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.1234"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name="circle-multiple-outline"
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
"marginLeft": 5,
|
||||
}
|
||||
}
|
||||
testID="ChannelListRow.display_name"
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
158
app/screens/integration_selector/channel_list_row/index.test.tsx
Normal file
158
app/screens/integration_selector/channel_list_row/index.test.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
|
||||
import {Preferences} from '@app/constants';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import ChannelListRow from '.';
|
||||
|
||||
describe('components/integration_selector/channel_list_row', () => {
|
||||
let database: Database;
|
||||
const channel: Channel = {
|
||||
id: '1',
|
||||
create_at: 1111,
|
||||
update_at: 1111,
|
||||
delete_at: 0,
|
||||
team_id: 'my team',
|
||||
type: 'O',
|
||||
display_name: 'channel',
|
||||
name: 'channel',
|
||||
header: 'channel',
|
||||
purpose: '',
|
||||
last_post_at: 1,
|
||||
total_msg_count: 1,
|
||||
extra_update_at: 1,
|
||||
creator_id: '1',
|
||||
scheme_id: null,
|
||||
group_constrained: null,
|
||||
shared: true,
|
||||
};
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase();
|
||||
database = server.database;
|
||||
});
|
||||
|
||||
it('should match snapshot with open channel icon', () => {
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelListRow
|
||||
id='1234'
|
||||
theme={Preferences.THEMES.denim}
|
||||
channel={channel}
|
||||
selected={false}
|
||||
selectable={false}
|
||||
enabled={true}
|
||||
testID='ChannelListRow'
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
>
|
||||
<br/>
|
||||
</ChannelListRow>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with private channel icon', () => {
|
||||
channel.type = 'P';
|
||||
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelListRow
|
||||
id='1234'
|
||||
theme={Preferences.THEMES.denim}
|
||||
channel={channel}
|
||||
selected={false}
|
||||
selectable={false}
|
||||
enabled={true}
|
||||
testID='ChannelListRow'
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
>
|
||||
<br/>
|
||||
</ChannelListRow>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with delete_at filled in', () => {
|
||||
channel.delete_at = 1111;
|
||||
channel.shared = false;
|
||||
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelListRow
|
||||
id='1234'
|
||||
theme={Preferences.THEMES.denim}
|
||||
channel={channel}
|
||||
testID='ChannelListRow'
|
||||
enabled={true}
|
||||
selectable={false}
|
||||
selected={false}
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
>
|
||||
<br/>
|
||||
</ChannelListRow>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with shared filled in', () => {
|
||||
channel.delete_at = 0;
|
||||
channel.shared = true;
|
||||
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelListRow
|
||||
id='1234'
|
||||
theme={Preferences.THEMES.denim}
|
||||
channel={channel}
|
||||
testID='ChannelListRow'
|
||||
enabled={true}
|
||||
selectable={false}
|
||||
selected={false}
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
>
|
||||
<br/>
|
||||
</ChannelListRow>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with purpose filled in', () => {
|
||||
channel.purpose = 'My purpose';
|
||||
|
||||
const wrapper = renderWithEverything(
|
||||
<ChannelListRow
|
||||
id='1234'
|
||||
theme={Preferences.THEMES.denim}
|
||||
channel={channel}
|
||||
testID='ChannelListRow'
|
||||
enabled={true}
|
||||
selectable={false}
|
||||
selected={false}
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
>
|
||||
<br/>
|
||||
</ChannelListRow>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
137
app/screens/integration_selector/channel_list_row/index.tsx
Normal file
137
app/screens/integration_selector/channel_list_row/index.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {typography} from '@app/utils/typography';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
import CustomListRow, {Props as CustomListRowProps} from '../custom_list_row';
|
||||
|
||||
type ChannelListRowProps = {
|
||||
id: string;
|
||||
theme: object;
|
||||
channel: Channel;
|
||||
onPress: (item: Channel) => void;
|
||||
};
|
||||
|
||||
type Props = ChannelListRowProps & CustomListRowProps;
|
||||
|
||||
const getIconForChannel = (selectedChannel: Channel): string => {
|
||||
let icon = 'globe';
|
||||
|
||||
if (selectedChannel.type === 'P') {
|
||||
icon = 'padlock';
|
||||
}
|
||||
|
||||
if (selectedChannel.delete_at) {
|
||||
icon = 'archive-outline';
|
||||
} else if (selectedChannel.shared) {
|
||||
icon = 'circle-multiple-outline';
|
||||
}
|
||||
|
||||
return icon;
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
displayName: {
|
||||
...typography('Body', 200, 'Regular'),
|
||||
color: theme.centerChannelColor,
|
||||
marginLeft: 5,
|
||||
},
|
||||
icon: {
|
||||
...typography('Body', 200, 'Regular'),
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
outerContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 15,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
purpose: {
|
||||
marginTop: 7,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
...typography('Body', 100, 'Regular'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ChannelListRow = ({
|
||||
onPress, id, theme, channel, testID, enabled, selectable, selected,
|
||||
}: Props) => {
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const onPressRow = useCallback((): void => {
|
||||
onPress(channel);
|
||||
}, [onPress, channel]);
|
||||
|
||||
const renderPurpose = (channelPurpose: string): JSX.Element | null => {
|
||||
if (!channelPurpose) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={style.purpose}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{channel.purpose}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const itemTestID = `${testID}.${id}`;
|
||||
const channelDisplayNameTestID = `${testID}.display_name`;
|
||||
const channelIcon = getIconForChannel(channel);
|
||||
|
||||
return (
|
||||
<View style={style.outerContainer}>
|
||||
<CustomListRow
|
||||
id={id}
|
||||
onPress={onPressRow}
|
||||
enabled={enabled}
|
||||
selectable={selectable}
|
||||
selected={selected}
|
||||
testID={testID}
|
||||
>
|
||||
<View
|
||||
style={style.container}
|
||||
testID={itemTestID}
|
||||
>
|
||||
<View style={style.titleContainer}>
|
||||
<CompassIcon
|
||||
name={channelIcon}
|
||||
style={style.icon}
|
||||
/>
|
||||
<Text
|
||||
style={style.displayName}
|
||||
testID={channelDisplayNameTestID}
|
||||
>
|
||||
{channel.display_name}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{renderPurpose(channel.purpose)}
|
||||
</View>
|
||||
</CustomListRow>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelListRow;
|
||||
68
app/screens/integration_selector/custom_list/index.test.tsx
Normal file
68
app/screens/integration_selector/custom_list/index.test.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import {Preferences} from '@app/constants';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import CustomList, {FLATLIST} from '.';
|
||||
|
||||
describe('components/integration_selector/custom_list', () => {
|
||||
let database: Database;
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase();
|
||||
database = server.database;
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const channel: Channel = {
|
||||
id: '1',
|
||||
create_at: 1111,
|
||||
update_at: 1111,
|
||||
delete_at: 1111,
|
||||
team_id: 'my team',
|
||||
type: 'O',
|
||||
display_name: 'channel',
|
||||
name: 'channel',
|
||||
header: 'channel',
|
||||
purpose: 'channel',
|
||||
last_post_at: 1,
|
||||
total_msg_count: 1,
|
||||
extra_update_at: 1,
|
||||
creator_id: '1',
|
||||
scheme_id: null,
|
||||
group_constrained: null,
|
||||
shared: true,
|
||||
};
|
||||
const wrapper = renderWithEverything(
|
||||
<CustomList
|
||||
data={[channel]}
|
||||
key='custom_list'
|
||||
listType={FLATLIST}
|
||||
loading={false}
|
||||
theme={Preferences.THEMES.denim}
|
||||
testID='ChannelListRow'
|
||||
noResults={() => {
|
||||
return <Text>{'No Results'}</Text>;
|
||||
}}
|
||||
onLoadMore={() => {
|
||||
// noop
|
||||
}}
|
||||
onRowPress={() => {
|
||||
// noop
|
||||
}}
|
||||
renderItem={(props: object): JSX.Element => {
|
||||
return (<Text>{props.toString()}</Text>);
|
||||
}}
|
||||
loadingComponent={null}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
231
app/screens/integration_selector/custom_list/index.tsx
Normal file
231
app/screens/integration_selector/custom_list/index.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useCallback} from 'react';
|
||||
import {
|
||||
Text, Platform, FlatList, RefreshControl, View, SectionList,
|
||||
} from 'react-native';
|
||||
|
||||
import {typography} from '@app/utils/typography';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
export const FLATLIST = 'flat';
|
||||
export const SECTIONLIST = 'section';
|
||||
const INITIAL_BATCH_TO_RENDER = 15;
|
||||
|
||||
type UserProfileSection = {
|
||||
id: string;
|
||||
data: UserProfile[];
|
||||
};
|
||||
type DataType = DialogOption[] | Channel[] | UserProfile[] | UserProfileSection[];
|
||||
type ListItemProps = {
|
||||
id: string;
|
||||
item: DialogOption | Channel | UserProfile;
|
||||
selected: boolean;
|
||||
selectable?: boolean;
|
||||
enabled: boolean;
|
||||
onPress: (item: DialogOption) => void;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
data: DataType;
|
||||
canRefresh?: boolean;
|
||||
listType?: string;
|
||||
loading?: boolean;
|
||||
loadingComponent?: React.ReactElement<any, string> | null;
|
||||
noResults: () => JSX.Element | null;
|
||||
refreshing?: boolean;
|
||||
onRefresh?: () => void;
|
||||
onLoadMore: () => void;
|
||||
onRowPress: (item: UserProfile | Channel | DialogOption) => void;
|
||||
renderItem: (props: ListItemProps) => JSX.Element;
|
||||
selectable?: boolean;
|
||||
theme?: object;
|
||||
shouldRenderSeparator?: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const keyExtractor = (item: any): string => {
|
||||
return item.id || item.key || item.value || item;
|
||||
};
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
list: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
}),
|
||||
},
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
},
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
}),
|
||||
},
|
||||
loadingText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
},
|
||||
searching: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
sectionContainer: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.07),
|
||||
paddingLeft: 10,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
sectionWrapper: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
sectionText: {
|
||||
fontWeight: '600',
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
noResultContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
noResultText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
...typography('Body', 600, 'Regular'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function CustomList({
|
||||
data, shouldRenderSeparator, listType, loading, loadingComponent, noResults,
|
||||
onLoadMore, onRowPress, selectable, renderItem, theme,
|
||||
canRefresh = true, testID, refreshing = false, onRefresh,
|
||||
}: Props) {
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
// Renders
|
||||
const renderEmptyList = useCallback(() => {
|
||||
return noResults || null;
|
||||
}, [noResults]);
|
||||
|
||||
const renderSeparator = useCallback(() => {
|
||||
if (!shouldRenderSeparator) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.separator}/>
|
||||
);
|
||||
}, [shouldRenderSeparator, style]);
|
||||
|
||||
const renderListItem = useCallback(({item}: any) => {
|
||||
const props: ListItemProps = {
|
||||
id: item.key,
|
||||
item,
|
||||
selected: item.selected,
|
||||
selectable,
|
||||
enabled: true,
|
||||
onPress: onRowPress,
|
||||
};
|
||||
|
||||
if ('disableSelect' in item) {
|
||||
props.enabled = !item.disableSelect;
|
||||
}
|
||||
|
||||
return renderItem(props);
|
||||
}, [onRowPress, selectable, renderItem]);
|
||||
|
||||
const renderFooter = useCallback((): React.ReactElement<any, string> | null => {
|
||||
if (!loading || !loadingComponent) {
|
||||
return null;
|
||||
}
|
||||
return loadingComponent;
|
||||
}, [loading, loadingComponent]);
|
||||
|
||||
const renderSectionHeader = useCallback(({section}: any) => {
|
||||
return (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.sectionContainer}>
|
||||
<Text style={style.sectionText}>{section.id}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}, [style]);
|
||||
|
||||
const renderSectionList = () => {
|
||||
return (
|
||||
<SectionList
|
||||
contentContainerStyle={style.container}
|
||||
keyExtractor={keyExtractor}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
ItemSeparatorComponent={renderSeparator}
|
||||
ListEmptyComponent={renderEmptyList()}
|
||||
ListFooterComponent={renderFooter}
|
||||
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
|
||||
onEndReached={onLoadMore}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={renderListItem}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
scrollEventThrottle={60}
|
||||
sections={data as UserProfileSection[]}
|
||||
style={style.list}
|
||||
stickySectionHeadersEnabled={false}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFlatList = () => {
|
||||
let refreshControl;
|
||||
if (canRefresh) {
|
||||
refreshControl = (
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={style.container}
|
||||
data={data}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={keyExtractor}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
ItemSeparatorComponent={renderSeparator}
|
||||
ListEmptyComponent={renderEmptyList()}
|
||||
ListFooterComponent={renderFooter}
|
||||
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
|
||||
onEndReached={onLoadMore}
|
||||
refreshControl={refreshControl}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={renderListItem}
|
||||
scrollEventThrottle={60}
|
||||
style={style.list}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (listType === FLATLIST) {
|
||||
return renderFlatList();
|
||||
}
|
||||
|
||||
return renderSectionList();
|
||||
}
|
||||
|
||||
export default CustomList;
|
||||
@@ -0,0 +1,84 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/integration_selector/custom_list_row should match snapshot 1`] = `
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 65,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"height": 50,
|
||||
"justifyContent": "center",
|
||||
"paddingRight": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
[
|
||||
{
|
||||
"alignItems": "center",
|
||||
"borderColor": "rgba(61, 60, 64, 0.32)",
|
||||
"borderRadius": 14,
|
||||
"borderWidth": 1,
|
||||
"height": 28,
|
||||
"justifyContent": "center",
|
||||
"width": 28,
|
||||
},
|
||||
{
|
||||
"backgroundColor": "#166DE0",
|
||||
"borderWidth": 0,
|
||||
},
|
||||
false,
|
||||
]
|
||||
}
|
||||
testID="1"
|
||||
>
|
||||
<Icon
|
||||
color="#fff"
|
||||
name="check"
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="1"
|
||||
>
|
||||
<View>
|
||||
<View>
|
||||
<Icon
|
||||
name="globe"
|
||||
/>
|
||||
<Text>
|
||||
My channel
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@app/components/compass_icon';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import CustomListRow from '.';
|
||||
|
||||
describe('components/integration_selector/custom_list_row', () => {
|
||||
let database: Database;
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase();
|
||||
database = server.database;
|
||||
});
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = renderWithEverything(
|
||||
<CustomListRow
|
||||
id='1'
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
enabled={true}
|
||||
selectable={true}
|
||||
selected={true}
|
||||
>
|
||||
<View>
|
||||
<View>
|
||||
<CompassIcon
|
||||
name={'globe'}
|
||||
/>
|
||||
<Text>
|
||||
{'My channel'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CustomListRow>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
100
app/screens/integration_selector/custom_list_row/index.tsx
Normal file
100
app/screens/integration_selector/custom_list_row/index.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
GestureResponderEvent,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import CompassIcon from '@app/components/compass_icon';
|
||||
|
||||
export type Props = {
|
||||
id: string;
|
||||
onPress?: (event: GestureResponderEvent) => void;
|
||||
enabled: boolean;
|
||||
selectable: boolean;
|
||||
selected: boolean;
|
||||
children: React.ReactNode;
|
||||
testID?: string;
|
||||
};
|
||||
|
||||
const style = StyleSheet.create({
|
||||
touchable: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: 65,
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
children: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
selector: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(61, 60, 64, 0.32)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
selectorContainer: {
|
||||
height: 50,
|
||||
paddingRight: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
selectorDisabled: {
|
||||
borderColor: 'rgba(61, 60, 64, 0.16)',
|
||||
},
|
||||
selectorFilled: {
|
||||
backgroundColor: '#166DE0',
|
||||
borderWidth: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const CustomListRow = ({
|
||||
onPress, children, enabled, selectable, selected, id, testID,
|
||||
}: Props) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={style.container}
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
>
|
||||
{selectable &&
|
||||
<View style={style.selectorContainer}>
|
||||
<View
|
||||
testID={id}
|
||||
style={[style.selector, (selected && style.selectorFilled), (!enabled &&
|
||||
style.selectorDisabled)]}
|
||||
>
|
||||
{selected &&
|
||||
<CompassIcon
|
||||
name='check'
|
||||
size={24}
|
||||
color='#fff'
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
|
||||
<View
|
||||
testID={id}
|
||||
style={style.children}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomListRow;
|
||||
622
app/screens/integration_selector/index.tsx
Normal file
622
app/screens/integration_selector/index.tsx
Normal file
@@ -0,0 +1,622 @@
|
||||
// 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, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {fetchChannels, searchChannels} from '@actions/remote/channel';
|
||||
import {fetchProfiles, searchProfiles} from '@actions/remote/user';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import SearchBar from '@components/search';
|
||||
import UserListRow from '@components/user_list_row';
|
||||
import {General, View as ViewConstants} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {
|
||||
buildNavigationButton,
|
||||
popTopScreen, setButtons,
|
||||
} from '@screens/navigation';
|
||||
import {filterChannelsMatchingTerm} from '@utils/channel';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {filterProfilesMatchingTerm} from '@utils/user';
|
||||
|
||||
import {createProfilesSections} from '../create_direct_message/user_list';
|
||||
|
||||
import ChannelListRow from './channel_list_row';
|
||||
import CustomList, {FLATLIST, SECTIONLIST} from './custom_list';
|
||||
import OptionListRow from './option_list_row';
|
||||
import SelectedOptions from './selected_options';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
type DataType = DialogOption[] | Channel[] | UserProfile[];
|
||||
type Selection = DialogOption | Channel | UserProfile | DataType;
|
||||
type MultiselectSelectedMap = Dictionary<DialogOption> | Dictionary<Channel> | Dictionary<UserProfile>;
|
||||
type UserProfileSection = {
|
||||
id: string;
|
||||
data: UserProfile[];
|
||||
};
|
||||
|
||||
const VALID_DATASOURCES = [
|
||||
ViewConstants.DATA_SOURCE_CHANNELS,
|
||||
ViewConstants.DATA_SOURCE_USERS,
|
||||
ViewConstants.DATA_SOURCE_DYNAMIC];
|
||||
const SUBMIT_BUTTON_ID = 'submit-integration-selector-multiselect';
|
||||
|
||||
const close = () => {
|
||||
popTopScreen();
|
||||
};
|
||||
|
||||
const extractItemKey = (dataSource: string, item: Selection): string => {
|
||||
switch (dataSource) {
|
||||
case ViewConstants.DATA_SOURCE_USERS: {
|
||||
const typedItem = item as UserProfile;
|
||||
return typedItem.id;
|
||||
}
|
||||
case ViewConstants.DATA_SOURCE_CHANNELS: {
|
||||
const typedItem = item as Channel;
|
||||
return typedItem.id;
|
||||
}
|
||||
default: {
|
||||
const typedItem = item as DialogOption;
|
||||
return typedItem.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const filterSearchData = (source: string, searchData: DataType, searchTerm: string) => {
|
||||
if (!searchData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lowerCasedTerm = searchTerm.toLowerCase();
|
||||
|
||||
if (source === ViewConstants.DATA_SOURCE_USERS) {
|
||||
return filterProfilesMatchingTerm(searchData as UserProfile[], lowerCasedTerm);
|
||||
} else if (source === ViewConstants.DATA_SOURCE_CHANNELS) {
|
||||
return filterChannelsMatchingTerm(searchData as Channel[], lowerCasedTerm);
|
||||
} else if (source === ViewConstants.DATA_SOURCE_DYNAMIC) {
|
||||
return searchData;
|
||||
}
|
||||
|
||||
return (searchData as DialogOption[]).filter((option) => option.text && option.text.includes(lowerCasedTerm));
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
getDynamicOptions?: (userInput?: string) => Promise<DialogOption[]>;
|
||||
options?: PostActionOption[];
|
||||
currentTeamId: string;
|
||||
data?: DataType;
|
||||
dataSource: string;
|
||||
handleSelect: (opt: Selection) => void;
|
||||
isMultiselect?: boolean;
|
||||
selected?: DialogOption[];
|
||||
theme: Theme;
|
||||
teammateNameDisplay: string;
|
||||
componentId: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
searchBar: {
|
||||
marginVertical: 5,
|
||||
height: 38,
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
height: 70,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
},
|
||||
noResultContainer: {
|
||||
flexGrow: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
noResultText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
...typography('Body', 600, 'Regular'),
|
||||
},
|
||||
searchBarInput: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
flex: 0,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function IntegrationSelector(
|
||||
{dataSource, data, isMultiselect = false, selected, handleSelect,
|
||||
currentTeamId, componentId, getDynamicOptions, options, teammateNameDisplay}: Props) {
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
const searchTimeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||
const style = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
|
||||
// HOOKS
|
||||
const [integrationData, setIntegrationData] = useState<DataType>(data || []);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [term, setTerm] = useState<string>('');
|
||||
const [searchResults, setSearchResults] = useState<DataType>([]);
|
||||
const [multiselectSelected, setMultiselectSelected] = useState<MultiselectSelectedMap>({});
|
||||
const [customListData, setCustomListData] = useState<DataType | UserProfileSection[]>([]);
|
||||
|
||||
const page = useRef<number>(-1);
|
||||
const next = useRef<boolean>(VALID_DATASOURCES.includes(dataSource));
|
||||
|
||||
// Callbacks
|
||||
const clearSearch = useCallback(() => {
|
||||
setTerm('');
|
||||
setSearchResults([]);
|
||||
}, []);
|
||||
|
||||
// This is the button to submit multiselect options
|
||||
const rightButton = useMemo(() => {
|
||||
const base = buildNavigationButton(
|
||||
SUBMIT_BUTTON_ID,
|
||||
'integration_selector.multiselect.submit.button',
|
||||
undefined,
|
||||
intl.formatMessage({id: 'integration_selector.multiselect.submit', defaultMessage: 'Done'}),
|
||||
);
|
||||
base.enabled = true;
|
||||
base.showAsAction = 'always';
|
||||
base.color = theme.sidebarHeaderTextColor;
|
||||
return base;
|
||||
}, [theme.sidebarHeaderTextColor, intl]);
|
||||
|
||||
const handleSelectItem = useCallback((item: Selection) => {
|
||||
if (!isMultiselect) {
|
||||
handleSelect(item);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
const itemKey = extractItemKey(dataSource, item);
|
||||
const currentSelected: Dictionary<UserProfile> | Dictionary<DialogOption> | Dictionary<Channel> = multiselectSelected;
|
||||
const multiselectSelectedItems = {...currentSelected};
|
||||
|
||||
switch (dataSource) {
|
||||
case ViewConstants.DATA_SOURCE_USERS: {
|
||||
if (currentSelected[itemKey]) {
|
||||
delete multiselectSelectedItems[itemKey];
|
||||
} else {
|
||||
multiselectSelectedItems[itemKey] = item as UserProfile;
|
||||
}
|
||||
|
||||
setMultiselectSelected(multiselectSelectedItems);
|
||||
return;
|
||||
}
|
||||
case ViewConstants.DATA_SOURCE_CHANNELS: {
|
||||
if (currentSelected[itemKey]) {
|
||||
delete multiselectSelectedItems[itemKey];
|
||||
} else {
|
||||
multiselectSelectedItems[itemKey] = item as Channel;
|
||||
}
|
||||
|
||||
setMultiselectSelected(multiselectSelectedItems);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
if (currentSelected[itemKey]) {
|
||||
delete multiselectSelectedItems[itemKey];
|
||||
} else {
|
||||
multiselectSelectedItems[itemKey] = item as DialogOption;
|
||||
}
|
||||
setMultiselectSelected(multiselectSelectedItems);
|
||||
}
|
||||
}
|
||||
}, [integrationData, multiselectSelected, isMultiselect, dataSource, handleSelect]);
|
||||
|
||||
const handleRemoveOption = useCallback((item: UserProfile | Channel | DialogOption) => {
|
||||
const currentSelected: Dictionary<UserProfile> | Dictionary<DialogOption> | Dictionary<Channel> = multiselectSelected;
|
||||
const itemKey = extractItemKey(dataSource, item);
|
||||
const multiselectSelectedItems = {...currentSelected};
|
||||
delete multiselectSelectedItems[itemKey];
|
||||
setMultiselectSelected(multiselectSelectedItems);
|
||||
}, [dataSource, multiselectSelected]);
|
||||
|
||||
const getChannels = useCallback(debounce(async () => {
|
||||
if (next.current && !loading && !term) {
|
||||
setLoading(true);
|
||||
page.current += 1;
|
||||
|
||||
const {channels: channelData} = await fetchChannels(serverUrl, currentTeamId, page.current);
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (channelData && channelData.length > 0) {
|
||||
setIntegrationData([...integrationData as Channel[], ...channelData]);
|
||||
} else {
|
||||
next.current = false;
|
||||
}
|
||||
}
|
||||
}, 100), [loading, term, serverUrl, currentTeamId, integrationData]);
|
||||
|
||||
const getProfiles = useCallback(debounce(async () => {
|
||||
if (next.current && !loading && !term) {
|
||||
setLoading(true);
|
||||
page.current += 1;
|
||||
|
||||
const {users: profiles} = await fetchProfiles(serverUrl, page.current);
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (profiles && profiles.length > 0) {
|
||||
setIntegrationData([...integrationData as UserProfile[], ...profiles]);
|
||||
} else {
|
||||
next.current = false;
|
||||
}
|
||||
}
|
||||
}, 100), [loading, term, integrationData]);
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (dataSource === ViewConstants.DATA_SOURCE_USERS) {
|
||||
await getProfiles();
|
||||
} else if (dataSource === ViewConstants.DATA_SOURCE_CHANNELS) {
|
||||
await getChannels();
|
||||
}
|
||||
|
||||
// dynamic options are not paged so are not reloaded on scroll
|
||||
}, [getProfiles, getChannels, dataSource]);
|
||||
|
||||
const searchDynamicOptions = useCallback(async (searchTerm = '') => {
|
||||
if (options && options !== integrationData && !searchTerm) {
|
||||
setIntegrationData(options);
|
||||
}
|
||||
|
||||
if (!getDynamicOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results: DialogOption[] = await getDynamicOptions(searchTerm.toLowerCase());
|
||||
const searchData = results || [];
|
||||
|
||||
if (searchTerm) {
|
||||
setSearchResults(searchData);
|
||||
} else {
|
||||
setIntegrationData(searchData);
|
||||
}
|
||||
}, [options, getDynamicOptions, integrationData]);
|
||||
|
||||
const onHandleMultiselectSubmit = useCallback(() => {
|
||||
handleSelect(getMultiselectData(multiselectSelected));
|
||||
close();
|
||||
}, [multiselectSelected, handleSelect]);
|
||||
|
||||
const onSearch = useCallback((text: string) => {
|
||||
if (!text) {
|
||||
clearSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
setTerm(text);
|
||||
|
||||
if (searchTimeoutId.current) {
|
||||
clearTimeout(searchTimeoutId.current);
|
||||
}
|
||||
|
||||
searchTimeoutId.current = setTimeout(async () => {
|
||||
if (!dataSource) {
|
||||
setSearchResults(filterSearchData('', integrationData, text));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
if (dataSource === ViewConstants.DATA_SOURCE_USERS) {
|
||||
const {data: userData} = await searchProfiles(
|
||||
serverUrl, text.toLowerCase(),
|
||||
{team_id: currentTeamId, allow_inactive: true});
|
||||
|
||||
if (userData) {
|
||||
setSearchResults(userData);
|
||||
}
|
||||
} else if (dataSource === ViewConstants.DATA_SOURCE_CHANNELS) {
|
||||
const isSearch = true;
|
||||
const {channels: receivedChannels} = await searchChannels(
|
||||
serverUrl, text, currentTeamId, isSearch);
|
||||
|
||||
if (receivedChannels) {
|
||||
setSearchResults(receivedChannels);
|
||||
}
|
||||
} else if (dataSource === ViewConstants.DATA_SOURCE_DYNAMIC) {
|
||||
await searchDynamicOptions(text);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
}, [dataSource, integrationData, currentTeamId]);
|
||||
|
||||
const getMultiselectData = useCallback((multiselectSelectedElems: MultiselectSelectedMap): Selection => {
|
||||
let myItems;
|
||||
let multiselectItems: Selection = [];
|
||||
|
||||
switch (dataSource) {
|
||||
case ViewConstants.DATA_SOURCE_USERS:
|
||||
myItems = multiselectSelectedElems as Dictionary<UserProfile>;
|
||||
multiselectItems = multiselectItems as UserProfile[];
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const index in myItems) {
|
||||
multiselectItems.push(myItems[index]);
|
||||
}
|
||||
return multiselectItems;
|
||||
case ViewConstants.DATA_SOURCE_CHANNELS:
|
||||
myItems = multiselectSelectedElems as Dictionary<Channel>;
|
||||
multiselectItems = multiselectItems as Channel[];
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const index in myItems) {
|
||||
multiselectItems.push(myItems[index]);
|
||||
}
|
||||
return multiselectItems;
|
||||
default:
|
||||
myItems = multiselectSelectedElems as Dictionary<DialogOption>;
|
||||
multiselectItems = multiselectItems as DialogOption[];
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const index in myItems) {
|
||||
multiselectItems.push(myItems[index]);
|
||||
}
|
||||
return multiselectItems;
|
||||
}
|
||||
}, [multiselectSelected, dataSource]);
|
||||
|
||||
// Effects
|
||||
useNavButtonPressed(SUBMIT_BUTTON_ID, componentId, onHandleMultiselectSubmit, [onHandleMultiselectSubmit]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (searchTimeoutId.current) {
|
||||
clearTimeout(searchTimeoutId.current);
|
||||
searchTimeoutId.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataSource === ViewConstants.DATA_SOURCE_USERS) {
|
||||
getProfiles();
|
||||
} else if (dataSource === ViewConstants.DATA_SOURCE_CHANNELS) {
|
||||
getChannels();
|
||||
} else {
|
||||
// Static and dynamic option search
|
||||
searchDynamicOptions('');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let listData: (DataType | UserProfileSection[]) = integrationData;
|
||||
|
||||
if (term) {
|
||||
listData = searchResults;
|
||||
}
|
||||
|
||||
if (dataSource === ViewConstants.DATA_SOURCE_USERS) {
|
||||
listData = createProfilesSections(listData as UserProfile[]);
|
||||
}
|
||||
|
||||
if (dataSource === ViewConstants.DATA_SOURCE_DYNAMIC) {
|
||||
listData = (integrationData as DialogOption[]).filter((option) => option.text && option.text.toLowerCase().includes(term));
|
||||
}
|
||||
|
||||
setCustomListData(listData);
|
||||
}, [searchResults, integrationData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMultiselect) {
|
||||
return;
|
||||
}
|
||||
|
||||
setButtons(componentId, {
|
||||
rightButtons: [rightButton],
|
||||
});
|
||||
}, [rightButton, componentId, isMultiselect]);
|
||||
|
||||
useEffect(() => {
|
||||
const multiselectItems: MultiselectSelectedMap = {};
|
||||
|
||||
if (multiselectSelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMultiselect && selected && !([ViewConstants.DATA_SOURCE_USERS, ViewConstants.DATA_SOURCE_CHANNELS].includes(dataSource))) {
|
||||
selected.forEach((opt) => {
|
||||
multiselectItems[opt.value] = opt;
|
||||
});
|
||||
|
||||
setMultiselectSelected(multiselectItems);
|
||||
}
|
||||
}, [multiselectSelected]);
|
||||
|
||||
// Renders
|
||||
const renderLoading = useCallback(() => {
|
||||
if (!loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let text;
|
||||
switch (dataSource) {
|
||||
case ViewConstants.DATA_SOURCE_USERS:
|
||||
text = {
|
||||
id: intl.formatMessage({id: 'mobile.integration_selector.loading_users'}),
|
||||
defaultMessage: 'Loading Users...',
|
||||
};
|
||||
break;
|
||||
case ViewConstants.DATA_SOURCE_CHANNELS:
|
||||
text = {
|
||||
id: intl.formatMessage({id: 'mobile.integration_selector.loading_channels'}),
|
||||
defaultMessage: 'Loading Channels...',
|
||||
};
|
||||
break;
|
||||
default:
|
||||
text = {
|
||||
id: intl.formatMessage({id: 'mobile.integration_selector.loading_options'}),
|
||||
defaultMessage: 'Loading Options...',
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.loadingContainer}>
|
||||
<FormattedText
|
||||
{...text}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}, [style, dataSource, loading, intl]);
|
||||
|
||||
const renderNoResults = useCallback((): JSX.Element | null => {
|
||||
if (loading || page.current === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}, [loading, style]);
|
||||
|
||||
const renderChannelItem = useCallback((itemProps: any) => {
|
||||
const itemSelected = Boolean(multiselectSelected[itemProps.item.id]);
|
||||
return (
|
||||
<ChannelListRow
|
||||
key={itemProps.id}
|
||||
{...itemProps}
|
||||
|
||||
theme={theme}
|
||||
channel={itemProps.item as Channel}
|
||||
selectable={isMultiselect || false}
|
||||
selected={itemSelected}
|
||||
/>
|
||||
);
|
||||
}, [multiselectSelected, theme, isMultiselect]);
|
||||
|
||||
const renderOptionItem = useCallback((itemProps: any) => {
|
||||
const itemSelected = Boolean(multiselectSelected[itemProps.item.value]);
|
||||
return (
|
||||
<OptionListRow
|
||||
key={itemProps.id}
|
||||
{...itemProps}
|
||||
theme={theme}
|
||||
selectable={isMultiselect}
|
||||
selected={itemSelected}
|
||||
/>
|
||||
);
|
||||
}, [multiselectSelected, theme, isMultiselect]);
|
||||
|
||||
const renderUserItem = useCallback((itemProps: any): JSX.Element => {
|
||||
const itemSelected = Boolean(multiselectSelected[itemProps.item.id]);
|
||||
|
||||
return (
|
||||
<UserListRow
|
||||
key={itemProps.id}
|
||||
{...itemProps}
|
||||
theme={theme}
|
||||
selectable={isMultiselect}
|
||||
user={itemProps.item}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
selected={itemSelected}
|
||||
/>
|
||||
);
|
||||
}, [multiselectSelected, theme, isMultiselect, teammateNameDisplay]);
|
||||
|
||||
const getRenderItem = (): (itemProps: any) => JSX.Element => {
|
||||
switch (dataSource) {
|
||||
case ViewConstants.DATA_SOURCE_USERS:
|
||||
return renderUserItem;
|
||||
case ViewConstants.DATA_SOURCE_CHANNELS:
|
||||
return renderChannelItem;
|
||||
default:
|
||||
return renderOptionItem;
|
||||
}
|
||||
};
|
||||
|
||||
const renderSelectedOptions = useCallback((): React.ReactElement<any, string> | null => {
|
||||
const selectedItems: any = Object.values(multiselectSelected);
|
||||
|
||||
if (!selectedItems.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SelectedOptions
|
||||
theme={theme}
|
||||
selectedOptions={selectedItems}
|
||||
dataSource={dataSource}
|
||||
onRemove={handleRemoveOption}
|
||||
/>
|
||||
<View style={style.separator}/>
|
||||
</>
|
||||
);
|
||||
}, [multiselectSelected, style, theme]);
|
||||
|
||||
const listType = dataSource === ViewConstants.DATA_SOURCE_USERS ? SECTIONLIST : FLATLIST;
|
||||
const selectedOptionsComponent = renderSelectedOptions();
|
||||
|
||||
return (
|
||||
<SafeAreaView style={style.container}>
|
||||
<View
|
||||
testID='integration_selector.screen'
|
||||
style={style.searchBar}
|
||||
>
|
||||
<SearchBar
|
||||
testID='selector.search_bar'
|
||||
placeholder={intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
inputStyle={style.searchBarInput}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
onChangeText={onSearch}
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{selectedOptionsComponent}
|
||||
|
||||
<CustomList
|
||||
data={customListData as DataType}
|
||||
key='custom_list'
|
||||
listType={listType}
|
||||
loading={loading}
|
||||
loadingComponent={renderLoading()}
|
||||
noResults={renderNoResults}
|
||||
onLoadMore={loadMore}
|
||||
onRowPress={handleSelectItem}
|
||||
renderItem={getRenderItem()}
|
||||
theme={theme}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const withTeamId = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
}));
|
||||
|
||||
export default withDatabase(withTeamId(IntegrationSelector));
|
||||
@@ -0,0 +1,74 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/integration_selector/option_list_row should match snapshot for option 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#ffffff",
|
||||
"flexDirection": "row",
|
||||
"height": 65,
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 65,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
testID="1"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
"justifyContent": "center",
|
||||
"marginLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 24,
|
||||
}
|
||||
}
|
||||
>
|
||||
my text
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
|
||||
import {Preferences} from '@app/constants';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import OptionListRow from '.';
|
||||
|
||||
describe('components/integration_selector/option_list_row', () => {
|
||||
let database: Database;
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase();
|
||||
database = server.database;
|
||||
});
|
||||
|
||||
it('should match snapshot for option', () => {
|
||||
const myItem = {
|
||||
value: '1',
|
||||
text: 'my text',
|
||||
};
|
||||
const wrapper = renderWithEverything(
|
||||
<OptionListRow
|
||||
enabled={true}
|
||||
selectable={false}
|
||||
selected={false}
|
||||
theme={Preferences.THEMES.denim}
|
||||
item={myItem}
|
||||
id='1'
|
||||
onPress={() => {
|
||||
// noop
|
||||
}}
|
||||
>
|
||||
<br/>
|
||||
</OptionListRow>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
77
app/screens/integration_selector/option_list_row/index.tsx
Normal file
77
app/screens/integration_selector/option_list_row/index.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {typography} from '@app/utils/typography';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import CustomListRow, {Props as CustomListRowProps} from '../custom_list_row';
|
||||
|
||||
type OptionListRowProps = {
|
||||
id: string;
|
||||
theme: object;
|
||||
item: { text: string; value: string };
|
||||
onPress: (item: DialogOption) => void;
|
||||
}
|
||||
|
||||
type Props = OptionListRowProps & CustomListRowProps;
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: 65,
|
||||
paddingHorizontal: 15,
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
textContainer: {
|
||||
marginLeft: 10,
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
},
|
||||
optionText: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Body', 200, 'Regular'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const OptionListRow = ({
|
||||
enabled, selectable, selected, theme, item, onPress, id,
|
||||
}: Props) => {
|
||||
const {text} = item;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const onPressRow = useCallback((): void => {
|
||||
onPress(item);
|
||||
}, [onPress, item]);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<CustomListRow
|
||||
id={id}
|
||||
onPress={onPressRow}
|
||||
enabled={enabled}
|
||||
selectable={selectable}
|
||||
selected={selected}
|
||||
>
|
||||
<View style={style.textContainer}>
|
||||
<View>
|
||||
<Text style={style.optionText}>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</CustomListRow>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptionListRow;
|
||||
@@ -0,0 +1,172 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/integration_selector/selected_option should match snapshot for channel 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(63,67,80,0.2)",
|
||||
"borderRadius": 3,
|
||||
"flexDirection": "row",
|
||||
"height": 27,
|
||||
"marginBottom": 4,
|
||||
"marginRight": 10,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 20,
|
||||
"maxWidth": "90%",
|
||||
}
|
||||
}
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="#3f4350"
|
||||
name="close"
|
||||
size={14}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/integration_selector/selected_option should match snapshot for option 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(63,67,80,0.2)",
|
||||
"borderRadius": 3,
|
||||
"flexDirection": "row",
|
||||
"height": 27,
|
||||
"marginBottom": 4,
|
||||
"marginRight": 10,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 20,
|
||||
"maxWidth": "90%",
|
||||
}
|
||||
}
|
||||
>
|
||||
my text
|
||||
</Text>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="#3f4350"
|
||||
name="close"
|
||||
size={14}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`components/integration_selector/selected_option should match snapshot for userProfile 1`] = `
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(63,67,80,0.2)",
|
||||
"borderRadius": 3,
|
||||
"flexDirection": "row",
|
||||
"height": 27,
|
||||
"marginBottom": 4,
|
||||
"marginRight": 10,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 20,
|
||||
"maxWidth": "90%",
|
||||
}
|
||||
}
|
||||
>
|
||||
johndoe
|
||||
</Text>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="#3f4350"
|
||||
name="close"
|
||||
size={14}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
117
app/screens/integration_selector/selected_option/index.test.tsx
Normal file
117
app/screens/integration_selector/selected_option/index.test.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
|
||||
import {Preferences} from '@app/constants';
|
||||
import {View as ViewConstants} from '@constants';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import SelectedOption from '.';
|
||||
|
||||
describe('components/integration_selector/selected_option', () => {
|
||||
let database: Database;
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase();
|
||||
database = server.database;
|
||||
});
|
||||
|
||||
it('should match snapshot for option', () => {
|
||||
const myItem = {
|
||||
value: '1',
|
||||
text: 'my text',
|
||||
};
|
||||
const wrapper = renderWithEverything(
|
||||
<SelectedOption
|
||||
theme={Preferences.THEMES.denim}
|
||||
option={myItem}
|
||||
dataSource={ViewConstants.DATA_SOURCE_DYNAMIC}
|
||||
onRemove={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot for userProfile', () => {
|
||||
const userProfile: UserProfile = {
|
||||
id: '1',
|
||||
create_at: 1111,
|
||||
update_at: 1111,
|
||||
delete_at: 0,
|
||||
username: 'johndoe',
|
||||
nickname: 'johndoe',
|
||||
first_name: 'johndoe',
|
||||
last_name: 'johndoe',
|
||||
position: 'hacker',
|
||||
roles: 'admin',
|
||||
locale: 'en_US',
|
||||
notify_props: {
|
||||
channel: 'true',
|
||||
comments: 'never',
|
||||
desktop: 'all',
|
||||
desktop_sound: 'true',
|
||||
email: 'true',
|
||||
first_name: 'true',
|
||||
mention_keys: 'false',
|
||||
push: 'mention',
|
||||
push_status: 'ooo',
|
||||
},
|
||||
email: 'johndoe@me.com',
|
||||
auth_service: 'dummy',
|
||||
};
|
||||
const wrapper = renderWithEverything(
|
||||
<SelectedOption
|
||||
theme={Preferences.THEMES.denim}
|
||||
option={userProfile}
|
||||
dataSource={ViewConstants.DATA_SOURCE_USERS}
|
||||
onRemove={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot for channel', () => {
|
||||
const channel: Channel = {
|
||||
id: '1',
|
||||
create_at: 1111,
|
||||
update_at: 1111,
|
||||
delete_at: 0,
|
||||
team_id: 'my team',
|
||||
type: 'O',
|
||||
display_name: 'channel',
|
||||
name: 'channel',
|
||||
header: 'channel',
|
||||
purpose: '',
|
||||
last_post_at: 1,
|
||||
total_msg_count: 1,
|
||||
extra_update_at: 1,
|
||||
creator_id: '1',
|
||||
scheme_id: null,
|
||||
group_constrained: null,
|
||||
shared: true,
|
||||
};
|
||||
const wrapper = renderWithEverything(
|
||||
<SelectedOption
|
||||
theme={Preferences.THEMES.denim}
|
||||
option={channel}
|
||||
dataSource={ViewConstants.DATA_SOURCE_CHANNELS}
|
||||
onRemove={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
88
app/screens/integration_selector/selected_option/index.tsx
Normal file
88
app/screens/integration_selector/selected_option/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {typography} from '@app/utils/typography';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {View as ViewConstants} from '@constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
option: DialogOption | UserProfile | Channel;
|
||||
dataSource: string;
|
||||
onRemove: (opt: DialogOption | UserProfile | Channel) => void;
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
height: 27,
|
||||
borderRadius: 3,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
marginBottom: 4,
|
||||
marginRight: 10,
|
||||
paddingLeft: 10,
|
||||
},
|
||||
remove: {
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
text: {
|
||||
color: theme.centerChannelColor,
|
||||
maxWidth: '90%',
|
||||
...typography('Body', 100, 'Regular'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const SelectedOption = ({theme, option, onRemove, dataSource}: Props) => {
|
||||
const style = getStyleFromTheme(theme);
|
||||
const onPress = useCallback(
|
||||
() => onRemove(option),
|
||||
[onRemove, option],
|
||||
);
|
||||
|
||||
let text;
|
||||
switch (dataSource) {
|
||||
case ViewConstants.DATA_SOURCE_USERS:
|
||||
text = (option as UserProfile).username;
|
||||
break;
|
||||
case ViewConstants.DATA_SOURCE_CHANNELS:
|
||||
text = (option as Channel).display_name;
|
||||
break;
|
||||
default:
|
||||
text = (option as DialogOption).text;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<Text
|
||||
style={style.text}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={style.remove}
|
||||
onPress={onPress}
|
||||
>
|
||||
<CompassIcon
|
||||
name='close'
|
||||
size={14}
|
||||
color={theme.centerChannelColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectedOption;
|
||||
@@ -0,0 +1,241 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/integration_selector/selected_options should match snapshot for channels 1`] = `
|
||||
<RCTScrollView
|
||||
style={
|
||||
{
|
||||
"flexGrow": 0,
|
||||
"marginBottom": 5,
|
||||
"marginLeft": 5,
|
||||
"maxHeight": 100,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "flex-start",
|
||||
"flexDirection": "row",
|
||||
"flexWrap": "wrap",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(63,67,80,0.2)",
|
||||
"borderRadius": 3,
|
||||
"flexDirection": "row",
|
||||
"height": 27,
|
||||
"marginBottom": 4,
|
||||
"marginRight": 10,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 20,
|
||||
"maxWidth": "90%",
|
||||
}
|
||||
}
|
||||
>
|
||||
channel
|
||||
</Text>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="#3f4350"
|
||||
name="close"
|
||||
size={14}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
`;
|
||||
|
||||
exports[`components/integration_selector/selected_options should match snapshot for options 1`] = `
|
||||
<RCTScrollView
|
||||
style={
|
||||
{
|
||||
"flexGrow": 0,
|
||||
"marginBottom": 5,
|
||||
"marginLeft": 5,
|
||||
"maxHeight": 100,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "flex-start",
|
||||
"flexDirection": "row",
|
||||
"flexWrap": "wrap",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(63,67,80,0.2)",
|
||||
"borderRadius": 3,
|
||||
"flexDirection": "row",
|
||||
"height": 27,
|
||||
"marginBottom": 4,
|
||||
"marginRight": 10,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 20,
|
||||
"maxWidth": "90%",
|
||||
}
|
||||
}
|
||||
>
|
||||
my text
|
||||
</Text>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="#3f4350"
|
||||
name="close"
|
||||
size={14}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
`;
|
||||
|
||||
exports[`components/integration_selector/selected_options should match snapshot for users 1`] = `
|
||||
<RCTScrollView
|
||||
style={
|
||||
{
|
||||
"flexGrow": 0,
|
||||
"marginBottom": 5,
|
||||
"marginLeft": 5,
|
||||
"maxHeight": 100,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "flex-start",
|
||||
"flexDirection": "row",
|
||||
"flexWrap": "wrap",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "rgba(63,67,80,0.2)",
|
||||
"borderRadius": 3,
|
||||
"flexDirection": "row",
|
||||
"height": 27,
|
||||
"marginBottom": 4,
|
||||
"marginRight": 10,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={
|
||||
{
|
||||
"color": "#3f4350",
|
||||
"fontFamily": "OpenSans",
|
||||
"fontSize": 14,
|
||||
"fontWeight": "400",
|
||||
"lineHeight": 20,
|
||||
"maxWidth": "90%",
|
||||
}
|
||||
}
|
||||
>
|
||||
johndoe
|
||||
</Text>
|
||||
<View
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
"paddingHorizontal": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
color="#3f4350"
|
||||
name="close"
|
||||
size={14}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
`;
|
||||
119
app/screens/integration_selector/selected_options/index.test.tsx
Normal file
119
app/screens/integration_selector/selected_options/index.test.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Database from '@nozbe/watermelondb/Database';
|
||||
import React from 'react';
|
||||
|
||||
import {Preferences} from '@app/constants';
|
||||
import {View as ViewConstants} from '@constants';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import SelectedOptions from '.';
|
||||
|
||||
describe('components/integration_selector/selected_options', () => {
|
||||
let database: Database;
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase();
|
||||
database = server.database;
|
||||
});
|
||||
|
||||
it('should match snapshot for users', () => {
|
||||
const userProfile: UserProfile = {
|
||||
id: '1',
|
||||
create_at: 1111,
|
||||
update_at: 1111,
|
||||
delete_at: 1111,
|
||||
username: 'johndoe',
|
||||
nickname: 'johndoe',
|
||||
first_name: 'johndoe',
|
||||
last_name: 'johndoe',
|
||||
position: 'hacker',
|
||||
roles: 'admin',
|
||||
locale: 'en_US',
|
||||
notify_props: {
|
||||
channel: 'true',
|
||||
comments: 'never',
|
||||
desktop: 'all',
|
||||
desktop_sound: 'true',
|
||||
email: 'true',
|
||||
first_name: 'true',
|
||||
mention_keys: 'false',
|
||||
push: 'mention',
|
||||
push_status: 'ooo',
|
||||
},
|
||||
email: 'johndoe@me.com',
|
||||
auth_service: 'dummy',
|
||||
};
|
||||
const wrapper = renderWithEverything(
|
||||
<SelectedOptions
|
||||
theme={Preferences.THEMES.denim}
|
||||
selectedOptions={[userProfile]}
|
||||
dataSource={ViewConstants.DATA_SOURCE_USERS}
|
||||
onRemove={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot for channels', () => {
|
||||
const channel: Channel = {
|
||||
id: '1',
|
||||
create_at: 1111,
|
||||
update_at: 1111,
|
||||
delete_at: 0,
|
||||
team_id: 'my team',
|
||||
type: 'O',
|
||||
display_name: 'channel',
|
||||
name: 'channel',
|
||||
header: 'channel',
|
||||
purpose: '',
|
||||
last_post_at: 1,
|
||||
total_msg_count: 1,
|
||||
extra_update_at: 1,
|
||||
creator_id: '1',
|
||||
scheme_id: null,
|
||||
group_constrained: null,
|
||||
shared: true,
|
||||
};
|
||||
|
||||
const wrapper = renderWithEverything(
|
||||
<SelectedOptions
|
||||
theme={Preferences.THEMES.denim}
|
||||
selectedOptions={[channel]}
|
||||
dataSource={ViewConstants.DATA_SOURCE_CHANNELS}
|
||||
onRemove={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot for options', () => {
|
||||
const myItem = {
|
||||
value: '1',
|
||||
text: 'my text',
|
||||
};
|
||||
|
||||
const wrapper = renderWithEverything(
|
||||
<SelectedOptions
|
||||
theme={Preferences.THEMES.denim}
|
||||
selectedOptions={[myItem]}
|
||||
dataSource={ViewConstants.DATA_SOURCE_DYNAMIC}
|
||||
onRemove={() => {
|
||||
// noop
|
||||
}}
|
||||
/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
79
app/screens/integration_selector/selected_options/index.tsx
Normal file
79
app/screens/integration_selector/selected_options/index.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';
|
||||
import {View, ScrollView} from 'react-native';
|
||||
|
||||
import {View as ViewConstants} from '@constants';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import SelectedOption from '../selected_option';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
selectedOptions: DialogOption[] | UserProfile[] | Channel[];
|
||||
dataSource: string;
|
||||
onRemove: (opt: DialogOption | UserProfile | Channel) => void;
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme(() => {
|
||||
return {
|
||||
container: {
|
||||
marginLeft: 5,
|
||||
marginBottom: 5,
|
||||
maxHeight: 100,
|
||||
flexGrow: 0,
|
||||
},
|
||||
users: {
|
||||
alignItems: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const SelectedOptions = ({
|
||||
theme, selectedOptions, onRemove, dataSource,
|
||||
}: Props) => {
|
||||
const style = getStyleFromTheme(theme);
|
||||
const options: React.ReactNode[] = selectedOptions.map((optionItem) => {
|
||||
let key: string;
|
||||
|
||||
switch (dataSource) {
|
||||
case ViewConstants.DATA_SOURCE_USERS:
|
||||
key = (optionItem as UserProfile).id;
|
||||
break;
|
||||
case ViewConstants.DATA_SOURCE_CHANNELS:
|
||||
key = (optionItem as Channel).id;
|
||||
break;
|
||||
default:
|
||||
key = (optionItem as DialogOption).value;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectedOption
|
||||
key={key}
|
||||
option={optionItem}
|
||||
theme={theme}
|
||||
dataSource={dataSource}
|
||||
onRemove={onRemove}
|
||||
/>);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-warning-comments
|
||||
// TODO Consider using a Virtualized List since the number of elements is potentially unbounded.
|
||||
// https://mattermost.atlassian.net/browse/MM-48420
|
||||
return (
|
||||
<ScrollView
|
||||
style={style.container}
|
||||
contentContainerStyle={style.scrollViewContent}
|
||||
>
|
||||
<View style={style.users}>
|
||||
{options}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectedOptions;
|
||||
@@ -142,3 +142,18 @@ export function compareNotifyProps(propsA: Partial<ChannelNotifyProps>, propsB:
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function filterChannelsMatchingTerm(channels: Channel[], term: string): Channel[] {
|
||||
const lowercasedTerm = term.toLowerCase();
|
||||
|
||||
return channels.filter((channel: Channel): boolean => {
|
||||
if (!channel) {
|
||||
return false;
|
||||
}
|
||||
const name = (channel.name || '').toLowerCase();
|
||||
const displayName = (channel.display_name || '').toLowerCase();
|
||||
|
||||
return name.startsWith(lowercasedTerm) ||
|
||||
displayName.startsWith(lowercasedTerm);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -448,6 +448,9 @@
|
||||
"mobile.files_paste.error_dismiss": "Dismiss",
|
||||
"mobile.files_paste.error_title": "Paste failed",
|
||||
"mobile.gallery.title": "{index} of {total}",
|
||||
"mobile.integration_selector.loading_users": "Loading users...",
|
||||
"mobile.integration_selector.loading_channels": "Loading channels...",
|
||||
"mobile.integration_selector.loading_options": "Loading options...",
|
||||
"mobile.ios.photos_permission_denied_description": "Upload photos and videos to your server or save them to your device. Open Settings to grant {applicationName} Read and Write access to your photo and video library.",
|
||||
"mobile.ios.photos_permission_denied_title": "{applicationName} would like to access your photos",
|
||||
"mobile.join_channel.error": "We couldn't join the channel {displayName}.",
|
||||
|
||||
Reference in New Issue
Block a user