1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-29 10:09:44 +02:00

feat: Labels reordering

Closes #289
This commit is contained in:
Maksim Eltyshev 2023-01-09 12:17:06 +01:00
parent d260d2dac0
commit a741e26ccb
33 changed files with 370 additions and 104 deletions

View file

@ -25,6 +25,7 @@ const BoardActions = React.memo(
onLabelFromFilterRemove,
onLabelCreate,
onLabelUpdate,
onLabelMove,
onLabelDelete,
}) => {
return (
@ -54,6 +55,7 @@ const BoardActions = React.memo(
onLabelRemove={onLabelFromFilterRemove}
onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelMove={onLabelMove}
onLabelDelete={onLabelDelete}
/>
</div>
@ -82,6 +84,7 @@ BoardActions.propTypes = {
onLabelFromFilterRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
};

View file

@ -22,6 +22,7 @@ const Filters = React.memo(
onLabelRemove,
onLabelCreate,
onLabelUpdate,
onLabelMove,
onLabelDelete,
}) => {
const [t] = useTranslation();
@ -80,6 +81,7 @@ const Filters = React.memo(
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onMove={onLabelMove}
onDelete={onLabelDelete}
>
<button type="button" className={styles.filterButton}>
@ -117,6 +119,7 @@ Filters.propTypes = {
onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
};

View file

@ -45,6 +45,7 @@ const ActionsStep = React.memo(
onLabelRemove,
onLabelCreate,
onLabelUpdate,
onLabelMove,
onLabelDelete,
onClose,
}) => {
@ -119,6 +120,7 @@ const ActionsStep = React.memo(
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onMove={onLabelMove}
onDelete={onLabelDelete}
onBack={handleBack}
/>
@ -241,6 +243,7 @@ ActionsStep.propTypes = {
onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};

View file

@ -48,6 +48,7 @@ const Card = React.memo(
onLabelRemove,
onLabelCreate,
onLabelUpdate,
onLabelMove,
onLabelDelete,
}) => {
const nameEdit = useRef(null);
@ -189,6 +190,7 @@ const Card = React.memo(
onLabelRemove={onLabelRemove}
onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelMove={onLabelMove}
onLabelDelete={onLabelDelete}
>
<Button className={classNames(styles.actionsButton, styles.target)}>
@ -241,6 +243,7 @@ Card.propTypes = {
onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
};

View file

@ -62,6 +62,7 @@ const CardModal = React.memo(
onLabelRemove,
onLabelCreate,
onLabelUpdate,
onLabelMove,
onLabelDelete,
onTaskCreate,
onTaskUpdate,
@ -235,6 +236,7 @@ const CardModal = React.memo(
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onMove={onLabelMove}
onDelete={onLabelDelete}
>
<Label name={label.name} color={label.color} />
@ -252,6 +254,7 @@ const CardModal = React.memo(
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onMove={onLabelMove}
onDelete={onLabelDelete}
>
<button
@ -419,6 +422,7 @@ const CardModal = React.memo(
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onMove={onLabelMove}
onDelete={onLabelDelete}
>
<Button fluid className={styles.actionButton}>
@ -546,6 +550,7 @@ CardModal.propTypes = {
onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
onTaskCreate: PropTypes.func.isRequired,
onTaskUpdate: PropTypes.func.isRequired,

View file

@ -1,53 +1,69 @@
import upperFirst from 'lodash/upperFirst';
import camelCase from 'lodash/camelCase';
import React, { useCallback } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Draggable } from 'react-beautiful-dnd';
import { Button } from 'semantic-ui-react';
import styles from './Item.module.scss';
import globalStyles from '../../styles.module.scss';
const Item = React.memo(
({ name, color, isPersisted, isActive, canEdit, onSelect, onDeselect, onEdit }) => {
({ id, index, name, color, isPersisted, isActive, canEdit, onSelect, onDeselect, onEdit }) => {
const handleToggleClick = useCallback(() => {
if (isActive) {
onDeselect();
} else {
onSelect();
if (isPersisted) {
if (isActive) {
onDeselect();
} else {
onSelect();
}
}
}, [isActive, onSelect, onDeselect]);
}, [isPersisted, isActive, onSelect, onDeselect]);
return (
<div className={styles.wrapper}>
<Button
fluid
content={name}
active={isActive}
disabled={!isPersisted}
className={classNames(
styles.labelButton,
isActive && styles.labelButtonActive,
globalStyles[`background${upperFirst(camelCase(color))}`],
)}
onClick={handleToggleClick}
/>
{canEdit && (
<Button
icon="pencil"
size="small"
floated="right"
disabled={!isPersisted}
className={styles.editButton}
onClick={onEdit}
/>
)}
</div>
<Draggable draggableId={id} index={index} isDragDisabled={!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 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(color))}`],
)}
onClick={handleToggleClick}
>
{name}
</span>
{canEdit && (
<Button
icon="pencil"
size="small"
floated="right"
disabled={!isPersisted}
className={styles.editButton}
onClick={onEdit}
/>
)}
</div>
);
return isDragging ? ReactDOM.createPortal(contentNode, document.body) : contentNode;
}}
</Draggable>
);
},
);
Item.propTypes = {
id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
name: PropTypes.string,
color: PropTypes.string.isRequired,
isPersisted: PropTypes.bool.isRequired,

View file

@ -12,16 +12,15 @@
}
}
.labelButton {
.name {
border-radius: 3px;
color: #fff;
cursor: pointer;
flex: 1 1 auto;
font-size: 14px;
font-weight: bold;
margin-right: 0;
overflow: hidden;
padding: 8px 32px 8px 10px;
position: relative;
text-align: left;
text-overflow: ellipsis;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2);
@ -30,7 +29,7 @@
}
}
.labelButtonActive:before {
.nameActive:before {
bottom: 1px;
content: "Г";
font-size: 18px;

View file

@ -2,10 +2,12 @@ import pick from 'lodash/pick';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { Button } from 'semantic-ui-react';
import { Input, Popup } from '../../lib/custom-ui';
import { useField, useSteps } from '../../hooks';
import DroppableTypes from '../../constants/DroppableTypes';
import AddStep from './AddStep';
import EditStep from './EditStep';
import Item from './Item';
@ -27,6 +29,7 @@ const LabelsStep = React.memo(
onDeselect,
onCreate,
onUpdate,
onMove,
onDelete,
onBack,
}) => {
@ -74,6 +77,17 @@ const LabelsStep = React.memo(
[onDeselect],
);
const handleDragEnd = useCallback(
({ draggableId, source, destination }) => {
if (!destination || source.index === destination.index) {
return;
}
onMove(draggableId, destination.index);
},
[onMove],
);
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
@ -145,21 +159,45 @@ const LabelsStep = React.memo(
onChange={handleSearchChange}
/>
{filteredItems.length > 0 && (
<div className={styles.items}>
{filteredItems.map((item) => (
<Item
key={item.id}
name={item.name}
color={item.color}
isPersisted={item.isPersisted}
isActive={currentIds.includes(item.id)}
canEdit={canEdit}
onSelect={() => handleSelect(item.id)}
onDeselect={() => handleDeselect(item.id)}
onEdit={() => handleEdit(item.id)}
/>
))}
</div>
<DragDropContext 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}
>
{filteredItems.map((item, index) => (
<Item
key={item.id}
id={item.id}
index={index}
name={item.name}
color={item.color}
isPersisted={item.isPersisted}
isActive={currentIds.includes(item.id)}
canEdit={canEdit}
onSelect={() => handleSelect(item.id)}
onDeselect={() => handleDeselect(item.id)}
onEdit={() => handleEdit(item.id)}
/>
))}
{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>
)}
{canEdit && (
<Button
@ -186,6 +224,7 @@ LabelsStep.propTypes = {
onDeselect: PropTypes.func.isRequired,
onCreate: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onBack: PropTypes.func,
};

View file

@ -15,6 +15,11 @@
}
}
.droppableHack {
display: none;
position: fixed;
}
.items {
margin-top: 8px;
max-height: 60vh;