1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 12:49:43 +02:00

feat: Open card actions on right-click

This commit is contained in:
Maksim Eltyshev 2025-07-17 17:43:01 +02:00
parent e659ed4a2d
commit fdac299fc7
2 changed files with 33 additions and 7 deletions

View file

@ -5,13 +5,13 @@
import upperFirst from 'lodash/upperFirst'; import upperFirst from 'lodash/upperFirst';
import camelCase from 'lodash/camelCase'; import camelCase from 'lodash/camelCase';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Button, Icon } from 'semantic-ui-react'; import { Button, Icon } from 'semantic-ui-react';
import { push } from '../../../lib/redux-router'; import { push } from '../../../lib/redux-router';
import { usePopup } from '../../../lib/popup'; import { closePopup, usePopup } from '../../../lib/popup';
import selectors from '../../../selectors'; import selectors from '../../../selectors';
import Paths from '../../../constants/Paths'; import Paths from '../../../constants/Paths';
@ -51,6 +51,8 @@ const Card = React.memo(({ id, isInline }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isEditNameOpened, setIsEditNameOpened] = useState(false); const [isEditNameOpened, setIsEditNameOpened] = useState(false);
const actionsPopupRef = useRef(null);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (document.activeElement) { if (document.activeElement) {
document.activeElement.blur(); document.activeElement.blur();
@ -59,6 +61,17 @@ const Card = React.memo(({ id, isInline }) => {
dispatch(push(Paths.CARDS.replace(':id', id))); dispatch(push(Paths.CARDS.replace(':id', id)));
}, [id, dispatch]); }, [id, dispatch]);
const handleContextMenu = useCallback((event) => {
if (!actionsPopupRef.current) {
return;
}
event.preventDefault();
closePopup();
actionsPopupRef.current.open();
}, []);
const handleNameEdit = useCallback(() => { const handleNameEdit = useCallback(() => {
setIsEditNameOpened(true); setIsEditNameOpened(true);
}, []); }, []);
@ -110,12 +123,13 @@ const Card = React.memo(({ id, isInline }) => {
<div <div
className={classNames(styles.content, card.isClosed && styles.contentDisabled)} className={classNames(styles.content, card.isClosed && styles.contentDisabled)}
onClick={handleClick} onClick={handleClick}
onContextMenu={handleContextMenu}
> >
<Content cardId={id} /> <Content cardId={id} />
{colorLineNode} {colorLineNode}
</div> </div>
{canUseActions && ( {canUseActions && (
<ActionsPopup cardId={id} onNameEdit={handleNameEdit}> <ActionsPopup ref={actionsPopupRef} cardId={id} onNameEdit={handleNameEdit}>
<Button className={styles.actionsButton}> <Button className={styles.actionsButton}>
<Icon fitted name="pencil" size="small" /> <Icon fitted name="pencil" size="small" />
</Button> </Button>

View file

@ -4,7 +4,7 @@
*/ */
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react'; import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
@ -12,13 +12,13 @@ import styles from './Popup.module.css';
export default (Step, { position, onOpen, onClose } = {}) => { export default (Step, { position, onOpen, onClose } = {}) => {
return useMemo(() => { return useMemo(() => {
const Popup = React.memo(({ children, ...stepProps }) => { const Popup = React.forwardRef(({ children, ...stepProps }, ref) => {
const [isOpened, setIsOpened] = useState(false); const [isOpened, setIsOpened] = useState(false);
const wrapperRef = useRef(null); const wrapperRef = useRef(null);
const resizeObserverRef = useRef(null); const resizeObserverRef = useRef(null);
const handleOpen = useCallback(() => { const open = useCallback(() => {
setIsOpened(true); setIsOpened(true);
if (onOpen) { if (onOpen) {
@ -26,6 +26,10 @@ export default (Step, { position, onOpen, onClose } = {}) => {
} }
}, []); }, []);
const handleOpen = useCallback(() => {
open();
}, [open]);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
setIsOpened(false); setIsOpened(false);
}, []); }, []);
@ -73,6 +77,14 @@ export default (Step, { position, onOpen, onClose } = {}) => {
resizeObserverRef.current.observe(element); resizeObserverRef.current.observe(element);
}, []); }, []);
useImperativeHandle(
ref,
() => ({
open,
}),
[open],
);
const tigger = React.cloneElement(children, { const tigger = React.cloneElement(children, {
onClick: handleTriggerClick, onClick: handleTriggerClick,
}); });
@ -116,6 +128,6 @@ export default (Step, { position, onOpen, onClose } = {}) => {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
}; };
return Popup; return React.memo(Popup);
}, [position, onOpen, onClose]); }, [position, onOpen, onClose]);
}; };