fix multiselect for app forms

This commit is contained in:
Michael Kochell
2022-11-22 01:00:30 -05:00
parent 7410877983
commit 730d92438e
6 changed files with 121 additions and 50 deletions

View File

@@ -24,8 +24,11 @@ import {displayUsername} from '@utils/user';
import type {WithDatabaseArgs} from '@typings/database/database';
type Selection = DialogOption | AppSelectOption | Channel | UserProfile | DialogOption[] | AppSelectOption[] | Channel[] | UserProfile[];
type Option = DialogOption | AppSelectOption;
type DialogOptionTextOptional = Omit<DialogOption, 'text'> & {text?: string};
type SelectedDialogOptionsTextOptional = DialogOptionTextOptional | DialogOptionTextOptional[] | undefined;
type Selection = DialogOption | Channel | UserProfile | DialogOption[] | Channel[] | UserProfile[];
type SelectedValue = DialogOption | DialogOption[] | undefined;
type AutoCompleteSelectorProps = {
dataSource?: string;
@@ -34,12 +37,12 @@ type AutoCompleteSelectorProps = {
getDynamicOptions?: (userInput?: string) => Promise<DialogOption[]>;
helpText?: string;
label?: string;
onSelected?: (value: string | string[]) => void;
onSelected?: (value: SelectedValue) => void;
optional?: boolean;
options?: PostActionOption[];
placeholder?: string;
roundedBorders?: boolean;
selected?: string | string[];
selected?: SelectedDialogOptionsTextOptional;
showRequiredAsterisk?: boolean;
teammateNameDisplay: string;
isMultiselect?: boolean;
@@ -90,7 +93,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
};
});
async function getItemName(serverUrl: string, selected: string | Option, teammateNameDisplay: string, intl: IntlShape, dataSource?: string, options?: Option[]) {
async function getItemName(serverUrl: string, selected: DialogOptionTextOptional, teammateNameDisplay: string, intl: IntlShape, dataSource?: string, options?: DialogOption[]): Promise<string> {
if (!selected) {
return '';
}
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
switch (dataSource) {
@@ -98,24 +105,26 @@ async function getItemName(serverUrl: string, selected: string | Option, teammat
if (!database) {
return intl.formatMessage({id: 'channel_loader.someone', defaultMessage: 'Someone'});
}
const user = await getUserById(database, selected as string);
const user = await getUserById(database, selected.value);
return displayUsername(user, intl.locale, teammateNameDisplay, true);
}
case ViewConstants.DATA_SOURCE_CHANNELS: {
if (!database) {
return intl.formatMessage({id: 'autocomplete_selector.unknown_channel', defaultMessage: 'Unknown channel'});
}
const channel = await getChannelById(database, selected as string);
const channel = await getChannelById(database, selected.value);
return channel?.displayName || intl.formatMessage({id: 'autocomplete_selector.unknown_channel', defaultMessage: 'Unknown channel'});
}
}
if (typeof selected === 'string') {
const option = options?.find((o) => o.value === selected);
return (option as DialogOption)?.text || (option as AppSelectOption)?.label || selected;
if (selected.text) {
return selected.text;
}
return (selected as DialogOption)?.text || (selected as AppSelectOption)?.label || '';
const option = options?.find((opt) => opt.value === selected.value);
return option?.text || '';
}
function getTextAndValueFromSelectedItem(item: Selection, teammateNameDisplay: string, locale: string, dataSource?: string) {
@@ -125,8 +134,6 @@ function getTextAndValueFromSelectedItem(item: Selection, teammateNameDisplay: s
} else if (dataSource === ViewConstants.DATA_SOURCE_CHANNELS) {
const channel = item as Channel;
return {text: channel.display_name, value: channel.id};
} else if ('label' in item) {
return {text: item.label, value: item.value};
}
return item as DialogOption;
}
@@ -147,31 +154,25 @@ function AutoCompleteSelector({
goToScreen(screen, title, {dataSource, handleSelect, options, getDynamicOptions, selected, isMultiselect});
}), [dataSource, options, getDynamicOptions]);
const handleSelect = useCallback((item?: Selection) => {
if (!item) {
const handleSelect = useCallback((newSelection?: Selection) => {
if (!newSelection) {
return;
}
if (!Array.isArray(item)) {
const {text: selectedText, value: selectedValue} = getTextAndValueFromSelectedItem(item, teammateNameDisplay, intl.locale, dataSource);
setItemText(selectedText);
if (!Array.isArray(newSelection)) {
const selectedOption = getTextAndValueFromSelectedItem(newSelection, teammateNameDisplay, intl.locale, dataSource);
setItemText(selectedOption.text);
if (onSelected) {
onSelected(selectedValue);
onSelected(selectedOption);
}
return;
}
const allSelectedTexts = [];
const allSelectedValues = [];
for (const i of item) {
const {text: selectedText, value: selectedValue} = getTextAndValueFromSelectedItem(i, teammateNameDisplay, intl.locale, dataSource);
allSelectedTexts.push(selectedText);
allSelectedValues.push(selectedValue);
}
setItemText(allSelectedTexts.join(', '));
const selectedOptions = newSelection.map((option) => getTextAndValueFromSelectedItem(option, teammateNameDisplay, intl.locale, dataSource));
setItemText(selectedOptions.map((option) => option.text).join(', '));
if (onSelected) {
onSelected(allSelectedValues);
onSelected(selectedOptions);
}
}, [teammateNameDisplay, intl, dataSource]);

View File

@@ -37,6 +37,13 @@ export const AppFieldTypes: { [name: string]: AppFieldType } = {
MARKDOWN: 'markdown',
};
export const SelectableAppFieldTypes = [
AppFieldTypes.CHANNEL,
AppFieldTypes.USER,
AppFieldTypes.STATIC_SELECT,
AppFieldTypes.DYNAMIC_SELECT,
];
export const COMMAND_SUGGESTION_ERROR = 'error';
export const COMMAND_SUGGESTION_CHANNEL = 'channel';
export const COMMAND_SUGGESTION_USER = 'user';

View File

@@ -99,9 +99,9 @@ function valuesReducer(state: AppFormValues, action: ValuesAction) {
return {...state, [action.name]: action.value};
}
function initValues(elements?: AppField[]) {
function initValues(fields?: AppField[]) {
const values: AppFormValues = {};
elements?.forEach((e) => {
fields?.forEach((e) => {
if (e.type === 'bool') {
values[e.name] = (e.value === true || String(e.value).toLowerCase() === 'true');
} else if (e.value) {
@@ -204,7 +204,7 @@ function AppsFormComponent({
return hasErrors;
}, [intl]);
const onChange = useCallback((name: string, value: any) => {
const onChange = useCallback((name: string, value: AppFormValue) => {
const field = form.fields?.find((f) => f.name === name);
if (!field) {
return;

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import React, {useCallback, useMemo} from 'react';
import {View} from 'react-native';
import AutocompleteSelector from '@components/autocomplete_selector';
@@ -9,7 +9,7 @@ import Markdown from '@components/markdown';
import BoolSetting from '@components/settings/bool_setting';
import TextSetting from '@components/settings/text_setting';
import {View as ViewConstants} from '@constants';
import {AppFieldTypes} from '@constants/apps';
import {AppFieldTypes, SelectableAppFieldTypes} from '@constants/apps';
import {useTheme} from '@context/theme';
import {selectKeyboardType} from '@utils/integrations';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
@@ -23,10 +23,20 @@ export type Props = {
name: string;
errorText?: string;
value: AppFormValue;
onChange: (name: string, value: string | string[] | boolean) => void;
onChange: (name: string, value: AppFormValue) => void;
performLookup: (name: string, userInput: string) => Promise<AppSelectOption[]>;
}
const dialogOptionToAppSelectOption = (option: DialogOption): AppSelectOption => ({
label: option.text,
value: option.value,
});
const appSelectOptionToDialogOption = (option: AppSelectOption): DialogOption => ({
text: option.label,
value: option.value,
});
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
markdownFieldContainer: {
@@ -69,18 +79,55 @@ function AppsFormField({
const placeholder = field.hint || '';
const displayName = field.modal_label || field.label || '';
const handleChange = useCallback((newValue: string | boolean | string[]) => {
const handleChange = useCallback((newValue: string | boolean) => {
onChange(name, newValue);
}, [name]);
const handleSelect = useCallback((newValue: DialogOption | DialogOption[] | undefined) => {
if (!newValue) {
const emptyValue = field.multiselect ? [] : null;
onChange(name, emptyValue);
return;
}
if (Array.isArray(newValue)) {
const selectedOptions = newValue.map(dialogOptionToAppSelectOption);
onChange(name, selectedOptions);
return;
}
onChange(name, dialogOptionToAppSelectOption(newValue));
}, [onChange, field, name]);
const getDynamicOptions = useCallback(async (userInput = ''): Promise<DialogOption[]> => {
const options = await performLookup(field.name, userInput);
return options.map((option) => ({
text: option.label,
value: option.value,
}));
return options.map(appSelectOptionToDialogOption);
}, [performLookup, field]);
const options = useMemo(() => {
if (field.type !== AppFieldTypes.STATIC_SELECT) {
return undefined;
}
return field.options?.map(appSelectOptionToDialogOption);
}, [field]);
const selectedValue = useMemo(() => {
if (!SelectableAppFieldTypes.includes(field.type)) {
return undefined;
}
if (!value) {
return undefined;
}
if (Array.isArray(value)) {
return value.map(appSelectOptionToDialogOption);
}
return appSelectOptionToDialogOption(value as AppSelectOption);
}, [field, value]);
switch (field.type) {
case AppFieldTypes.TEXT: {
return (
@@ -105,24 +152,19 @@ function AppsFormField({
case AppFieldTypes.CHANNEL:
case AppFieldTypes.STATIC_SELECT:
case AppFieldTypes.DYNAMIC_SELECT: {
let options: DialogOption[] | undefined;
if (field.type === AppFieldTypes.STATIC_SELECT && field.options) {
options = field.options.map((option) => ({text: option.label, value: option.value}));
}
return (
<AutocompleteSelector
label={displayName}
dataSource={selectDataSource(field.type)}
options={options}
optional={!field.is_required}
onSelected={handleChange}
onSelected={handleSelect}
getDynamicOptions={getDynamicOptions}
helpText={field.description}
errorText={errorText}
placeholder={placeholder}
showRequiredAsterisk={true}
selected={value as string | string[]}
selected={selectedValue}
roundedBorders={false}
disabled={field.readonly}
isMultiselect={field.multiselect}

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import React, {useCallback, useMemo} from 'react';
import {KeyboardTypeOptions} from 'react-native';
import AutocompleteSelector from '@components/autocomplete_selector';
@@ -60,6 +60,27 @@ function DialogElement({
onChange(name, newValue);
}, [onChange, type, subtype]);
const handleSelect = useCallback((newValue: DialogOption | undefined) => {
if (!newValue) {
onChange(name, '');
return;
}
onChange(name, newValue.value);
}, [onChange]);
const selectedValue = useMemo(() => {
if (type !== 'select') {
return undefined;
}
if (!value || typeof value !== 'string') {
return undefined;
}
return {value};
}, [value]);
switch (type) {
case 'text':
case 'textarea':
@@ -87,12 +108,12 @@ function DialogElement({
dataSource={dataSource}
options={options}
optional={optional}
onSelected={handleChange}
onSelected={handleSelect}
helpText={helpText}
errorText={errorText}
placeholder={placeholder}
showRequiredAsterisk={true}
selected={value as string | string[]}
selected={selectedValue}
roundedBorders={false}
testID={testID}
/>

2
types/api/apps.d.ts vendored
View File

@@ -148,7 +148,7 @@ type AppForm = {
depends_on?: string[];
};
type AppFormValue = string | boolean | number | AppSelectOption | AppSelectOption[];
type AppFormValue = string | boolean | number | AppSelectOption | AppSelectOption[] | null;
type AppFormValues = {[name: string]: AppFormValue};
type AppSelectOption = {