Add Markdown fields to modals (#5407)

* Add Markdown fields to modals

* Add better handling for readonly and default values on commands

* Create stylesheet instead of passing the style directly to the components

* Fix unhandled promise error and markdown not showing

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Daniel Espino García 2021-06-30 18:55:08 +02:00 committed by GitHub
parent 1f843ddb4f
commit bdbdc0b7ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 118 additions and 9 deletions

View file

@ -23,7 +23,7 @@ import {
AppCallTypes,
AppFieldTypes,
getAppsBindings,
getChannel,
selectChannel,
getCurrentTeamId,
doAppCall,
getStore,
@ -38,6 +38,9 @@ import {
getCurrentTeam,
selectChannelByName,
errorMessage as parserErrorMessage,
selectUser,
getUser,
getChannel,
} from './app_command_parser_dependencies';
export type Store = {
@ -236,6 +239,7 @@ export class ParsedCommand {
fields = this.form.fields;
}
fields = fields.filter((f) => f.type !== AppFieldTypes.MARKDOWN && !f.readonly);
this.state = ParseState.StartParameter;
this.i = this.incompleteStart || 0;
let flagEqualsUsed = false;
@ -563,6 +567,8 @@ export class AppCommandParser {
return {call: null, errorMessage: parserErrorMessage(this.intl, parsed.error, parsed.command, parsed.i)};
}
await this.addDefaultAndReadOnlyValues(parsed);
const missing = this.getMissingFields(parsed);
if (missing.length > 0) {
const missingStr = missing.map((f) => f.label).join(', ');
@ -578,6 +584,62 @@ export class AppCommandParser {
return this.composeCallFromParsed(parsed);
}
private async addDefaultAndReadOnlyValues(parsed: ParsedCommand) {
await Promise.all(parsed.form?.fields?.map(async (f) => {
if (!f.value) {
return;
}
if (!f.readonly || f.name in parsed.values) {
return;
}
switch (f.type) {
case AppFieldTypes.TEXT:
parsed.values[f.name] = f.value as string;
break;
case AppFieldTypes.BOOL:
parsed.values[f.name] = 'true';
break;
case AppFieldTypes.USER: {
const userID = (f.value as AppSelectOption).value;
let user = selectUser(this.store.getState(), userID);
if (!user) {
const dispatchResult = await this.store.dispatch(getUser(userID));
if ('error' in dispatchResult) {
// Silently fail on default value
break;
}
user = dispatchResult.data;
}
parsed.values[f.name] = user.username;
break;
}
case AppFieldTypes.CHANNEL: {
const channelID = (f.value as AppSelectOption).label;
let channel = selectChannel(this.store.getState(), channelID);
if (!channel) {
const dispatchResult = await this.store.dispatch(getChannel(channelID));
if ('error' in dispatchResult) {
// Silently fail on default value
break;
}
channel = dispatchResult.data;
}
parsed.values[f.name] = channel.name;
break;
}
case AppFieldTypes.STATIC_SELECT:
case AppFieldTypes.DYNAMIC_SELECT:
parsed.values[f.name] = (f.value as AppSelectOption).value;
break;
case AppFieldTypes.MARKDOWN:
// Do nothing
}
}) || []);
}
// getSuggestionsBase is a synchronous function that returns results for base commands
public getSuggestionsBase = (pretext: string): AutocompleteSuggestion[] => {
const command = pretext.toLowerCase();
@ -837,7 +899,7 @@ export class AppCommandParser {
// getChannel gets the channel in which the user is typing the command
getChannel = (): Channel | null => {
const state = this.store.getState();
return getChannel(state, this.channelID);
return selectChannel(state, this.channelID);
}
setChannelContext = (channelID: string, rootPostID?: string) => {

View file

@ -43,12 +43,12 @@ export {
export {getAppsBindings} from '@mm-redux/selectors/entities/apps';
export {getPost} from '@mm-redux/selectors/entities/posts';
export {getChannel, getCurrentChannel, getChannelByName as selectChannelByName} from '@mm-redux/selectors/entities/channels';
export {getChannel as selectChannel, getCurrentChannel, getChannelByName as selectChannelByName} from '@mm-redux/selectors/entities/channels';
export {getCurrentTeamId, getCurrentTeam} from '@mm-redux/selectors/entities/teams';
export {getUserByUsername as selectUserByUsername} from '@mm-redux/selectors/entities/users';
export {getUserByUsername as selectUserByUsername, getUser as selectUser} from '@mm-redux/selectors/entities/users';
export {getUserByUsername} from '@mm-redux/actions/users';
export {getChannelByNameAndTeamName} from '@mm-redux/actions/channels';
export {getUserByUsername, getUser} from '@mm-redux/actions/users';
export {getChannelByNameAndTeamName, getChannel} from '@mm-redux/actions/channels';
export {doAppCall} from '@actions/apps';
export {createCallRequest} from '@utils/apps';

View file

@ -43,4 +43,5 @@ export const AppFieldTypes: { [name: string]: AppFieldType } = {
BOOL: 'bool',
USER: 'user',
CHANNEL: 'channel',
MARKDOWN: 'markdown',
};

View file

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import {Theme} from '@mm-redux/types/preferences';
import {AppField, AppFormValue, AppSelectOption} from '@mm-redux/types/apps';
@ -13,6 +14,9 @@ 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';
import Markdown from '@components/markdown/markdown';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {makeStyleSheetFromTheme} from '@utils/theme';
const TEXT_DEFAULT_MAX_LENGTH = 150;
const TEXTAREA_DEFAULT_MAX_LENGTH = 3000;
@ -32,6 +36,20 @@ type State = {
selected: DialogOption | null;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
markdownFieldContainer: {
marginTop: 15,
marginBottom: 10,
marginLeft: 15,
},
markdownFieldText: {
fontSize: 14,
color: theme.centerChannelColor,
},
};
});
export default class AppsFormField extends React.PureComponent<Props, State> {
state = {
selected: null,
@ -78,7 +96,8 @@ export default class AppsFormField extends React.PureComponent<Props, State> {
const placeholder = field.hint || '';
const displayName = (field.modal_label || field.label) as string;
if (field.type === 'text') {
switch (field.type) {
case AppFieldTypes.TEXT: {
let keyboardType = 'default';
let multiline = false;
let secureTextEntry = false;
@ -141,7 +160,11 @@ export default class AppsFormField extends React.PureComponent<Props, State> {
disabled={field.readonly}
/>
);
} else if ([AppFieldTypes.USER, AppFieldTypes.CHANNEL, AppFieldTypes.STATIC_SELECT, AppFieldTypes.DYNAMIC_SELECT].includes(field.type)) {
}
case AppFieldTypes.USER:
case AppFieldTypes.CHANNEL:
case AppFieldTypes.STATIC_SELECT:
case AppFieldTypes.DYNAMIC_SELECT: {
let dataSource = '';
let options: DialogOption[] = [];
@ -179,7 +202,8 @@ export default class AppsFormField extends React.PureComponent<Props, State> {
disabled={field.readonly}
/>
);
} else if (field.type === AppFieldTypes.BOOL) {
}
case AppFieldTypes.BOOL: {
const boolValue = value as boolean;
return (
<BoolSetting
@ -196,6 +220,28 @@ export default class AppsFormField extends React.PureComponent<Props, State> {
/>
);
}
case AppFieldTypes.MARKDOWN: {
if (!field.description) {
return null;
}
const style = getStyleSheet(theme);
return (
<View
style={style.markdownFieldContainer}
>
<Markdown
value={field.description}
mentionKeys={[]}
blockStyles={getMarkdownBlockStyles(theme)}
textStyles={getMarkdownTextStyles(theme)}
baseTextStyle={style.markdownFieldText}
theme={theme}
/>
</View>
);
}
}
return null;
}