mirror of
https://github.com/plankanban/planka.git
synced 2025-07-19 13:19:44 +02:00
feat: Add search by users, members and labels
This commit is contained in:
parent
45d2eede58
commit
c3eb66913e
10 changed files with 195 additions and 47 deletions
|
@ -1,9 +1,10 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Menu } from 'semantic-ui-react';
|
import { Menu } from 'semantic-ui-react';
|
||||||
import { Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
|
import { useField } from '../../hooks';
|
||||||
import Item from './Item';
|
import Item from './Item';
|
||||||
|
|
||||||
import styles from './BoardMembershipsStep.module.scss';
|
import styles from './BoardMembershipsStep.module.scss';
|
||||||
|
@ -11,6 +12,21 @@ import styles from './BoardMembershipsStep.module.scss';
|
||||||
const BoardMembershipsStep = React.memo(
|
const BoardMembershipsStep = React.memo(
|
||||||
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
|
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
|
const [searchValue, handleSearchFieldChange] = useField('');
|
||||||
|
const search = useMemo(() => searchValue.trim().toLowerCase(), [searchValue]);
|
||||||
|
|
||||||
|
const filteredItems = useMemo(
|
||||||
|
() =>
|
||||||
|
items.filter(
|
||||||
|
({ user }) =>
|
||||||
|
user.email.includes(search) ||
|
||||||
|
user.name.toLowerCase().includes(search) ||
|
||||||
|
(user.username && user.username.includes(search)),
|
||||||
|
),
|
||||||
|
[items, search],
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchField = useRef(null);
|
||||||
|
|
||||||
const handleUserSelect = useCallback(
|
const handleUserSelect = useCallback(
|
||||||
(id) => {
|
(id) => {
|
||||||
|
@ -26,12 +42,25 @@ const BoardMembershipsStep = React.memo(
|
||||||
[onUserDeselect],
|
[onUserDeselect],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
searchField.current.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header>
|
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header>
|
||||||
<Popup.Content>
|
<Popup.Content>
|
||||||
|
<Input
|
||||||
|
fluid
|
||||||
|
ref={searchField}
|
||||||
|
value={searchValue}
|
||||||
|
placeholder={t('common.searchMembers')}
|
||||||
|
icon="search"
|
||||||
|
onChange={handleSearchFieldChange}
|
||||||
|
/>
|
||||||
|
{filteredItems.length > 0 && (
|
||||||
<Menu secondary vertical className={styles.menu}>
|
<Menu secondary vertical className={styles.menu}>
|
||||||
{items.map((item) => (
|
{filteredItems.map((item) => (
|
||||||
<Item
|
<Item
|
||||||
key={item.id}
|
key={item.id}
|
||||||
isPersisted={item.isPersisted}
|
isPersisted={item.isPersisted}
|
||||||
|
@ -42,6 +71,7 @@ const BoardMembershipsStep = React.memo(
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
)}
|
||||||
</Popup.Content>
|
</Popup.Content>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
:global(#app) {
|
:global(#app) {
|
||||||
.menu {
|
.menu {
|
||||||
margin: -7px auto -5px;
|
margin: 8px auto 0;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
.menuItem {
|
.menuItem {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from 'semantic-ui-react';
|
import { Button } from 'semantic-ui-react';
|
||||||
import { Popup } from '../../lib/custom-ui';
|
import { Input, Popup } from '../../lib/custom-ui';
|
||||||
|
|
||||||
import { useSteps } from '../../hooks';
|
import { useField, useSteps } from '../../hooks';
|
||||||
import AddStep from './AddStep';
|
import AddStep from './AddStep';
|
||||||
import EditStep from './EditStep';
|
import EditStep from './EditStep';
|
||||||
import Item from './Item';
|
import Item from './Item';
|
||||||
|
@ -21,6 +21,20 @@ const LabelsStep = React.memo(
|
||||||
({ items, currentIds, title, onSelect, onDeselect, onCreate, onUpdate, onDelete, onBack }) => {
|
({ items, currentIds, title, onSelect, onDeselect, onCreate, onUpdate, onDelete, onBack }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [step, openStep, handleBack] = useSteps();
|
const [step, openStep, handleBack] = useSteps();
|
||||||
|
const [searchValue, handleSearchFieldChange] = useField('');
|
||||||
|
const search = useMemo(() => searchValue.trim().toLowerCase(), [searchValue]);
|
||||||
|
|
||||||
|
const filteredItems = useMemo(
|
||||||
|
() =>
|
||||||
|
items.filter(
|
||||||
|
(label) =>
|
||||||
|
(label.name && label.name.toLowerCase().includes(search)) ||
|
||||||
|
label.color.includes(search),
|
||||||
|
),
|
||||||
|
[items, search],
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchField = useRef(null);
|
||||||
|
|
||||||
const handleAddClick = useCallback(() => {
|
const handleAddClick = useCallback(() => {
|
||||||
openStep(StepTypes.ADD);
|
openStep(StepTypes.ADD);
|
||||||
|
@ -63,6 +77,10 @@ const LabelsStep = React.memo(
|
||||||
[onDelete],
|
[onDelete],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
searchField.current.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (step) {
|
if (step) {
|
||||||
switch (step.type) {
|
switch (step.type) {
|
||||||
case StepTypes.ADD:
|
case StepTypes.ADD:
|
||||||
|
@ -93,7 +111,17 @@ const LabelsStep = React.memo(
|
||||||
<>
|
<>
|
||||||
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header>
|
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header>
|
||||||
<Popup.Content>
|
<Popup.Content>
|
||||||
{items.map((item) => (
|
<Input
|
||||||
|
fluid
|
||||||
|
ref={searchField}
|
||||||
|
value={searchValue}
|
||||||
|
placeholder={t('common.searchLabels')}
|
||||||
|
icon="search"
|
||||||
|
onChange={handleSearchFieldChange}
|
||||||
|
/>
|
||||||
|
{filteredItems.length > 0 && (
|
||||||
|
<div className={styles.items}>
|
||||||
|
{filteredItems.map((item) => (
|
||||||
<Item
|
<Item
|
||||||
key={item.id}
|
key={item.id}
|
||||||
name={item.name}
|
name={item.name}
|
||||||
|
@ -105,6 +133,8 @@ const LabelsStep = React.memo(
|
||||||
onEdit={() => handleEdit(item.id)}
|
onEdit={() => handleEdit(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
fluid
|
fluid
|
||||||
content={t('action.createNewLabel')}
|
content={t('action.createNewLabel')}
|
||||||
|
|
|
@ -14,4 +14,24 @@
|
||||||
background: #e9e9e9;
|
background: #e9e9e9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
margin-top: 8px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,31 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { withPopup } from '../../../lib/popup';
|
import { withPopup } from '../../../lib/popup';
|
||||||
import { Popup } from '../../../lib/custom-ui';
|
import { Input, Popup } from '../../../lib/custom-ui';
|
||||||
|
|
||||||
|
import { useField } from '../../../hooks';
|
||||||
import UserItem from './UserItem';
|
import UserItem from './UserItem';
|
||||||
|
|
||||||
import styles from './AddPopup.module.scss';
|
import styles from './AddPopup.module.scss';
|
||||||
|
|
||||||
const AddStep = React.memo(({ users, currentUserIds, title, onCreate, onClose }) => {
|
const AddStep = React.memo(({ users, currentUserIds, title, onCreate, onClose }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
|
const [searchValue, handleSearchFieldChange] = useField('');
|
||||||
|
const search = useMemo(() => searchValue.trim().toLowerCase(), [searchValue]);
|
||||||
|
|
||||||
|
const filteredUsers = useMemo(
|
||||||
|
() =>
|
||||||
|
users.filter(
|
||||||
|
(user) =>
|
||||||
|
user.email.includes(search) ||
|
||||||
|
user.name.toLowerCase().includes(search) ||
|
||||||
|
(user.username && user.username.includes(search)),
|
||||||
|
),
|
||||||
|
[users, search],
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchField = useRef(null);
|
||||||
|
|
||||||
const handleUserSelect = useCallback(
|
const handleUserSelect = useCallback(
|
||||||
(id) => {
|
(id) => {
|
||||||
|
@ -22,6 +38,10 @@ const AddStep = React.memo(({ users, currentUserIds, title, onCreate, onClose })
|
||||||
[onCreate, onClose],
|
[onCreate, onClose],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
searchField.current.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popup.Header>
|
<Popup.Header>
|
||||||
|
@ -30,8 +50,17 @@ const AddStep = React.memo(({ users, currentUserIds, title, onCreate, onClose })
|
||||||
})}
|
})}
|
||||||
</Popup.Header>
|
</Popup.Header>
|
||||||
<Popup.Content>
|
<Popup.Content>
|
||||||
<div className={styles.menu}>
|
<Input
|
||||||
{users.map((user) => (
|
fluid
|
||||||
|
ref={searchField}
|
||||||
|
value={searchValue}
|
||||||
|
placeholder={t('common.searchUsers')}
|
||||||
|
icon="search"
|
||||||
|
onChange={handleSearchFieldChange}
|
||||||
|
/>
|
||||||
|
{filteredUsers.length > 0 && (
|
||||||
|
<div className={styles.users}>
|
||||||
|
{filteredUsers.map((user) => (
|
||||||
<UserItem
|
<UserItem
|
||||||
key={user.id}
|
key={user.id}
|
||||||
name={user.name}
|
name={user.name}
|
||||||
|
@ -41,6 +70,7 @@ const AddStep = React.memo(({ users, currentUserIds, title, onCreate, onClose })
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Popup.Content>
|
</Popup.Content>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,21 @@
|
||||||
:global(#app) {
|
:global(#app) {
|
||||||
.menu {
|
.users {
|
||||||
border: none;
|
margin-top: 8px;
|
||||||
margin: -7px auto -5px;
|
max-height: 320px;
|
||||||
width: 100%;
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
overflow: hidden;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -121,6 +121,9 @@ export default {
|
||||||
projectNotFound_title: 'Project Not Found',
|
projectNotFound_title: 'Project Not Found',
|
||||||
removeManager_title: 'Remove Manager',
|
removeManager_title: 'Remove Manager',
|
||||||
removeMember_title: 'Remove Member',
|
removeMember_title: 'Remove Member',
|
||||||
|
searchLabels: 'Search labels...',
|
||||||
|
searchMembers: 'Search members...',
|
||||||
|
searchUsers: 'Search users...',
|
||||||
seconds: 'Seconds',
|
seconds: 'Seconds',
|
||||||
selectBoard: 'Select board',
|
selectBoard: 'Select board',
|
||||||
selectList: 'Select list',
|
selectList: 'Select list',
|
||||||
|
|
|
@ -116,6 +116,9 @@ export default {
|
||||||
projectActions: 'Действия с проектом',
|
projectActions: 'Действия с проектом',
|
||||||
projectNotFound: 'Проект не найден',
|
projectNotFound: 'Проект не найден',
|
||||||
removeMember: 'Удаление участника',
|
removeMember: 'Удаление участника',
|
||||||
|
searchLabels: 'Поиск меток...',
|
||||||
|
searchMembers: 'Поиск участников...',
|
||||||
|
searchUsers: 'Поиск пользователей...',
|
||||||
seconds: 'Секунды',
|
seconds: 'Секунды',
|
||||||
selectBoard: 'Выберите доску',
|
selectBoard: 'Выберите доску',
|
||||||
selectList: 'Выберите список',
|
selectList: 'Выберите список',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue