Skip to content

Commit

Permalink
feat: Filter cards by keyword with advanced capabilities (plankanban#713
Browse files Browse the repository at this point in the history
  • Loading branch information
emmguyot authored and robertocjunior committed May 14, 2024
1 parent 6748d16 commit bd893db
Show file tree
Hide file tree
Showing 19 changed files with 267 additions and 1 deletion.
9 changes: 9 additions & 0 deletions client/src/actions/cards.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ const handleCardDelete = (card) => ({
},
});

const filterText = (boardId, text) => ({
type: ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD,
payload: {
boardId,
text,
},
});

export default {
createCard,
handleCardCreate,
Expand All @@ -129,4 +137,5 @@ export default {
duplicateCard,
deleteCard,
handleCardDelete,
filterText,
};
6 changes: 6 additions & 0 deletions client/src/components/BoardActions/BoardActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const BoardActions = React.memo(
labels,
filterUsers,
filterLabels,
filterText,
allUsers,
canEdit,
canEditMemberships,
Expand All @@ -27,6 +28,7 @@ const BoardActions = React.memo(
onLabelUpdate,
onLabelMove,
onLabelDelete,
onTextFilterUpdate,
}) => {
return (
<div className={styles.wrapper}>
Expand All @@ -46,6 +48,7 @@ const BoardActions = React.memo(
<Filters
users={filterUsers}
labels={filterLabels}
filterText={filterText}
allBoardMemberships={memberships}
allLabels={labels}
canEdit={canEdit}
Expand All @@ -57,6 +60,7 @@ const BoardActions = React.memo(
onLabelUpdate={onLabelUpdate}
onLabelMove={onLabelMove}
onLabelDelete={onLabelDelete}
onTextFilterUpdate={onTextFilterUpdate}
/>
</div>
</div>
Expand All @@ -71,6 +75,7 @@ BoardActions.propTypes = {
labels: PropTypes.array.isRequired,
filterUsers: PropTypes.array.isRequired,
filterLabels: PropTypes.array.isRequired,
filterText: PropTypes.string.isRequired,
allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired,
Expand All @@ -86,6 +91,7 @@ BoardActions.propTypes = {
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
onTextFilterUpdate: PropTypes.func.isRequired,
};

export default BoardActions;
66 changes: 65 additions & 1 deletion client/src/components/BoardActions/Filters.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React, { useCallback } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup';
import { Input } from '../../lib/custom-ui';

import User from '../User';
import Label from '../Label';
Expand All @@ -14,6 +17,7 @@ const Filters = React.memo(
({
users,
labels,
filterText,
allBoardMemberships,
allLabels,
canEdit,
Expand All @@ -25,8 +29,17 @@ const Filters = React.memo(
onLabelUpdate,
onLabelMove,
onLabelDelete,
onTextFilterUpdate,
}) => {
const [t] = useTranslation();
const [isSearchFocused, setIsSearchFocused] = useState(false);

const searchFieldRef = useRef(null);

const cancelSearch = useCallback(() => {
onTextFilterUpdate('');
searchFieldRef.current.blur();
}, [onTextFilterUpdate]);

const handleRemoveUserClick = useCallback(
(id) => {
Expand All @@ -42,9 +55,39 @@ const Filters = React.memo(
[onLabelRemove],
);

const handleSearchChange = useCallback(
(_, { value }) => {
onTextFilterUpdate(value);
},
[onTextFilterUpdate],
);

const handleSearchFocus = useCallback(() => {
setIsSearchFocused(true);
}, []);

const handleSearchKeyDown = useCallback(
(event) => {
if (event.key === 'Escape') {
cancelSearch();
}
},
[cancelSearch],
);

const handleSearchBlur = useCallback(() => {
setIsSearchFocused(false);
}, []);

const handleCancelSearchClick = useCallback(() => {
cancelSearch();
}, [cancelSearch]);

const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
const LabelsPopup = usePopup(LabelsStep);

const isSearchActive = filterText || isSearchFocused;

return (
<>
<span className={styles.filter}>
Expand Down Expand Up @@ -100,6 +143,25 @@ const Filters = React.memo(
</span>
))}
</span>
<span className={styles.filter}>
<Input
ref={searchFieldRef}
value={filterText}
placeholder={t('common.searchCards')}
icon={
isSearchActive ? (
<Icon link name="cancel" onClick={handleCancelSearchClick} />
) : (
'search'
)
}
className={classNames(styles.search, !isSearchActive && styles.searchInactive)}
onFocus={handleSearchFocus}
onKeyDown={handleSearchKeyDown}
onChange={handleSearchChange}
onBlur={handleSearchBlur}
/>
</span>
</>
);
},
Expand All @@ -109,6 +171,7 @@ Filters.propTypes = {
/* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
filterText: PropTypes.string.isRequired,
allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
Expand All @@ -121,6 +184,7 @@ Filters.propTypes = {
onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired,
onTextFilterUpdate: PropTypes.func.isRequired,
};

export default Filters;
28 changes: 28 additions & 0 deletions client/src/components/BoardActions/Filters.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,32 @@
line-height: 20px;
padding: 2px 12px;
}

.search {
height: 30px;
margin: 0 12px;
transition: width 0.2s ease;
width: 280px;

input {
font-size: 13px;
}
}

.searchInactive {
color: #fff;
height: 24px;
width: 220px;

input {
background: rgba(0, 0, 0, 0.24);
border: none;
color: #fff !important;
font-size: 12px;

&::placeholder {
color: #fff;
}
}
}
}
1 change: 1 addition & 0 deletions client/src/constants/ActionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export default {
CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS',
CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE',
CARD_DELETE_HANDLE: 'CARD_DELETE_HANDLE',
TEXT_FILTER_IN_CURRENT_BOARD: 'TEXT_FILTER_IN_CURRENT_BOARD',

/* Tasks */

Expand Down
1 change: 1 addition & 0 deletions client/src/constants/EntryActionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export default {
CARD_DELETE: `${PREFIX}/CARD_DELETE`,
CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`,
CARD_DELETE_HANDLE: `${PREFIX}/CARD_DELETE_HANDLE`,
TEXT_FILTER_IN_CURRENT_BOARD: `${PREFIX}/FILTER_TEXT_HANDLE`,

/* Tasks */

Expand Down
3 changes: 3 additions & 0 deletions client/src/containers/BoardActionsContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const mapStateToProps = (state) => {
const labels = selectors.selectLabelsForCurrentBoard(state);
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
const filterText = selectors.selectFilterTextForCurrentBoard(state);
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);

const isCurrentUserEditor =
Expand All @@ -23,6 +24,7 @@ const mapStateToProps = (state) => {
labels,
filterUsers,
filterLabels,
filterText,
allUsers,
canEdit: isCurrentUserEditor,
canEditMemberships: isCurrentUserManager,
Expand All @@ -43,6 +45,7 @@ const mapDispatchToProps = (dispatch) =>
onLabelUpdate: entryActions.updateLabel,
onLabelMove: entryActions.moveLabel,
onLabelDelete: entryActions.deleteLabel,
onTextFilterUpdate: entryActions.filterText,
},
dispatch,
);
Expand Down
8 changes: 8 additions & 0 deletions client/src/entry-actions/cards.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ const handleCardDelete = (card) => ({
},
});

const filterText = (text) => ({
type: EntryActionTypes.TEXT_FILTER_IN_CURRENT_BOARD,
payload: {
text,
},
});

export default {
createCard,
handleCardCreate,
Expand All @@ -120,4 +127,5 @@ export default {
deleteCard,
deleteCurrentCard,
handleCardDelete,
filterText,
};
2 changes: 2 additions & 0 deletions client/src/lib/custom-ui/components/Input/Input.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export default class Input extends SemanticUIInput {
static Mask = InputMask;

focus = (options) => this.inputRef.current.focus(options);

blur = () => this.inputRef.current.blur();
}
1 change: 1 addition & 0 deletions client/src/locales/en/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export default {
searchLabels: 'Search labels...',
searchMembers: 'Search members...',
searchUsers: 'Search users...',
searchCards: 'Search cards...',
seconds: 'Seconds',
selectBoard: 'Select board',
selectList: 'Select list',
Expand Down
4 changes: 4 additions & 0 deletions client/src/locales/fr/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export default {
project: 'Projet',
projectNotFound_title: 'Projet introuvable',
removeMember_title: 'Supprimer le membre',
searchLabels: 'Chercher une étiquette...',
searchMembers: 'Chercher un membre...',
searchUsers: 'Chercher un utilisateur...',
searchCards: 'Chercher une carte...',
seconds: 'Secondes',
selectBoard: 'Sélectionner une carte',
selectList: 'Sélectionner une liste',
Expand Down
47 changes: 47 additions & 0 deletions client/src/models/Board.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { attr, fk, many } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';

import User from './User';
import Label from './Label';

export default class extends BaseModel {
static modelName = 'Board';

Expand All @@ -25,6 +28,9 @@ export default class extends BaseModel {
}),
filterUsers: many('User', 'filterBoards'),
filterLabels: many('Label', 'filterBoards'),
filterText: attr({
getDefault: () => '',
}),
};

static reducer({ type, payload }, Board) {
Expand Down Expand Up @@ -167,6 +173,47 @@ export default class extends BaseModel {
Board.withId(payload.boardId).filterLabels.remove(payload.id);

break;
case ActionTypes.TEXT_FILTER_IN_CURRENT_BOARD: {
const board = Board.withId(payload.boardId);
let filterText = payload.text;
const posSpace = filterText.indexOf(' ');

// Shortcut to user filters
const posAT = filterText.indexOf('@');
if (posAT >= 0 && posSpace > 0 && posAT < posSpace) {
const userId = User.findUsersFromText(
filterText.substring(posAT + 1, posSpace),
board.memberships.toModelArray().map((membership) => membership.user),
);
if (
userId &&
board.filterUsers.toModelArray().filter((user) => user.id === userId).length === 0
) {
board.filterUsers.add(userId);
filterText = filterText.substring(0, posAT);
}
}

// Shortcut to label filters
const posSharp = filterText.indexOf('#');
if (posSharp >= 0 && posSpace > 0 && posSharp < posSpace) {
const labelId = Label.findLabelsFromText(
filterText.substring(posSharp + 1, posSpace),
board.labels.toModelArray(),
);
if (
labelId &&
board.filterLabels.toModelArray().filter((label) => label.id === labelId).length === 0
) {
board.filterLabels.add(labelId);
filterText = filterText.substring(0, posSharp);
}
}

board.update({ filterText });

break;
}
default:
}
}
Expand Down
5 changes: 5 additions & 0 deletions client/src/models/Card.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export default class extends BaseModel {
position: attr(),
name: attr(),
description: attr(),
creatorUserId: oneToOne({
to: 'User',
as: 'creatorUser',
relatedName: 'ownCards',
}),
dueDate: attr(),
stopwatch: attr(),
isSubscribed: attr({
Expand Down
Loading

0 comments on commit bd893db

Please sign in to comment.