Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions worklenz-backend/src/controllers/tasks-controller-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
end: l.end,
names: l.names,
})) || [],
all_labels: task.all_labels || [],
dueDate: task.end_date || task.END_DATE,
startDate: task.start_date,
timeTracking: {
Expand Down
2 changes: 1 addition & 1 deletion worklenz-frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ const App: React.FC = memo(() => {
return (
<Suspense fallback={<SuspenseFallback />}>
<ThemeWrapper>
<UpdateNotificationProvider>
<UpdateNotificationProvider enableAutoCheck={false}>
<ModuleErrorBoundary>
<RouterProvider
router={router}
Expand Down
48 changes: 28 additions & 20 deletions worklenz-frontend/src/components/LabelsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { useAuthService } from '@/hooks/useAuth';
import { Button, Checkbox, Tag } from '@/components';
import { sortLabelsBySelection, isLabelSelected } from '@/utils/labelUtils';

interface LabelsSelectorProps {
task: IProjectTask;
Expand All @@ -30,12 +31,13 @@ const LabelsSelector: React.FC<LabelsSelectorProps> = ({ task, isDarkMode = fals
const { t } = useTranslation('task-list-table');

const filteredLabels = useMemo(() => {
return (
(labels as ITaskLabel[])?.filter(label =>
label.name?.toLowerCase().includes(searchQuery.toLowerCase())
) || []
);
}, [labels, searchQuery]);
const filtered = (labels as ITaskLabel[])?.filter(label =>
label.name?.toLowerCase().includes(searchQuery.toLowerCase())
) || [];

// Sort to show selected labels first using shared utility
return sortLabelsBySelection(filtered, task?.labels || []);
}, [labels, searchQuery, task?.labels]);

// Update dropdown position
const updateDropdownPosition = useCallback(() => {
Expand Down Expand Up @@ -149,7 +151,8 @@ const LabelsSelector: React.FC<LabelsSelectorProps> = ({ task, isDarkMode = fals
};

const checkLabelSelected = (labelId: string) => {
return task?.all_labels?.some(existingLabel => existingLabel.id === labelId) || false;
// Use task.labels (currently selected labels) instead of all_labels
return isLabelSelected(labelId, task?.labels);
};

const handleKeyDown = (e: React.KeyboardEvent) => {
Expand Down Expand Up @@ -261,19 +264,24 @@ const LabelsSelector: React.FC<LabelsSelectorProps> = ({ task, isDarkMode = fals
>
<div className="text-xs">{t('noLabelsFound')}</div>
{searchQuery.trim() && (
<button
onClick={handleCreateLabel}
className={`
mt-2 px-3 py-1 text-xs rounded border transition-colors
${
isDarkMode
? 'border-gray-600 text-gray-300 hover:bg-gray-700'
: 'border-gray-300 text-gray-600 hover:bg-gray-50'
}
`}
>
{t('createLabelButton', { name: searchQuery.trim() })}
</button>
<>
<button
onClick={handleCreateLabel}
className={`
mt-2 px-3 py-1 text-xs rounded border transition-colors
${
isDarkMode
? 'border-gray-600 text-gray-300 hover:bg-gray-700'
: 'border-gray-300 text-gray-600 hover:bg-gray-50'
}
`}
>
{t('createLabelButton', { name: searchQuery.trim() })}
</button>
<div className={`mt-2 text-xs ${isDarkMode ? 'text-gray-500' : 'text-gray-400'}`}>
{t('labelsSelectorInputTip')}
</div>
</>
)}
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { setBoardLabels, updateBoardTaskLabel } from '@/features/board/board-sli
import { updateEnhancedKanbanTaskLabels } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { ILabelsChangeResponse } from '@/types/tasks/taskList.types';
import { ITaskLabelFilter } from '@/types/tasks/taskLabel.types';
import { sortLabelsBySelection, isLabelSelected } from '@/utils/labelUtils';

interface TaskDrawerLabelsProps {
task: ITaskViewModel;
Expand Down Expand Up @@ -98,8 +99,13 @@ const TaskDrawerLabels = ({ task, t }: TaskDrawerLabelsProps) => {

// used useMemo hook for re render the list when searching
const filteredLabelData = useMemo(() => {
return labelList.filter(label => label.name?.toLowerCase().includes(searchQuery.toLowerCase()));
}, [labelList, searchQuery]);
const filtered = labelList.filter(label =>
label.name?.toLowerCase().includes(searchQuery.toLowerCase())
);

// Sort to show selected labels first using shared utility
return sortLabelsBySelection(filtered, task?.labels || []);
}, [labelList, searchQuery, task?.labels]);

const labelDropdownContent = (
<Card
Expand Down Expand Up @@ -143,11 +149,7 @@ const TaskDrawerLabels = ({ task, t }: TaskDrawerLabelsProps) => {
>
<Checkbox
id={label.id}
checked={
task?.labels
? task?.labels.some(existingLabel => existingLabel.id === label.id)
: false
}
checked={isLabelSelected(label.id || '', task?.labels)}
onChange={e => e.preventDefault()}
>
<Flex gap={8}>
Expand Down Expand Up @@ -186,6 +188,11 @@ const TaskDrawerLabels = ({ task, t }: TaskDrawerLabelsProps) => {
<Tag
key={label.id}
color={label.color_code + ALPHA_CHANNEL}
closable
onClose={(e) => {
e.preventDefault();
handleLabelChange(label);
}}
style={{
display: 'flex',
alignItems: 'center',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo } from 'react';
import React, { memo } from 'react';
import { CheckCircleOutlined, HolderOutlined } from '@/shared/antd-imports';
import { Checkbox } from '@/shared/antd-imports';
import { Task } from '@/types/task-management.types';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { useAuthService } from '@/hooks/useAuth';
import { SocketEvents } from '@/shared/socket-events';
import { useSocket } from '@/socket/socketContext';
import { sortLabelsBySelection, isLabelSelected } from '@/utils/labelUtils';

interface LabelsSelectorProps {
task: IProjectTask;
Expand Down Expand Up @@ -67,8 +68,13 @@ const LabelsSelector = ({ task }: LabelsSelectorProps) => {

// used useMemo hook for re render the list when searching
const filteredLabelData = useMemo(() => {
return labelList.filter(label => label.name?.toLowerCase().includes(searchQuery.toLowerCase()));
}, [labelList, searchQuery]);
const filtered = labelList.filter(label =>
label.name?.toLowerCase().includes(searchQuery.toLowerCase())
);

// Sort to show selected labels first using shared utility
return sortLabelsBySelection(filtered, task?.labels || []);
}, [labelList, searchQuery, task?.labels]);

const labelDropdownContent = (
<Card className="custom-card" styles={{ body: { padding: 8, overflow: 'hidden' } }}>
Expand Down Expand Up @@ -112,11 +118,7 @@ const LabelsSelector = ({ task }: LabelsSelectorProps) => {
>
<Checkbox
id={label.id}
checked={
task?.all_labels
? task?.all_labels.some(existingLabel => existingLabel.id === label.id)
: false
}
checked={isLabelSelected(label.id || '', task?.labels)}
onChange={() => handleLabelChange(label)}
>
<Flex gap={8}>
Expand Down
5 changes: 2 additions & 3 deletions worklenz-frontend/src/hooks/useTaskSocketHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,8 @@ export const useTaskSocketHandlers = () => {
await Promise.all([
dispatch(updateTaskLabel(labels)),
dispatch(setTaskLabels(labels)),
// Remove unnecessary refetches - real-time updates handle this
// dispatch(fetchLabels()),
// projectId && dispatch(fetchLabelsByProject(projectId)),
// Fetch labels when a new label is created to update the global labels list
labels.is_new && dispatch(fetchLabels()),
]);

// Update enhanced kanban slice
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,12 @@ const LabelsSettings = () => {
onConfirm={() => deleteLabel(record.id!)}
>
<Tooltip title={t('deleteTooltip', 'Delete')}>
<Button shape="default" icon={<DeleteOutlined />} size="small" />
<Button
shape="default"
icon={<DeleteOutlined />}
size="small"
onClick={(e) => e.stopPropagation()}
/>
</Tooltip>
</Popconfirm>
</Flex>
Expand Down
2 changes: 1 addition & 1 deletion worklenz-frontend/src/types/tasks/task.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface ITask {
project_id: string;
team_id: string;
task_key: string;
labels: string[];
labels: ITaskLabel[];
assignees: string[];
names: string[];
sub_tasks_count: number;
Expand Down
35 changes: 35 additions & 0 deletions worklenz-frontend/src/utils/labelUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ITaskLabel } from '@/types/tasks/taskLabel.types';

/**
* Sorts labels to show selected labels first
* @param labels - All available labels
* @param selectedLabels - Currently selected labels
* @returns Sorted array with selected labels first
*/
export const sortLabelsBySelection = (
labels: ITaskLabel[],
selectedLabels: ITaskLabel[]
): ITaskLabel[] => {
return [...labels].sort((a, b) => {
const aSelected = selectedLabels.some(label => label.id === a.id);
const bSelected = selectedLabels.some(label => label.id === b.id);

if (aSelected && !bSelected) return -1;
if (!aSelected && bSelected) return 1;
return 0;
});
};

/**
* Checks if a label is selected
* @param labelId - ID of the label to check
* @param selectedLabels - Currently selected labels
* @returns true if label is selected
*/
export const isLabelSelected = (
labelId: string,
selectedLabels?: ITaskLabel[]
): boolean => {
if (!selectedLabels || selectedLabels.length === 0) return false;
return selectedLabels.some(label => label.id === labelId);
};