Merge pull request #1683 from symonbaikov/feat/negative-label-filter
Add negative label filtering for cards Two conflicts, both from work that landed while this branch was open. In the endless list query, master had added an ORDER BY so the cursor and the limit agree. The exclusion clause belongs in the WHERE part, so it is placed before it rather than after; the other way round the statement does not parse. The label item had been restructured here for the tri-state filter and had gained a tooltip on master. The restructured version is kept and the tooltip put back on top of it, which also brings back the translation hook this branch had dropped.
This commit is contained in:
@@ -20,6 +20,7 @@ import UserAvatar from '../../users/UserAvatar';
|
||||
import BoardMembershipsStep from '../../board-memberships/BoardMembershipsStep';
|
||||
import LabelChip from '../../labels/LabelChip';
|
||||
import LabelsStep from '../../labels/LabelsStep';
|
||||
import { LabelFilterModes } from '../../../constants/Enums';
|
||||
|
||||
import styles from './Filters.module.scss';
|
||||
|
||||
@@ -27,6 +28,7 @@ const Filters = React.memo(() => {
|
||||
const board = useSelector(selectors.selectCurrentBoard);
|
||||
const userIds = useSelector(selectors.selectFilterUserIdsForCurrentBoard);
|
||||
const labelIds = useSelector(selectors.selectFilterLabelIdsForCurrentBoard);
|
||||
const excludedLabelIds = useSelector(selectors.selectFilterExcludedLabelIdsForCurrentBoard);
|
||||
const currentUserId = useSelector(selectors.selectCurrentUserId);
|
||||
|
||||
const withCurrentUserSelector = useSelector(
|
||||
@@ -48,6 +50,26 @@ const Filters = React.memo(() => {
|
||||
|
||||
const [searchFieldRef, handleSearchFieldRef] = useNestedRef('inputRef');
|
||||
|
||||
const labelModes = useMemo(
|
||||
() => ({
|
||||
...labelIds.reduce(
|
||||
(result, labelId) => ({
|
||||
...result,
|
||||
[labelId]: LabelFilterModes.INCLUDE,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
...excludedLabelIds.reduce(
|
||||
(result, labelId) => ({
|
||||
...result,
|
||||
[labelId]: LabelFilterModes.EXCLUDE,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
[labelIds, excludedLabelIds],
|
||||
);
|
||||
|
||||
const cancelSearch = useCallback(() => {
|
||||
debouncedSearch.cancel();
|
||||
setSearch('');
|
||||
@@ -84,27 +106,20 @@ const Filters = React.memo(() => {
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleLabelSelect = useCallback(
|
||||
(labelId) => {
|
||||
dispatch(entryActions.addLabelToFilterInCurrentBoard(labelId));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleLabelDeselect = useCallback(
|
||||
(labelId) => {
|
||||
dispatch(entryActions.removeLabelFromFilterInCurrentBoard(labelId));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleLabelClick = useCallback(
|
||||
({
|
||||
currentTarget: {
|
||||
dataset: { id: labelId },
|
||||
},
|
||||
}) => {
|
||||
dispatch(entryActions.removeLabelFromFilterInCurrentBoard(labelId));
|
||||
dispatch(entryActions.updateLabelFilterInCurrentBoard(labelId, LabelFilterModes.NONE));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleLabelModeChange = useCallback(
|
||||
(labelId, mode) => {
|
||||
dispatch(entryActions.updateLabelFilterInCurrentBoard(labelId, mode));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
@@ -178,14 +193,17 @@ const Filters = React.memo(() => {
|
||||
</span>
|
||||
<span className={styles.filter}>
|
||||
<LabelsPopup
|
||||
currentIds={labelIds}
|
||||
currentIds={[]}
|
||||
currentModes={labelModes}
|
||||
isFilterModeEnabled
|
||||
title="common.filterByLabels"
|
||||
onSelect={handleLabelSelect}
|
||||
onDeselect={handleLabelDeselect}
|
||||
onModeChange={handleLabelModeChange}
|
||||
>
|
||||
<button type="button" className={styles.filterButton}>
|
||||
<span className={styles.filterTitle}>{`${t('common.labels')}:`}</span>
|
||||
{labelIds.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
|
||||
{labelIds.length === 0 && excludedLabelIds.length === 0 && (
|
||||
<span className={styles.filterLabel}>{t('common.all')}</span>
|
||||
)}
|
||||
</button>
|
||||
</LabelsPopup>
|
||||
{labelIds.map((labelId) => (
|
||||
@@ -193,6 +211,11 @@ const Filters = React.memo(() => {
|
||||
<LabelChip id={labelId} size="small" onClick={handleLabelClick} />
|
||||
</span>
|
||||
))}
|
||||
{excludedLabelIds.map((labelId) => (
|
||||
<span key={labelId} className={styles.filterItem}>
|
||||
<LabelChip id={labelId} size="small" isExcluded onClick={handleLabelClick} />
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<span className={styles.filter}>
|
||||
<Input
|
||||
|
||||
@@ -21,7 +21,7 @@ const Sizes = {
|
||||
MEDIUM: 'medium',
|
||||
};
|
||||
|
||||
const LabelChip = React.memo(({ id, size, onClick }) => {
|
||||
const LabelChip = React.memo(({ id, size, isExcluded, onClick }) => {
|
||||
const selectLabelById = useMemo(() => selectors.makeSelectLabelById(), []);
|
||||
|
||||
const label = useSelector((state) => selectLabelById(state, id));
|
||||
@@ -33,6 +33,7 @@ const LabelChip = React.memo(({ id, size, onClick }) => {
|
||||
styles.wrapper,
|
||||
!label.name && styles.wrapperNameless,
|
||||
styles[`wrapper${upperFirst(size)}`],
|
||||
isExcluded && styles.wrapperExcluded,
|
||||
onClick && styles.wrapperHoverable,
|
||||
globalStyles[`background${upperFirst(camelCase(label.color))}`],
|
||||
)}
|
||||
@@ -59,11 +60,13 @@ const LabelChip = React.memo(({ id, size, onClick }) => {
|
||||
LabelChip.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
size: PropTypes.oneOf(Object.values(Sizes)),
|
||||
isExcluded: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
LabelChip.defaultProps = {
|
||||
size: Sizes.MEDIUM,
|
||||
isExcluded: false,
|
||||
onClick: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.wrapperExcluded {
|
||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.75);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
|
||||
.wrapperTiny {
|
||||
|
||||
@@ -16,83 +16,126 @@ import { Button } from 'semantic-ui-react';
|
||||
import { Tooltip } from '../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import { BoardMembershipRoles } from '../../../constants/Enums';
|
||||
import { BoardMembershipRoles, LabelFilterModes } from '../../../constants/Enums';
|
||||
|
||||
import styles from './Item.module.scss';
|
||||
import globalStyles from '../../../styles.module.scss';
|
||||
|
||||
const Item = React.memo(({ id, index, isActive, onSelect, onDeselect, onEdit }) => {
|
||||
const selectLabelById = useMemo(() => selectors.makeSelectLabelById(), []);
|
||||
const NEXT_FILTER_MODE_BY_MODE = {
|
||||
[LabelFilterModes.NONE]: LabelFilterModes.INCLUDE,
|
||||
[LabelFilterModes.INCLUDE]: LabelFilterModes.EXCLUDE,
|
||||
[LabelFilterModes.EXCLUDE]: LabelFilterModes.NONE,
|
||||
};
|
||||
|
||||
const label = useSelector((state) => selectLabelById(state, id));
|
||||
const [t] = useTranslation();
|
||||
const Item = React.memo(
|
||||
({
|
||||
id,
|
||||
index,
|
||||
isActive,
|
||||
mode,
|
||||
isFilterModeEnabled,
|
||||
onSelect,
|
||||
onDeselect,
|
||||
onModeChange,
|
||||
onEdit,
|
||||
}) => {
|
||||
const selectLabelById = useMemo(() => selectors.makeSelectLabelById(), []);
|
||||
|
||||
const canEdit = useSelector((state) => {
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
});
|
||||
const label = useSelector((state) => selectLabelById(state, id));
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleToggleClick = useCallback(() => {
|
||||
if (label.isPersisted) {
|
||||
if (isActive) {
|
||||
onDeselect(id);
|
||||
} else {
|
||||
onSelect(id);
|
||||
const canEdit = useSelector((state) => {
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
});
|
||||
|
||||
const handleToggleClick = useCallback(() => {
|
||||
if (label.isPersisted) {
|
||||
if (isFilterModeEnabled) {
|
||||
onModeChange(id, NEXT_FILTER_MODE_BY_MODE[mode]);
|
||||
} else if (isActive) {
|
||||
onDeselect(id);
|
||||
} else {
|
||||
onSelect(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [id, isActive, onSelect, onDeselect, label.isPersisted]);
|
||||
}, [
|
||||
id,
|
||||
isActive,
|
||||
mode,
|
||||
isFilterModeEnabled,
|
||||
onSelect,
|
||||
onDeselect,
|
||||
onModeChange,
|
||||
label.isPersisted,
|
||||
]);
|
||||
|
||||
const handleEditClick = useCallback(() => {
|
||||
onEdit(id);
|
||||
}, [id, onEdit]);
|
||||
const handleEditClick = useCallback(() => {
|
||||
onEdit(id);
|
||||
}, [id, onEdit]);
|
||||
|
||||
return (
|
||||
<Draggable draggableId={id} index={index} isDragDisabled={!label.isPersisted || !canEdit}>
|
||||
{({ innerRef, draggableProps, dragHandleProps }, { isDragging }) => {
|
||||
const contentNode = (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<div {...draggableProps} ref={innerRef} className={styles.wrapper}>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
|
||||
return (
|
||||
<Draggable draggableId={id} index={index} isDragDisabled={!label.isPersisted || !canEdit}>
|
||||
{({ innerRef, draggableProps, dragHandleProps }, { isDragging }) => {
|
||||
const contentNode = (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<div {...draggableProps} ref={innerRef} className={styles.wrapper}>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions */}
|
||||
<span
|
||||
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
className={classNames(
|
||||
styles.name,
|
||||
isActive && styles.nameActive,
|
||||
globalStyles[`background${upperFirst(camelCase(label.color))}`],
|
||||
<span
|
||||
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
className={classNames(
|
||||
styles.name,
|
||||
isFilterModeEnabled && styles.nameFilterMode,
|
||||
((!isFilterModeEnabled && isActive) || mode === LabelFilterModes.INCLUDE) &&
|
||||
styles.nameActive,
|
||||
mode === LabelFilterModes.EXCLUDE && styles.nameExcluded,
|
||||
globalStyles[`background${upperFirst(camelCase(label.color))}`],
|
||||
)}
|
||||
onClick={handleToggleClick}
|
||||
>
|
||||
{label.name}
|
||||
</span>
|
||||
{canEdit && (
|
||||
<Tooltip content={t('action.edit', { context: 'title' })}>
|
||||
<Button
|
||||
icon="pencil"
|
||||
size="small"
|
||||
floated="right"
|
||||
disabled={!label.isPersisted}
|
||||
className={styles.editButton}
|
||||
onClick={handleEditClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
onClick={handleToggleClick}
|
||||
>
|
||||
{label.name}
|
||||
</span>
|
||||
{canEdit && (
|
||||
<Tooltip content={t('action.edit', { context: 'title' })}>
|
||||
<Button
|
||||
icon="pencil"
|
||||
size="small"
|
||||
floated="right"
|
||||
disabled={!label.isPersisted}
|
||||
className={styles.editButton}
|
||||
onClick={handleEditClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
|
||||
return isDragging ? ReactDOM.createPortal(contentNode, document.body) : contentNode;
|
||||
}}
|
||||
</Draggable>
|
||||
);
|
||||
});
|
||||
return isDragging ? ReactDOM.createPortal(contentNode, document.body) : contentNode;
|
||||
}}
|
||||
</Draggable>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Item.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onDeselect: PropTypes.func.isRequired,
|
||||
mode: PropTypes.oneOf(Object.values(LabelFilterModes)),
|
||||
isFilterModeEnabled: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
onDeselect: PropTypes.func,
|
||||
onModeChange: PropTypes.func,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Item.defaultProps = {
|
||||
mode: LabelFilterModes.NONE,
|
||||
isFilterModeEnabled: false,
|
||||
onSelect: undefined,
|
||||
onDeselect: undefined,
|
||||
onModeChange: undefined,
|
||||
};
|
||||
|
||||
export default Item;
|
||||
|
||||
@@ -34,6 +34,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.nameFilterMode:before {
|
||||
bottom: 1px;
|
||||
content: "○";
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
line-height: 36px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.nameActive:before {
|
||||
bottom: 1px;
|
||||
content: "Г";
|
||||
@@ -47,6 +60,18 @@
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.nameExcluded:before {
|
||||
bottom: 1px;
|
||||
content: "−";
|
||||
font-size: 22px;
|
||||
font-weight: normal;
|
||||
line-height: 36px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
text-align: center;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
margin-bottom: 4px;
|
||||
|
||||
@@ -15,7 +15,7 @@ import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { useField, useNestedRef, useSteps } from '../../../hooks';
|
||||
import DroppableTypes from '../../../constants/DroppableTypes';
|
||||
import { BoardMembershipRoles } from '../../../constants/Enums';
|
||||
import { BoardMembershipRoles, LabelFilterModes } from '../../../constants/Enums';
|
||||
import Item from './Item';
|
||||
import AddStep from './AddStep';
|
||||
import EditStep from './EditStep';
|
||||
@@ -28,175 +28,198 @@ const StepTypes = {
|
||||
EDIT: 'EDIT',
|
||||
};
|
||||
|
||||
const LabelsStep = React.memo(({ currentIds, cardId, title, onSelect, onDeselect, onBack }) => {
|
||||
const labels = useSelector(selectors.selectLabelsForCurrentBoard);
|
||||
const LabelsStep = React.memo(
|
||||
({
|
||||
currentIds,
|
||||
currentModes,
|
||||
isFilterModeEnabled,
|
||||
cardId,
|
||||
title,
|
||||
onSelect,
|
||||
onDeselect,
|
||||
onModeChange,
|
||||
onBack,
|
||||
}) => {
|
||||
const labels = useSelector(selectors.selectLabelsForCurrentBoard);
|
||||
|
||||
const canAdd = useSelector((state) => {
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
const [search, handleSearchChange] = useField('');
|
||||
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||
|
||||
const filteredLabels = useMemo(
|
||||
() =>
|
||||
labels.filter(
|
||||
(label) =>
|
||||
(label.name && label.name.toLowerCase().includes(cleanSearch)) ||
|
||||
label.color.includes(cleanSearch),
|
||||
),
|
||||
[labels, cleanSearch],
|
||||
);
|
||||
|
||||
const [searchFieldRef, handleSearchFieldRef] = useNestedRef('inputRef');
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
document.body.classList.add(globalStyles.dragging);
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
({ draggableId, source, destination }) => {
|
||||
document.body.classList.remove(globalStyles.dragging);
|
||||
|
||||
if (!destination || source.index === destination.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(entryActions.moveLabel(draggableId, destination.index));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleAddClick = useCallback(() => {
|
||||
openStep(StepTypes.ADD);
|
||||
}, [openStep]);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(id) => {
|
||||
openStep(StepTypes.EDIT, {
|
||||
id,
|
||||
});
|
||||
},
|
||||
[openStep],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
searchFieldRef.current.focus({
|
||||
preventScroll: true,
|
||||
const canAdd = useSelector((state) => {
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
return !!boardMembership && boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
});
|
||||
}, [searchFieldRef]);
|
||||
|
||||
if (step) {
|
||||
switch (step.type) {
|
||||
case StepTypes.ADD:
|
||||
return (
|
||||
<AddStep
|
||||
cardId={cardId}
|
||||
// TODO: memoize?
|
||||
defaultData={{
|
||||
name: search,
|
||||
}}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.EDIT: {
|
||||
const currentLabel = labels.find((label) => label.id === step.params.id);
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
const [search, handleSearchChange] = useField('');
|
||||
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||
|
||||
if (currentLabel) {
|
||||
return <EditStep labelId={currentLabel.id} onBack={handleBack} />;
|
||||
const filteredLabels = useMemo(
|
||||
() =>
|
||||
labels.filter(
|
||||
(label) =>
|
||||
(label.name && label.name.toLowerCase().includes(cleanSearch)) ||
|
||||
label.color.includes(cleanSearch),
|
||||
),
|
||||
[labels, cleanSearch],
|
||||
);
|
||||
|
||||
const [searchFieldRef, handleSearchFieldRef] = useNestedRef('inputRef');
|
||||
|
||||
const handleDragStart = useCallback(() => {
|
||||
document.body.classList.add(globalStyles.dragging);
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
({ draggableId, source, destination }) => {
|
||||
document.body.classList.remove(globalStyles.dragging);
|
||||
|
||||
if (!destination || source.index === destination.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
openStep(null);
|
||||
dispatch(entryActions.moveLabel(draggableId, destination.index));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
break;
|
||||
const handleAddClick = useCallback(() => {
|
||||
openStep(StepTypes.ADD);
|
||||
}, [openStep]);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(id) => {
|
||||
openStep(StepTypes.EDIT, {
|
||||
id,
|
||||
});
|
||||
},
|
||||
[openStep],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
searchFieldRef.current.focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
}, [searchFieldRef]);
|
||||
|
||||
if (step) {
|
||||
switch (step.type) {
|
||||
case StepTypes.ADD:
|
||||
return (
|
||||
<AddStep
|
||||
cardId={cardId}
|
||||
// TODO: memoize?
|
||||
defaultData={{
|
||||
name: search,
|
||||
}}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.EDIT: {
|
||||
const currentLabel = labels.find((label) => label.id === step.params.id);
|
||||
|
||||
if (currentLabel) {
|
||||
return <EditStep labelId={currentLabel.id} onBack={handleBack} />;
|
||||
}
|
||||
|
||||
openStep(null);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Input
|
||||
fluid
|
||||
ref={handleSearchFieldRef}
|
||||
value={search}
|
||||
placeholder={t('common.searchLabels')}
|
||||
maxLength={128}
|
||||
icon="search"
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
{filteredLabels.length > 0 && (
|
||||
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<Droppable droppableId="labels" type={DroppableTypes.LABEL}>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<div
|
||||
{...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
ref={innerRef}
|
||||
className={styles.items}
|
||||
>
|
||||
{filteredLabels.map((item, index) => (
|
||||
<Item
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
index={index}
|
||||
isActive={currentIds.includes(item.id)}
|
||||
onSelect={onSelect}
|
||||
onDeselect={onDeselect}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
))}
|
||||
{placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
<Droppable droppableId="labels:hack" type={DroppableTypes.LABEL}>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<div
|
||||
{...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
ref={innerRef}
|
||||
className={styles.droppableHack}
|
||||
>
|
||||
{placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)}
|
||||
{canAdd && (
|
||||
<Button
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Input
|
||||
fluid
|
||||
content={t('action.createNewLabel')}
|
||||
className={styles.addButton}
|
||||
onClick={handleAddClick}
|
||||
ref={handleSearchFieldRef}
|
||||
value={search}
|
||||
placeholder={t('common.searchLabels')}
|
||||
maxLength={128}
|
||||
icon="search"
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
)}
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
{filteredLabels.length > 0 && (
|
||||
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<Droppable droppableId="labels" type={DroppableTypes.LABEL}>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<div
|
||||
{...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
ref={innerRef}
|
||||
className={styles.items}
|
||||
>
|
||||
{filteredLabels.map((item, index) => (
|
||||
<Item
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
index={index}
|
||||
isActive={currentIds.includes(item.id)}
|
||||
mode={currentModes[item.id] || LabelFilterModes.NONE}
|
||||
isFilterModeEnabled={isFilterModeEnabled}
|
||||
onSelect={onSelect}
|
||||
onDeselect={onDeselect}
|
||||
onModeChange={onModeChange}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
))}
|
||||
{placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
<Droppable droppableId="labels:hack" type={DroppableTypes.LABEL}>
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<div
|
||||
{...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
ref={innerRef}
|
||||
className={styles.droppableHack}
|
||||
>
|
||||
{placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)}
|
||||
{canAdd && (
|
||||
<Button
|
||||
fluid
|
||||
content={t('action.createNewLabel')}
|
||||
className={styles.addButton}
|
||||
onClick={handleAddClick}
|
||||
/>
|
||||
)}
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
LabelsStep.propTypes = {
|
||||
currentIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
currentModes: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
isFilterModeEnabled: PropTypes.bool,
|
||||
cardId: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onDeselect: PropTypes.func.isRequired,
|
||||
onSelect: PropTypes.func,
|
||||
onDeselect: PropTypes.func,
|
||||
onModeChange: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
LabelsStep.defaultProps = {
|
||||
currentModes: {},
|
||||
isFilterModeEnabled: false,
|
||||
cardId: undefined,
|
||||
title: 'common.labels',
|
||||
onSelect: undefined,
|
||||
onDeselect: undefined,
|
||||
onModeChange: undefined,
|
||||
onBack: undefined,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user