feat(task-management): enhance task grouping and localization support
- Implemented unmapped task grouping for better organization of tasks without valid phases. - Updated task distribution logic to handle unmapped tasks and added a corresponding group in the response. - Enhanced localization by adding translations for "noTasksInGroup" in multiple languages. - Improved task list components to support custom columns and better task management features. - Refactored task management slice to include loading states for columns and custom columns.
This commit is contained in:
@@ -1135,22 +1135,61 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Distribute tasks into groups
|
// Distribute tasks into groups
|
||||||
|
const unmappedTasks: any[] = [];
|
||||||
|
|
||||||
transformedTasks.forEach(task => {
|
transformedTasks.forEach(task => {
|
||||||
let groupKey: string;
|
let groupKey: string;
|
||||||
|
let taskAssigned = false;
|
||||||
|
|
||||||
if (groupBy === GroupBy.STATUS) {
|
if (groupBy === GroupBy.STATUS) {
|
||||||
groupKey = task.status;
|
groupKey = task.status;
|
||||||
|
if (groupedResponse[groupKey]) {
|
||||||
|
groupedResponse[groupKey].tasks.push(task);
|
||||||
|
groupedResponse[groupKey].taskIds.push(task.id);
|
||||||
|
taskAssigned = true;
|
||||||
|
}
|
||||||
} else if (groupBy === GroupBy.PRIORITY) {
|
} else if (groupBy === GroupBy.PRIORITY) {
|
||||||
groupKey = task.priority;
|
groupKey = task.priority;
|
||||||
} else {
|
if (groupedResponse[groupKey]) {
|
||||||
groupKey = task.phase.toLowerCase().replace(/\s+/g, "_");
|
groupedResponse[groupKey].tasks.push(task);
|
||||||
}
|
groupedResponse[groupKey].taskIds.push(task.id);
|
||||||
|
taskAssigned = true;
|
||||||
if (groupedResponse[groupKey]) {
|
}
|
||||||
groupedResponse[groupKey].tasks.push(task);
|
} else if (groupBy === GroupBy.PHASE) {
|
||||||
groupedResponse[groupKey].taskIds.push(task.id);
|
// For phase grouping, check if task has a valid phase
|
||||||
|
if (task.phase && task.phase.trim() !== "") {
|
||||||
|
groupKey = task.phase.toLowerCase().replace(/\s+/g, "_");
|
||||||
|
if (groupedResponse[groupKey]) {
|
||||||
|
groupedResponse[groupKey].tasks.push(task);
|
||||||
|
groupedResponse[groupKey].taskIds.push(task.id);
|
||||||
|
taskAssigned = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If task doesn't have a valid phase, add to unmapped
|
||||||
|
if (!taskAssigned) {
|
||||||
|
unmappedTasks.push(task);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create unmapped group if there are tasks without proper phase assignment
|
||||||
|
if (unmappedTasks.length > 0 && groupBy === GroupBy.PHASE) {
|
||||||
|
groupedResponse[UNMAPPED.toLowerCase()] = {
|
||||||
|
id: UNMAPPED,
|
||||||
|
title: UNMAPPED,
|
||||||
|
groupType: groupBy,
|
||||||
|
groupValue: UNMAPPED.toLowerCase(),
|
||||||
|
collapsed: false,
|
||||||
|
tasks: unmappedTasks,
|
||||||
|
taskIds: unmappedTasks.map(task => task.id),
|
||||||
|
color: "#fbc84c69", // Orange color with transparency
|
||||||
|
category_id: null,
|
||||||
|
start_date: null,
|
||||||
|
end_date: null,
|
||||||
|
sort_index: 999, // Put unmapped group at the end
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Sort tasks within each group by order
|
// Sort tasks within each group by order
|
||||||
Object.values(groupedResponse).forEach((group: any) => {
|
Object.values(groupedResponse).forEach((group: any) => {
|
||||||
group.tasks.sort((a: any, b: any) => a.order - b.order);
|
group.tasks.sort((a: any, b: any) => a.order - b.order);
|
||||||
@@ -1168,6 +1207,11 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
return groupedResponse[groupKey];
|
return groupedResponse[groupKey];
|
||||||
})
|
})
|
||||||
.filter(group => group && (group.tasks.length > 0 || req.query.include_empty === "true"));
|
.filter(group => group && (group.tasks.length > 0 || req.query.include_empty === "true"));
|
||||||
|
|
||||||
|
// Add unmapped group to the end if it exists
|
||||||
|
if (groupedResponse[UNMAPPED.toLowerCase()]) {
|
||||||
|
responseGroups.push(groupedResponse[UNMAPPED.toLowerCase()]);
|
||||||
|
}
|
||||||
|
|
||||||
const groupingEndTime = performance.now();
|
const groupingEndTime = performance.now();
|
||||||
|
|
||||||
@@ -1183,15 +1227,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
groups: responseGroups,
|
groups: responseGroups,
|
||||||
allTasks: transformedTasks,
|
allTasks: transformedTasks,
|
||||||
grouping: groupBy,
|
grouping: groupBy,
|
||||||
totalTasks: transformedTasks.length,
|
totalTasks: transformedTasks.length
|
||||||
performanceMetrics: {
|
|
||||||
totalTime: Math.round(totalTime),
|
|
||||||
queryTime: Math.round(queryEndTime - queryStartTime),
|
|
||||||
transformTime: Math.round(transformEndTime - transformStartTime),
|
|
||||||
groupingTime: Math.round(groupingEndTime - groupingStartTime),
|
|
||||||
progressRefreshTime: shouldRefreshProgress ? Math.round(queryStartTime - startTime) : 0,
|
|
||||||
taskCount: transformedTasks.length
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1213,6 +1249,7 @@ export default class TasksControllerV2 extends TasksControllerBase {
|
|||||||
development: "#1890ff",
|
development: "#1890ff",
|
||||||
testing: "#faad14",
|
testing: "#faad14",
|
||||||
deployment: "#52c41a",
|
deployment: "#52c41a",
|
||||||
|
unmapped: "#fbc84c69",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
"addTaskText": "Shto Detyrë",
|
"addTaskText": "Shto Detyrë",
|
||||||
"addSubTaskText": "+ Shto Nën-Detyrë",
|
"addSubTaskText": "+ Shto Nën-Detyrë",
|
||||||
|
"noTasksInGroup": "Nuk ka detyra në këtë grup",
|
||||||
"addTaskInputPlaceholder": "Shkruaj detyrën dhe shtyp Enter",
|
"addTaskInputPlaceholder": "Shkruaj detyrën dhe shtyp Enter",
|
||||||
|
|
||||||
"openButton": "Hap",
|
"openButton": "Hap",
|
||||||
@@ -65,5 +66,47 @@
|
|||||||
"clearDueDate": "Pastro datën e afatit",
|
"clearDueDate": "Pastro datën e afatit",
|
||||||
"clearStartDate": "Pastro datën e fillimit",
|
"clearStartDate": "Pastro datën e fillimit",
|
||||||
"dueDatePlaceholder": "Data e afatit",
|
"dueDatePlaceholder": "Data e afatit",
|
||||||
"startDatePlaceholder": "Data e fillimit"
|
"startDatePlaceholder": "Data e fillimit",
|
||||||
|
|
||||||
|
"customColumns": {
|
||||||
|
"addCustomColumn": "Shto një kolonë të personalizuar",
|
||||||
|
"customColumnHeader": "Kolona e Personalizuar",
|
||||||
|
"customColumnSettings": "Cilësimet e kolonës së personalizuar",
|
||||||
|
"noCustomValue": "Asnjë vlerë",
|
||||||
|
"peopleField": "Fusha e njerëzve",
|
||||||
|
"noDate": "Asnjë datë",
|
||||||
|
"unsupportedField": "Lloj fushe i pambështetur",
|
||||||
|
|
||||||
|
"modal": {
|
||||||
|
"addFieldTitle": "Shto fushë",
|
||||||
|
"editFieldTitle": "Redakto fushën",
|
||||||
|
"fieldTitle": "Titulli i fushës",
|
||||||
|
"fieldTitleRequired": "Titulli i fushës është i kërkuar",
|
||||||
|
"columnTitlePlaceholder": "Titulli i kolonës",
|
||||||
|
"type": "Lloji",
|
||||||
|
"deleteConfirmTitle": "Jeni i sigurt që doni të fshini këtë kolonë të personalizuar?",
|
||||||
|
"deleteConfirmDescription": "Kjo veprim nuk mund të zhbëhet. Të gjitha të dhënat e lidhura me këtë kolonë do të fshihen përgjithmonë.",
|
||||||
|
"deleteButton": "Fshi",
|
||||||
|
"cancelButton": "Anulo",
|
||||||
|
"createButton": "Krijo",
|
||||||
|
"updateButton": "Përditëso",
|
||||||
|
"createSuccessMessage": "Kolona e personalizuar u krijua me sukses",
|
||||||
|
"updateSuccessMessage": "Kolona e personalizuar u përditësua me sukses",
|
||||||
|
"deleteSuccessMessage": "Kolona e personalizuar u fshi me sukses",
|
||||||
|
"deleteErrorMessage": "Dështoi në fshirjen e kolonës së personalizuar",
|
||||||
|
"createErrorMessage": "Dështoi në krijimin e kolonës së personalizuar",
|
||||||
|
"updateErrorMessage": "Dështoi në përditësimin e kolonës së personalizuar"
|
||||||
|
},
|
||||||
|
|
||||||
|
"fieldTypes": {
|
||||||
|
"people": "Njerëz",
|
||||||
|
"number": "Numër",
|
||||||
|
"date": "Data",
|
||||||
|
"selection": "Zgjedhje",
|
||||||
|
"checkbox": "Kutia e kontrollit",
|
||||||
|
"labels": "Etiketat",
|
||||||
|
"key": "Çelësi",
|
||||||
|
"formula": "Formula"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"addTaskText": "Aufgabe hinzufügen",
|
"addTaskText": "Aufgabe hinzufügen",
|
||||||
"addSubTaskText": "+ Unteraufgabe hinzufügen",
|
"addSubTaskText": "+ Unteraufgabe hinzufügen",
|
||||||
"addTaskInputPlaceholder": "Aufgabe eingeben und Enter drücken",
|
"addTaskInputPlaceholder": "Aufgabe eingeben und Enter drücken",
|
||||||
|
"noTasksInGroup": "Keine Aufgaben in dieser Gruppe",
|
||||||
|
|
||||||
"openButton": "Öffnen",
|
"openButton": "Öffnen",
|
||||||
"okButton": "OK",
|
"okButton": "OK",
|
||||||
@@ -65,5 +66,47 @@
|
|||||||
"clearDueDate": "Fälligkeitsdatum löschen",
|
"clearDueDate": "Fälligkeitsdatum löschen",
|
||||||
"clearStartDate": "Startdatum löschen",
|
"clearStartDate": "Startdatum löschen",
|
||||||
"dueDatePlaceholder": "Fälligkeitsdatum",
|
"dueDatePlaceholder": "Fälligkeitsdatum",
|
||||||
"startDatePlaceholder": "Startdatum"
|
"startDatePlaceholder": "Startdatum",
|
||||||
|
|
||||||
|
"customColumns": {
|
||||||
|
"addCustomColumn": "Benutzerdefinierte Spalte hinzufügen",
|
||||||
|
"customColumnHeader": "Benutzerdefinierte Spalte",
|
||||||
|
"customColumnSettings": "Einstellungen für benutzerdefinierte Spalte",
|
||||||
|
"noCustomValue": "Kein Wert",
|
||||||
|
"peopleField": "Personenfeld",
|
||||||
|
"noDate": "Kein Datum",
|
||||||
|
"unsupportedField": "Nicht unterstützter Feldtyp",
|
||||||
|
|
||||||
|
"modal": {
|
||||||
|
"addFieldTitle": "Feld hinzufügen",
|
||||||
|
"editFieldTitle": "Feld bearbeiten",
|
||||||
|
"fieldTitle": "Feldtitel",
|
||||||
|
"fieldTitleRequired": "Feldtitel ist erforderlich",
|
||||||
|
"columnTitlePlaceholder": "Spaltentitel",
|
||||||
|
"type": "Typ",
|
||||||
|
"deleteConfirmTitle": "Sind Sie sicher, dass Sie diese benutzerdefinierte Spalte löschen möchten?",
|
||||||
|
"deleteConfirmDescription": "Diese Aktion kann nicht rückgängig gemacht werden. Alle mit dieser Spalte verbundenen Daten werden dauerhaft gelöscht.",
|
||||||
|
"deleteButton": "Löschen",
|
||||||
|
"cancelButton": "Abbrechen",
|
||||||
|
"createButton": "Erstellen",
|
||||||
|
"updateButton": "Aktualisieren",
|
||||||
|
"createSuccessMessage": "Benutzerdefinierte Spalte erfolgreich erstellt",
|
||||||
|
"updateSuccessMessage": "Benutzerdefinierte Spalte erfolgreich aktualisiert",
|
||||||
|
"deleteSuccessMessage": "Benutzerdefinierte Spalte erfolgreich gelöscht",
|
||||||
|
"deleteErrorMessage": "Fehler beim Löschen der benutzerdefinierten Spalte",
|
||||||
|
"createErrorMessage": "Fehler beim Erstellen der benutzerdefinierten Spalte",
|
||||||
|
"updateErrorMessage": "Fehler beim Aktualisieren der benutzerdefinierten Spalte"
|
||||||
|
},
|
||||||
|
|
||||||
|
"fieldTypes": {
|
||||||
|
"people": "Personen",
|
||||||
|
"number": "Zahl",
|
||||||
|
"date": "Datum",
|
||||||
|
"selection": "Auswahl",
|
||||||
|
"checkbox": "Kontrollkästchen",
|
||||||
|
"labels": "Etiketten",
|
||||||
|
"key": "Schlüssel",
|
||||||
|
"formula": "Formel"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"addTaskText": "Add Task",
|
"addTaskText": "Add Task",
|
||||||
"addSubTaskText": "Add Sub Task",
|
"addSubTaskText": "Add Sub Task",
|
||||||
"addTaskInputPlaceholder": "Type your task and hit enter",
|
"addTaskInputPlaceholder": "Type your task and hit enter",
|
||||||
|
"noTasksInGroup": "No tasks in this group",
|
||||||
|
|
||||||
"openButton": "Open",
|
"openButton": "Open",
|
||||||
"okButton": "Ok",
|
"okButton": "Ok",
|
||||||
@@ -65,5 +66,47 @@
|
|||||||
"clearDueDate": "Clear due date",
|
"clearDueDate": "Clear due date",
|
||||||
"clearStartDate": "Clear start date",
|
"clearStartDate": "Clear start date",
|
||||||
"dueDatePlaceholder": "Due Date",
|
"dueDatePlaceholder": "Due Date",
|
||||||
"startDatePlaceholder": "Start Date"
|
"startDatePlaceholder": "Start Date",
|
||||||
|
|
||||||
|
"customColumns": {
|
||||||
|
"addCustomColumn": "Add a custom column",
|
||||||
|
"customColumnHeader": "Custom Column",
|
||||||
|
"customColumnSettings": "Custom column settings",
|
||||||
|
"noCustomValue": "No value",
|
||||||
|
"peopleField": "People field",
|
||||||
|
"noDate": "No date",
|
||||||
|
"unsupportedField": "Unsupported field type",
|
||||||
|
|
||||||
|
"modal": {
|
||||||
|
"addFieldTitle": "Add field",
|
||||||
|
"editFieldTitle": "Edit field",
|
||||||
|
"fieldTitle": "Field title",
|
||||||
|
"fieldTitleRequired": "Field title is required",
|
||||||
|
"columnTitlePlaceholder": "Column title",
|
||||||
|
"type": "Type",
|
||||||
|
"deleteConfirmTitle": "Are you sure you want to delete this custom column?",
|
||||||
|
"deleteConfirmDescription": "This action cannot be undone. All data associated with this column will be permanently deleted.",
|
||||||
|
"deleteButton": "Delete",
|
||||||
|
"cancelButton": "Cancel",
|
||||||
|
"createButton": "Create",
|
||||||
|
"updateButton": "Update",
|
||||||
|
"createSuccessMessage": "Custom column created successfully",
|
||||||
|
"updateSuccessMessage": "Custom column updated successfully",
|
||||||
|
"deleteSuccessMessage": "Custom column deleted successfully",
|
||||||
|
"deleteErrorMessage": "Failed to delete custom column",
|
||||||
|
"createErrorMessage": "Failed to create custom column",
|
||||||
|
"updateErrorMessage": "Failed to update custom column"
|
||||||
|
},
|
||||||
|
|
||||||
|
"fieldTypes": {
|
||||||
|
"people": "People",
|
||||||
|
"number": "Number",
|
||||||
|
"date": "Date",
|
||||||
|
"selection": "Selection",
|
||||||
|
"checkbox": "Checkbox",
|
||||||
|
"labels": "Labels",
|
||||||
|
"key": "Key",
|
||||||
|
"formula": "Formula"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
"addTaskText": "Agregar tarea",
|
"addTaskText": "Agregar tarea",
|
||||||
"addSubTaskText": "Agregar subtarea",
|
"addSubTaskText": "Agregar subtarea",
|
||||||
|
"noTasksInGroup": "No hay tareas en este grupo",
|
||||||
"addTaskInputPlaceholder": "Escribe tu tarea y presiona enter",
|
"addTaskInputPlaceholder": "Escribe tu tarea y presiona enter",
|
||||||
|
|
||||||
"openButton": "Abrir",
|
"openButton": "Abrir",
|
||||||
@@ -65,5 +66,47 @@
|
|||||||
"clearDueDate": "Limpiar fecha de vencimiento",
|
"clearDueDate": "Limpiar fecha de vencimiento",
|
||||||
"clearStartDate": "Limpiar fecha de inicio",
|
"clearStartDate": "Limpiar fecha de inicio",
|
||||||
"dueDatePlaceholder": "Fecha de vencimiento",
|
"dueDatePlaceholder": "Fecha de vencimiento",
|
||||||
"startDatePlaceholder": "Fecha de inicio"
|
"startDatePlaceholder": "Fecha de inicio",
|
||||||
|
|
||||||
|
"customColumns": {
|
||||||
|
"addCustomColumn": "Agregar una columna personalizada",
|
||||||
|
"customColumnHeader": "Columna Personalizada",
|
||||||
|
"customColumnSettings": "Configuración de columna personalizada",
|
||||||
|
"noCustomValue": "Sin valor",
|
||||||
|
"peopleField": "Campo de personas",
|
||||||
|
"noDate": "Sin fecha",
|
||||||
|
"unsupportedField": "Tipo de campo no compatible",
|
||||||
|
|
||||||
|
"modal": {
|
||||||
|
"addFieldTitle": "Agregar campo",
|
||||||
|
"editFieldTitle": "Editar campo",
|
||||||
|
"fieldTitle": "Título del campo",
|
||||||
|
"fieldTitleRequired": "El título del campo es obligatorio",
|
||||||
|
"columnTitlePlaceholder": "Título de la columna",
|
||||||
|
"type": "Tipo",
|
||||||
|
"deleteConfirmTitle": "¿Está seguro de que desea eliminar esta columna personalizada?",
|
||||||
|
"deleteConfirmDescription": "Esta acción no se puede deshacer. Todos los datos asociados con esta columna se eliminarán permanentemente.",
|
||||||
|
"deleteButton": "Eliminar",
|
||||||
|
"cancelButton": "Cancelar",
|
||||||
|
"createButton": "Crear",
|
||||||
|
"updateButton": "Actualizar",
|
||||||
|
"createSuccessMessage": "Columna personalizada creada exitosamente",
|
||||||
|
"updateSuccessMessage": "Columna personalizada actualizada exitosamente",
|
||||||
|
"deleteSuccessMessage": "Columna personalizada eliminada exitosamente",
|
||||||
|
"deleteErrorMessage": "Error al eliminar la columna personalizada",
|
||||||
|
"createErrorMessage": "Error al crear la columna personalizada",
|
||||||
|
"updateErrorMessage": "Error al actualizar la columna personalizada"
|
||||||
|
},
|
||||||
|
|
||||||
|
"fieldTypes": {
|
||||||
|
"people": "Personas",
|
||||||
|
"number": "Número",
|
||||||
|
"date": "Fecha",
|
||||||
|
"selection": "Selección",
|
||||||
|
"checkbox": "Casilla de verificación",
|
||||||
|
"labels": "Etiquetas",
|
||||||
|
"key": "Clave",
|
||||||
|
"formula": "Fórmula"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
"addTaskText": "Adicionar Tarefa",
|
"addTaskText": "Adicionar Tarefa",
|
||||||
"addSubTaskText": "+ Adicionar Subtarefa",
|
"addSubTaskText": "+ Adicionar Subtarefa",
|
||||||
|
"noTasksInGroup": "Nenhuma tarefa neste grupo",
|
||||||
"addTaskInputPlaceholder": "Digite sua tarefa e pressione enter",
|
"addTaskInputPlaceholder": "Digite sua tarefa e pressione enter",
|
||||||
|
|
||||||
"openButton": "Abrir",
|
"openButton": "Abrir",
|
||||||
@@ -65,5 +66,47 @@
|
|||||||
"clearDueDate": "Limpar data de vencimento",
|
"clearDueDate": "Limpar data de vencimento",
|
||||||
"clearStartDate": "Limpar data de início",
|
"clearStartDate": "Limpar data de início",
|
||||||
"dueDatePlaceholder": "Data de vencimento",
|
"dueDatePlaceholder": "Data de vencimento",
|
||||||
"startDatePlaceholder": "Data de início"
|
"startDatePlaceholder": "Data de início",
|
||||||
|
|
||||||
|
"customColumns": {
|
||||||
|
"addCustomColumn": "Adicionar uma coluna personalizada",
|
||||||
|
"customColumnHeader": "Coluna Personalizada",
|
||||||
|
"customColumnSettings": "Configurações da coluna personalizada",
|
||||||
|
"noCustomValue": "Sem valor",
|
||||||
|
"peopleField": "Campo de pessoas",
|
||||||
|
"noDate": "Sem data",
|
||||||
|
"unsupportedField": "Tipo de campo não suportado",
|
||||||
|
|
||||||
|
"modal": {
|
||||||
|
"addFieldTitle": "Adicionar campo",
|
||||||
|
"editFieldTitle": "Editar campo",
|
||||||
|
"fieldTitle": "Título do campo",
|
||||||
|
"fieldTitleRequired": "O título do campo é obrigatório",
|
||||||
|
"columnTitlePlaceholder": "Título da coluna",
|
||||||
|
"type": "Tipo",
|
||||||
|
"deleteConfirmTitle": "Tem certeza de que deseja excluir esta coluna personalizada?",
|
||||||
|
"deleteConfirmDescription": "Esta ação não pode ser desfeita. Todos os dados associados a esta coluna serão excluídos permanentemente.",
|
||||||
|
"deleteButton": "Excluir",
|
||||||
|
"cancelButton": "Cancelar",
|
||||||
|
"createButton": "Criar",
|
||||||
|
"updateButton": "Atualizar",
|
||||||
|
"createSuccessMessage": "Coluna personalizada criada com sucesso",
|
||||||
|
"updateSuccessMessage": "Coluna personalizada atualizada com sucesso",
|
||||||
|
"deleteSuccessMessage": "Coluna personalizada excluída com sucesso",
|
||||||
|
"deleteErrorMessage": "Falha ao excluir a coluna personalizada",
|
||||||
|
"createErrorMessage": "Falha ao criar a coluna personalizada",
|
||||||
|
"updateErrorMessage": "Falha ao atualizar a coluna personalizada"
|
||||||
|
},
|
||||||
|
|
||||||
|
"fieldTypes": {
|
||||||
|
"people": "Pessoas",
|
||||||
|
"number": "Número",
|
||||||
|
"date": "Data",
|
||||||
|
"selection": "Seleção",
|
||||||
|
"checkbox": "Caixa de seleção",
|
||||||
|
"labels": "Etiquetas",
|
||||||
|
"key": "Chave",
|
||||||
|
"formula": "Fórmula"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
|
|||||||
import { toggleDrawer } from '../../../features/projects/status/StatusSlice';
|
import { toggleDrawer } from '../../../features/projects/status/StatusSlice';
|
||||||
import { colors } from '@/styles/colors';
|
import { colors } from '@/styles/colors';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
|
||||||
const CreateStatusButton = () => {
|
const CreateStatusButton = () => {
|
||||||
const { t } = useTranslation('task-list-filters');
|
const { t } = useTranslation('task-list-filters');
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@@ -19,9 +21,7 @@ const CreateStatusButton = () => {
|
|||||||
onClick={() => dispatch(toggleDrawer())}
|
onClick={() => dispatch(toggleDrawer())}
|
||||||
icon={
|
icon={
|
||||||
<SettingOutlined
|
<SettingOutlined
|
||||||
style={{
|
style={{ color: themeMode === 'dark' ? colors.white : colors.midBlue }}
|
||||||
color: colors.skyBlue,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
className={`inline-flex w-max items-center px-4 py-2 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out border-b border-gray-200 dark:border-gray-700 rounded-t-md ${
|
className={`inline-flex w-max items-center px-4 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out border-b border-gray-200 dark:border-gray-700 rounded-t-md ${
|
||||||
isOver ? 'ring-2 ring-blue-400 ring-opacity-50' : ''
|
isOver ? 'ring-2 ring-blue-400 ring-opacity-50' : ''
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@@ -87,7 +87,10 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
|||||||
color: headerTextColor,
|
color: headerTextColor,
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
zIndex: 20 // Higher than sticky columns (zIndex: 1) and column headers (zIndex: 2)
|
zIndex: 25, // Higher than task rows but lower than column headers (z-30)
|
||||||
|
height: '36px',
|
||||||
|
minHeight: '36px',
|
||||||
|
maxHeight: '36px'
|
||||||
}}
|
}}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
>
|
>
|
||||||
@@ -95,18 +98,22 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
|||||||
<div style={{ width: '32px' }} className="flex items-center justify-center">
|
<div style={{ width: '32px' }} className="flex items-center justify-center">
|
||||||
{/* Chevron button */}
|
{/* Chevron button */}
|
||||||
<button
|
<button
|
||||||
className="p-1 rounded-md hover:bg-opacity-20 transition-colors"
|
className="p-1 rounded-md hover:shadow-lg hover:scale-105 transition-all duration-300 ease-out"
|
||||||
style={{ backgroundColor: headerBackgroundColor, color: headerTextColor, borderColor: headerTextColor, border: '1px solid' }}
|
style={{ backgroundColor: 'transparent', color: headerTextColor }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggle();
|
onToggle();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isCollapsed ? (
|
<div
|
||||||
<ChevronRightIcon className="h-4 w-4" style={{ color: headerTextColor }} />
|
className="transition-transform duration-300 ease-out"
|
||||||
) : (
|
style={{
|
||||||
<ChevronDownIcon className="h-4 w-4" style={{ color: headerTextColor }} />
|
transform: isCollapsed ? 'rotate(0deg)' : 'rotate(90deg)',
|
||||||
)}
|
transformOrigin: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon className="h-3.5 w-3.5" style={{ color: headerTextColor }} />
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -124,12 +131,12 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Group indicator and name */}
|
{/* Group indicator and name */}
|
||||||
<div className="ml-2 flex items-center gap-3 flex-1">
|
<div className="ml-1 flex items-center gap-2 flex-1">
|
||||||
{/* Color indicator (removed as full header is colored) */}
|
{/* Color indicator (removed as full header is colored) */}
|
||||||
|
|
||||||
{/* Group name and count */}
|
{/* Group name and count */}
|
||||||
<div className="flex items-center flex-1">
|
<div className="flex items-center flex-1">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-semibold">
|
||||||
{group.name} ({group.count})
|
{group.name} ({group.count})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import React, { memo, useMemo, useCallback, useState } from 'react';
|
import React, { memo, useMemo, useCallback, useState } from 'react';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { CheckCircleOutlined, HolderOutlined, CloseOutlined, DownOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons';
|
import { CheckCircleOutlined, HolderOutlined, CloseOutlined, DownOutlined, RightOutlined, DoubleRightOutlined, ArrowsAltOutlined } from '@ant-design/icons';
|
||||||
import { Checkbox, DatePicker } from 'antd';
|
import { Checkbox, DatePicker } from 'antd';
|
||||||
import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports';
|
import { dayjs, taskManagementAntdConfig } from '@/shared/antd-imports';
|
||||||
import { Task } from '@/types/task-management.types';
|
import { Task } from '@/types/task-management.types';
|
||||||
@@ -35,6 +35,7 @@ interface TaskRowProps {
|
|||||||
isSticky?: boolean;
|
isSticky?: boolean;
|
||||||
}>;
|
}>;
|
||||||
isSubtask?: boolean;
|
isSubtask?: boolean;
|
||||||
|
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskLabelsCellProps {
|
interface TaskLabelsCellProps {
|
||||||
@@ -91,7 +92,7 @@ const formatDate = (dateString: string): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumns, isSubtask = false }) => {
|
const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumns, isSubtask = false, updateTaskCustomColumnValue }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||||
const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId));
|
const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId));
|
||||||
@@ -277,7 +278,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
case 'taskKey':
|
case 'taskKey':
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center" style={baseStyle}>
|
<div className="flex items-center" style={baseStyle}>
|
||||||
<span className="text-sm font-medium text-gray-900 dark:text-white whitespace-nowrap">
|
<span className="text-sm font-medium text-gray-900 dark:text-white whitespace-nowrap badge badge-primary">
|
||||||
{task.task_key || 'N/A'}
|
{task.task_key || 'N/A'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -294,21 +295,21 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
{!isSubtask && (
|
{!isSubtask && (
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleExpansion}
|
onClick={handleToggleExpansion}
|
||||||
className={`flex h-4 w-4 items-center justify-center rounded-sm text-xs mr-2 hover:border hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors ${
|
className={`flex h-4 w-4 items-center justify-center rounded-sm text-xs mr-2 hover:border hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:scale-110 transition-all duration-300 ease-out ${
|
||||||
task.sub_tasks_count && task.sub_tasks_count > 0
|
task.sub_tasks_count && task.sub_tasks_count > 0
|
||||||
? 'opacity-100'
|
? 'opacity-100'
|
||||||
: 'opacity-0 group-hover:opacity-100'
|
: 'opacity-0 group-hover:opacity-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{task.sub_tasks_count && task.sub_tasks_count > 0 ? (
|
<div
|
||||||
task.show_sub_tasks ? (
|
className="transition-transform duration-300 ease-out"
|
||||||
<DownOutlined className="text-gray-600 dark:text-gray-400" />
|
style={{
|
||||||
) : (
|
transform: task.show_sub_tasks ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||||
<RightOutlined className="text-gray-600 dark:text-gray-400" />
|
transformOrigin: 'center'
|
||||||
)
|
}}
|
||||||
) : (
|
>
|
||||||
<RightOutlined className="text-gray-600 dark:text-gray-400" />
|
<RightOutlined className="text-gray-600 dark:text-gray-400" />
|
||||||
)}
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -333,13 +334,14 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity duration-200 ml-2 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 border-none bg-transparent cursor-pointer"
|
className="opacity-0 group-hover:opacity-100 transition-all duration-200 ml-2 mr-2 px-3 py-1.5 text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 cursor-pointer rounded-md shadow-sm hover:shadow-md flex items-center gap-1"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dispatch(setSelectedTaskId(task.id));
|
dispatch(setSelectedTaskId(task.id));
|
||||||
dispatch(setShowTaskDrawer(true));
|
dispatch(setShowTaskDrawer(true));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<ArrowsAltOutlined />
|
||||||
{t('openButton')}
|
{t('openButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -642,9 +644,11 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
|
|||||||
isDragging ? 'shadow-lg border border-blue-300' : ''
|
isDragging ? 'shadow-lg border border-blue-300' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{visibleColumns.map((column, index) =>
|
{visibleColumns.map((column, index) => (
|
||||||
renderColumn(column.id, column.width, column.isSticky, index)
|
<React.Fragment key={column.id}>
|
||||||
)}
|
{renderColumn(column.id, column.width, column.isSticky, index)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface TaskRowWithSubtasksProps {
|
|||||||
width: string;
|
width: string;
|
||||||
isSticky?: boolean;
|
isSticky?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddSubtaskRowProps {
|
interface AddSubtaskRowProps {
|
||||||
@@ -140,7 +141,8 @@ AddSubtaskRow.displayName = 'AddSubtaskRow';
|
|||||||
const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
||||||
taskId,
|
taskId,
|
||||||
projectId,
|
projectId,
|
||||||
visibleColumns
|
visibleColumns,
|
||||||
|
updateTaskCustomColumnValue
|
||||||
}) => {
|
}) => {
|
||||||
const task = useAppSelector(state => selectTaskById(state, taskId));
|
const task = useAppSelector(state => selectTaskById(state, taskId));
|
||||||
const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId));
|
const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId));
|
||||||
@@ -162,6 +164,7 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
|||||||
taskId={taskId}
|
taskId={taskId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
visibleColumns={visibleColumns}
|
visibleColumns={visibleColumns}
|
||||||
|
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Subtasks and add subtask row when expanded */}
|
{/* Subtasks and add subtask row when expanded */}
|
||||||
@@ -182,6 +185,7 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
visibleColumns={visibleColumns}
|
visibleColumns={visibleColumns}
|
||||||
isSubtask={true}
|
isSubtask={true}
|
||||||
|
updateTaskCustomColumnValue={updateTaskCustomColumnValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -443,11 +443,11 @@ const FilterDropdown: React.FC<{
|
|||||||
${
|
${
|
||||||
selectedCount > 0
|
selectedCount > 0
|
||||||
? isDarkMode
|
? isDarkMode
|
||||||
? 'bg-blue-600 text-white border-blue-500'
|
? 'bg-gray-600 text-white border-gray-500'
|
||||||
: 'bg-blue-50 text-blue-800 border-blue-300 font-semibold'
|
: 'bg-blue-50 text-blue-800 border-blue-300 font-semibold'
|
||||||
: `${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText}`
|
: `${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText}`
|
||||||
}
|
}
|
||||||
hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2
|
||||||
${isDarkMode ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'}
|
${isDarkMode ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'}
|
||||||
`}
|
`}
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
@@ -456,7 +456,7 @@ const FilterDropdown: React.FC<{
|
|||||||
<IconComponent className="w-3.5 h-3.5" />
|
<IconComponent className="w-3.5 h-3.5" />
|
||||||
<span>{section.label}</span>
|
<span>{section.label}</span>
|
||||||
{selectedCount > 0 && (
|
{selectedCount > 0 && (
|
||||||
<span className="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-500 rounded-full">
|
<span className="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-gray-500 rounded-full">
|
||||||
{selectedCount}
|
{selectedCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -518,7 +518,7 @@ const FilterDropdown: React.FC<{
|
|||||||
${
|
${
|
||||||
isSelected
|
isSelected
|
||||||
? isDarkMode
|
? isDarkMode
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-gray-600 text-white'
|
||||||
: 'bg-blue-50 text-blue-800 font-semibold'
|
: 'bg-blue-50 text-blue-800 font-semibold'
|
||||||
: `${themeClasses.optionText} ${themeClasses.optionHover}`
|
: `${themeClasses.optionText} ${themeClasses.optionHover}`
|
||||||
}
|
}
|
||||||
@@ -530,7 +530,7 @@ const FilterDropdown: React.FC<{
|
|||||||
flex items-center justify-center w-3.5 h-3.5 border rounded
|
flex items-center justify-center w-3.5 h-3.5 border rounded
|
||||||
${
|
${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-blue-500 border-blue-500 text-white'
|
? 'bg-gray-600 border-gray-800 text-white'
|
||||||
: 'border-gray-300 dark:border-gray-600'
|
: 'border-gray-300 dark:border-gray-600'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
@@ -730,7 +730,7 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
|||||||
${
|
${
|
||||||
visibleCount > 0
|
visibleCount > 0
|
||||||
? isDarkMode
|
? isDarkMode
|
||||||
? 'bg-blue-600 text-white border-blue-500'
|
? 'bg-gray-600 text-white border-gray-500'
|
||||||
: 'bg-blue-50 text-blue-800 border-blue-300 font-semibold'
|
: 'bg-blue-50 text-blue-800 border-blue-300 font-semibold'
|
||||||
: `${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText}`
|
: `${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText}`
|
||||||
}
|
}
|
||||||
@@ -743,7 +743,9 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
|||||||
<EyeOutlined className="w-3.5 h-3.5" />
|
<EyeOutlined className="w-3.5 h-3.5" />
|
||||||
<span>Fields</span>
|
<span>Fields</span>
|
||||||
{visibleCount > 0 && (
|
{visibleCount > 0 && (
|
||||||
<span className="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-500 rounded-full">
|
<span
|
||||||
|
className={`inline-flex items-center justify-center w-4 h-4 text-xs font-bold ${isDarkMode ? 'text-white bg-gray-500' : 'text-gray-800 bg-gray-300'} rounded-full`}
|
||||||
|
>
|
||||||
{visibleCount}
|
{visibleCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -778,8 +780,8 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
|||||||
${
|
${
|
||||||
isSelected
|
isSelected
|
||||||
? isDarkMode
|
? isDarkMode
|
||||||
? 'bg-blue-600 text-white'
|
? 'text-white font-semibold'
|
||||||
: 'bg-blue-50 text-blue-800 font-semibold'
|
: 'text-gray-800 font-semibold'
|
||||||
: `${themeClasses.optionText} ${themeClasses.optionHover}`
|
: `${themeClasses.optionText} ${themeClasses.optionHover}`
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
@@ -790,7 +792,7 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
|
|||||||
flex items-center justify-center w-3.5 h-3.5 border rounded
|
flex items-center justify-center w-3.5 h-3.5 border rounded
|
||||||
${
|
${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-blue-500 border-blue-500 text-white'
|
? 'bg-gray-600 border-gray-600 text-white'
|
||||||
: 'border-gray-300 dark:border-gray-600'
|
: 'border-gray-300 dark:border-gray-600'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const ConfigPhaseButton = () => {
|
|||||||
onClick={() => dispatch(toggleDrawer())}
|
onClick={() => dispatch(toggleDrawer())}
|
||||||
icon={
|
icon={
|
||||||
<SettingOutlined
|
<SettingOutlined
|
||||||
style={{ color: themeMode === 'dark' ? colors.white : colors.skyBlue }}
|
style={{ color: themeMode === 'dark' ? colors.white : 'black' }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { CustomTableColumnsType } from '../taskListColumns/taskColumnsSlice';
|
import { CustomTableColumnsType } from '../taskListColumns/taskColumnsSlice';
|
||||||
import { LabelType } from '../../../../types/label.type';
|
import { LabelType } from '../../../../pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/label-type-column/label-type-column';
|
||||||
import { SelectionType } from '../../../../pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/selection-type-column/selection-type-column';
|
import { SelectionType } from '../../../../pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/selection-type-column/selection-type-column';
|
||||||
|
|
||||||
export type CustomFieldsTypes =
|
export type CustomFieldsTypes =
|
||||||
@@ -21,6 +21,7 @@ type TaskListCustomColumnsState = {
|
|||||||
isCustomColumnModalOpen: boolean;
|
isCustomColumnModalOpen: boolean;
|
||||||
customColumnModalType: 'create' | 'edit';
|
customColumnModalType: 'create' | 'edit';
|
||||||
customColumnId: string | null;
|
customColumnId: string | null;
|
||||||
|
currentColumnData: any | null; // Store the current column data for editing
|
||||||
|
|
||||||
customFieldType: CustomFieldsTypes;
|
customFieldType: CustomFieldsTypes;
|
||||||
customFieldNumberType: CustomFieldNumberTypes;
|
customFieldNumberType: CustomFieldNumberTypes;
|
||||||
@@ -39,6 +40,7 @@ const initialState: TaskListCustomColumnsState = {
|
|||||||
isCustomColumnModalOpen: false,
|
isCustomColumnModalOpen: false,
|
||||||
customColumnModalType: 'create',
|
customColumnModalType: 'create',
|
||||||
customColumnId: null,
|
customColumnId: null,
|
||||||
|
currentColumnData: null,
|
||||||
|
|
||||||
customFieldType: 'people',
|
customFieldType: 'people',
|
||||||
customFieldNumberType: 'formatted',
|
customFieldNumberType: 'formatted',
|
||||||
@@ -62,10 +64,11 @@ const taskListCustomColumnsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setCustomColumnModalAttributes: (
|
setCustomColumnModalAttributes: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ modalType: 'create' | 'edit'; columnId: string | null }>
|
action: PayloadAction<{ modalType: 'create' | 'edit'; columnId: string | null; columnData?: any }>
|
||||||
) => {
|
) => {
|
||||||
state.customColumnModalType = action.payload.modalType;
|
state.customColumnModalType = action.payload.modalType;
|
||||||
state.customColumnId = action.payload.columnId;
|
state.customColumnId = action.payload.columnId;
|
||||||
|
state.currentColumnData = action.payload.columnData || null;
|
||||||
},
|
},
|
||||||
setCustomFieldType: (state, action: PayloadAction<CustomFieldsTypes>) => {
|
setCustomFieldType: (state, action: PayloadAction<CustomFieldsTypes>) => {
|
||||||
state.customFieldType = action.payload;
|
state.customFieldType = action.payload;
|
||||||
@@ -98,7 +101,19 @@ const taskListCustomColumnsSlice = createSlice({
|
|||||||
state.selectionsList = action.payload;
|
state.selectionsList = action.payload;
|
||||||
},
|
},
|
||||||
resetCustomFieldValues: state => {
|
resetCustomFieldValues: state => {
|
||||||
state = initialState;
|
// Reset all field values to initial state while keeping modal state
|
||||||
|
state.customFieldType = initialState.customFieldType;
|
||||||
|
state.customFieldNumberType = initialState.customFieldNumberType;
|
||||||
|
state.decimals = initialState.decimals;
|
||||||
|
state.label = initialState.label;
|
||||||
|
state.labelPosition = initialState.labelPosition;
|
||||||
|
state.previewValue = initialState.previewValue;
|
||||||
|
state.expression = initialState.expression;
|
||||||
|
state.firstNumericColumn = initialState.firstNumericColumn;
|
||||||
|
state.secondNumericColumn = initialState.secondNumericColumn;
|
||||||
|
state.labelsList = initialState.labelsList;
|
||||||
|
state.selectionsList = initialState.selectionsList;
|
||||||
|
state.currentColumnData = initialState.currentColumnData;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -137,6 +137,14 @@ export const selectTaskGroups = createSelector(
|
|||||||
tasks.map(task => {
|
tasks.map(task => {
|
||||||
if (currentGrouping === 'status') return task.status;
|
if (currentGrouping === 'status') return task.status;
|
||||||
if (currentGrouping === 'priority') return task.priority;
|
if (currentGrouping === 'priority') return task.priority;
|
||||||
|
if (currentGrouping === 'phase') {
|
||||||
|
// For phase grouping, use 'Unmapped' for tasks without a phase
|
||||||
|
if (!task.phase || task.phase.trim() === '') {
|
||||||
|
return 'Unmapped';
|
||||||
|
} else {
|
||||||
|
return task.phase;
|
||||||
|
}
|
||||||
|
}
|
||||||
return task.phase;
|
return task.phase;
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
@@ -148,6 +156,13 @@ export const selectTaskGroups = createSelector(
|
|||||||
.filter(task => {
|
.filter(task => {
|
||||||
if (currentGrouping === 'status') return task.status === value;
|
if (currentGrouping === 'status') return task.status === value;
|
||||||
if (currentGrouping === 'priority') return task.priority === value;
|
if (currentGrouping === 'priority') return task.priority === value;
|
||||||
|
if (currentGrouping === 'phase') {
|
||||||
|
if (value === 'Unmapped') {
|
||||||
|
return !task.phase || task.phase.trim() === '';
|
||||||
|
} else {
|
||||||
|
return task.phase === value;
|
||||||
|
}
|
||||||
|
}
|
||||||
return task.phase === value;
|
return task.phase === value;
|
||||||
})
|
})
|
||||||
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||||
@@ -178,9 +193,20 @@ export const selectTasksByCurrentGrouping = createSelector(
|
|||||||
|
|
||||||
tasks.forEach(task => {
|
tasks.forEach(task => {
|
||||||
let key: string;
|
let key: string;
|
||||||
if (currentGrouping === 'status') key = task.status;
|
if (currentGrouping === 'status') {
|
||||||
else if (currentGrouping === 'priority') key = task.priority;
|
key = task.status;
|
||||||
else key = task.phase || 'Development';
|
} else if (currentGrouping === 'priority') {
|
||||||
|
key = task.priority;
|
||||||
|
} else if (currentGrouping === 'phase') {
|
||||||
|
// For phase grouping, use 'Unmapped' for tasks without a phase
|
||||||
|
if (!task.phase || task.phase.trim() === '') {
|
||||||
|
key = 'Unmapped';
|
||||||
|
} else {
|
||||||
|
key = task.phase;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
key = task.phase || 'Development';
|
||||||
|
}
|
||||||
|
|
||||||
if (!grouped[key]) grouped[key] = [];
|
if (!grouped[key]) grouped[key] = [];
|
||||||
grouped[key].push(task);
|
grouped[key].push(task);
|
||||||
@@ -214,6 +240,7 @@ const getGroupColor = (groupType: GroupingType, value: string): string => {
|
|||||||
Development: '#1890ff',
|
Development: '#1890ff',
|
||||||
Testing: '#faad14',
|
Testing: '#faad14',
|
||||||
Deployment: '#52c41a',
|
Deployment: '#52c41a',
|
||||||
|
Unmapped: '#fbc84c69',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import {
|
|||||||
EntityId,
|
EntityId,
|
||||||
} from '@reduxjs/toolkit';
|
} from '@reduxjs/toolkit';
|
||||||
import { Task, TaskManagementState, TaskGroup, TaskGrouping } from '@/types/task-management.types';
|
import { Task, TaskManagementState, TaskGroup, TaskGrouping } from '@/types/task-management.types';
|
||||||
|
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
import {
|
import {
|
||||||
tasksApiService,
|
tasksApiService,
|
||||||
ITaskListConfigV2,
|
ITaskListConfigV2,
|
||||||
ITaskListV3Response,
|
ITaskListV3Response,
|
||||||
} from '@/api/tasks/tasks.api.service';
|
} from '@/api/tasks/tasks.api.service';
|
||||||
|
import { tasksCustomColumnsService } from '@/api/tasks/tasks-custom-columns.service';
|
||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
import { DEFAULT_TASK_NAME } from '@/shared/constants';
|
import { DEFAULT_TASK_NAME } from '@/shared/constants';
|
||||||
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
||||||
@@ -56,6 +58,10 @@ const initialState: TaskManagementState = {
|
|||||||
selectedPriorities: [],
|
selectedPriorities: [],
|
||||||
search: '',
|
search: '',
|
||||||
loadingSubtasks: {},
|
loadingSubtasks: {},
|
||||||
|
// Add column-related state
|
||||||
|
loadingColumns: false,
|
||||||
|
columns: [],
|
||||||
|
customColumns: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Async thunk to fetch tasks from API
|
// Async thunk to fetch tasks from API
|
||||||
@@ -435,6 +441,59 @@ export const updateTaskWithSubtasks = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add async thunk to fetch task list columns
|
||||||
|
export const fetchTaskListColumns = createAsyncThunk(
|
||||||
|
'taskManagement/fetchTaskListColumns',
|
||||||
|
async (projectId: string, { dispatch }) => {
|
||||||
|
const [standardColumns, customColumns] = await Promise.all([
|
||||||
|
tasksApiService.fetchTaskListColumns(projectId),
|
||||||
|
dispatch(fetchCustomColumns(projectId)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
standard: standardColumns.body,
|
||||||
|
custom: customColumns.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add async thunk to fetch custom columns
|
||||||
|
export const fetchCustomColumns = createAsyncThunk(
|
||||||
|
'taskManagement/fetchCustomColumns',
|
||||||
|
async (projectId: string, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await tasksCustomColumnsService.getCustomColumns(projectId);
|
||||||
|
return response.body;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Fetch Custom Columns', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return rejectWithValue(error.message);
|
||||||
|
}
|
||||||
|
return rejectWithValue('Failed to fetch custom columns');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add async thunk to update column visibility
|
||||||
|
export const updateColumnVisibility = createAsyncThunk(
|
||||||
|
'taskManagement/updateColumnVisibility',
|
||||||
|
async (
|
||||||
|
{ projectId, item }: { projectId: string; item: ITaskListColumn },
|
||||||
|
{ rejectWithValue }
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await tasksApiService.toggleColumnVisibility(projectId, item);
|
||||||
|
return response.body;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Update Column Visibility', error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return rejectWithValue(error.message);
|
||||||
|
}
|
||||||
|
return rejectWithValue('Failed to update column visibility');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Create the slice
|
// Create the slice
|
||||||
const taskManagementSlice = createSlice({
|
const taskManagementSlice = createSlice({
|
||||||
name: 'taskManagement',
|
name: 'taskManagement',
|
||||||
@@ -627,7 +686,12 @@ const taskManagementSlice = createSlice({
|
|||||||
updatedTask.priority = destinationGroup.id;
|
updatedTask.priority = destinationGroup.id;
|
||||||
break;
|
break;
|
||||||
case IGroupBy.PHASE:
|
case IGroupBy.PHASE:
|
||||||
updatedTask.phase = destinationGroup.id;
|
// Handle unmapped group specially
|
||||||
|
if (destinationGroup.id === 'Unmapped' || destinationGroup.title === 'Unmapped') {
|
||||||
|
updatedTask.phase = ''; // Clear phase for unmapped group
|
||||||
|
} else {
|
||||||
|
updatedTask.phase = destinationGroup.id;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case IGroupBy.MEMBERS:
|
case IGroupBy.MEMBERS:
|
||||||
// If moving to a member group, ensure task is assigned to that member
|
// If moving to a member group, ensure task is assigned to that member
|
||||||
@@ -782,7 +846,57 @@ const taskManagementSlice = createSlice({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Add column-related reducers
|
||||||
|
toggleColumnVisibility: (state, action: PayloadAction<string>) => {
|
||||||
|
const column = state.columns.find(col => col.key === action.payload);
|
||||||
|
if (column) {
|
||||||
|
column.pinned = !column.pinned;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addCustomColumn: (state, action: PayloadAction<ITaskListColumn>) => {
|
||||||
|
state.customColumns.push(action.payload);
|
||||||
|
// Also add to columns array to maintain visibility
|
||||||
|
state.columns.push({
|
||||||
|
...action.payload,
|
||||||
|
pinned: true, // New columns are visible by default
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateCustomColumn: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ key: string; column: ITaskListColumn }>
|
||||||
|
) => {
|
||||||
|
const { key, column } = action.payload;
|
||||||
|
const index = state.customColumns.findIndex(col => col.key === key);
|
||||||
|
if (index !== -1) {
|
||||||
|
state.customColumns[index] = column;
|
||||||
|
// Update in columns array as well
|
||||||
|
const colIndex = state.columns.findIndex(col => col.key === key);
|
||||||
|
if (colIndex !== -1) {
|
||||||
|
state.columns[colIndex] = { ...column, pinned: state.columns[colIndex].pinned };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteCustomColumn: (state, action: PayloadAction<string>) => {
|
||||||
|
const key = action.payload;
|
||||||
|
state.customColumns = state.customColumns.filter(col => col.key !== key);
|
||||||
|
// Remove from columns array as well
|
||||||
|
state.columns = state.columns.filter(col => col.key !== key);
|
||||||
|
},
|
||||||
|
// Add action to sync backend columns with local fields
|
||||||
|
syncColumnsWithFields: (state, action: PayloadAction<{ projectId: string; fields: any[] }>) => {
|
||||||
|
const { fields } = action.payload;
|
||||||
|
// Update columns based on local fields
|
||||||
|
state.columns = state.columns.map(column => {
|
||||||
|
const field = fields.find(f => f.key === column.key);
|
||||||
|
if (field) {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
pinned: field.visible
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: builder => {
|
extraReducers: builder => {
|
||||||
builder
|
builder
|
||||||
@@ -885,6 +999,60 @@ const taskManagementSlice = createSlice({
|
|||||||
state.ids = [];
|
state.ids = [];
|
||||||
state.entities = {};
|
state.entities = {};
|
||||||
state.groups = [];
|
state.groups = [];
|
||||||
|
})
|
||||||
|
// Add column-related extraReducers
|
||||||
|
.addCase(fetchTaskListColumns.pending, state => {
|
||||||
|
state.loadingColumns = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(fetchTaskListColumns.fulfilled, (state, action) => {
|
||||||
|
state.loadingColumns = false;
|
||||||
|
|
||||||
|
// Process standard columns
|
||||||
|
const standardColumns = action.payload.standard;
|
||||||
|
standardColumns.splice(1, 0, {
|
||||||
|
key: 'TASK',
|
||||||
|
name: 'Task',
|
||||||
|
index: 1,
|
||||||
|
pinned: true,
|
||||||
|
});
|
||||||
|
// Process custom columns
|
||||||
|
const customColumns = (action.payload as { custom: any[] }).custom.map((col: any) => ({
|
||||||
|
...col,
|
||||||
|
isCustom: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Merge columns
|
||||||
|
state.columns = [...standardColumns, ...customColumns];
|
||||||
|
state.customColumns = customColumns;
|
||||||
|
})
|
||||||
|
.addCase(fetchTaskListColumns.rejected, (state, action) => {
|
||||||
|
state.loadingColumns = false;
|
||||||
|
state.error = action.error.message || 'Failed to fetch task list columns';
|
||||||
|
})
|
||||||
|
.addCase(fetchCustomColumns.pending, state => {
|
||||||
|
state.loadingColumns = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(fetchCustomColumns.fulfilled, (state, action) => {
|
||||||
|
state.loadingColumns = false;
|
||||||
|
state.customColumns = action.payload;
|
||||||
|
// Add custom columns to the columns array
|
||||||
|
const customColumnsForVisibility = action.payload;
|
||||||
|
state.columns = [...state.columns, ...customColumnsForVisibility];
|
||||||
|
})
|
||||||
|
.addCase(fetchCustomColumns.rejected, (state, action) => {
|
||||||
|
state.loadingColumns = false;
|
||||||
|
state.error = action.error.message || 'Failed to fetch custom columns';
|
||||||
|
})
|
||||||
|
.addCase(updateColumnVisibility.fulfilled, (state, action) => {
|
||||||
|
const column = state.columns.find(col => col.key === action.payload.key);
|
||||||
|
if (column) {
|
||||||
|
column.pinned = action.payload.pinned;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(updateColumnVisibility.rejected, (state, action) => {
|
||||||
|
state.error = action.payload as string;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -913,6 +1081,12 @@ export const {
|
|||||||
updateTaskAssignees,
|
updateTaskAssignees,
|
||||||
createSubtask,
|
createSubtask,
|
||||||
removeTemporarySubtask,
|
removeTemporarySubtask,
|
||||||
|
// Add column-related actions
|
||||||
|
toggleColumnVisibility,
|
||||||
|
addCustomColumn,
|
||||||
|
updateCustomColumn,
|
||||||
|
deleteCustomColumn,
|
||||||
|
syncColumnsWithFields,
|
||||||
} = taskManagementSlice.actions;
|
} = taskManagementSlice.actions;
|
||||||
|
|
||||||
// Export the selectors
|
// Export the selectors
|
||||||
@@ -944,3 +1118,24 @@ export default taskManagementSlice.reducer;
|
|||||||
// V3 API selectors - no processing needed, data is pre-processed by backend
|
// V3 API selectors - no processing needed, data is pre-processed by backend
|
||||||
export const selectTaskGroupsV3 = (state: RootState) => state.taskManagement.groups;
|
export const selectTaskGroupsV3 = (state: RootState) => state.taskManagement.groups;
|
||||||
export const selectCurrentGroupingV3 = (state: RootState) => state.taskManagement.grouping;
|
export const selectCurrentGroupingV3 = (state: RootState) => state.taskManagement.grouping;
|
||||||
|
|
||||||
|
// Column-related selectors
|
||||||
|
export const selectColumns = (state: RootState) => state.taskManagement.columns;
|
||||||
|
export const selectCustomColumns = (state: RootState) => state.taskManagement.customColumns;
|
||||||
|
export const selectLoadingColumns = (state: RootState) => state.taskManagement.loadingColumns;
|
||||||
|
|
||||||
|
// Helper selector to check if columns are in sync with local fields
|
||||||
|
export const selectColumnsInSync = (state: RootState) => {
|
||||||
|
const columns = state.taskManagement.columns;
|
||||||
|
const fields = state.taskManagementFields || [];
|
||||||
|
|
||||||
|
if (columns.length === 0 || fields.length === 0) return true;
|
||||||
|
|
||||||
|
return !fields.some(field => {
|
||||||
|
const backendColumn = columns.find(c => c.key === field.key);
|
||||||
|
if (backendColumn) {
|
||||||
|
return (backendColumn.pinned ?? false) !== field.visible;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -59,13 +59,21 @@ const taskListFieldsSlice = createSlice({
|
|||||||
const field = state.find(f => f.key === action.payload);
|
const field = state.find(f => f.key === action.payload);
|
||||||
if (field) {
|
if (field) {
|
||||||
field.visible = !field.visible;
|
field.visible = !field.visible;
|
||||||
|
// Save to localStorage immediately after toggle
|
||||||
|
saveFields(state);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setFields(state, action: PayloadAction<TaskListField[]>) {
|
setFields(state, action: PayloadAction<TaskListField[]>) {
|
||||||
return action.payload;
|
const newState = action.payload;
|
||||||
|
// Save to localStorage when fields are set
|
||||||
|
saveFields(newState);
|
||||||
|
return newState;
|
||||||
},
|
},
|
||||||
resetFields() {
|
resetFields() {
|
||||||
return DEFAULT_FIELDS;
|
const defaultFields = DEFAULT_FIELDS;
|
||||||
|
// Save to localStorage when fields are reset
|
||||||
|
saveFields(defaultFields);
|
||||||
|
return defaultFields;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -481,18 +481,18 @@ export const useTaskSocketHandlers = () => {
|
|||||||
// Find target group based on new phase value
|
// Find target group based on new phase value
|
||||||
let targetGroup: any = null;
|
let targetGroup: any = null;
|
||||||
|
|
||||||
if (newPhaseValue) {
|
if (newPhaseValue && newPhaseValue.trim() !== '') {
|
||||||
// Find group by phase name
|
// Find group by phase name
|
||||||
targetGroup = groups.find(
|
targetGroup = groups.find(
|
||||||
group => group.groupValue === newPhaseValue || group.title === newPhaseValue
|
group => group.groupValue === newPhaseValue || group.title === newPhaseValue
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Find "No Phase" or similar group
|
// Find "Unmapped" group for tasks without a phase or with default phase
|
||||||
targetGroup = groups.find(
|
targetGroup = groups.find(
|
||||||
group =>
|
group =>
|
||||||
group.groupValue === '' ||
|
group.groupValue === 'Unmapped' ||
|
||||||
group.title.toLowerCase().includes('no phase') ||
|
group.title === 'Unmapped' ||
|
||||||
group.title.toLowerCase().includes('unassigned')
|
group.title.toLowerCase().includes('unmapped')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,217 +5,26 @@
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhanced Project View Tab Styles - Compact */
|
/* Light mode - selected tab header bold */
|
||||||
.project-view-tabs {
|
[data-theme="light"] .project-view-tabs .ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||||
margin-top: 16px;
|
font-weight: 700;
|
||||||
|
color: #000000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove default tab border */
|
/* Dark mode - selected tab header bold and white */
|
||||||
.project-view-tabs .ant-tabs-nav::before {
|
[data-theme="dark"] .project-view-tabs .ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||||
border: none !important;
|
font-weight: 900;
|
||||||
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tab bar container */
|
/* Light mode - selected tab underline black */
|
||||||
.project-view-tabs .ant-tabs-nav {
|
[data-theme="light"] .project-view-tabs .ant-tabs-ink-bar {
|
||||||
margin-bottom: 8px;
|
background-color: #000000 !important;
|
||||||
background: transparent;
|
|
||||||
padding: 0 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Individual tab styling - Compact */
|
/* Dark mode - selected tab underline white */
|
||||||
.project-view-tabs .ant-tabs-tab {
|
[data-theme="dark"] .project-view-tabs .ant-tabs-ink-bar {
|
||||||
position: relative;
|
background-color: #ffffff;
|
||||||
margin: 0 4px 0 0;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 6px 6px 0 0;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 13px;
|
|
||||||
min-height: 36px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light mode tab styles */
|
|
||||||
[data-theme="default"] .project-view-tabs .ant-tabs-tab {
|
|
||||||
color: #64748b;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="default"] .project-view-tabs .ant-tabs-tab:hover {
|
|
||||||
color: #3b82f6;
|
|
||||||
background: #eff6ff;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="default"] .project-view-tabs .ant-tabs-tab-active {
|
|
||||||
color: #1e40af !important;
|
|
||||||
background: #ffffff !important;
|
|
||||||
box-shadow:
|
|
||||||
0 -2px 8px rgba(59, 130, 246, 0.1),
|
|
||||||
0 4px 16px rgba(59, 130, 246, 0.1);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode tab styles - matching task list row colors */
|
|
||||||
[data-theme="dark"] .project-view-tabs .ant-tabs-tab {
|
|
||||||
color: #94a3b8;
|
|
||||||
background: #141414;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .project-view-tabs .ant-tabs-tab:hover {
|
|
||||||
color: #60a5fa;
|
|
||||||
background: #262626;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(96, 165, 250, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .project-view-tabs .ant-tabs-tab-active {
|
|
||||||
color: #60a5fa !important;
|
|
||||||
background: #1f1f1f !important;
|
|
||||||
box-shadow:
|
|
||||||
0 -2px 8px rgba(96, 165, 250, 0.15),
|
|
||||||
0 4px 16px rgba(96, 165, 250, 0.15);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tab content area - Compact */
|
|
||||||
.project-view-tabs .ant-tabs-content-holder {
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 6px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 0;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="default"] .project-view-tabs .ant-tabs-content-holder {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .project-view-tabs .ant-tabs-content-holder {
|
|
||||||
background: #1f1f1f;
|
|
||||||
border: 1px solid #303030;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-view-tabs .ant-tabs-tabpane {
|
|
||||||
padding: 0;
|
|
||||||
min-height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pin button styling - Compact */
|
|
||||||
.project-view-tabs .borderless-icon-btn {
|
|
||||||
margin-left: 6px;
|
|
||||||
padding: 2px;
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-view-tabs .borderless-icon-btn:hover {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="default"] .project-view-tabs .borderless-icon-btn:hover {
|
|
||||||
background: rgba(59, 130, 246, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .project-view-tabs .borderless-icon-btn:hover {
|
|
||||||
background: rgba(96, 165, 250, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pinned tab indicator */
|
|
||||||
.project-view-tabs .ant-tabs-tab-active .borderless-icon-btn {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="default"] .project-view-tabs .ant-tabs-tab-active .borderless-icon-btn {
|
|
||||||
background: rgba(59, 130, 246, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .project-view-tabs .ant-tabs-tab-active .borderless-icon-btn {
|
|
||||||
background: rgba(96, 165, 250, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tab label flex container */
|
|
||||||
.project-view-tabs .ant-tabs-tab .ant-tabs-tab-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments - Compact */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.project-view-tabs .ant-tabs-nav {
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-view-tabs .ant-tabs-tab {
|
|
||||||
margin: 0 2px 0 0;
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
min-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-view-tabs .borderless-icon-btn {
|
|
||||||
margin-left: 4px;
|
|
||||||
padding: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.project-view-tabs .ant-tabs-tab {
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 11px;
|
|
||||||
min-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-view-tabs .borderless-icon-btn {
|
|
||||||
display: none; /* Hide pin buttons on very small screens */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animation for tab switching */
|
|
||||||
.project-view-tabs .ant-tabs-content {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-view-tabs .ant-tabs-tabpane-active {
|
|
||||||
animation: fadeInUp 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus states for accessibility - Compact */
|
|
||||||
.project-view-tabs .ant-tabs-tab:focus-visible {
|
|
||||||
outline: 1px solid #3b82f6;
|
|
||||||
outline-offset: 1px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .project-view-tabs .ant-tabs-tab:focus-visible {
|
|
||||||
outline-color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading state for tab content */
|
|
||||||
.project-view-tabs .ant-tabs-tabpane .suspense-fallback {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -351,20 +351,12 @@ const ProjectView = React.memo(() => {
|
|||||||
activeKey={activeTab}
|
activeKey={activeTab}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
items={tabMenuItems}
|
items={tabMenuItems}
|
||||||
tabBarStyle={{
|
destroyOnHidden={true}
|
||||||
paddingInline: 0,
|
|
||||||
marginBottom: 8,
|
|
||||||
background: 'transparent',
|
|
||||||
minHeight: '36px',
|
|
||||||
}}
|
|
||||||
tabBarGutter={0}
|
|
||||||
destroyInactiveTabPane={true}
|
|
||||||
animated={{
|
animated={{
|
||||||
inkBar: true,
|
inkBar: true,
|
||||||
tabPane: false,
|
tabPane: false,
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
type="card"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{portalElements}
|
{portalElements}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import SelectionTypeColumn from './selection-type-column/selection-type-column';
|
import SelectionTypeColumn from './selection-type-column/selection-type-column';
|
||||||
import NumberTypeColumn from './number-type-column/number-type-column';
|
import NumberTypeColumn from './number-type-column/number-type-column';
|
||||||
import LabelTypeColumn from './label-type-column/label-type-column';
|
import LabelTypeColumn from './label-type-column/label-type-column';
|
||||||
@@ -31,6 +32,7 @@ import {
|
|||||||
setSecondNumericColumn,
|
setSecondNumericColumn,
|
||||||
setSelectionsList,
|
setSelectionsList,
|
||||||
setLabelsList,
|
setLabelsList,
|
||||||
|
resetCustomFieldValues,
|
||||||
} from '@features/projects/singleProject/task-list-custom-columns/task-list-custom-columns-slice';
|
} from '@features/projects/singleProject/task-list-custom-columns/task-list-custom-columns-slice';
|
||||||
import CustomColumnHeader from '../custom-column-header/custom-column-header';
|
import CustomColumnHeader from '../custom-column-header/custom-column-header';
|
||||||
import { nanoid } from '@reduxjs/toolkit';
|
import { nanoid } from '@reduxjs/toolkit';
|
||||||
@@ -41,10 +43,12 @@ import {
|
|||||||
import { themeWiseColor } from '@/utils/themeWiseColor';
|
import { themeWiseColor } from '@/utils/themeWiseColor';
|
||||||
import KeyTypeColumn from './key-type-column/key-type-column';
|
import KeyTypeColumn from './key-type-column/key-type-column';
|
||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
import {
|
import {
|
||||||
|
fetchTasksV3,
|
||||||
|
fetchTaskListColumns,
|
||||||
addCustomColumn,
|
addCustomColumn,
|
||||||
deleteCustomColumn as deleteCustomColumnFromTasks,
|
deleteCustomColumn as deleteCustomColumnFromTaskManagement,
|
||||||
} from '@/features/tasks/tasks.slice';
|
} from '@/features/task-management/task-management.slice';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { tasksCustomColumnsService } from '@/api/tasks/tasks-custom-columns.service';
|
import { tasksCustomColumnsService } from '@/api/tasks/tasks-custom-columns.service';
|
||||||
import { ExclamationCircleFilled } from '@ant-design/icons';
|
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
@@ -52,6 +56,7 @@ import { ExclamationCircleFilled } from '@ant-design/icons';
|
|||||||
const CustomColumnModal = () => {
|
const CustomColumnModal = () => {
|
||||||
const [mainForm] = Form.useForm();
|
const [mainForm] = Form.useForm();
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
|
const { t } = useTranslation('task-list-table');
|
||||||
|
|
||||||
// get theme details from theme reducer
|
// get theme details from theme reducer
|
||||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
@@ -62,6 +67,7 @@ const CustomColumnModal = () => {
|
|||||||
customColumnId,
|
customColumnId,
|
||||||
customColumnModalType,
|
customColumnModalType,
|
||||||
isCustomColumnModalOpen,
|
isCustomColumnModalOpen,
|
||||||
|
currentColumnData,
|
||||||
decimals,
|
decimals,
|
||||||
label,
|
label,
|
||||||
labelPosition,
|
labelPosition,
|
||||||
@@ -82,35 +88,84 @@ const CustomColumnModal = () => {
|
|||||||
state => state.taskListCustomColumnsReducer.customFieldNumberType
|
state => state.taskListCustomColumnsReducer.customFieldNumberType
|
||||||
);
|
);
|
||||||
|
|
||||||
// if it is already created column get the column data
|
// Use the column data passed from TaskListV2
|
||||||
const openedColumn = useAppSelector(state => state.taskReducer.customColumns).find(
|
const openedColumn = currentColumnData;
|
||||||
col => col.id === customColumnId
|
|
||||||
);
|
// Debug logging
|
||||||
|
console.log('Modal Debug Info:', {
|
||||||
|
customColumnId,
|
||||||
|
customColumnModalType,
|
||||||
|
currentColumnData,
|
||||||
|
openedColumn,
|
||||||
|
openedColumnFound: !!openedColumn,
|
||||||
|
openedColumnId: openedColumn?.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to reset all form and Redux state
|
||||||
|
const resetModalData = () => {
|
||||||
|
mainForm.resetFields();
|
||||||
|
dispatch(resetCustomFieldValues());
|
||||||
|
dispatch(setCustomColumnModalAttributes({ modalType: 'create', columnId: null }));
|
||||||
|
};
|
||||||
|
|
||||||
// Function to handle deleting a custom column
|
// Function to handle deleting a custom column
|
||||||
const handleDeleteColumn = async () => {
|
const handleDeleteColumn = async () => {
|
||||||
if (!customColumnId) return;
|
console.log('Delete function called with:', {
|
||||||
|
customColumnId,
|
||||||
|
openedColumn,
|
||||||
|
openedColumnId: openedColumn?.id,
|
||||||
|
openedColumnKey: openedColumn?.key,
|
||||||
|
fullColumnData: openedColumn
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to get UUID from different possible locations in the column data
|
||||||
|
const columnUUID = openedColumn?.id ||
|
||||||
|
openedColumn?.uuid ||
|
||||||
|
openedColumn?.custom_column_obj?.id ||
|
||||||
|
openedColumn?.custom_column_obj?.uuid;
|
||||||
|
|
||||||
|
console.log('Extracted UUID candidates:', {
|
||||||
|
'openedColumn?.id': openedColumn?.id,
|
||||||
|
'openedColumn?.uuid': openedColumn?.uuid,
|
||||||
|
'openedColumn?.custom_column_obj?.id': openedColumn?.custom_column_obj?.id,
|
||||||
|
'openedColumn?.custom_column_obj?.uuid': openedColumn?.custom_column_obj?.uuid,
|
||||||
|
'finalColumnUUID': columnUUID
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!customColumnId || !columnUUID) {
|
||||||
|
console.error('Missing required data for deletion:', {
|
||||||
|
customColumnId,
|
||||||
|
columnUUID,
|
||||||
|
openedColumn
|
||||||
|
});
|
||||||
|
message.error('Cannot delete column: Missing UUID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Attempting to delete column with UUID:', columnUUID);
|
||||||
// Make API request to delete the custom column using the service
|
// Make API request to delete the custom column using the service
|
||||||
await tasksCustomColumnsService.deleteCustomColumn(openedColumn?.id || customColumnId);
|
await tasksCustomColumnsService.deleteCustomColumn(columnUUID);
|
||||||
|
|
||||||
// Dispatch actions to update the Redux store
|
// Dispatch actions to update the Redux store
|
||||||
dispatch(deleteCustomColumnFromTasks(customColumnId));
|
dispatch(deleteCustomColumnFromTaskManagement(customColumnId));
|
||||||
dispatch(deleteCustomColumnFromColumns(customColumnId));
|
dispatch(deleteCustomColumnFromColumns(customColumnId));
|
||||||
|
|
||||||
// Close the modal
|
// Close the modal and reset data
|
||||||
dispatch(toggleCustomColumnModalOpen(false));
|
dispatch(toggleCustomColumnModalOpen(false));
|
||||||
dispatch(setCustomColumnModalAttributes({ modalType: 'create', columnId: null }));
|
resetModalData();
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
message.success('Custom column deleted successfully');
|
message.success(t('customColumns.modal.deleteSuccessMessage'));
|
||||||
|
|
||||||
// Reload the page to reflect the changes
|
// Refresh tasks and columns to reflect the deleted custom column
|
||||||
window.location.reload();
|
if (projectId) {
|
||||||
|
dispatch(fetchTaskListColumns(projectId));
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error deleting custom column:', error);
|
logger.error('Error deleting custom column:', error);
|
||||||
message.error('Failed to delete custom column');
|
message.error(t('customColumns.modal.deleteErrorMessage'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,49 +173,49 @@ const CustomColumnModal = () => {
|
|||||||
{
|
{
|
||||||
key: 'people',
|
key: 'people',
|
||||||
value: 'people',
|
value: 'people',
|
||||||
label: 'People',
|
label: t('customColumns.fieldTypes.people'),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'number',
|
key: 'number',
|
||||||
value: 'number',
|
value: 'number',
|
||||||
label: 'Number',
|
label: t('customColumns.fieldTypes.number'),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'date',
|
key: 'date',
|
||||||
value: 'date',
|
value: 'date',
|
||||||
label: 'Date',
|
label: t('customColumns.fieldTypes.date'),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'selection',
|
key: 'selection',
|
||||||
value: 'selection',
|
value: 'selection',
|
||||||
label: 'Selection',
|
label: t('customColumns.fieldTypes.selection'),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'checkbox',
|
key: 'checkbox',
|
||||||
value: 'checkbox',
|
value: 'checkbox',
|
||||||
label: 'Checkbox',
|
label: t('customColumns.fieldTypes.checkbox'),
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'labels',
|
key: 'labels',
|
||||||
value: 'labels',
|
value: 'labels',
|
||||||
label: 'Labels',
|
label: t('customColumns.fieldTypes.labels'),
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'key',
|
key: 'key',
|
||||||
value: 'key',
|
value: 'key',
|
||||||
label: 'Key',
|
label: t('customColumns.fieldTypes.key'),
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'formula',
|
key: 'formula',
|
||||||
value: 'formula',
|
value: 'formula',
|
||||||
label: 'Formula',
|
label: t('customColumns.fieldTypes.formula'),
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -231,12 +286,21 @@ const CustomColumnModal = () => {
|
|||||||
if (res.done) {
|
if (res.done) {
|
||||||
if (res.body.id) newColumn.id = res.body.id;
|
if (res.body.id) newColumn.id = res.body.id;
|
||||||
dispatch(addCustomColumn(newColumn));
|
dispatch(addCustomColumn(newColumn));
|
||||||
dispatch(setCustomColumnModalAttributes({ modalType: 'create', columnId: null }));
|
|
||||||
dispatch(toggleCustomColumnModalOpen(false));
|
dispatch(toggleCustomColumnModalOpen(false));
|
||||||
|
resetModalData();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
message.success(t('customColumns.modal.createSuccessMessage'));
|
||||||
|
|
||||||
|
// Refresh tasks and columns to include the new custom column values
|
||||||
|
if (projectId) {
|
||||||
|
dispatch(fetchTaskListColumns(projectId));
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error creating custom column:', error);
|
logger.error('Error creating custom column:', error);
|
||||||
message.error('Failed to create custom column');
|
message.error(t('customColumns.modal.createErrorMessage'));
|
||||||
}
|
}
|
||||||
} else if (customColumnModalType === 'edit' && customColumnId) {
|
} else if (customColumnModalType === 'edit' && customColumnId) {
|
||||||
const updatedColumn = openedColumn
|
const updatedColumn = openedColumn
|
||||||
@@ -264,7 +328,7 @@ const CustomColumnModal = () => {
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (updatedColumn) {
|
if (updatedColumn && openedColumn?.id) {
|
||||||
try {
|
try {
|
||||||
// Prepare the configuration object
|
// Prepare the configuration object
|
||||||
const configuration = {
|
const configuration = {
|
||||||
@@ -299,7 +363,7 @@ const CustomColumnModal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make API request to update custom column using the service
|
// Make API request to update custom column using the service
|
||||||
await tasksCustomColumnsService.updateCustomColumn(openedColumn?.id || customColumnId, {
|
await tasksCustomColumnsService.updateCustomColumn(openedColumn.id, {
|
||||||
name: value.fieldTitle,
|
name: value.fieldTitle,
|
||||||
field_type: value.fieldType,
|
field_type: value.fieldType,
|
||||||
width: 150,
|
width: 150,
|
||||||
@@ -307,15 +371,21 @@ const CustomColumnModal = () => {
|
|||||||
configuration,
|
configuration,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close modal
|
// Close modal and reset data
|
||||||
dispatch(toggleCustomColumnModalOpen(false));
|
dispatch(toggleCustomColumnModalOpen(false));
|
||||||
dispatch(setCustomColumnModalAttributes({ modalType: 'create', columnId: null }));
|
resetModalData();
|
||||||
|
|
||||||
// Reload the page instead of updating the slice
|
// Show success message
|
||||||
window.location.reload();
|
message.success(t('customColumns.modal.updateSuccessMessage'));
|
||||||
|
|
||||||
|
// Refresh tasks and columns to reflect the updated custom column
|
||||||
|
if (projectId) {
|
||||||
|
dispatch(fetchTaskListColumns(projectId));
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error updating custom column:', error);
|
logger.error('Error updating custom column:', error);
|
||||||
message.error('Failed to update custom column');
|
message.error(t('customColumns.modal.updateErrorMessage'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,20 +398,17 @@ const CustomColumnModal = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={customColumnModalType === 'create' ? 'Add field' : 'Edit field'}
|
title={customColumnModalType === 'create' ? t('customColumns.modal.addFieldTitle') : t('customColumns.modal.editFieldTitle')}
|
||||||
centered
|
centered
|
||||||
open={isCustomColumnModalOpen}
|
open={isCustomColumnModalOpen}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
dispatch(toggleCustomColumnModalOpen(false));
|
dispatch(toggleCustomColumnModalOpen(false));
|
||||||
dispatch(setCustomColumnModalAttributes({ modalType: 'create', columnId: null }));
|
resetModalData();
|
||||||
}}
|
}}
|
||||||
styles={{
|
styles={{
|
||||||
header: { position: 'relative' },
|
header: { position: 'relative' },
|
||||||
footer: { display: 'none' },
|
footer: { display: 'none' },
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
|
||||||
mainForm.resetFields();
|
|
||||||
}}
|
|
||||||
afterOpenChange={open => {
|
afterOpenChange={open => {
|
||||||
if (open && customColumnModalType === 'edit' && openedColumn) {
|
if (open && customColumnModalType === 'edit' && openedColumn) {
|
||||||
// Set the field type first so the correct form fields are displayed
|
// Set the field type first so the correct form fields are displayed
|
||||||
@@ -394,9 +461,11 @@ const CustomColumnModal = () => {
|
|||||||
secondNumericColumn: openedColumn.custom_column_obj?.secondNumericColumn,
|
secondNumericColumn: openedColumn.custom_column_obj?.secondNumericColumn,
|
||||||
});
|
});
|
||||||
} else if (open && customColumnModalType === 'create') {
|
} else if (open && customColumnModalType === 'create') {
|
||||||
// Reset form for create mode
|
// Reset all data for create mode
|
||||||
mainForm.resetFields();
|
resetModalData();
|
||||||
dispatch(setCustomFieldType('people'));
|
} else if (!open) {
|
||||||
|
// Reset data when modal closes
|
||||||
|
resetModalData();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -437,22 +506,22 @@ const CustomColumnModal = () => {
|
|||||||
<Flex gap={16} align="center" justify="space-between">
|
<Flex gap={16} align="center" justify="space-between">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={'fieldTitle'}
|
name={'fieldTitle'}
|
||||||
label={<Typography.Text>Field title</Typography.Text>}
|
label={<Typography.Text>{t('customColumns.modal.fieldTitle')}</Typography.Text>}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: 'Field title is required',
|
message: t('customColumns.modal.fieldTitleRequired'),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
required={false}
|
required={false}
|
||||||
>
|
>
|
||||||
<Input placeholder="title" style={{ minWidth: '100%', width: 300 }} />
|
<Input placeholder={t('customColumns.modal.columnTitlePlaceholder')} style={{ minWidth: '100%', width: 300 }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={'fieldType'}
|
name={'fieldType'}
|
||||||
label={<Typography.Text>Type</Typography.Text>}
|
label={<Typography.Text>{t('customColumns.modal.type')}</Typography.Text>}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
@@ -485,27 +554,30 @@ const CustomColumnModal = () => {
|
|||||||
>
|
>
|
||||||
{customColumnModalType === 'edit' && customColumnId && (
|
{customColumnModalType === 'edit' && customColumnId && (
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="Are you sure you want to delete this custom column?"
|
title={t('customColumns.modal.deleteConfirmTitle')}
|
||||||
description="This action cannot be undone. All data associated with this column will be permanently deleted."
|
description={t('customColumns.modal.deleteConfirmDescription')}
|
||||||
icon={<ExclamationCircleFilled style={{ color: 'red' }} />}
|
icon={<ExclamationCircleFilled style={{ color: 'red' }} />}
|
||||||
onConfirm={handleDeleteColumn}
|
onConfirm={handleDeleteColumn}
|
||||||
okText="Delete"
|
okText={t('customColumns.modal.deleteButton')}
|
||||||
cancelText="Cancel"
|
cancelText={t('customColumns.modal.cancelButton')}
|
||||||
okButtonProps={{ danger: true }}
|
okButtonProps={{ danger: true }}
|
||||||
>
|
>
|
||||||
<Button danger>Delete</Button>
|
<Button danger>{t('customColumns.modal.deleteButton')}</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Flex gap={8}>
|
<Flex gap={8}>
|
||||||
<Button onClick={() => dispatch(toggleCustomColumnModalOpen(false))}>Cancel</Button>
|
<Button onClick={() => {
|
||||||
|
dispatch(toggleCustomColumnModalOpen(false));
|
||||||
|
resetModalData();
|
||||||
|
}}>{t('customColumns.modal.cancelButton')}</Button>
|
||||||
{customColumnModalType === 'create' ? (
|
{customColumnModalType === 'create' ? (
|
||||||
<Button type="primary" htmlType="submit">
|
<Button type="primary" htmlType="submit">
|
||||||
Create
|
{t('customColumns.modal.createButton')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button type="primary" htmlType="submit">
|
<Button type="primary" htmlType="submit">
|
||||||
Update
|
{t('customColumns.modal.updateButton')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export {
|
|||||||
RetweetOutlined,
|
RetweetOutlined,
|
||||||
DoubleRightOutlined,
|
DoubleRightOutlined,
|
||||||
UserAddOutlined,
|
UserAddOutlined,
|
||||||
|
ArrowsAltOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
// Re-export all components with React
|
// Re-export all components with React
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { InlineMember } from './teamMembers/inlineMember.types';
|
import { InlineMember } from './teamMembers/inlineMember.types';
|
||||||
import { EntityState } from '@reduxjs/toolkit';
|
import { EntityState } from '@reduxjs/toolkit';
|
||||||
|
import { ITaskListColumn } from './tasks/taskList.types';
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -25,12 +26,13 @@ export interface Task {
|
|||||||
sub_tasks_count?: number;
|
sub_tasks_count?: number;
|
||||||
show_sub_tasks?: boolean;
|
show_sub_tasks?: boolean;
|
||||||
parent_task_id?: string;
|
parent_task_id?: string;
|
||||||
|
is_sub_task?: boolean; // Add this property
|
||||||
progress?: number;
|
progress?: number;
|
||||||
weight?: number;
|
weight?: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
statusColor?: string;
|
statusColor?: string;
|
||||||
priorityColor?: string;
|
priorityColor?: string;
|
||||||
labels?: { id: string; name: string; color: string }[];
|
labels?: { id: string; name: string; color: string; end?: boolean; names?: string[] }[];
|
||||||
comments_count?: number;
|
comments_count?: number;
|
||||||
attachments_count?: number;
|
attachments_count?: number;
|
||||||
has_dependencies?: boolean;
|
has_dependencies?: boolean;
|
||||||
@@ -97,6 +99,9 @@ export interface TaskManagementState {
|
|||||||
selectedPriorities: string[];
|
selectedPriorities: string[];
|
||||||
search: string;
|
search: string;
|
||||||
loadingSubtasks: Record<string, boolean>; // Track loading state for individual tasks
|
loadingSubtasks: Record<string, boolean>; // Track loading state for individual tasks
|
||||||
|
loadingColumns: boolean;
|
||||||
|
columns: ITaskListColumn[];
|
||||||
|
customColumns: ITaskListColumn[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskGroupsState {
|
export interface TaskGroupsState {
|
||||||
|
|||||||
Reference in New Issue
Block a user