From bdbdc0b7ba1a30c0accd8be692a2f21016ec0621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Wed, 30 Jun 2021 18:55:08 +0200 Subject: [PATCH] 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 --- .../app_command_parser/app_command_parser.ts | 66 ++++++++++++++++++- .../app_command_parser_dependencies.ts | 8 +-- app/mm-redux/constants/apps.ts | 1 + app/screens/apps_form/apps_form_field.tsx | 52 ++++++++++++++- 4 files changed, 118 insertions(+), 9 deletions(-) diff --git a/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts b/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts index 00a289480..4745b31bf 100644 --- a/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts +++ b/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser.ts @@ -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) => { diff --git a/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser_dependencies.ts b/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser_dependencies.ts index 992fb4e79..7318d47a7 100644 --- a/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser_dependencies.ts +++ b/app/components/autocomplete/slash_suggestion/app_command_parser/app_command_parser_dependencies.ts @@ -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'; diff --git a/app/mm-redux/constants/apps.ts b/app/mm-redux/constants/apps.ts index 49196a40b..4a22c9cce 100644 --- a/app/mm-redux/constants/apps.ts +++ b/app/mm-redux/constants/apps.ts @@ -43,4 +43,5 @@ export const AppFieldTypes: { [name: string]: AppFieldType } = { BOOL: 'bool', USER: 'user', CHANNEL: 'channel', + MARKDOWN: 'markdown', }; diff --git a/app/screens/apps_form/apps_form_field.tsx b/app/screens/apps_form/apps_form_field.tsx index 4358e30ee..95a9faec0 100644 --- a/app/screens/apps_form/apps_form_field.tsx +++ b/app/screens/apps_form/apps_form_field.tsx @@ -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 { state = { selected: null, @@ -78,7 +96,8 @@ export default class AppsFormField extends React.PureComponent { 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 { 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 { disabled={field.readonly} /> ); - } else if (field.type === AppFieldTypes.BOOL) { + } + case AppFieldTypes.BOOL: { const boolValue = value as boolean; return ( { /> ); } + case AppFieldTypes.MARKDOWN: { + if (!field.description) { + return null; + } + const style = getStyleSheet(theme); + + return ( + + + + ); + } + } return null; }