feat(task-management): enhance bulk action bar and localization updates

- Added new features to the OptimizedBulkActionBar, including dropdowns for labels and assignees, improving task management capabilities.
- Integrated task template creation functionality for owners/admins, allowing users to create templates from selected tasks.
- Updated localization files for multiple languages, adding new strings for label searching and template name requirements to enhance user experience.
- Refactored LabelsDropdown component to support label filtering and improved UI feedback for label creation.
This commit is contained in:
chamikaJ
2025-07-02 16:01:04 +05:30
parent 365369cc31
commit a1e8a4c464
10 changed files with 425 additions and 51 deletions

View File

@@ -17,7 +17,9 @@
"createTaskTemplate": "Krijo Shabllon Detyre",
"apply": "Apliko",
"createLabel": "+ Krijo Etiketë",
"searchOrCreateLabel": "Kërko ose krijo etiketë...",
"hitEnterToCreate": "Shtyp Enter për të krijuar",
"labelExists": "Etiketa ekziston tashmë",
"pendingInvitation": "Ftesë në Pritje",
"noMatchingLabels": "Asnjë etiketë që përputhet",
"noLabels": "Asnjë etiketë"

View File

@@ -17,7 +17,9 @@
"createTaskTemplate": "Aufgabenvorlage erstellen",
"apply": "Anwenden",
"createLabel": "+ Label erstellen",
"searchOrCreateLabel": "Label suchen oder erstellen...",
"hitEnterToCreate": "Enter drücken zum Erstellen",
"labelExists": "Label existiert bereits",
"pendingInvitation": "Einladung ausstehend",
"noMatchingLabels": "Keine passenden Labels",
"noLabels": "Keine Labels",

View File

@@ -4,6 +4,7 @@
"cancelText": "Cancel",
"saveText": "Save",
"templateNameText": "Template Name",
"templateNameRequired": "Template name is required",
"selectedTasks": "Selected Tasks",
"removeTask": "Remove",
"cancelButton": "Cancel",

View File

@@ -17,7 +17,9 @@
"createTaskTemplate": "Create Task Template",
"apply": "Apply",
"createLabel": "+ Create Label",
"searchOrCreateLabel": "Search or create label...",
"hitEnterToCreate": "Press Enter to create",
"labelExists": "Label already exists",
"pendingInvitation": "Pending Invitation",
"noMatchingLabels": "No matching labels",
"noLabels": "No labels",

View File

@@ -17,7 +17,9 @@
"createTaskTemplate": "Crear plantilla de tarea",
"apply": "Aplicar",
"createLabel": "+ Crear etiqueta",
"searchOrCreateLabel": "Buscar o crear etiqueta...",
"hitEnterToCreate": "Presione Enter para crear",
"labelExists": "La etiqueta ya existe",
"pendingInvitation": "Invitación Pendiente",
"noMatchingLabels": "No hay etiquetas coincidentes",
"noLabels": "Sin etiquetas",

View File

@@ -17,7 +17,9 @@
"createTaskTemplate": "Criar Modelo de Tarefa",
"apply": "Aplicar",
"createLabel": "+ Criar etiqueta",
"searchOrCreateLabel": "Pesquisar ou criar etiqueta...",
"hitEnterToCreate": "Pressione Enter para criar",
"labelExists": "A etiqueta já existe",
"pendingInvitation": "Convite Pendente",
"noMatchingLabels": "Nenhuma etiqueta correspondente",
"noLabels": "Sem etiquetas",

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import {
Button,
@@ -19,12 +19,23 @@ import {
TagsOutlined,
UsergroupAddOutlined,
FlagOutlined,
BulbOutlined
BulbOutlined,
MoreOutlined
} from '@ant-design/icons';
import { useSelector } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '@/app/store';
import { useAppSelector } from '@/hooks/useAppSelector';
import { selectTasks } from '@/features/projects/bulkActions/bulkActionSlice';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { useBulkActionTranslations } from '@/hooks/useTranslationPreloader';
import LabelsDropdown from '@/components/taskListCommon/task-list-bulk-actions-bar/components/LabelsDropdown';
import AssigneesDropdown from '@/components/taskListCommon/task-list-bulk-actions-bar/components/AssigneesDropdown';
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
import { InputRef } from 'antd/es/input';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import TaskTemplateDrawer from '@/components/task-templates/task-template-drawer';
import { useAuthService } from '@/hooks/useAuth';
const { Text } = Typography;
@@ -139,12 +150,16 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
onBulkSetDueDate,
}) => {
const { t, ready, isLoading } = useBulkActionTranslations();
const dispatch = useDispatch();
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
// Get data from Redux store
const statusList = useAppSelector(state => state.taskStatusReducer.status);
const priorityList = useAppSelector(state => state.priorityReducer.priorities);
const phaseList = useAppSelector(state => state.phaseReducer.phaseList);
const labelsList = useAppSelector(state => state.taskLabelsReducer.labels);
const members = useAppSelector(state => state.teamMembersReducer.teamMembers);
const tasks = useAppSelector(state => state.taskManagement.entities);
// Performance state management
const [isVisible, setIsVisible] = useState(false);
@@ -162,6 +177,20 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
dueDate: false,
});
// Labels dropdown state
const [selectedLabels, setSelectedLabels] = useState<ITaskLabel[]>([]);
const [createLabelText, setCreateLabelText] = useState<string>('');
const labelsInputRef = useRef<InputRef>(null);
// Assignees dropdown state
const [assigneeDropdownOpen, setAssigneeDropdownOpen] = useState(false);
// Task template state
const [showDrawer, setShowDrawer] = useState(false);
// Auth service for permissions
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
// Smooth entrance animation
useEffect(() => {
if (totalSelected > 0) {
@@ -200,6 +229,8 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
})), [phaseList]
);
// Menu click handlers
const handleStatusMenuClick = useCallback((e: any) => {
onBulkStatusChange?.(e.key);
@@ -213,6 +244,126 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
onBulkPhaseChange?.(e.key);
}, [onBulkPhaseChange]);
const handleLabelsMenuClick = useCallback((e: any) => {
onBulkAddLabels?.([e.key]);
}, [onBulkAddLabels]);
// Labels dropdown handlers
const handleLabelChange = useCallback((e: CheckboxChangeEvent, label: ITaskLabel) => {
if (e.target.checked) {
setSelectedLabels(prev => [...prev, label]);
} else {
setSelectedLabels(prev => prev.filter(l => l.id !== label.id));
}
}, []);
const handleApplyLabels = useCallback(async () => {
if (!projectId) return;
try {
updateLoadingState('labels', true);
const body = {
tasks: selectedTaskIds,
labels: selectedLabels,
text: selectedLabels.length > 0 ? null : createLabelText.trim() !== '' ? createLabelText.trim() : null,
};
await onBulkAddLabels?.(selectedLabels.map(l => l.id).filter((id): id is string => id !== undefined));
setCreateLabelText('');
setSelectedLabels([]);
} catch (error) {
// Error handling is done in the parent component
} finally {
updateLoadingState('labels', false);
}
}, [selectedLabels, createLabelText, selectedTaskIds, projectId, onBulkAddLabels, updateLoadingState]);
// Assignees dropdown handlers
const handleChangeAssignees = useCallback(async (selectedAssignees: ITeamMemberViewModel[]) => {
if (!projectId) return;
try {
updateLoadingState('assignMembers', true);
await onBulkAssignMembers?.(selectedAssignees.map(m => m.id).filter((id): id is string => id !== undefined));
} catch (error) {
// Error handling is done in the parent component
} finally {
updateLoadingState('assignMembers', false);
}
}, [projectId, onBulkAssignMembers, updateLoadingState]);
const onAssigneeDropdownOpenChange = useCallback((open: boolean) => {
setAssigneeDropdownOpen(open);
}, []);
// Get selected task objects for template creation
const selectedTaskObjects = useMemo(() => {
return Object.values(tasks).filter((task: any) => selectedTaskIds.includes(task.id));
}, [tasks, selectedTaskIds]);
// Update Redux state when opening template drawer
const handleOpenTemplateDrawer = useCallback(() => {
// Convert Task objects to IProjectTask format for template creation
const projectTasks: IProjectTask[] = selectedTaskObjects.map((task: any) => ({
id: task.id,
name: task.title, // Always use title as the name
task_key: task.task_key,
status: task.status,
status_id: task.status,
priority: task.priority,
phase_id: task.phase,
phase_name: task.phase,
description: task.description,
start_date: task.startDate,
end_date: task.dueDate,
total_hours: task.timeTracking?.estimated || 0,
total_minutes: task.timeTracking?.logged || 0,
progress: task.progress,
sub_tasks_count: task.sub_tasks_count || 0,
assignees: task.assignees?.map((assigneeId: string) => ({
id: assigneeId,
name: '',
email: '',
avatar_url: '',
team_member_id: assigneeId,
project_member_id: assigneeId,
})) || [],
labels: task.labels || [],
manual_progress: false,
created_at: task.createdAt,
updated_at: task.updatedAt,
sort_order: task.order,
}));
// Update the bulkActionReducer with selected tasks
dispatch(selectTasks(projectTasks));
setShowDrawer(true);
}, [selectedTaskObjects, dispatch]);
// Labels dropdown content
const labelsDropdownContent = useMemo(() => (
<LabelsDropdown
labelsList={labelsList || []}
themeMode={isDarkMode ? 'dark' : 'light'}
createLabelText={createLabelText}
selectedLabels={selectedLabels}
labelsInputRef={labelsInputRef as React.RefObject<InputRef>}
onLabelChange={handleLabelChange}
onCreateLabelTextChange={setCreateLabelText}
onApply={handleApplyLabels}
t={t}
loading={loadingStates.labels}
/>
), [labelsList, isDarkMode, createLabelText, selectedLabels, handleLabelChange, handleApplyLabels, t, loadingStates.labels]);
// Assignees dropdown content
const assigneesDropdownContent = useMemo(() => (
<AssigneesDropdown
members={members?.data || []}
themeMode={isDarkMode ? 'dark' : 'light'}
onApply={handleChangeAssignees}
onClose={() => setAssigneeDropdownOpen(false)}
t={t}
/>
), [members?.data, isDarkMode, handleChangeAssignees, t]);
// Memoized handlers with loading states
const handleStatusChange = useCallback(async () => {
updateLoadingState('status', true);
@@ -466,13 +617,41 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
</Tooltip>
{/* Change Labels */}
<ActionButton
<Tooltip title={t('ADD_LABELS')} placement="top">
<Dropdown
dropdownRender={() => labelsDropdownContent}
trigger={['click']}
placement="top"
arrow
onOpenChange={(open) => {
if (!open) {
setSelectedLabels([]);
setCreateLabelText('');
}
}}
>
<Button
icon={<TagsOutlined />}
tooltip={t('ADD_LABELS')}
onClick={() => onBulkAddLabels?.([])}
style={{
background: 'transparent',
color: isDarkMode ? '#e5e7eb' : '#374151',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '6px',
height: '32px',
width: '32px',
fontSize: '14px',
borderRadius: '6px',
transition: 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
}}
size="small"
type="text"
loading={loadingStates.labels}
isDarkMode={isDarkMode}
/>
</Dropdown>
</Tooltip>
{/* Assign to Me */}
<ActionButton
@@ -484,13 +663,37 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
/>
{/* Change Assignees */}
<ActionButton
<Tooltip title={t('ASSIGN_MEMBERS')} placement="top">
<Dropdown
dropdownRender={() => assigneesDropdownContent}
open={assigneeDropdownOpen}
onOpenChange={onAssigneeDropdownOpenChange}
trigger={['click']}
placement="top"
arrow
>
<Button
icon={<UsergroupAddOutlined />}
tooltip={t('ASSIGN_MEMBERS')}
onClick={() => onBulkAssignMembers?.([])}
style={{
background: 'transparent',
color: isDarkMode ? '#e5e7eb' : '#374151',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '6px',
height: '32px',
width: '32px',
fontSize: '14px',
borderRadius: '6px',
transition: 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
}}
size="small"
type="text"
loading={loadingStates.assignMembers}
isDarkMode={isDarkMode}
/>
</Dropdown>
</Tooltip>
{/* Archive */}
<ActionButton
@@ -520,6 +723,46 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
/>
</Popconfirm>
{/* More Options (Create Task Template) - Only for owners/admins */}
{isOwnerOrAdmin && (
<Tooltip title={t('moreOptions')} placement="top">
<Dropdown
trigger={['click']}
menu={{
items: [
{
key: '1',
label: t('createTaskTemplate'),
onClick: handleOpenTemplateDrawer,
},
],
}}
placement="top"
arrow
>
<Button
icon={<MoreOutlined />}
style={{
background: 'transparent',
color: isDarkMode ? '#e5e7eb' : '#374151',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '6px',
height: '32px',
width: '32px',
fontSize: '14px',
borderRadius: '6px',
transition: 'all 0.15s cubic-bezier(0.4, 0, 0.2, 1)',
}}
size="small"
type="text"
/>
</Dropdown>
</Tooltip>
)}
<Divider
type="vertical"
style={{
@@ -537,6 +780,20 @@ const OptimizedBulkActionBarContent: React.FC<OptimizedBulkActionBarProps> = Rea
isDarkMode={isDarkMode}
/>
</Space>
{/* Task Template Drawer */}
{createPortal(
<TaskTemplateDrawer
showDrawer={showDrawer}
selectedTemplateId={null}
onClose={() => {
setShowDrawer(false);
onClearSelection?.();
}}
/>,
document.body,
'create-task-template'
)}
</div>
);
});

View File

@@ -38,6 +38,11 @@ import {
toggleTaskSelection,
clearSelection,
} from '@/features/task-management/selection.slice';
import {
selectTaskIds,
selectTasks,
deselectAll as deselectAllBulk,
} from '@/features/projects/bulkActions/bulkActionSlice';
import { Task } from '@/types/task-management.types';
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
import { useSocket } from '@/socket/socketContext';
@@ -49,7 +54,6 @@ import OptimizedBulkActionBar from './optimized-bulk-action-bar';
import VirtualizedTaskList from './virtualized-task-list';
import { AppDispatch } from '@/app/store';
import { shallowEqual } from 'react-redux';
import { deselectAll } from '@/features/projects/bulkActions/bulkActionSlice';
import { taskListBulkActionsApiService } from '@/api/tasks/task-list-bulk-actions.api.service';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
import {
@@ -73,6 +77,7 @@ import { ITaskPriority } from '@/types/tasks/taskPriority.types';
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
import alertService from '@/services/alerts/alertService';
import logger from '@/utils/errorLogger';
@@ -153,7 +158,9 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const tasks = useSelector(taskManagementSelectors.selectAll);
const taskGroups = useSelector(selectTaskGroupsV3, shallowEqual);
const currentGrouping = useSelector(selectCurrentGroupingV3, shallowEqual);
const selectedTaskIds = useSelector(selectSelectedTaskIds);
// Use bulk action slice for selected tasks instead of selection slice
const selectedTaskIds = useSelector((state: RootState) => state.bulkActionReducer.selectedTaskIdsList);
const selectedTasks = useSelector((state: RootState) => state.bulkActionReducer.selectedTasks);
const loading = useSelector((state: RootState) => state.taskManagement.loading, shallowEqual);
const error = useSelector((state: RootState) => state.taskManagement.error);
@@ -403,9 +410,53 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const handleSelectTask = useCallback(
(taskId: string, selected: boolean) => {
dispatch(toggleTaskSelection(taskId));
if (selected) {
// Add task to bulk selection
const task = tasks.find(t => t.id === taskId);
if (task) {
// Convert Task to IProjectTask format for bulk actions
const projectTask: IProjectTask = {
id: task.id,
name: task.title, // Always use title as the name
task_key: task.task_key,
status: task.status,
status_id: task.status,
priority: task.priority,
phase_id: task.phase,
phase_name: task.phase,
description: task.description,
start_date: task.startDate,
end_date: task.dueDate,
total_hours: task.timeTracking.estimated || 0,
total_minutes: task.timeTracking.logged || 0,
progress: task.progress,
sub_tasks_count: task.sub_tasks_count || 0,
assignees: task.assignees.map(assigneeId => ({
id: assigneeId,
name: '',
email: '',
avatar_url: '',
team_member_id: assigneeId,
project_member_id: assigneeId,
})),
labels: task.labels,
manual_progress: false, // Default value for Task type
created_at: task.createdAt,
updated_at: task.updatedAt,
sort_order: task.order,
};
dispatch(selectTasks([...selectedTasks, projectTask]));
dispatch(selectTaskIds([...selectedTaskIds, taskId]));
}
} else {
// Remove task from bulk selection
const updatedTasks = selectedTasks.filter(t => t.id !== taskId);
const updatedTaskIds = selectedTaskIds.filter(id => id !== taskId);
dispatch(selectTasks(updatedTasks));
dispatch(selectTaskIds(updatedTaskIds));
}
},
[dispatch]
[dispatch, selectedTasks, selectedTaskIds, tasks]
);
const handleToggleSubtasks = useCallback((taskId: string) => {
@@ -430,7 +481,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
// Bulk action handlers - implementing real functionality from task-list-bulk-actions-bar
const handleClearSelection = useCallback(() => {
dispatch(deselectAll());
dispatch(deselectAllBulk());
dispatch(clearSelection());
}, [dispatch]);
@@ -468,7 +519,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const res = await taskListBulkActionsApiService.changeStatus(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_status);
dispatch(deselectAll());
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
@@ -490,7 +541,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const res = await taskListBulkActionsApiService.changePriority(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_priority);
dispatch(deselectAll());
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
@@ -512,7 +563,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const res = await taskListBulkActionsApiService.changePhase(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_change_phase);
dispatch(deselectAll());
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
@@ -531,7 +582,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const res = await taskListBulkActionsApiService.assignToMe(body);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_assign_me);
dispatch(deselectAll());
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
@@ -563,7 +614,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const res = await taskListBulkActionsApiService.assignTasks(body);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_assign_members);
dispatch(deselectAll());
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
@@ -588,7 +639,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const res = await taskListBulkActionsApiService.assignLabels(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_update_labels);
dispatch(deselectAll());
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
dispatch(fetchLabels());
@@ -608,7 +659,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const res = await taskListBulkActionsApiService.archiveTasks(body, archived);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_archive);
dispatch(deselectAll());
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}
@@ -627,7 +678,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const res = await taskListBulkActionsApiService.deleteTasks(body, projectId);
if (res.done) {
trackMixpanelEvent(evt_project_task_list_bulk_delete);
dispatch(deselectAll());
dispatch(deselectAllBulk());
dispatch(clearSelection());
dispatch(fetchTasksV3(projectId));
}

View File

@@ -59,6 +59,7 @@ const TaskTemplateDrawer = ({
fetchTemplateData();
return;
}
// Tasks should already have the name property set correctly
setTemplateData({ tasks: selectedTasks });
};
@@ -126,7 +127,7 @@ const TaskTemplateDrawer = ({
open={showDrawer}
onClose={onCloseDrawer}
afterOpenChange={afterOpenChange}
destroyOnClose={true}
destroyOnHidden={true}
footer={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', justifyContent: 'right' }}>
<Button onClick={onCloseDrawer}>{t('cancelButton')}</Button>

View File

@@ -35,6 +35,23 @@ const LabelsDropdown = ({
}
}, []);
// Filter labels based on search input
const filteredLabels = useMemo(() => {
if (!createLabelText.trim()) {
return labelsList;
}
return labelsList.filter(label =>
label.name?.toLowerCase().includes(createLabelText.toLowerCase())
);
}, [labelsList, createLabelText]);
// Check if the search text matches any existing label exactly
const exactMatch = useMemo(() => {
return labelsList.some(label =>
label.name?.toLowerCase() === createLabelText.toLowerCase()
);
}, [labelsList, createLabelText]);
const isOnApply = () => {
if (!createLabelText.trim() && selectedLabels.length === 0) return;
onApply();
@@ -42,18 +59,17 @@ const LabelsDropdown = ({
return (
<Card className="custom-card" styles={{ body: { padding: 8 } }}>
<Flex vertical>
{/* Always show the list, filtered by input */}
{!createLabelText && (
{/* Show filtered labels list */}
<List
style={{
padding: 0,
overflow: 'auto',
maxHeight: labelsList.length > 10 ? '200px' : 'auto', // Set max height if more than 10 labels
maxHeight: filteredLabels.length > 10 ? '200px' : 'auto',
maxWidth: 250,
}}
>
{labelsList.length > 0 && (
labelsList.map(label => (
{filteredLabels.length > 0 ? (
filteredLabels.map(label => (
<List.Item
className={themeMode === 'dark' ? 'custom-list-item dark' : 'custom-list-item'}
key={label.id}
@@ -75,30 +91,68 @@ const LabelsDropdown = ({
</Checkbox>
</List.Item>
))
) : createLabelText.trim() ? (
<List.Item
className={themeMode === 'dark' ? 'custom-list-item dark' : 'custom-list-item'}
style={{
display: 'flex',
gap: 8,
justifyContent: 'flex-start',
padding: '4px 8px',
border: 'none',
cursor: 'pointer',
}}
>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{t('noMatchingLabels')}
</Typography.Text>
</List.Item>
) : (
<List.Item
className={themeMode === 'dark' ? 'custom-list-item dark' : 'custom-list-item'}
style={{
display: 'flex',
gap: 8,
justifyContent: 'flex-start',
padding: '4px 8px',
border: 'none',
cursor: 'pointer',
}}
>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{t('noLabels')}
</Typography.Text>
</List.Item>
)}
</List>
)}
<Flex style={{ paddingTop: 8 }} vertical justify="space-between" gap={8}>
<Input
ref={labelsInputRef}
value={createLabelText}
onChange={e => onCreateLabelTextChange(e.currentTarget.value)}
placeholder={t('createLabel')}
placeholder={t('searchOrCreateLabel')}
onPressEnter={() => {
isOnApply();
}}
/>
{createLabelText && (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{t('hitEnterToCreate')}
{exactMatch
? t('labelExists')
: t('hitEnterToCreate')
}
</Typography.Text>
)}
{!createLabelText && (
<Button type="primary" size="small" onClick={isOnApply} style={{ width: '100%' }}>
<Button
type="primary"
size="small"
onClick={isOnApply}
style={{ width: '100%' }}
disabled={!createLabelText.trim() && selectedLabels.length === 0}
>
{t('apply')}
</Button>
)}
</Flex>
</Flex>
</Card>