Skip to content

Commit

Permalink
Implement FileDropdown Actions Plugin Method (mattermost#8139)
Browse files Browse the repository at this point in the history
* Add file drpdown action plugins framework

* Fix search-item-snippet CSS

* Show post menu when file dropdown is open

* Code refactoring

* Fix lint

* Update tests props

* make handleFileDropdownOpened prop optional

* Update handleFileDropdownOpened implementation

* Fix lint

* Update CSS styles

* Use memoization for plugin file dropdown items

* Fix lint

* Fixed type check errors

* update snapshots

* Update snapshots

* use createSelector for files dropdown plugin selector

* Update components/file_attachment/file_attachment.tsx

Co-authored-by: Daniel Espino García <[email protected]>

Co-authored-by: Manoj <[email protected]>
Co-authored-by: Daniel Espino García <[email protected]>
  • Loading branch information
3 people committed Jun 21, 2021
1 parent 46b3032 commit 507425b
Show file tree
Hide file tree
Showing 21 changed files with 353 additions and 12 deletions.
3 changes: 3 additions & 0 deletions components/file_attachment/file_attachment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ describe('FileAttachment', () => {
index: 3,
canDownloadFiles: true,
enableSVGs: false,
enablePublicLink: false,
pluginMenuItems: [],
handleFileDropdownOpened: jest.fn(() => null),
};

test('should match snapshot, regular file', () => {
Expand Down
151 changes: 149 additions & 2 deletions components/file_attachment/file_attachment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@
// See LICENSE.txt for license information.

import React from 'react';
import {Tooltip} from 'react-bootstrap';
import classNames from 'classnames';

import * as GlobalActions from 'actions/global_actions';

import {getFileThumbnailUrl, getFileUrl} from 'mattermost-redux/utils/file_utils';
import {FileInfo} from 'mattermost-redux/types/files';
import {FileDropdownPluginComponent} from 'types/store/plugins';

import OverlayTrigger from 'components/overlay_trigger';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import Menu from 'components/widgets/menu/menu';

import {FileTypes} from 'utils/constants';
import {Constants, FileTypes} from 'utils/constants';
import {trimFilename} from 'utils/file_utils';
import {
fileSizeToString,
Expand Down Expand Up @@ -41,23 +50,32 @@ interface Props {
compactDisplay?: boolean;
canDownloadFiles?: boolean;
enableSVGs: boolean;
enablePublicLink: boolean;
pluginMenuItems: FileDropdownPluginComponent[];
handleFileDropdownOpened?: (open: boolean) => void;
}

interface State {
loaded: boolean;
keepOpen: boolean;
openUp: boolean;
fileInfo: FileInfo;
}

export default class FileAttachment extends React.PureComponent<Props, State> {
mounted = false;
private readonly buttonRef: React.RefObject<HTMLButtonElement>;

constructor(props: Props) {
super(props);

this.state = {
loaded: getFileType(props.fileInfo.extension) !== FileTypes.IMAGE,
fileInfo: props.fileInfo,
keepOpen: false,
openUp: false,
};
this.buttonRef = React.createRef<HTMLButtonElement>();
}

componentDidMount() {
Expand Down Expand Up @@ -121,6 +139,124 @@ export default class FileAttachment extends React.PureComponent<Props, State> {
}
}

refCallback = (menuRef: Menu) => {
if (menuRef) {
const anchorRect = this.buttonRef.current?.getBoundingClientRect();
let y;
if (typeof anchorRect?.y === 'undefined') {
y = typeof anchorRect?.top === 'undefined' ? 0 : anchorRect?.top;
} else {
y = anchorRect?.y;
}
const windowHeight = window.innerHeight;

const totalSpace = windowHeight - 80;
const spaceOnTop = y - Constants.CHANNEL_HEADER_HEIGHT;
const spaceOnBottom = (totalSpace - (spaceOnTop + Constants.POST_AREA_HEIGHT));

this.setState({
openUp: (spaceOnTop > spaceOnBottom),
});
}
}

private handleDropdownOpened = (open: boolean) => {
this.props.handleFileDropdownOpened?.(open);
this.setState({keepOpen: open});
}

handleGetPublicLink = () => {
GlobalActions.showGetPublicLinkModal(this.props.fileInfo.id);
}

private renderFileMenuItems = () => {
const {enablePublicLink, fileInfo, pluginMenuItems} = this.props;

let divider;
const defaultItems = [];
if (enablePublicLink) {
defaultItems.push(
<Menu.ItemAction
data-title='Public Image'
onClick={this.handleGetPublicLink}
ariaLabel={localizeMessage('view_image_popover.publicLink', 'Get a public link')}
text={localizeMessage('view_image_popover.publicLink', 'Get a public link')}
/>,
);
}

const pluginItems = pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => {
return (
<Menu.ItemAction
id={item.id + '_pluginmenuitem'}
key={item.id + '_pluginmenuitem'}
onClick={() => item?.action(fileInfo)}
text={item.text}
/>
);
});

const isMenuVisible = defaultItems?.length || pluginItems?.length;
if (!isMenuVisible) {
return null;
}

const isDividerVisible = defaultItems?.length && pluginItems?.length;
if (isDividerVisible) {
divider = (
<li
id={`divider_file_${fileInfo.id}_plugins`}
className='MenuItem__divider'
role='menuitem'
/>
);
}

const tooltip = (
<Tooltip id='file-name__tooltip'>
{localizeMessage('file_search_result_item.more_actions', 'More Actions')}
</Tooltip>
);

return (
<MenuWrapper
onToggle={this.handleDropdownOpened}
stopPropagationOnToggle={true}
>
<OverlayTrigger
className='hidden-xs'
delayShow={1000}
placement='top'
overlay={tooltip}
>
<button
ref={this.buttonRef}
id={`file_action_button_${this.props.fileInfo.id}`}
aria-label={localizeMessage('file_search_result_item.more_actions', 'More Actions').toLowerCase()}
className={classNames(
'file-dropdown-icon', 'dots-icon',
{'a11y--active': this.state.keepOpen},
)}
aria-expanded={this.state.keepOpen}
>
<i className='icon icon-dots-vertical'/>
</button>
</OverlayTrigger>
<Menu
id={`file_dropdown_${this.props.fileInfo.id}`}
ariaLabel={'file menu'}
openLeft={true}
openUp={this.state.openUp}
ref={this.refCallback}
>
{defaultItems}
{divider}
{pluginItems}
</Menu>
</MenuWrapper>
);
}

render() {
const {
compactDisplay,
Expand All @@ -130,6 +266,7 @@ export default class FileAttachment extends React.PureComponent<Props, State> {
const trimmedFilename = trimFilename(fileInfo.name);
let fileThumbnail;
let fileDetail;
let fileActions;
const ariaLabelImage = `${localizeMessage('file_attachment.thumbnail', 'file thumbnail')} ${fileInfo.name}`.toLowerCase();

if (!compactDisplay) {
Expand Down Expand Up @@ -162,6 +299,8 @@ export default class FileAttachment extends React.PureComponent<Props, State> {
</div>
</div>
);

fileActions = this.renderFileMenuItems();
}

let filenameOverlay;
Expand All @@ -180,10 +319,18 @@ export default class FileAttachment extends React.PureComponent<Props, State> {
}

return (
<div className='post-image__column'>
<div
className={
classNames([
'post-image__column',
{'keep-open': this.state.keepOpen},
])
}
>
{fileThumbnail}
<div className='post-image__details'>
{fileDetail}
{fileActions}
{filenameOverlay}
</div>
</div>
Expand Down
4 changes: 3 additions & 1 deletion components/file_attachment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {connect} from 'react-redux';

import {getConfig} from 'mattermost-redux/selectors/entities/general';

import {getFilesDropdownPluginMenuItems} from 'selectors/plugins';
import {GlobalState} from 'types/store';

import {canDownloadFiles} from 'utils/file_utils';

import FileAttachment from './file_attachment';
Expand All @@ -17,6 +17,8 @@ function mapStateToProps(state: GlobalState) {
return {
canDownloadFiles: canDownloadFiles(config),
enableSVGs: config.EnableSVGs === 'true',
enablePublicLink: config.EnablePublicLink === 'true',
pluginMenuItems: getFilesDropdownPluginMenuItems(state),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('FileAttachmentList', () => {
enableSVGs: false,
isEmbedVisible: false,
locale: 'en',
handleFileDropdownOpened: jest.fn(),
actions: {getMissingFilesForPost: jest.fn()},
};

Expand Down
2 changes: 2 additions & 0 deletions components/file_attachment_list/file_attachment_list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type Props = {
isEmbedVisible?: boolean;
locale: string;

handleFileDropdownOpened: (open: boolean) => void;
}

type State = {
Expand Down Expand Up @@ -95,6 +96,7 @@ export default class FileAttachmentList extends React.PureComponent<Props, State
index={i}
handleImageClick={this.handleImageClick}
compactDisplay={compactDisplay}
handleFileDropdownOpened={this.props.handleFileDropdownOpened}
/>,
);
}
Expand Down
3 changes: 1 addition & 2 deletions components/file_search_results/file_search_result_item.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,13 @@
.action-icon {
@include single-transition(opacity, .15s, ease);
@include border-radius(4px);
display: flex;
align-items: center;
height: 32px;
width: 32px;
min-width: 32px;
background-color: var(--center-channel-bg);
color: rgba(var(--center-channel-color-rgb), 0.72);
fill: rgba(var(--center-channel-color-rgb), 0.72);
display: flex;
align-items: center;
justify-content: center;
margin-left: 8px;
Expand Down
33 changes: 33 additions & 0 deletions components/file_search_results/file_search_result_item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {FormattedMessage} from 'react-intl';

import {FileInfo} from 'mattermost-redux/types/files';

import {FileDropdownPluginComponent} from 'types/store/plugins';

import {fileSizeToString, copyToClipboard, localizeMessage} from 'utils/utils';
import {browserHistory} from 'utils/browser_history';
import {getSiteURL} from 'utils/url';
Expand All @@ -27,6 +29,7 @@ type Props = {
channelDisplayName: string;
channelType: string;
teamName: string;
pluginMenuItems?: FileDropdownPluginComponent[];
};

type State = {
Expand Down Expand Up @@ -62,6 +65,35 @@ export default class FileSearchResultItem extends React.PureComponent<Props, Sta
this.setState({keepOpen: open});
}

private renderPluginItems = () => {
const {fileInfo} = this.props;
const pluginItems = this.props.pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => {
return (
<Menu.ItemAction
id={item.id + '_pluginmenuitem'}
key={item.id + '_pluginmenuitem'}
onClick={() => item.action?.(fileInfo)}
text={item.text}
/>
);
});

if (!pluginItems?.length) {
return null;
}

return (
<>
<li
id={`divider_file_${this.props.fileInfo.id}_plugins`}
className='MenuItem__divider'
role='menuitem'
/>
{pluginItems}
</>
);
}

public render(): React.ReactNode {
const {fileInfo, channelDisplayName, channelType} = this.props;
let channelName: React.ReactNode = channelDisplayName;
Expand Down Expand Up @@ -136,6 +168,7 @@ export default class FileSearchResultItem extends React.PureComponent<Props, Sta
ariaLabel={localizeMessage('file_search_result_item.copy_link', 'Copy link')}
text={localizeMessage('file_search_result_item.copy_link', 'Copy link')}
/>
{this.renderPluginItems()}
</Menu>
</MenuWrapper>
</OverlayTrigger>
Expand Down
4 changes: 4 additions & 0 deletions components/post_view/post/__snapshots__/post.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ exports[`Post should handle highlighting properly when jumping to pinned or flag
<Connect(PostBody)
compactDisplay={false}
handleCommentClick={[Function]}
handleFileDropdownOpened={[Function]}
isCommentMention={false}
isFirstReply={true}
post={
Expand Down Expand Up @@ -143,6 +144,7 @@ exports[`Post should handle highlighting properly when jumping to pinned or flag
<Connect(PostBody)
compactDisplay={false}
handleCommentClick={[Function]}
handleFileDropdownOpened={[Function]}
isCommentMention={false}
isFirstReply={true}
post={
Expand Down Expand Up @@ -222,6 +224,7 @@ exports[`Post should not remove post--highlight class after timeout so the class
<Connect(PostBody)
compactDisplay={false}
handleCommentClick={[Function]}
handleFileDropdownOpened={[Function]}
isCommentMention={false}
isFirstReply={true}
post={
Expand Down Expand Up @@ -301,6 +304,7 @@ exports[`Post should not remove post--highlight class after timeout so the class
<Connect(PostBody)
compactDisplay={false}
handleCommentClick={[Function]}
handleFileDropdownOpened={[Function]}
isCommentMention={false}
isFirstReply={true}
post={
Expand Down
Loading

0 comments on commit 507425b

Please sign in to comment.