Refactor apps modal autocomplete into AutocompleteSelector component (#5229)
* Apps bindings support (#5012) * Add redux related information * Add binding loading on channel refresh * Add channel header and post option bindings * Fix test * Minor fixes * Fix snapshots * Handle errors and show bindings only on main channel * Update Expand Levels keys and values to match latest changes * Add NAVIGATE call response handling. * Add more isolation to apps related code * Add defaults to send ephemeral * Update variable naming * Rename shouldProcessApps by a more meaningful appsEnabled * Embedded forms (#5169) * Add redux related information * Add binding loading on channel refresh * Add channel header and post option bindings * Fix test * Minor fixes * Fix snapshots * Handle errors and show bindings only on main channel * Update Expand Levels keys and values to match latest changes * Add NAVIGATE call response handling. * Add more isolation to apps related code * Add Embedded Forms * Fix snapshots * Add Embedded Forms * Improve naming, change the root element to be a binding and improve binding handling * Get post down on the buttons, remove unneeded variables and minor fixes from the review * Allow undefined bindings to fillBindingsInformation and add logging for error. * Address review feedback * Add App Forms (#5177) * Add App Forms * Address feedback and self review * Add dynamic select * Fixes and improvements * Add the ability to refresh on submit. * Use AppFormValue instead of redoing the type * Address feedback * [MM-31508] Rename URL to Path in Call (#5186) * Add user agent to call context (#5193) * Remove unneeded state reference (#5196) * Change user agent query on fetch bindings (#5201) * Add refresh websocket event to refetch bindings (#5194) * Add refresh websocket event to refetch bindings * Add missing changes * Declare socket event constant and separate the handler to a different function * Add binding validation on binding fetch (#5200) * Apps commands (#5107) * Add redux related information * Add binding loading on channel refresh * Add channel header and post option bindings * Fix test * Minor fixes * Fix snapshots * apps modals draft * Handle errors and show bindings only on main channel * Update Expand Levels keys and values to match latest changes * Add NAVIGATE call response handling. * Add more isolation to apps related code * reuse command parser throughout slash_suggestion component's lifecycle * fix prop and lint * using alert to show error message. need another way * duplicate import * types * types * types * rename file * copy webapp parser and test * dependencies moved. tests pass * move app command parser into its own folder * converted to typescript, all tests are passing * automated and manual tests work * lint * lint * remove fall throughs with blocks * types * doAppCall type * extract displayError to deps file * test types * lint * fix tests * unused import * PR feedback * fix imports and show errors * types * remove execute suggestion for mobile * watch feature flag * fix tests * change form text arugment behavior to show user input and not hint * return call response error in doAppCall * update tests to remove hint from text field suggestions * lint * use new base command structure * fix tests * wrap appsEnabled * typescript actions/command.ts * update app constants * PR feedback * fix error handling from doAppCall action * Use App CallRequest structure (#5212) * error handling * move test files * remove unused import Co-authored-by: Daniel Espino García <larkox@gmail.com> * Add feature flag (#5207) * Add feature flag * Simplify return * Add localization, call validation and use call type on subpath (#5221) * Add localization, call validation and use call type on subpath * Add more localization to the parser and bring fixes from webapp * Fix ephemerals and channel header / post options crashes * fix app command parser deps and alert messages * Improve suggestion handling * Fix test * Fix lint * Return errors as error * Address PR feedback * return error property * fix error string wordings Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * migrate app selector to AutocompleteSelector * add if statement to avoid missing prop * add tests for dynamic values * clean up * remove old files Co-authored-by: Daniel Espino García <larkox@gmail.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
This commit is contained in:
parent
d73d3460b4
commit
11a9590402
15 changed files with 176 additions and 1587 deletions
|
|
@ -73,13 +73,14 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
|
|||
};
|
||||
}
|
||||
|
||||
export function setAutocompleteSelector(dataSource, onSelect, options) {
|
||||
export function setAutocompleteSelector(dataSource, onSelect, options, getDynamicOptions) {
|
||||
return {
|
||||
type: ViewTypes.SELECTED_ACTION_MENU,
|
||||
data: {
|
||||
dataSource,
|
||||
onSelect,
|
||||
options,
|
||||
getDynamicOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export default class AutocompleteSelector extends PureComponent {
|
|||
actions: PropTypes.shape({
|
||||
setAutocompleteSelector: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
getDynamicOptions: PropTypes.func,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.string,
|
||||
|
|
@ -98,11 +99,11 @@ export default class AutocompleteSelector extends PureComponent {
|
|||
|
||||
goToSelectorScreen = preventDoubleTap(() => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {actions, dataSource, options, placeholder} = this.props;
|
||||
const {actions, dataSource, options, placeholder, getDynamicOptions} = this.props;
|
||||
const screen = 'SelectorScreen';
|
||||
const title = placeholder || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
|
||||
|
||||
actions.setAutocompleteSelector(dataSource, this.handleSelect, options);
|
||||
actions.setAutocompleteSelector(dataSource, this.handleSelect, options, getDynamicOptions);
|
||||
goToScreen(screen, title);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ export default {
|
|||
PROFILE_PICTURE_EMOJI_SIZE: 28,
|
||||
DATA_SOURCE_USERS: 'users',
|
||||
DATA_SOURCE_CHANNELS: 'channels',
|
||||
DATA_SOURCE_DYNAMIC: 'dynamic',
|
||||
NotificationLevels,
|
||||
SidebarSectionTypes,
|
||||
IOS_HORIZONTAL_LANDSCAPE: 44,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ export type DialogSubmission = {
|
|||
};
|
||||
cancelled: boolean;
|
||||
};
|
||||
export type DialogOption = {
|
||||
text: string;
|
||||
value: string;
|
||||
};
|
||||
export type DialogElement = {
|
||||
display_name: string;
|
||||
name: string;
|
||||
|
|
@ -106,10 +110,7 @@ export type DialogElement = {
|
|||
min_length: number;
|
||||
max_length: number;
|
||||
data_source: string;
|
||||
options: Array<{
|
||||
text: string;
|
||||
value: any;
|
||||
}>;
|
||||
options: Array<DialogOption>;
|
||||
};
|
||||
export type InteractiveDialogConfig = {
|
||||
app_id: string;
|
||||
|
|
|
|||
|
|
@ -1,626 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectorScreen should match snapshot for channels 1`] = `
|
||||
<RNCSafeAreaView
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"height": 38,
|
||||
"marginVertical": 5,
|
||||
"paddingLeft": 8,
|
||||
}
|
||||
}
|
||||
testID="selector.screen"
|
||||
>
|
||||
<Search
|
||||
autoCapitalize="none"
|
||||
backArrowSize={24}
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={false}
|
||||
cancelTitle="Cancel"
|
||||
containerHeight={40}
|
||||
deleteIconSize={20}
|
||||
editable={true}
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
keyboardAppearance="light"
|
||||
keyboardShouldPersist={false}
|
||||
keyboardType="default"
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
returnKeyType="search"
|
||||
searchBarRightMargin={0}
|
||||
searchIconSize={24}
|
||||
showArrow={false}
|
||||
showCancel={true}
|
||||
testID="selector.search_bar"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</View>
|
||||
<CustomList
|
||||
canRefresh={true}
|
||||
data={Array []}
|
||||
listType="flat"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
shouldRenderSeparator={true}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
exports[`SelectorScreen should match snapshot for channels 2`] = `
|
||||
<RNCSafeAreaView
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"height": 38,
|
||||
"marginVertical": 5,
|
||||
"paddingLeft": 8,
|
||||
}
|
||||
}
|
||||
testID="selector.screen"
|
||||
>
|
||||
<Search
|
||||
autoCapitalize="none"
|
||||
backArrowSize={24}
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={false}
|
||||
cancelTitle="Cancel"
|
||||
containerHeight={40}
|
||||
deleteIconSize={20}
|
||||
editable={true}
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
keyboardAppearance="light"
|
||||
keyboardShouldPersist={false}
|
||||
keyboardType="default"
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
returnKeyType="search"
|
||||
searchBarRightMargin={0}
|
||||
searchIconSize={24}
|
||||
showArrow={false}
|
||||
showCancel={true}
|
||||
testID="selector.search_bar"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</View>
|
||||
<CustomList
|
||||
canRefresh={true}
|
||||
data={Array []}
|
||||
listType="flat"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
shouldRenderSeparator={true}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
exports[`SelectorScreen should match snapshot for explicit options 1`] = `
|
||||
<RNCSafeAreaView
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"height": 38,
|
||||
"marginVertical": 5,
|
||||
"paddingLeft": 8,
|
||||
}
|
||||
}
|
||||
testID="selector.screen"
|
||||
>
|
||||
<Search
|
||||
autoCapitalize="none"
|
||||
backArrowSize={24}
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={false}
|
||||
cancelTitle="Cancel"
|
||||
containerHeight={40}
|
||||
deleteIconSize={20}
|
||||
editable={true}
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
keyboardAppearance="light"
|
||||
keyboardShouldPersist={false}
|
||||
keyboardType="default"
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
returnKeyType="search"
|
||||
searchBarRightMargin={0}
|
||||
searchIconSize={24}
|
||||
showArrow={false}
|
||||
showCancel={true}
|
||||
testID="selector.search_bar"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</View>
|
||||
<CustomList
|
||||
canRefresh={true}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"label": "text",
|
||||
"value": "value",
|
||||
},
|
||||
]
|
||||
}
|
||||
listType="flat"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
shouldRenderSeparator={true}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
exports[`SelectorScreen should match snapshot for searching 1`] = `
|
||||
<RNCSafeAreaView
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"height": 38,
|
||||
"marginVertical": 5,
|
||||
"paddingLeft": 8,
|
||||
}
|
||||
}
|
||||
testID="selector.screen"
|
||||
>
|
||||
<Search
|
||||
autoCapitalize="none"
|
||||
backArrowSize={24}
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={false}
|
||||
cancelTitle="Cancel"
|
||||
containerHeight={40}
|
||||
deleteIconSize={20}
|
||||
editable={true}
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
keyboardAppearance="light"
|
||||
keyboardShouldPersist={false}
|
||||
keyboardType="default"
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
returnKeyType="search"
|
||||
searchBarRightMargin={0}
|
||||
searchIconSize={24}
|
||||
showArrow={false}
|
||||
showCancel={true}
|
||||
testID="selector.search_bar"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value="name2"
|
||||
/>
|
||||
</View>
|
||||
<CustomList
|
||||
canRefresh={true}
|
||||
data={Array []}
|
||||
listType="flat"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
shouldRenderSeparator={true}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
exports[`SelectorScreen should match snapshot for users 1`] = `
|
||||
<RNCSafeAreaView
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"height": 38,
|
||||
"marginVertical": 5,
|
||||
"paddingLeft": 8,
|
||||
}
|
||||
}
|
||||
testID="selector.screen"
|
||||
>
|
||||
<Search
|
||||
autoCapitalize="none"
|
||||
backArrowSize={24}
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={false}
|
||||
cancelTitle="Cancel"
|
||||
containerHeight={40}
|
||||
deleteIconSize={20}
|
||||
editable={true}
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
keyboardAppearance="light"
|
||||
keyboardShouldPersist={false}
|
||||
keyboardType="default"
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
returnKeyType="search"
|
||||
searchBarRightMargin={0}
|
||||
searchIconSize={24}
|
||||
showArrow={false}
|
||||
showCancel={true}
|
||||
testID="selector.search_bar"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</View>
|
||||
<CustomList
|
||||
canRefresh={true}
|
||||
data={Array []}
|
||||
listType="flat"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
shouldRenderSeparator={true}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
exports[`SelectorScreen should match snapshot for users 2`] = `
|
||||
<RNCSafeAreaView
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"height": 38,
|
||||
"marginVertical": 5,
|
||||
"paddingLeft": 8,
|
||||
}
|
||||
}
|
||||
testID="selector.screen"
|
||||
>
|
||||
<Search
|
||||
autoCapitalize="none"
|
||||
backArrowSize={24}
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={false}
|
||||
cancelTitle="Cancel"
|
||||
containerHeight={40}
|
||||
deleteIconSize={20}
|
||||
editable={true}
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
keyboardAppearance="light"
|
||||
keyboardShouldPersist={false}
|
||||
keyboardType="default"
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
returnKeyType="search"
|
||||
searchBarRightMargin={0}
|
||||
searchIconSize={24}
|
||||
showArrow={false}
|
||||
showCancel={true}
|
||||
testID="selector.search_bar"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</View>
|
||||
<CustomList
|
||||
canRefresh={true}
|
||||
data={Array []}
|
||||
listType="flat"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
shouldRenderSeparator={true}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import AppSelectorScreen from './app_selector_screen';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
|
||||
const user1 = {id: 'id', username: 'username'} as UserProfile;
|
||||
const user2 = {id: 'id2', username: 'username2'} as UserProfile;
|
||||
|
||||
const getProfiles = async () => {
|
||||
return {
|
||||
data: [user1, user2],
|
||||
error: {},
|
||||
};
|
||||
};
|
||||
|
||||
const searchProfiles = async () => {
|
||||
return {
|
||||
data: [user2],
|
||||
error: {},
|
||||
};
|
||||
};
|
||||
|
||||
const channel1 = {id: 'id', name: 'name', display_name: 'display_name'} as Channel;
|
||||
const channel2 = {id: 'id2', name: 'name2', display_name: 'display_name2'} as Channel;
|
||||
|
||||
const getChannels = async () => {
|
||||
return {
|
||||
data: [channel1, channel2],
|
||||
error: {},
|
||||
};
|
||||
};
|
||||
|
||||
const searchChannels = async () => {
|
||||
return {
|
||||
data: [channel2],
|
||||
error: {},
|
||||
};
|
||||
};
|
||||
|
||||
const intlProvider = new IntlProvider({locale: 'en'}, {});
|
||||
const {intl} = intlProvider.getChildContext();
|
||||
|
||||
describe('SelectorScreen', () => {
|
||||
const actions = {
|
||||
getProfiles,
|
||||
getChannels,
|
||||
searchProfiles,
|
||||
searchChannels,
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
actions,
|
||||
currentTeamId: 'someId',
|
||||
onSelect: jest.fn(),
|
||||
data: [{label: 'text', value: 'value'}],
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
test('should match snapshot for explicit options', async () => {
|
||||
const wrapper = shallow(
|
||||
<AppSelectorScreen {...baseProps}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for users', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
dataSource: 'users',
|
||||
data: [user1, user2],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<AppSelectorScreen {...props}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
wrapper.setState({isLoading: false});
|
||||
wrapper.update();
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for channels', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
dataSource: 'channels',
|
||||
data: [channel1, channel2],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<AppSelectorScreen {...props}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
wrapper.setState({isLoading: false});
|
||||
wrapper.update();
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for searching', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
dataSource: 'channels',
|
||||
data: [channel1, channel2],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<AppSelectorScreen {...props}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
wrapper.setState({isLoading: false, searching: true, term: 'name2'});
|
||||
wrapper.update();
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,465 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Platform,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {popTopScreen} from '@actions/navigation';
|
||||
import CustomList, {FLATLIST} from '@components/custom_list';
|
||||
import UserListRow from '@components/custom_list/user_list_row';
|
||||
import ChannelListRow from '@components/custom_list/channel_list_row';
|
||||
import OptionListRow from '@components/custom_list/option_list_row';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import SearchBar from '@components/search_bar';
|
||||
import StatusBar from '@components/status_bar';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {debounce} from '@mm-redux/actions/helpers';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {filterProfilesMatchingTerm} from '@mm-redux/utils/user_utils';
|
||||
import {filterChannelsMatchingTerm} from '@mm-redux/utils/channel_utils';
|
||||
import {memoizeResult} from '@mm-redux/utils/helpers';
|
||||
import {t} from '@utils/i18n';
|
||||
import {loadingText} from '@utils/member_list';
|
||||
import {
|
||||
changeOpacity,
|
||||
makeStyleSheetFromTheme,
|
||||
getKeyboardAppearanceFromTheme,
|
||||
} from '@utils/theme';
|
||||
import {AppSelectOption} from '@mm-redux/types/apps';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {ActionResult} from '@mm-redux/types/actions';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
|
||||
type Props = {
|
||||
currentTeamId: string;
|
||||
data: OptionsData;
|
||||
dataSource?: string;
|
||||
onSelect: (option: UserProfile | Channel | AppSelectOption) => void;
|
||||
theme: Theme;
|
||||
performLookupCall?: (userInput: string) => Promise<AppSelectOption[]>;
|
||||
actions: {
|
||||
getProfiles: (page?: number, perPage?: number, options?: any) => Promise<ActionResult>;
|
||||
getChannels: (teamId: string, page?: number, perPage?: number) => Promise<ActionResult>;
|
||||
searchProfiles: (term: string, options?: any) => Promise<ActionResult>;
|
||||
searchChannels: (teamId: string, term: string, archived?: boolean) => Promise<ActionResult>;
|
||||
}
|
||||
}
|
||||
|
||||
type State = {
|
||||
data: OptionsData;
|
||||
loading: boolean;
|
||||
searchResults: OptionsData;
|
||||
term: string;
|
||||
}
|
||||
|
||||
type OptionsData = UserProfile[] | Channel[] | AppSelectOption[];
|
||||
|
||||
type RowProps = {
|
||||
id: string;
|
||||
item: UserProfile | Channel | AppSelectOption;
|
||||
selected: boolean;
|
||||
selectable: boolean;
|
||||
enabled: boolean;
|
||||
onPress: (id: string, item: UserProfile | Channel | AppSelectOption) => void;
|
||||
}
|
||||
|
||||
export default class AppSelectorScreen extends PureComponent<Props, State> {
|
||||
private searchTimeoutId?: NodeJS.Timeout;
|
||||
private page = -1;
|
||||
private next: boolean;
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.next = props.dataSource === ViewTypes.DATA_SOURCE_USERS ||
|
||||
props.dataSource === ViewTypes.DATA_SOURCE_CHANNELS ||
|
||||
props.dataSource === 'app';
|
||||
|
||||
let data: OptionsData = [];
|
||||
if (!props.dataSource) {
|
||||
data = props.data;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
data,
|
||||
loading: false,
|
||||
searchResults: [],
|
||||
term: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {dataSource} = this.props;
|
||||
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
|
||||
this.getProfiles();
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
this.getChannels();
|
||||
} else if (dataSource === 'app') {
|
||||
this.getAppOptions();
|
||||
}
|
||||
}
|
||||
|
||||
clearSearch = () => {
|
||||
this.setState({term: '', searchResults: []});
|
||||
};
|
||||
|
||||
close = () => {
|
||||
popTopScreen();
|
||||
};
|
||||
|
||||
handleSelectItem = (id: string, item: UserProfile | Channel | AppSelectOption) => {
|
||||
this.props.onSelect(item);
|
||||
this.close();
|
||||
};
|
||||
|
||||
getChannels = debounce(() => {
|
||||
const {actions, currentTeamId} = this.props;
|
||||
const {loading, term} = this.state;
|
||||
if (this.next && !loading && !term) {
|
||||
this.setState({loading: true}, () => {
|
||||
actions.getChannels(
|
||||
currentTeamId,
|
||||
this.page += 1,
|
||||
General.CHANNELS_CHUNK_SIZE,
|
||||
).then(this.loadedChannels);
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
|
||||
getAppOptions = debounce(() => {
|
||||
const {performLookupCall} = this.props;
|
||||
const {loading, term} = this.state;
|
||||
if (this.next && !loading && !term) {
|
||||
this.setState({loading: true}, () => {
|
||||
performLookupCall?.(term).then(this.loadedAppOptions);
|
||||
});
|
||||
}
|
||||
}, 100)
|
||||
|
||||
getDataResults = () => {
|
||||
const {dataSource} = this.props;
|
||||
const {data, searchResults, term} = this.state;
|
||||
|
||||
const result = {
|
||||
data,
|
||||
listType: FLATLIST};
|
||||
if (term) {
|
||||
result.data = filterSearchData(dataSource, searchResults, term);
|
||||
}
|
||||
|
||||
// TODO re-add this to add sections by first username letter if desired
|
||||
// } else if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
|
||||
// result.data = createProfilesSections(data);
|
||||
// result.listType = SECTIONLIST;
|
||||
// }
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
getProfiles = debounce(() => {
|
||||
const {loading, term} = this.state;
|
||||
if (this.next && !loading && !term) {
|
||||
this.setState({loading: true}, () => {
|
||||
const {actions} = this.props;
|
||||
|
||||
actions.getProfiles(
|
||||
this.page + 1,
|
||||
General.PROFILE_CHUNK_SIZE,
|
||||
).then(this.loadedProfiles);
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
|
||||
loadedChannels = ({data: channels}: {data: Channel[]}) => {
|
||||
const data = this.state.data as Channel[];
|
||||
if (channels && !channels.length) {
|
||||
this.next = false;
|
||||
}
|
||||
|
||||
this.page += 1;
|
||||
this.setState({loading: false, data: [...channels, ...data]});
|
||||
};
|
||||
|
||||
loadedProfiles = ({data: profiles}: {data: UserProfile[]}) => {
|
||||
const data = this.state.data as UserProfile[];
|
||||
if (profiles && !profiles.length) {
|
||||
this.next = false;
|
||||
}
|
||||
|
||||
this.page += 1;
|
||||
this.setState({loading: false, data: [...profiles, ...data]});
|
||||
};
|
||||
|
||||
loadedAppOptions = (options: AppSelectOption[]) => {
|
||||
const data = this.state.data as AppSelectOption[];
|
||||
if (options && !options.length) {
|
||||
this.next = false;
|
||||
}
|
||||
|
||||
this.page += 1;
|
||||
this.setState({loading: false, data: [...options, ...data]});
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
const {dataSource} = this.props;
|
||||
|
||||
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
|
||||
this.getProfiles();
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
this.getChannels();
|
||||
}
|
||||
};
|
||||
|
||||
onSearch = (text: string) => {
|
||||
if (text) {
|
||||
const {dataSource, data} = this.props;
|
||||
this.setState({term: text});
|
||||
if (this.searchTimeoutId) {
|
||||
clearTimeout(this.searchTimeoutId);
|
||||
}
|
||||
|
||||
this.searchTimeoutId = setTimeout(() => {
|
||||
if (!dataSource) {
|
||||
this.setState({searchResults: filterSearchData(null, data, text)});
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
|
||||
this.searchProfiles(text);
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
this.searchChannels(text);
|
||||
}
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
} else {
|
||||
this.clearSearch();
|
||||
}
|
||||
};
|
||||
|
||||
searchChannels = (term: string) => {
|
||||
const {actions, currentTeamId} = this.props;
|
||||
|
||||
actions.searchChannels(currentTeamId, term.toLowerCase()).then(({data}) => {
|
||||
this.setState({searchResults: data, loading: false});
|
||||
});
|
||||
};
|
||||
|
||||
searchProfiles = (term: string) => {
|
||||
const {actions} = this.props;
|
||||
this.setState({loading: true});
|
||||
|
||||
actions.searchProfiles(term.toLowerCase()).then((results) => {
|
||||
let data = [];
|
||||
if (results.data) {
|
||||
data = results.data;
|
||||
}
|
||||
this.setState({searchResults: data, loading: false});
|
||||
});
|
||||
};
|
||||
|
||||
renderLoading = () => {
|
||||
const {dataSource, theme} = this.props;
|
||||
const {loading} = this.state;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (!loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let text;
|
||||
switch (dataSource) {
|
||||
case ViewTypes.DATA_SOURCE_USERS:
|
||||
text = loadingText;
|
||||
break;
|
||||
case ViewTypes.DATA_SOURCE_CHANNELS:
|
||||
text = {
|
||||
id: t('mobile.loading_channels'),
|
||||
defaultMessage: 'Loading Channels...',
|
||||
};
|
||||
break;
|
||||
default:
|
||||
text = {
|
||||
id: t('mobile.loading_options'),
|
||||
defaultMessage: 'Loading Options...',
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.loadingContainer}>
|
||||
<FormattedText
|
||||
{...text}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderNoResults = () => {
|
||||
const {loading} = this.state;
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (loading || this.page === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderChannelItem = (props: RowProps) => {
|
||||
return <ChannelListRow {...props}/>;
|
||||
};
|
||||
|
||||
renderOptionItem = (props: RowProps) => {
|
||||
const item = props.item as AppSelectOption;
|
||||
const newProps = {
|
||||
...props,
|
||||
item: {text: item.label, value: item.value},
|
||||
onPress: (id: string, option: {text: string, value: string}) => {
|
||||
props.onPress(id, {label: option.text, value: option.value});
|
||||
},
|
||||
};
|
||||
return <OptionListRow {...newProps}/>;
|
||||
};
|
||||
|
||||
renderUserItem = (props: RowProps) => {
|
||||
return <UserListRow {...props}/>;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {theme, dataSource} = this.props;
|
||||
const {loading, term} = this.state;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const searchBarInput = {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
};
|
||||
|
||||
let rowComponent;
|
||||
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
|
||||
rowComponent = this.renderUserItem;
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
rowComponent = this.renderChannelItem;
|
||||
} else {
|
||||
rowComponent = this.renderOptionItem;
|
||||
}
|
||||
|
||||
const {data, listType} = this.getDataResults();
|
||||
|
||||
return (
|
||||
<SafeAreaView style={style.container}>
|
||||
<StatusBar/>
|
||||
<View
|
||||
testID='selector.screen'
|
||||
style={style.searchBar}
|
||||
>
|
||||
<SearchBar
|
||||
testID='selector.search_bar'
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={33}
|
||||
inputStyle={searchBarInput}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={this.onSearch}
|
||||
onSearchButtonPress={this.onSearch}
|
||||
onCancelButtonPress={this.clearSearch}
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
<CustomList
|
||||
data={data}
|
||||
key='custom_list'
|
||||
listType={listType}
|
||||
loading={loading}
|
||||
loadingComponent={this.renderLoading()}
|
||||
noResults={this.renderNoResults()}
|
||||
onLoadMore={this.loadMore}
|
||||
onRowPress={this.handleSelectItem}
|
||||
renderItem={rowComponent}
|
||||
theme={theme}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
searchBar: {
|
||||
marginVertical: 5,
|
||||
height: 38,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
paddingLeft: 8,
|
||||
},
|
||||
}),
|
||||
},
|
||||
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: {
|
||||
fontSize: 26,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const filterSearchData = memoizeResult((dataSource: string, data: OptionsData, term: string): OptionsData => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lowerCasedTerm = term.toLowerCase();
|
||||
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
|
||||
const users = data as UserProfile[];
|
||||
return filterProfilesMatchingTerm(users, lowerCasedTerm);
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
const channels = data as Channel[];
|
||||
return filterChannelsMatchingTerm(channels, lowerCasedTerm);
|
||||
}
|
||||
|
||||
const options = data as AppSelectOption[];
|
||||
return options.filter((option) => option.label && option.label.toLowerCase().startsWith(lowerCasedTerm));
|
||||
});
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getProfiles, searchProfiles} from '@mm-redux/actions/users';
|
||||
import {getChannels, searchChannels} from '@mm-redux/actions/channels';
|
||||
import AppSelectorScreen from './app_selector_screen';
|
||||
import {ActionFunc, ActionResult, GenericAction} from '@mm-redux/types/actions';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
getProfiles: (page?: number, perPage?: number, options?: any) => Promise<ActionResult>;
|
||||
getChannels: (teamId: string, page?: number, perPage?: number) => Promise<ActionResult>;
|
||||
searchProfiles: (term: string, options?: any) => Promise<ActionResult>;
|
||||
searchChannels: (teamId: string, term: string, archived?: boolean) => Promise<ActionResult>;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
||||
getProfiles,
|
||||
getChannels,
|
||||
searchProfiles,
|
||||
searchChannels,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AppSelectorScreen);
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent, ReactNode} from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import {AppSelectOption} from '@mm-redux/types/apps';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
dataSource?: string;
|
||||
options?: AppSelectOption[];
|
||||
selected?: AppSelectOption;
|
||||
optional?: boolean;
|
||||
showRequiredAsterisk?: boolean;
|
||||
teammateNameDisplay?: string;
|
||||
theme: Theme;
|
||||
onSelected?: (option: AppSelectOption) => void;
|
||||
helpText?: string;
|
||||
errorText?: ReactNode;
|
||||
roundedBorders?: boolean;
|
||||
disabled?: boolean;
|
||||
performLookupCall?: (term: string) => Promise<AppSelectOption[]>;
|
||||
}
|
||||
|
||||
export default class AppFormSelector extends PureComponent<Props> {
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
optional: false,
|
||||
showRequiredAsterisk: false,
|
||||
roundedBorders: true,
|
||||
};
|
||||
|
||||
handleSelect = (selected: UserProfile | Channel | AppSelectOption) => {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
dataSource,
|
||||
teammateNameDisplay,
|
||||
} = this.props;
|
||||
|
||||
let selectedLabel;
|
||||
let selectedValue;
|
||||
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
|
||||
const user = selected as UserProfile;
|
||||
selectedLabel = user.username;
|
||||
if (teammateNameDisplay) {
|
||||
selectedLabel = displayUsername(user, teammateNameDisplay);
|
||||
}
|
||||
selectedValue = user.id;
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
const channel = selected as Channel;
|
||||
selectedLabel = channel.display_name;
|
||||
selectedValue = channel.id;
|
||||
} else {
|
||||
const option = selected as AppSelectOption;
|
||||
selectedLabel = option.label;
|
||||
selectedValue = option.value;
|
||||
}
|
||||
|
||||
const selectedOption = {label: selectedLabel, value: selectedValue};
|
||||
|
||||
if (this.props.onSelected) {
|
||||
this.props.onSelected(selectedOption);
|
||||
}
|
||||
};
|
||||
|
||||
goToSelectorScreen = preventDoubleTap(() => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {dataSource, options, placeholder, performLookupCall} = this.props;
|
||||
const screen = 'AppSelectorScreen';
|
||||
const title = placeholder || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
|
||||
|
||||
const selectorProps = {
|
||||
data: options,
|
||||
dataSource,
|
||||
onSelect: this.handleSelect,
|
||||
performLookupCall,
|
||||
};
|
||||
|
||||
goToScreen(screen, title, selectorProps);
|
||||
});
|
||||
|
||||
render() {
|
||||
const {intl} = this.context;
|
||||
const {
|
||||
placeholder,
|
||||
theme,
|
||||
label,
|
||||
helpText,
|
||||
errorText,
|
||||
optional,
|
||||
showRequiredAsterisk,
|
||||
roundedBorders,
|
||||
disabled,
|
||||
selected,
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let text = placeholder || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
|
||||
let selectedStyle = style.dropdownPlaceholder;
|
||||
|
||||
if (selected) {
|
||||
text = selected.label;
|
||||
selectedStyle = style.dropdownSelected;
|
||||
}
|
||||
|
||||
let inputStyle = style.input;
|
||||
if (roundedBorders) {
|
||||
inputStyle = style.roundedInput;
|
||||
}
|
||||
|
||||
let optionalContent;
|
||||
let asterisk;
|
||||
if (optional) {
|
||||
optionalContent = (
|
||||
<FormattedText
|
||||
style={style.optional}
|
||||
id='channel_modal.optional'
|
||||
defaultMessage='(optional)'
|
||||
/>
|
||||
);
|
||||
} else if (showRequiredAsterisk) {
|
||||
asterisk = <Text style={style.asterisk}>{' *'}</Text>;
|
||||
}
|
||||
|
||||
let labelContent;
|
||||
if (label) {
|
||||
labelContent = (
|
||||
<View style={style.labelContainer}>
|
||||
<Text style={style.label}>
|
||||
{label}
|
||||
</Text>
|
||||
{asterisk}
|
||||
{optionalContent}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let helpTextContent;
|
||||
if (helpText) {
|
||||
helpTextContent = (
|
||||
<Text style={style.helpText}>
|
||||
{helpText}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
let errorTextContent;
|
||||
if (errorText) {
|
||||
errorTextContent = (
|
||||
<Text style={style.errorText}>
|
||||
{errorText}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
{labelContent}
|
||||
<TouchableWithFeedback
|
||||
style={disabled ? style.disabled : null}
|
||||
onPress={this.goToSelectorScreen}
|
||||
type={'opacity'}
|
||||
disabled={disabled}
|
||||
>
|
||||
<View style={inputStyle}>
|
||||
<Text
|
||||
style={selectedStyle}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
<CompassIcon
|
||||
name='chevron-down'
|
||||
color={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
style={style.icon}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
{helpTextContent}
|
||||
{errorTextContent}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
labelContainer: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 15,
|
||||
marginBottom: 10,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
color: theme.centerChannelColor,
|
||||
marginLeft: 15,
|
||||
},
|
||||
optional: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 14,
|
||||
marginLeft: 5,
|
||||
},
|
||||
helpText: {
|
||||
fontSize: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
marginHorizontal: 15,
|
||||
marginVertical: 10,
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 12,
|
||||
color: theme.errorTextColor,
|
||||
marginHorizontal: 15,
|
||||
marginVertical: 10,
|
||||
},
|
||||
asterisk: {
|
||||
color: theme.errorTextColor,
|
||||
fontSize: 14,
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import AppFormSelector from './app_form_selector';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(AppFormSelector);
|
||||
|
|
@ -1,14 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import BoolSetting from '@components/widgets/settings/bool_setting';
|
||||
import TextSetting from '@components/widgets/settings/text_setting';
|
||||
import {ViewTypes} from '@constants/index';
|
||||
import {AppField, AppFormValue, AppSelectOption} from '@mm-redux/types/apps';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import React from 'react';
|
||||
|
||||
import AppFormSelector from './app_form_selector';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {AppField, AppFormValue, AppSelectOption} from '@mm-redux/types/apps';
|
||||
import {AppFieldTypes} from '@mm-redux/constants/apps';
|
||||
import {DialogOption} from '@mm-redux/types/integrations';
|
||||
|
||||
import {ViewTypes} from '@constants/index';
|
||||
|
||||
import BoolSetting from '@components/widgets/settings/bool_setting';
|
||||
import TextSetting from '@components/widgets/settings/text_setting';
|
||||
import AutocompleteSelector from '@components/autocomplete_selector';
|
||||
|
||||
const TEXT_DEFAULT_MAX_LENGTH = 150;
|
||||
const TEXTAREA_DEFAULT_MAX_LENGTH = 3000;
|
||||
|
|
@ -24,7 +28,43 @@ export type Props = {
|
|||
performLookup: (name: string, userInput: string) => Promise<AppSelectOption[]>;
|
||||
}
|
||||
|
||||
export default class AppsFormField extends React.PureComponent<Props> {
|
||||
type State = {
|
||||
selected: DialogOption | null;
|
||||
}
|
||||
|
||||
export default class AppsFormField extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
selected: null,
|
||||
};
|
||||
|
||||
handleAutocompleteSelect = (selected: DialogOption) => {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
field,
|
||||
} = this.props;
|
||||
|
||||
this.setState({selected});
|
||||
|
||||
const selectedOption = {
|
||||
label: selected.text,
|
||||
value: selected.value,
|
||||
};
|
||||
|
||||
this.props.onChange(field.name, selectedOption);
|
||||
};
|
||||
|
||||
getDynamicOptions = async (userInput = ''): Promise<{data: DialogOption[]}> => {
|
||||
const options = await this.props.performLookup(this.props.field.name, userInput);
|
||||
return {
|
||||
data: options.map((option) => ({
|
||||
text: option.label,
|
||||
value: option.value,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
field,
|
||||
|
|
@ -101,33 +141,45 @@ export default class AppsFormField extends React.PureComponent<Props> {
|
|||
disabled={field.readonly}
|
||||
/>
|
||||
);
|
||||
} else if (field.type === 'channel' || field.type === 'user' || field.type === 'dynamic_select' || field.type === 'static_select') {
|
||||
let dataSource = ViewTypes.DATA_SOURCE_CHANNELS;
|
||||
if (field.type === 'user') {
|
||||
} else if ([AppFieldTypes.USER, AppFieldTypes.CHANNEL, AppFieldTypes.STATIC_SELECT, AppFieldTypes.DYNAMIC_SELECT].includes(field.type)) {
|
||||
let dataSource = '';
|
||||
let options: DialogOption[] = [];
|
||||
|
||||
switch (field.type) {
|
||||
case AppFieldTypes.USER:
|
||||
dataSource = ViewTypes.DATA_SOURCE_USERS;
|
||||
break;
|
||||
case AppFieldTypes.CHANNEL:
|
||||
dataSource = ViewTypes.DATA_SOURCE_CHANNELS;
|
||||
break;
|
||||
case AppFieldTypes.DYNAMIC_SELECT:
|
||||
dataSource = ViewTypes.DATA_SOURCE_DYNAMIC;
|
||||
break;
|
||||
case AppFieldTypes.STATIC_SELECT:
|
||||
if (field.options) {
|
||||
options = field.options.map((option) => ({text: option.label, value: option.value}));
|
||||
}
|
||||
}
|
||||
if (field.type === 'dynamic_select') {
|
||||
dataSource = 'app';
|
||||
}
|
||||
const option = value as AppSelectOption;
|
||||
|
||||
return (
|
||||
<AppFormSelector
|
||||
<AutocompleteSelector
|
||||
id={name}
|
||||
label={displayName}
|
||||
options={field.options}
|
||||
dataSource={dataSource}
|
||||
options={options}
|
||||
optional={!field.is_required}
|
||||
onSelected={(selected: AppSelectOption) => this.props.onChange(field.name, selected)}
|
||||
onSelected={this.handleAutocompleteSelect}
|
||||
getDynamicOptions={this.getDynamicOptions}
|
||||
helpText={field.description}
|
||||
errorText={errorText}
|
||||
placeholder={placeholder}
|
||||
showRequiredAsterisk={true}
|
||||
selected={option}
|
||||
selected={this.state.selected}
|
||||
roundedBorders={false}
|
||||
disabled={field.readonly}
|
||||
performLookupCall={(term: string) => this.props.performLookup(field.name, term)}
|
||||
/>
|
||||
);
|
||||
} else if (field.type === 'bool') {
|
||||
} else if (field.type === AppFieldTypes.BOOL) {
|
||||
const boolValue = value as boolean;
|
||||
return (
|
||||
<BoolSetting
|
||||
|
|
|
|||
|
|
@ -49,9 +49,6 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case 'AppForm':
|
||||
screen = require('@screens/apps_form').default;
|
||||
break;
|
||||
case 'AppSelectorScreen':
|
||||
screen = require('@screens/app_selector_screen').default;
|
||||
break;
|
||||
case 'ChannelAddMembers':
|
||||
screen = require('@screens/channel_add_members').default;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ function mapStateToProps(state) {
|
|||
data,
|
||||
dataSource: menuAction.dataSource,
|
||||
onSelect: menuAction.onSelect,
|
||||
getDynamicOptions: menuAction.getDynamicOptions,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export default class SelectorScreen extends PureComponent {
|
|||
searchProfiles: PropTypes.func.isRequired,
|
||||
searchChannels: PropTypes.func.isRequired,
|
||||
}),
|
||||
getDynamicOptions: PropTypes.func,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
data: PropTypes.arrayOf(PropTypes.object),
|
||||
dataSource: PropTypes.string,
|
||||
|
|
@ -56,7 +57,7 @@ export default class SelectorScreen extends PureComponent {
|
|||
|
||||
this.searchTimeoutId = 0;
|
||||
this.page = -1;
|
||||
this.next = props.dataSource === ViewTypes.DATA_SOURCE_USERS || props.dataSource === ViewTypes.DATA_SOURCE_CHANNELS;
|
||||
this.next = props.dataSource === ViewTypes.DATA_SOURCE_USERS || props.dataSource === ViewTypes.DATA_SOURCE_CHANNELS || props.dataSource === ViewTypes.DATA_SOURCE_DYNAMIC;
|
||||
|
||||
let data = [];
|
||||
if (!props.dataSource) {
|
||||
|
|
@ -78,6 +79,8 @@ export default class SelectorScreen extends PureComponent {
|
|||
this.getProfiles();
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
this.getChannels();
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_DYNAMIC) {
|
||||
this.getDynamicOptions();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,6 +150,13 @@ export default class SelectorScreen extends PureComponent {
|
|||
}
|
||||
}, 100);
|
||||
|
||||
getDynamicOptions = debounce(() => {
|
||||
const {loading, term} = this.state;
|
||||
if (this.next && !loading && !term) {
|
||||
this.searchDynamicOptions('');
|
||||
}
|
||||
}, 100);
|
||||
|
||||
loadedChannels = ({data: channels}) => {
|
||||
const {data} = this.state;
|
||||
if (channels && !channels.length) {
|
||||
|
|
@ -175,6 +185,8 @@ export default class SelectorScreen extends PureComponent {
|
|||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
this.getChannels();
|
||||
}
|
||||
|
||||
// dynamic options are not paged so are not reloaded on scroll
|
||||
};
|
||||
|
||||
onSearch = (text) => {
|
||||
|
|
@ -193,6 +205,8 @@ export default class SelectorScreen extends PureComponent {
|
|||
this.searchProfiles(text);
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
this.searchChannels(text);
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_DYNAMIC) {
|
||||
this.searchDynamicOptions(text);
|
||||
}
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
} else {
|
||||
|
|
@ -221,6 +235,27 @@ export default class SelectorScreen extends PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
searchDynamicOptions = (term = '') => {
|
||||
if (!this.props.getDynamicOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({loading: true});
|
||||
|
||||
this.props.getDynamicOptions(term.toLowerCase()).then((results) => {
|
||||
let data = [];
|
||||
if (results.data) {
|
||||
data = results.data;
|
||||
}
|
||||
|
||||
if (term) {
|
||||
this.setState({searchResults: data, loading: false});
|
||||
} else {
|
||||
this.setState({data, loading: false});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
renderLoading = () => {
|
||||
const {dataSource, theme} = this.props;
|
||||
const {loading} = this.state;
|
||||
|
|
@ -404,6 +439,8 @@ const filterSearchData = memoizeResult((dataSource, data, term) => {
|
|||
return filterProfilesMatchingTerm(data, lowerCasedTerm);
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
return filterChannelsMatchingTerm(data, lowerCasedTerm);
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_DYNAMIC) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return data.filter((option) => option.text && option.text.toLowerCase().startsWith(lowerCasedTerm));
|
||||
|
|
|
|||
|
|
@ -123,4 +123,58 @@ describe('SelectorScreen', () => {
|
|||
wrapper.update();
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should call getDynamicOptions if data source is dynamic', async () => {
|
||||
const getDynamicOptions = jest.fn(async (term) => {
|
||||
if (term) {
|
||||
return {data: [{text: 'With Query Text', value: 'with_query'}]};
|
||||
}
|
||||
|
||||
return {data: [{text: 'Without Query Text', value: 'without_query'}]};
|
||||
});
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
dataSource: 'dynamic',
|
||||
getDynamicOptions,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<SelectorScreen {...props}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
|
||||
jest.runAllTimers();
|
||||
await (() => new Promise(setImmediate))();
|
||||
|
||||
expect(props.getDynamicOptions).toHaveBeenCalledWith('');
|
||||
expect(wrapper.state().data).toEqual([
|
||||
{text: 'Without Query Text', value: 'without_query'},
|
||||
]);
|
||||
expect(wrapper.state().searchResults).toEqual([]);
|
||||
|
||||
let customList = wrapper.find('CustomList');
|
||||
expect(customList.props().data).toEqual([
|
||||
{text: 'Without Query Text', value: 'without_query'},
|
||||
]);
|
||||
|
||||
// Search for value
|
||||
wrapper.instance().onSearch('mysearch');
|
||||
|
||||
jest.runAllTimers();
|
||||
await (() => new Promise(setImmediate))();
|
||||
|
||||
expect(props.getDynamicOptions).toHaveBeenCalledWith('mysearch');
|
||||
expect(wrapper.state().data).toEqual([
|
||||
{text: 'Without Query Text', value: 'without_query'},
|
||||
]);
|
||||
expect(wrapper.state().searchResults).toEqual([
|
||||
{text: 'With Query Text', value: 'with_query'},
|
||||
]);
|
||||
|
||||
customList = wrapper.find('CustomList');
|
||||
expect(customList.props().data).toEqual([
|
||||
{text: 'With Query Text', value: 'with_query'},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue