pull/380/merge
RAR 2 years ago committed by GitHub
commit cfaec45299
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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 }) => {

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import Filters from './Filters'; import Filters from './Filters';
import Memberships from '../Memberships'; import Memberships from '../Memberships';
import BoardMembershipPermissionsSelectStep from '../BoardMembershipPermissionsSelectStep'; import BoardMembershipPermissionsSelectStep from '../BoardMembershipPermissionsSelectStep';
@ -9,6 +11,7 @@ import styles from './BoardActions.module.scss';
const BoardActions = React.memo( const BoardActions = React.memo(
({ ({
cardCount,
memberships, memberships,
labels, labels,
filterUsers, filterUsers,
@ -28,9 +31,14 @@ const BoardActions = React.memo(
onLabelMove, onLabelMove,
onLabelDelete, onLabelDelete,
}) => { }) => {
const [t] = useTranslation();
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.actions}> <div className={styles.actions}>
<div className={classNames(styles.cardsCount, styles.action)}>
{cardCount} {[cardCount !== 1 ? t('common.cards') : t('common.card')]}
</div>
<div className={styles.action}> <div className={styles.action}>
<Memberships <Memberships
items={memberships} items={memberships}
@ -67,6 +75,7 @@ const BoardActions = React.memo(
BoardActions.propTypes = { BoardActions.propTypes = {
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
cardCount: PropTypes.number.isRequired,
memberships: PropTypes.array.isRequired, memberships: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired, labels: PropTypes.array.isRequired,
filterUsers: PropTypes.array.isRequired, filterUsers: PropTypes.array.isRequired,

@ -1,4 +1,9 @@
:global(#app) { :global(#app) {
.cardsCount {
color: #798d99;
font-size: 12px;
}
.action { .action {
flex: 0 0 auto; flex: 0 0 auto;
margin-right: 20px; margin-right: 20px;

@ -16,14 +16,23 @@ import { ReactComponent as PlusMathIcon } from '../../assets/images/plus-math-ic
import styles from './List.module.scss'; import styles from './List.module.scss';
const List = React.memo( const List = React.memo(
({ id, index, name, isPersisted, cardIds, canEdit, onUpdate, onDelete, onCardCreate }) => { // eslint-disable-next-line prettier/prettier
({ id, index, name, isPersisted, isCollapsed, cardIds, isFiltered, filteredCardIds, 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();
} }
@ -58,7 +67,7 @@ const List = React.memo(
if (isAddCardOpened) { if (isAddCardOpened) {
listWrapper.current.scrollTop = listWrapper.current.scrollHeight; listWrapper.current.scrollTop = listWrapper.current.scrollHeight;
} }
}, [cardIds, isAddCardOpened]); }, [filteredCardIds, isAddCardOpened]);
const ActionsPopup = usePopup(ActionsStep); const ActionsPopup = usePopup(ActionsStep);
@ -72,7 +81,7 @@ const List = React.memo(
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
<div {...droppableProps} ref={innerRef}> <div {...droppableProps} ref={innerRef}>
<div className={styles.cards}> <div className={styles.cards}>
{cardIds.map((cardId, cardIndex) => ( {filteredCardIds.map((cardId, cardIndex) => (
<CardContainer key={cardId} id={cardId} index={cardIndex} /> <CardContainer key={cardId} id={cardId} index={cardIndex} />
))} ))}
{placeholder} {placeholder}
@ -89,6 +98,55 @@ const List = React.memo(
</Droppable> </Droppable>
); );
const cardsCountText = () => {
return (
[
isFiltered
? `${filteredCardIds.length} ${t('common.of')} ${cardIds.length} `
: `${cardIds.length} `,
] + [cardIds.length !== 1 ? t('common.cards') : t('common.card')]
);
};
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" />
</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 }) => (
@ -99,15 +157,30 @@ const List = React.memo(
className={styles.innerWrapper} className={styles.innerWrapper}
> >
<div className={styles.outerWrapper}> <div className={styles.outerWrapper}>
<div
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
className={styles.header}
>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */} jsx-a11y/no-static-element-interactions */}
<div <div
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading className={classNames(
className={classNames(styles.header, canEdit && styles.headerEditable)} styles.headerCollapseButton,
onClick={handleHeaderClick} canEdit && styles.headerEditable,
)}
onClick={handleToggleCollapseClick}
> >
<Icon fitted name="triangle right" />
</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
@ -120,6 +193,7 @@ const List = React.memo(
</Button> </Button>
</ActionsPopup> </ActionsPopup>
)} )}
<div className={styles.headerCardsCount}>{cardsCountText()}</div>
</div> </div>
<div <div
ref={listWrapper} ref={listWrapper}
@ -139,7 +213,7 @@ const List = React.memo(
> >
<PlusMathIcon className={styles.addCardButtonIcon} /> <PlusMathIcon className={styles.addCardButtonIcon} />
<span className={styles.addCardButtonText}> <span className={styles.addCardButtonText}>
{cardIds.length > 0 ? t('action.addAnotherCard') : t('action.addCard')} {filteredCardIds.length > 0 ? t('action.addAnotherCard') : t('action.addCard')}
</span> </span>
</button> </button>
)} )}
@ -155,8 +229,11 @@ 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,
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
isFiltered: PropTypes.bool.isRequired,
filteredCardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,

@ -74,7 +74,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 {
@ -82,6 +82,15 @@
} }
} }
.headerCollapsed {
outline: none;
height: calc(100vh - 182px);
&:hover .target {
opacity: 1;
}
}
.headerEditable { .headerEditable {
cursor: pointer; cursor: pointer;
} }
@ -109,12 +118,53 @@
color: #17394d; color: #17394d;
font-weight: bold; font-weight: bold;
line-height: 20px; line-height: 20px;
max-height: 256px; max-height: 82px;
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;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
}
.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 {
@ -122,6 +172,11 @@
width: 272px; width: 272px;
} }
.innerWrapperCollapsed {
margin-right: 8px;
width: 30px;
}
.outerWrapper { .outerWrapper {
background: #dfe3e6; background: #dfe3e6;
border-radius: 3px; border-radius: 3px;

@ -7,6 +7,9 @@ import { BoardMembershipRoles } from '../constants/Enums';
import BoardActions from '../components/BoardActions'; import BoardActions from '../components/BoardActions';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const listIds = selectors.selectListIdsForCurrentBoard(state);
const listCardsCount = listIds.map((list) => selectors.selectCardIdsByListId(state, list).length);
const cardCount = listCardsCount.reduce((sum, count) => sum + count, 0);
const allUsers = selectors.selectUsers(state); const allUsers = selectors.selectUsers(state);
const isCurrentUserManager = selectors.selectIsCurrentUserManagerForCurrentProject(state); const isCurrentUserManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
const memberships = selectors.selectMembershipsForCurrentBoard(state); const memberships = selectors.selectMembershipsForCurrentBoard(state);
@ -19,6 +22,7 @@ const mapStateToProps = (state) => {
!!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR; !!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR;
return { return {
cardCount,
memberships, memberships,
labels, labels,
filterUsers, filterUsers,

@ -9,10 +9,14 @@ import List from '../components/List';
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const selectListById = selectors.makeSelectListById(); const selectListById = selectors.makeSelectListById();
const selectCardIdsByListId = selectors.makeSelectCardIdsByListId(); const selectCardIdsByListId = selectors.makeSelectCardIdsByListId();
const selectIsFilteredByListId = selectors.makeSelectIsFilteredByListId();
const selectFilteredCardIdsByListId = selectors.makeSelectFilteredCardIdsByListId();
return (state, { id, index }) => { return (state, { id, index }) => {
const { name, isPersisted } = selectListById(state, id); const { name, isPersisted, isCollapsed } = selectListById(state, id);
const cardIds = selectCardIdsByListId(state, id); const cardIds = selectCardIdsByListId(state, id);
const isFiltered = selectIsFilteredByListId(state, id);
const filteredCardIds = selectFilteredCardIdsByListId(state, id);
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const isCurrentUserEditor = const isCurrentUserEditor =
@ -22,8 +26,11 @@ const makeMapStateToProps = () => {
id, id,
index, index,
name, name,
isCollapsed,
isPersisted, isPersisted,
cardIds, cardIds,
isFiltered,
filteredCardIds,
canEdit: isCurrentUserEditor, canEdit: isCurrentUserEditor,
}; };
}; };

@ -162,6 +162,9 @@ export default {
version: 'Version', version: 'Version',
viewer: 'Viewer', viewer: 'Viewer',
writeComment: 'Write a comment...', writeComment: 'Write a comment...',
card: 'card',
cards: 'cards',
of: 'of',
}, },
action: { action: {

@ -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',
@ -84,6 +85,16 @@ export default class extends BaseModel {
return this.cards.orderBy('position'); return this.cards.orderBy('position');
} }
getOrderedCardsModelArray() {
return this.getOrderedCardsQuerySet().toModelArray();
}
getIsFiltered() {
const filterUserIds = this.board.filterUsers.toRefArray().map((user) => user.id);
const filterLabelIds = this.board.filterLabels.toRefArray().map((label) => label.id);
return filterUserIds.length > 0 || filterLabelIds.length > 0;
}
getFilteredOrderedCardsModelArray() { getFilteredOrderedCardsModelArray() {
let cardModels = this.getOrderedCardsQuerySet().toModelArray(); let cardModels = this.getOrderedCardsQuerySet().toModelArray();
@ -93,7 +104,6 @@ export default class extends BaseModel {
if (filterUserIds.length > 0) { if (filterUserIds.length > 0) {
cardModels = cardModels.filter((cardModel) => { cardModels = cardModels.filter((cardModel) => {
const users = cardModel.users.toRefArray(); const users = cardModel.users.toRefArray();
return users.some((user) => filterUserIds.includes(user.id)); return users.some((user) => filterUserIds.includes(user.id));
}); });
} }
@ -101,7 +111,6 @@ export default class extends BaseModel {
if (filterLabelIds.length > 0) { if (filterLabelIds.length > 0) {
cardModels = cardModels.filter((cardModel) => { cardModels = cardModels.filter((cardModel) => {
const labels = cardModel.labels.toRefArray(); const labels = cardModel.labels.toRefArray();
return labels.some((label) => filterLabelIds.includes(label.id)); return labels.some((label) => filterLabelIds.includes(label.id));
}); });
} }

@ -91,6 +91,7 @@ export const selectNextCardPosition = createSelector(
return listModel; return listModel;
} }
// eslint-disable-next-line prettier/prettier
return nextPosition(listModel.getFilteredOrderedCardsModelArray(), index, excludedId); return nextPosition(listModel.getFilteredOrderedCardsModelArray(), index, excludedId);
}, },
); );

@ -34,15 +34,53 @@ export const makeSelectCardIdsByListId = () =>
return listModel; return listModel;
} }
return listModel.getFilteredOrderedCardsModelArray().map((cardModel) => cardModel.id); return listModel.getOrderedCardsModelArray().map((cardModel) => cardModel.id);
}, },
); );
export const selectCardIdsByListId = makeSelectCardIdsByListId(); export const selectCardIdsByListId = makeSelectCardIdsByListId();
export const makeSelectIsFilteredByListId = () =>
createSelector(
orm,
(_, id) => id,
({ List }, id) => {
const listModel = List.withId(id);
if (!listModel) {
return listModel;
}
return listModel.getIsFiltered();
},
);
export const selectIsFilteredByListId = makeSelectIsFilteredByListId();
export const makeSelectFilteredCardIdsByListId = () =>
createSelector(
orm,
(_, id) => id,
({ List }, id) => {
const listModel = List.withId(id);
if (!listModel) {
return listModel;
}
return listModel.getFilteredOrderedCardsModelArray().map((cardModel) => cardModel.id);
},
);
export const selectFilteredCardIdsByListId = makeSelectFilteredCardIdsByListId();
export default { export default {
makeSelectListById, makeSelectListById,
selectListById, selectListById,
makeSelectCardIdsByListId, makeSelectCardIdsByListId,
selectCardIdsByListId, selectCardIdsByListId,
makeSelectIsFilteredByListId,
selectIsFilteredByListId,
makeSelectFilteredCardIdsByListId,
selectFilteredCardIdsByListId,
}; };

@ -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…
Cancel
Save