560 lines
16 KiB
JavaScript
Executable file
560 lines
16 KiB
JavaScript
Executable file
/* @flow */
|
|
|
|
import React from 'react';
|
|
import Flux from '../../lib/flux';
|
|
import lodash from 'lodash';
|
|
import i18n from '../../i18n';
|
|
import classNames from 'classnames';
|
|
|
|
import Tooltip from '../common/Tooltip';
|
|
import GuildRole from '../common/GuildRole';
|
|
import PermissionUtils from '../../utils/PermissionUtils';
|
|
import HelpdeskUtils from '../../utils/HelpdeskUtils';
|
|
|
|
import GuildSettingsStore from '../../stores/GuildSettingsStore';
|
|
import UserStore from '../../stores/UserStore';
|
|
import UserSettingsStore from '../../stores/UserSettingsStore';
|
|
import GuildSettingsRolesStore from '../../stores/GuildSettingsRolesStore';
|
|
|
|
import GuildSettingsActionCreators from '../../actions/GuildSettingsActionCreators';
|
|
import GuildActionCreators from '../../actions/GuildActionCreators';
|
|
import AlertActionCreators from '../../actions/AlertActionCreators';
|
|
import {
|
|
init,
|
|
updateRoleSort,
|
|
updateRolePermissions,
|
|
toggleRoleSettings,
|
|
updateRoleName,
|
|
updateRoleColor,
|
|
saveRoleSettings,
|
|
} from '../../actions/GuildSettingsRolesActionCreators';
|
|
|
|
import Layout from '../../uikit/Layout';
|
|
import {makeDragItem, makeDragList} from '../../../discord_common/js/containers/makeDraggable';
|
|
import TabBar, {TabBarHeader} from '../../uikit/TabBar';
|
|
import ColorPicker from '../common/ColorPicker';
|
|
import FormSection from '../../uikit/form/FormSection';
|
|
import FormItem from '../../uikit/form/FormItem';
|
|
import FormDivider from '../../uikit/form/FormDivider';
|
|
import FormText, {Types} from '../../uikit/form/FormText';
|
|
import FormTitle, {Tags} from '../../uikit/form/FormTitle';
|
|
import TextInput from '../../uikit/TextInput';
|
|
import SwitchItem from '../../uikit/SwitchItem';
|
|
import SettingsNotice from '../common/SettingsNotice';
|
|
import PermissionsForm from '../common/PermissionsForm';
|
|
import Flex from '../../uikit/Flex';
|
|
import Button from '../../uikit/Button';
|
|
import Card, {Types as CardTypes} from '../../uikit/Card';
|
|
import NotAllowedIcon from '../../uikit/icons/NotAllowedIcon';
|
|
|
|
import type {GuildRole as GuildRoleType} from '../../flow/Server';
|
|
import {ROLE_COLORS, DEFAULT_ROLE_COLOR, FormStates, ThemeTypes} from '../../Constants';
|
|
import './GuildSettingsRoles.styl';
|
|
|
|
const DEFAULT_STYLE = {
|
|
paddingTop: 60,
|
|
paddingBottom: 80,
|
|
paddingLeft: 20,
|
|
paddingRight: 20,
|
|
color: '#fff',
|
|
};
|
|
|
|
function wrapTooltip(children, text) {
|
|
return (
|
|
<Tooltip type={Tooltip.ERROR} delay={500} position={Tooltip.TOP} text={text}>
|
|
<div className="not-allowed">{children}</div>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
export const GuildSettingsRolesNotice = Flux.connectStores([GuildSettingsStore, GuildSettingsRolesStore], () => {
|
|
const {guild} = GuildSettingsStore.getProps();
|
|
const formState = GuildSettingsRolesStore.formState;
|
|
return {
|
|
guild,
|
|
submitting: formState === FormStates.SUBMITTING,
|
|
onReset() {
|
|
init();
|
|
},
|
|
onSave() {
|
|
const {id: guildId} = guild;
|
|
const roles = GuildSettingsRolesStore.editedRoleIds.map(id => GuildSettingsRolesStore.getRole(id));
|
|
const deltas = GuildSettingsRolesStore.getSortDeltas();
|
|
saveRoleSettings(guildId, roles, deltas);
|
|
},
|
|
};
|
|
})(SettingsNotice);
|
|
|
|
const GuildRoleDraggable = makeDragItem('ITEM')(GuildRole);
|
|
const TabBarDraggable = makeDragList('ITEM')(TabBar);
|
|
|
|
type SpecItem = {
|
|
title: string,
|
|
description?: string,
|
|
flag: number,
|
|
};
|
|
|
|
type Spec = {
|
|
title: string,
|
|
permissions: Array<SpecItem>,
|
|
};
|
|
|
|
class GuildRoleSettings extends React.PureComponent {
|
|
generalSpec: Spec;
|
|
textSpec: Spec;
|
|
voiceSpec: Spec;
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.generalSpec = PermissionUtils.generateGuildGeneralPermissionSpec();
|
|
this.textSpec = PermissionUtils.generateGuildTextPermissionSpec();
|
|
this.voiceSpec = PermissionUtils.generateGuildVoicePermissionSpec();
|
|
lodash.bindAll(this, [
|
|
'handleNameChange',
|
|
'handleColorChange',
|
|
'handleDeleteRole',
|
|
'handlePermissionChange',
|
|
'handlePermissionRender',
|
|
]);
|
|
}
|
|
|
|
handleNameChange(name) {
|
|
const {id} = this.props.role;
|
|
updateRoleName(id, name);
|
|
}
|
|
|
|
handleColorChange(color) {
|
|
const {role} = this.props;
|
|
const {id} = role;
|
|
|
|
// No changes, do nothing
|
|
if (role.color === color || (color === DEFAULT_ROLE_COLOR && role.color === 0)) {
|
|
return;
|
|
}
|
|
|
|
// The way the backend understands default is by setting 0
|
|
if (color === DEFAULT_ROLE_COLOR) {
|
|
color = 0;
|
|
}
|
|
|
|
updateRoleColor(id, color);
|
|
}
|
|
|
|
handleSettingChange(key, {target: {checked: value}}) {
|
|
let {hoist, mentionable, id} = this.props.role;
|
|
if (key === 'hoist') {
|
|
hoist = value;
|
|
} else {
|
|
mentionable = value;
|
|
}
|
|
toggleRoleSettings(id, hoist, mentionable);
|
|
}
|
|
|
|
handlePermissionChange(flag, allow) {
|
|
const {id} = this.props.role;
|
|
updateRolePermissions(id, flag, allow);
|
|
}
|
|
|
|
handleDeleteRole() {
|
|
const {guild: {id: guildId}, role: {id: roleId, name}} = this.props;
|
|
AlertActionCreators.show({
|
|
title: i18n.Messages.SETTINGS_ROLES_DELETE_TITLE,
|
|
body: i18n.Messages.SETTINGS_ROLES_DELETE_BODY.format({name}),
|
|
cancelText: i18n.Messages.CANCEL,
|
|
onConfirm: () => GuildActionCreators.deleteRole(guildId, roleId),
|
|
iconUrl: require('../../images/icon-alert-alert.svg'),
|
|
});
|
|
}
|
|
|
|
renderName() {
|
|
const {role, locked, everyone} = this.props;
|
|
return (
|
|
<FormItem title={i18n.Messages.FORM_LABEL_ROLE_NAME} className="margin-bottom-20">
|
|
<TextInput
|
|
disabled={locked || everyone}
|
|
style={{color: role.colorString}}
|
|
value={role.name}
|
|
onChange={this.handleNameChange}
|
|
/>
|
|
</FormItem>
|
|
);
|
|
}
|
|
|
|
renderColorPicker() {
|
|
const {role, locked, everyone} = this.props;
|
|
let colorPicker = (
|
|
<ColorPicker
|
|
defaultColor={DEFAULT_ROLE_COLOR}
|
|
colors={ROLE_COLORS}
|
|
value={role.color}
|
|
disabled={locked || everyone}
|
|
onChange={this.handleColorChange}
|
|
/>
|
|
);
|
|
if (locked || everyone) {
|
|
colorPicker = wrapTooltip(colorPicker, i18n.Messages.FORM_LABEL_DISABLED_FOR_EVERYONE);
|
|
}
|
|
return (
|
|
<FormItem title={i18n.Messages.FORM_LABEL_ROLE_COLOR} className="margin-bottom-40">
|
|
{colorPicker}
|
|
</FormItem>
|
|
);
|
|
}
|
|
|
|
renderDisabledIndicator(disabledMessage: string) {
|
|
return (
|
|
<Tooltip text={disabledMessage} position={Tooltip.TOP} type={Tooltip.ERROR}>
|
|
<span>
|
|
<NotAllowedIcon className="not-allowed-icon" width={18} height={18} />
|
|
</span>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
renderRoleSettings() {
|
|
const {role, locked} = this.props;
|
|
const disabledMessage = this.handlePermissionRender();
|
|
const disabled = !!(locked || disabledMessage);
|
|
const disabledIcon = typeof disabledMessage === 'string' ? this.renderDisabledIndicator(disabledMessage) : null;
|
|
const className = classNames({'role-permission-title-disabled': disabledIcon != null});
|
|
|
|
return (
|
|
<FormSection className="margin-bottom-40">
|
|
<FormTitle tag={Tags.H5} className="margin-bottom-20">
|
|
{i18n.Messages.FORM_LABEL_ROLE_SETTINGS}
|
|
</FormTitle>
|
|
<SwitchItem
|
|
className={classNames('permission-item', 'margin-bottom-20', {disabled})}
|
|
disabled={disabled}
|
|
onChange={this.handleSettingChange.bind(this, 'hoist')}
|
|
value={role.hoist}>
|
|
{disabledIcon}
|
|
<span className={className}>
|
|
{i18n.Messages.FORM_LABEL_HOIST_DESCRIPTION}
|
|
</span>
|
|
</SwitchItem>
|
|
<SwitchItem
|
|
disabled={disabled}
|
|
onChange={this.handleSettingChange.bind(this, 'mentionable')}
|
|
value={role.mentionable}>
|
|
{disabledIcon}
|
|
<span className={className}>
|
|
{i18n.Messages.FORM_LABEL_MENTIONABLE.format()}
|
|
</span>
|
|
</SwitchItem>
|
|
</FormSection>
|
|
);
|
|
}
|
|
|
|
renderDeleteButton() {
|
|
const {locked, role, everyone} = this.props;
|
|
|
|
if (locked || role.managed || everyone) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Button look={Button.Looks.OUTLINED} color={Button.Colors.RED} onClick={this.handleDeleteRole}>
|
|
{i18n.Messages.DELETE} {role.name}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
renderManagedNote() {
|
|
const {role} = this.props;
|
|
if (role.managed) {
|
|
return (
|
|
<Card type={CardTypes.WARNING} className="padded margin-bottom-20">
|
|
{i18n.Messages.MANAGED_ROLE_EXPLAINATION}
|
|
</Card>
|
|
);
|
|
}
|
|
}
|
|
|
|
handlePermissionRender(flag) {
|
|
const {locked, guild, currentUser, everyone, role} = this.props;
|
|
const highestRole = PermissionUtils.getHighestRole(guild, currentUser.id);
|
|
const isMineLocked = locked && role === highestRole;
|
|
|
|
if (isMineLocked) {
|
|
return i18n.Messages.HELP_ROLE_LOCKED_MINE;
|
|
} else if (locked) {
|
|
return i18n.Messages.HELP_ROLE_LOCKED;
|
|
} else if (flag == null && everyone) {
|
|
return i18n.Messages.FORM_LABEL_DISABLED_FOR_EVERYONE;
|
|
} else if (flag != null && !PermissionUtils.can(flag, currentUser, guild)) {
|
|
return i18n.Messages.HELP_MISSING_PERMISSION;
|
|
} else if (
|
|
flag != null &&
|
|
!PermissionUtils.can(flag, currentUser, guild, null, {
|
|
[role.id]: {...role, permissions: role.permissions ^ (role.permissions & flag)},
|
|
})
|
|
) {
|
|
// eslint-disable-line max-len
|
|
return i18n.Messages.HELP_SINGULAR_PERMISSION;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const {locked, role: {permissions}} = this.props;
|
|
return (
|
|
<FormSection>
|
|
{this.renderManagedNote()}
|
|
{this.renderName()}
|
|
{this.renderColorPicker()}
|
|
<FormDivider className="margin-bottom-40" />
|
|
{this.renderRoleSettings()}
|
|
<PermissionsForm
|
|
spec={this.generalSpec}
|
|
permissions={permissions}
|
|
locked={locked}
|
|
onChange={this.handlePermissionChange}
|
|
className="margin-bottom-40"
|
|
permissionRender={this.handlePermissionRender}
|
|
/>
|
|
<PermissionsForm
|
|
spec={this.textSpec}
|
|
permissions={permissions}
|
|
locked={locked}
|
|
onChange={this.handlePermissionChange}
|
|
className="margin-bottom-40"
|
|
permissionRender={this.handlePermissionRender}
|
|
/>
|
|
<PermissionsForm
|
|
spec={this.voiceSpec}
|
|
permissions={permissions}
|
|
locked={locked}
|
|
onChange={this.handlePermissionChange}
|
|
permissionRender={this.handlePermissionRender}
|
|
/>
|
|
{this.renderDeleteButton()}
|
|
</FormSection>
|
|
);
|
|
}
|
|
}
|
|
|
|
type State = {
|
|
dragging: ?string,
|
|
roles: ?Array<GuildRoleType>,
|
|
};
|
|
|
|
class GuildSettingsRoles extends React.PureComponent {
|
|
state: State;
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
lodash.bindAll(this, [
|
|
'handleSelectRole',
|
|
'handleDragStart',
|
|
'handleDrop',
|
|
'handleHover',
|
|
'handleCancel',
|
|
'handleDragLeave',
|
|
'handleCreateRole',
|
|
]);
|
|
|
|
// NOTE: Internal state is ONLY used during a drag operation
|
|
this.state = {
|
|
dragging: null,
|
|
roles: null,
|
|
};
|
|
}
|
|
|
|
getSelectedRole() {
|
|
return GuildSettingsRolesStore.getRole(this.props.selectedRoleId);
|
|
}
|
|
|
|
handleCreateRole() {
|
|
GuildActionCreators.createRole(this.props.guild.id);
|
|
}
|
|
|
|
handleSelectRole(roleId) {
|
|
GuildSettingsActionCreators.selectRole(roleId);
|
|
}
|
|
|
|
handleDragStart(dragging) {
|
|
const {roles} = this.props;
|
|
this.setState({
|
|
dragging,
|
|
roles: [...roles],
|
|
});
|
|
}
|
|
|
|
handleDragLeave() {
|
|
const {roles} = this.props;
|
|
this.setState({roles: [...roles]});
|
|
}
|
|
|
|
handleDrop() {
|
|
if (this.state.roles == null) {
|
|
return;
|
|
}
|
|
updateRoleSort(this.state.roles.map(({id}) => id));
|
|
this.setState({dragging: null, roles: null});
|
|
}
|
|
|
|
handleHover(selectedId: string, draggedId: string) {
|
|
if (this.state.roles == null) {
|
|
return;
|
|
}
|
|
|
|
const roles = [...this.state.roles];
|
|
const dragged = roles.find(({id}) => draggedId === id);
|
|
const selected = roles.find(({id}) => selectedId === id);
|
|
if (dragged == null || selected == null) {
|
|
return;
|
|
}
|
|
|
|
const draggedIndex = roles.indexOf(dragged);
|
|
let selectedIndex = roles.indexOf(selected);
|
|
const direction = selectedIndex - draggedIndex > 0 ? 1 : 0;
|
|
|
|
roles.splice(draggedIndex, 1);
|
|
selectedIndex = roles.indexOf(selected);
|
|
|
|
roles.splice(selectedIndex + direction, 0, dragged);
|
|
this.setState({roles});
|
|
}
|
|
|
|
handleCancel() {
|
|
this.setState({dragging: null, roles: null});
|
|
}
|
|
|
|
renderRoleList() {
|
|
const {currentUser, guild, selectedRoleId, theme} = this.props;
|
|
let {roles} = this.props;
|
|
const highestRole = PermissionUtils.getHighestRole(guild, currentUser.id);
|
|
// If we are dragging, then we should use the internal state
|
|
if (this.state.roles) {
|
|
roles = this.state.roles;
|
|
}
|
|
|
|
return roles.map(role => {
|
|
const locked = !PermissionUtils.isRoleHigher(guild, currentUser.id, highestRole, role);
|
|
const isMineLocked = locked && role === highestRole;
|
|
const {id, colorString, name} = role;
|
|
|
|
let lockTooltip;
|
|
if (isMineLocked) {
|
|
lockTooltip = i18n.Messages.HELP_ROLE_LOCKED_MINE;
|
|
} else if (locked) {
|
|
lockTooltip = i18n.Messages.HELP_ROLE_LOCKED;
|
|
}
|
|
|
|
// Locked and @everyone is not draggable
|
|
if (locked || isMineLocked || id === guild.id) {
|
|
return (
|
|
<GuildRole
|
|
key={`${selectedRoleId}-${id}`}
|
|
theme={theme}
|
|
id={id}
|
|
locked={locked}
|
|
lockTooltip={lockTooltip}
|
|
color={colorString}>
|
|
{name}
|
|
</GuildRole>
|
|
);
|
|
}
|
|
return (
|
|
<GuildRoleDraggable
|
|
key={`${selectedRoleId}-${id}`}
|
|
theme={theme}
|
|
dragId={id}
|
|
id={id}
|
|
locked={locked}
|
|
lockTooltip={lockTooltip}
|
|
color={colorString}>
|
|
{name}
|
|
</GuildRoleDraggable>
|
|
);
|
|
});
|
|
}
|
|
|
|
renderRoleInfo() {
|
|
return (
|
|
<div>
|
|
<div className="ui-tab-bar-separator margin-top-20" style={{marginBottom: 14}} />
|
|
<FormText type={Types.DESCRIPTION} className="margin-bottom-20">
|
|
{i18n.Messages.FORM_LABEL_ROLES_PRO_TIP_DESCRIPTION}
|
|
</FormText>
|
|
<FormText type={Types.DESCRIPTION}>
|
|
<a href={HelpdeskUtils.getArticleURL(206029707)} target="_blank">
|
|
{i18n.Messages.PERMISSION_HELPDESK}
|
|
</a>
|
|
</FormText>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
renderRoles() {
|
|
const {selectedRoleId, theme} = this.props;
|
|
const {dragging} = this.state;
|
|
const iconSrc = this.props.theme === ThemeTypes.DARK
|
|
? require('../../images/ic_roles_add_primary_400_16px.svg')
|
|
: require('../../images/ic_roles_add_primary_300_16px.svg');
|
|
|
|
return (
|
|
<Layout.Sidebar style={{...DEFAULT_STYLE, overflow: 'hidden'}} scrollable theme={theme}>
|
|
<TabBarDraggable
|
|
onItemSelect={this.handleSelectRole}
|
|
selectedItem={dragging || selectedRoleId}
|
|
onDragStart={this.handleDragStart}
|
|
onHover={this.handleHover}
|
|
onDrop={this.handleDrop}
|
|
onCancel={this.handleCancel}
|
|
onDragLeave={this.handleDragLeave}>
|
|
<TabBarHeader>
|
|
<Flex className="sidebar-header" onClick={this.handleCreateRole} justify={Flex.Justify.BETWEEN}>
|
|
{i18n.Messages.ROLES}
|
|
<img className="add-role-icon" src={iconSrc} />
|
|
</Flex>
|
|
</TabBarHeader>
|
|
{this.renderRoleList()}
|
|
{this.renderRoleInfo()}
|
|
</TabBarDraggable>
|
|
</Layout.Sidebar>
|
|
);
|
|
}
|
|
|
|
renderRoleSettings() {
|
|
const {guild, currentUser} = this.props;
|
|
const role = this.getSelectedRole();
|
|
const highestRole = PermissionUtils.getHighestRole(guild, currentUser.id);
|
|
const locked = !PermissionUtils.isRoleHigher(guild, currentUser.id, highestRole, role);
|
|
const everyone = role != null && role.id === guild.id;
|
|
return (
|
|
<Layout.Content style={{...DEFAULT_STYLE, paddingRight: 0}} grow={0}>
|
|
<GuildRoleSettings guild={guild} role={role} locked={locked} everyone={everyone} currentUser={currentUser} />
|
|
</Layout.Content>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<Layout className="guild-settings-roles">
|
|
{this.renderRoles()}
|
|
{this.renderRoleSettings()}
|
|
</Layout>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default Flux.connectStores([GuildSettingsStore, UserStore, UserSettingsStore, GuildSettingsRolesStore], () => {
|
|
const {guild, selectedRoleId} = GuildSettingsStore.getProps();
|
|
const currentUser = UserStore.getCurrentUser();
|
|
const roles = GuildSettingsRolesStore.roles;
|
|
return {
|
|
currentUser,
|
|
guild,
|
|
selectedRoleId,
|
|
roles,
|
|
theme: UserSettingsStore.theme,
|
|
};
|
|
})(GuildSettingsRoles);
|
|
|
|
|
|
|
|
// WEBPACK FOOTER //
|
|
// ./discord_app/components/guild_settings/GuildSettingsRoles.js
|