forked from Ivasoft/mattermost-mobile
fix multiselect for app forms
This commit is contained in:
@@ -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]);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
2
types/api/apps.d.ts
vendored
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user