From 76adb89caf91a0ec9e64ffbe0b5cd0cf518189c7 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 28 Jul 2025 15:45:12 +0530 Subject: [PATCH] feat(task-filters): enhance sorting functionality and localization updates - Added sorting options to task filters, including clear sort, sort ascending, sort descending, and sort by field. - Updated localization files for multiple languages (Albanian, German, English, Spanish, Portuguese, Chinese) to include new sorting terms. - Implemented a SortDropdown component for improved user experience in task management. - Integrated sorting state management in the task management slice for better data handling. --- .../public/locales/alb/task-list-filters.json | 9 +- .../public/locales/de/task-list-filters.json | 9 +- .../public/locales/en/task-list-filters.json | 9 +- .../public/locales/es/task-list-filters.json | 9 +- .../public/locales/pt/task-list-filters.json | 9 +- .../public/locales/zh/task-list-filters.json | 9 +- .../public/locales/alb/task-list-filters.json | 9 +- .../public/locales/de/task-list-filters.json | 9 +- .../public/locales/en/task-list-filters.json | 9 +- .../public/locales/es/task-list-filters.json | 9 +- .../public/locales/pt/task-list-filters.json | 9 +- .../public/locales/zh/task-list-filters.json | 9 +- .../components/TaskRowColumns.tsx | 2 +- .../task-management/improved-task-filters.tsx | 229 +++++++++++++++++- .../task-management/task-management.slice.ts | 29 ++- .../src/types/task-management.types.ts | 3 + 16 files changed, 351 insertions(+), 20 deletions(-) diff --git a/worklenz-backend/src/public/locales/alb/task-list-filters.json b/worklenz-backend/src/public/locales/alb/task-list-filters.json index c3156498..595801b1 100644 --- a/worklenz-backend/src/public/locales/alb/task-list-filters.json +++ b/worklenz-backend/src/public/locales/alb/task-list-filters.json @@ -81,5 +81,12 @@ "delete": "Fshi", "enterStatusName": "Shkruani emrin e statusit", "selectCategory": "Zgjidh kategorinë", - "close": "Mbyll" + "close": "Mbyll", + "clearSort": "Pastro Renditjen", + "sortAscending": "Rendit në Rritje", + "sortDescending": "Rendit në Zbritje", + "sortByField": "Rendit sipas {{field}}", + "ascendingOrder": "Rritës", + "descendingOrder": "Zbritës", + "currentSort": "Renditja aktuale: {{field}} {{order}}" } diff --git a/worklenz-backend/src/public/locales/de/task-list-filters.json b/worklenz-backend/src/public/locales/de/task-list-filters.json index 0854c34f..b298139c 100644 --- a/worklenz-backend/src/public/locales/de/task-list-filters.json +++ b/worklenz-backend/src/public/locales/de/task-list-filters.json @@ -81,5 +81,12 @@ "delete": "Löschen", "enterStatusName": "Statusnamen eingeben", "selectCategory": "Kategorie auswählen", - "close": "Schließen" + "close": "Schließen", + "clearSort": "Sortierung löschen", + "sortAscending": "Aufsteigend sortieren", + "sortDescending": "Absteigend sortieren", + "sortByField": "Sortieren nach {{field}}", + "ascendingOrder": "Aufsteigend", + "descendingOrder": "Absteigend", + "currentSort": "Aktuelle Sortierung: {{field}} {{order}}" } diff --git a/worklenz-backend/src/public/locales/en/task-list-filters.json b/worklenz-backend/src/public/locales/en/task-list-filters.json index a38356c6..6fa2ce3c 100644 --- a/worklenz-backend/src/public/locales/en/task-list-filters.json +++ b/worklenz-backend/src/public/locales/en/task-list-filters.json @@ -81,5 +81,12 @@ "delete": "Delete", "enterStatusName": "Enter status name", "selectCategory": "Select category", - "close": "Close" + "close": "Close", + "clearSort": "Clear Sort", + "sortAscending": "Sort Ascending", + "sortDescending": "Sort Descending", + "sortByField": "Sort by {{field}}", + "ascendingOrder": "Ascending", + "descendingOrder": "Descending", + "currentSort": "Current sort: {{field}} {{order}}" } diff --git a/worklenz-backend/src/public/locales/es/task-list-filters.json b/worklenz-backend/src/public/locales/es/task-list-filters.json index 465368f0..8ee72c45 100644 --- a/worklenz-backend/src/public/locales/es/task-list-filters.json +++ b/worklenz-backend/src/public/locales/es/task-list-filters.json @@ -77,5 +77,12 @@ "delete": "Eliminar", "enterStatusName": "Introducir nombre del estado", "selectCategory": "Seleccionar categoría", - "close": "Cerrar" + "close": "Cerrar", + "clearSort": "Limpiar Ordenamiento", + "sortAscending": "Ordenar Ascendente", + "sortDescending": "Ordenar Descendente", + "sortByField": "Ordenar por {{field}}", + "ascendingOrder": "Ascendente", + "descendingOrder": "Descendente", + "currentSort": "Ordenamiento actual: {{field}} {{order}}" } diff --git a/worklenz-backend/src/public/locales/pt/task-list-filters.json b/worklenz-backend/src/public/locales/pt/task-list-filters.json index 21e8806b..4464c2c1 100644 --- a/worklenz-backend/src/public/locales/pt/task-list-filters.json +++ b/worklenz-backend/src/public/locales/pt/task-list-filters.json @@ -78,5 +78,12 @@ "delete": "Excluir", "enterStatusName": "Digite o nome do status", "selectCategory": "Selecionar categoria", - "close": "Fechar" + "close": "Fechar", + "clearSort": "Limpar Ordenação", + "sortAscending": "Ordenar Crescente", + "sortDescending": "Ordenar Decrescente", + "sortByField": "Ordenar por {{field}}", + "ascendingOrder": "Crescente", + "descendingOrder": "Decrescente", + "currentSort": "Ordenação atual: {{field}} {{order}}" } diff --git a/worklenz-backend/src/public/locales/zh/task-list-filters.json b/worklenz-backend/src/public/locales/zh/task-list-filters.json index 84387509..95a4f166 100644 --- a/worklenz-backend/src/public/locales/zh/task-list-filters.json +++ b/worklenz-backend/src/public/locales/zh/task-list-filters.json @@ -75,5 +75,12 @@ "delete": "删除", "enterStatusName": "输入状态名称", "selectCategory": "选择类别", - "close": "关闭" + "close": "关闭", + "clearSort": "清除排序", + "sortAscending": "升序排列", + "sortDescending": "降序排列", + "sortByField": "按{{field}}排序", + "ascendingOrder": "升序", + "descendingOrder": "降序", + "currentSort": "当前排序:{{field}} {{order}}" } \ No newline at end of file diff --git a/worklenz-frontend/public/locales/alb/task-list-filters.json b/worklenz-frontend/public/locales/alb/task-list-filters.json index 4fc4dbdf..27806a76 100644 --- a/worklenz-frontend/public/locales/alb/task-list-filters.json +++ b/worklenz-frontend/public/locales/alb/task-list-filters.json @@ -84,5 +84,12 @@ "close": "Mbyll", "cannotMoveStatus": "Nuk mund të lëvizet statusi", "cannotMoveStatusMessage": "Nuk mund të lëvizet ky status sepse do të linte kategorinë '{{categoryName}}' bosh. Çdo kategori duhet të ketë të paktën një status.", - "ok": "OK" + "ok": "OK", + "clearSort": "Pastro Renditjen", + "sortAscending": "Rendit në Rritje", + "sortDescending": "Rendit në Zbritje", + "sortByField": "Rendit sipas {{field}}", + "ascendingOrder": "Rritës", + "descendingOrder": "Zbritës", + "currentSort": "Renditja aktuale: {{field}} {{order}}" } diff --git a/worklenz-frontend/public/locales/de/task-list-filters.json b/worklenz-frontend/public/locales/de/task-list-filters.json index 18d50b6c..19280036 100644 --- a/worklenz-frontend/public/locales/de/task-list-filters.json +++ b/worklenz-frontend/public/locales/de/task-list-filters.json @@ -84,5 +84,12 @@ "close": "Schließen", "cannotMoveStatus": "Status kann nicht verschoben werden", "cannotMoveStatusMessage": "Dieser Status kann nicht verschoben werden, da die Kategorie '{{categoryName}}' leer bleiben würde. Jede Kategorie muss mindestens einen Status haben.", - "ok": "OK" + "ok": "OK", + "clearSort": "Sortierung löschen", + "sortAscending": "Aufsteigend sortieren", + "sortDescending": "Absteigend sortieren", + "sortByField": "Sortieren nach {{field}}", + "ascendingOrder": "Aufsteigend", + "descendingOrder": "Absteigend", + "currentSort": "Aktuelle Sortierung: {{field}} {{order}}" } diff --git a/worklenz-frontend/public/locales/en/task-list-filters.json b/worklenz-frontend/public/locales/en/task-list-filters.json index 118ac4ce..b104052c 100644 --- a/worklenz-frontend/public/locales/en/task-list-filters.json +++ b/worklenz-frontend/public/locales/en/task-list-filters.json @@ -84,5 +84,12 @@ "close": "Close", "cannotMoveStatus": "Cannot Move Status", "cannotMoveStatusMessage": "Cannot move this status because it would leave the '{{categoryName}}' category empty. Each category must have at least one status.", - "ok": "OK" + "ok": "OK", + "clearSort": "Clear Sort", + "sortAscending": "Sort Ascending", + "sortDescending": "Sort Descending", + "sortByField": "Sort by {{field}}", + "ascendingOrder": "Ascending", + "descendingOrder": "Descending", + "currentSort": "Current sort: {{field}} {{order}}" } diff --git a/worklenz-frontend/public/locales/es/task-list-filters.json b/worklenz-frontend/public/locales/es/task-list-filters.json index 00c27f16..6aa9a8a1 100644 --- a/worklenz-frontend/public/locales/es/task-list-filters.json +++ b/worklenz-frontend/public/locales/es/task-list-filters.json @@ -84,5 +84,12 @@ "close": "Cerrar", "cannotMoveStatus": "No se puede mover el estado", "cannotMoveStatusMessage": "No se puede mover este estado porque dejaría vacía la categoría '{{categoryName}}'. Cada categoría debe tener al menos un estado.", - "ok": "OK" + "ok": "OK", + "clearSort": "Limpiar Ordenamiento", + "sortAscending": "Ordenar Ascendente", + "sortDescending": "Ordenar Descendente", + "sortByField": "Ordenar por {{field}}", + "ascendingOrder": "Ascendente", + "descendingOrder": "Descendente", + "currentSort": "Ordenamiento actual: {{field}} {{order}}" } diff --git a/worklenz-frontend/public/locales/pt/task-list-filters.json b/worklenz-frontend/public/locales/pt/task-list-filters.json index 3674a29a..a6abcf22 100644 --- a/worklenz-frontend/public/locales/pt/task-list-filters.json +++ b/worklenz-frontend/public/locales/pt/task-list-filters.json @@ -84,5 +84,12 @@ "close": "Fechar", "cannotMoveStatus": "Não é possível mover o status", "cannotMoveStatusMessage": "Não é possível mover este status porque deixaria a categoria '{{categoryName}}' vazia. Cada categoria deve ter pelo menos um status.", - "ok": "OK" + "ok": "OK", + "clearSort": "Limpar Ordenação", + "sortAscending": "Ordenar Crescente", + "sortDescending": "Ordenar Decrescente", + "sortByField": "Ordenar por {{field}}", + "ascendingOrder": "Crescente", + "descendingOrder": "Decrescente", + "currentSort": "Ordenação atual: {{field}} {{order}}" } diff --git a/worklenz-frontend/public/locales/zh/task-list-filters.json b/worklenz-frontend/public/locales/zh/task-list-filters.json index 4d1d6b43..f5617aac 100644 --- a/worklenz-frontend/public/locales/zh/task-list-filters.json +++ b/worklenz-frontend/public/locales/zh/task-list-filters.json @@ -79,5 +79,12 @@ "close": "关闭", "cannotMoveStatus": "无法移动状态", "cannotMoveStatusMessage": "无法移动此状态,因为这会使\"{{categoryName}}\"类别为空。每个类别必须至少有一个状态。", - "ok": "确定" + "ok": "确定", + "clearSort": "清除排序", + "sortAscending": "升序排列", + "sortDescending": "降序排列", + "sortByField": "按{{field}}排序", + "ascendingOrder": "升序", + "descendingOrder": "降序", + "currentSort": "当前排序:{{field}} {{order}}" } \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-list-v2/components/TaskRowColumns.tsx b/worklenz-frontend/src/components/task-list-v2/components/TaskRowColumns.tsx index c5fdadd1..3681140e 100644 --- a/worklenz-frontend/src/components/task-list-v2/components/TaskRowColumns.tsx +++ b/worklenz-frontend/src/components/task-list-v2/components/TaskRowColumns.tsx @@ -364,7 +364,7 @@ interface ReporterColumnProps { export const ReporterColumn: React.FC = memo(({ width, reporter }) => (
{reporter ? ( - {reporter} + {reporter} ) : ( - )} diff --git a/worklenz-frontend/src/components/task-management/improved-task-filters.tsx b/worklenz-frontend/src/components/task-management/improved-task-filters.tsx index bde1aee6..80e2d91c 100644 --- a/worklenz-frontend/src/components/task-management/improved-task-filters.tsx +++ b/worklenz-frontend/src/components/task-management/improved-task-filters.tsx @@ -14,6 +14,8 @@ import { EyeOutlined, InboxOutlined, CheckOutlined, + SortAscendingOutlined, + SortDescendingOutlined, } from '@/shared/antd-imports'; import { RootState } from '@/app/store'; import { useAppSelector } from '@/hooks/useAppSelector'; @@ -30,6 +32,12 @@ import { setArchived as setTaskManagementArchived, toggleArchived as toggleTaskManagementArchived, selectArchived, + setSort, + setSortField, + setSortOrder, + selectSort, + selectSortField, + selectSortOrder, } from '@/features/task-management/task-management.slice'; import { setCurrentGrouping, @@ -44,11 +52,13 @@ import { setLabels, setSearch, setPriorities, + setFields, } from '@/features/tasks/tasks.slice'; import { getTeamMembers } from '@/features/team-members/team-members.slice'; import { ITaskPriority } from '@/types/tasks/taskPriority.types'; import { ITaskListColumn } from '@/types/tasks/taskList.types'; import { IGroupBy } from '@/features/tasks/tasks.slice'; +import { ITaskListSortableColumn } from '@/types/tasks/taskListFilters.types'; // --- Enhanced Kanban imports --- import { setGroupBy as setKanbanGroupBy, @@ -84,6 +94,12 @@ const FILTER_DEBOUNCE_DELAY = 300; // ms const SEARCH_DEBOUNCE_DELAY = 500; // ms const MAX_FILTER_OPTIONS = 100; +// Sort order enum +enum SORT_ORDER { + ASCEND = 'ascend', + DESCEND = 'descend', +} + // Limit options to prevent UI lag // Optimized selectors with proper transformation logic @@ -740,6 +756,192 @@ const SearchFilter: React.FC<{ ); }; +// Sort Dropdown Component - Simplified version using task-management slice +const SortDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({ + themeClasses, + isDarkMode, +}) => { + const { t } = useTranslation('task-list-filters'); + const dispatch = useAppDispatch(); + const { projectId } = useAppSelector(state => state.projectReducer); + + // Get current sort state from task-management slice + const currentSortField = useAppSelector(selectSortField); + const currentSortOrder = useAppSelector(selectSortOrder); + + const [open, setOpen] = React.useState(false); + const dropdownRef = useRef(null); + + // Close dropdown on outside click + React.useEffect(() => { + if (!open) return; + const handleClick = (e: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setOpen(false); + } + }; + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [open]); + + const sortFieldsList = [ + { label: t('taskText'), key: 'name' }, + { label: t('statusText'), key: 'status' }, + { label: t('priorityText'), key: 'priority' }, + { label: t('startDateText'), key: 'start_date' }, + { label: t('endDateText'), key: 'end_date' }, + { label: t('completedDateText'), key: 'completed_at' }, + { label: t('createdDateText'), key: 'created_at' }, + { label: t('lastUpdatedText'), key: 'updated_at' }, + ]; + + const handleSortFieldChange = (fieldKey: string) => { + // If clicking the same field, toggle order, otherwise set new field with ASC + if (currentSortField === fieldKey) { + const newOrder = currentSortOrder === 'ASC' ? 'DESC' : 'ASC'; + dispatch(setSort({ field: fieldKey, order: newOrder })); + } else { + dispatch(setSort({ field: fieldKey, order: 'ASC' })); + } + + // Fetch updated tasks + if (projectId) { + dispatch(fetchTasksV3(projectId)); + } + + setOpen(false); + }; + + const clearSort = () => { + dispatch(setSort({ field: '', order: 'ASC' })); + if (projectId) { + dispatch(fetchTasksV3(projectId)); + } + }; + + const isActive = currentSortField !== ''; + const currentFieldLabel = sortFieldsList.find(f => f.key === currentSortField)?.label; + const orderText = currentSortOrder === 'ASC' ? t('ascendingOrder') : t('descendingOrder'); + + return ( +
+ {/* Trigger Button - matching FilterDropdown style */} + + + {/* Dropdown Panel - matching FilterDropdown style */} + {open && ( +
+ {/* Clear Sort Option */} + {isActive && ( +
+ +
+ )} + + {/* Options List */} +
+
+ {sortFieldsList.map(sortField => { + const isSelected = currentSortField === sortField.key; + + return ( + + ); + })} +
+
+
+ )} +
+ ); +}; + const LOCAL_STORAGE_KEY = 'worklenz.taskManagement.fields'; const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({ @@ -1050,14 +1252,20 @@ const ImprovedTaskFilters: React.FC = ({ position, cla }; }, [dispatch, projectView]); + // Get sort fields for active count calculation + const sortFields = useAppSelector(state => state.taskReducer.fields); + const taskManagementSortField = useAppSelector(selectSortField); + // Calculate active filters count - memoized to prevent unnecessary recalculations const calculatedActiveFiltersCount = useMemo(() => { const count = filterSections.reduce( (acc, section) => (section.id === 'groupBy' ? acc : acc + section.selectedValues.length), 0 ); - return count + (searchValue ? 1 : 0); - }, [filterSections, searchValue]); + const sortFieldsCount = position === 'list' ? sortFields.length : 0; + const taskManagementSortCount = position === 'list' && taskManagementSortField ? 1 : 0; + return count + (searchValue ? 1 : 0) + sortFieldsCount + taskManagementSortCount; + }, [filterSections, searchValue, sortFields, taskManagementSortField, position]); useEffect(() => { if (activeFiltersCount !== calculatedActiveFiltersCount) { @@ -1231,6 +1439,12 @@ const ImprovedTaskFilters: React.FC = ({ position, cla // Clear priority filters dispatch(setPriorities([])); + // Clear sort fields + dispatch(setFields([])); + + // Clear sort from task-management slice + dispatch(setSort({ field: '', order: 'ASC' })); + // Clear archived state based on position if (position === 'list') { dispatch(setTaskManagementArchived(false)); @@ -1276,9 +1490,9 @@ const ImprovedTaskFilters: React.FC = ({ position, cla
-
+
{/* Left Section - Main Filters */} -
+
{/* Search */} = ({ position, cla themeClasses={themeClasses} /> + {/* Sort Filter Button (for list view) - appears after search */} + {position === 'list' && ( + + )} + {/* Filter Dropdowns - Only render when data is loaded */} {isDataLoaded ? ( filterSectionsData.map(section => ( @@ -1316,7 +1535,7 @@ const ImprovedTaskFilters: React.FC = ({ position, cla
{/* Right Section - Additional Controls */} -
+
{/* Active Filters Indicator */} {activeFiltersCount > 0 && (
diff --git a/worklenz-frontend/src/features/task-management/task-management.slice.ts b/worklenz-frontend/src/features/task-management/task-management.slice.ts index b28da652..c72dc392 100644 --- a/worklenz-frontend/src/features/task-management/task-management.slice.ts +++ b/worklenz-frontend/src/features/task-management/task-management.slice.ts @@ -64,6 +64,9 @@ const initialState: TaskManagementState = { loadingColumns: false, columns: [], customColumns: [], + // Add sort-related state + sortField: '', + sortOrder: 'ASC', }; // Async thunk to fetch tasks from API @@ -233,12 +236,16 @@ export const fetchTasksV3 = createAsyncThunk( // Get archived state from task management slice const archivedState = state.taskManagement.archived; + // Get sort state from task management slice + const sortField = state.taskManagement.sortField; + const sortOrder = state.taskManagement.sortOrder; + const config: ITaskListConfigV2 = { id: projectId, archived: archivedState, group: currentGrouping || '', - field: '', - order: '', + field: sortField, + order: sortOrder, search: searchValue, statuses: '', members: selectedAssignees, @@ -737,6 +744,16 @@ const taskManagementSlice = createSlice({ toggleArchived: (state) => { state.archived = !state.archived; }, + setSortField: (state, action: PayloadAction) => { + state.sortField = action.payload; + }, + setSortOrder: (state, action: PayloadAction<'ASC' | 'DESC'>) => { + state.sortOrder = action.payload; + }, + setSort: (state, action: PayloadAction<{ field: string; order: 'ASC' | 'DESC' }>) => { + state.sortField = action.payload.field; + state.sortOrder = action.payload.order; + }, resetTaskManagement: state => { state.loading = false; state.error = null; @@ -745,6 +762,8 @@ const taskManagementSlice = createSlice({ state.selectedPriorities = []; state.search = ''; state.archived = false; + state.sortField = ''; + state.sortOrder = 'ASC'; state.ids = []; state.entities = {}; }, @@ -1129,6 +1148,9 @@ export const { setSearch, setArchived, toggleArchived, + setSortField, + setSortOrder, + setSort, resetTaskManagement, toggleTaskExpansion, addSubtaskToParent, @@ -1160,6 +1182,9 @@ export const selectLoading = (state: RootState) => state.taskManagement.loading; export const selectError = (state: RootState) => state.taskManagement.error; export const selectSelectedPriorities = (state: RootState) => state.taskManagement.selectedPriorities; export const selectSearch = (state: RootState) => state.taskManagement.search; +export const selectSortField = (state: RootState) => state.taskManagement.sortField; +export const selectSortOrder = (state: RootState) => state.taskManagement.sortOrder; +export const selectSort = (state: RootState) => ({ field: state.taskManagement.sortField, order: state.taskManagement.sortOrder }); export const selectSubtaskLoading = (state: RootState, taskId: string) => state.taskManagement.loadingSubtasks[taskId] || false; // Memoized selectors to prevent unnecessary re-renders diff --git a/worklenz-frontend/src/types/task-management.types.ts b/worklenz-frontend/src/types/task-management.types.ts index d6942d0b..cd944a50 100644 --- a/worklenz-frontend/src/types/task-management.types.ts +++ b/worklenz-frontend/src/types/task-management.types.ts @@ -114,6 +114,9 @@ export interface TaskManagementState { loadingColumns: boolean; columns: ITaskListColumn[]; customColumns: ITaskListColumn[]; + // Add sort-related state + sortField: string; + sortOrder: 'ASC' | 'DESC'; } export interface TaskGroupsState {