From ab11dc9f1bcfc2fb0477a1aa5ee4f0c8a8c6563b Mon Sep 17 00:00:00 2001 From: Felipe Freitas Date: Fri, 12 Apr 2024 10:54:15 -0300 Subject: [PATCH] feat: Add filter by Keyword --- client/src/actions/boards.js | 9 + .../components/BoardActions/BoardActions.jsx | 6 + .../src/components/BoardActions/Filters.jsx | 112 ++++------ .../BoardActions/Filters.module.scss | 41 ---- .../components/FiltersStep/FiltersStep.jsx | 192 ++++++++++++++++++ .../FiltersStep/FiltersStep.module.scss | 50 +++++ client/src/components/FiltersStep/index.js | 3 + client/src/constants/ActionTypes.js | 1 + client/src/constants/EntryActionTypes.js | 1 + .../src/containers/BoardActionsContainer.js | 3 + client/src/entry-actions/boards.js | 8 + client/src/locales/en/core.js | 5 + client/src/locales/pt/core.js | 5 + client/src/models/Board.js | 9 + client/src/models/List.js | 7 + client/src/sagas/core/services/boards.js | 12 ++ client/src/sagas/core/watchers/boards.js | 4 + client/src/selectors/boards.js | 19 ++ 18 files changed, 375 insertions(+), 112 deletions(-) create mode 100644 client/src/components/FiltersStep/FiltersStep.jsx create mode 100644 client/src/components/FiltersStep/FiltersStep.module.scss create mode 100644 client/src/components/FiltersStep/index.js 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,