Feature: Wrangler (#23602)
* Migrate feature/wrangler to mono-repo * Add wrangler files * Fix linters, types, etc * Fix snapshots * Fix playwright * Fix pipelines * Fix more pipeline * Fixes for pipelines * More changes for pipeline * Fix types * Add support for a feature flag, but leave it defaulted on for spinwick usage for now * Update snapshot * fix js error when removing last value of multiselect, support CSV marshaling to string array for textsetting * Fix linter * Remove TODO * Remove another TODO * fix tests * Fix i18n * Add server tests * Fix linter * Fix linter * Use proper icon for dot menu * Update snapshot * Add Cypress UI tests for various entrypoints to move thread modal, split SCSS out from forward post into its own thing * clean up * fix linter * More cleanup * Revert files to master * Fix linter for e2e tests * Make ForwardPostChannelSelect channel types configurable with a prop * Add missing return * Fixes from PR feedback * First batch of PR Feedback * Another batch of PR changes * Fix linter * Update snapshots * Wrangler system messages are translated to each user's locale * Initially translate Wrangler into system locale rather than initiating user * More fixes for PR Feedback * Fix some server tests * More updates with master. Fixes around pipelines. Enforce Enterprise license on front/back end * Add tests for dot_menu * More pipeline fixes * Fix e2etests prettier * Update cypress tests, change occurrences of 'Wrangler' with 'Move Thread' * Fix linter * Remove enterprise lock * A couple more occurrences of wrangler strings, and one more enterprise lock * Fix server tests * Fix i18n * Fix e2e linter * Feature flag shouldn't be on by default * Enable move threads feature in smoke tests (#25657) * enable move threads feature * add @prod tag * Fix move_thread_from_public_channel e2e test * Fix e2e style --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
Этот коммит содержится в:
@@ -80,7 +80,7 @@ exports[`components/admin_console/SchemaAdminSettings should match snapshot with
|
||||
onChange={[Function]}
|
||||
placeholder="e.g. some setting"
|
||||
setByEnv={false}
|
||||
type="input"
|
||||
type="text"
|
||||
value="fsdsdg"
|
||||
/>
|
||||
<BooleanSetting
|
||||
|
||||
@@ -3199,6 +3199,91 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
],
|
||||
},
|
||||
},
|
||||
wrangler: {
|
||||
url: 'site_config/wrangler',
|
||||
title: t('admin.sidebar.move_thread'),
|
||||
title_default: 'Move Thread (Beta)',
|
||||
isHidden: it.any(it.not(it.userHasReadPermissionOnResource(RESOURCE_KEYS.SITE.POSTS)), it.configIsFalse('FeatureFlags', 'MoveThreadsEnabled')),
|
||||
schema: {
|
||||
id: 'WranglerSettings',
|
||||
name: t('admin.site.move_thread'),
|
||||
name_default: 'Move Thread (Beta)',
|
||||
settings: [
|
||||
{
|
||||
type: 'roles',
|
||||
multiple: true,
|
||||
key: 'WranglerSettings.PermittedWranglerRoles',
|
||||
label: t('admin.experimental.PermittedMoveThreadRoles.title'),
|
||||
label_default: 'Permitted Roles',
|
||||
help_text: t('admin.experimental.PermittedMoveThreadRoles.desc'),
|
||||
help_text_default: 'Choose who is allowed to move threads to other channels based on roles. (Other permissions below still apply).',
|
||||
help_text_markdown: false,
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'WranglerSettings.AllowedEmailDomain',
|
||||
multiple: true,
|
||||
label: t('admin.experimental.allowedEmailDomain.title'),
|
||||
label_default: 'Allowed Email Domain',
|
||||
help_text: t('admin.experimental.allowedEmailDomain.desc'),
|
||||
help_text_default: '(Optional) When set, users must have an email ending in this domain to move threads. Multiple domains can be specified by separating them with commas.',
|
||||
help_text_markdown: false,
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
key: 'WranglerSettings.MoveThreadMaxCount',
|
||||
label: t('admin.experimental.moveThreadMaxCount.title'),
|
||||
label_default: 'Max Thread Count Move Size',
|
||||
help_text: t('admin.experimental.moveThreadMaxCount.desc'),
|
||||
help_text_default: 'The maximum number of messages in a thread that the plugin is allowed to move. Leave empty for unlimited messages.',
|
||||
help_text_markdown: false,
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'WranglerSettings.MoveThreadToAnotherTeamEnable',
|
||||
label: t('admin.experimental.moveThreadToAnotherTeamEnable.title'),
|
||||
label_default: 'Enable Moving Threads To Different Teams',
|
||||
help_text: t('admin.experimental.moveThreadToAnotherTeamEnable.desc'),
|
||||
help_text_default: 'Control whether Wrangler is permitted to move message threads from one team to another or not.',
|
||||
help_text_markdown: false,
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'WranglerSettings.MoveThreadFromPrivateChannelEnable',
|
||||
label: t('admin.experimental.moveThreadFromPrivateChannelEnable.title'),
|
||||
label_default: 'Enable Moving Threads From Private Channels',
|
||||
help_text: t('admin.experimental.moveThreadFromPrivateChannelEnable.desc'),
|
||||
help_text_default: 'Control whether Wrangler is permitted to move message threads from private channels or not.',
|
||||
help_text_markdown: false,
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'WranglerSettings.MoveThreadFromDirectMessageChannelEnable',
|
||||
label: t('admin.experimental.moveThreadFromDirectMessageChannelEnable.title'),
|
||||
label_default: 'Enable Moving Threads From Direct Message Channels',
|
||||
help_text: t('admin.experimental.moveThreadFromDirectMessageChannelEnable.desc'),
|
||||
help_text_default: 'Control whether Wrangler is permitted to move message threads from direct message channels or not.',
|
||||
help_text_markdown: false,
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'WranglerSettings.MoveThreadFromGroupMessageChannelEnable',
|
||||
label: t('admin.experimental.moveThreadFromGroupMessageChannelEnable.title'),
|
||||
label_default: 'Enable Moving Threads From Group Message Channels',
|
||||
help_text: t('admin.experimental.moveThreadFromGroupMessageChannelEnable.desc'),
|
||||
help_text_default: 'Control whether Wrangler is permitted to move message threads from group message channels or not.',
|
||||
help_text_markdown: false,
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
file_sharing_downloads: {
|
||||
url: 'site_config/file_sharing_downloads',
|
||||
title: t('admin.sidebar.fileSharingDownloads'),
|
||||
|
||||
@@ -46,7 +46,7 @@ exports[`components/admin_console/CustomPluginSettings should match snapshot wit
|
||||
onChange={[Function]}
|
||||
placeholder="e.g. some setting"
|
||||
setByEnv={false}
|
||||
type="input"
|
||||
type="text"
|
||||
value="setting_default"
|
||||
/>
|
||||
<BooleanSetting
|
||||
@@ -356,7 +356,7 @@ exports[`components/admin_console/CustomPluginSettings should match snapshot wit
|
||||
onChange={[Function]}
|
||||
placeholder="e.g. some setting"
|
||||
setByEnv={false}
|
||||
type="input"
|
||||
type="text"
|
||||
value="fsdsdg"
|
||||
/>
|
||||
<BooleanSetting
|
||||
|
||||
@@ -45,9 +45,9 @@ State
|
||||
}
|
||||
|
||||
handleChange = (newValue: ValueType<Option>) => {
|
||||
const values = (newValue as Option[]).map((n) => {
|
||||
const values = newValue ? (newValue as Option[]).map((n) => {
|
||||
return n.value;
|
||||
});
|
||||
}) : [];
|
||||
|
||||
this.props.onChange(this.props.id, values);
|
||||
this.setState({error: false});
|
||||
|
||||
@@ -37,6 +37,8 @@ import Setting from './setting';
|
||||
|
||||
import './schema_admin_settings.scss';
|
||||
|
||||
const emptyList = [];
|
||||
|
||||
export default class SchemaAdminSettings extends React.PureComponent {
|
||||
static propTypes = {
|
||||
config: PropTypes.object,
|
||||
@@ -75,6 +77,7 @@ export default class SchemaAdminSettings extends React.PureComponent {
|
||||
[Constants.SettingsTypes.TYPE_LANGUAGE]: this.buildLanguageSetting,
|
||||
[Constants.SettingsTypes.TYPE_JOBSTABLE]: this.buildJobsTableSetting,
|
||||
[Constants.SettingsTypes.TYPE_FILE_UPLOAD]: this.buildFileUploadSetting,
|
||||
[Constants.SettingsTypes.TYPE_ROLES]: this.buildRolesSetting,
|
||||
[Constants.SettingsTypes.TYPE_CUSTOM]: this.buildCustomSetting,
|
||||
};
|
||||
this.state = {
|
||||
@@ -439,16 +442,20 @@ export default class SchemaAdminSettings extends React.PureComponent {
|
||||
};
|
||||
|
||||
buildTextSetting = (setting) => {
|
||||
let inputType = 'input';
|
||||
let inputType = 'text';
|
||||
if (setting.type === Constants.SettingsTypes.TYPE_NUMBER) {
|
||||
inputType = 'number';
|
||||
} else if (setting.type === Constants.SettingsTypes.TYPE_LONG_TEXT) {
|
||||
inputType = 'textarea';
|
||||
}
|
||||
|
||||
let value = this.state[setting.key] ?? '';
|
||||
let value = '';
|
||||
if (setting.dynamic_value) {
|
||||
value = setting.dynamic_value(value, this.props.config, this.state, this.props.license);
|
||||
} else if (setting.multiple) {
|
||||
value = this.state[setting.key] ? this.state[setting.key].join(',') : '';
|
||||
} else {
|
||||
value = this.state[setting.key] || '';
|
||||
}
|
||||
|
||||
let footer = null;
|
||||
@@ -466,6 +473,7 @@ export default class SchemaAdminSettings extends React.PureComponent {
|
||||
<TextSetting
|
||||
key={this.props.schema.id + '_text_' + setting.key}
|
||||
id={setting.key}
|
||||
multiple={setting.multiple}
|
||||
type={inputType}
|
||||
label={this.renderLabel(setting)}
|
||||
helpText={this.renderHelpText(setting)}
|
||||
@@ -570,6 +578,53 @@ export default class SchemaAdminSettings extends React.PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
buildRolesSetting = (setting) => {
|
||||
const {roles} = this.props;
|
||||
|
||||
const values = Object.keys(roles).map((r) => {
|
||||
return {
|
||||
value: roles[r].name,
|
||||
text: roles[r].name,
|
||||
};
|
||||
});
|
||||
|
||||
if (setting.multiple) {
|
||||
const noResultText = (
|
||||
<FormattedMessage
|
||||
id={setting.no_result}
|
||||
defaultMessage={setting.no_result_default}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<MultiSelectSetting
|
||||
key={this.props.schema.id + '_language_' + setting.key}
|
||||
id={setting.key}
|
||||
label={this.renderLabel(setting)}
|
||||
values={values}
|
||||
helpText={this.renderHelpText(setting)}
|
||||
selected={(this.state[setting.key] || emptyList)}
|
||||
disabled={this.isDisabled(setting)}
|
||||
setByEnv={this.isSetByEnv(setting.key)}
|
||||
onChange={this.handleChange}
|
||||
noResultText={noResultText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DropdownSetting
|
||||
key={this.props.schema.id + '_language_' + setting.key}
|
||||
id={setting.key}
|
||||
label={this.renderLabel(setting)}
|
||||
values={values}
|
||||
helpText={this.renderHelpText(setting)}
|
||||
value={this.state[setting.key] || values[0].value}
|
||||
disabled={this.isDisabled(setting)}
|
||||
setByEnv={this.isSetByEnv(setting.key)}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
buildLanguageSetting = (setting) => {
|
||||
const locales = I18n.getAllLanguages();
|
||||
const values = Object.keys(locales).map((l) => {
|
||||
|
||||
@@ -45,6 +45,14 @@ type AdminDefinitionSettingBanner = AdminDefinitionSettingBase & {
|
||||
banner_type: 'info' | 'warning';
|
||||
}
|
||||
|
||||
type AdminDefinitionSettingRole = AdminDefinitionSettingBase & {
|
||||
type: 'roles';
|
||||
multiple?: boolean;
|
||||
help_text?: string;
|
||||
help_text_default?: string;
|
||||
help_text_markdown?: boolean;
|
||||
}
|
||||
|
||||
type AdminDefinitionSettingInput = AdminDefinitionSettingBase & {
|
||||
type: 'text' | 'bool' | 'longtext' | 'number' | 'color';
|
||||
help_text?: string | JSX.Element;
|
||||
@@ -55,6 +63,7 @@ type AdminDefinitionSettingInput = AdminDefinitionSettingBase & {
|
||||
disabled_help_text_default?: string;
|
||||
disabled_help_text_markdown?: boolean;
|
||||
placeholder?: string;
|
||||
multiple?: boolean;
|
||||
placeholder_default?: string;
|
||||
validate?: Validator;
|
||||
setFromMetadataField?: string;
|
||||
@@ -155,7 +164,7 @@ export type AdminDefinitionSetting = AdminDefinitionSettingCustom |
|
||||
AdminDefinitionSettingInput | AdminDefinitionSettingGenerated |
|
||||
AdminDefinitionSettingBanner | AdminDefinitionSettingDropdown |
|
||||
AdminDefinitionSettingButton | AdminDefinitionSettingFileUpload |
|
||||
AdminDefinitionSettingJobsTable | AdminDefinitionSettingLanguage;
|
||||
AdminDefinitionSettingJobsTable | AdminDefinitionSettingLanguage | AdminDefinitionSettingRole;
|
||||
|
||||
type AdminDefinitionConfigSchemaSettings = {
|
||||
id: string;
|
||||
|
||||
@@ -1,5 +1,114 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/dot_menu/DotMenu should match snapshot, can move 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<button
|
||||
aria-controls="CENTER_dropdown_post_id_1"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="more"
|
||||
class="post-menu__item"
|
||||
data-testid="PostDotMenu-Button-post_id_1"
|
||||
id="CENTER_button_post_id_1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<button
|
||||
aria-controls="CENTER_dropdown_post_id_1"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="more"
|
||||
class="post-menu__item"
|
||||
data-testid="PostDotMenu-Button-post_id_1"
|
||||
id="CENTER_button_post_id_1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"replaceStoreState": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
"updateStoreState": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`components/dot_menu/DotMenu should match snapshot, canDelete 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
@@ -109,6 +218,115 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`components/dot_menu/DotMenu should match snapshot, cannot move 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<button
|
||||
aria-controls="CENTER_dropdown_post_id_1"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="more"
|
||||
class="post-menu__item"
|
||||
data-testid="PostDotMenu-Button-post_id_1"
|
||||
id="CENTER_button_post_id_1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<button
|
||||
aria-controls="CENTER_dropdown_post_id_1"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="more"
|
||||
class="post-menu__item"
|
||||
data-testid="PostDotMenu-Button-post_id_1"
|
||||
id="CENTER_button_post_id_1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"replaceStoreState": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
"updateStoreState": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = `
|
||||
<Menu
|
||||
menu={
|
||||
@@ -301,6 +519,26 @@ exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = `
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<MenuItem
|
||||
id="move_thread_post_id_1"
|
||||
labels={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Move Thread"
|
||||
id="post_info.move_thread"
|
||||
/>
|
||||
}
|
||||
leadingElement={
|
||||
<MessageArrowRightOutlineIcon
|
||||
size={18}
|
||||
/>
|
||||
}
|
||||
onClick={[Function]}
|
||||
trailingElements={
|
||||
<ShortcutKey
|
||||
shortcutKey="W"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<MenuItemSeparator />
|
||||
<MenuItem
|
||||
data-testid="permalink_post_id_1"
|
||||
|
||||
@@ -146,6 +146,7 @@ describe('components/dot_menu/DotMenu', () => {
|
||||
threadReplyCount: 0,
|
||||
userId: 'user_id_1',
|
||||
isMilitaryTime: false,
|
||||
canMove: true,
|
||||
};
|
||||
|
||||
test('should match snapshot, on Center', () => {
|
||||
@@ -180,6 +181,32 @@ describe('components/dot_menu/DotMenu', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, can move', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
canMove: true,
|
||||
};
|
||||
const wrapper = renderWithContext(
|
||||
<DotMenu {...props}/>,
|
||||
initialState,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, cannot move', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
canMove: false,
|
||||
};
|
||||
const wrapper = renderWithContext(
|
||||
<DotMenu {...props}/>,
|
||||
initialState,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show mark as unread when channel is not archived', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
EmoticonPlusOutlineIcon,
|
||||
LinkVariantIcon,
|
||||
MarkAsUnreadIcon,
|
||||
MessageArrowRightOutlineIcon,
|
||||
MessageCheckOutlineIcon,
|
||||
MessageMinusOutlineIcon,
|
||||
PencilOutlineIcon,
|
||||
@@ -31,6 +32,7 @@ import Permissions from 'mattermost-redux/constants/permissions';
|
||||
import DeletePostModal from 'components/delete_post_modal';
|
||||
import ForwardPostModal from 'components/forward_post_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
import MoveThreadModal from 'components/move_thread_modal';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
|
||||
import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS} from 'utils/constants';
|
||||
@@ -76,6 +78,7 @@ type Props = {
|
||||
isMobileView: boolean;
|
||||
timezone?: string;
|
||||
isMilitaryTime: boolean;
|
||||
canMove: boolean;
|
||||
|
||||
actions: {
|
||||
|
||||
@@ -118,6 +121,7 @@ type Props = {
|
||||
* Function to set the thread as followed/unfollowed
|
||||
*/
|
||||
setThreadFollow: (userId: string, teamId: string, threadId: string, newState: boolean) => void;
|
||||
|
||||
}; // TechDebt: Made non-mandatory while converting to typescript
|
||||
|
||||
canEdit: boolean;
|
||||
@@ -251,6 +255,24 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
trackDotMenuEvent(e, TELEMETRY_LABELS.DELETE);
|
||||
};
|
||||
|
||||
handleMoveThreadMenuItemActivated = (e: ChangeEvent): void => {
|
||||
e.preventDefault();
|
||||
if (!this.props.canMove) {
|
||||
return;
|
||||
}
|
||||
|
||||
trackDotMenuEvent(e, TELEMETRY_LABELS.MOVE_THREAD);
|
||||
const moveThreadModalData = {
|
||||
modalId: ModalIdentifiers.MOVE_THREAD_MODAL,
|
||||
dialogType: MoveThreadModal,
|
||||
dialogProps: {
|
||||
post: this.props.post,
|
||||
},
|
||||
};
|
||||
|
||||
this.props.actions.openModal(moveThreadModalData);
|
||||
};
|
||||
|
||||
handleForwardMenuItemActivated = (e: ChangeEvent): void => {
|
||||
if (!this.canPostBeForwarded) {
|
||||
// adding this early return since only hiding the Item from the menu is not enough,
|
||||
@@ -361,6 +383,12 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
this.handleDeleteMenuItemActivated(event);
|
||||
break;
|
||||
|
||||
// move thread
|
||||
case Keyboard.isKeyPressed(event, Constants.KeyCodes.W):
|
||||
this.handleMoveThreadMenuItemActivated(event);
|
||||
this.props.handleDropdownOpened(false);
|
||||
break;
|
||||
|
||||
// pin / unpin
|
||||
case Keyboard.isKeyPressed(event, Constants.KeyCodes.P):
|
||||
forceCloseMenu();
|
||||
@@ -595,6 +623,19 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
onClick={this.handlePinMenuItemActivated}
|
||||
/>
|
||||
}
|
||||
{Boolean(!isSystemMessage && this.props.canMove) &&
|
||||
<Menu.Item
|
||||
id={`move_thread_${this.props.post.id}`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id={'post_info.move_thread'}
|
||||
defaultMessage={'Move Thread'}
|
||||
/>}
|
||||
leadingElement={<MessageArrowRightOutlineIcon size={18}/>}
|
||||
trailingElements={<ShortcutKey shortcutKey='W'/>}
|
||||
onClick={this.handleMoveThreadMenuItemActivated}
|
||||
/>
|
||||
}
|
||||
{!isSystemMessage && (this.state.canEdit || this.state.canDelete) && <Menu.Separator/>}
|
||||
{!isSystemMessage &&
|
||||
<Menu.Item
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('components/dot_menu/DotMenu returning empty ("")', () => {
|
||||
threadId: 'post_id_1',
|
||||
userId: 'user_id_1',
|
||||
isMilitaryTime: false,
|
||||
canMove: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('components/dot_menu/DotMenu on mobile view', () => {
|
||||
threadId: 'post_id_1',
|
||||
userId: 'user_id_1',
|
||||
isMilitaryTime: false,
|
||||
canMove: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
markPostAsUnread,
|
||||
} from 'actions/post_actions';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {makeCanWrangler} from 'selectors/posts';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
import {isArchivedChannel} from 'utils/channel_utils';
|
||||
@@ -58,6 +59,7 @@ type Props = {
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getThreadOrSynthetic = makeGetThreadOrSynthetic();
|
||||
const canWrangler = makeCanWrangler();
|
||||
|
||||
return function mapStateToProps(state: GlobalState, ownProps: Props) {
|
||||
const {post} = ownProps;
|
||||
@@ -123,6 +125,7 @@ function makeMapStateToProps() {
|
||||
isMobileView: getIsMobileView(state),
|
||||
timezone: getCurrentTimezone(state),
|
||||
isMilitaryTime,
|
||||
canMove: canWrangler(state, channel.type, threadReplyCount),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,15 +222,14 @@ const DropdownIndicator = (props: IndicatorProps<ChannelOption>) => {
|
||||
);
|
||||
};
|
||||
|
||||
const validChannelTypes = ['O', 'P', 'D', 'G'];
|
||||
|
||||
type Props<O extends OptionTypeBase> = {
|
||||
onSelect: (channel: ValueType<O>) => void;
|
||||
currentBodyHeight: number;
|
||||
value?: O;
|
||||
validChannelTypes?: string[];
|
||||
}
|
||||
|
||||
function ForwardPostChannelSelect({onSelect, value, currentBodyHeight}: Props<ChannelOption>) {
|
||||
function ForwardPostChannelSelect({onSelect, value, currentBodyHeight, validChannelTypes = ['O', 'P', 'D', 'G']}: Props<ChannelOption>) {
|
||||
const {formatMessage} = useIntl();
|
||||
const {current: provider} = useRef<SwitchChannelProvider>(new SwitchChannelProvider());
|
||||
|
||||
|
||||
@@ -141,5 +141,14 @@
|
||||
opacity: 0.64 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.post-error {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.85em;
|
||||
font-weight: normal;
|
||||
opacity: 0.55;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ const ForwardPostModal = ({onExited, post, actions}: Props) => {
|
||||
|
||||
const postPreviewFooterMessage = formatMessage({
|
||||
id: 'forward_post_modal.preview.footer_message',
|
||||
defaultMessage: 'Originally posted in ~{channelName}',
|
||||
defaultMessage: 'Originally posted in ~{channel}',
|
||||
},
|
||||
{
|
||||
channel: channel.display_name,
|
||||
|
||||
29
webapp/channels/src/components/move_thread_modal/index.ts
Обычный файл
29
webapp/channels/src/components/move_thread_modal/index.ts
Обычный файл
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {connect} from 'react-redux';
|
||||
import type {ActionCreatorsMapObject, Dispatch} from 'redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {moveThread} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import {joinChannelById, switchToChannel} from 'actions/views/channel';
|
||||
|
||||
import type {ActionProps} from './move_thread_modal';
|
||||
import MoveThreadModal from './move_thread_modal';
|
||||
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<any>, ActionProps>({
|
||||
joinChannelById,
|
||||
switchToChannel,
|
||||
moveThread,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
export default connector(MoveThreadModal);
|
||||
@@ -0,0 +1,158 @@
|
||||
.move-thread {
|
||||
.modal-body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&.modal-dialog {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
overflow: visible;
|
||||
flex-direction: column;
|
||||
color: var(--center-channel-color);
|
||||
gap: 24px;
|
||||
|
||||
.custom-textarea,
|
||||
#move_thread_textbox {
|
||||
min-height: 40px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
#move_thread_textbox {
|
||||
overflow: auto;
|
||||
border: none;
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.16);
|
||||
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 2px var(--button-bg);
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.16);
|
||||
}
|
||||
|
||||
&:focus:focus-visible {
|
||||
box-shadow: inset 0 0 0 2px var(--button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__select {
|
||||
position: relative;
|
||||
|
||||
.option,
|
||||
.singleValue {
|
||||
top: 100%;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
color: var(--center-channel-color);
|
||||
gap: 12px;
|
||||
grid-auto-columns: minmax(20%, max-content);
|
||||
grid-template-columns: 24px 1fr;
|
||||
grid-template-rows: 24px;
|
||||
|
||||
svg,
|
||||
.status-wrapper,
|
||||
.status--group {
|
||||
place-self: center;
|
||||
}
|
||||
|
||||
.status--group {
|
||||
display: flex;
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.12);
|
||||
}
|
||||
|
||||
.status-wrapper {
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
&--text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&--description {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
}
|
||||
|
||||
.emoticon {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__team-name {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
grid-column: 3 / 4;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__post-preview {
|
||||
&--title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&--override {
|
||||
padding: 2px 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:not(.post--editing):not(.post--editing):hover {
|
||||
background-color: transparent;
|
||||
|
||||
.attachment__content.attachment__content--permalink {
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.console__body .modal .GenericModal.move-thread,
|
||||
.app__body .modal .GenericModal.move-thread {
|
||||
margin-top: 64px;
|
||||
|
||||
.modal-body {
|
||||
max-height: calc(100vh - 128px - 61px - 88px);
|
||||
|
||||
.form-control {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
||||
&#move_thread_textbox_placeholder {
|
||||
opacity: 0.64 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.post-error {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.85em;
|
||||
font-weight: normal;
|
||||
opacity: 0.55;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
import type {ValueType} from 'react-select';
|
||||
|
||||
import type {ClientError} from '@mattermost/client';
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {Post, PostPreviewMetadata} from '@mattermost/types/posts';
|
||||
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import type {ChannelOption} from 'components/forward_post_modal/forward_post_channel_select';
|
||||
import ChannelSelector from 'components/forward_post_modal/forward_post_channel_select';
|
||||
import NotificationBox from 'components/notification_box';
|
||||
import PostMessagePreview from 'components/post_view/post_message_preview';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import './move_thread_modal.scss';
|
||||
|
||||
export type ActionProps = {
|
||||
|
||||
// join the selected channel when necessary
|
||||
joinChannelById: (channelId: string) => Promise<ActionResult>;
|
||||
|
||||
// switch to the selected channel
|
||||
switchToChannel: (channel: Channel) => Promise<ActionResult>;
|
||||
|
||||
// action called to move the post from the original channel to a new channel
|
||||
moveThread: (postId: string, channelId: string) => Promise<ActionResult>;
|
||||
}
|
||||
|
||||
export type OwnProps = {
|
||||
|
||||
// The function called immediately after the modal is hidden
|
||||
onExited?: () => void;
|
||||
|
||||
// the post that is going to be moved
|
||||
post: Post;
|
||||
};
|
||||
|
||||
export type Props = OwnProps & {actions: ActionProps};
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
const getChannel = makeGetChannel();
|
||||
|
||||
// since the original post has a click handler specified we should prevent any action here
|
||||
const preventActionOnPreview = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const MoveThreadModal = ({onExited, post, actions}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const originalChannel = useSelector((state: GlobalState) => getChannel(state, {id: post.channel_id}));
|
||||
const currentTeam = useSelector(getCurrentTeam);
|
||||
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const [bodyHeight, setBodyHeight] = useState<number>(0);
|
||||
const [hasError, setHasError] = useState<boolean>(false);
|
||||
const [postError, setPostError] = useState<React.ReactNode>(null);
|
||||
const [selectedChannel, setSelectedChannel] = useState<ChannelOption>();
|
||||
const [isButtonClicked, setIsButtonClicked] = useState<boolean>(false);
|
||||
|
||||
const bodyRef = useRef<HTMLDivElement>();
|
||||
|
||||
const measuredRef = useCallback((node) => {
|
||||
if (node !== null) {
|
||||
bodyRef.current = node;
|
||||
setBodyHeight(node.getBoundingClientRect().height);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onHide = useCallback(() => {
|
||||
onExited?.();
|
||||
}, [onExited]);
|
||||
|
||||
const handleChannelSelect = useCallback((channel: ValueType<ChannelOption>) => {
|
||||
if (Array.isArray(channel)) {
|
||||
setSelectedChannel(channel[0]);
|
||||
return;
|
||||
}
|
||||
setSelectedChannel(channel as ChannelOption);
|
||||
}, []);
|
||||
|
||||
const messagePreviewTitle = formatMessage({
|
||||
id: 'move_thread_modal.preview.title',
|
||||
defaultMessage: 'Message preview',
|
||||
});
|
||||
|
||||
const previewMetaData: PostPreviewMetadata = useMemo(() => ({
|
||||
post,
|
||||
post_id: post.id,
|
||||
team_name: currentTeam.name,
|
||||
channel_display_name: originalChannel.display_name,
|
||||
channel_type: originalChannel.type,
|
||||
channel_id: originalChannel.id,
|
||||
}), [post, currentTeam.name, originalChannel.display_name, originalChannel.type, originalChannel.id]);
|
||||
|
||||
const notificationText = formatMessage({
|
||||
id: 'move_thread_modal.notification.dm_or_gm',
|
||||
defaultMessage: 'Moving this thread changes who has access',
|
||||
});
|
||||
|
||||
const notification = (
|
||||
<NotificationBox
|
||||
variant={'info'}
|
||||
text={notificationText}
|
||||
id={'move_thread'}
|
||||
/>
|
||||
);
|
||||
|
||||
const handlePostError = useCallback((error: ClientError) => {
|
||||
setIsButtonClicked(false);
|
||||
setPostError(error.message);
|
||||
setHasError(true);
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setHasError(false);
|
||||
timeoutRef.current = null;
|
||||
}, Constants.ANIMATION_TIMEOUT);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
setIsButtonClicked(true);
|
||||
if (!selectedChannel) {
|
||||
setIsButtonClicked(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = selectedChannel.details;
|
||||
|
||||
let result = await actions.moveThread(post.root_id || post.id, channel.id);
|
||||
|
||||
if (result.error) {
|
||||
handlePostError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
channel.type === Constants.MENTION_MORE_CHANNELS &&
|
||||
channel.type === Constants.OPEN_CHANNEL
|
||||
) {
|
||||
result = await actions.joinChannelById(channel.id);
|
||||
|
||||
if (result.error) {
|
||||
handlePostError(result.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
result = await actions.switchToChannel(channel);
|
||||
|
||||
if (result.error) {
|
||||
handlePostError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
onHide();
|
||||
}, [selectedChannel, post, actions, handlePostError, onHide]);
|
||||
|
||||
const postPreviewFooterMessage = formatMessage({
|
||||
id: 'move_thread_modal.preview.footer_message',
|
||||
defaultMessage: 'Originally posted in ~{channelName}',
|
||||
},
|
||||
{
|
||||
channelName: originalChannel.display_name,
|
||||
});
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
className='a11y__modal forward-post move-thread'
|
||||
id='move-thread-modal'
|
||||
show={true}
|
||||
autoCloseOnConfirmButton={false}
|
||||
compassDesign={true}
|
||||
modalHeaderText={formatMessage({
|
||||
id: 'move_thread_modal.title',
|
||||
defaultMessage: 'Move thread',
|
||||
})}
|
||||
confirmButtonText={formatMessage({
|
||||
id: 'move_thread_modal.button.forward',
|
||||
defaultMessage: 'Move',
|
||||
})}
|
||||
cancelButtonText={formatMessage({
|
||||
id: 'move_thread_modal.button.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
cancelButtonClassName={'MoveThreadModal__cancel-button'}
|
||||
isConfirmDisabled={isButtonClicked}
|
||||
handleConfirm={handleSubmit}
|
||||
handleEnterKeyPress={handleSubmit}
|
||||
handleCancel={onHide}
|
||||
onExited={onHide}
|
||||
>
|
||||
<div
|
||||
className={'move-thread__body'}
|
||||
ref={measuredRef}
|
||||
>
|
||||
{notification}
|
||||
<ChannelSelector
|
||||
onSelect={handleChannelSelect}
|
||||
value={selectedChannel}
|
||||
currentBodyHeight={bodyHeight}
|
||||
validChannelTypes={['O', 'P']}
|
||||
/>
|
||||
<div className={'move-thread__post-preview'}>
|
||||
<span className={'move-thread__post-preview--title'}>
|
||||
{messagePreviewTitle}
|
||||
</span>
|
||||
<div
|
||||
className='post move-thread__post-preview--override'
|
||||
onClick={preventActionOnPreview}
|
||||
>
|
||||
<PostMessagePreview
|
||||
metadata={previewMetaData}
|
||||
handleFileDropdownOpened={noop}
|
||||
preventClickAction={true}
|
||||
previewFooterMessage={postPreviewFooterMessage}
|
||||
/>
|
||||
</div>
|
||||
{postError && (
|
||||
<label
|
||||
className={classNames('post-error', {
|
||||
'animation--highlight': hasError,
|
||||
})}
|
||||
>
|
||||
{postError}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoveThreadModal;
|
||||
@@ -12,7 +12,7 @@ import Markdown from 'components/markdown';
|
||||
|
||||
import type {TextFormattingOptions} from 'utils/text_formatting';
|
||||
|
||||
import {renderReminderSystemBotMessage, renderSystemMessage} from './system_message_helpers';
|
||||
import {renderReminderSystemBotMessage, renderSystemMessage, renderWranglerSystemMessage} from './system_message_helpers';
|
||||
|
||||
import {type PropsFromRedux} from './index';
|
||||
|
||||
@@ -82,6 +82,11 @@ export default class PostMarkdown extends React.PureComponent<Props> {
|
||||
return <div>{renderedSystemBotMessage}</div>;
|
||||
}
|
||||
|
||||
if (this.props.post && this.props.post.type === Posts.POST_TYPES.WRANGLER) {
|
||||
const renderedWranglerMessage = renderWranglerSystemMessage(this.props.post);
|
||||
return <div>{renderedWranglerMessage}</div>;
|
||||
}
|
||||
|
||||
// Proxy images if we have an image proxy and the server hasn't already rewritten the this.props.post's image URLs.
|
||||
const proxyImages = !this.props.post || !this.props.post.message_source || this.props.post.message === this.props.post.message_source;
|
||||
const channelNamesMap = this.props.post && this.props.post.props && this.props.post.props.channel_mentions;
|
||||
|
||||
@@ -17,6 +17,7 @@ import CombinedSystemMessage from 'components/post_view/combined_system_message'
|
||||
import GMConversionMessage from 'components/post_view/gm_conversion_message/gm_conversion_message';
|
||||
import PostAddChannelMember from 'components/post_view/post_add_channel_member';
|
||||
|
||||
import {t} from 'utils/i18n';
|
||||
import type {TextFormattingOptions} from 'utils/text_formatting';
|
||||
import {getSiteURL} from 'utils/url';
|
||||
|
||||
@@ -489,3 +490,29 @@ export function renderReminderSystemBotMessage(post: Post, currentTeam: Team): R
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
t('app.post.move_thread_command.direct_or_group.multiple_messages');
|
||||
t('app.post.move_thread_command.direct_or_group.one_message');
|
||||
t('app.post.move_thread_command.channel.multiple_messages');
|
||||
t('app.post.move_thread_command.channel.one_message');
|
||||
t('app.post.move_thread.from_another_channel');
|
||||
export function renderWranglerSystemMessage(post: Post): ReactNode {
|
||||
let values = {} as any;
|
||||
const id = post.props.TranslationID;
|
||||
if (post.props && post.props.MovedThreadPermalink) {
|
||||
values = {
|
||||
Link: post.props.MovedThreadPermalink,
|
||||
};
|
||||
if (post.props.NumMessages > 1) {
|
||||
values.NumMessages = post.props.NumMessages;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<FormattedMessage
|
||||
id={id}
|
||||
defaultMessage={post.message}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ export type Props = {
|
||||
label: ReactNode;
|
||||
labelClassName?: string;
|
||||
placeholder?: string;
|
||||
value: string | number | string[];
|
||||
helpText?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
value: string | number;
|
||||
inputClassName?: string;
|
||||
maxLength?: number;
|
||||
resizable?: boolean;
|
||||
@@ -26,6 +26,7 @@ export type Props = {
|
||||
// This is a custom prop that is not part of the HTML input element type
|
||||
type?: InputTypes;
|
||||
autoFocus?: boolean;
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
function TextSetting(props: Props) {
|
||||
@@ -34,6 +35,12 @@ function TextSetting(props: Props) {
|
||||
function handleChange(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
||||
if (props.type === 'number') {
|
||||
props.onChange(props.id, parseInt(event.target.value, 10));
|
||||
} else if (props.type === 'text' && props.multiple) {
|
||||
if (event.target.value === '') {
|
||||
props.onChange(props.id, []);
|
||||
} else {
|
||||
props.onChange(props.id, event.target.value.split(','));
|
||||
}
|
||||
} else {
|
||||
props.onChange(props.id, event.target.value);
|
||||
}
|
||||
|
||||
@@ -887,6 +887,8 @@
|
||||
"admin.environment.webServer": "Web Server",
|
||||
"admin.experimental.allowCustomThemes.desc": "Enables the **Display > Theme > Custom Theme** section in Settings.",
|
||||
"admin.experimental.allowCustomThemes.title": "Allow Custom Themes:",
|
||||
"admin.experimental.allowedEmailDomain.desc": "(Optional) When set, users must have an email ending in this domain to move threads. Multiple domains can be specified by separating them with commas.",
|
||||
"admin.experimental.allowedEmailDomain.title": "Allowed Email Domain",
|
||||
"admin.experimental.clientSideCertCheck.desc": "When **primary**, after the client side certificate is verified, user’s email is retrieved from the certificate and is used to log in without a password. When **secondary**, after the client side certificate is verified, user’s email is retrieved from the certificate and matched against the one supplied by the user. If they match, the user logs in with regular email/password credentials.",
|
||||
"admin.experimental.clientSideCertCheck.title": "Client-Side Certification Login Method:",
|
||||
"admin.experimental.clientSideCertEnable.desc": "Enables client-side certification for your Mattermost server. See <link>documentation</link> to learn more.",
|
||||
@@ -954,6 +956,18 @@
|
||||
"admin.experimental.linkMetadataTimeoutMilliseconds.desc": "The number of milliseconds to wait for metadata from a third-party link. Used with Post Metadata.",
|
||||
"admin.experimental.linkMetadataTimeoutMilliseconds.example": "E.g.: \"5000\"",
|
||||
"admin.experimental.linkMetadataTimeoutMilliseconds.title": "Link Metadata Timeout:",
|
||||
"admin.experimental.moveThreadFromDirectMessageChannelEnable.desc": "Control whether move thread is permitted to move message threads from direct message channels or not.",
|
||||
"admin.experimental.moveThreadFromDirectMessageChannelEnable.title": "Enable Moving Threads From Direct Message Channels",
|
||||
"admin.experimental.moveThreadFromGroupMessageChannelEnable.desc": "Control whether move thread is permitted to move message threads from group message channels or not.",
|
||||
"admin.experimental.moveThreadFromGroupMessageChannelEnable.title": "Enable Moving Threads From Group Message Channels",
|
||||
"admin.experimental.moveThreadFromPrivateChannelEnable.desc": "Control whether move thread is permitted to move message threads from private channels or not.",
|
||||
"admin.experimental.moveThreadFromPrivateChannelEnable.title": "Enable Moving Threads From Private Channels",
|
||||
"admin.experimental.moveThreadMaxCount.desc": "The maximum number of messages in a thread that the plugin is allowed to move. Leave empty for unlimited messages.",
|
||||
"admin.experimental.moveThreadMaxCount.title": "Max Thread Count Move Size",
|
||||
"admin.experimental.moveThreadToAnotherTeamEnable.desc": "Control whether move thread is permitted to move message threads from one team to another or not.",
|
||||
"admin.experimental.moveThreadToAnotherTeamEnable.title": "Enable Moving Threads To Different Teams",
|
||||
"admin.experimental.PermittedMoveThreadRoles.desc": "Choose who is allowed to move threads to other channels based on roles. (Other permissions below still apply).",
|
||||
"admin.experimental.PermittedMoveThreadRoles.title": "Permitted Roles",
|
||||
"admin.experimental.samlSettingsLoginButtonBorderColor.desc": "Specify the color of the SAML login button border for white labeling purposes. Use a hex code with a #-sign before the code. This setting only applies to the mobile apps.",
|
||||
"admin.experimental.samlSettingsLoginButtonBorderColor.title": "SAML login Button Border Color:",
|
||||
"admin.experimental.samlSettingsLoginButtonColor.desc": "Specify the color of the SAML login button for white labeling purposes. Use a hex code with a #-sign before the code. This setting only applies to the mobile apps.",
|
||||
@@ -2335,6 +2349,7 @@
|
||||
"admin.sidebar.logs": "Server Logs",
|
||||
"admin.sidebar.metrics": "Performance Monitoring",
|
||||
"admin.sidebar.mfa": "MFA",
|
||||
"admin.sidebar.move_thread": "Move thread (Beta)",
|
||||
"admin.sidebar.notices": "Notices",
|
||||
"admin.sidebar.notifications": "Notifications",
|
||||
"admin.sidebar.oauth": "OAuth 2.0",
|
||||
@@ -2370,6 +2385,7 @@
|
||||
"admin.site.emoji": "Emoji",
|
||||
"admin.site.fileSharingDownloads": "File Sharing and Downloads",
|
||||
"admin.site.localization": "Localization",
|
||||
"admin.site.move_thread": "Move thread",
|
||||
"admin.site.notices": "Notices",
|
||||
"admin.site.posts": "Posts",
|
||||
"admin.site.public_links": "Public Links",
|
||||
@@ -2742,6 +2758,11 @@
|
||||
"app.channel.post_update_channel_purpose_message.removed": "{username} removed the channel purpose (was: {old})",
|
||||
"app.channel.post_update_channel_purpose_message.updated_from": "{username} updated the channel purpose from: {old} to: {new}",
|
||||
"app.channel.post_update_channel_purpose_message.updated_to": "{username} updated the channel purpose to: {new}",
|
||||
"app.post.move_thread_command.channel.multiple_messages": "A thread with {{.NumMessages}} messages has been moved: {{.Link}}\n",
|
||||
"app.post.move_thread_command.channel.one_message": "A message has been moved: {{.Link}}\n",
|
||||
"app.post.move_thread_command.direct_or_group.multiple_messages": "A thread with {{.NumMessages}} messages has been moved to a Direct/Group Message\n",
|
||||
"app.post.move_thread_command.direct_or_group.one_message": "A message has been moved to a Direct/Group Message\n",
|
||||
"app.post.move_thread.from_another_channel": "This thread was moved from another channel",
|
||||
"apps.error": "Error: {error}",
|
||||
"apps.error.command.field_missing": "Required fields missing: `{fieldName}`.",
|
||||
"apps.error.command.same_channel": "Channel repeated for field `{fieldName}`: `{option}`.",
|
||||
@@ -4131,6 +4152,12 @@
|
||||
"more_direct_channels.new_convo_note.full": "You've reached the maximum number of people for this conversation. Consider creating a private channel instead.",
|
||||
"more_direct_channels.title": "Direct Messages",
|
||||
"more.details": "More details",
|
||||
"move_thread_modal.button.cancel": "Cancel",
|
||||
"move_thread_modal.button.forward": "Move",
|
||||
"move_thread_modal.notification.dm_or_gm": "Moving this thread changes who has access",
|
||||
"move_thread_modal.preview.footer_message": "Originally posted in ~{channelName}",
|
||||
"move_thread_modal.preview.title": "Message preview",
|
||||
"move_thread_modal.title": "Move thread",
|
||||
"msg_typing.areTyping": "{users} and {last} are typing...",
|
||||
"msg_typing.isTyping": "{user} is typing...",
|
||||
"multiselect.add": "Add",
|
||||
@@ -4428,6 +4455,7 @@
|
||||
"post_info.message.show_more": "Show more",
|
||||
"post_info.message.visible": "(Only visible to you)",
|
||||
"post_info.message.visible.compact": " (Only visible to you)",
|
||||
"post_info.move_thread": "Move Thread",
|
||||
"post_info.permalink": "Copy Link",
|
||||
"post_info.pin": "Pin to Channel",
|
||||
"post_info.post_reminder.menu": "Remind",
|
||||
|
||||
@@ -58,4 +58,7 @@ export default keyMirror({
|
||||
CREATE_ACK_POST_SUCCESS: null,
|
||||
|
||||
DELETE_ACK_POST_SUCCESS: null,
|
||||
|
||||
MOVE_POST_SUCCESS: null,
|
||||
MOVE_POST_FAILURE: null,
|
||||
});
|
||||
|
||||
@@ -1260,6 +1260,23 @@ export function selectPost(postId: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export function moveThread(postId: string, channelId: string) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
try {
|
||||
await Client4.moveThread(postId, channelId);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch({type: PostTypes.MOVE_POST_FAILURE, error});
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
dispatch({type: PostTypes.MOVE_POST_SUCCESS});
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function selectFocusedPostId(postId: string) {
|
||||
return {
|
||||
type: PostTypes.RECEIVED_FOCUSED_POST,
|
||||
|
||||
@@ -33,6 +33,7 @@ export const PostTypes = {
|
||||
ADD_BOT_TEAMS_CHANNELS: 'add_bot_teams_channels' as PostType,
|
||||
SYSTEM_WARN_METRIC_STATUS: 'warn_metric_status' as PostType,
|
||||
REMINDER: 'reminder' as PostType,
|
||||
WRANGLER: 'system_wrangler' as PostType,
|
||||
GM_CONVERTED_TO_CHANNEL: 'system_gm_to_channel' as PostType,
|
||||
};
|
||||
|
||||
|
||||
@@ -276,6 +276,10 @@ export function cloudReverseTrial(state: GlobalState): boolean {
|
||||
return getFeatureFlagValue(state, 'CloudReverseTrial') === 'true';
|
||||
}
|
||||
|
||||
export function moveThreadsEnabled(state: GlobalState): boolean {
|
||||
return getFeatureFlagValue(state, 'MoveThreadsEnabled') === 'true';
|
||||
}
|
||||
|
||||
export function streamlinedMarketplaceEnabled(state: GlobalState): boolean {
|
||||
return getFeatureFlagValue(state, 'StreamlinedMarketplace') === 'true';
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {ClientConfig} from '@mattermost/types/config';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/common';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {moveThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {arePreviewsCollapsed} from 'selectors/preferences';
|
||||
@@ -52,3 +58,69 @@ export function isInlineImageVisible(state: GlobalState, postId: string, imageKe
|
||||
|
||||
return getGlobalItem(state, StoragePrefixes.INLINE_IMAGE_VISIBLE + currentUserId + '_' + postId + '_' + imageKey, !imageCollapsed);
|
||||
}
|
||||
|
||||
export function makeCanWrangler() {
|
||||
return createSelector(
|
||||
'makeCanWrangler',
|
||||
getConfig,
|
||||
getCurrentUser,
|
||||
moveThreadsEnabled,
|
||||
(_state: GlobalState, channelType: Channel['type']) => channelType,
|
||||
(_state: GlobalState, _channelType: Channel['type'], replyCount: number) => replyCount,
|
||||
(config: Partial<ClientConfig>, user: UserProfile, enabled: boolean, channelType: Channel['type'], replyCount: number) => {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
const {
|
||||
WranglerPermittedWranglerRoles,
|
||||
WranglerAllowedEmailDomain,
|
||||
WranglerMoveThreadMaxCount,
|
||||
WranglerMoveThreadFromPrivateChannelEnable,
|
||||
WranglerMoveThreadFromDirectMessageChannelEnable,
|
||||
WranglerMoveThreadFromGroupMessageChannelEnable,
|
||||
} = config;
|
||||
|
||||
let permittedUsers: string[] = [];
|
||||
if (WranglerPermittedWranglerRoles && WranglerPermittedWranglerRoles !== '') {
|
||||
permittedUsers = WranglerPermittedWranglerRoles?.split(',');
|
||||
}
|
||||
|
||||
let allowedEmailDomains: string[] = [];
|
||||
if (WranglerAllowedEmailDomain && WranglerAllowedEmailDomain !== '') {
|
||||
allowedEmailDomains = WranglerAllowedEmailDomain?.split(',') || [];
|
||||
}
|
||||
|
||||
if (permittedUsers.length > 0 && !user.roles.includes('system_admin')) {
|
||||
const roles = user.roles.split(' ');
|
||||
const hasRole = roles.some((role) => permittedUsers.includes(role));
|
||||
if (!hasRole) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (allowedEmailDomains?.length > 0) {
|
||||
if (!user.email || !allowedEmailDomains.includes(user.email.split('@')[1])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Number(WranglerMoveThreadMaxCount) < replyCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (channelType === 'P' && WranglerMoveThreadFromPrivateChannelEnable === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (channelType === 'D' && WranglerMoveThreadFromDirectMessageChannelEnable === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (channelType === 'G' && WranglerMoveThreadFromGroupMessageChannelEnable === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export const SettingsTypes = {
|
||||
TYPE_JOBSTABLE: 'jobstable',
|
||||
TYPE_FILE_UPLOAD: 'fileupload',
|
||||
TYPE_CUSTOM: 'custom',
|
||||
TYPE_ROLES: 'roles',
|
||||
};
|
||||
|
||||
export const InviteTypes = {
|
||||
@@ -446,6 +447,7 @@ export const ModalIdentifiers = {
|
||||
SELF_HOSTED_EXPANSION: 'self_hosted_expansion',
|
||||
START_TRIAL_FORM_MODAL: 'start_trial_form_modal',
|
||||
START_TRIAL_FORM_MODAL_RESULT: 'start_trial_form_modal_result',
|
||||
MOVE_THREAD_MODAL: 'move_thread_modal',
|
||||
CONVERT_GM_TO_CHANNEL: 'convert_gm_to_channel',
|
||||
IP_FILTERING_ADD_EDIT_MODAL: 'ip_filtering_add_edit_modal',
|
||||
IP_FILTERING_DELETE_CONFIRMATION_MODAL: 'ip_filtering_delete_confirmation_modal',
|
||||
@@ -769,6 +771,7 @@ export const TELEMETRY_LABELS = {
|
||||
REPLY: 'reply',
|
||||
UNREAD: 'unread',
|
||||
FORWARD: 'forward',
|
||||
MOVE_THREAD: 'move_thread',
|
||||
};
|
||||
|
||||
export const PostTypes = {
|
||||
@@ -795,6 +798,7 @@ export const PostTypes = {
|
||||
REMOVE_LINK_PREVIEW: 'remove_link_preview',
|
||||
ME: 'me',
|
||||
REMINDER: 'reminder',
|
||||
WRANGLER: 'system_wrangler',
|
||||
CUSTOM_CALLS: 'custom_calls',
|
||||
CUSTOM_CALLS_RECORDING: 'custom_calls_recording',
|
||||
};
|
||||
|
||||
@@ -1059,6 +1059,14 @@ export default class Client4 {
|
||||
);
|
||||
}
|
||||
|
||||
moveThread = (postId: string, channelId: string) => {
|
||||
const url = this.getPostRoute(postId) + '/move';
|
||||
return this.doFetch<StatusOK>(
|
||||
url,
|
||||
{method: 'post', body: JSON.stringify({channel_id: channelId})},
|
||||
);
|
||||
}
|
||||
|
||||
switchEmailToOAuth = (service: string, email: string, password: string, mfaCode = '') => {
|
||||
this.trackEvent('api', 'api_users_email_to_oauth');
|
||||
|
||||
|
||||
@@ -199,6 +199,12 @@ export type ClientConfig = {
|
||||
PersistentNotificationIntervalMinutes: string;
|
||||
AllowPersistentNotificationsForGuests: string;
|
||||
DelayChannelAutocomplete: 'true' | 'false';
|
||||
WranglerPermittedWranglerRoles: string;
|
||||
WranglerAllowedEmailDomain: string;
|
||||
WranglerMoveThreadMaxCount: string;
|
||||
WranglerMoveThreadFromPrivateChannelEnable: string;
|
||||
WranglerMoveThreadFromDirectMessageChannelEnable: string;
|
||||
WranglerMoveThreadFromGroupMessageChannelEnable: string;
|
||||
ServiceEnvironment: string;
|
||||
UniqueEmojiReactionLimitPerPost: string;
|
||||
};
|
||||
@@ -485,6 +491,16 @@ export type PasswordSettings = {
|
||||
EnableForgotLink: boolean;
|
||||
};
|
||||
|
||||
export type WranglerSettings = {
|
||||
PermittedWranglerRoles: string[];
|
||||
AllowedEmailDomain: string[];
|
||||
MoveThreadMaxCount: number;
|
||||
MoveThreadToAnotherTeamEnable: boolean;
|
||||
MoveThreadFromPrivateChannelEnable: boolean;
|
||||
MoveThreadFromDirectMessageChannelEnable: boolean;
|
||||
MoveThreadFromGroupMessageChannelEnable: boolean;
|
||||
};
|
||||
|
||||
export type FileSettings = {
|
||||
EnableFileAttachments: boolean;
|
||||
EnableMobileUpload: boolean;
|
||||
@@ -947,6 +963,7 @@ export type AdminConfig = {
|
||||
FeatureFlags: FeatureFlags;
|
||||
ImportSettings: ImportSettings;
|
||||
ExportSettings: ExportSettings;
|
||||
WranglerSettings: WranglerSettings;
|
||||
};
|
||||
|
||||
export type ReplicaLagSetting = {
|
||||
|
||||
@@ -31,6 +31,7 @@ export type PostType = 'system_add_remove' |
|
||||
'system_fake_parent_deleted' |
|
||||
'system_generic' |
|
||||
'reminder' |
|
||||
'system_wrangler' |
|
||||
'';
|
||||
|
||||
export type PostEmbedType = 'image' | 'link' | 'message_attachment' | 'opengraph' | 'permalink';
|
||||
|
||||
Ссылка в новой задаче
Block a user