fix(app): overwrite context menu hover and delete danger item (#2106)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-30 00:12:41 +02:00
1 parent 8f20b29b16
commit b1f7c78c7e
5 files changed
+136 -29

No files matched your search

@@ -174,7 +174,8 @@
cursor: pointer;
background: transparent;
}
.overwriteItem:not(.overwriteItemSelected):hover {
.overwriteItem:not(.overwriteItemSelected):hover,
.overwriteItemContextMenuOpen:not(.overwriteItemSelected) {
background: var(--background-modifier-hover);
}
.overwriteItemSelected {
@@ -519,27 +519,34 @@ const ChannelPermissionsTab: React.FC<{channelId: string}> = observer(({channelI
});
setSelectedOverwriteId(id);
}, []);
const handleDeleteOverride = useCallback(() => {
if (!selectedOverwrite || !guild || selectedOverwrite.id === guild.id) return;
const futureOverwrites = overwritesWithUpdates.filter((o) => o.id !== selectedOverwrite.id);
const currentIndex = overwritesWithUpdates.findIndex((o) => o.id === selectedOverwrite.id);
const nextOverwrite = futureOverwrites[currentIndex] ?? futureOverwrites[0];
if (newOverwriteIds.has(selectedOverwrite.id)) {
setNewOverwriteIds((prev) => {
const newSet = new Set(prev);
newSet.delete(selectedOverwrite.id);
return newSet;
const removeOverwrite = useCallback(
(overwriteId: string) => {
if (!guild || overwriteId === guild.id) return;
const futureOverwrites = overwritesWithUpdates.filter((o) => o.id !== overwriteId);
const currentIndex = overwritesWithUpdates.findIndex((o) => o.id === overwriteId);
const nextOverwrite = futureOverwrites[currentIndex] ?? futureOverwrites[0];
if (newOverwriteIds.has(overwriteId)) {
setNewOverwriteIds((prev) => {
const newSet = new Set(prev);
newSet.delete(overwriteId);
return newSet;
});
} else {
setDeletedOverwriteIds((prev) => new Set(prev).add(overwriteId));
}
setOverwriteUpdates((prev) => {
const newMap = new Map(prev);
newMap.delete(overwriteId);
return newMap;
});
} else {
setDeletedOverwriteIds((prev) => new Set(prev).add(selectedOverwrite.id));
}
setOverwriteUpdates((prev) => {
const newMap = new Map(prev);
newMap.delete(selectedOverwrite.id);
return newMap;
});
setSelectedOverwriteId(nextOverwrite?.id ?? null);
}, [selectedOverwrite, guild, overwritesWithUpdates, newOverwriteIds]);
setSelectedOverwriteId((prev) => (prev === overwriteId ? (nextOverwrite?.id ?? null) : prev));
},
[guild, overwritesWithUpdates, newOverwriteIds],
);
const handleDeleteOverride = useCallback(() => {
if (!selectedOverwrite) return;
removeOverwrite(selectedOverwrite.id);
}, [selectedOverwrite, removeOverwrite]);
const getOverwriteName = useCallback(
(overwrite: PermissionOverwrite): string => {
if (!guild) return '';
@@ -585,6 +592,8 @@ const ChannelPermissionsTab: React.FC<{channelId: string}> = observer(({channelI
selectedOverwriteId={selectedOverwriteId}
canManageChannels={canManageChannels}
canManageRoles={canManageRoles}
canManageOverwrites={canManageChannels && canManageRoles}
onDeleteOverwrite={removeOverwrite}
isAddOverrideOpen={isAddOverrideOpen}
setIsAddOverrideOpen={setIsAddOverrideOpen}
existingOverwriteIds={existingOverwriteIds}
@@ -615,6 +624,7 @@ const ChannelPermissionsTab: React.FC<{channelId: string}> = observer(({channelI
overwritesWithUpdates,
canManageChannels,
canManageRoles,
removeOverwrite,
isAddOverrideOpen,
addOverrideContext,
addOverrideFloatingStyles,
@@ -17,6 +17,8 @@ interface ChannelPermissionsSidebarProps {
selectedOverwriteId: string | null;
canManageChannels: boolean;
canManageRoles: boolean;
canManageOverwrites: boolean;
onDeleteOverwrite: (overwriteId: string) => void;
isAddOverrideOpen: boolean;
setIsAddOverrideOpen: (open: boolean) => void;
existingOverwriteIds: Set<string>;
@@ -40,6 +42,8 @@ export const ChannelPermissionsSidebar: React.FC<ChannelPermissionsSidebarProps>
selectedOverwriteId,
canManageChannels,
canManageRoles,
canManageOverwrites,
onDeleteOverwrite,
isAddOverrideOpen,
setIsAddOverrideOpen,
existingOverwriteIds,
@@ -136,6 +140,8 @@ export const ChannelPermissionsSidebar: React.FC<ChannelPermissionsSidebarProps>
roleId={roleId}
isSelected={isSelected}
isEveryone={isEveryone}
canDelete={canManageOverwrites && !isEveryone}
onDelete={onDeleteOverwrite}
onClick={() => onSelectOverwrite(overwrite.id)}
guildId={guildId}
data-flx="channel.channel-tabs.channel-permissions-tab.sidebar-content.overwrite-item.set-selected-overwrite-id"
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {PermissionOverwrite} from '@app/features/channel/components/modals/channel_tabs/channel_permissions_tab/shared';
import {DeleteIcon} from '@app/features/ui/action_menu/ContextMenuIcons';
import {CopyRoleIdMenuItem, CopyUserIdMenuItem} from '@app/features/ui/action_menu/items/CopyMenuItems';
import {MenuGroup} from '@app/features/ui/action_menu/MenuGroup';
import {MenuItem} from '@app/features/ui/action_menu/MenuItem';
import type {User} from '@app/features/user/models/User';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
const DELETE_ROLE_DESCRIPTOR = msg({
message: 'Delete role',
comment:
'Danger action in the channel permissions overwrite context menu that removes a role permission override from the channel.',
});
const DELETE_USER_DESCRIPTOR = msg({
message: 'Delete user',
comment:
'Danger action in the channel permissions overwrite context menu that removes a member permission override from the channel.',
});
interface OverwriteContextMenuProps {
overwrite: PermissionOverwrite;
roleId: string | null;
user: User | null;
canDelete: boolean;
onDelete: () => void;
onClose: () => void;
}
export const OverwriteContextMenu: React.FC<OverwriteContextMenuProps> = observer(
({overwrite, roleId, user, canDelete, onDelete, onClose}) => {
const {i18n} = useLingui();
return (
<>
<MenuGroup data-flx="channel.channel-tabs.channel-permissions-tab.overwrite-context-menu.menu-group">
{overwrite.type === 0 && roleId ? (
<CopyRoleIdMenuItem
roleId={roleId}
onClose={onClose}
data-flx="channel.channel-tabs.channel-permissions-tab.overwrite-context-menu.copy-role-id-menu-item"
/>
) : user ? (
<CopyUserIdMenuItem
user={user}
onClose={onClose}
data-flx="channel.channel-tabs.channel-permissions-tab.overwrite-context-menu.copy-user-id-menu-item"
/>
) : null}
</MenuGroup>
{canDelete && (
<MenuGroup data-flx="channel.channel-tabs.channel-permissions-tab.overwrite-context-menu.menu-group--2">
<MenuItem
icon={
<DeleteIcon data-flx="channel.channel-tabs.channel-permissions-tab.overwrite-context-menu.delete-icon" />
}
danger
onClick={onDelete}
data-flx="channel.channel-tabs.channel-permissions-tab.overwrite-context-menu.menu-item.delete"
>
{overwrite.type === 0 ? i18n._(DELETE_ROLE_DESCRIPTOR) : i18n._(DELETE_USER_DESCRIPTOR)}
</MenuItem>
</MenuGroup>
)}
</>
);
},
);
@@ -1,16 +1,18 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {DEFAULT_ROLE_COLOR_HEX, getRoleColor} from '@app/features/app/components/dialogs/shared/PermissionComponents';
import {useContextMenuHoverState} from '@app/features/app/hooks/useContextMenuHoverState';
import styles from '@app/features/channel/components/modals/channel_tabs/ChannelPermissionsTab.module.css';
import {OverwriteContextMenu} from '@app/features/channel/components/modals/channel_tabs/channel_permissions_tab/OverwriteContextMenu';
import type {PermissionOverwrite} from '@app/features/channel/components/modals/channel_tabs/channel_permissions_tab/shared';
import {openRoleContextMenu} from '@app/features/ui/action_menu/RoleContextMenu';
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
import {Avatar} from '@app/features/ui/components/Avatar';
import type {User} from '@app/features/user/models/User';
import {UsersIcon} from '@phosphor-icons/react';
import {clsx} from 'clsx';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useCallback} from 'react';
import {useCallback, useRef} from 'react';
interface OverwriteItemProps {
overwrite: PermissionOverwrite;
@@ -20,26 +22,43 @@ interface OverwriteItemProps {
roleId?: string | null;
isSelected: boolean;
isEveryone: boolean;
canDelete: boolean;
onDelete: (overwriteId: string) => void;
onClick: () => void;
guildId: string;
}
export const OverwriteItem: React.FC<OverwriteItemProps> = observer(
({overwrite, name, color, user, roleId, isSelected, isEveryone, onClick, guildId}) => {
({overwrite, name, color, user, roleId, isSelected, isEveryone, canDelete, onDelete, onClick, guildId}) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const contextMenuOpen = useContextMenuHoverState(buttonRef);
const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
if (!roleId) return;
openRoleContextMenu(event, roleId);
ContextMenuCommands.openFromEvent(event, ({onClose}) => (
<OverwriteContextMenu
overwrite={overwrite}
roleId={roleId ?? null}
user={user ?? null}
canDelete={canDelete}
onDelete={() => onDelete(overwrite.id)}
onClose={onClose}
data-flx="channel.channel-tabs.channel-permissions-tab.overwrite-item.handle-context-menu.overwrite-context-menu"
/>
));
},
[roleId],
[overwrite, roleId, user, canDelete, onDelete],
);
return (
<button
ref={buttonRef}
type="button"
aria-pressed={isSelected}
className={clsx(styles.overwriteItem, {[styles.overwriteItemSelected]: isSelected})}
className={clsx(styles.overwriteItem, {
[styles.overwriteItemSelected]: isSelected,
[styles.overwriteItemContextMenuOpen]: contextMenuOpen && !isSelected,
})}
onClick={onClick}
onContextMenu={roleId ? handleContextMenu : undefined}
onContextMenu={handleContextMenu}
data-flx="channel.channel-tabs.channel-permissions-tab.overwrite-item.overwrite-item.click.button"
>
{overwrite.type === 0 && !isEveryone ? (