mirror of
https://github.com/plankanban/planka.git
synced 2025-08-09 07:25:24 +02:00
feat: Added column collapsing with card counts support
This commit is contained in:
parent
e009ceaccd
commit
7cdac9ca9f
9 changed files with 152 additions and 18 deletions
|
@ -10,6 +10,7 @@ import styles from './ListAdd.module.scss';
|
||||||
|
|
||||||
const DEFAULT_DATA = {
|
const DEFAULT_DATA = {
|
||||||
name: '',
|
name: '',
|
||||||
|
isCollapsed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListAdd = React.memo(({ onCreate, onClose }) => {
|
const ListAdd = React.memo(({ onCreate, onClose }) => {
|
||||||
|
|
|
@ -16,14 +16,22 @@ import styles from './List.module.scss';
|
||||||
|
|
||||||
const List = React.memo(
|
const List = React.memo(
|
||||||
// eslint-disable-next-line prettier/prettier
|
// eslint-disable-next-line prettier/prettier
|
||||||
({ id, index, name, isPersisted, isFiltered, cardIds, cardIdsFull, canEdit, onUpdate, onDelete, onCardCreate }) => {
|
({ id, index, name, isPersisted, isCollapsed, isFiltered, cardIds, cardIdsFull, canEdit, onUpdate, onDelete, onCardCreate }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
|
const [isAddCardOpened, setIsAddCardOpened] = useState(false);
|
||||||
|
|
||||||
const nameEdit = useRef(null);
|
const nameEdit = useRef(null);
|
||||||
const listWrapper = useRef(null);
|
const listWrapper = useRef(null);
|
||||||
|
|
||||||
const handleHeaderClick = useCallback(() => {
|
const handleToggleCollapseClick = useCallback(() => {
|
||||||
|
if (isPersisted && canEdit) {
|
||||||
|
onUpdate({
|
||||||
|
isCollapsed: !isCollapsed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isPersisted, canEdit, onUpdate, isCollapsed]);
|
||||||
|
|
||||||
|
const handleHeaderNameClick = useCallback(() => {
|
||||||
if (isPersisted && canEdit) {
|
if (isPersisted && canEdit) {
|
||||||
nameEdit.current.open();
|
nameEdit.current.open();
|
||||||
}
|
}
|
||||||
|
@ -97,6 +105,45 @@ const List = React.memo(
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
return (
|
||||||
|
<Draggable
|
||||||
|
draggableId={`list:${id}`}
|
||||||
|
index={index}
|
||||||
|
isDragDisabled={!isPersisted || !canEdit}
|
||||||
|
>
|
||||||
|
{({ innerRef, draggableProps, dragHandleProps }) => (
|
||||||
|
<div
|
||||||
|
{...draggableProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
|
data-drag-scroller
|
||||||
|
ref={innerRef}
|
||||||
|
className={styles.innerWrapperCollapsed}
|
||||||
|
>
|
||||||
|
<div className={styles.outerWrapper}>
|
||||||
|
<div
|
||||||
|
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
|
className={styles.headerCollapsed}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
|
||||||
|
jsx-a11y/no-static-element-interactions */}
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.headerCollapseButtonCollapsed,
|
||||||
|
canEdit && styles.headerEditable,
|
||||||
|
)}
|
||||||
|
onClick={handleToggleCollapseClick}
|
||||||
|
>
|
||||||
|
<Icon fitted name="triangle down" size="mid" />
|
||||||
|
</div>
|
||||||
|
<div className={styles.headerNameCollapsed}>{name}</div>
|
||||||
|
<div className={styles.headerCardsCountCollapsed}>{cardsCountText()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={`list:${id}`} index={index} isDragDisabled={!isPersisted || !canEdit}>
|
<Draggable draggableId={`list:${id}`} index={index} isDragDisabled={!isPersisted || !canEdit}>
|
||||||
{({ innerRef, draggableProps, dragHandleProps }) => (
|
{({ innerRef, draggableProps, dragHandleProps }) => (
|
||||||
|
@ -107,15 +154,30 @@ const List = React.memo(
|
||||||
className={styles.innerWrapper}
|
className={styles.innerWrapper}
|
||||||
>
|
>
|
||||||
<div className={styles.outerWrapper}>
|
<div className={styles.outerWrapper}>
|
||||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
|
|
||||||
jsx-a11y/no-static-element-interactions */}
|
|
||||||
<div
|
<div
|
||||||
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
|
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||||
className={classNames(styles.header, canEdit && styles.headerEditable)}
|
className={styles.header}
|
||||||
onClick={handleHeaderClick}
|
|
||||||
>
|
>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
|
||||||
|
jsx-a11y/no-static-element-interactions */}
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.headerCollapseButton,
|
||||||
|
canEdit && styles.headerEditable,
|
||||||
|
)}
|
||||||
|
onClick={handleToggleCollapseClick}
|
||||||
|
>
|
||||||
|
<Icon fitted name="triangle right" size="mid" />
|
||||||
|
</div>
|
||||||
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}>
|
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}>
|
||||||
<div className={styles.headerName}>{name}</div>
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
|
||||||
|
jsx-a11y/no-static-element-interactions */}
|
||||||
|
<div
|
||||||
|
className={classNames(styles.headerName, canEdit && styles.headerEditable)}
|
||||||
|
onClick={handleHeaderNameClick}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
</NameEdit>
|
</NameEdit>
|
||||||
{isPersisted && canEdit && (
|
{isPersisted && canEdit && (
|
||||||
<ActionsPopup
|
<ActionsPopup
|
||||||
|
@ -128,7 +190,7 @@ const List = React.memo(
|
||||||
</Button>
|
</Button>
|
||||||
</ActionsPopup>
|
</ActionsPopup>
|
||||||
)}
|
)}
|
||||||
<div className={styles.cardsCount}>{cardsCountText()}</div>
|
<div className={styles.headerCardsCount}>{cardsCountText()}</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={listWrapper}
|
ref={listWrapper}
|
||||||
|
@ -164,6 +226,7 @@ List.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
index: PropTypes.number.isRequired,
|
index: PropTypes.number.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
|
isCollapsed: PropTypes.bool.isRequired,
|
||||||
isPersisted: PropTypes.bool.isRequired,
|
isPersisted: PropTypes.bool.isRequired,
|
||||||
isFiltered: PropTypes.bool.isRequired,
|
isFiltered: PropTypes.bool.isRequired,
|
||||||
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
|
|
|
@ -39,11 +39,6 @@
|
||||||
min-height: 1px;
|
min-height: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardsCount {
|
|
||||||
color: #798d99;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardsInnerWrapper {
|
.cardsInnerWrapper {
|
||||||
max-height: calc(100vh - 268px);
|
max-height: calc(100vh - 268px);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
@ -80,7 +75,7 @@
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 6px 36px 4px 8px;
|
padding: 4px 36px 4px 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover .target {
|
&:hover .target {
|
||||||
|
@ -88,6 +83,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headerCollapsed {
|
||||||
|
outline: none;
|
||||||
|
height: calc(100vh - 182px);
|
||||||
|
|
||||||
|
&:hover .target {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.headerEditable {
|
.headerEditable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -119,15 +123,58 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
padding: 4px 8px;
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headerNameCollapsed {
|
||||||
|
color: #17394d;
|
||||||
|
font-weight: bold;
|
||||||
|
outline: none;
|
||||||
|
overflow: hidden;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
padding: 0px 5px 10px 5px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-height: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerCollapseButton {
|
||||||
|
width: 10px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 2px 0px 0px 2px;
|
||||||
|
position: absolute;
|
||||||
|
left: 2px;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerCollapseButtonCollapsed {
|
||||||
|
height: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerCardsCount {
|
||||||
|
color: #798d99;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerCardsCountCollapsed {
|
||||||
|
color: #798d99;
|
||||||
|
font-size: 12px;
|
||||||
|
max-height: 10%;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
padding: 0px 5px 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.innerWrapper {
|
.innerWrapper {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
width: 272px;
|
width: 272px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.innerWrapperCollapsed {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.outerWrapper {
|
.outerWrapper {
|
||||||
background: #dfe3e6;
|
background: #dfe3e6;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
|
@ -11,7 +11,7 @@ const makeMapStateToProps = () => {
|
||||||
const selectCardIdsByListId = selectors.makeSelectCardIdsByListId();
|
const selectCardIdsByListId = selectors.makeSelectCardIdsByListId();
|
||||||
|
|
||||||
return (state, { id, index }) => {
|
return (state, { id, index }) => {
|
||||||
const { name, isPersisted } = selectListById(state, id);
|
const { name, isPersisted, isCollapsed } = selectListById(state, id);
|
||||||
const { cardIds, cardIdsFull, isFiltered } = selectCardIdsByListId(state, id);
|
const { cardIds, cardIdsFull, isFiltered } = selectCardIdsByListId(state, id);
|
||||||
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ const makeMapStateToProps = () => {
|
||||||
id,
|
id,
|
||||||
index,
|
index,
|
||||||
name,
|
name,
|
||||||
|
isCollapsed,
|
||||||
isPersisted,
|
isPersisted,
|
||||||
isFiltered,
|
isFiltered,
|
||||||
cardIds,
|
cardIds,
|
||||||
|
|
|
@ -10,6 +10,7 @@ export default class extends BaseModel {
|
||||||
id: attr(),
|
id: attr(),
|
||||||
position: attr(),
|
position: attr(),
|
||||||
name: attr(),
|
name: attr(),
|
||||||
|
isCollapsed: attr(),
|
||||||
boardId: fk({
|
boardId: fk({
|
||||||
to: 'Board',
|
to: 'Board',
|
||||||
as: 'board',
|
as: 'board',
|
||||||
|
|
|
@ -22,6 +22,10 @@ module.exports = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
isCollapsed: {
|
||||||
|
type: 'boolean',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
exits: {
|
exits: {
|
||||||
|
@ -53,7 +57,7 @@ module.exports = {
|
||||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = _.pick(inputs, ['position', 'name']);
|
const values = _.pick(inputs, ['position', 'name', 'isCollapsed']);
|
||||||
|
|
||||||
const list = await sails.helpers.lists.createOne.with({
|
const list = await sails.helpers.lists.createOne.with({
|
||||||
values: {
|
values: {
|
||||||
|
|
|
@ -21,6 +21,9 @@ module.exports = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
isNotEmptyString: true,
|
isNotEmptyString: true,
|
||||||
},
|
},
|
||||||
|
isCollapsed: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
exits: {
|
exits: {
|
||||||
|
@ -52,7 +55,7 @@ module.exports = {
|
||||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = _.pick(inputs, ['position', 'name']);
|
const values = _.pick(inputs, ['position', 'name', 'isCollapsed']);
|
||||||
|
|
||||||
list = await sails.helpers.lists.updateOne.with({
|
list = await sails.helpers.lists.updateOne.with({
|
||||||
values,
|
values,
|
||||||
|
|
|
@ -19,6 +19,11 @@ module.exports = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
isCollapsed: {
|
||||||
|
type: 'boolean',
|
||||||
|
required: true,
|
||||||
|
columnName: 'is_collapsed',
|
||||||
|
},
|
||||||
|
|
||||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports.up = (knex) =>
|
||||||
|
knex.schema.alterTable('list', (table) => {
|
||||||
|
table.boolean('is_collapsed').notNullable().defaultTo(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.down = (knex) =>
|
||||||
|
knex.schema.alterTable('list', (table) => {
|
||||||
|
table.dropColumn('is_collapsed');
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue