diff --git a/client/src/actions/boards.js b/client/src/actions/boards.js
index 484ae23..4b99f1c 100644
--- a/client/src/actions/boards.js
+++ b/client/src/actions/boards.js
@@ -134,6 +134,14 @@ const handleBoardDelete = (board) => ({
},
});
+const updateKeywordToBoardFilter = (keyword, boardId) => ({
+ type: ActionTypes.KEYWORD_TO_BOARD_FILTER_UPDATE,
+ payload: {
+ keyword,
+ boardId,
+ },
+});
+
export default {
createBoard,
handleBoardCreate,
@@ -142,4 +150,5 @@ export default {
handleBoardUpdate,
deleteBoard,
handleBoardDelete,
+ updateKeywordToBoardFilter,
};
diff --git a/client/src/components/BoardActions/BoardActions.jsx b/client/src/components/BoardActions/BoardActions.jsx
index 0f1d552..fea9404 100644
--- a/client/src/components/BoardActions/BoardActions.jsx
+++ b/client/src/components/BoardActions/BoardActions.jsx
@@ -11,6 +11,7 @@ const BoardActions = React.memo(
({
memberships,
labels,
+ filterKeyword,
filterUsers,
filterLabels,
allUsers,
@@ -19,6 +20,7 @@ const BoardActions = React.memo(
onMembershipCreate,
onMembershipUpdate,
onMembershipDelete,
+ onKeywordToFilterUpdate,
onUserToFilterAdd,
onUserFromFilterRemove,
onLabelToFilterAdd,
@@ -44,11 +46,13 @@ const BoardActions = React.memo(
{
const [t] = useTranslation();
- const handleRemoveUserClick = useCallback(
- (id) => {
- onUserRemove(id);
- },
- [onUserRemove],
+ const isFiltering = useMemo(
+ () => !!(keyword || users.length || labels.length),
+ [keyword, users, labels],
);
- const handleRemoveLabelClick = useCallback(
- (id) => {
- onLabelRemove(id);
- },
- [onLabelRemove],
- );
+ const handleClickClear = useCallback(() => {
+ onKeywordUpdate('');
+ users.forEach((user) => onUserRemove(user.id));
+ labels.forEach((label) => onLabelRemove(label.id));
+ }, [users, labels, onKeywordUpdate, onUserRemove, onLabelRemove]);
- const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
- const LabelsPopup = usePopup(LabelsStep);
+ const FiltersPopup = usePopup(FiltersStep);
return (
- <>
-
- user.id)}
- title="common.filterByMembers"
- onUserSelect={onUserAdd}
- onUserDeselect={onUserRemove}
- >
-
-
- {users.map((user) => (
-
- handleRemoveUserClick(user.id)}
- />
-
- ))}
-
-
- label.id)}
- title="common.filterByLabels"
+
+
+
-
-
- {labels.map((label) => (
-
-
- ))}
-
- >
+
+
+ {isFiltering && }
+
+
);
},
);
Filters.propTypes = {
/* eslint-disable react/forbid-prop-types */
+ keyword: PropTypes.string.isRequired,
users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired,
allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired,
+ onKeywordUpdate: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
onLabelAdd: PropTypes.func.isRequired,
diff --git a/client/src/components/BoardActions/Filters.module.scss b/client/src/components/BoardActions/Filters.module.scss
index 1af6dd2..71d3884 100644
--- a/client/src/components/BoardActions/Filters.module.scss
+++ b/client/src/components/BoardActions/Filters.module.scss
@@ -2,45 +2,4 @@
.filter {
margin-right: 10px;
}
-
- .filterButton {
- background: transparent;
- border: none;
- cursor: pointer;
- display: inline-block;
- outline: none;
- padding: 0;
- }
-
- .filterItem {
- display: inline-block;
- font-size: 0;
- line-height: 0;
- margin-right: 4px;
- max-width: 190px;
- vertical-align: top;
- }
-
- .filterLabel {
- background: rgba(0, 0, 0, 0.24);
- border-radius: 3px;
- color: #fff;
- display: inline-block;
- font-size: 12px;
- line-height: 20px;
- padding: 2px 8px;
-
- &:hover {
- background: rgba(0, 0, 0, 0.32);
- }
- }
-
- .filterTitle {
- border-radius: 3px;
- color: #fff;
- display: inline-block;
- font-size: 12px;
- line-height: 20px;
- padding: 2px 12px;
- }
}
diff --git a/client/src/components/FiltersStep/FiltersStep.jsx b/client/src/components/FiltersStep/FiltersStep.jsx
new file mode 100644
index 0000000..b89e956
--- /dev/null
+++ b/client/src/components/FiltersStep/FiltersStep.jsx
@@ -0,0 +1,192 @@
+import React, { useCallback } from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+import { Icon } from 'semantic-ui-react';
+import { Input, Popup } from '../../lib/custom-ui';
+
+import { useSteps } from '../../hooks';
+import User from '../User';
+import Label from '../Label';
+import BoardMembershipsStep from '../BoardMembershipsStep';
+import LabelsStep from '../LabelsStep';
+
+import styles from './FiltersStep.module.scss';
+
+const StepTypes = {
+ MEMBERS: 'MEMBERS',
+ LABELS: 'LABELS',
+};
+
+const FiltersStep = React.memo(
+ ({
+ keyword,
+ users,
+ labels,
+ allBoardMemberships,
+ allLabels,
+ title,
+ canEdit,
+ onKeywordUpdate,
+ onUserAdd,
+ onUserRemove,
+ onLabelAdd,
+ onLabelRemove,
+ onLabelCreate,
+ onLabelUpdate,
+ onLabelMove,
+ onLabelDelete,
+ onBack,
+ }) => {
+ const [t] = useTranslation();
+ const [step, openStep, handleBack] = useSteps();
+
+ const handleKeywordChange = useCallback(
+ (newValue) => {
+ onKeywordUpdate(newValue);
+ },
+ [onKeywordUpdate],
+ );
+
+ const handleRemoveUserClick = useCallback(
+ (id) => {
+ onUserRemove(id);
+ },
+ [onUserRemove],
+ );
+
+ const handleRemoveLabelClick = useCallback(
+ (id) => {
+ onLabelRemove(id);
+ },
+ [onLabelRemove],
+ );
+
+ if (step) {
+ switch (step.type) {
+ case StepTypes.MEMBERS:
+ return (
+ user.id)}
+ title="common.filterByMembers"
+ onUserSelect={onUserAdd}
+ onUserDeselect={onUserRemove}
+ onBack={handleBack}
+ />
+ );
+ case StepTypes.LABELS:
+ return (
+ label.id)}
+ title="common.filterByLabels"
+ canEdit={canEdit}
+ onSelect={onLabelAdd}
+ onDeselect={onLabelRemove}
+ onCreate={onLabelCreate}
+ onUpdate={onLabelUpdate}
+ onMove={onLabelMove}
+ onDelete={onLabelDelete}
+ onBack={handleBack}
+ />
+ );
+ default:
+ }
+ }
+
+ return (
+ <>
+
+ {t(title, {
+ context: 'title',
+ })}
+
+
+
+ {t('common.keyword')}
+ handleKeywordChange(e.target.value)}
+ />
+
+
+ {t('common.members')}
+ {users.slice(0, 5).map((user) => (
+
+ handleRemoveUserClick(user.id)}
+ />
+
+ ))}
+
+
+
+ {t('common.labels')}
+ {labels.slice(0, 5).map((label) => (
+
+
+ ))}
+
+
+
+ >
+ );
+ },
+);
+
+FiltersStep.propTypes = {
+ /* eslint-disable react/forbid-prop-types */
+ keyword: PropTypes.string.isRequired,
+ users: PropTypes.array.isRequired,
+ labels: PropTypes.array.isRequired,
+ allBoardMemberships: PropTypes.array.isRequired,
+ allLabels: PropTypes.array.isRequired,
+ /* eslint-enable react/forbid-prop-types */
+ title: PropTypes.string,
+ canEdit: PropTypes.bool.isRequired,
+ onKeywordUpdate: PropTypes.func.isRequired,
+ onUserAdd: PropTypes.func.isRequired,
+ onUserRemove: PropTypes.func.isRequired,
+ onLabelAdd: PropTypes.func.isRequired,
+ onLabelRemove: PropTypes.func.isRequired,
+ onLabelCreate: PropTypes.func.isRequired,
+ onLabelUpdate: PropTypes.func.isRequired,
+ onLabelMove: PropTypes.func.isRequired,
+ onLabelDelete: PropTypes.func.isRequired,
+ onBack: PropTypes.func,
+};
+
+FiltersStep.defaultProps = {
+ title: 'common.filters_title',
+ onBack: undefined,
+};
+
+export default FiltersStep;
diff --git a/client/src/components/FiltersStep/FiltersStep.module.scss b/client/src/components/FiltersStep/FiltersStep.module.scss
new file mode 100644
index 0000000..9997765
--- /dev/null
+++ b/client/src/components/FiltersStep/FiltersStep.module.scss
@@ -0,0 +1,50 @@
+:global(#app) {
+ .container {
+ display: flex;
+ flex-direction: column;
+ row-gap: 10px;
+ }
+
+ .filter {
+ margin-right: 10px;
+ }
+
+ .filterButton {
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ display: inline-block;
+ outline: none;
+ padding: 0;
+ }
+
+ .filterItem {
+ display: inline-block;
+ font-size: 0;
+ line-height: 0;
+ margin-right: 4px;
+ max-width: 190px;
+ vertical-align: top;
+ }
+
+ .filterLabel {
+ background: rgba(0, 0, 0, 0.24);
+ border-radius: 3px;
+ color: #fff;
+ display: inline-block;
+ font-size: 12px;
+ line-height: 20px;
+ padding: 2px 8px;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.32);
+ }
+ }
+
+ .filterTitle {
+ color: #444444;
+ font-size: 12px;
+ font-weight: bold;
+ padding-bottom: 6px;
+ }
+}
diff --git a/client/src/components/FiltersStep/index.js b/client/src/components/FiltersStep/index.js
new file mode 100644
index 0000000..6e12389
--- /dev/null
+++ b/client/src/components/FiltersStep/index.js
@@ -0,0 +1,3 @@
+import FiltersStep from './FiltersStep';
+
+export default FiltersStep;
diff --git a/client/src/constants/ActionTypes.js b/client/src/constants/ActionTypes.js
index d5724e1..3debe01 100644
--- a/client/src/constants/ActionTypes.js
+++ b/client/src/constants/ActionTypes.js
@@ -121,6 +121,7 @@ export default {
BOARD_DELETE__SUCCESS: 'BOARD_DELETE__SUCCESS',
BOARD_DELETE__FAILURE: 'BOARD_DELETE__FAILURE',
BOARD_DELETE_HANDLE: 'BOARD_DELETE_HANDLE',
+ KEYWORD_TO_BOARD_FILTER_UPDATE: 'KEYWORD_TO_BOARD_FILTER_UPDATE',
/* Board memberships */
diff --git a/client/src/constants/EntryActionTypes.js b/client/src/constants/EntryActionTypes.js
index 9cd0694..ad404a8 100755
--- a/client/src/constants/EntryActionTypes.js
+++ b/client/src/constants/EntryActionTypes.js
@@ -83,6 +83,7 @@ export default {
BOARD_MOVE: `${PREFIX}/BOARD_MOVE`,
BOARD_DELETE: `${PREFIX}/BOARD_DELETE`,
BOARD_DELETE_HANDLE: `${PREFIX}/BOARD_DELETE_HANDLE`,
+ KEYWORD_TO_FILTER_IN_CURRENT_BOARD_UPDATE: `${PREFIX}/KEYWORD_TO_FILTER_IN_CURRENT_BOARD_UPDATE`,
/* Board memberships */
diff --git a/client/src/containers/BoardActionsContainer.js b/client/src/containers/BoardActionsContainer.js
index 40170cb..6723f26 100644
--- a/client/src/containers/BoardActionsContainer.js
+++ b/client/src/containers/BoardActionsContainer.js
@@ -11,6 +11,7 @@ const mapStateToProps = (state) => {
const isCurrentUserManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
const memberships = selectors.selectMembershipsForCurrentBoard(state);
const labels = selectors.selectLabelsForCurrentBoard(state);
+ const filterKeyword = selectors.selectFilterKeywordForCurrentBoard(state);
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
@@ -21,6 +22,7 @@ const mapStateToProps = (state) => {
return {
memberships,
labels,
+ filterKeyword,
filterUsers,
filterLabels,
allUsers,
@@ -35,6 +37,7 @@ const mapDispatchToProps = (dispatch) =>
onMembershipCreate: entryActions.createMembershipInCurrentBoard,
onMembershipUpdate: entryActions.updateBoardMembership,
onMembershipDelete: entryActions.deleteBoardMembership,
+ onKeywordToFilterUpdate: entryActions.updateKeywordToFilterInCurrentBoard,
onUserToFilterAdd: entryActions.addUserToFilterInCurrentBoard,
onUserFromFilterRemove: entryActions.removeUserFromFilterInCurrentBoard,
onLabelToFilterAdd: entryActions.addLabelToFilterInCurrentBoard,
diff --git a/client/src/entry-actions/boards.js b/client/src/entry-actions/boards.js
index 092fddf..e74ef78 100755
--- a/client/src/entry-actions/boards.js
+++ b/client/src/entry-actions/boards.js
@@ -59,6 +59,13 @@ const handleBoardDelete = (board) => ({
},
});
+const updateKeywordToFilterInCurrentBoard = (keyword) => ({
+ type: EntryActionTypes.KEYWORD_TO_FILTER_IN_CURRENT_BOARD_UPDATE,
+ payload: {
+ keyword,
+ },
+});
+
export default {
createBoardInCurrentProject,
handleBoardCreate,
@@ -68,4 +75,5 @@ export default {
moveBoard,
deleteBoard,
handleBoardDelete,
+ updateKeywordToFilterInCurrentBoard,
};
diff --git a/client/src/locales/en/core.js b/client/src/locales/en/core.js
index 41e4494..ddc9169 100644
--- a/client/src/locales/en/core.js
+++ b/client/src/locales/en/core.js
@@ -50,6 +50,7 @@ export default {
cardActions_title: 'Card Actions',
cardNotFound_title: 'Card Not Found',
cardOrActionAreDeleted: 'Card or action are deleted.',
+ clear: 'Clear',
color: 'Color',
copy_inline: 'copy',
createBoard_title: 'Create Board',
@@ -90,17 +91,21 @@ export default {
enterCardTitle: 'Enter card title... [Ctrl+Enter] to auto-open.',
enterDescription: 'Enter description...',
enterFilename: 'Enter filename',
+ enterKeyword: 'Enter a keyword...',
enterListTitle: 'Enter list title...',
enterProjectTitle: 'Enter project title',
enterTaskDescription: 'Enter task description...',
filterByLabels_title: 'Filter By Labels',
filterByMembers_title: 'Filter By Members',
+ filters: 'Filters',
+ filters_title: 'Filter',
fromComputer_title: 'From Computer',
fromTrello: 'From Trello',
general: 'General',
hours: 'Hours',
importBoard_title: 'Import Board',
invalidCurrentPassword: 'Invalid current password',
+ keyword: 'Keyword',
labels: 'Labels',
language: 'Language',
leaveBoard_title: 'Leave Board',
diff --git a/client/src/locales/pt/core.js b/client/src/locales/pt/core.js
index 3d557e8..43d7a54 100644
--- a/client/src/locales/pt/core.js
+++ b/client/src/locales/pt/core.js
@@ -54,6 +54,7 @@ export default {
cardActions_title: 'Ações do Cartão',
cardNotFound_title: 'Cartão não encontrado',
cardOrActionAreDeleted: 'Cartão ou ação foram excluídos.',
+ clear: 'Limpar',
color: 'Cor',
createBoard_title: 'Criar Quadro',
createLabel_title: 'Criar Rótulo',
@@ -93,17 +94,21 @@ export default {
enterCardTitle: 'Digite o título do cartão... [Ctrl+Enter] para abrir automaticamente.',
enterDescription: 'Digite a descrição...',
enterFilename: 'Digite o nome do arquivo',
+ enterKeyword: 'Informe uma palavra-chave...',
enterListTitle: 'Digite o título da lista...',
enterProjectTitle: 'Digite o título do projeto',
enterTaskDescription: 'Digite a descrição da tarefa...',
filterByLabels_title: 'Filtrar por Rótulos',
filterByMembers_title: 'Filtrar por Membros',
+ filters: 'Filtros',
+ filters_title: 'Filtro',
fromComputer_title: 'Do Computador',
fromTrello: 'Do Trello',
general: 'Geral',
hours: 'Horas',
importBoard_title: 'Importar Quadro',
invalidCurrentPassword: 'Senha atual inválida',
+ keyword: 'Palavra-chave',
labels: 'Rótulos',
language: 'Idioma',
leaveBoard_title: 'Sair do Quadro',
diff --git a/client/src/models/Board.js b/client/src/models/Board.js
index 3b23503..6d50c7a 100755
--- a/client/src/models/Board.js
+++ b/client/src/models/Board.js
@@ -23,6 +23,9 @@ export default class extends BaseModel {
through: 'BoardMembership',
relatedName: 'boards',
}),
+ filterKeyword: attr({
+ getDefault: () => '',
+ }),
filterUsers: many('User', 'filterBoards'),
filterLabels: many('Label', 'filterBoards'),
};
@@ -166,6 +169,12 @@ export default class extends BaseModel {
case ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE:
Board.withId(payload.boardId).filterLabels.remove(payload.id);
+ break;
+ case ActionTypes.KEYWORD_TO_BOARD_FILTER_UPDATE:
+ Board.withId(payload.boardId).update({
+ filterKeyword: payload.keyword,
+ });
+
break;
default:
}
diff --git a/client/src/models/List.js b/client/src/models/List.js
index b634a66..c49b44d 100755
--- a/client/src/models/List.js
+++ b/client/src/models/List.js
@@ -87,9 +87,16 @@ export default class extends BaseModel {
getFilteredOrderedCardsModelArray() {
let cardModels = this.getOrderedCardsQuerySet().toModelArray();
+ const { filterKeyword } = this.board;
const filterUserIds = this.board.filterUsers.toRefArray().map((user) => user.id);
const filterLabelIds = this.board.filterLabels.toRefArray().map((label) => label.id);
+ if (filterKeyword) {
+ cardModels = cardModels.filter((cardModel) => {
+ return cardModel.name.toLowerCase().includes(filterKeyword.toLowerCase());
+ });
+ }
+
if (filterUserIds.length > 0) {
cardModels = cardModels.filter((cardModel) => {
const users = cardModel.users.toRefArray();
diff --git a/client/src/sagas/core/services/boards.js b/client/src/sagas/core/services/boards.js
index de8280c..6d55298 100644
--- a/client/src/sagas/core/services/boards.js
+++ b/client/src/sagas/core/services/boards.js
@@ -176,6 +176,16 @@ export function* handleBoardDelete(board) {
yield put(actions.handleBoardDelete(board));
}
+export function* updateKeywordToBoardFilter(keyword, boardId) {
+ yield put(actions.updateKeywordToBoardFilter(keyword, boardId));
+}
+
+export function* updateKeywordToFilterInCurrentBoard(keyword) {
+ const { boardId } = yield select(selectors.selectPath);
+
+ yield call(updateKeywordToBoardFilter, keyword, boardId);
+}
+
export default {
createBoard,
createBoardInCurrentProject,
@@ -186,4 +196,6 @@ export default {
moveBoard,
deleteBoard,
handleBoardDelete,
+ updateKeywordToBoardFilter,
+ updateKeywordToFilterInCurrentBoard,
};
diff --git a/client/src/sagas/core/watchers/boards.js b/client/src/sagas/core/watchers/boards.js
index 1d6f534..313bd0c 100644
--- a/client/src/sagas/core/watchers/boards.js
+++ b/client/src/sagas/core/watchers/boards.js
@@ -25,5 +25,9 @@ export default function* boardsWatchers() {
takeEvery(EntryActionTypes.BOARD_DELETE_HANDLE, ({ payload: { board } }) =>
services.handleBoardDelete(board),
),
+ takeEvery(
+ EntryActionTypes.KEYWORD_TO_FILTER_IN_CURRENT_BOARD_UPDATE,
+ ({ payload: { keyword } }) => services.updateKeywordToFilterInCurrentBoard(keyword),
+ ),
]);
}
diff --git a/client/src/selectors/boards.js b/client/src/selectors/boards.js
index c649787..c54a7a0 100644
--- a/client/src/selectors/boards.js
+++ b/client/src/selectors/boards.js
@@ -139,6 +139,24 @@ export const selectListIdsForCurrentBoard = createSelector(
},
);
+export const selectFilterKeywordForCurrentBoard = createSelector(
+ orm,
+ (state) => selectPath(state).boardId,
+ ({ Board }, id) => {
+ if (!id) {
+ return id;
+ }
+
+ const boardModel = Board.withId(id);
+
+ if (!boardModel) {
+ return boardModel;
+ }
+
+ return boardModel.filterKeyword;
+ },
+);
+
export const selectFilterUsersForCurrentBoard = createSelector(
orm,
(state) => selectPath(state).boardId,
@@ -189,6 +207,7 @@ export default {
selectCurrentUserMembershipForCurrentBoard,
selectLabelsForCurrentBoard,
selectListIdsForCurrentBoard,
+ selectFilterKeywordForCurrentBoard,
selectFilterUsersForCurrentBoard,
selectFilterLabelsForCurrentBoard,
selectIsBoardWithIdExists,