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:
parent
1f843ddb4f
commit
bdbdc0b7ba
4 changed files with 118 additions and 9 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -43,4 +43,5 @@ export const AppFieldTypes: { [name: string]: AppFieldType } = {
|
|||
BOOL: 'bool',
|
||||
USER: 'user',
|
||||
CHANNEL: 'channel',
|
||||
MARKDOWN: 'markdown',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue