Merge branch 'main' of https://github.com/Worklenz/worklenz into test/row-kanban-board-v1.2.0
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
{
|
||||
"configurePhases": "Konfiguro Fazat",
|
||||
"configure": "Konfiguro",
|
||||
"phaseLabel": "Etiketa e Fazës",
|
||||
"enterPhaseName": "Shkruani emrin e fazës",
|
||||
"enterPhaseName": "Shkruaj emrin e fazës",
|
||||
"addOption": "Shto Opsion",
|
||||
"phaseOptions": "Opsionet e Fazës",
|
||||
"dragToReorderPhases": "Zvarrit fazat për t'i rirenditur. Çdo fazë mund të ketë një ngjyrë të ndryshme.",
|
||||
"enterNewPhaseName": "Shkruani emrin e fazës së re...",
|
||||
"optionsText": "Opsione",
|
||||
"dragToReorderPhases": "Tërhiq fazat për t'i rirenditur. Çdo fazë mund të ketë një ngjyrë të ndryshme.",
|
||||
"enterNewPhaseName": "Shkruaj emrin e fazës së re...",
|
||||
"addPhase": "Shto Fazë",
|
||||
"noPhasesFound": "Nuk u gjetën faza",
|
||||
"no": "Asnjë",
|
||||
"found": "u gjet",
|
||||
"deletePhase": "Fshi Fazën",
|
||||
"deletePhaseConfirm": "Jeni të sigurt që doni të fshini këtë fazë? Ky veprim nuk mund të zhbëhet.",
|
||||
"rename": "Riemëro",
|
||||
"deletePhaseConfirm": "Jeni i sigurt që doni të fshini këtë fazë? Ky veprim nuk mund të zhbëhet.",
|
||||
"rename": "Riemërto",
|
||||
"delete": "Fshi",
|
||||
"create": "Krijo",
|
||||
"cancel": "Anulo",
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
{
|
||||
"importTasks": "Importo detyra",
|
||||
"importTask": "Importo detyrë",
|
||||
"importTasks": "Importo detyrat",
|
||||
"importTask": "Importo detyrën",
|
||||
"createTask": "Krijo detyrë",
|
||||
"settings": "Cilësimet",
|
||||
"subscribe": "Abonohu",
|
||||
"unsubscribe": "Çabonohu",
|
||||
"deleteProject": "Fshi projektin",
|
||||
"deleteProject": "Fshij projektin",
|
||||
"startDate": "Data e fillimit",
|
||||
"endDate": "Data e mbarimit",
|
||||
"endDate": "Data e përfundimit",
|
||||
"projectSettings": "Cilësimet e projektit",
|
||||
"projectSummary": "Përmbledhja e projektit",
|
||||
"receiveProjectSummary": "Merrni një përmbledhje të projektit çdo mbrëmje.",
|
||||
"receiveProjectSummary": "Merr një përmbledhje të projektit çdo mbrëmje.",
|
||||
"refreshProject": "Rifresko projektin",
|
||||
"saveAsTemplate": "Ruaj si model",
|
||||
"saveAsTemplate": "Ruaj si shabllon",
|
||||
"invite": "Fto",
|
||||
"share": "Ndaj",
|
||||
"subscribeTooltip": "Abonohu tek njoftimet e projektit",
|
||||
"subscribeTooltip": "Abonohu në njoftimet e projektit",
|
||||
"unsubscribeTooltip": "Çabonohu nga njoftimet e projektit",
|
||||
"refreshTooltip": "Rifresko të dhënat e projektit",
|
||||
"settingsTooltip": "Hap cilësimet e projektit",
|
||||
"saveAsTemplateTooltip": "Ruaj këtë projekt si model",
|
||||
"inviteTooltip": "Fto anëtarë të ekipit në këtë projekt",
|
||||
"saveAsTemplateTooltip": "Ruaj këtë projekt si shabllon",
|
||||
"inviteTooltip": "Fto anëtarët e ekipit në këtë projekt",
|
||||
"createTaskTooltip": "Krijo një detyrë të re",
|
||||
"importTaskTooltip": "Importo detyrë nga modeli",
|
||||
"navigateBackTooltip": "Kthehu tek lista e projekteve",
|
||||
"importTaskTooltip": "Importo detyrë nga shablloni",
|
||||
"navigateBackTooltip": "Kthehu në listën e projekteve",
|
||||
"projectStatusTooltip": "Statusi i projektit",
|
||||
"projectDatesInfo": "Informacion për kohëzgjatjen e projektit",
|
||||
"projectCategoryTooltip": "Kategoria e projektit"
|
||||
"projectDatesInfo": "Informacioni i afateve të projektit",
|
||||
"projectCategoryTooltip": "Kategoria e projektit",
|
||||
"defaultTaskName": "Detyrë Pa Emër"
|
||||
}
|
||||
|
||||
@@ -68,9 +68,10 @@
|
||||
"clearing": "Po pastron...",
|
||||
"cancel": "Anulo",
|
||||
"search": "Kërko",
|
||||
"groupedBy": "I grupuar sipas",
|
||||
"manageStatuses": "Menaxho statuset",
|
||||
"managePhases": "Menaxho fazat",
|
||||
"groupedBy": "Grupuar sipas",
|
||||
"manage": "Menaxho",
|
||||
"manageStatuses": "Menaxho Statuset",
|
||||
"managePhases": "Menaxho Fazat",
|
||||
"dragToReorderStatuses": "Statuset janë të organizuara sipas kategorive. Tërhiq për të rirenditur brenda kategorive. Kliko 'Shto status' për të krijuar statuse të reja në çdo kategori.",
|
||||
"enterNewStatusName": "Shkruani emrin e statusit të ri...",
|
||||
"addStatus": "Shto status",
|
||||
|
||||
@@ -1,21 +1,39 @@
|
||||
{
|
||||
"noTasksInGroup": "Nuk ka detyra në këtë grup",
|
||||
"noTasksInGroupDescription": "Shtoni një detyrë për të filluar",
|
||||
"addFirstTask": "Shtoni detyrën tuaj të parë",
|
||||
"noTasksInGroupDescription": "Shto një detyrë për të filluar",
|
||||
"addFirstTask": "Shto detyrën e parë",
|
||||
"openTask": "Hap",
|
||||
"subtask": "nën-detyrë",
|
||||
"subtasks": "nën-detyra",
|
||||
"subtask": "nëndetyrë",
|
||||
"subtasks": "nëndetyra",
|
||||
"comment": "koment",
|
||||
"comments": "komente",
|
||||
"attachment": "bashkëngjitje",
|
||||
"attachments": "bashkëngjitje",
|
||||
"enterSubtaskName": "Shkruani emrin e nën-detyrës...",
|
||||
"enterSubtaskName": "Shkruani emrin e nëndetyrës...",
|
||||
"add": "Shto",
|
||||
"cancel": "Anulo",
|
||||
"renameGroup": "Riemërto Grupin",
|
||||
"renameStatus": "Riemërto Statusin",
|
||||
"renamePhase": "Riemërto Fazën",
|
||||
"changeCategory": "Ndrysho Kategorinë",
|
||||
"clickToEditGroupName": "Kliko për të ndryshuar emrin e grupit",
|
||||
"enterGroupName": "Shkruani emrin e grupit"
|
||||
"clickToEditGroupName": "Kliko për të redaktuar emrin e grupit",
|
||||
"enterGroupName": "Shkruani emrin e grupit",
|
||||
"todo": "Për t'u Bërë",
|
||||
"inProgress": "Në Progres",
|
||||
"done": "E Kryer",
|
||||
"defaultTaskName": "Detyrë Pa Emër",
|
||||
|
||||
"indicators": {
|
||||
"tooltips": {
|
||||
"subtasks": "{{count}} nëndetyrë",
|
||||
"subtasks_plural": "{{count}} nëndetyra",
|
||||
"comments": "{{count}} koment",
|
||||
"comments_plural": "{{count}} komente",
|
||||
"attachments": "{{count}} bashkëngjitje",
|
||||
"attachments_plural": "{{count}} bashkëngjitje",
|
||||
"subscribers": "Detyra ka abonues",
|
||||
"dependencies": "Detyra ka varësi",
|
||||
"recurring": "Detyrë e përsëritur"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"configurePhases": "Phasen konfigurieren",
|
||||
"phaseLabel": "Phasenbezeichnung",
|
||||
"enterPhaseName": "Phasennamen eingeben",
|
||||
"configure": "Konfigurieren",
|
||||
"phaseLabel": "Phasen-Label",
|
||||
"enterPhaseName": "Phasenname eingeben",
|
||||
"addOption": "Option hinzufügen",
|
||||
"phaseOptions": "Phasenoptionen",
|
||||
"optionsText": "Optionen",
|
||||
"dragToReorderPhases": "Ziehen Sie Phasen, um sie neu zu ordnen. Jede Phase kann eine andere Farbe haben.",
|
||||
"enterNewPhaseName": "Neuen Phasennamen eingeben...",
|
||||
"addPhase": "Phase hinzufügen",
|
||||
"noPhasesFound": "Keine Phasen gefunden",
|
||||
"no": "Keine",
|
||||
"found": "gefunden",
|
||||
"deletePhase": "Phase löschen",
|
||||
"deletePhaseConfirm": "Sind Sie sicher, dass Sie diese Phase löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"rename": "Umbenennen",
|
||||
|
||||
@@ -4,27 +4,28 @@
|
||||
"createTask": "Aufgabe erstellen",
|
||||
"settings": "Einstellungen",
|
||||
"subscribe": "Abonnieren",
|
||||
"unsubscribe": "Abonnement beenden",
|
||||
"unsubscribe": "Abmelden",
|
||||
"deleteProject": "Projekt löschen",
|
||||
"startDate": "Startdatum",
|
||||
"endDate": "Enddatum",
|
||||
"projectSettings": "Projekteinstellungen",
|
||||
"projectSummary": "Projektzusammenfassung",
|
||||
"receiveProjectSummary": "Erhalten Sie jeden Abend eine Projektzusammenfassung.",
|
||||
"receiveProjectSummary": "Jeden Abend eine Projektzusammenfassung erhalten.",
|
||||
"refreshProject": "Projekt aktualisieren",
|
||||
"saveAsTemplate": "Als Vorlage speichern",
|
||||
"invite": "Einladen",
|
||||
"share": "Teilen",
|
||||
"subscribeTooltip": "Projektbenachrichtigungen abonnieren",
|
||||
"unsubscribeTooltip": "Projektbenachrichtigungen beenden",
|
||||
"unsubscribeTooltip": "Projektbenachrichtigungen abmelden",
|
||||
"refreshTooltip": "Projektdaten aktualisieren",
|
||||
"settingsTooltip": "Projekteinstellungen öffnen",
|
||||
"saveAsTemplateTooltip": "Dieses Projekt als Vorlage speichern",
|
||||
"inviteTooltip": "Teammitglieder zu diesem Projekt einladen",
|
||||
"createTaskTooltip": "Neue Aufgabe erstellen",
|
||||
"createTaskTooltip": "Eine neue Aufgabe erstellen",
|
||||
"importTaskTooltip": "Aufgabe aus Vorlage importieren",
|
||||
"navigateBackTooltip": "Zurück zur Projektliste",
|
||||
"projectStatusTooltip": "Projektstatus",
|
||||
"projectDatesInfo": "Informationen zum Projektzeitraum",
|
||||
"projectCategoryTooltip": "Projektkategorie"
|
||||
"projectDatesInfo": "Projekt-Zeitleisten-Informationen",
|
||||
"projectCategoryTooltip": "Projektkategorie",
|
||||
"defaultTaskName": "Unbenannte Aufgabe"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"cancel": "Abbrechen",
|
||||
"search": "Suchen",
|
||||
"groupedBy": "Gruppiert nach",
|
||||
"manage": "Verwalten",
|
||||
"manageStatuses": "Status verwalten",
|
||||
"managePhases": "Phasen verwalten",
|
||||
"dragToReorderStatuses": "Status sind nach Kategorien organisiert. Ziehen Sie, um innerhalb von Kategorien neu zu ordnen. Klicken Sie auf 'Status hinzufügen', um neue Status in jeder Kategorie zu erstellen.",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"comments": "Kommentare",
|
||||
"attachment": "Anhang",
|
||||
"attachments": "Anhänge",
|
||||
"enterSubtaskName": "Unteraufgabenname eingeben...",
|
||||
"enterSubtaskName": "Geben Sie den Namen der Unteraufgabe ein...",
|
||||
"add": "Hinzufügen",
|
||||
"cancel": "Abbrechen",
|
||||
"renameGroup": "Gruppe umbenennen",
|
||||
@@ -17,5 +17,23 @@
|
||||
"renamePhase": "Phase umbenennen",
|
||||
"changeCategory": "Kategorie ändern",
|
||||
"clickToEditGroupName": "Klicken Sie, um den Gruppennamen zu bearbeiten",
|
||||
"enterGroupName": "Gruppennamen eingeben"
|
||||
"enterGroupName": "Geben Sie den Gruppennamen ein",
|
||||
"todo": "Zu erledigen",
|
||||
"inProgress": "In Bearbeitung",
|
||||
"done": "Erledigt",
|
||||
"defaultTaskName": "Unbenannte Aufgabe",
|
||||
|
||||
"indicators": {
|
||||
"tooltips": {
|
||||
"subtasks": "{{count}} Unteraufgabe",
|
||||
"subtasks_plural": "{{count}} Unteraufgaben",
|
||||
"comments": "{{count}} Kommentar",
|
||||
"comments_plural": "{{count}} Kommentare",
|
||||
"attachments": "{{count}} Anhang",
|
||||
"attachments_plural": "{{count}} Anhänge",
|
||||
"subscribers": "Aufgabe hat Abonnenten",
|
||||
"dependencies": "Aufgabe hat Abhängigkeiten",
|
||||
"recurring": "Wiederkehrende Aufgabe"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"configurePhases": "Configure Phases",
|
||||
"configure": "Configure",
|
||||
"phaseLabel": "Phase Label",
|
||||
"enterPhaseName": "Enter phase name",
|
||||
"addOption": "Add Option",
|
||||
"phaseOptions": "Phase Options",
|
||||
"optionsText": "Options",
|
||||
"dragToReorderPhases": "Drag phases to reorder them. Each phase can have a different color.",
|
||||
"enterNewPhaseName": "Enter new phase name...",
|
||||
"addPhase": "Add Phase",
|
||||
"noPhasesFound": "No phases found",
|
||||
"no": "No",
|
||||
"found": "found",
|
||||
"deletePhase": "Delete Phase",
|
||||
"deletePhaseConfirm": "Are you sure you want to delete this phase? This action cannot be undone.",
|
||||
"rename": "Rename",
|
||||
|
||||
@@ -26,5 +26,6 @@
|
||||
"navigateBackTooltip": "Go back to projects list",
|
||||
"projectStatusTooltip": "Project status",
|
||||
"projectDatesInfo": "Project timeline information",
|
||||
"projectCategoryTooltip": "Project category"
|
||||
"projectCategoryTooltip": "Project category",
|
||||
"defaultTaskName": "Untitled Task"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"groupedBy": "Grouped by",
|
||||
"manage": "Manage",
|
||||
"manageStatuses": "Manage Statuses",
|
||||
"managePhases": "Manage Phases",
|
||||
"dragToReorderStatuses": "Statuses are organized by categories. Drag to reorder within categories. Click 'Add Status' to create new statuses in each category.",
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
"changeCategory": "Change Category",
|
||||
"clickToEditGroupName": "Click to edit group name",
|
||||
"enterGroupName": "Enter group name",
|
||||
"todo": "To Do",
|
||||
"inProgress": "Doing",
|
||||
"done": "Done",
|
||||
"defaultTaskName": "Untitled Task",
|
||||
|
||||
"indicators": {
|
||||
"tooltips": {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"configurePhases": "Configurar fases",
|
||||
"phaseLabel": "Etiqueta de fase",
|
||||
"enterPhaseName": "Introducir nombre de la fase",
|
||||
"addOption": "Agregar opción",
|
||||
"phaseOptions": "Opciones de fase",
|
||||
"configurePhases": "Configurar Fases",
|
||||
"configure": "Configurar",
|
||||
"phaseLabel": "Etiqueta de Fase",
|
||||
"enterPhaseName": "Ingresa el nombre de la fase",
|
||||
"addOption": "Agregar Opción",
|
||||
"phaseOptions": "Opciones de Fase",
|
||||
"optionsText": "Opciones",
|
||||
"dragToReorderPhases": "Arrastra las fases para reordenarlas. Cada fase puede tener un color diferente.",
|
||||
"enterNewPhaseName": "Introducir nuevo nombre de fase...",
|
||||
"addPhase": "Añadir Fase",
|
||||
"enterNewPhaseName": "Ingresa el nombre de la nueva fase...",
|
||||
"addPhase": "Agregar Fase",
|
||||
"noPhasesFound": "No se encontraron fases",
|
||||
"no": "No",
|
||||
"found": "encontrado",
|
||||
"deletePhase": "Eliminar Fase",
|
||||
"deletePhaseConfirm": "¿Estás seguro de que quieres eliminar esta fase? Esta acción no se puede deshacer.",
|
||||
"rename": "Renombrar",
|
||||
|
||||
@@ -4,19 +4,19 @@
|
||||
"createTask": "Crear tarea",
|
||||
"settings": "Configuración",
|
||||
"subscribe": "Suscribirse",
|
||||
"unsubscribe": "Cancelar suscripción",
|
||||
"unsubscribe": "Desuscribirse",
|
||||
"deleteProject": "Eliminar proyecto",
|
||||
"startDate": "Fecha de inicio",
|
||||
"endDate": "Fecha de finalización",
|
||||
"endDate": "Fecha de fin",
|
||||
"projectSettings": "Configuración del proyecto",
|
||||
"projectSummary": "Resumen del proyecto",
|
||||
"receiveProjectSummary": "Recibe un resumen del proyecto cada noche.",
|
||||
"receiveProjectSummary": "Recibir un resumen del proyecto cada noche.",
|
||||
"refreshProject": "Actualizar proyecto",
|
||||
"saveAsTemplate": "Guardar como plantilla",
|
||||
"invite": "Invitar",
|
||||
"share": "Compartir",
|
||||
"subscribeTooltip": "Suscribirse a notificaciones del proyecto",
|
||||
"unsubscribeTooltip": "Cancelar suscripción a notificaciones del proyecto",
|
||||
"subscribeTooltip": "Suscribirse a las notificaciones del proyecto",
|
||||
"unsubscribeTooltip": "Desuscribirse de las notificaciones del proyecto",
|
||||
"refreshTooltip": "Actualizar datos del proyecto",
|
||||
"settingsTooltip": "Abrir configuración del proyecto",
|
||||
"saveAsTemplateTooltip": "Guardar este proyecto como plantilla",
|
||||
@@ -25,6 +25,7 @@
|
||||
"importTaskTooltip": "Importar tarea desde plantilla",
|
||||
"navigateBackTooltip": "Volver a la lista de proyectos",
|
||||
"projectStatusTooltip": "Estado del proyecto",
|
||||
"projectDatesInfo": "Información de cronograma del proyecto",
|
||||
"projectCategoryTooltip": "Categoría del proyecto"
|
||||
"projectDatesInfo": "Información de la cronología del proyecto",
|
||||
"projectCategoryTooltip": "Categoría del proyecto",
|
||||
"defaultTaskName": "Tarea Sin Título"
|
||||
}
|
||||
|
||||
@@ -69,8 +69,9 @@
|
||||
"cancel": "Cancelar",
|
||||
"search": "Buscar",
|
||||
"groupedBy": "Agrupado por",
|
||||
"manageStatuses": "Gestionar estados",
|
||||
"managePhases": "Gestionar fases",
|
||||
"manage": "Gestionar",
|
||||
"manageStatuses": "Gestionar Estados",
|
||||
"managePhases": "Gestionar Fases",
|
||||
"dragToReorderStatuses": "Los estados están organizados por categorías. Arrastra para reordenar dentro de las categorías. Haz clic en 'Agregar estado' para crear nuevos estados en cada categoría.",
|
||||
"enterNewStatusName": "Ingrese el nombre del nuevo estado...",
|
||||
"addStatus": "Agregar estado",
|
||||
|
||||
@@ -17,5 +17,23 @@
|
||||
"renamePhase": "Renombrar Fase",
|
||||
"changeCategory": "Cambiar Categoría",
|
||||
"clickToEditGroupName": "Haz clic para editar el nombre del grupo",
|
||||
"enterGroupName": "Ingresa el nombre del grupo"
|
||||
"enterGroupName": "Ingresa el nombre del grupo",
|
||||
"todo": "Por Hacer",
|
||||
"inProgress": "En Progreso",
|
||||
"done": "Hecho",
|
||||
"defaultTaskName": "Tarea Sin Título",
|
||||
|
||||
"indicators": {
|
||||
"tooltips": {
|
||||
"subtasks": "{{count}} subtarea",
|
||||
"subtasks_plural": "{{count}} subtareas",
|
||||
"comments": "{{count}} comentario",
|
||||
"comments_plural": "{{count}} comentarios",
|
||||
"attachments": "{{count}} adjunto",
|
||||
"attachments_plural": "{{count}} adjuntos",
|
||||
"subscribers": "La tarea tiene suscriptores",
|
||||
"dependencies": "La tarea tiene dependencias",
|
||||
"recurring": "Tarea recurrente"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"configurePhases": "Configurar fases",
|
||||
"phaseLabel": "Etiqueta de fase",
|
||||
"configurePhases": "Configurar Fases",
|
||||
"configure": "Configurar",
|
||||
"phaseLabel": "Rótulo da Fase",
|
||||
"enterPhaseName": "Digite o nome da fase",
|
||||
"addOption": "Adicionar Opção",
|
||||
"phaseOptions": "Opções de Fase",
|
||||
"optionsText": "Opções",
|
||||
"dragToReorderPhases": "Arraste as fases para reordená-las. Cada fase pode ter uma cor diferente.",
|
||||
"enterNewPhaseName": "Digite o novo nome da fase...",
|
||||
"enterNewPhaseName": "Digite o nome da nova fase...",
|
||||
"addPhase": "Adicionar Fase",
|
||||
"noPhasesFound": "Nenhuma fase encontrada",
|
||||
"no": "Nenhuma",
|
||||
"found": "encontrada",
|
||||
"deletePhase": "Excluir Fase",
|
||||
"deletePhaseConfirm": "Tem certeza de que deseja excluir esta fase? Esta ação não pode ser desfeita.",
|
||||
"rename": "Renomear",
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"unsubscribe": "Cancelar inscrição",
|
||||
"deleteProject": "Excluir projeto",
|
||||
"startDate": "Data de início",
|
||||
"endDate": "Data de término",
|
||||
"endDate": "Data de fim",
|
||||
"projectSettings": "Configurações do projeto",
|
||||
"projectSummary": "Resumo do projeto",
|
||||
"receiveProjectSummary": "Receba um resumo do projeto todas as noites.",
|
||||
"receiveProjectSummary": "Receber um resumo do projeto todas as noites.",
|
||||
"refreshProject": "Atualizar projeto",
|
||||
"saveAsTemplate": "Salvar como modelo",
|
||||
"invite": "Convidar",
|
||||
@@ -22,9 +22,10 @@
|
||||
"saveAsTemplateTooltip": "Salvar este projeto como modelo",
|
||||
"inviteTooltip": "Convidar membros da equipe para este projeto",
|
||||
"createTaskTooltip": "Criar uma nova tarefa",
|
||||
"importTaskTooltip": "Importar tarefa de modelo",
|
||||
"navigateBackTooltip": "Voltar para lista de projetos",
|
||||
"importTaskTooltip": "Importar tarefa do modelo",
|
||||
"navigateBackTooltip": "Voltar para a lista de projetos",
|
||||
"projectStatusTooltip": "Status do projeto",
|
||||
"projectDatesInfo": "Informações do cronograma do projeto",
|
||||
"projectCategoryTooltip": "Categoria do projeto"
|
||||
"projectDatesInfo": "Informações da linha do tempo do projeto",
|
||||
"projectCategoryTooltip": "Categoria do projeto",
|
||||
"defaultTaskName": "Tarefa Sem Título"
|
||||
}
|
||||
|
||||
@@ -69,8 +69,9 @@
|
||||
"cancel": "Cancelar",
|
||||
"search": "Pesquisar",
|
||||
"groupedBy": "Agrupado por",
|
||||
"manageStatuses": "Gerenciar status",
|
||||
"managePhases": "Gerenciar fases",
|
||||
"manage": "Gerenciar",
|
||||
"manageStatuses": "Gerenciar Status",
|
||||
"managePhases": "Gerenciar Fases",
|
||||
"dragToReorderStatuses": "Os status estão organizados por categorias. Arraste para reordenar dentro das categorias. Clique em 'Adicionar status' para criar novos status em cada categoria.",
|
||||
"enterNewStatusName": "Digite o nome do novo status...",
|
||||
"addStatus": "Adicionar status",
|
||||
|
||||
@@ -17,5 +17,23 @@
|
||||
"renamePhase": "Renomear Fase",
|
||||
"changeCategory": "Alterar Categoria",
|
||||
"clickToEditGroupName": "Clique para editar o nome do grupo",
|
||||
"enterGroupName": "Digite o nome do grupo"
|
||||
"enterGroupName": "Digite o nome do grupo",
|
||||
"todo": "A Fazer",
|
||||
"inProgress": "Em Andamento",
|
||||
"done": "Concluído",
|
||||
"defaultTaskName": "Tarefa Sem Título",
|
||||
|
||||
"indicators": {
|
||||
"tooltips": {
|
||||
"subtasks": "{{count}} subtarefa",
|
||||
"subtasks_plural": "{{count}} subtarefas",
|
||||
"comments": "{{count}} comentário",
|
||||
"comments_plural": "{{count}} comentários",
|
||||
"attachments": "{{count}} anexo",
|
||||
"attachments_plural": "{{count}} anexos",
|
||||
"subscribers": "Tarefa tem assinantes",
|
||||
"dependencies": "Tarefa tem dependências",
|
||||
"recurring": "Tarefa recorrente"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
{
|
||||
"configurePhases": "配置阶段",
|
||||
"phaseLabel": "阶段标签",
|
||||
"enterPhaseName": "输入阶段名称",
|
||||
"addOption": "添加选项",
|
||||
"phaseOptions": "阶段选项",
|
||||
"dragToReorderPhases": "拖拽阶段以重新排序。每个阶段可以有不同的颜色。",
|
||||
"enterNewPhaseName": "输入新阶段名称...",
|
||||
"addPhase": "添加阶段",
|
||||
"noPhasesFound": "未找到阶段",
|
||||
"deletePhase": "删除阶段",
|
||||
"deletePhaseConfirm": "您确定要删除此阶段吗?此操作无法撤销。",
|
||||
"rename": "重命名",
|
||||
"delete": "删除",
|
||||
"create": "创建",
|
||||
"cancel": "取消",
|
||||
"selectColor": "选择颜色",
|
||||
"managePhases": "管理阶段",
|
||||
"close": "关闭"
|
||||
"configurePhases": "配置阶段",
|
||||
"configure": "配置",
|
||||
"phaseLabel": "阶段标签",
|
||||
"enterPhaseName": "输入阶段名称",
|
||||
"addOption": "添加选项",
|
||||
"phaseOptions": "阶段选项",
|
||||
"optionsText": "选项",
|
||||
"dragToReorderPhases": "拖拽阶段来重新排序。每个阶段可以有不同的颜色。",
|
||||
"enterNewPhaseName": "输入新阶段名称...",
|
||||
"addPhase": "添加阶段",
|
||||
"noPhasesFound": "未找到阶段",
|
||||
"no": "没有",
|
||||
"found": "找到",
|
||||
"deletePhase": "删除阶段",
|
||||
"deletePhaseConfirm": "您确定要删除此阶段吗?此操作无法撤消。",
|
||||
"rename": "重命名",
|
||||
"delete": "删除",
|
||||
"create": "创建",
|
||||
"cancel": "取消",
|
||||
"selectColor": "选择颜色",
|
||||
"managePhases": "管理阶段",
|
||||
"close": "关闭"
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
"endDate": "结束日期",
|
||||
"projectSettings": "项目设置",
|
||||
"projectSummary": "项目摘要",
|
||||
"receiveProjectSummary": "每晚接收项目摘要。",
|
||||
"receiveProjectSummary": "每天晚上接收项目摘要。",
|
||||
"refreshProject": "刷新项目",
|
||||
"saveAsTemplate": "保存为模板",
|
||||
"invite": "邀请",
|
||||
@@ -25,6 +25,7 @@
|
||||
"importTaskTooltip": "从模板导入任务",
|
||||
"navigateBackTooltip": "返回项目列表",
|
||||
"projectStatusTooltip": "项目状态",
|
||||
"projectDatesInfo": "项目时间安排信息",
|
||||
"projectCategoryTooltip": "项目类别"
|
||||
"projectDatesInfo": "项目时间线信息",
|
||||
"projectCategoryTooltip": "项目类别",
|
||||
"defaultTaskName": "无标题任务"
|
||||
}
|
||||
@@ -62,7 +62,8 @@
|
||||
"clearing": "清除中...",
|
||||
"cancel": "取消",
|
||||
"search": "搜索",
|
||||
"groupedBy": "分组依据",
|
||||
"groupedBy": "分组方式",
|
||||
"manage": "管理",
|
||||
"manageStatuses": "管理状态",
|
||||
"managePhases": "管理阶段",
|
||||
"dragToReorderStatuses": "拖拽状态以重新排序。每个状态可以有不同的类别。",
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
"changeCategory": "更改类别",
|
||||
"clickToEditGroupName": "点击编辑组名称",
|
||||
"enterGroupName": "输入组名称",
|
||||
"todo": "待办",
|
||||
"inProgress": "进行中",
|
||||
"done": "已完成",
|
||||
"defaultTaskName": "无标题任务",
|
||||
|
||||
"indicators": {
|
||||
"tooltips": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import apiClient from '@/api/api-client';
|
||||
import { API_BASE_URL } from '@/shared/constants';
|
||||
import { IServerResponse } from '@/types/common.types';
|
||||
import apiClient from '@api/api-client';
|
||||
import { API_BASE_URL } from '@/shared/constants';
|
||||
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
|
||||
import { toQueryString } from '@/utils/toQueryString';
|
||||
|
||||
const rootUrl = `${API_BASE_URL}/task-phases`;
|
||||
|
||||
interface UpdateSortOrderBody {
|
||||
export interface UpdateSortOrderBody {
|
||||
from_index: number;
|
||||
to_index: number;
|
||||
phases: ITaskPhase[];
|
||||
@@ -14,9 +14,10 @@ interface UpdateSortOrderBody {
|
||||
}
|
||||
|
||||
export const phasesApiService = {
|
||||
addPhaseOption: async (projectId: string) => {
|
||||
addPhaseOption: async (projectId: string, name?: string) => {
|
||||
const q = toQueryString({ id: projectId, current_project_id: projectId });
|
||||
const response = await apiClient.post<IServerResponse<ITaskPhase>>(`${rootUrl}${q}`);
|
||||
const body = name ? { name } : {};
|
||||
const response = await apiClient.post<IServerResponse<ITaskPhase>>(`${rootUrl}${q}`, body);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ const EnhancedKanbanCreateSection: React.FC = () => {
|
||||
}
|
||||
if (groupBy === IGroupBy.PHASE) {
|
||||
try {
|
||||
const response = await phasesApiService.addPhaseOption(projectId);
|
||||
const response = await phasesApiService.addPhaseOption(projectId, name);
|
||||
if (response.done && response.body) {
|
||||
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||
}
|
||||
|
||||
@@ -73,8 +73,17 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = ({
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="virtualized-empty-state" style={{ height }}>
|
||||
<div className="empty-message">No tasks in this group</div>
|
||||
<div className="virtualized-empty-state" style={{ height, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div className="empty-message" style={{
|
||||
padding: '32px 24px',
|
||||
color: '#8c8c8c',
|
||||
fontSize: '14px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #f0f0f0'
|
||||
}}>
|
||||
No tasks in this group
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ const KanbanGroup: React.FC<TaskGroupProps> = ({
|
||||
.kanban-group-empty {
|
||||
text-align: center;
|
||||
color: #bfbfbf;
|
||||
padding: 32px 0;
|
||||
padding: 48px 16px;
|
||||
}
|
||||
.kanban-group-add-task {
|
||||
padding: 12px;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface GroupProgressBarProps {
|
||||
todoProgress: number;
|
||||
doingProgress: number;
|
||||
doneProgress: number;
|
||||
groupType: string;
|
||||
}
|
||||
|
||||
const GroupProgressBar: React.FC<GroupProgressBarProps> = ({
|
||||
todoProgress,
|
||||
doingProgress,
|
||||
doneProgress,
|
||||
groupType
|
||||
}) => {
|
||||
const { t } = useTranslation('task-management');
|
||||
|
||||
// Only show for priority and phase grouping
|
||||
if (groupType !== 'priority' && groupType !== 'phase') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const total = todoProgress + doingProgress + doneProgress;
|
||||
|
||||
// Don't show if no progress values exist
|
||||
if (total === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Compact progress text */}
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap font-medium">
|
||||
{doneProgress}% {t('done')}
|
||||
</span>
|
||||
|
||||
{/* Compact progress bar */}
|
||||
<div className="w-20 bg-gray-200 dark:bg-gray-700 rounded-full h-1.5 overflow-hidden">
|
||||
<div className="h-full flex">
|
||||
{/* Todo section - light green */}
|
||||
{todoProgress > 0 && (
|
||||
<div
|
||||
className="bg-green-200 dark:bg-green-800 transition-all duration-300"
|
||||
style={{ width: `${(todoProgress / total) * 100}%` }}
|
||||
title={`${t('todo')}: ${todoProgress}%`}
|
||||
/>
|
||||
)}
|
||||
{/* Doing section - medium green */}
|
||||
{doingProgress > 0 && (
|
||||
<div
|
||||
className="bg-green-400 dark:bg-green-600 transition-all duration-300"
|
||||
style={{ width: `${(doingProgress / total) * 100}%` }}
|
||||
title={`${t('inProgress')}: ${doingProgress}%`}
|
||||
/>
|
||||
)}
|
||||
{/* Done section - dark green */}
|
||||
{doneProgress > 0 && (
|
||||
<div
|
||||
className="bg-green-600 dark:bg-green-400 transition-all duration-300"
|
||||
style={{ width: `${(doneProgress / total) * 100}%` }}
|
||||
title={`${t('done')}: ${doneProgress}%`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Small legend dots with better spacing */}
|
||||
<div className="flex items-center gap-1">
|
||||
{todoProgress > 0 && (
|
||||
<div
|
||||
className="w-1.5 h-1.5 bg-green-200 dark:bg-green-800 rounded-full"
|
||||
title={`${t('todo')}: ${todoProgress}%`}
|
||||
/>
|
||||
)}
|
||||
{doingProgress > 0 && (
|
||||
<div
|
||||
className="w-1.5 h-1.5 bg-green-400 dark:bg-green-600 rounded-full"
|
||||
title={`${t('inProgress')}: ${doingProgress}%`}
|
||||
/>
|
||||
)}
|
||||
{doneProgress > 0 && (
|
||||
<div
|
||||
className="w-1.5 h-1.5 bg-green-600 dark:bg-green-400 rounded-full"
|
||||
title={`${t('done')}: ${doneProgress}%`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupProgressBar;
|
||||
@@ -3,12 +3,13 @@ import { useDroppable } from '@dnd-kit/core';
|
||||
// @ts-ignore: Heroicons module types
|
||||
import { ChevronDownIcon, ChevronRightIcon, EllipsisHorizontalIcon, PencilIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||
import { Checkbox, Dropdown, Menu, Input, Modal, Badge, Flex } from 'antd';
|
||||
import GroupProgressBar from './GroupProgressBar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getContrastColor } from '@/utils/colorUtils';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { selectSelectedTaskIds, selectTask, deselectTask } from '@/features/task-management/selection.slice';
|
||||
import { selectGroups, fetchTasksV3 } from '@/features/task-management/task-management.slice';
|
||||
import { selectGroups, fetchTasksV3, selectAllTasksArray } from '@/features/task-management/task-management.slice';
|
||||
import { selectCurrentGrouping } from '@/features/task-management/grouping.slice';
|
||||
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
|
||||
import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service';
|
||||
@@ -27,6 +28,10 @@ interface TaskGroupHeaderProps {
|
||||
name: string;
|
||||
count: number;
|
||||
color?: string; // Color for the group indicator
|
||||
todo_progress?: number;
|
||||
doing_progress?: number;
|
||||
done_progress?: number;
|
||||
groupType?: string;
|
||||
};
|
||||
isCollapsed: boolean;
|
||||
onToggle: () => void;
|
||||
@@ -38,13 +43,14 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedTaskIds = useAppSelector(selectSelectedTaskIds);
|
||||
const groups = useAppSelector(selectGroups);
|
||||
const allTasks = useAppSelector(selectAllTasksArray);
|
||||
const currentGrouping = useAppSelector(selectCurrentGrouping);
|
||||
const { statusCategories } = useAppSelector(state => state.taskStatusReducer);
|
||||
const { statusCategories, status: statusList } = useAppSelector(state => state.taskStatusReducer);
|
||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||
const { isOwnerOrAdmin } = useAuthService();
|
||||
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
const [categoryModalVisible, setCategoryModalVisible] = useState(false);
|
||||
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [isChangingCategory, setIsChangingCategory] = useState(false);
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
@@ -62,6 +68,74 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
return currentGroup?.taskIds || [];
|
||||
}, [currentGroup]);
|
||||
|
||||
// Calculate group progress values dynamically
|
||||
const groupProgressValues = useMemo(() => {
|
||||
if (!currentGroup || !allTasks.length) {
|
||||
return { todoProgress: 0, doingProgress: 0, doneProgress: 0 };
|
||||
}
|
||||
|
||||
const tasksInCurrentGroup = currentGroup.taskIds
|
||||
.map(taskId => allTasks.find(task => task.id === taskId))
|
||||
.filter(task => task !== undefined);
|
||||
|
||||
if (tasksInCurrentGroup.length === 0) {
|
||||
return { todoProgress: 0, doingProgress: 0, doneProgress: 0 };
|
||||
}
|
||||
|
||||
// If we're grouping by status, show progress based on task completion
|
||||
if (currentGrouping === 'status') {
|
||||
// For status grouping, calculate based on task progress values
|
||||
const progressStats = tasksInCurrentGroup.reduce((acc, task) => {
|
||||
const progress = task.progress || 0;
|
||||
if (progress === 0) {
|
||||
acc.todo += 1;
|
||||
} else if (progress === 100) {
|
||||
acc.done += 1;
|
||||
} else {
|
||||
acc.doing += 1;
|
||||
}
|
||||
return acc;
|
||||
}, { todo: 0, doing: 0, done: 0 });
|
||||
|
||||
const totalTasks = tasksInCurrentGroup.length;
|
||||
|
||||
return {
|
||||
todoProgress: totalTasks > 0 ? Math.round((progressStats.todo / totalTasks) * 100) : 0,
|
||||
doingProgress: totalTasks > 0 ? Math.round((progressStats.doing / totalTasks) * 100) : 0,
|
||||
doneProgress: totalTasks > 0 ? Math.round((progressStats.done / totalTasks) * 100) : 0,
|
||||
};
|
||||
} else {
|
||||
// For priority/phase grouping, show progress based on status distribution
|
||||
// Use a simplified approach based on status names and common patterns
|
||||
const statusCounts = tasksInCurrentGroup.reduce((acc, task) => {
|
||||
// Find the status by ID first
|
||||
const statusInfo = statusList.find(s => s.id === task.status);
|
||||
const statusName = statusInfo?.name?.toLowerCase() || task.status?.toLowerCase() || '';
|
||||
|
||||
// Categorize based on common status name patterns
|
||||
if (statusName.includes('todo') || statusName.includes('to do') || statusName.includes('pending') || statusName.includes('open') || statusName.includes('backlog')) {
|
||||
acc.todo += 1;
|
||||
} else if (statusName.includes('doing') || statusName.includes('progress') || statusName.includes('active') || statusName.includes('working') || statusName.includes('development')) {
|
||||
acc.doing += 1;
|
||||
} else if (statusName.includes('done') || statusName.includes('completed') || statusName.includes('finished') || statusName.includes('closed') || statusName.includes('resolved')) {
|
||||
acc.done += 1;
|
||||
} else {
|
||||
// Default unknown statuses to "doing" (in progress)
|
||||
acc.doing += 1;
|
||||
}
|
||||
return acc;
|
||||
}, { todo: 0, doing: 0, done: 0 });
|
||||
|
||||
const totalTasks = tasksInCurrentGroup.length;
|
||||
|
||||
return {
|
||||
todoProgress: totalTasks > 0 ? Math.round((statusCounts.todo / totalTasks) * 100) : 0,
|
||||
doingProgress: totalTasks > 0 ? Math.round((statusCounts.doing / totalTasks) * 100) : 0,
|
||||
doneProgress: totalTasks > 0 ? Math.round((statusCounts.done / totalTasks) * 100) : 0,
|
||||
};
|
||||
}
|
||||
}, [currentGroup, allTasks, statusList, currentGrouping]);
|
||||
|
||||
// Calculate selection state for this group
|
||||
const { isAllSelected, isPartiallySelected } = useMemo(() => {
|
||||
if (tasksInGroup.length === 0) {
|
||||
@@ -94,7 +168,12 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
|
||||
// Handle inline name editing
|
||||
const handleNameSave = useCallback(async () => {
|
||||
if (!editingName.trim() || editingName.trim() === group.name || isRenaming) return;
|
||||
// If no changes or already renaming, just exit editing mode
|
||||
if (!editingName.trim() || editingName.trim() === group.name || isRenaming) {
|
||||
setIsEditingName(false);
|
||||
setEditingName(group.name);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRenaming(true);
|
||||
try {
|
||||
@@ -122,12 +201,12 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
|
||||
// Refresh task list to get updated group names
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
setIsEditingName(false);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error renaming group:', error);
|
||||
setEditingName(group.name);
|
||||
} finally {
|
||||
setIsEditingName(false);
|
||||
setIsRenaming(false);
|
||||
}
|
||||
}, [editingName, group.name, group.id, currentGrouping, projectId, dispatch, trackMixpanelEvent, isRenaming]);
|
||||
@@ -150,9 +229,8 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
}, [group.name, handleNameSave]);
|
||||
|
||||
const handleNameBlur = useCallback(() => {
|
||||
setIsEditingName(false);
|
||||
setEditingName(group.name);
|
||||
}, [group.name]);
|
||||
handleNameSave();
|
||||
}, [handleNameSave]);
|
||||
|
||||
// Handle dropdown menu actions
|
||||
const handleRenameGroup = useCallback(() => {
|
||||
@@ -161,10 +239,7 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
setEditingName(group.name);
|
||||
}, [group.name]);
|
||||
|
||||
const handleChangeCategory = useCallback(() => {
|
||||
setDropdownVisible(false);
|
||||
setCategoryModalVisible(true);
|
||||
}, []);
|
||||
|
||||
|
||||
// Handle category change
|
||||
const handleCategoryChange = useCallback(async (categoryId: string, e?: React.MouseEvent) => {
|
||||
@@ -182,7 +257,6 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
// Refresh status list and tasks
|
||||
dispatch(fetchStatuses(projectId));
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
setCategoryModalVisible(false);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error changing category:', error);
|
||||
@@ -209,19 +283,30 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
|
||||
// Only show "Change Category" when grouped by status
|
||||
if (currentGrouping === 'status') {
|
||||
items.push({
|
||||
key: 'changeCategory',
|
||||
icon: <ArrowPathIcon className="h-4 w-4" />,
|
||||
label: t('changeCategory'),
|
||||
const categorySubMenuItems = statusCategories.map((category) => ({
|
||||
key: `category-${category.id}`,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge color={category.color_code} />
|
||||
<span>{category.name}</span>
|
||||
</div>
|
||||
),
|
||||
onClick: (e: any) => {
|
||||
e?.domEvent?.stopPropagation();
|
||||
handleChangeCategory();
|
||||
handleCategoryChange(category.id || '', e?.domEvent);
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
items.push({
|
||||
key: 'changeCategory',
|
||||
icon: <ArrowPathIcon className="h-4 w-4" />,
|
||||
label: t('changeCategory'),
|
||||
children: categorySubMenuItems,
|
||||
} as any);
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [currentGrouping, handleRenameGroup, handleChangeCategory, isOwnerOrAdmin]);
|
||||
}, [currentGrouping, handleRenameGroup, handleCategoryChange, isOwnerOrAdmin, statusCategories, t]);
|
||||
|
||||
// Make the group header droppable
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
@@ -232,75 +317,146 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={`inline-flex w-max items-center px-1 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out border-t border-b border-gray-200 dark:border-gray-700 rounded-t-md pr-2 ${
|
||||
isOver ? 'ring-2 ring-blue-400 ring-opacity-50' : ''
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: isOver ? `${headerBackgroundColor}dd` : headerBackgroundColor,
|
||||
color: headerTextColor,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 25, // Higher than task rows but lower than column headers (z-30)
|
||||
height: '36px',
|
||||
minHeight: '36px',
|
||||
maxHeight: '36px'
|
||||
}}
|
||||
onClick={onToggle}
|
||||
>
|
||||
{/* Drag Handle Space - ultra minimal width */}
|
||||
<div style={{ width: '20px' }} className="flex items-center justify-center">
|
||||
{/* Chevron button */}
|
||||
<button
|
||||
className="p-0 rounded-sm hover:shadow-lg hover:scale-105 transition-all duration-300 ease-out"
|
||||
style={{ backgroundColor: 'transparent', color: headerTextColor }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="transition-transform duration-300 ease-out"
|
||||
style={{
|
||||
transform: isCollapsed ? 'rotate(0deg)' : 'rotate(90deg)',
|
||||
transformOrigin: 'center'
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={`inline-flex w-max items-center px-1 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out border-t border-b border-gray-200 dark:border-gray-700 rounded-t-md pr-2 ${
|
||||
isOver ? 'ring-2 ring-blue-400 ring-opacity-50' : ''
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: isOver ? `${headerBackgroundColor}dd` : headerBackgroundColor,
|
||||
color: headerTextColor,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 25, // Higher than task rows but lower than column headers (z-30)
|
||||
height: '36px',
|
||||
minHeight: '36px',
|
||||
maxHeight: '36px'
|
||||
}}
|
||||
onClick={onToggle}
|
||||
>
|
||||
{/* Drag Handle Space - ultra minimal width */}
|
||||
<div style={{ width: '20px' }} className="flex items-center justify-center">
|
||||
{/* Chevron button */}
|
||||
<button
|
||||
className="p-0 rounded-sm hover:shadow-lg hover:scale-105 transition-all duration-300 ease-out"
|
||||
style={{ backgroundColor: 'transparent', color: headerTextColor }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
>
|
||||
<ChevronRightIcon className="h-3 w-3" style={{ color: headerTextColor }} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="transition-transform duration-300 ease-out"
|
||||
style={{
|
||||
transform: isCollapsed ? 'rotate(0deg)' : 'rotate(90deg)',
|
||||
transformOrigin: 'center'
|
||||
}}
|
||||
>
|
||||
<ChevronRightIcon className="h-3 w-3" style={{ color: headerTextColor }} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Select All Checkbox Space - ultra minimal width */}
|
||||
<div style={{ width: '28px' }} className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
indeterminate={isPartiallySelected}
|
||||
onChange={handleSelectAllChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
color: headerTextColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Select All Checkbox Space - ultra minimal width */}
|
||||
<div style={{ width: '28px' }} className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
indeterminate={isPartiallySelected}
|
||||
onChange={handleSelectAllChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
color: headerTextColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Group indicator and name - no gap at all */}
|
||||
{/* Group indicator and name - no gap at all */}
|
||||
<div className="flex items-center flex-1 ml-1">
|
||||
{/* Group name and count */}
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className="text-sm font-semibold pr-2"
|
||||
style={{ color: headerTextColor }}
|
||||
>
|
||||
{group.name}
|
||||
</span>
|
||||
{isEditingName ? (
|
||||
<Input
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
onBlur={handleNameBlur}
|
||||
autoFocus
|
||||
size="small"
|
||||
className="text-sm font-semibold"
|
||||
style={{
|
||||
width: 'auto',
|
||||
minWidth: '100px',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: headerTextColor,
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="text-sm font-semibold pr-2 cursor-pointer hover:underline"
|
||||
style={{ color: headerTextColor }}
|
||||
onClick={handleNameClick}
|
||||
>
|
||||
{group.name}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm font-semibold ml-1" style={{ color: headerTextColor }}>
|
||||
({group.count})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Three-dot menu - only show for status and phase grouping */}
|
||||
{menuItems.length > 0 && (currentGrouping === 'status' || currentGrouping === 'phase') && (
|
||||
<div className="flex items-center ml-2">
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
trigger={['click']}
|
||||
open={dropdownVisible}
|
||||
onOpenChange={setDropdownVisible}
|
||||
placement="bottomRight"
|
||||
overlayStyle={{ zIndex: 1000 }}
|
||||
>
|
||||
<button
|
||||
className="p-1 rounded-sm hover:bg-black hover:bg-opacity-10 transition-colors duration-200"
|
||||
style={{ color: headerTextColor }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDropdownVisible(!dropdownVisible);
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Progress Bar - sticky to the right edge during horizontal scroll */}
|
||||
{(currentGrouping === 'priority' || currentGrouping === 'phase') &&
|
||||
(groupProgressValues.todoProgress || groupProgressValues.doingProgress || groupProgressValues.doneProgress) && (
|
||||
<div
|
||||
className="flex items-center bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm px-3 py-1.5 ml-auto"
|
||||
style={{
|
||||
position: 'sticky',
|
||||
right: '16px',
|
||||
zIndex: 35, // Higher than header
|
||||
minWidth: '160px',
|
||||
height: '30px'
|
||||
}}
|
||||
>
|
||||
<GroupProgressBar
|
||||
todoProgress={groupProgressValues.todoProgress}
|
||||
doingProgress={groupProgressValues.doingProgress}
|
||||
doneProgress={groupProgressValues.doneProgress}
|
||||
groupType={group.groupType || currentGrouping || ''}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
setCustomColumnModalAttributes,
|
||||
toggleCustomColumnModalOpen,
|
||||
} from '@/features/projects/singleProject/task-list-custom-columns/task-list-custom-columns-slice';
|
||||
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
|
||||
|
||||
// Components
|
||||
import TaskRowWithSubtasks from './TaskRowWithSubtasks';
|
||||
@@ -64,6 +65,7 @@ import CustomColumnModal from '@/pages/projects/projectView/taskList/task-list-t
|
||||
import AddTaskRow from './components/AddTaskRow';
|
||||
import { AddCustomColumnButton, CustomColumnHeader } from './components/CustomColumnComponents';
|
||||
import TaskListSkeleton from './components/TaskListSkeleton';
|
||||
import ConvertToSubtaskDrawer from '@/components/task-list-common/convert-to-subtask-drawer/convert-to-subtask-drawer';
|
||||
|
||||
// Hooks and utilities
|
||||
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||
@@ -212,6 +214,7 @@ const TaskListV2Section: React.FC = () => {
|
||||
if (urlProjectId) {
|
||||
dispatch(fetchTasksV3(urlProjectId));
|
||||
dispatch(fetchTaskListColumns(urlProjectId));
|
||||
dispatch(fetchPhasesByProjectId(urlProjectId));
|
||||
}
|
||||
}, [dispatch, urlProjectId]);
|
||||
|
||||
@@ -452,6 +455,10 @@ const TaskListV2Section: React.FC = () => {
|
||||
name: group.title,
|
||||
count: group.actualCount,
|
||||
color: group.color,
|
||||
todo_progress: group.todo_progress,
|
||||
doing_progress: group.doing_progress,
|
||||
done_progress: group.done_progress,
|
||||
groupType: group.groupType,
|
||||
}}
|
||||
isCollapsed={isGroupCollapsed}
|
||||
onToggle={() => handleGroupCollapse(group.id)}
|
||||
@@ -459,7 +466,7 @@ const TaskListV2Section: React.FC = () => {
|
||||
/>
|
||||
{isGroupEmpty && !isGroupCollapsed && (
|
||||
<div className="relative w-full">
|
||||
<div className="flex items-center min-w-max px-1 py-3">
|
||||
<div className="flex items-center min-w-max px-1 py-6">
|
||||
{visibleColumns.map((column, index) => {
|
||||
const emptyColumnStyle = {
|
||||
width: column.width,
|
||||
@@ -478,7 +485,7 @@ const TaskListV2Section: React.FC = () => {
|
||||
})}
|
||||
</div>
|
||||
<div className="absolute left-4 top-1/2 transform -translate-y-1/2 flex items-center">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-900 px-3 py-1.5 rounded-md border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-900 px-4 py-3 rounded-md border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
{t('noTasksInGroup')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -760,6 +767,9 @@ const TaskListV2Section: React.FC = () => {
|
||||
|
||||
{/* Custom Column Modal */}
|
||||
{createPortal(<CustomColumnModal />, document.body, 'custom-column-modal')}
|
||||
|
||||
{/* Convert To Subtask Drawer */}
|
||||
{createPortal(<ConvertToSubtaskDrawer />, document.body, 'convert-to-subtask-drawer')}
|
||||
</div>
|
||||
</DndContext>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,529 @@
|
||||
import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { tasksApiService } from '@/api/tasks/tasks.api.service';
|
||||
import { taskListBulkActionsApiService } from '@/api/tasks/task-list-bulk-actions.api.service';
|
||||
import { IBulkAssignRequest } from '@/types/tasks/bulk-action-bar.types';
|
||||
import {
|
||||
deleteTask,
|
||||
fetchTasksV3,
|
||||
IGroupBy,
|
||||
toggleTaskExpansion,
|
||||
updateTaskAssignees,
|
||||
} from '@/features/task-management/task-management.slice';
|
||||
import { deselectAll, selectTasks } from '@/features/projects/bulkActions/bulkActionSlice';
|
||||
import { setConvertToSubtaskDrawerOpen } from '@/features/tasks/tasks.slice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||
import {
|
||||
evt_project_task_list_context_menu_archive,
|
||||
evt_project_task_list_context_menu_assign_me,
|
||||
evt_project_task_list_context_menu_delete,
|
||||
} from '@/shared/worklenz-analytics-events';
|
||||
import {
|
||||
DeleteOutlined,
|
||||
DoubleRightOutlined,
|
||||
InboxOutlined,
|
||||
RetweetOutlined,
|
||||
UserAddOutlined,
|
||||
LoadingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
interface TaskContextMenuProps {
|
||||
task: Task;
|
||||
projectId: string;
|
||||
position: { x: number; y: number };
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TaskContextMenu: React.FC<TaskContextMenuProps> = ({
|
||||
task,
|
||||
projectId,
|
||||
position,
|
||||
onClose,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation('task-list-table');
|
||||
const { socket, connected } = useSocket();
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||
|
||||
const { groups: taskGroups } = useAppSelector(state => state.taskManagement);
|
||||
const statusList = useAppSelector(state => state.taskStatusReducer.status);
|
||||
const priorityList = useAppSelector(state => state.priorityReducer.priorities);
|
||||
const phaseList = useAppSelector(state => state.phaseReducer.phaseList);
|
||||
const currentGrouping = useAppSelector(state => state.grouping.currentGrouping);
|
||||
const archived = useAppSelector(state => state.taskReducer.archived);
|
||||
|
||||
const [updatingAssignToMe, setUpdatingAssignToMe] = useState(false);
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
const handleAssignToMe = useCallback(async () => {
|
||||
if (!projectId || !task.id || !currentSession?.team_member_id) return;
|
||||
|
||||
try {
|
||||
setUpdatingAssignToMe(true);
|
||||
|
||||
// Immediate UI update - add current user to assignees
|
||||
const currentUser = {
|
||||
id: currentSession.team_member_id,
|
||||
name: currentSession.name || '',
|
||||
email: currentSession.email || '',
|
||||
avatar_url: currentSession.avatar_url || '',
|
||||
team_member_id: currentSession.team_member_id,
|
||||
};
|
||||
|
||||
const updatedAssignees = task.assignees || [];
|
||||
const updatedAssigneeNames = task.assignee_names || [];
|
||||
|
||||
// Check if current user is already assigned
|
||||
const isAlreadyAssigned = updatedAssignees.includes(currentSession.team_member_id);
|
||||
|
||||
if (!isAlreadyAssigned) {
|
||||
// Add current user to assignees for immediate UI feedback
|
||||
const newAssignees = [...updatedAssignees, currentSession.team_member_id];
|
||||
const newAssigneeNames = [...updatedAssigneeNames, currentUser];
|
||||
|
||||
// Update Redux store immediately for instant UI feedback
|
||||
dispatch(
|
||||
updateTaskAssignees({
|
||||
taskId: task.id,
|
||||
assigneeIds: newAssignees,
|
||||
assigneeNames: newAssigneeNames,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const body: IBulkAssignRequest = {
|
||||
tasks: [task.id],
|
||||
project_id: projectId,
|
||||
};
|
||||
const res = await taskListBulkActionsApiService.assignToMe(body);
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_context_menu_assign_me);
|
||||
// Socket event will handle syncing with other users
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error assigning to me:', error);
|
||||
// Revert the optimistic update on error
|
||||
dispatch(
|
||||
updateTaskAssignees({
|
||||
taskId: task.id,
|
||||
assigneeIds: task.assignees || [],
|
||||
assigneeNames: task.assignee_names || [],
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setUpdatingAssignToMe(false);
|
||||
onClose();
|
||||
}
|
||||
}, [projectId, task.id, task.assignees, task.assignee_names, currentSession, dispatch, onClose, trackMixpanelEvent]);
|
||||
|
||||
const handleArchive = useCallback(async () => {
|
||||
if (!projectId || !task.id) return;
|
||||
|
||||
try {
|
||||
const res = await taskListBulkActionsApiService.archiveTasks(
|
||||
{
|
||||
tasks: [task.id],
|
||||
project_id: projectId,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_context_menu_archive);
|
||||
dispatch(deleteTask(task.id));
|
||||
dispatch(deselectAll());
|
||||
if (task.parent_task_id) {
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error archiving task:', error);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
}, [projectId, task.id, task.parent_task_id, dispatch, socket, onClose, trackMixpanelEvent]);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!projectId || !task.id) return;
|
||||
|
||||
try {
|
||||
const res = await taskListBulkActionsApiService.deleteTasks({ tasks: [task.id] }, projectId);
|
||||
|
||||
if (res.done) {
|
||||
trackMixpanelEvent(evt_project_task_list_context_menu_delete);
|
||||
dispatch(deleteTask(task.id));
|
||||
dispatch(deselectAll());
|
||||
if (task.parent_task_id) {
|
||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error deleting task:', error);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
}, [projectId, task.id, task.parent_task_id, dispatch, socket, onClose, trackMixpanelEvent]);
|
||||
|
||||
const handleStatusMoveTo = useCallback(
|
||||
async (targetId: string) => {
|
||||
if (!projectId || !task.id || !targetId) return;
|
||||
|
||||
try {
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_STATUS_CHANGE.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
status_id: targetId,
|
||||
parent_task: task.parent_task_id || null,
|
||||
team_id: currentSession?.team_id,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Error moving status:', error);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[projectId, task.id, task.parent_task_id, currentSession?.team_id, socket, onClose]
|
||||
);
|
||||
|
||||
const handlePriorityMoveTo = useCallback(
|
||||
async (targetId: string) => {
|
||||
if (!projectId || !task.id || !targetId) return;
|
||||
|
||||
try {
|
||||
socket?.emit(
|
||||
SocketEvents.TASK_PRIORITY_CHANGE.toString(),
|
||||
JSON.stringify({
|
||||
task_id: task.id,
|
||||
priority_id: targetId,
|
||||
parent_task: task.parent_task_id || null,
|
||||
team_id: currentSession?.team_id,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Error moving priority:', error);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[projectId, task.id, task.parent_task_id, currentSession?.team_id, socket, onClose]
|
||||
);
|
||||
|
||||
const handlePhaseMoveTo = useCallback(
|
||||
async (targetId: string) => {
|
||||
if (!projectId || !task.id || !targetId) return;
|
||||
|
||||
try {
|
||||
socket?.emit(SocketEvents.TASK_PHASE_CHANGE.toString(), {
|
||||
task_id: task.id,
|
||||
phase_id: targetId,
|
||||
parent_task: task.parent_task_id || null,
|
||||
team_id: currentSession?.team_id,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error moving phase:', error);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[projectId, task.id, task.parent_task_id, currentSession?.team_id, socket, onClose]
|
||||
);
|
||||
|
||||
const getMoveToOptions = useCallback(() => {
|
||||
let options: { key: string; label: React.ReactNode; onClick: () => void }[] = [];
|
||||
|
||||
if (currentGrouping === IGroupBy.STATUS) {
|
||||
options = statusList.filter(status => status.id).map(status => ({
|
||||
key: status.id!,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: status.color_code }}
|
||||
></span>
|
||||
<span>{status.name}</span>
|
||||
</div>
|
||||
),
|
||||
onClick: () => handleStatusMoveTo(status.id!),
|
||||
}));
|
||||
} else if (currentGrouping === IGroupBy.PRIORITY) {
|
||||
options = priorityList.filter(priority => priority.id).map(priority => ({
|
||||
key: priority.id!,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: priority.color_code }}
|
||||
></span>
|
||||
<span>{priority.name}</span>
|
||||
</div>
|
||||
),
|
||||
onClick: () => handlePriorityMoveTo(priority.id!),
|
||||
}));
|
||||
} else if (currentGrouping === IGroupBy.PHASE) {
|
||||
options = phaseList.filter(phase => phase.id).map(phase => ({
|
||||
key: phase.id!,
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: phase.color_code }}
|
||||
></span>
|
||||
<span>{phase.name}</span>
|
||||
</div>
|
||||
),
|
||||
onClick: () => handlePhaseMoveTo(phase.id!),
|
||||
}));
|
||||
}
|
||||
return options;
|
||||
}, [
|
||||
currentGrouping,
|
||||
statusList,
|
||||
priorityList,
|
||||
phaseList,
|
||||
handleStatusMoveTo,
|
||||
handlePriorityMoveTo,
|
||||
handlePhaseMoveTo,
|
||||
]);
|
||||
|
||||
const handleConvertToTask = useCallback(async () => {
|
||||
if (!task?.id || !projectId) return;
|
||||
|
||||
try {
|
||||
const res = await tasksApiService.convertToTask(task.id as string, projectId as string);
|
||||
if (res.done) {
|
||||
dispatch(deselectAll());
|
||||
dispatch(fetchTasksV3(projectId));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error converting to task', error);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
}, [task?.id, projectId, dispatch, onClose]);
|
||||
|
||||
const menuItems = useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
key: 'assignToMe',
|
||||
label: (
|
||||
<button
|
||||
onClick={handleAssignToMe}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 w-full text-left"
|
||||
disabled={updatingAssignToMe}
|
||||
>
|
||||
{updatingAssignToMe ? (
|
||||
<LoadingOutlined className="text-gray-500 dark:text-gray-400" />
|
||||
) : (
|
||||
<UserAddOutlined className="text-gray-500 dark:text-gray-400" />
|
||||
)}
|
||||
<span>{t('contextMenu.assignToMe')}</span>
|
||||
</button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Add Move To submenu if there are options
|
||||
const moveToOptions = getMoveToOptions();
|
||||
if (moveToOptions.length > 0) {
|
||||
items.push({
|
||||
key: 'moveTo',
|
||||
label: (
|
||||
<div className="relative group">
|
||||
<button className="flex items-center justify-between gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 w-full text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<RetweetOutlined className="text-gray-500 dark:text-gray-400" />
|
||||
<span>{t('contextMenu.moveTo')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-500 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<ul className="absolute left-full top-0 mt-0 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg z-20 hidden group-hover:block">
|
||||
{moveToOptions.map(option => (
|
||||
<li key={option.key}>
|
||||
<button
|
||||
onClick={option.onClick}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 w-full text-left"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Add Archive/Unarchive for parent tasks only
|
||||
if (!task?.parent_task_id) {
|
||||
items.push({
|
||||
key: 'archive',
|
||||
label: (
|
||||
<button
|
||||
onClick={handleArchive}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 w-full text-left"
|
||||
>
|
||||
<InboxOutlined className="text-gray-500 dark:text-gray-400" />
|
||||
<span>{archived ? t('contextMenu.unarchive') : t('contextMenu.archive')}</span>
|
||||
</button>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Add Convert to Sub Task for parent tasks with no subtasks
|
||||
if (task?.sub_tasks_count === 0 && !task?.parent_task_id) {
|
||||
items.push({
|
||||
key: 'convertToSubTask',
|
||||
label: (
|
||||
<button
|
||||
onClick={() => {
|
||||
// Convert task to the format expected by bulkActionSlice
|
||||
const projectTask = {
|
||||
id: task.id,
|
||||
name: task.title || task.name || '',
|
||||
task_key: task.task_key,
|
||||
status: task.status,
|
||||
status_id: task.status,
|
||||
priority: task.priority,
|
||||
phase_id: task.phase,
|
||||
phase_name: task.phase,
|
||||
description: task.description,
|
||||
start_date: task.startDate,
|
||||
end_date: task.dueDate,
|
||||
total_hours: task.timeTracking?.estimated || 0,
|
||||
total_minutes: task.timeTracking?.logged || 0,
|
||||
progress: task.progress,
|
||||
sub_tasks_count: task.sub_tasks_count || 0,
|
||||
assignees: task.assignees?.map((assigneeId: string) => ({
|
||||
id: assigneeId,
|
||||
name: '',
|
||||
email: '',
|
||||
avatar_url: '',
|
||||
team_member_id: assigneeId,
|
||||
project_member_id: assigneeId,
|
||||
})) || [],
|
||||
labels: task.labels || [],
|
||||
manual_progress: false,
|
||||
created_at: task.createdAt,
|
||||
updated_at: task.updatedAt,
|
||||
sort_order: task.order,
|
||||
};
|
||||
|
||||
// Select the task in bulk action reducer
|
||||
dispatch(selectTasks([projectTask]));
|
||||
|
||||
// Open the drawer
|
||||
dispatch(setConvertToSubtaskDrawerOpen(true));
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 w-full text-left"
|
||||
>
|
||||
<DoubleRightOutlined className="text-gray-500 dark:text-gray-400" />
|
||||
<span>{t('contextMenu.convertToSubTask')}</span>
|
||||
</button>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Add Convert to Task for subtasks
|
||||
if (task?.parent_task_id) {
|
||||
items.push({
|
||||
key: 'convertToTask',
|
||||
label: (
|
||||
<button
|
||||
onClick={handleConvertToTask}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 w-full text-left"
|
||||
>
|
||||
<DoubleRightOutlined className="text-gray-500 dark:text-gray-400" />
|
||||
<span>{t('contextMenu.convertToTask')}</span>
|
||||
</button>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Add Delete
|
||||
items.push({
|
||||
key: 'delete',
|
||||
label: (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/20 w-full text-left"
|
||||
>
|
||||
<DeleteOutlined className="text-red-500 dark:text-red-400" />
|
||||
<span>{t('contextMenu.delete')}</span>
|
||||
</button>
|
||||
),
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [
|
||||
task,
|
||||
projectId,
|
||||
updatingAssignToMe,
|
||||
archived,
|
||||
handleAssignToMe,
|
||||
handleArchive,
|
||||
handleDelete,
|
||||
handleConvertToTask,
|
||||
getMoveToOptions,
|
||||
dispatch,
|
||||
t,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="fixed bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg py-1 min-w-48"
|
||||
style={{
|
||||
top: position.y,
|
||||
left: position.x,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<ul className="list-none p-0 m-0">
|
||||
{menuItems.map(item => (
|
||||
<li key={item.key} className="relative group">
|
||||
{item.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskContextMenu;
|
||||
@@ -2,6 +2,7 @@ import React, { memo, useCallback, useState, useRef, useEffect } from 'react';
|
||||
import { RightOutlined, DoubleRightOutlined, ArrowsAltOutlined, CommentOutlined, EyeOutlined, PaperClipOutlined, MinusCircleOutlined, RetweetOutlined } from '@ant-design/icons';
|
||||
import { Input, Tooltip } from 'antd';
|
||||
import type { InputRef } from 'antd';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { toggleTaskExpansion, fetchSubTasks } from '@/features/task-management/task-management.slice';
|
||||
@@ -10,6 +11,7 @@ import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTaskDisplayName } from './TaskRowColumns';
|
||||
import TaskContextMenu from './TaskContextMenu';
|
||||
|
||||
interface TitleColumnProps {
|
||||
width: string;
|
||||
@@ -41,6 +43,10 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
|
||||
const { t } = useTranslation('task-list-table');
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Context menu state
|
||||
const [contextMenuVisible, setContextMenuVisible] = useState(false);
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
// Handle task expansion toggle
|
||||
const handleToggleExpansion = useCallback((e: React.MouseEvent) => {
|
||||
@@ -71,6 +77,24 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
|
||||
onEditTaskName(false);
|
||||
}, [taskName, connected, socket, task.id, task.parent_task_id, task.title, task.name, onEditTaskName]);
|
||||
|
||||
// Handle context menu
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Use clientX and clientY directly for fixed positioning
|
||||
setContextMenuPosition({
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
});
|
||||
setContextMenuVisible(true);
|
||||
}, []);
|
||||
|
||||
// Handle context menu close
|
||||
const handleContextMenuClose = useCallback(() => {
|
||||
setContextMenuVisible(false);
|
||||
}, []);
|
||||
|
||||
// Handle click outside for task name editing
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@@ -169,6 +193,7 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
|
||||
e.preventDefault();
|
||||
onEditTaskName(true);
|
||||
}}
|
||||
onContextMenu={handleContextMenu}
|
||||
title={taskDisplayName}
|
||||
>
|
||||
{taskDisplayName}
|
||||
@@ -251,6 +276,17 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Context Menu */}
|
||||
{contextMenuVisible && createPortal(
|
||||
<TaskContextMenu
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
position={contextMenuPosition}
|
||||
onClose={handleContextMenuClose}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,6 +20,112 @@
|
||||
border-top: 1px solid #303030;
|
||||
}
|
||||
|
||||
/* Dark mode confirmation modal styling */
|
||||
.dark .ant-modal-confirm .ant-modal-content,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-content {
|
||||
background-color: #1f1f1f !important;
|
||||
border: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-header,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-header {
|
||||
background-color: #1f1f1f !important;
|
||||
border-bottom: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-body,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-body {
|
||||
background-color: #1f1f1f !important;
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-footer,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-footer {
|
||||
background-color: #1f1f1f !important;
|
||||
border-top: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-confirm-title,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-confirm-title {
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-confirm-content,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-confirm-content {
|
||||
color: #8c8c8c !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-default,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-default {
|
||||
background-color: #141414 !important;
|
||||
border-color: #303030 !important;
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-default:hover,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-default:hover {
|
||||
background-color: #262626 !important;
|
||||
border-color: #40a9ff !important;
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-primary,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-primary {
|
||||
background-color: #1890ff !important;
|
||||
border-color: #1890ff !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-primary:hover,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-primary:hover {
|
||||
background-color: #40a9ff !important;
|
||||
border-color: #40a9ff !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-dangerous,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-dangerous {
|
||||
background-color: #ff4d4f !important;
|
||||
border-color: #ff4d4f !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-dangerous:hover,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-dangerous:hover {
|
||||
background-color: #ff7875 !important;
|
||||
border-color: #ff7875 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Light mode confirmation modal styling (ensure consistency) */
|
||||
.ant-modal-confirm .ant-modal-content {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-header {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-body {
|
||||
background-color: #ffffff;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-footer {
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-confirm-title {
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-confirm-content {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.dark-modal .ant-form-item-label > label {
|
||||
color: #d9d9d9;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
deletePhaseOption,
|
||||
updatePhaseColor,
|
||||
} from '@/features/projects/singleProject/phase/phases.slice';
|
||||
import { updatePhaseLabel } from '@/features/project/project.slice';
|
||||
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
|
||||
import { Modal as AntModal } from 'antd';
|
||||
import { fetchTasksV3 } from '@/features/task-management/task-management.slice';
|
||||
@@ -307,7 +308,7 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
if (!newPhaseName.trim() || !finalProjectId) return;
|
||||
|
||||
try {
|
||||
await dispatch(addPhaseOption({ projectId: finalProjectId }));
|
||||
await dispatch(addPhaseOption({ projectId: finalProjectId, name: newPhaseName.trim() }));
|
||||
await dispatch(fetchPhasesByProjectId(finalProjectId));
|
||||
await refreshTasks();
|
||||
setNewPhaseName('');
|
||||
@@ -408,6 +409,7 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
).unwrap();
|
||||
|
||||
if (res.done) {
|
||||
dispatch(updatePhaseLabel(phaseName));
|
||||
setInitialPhaseName(phaseName);
|
||||
await refreshTasks();
|
||||
}
|
||||
@@ -428,7 +430,7 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
<Title level={4} className={`m-0 font-semibold ${
|
||||
isDarkMode ? 'text-gray-100' : 'text-gray-800'
|
||||
}`}>
|
||||
{t('configurePhases')}
|
||||
{t('configure')} {phaseName || project?.phase_label || t('phasesText')}
|
||||
</Title>
|
||||
}
|
||||
open={open}
|
||||
@@ -495,7 +497,7 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
<Text className={`text-xs font-medium ${
|
||||
isDarkMode ? 'text-gray-300' : 'text-blue-700'
|
||||
}`}>
|
||||
🎨 Drag phases to reorder them. Click on a phase name to rename it. Each phase can have a custom color.
|
||||
🎨 Drag {(phaseName || project?.phase_label || t('phasesText')).toLowerCase()} to reorder them. Click on a {(phaseName || project?.phase_label || t('phaseText')).toLowerCase()} name to rename it. Each {(phaseName || project?.phase_label || t('phaseText')).toLowerCase()} can have a custom color.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -558,7 +560,7 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
<Text className={`text-xs font-medium ${
|
||||
isDarkMode ? 'text-gray-300' : 'text-gray-700'
|
||||
}`}>
|
||||
{t('phaseOptions')}
|
||||
{phaseName || project?.phase_label || t('phasesText')} {t('optionsText')}
|
||||
</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -601,7 +603,7 @@ const ManagePhaseModal: React.FC<ManagePhaseModalProps> = ({
|
||||
isDarkMode ? 'text-gray-400' : 'text-gray-500'
|
||||
}`}>
|
||||
<Text className="text-sm font-medium">
|
||||
{t('noPhasesFound')}
|
||||
{t('no')} {(phaseName || project?.phase_label || t('phasesText')).toLowerCase()} {t('found')}
|
||||
</Text>
|
||||
<br />
|
||||
<Button
|
||||
|
||||
@@ -20,6 +20,112 @@
|
||||
border-top: 1px solid #303030;
|
||||
}
|
||||
|
||||
/* Dark mode confirmation modal styling */
|
||||
.dark .ant-modal-confirm .ant-modal-content,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-content {
|
||||
background-color: #1f1f1f !important;
|
||||
border: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-header,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-header {
|
||||
background-color: #1f1f1f !important;
|
||||
border-bottom: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-body,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-body {
|
||||
background-color: #1f1f1f !important;
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-footer,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-footer {
|
||||
background-color: #1f1f1f !important;
|
||||
border-top: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-confirm-title,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-confirm-title {
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-confirm-content,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-confirm-content {
|
||||
color: #8c8c8c !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-default,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-default {
|
||||
background-color: #141414 !important;
|
||||
border-color: #303030 !important;
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-default:hover,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-default:hover {
|
||||
background-color: #262626 !important;
|
||||
border-color: #40a9ff !important;
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-primary,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-primary {
|
||||
background-color: #1890ff !important;
|
||||
border-color: #1890ff !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-primary:hover,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-primary:hover {
|
||||
background-color: #40a9ff !important;
|
||||
border-color: #40a9ff !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-dangerous,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-dangerous {
|
||||
background-color: #ff4d4f !important;
|
||||
border-color: #ff4d4f !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-dangerous:hover,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-dangerous:hover {
|
||||
background-color: #ff7875 !important;
|
||||
border-color: #ff7875 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Light mode confirmation modal styling (ensure consistency) */
|
||||
.ant-modal-confirm .ant-modal-content {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-header {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-body {
|
||||
background-color: #ffffff;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-footer {
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-confirm-title {
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-confirm-content {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.dark-modal .ant-form-item-label > label {
|
||||
color: #d9d9d9;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { IKanbanTaskStatus } from '@/types/tasks/taskStatus.types';
|
||||
import { Modal as AntModal } from 'antd';
|
||||
import { fetchTasksV3 } from '@/features/task-management/task-management.slice';
|
||||
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||
import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
|
||||
import './ManageStatusModal.css';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
@@ -594,7 +593,6 @@ const ManageStatusModal: React.FC<ManageStatusModalProps> = ({
|
||||
// Refresh from server to ensure consistency
|
||||
dispatch(fetchStatuses(finalProjectId));
|
||||
dispatch(fetchTasksV3(finalProjectId));
|
||||
dispatch(fetchTaskGroups(finalProjectId));
|
||||
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
||||
} catch (error) {
|
||||
console.error('Error changing status category:', error);
|
||||
@@ -736,7 +734,6 @@ const ManageStatusModal: React.FC<ManageStatusModalProps> = ({
|
||||
statusApiService.updateStatusOrder(requestBody, finalProjectId).then(() => {
|
||||
// Refresh task lists after status order change
|
||||
dispatch(fetchTasksV3(finalProjectId));
|
||||
dispatch(fetchTaskGroups(finalProjectId));
|
||||
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
||||
}).catch(error => {
|
||||
console.error('Error updating status order:', error);
|
||||
@@ -767,7 +764,6 @@ const ManageStatusModal: React.FC<ManageStatusModalProps> = ({
|
||||
if (res.done) {
|
||||
dispatch(fetchStatuses(finalProjectId));
|
||||
dispatch(fetchTasksV3(finalProjectId));
|
||||
dispatch(fetchTaskGroups(finalProjectId));
|
||||
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -791,7 +787,6 @@ const ManageStatusModal: React.FC<ManageStatusModalProps> = ({
|
||||
await statusApiService.updateNameOfStatus(id, body, finalProjectId);
|
||||
dispatch(fetchStatuses(finalProjectId));
|
||||
dispatch(fetchTasksV3(finalProjectId));
|
||||
dispatch(fetchTaskGroups(finalProjectId));
|
||||
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
||||
} catch (error) {
|
||||
console.error('Error renaming status:', error);
|
||||
@@ -813,7 +808,6 @@ const ManageStatusModal: React.FC<ManageStatusModalProps> = ({
|
||||
await statusApiService.deleteStatus(id, finalProjectId, replacingStatusId);
|
||||
dispatch(fetchStatuses(finalProjectId));
|
||||
dispatch(fetchTasksV3(finalProjectId));
|
||||
dispatch(fetchTaskGroups(finalProjectId));
|
||||
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
||||
} catch (error) {
|
||||
console.error('Error deleting status:', error);
|
||||
|
||||
@@ -369,6 +369,7 @@ const FilterDropdown: React.FC<{
|
||||
dispatch?: any;
|
||||
onManageStatus?: () => void;
|
||||
onManagePhase?: () => void;
|
||||
projectPhaseLabel?: string; // Add this prop
|
||||
}> = ({
|
||||
section,
|
||||
onSelectionChange,
|
||||
@@ -380,6 +381,7 @@ const FilterDropdown: React.FC<{
|
||||
dispatch,
|
||||
onManageStatus,
|
||||
onManagePhase,
|
||||
projectPhaseLabel, // Add this prop
|
||||
}) => {
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
// Add permission checks for groupBy section
|
||||
@@ -495,7 +497,7 @@ const FilterDropdown: React.FC<{
|
||||
isDarkMode ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'
|
||||
}`}
|
||||
>
|
||||
{t('managePhases')}
|
||||
{t('manage')} {projectPhaseLabel || t('phasesText')}
|
||||
</button>
|
||||
)}
|
||||
{section.selectedValues[0] === 'status' && (
|
||||
@@ -994,6 +996,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
|
||||
const isDarkMode = useAppSelector(state => state.themeReducer?.mode === 'dark');
|
||||
const { projectId } = useAppSelector(state => state.projectReducer);
|
||||
const { projectView } = useTabSearchParam();
|
||||
const projectPhaseLabel = useAppSelector(state => state.projectReducer.project?.phase_label);
|
||||
|
||||
// Theme-aware class names - memoize to prevent unnecessary re-renders
|
||||
// Using greyish colors for both dark and light modes
|
||||
@@ -1298,6 +1301,7 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
|
||||
dispatch={dispatch}
|
||||
onManageStatus={() => setShowManageStatusModal(true)}
|
||||
onManagePhase={() => setShowManagePhaseModal(true)}
|
||||
projectPhaseLabel={projectPhaseLabel}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -312,7 +312,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(
|
||||
{groupTasks.length === 0 ? (
|
||||
<div className="task-group-empty">
|
||||
<div className="task-table-fixed-columns">
|
||||
<div style={{ width: '380px', padding: '20px 12px' }}>
|
||||
<div style={{ width: '380px', padding: '32px 12px' }}>
|
||||
<div className="text-center text-gray-500">
|
||||
<Text type="secondary">No tasks in this group</Text>
|
||||
<br />
|
||||
@@ -487,7 +487,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(
|
||||
|
||||
.task-group-empty {
|
||||
display: flex;
|
||||
height: 80px;
|
||||
height: 120px;
|
||||
align-items: center;
|
||||
background: var(--task-bg-primary, white);
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
@@ -35,8 +35,6 @@ const TaskPhaseDropdown: React.FC<TaskPhaseDropdownProps> = ({
|
||||
(phaseId: string, phaseName: string) => {
|
||||
if (!task.id || !phaseId || !connected) return;
|
||||
|
||||
console.log('🎯 Phase change initiated:', { taskId: task.id, phaseId, phaseName });
|
||||
|
||||
socket?.emit(SocketEvents.TASK_PHASE_CHANGE.toString(), {
|
||||
task_id: task.id,
|
||||
phase_id: phaseId,
|
||||
@@ -51,8 +49,6 @@ const TaskPhaseDropdown: React.FC<TaskPhaseDropdownProps> = ({
|
||||
const handlePhaseClear = useCallback(() => {
|
||||
if (!task.id || !connected) return;
|
||||
|
||||
console.log('🎯 Phase clear initiated:', { taskId: task.id });
|
||||
|
||||
socket?.emit(SocketEvents.TASK_PHASE_CHANGE.toString(), {
|
||||
task_id: task.id,
|
||||
phase_id: null,
|
||||
|
||||
@@ -33,7 +33,7 @@ export const GROUP_BY_OPTIONS: IGroupByOption[] = [
|
||||
{ label: 'Phase', value: IGroupBy.PHASE },
|
||||
];
|
||||
|
||||
const LOCALSTORAGE_GROUP_KEY = 'worklenz.enhanced-kanban.group_by';
|
||||
const LOCALSTORAGE_GROUP_KEY = 'worklenz.kanban.group_by';
|
||||
|
||||
export const getCurrentGroup = (): IGroupBy => {
|
||||
const key = localStorage.getItem(LOCALSTORAGE_GROUP_KEY);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { useSelectedProject } from '../../../../hooks/useSelectedProject';
|
||||
import { useAppSelector } from '../../../../hooks/useAppSelector';
|
||||
import { Flex } from 'antd';
|
||||
import ConfigPhaseButton from './ConfigPhaseButton';
|
||||
@@ -10,19 +9,13 @@ const PhaseHeader = () => {
|
||||
// localization
|
||||
const { t } = useTranslation('task-list-filters');
|
||||
|
||||
// get selected project for useSelectedProject hook
|
||||
const selectedProject = useSelectedProject();
|
||||
|
||||
// get phase data from redux
|
||||
const phaseList = useAppSelector(state => state.phaseReducer.phaseList);
|
||||
|
||||
//get phases details from phases slice
|
||||
const phase = phaseList.find(el => el.projectId === selectedProject?.projectId);
|
||||
// get project data from redux
|
||||
const { project } = useAppSelector(state => state.projectReducer);
|
||||
|
||||
return (
|
||||
<Flex align="center" justify="space-between">
|
||||
{phase?.phase || t('phasesText')}
|
||||
<ConfigPhaseButton color={colors.darkGray} />
|
||||
{project?.phase_label || t('phasesText')}
|
||||
<ConfigPhaseButton />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,9 +16,9 @@ const initialState: PhaseState = {
|
||||
|
||||
export const addPhaseOption = createAsyncThunk(
|
||||
'phase/addPhaseOption',
|
||||
async ({ projectId }: { projectId: string }, { rejectWithValue }) => {
|
||||
async ({ projectId, name }: { projectId: string; name?: string }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await phasesApiService.addPhaseOption(projectId);
|
||||
const response = await phasesApiService.addPhaseOption(projectId, name);
|
||||
return response;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error);
|
||||
|
||||
@@ -17,8 +17,36 @@ interface LocalGroupingState {
|
||||
collapsedGroups: string[];
|
||||
}
|
||||
|
||||
// Local storage constants
|
||||
const LOCALSTORAGE_GROUP_KEY = 'worklenz.tasklist.group_by';
|
||||
|
||||
// Utility functions for local storage
|
||||
const loadGroupingFromLocalStorage = (): GroupingType | null => {
|
||||
try {
|
||||
const stored = localStorage.getItem(LOCALSTORAGE_GROUP_KEY);
|
||||
if (stored && ['status', 'priority', 'phase'].includes(stored)) {
|
||||
return stored as GroupingType;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load grouping from localStorage:', error);
|
||||
}
|
||||
return 'status'; // Default to 'status' instead of null
|
||||
};
|
||||
|
||||
const saveGroupingToLocalStorage = (grouping: GroupingType | null): void => {
|
||||
try {
|
||||
if (grouping) {
|
||||
localStorage.setItem(LOCALSTORAGE_GROUP_KEY, grouping);
|
||||
} else {
|
||||
localStorage.removeItem(LOCALSTORAGE_GROUP_KEY);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to save grouping to localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const initialState: LocalGroupingState = {
|
||||
currentGrouping: null,
|
||||
currentGrouping: loadGroupingFromLocalStorage(),
|
||||
customPhases: ['Planning', 'Development', 'Testing', 'Deployment'],
|
||||
groupOrder: {
|
||||
status: ['todo', 'doing', 'done'],
|
||||
@@ -35,6 +63,7 @@ const groupingSlice = createSlice({
|
||||
reducers: {
|
||||
setCurrentGrouping: (state, action: PayloadAction<GroupingType | null>) => {
|
||||
state.currentGrouping = action.payload;
|
||||
saveGroupingToLocalStorage(action.payload);
|
||||
},
|
||||
|
||||
addCustomPhase: (state, action: PayloadAction<string>) => {
|
||||
|
||||
@@ -123,11 +123,11 @@ const TasksList: React.FC = React.memo(() => {
|
||||
<span>{t('tasks.name')}</span>
|
||||
</Flex>
|
||||
),
|
||||
width: '150px',
|
||||
width: '40%',
|
||||
render: (_, record) => (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Tooltip title={record.name}>
|
||||
<Typography.Text ellipsis={{ tooltip: true }} style={{ maxWidth: 150 }}>
|
||||
<Typography.Text style={{ flex: 1, marginRight: 8 }}>
|
||||
{record.name}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
@@ -155,15 +155,14 @@ const TasksList: React.FC = React.memo(() => {
|
||||
{
|
||||
key: 'project',
|
||||
title: t('tasks.project'),
|
||||
width: '120px',
|
||||
width: '25%',
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Tooltip title={record.project_name}>
|
||||
<Typography.Paragraph
|
||||
style={{ margin: 0, paddingInlineEnd: 6, maxWidth: 120 }}
|
||||
ellipsis={{ tooltip: true }}
|
||||
style={{ margin: 0, paddingInlineEnd: 6 }}
|
||||
>
|
||||
<Badge color={record.phase_color || 'blue'} style={{ marginInlineEnd: 4 }} />
|
||||
<Badge color={record.project_color || 'blue'} style={{ marginInlineEnd: 4 }} />
|
||||
{record.project_name}
|
||||
</Typography.Paragraph>
|
||||
</Tooltip>
|
||||
@@ -173,7 +172,7 @@ const TasksList: React.FC = React.memo(() => {
|
||||
{
|
||||
key: 'status',
|
||||
title: t('tasks.status'),
|
||||
width: '180px',
|
||||
width: '20%',
|
||||
render: (_, record) => (
|
||||
<HomeTasksStatusDropdown task={record} teamId={record.team_id || ''} />
|
||||
),
|
||||
@@ -181,7 +180,7 @@ const TasksList: React.FC = React.memo(() => {
|
||||
{
|
||||
key: 'dueDate',
|
||||
title: t('tasks.dueDate'),
|
||||
width: '180px',
|
||||
width: '15%',
|
||||
dataIndex: 'end_date',
|
||||
render: (_, record) => <HomeTasksDatePicker record={record} />,
|
||||
},
|
||||
|
||||
@@ -106,13 +106,8 @@ const BoardCreateSectionCard = () => {
|
||||
}
|
||||
|
||||
if (groupBy === IGroupBy.PHASE && projectId) {
|
||||
const body = {
|
||||
name: sectionName,
|
||||
project_id: projectId,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await phasesApiService.addPhaseOption(projectId);
|
||||
const response = await phasesApiService.addPhaseOption(projectId, sectionName);
|
||||
if (response.done && response.body) {
|
||||
dispatch(fetchBoardTaskGroups(projectId));
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ const ProjectViewHeader = memo(() => {
|
||||
setCreatingTask(true);
|
||||
|
||||
const body: Partial<ITaskCreateRequest> = {
|
||||
name: DEFAULT_TASK_NAME,
|
||||
name: t('defaultTaskName'),
|
||||
project_id: selectedProject.id,
|
||||
reporter_id: currentSession.id,
|
||||
team_id: currentSession.team_id,
|
||||
@@ -242,7 +242,7 @@ const ProjectViewHeader = memo(() => {
|
||||
logger.error('Error creating task', error);
|
||||
setCreatingTask(false);
|
||||
}
|
||||
}, [selectedProject?.id, currentSession, socket, dispatch, groupBy, tab]);
|
||||
}, [selectedProject?.id, currentSession, socket, dispatch, groupBy, tab, t]);
|
||||
|
||||
// Memoized import task template handler
|
||||
const handleImportTaskTemplate = useCallback(() => {
|
||||
|
||||
@@ -524,6 +524,69 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
||||
});
|
||||
}
|
||||
|
||||
// NEW SIMPLIFIED APPROACH: Calculate all affected task updates and send them
|
||||
const taskUpdates: Array<{
|
||||
task_id: string;
|
||||
sort_order: number;
|
||||
status_id?: string;
|
||||
priority_id?: string;
|
||||
phase_id?: string;
|
||||
}> = [];
|
||||
|
||||
// Add updates for all tasks in affected groups
|
||||
if (activeGroupId === overGroupId) {
|
||||
// Same group - just reorder
|
||||
const updatedTasks = [...sourceGroup.tasks];
|
||||
updatedTasks.splice(fromIndex, 1);
|
||||
updatedTasks.splice(toIndex, 0, task);
|
||||
|
||||
updatedTasks.forEach((task, index) => {
|
||||
taskUpdates.push({
|
||||
task_id: task.id,
|
||||
sort_order: index + 1, // 1-based indexing
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Different groups - update both source and target
|
||||
const updatedSourceTasks = sourceGroup.tasks.filter((_, i) => i !== fromIndex);
|
||||
const updatedTargetTasks = [...targetGroup.tasks];
|
||||
|
||||
if (isTargetGroupEmpty) {
|
||||
updatedTargetTasks.push(task);
|
||||
} else if (toIndex >= 0 && toIndex <= updatedTargetTasks.length) {
|
||||
updatedTargetTasks.splice(toIndex, 0, task);
|
||||
} else {
|
||||
updatedTargetTasks.push(task);
|
||||
}
|
||||
|
||||
// Add updates for source group
|
||||
updatedSourceTasks.forEach((task, index) => {
|
||||
taskUpdates.push({
|
||||
task_id: task.id,
|
||||
sort_order: index + 1,
|
||||
});
|
||||
});
|
||||
|
||||
// Add updates for target group (including the moved task)
|
||||
updatedTargetTasks.forEach((task, index) => {
|
||||
const update: any = {
|
||||
task_id: task.id,
|
||||
sort_order: index + 1,
|
||||
};
|
||||
|
||||
// Add group-specific updates
|
||||
if (groupBy === IGroupBy.STATUS) {
|
||||
update.status_id = targetGroup.id;
|
||||
} else if (groupBy === IGroupBy.PRIORITY) {
|
||||
update.priority_id = targetGroup.id;
|
||||
} else if (groupBy === IGroupBy.PHASE) {
|
||||
update.phase_id = targetGroup.id;
|
||||
}
|
||||
|
||||
taskUpdates.push(update);
|
||||
});
|
||||
}
|
||||
|
||||
socket?.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
||||
project_id: projectId,
|
||||
from_index: sourceGroup.tasks[fromIndex].sort_order,
|
||||
@@ -534,6 +597,7 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
||||
group_by: groupBy,
|
||||
task: sourceGroup.tasks[fromIndex],
|
||||
team_id: currentSession?.team_id,
|
||||
task_updates: taskUpdates, // NEW: Send calculated updates
|
||||
});
|
||||
|
||||
setTimeout(resetTaskRowStyles, 0);
|
||||
|
||||
@@ -208,6 +208,18 @@ const TaskListTableWrapper = ({
|
||||
>
|
||||
<Flex vertical>
|
||||
<Flex style={{ transform: 'translateY(6px)' }}>
|
||||
{groupBy !== IGroupBy.PRIORITY &&
|
||||
!showRenameInput &&
|
||||
isEditable &&
|
||||
name !== 'Unmapped' && (
|
||||
<Dropdown menu={{ items }}>
|
||||
<Button
|
||||
icon={<EllipsisOutlined />}
|
||||
className="borderless-icon-btn"
|
||||
title={isEditable ? undefined : t('noPermission')}
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
<Button
|
||||
className="custom-collapse-button"
|
||||
style={{
|
||||
@@ -243,18 +255,6 @@ const TaskListTableWrapper = ({
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Button>
|
||||
{groupBy !== IGroupBy.PRIORITY &&
|
||||
!showRenameInput &&
|
||||
isEditable &&
|
||||
name !== 'Unmapped' && (
|
||||
<Dropdown menu={{ items }}>
|
||||
<Button
|
||||
icon={<EllipsisOutlined />}
|
||||
className="borderless-icon-btn"
|
||||
title={isEditable ? undefined : t('noPermission')}
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
</Flex>
|
||||
<Collapsible
|
||||
isOpen={isExpanded}
|
||||
|
||||
@@ -944,18 +944,7 @@ const SelectionFieldCell: React.FC<{
|
||||
columnKey: string;
|
||||
updateValue: (taskId: string, columnKey: string, value: string) => void;
|
||||
}> = ({ selectionsList, value, task, columnKey, updateValue }) => {
|
||||
// Debug the selectionsList data
|
||||
const [loggedInfo, setLoggedInfo] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loggedInfo) {
|
||||
console.log('Selection column data:', {
|
||||
columnKey,
|
||||
selectionsList,
|
||||
});
|
||||
setLoggedInfo(true);
|
||||
}
|
||||
}, [columnKey, selectionsList, loggedInfo]);
|
||||
|
||||
return (
|
||||
<CustomColumnSelectionCell
|
||||
@@ -1256,19 +1245,6 @@ const renderCustomColumnContent = (
|
||||
);
|
||||
},
|
||||
selection: () => {
|
||||
// Debug the selectionsList data
|
||||
const [loggedInfo, setLoggedInfo] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loggedInfo) {
|
||||
console.log('Selection column data:', {
|
||||
columnKey,
|
||||
selectionsList: columnObj?.selectionsList,
|
||||
});
|
||||
setLoggedInfo(true);
|
||||
}
|
||||
}, [columnKey, loggedInfo]);
|
||||
|
||||
return (
|
||||
<SelectionFieldCell
|
||||
selectionsList={columnObj?.selectionsList || []}
|
||||
@@ -1650,35 +1626,12 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
|
||||
|
||||
const activeTask = displayTasks.find(task => task.id === active.id);
|
||||
if (!activeTask) {
|
||||
console.error('Active task not found:', {
|
||||
activeId: active.id,
|
||||
displayTasks: displayTasks.map(t => ({ id: t.id, name: t.name })),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Found activeTask:', {
|
||||
id: activeTask.id,
|
||||
name: activeTask.name,
|
||||
status_id: activeTask.status_id,
|
||||
status: activeTask.status,
|
||||
priority: activeTask.priority,
|
||||
project_id: project?.id,
|
||||
team_id: project?.team_id,
|
||||
fullProject: project,
|
||||
});
|
||||
|
||||
// Use the tableId directly as the group ID (it should be the group ID)
|
||||
const currentGroupId = tableId;
|
||||
|
||||
console.log('Drag operation:', {
|
||||
activeId: active.id,
|
||||
overId: over.id,
|
||||
tableId,
|
||||
currentGroupId,
|
||||
displayTasksLength: displayTasks.length,
|
||||
});
|
||||
|
||||
// Check if this is a reorder within the same group
|
||||
const overTask = displayTasks.find(task => task.id === over.id);
|
||||
if (overTask) {
|
||||
@@ -1686,36 +1639,17 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
|
||||
const oldIndex = displayTasks.findIndex(task => task.id === active.id);
|
||||
const newIndex = displayTasks.findIndex(task => task.id === over.id);
|
||||
|
||||
console.log('Reorder details:', { oldIndex, newIndex, activeTask: activeTask.name });
|
||||
|
||||
if (oldIndex !== newIndex && oldIndex !== -1 && newIndex !== -1) {
|
||||
// Get the actual sort_order values from the tasks
|
||||
const fromSortOrder = activeTask.sort_order || oldIndex;
|
||||
const overTaskAtNewIndex = displayTasks[newIndex];
|
||||
const toSortOrder = overTaskAtNewIndex?.sort_order || newIndex;
|
||||
|
||||
console.log('Sort order details:', {
|
||||
oldIndex,
|
||||
newIndex,
|
||||
fromSortOrder,
|
||||
toSortOrder,
|
||||
activeTaskSortOrder: activeTask.sort_order,
|
||||
overTaskSortOrder: overTaskAtNewIndex?.sort_order,
|
||||
});
|
||||
|
||||
// Create updated task list with reordered tasks
|
||||
const updatedTasks = [...displayTasks];
|
||||
const [movedTask] = updatedTasks.splice(oldIndex, 1);
|
||||
updatedTasks.splice(newIndex, 0, movedTask);
|
||||
|
||||
console.log('Dispatching reorderTasks with:', {
|
||||
activeGroupId: currentGroupId,
|
||||
overGroupId: currentGroupId,
|
||||
fromIndex: oldIndex,
|
||||
toIndex: newIndex,
|
||||
taskName: activeTask.name,
|
||||
});
|
||||
|
||||
// Update local state immediately for better UX
|
||||
dispatch(
|
||||
reorderTasks({
|
||||
@@ -1758,34 +1692,10 @@ const TaskListTable: React.FC<TaskListTableProps> = ({ taskList, tableId, active
|
||||
|
||||
// Validate required fields before sending
|
||||
if (!body.task.id) {
|
||||
console.error('Cannot send socket event: task.id is missing', { activeTask, active });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Validated values:', {
|
||||
from_index: body.from_index,
|
||||
to_index: body.to_index,
|
||||
status: body.task.status,
|
||||
priority: body.task.priority,
|
||||
team_id: body.team_id,
|
||||
originalStatus: activeTask.status_id || activeTask.status,
|
||||
originalPriority: activeTask.priority,
|
||||
originalTeamId: project.team_id,
|
||||
sessionTeamId: currentSession?.team_id,
|
||||
finalTeamId: body.team_id,
|
||||
});
|
||||
|
||||
console.log('Sending socket event:', body);
|
||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), body);
|
||||
} else {
|
||||
console.error('Cannot send socket event: missing required data', {
|
||||
hasSocket: !!socket,
|
||||
hasProjectId: !!project?.id,
|
||||
hasActiveId: !!active.id,
|
||||
hasActiveTaskId: !!activeTask.id,
|
||||
activeTask,
|
||||
active,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,152 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Global Confirmation Modal Styles */
|
||||
/* Light mode confirmation modal styling (default) */
|
||||
.ant-modal-confirm .ant-modal-content {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-header {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-body {
|
||||
background-color: #ffffff;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-footer {
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-confirm-title {
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-confirm-content {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
/* Dark mode confirmation modal styling */
|
||||
.dark .ant-modal-confirm .ant-modal-content,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-content,
|
||||
html.dark .ant-modal-confirm .ant-modal-content {
|
||||
background-color: #1f1f1f !important;
|
||||
border: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-header,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-header,
|
||||
html.dark .ant-modal-confirm .ant-modal-header {
|
||||
background-color: #1f1f1f !important;
|
||||
border-bottom: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-body,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-body,
|
||||
html.dark .ant-modal-confirm .ant-modal-body {
|
||||
background-color: #1f1f1f !important;
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-footer,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-footer,
|
||||
html.dark .ant-modal-confirm .ant-modal-footer {
|
||||
background-color: #1f1f1f !important;
|
||||
border-top: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-confirm-title,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-confirm-title,
|
||||
html.dark .ant-modal-confirm .ant-modal-confirm-title {
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-modal-confirm-content,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-modal-confirm-content,
|
||||
html.dark .ant-modal-confirm .ant-modal-confirm-content {
|
||||
color: #8c8c8c !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-default,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-default,
|
||||
html.dark .ant-modal-confirm .ant-btn-default {
|
||||
background-color: #141414 !important;
|
||||
border-color: #303030 !important;
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-default:hover,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-default:hover,
|
||||
html.dark .ant-modal-confirm .ant-btn-default:hover {
|
||||
background-color: #262626 !important;
|
||||
border-color: #40a9ff !important;
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-primary,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-primary,
|
||||
html.dark .ant-modal-confirm .ant-btn-primary {
|
||||
background-color: #1890ff !important;
|
||||
border-color: #1890ff !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-primary:hover,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-primary:hover,
|
||||
html.dark .ant-modal-confirm .ant-btn-primary:hover {
|
||||
background-color: #40a9ff !important;
|
||||
border-color: #40a9ff !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-dangerous,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-dangerous,
|
||||
html.dark .ant-modal-confirm .ant-btn-dangerous {
|
||||
background-color: #ff4d4f !important;
|
||||
border-color: #ff4d4f !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-confirm .ant-btn-dangerous:hover,
|
||||
[data-theme="dark"] .ant-modal-confirm .ant-btn-dangerous:hover,
|
||||
html.dark .ant-modal-confirm .ant-btn-dangerous:hover {
|
||||
background-color: #ff7875 !important;
|
||||
border-color: #ff7875 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Error modal specific styling */
|
||||
.dark .ant-modal-error .ant-modal-content,
|
||||
[data-theme="dark"] .ant-modal-error .ant-modal-content,
|
||||
html.dark .ant-modal-error .ant-modal-content {
|
||||
background-color: #1f1f1f !important;
|
||||
border: 1px solid #303030 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-error .ant-modal-body,
|
||||
[data-theme="dark"] .ant-modal-error .ant-modal-body,
|
||||
html.dark .ant-modal-error .ant-modal-body {
|
||||
background-color: #1f1f1f !important;
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-error .ant-modal-confirm-title,
|
||||
[data-theme="dark"] .ant-modal-error .ant-modal-confirm-title,
|
||||
html.dark .ant-modal-error .ant-modal-confirm-title {
|
||||
color: #d9d9d9 !important;
|
||||
}
|
||||
|
||||
.dark .ant-modal-error .ant-modal-confirm-content,
|
||||
[data-theme="dark"] .ant-modal-error .ant-modal-content,
|
||||
html.dark .ant-modal-error .ant-modal-confirm-content {
|
||||
color: #8c8c8c !important;
|
||||
}
|
||||
|
||||
.task-group {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user