MM-61525, MM-61868: Autohide channel bookmarks when empty (#29454)

Этот коммит содержится в:
Caleb Roseland
2024-12-19 09:23:29 -06:00
коммит произвёл GitHub
родитель cf392f7cca
Коммит 09add85a51
10 изменённых файлов: 474 добавлений и 222 удалений

Просмотреть файл

@@ -11,7 +11,7 @@ import type {IDMappedObjects} from '@mattermost/types/utilities';
import BookmarkItem from './bookmark_item';
import BookmarksMenu from './channel_bookmarks_menu';
import {useChannelBookmarkPermission, useChannelBookmarks, MAX_BOOKMARKS_PER_CHANNEL, useCanUploadFiles} from './utils';
import {useChannelBookmarks, MAX_BOOKMARKS_PER_CHANNEL, useCanUploadFiles, useChannelBookmarkPermission} from './utils';
import './channel_bookmarks.scss';
@@ -23,12 +23,12 @@ function ChannelBookmarks({
channelId,
}: Props) {
const {order, bookmarks, reorder} = useChannelBookmarks(channelId);
const canReorder = useChannelBookmarkPermission(channelId, 'order');
const canUploadFiles = useCanUploadFiles();
const canAdd = useChannelBookmarkPermission(channelId, 'add');
const hasBookmarks = Boolean(order?.length);
const limitReached = order.length >= MAX_BOOKMARKS_PER_CHANNEL;
if (!hasBookmarks && !canAdd) {
if (!hasBookmarks) {
return null;
}
@@ -53,7 +53,7 @@ function ChannelBookmarks({
data-testid='channel-bookmarks-container'
{...drop.droppableProps}
>
{order.map(makeItemRenderer(bookmarks, snap.isDraggingOver))}
{order.map(makeItemRenderer(bookmarks, snap.isDraggingOver, !canReorder))}
{drop.placeholder}
<BookmarksMenu
channelId={channelId}
@@ -69,12 +69,13 @@ function ChannelBookmarks({
);
}
const makeItemRenderer = (bookmarks: IDMappedObjects<ChannelBookmark>, disableInteractions: boolean) => (id: string, index: number) => {
const makeItemRenderer = (bookmarks: IDMappedObjects<ChannelBookmark>, disableInteractions: boolean, disableDrag: boolean) => (id: string, index: number) => {
return (
<Draggable
key={id}
draggableId={id}
index={index}
isDragDisabled={disableDrag}
>
{(drag, snap) => {
return (

Просмотреть файл

@@ -2,8 +2,7 @@
// See LICENSE.txt for license information.
import classNames from 'classnames';
import type {ChangeEvent} from 'react';
import React, {useCallback, useRef} from 'react';
import React, {memo, useCallback} from 'react';
import {useIntl} from 'react-intl';
import {useDispatch} from 'react-redux';
import styled, {css} from 'styled-components';
@@ -15,73 +14,32 @@ import {
} from '@mattermost/compass-icons/components';
import type {ChannelBookmarkCreate} from '@mattermost/types/channel_bookmarks';
import type {ActionResult} from 'mattermost-redux/types/actions';
import {createBookmark} from 'actions/channel_bookmarks';
import {openModal} from 'actions/views/modals';
import * as Menu from 'components/menu';
import {ModalIdentifiers} from 'utils/constants';
import {clearFileInput} from 'utils/utils';
import ChannelBookmarkCreateModal from './channel_bookmarks_create_modal';
import {MAX_BOOKMARKS_PER_CHANNEL} from './utils';
import {MAX_BOOKMARKS_PER_CHANNEL, useChannelBookmarkPermission} from './utils';
type BookmarksMenuProps = {
channelId: string;
hasBookmarks: boolean;
limitReached: boolean;
canUploadFiles: boolean;};
export default ({
function BookmarksMenu({
channelId,
hasBookmarks,
limitReached,
canUploadFiles,
}: BookmarksMenuProps) => {
}: BookmarksMenuProps) {
const {formatMessage} = useIntl();
const dispatch = useDispatch();
const showLabel = !hasBookmarks;
const handleCreate = useCallback((file?: File) => {
dispatch(openModal({
modalId: ModalIdentifiers.CHANNEL_BOOKMARK_CREATE,
dialogType: ChannelBookmarkCreateModal,
dialogProps: {
channelId,
bookmarkType: file ? 'file' : 'link',
file,
onConfirm: async (data: ChannelBookmarkCreate) => dispatch(createBookmark(channelId, data)) as ActionResult<boolean>,
},
}));
}, [channelId, dispatch]);
const handleFileChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files?.length) {
const [file] = e.target.files;
handleCreate(file);
clearFileInput(e.target);
}
}, [handleCreate]);
const fileInputRef = useRef<HTMLInputElement>(null);
const fileInput = (
<input
type='file'
id='bookmark-create-file-input'
className='bookmark-create-file-input'
ref={fileInputRef}
onChange={handleFileChanged}
/>
);
const handleCreateLink = useCallback(() => {
handleCreate();
}, [handleCreate]);
const handleCreateFile = useCallback(() => {
fileInputRef.current?.click();
}, [fileInputRef.current]);
const {handleCreateLink, handleCreateFile} = useBookmarkAddActions(channelId);
const canAdd = useChannelBookmarkPermission(channelId, 'add');
const addBookmarkLabel = formatMessage({id: 'channel_bookmarks.addBookmark', defaultMessage: 'Add a bookmark'});
@@ -93,9 +51,14 @@ export default ({
} else if (hasBookmarks) {
addBookmarkTooltipText = addBookmarkLabel;
}
const addLinkLabel = formatMessage({id: 'channel_bookmarks.addLink', defaultMessage: 'Add a link'});
const attachFileLabel = formatMessage({id: 'channel_bookmarks.attachFile', defaultMessage: 'Attach a file'});
if (!canAdd) {
return null;
}
return (
<MenuButtonContainer
withLabel={showLabel}
@@ -129,7 +92,6 @@ export default ({
onClick={handleCreateLink}
leadingElement={<LinkVariantIcon size={18}/>}
labels={<span>{addLinkLabel}</span>}
aria-label={addLinkLabel}
/>
{canUploadFiles && (
<Menu.Item
@@ -138,14 +100,14 @@ export default ({
onClick={handleCreateFile}
leadingElement={<PaperclipIcon size={18}/>}
labels={<span>{attachFileLabel}</span>}
aria-label={attachFileLabel}
/>
)}
</Menu.Container>
{fileInput}
</MenuButtonContainer>
);
};
}
export default memo(BookmarksMenu);
const MenuButtonContainer = styled.div<{withLabel: boolean}>`
position: sticky;
@@ -153,3 +115,46 @@ const MenuButtonContainer = styled.div<{withLabel: boolean}>`
${({withLabel}) => !withLabel && css`padding: 0 1rem;`}
background: linear-gradient(to right, rgba(var(--center-channel-bg-rgb), .16), rgba(var(--center-channel-bg-rgb), 1) 25%);
`;
export const useBookmarkAddActions = (channelId: string) => {
const dispatch = useDispatch();
const handleCreate = useCallback((file?: File) => {
dispatch(openModal({
modalId: ModalIdentifiers.CHANNEL_BOOKMARK_CREATE,
dialogType: ChannelBookmarkCreateModal,
dialogProps: {
channelId,
bookmarkType: file ? 'file' : 'link',
file,
onConfirm: async (data: ChannelBookmarkCreate) => dispatch(createBookmark(channelId, data)),
},
}));
}, [channelId, dispatch]);
const handleCreateLink = useCallback(() => {
handleCreate();
}, [handleCreate]);
const handleCreateFile = useCallback(() => {
const input: HTMLInputElement = document.createElement('input');
input.type = 'file';
input.id = 'bookmark-create-file-input';
input.hidden = true;
input.addEventListener('change', () => {
const file = input.files?.[0];
if (file) {
handleCreate(file);
}
input.remove();
});
input.addEventListener('cancel', input.remove);
document.getElementById('root-portal')?.appendChild(input);
input.click();
}, [handleCreate]);
return {handleCreateLink, handleCreateFile};
};

Просмотреть файл

@@ -0,0 +1,75 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Purpose of this file to exists is only required until channel header dropdown is migrated to new menus
import type {ComponentProps} from 'react';
import React, {memo} from 'react';
import {useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import {
LinkVariantIcon,
PaperclipIcon,
} from '@mattermost/compass-icons/components';
import type {Channel} from '@mattermost/types/channels';
import {getChannelBookmarks} from 'mattermost-redux/selectors/entities/channel_bookmarks';
import {useBookmarkAddActions} from 'components/channel_bookmarks/channel_bookmarks_menu';
import {MAX_BOOKMARKS_PER_CHANNEL, useCanUploadFiles, useChannelBookmarkPermission} from 'components/channel_bookmarks/utils';
import Menu from 'components/widgets/menu/menu';
import type {GlobalState} from 'types/store';
type Props = {
channel: Channel;
inHeaderDropdown?: boolean;
};
const ChannelBookmarksSubmenu = (props: Props) => {
const {formatMessage} = useIntl();
const {handleCreateLink, handleCreateFile} = useBookmarkAddActions(props.channel.id);
const canAdd = useChannelBookmarkPermission(props.channel.id, 'add');
const canUploadFiles = useCanUploadFiles();
const limitReached = useSelector((state: GlobalState) => {
const bookmarks = getChannelBookmarks(state, props.channel.id);
return bookmarks && Object.keys(bookmarks).length >= MAX_BOOKMARKS_PER_CHANNEL;
});
if (!canAdd || limitReached) {
return null;
}
const items: ComponentProps<typeof Menu.ItemSubMenu>['subMenu'] = [
{
id: 'channelBookmarksAddLink',
icon: <LinkVariantIcon size={16}/>,
direction: 'right',
text: formatMessage({id: 'channel_bookmarks.addLink', defaultMessage: 'Add a link'}),
action: handleCreateLink,
},
];
if (canUploadFiles) {
items.push({
id: 'channelBookmarksAttachFile',
icon: <PaperclipIcon size={16}/>,
direction: 'right',
text: formatMessage({id: 'channel_bookmarks.attachFile', defaultMessage: 'Attach a file'}),
action: handleCreateFile,
});
}
return (
<Menu.ItemSubMenu
id={`channel-menu-${props.channel.id}-bookmarks`}
subMenu={items}
text={formatMessage({id: 'sidebar_left.sidebar_channel_menu.bookmarks', defaultMessage: 'Bookmarks Bar'})}
direction={'right'}
styleSelectableItem={true}
/>
);
};
export default memo(ChannelBookmarksSubmenu);

Просмотреть файл

@@ -22,6 +22,7 @@ describe('components/ChannelHeaderDropdown', () => {
penultimateViewedChannelName: 'test-channel',
pluginMenuItems: [],
isLicensedForLDAPGroups: false,
isChannelBookmarksEnabled: false,
};
test('should match snapshot with no plugin items', () => {
const wrapper = shallow(<ChannelHeaderDropdown {...defaultProps}/>);

Просмотреть файл

@@ -10,6 +10,7 @@ import {Permissions} from 'mattermost-redux/constants';
import {isGuest} from 'mattermost-redux/utils/user_utils';
import AddGroupsToChannelModal from 'components/add_groups_to_channel_modal';
import ChannelBookmarksSubmenu from 'components/channel_bookmarks_sub_menu';
import ChannelGroupsManageModal from 'components/channel_groups_manage_modal';
import ChannelInviteModal from 'components/channel_invite_modal';
import ChannelMoveToSubMenuOld from 'components/channel_move_to_sub_menu_old';
@@ -52,6 +53,7 @@ export type Props = {
penultimateViewedChannelName: string;
pluginMenuItems: PluginComponent[];
isLicensedForLDAPGroups: boolean;
isChannelBookmarksEnabled: boolean;
}
export default class ChannelHeaderDropdown extends React.PureComponent<Props> {
@@ -67,6 +69,7 @@ export default class ChannelHeaderDropdown extends React.PureComponent<Props> {
isMobile,
penultimateViewedChannelName,
isLicensedForLDAPGroups,
isChannelBookmarksEnabled,
} = this.props;
if (!channel) {
@@ -238,6 +241,7 @@ export default class ChannelHeaderDropdown extends React.PureComponent<Props> {
</Menu.Group>
<Menu.Group divider={divider}>
{isChannelBookmarksEnabled && <ChannelBookmarksSubmenu channel={channel}/>}
<ChannelPermissionGate
channelId={channel.id}
teamId={channel.team_id}

Просмотреть файл

@@ -23,6 +23,8 @@ import {
import {getPenultimateViewedChannelName} from 'selectors/local_storage';
import {getChannelHeaderMenuPluginComponents} from 'selectors/plugins';
import {getIsChannelBookmarksEnabled} from 'components/channel_bookmarks/utils';
import {Constants} from 'utils/constants';
import * as Utils from 'utils/utils';
@@ -69,6 +71,7 @@ const mapStateToProps = (state: GlobalState) => ({
penultimateViewedChannelName: getPenultimateViewedChannelName(state) || getRedirectChannelNameForTeam(state, getCurrentTeamId(state)),
pluginMenuItems: getChannelHeaderMenuPluginComponents(state),
isLicensedForLDAPGroups: state.entities.general.license.LDAPGroups === 'true',
isChannelBookmarksEnabled: getIsChannelBookmarksEnabled(state),
});
const mobileMapStateToProps = (state: GlobalState) => {

Просмотреть файл

@@ -5138,6 +5138,7 @@
"sidebar_left.sidebar_category.newLabel": "new",
"sidebar_left.sidebar_channel_menu_convert_to_channel": "Convert to Private Channel",
"sidebar_left.sidebar_channel_menu.addMembers": "Add Members",
"sidebar_left.sidebar_channel_menu.bookmarks": "Bookmarks Bar",
"sidebar_left.sidebar_channel_menu.channels": "Channels",
"sidebar_left.sidebar_channel_menu.copyLink": "Copy Link",
"sidebar_left.sidebar_channel_menu.dropdownAriaLabel": "Edit channel menu",