Unverified Commit 3337e111 authored by Joram Wilander's avatar Joram Wilander Committed by GitHub
Browse files

MM-12844/MM-13001 Add interactive dialogs (#2049)

* Add UI widget for TextSetting

* Add UI widget for AutocompleteSelector

* Add interactive dialogs

* Move common functions to mattermost-redux

* Handle race where trigger ID is returned before dialog WS event arrives

* Updates per feedback

* Updating modal css

* Updating modal height on mobile

* Updating dialog css for mobile

* Updating modal padding

* Updating dialog css to flex
parent 5a143465
......@@ -7,6 +7,7 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import {IntegrationTypes} from 'mattermost-redux/action_types';
import {openModal} from 'actions/views/modals';
import * as GlobalActions from 'actions/global_actions.jsx';
......@@ -101,6 +102,10 @@ export function executeCommand(message, args) {
return {data: true};
}
if (data.trigger_id) {
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
}
if (hasGotoLocation) {
if (data.goto_location.startsWith('/')) {
browserHistory.push(data.goto_location);
......
......@@ -6,6 +6,11 @@ import * as IntegrationActions from 'mattermost-redux/actions/integrations';
import {getProfilesByIds} from 'mattermost-redux/actions/users';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import {ModalIdentifiers} from 'utils/constants';
import {openModal} from 'actions/views/modals';
import InteractiveDialog from 'components/interactive_dialog';
import store from 'stores/redux_store.jsx';
import {Integrations} from 'utils/constants.jsx';
export function loadIncomingHooksAndProfilesForTeam(teamId, page = 0, perPage = Integrations.PAGE_SIZE) {
......@@ -110,3 +115,22 @@ export function getYoutubeVideoInfo(googleKey, videoId, success, error) {
return success(res.body);
});
}
let previousTriggerId = '';
store.subscribe(() => {
const state = store.getState();
const currentTriggerId = state.entities.integrations.dialogTriggerId;
if (currentTriggerId === previousTriggerId) {
return;
}
previousTriggerId = currentTriggerId;
const dialog = state.entities.integrations.dialog || {};
if (dialog.trigger_id !== currentTriggerId) {
return;
}
store.dispatch(openModal({modalId: ModalIdentifiers.INTERACTIVE_DIALOG, dialogType: InteractiveDialog}));
});
......@@ -24,14 +24,14 @@ export function editPost(post) {
};
}
export function selectAttachmentMenuAction(postId, actionId, dataSource, displayText, value) {
export function selectAttachmentMenuAction(postId, actionId, dataSource, text, value) {
return async (dispatch) => {
dispatch({
type: ActionTypes.SELECT_ATTACHMENT_MENU_ACTION,
postId,
data: {
[actionId]: {
displayText,
text,
value,
},
},
......
......@@ -2,7 +2,17 @@
// See LICENSE.txt for license information.
import {batchActions} from 'redux-batched-actions';
import {ChannelTypes, EmojiTypes, PostTypes, TeamTypes, UserTypes, RoleTypes, GeneralTypes, AdminTypes} from 'mattermost-redux/action_types';
import {
ChannelTypes,
EmojiTypes,
PostTypes,
TeamTypes,
UserTypes,
RoleTypes,
GeneralTypes,
AdminTypes,
IntegrationTypes,
} from 'mattermost-redux/action_types';
import {WebsocketEvents, General} from 'mattermost-redux/constants';
import {
getChannelAndMyMember,
......@@ -38,6 +48,7 @@ import {ActionTypes, Constants, AnnouncementBarMessages, SocketEvents, UserStatu
import {fromAutoResponder} from 'utils/post_utils';
import {getSiteURL} from 'utils/url.jsx';
import RemovedFromChannelModal from 'components/removed_from_channel_modal';
import InteractiveDialog from 'components/interactive_dialog';
const dispatch = store.dispatch;
const getState = store.getState;
......@@ -335,6 +346,10 @@ function handleEvent(msg) {
handlePluginStatusesChangedEvent(msg);
break;
case SocketEvents.OPEN_DIALOG:
handleOpenDialogEvent(msg);
break;
default:
}
......@@ -845,3 +860,18 @@ function handleLicenseChanged(msg) {
function handlePluginStatusesChangedEvent(msg) {
store.dispatch({type: AdminTypes.RECEIVED_PLUGIN_STATUSES, data: msg.data.plugin_statuses});
}
function handleOpenDialogEvent(msg) {
const data = (msg.data && msg.data.dialog) || {};
const dialog = JSON.parse(data);
store.dispatch({type: IntegrationTypes.RECEIVED_DIALOG, data: dialog});
const currentTriggerId = getState().entities.integrations.dialogTriggerId;
if (dialog.trigger_id !== currentTriggerId) {
return;
}
store.dispatch(openModal({modalId: ModalIdentifiers.INTERACTIVE_DIALOG, dialogType: InteractiveDialog}));
}
\ No newline at end of file
......@@ -53,7 +53,7 @@ exports[`components/MessageExportSettings should match snapshot, disabled, actia
}
value={false}
/>
<TextSetting
<AdminTextSetting
disabled={true}
helpText={
<FormattedHTMLMessage
......@@ -70,11 +70,9 @@ exports[`components/MessageExportSettings should match snapshot, disabled, actia
values={Object {}}
/>
}
maxLength={null}
onChange={[Function]}
placeholder="E.g.: \\"02:00\\""
setByEnv={false}
type="input"
value="01:00"
/>
<DropdownSetting
......@@ -232,7 +230,7 @@ exports[`components/MessageExportSettings should match snapshot, disabled, globa
}
value={false}
/>
<TextSetting
<AdminTextSetting
disabled={true}
helpText={
<FormattedHTMLMessage
......@@ -249,11 +247,9 @@ exports[`components/MessageExportSettings should match snapshot, disabled, globa
values={Object {}}
/>
}
maxLength={null}
onChange={[Function]}
placeholder="E.g.: \\"02:00\\""
setByEnv={false}
type="input"
value="01:00"
/>
<DropdownSetting
......@@ -335,7 +331,7 @@ exports[`components/MessageExportSettings should match snapshot, disabled, globa
]
}
/>
<TextSetting
<AdminTextSetting
disabled={true}
helpText={
<FormattedMessage
......@@ -352,14 +348,12 @@ exports[`components/MessageExportSettings should match snapshot, disabled, globa
values={Object {}}
/>
}
maxLength={null}
onChange={[Function]}
placeholder="E.g.: \\"globalRelayUser\\""
setByEnv={false}
type="input"
value="globalRelayUser"
/>
<TextSetting
<AdminTextSetting
disabled={true}
helpText={
<FormattedMessage
......@@ -376,14 +370,12 @@ exports[`components/MessageExportSettings should match snapshot, disabled, globa
values={Object {}}
/>
}
maxLength={null}
onChange={[Function]}
placeholder="E.g.: \\"globalRelayPassword\\""
setByEnv={false}
type="input"
value="globalRelayPassword"
/>
<TextSetting
<AdminTextSetting
disabled={true}
helpText={
<FormattedHTMLMessage
......@@ -400,11 +392,9 @@ exports[`components/MessageExportSettings should match snapshot, disabled, globa
values={Object {}}
/>
}
maxLength={null}
onChange={[Function]}
placeholder="E.g.: \\"globalrelay@mattermost.com\\""
setByEnv={false}
type="input"
value="globalRelay@mattermost.com"
/>
</SettingsGroup>
......@@ -521,7 +511,7 @@ exports[`components/MessageExportSettings should match snapshot, enabled, actian
}
value={true}
/>
<TextSetting
<AdminTextSetting
disabled={false}
helpText={
<FormattedHTMLMessage
......@@ -538,11 +528,9 @@ exports[`components/MessageExportSettings should match snapshot, enabled, actian
values={Object {}}
/>
}
maxLength={null}
onChange={[Function]}
placeholder="E.g.: \\"02:00\\""
setByEnv={false}
type="input"
value="01:00"
/>
<DropdownSetting
......@@ -700,7 +688,7 @@ exports[`components/MessageExportSettings should match snapshot, enabled, global
}
value={true}
/>
<TextSetting
<AdminTextSetting
disabled={false}
helpText={
<FormattedHTMLMessage
......@@ -717,11 +705,9 @@ exports[`components/MessageExportSettings should match snapshot, enabled, global
values={Object {}}
/>
}
maxLength={null}
onChange={[Function]}
placeholder="E.g.: \\"02:00\\""
setByEnv={false}
type="input"
value="01:00"
/>
<DropdownSetting
......@@ -803,7 +789,7 @@ exports[`components/MessageExportSettings should match snapshot, enabled, global
]
}
/>
<TextSetting
<AdminTextSetting
disabled={false}
helpText={
<FormattedMessage
......@@ -820,14 +806,12 @@ exports[`components/MessageExportSettings should match snapshot, enabled, global
values={Object {}}
/>
}
maxLength={null}
onChange={[Function]}
placeholder="E.g.: \\"globalRelayUser\\""
setByEnv={false}
type="input"
value="globalRelayUser"
/>
<TextSetting
<AdminTextSetting
disabled={false}
helpText={
<FormattedMessage
......@@ -844,14 +828,12 @@ exports[`components/MessageExportSettings should match snapshot, enabled, global
values={Object {}}
/>
}
maxLength={null}
onChange={[Function]}
placeholder="E.g.: \\"globalRelayPassword\\""
setByEnv={false}
type="input"
value="globalRelayPassword"
/>
<TextSetting
<AdminTextSetting
disabled={false}
helpText={
<FormattedHTMLMessage
......@@ -868,11 +850,9 @@ exports[`components/MessageExportSettings should match snapshot, enabled, global
values={Object {}}
/>
}
maxLength={null}
onChange={[Function]}
placeholder="E.g.: \\"globalrelay@mattermost.com\\""
setByEnv={false}
type="input"
value="globalRelay@mattermost.com"
/>
</SettingsGroup>
......
......@@ -51,7 +51,7 @@ exports[`components/admin_console/SchemaAdminSettings should match snapshot with
<SettingsGroup
show={true}
>
<TextSetting
<AdminTextSetting
disabled={false}
helpText={
<FormattedMessage
......@@ -63,7 +63,6 @@ exports[`components/admin_console/SchemaAdminSettings should match snapshot with
id="FirstSettings.settinga"
key="Config_text_FirstSettings.settinga"
label="Setting One"
maxLength={null}
onChange={[Function]}
placeholder="e.g. some setting"
setByEnv={false}
......@@ -206,7 +205,7 @@ exports[`components/admin_console/SchemaAdminSettings should match snapshot with
placeholder="Type a username here"
value="3xz3r6n7dtbbmgref3yw4zg7sr"
/>
<TextSetting
<AdminTextSetting
disabled={false}
helpText={
<FormattedMessage
......@@ -218,14 +217,13 @@ exports[`components/admin_console/SchemaAdminSettings should match snapshot with
id="SecondSettings.settingg"
key="Config_text_SecondSettings.settingg"
label="Setting Seven"
maxLength={null}
onChange={[Function]}
placeholder="e.g. some setting"
setByEnv={false}
type="number"
value={7}
/>
<TextSetting
<AdminTextSetting
disabled={false}
helpText={
<FormattedMessage
......@@ -237,7 +235,6 @@ exports[`components/admin_console/SchemaAdminSettings should match snapshot with
id="SecondSettings.settingh"
key="Config_text_SecondSettings.settingh"
label="Setting Eight"
maxLength={null}
onChange={[Function]}
placeholder="e.g. some setting"
setByEnv={false}
......
......@@ -24,7 +24,7 @@ exports[`components/admin_console/CustomPluginSettings should match snapshot wit
}
}
/>
<TextSetting
<AdminTextSetting
disabled={false}
helpText={
<span>
......@@ -34,7 +34,6 @@ exports[`components/admin_console/CustomPluginSettings should match snapshot wit
id="settinga"
key="testplugin_text_settinga"
label="Setting One"
maxLength={null}
onChange={[Function]}
placeholder="e.g. some setting"
setByEnv={false}
......@@ -296,7 +295,7 @@ exports[`components/admin_console/CustomPluginSettings should match snapshot wit
}
}
/>
<TextSetting
<AdminTextSetting
disabled={false}
helpText={
<span>
......@@ -306,7 +305,6 @@ exports[`components/admin_console/CustomPluginSettings should match snapshot wit
id="settinga"
key="testplugin_text_settinga"
label="Setting One"
maxLength={null}
onChange={[Function]}
placeholder="e.g. some setting"
setByEnv={false}
......
......@@ -4,98 +4,27 @@
import PropTypes from 'prop-types';
import React from 'react';
import Setting from './setting.jsx';
import TextSetting from 'components/widgets/settings/text_setting';
export default class TextSetting extends React.Component {
static get propTypes() {
return {
id: PropTypes.string.isRequired,
label: PropTypes.node.isRequired,
placeholder: PropTypes.string,
helpText: PropTypes.node,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
maxLength: PropTypes.number,
onChange: PropTypes.func,
disabled: PropTypes.bool,
setByEnv: PropTypes.bool.isRequired,
type: PropTypes.oneOf([
'number',
'input',
'textarea',
]),
};
}
import SetByEnv from './set_by_env';
static get defaultProps() {
return {
type: 'input',
maxLength: null,
};
}
const AdminTextSetting = (props) => {
const {setByEnv, disabled, ...sharedProps} = props;
handleChange = (e) => {
if (this.props.type === 'number') {
this.props.onChange(this.props.id, parseInt(e.target.value, 10));
} else {
this.props.onChange(this.props.id, e.target.value);
}
}
return (
<TextSetting
{...sharedProps}
labelClassName='col-sm-4'
inputClassName='col-sm-8'
disabled={disabled || setByEnv}
footer={setByEnv ? <SetByEnv/> : null}
/>
);
};
render() {
let input = null;
if (this.props.type === 'input') {
input = (
<input
id={this.props.id}
className='form-control'
type='text'
placeholder={this.props.placeholder}
value={this.props.value}
maxLength={this.props.maxLength}
onChange={this.handleChange}
disabled={this.props.disabled || this.props.setByEnv}
/>
);
} else if (this.props.type === 'number') {
input = (
<input
id={this.props.id}
className='form-control'
type='number'
placeholder={this.props.placeholder}
value={this.props.value}
maxLength={this.props.maxLength}
onChange={this.handleChange}
disabled={this.props.disabled || this.props.setByEnv}
/>
);
} else if (this.props.type === 'textarea') {
input = (
<textarea
id={this.props.id}
className='form-control'
rows='5'
placeholder={this.props.placeholder}
value={this.props.value}
maxLength={this.props.maxLength}
onChange={this.handleChange}
disabled={this.props.disabled || this.props.setByEnv}
/>
);
}
AdminTextSetting.propTypes = {
...TextSetting.propTypes,
setByEnv: PropTypes.bool.isRequired,
};
return (
<Setting
label={this.props.label}
helpText={this.props.helpText}
inputId={this.props.id}
setByEnv={this.props.setByEnv}
>
{input}
</Setting>
);
}
}
export default AdminTextSetting;
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
import MenuActionProvider from 'components/suggestion/menu_action_provider';
import GenericUserProvider from 'components/suggestion/generic_user_provider.jsx';
import GenericChannelProvider from 'components/suggestion/generic_channel_provider.jsx';
import TextSetting from 'components/widgets/settings/text_setting';
import AutocompleteSelector from 'components/widgets/settings/autocomplete_selector';
const TEXT_DEFAULT_MAX_LENGTH = 150;
const TEXTAREA_DEFAULT_MAX_LENGTH = 3000;
export default class DialogElement extends React.PureComponent {
static propTypes = {
displayName: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
subtype: PropTypes.string,
placeholder: PropTypes.string,
helpText: PropTypes.string,
errorText: PropTypes.node,
maxLength: PropTypes.number,
dataSource: PropTypes.string,
optional: PropTypes.bool,
options: PropTypes.arrayOf(PropTypes.object),
value: PropTypes.any,
onChange: PropTypes.func,
}
constructor(props) {
super(props);
this.providers = [];
if (props.type === 'select') {
if (props.dataSource === 'users') {
this.providers = [new GenericUserProvider()];
} else if (props.dataSource === 'channels') {
this.providers = [new GenericChannelProvider()];
} else if (props.options) {
this.providers = [new MenuActionProvider(props.options)];
}
}
this.state = {
value: '',
};
}
handleSelected = (selected) => {
const {name, dataSource, onChange} = this.props;
if (dataSource === 'users') {
onChange(name, selected.id);
this.setState({value: selected.username});
} else if (dataSource === 'channels') {
onChange(name, selected.id);
this.setState({value: selected.display_name});
} else {
onChange(name, selected.value);
this.setState({value: selected.text});
}
}
render() {
const {
name,
subtype,
displayName,
value,
placeholder,
onChange,
helpText,
errorText,
optional,
} = this.props;
let {type, maxLength} = this.props;
let displayNameContent = displayName;
if (optional) {
displayNameContent = (
<React.Fragment>
{displayName + ' '}
<span className='font-weight--normal light'>
<FormattedMessage
id='interactive_dialog.element.optional'
defaultMessage='(optional)'
/>
</span>
</React.Fragment>
);
} else {
displayNameContent = (
<React.Fragment>