feat: add tooltips for icon buttons

This commit is contained in:
Symon
2026-06-07 18:06:04 +03:00
parent 856768c45e
commit 4a70e995d3
38 changed files with 612 additions and 253 deletions
@@ -9,6 +9,7 @@ import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Icon, Label, Loader } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
@@ -161,9 +162,11 @@ const ItemContent = React.forwardRef(({ id, onOpen }, ref) => {
</div>
{canEdit && (
<EditPopup attachmentId={id}>
<Button className={styles.editButton}>
<Icon fitted name="pencil" size="small" />
</Button>
<Tooltip content={t('action.editAttachment', { context: 'title' })}>
<Button className={styles.editButton}>
<Icon fitted name="pencil" size="small" />
</Button>
</Tooltip>
</EditPopup>
)}
</div>
@@ -8,7 +8,9 @@ import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Draggable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { Button, Icon } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
@@ -18,6 +20,7 @@ const CustomField = React.memo(({ id, index, onEdit }) => {
const selectCustomFieldById = useMemo(() => selectors.makeSelectCustomFieldById(), []);
const customField = useSelector((state) => selectCustomFieldById(state, id));
const [t] = useTranslation();
const handleEditClick = useCallback(() => {
onEdit(id);
@@ -36,14 +39,16 @@ const CustomField = React.memo(({ id, index, onEdit }) => {
{customField.showOnFrontOfCard && <Icon name="pin" className={styles.nameIcon} />}
{customField.name}
</span>
<Button
icon="pencil"
size="small"
floated="right"
disabled={!customField.isPersisted}
className={styles.editButton}
onClick={handleEditClick}
/>
<Tooltip content={t('action.edit', { context: 'title' })}>
<Button
icon="pencil"
size="small"
floated="right"
disabled={!customField.isPersisted}
className={styles.editButton}
onClick={handleEditClick}
/>
</Tooltip>
</div>
);
@@ -7,7 +7,9 @@ import groupBy from 'lodash/groupBy';
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import { usePopup } from '../../../lib/popup';
import selectors from '../../../selectors';
@@ -20,6 +22,7 @@ import styles from './BoardMemberships.module.scss';
const BoardMemberships = React.memo(() => {
const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard);
const [t] = useTranslation();
const canAdd = useSelector((state) => {
const user = selectors.selectCurrentUser(state);
@@ -57,7 +60,9 @@ const BoardMemberships = React.memo(() => {
)}
{canAdd && (
<AddPopup>
<Button icon="add user" className={classNames(styles.segment, styles.addButton)} />
<Tooltip content={t('action.addMember')}>
<Button icon="add user" className={classNames(styles.segment, styles.addButton)} />
</Tooltip>
</AddPopup>
)}
</>
@@ -11,7 +11,7 @@ import { useInView } from 'react-intersection-observer';
import { useTranslation } from 'react-i18next';
import { Button, Icon, Loader } from 'semantic-ui-react';
import { useWindowWidth } from '../../../lib/hooks';
import { Masonry } from '../../../lib/custom-ui';
import { Masonry, Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import { BoardMembershipRoles } from '../../../constants/Enums';
@@ -81,14 +81,16 @@ const GridView = React.memo(
</span>
</Button>
{onCardPaste && clipboard && canPasteCard && (
<Button
type="button"
disabled={!onCardCreate}
className={classNames(styles.addCardButton, styles.paste)}
onClick={onCardPaste}
>
<Icon fitted name="paste" />
</Button>
<Tooltip content={t('action.paste')}>
<Button
type="button"
disabled={!onCardCreate}
className={classNames(styles.addCardButton, styles.paste)}
onClick={onCardPaste}
>
<Icon fitted name="paste" />
</Button>
</Tooltip>
)}
</div>
</div>
@@ -10,6 +10,7 @@ import { shallowEqual, useSelector } from 'react-redux';
import { useInView } from 'react-intersection-observer';
import { useTranslation } from 'react-i18next';
import { Button, Icon, Loader } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import { BoardMembershipRoles } from '../../../constants/Enums';
@@ -74,14 +75,16 @@ const ListView = React.memo(
</span>
</Button>
{onCardPaste && clipboard && canPasteCard && (
<Button
type="button"
disabled={!onCardCreate}
className={classNames(styles.addCardButton, styles.paste)}
onClick={onCardPaste}
>
<Icon fitted name="paste" />
</Button>
<Tooltip content={t('action.paste')}>
<Button
type="button"
disabled={!onCardCreate}
className={classNames(styles.addCardButton, styles.paste)}
onClick={onCardPaste}
>
<Icon fitted name="paste" />
</Button>
</Tooltip>
)}
</div>
))}
@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import { useDidUpdate } from '../../../lib/hooks';
import { usePopup } from '../../../lib/popup';
import { Input } from '../../../lib/custom-ui';
import { Input, Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
@@ -162,11 +162,13 @@ const Filters = React.memo(() => {
</button>
</BoardMembershipsPopup>
{userIds.length === 0 && withCurrentUserSelector && (
<button type="button" className={styles.filterButton} onClick={handleCurrentUserSelect}>
<span className={styles.filterLabel}>
<Icon fitted name="target" className={styles.filterLabelIcon} />
</span>
</button>
<Tooltip content={t('action.filterByCurrentUser')}>
<button type="button" className={styles.filterButton} onClick={handleCurrentUserSelect}>
<span className={styles.filterLabel}>
<Icon fitted name="target" className={styles.filterLabelIcon} />
</span>
</button>
</Tooltip>
)}
{userIds.map((userId) => (
<span key={userId} className={styles.filterItem}>
@@ -5,7 +5,9 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import { Tooltip } from '../../../../lib/custom-ui';
import { usePopup } from '../../../../lib/popup';
import selectors from '../../../../selectors';
@@ -20,6 +22,7 @@ const RightSide = React.memo(() => {
const board = useSelector(selectors.selectCurrentBoard);
const dispatch = useDispatch();
const [t] = useTranslation();
const handleSelectViewClick = useCallback(
({ currentTarget: { value: view } }) => {
@@ -40,24 +43,33 @@ const RightSide = React.memo(() => {
<div className={styles.action}>
<div className={styles.buttonGroup}>
{views.map((view) => (
<button
<Tooltip
key={view}
type="button"
value={view}
content={t('action.switchToView', {
view: t(`common.${view}`).toLowerCase(),
})}
disabled={view === board.view}
className={styles.button}
onClick={handleSelectViewClick}
>
<Icon fitted name={BoardViewIcons[view]} />
</button>
<button
type="button"
value={view}
disabled={view === board.view}
className={styles.button}
onClick={handleSelectViewClick}
>
<Icon fitted name={BoardViewIcons[view]} />
</button>
</Tooltip>
))}
</div>
</div>
<div className={styles.action}>
<ActionsPopup>
<button type="button" className={styles.button}>
<Icon fitted name="ellipsis vertical" />
</button>
<Tooltip content={t('common.openBoardActions')}>
<button type="button" className={styles.button}>
<Icon fitted name="ellipsis vertical" />
</button>
</Tooltip>
</ActionsPopup>
</div>
</>
@@ -6,7 +6,9 @@
import React, { useCallback, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import { closePopup, usePopup } from '../../../lib/popup';
import selectors from '../../../selectors';
@@ -32,6 +34,7 @@ const Boards = React.memo(() => {
});
const dispatch = useDispatch();
const [t] = useTranslation();
const tabsWrapperRef = useRef(null);
@@ -75,7 +78,9 @@ const Boards = React.memo(() => {
{placeholder}
{canAdd && (
<AddBoardPopup>
<Button icon="plus" className={styles.addButton} />
<Tooltip content={t('action.createBoard')}>
<Button icon="plus" className={styles.addButton} />
</Tooltip>
</AddBoardPopup>
)}
</div>
+8 -3
View File
@@ -9,7 +9,9 @@ import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router';
import { Draggable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { Button, Icon } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
@@ -40,6 +42,7 @@ const Item = React.memo(({ id, index }) => {
});
const dispatch = useDispatch();
const [t] = useTranslation();
const handleEditClick = useCallback(() => {
dispatch(entryActions.openBoardSettingsModal(id));
@@ -64,9 +67,11 @@ const Item = React.memo(({ id, index }) => {
<span className={styles.name}>{board.name}</span>
</Link>
{canEdit && (
<Button className={styles.editButton} onClick={handleEditClick}>
<Icon fitted name="pencil" size="small" />
</Button>
<Tooltip content={t('action.edit', { context: 'title' })}>
<Button className={styles.editButton} onClick={handleEditClick}>
<Icon fitted name="pencil" size="small" />
</Button>
</Tooltip>
)}
</>
) : (
+8 -3
View File
@@ -7,7 +7,9 @@ import React, { useCallback, useContext, useMemo, useRef, useState } from 'react
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Icon } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import { push } from '../../../lib/redux-router';
import { closePopup, usePopup } from '../../../lib/popup';
@@ -57,6 +59,7 @@ const Card = React.memo(({ id, isInline }) => {
});
const dispatch = useDispatch();
const [t] = useTranslation();
const [isEditNameOpened, setIsEditNameOpened] = useState(false);
const [, , handleCardMouseEnter, handleCardMouseLeave] = useContext(BoardShortcutsContext);
@@ -151,9 +154,11 @@ const Card = React.memo(({ id, isInline }) => {
</div>
{canUseActions && (
<CardActionsPopup ref={actionsPopupRef} cardId={id} onNameEdit={handleNameEdit}>
<Button className={styles.actionsButton}>
<Icon fitted name="pencil" size="small" />
</Button>
<Tooltip content={t('common.openCardActions')}>
<Button className={styles.actionsButton}>
<Icon fitted name="pencil" size="small" />
</Button>
</Tooltip>
</CardActionsPopup>
)}
</>
@@ -6,7 +6,9 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import { push } from '../../../lib/redux-router';
import selectors from '../../../selectors';
@@ -45,6 +47,7 @@ const CardModal = React.memo(() => {
});
const dispatch = useDispatch();
const [t] = useTranslation();
const handleClose = useCallback(() => {
dispatch(push(Paths.BOARDS.replace(':id', card.boardId)));
@@ -102,9 +105,11 @@ const CardModal = React.memo(() => {
onClose={handleClose}
>
{prevCardId && (
<button type="button" className={styles.prevButton} onClick={handlePrevClick}>
<Icon fitted name="arrow left" size="large" className={styles.prevButtonIcon} />
</button>
<Tooltip content={t('action.goBack')} position="right center">
<button type="button" className={styles.prevButton} onClick={handlePrevClick}>
<Icon fitted name="arrow left" size="large" className={styles.prevButtonIcon} />
</button>
</Tooltip>
)}
{canEdit ? (
<AddAttachmentZone>
@@ -7,7 +7,9 @@ import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Icon } from 'semantic-ui-react';
import { Tooltip } from '../../../../lib/custom-ui';
import selectors from '../../../../selectors';
import { usePopupInClosableContext } from '../../../../hooks';
@@ -23,6 +25,7 @@ const Item = React.memo(({ id, dragHandleProps }) => {
const selectListById = useMemo(() => selectors.makeSelectListById(), []);
const customFieldGroup = useSelector((state) => selectCustomFieldGroupById(state, id));
const [t] = useTranslation();
const canEdit = useSelector((state) => {
if (customFieldGroup.boardId) {
@@ -51,9 +54,11 @@ const Item = React.memo(({ id, dragHandleProps }) => {
<div className={classNames(styles.moduleHeader, canEdit && styles.moduleHeaderEditable)}>
{customFieldGroup.isPersisted && canEdit && (
<CustomFieldGroupPopup id={customFieldGroup.id}>
<Button className={styles.editButton}>
<Icon fitted name="pencil" size="small" />
</Button>
<Tooltip content={t('action.editGroup')}>
<Button className={styles.editButton}>
<Icon fitted name="pencil" size="small" />
</Button>
</Tooltip>
</CustomFieldGroupPopup>
)}
<span className={styles.moduleHeaderTitle}>{customFieldGroup.name}</span>
@@ -9,6 +9,7 @@ import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Checkbox, Grid, Icon } from 'semantic-ui-react';
import { useDidUpdate } from '../../../lib/hooks';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
@@ -361,12 +362,14 @@ const ProjectContent = React.memo(() => {
onUserSelect={handleUserSelect}
onUserDeselect={handleUserDeselect}
>
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
<Tooltip content={t('action.addMember')}>
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
</Tooltip>
</BoardMembershipsPopup>
)}
</div>
@@ -401,12 +404,14 @@ const ProjectContent = React.memo(() => {
onSelect={handleLabelSelect}
onDeselect={handleLabelDeselect}
>
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
<Tooltip content={t('action.addLabel')}>
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
</Tooltip>
</LabelsPopup>
)}
</div>
@@ -465,17 +470,19 @@ const ProjectContent = React.memo(() => {
)}
</span>
{canEditStopwatch && (
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
onClick={handleToggleStopwatchClick}
>
<Icon
name={card.stopwatch.startedAt ? 'pause' : 'play'}
size="small"
className={styles.addAttachment}
/>
</button>
<Tooltip content={t(card.stopwatch.startedAt ? 'action.stop' : 'action.start')}>
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
onClick={handleToggleStopwatchClick}
>
<Icon
name={card.stopwatch.startedAt ? 'pause' : 'play'}
size="small"
className={styles.addAttachment}
/>
</button>
</Tooltip>
)}
</div>
)}
@@ -507,9 +514,11 @@ const ProjectContent = React.memo(() => {
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */
<div className={styles.cursorPointer} onClick={handleEditDescriptionClick}>
<Button className={styles.editButton}>
<Icon fitted name="pencil" size="small" />
</Button>
<Tooltip content={t('action.editDescription', { context: 'title' })}>
<Button className={styles.editButton}>
<Icon fitted name="pencil" size="small" />
</Button>
</Tooltip>
<ExpandableMarkdown>{card.description}</ExpandableMarkdown>
</div>
) : (
@@ -554,14 +563,16 @@ const ProjectContent = React.memo(() => {
<div className={classNames(styles.text, styles.textList)}>{t('common.list')}</div>
{canUseLists ? (
<ListsPopup currentId={list.id} onSelect={handleListSelect}>
<button type="button" className={styles.listButton}>
<span className={classNames(styles.list, styles.listHoverable)}>
<Icon name="columns" size="small" className={styles.listIcon} />
<span className={styles.hidable}>
{list.name || t(`common.${list.type}`)}
<Tooltip content={t('common.selectList')}>
<button type="button" className={styles.listButton}>
<span className={classNames(styles.list, styles.listHoverable)}>
<Icon name="columns" size="small" className={styles.listIcon} />
<span className={styles.hidable}>
{list.name || t(`common.${list.type}`)}
</span>
</span>
</span>
</button>
</button>
</Tooltip>
</ListsPopup>
) : (
<span className={styles.list}>
@@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next';
import { Gallery, Item as GalleryItem } from 'react-photoswipe-gallery';
import { Button, Grid, Icon } from 'semantic-ui-react';
import { useDidUpdate } from '../../../lib/hooks';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
@@ -383,12 +384,14 @@ const StoryContent = React.memo(() => {
onSelect={handleLabelSelect}
onDeselect={handleLabelDeselect}
>
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
<Tooltip content={t('action.addLabel')}>
<button
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<Icon name="add" size="small" className={styles.addAttachment} />
</button>
</Tooltip>
</LabelsPopup>
)}
</div>
@@ -419,9 +422,11 @@ const StoryContent = React.memo(() => {
className={classNames(styles.descriptionText, styles.cursorPointer)}
onClick={handleEditDescriptionClick}
>
<Button className={styles.editButton}>
<Icon fitted name="pencil" size="small" />
</Button>
<Tooltip content={t('action.editDescription', { context: 'title' })}>
<Button className={styles.editButton}>
<Icon fitted name="pencil" size="small" />
</Button>
</Tooltip>
<Markdown>{card.description}</Markdown>
</div>
) : (
@@ -477,14 +482,16 @@ const StoryContent = React.memo(() => {
<div className={classNames(styles.text, styles.textList)}>{t('common.list')}</div>
{canUseLists ? (
<ListsPopup currentId={list.id} onSelect={handleListSelect}>
<button type="button" className={styles.listButton}>
<span className={classNames(styles.list, styles.listHoverable)}>
<Icon name="columns" size="small" className={styles.listIcon} />
<span className={styles.hidable}>
{list.name || t(`common.${list.type}`)}
<Tooltip content={t('common.selectList')}>
<button type="button" className={styles.listButton}>
<span className={classNames(styles.list, styles.listHoverable)}>
<Icon name="columns" size="small" className={styles.listIcon} />
<span className={styles.hidable}>
{list.name || t(`common.${list.type}`)}
</span>
</span>
</span>
</button>
</button>
</Tooltip>
</ListsPopup>
) : (
<span className={styles.list}>
@@ -9,8 +9,10 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { Draggable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { Button, Icon } from 'semantic-ui-react';
import { useToggle } from '../../../../lib/hooks';
import { Tooltip } from '../../../../lib/custom-ui';
import selectors from '../../../../selectors';
import { usePopupInClosableContext } from '../../../../hooks';
@@ -24,6 +26,7 @@ const Item = React.memo(({ id, index }) => {
const selectTaskListById = useMemo(() => selectors.makeSelectTaskListById(), []);
const taskList = useSelector((state) => selectTaskListById(state, id));
const [t] = useTranslation();
const canEdit = useSelector((state) => {
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
@@ -67,22 +70,30 @@ const Item = React.memo(({ id, index }) => {
{taskList.isPersisted && withActions && (
<div className={classNames(styles.actions)}>
{taskList.hideCompletedTasks && (
<Button
className={styles.button}
onClick={handleToggleCompletedVisibleClick}
<Tooltip
content={t(
isCompletedVisible ? 'common.hideCompletedTasks' : 'action.showActive',
)}
>
<Icon
fitted
name={isCompletedVisible ? 'eye slash' : 'eye'}
size="small"
/>
</Button>
<Button
className={styles.button}
onClick={handleToggleCompletedVisibleClick}
>
<Icon
fitted
name={isCompletedVisible ? 'eye slash' : 'eye'}
size="small"
/>
</Button>
</Tooltip>
)}
{canEdit && (
<EditPopup taskListId={taskList.id}>
<Button className={styles.button}>
<Icon fitted name="pencil" size="small" />
</Button>
<Tooltip content={t('action.edit', { context: 'title' })}>
<Button className={styles.button}>
<Icon fitted name="pencil" size="small" />
</Button>
</Tooltip>
</EditPopup>
)}
</div>
@@ -8,7 +8,7 @@ import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Icon, Input, Message } from 'semantic-ui-react';
import { Popup } from '../../../../lib/custom-ui';
import { Popup, Tooltip } from '../../../../lib/custom-ui';
import selectors from '../../../../selectors';
import entryActions from '../../../../entry-actions';
@@ -115,9 +115,11 @@ const ApiKeyStep = React.memo(({ userId, onBack, onClose }) => {
/>
<div className={styles.valueWrapper}>
<Input fluid readOnly value={user.apiKeyState.value} className={styles.value} />
<Button className={styles.copyButton} onClick={handleCopyClick}>
<Icon fitted name={isCopied ? 'check' : 'copy'} />
</Button>
<Tooltip content={t('action.copy')}>
<Button className={styles.copyButton} onClick={handleCopyClick}>
<Icon fitted name={isCopied ? 'check' : 'copy'} />
</Button>
</Tooltip>
</div>
</>
) : (
@@ -10,6 +10,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Icon, Table } from 'semantic-ui-react';
import { useEventCallback } from '../../../../lib/hooks';
import { Tooltip } from '../../../../lib/custom-ui';
import selectors from '../../../../selectors';
import entryActions from '../../../../entry-actions';
@@ -84,9 +85,11 @@ const Item = React.memo(({ id }) => {
</Table.Cell>
<Table.Cell textAlign="right">
<ActionsPopup userId={id}>
<Button className={styles.button}>
<Icon fitted name="pencil" />
</Button>
<Tooltip content={t('common.userActions', { context: 'title' })}>
<Button className={styles.button}>
<Icon fitted name="pencil" />
</Button>
</Tooltip>
</ActionsPopup>
</Table.Cell>
</Table.Row>
+51 -34
View File
@@ -7,7 +7,9 @@ import React, { useCallback } from 'react';
import classNames from 'classnames';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router';
import { useTranslation } from 'react-i18next';
import { Button, Icon, Menu } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import { usePopup } from '../../../lib/popup';
import selectors from '../../../selectors';
@@ -72,6 +74,7 @@ const Header = React.memo(() => {
}, shallowEqual);
const dispatch = useDispatch();
const [t] = useTranslation();
const handleToggleFavoritesClick = useCallback(() => {
dispatch(entryActions.toggleFavorites(!isFavoritesEnabled));
@@ -102,55 +105,69 @@ const Header = React.memo(() => {
<Menu inverted size="large" className={styles.menu}>
{project && (
<Menu.Menu position="left">
<Menu.Item
as={Link}
to={Paths.ROOT}
className={classNames(styles.item, styles.itemHoverable)}
>
<Icon fitted name="arrow left" />
</Menu.Item>
<Tooltip content={t('action.returnToProjects')}>
<Menu.Item
as={Link}
to={Paths.ROOT}
className={classNames(styles.item, styles.itemHoverable)}
>
<Icon fitted name="arrow left" />
</Menu.Item>
</Tooltip>
<Menu.Item className={classNames(styles.item, styles.title)}>
{project.name}
{canEditProject && (
<Button className={styles.editButton} onClick={handleProjectSettingsClick}>
<Icon fitted name="pencil" size="small" />
</Button>
<Tooltip content={t('common.openProjectSettings')}>
<Button className={styles.editButton} onClick={handleProjectSettingsClick}>
<Icon fitted name="pencil" size="small" />
</Button>
</Tooltip>
)}
</Menu.Item>
</Menu.Menu>
)}
<Menu.Menu position="right">
{withFavoritesToggler && (
<Menu.Item
className={classNames(styles.item, styles.itemHoverable)}
onClick={handleToggleFavoritesClick}
<Tooltip
content={t(isFavoritesEnabled ? 'action.hideProjectList' : 'action.showProjectList')}
>
<Icon
fitted
name={isFavoritesEnabled ? 'star' : 'star outline'}
className={classNames(isFavoritesEnabled && styles.itemIconEnabled)}
/>
</Menu.Item>
<Menu.Item
className={classNames(styles.item, styles.itemHoverable)}
onClick={handleToggleFavoritesClick}
>
<Icon
fitted
name={isFavoritesEnabled ? 'star' : 'star outline'}
className={classNames(isFavoritesEnabled && styles.itemIconEnabled)}
/>
</Menu.Item>
</Tooltip>
)}
{withEditModeToggler && (
<Menu.Item
className={classNames(styles.item, styles.itemHoverable)}
onClick={handleToggleEditModeClick}
<Tooltip
content={t(isEditModeEnabled ? 'action.disableEditMode' : 'action.enableEditMode')}
>
<Icon
fitted
name={isEditModeEnabled ? 'unlock' : 'lock'}
className={classNames(isEditModeEnabled && styles.itemIconEnabled)}
/>
</Menu.Item>
<Menu.Item
className={classNames(styles.item, styles.itemHoverable)}
onClick={handleToggleEditModeClick}
>
<Icon
fitted
name={isEditModeEnabled ? 'unlock' : 'lock'}
className={classNames(isEditModeEnabled && styles.itemIconEnabled)}
/>
</Menu.Item>
</Tooltip>
)}
<NotificationsPopup>
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
<Icon fitted name="bell" />
{notificationIds.length > 0 && (
<span className={styles.notification}>{notificationIds.length}</span>
)}
</Menu.Item>
<Tooltip content={t('common.openNotifications')}>
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
<Icon fitted name="bell" />
{notificationIds.length > 0 && (
<span className={styles.notification}>{notificationIds.length}</span>
)}
</Menu.Item>
</Tooltip>
</NotificationsPopup>
<UserActionsPopup>
<Menu.Item className={classNames(styles.item, styles.itemHoverable)}>
@@ -6,7 +6,9 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import { Tooltip } from '../../../../lib/custom-ui';
import { usePopup } from '../../../../lib/popup';
import selectors from '../../../../selectors';
@@ -17,12 +19,18 @@ import SelectOrderStep from './SelectOrderStep';
import styles from './RightSide.module.scss';
const LABEL_BY_VIEW = {
[HomeViews.GRID_PROJECTS]: 'common.gridProjects',
[HomeViews.GROUPED_PROJECTS]: 'common.groupedProjects',
};
const RightSide = React.memo(() => {
const currentView = useSelector(selectors.selectHomeView); // TODO: rename?
const currentOrder = useSelector(selectors.selectProjectsOrder); // TODO: rename?
const isHiddenVisible = useSelector(selectors.selectIsHiddenProjectsVisible);
const dispatch = useDispatch();
const [t] = useTranslation();
const handleSelectViewClick = useCallback(
({ currentTarget: { value: view } }) => {
@@ -47,34 +55,47 @@ const RightSide = React.memo(() => {
return (
<>
<div className={styles.action}>
<button
type="button"
className={classNames(styles.button)}
onClick={handleToggleHiddenClick}
<Tooltip
content={t(isHiddenVisible ? 'action.hideHiddenProjects' : 'common.showHiddenProjects')}
>
<Icon fitted name={isHiddenVisible ? 'eye slash' : 'eye'} />
</button>
<button
type="button"
className={classNames(styles.button)}
onClick={handleToggleHiddenClick}
>
<Icon fitted name={isHiddenVisible ? 'eye slash' : 'eye'} />
</button>
</Tooltip>
</div>
<div className={styles.action}>
<SelectOrderPopup value={currentOrder} onSelect={handleOrderSelect}>
<button type="button" className={styles.button}>
<Icon fitted name={ProjectOrderIcons[currentOrder]} />
</button>
<Tooltip content={t('common.selectOrder', { context: 'title' })}>
<button type="button" className={styles.button}>
<Icon fitted name={ProjectOrderIcons[currentOrder]} />
</button>
</Tooltip>
</SelectOrderPopup>
</div>
<div className={styles.action}>
<div className={styles.buttonGroup}>
{[HomeViews.GRID_PROJECTS, HomeViews.GROUPED_PROJECTS].map((view) => (
<button
<Tooltip
key={view}
type="button"
value={view}
content={t('action.switchToView', {
view: t(LABEL_BY_VIEW[view]),
})}
disabled={view === currentView}
className={styles.button}
onClick={handleSelectViewClick}
>
<Icon fitted name={HomeViewIcons[view]} />
</button>
<button
type="button"
value={view}
disabled={view === currentView}
className={styles.button}
onClick={handleSelectViewClick}
>
<Icon fitted name={HomeViewIcons[view]} />
</button>
</Tooltip>
))}
</div>
</div>
@@ -8,7 +8,9 @@ import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Draggable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { Button, Icon } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
@@ -18,6 +20,7 @@ const CustomField = React.memo(({ id, index, onEdit }) => {
const selectCustomFieldById = useMemo(() => selectors.makeSelectCustomFieldById(), []);
const customField = useSelector((state) => selectCustomFieldById(state, id));
const [t] = useTranslation();
const handleEditClick = useCallback(() => {
onEdit(id);
@@ -36,14 +39,16 @@ const CustomField = React.memo(({ id, index, onEdit }) => {
{customField.showOnFrontOfCard && <Icon name="pin" className={styles.nameIcon} />}
{customField.name}
</span>
<Button
icon="pencil"
size="small"
floated="right"
disabled={!customField.isPersisted}
className={styles.editButton}
onClick={handleEditClick}
/>
<Tooltip content={t('action.edit', { context: 'title' })}>
<Button
icon="pencil"
size="small"
floated="right"
disabled={!customField.isPersisted}
className={styles.editButton}
onClick={handleEditClick}
/>
</Tooltip>
</div>
);
@@ -8,7 +8,9 @@ import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Draggable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
@@ -18,6 +20,7 @@ const Item = React.memo(({ id, index, onEdit }) => {
const selectCustomFieldGroupById = useMemo(() => selectors.makeSelectCustomFieldGroupById(), []);
const customFieldGroup = useSelector((state) => selectCustomFieldGroupById(state, id));
const [t] = useTranslation();
const handleEditClick = useCallback(() => {
onEdit(id);
@@ -35,14 +38,16 @@ const Item = React.memo(({ id, index, onEdit }) => {
>
{customFieldGroup.name}
</span>
<Button
icon="pencil"
size="small"
floated="right"
disabled={!customFieldGroup.isPersisted}
className={styles.editButton}
onClick={handleEditClick}
/>
<Tooltip content={t('action.editGroup')}>
<Button
icon="pencil"
size="small"
floated="right"
disabled={!customFieldGroup.isPersisted}
className={styles.editButton}
onClick={handleEditClick}
/>
</Tooltip>
</div>
);
@@ -6,7 +6,9 @@
import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Icon } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
@@ -49,6 +51,7 @@ const CustomField = React.memo(({ id, customFieldGroupId }) => {
});
const dispatch = useDispatch();
const [t] = useTranslation();
const [isCopied, setIsCopied] = useState(false);
const handleValueUpdate = useCallback(
@@ -95,9 +98,11 @@ const CustomField = React.memo(({ id, customFieldGroupId }) => {
</div>
)}
{customFieldValue && customFieldValue.content && (
<Button className={styles.copyButton} onClick={handleCopyClick}>
<Icon fitted name={isCopied ? 'check' : 'copy'} />
</Button>
<Tooltip content={t('action.copy')}>
<Button className={styles.copyButton} onClick={handleCopyClick}>
<Icon fitted name={isCopied ? 'check' : 'copy'} />
</Button>
</Tooltip>
)}
</div>
</div>
@@ -11,7 +11,9 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { Draggable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import { BoardMembershipRoles } from '../../../constants/Enums';
@@ -23,6 +25,7 @@ const Item = React.memo(({ id, index, isActive, onSelect, onDeselect, onEdit })
const selectLabelById = useMemo(() => selectors.makeSelectLabelById(), []);
const label = useSelector((state) => selectLabelById(state, id));
const [t] = useTranslation();
const canEdit = useSelector((state) => {
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
@@ -63,14 +66,16 @@ const Item = React.memo(({ id, index, isActive, onSelect, onDeselect, onEdit })
{label.name}
</span>
{canEdit && (
<Button
icon="pencil"
size="small"
floated="right"
disabled={!label.isPersisted}
className={styles.editButton}
onClick={handleEditClick}
/>
<Tooltip content={t('action.edit', { context: 'title' })}>
<Button
icon="pencil"
size="small"
floated="right"
disabled={!label.isPersisted}
className={styles.editButton}
onClick={handleEditClick}
/>
</Tooltip>
)}
</div>
);
+21 -14
View File
@@ -13,6 +13,7 @@ import { useTranslation } from 'react-i18next';
import { Draggable, Droppable } from 'react-beautiful-dnd';
import { Button, Icon } from 'semantic-ui-react';
import { useDidUpdate, useToggle, useTransitioning } from '../../../lib/hooks';
import { Tooltip } from '../../../lib/custom-ui';
import { usePopup } from '../../../lib/popup';
import selectors from '../../../selectors';
@@ -240,16 +241,20 @@ const List = React.memo(({ id, index }) => {
{list.isPersisted &&
(canEdit ? (
<ActionsPopup listId={id} onNameEdit={handleNameEdit} onCardAdd={handleCardAdd}>
<Button className={styles.headerButton}>
<Icon fitted name="pencil" size="small" />
</Button>
<Tooltip content={t('common.openListActions')}>
<Button className={styles.headerButton}>
<Icon fitted name="pencil" size="small" />
</Button>
</Tooltip>
</ActionsPopup>
) : (
canArchiveCards && (
<ArchiveCardsPopup listId={id}>
<Button className={styles.headerButton}>
<Icon fitted name="archive" size="small" />
</Button>
<Tooltip content={t('action.archiveCards')}>
<Button className={styles.headerButton}>
<Icon fitted name="archive" size="small" />
</Button>
</Tooltip>
</ArchiveCardsPopup>
)
))}
@@ -275,14 +280,16 @@ const List = React.memo(({ id, index }) => {
</span>
</button>
{clipboard && canPasteCard && (
<button
type="button"
disabled={!list.isPersisted}
className={classNames(styles.addCardButton, styles.paste)}
onClick={handlePasteCardClick}
>
<Icon name="paste" />
</button>
<Tooltip content={t('action.paste')}>
<button
type="button"
disabled={!list.isPersisted}
className={classNames(styles.addCardButton, styles.paste)}
onClick={handlePasteCardClick}
>
<Icon name="paste" />
</button>
</Tooltip>
)}
</div>
)}
@@ -6,8 +6,10 @@
import React, { useCallback, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Dropdown, Form, Icon, Input } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
@@ -31,6 +33,7 @@ const Item = React.memo(({ id }) => {
const notificationService = useSelector((state) => selectNotificationServiceById(state, id));
const dispatch = useDispatch();
const [t] = useTranslation();
const defaultData = useMemo(
() => ({
@@ -183,24 +186,32 @@ const Item = React.memo(({ id }) => {
onChange={handleFieldChange}
onBlur={handleUrlBlur}
/>
<Button
type="button"
loading={notificationService.isTesting}
disabled={!notificationService.isPersisted || notificationService.isTesting}
className={styles.button}
onClick={handleTestClick}
>
<Icon fitted name="paper plane outline" />
</Button>
<Tooltip content={t('action.sendTestEmail')}>
<Button
type="button"
loading={notificationService.isTesting}
disabled={!notificationService.isPersisted || notificationService.isTesting}
className={styles.button}
onClick={handleTestClick}
>
<Icon fitted name="paper plane outline" />
</Button>
</Tooltip>
<ConfirmationPopup
title="common.deleteNotificationService"
content="common.areYouSureYouWantToDeleteThisNotificationService"
buttonContent="action.deleteNotificationService"
onConfirm={handleDeleteConfirm}
>
<Button type="button" disabled={!notificationService.isPersisted} className={styles.button}>
<Icon fitted name="trash alternate outline" />
</Button>
<Tooltip content={t('action.deleteNotificationService')}>
<Button
type="button"
disabled={!notificationService.isPersisted}
className={styles.button}
>
<Icon fitted name="trash alternate outline" />
</Button>
</Tooltip>
</ConfirmationPopup>
</Form>
);
@@ -6,9 +6,10 @@
import React, { useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Trans } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import { Button, Dropdown, Form, Icon, Input, Message } from 'semantic-ui-react';
import { useDidUpdate, useToggle } from '../../../lib/hooks';
import { Tooltip } from '../../../lib/custom-ui';
import { useEscapeInterceptor, useForm, useNestedRef } from '../../../hooks';
import { NotificationServiceFormats } from '../../../constants/Enums';
@@ -22,6 +23,8 @@ const DEFAULT_DATA = {
};
const NotificationServices = React.memo(({ ids, onCreate }) => {
const [t] = useTranslation();
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [focusUrlFieldState, focusUrlField] = useToggle();
@@ -117,9 +120,11 @@ const NotificationServices = React.memo(({ ids, onCreate }) => {
onChange={handleFieldChange}
onBlur={handleUrlBlur}
/>
<Button positive className={styles.button}>
<Icon fitted name="plus" />
</Button>
<Tooltip content={t('common.addNotificationService', { context: 'title' })}>
<Button positive className={styles.button}>
<Icon fitted name="plus" />
</Button>
</Tooltip>
</Form>
)}
</>
@@ -5,7 +5,9 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react';
import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import { usePopupInClosableContext } from '../../../hooks';
@@ -18,6 +20,7 @@ import styles from './ProjectManagers.module.scss';
const ProjectManagers = React.memo(() => {
const projectManagers = useSelector(selectors.selectManagersForCurrentProject);
const [t] = useTranslation();
const canAdd = useSelector((state) => {
const user = selectors.selectCurrentUser(state);
@@ -47,7 +50,9 @@ const ProjectManagers = React.memo(() => {
))}
{canAdd && (
<AddPopup>
<Button icon="add user" className={styles.addButton} />
<Tooltip content={t('action.addMember')}>
<Button icon="add user" className={styles.addButton} />
</Tooltip>
</AddPopup>
)}
</div>
@@ -5,7 +5,9 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Icon, Label } from 'semantic-ui-react';
import { Tooltip } from '../../../../lib/custom-ui';
import { usePopupInClosableContext } from '../../../../hooks';
import ConfirmationStep from '../../../common/ConfirmationStep';
@@ -13,6 +15,8 @@ import ConfirmationStep from '../../../common/ConfirmationStep';
import styles from './Image.module.scss';
const Image = React.memo(({ url, isActive, onSelect, onDeselect, onDelete }) => {
const [t] = useTranslation();
const handleClick = useCallback(() => {
if (isActive) {
onDeselect();
@@ -52,9 +56,11 @@ const Image = React.memo(({ url, isActive, onSelect, onDeselect, onDelete }) =>
buttonContent="action.deleteBackgroundImage"
onConfirm={onDelete}
>
<Button className={styles.deleteButton}>
<Icon fitted name="trash alternate" size="small" />
</Button>
<Tooltip content={t('action.deleteBackgroundImage')}>
<Button className={styles.deleteButton}>
<Icon fitted name="trash alternate" size="small" />
</Button>
</Tooltip>
</ConfirmationPopup>
)}
</div>
@@ -10,8 +10,10 @@ import classNames from 'classnames';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router';
import { Draggable } from 'react-beautiful-dnd';
import { useTranslation } from 'react-i18next';
import { Button, Checkbox, Icon } from 'semantic-ui-react';
import { useDidUpdate } from '../../../../lib/hooks';
import { Tooltip } from '../../../../lib/custom-ui';
import selectors from '../../../../selectors';
import entryActions from '../../../../entry-actions';
@@ -60,6 +62,7 @@ const Task = React.memo(({ id, index }) => {
}, shallowEqual);
const dispatch = useDispatch();
const [t] = useTranslation();
const [isEditNameOpened, setIsEditNameOpened] = useState(false);
const [, , setIsClosableActive] = useContext(ClosableContext);
@@ -203,16 +206,20 @@ const Task = React.memo(({ id, index }) => {
className={styles.assigneeUserAvatar}
/>
) : (
<Button className={styles.button}>
<Icon fitted name="add user" size="small" />
</Button>
<Tooltip content={t('common.selectAssignee', { context: 'title' })}>
<Button className={styles.button}>
<Icon fitted name="add user" size="small" />
</Button>
</Tooltip>
)}
</SelectAssigneePopup>
)}
<ActionsPopup taskId={id} onNameEdit={handleNameEdit}>
<Button className={styles.button}>
<Icon fitted name="pencil" size="small" />
</Button>
<Tooltip content={t('common.taskActions', { context: 'title' })}>
<Button className={styles.button}>
<Icon fitted name="pencil" size="small" />
</Button>
</Tooltip>
</ActionsPopup>
</>
) : (
+21 -2
View File
@@ -5,6 +5,7 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Modal } from 'semantic-ui-react';
import useClosable from './use-closable';
@@ -22,6 +23,8 @@ export default (initialClosableValue) => {
const ClosableModal = useMemo(() => {
// eslint-disable-next-line no-shadow
const ClosableModal = React.memo(({ closeIcon, onClose, ...props }) => {
const [t] = useTranslation();
const handleClose = useCallback(
(event) => {
if (isClosableActiveRef.current) {
@@ -41,16 +44,32 @@ export default (initialClosableValue) => {
[closeIcon, onClose],
);
const semanticCloseIcon =
closeIcon === true
? {
name: 'close',
'aria-label': t('action.close'),
title: t('action.close'),
}
: closeIcon;
return (
<ClosableContext.Provider value={closableContextValue}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Modal open {...props} closeIcon={closeIcon} onClose={handleClose} />
<Modal open {...props} closeIcon={semanticCloseIcon} onClose={handleClose} />
</ClosableContext.Provider>
);
});
ClosableModal.propTypes = {
closeIcon: PropTypes.bool,
closeIcon: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.shape({
name: PropTypes.string,
'aria-label': PropTypes.string,
title: PropTypes.string,
}),
]),
onClose: PropTypes.func,
};
@@ -5,16 +5,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
import Tooltip from '../Tooltip';
import styles from './PopupHeader.module.css';
const PopupHeader = React.memo(({ children, onBack }) => (
<SemanticUIPopup.Header className={styles.wrapper}>
{onBack && <Button icon="angle left" onClick={onBack} className={styles.backButton} />}
<div className={styles.content}>{children}</div>
</SemanticUIPopup.Header>
));
const PopupHeader = React.memo(({ children, onBack }) => {
const [t] = useTranslation();
return (
<SemanticUIPopup.Header className={styles.wrapper}>
{onBack && (
<Tooltip content={t('action.goBack')}>
<Button icon="angle left" onClick={onBack} className={styles.backButton} />
</Tooltip>
)}
<div className={styles.content}>{children}</div>
</SemanticUIPopup.Header>
);
});
PopupHeader.propTypes = {
children: PropTypes.node.isRequired,
@@ -0,0 +1,69 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Popup as SemanticUIPopup } from 'semantic-ui-react';
import styles from './Tooltip.module.css';
const callAll =
(...callbacks) =>
(...args) => {
callbacks.forEach((callback) => {
if (callback) {
callback(...args);
}
});
};
const Tooltip = React.forwardRef(
({ children, content, disabled, position, 'aria-label': ariaLabel, onClick, ...props }, ref) => {
const trigger = useMemo(() => {
const contentAriaLabel = typeof content === 'string' ? content : undefined;
return React.cloneElement(children, {
...props,
ref,
'aria-label': children.props['aria-label'] || ariaLabel || contentAriaLabel,
onClick: callAll(children.props.onClick, onClick),
});
}, [children, content, props, ref, ariaLabel, onClick]);
if (disabled || !content) {
return trigger;
}
return (
<SemanticUIPopup
basic
inverted
trigger={trigger}
content={content}
on={['hover', 'focus']}
position={position}
className={styles.wrapper}
/>
);
},
);
Tooltip.propTypes = {
children: PropTypes.element.isRequired,
content: PropTypes.node.isRequired,
disabled: PropTypes.bool,
'aria-label': PropTypes.string,
onClick: PropTypes.func,
position: PropTypes.string,
};
Tooltip.defaultProps = {
disabled: false,
'aria-label': undefined,
onClick: undefined,
position: 'top center',
};
export default React.memo(Tooltip);
@@ -0,0 +1,12 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) .wrapper {
border-radius: 3px;
font-size: 12px;
font-weight: bold;
line-height: 16px;
padding: 6px 8px;
}
@@ -0,0 +1,8 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import Tooltip from './Tooltip';
export default Tooltip;
+2 -1
View File
@@ -7,5 +7,6 @@ import Input from './components/Input';
import Popup from './components/Popup';
import Masonry from './components/Masonry';
import FilePicker from './components/FilePicker';
import Tooltip from './components/Tooltip';
export { Input, Popup, Masonry, FilePicker };
export { Input, Popup, Masonry, FilePicker, Tooltip };
+7 -1
View File
@@ -6,13 +6,17 @@
import { ResizeObserver } from '@juggle/resize-observer';
import React, { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
import { Tooltip } from '../custom-ui';
import styles from './Popup.module.css';
export default (Step, { position, onOpen, onClose } = {}) => {
return useMemo(() => {
const Popup = React.forwardRef(({ children, ...stepProps }, ref) => {
const [t] = useTranslation();
const [stepParams, setStepParams] = useState(null);
const wrapperRef = useRef(null);
@@ -116,7 +120,9 @@ export default (Step, { position, onOpen, onClose } = {}) => {
onClick={handleClick}
>
<div ref={handleContentRef}>
<Button icon="close" onClick={handleClose} className={styles.closeButton} />
<Tooltip content={t('action.close')}>
<Button icon="close" onClick={handleClose} className={styles.closeButton} />
</Tooltip>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Step {...stepProps} {...stepParams} onClose={handleClose} />
</div>
+22
View File
@@ -27,6 +27,7 @@ export default {
addCustomField_title: 'Add Custom Field',
addManager_title: 'Add Manager',
addMember_title: 'Add Member',
addNotificationService_title: 'Add Notification Service',
addTaskList_title: 'Add Task List',
addUser_title: 'Add User',
admin: 'Admin',
@@ -216,6 +217,8 @@ export default {
general: 'General',
gradients: 'Gradients',
grid: 'Grid',
gridProjects: 'grid projects',
groupedProjects: 'grouped projects',
hideCompletedTasks: 'Hide completed tasks',
hideFromProjectListAndFavorites: 'Hide from project list and favorites',
host: 'Host',
@@ -263,6 +266,12 @@ export default {
onlyOneManagerShouldRemainToMakeThisProjectPrivate:
'Only one manager should remain to make this project private',
openBoard_title: 'Open Board',
openBoardActions: 'Open board actions',
openCardActions: 'Open card actions',
openListActions: 'Open list actions',
openNotifications: 'Open notifications',
openProjectActions: 'Open project actions',
openProjectSettings: 'Open project settings',
optional_inline: 'optional',
organization: 'Organization',
others: 'Others',
@@ -307,6 +316,7 @@ export default {
settings: 'Settings',
shared: 'Shared',
sharedWithMe_title: 'Shared With Me',
showHiddenProjects: 'Show hidden projects',
showOnFrontOfCard: 'Show on front of card',
smtp: 'SMTP',
sortList_title: 'Sort List',
@@ -387,6 +397,7 @@ export default {
addCustomField: 'Add custom field',
addCustomFieldGroup: 'Add custom field group',
addList: 'Add list',
addLabel: 'Add label',
addMember: 'Add member',
addMoreDetailedDescription: 'Add more detailed description',
addTask: 'Add task',
@@ -401,6 +412,7 @@ export default {
archiveCards_title: 'Archive Cards',
assignAsOwner: 'Assign as owner',
cancel: 'Cancel',
close: 'Close',
copy: 'Copy',
copyCard_title: 'Copy Card',
createApiKey: 'Create API key',
@@ -459,8 +471,14 @@ export default {
editTitle_title: 'Edit Title',
editType_title: 'Edit Type',
editUsername_title: 'Edit Username',
enableEditMode: 'Enable edit mode',
emptyTrash: 'Empty trash',
emptyTrash_title: 'Empty Trash',
disableEditMode: 'Disable edit mode',
filterByCurrentUser: 'Filter by current user',
goBack: 'Go back',
hideHiddenProjects: 'Hide hidden projects',
hideProjectList: 'Hide project list',
import: 'Import',
join: 'Join',
leave: 'Leave',
@@ -475,6 +493,7 @@ export default {
move: 'Move',
moveCard_title: 'Move Card',
moveList_title: 'Move List',
paste: 'Paste',
regenerateApiKey: 'Regenerate API key',
remove: 'Remove',
removeAssignee: 'Remove assignee',
@@ -486,8 +505,10 @@ export default {
removeMember: 'Remove member',
restoreToList: 'Restore to {{list}}',
returnToBoard: 'Return to board',
returnToProjects: 'Return to projects',
save: 'Save',
sendTestEmail: 'Send test email',
showProjectList: 'Show project list',
showActive: 'Show active',
showAllAttachments: 'Show all attachments ({{hidden}} hidden)',
showCardsWithThisUser: 'Show cards with this user',
@@ -499,6 +520,7 @@ export default {
start: 'Start',
stop: 'Stop',
subscribe: 'Subscribe',
switchToView: 'Switch to {{view}} view',
unlinkSso: 'Unlink SSO',
unlinkSso_title: 'Unlink SSO',
unsubscribe: 'Unsubscribe',
+22
View File
@@ -32,6 +32,7 @@ export default {
addCustomField_title: 'Добавить настраиваемое поле',
addManager_title: 'Добавление менеджера',
addMember_title: 'Добавление участника',
addNotificationService_title: 'Добавить сервис уведомлений',
addTaskList_title: 'Добавить список задач',
addUser_title: 'Добавление пользователя',
admin: 'Админ',
@@ -223,6 +224,8 @@ export default {
general: 'Основные',
gradients: 'Градиенты',
grid: 'Сетка',
gridProjects: 'проекты сеткой',
groupedProjects: 'сгруппированные проекты',
hideCompletedTasks: 'Скрыть выполненные задачи',
hideFromProjectListAndFavorites: 'Скрыть из списка проектов и избранного',
host: 'Хост',
@@ -270,6 +273,12 @@ export default {
onlyOneManagerShouldRemainToMakeThisProjectPrivate:
'Должен остаться только один менеджер, чтобы сделать этот проект частным',
openBoard_title: 'Откройте доску',
openBoardActions: 'Открыть действия доски',
openCardActions: 'Открыть действия карточки',
openListActions: 'Открыть действия списка',
openNotifications: 'Открыть уведомления',
openProjectActions: 'Открыть действия проекта',
openProjectSettings: 'Открыть настройки проекта',
optional_inline: 'необязательно',
organization: 'Организация',
others: 'Другие',
@@ -314,6 +323,7 @@ export default {
settings: 'Настройки',
shared: 'Общий',
sharedWithMe_title: 'Общие со мной',
showHiddenProjects: 'Показать скрытые проекты',
showOnFrontOfCard: 'Показать на лицевой стороне карточки',
smtp: 'SMTP',
sortList_title: 'Сортировка списка',
@@ -397,6 +407,7 @@ export default {
addCustomField: 'Добавить настраиваемое поле',
addCustomFieldGroup: 'Добавить группу настраиваемых полей',
addList: 'Добавить список',
addLabel: 'Добавить метку',
addMember: 'Добавить участника',
addMoreDetailedDescription: 'Добавить более подробное описание',
addTask: 'Добавить задачу',
@@ -411,6 +422,7 @@ export default {
archiveCards_title: 'Архивировать карточки',
assignAsOwner: 'Назначить владельцем',
cancel: 'Отменить',
close: 'Закрыть',
copy: 'Копировать',
copyCard_title: 'Копировать карточку',
createApiKey: 'Создать ключ API',
@@ -469,8 +481,14 @@ export default {
editTitle_title: 'Изменить название',
editType_title: 'Изменить тип',
editUsername_title: 'Изменить имя пользователя',
enableEditMode: 'Включить режим редактирования',
emptyTrash: 'Очистить корзину',
emptyTrash_title: 'Очистить корзину',
disableEditMode: 'Выключить режим редактирования',
filterByCurrentUser: 'Фильтр по текущему пользователю',
goBack: 'Назад',
hideHiddenProjects: 'Скрыть скрытые проекты',
hideProjectList: 'Скрыть список проектов',
import: 'Импорт',
join: 'Присоединиться',
leave: 'Покинуть',
@@ -485,6 +503,7 @@ export default {
move: 'Переместить',
moveCard_title: 'Переместить карточку',
moveList_title: 'Переместить список',
paste: 'Вставить',
regenerateApiKey: 'Перегенерировать ключ API',
remove: 'Убрать',
removeAssignee: 'Удалить исполнителя',
@@ -496,8 +515,10 @@ export default {
removeMember: 'Удалить участника',
restoreToList: 'Восстановить в {{list}}',
returnToBoard: 'Вернуться на доску',
returnToProjects: 'Вернуться к проектам',
save: 'Сохранить',
sendTestEmail: 'Отправить тестовое письмо',
showProjectList: 'Показать список проектов',
showActive: 'Показать активные',
showAllAttachments: 'Показать все вложения ({{hidden}} скрыто)',
showCardsWithThisUser: 'Показать карточки с этим пользователем',
@@ -509,6 +530,7 @@ export default {
start: 'Начать',
stop: 'Остановить',
subscribe: 'Подписаться',
switchToView: 'Переключиться на вид «{{view}}»',
unlinkSso: 'Отвязать SSO',
unlinkSso_title: 'Отвязать SSO',
unsubscribe: 'Отписаться',