feat: Add filter by Keyword

pull/707/head
Felipe Freitas 2 years ago
parent 16499052f7
commit ab11dc9f1b

@ -134,6 +134,14 @@ const handleBoardDelete = (board) => ({
}, },
}); });
const updateKeywordToBoardFilter = (keyword, boardId) => ({
type: ActionTypes.KEYWORD_TO_BOARD_FILTER_UPDATE,
payload: {
keyword,
boardId,
},
});
export default { export default {
createBoard, createBoard,
handleBoardCreate, handleBoardCreate,
@ -142,4 +150,5 @@ export default {
handleBoardUpdate, handleBoardUpdate,
deleteBoard, deleteBoard,
handleBoardDelete, handleBoardDelete,
updateKeywordToBoardFilter,
}; };

@ -11,6 +11,7 @@ const BoardActions = React.memo(
({ ({
memberships, memberships,
labels, labels,
filterKeyword,
filterUsers, filterUsers,
filterLabels, filterLabels,
allUsers, allUsers,
@ -19,6 +20,7 @@ const BoardActions = React.memo(
onMembershipCreate, onMembershipCreate,
onMembershipUpdate, onMembershipUpdate,
onMembershipDelete, onMembershipDelete,
onKeywordToFilterUpdate,
onUserToFilterAdd, onUserToFilterAdd,
onUserFromFilterRemove, onUserFromFilterRemove,
onLabelToFilterAdd, onLabelToFilterAdd,
@ -44,11 +46,13 @@ const BoardActions = React.memo(
</div> </div>
<div className={styles.action}> <div className={styles.action}>
<Filters <Filters
keyword={filterKeyword}
users={filterUsers} users={filterUsers}
labels={filterLabels} labels={filterLabels}
allBoardMemberships={memberships} allBoardMemberships={memberships}
allLabels={labels} allLabels={labels}
canEdit={canEdit} canEdit={canEdit}
onKeywordUpdate={onKeywordToFilterUpdate}
onUserAdd={onUserToFilterAdd} onUserAdd={onUserToFilterAdd}
onUserRemove={onUserFromFilterRemove} onUserRemove={onUserFromFilterRemove}
onLabelAdd={onLabelToFilterAdd} onLabelAdd={onLabelToFilterAdd}
@ -69,6 +73,7 @@ BoardActions.propTypes = {
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
memberships: PropTypes.array.isRequired, memberships: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired, labels: PropTypes.array.isRequired,
filterKeyword: PropTypes.string.isRequired,
filterUsers: PropTypes.array.isRequired, filterUsers: PropTypes.array.isRequired,
filterLabels: PropTypes.array.isRequired, filterLabels: PropTypes.array.isRequired,
allUsers: PropTypes.array.isRequired, allUsers: PropTypes.array.isRequired,
@ -78,6 +83,7 @@ BoardActions.propTypes = {
onMembershipCreate: PropTypes.func.isRequired, onMembershipCreate: PropTypes.func.isRequired,
onMembershipUpdate: PropTypes.func.isRequired, onMembershipUpdate: PropTypes.func.isRequired,
onMembershipDelete: PropTypes.func.isRequired, onMembershipDelete: PropTypes.func.isRequired,
onKeywordToFilterUpdate: PropTypes.func.isRequired,
onUserToFilterAdd: PropTypes.func.isRequired, onUserToFilterAdd: PropTypes.func.isRequired,
onUserFromFilterRemove: PropTypes.func.isRequired, onUserFromFilterRemove: PropTypes.func.isRequired,
onLabelToFilterAdd: PropTypes.func.isRequired, onLabelToFilterAdd: PropTypes.func.isRequired,

@ -1,22 +1,22 @@
import React, { useCallback } from 'react'; import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, ButtonGroup, Icon } from 'semantic-ui-react';
import { usePopup } from '../../lib/popup'; import { usePopup } from '../../lib/popup';
import User from '../User'; import FiltersStep from '../FiltersStep';
import Label from '../Label';
import BoardMembershipsStep from '../BoardMembershipsStep';
import LabelsStep from '../LabelsStep';
import styles from './Filters.module.scss'; import styles from './Filters.module.scss';
const Filters = React.memo( const Filters = React.memo(
({ ({
keyword,
users, users,
labels, labels,
allBoardMemberships, allBoardMemberships,
allLabels, allLabels,
canEdit, canEdit,
onKeywordUpdate,
onUserAdd, onUserAdd,
onUserRemove, onUserRemove,
onLabelAdd, onLabelAdd,
@ -28,91 +28,61 @@ const Filters = React.memo(
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const handleRemoveUserClick = useCallback( const isFiltering = useMemo(
(id) => { () => !!(keyword || users.length || labels.length),
onUserRemove(id); [keyword, users, labels],
},
[onUserRemove],
); );
const handleRemoveLabelClick = useCallback( const handleClickClear = useCallback(() => {
(id) => { onKeywordUpdate('');
onLabelRemove(id); users.forEach((user) => onUserRemove(user.id));
}, labels.forEach((label) => onLabelRemove(label.id));
[onLabelRemove], }, [users, labels, onKeywordUpdate, onUserRemove, onLabelRemove]);
);
const BoardMembershipsPopup = usePopup(BoardMembershipsStep); const FiltersPopup = usePopup(FiltersStep);
const LabelsPopup = usePopup(LabelsStep);
return ( return (
<> <span className={styles.filter}>
<span className={styles.filter}> <ButtonGroup>
<BoardMembershipsPopup <FiltersPopup
items={allBoardMemberships} keyword={keyword}
currentUserIds={users.map((user) => user.id)} users={users}
title="common.filterByMembers" labels={labels}
onUserSelect={onUserAdd} allBoardMemberships={allBoardMemberships}
onUserDeselect={onUserRemove} allLabels={allLabels}
>
<button type="button" className={styles.filterButton}>
<span className={styles.filterTitle}>{`${t('common.members')}:`}</span>
{users.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
</button>
</BoardMembershipsPopup>
{users.map((user) => (
<span key={user.id} className={styles.filterItem}>
<User
name={user.name}
avatarUrl={user.avatarUrl}
size="tiny"
onClick={() => handleRemoveUserClick(user.id)}
/>
</span>
))}
</span>
<span className={styles.filter}>
<LabelsPopup
items={allLabels}
currentIds={labels.map((label) => label.id)}
title="common.filterByLabels"
canEdit={canEdit} canEdit={canEdit}
onSelect={onLabelAdd} onKeywordUpdate={onKeywordUpdate}
onDeselect={onLabelRemove} onUserAdd={onUserAdd}
onCreate={onLabelCreate} onUserRemove={onUserRemove}
onUpdate={onLabelUpdate} onLabelAdd={onLabelAdd}
onMove={onLabelMove} onLabelRemove={onLabelRemove}
onDelete={onLabelDelete} onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate}
onLabelMove={onLabelMove}
onLabelDelete={onLabelDelete}
> >
<button type="button" className={styles.filterButton}> <Button icon labelPosition="left">
<span className={styles.filterTitle}>{`${t('common.labels')}:`}</span> <Icon name="filter" />
{labels.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>} {t('common.filters')}
</button> </Button>
</LabelsPopup> </FiltersPopup>
{labels.map((label) => ( {isFiltering && <Button onClick={handleClickClear}>{t('common.clear')}</Button>}
<span key={label.id} className={styles.filterItem}> </ButtonGroup>
<Label </span>
name={label.name}
color={label.color}
size="small"
onClick={() => handleRemoveLabelClick(label.id)}
/>
</span>
))}
</span>
</>
); );
}, },
); );
Filters.propTypes = { Filters.propTypes = {
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
keyword: PropTypes.string.isRequired,
users: PropTypes.array.isRequired, users: PropTypes.array.isRequired,
labels: PropTypes.array.isRequired, labels: PropTypes.array.isRequired,
allBoardMemberships: PropTypes.array.isRequired, allBoardMemberships: PropTypes.array.isRequired,
allLabels: PropTypes.array.isRequired, allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
onKeywordUpdate: PropTypes.func.isRequired,
onUserAdd: PropTypes.func.isRequired, onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired, onUserRemove: PropTypes.func.isRequired,
onLabelAdd: PropTypes.func.isRequired, onLabelAdd: PropTypes.func.isRequired,

@ -2,45 +2,4 @@
.filter { .filter {
margin-right: 10px; 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;
}
} }

@ -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 (
<BoardMembershipsStep
items={allBoardMemberships}
currentUserIds={users.map((user) => user.id)}
title="common.filterByMembers"
onUserSelect={onUserAdd}
onUserDeselect={onUserRemove}
onBack={handleBack}
/>
);
case StepTypes.LABELS:
return (
<LabelsStep
items={allLabels}
currentIds={labels.map((label) => label.id)}
title="common.filterByLabels"
canEdit={canEdit}
onSelect={onLabelAdd}
onDeselect={onLabelRemove}
onCreate={onLabelCreate}
onUpdate={onLabelUpdate}
onMove={onLabelMove}
onDelete={onLabelDelete}
onBack={handleBack}
/>
);
default:
}
}
return (
<>
<Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content className={styles.container}>
<span className={styles.filter}>
<div className={styles.filterTitle}>{t('common.keyword')}</div>
<Input
fluid
placeholder={t('common.enterKeyword')}
icon="search"
value={keyword}
onChange={(e) => handleKeywordChange(e.target.value)}
/>
</span>
<span className={styles.filter}>
<div className={styles.filterTitle}>{t('common.members')}</div>
{users.slice(0, 5).map((user) => (
<span key={user.id} className={styles.filterItem}>
<User
name={user.name}
avatarUrl={user.avatarUrl}
size="tiny"
onClick={() => handleRemoveUserClick(user.id)}
/>
</span>
))}
<button
type="button"
className={styles.filterButton}
onClick={() => openStep(StepTypes.MEMBERS)}
>
<span className={styles.filterLabel}>
{users.length === 0 ? t('common.all') : <Icon name="plus" />}
</span>
</button>
</span>
<span className={styles.filter}>
<div className={styles.filterTitle}>{t('common.labels')}</div>
{labels.slice(0, 5).map((label) => (
<span key={label.id} className={styles.filterItem}>
<Label
name={label.name}
color={label.color}
size="small"
onClick={() => handleRemoveLabelClick(label.id)}
/>
</span>
))}
<button
type="button"
className={styles.filterButton}
onClick={() => openStep(StepTypes.LABELS)}
>
<span className={styles.filterLabel}>
{labels.length === 0 ? t('common.all') : <Icon name="plus" />}
</span>
</button>
</span>
</Popup.Content>
</>
);
},
);
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;

@ -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;
}
}

@ -0,0 +1,3 @@
import FiltersStep from './FiltersStep';
export default FiltersStep;

@ -121,6 +121,7 @@ export default {
BOARD_DELETE__SUCCESS: 'BOARD_DELETE__SUCCESS', BOARD_DELETE__SUCCESS: 'BOARD_DELETE__SUCCESS',
BOARD_DELETE__FAILURE: 'BOARD_DELETE__FAILURE', BOARD_DELETE__FAILURE: 'BOARD_DELETE__FAILURE',
BOARD_DELETE_HANDLE: 'BOARD_DELETE_HANDLE', BOARD_DELETE_HANDLE: 'BOARD_DELETE_HANDLE',
KEYWORD_TO_BOARD_FILTER_UPDATE: 'KEYWORD_TO_BOARD_FILTER_UPDATE',
/* Board memberships */ /* Board memberships */

@ -83,6 +83,7 @@ export default {
BOARD_MOVE: `${PREFIX}/BOARD_MOVE`, BOARD_MOVE: `${PREFIX}/BOARD_MOVE`,
BOARD_DELETE: `${PREFIX}/BOARD_DELETE`, BOARD_DELETE: `${PREFIX}/BOARD_DELETE`,
BOARD_DELETE_HANDLE: `${PREFIX}/BOARD_DELETE_HANDLE`, BOARD_DELETE_HANDLE: `${PREFIX}/BOARD_DELETE_HANDLE`,
KEYWORD_TO_FILTER_IN_CURRENT_BOARD_UPDATE: `${PREFIX}/KEYWORD_TO_FILTER_IN_CURRENT_BOARD_UPDATE`,
/* Board memberships */ /* Board memberships */

@ -11,6 +11,7 @@ const mapStateToProps = (state) => {
const isCurrentUserManager = selectors.selectIsCurrentUserManagerForCurrentProject(state); const isCurrentUserManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
const memberships = selectors.selectMembershipsForCurrentBoard(state); const memberships = selectors.selectMembershipsForCurrentBoard(state);
const labels = selectors.selectLabelsForCurrentBoard(state); const labels = selectors.selectLabelsForCurrentBoard(state);
const filterKeyword = selectors.selectFilterKeywordForCurrentBoard(state);
const filterUsers = selectors.selectFilterUsersForCurrentBoard(state); const filterUsers = selectors.selectFilterUsersForCurrentBoard(state);
const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state); const filterLabels = selectors.selectFilterLabelsForCurrentBoard(state);
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
@ -21,6 +22,7 @@ const mapStateToProps = (state) => {
return { return {
memberships, memberships,
labels, labels,
filterKeyword,
filterUsers, filterUsers,
filterLabels, filterLabels,
allUsers, allUsers,
@ -35,6 +37,7 @@ const mapDispatchToProps = (dispatch) =>
onMembershipCreate: entryActions.createMembershipInCurrentBoard, onMembershipCreate: entryActions.createMembershipInCurrentBoard,
onMembershipUpdate: entryActions.updateBoardMembership, onMembershipUpdate: entryActions.updateBoardMembership,
onMembershipDelete: entryActions.deleteBoardMembership, onMembershipDelete: entryActions.deleteBoardMembership,
onKeywordToFilterUpdate: entryActions.updateKeywordToFilterInCurrentBoard,
onUserToFilterAdd: entryActions.addUserToFilterInCurrentBoard, onUserToFilterAdd: entryActions.addUserToFilterInCurrentBoard,
onUserFromFilterRemove: entryActions.removeUserFromFilterInCurrentBoard, onUserFromFilterRemove: entryActions.removeUserFromFilterInCurrentBoard,
onLabelToFilterAdd: entryActions.addLabelToFilterInCurrentBoard, onLabelToFilterAdd: entryActions.addLabelToFilterInCurrentBoard,

@ -59,6 +59,13 @@ const handleBoardDelete = (board) => ({
}, },
}); });
const updateKeywordToFilterInCurrentBoard = (keyword) => ({
type: EntryActionTypes.KEYWORD_TO_FILTER_IN_CURRENT_BOARD_UPDATE,
payload: {
keyword,
},
});
export default { export default {
createBoardInCurrentProject, createBoardInCurrentProject,
handleBoardCreate, handleBoardCreate,
@ -68,4 +75,5 @@ export default {
moveBoard, moveBoard,
deleteBoard, deleteBoard,
handleBoardDelete, handleBoardDelete,
updateKeywordToFilterInCurrentBoard,
}; };

@ -50,6 +50,7 @@ export default {
cardActions_title: 'Card Actions', cardActions_title: 'Card Actions',
cardNotFound_title: 'Card Not Found', cardNotFound_title: 'Card Not Found',
cardOrActionAreDeleted: 'Card or action are deleted.', cardOrActionAreDeleted: 'Card or action are deleted.',
clear: 'Clear',
color: 'Color', color: 'Color',
copy_inline: 'copy', copy_inline: 'copy',
createBoard_title: 'Create Board', createBoard_title: 'Create Board',
@ -90,17 +91,21 @@ export default {
enterCardTitle: 'Enter card title... [Ctrl+Enter] to auto-open.', enterCardTitle: 'Enter card title... [Ctrl+Enter] to auto-open.',
enterDescription: 'Enter description...', enterDescription: 'Enter description...',
enterFilename: 'Enter filename', enterFilename: 'Enter filename',
enterKeyword: 'Enter a keyword...',
enterListTitle: 'Enter list title...', enterListTitle: 'Enter list title...',
enterProjectTitle: 'Enter project title', enterProjectTitle: 'Enter project title',
enterTaskDescription: 'Enter task description...', enterTaskDescription: 'Enter task description...',
filterByLabels_title: 'Filter By Labels', filterByLabels_title: 'Filter By Labels',
filterByMembers_title: 'Filter By Members', filterByMembers_title: 'Filter By Members',
filters: 'Filters',
filters_title: 'Filter',
fromComputer_title: 'From Computer', fromComputer_title: 'From Computer',
fromTrello: 'From Trello', fromTrello: 'From Trello',
general: 'General', general: 'General',
hours: 'Hours', hours: 'Hours',
importBoard_title: 'Import Board', importBoard_title: 'Import Board',
invalidCurrentPassword: 'Invalid current password', invalidCurrentPassword: 'Invalid current password',
keyword: 'Keyword',
labels: 'Labels', labels: 'Labels',
language: 'Language', language: 'Language',
leaveBoard_title: 'Leave Board', leaveBoard_title: 'Leave Board',

@ -54,6 +54,7 @@ export default {
cardActions_title: 'Ações do Cartão', cardActions_title: 'Ações do Cartão',
cardNotFound_title: 'Cartão não encontrado', cardNotFound_title: 'Cartão não encontrado',
cardOrActionAreDeleted: 'Cartão ou ação foram excluídos.', cardOrActionAreDeleted: 'Cartão ou ação foram excluídos.',
clear: 'Limpar',
color: 'Cor', color: 'Cor',
createBoard_title: 'Criar Quadro', createBoard_title: 'Criar Quadro',
createLabel_title: 'Criar Rótulo', createLabel_title: 'Criar Rótulo',
@ -93,17 +94,21 @@ export default {
enterCardTitle: 'Digite o título do cartão... [Ctrl+Enter] para abrir automaticamente.', enterCardTitle: 'Digite o título do cartão... [Ctrl+Enter] para abrir automaticamente.',
enterDescription: 'Digite a descrição...', enterDescription: 'Digite a descrição...',
enterFilename: 'Digite o nome do arquivo', enterFilename: 'Digite o nome do arquivo',
enterKeyword: 'Informe uma palavra-chave...',
enterListTitle: 'Digite o título da lista...', enterListTitle: 'Digite o título da lista...',
enterProjectTitle: 'Digite o título do projeto', enterProjectTitle: 'Digite o título do projeto',
enterTaskDescription: 'Digite a descrição da tarefa...', enterTaskDescription: 'Digite a descrição da tarefa...',
filterByLabels_title: 'Filtrar por Rótulos', filterByLabels_title: 'Filtrar por Rótulos',
filterByMembers_title: 'Filtrar por Membros', filterByMembers_title: 'Filtrar por Membros',
filters: 'Filtros',
filters_title: 'Filtro',
fromComputer_title: 'Do Computador', fromComputer_title: 'Do Computador',
fromTrello: 'Do Trello', fromTrello: 'Do Trello',
general: 'Geral', general: 'Geral',
hours: 'Horas', hours: 'Horas',
importBoard_title: 'Importar Quadro', importBoard_title: 'Importar Quadro',
invalidCurrentPassword: 'Senha atual inválida', invalidCurrentPassword: 'Senha atual inválida',
keyword: 'Palavra-chave',
labels: 'Rótulos', labels: 'Rótulos',
language: 'Idioma', language: 'Idioma',
leaveBoard_title: 'Sair do Quadro', leaveBoard_title: 'Sair do Quadro',

@ -23,6 +23,9 @@ export default class extends BaseModel {
through: 'BoardMembership', through: 'BoardMembership',
relatedName: 'boards', relatedName: 'boards',
}), }),
filterKeyword: attr({
getDefault: () => '',
}),
filterUsers: many('User', 'filterBoards'), filterUsers: many('User', 'filterBoards'),
filterLabels: many('Label', 'filterBoards'), filterLabels: many('Label', 'filterBoards'),
}; };
@ -166,6 +169,12 @@ export default class extends BaseModel {
case ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE: case ActionTypes.LABEL_FROM_BOARD_FILTER_REMOVE:
Board.withId(payload.boardId).filterLabels.remove(payload.id); 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; break;
default: default:
} }

@ -87,9 +87,16 @@ export default class extends BaseModel {
getFilteredOrderedCardsModelArray() { getFilteredOrderedCardsModelArray() {
let cardModels = this.getOrderedCardsQuerySet().toModelArray(); let cardModels = this.getOrderedCardsQuerySet().toModelArray();
const { filterKeyword } = this.board;
const filterUserIds = this.board.filterUsers.toRefArray().map((user) => user.id); const filterUserIds = this.board.filterUsers.toRefArray().map((user) => user.id);
const filterLabelIds = this.board.filterLabels.toRefArray().map((label) => label.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) { if (filterUserIds.length > 0) {
cardModels = cardModels.filter((cardModel) => { cardModels = cardModels.filter((cardModel) => {
const users = cardModel.users.toRefArray(); const users = cardModel.users.toRefArray();

@ -176,6 +176,16 @@ export function* handleBoardDelete(board) {
yield put(actions.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 { export default {
createBoard, createBoard,
createBoardInCurrentProject, createBoardInCurrentProject,
@ -186,4 +196,6 @@ export default {
moveBoard, moveBoard,
deleteBoard, deleteBoard,
handleBoardDelete, handleBoardDelete,
updateKeywordToBoardFilter,
updateKeywordToFilterInCurrentBoard,
}; };

@ -25,5 +25,9 @@ export default function* boardsWatchers() {
takeEvery(EntryActionTypes.BOARD_DELETE_HANDLE, ({ payload: { board } }) => takeEvery(EntryActionTypes.BOARD_DELETE_HANDLE, ({ payload: { board } }) =>
services.handleBoardDelete(board), services.handleBoardDelete(board),
), ),
takeEvery(
EntryActionTypes.KEYWORD_TO_FILTER_IN_CURRENT_BOARD_UPDATE,
({ payload: { keyword } }) => services.updateKeywordToFilterInCurrentBoard(keyword),
),
]); ]);
} }

@ -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( export const selectFilterUsersForCurrentBoard = createSelector(
orm, orm,
(state) => selectPath(state).boardId, (state) => selectPath(state).boardId,
@ -189,6 +207,7 @@ export default {
selectCurrentUserMembershipForCurrentBoard, selectCurrentUserMembershipForCurrentBoard,
selectLabelsForCurrentBoard, selectLabelsForCurrentBoard,
selectListIdsForCurrentBoard, selectListIdsForCurrentBoard,
selectFilterKeywordForCurrentBoard,
selectFilterUsersForCurrentBoard, selectFilterUsersForCurrentBoard,
selectFilterLabelsForCurrentBoard, selectFilterLabelsForCurrentBoard,
selectIsBoardWithIdExists, selectIsBoardWithIdExists,

Loading…
Cancel
Save