From 1c981312d45e42050231d1e92baff99383612106 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 14 Jul 2025 12:28:04 +0530 Subject: [PATCH] feat(task-drawer): enhance task navigation and hierarchy display - Added functionality to navigate back to parent tasks for sub-tasks in the task drawer. - Introduced a breadcrumb component to visually represent the hierarchy of tasks, improving user navigation. - Updated task drawer header to display the current task name with truncation and tooltip for better readability. - Enhanced styling for task name display and breadcrumb for improved user experience. --- .../locales/alb/task-drawer/task-drawer.json | 96 ++++----- .../locales/de/task-drawer/task-drawer.json | 52 ++--- .../locales/en/task-drawer/task-drawer.json | 8 +- .../locales/es/task-drawer/task-drawer.json | 102 +++++----- .../locales/pt/task-drawer/task-drawer.json | 82 ++++---- .../locales/zh/task-drawer/task-drawer.json | 54 ++--- .../task-drawer-header/task-drawer-header.css | 25 +++ .../task-drawer-header/task-drawer-header.tsx | 107 +++++----- .../components/task-drawer/task-drawer.tsx | 33 ++- .../task-hierarchy-breadcrumb.css | 88 ++++++++ .../task-hierarchy-breadcrumb.tsx | 189 ++++++++++++++++++ 11 files changed, 605 insertions(+), 231 deletions(-) create mode 100644 worklenz-frontend/src/components/task-drawer/task-hierarchy-breadcrumb/task-hierarchy-breadcrumb.css create mode 100644 worklenz-frontend/src/components/task-drawer/task-hierarchy-breadcrumb/task-hierarchy-breadcrumb.tsx diff --git a/worklenz-frontend/public/locales/alb/task-drawer/task-drawer.json b/worklenz-frontend/public/locales/alb/task-drawer/task-drawer.json index 9d6c022f..e8944656 100644 --- a/worklenz-frontend/public/locales/alb/task-drawer/task-drawer.json +++ b/worklenz-frontend/public/locales/alb/task-drawer/task-drawer.json @@ -1,37 +1,43 @@ { "taskHeader": { - "taskNamePlaceholder": "Shkruani Detyrën tuaj", - "deleteTask": "Fshi Detyrën" + "taskNamePlaceholder": "Shkruani detyrën tuaj", + "deleteTask": "Fshi detyrën", + "parentTask": "Detyra kryesore", + "currentTask": "Detyra aktuale", + "back": "Kthehu", + "backToParent": "Kthehu te detyra kryesore", + "toParentTask": "te detyra kryesore", + "loadingHierarchy": "Duke ngarkuar hierarkinë..." }, "taskInfoTab": { "title": "Informacioni", "details": { "title": "Detajet", - "task-key": "Çelësi i Detyrës", + "task-key": "Çelësi i detyrës", "phase": "Faza", - "assignees": "Të Caktuar", - "due-date": "Data e Përfundimit", - "time-estimation": "Vlerësimi i Kohës", + "assignees": "Të caktuarit", + "due-date": "Data e përfundimit", + "time-estimation": "Vlerësimi i kohës", "priority": "Prioriteti", "labels": "Etiketat", - "billable": "E Faturueshme", + "billable": "I faturueshëm", "notify": "Njofto", "when-done-notify": "Kur përfundon, njofto", - "start-date": "Data e Fillimit", - "end-date": "Data e Përfundimit", - "hide-start-date": "Fshih Datën e Fillimit", - "show-start-date": "Shfaq Datën e Fillimit", + "start-date": "Data e fillimit", + "end-date": "Data e përfundimit", + "hide-start-date": "Fshih datën e fillimit", + "show-start-date": "Shfaq datën e fillimit", "hours": "Orë", "minutes": "Minuta", - "progressValue": "Vlera e Progresit", - "progressValueTooltip": "Vendosni përqindjen e progresit (0-100%)", + "progressValue": "Vlera e progresit", + "progressValueTooltip": "Vendos përqindjen e progresit (0-100%)", "progressValueRequired": "Ju lutemi vendosni një vlerë progresi", "progressValueRange": "Progresi duhet të jetë midis 0 dhe 100", - "taskWeight": "Pesha e Detyrës", - "taskWeightTooltip": "Vendosni peshën e kësaj nëndetyre (përqindje)", + "taskWeight": "Pesha e detyrës", + "taskWeightTooltip": "Vendos peshën e kësaj nëndetyre (përqindje)", "taskWeightRequired": "Ju lutemi vendosni një peshë detyre", "taskWeightRange": "Pesha duhet të jetë midis 0 dhe 100", - "recurring": "E Përsëritur" + "recurring": "Përsëritëse" }, "labels": { "labelInputPlaceholder": "Kërko ose krijo", @@ -43,71 +49,71 @@ }, "subTasks": { "title": "Nëndetyrat", - "addSubTask": "Shto Nëndetyrë", + "addSubTask": "Shto nëndetyrë", "addSubTaskInputPlaceholder": "Shkruani detyrën tuaj dhe shtypni enter", - "refreshSubTasks": "Rifresko Nëndetyrat", - "edit": "Modifiko", + "refreshSubTasks": "Rifresko nëndetyrat", + "edit": "Redakto", "delete": "Fshi", - "confirmDeleteSubTask": "Jeni i sigurt që doni të fshini këtë nëndetyrë?", - "deleteSubTask": "Fshi Nëndetyrën" + "confirmDeleteSubTask": "Jeni i sigurt që dëshironi ta fshini këtë nëndetyrë?", + "deleteSubTask": "Fshi nëndetyrën" }, "dependencies": { "title": "Varësitë", "addDependency": "+ Shto varësi të re", "blockedBy": "Bllokuar nga", - "searchTask": "Shkruani për të kërkuar detyrë", + "searchTask": "Shkruaj për të kërkuar detyrën", "noTasksFound": "Nuk u gjetën detyra", - "confirmDeleteDependency": "Jeni i sigurt që doni të fshini?" + "confirmDeleteDependency": "Jeni i sigurt që dëshironi ta fshini?" }, "attachments": { "title": "Bashkëngjitjet", - "chooseOrDropFileToUpload": "Zgjidhni ose hidhni skedar për të ngarkuar", + "chooseOrDropFileToUpload": "Zgjidh ose lësho skedarin për ta ngarkuar", "uploading": "Duke ngarkuar..." }, "comments": { "title": "Komentet", "addComment": "+ Shto koment të ri", - "noComments": "Ende pa komente. Bëhu i pari që komenton!", + "noComments": "Ende pa komente. Bëhu i pari që komentoni!", "delete": "Fshi", - "confirmDeleteComment": "Jeni i sigurt që doni të fshini këtë koment?", + "confirmDeleteComment": "Jeni i sigurt që dëshironi ta fshini këtë koment?", "addCommentPlaceholder": "Shto një koment...", "cancel": "Anulo", "commentButton": "Komento", "attachFiles": "Bashkëngjit skedarë", "addMoreFiles": "Shto më shumë skedarë", - "selectedFiles": "Skedarët e Zgjedhur (Deri në 25MB, Maksimumi {count})", - "maxFilesError": "Mund të ngarkoni maksimum {count} skedarë", - "processFilesError": "Dështoi përpunimi i skedarëve", + "selectedFiles": "Skedarët e zgjedhur (Deri në 25MB, Maksimumi {count})", + "maxFilesError": "Mund të ngarkoni maksimumi {count} skedarë", + "processFilesError": "Dështoi në përpunimin e skedarëve", "addCommentError": "Ju lutemi shtoni një koment ose bashkëngjitni skedarë", "createdBy": "Krijuar {{time}} nga {{user}}", "updatedTime": "Përditësuar {{time}}" }, "searchInputPlaceholder": "Kërko sipas emrit", - "pendingInvitation": "Ftesë në Pritje" + "pendingInvitation": "Ftesë në pritje" }, "taskTimeLogTab": { - "title": "Regjistri i Kohës", - "addTimeLog": "Shto regjistrim të ri kohe", - "totalLogged": "Totali i Regjistruar", + "title": "Regjistri i kohës", + "addTimeLog": "Shto regjistër të ri kohe", + "totalLogged": "Totali i regjistruar", "exportToExcel": "Eksporto në Excel", - "noTimeLogsFound": "Nuk u gjetën regjistra kohe", + "noTimeLogsFound": "Nuk u gjetën regjistrime kohe", "timeLogForm": { "date": "Data", - "startTime": "Koha e Fillimit", - "endTime": "Koha e Përfundimit", - "workDescription": "Përshkrimi i Punës", + "startTime": "Ora e fillimit", + "endTime": "Ora e përfundimit", + "workDescription": "Përshkrimi i punës", "descriptionPlaceholder": "Shto një përshkrim", "logTime": "Regjistro kohën", "updateTime": "Përditëso kohën", "cancel": "Anulo", "selectDateError": "Ju lutemi zgjidhni një datë", - "selectStartTimeError": "Ju lutemi zgjidhni kohën e fillimit", - "selectEndTimeError": "Ju lutemi zgjidhni kohën e përfundimit", - "endTimeAfterStartError": "Koha e përfundimit duhet të jetë pas kohës së fillimit" + "selectStartTimeError": "Ju lutemi zgjidhni orën e fillimit", + "selectEndTimeError": "Ju lutemi zgjidhni orën e përfundimit", + "endTimeAfterStartError": "Ora e përfundimit duhet të jetë pas orës së fillimit" } }, "taskActivityLogTab": { - "title": "Regjistri i Aktivitetit", + "title": "Regjistri i aktivitetit", "add": "SHTO", "remove": "HIQE", "none": "Asnjë", @@ -115,9 +121,9 @@ "createdTask": "krijoi detyrën." }, "taskProgress": { - "markAsDoneTitle": "Shëno Detyrën si të Kryer?", - "confirmMarkAsDone": "Po, shëno si të kryer", - "cancelMarkAsDone": "Jo, mbaj statusin aktual", - "markAsDoneDescription": "Keni vendosur progresin në 100%. Doni të përditësoni statusin e detyrës në \"Kryer\"?" + "markAsDoneTitle": "Shëno detyrën si të përfunduar?", + "confirmMarkAsDone": "Po, shënoje si të përfunduar", + "cancelMarkAsDone": "Jo, mbaj gjendjen aktuale", + "markAsDoneDescription": "Keni vendosur progresin në 100%. Dëshironi ta përditësoni gjendjen e detyrës në \"Përfunduar\"?" } } diff --git a/worklenz-frontend/public/locales/de/task-drawer/task-drawer.json b/worklenz-frontend/public/locales/de/task-drawer/task-drawer.json index 62e3f881..4bbc2559 100644 --- a/worklenz-frontend/public/locales/de/task-drawer/task-drawer.json +++ b/worklenz-frontend/public/locales/de/task-drawer/task-drawer.json @@ -1,22 +1,28 @@ { "taskHeader": { "taskNamePlaceholder": "Geben Sie Ihre Aufgabe ein", - "deleteTask": "Aufgabe löschen" + "deleteTask": "Aufgabe löschen", + "parentTask": "Übergeordnete Aufgabe", + "currentTask": "Aktuelle Aufgabe", + "back": "Zurück", + "backToParent": "Zurück zur übergeordneten Aufgabe", + "toParentTask": "zur übergeordneten Aufgabe", + "loadingHierarchy": "Hierarchie wird geladen..." }, "taskInfoTab": { "title": "Info", "details": { "title": "Details", - "task-key": "Aufgaben-Schlüssel", + "task-key": "Aufgabenschlüssel", "phase": "Phase", - "assignees": "Beauftragte", + "assignees": "Zugewiesene", "due-date": "Fälligkeitsdatum", "time-estimation": "Zeitschätzung", "priority": "Priorität", "labels": "Labels", "billable": "Abrechenbar", "notify": "Benachrichtigen", - "when-done-notify": "Bei Abschluss benachrichtigen", + "when-done-notify": "Bei Fertigstellung benachrichtigen", "start-date": "Startdatum", "end-date": "Enddatum", "hide-start-date": "Startdatum ausblenden", @@ -24,50 +30,50 @@ "hours": "Stunden", "minutes": "Minuten", "progressValue": "Fortschrittswert", - "progressValueTooltip": "Fortschritt in Prozent einstellen (0-100%)", + "progressValueTooltip": "Setzen Sie den Fortschrittsprozentsatz (0-100%)", "progressValueRequired": "Bitte geben Sie einen Fortschrittswert ein", "progressValueRange": "Fortschritt muss zwischen 0 und 100 liegen", "taskWeight": "Aufgabengewicht", - "taskWeightTooltip": "Gewicht dieser Teilaufgabe festlegen (Prozent)", + "taskWeightTooltip": "Setzen Sie das Gewicht dieser Unteraufgabe (Prozentsatz)", "taskWeightRequired": "Bitte geben Sie ein Aufgabengewicht ein", "taskWeightRange": "Gewicht muss zwischen 0 und 100 liegen", "recurring": "Wiederkehrend" }, "labels": { "labelInputPlaceholder": "Suchen oder erstellen", - "labelsSelectorInputTip": "Enter drücken zum Erstellen" + "labelsSelectorInputTip": "Drücken Sie Enter zum Erstellen" }, "description": { "title": "Beschreibung", - "placeholder": "Detailliertere Beschreibung hinzufügen..." + "placeholder": "Fügen Sie eine detailliertere Beschreibung hinzu..." }, "subTasks": { - "title": "Teilaufgaben", - "addSubTask": "Teilaufgabe hinzufügen", + "title": "Unteraufgaben", + "addSubTask": "Unteraufgabe hinzufügen", "addSubTaskInputPlaceholder": "Geben Sie Ihre Aufgabe ein und drücken Sie Enter", - "refreshSubTasks": "Teilaufgaben aktualisieren", + "refreshSubTasks": "Unteraufgaben aktualisieren", "edit": "Bearbeiten", "delete": "Löschen", - "confirmDeleteSubTask": "Sind Sie sicher, dass Sie diese Teilaufgabe löschen möchten?", - "deleteSubTask": "Teilaufgabe löschen" + "confirmDeleteSubTask": "Sind Sie sicher, dass Sie diese Unteraufgabe löschen möchten?", + "deleteSubTask": "Unteraufgabe löschen" }, "dependencies": { "title": "Abhängigkeiten", "addDependency": "+ Neue Abhängigkeit hinzufügen", "blockedBy": "Blockiert von", - "searchTask": "Aufgabe suchen", + "searchTask": "Zum Suchen der Aufgabe eingeben", "noTasksFound": "Keine Aufgaben gefunden", "confirmDeleteDependency": "Sind Sie sicher, dass Sie löschen möchten?" }, "attachments": { "title": "Anhänge", - "chooseOrDropFileToUpload": "Datei zum Hochladen wählen oder ablegen", + "chooseOrDropFileToUpload": "Datei zum Hochladen auswählen oder ablegen", "uploading": "Wird hochgeladen..." }, "comments": { "title": "Kommentare", "addComment": "+ Neuen Kommentar hinzufügen", - "noComments": "Noch keine Kommentare. Seien Sie der Erste!", + "noComments": "Noch keine Kommentare. Seien Sie der Erste, der kommentiert!", "delete": "Löschen", "confirmDeleteComment": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?", "addCommentPlaceholder": "Kommentar hinzufügen...", @@ -75,9 +81,9 @@ "commentButton": "Kommentieren", "attachFiles": "Dateien anhängen", "addMoreFiles": "Weitere Dateien hinzufügen", - "selectedFiles": "Ausgewählte Dateien (Bis zu 25MB, Maximum {count})", + "selectedFiles": "Ausgewählte Dateien (Bis zu 25MB, Maximum von {count})", "maxFilesError": "Sie können maximal {count} Dateien hochladen", - "processFilesError": "Fehler beim Verarbeiten der Dateien", + "processFilesError": "Dateien konnten nicht verarbeitet werden", "addCommentError": "Bitte fügen Sie einen Kommentar hinzu oder hängen Sie Dateien an", "createdBy": "Erstellt {{time}} von {{user}}", "updatedTime": "Aktualisiert {{time}}" @@ -86,18 +92,18 @@ "pendingInvitation": "Ausstehende Einladung" }, "taskTimeLogTab": { - "title": "Zeiterfassung", - "addTimeLog": "Neuen Zeiteintrag hinzufügen", - "totalLogged": "Gesamt erfasst", + "title": "Zeitprotokoll", + "addTimeLog": "Neues Zeitprotokoll hinzufügen", + "totalLogged": "Gesamt protokolliert", "exportToExcel": "Nach Excel exportieren", - "noTimeLogsFound": "Keine Zeiteinträge gefunden", + "noTimeLogsFound": "Keine Zeitprotokolle gefunden", "timeLogForm": { "date": "Datum", "startTime": "Startzeit", "endTime": "Endzeit", "workDescription": "Arbeitsbeschreibung", "descriptionPlaceholder": "Beschreibung hinzufügen", - "logTime": "Zeit erfassen", + "logTime": "Zeit protokollieren", "updateTime": "Zeit aktualisieren", "cancel": "Abbrechen", "selectDateError": "Bitte wählen Sie ein Datum", diff --git a/worklenz-frontend/public/locales/en/task-drawer/task-drawer.json b/worklenz-frontend/public/locales/en/task-drawer/task-drawer.json index b5147324..4aa0cfbb 100644 --- a/worklenz-frontend/public/locales/en/task-drawer/task-drawer.json +++ b/worklenz-frontend/public/locales/en/task-drawer/task-drawer.json @@ -1,7 +1,13 @@ { "taskHeader": { "taskNamePlaceholder": "Type your Task", - "deleteTask": "Delete Task" + "deleteTask": "Delete Task", + "parentTask": "Parent Task", + "currentTask": "Current Task", + "back": "Back", + "backToParent": "Back to Parent Task", + "toParentTask": "to parent task", + "loadingHierarchy": "Loading hierarchy..." }, "taskInfoTab": { "title": "Info", diff --git a/worklenz-frontend/public/locales/es/task-drawer/task-drawer.json b/worklenz-frontend/public/locales/es/task-drawer/task-drawer.json index 8e438716..df20b57b 100644 --- a/worklenz-frontend/public/locales/es/task-drawer/task-drawer.json +++ b/worklenz-frontend/public/locales/es/task-drawer/task-drawer.json @@ -1,35 +1,41 @@ { "taskHeader": { - "taskNamePlaceholder": "Escriba su Tarea", - "deleteTask": "Eliminar Tarea" + "taskNamePlaceholder": "Escribe tu tarea", + "deleteTask": "Eliminar tarea", + "parentTask": "Tarea principal", + "currentTask": "Tarea actual", + "back": "Volver", + "backToParent": "Volver a la tarea principal", + "toParentTask": "a la tarea principal", + "loadingHierarchy": "Cargando jerarquía..." }, "taskInfoTab": { "title": "Información", "details": { "title": "Detalles", - "task-key": "Clave de Tarea", + "task-key": "Clave de tarea", "phase": "Fase", "assignees": "Asignados", - "due-date": "Fecha de Vencimiento", - "time-estimation": "Estimación de Tiempo", + "due-date": "Fecha de vencimiento", + "time-estimation": "Estimación de tiempo", "priority": "Prioridad", "labels": "Etiquetas", "billable": "Facturable", "notify": "Notificar", - "when-done-notify": "Al terminar, notificar", - "start-date": "Fecha de Inicio", - "end-date": "Fecha de Fin", - "hide-start-date": "Ocultar Fecha de Inicio", - "show-start-date": "Mostrar Fecha de Inicio", + "when-done-notify": "Al finalizar, notificar", + "start-date": "Fecha de inicio", + "end-date": "Fecha de finalización", + "hide-start-date": "Ocultar fecha de inicio", + "show-start-date": "Mostrar fecha de inicio", "hours": "Horas", "minutes": "Minutos", - "progressValue": "Valor de Progreso", + "progressValue": "Valor de progreso", "progressValueTooltip": "Establecer el porcentaje de progreso (0-100%)", - "progressValueRequired": "Por favor, introduzca un valor de progreso", + "progressValueRequired": "Por favor ingrese un valor de progreso", "progressValueRange": "El progreso debe estar entre 0 y 100", - "taskWeight": "Peso de la Tarea", + "taskWeight": "Peso de la tarea", "taskWeightTooltip": "Establecer el peso de esta subtarea (porcentaje)", - "taskWeightRequired": "Por favor, introduzca un peso de tarea", + "taskWeightRequired": "Por favor ingrese un peso de tarea", "taskWeightRange": "El peso debe estar entre 0 y 100", "recurring": "Recurrente" }, @@ -39,85 +45,85 @@ }, "description": { "title": "Descripción", - "placeholder": "Añadir una descripción más detallada..." + "placeholder": "Añade una descripción más detallada..." }, "subTasks": { - "title": "Sub Tareas", - "addSubTask": "Agregar Sub Tarea", - "addSubTaskInputPlaceholder": "Escriba su tarea y presione enter", - "refreshSubTasks": "Actualizar Sub Tareas", + "title": "Subtareas", + "addSubTask": "Añadir subtarea", + "addSubTaskInputPlaceholder": "Escribe tu tarea y presiona enter", + "refreshSubTasks": "Actualizar subtareas", "edit": "Editar", "delete": "Eliminar", - "confirmDeleteSubTask": "¿Está seguro de que desea eliminar esta subtarea?", - "deleteSubTask": "Eliminar Sub Tarea" + "confirmDeleteSubTask": "¿Estás seguro de que quieres eliminar esta subtarea?", + "deleteSubTask": "Eliminar subtarea" }, "dependencies": { "title": "Dependencias", - "addDependency": "+ Agregar nueva dependencia", + "addDependency": "+ Añadir nueva dependencia", "blockedBy": "Bloqueado por", - "searchTask": "Escribir para buscar tarea", + "searchTask": "Escribe para buscar tarea", "noTasksFound": "No se encontraron tareas", - "confirmDeleteDependency": "¿Está seguro de que desea eliminar?" + "confirmDeleteDependency": "¿Estás seguro de que quieres eliminar?" }, "attachments": { "title": "Adjuntos", - "chooseOrDropFileToUpload": "Elija o arrastre un archivo para subir", + "chooseOrDropFileToUpload": "Elige o arrastra archivo para subir", "uploading": "Subiendo..." }, "comments": { "title": "Comentarios", - "addComment": "+ Agregar nuevo comentario", + "addComment": "+ Añadir nuevo comentario", "noComments": "Aún no hay comentarios. ¡Sé el primero en comentar!", "delete": "Eliminar", - "confirmDeleteComment": "¿Está seguro de que desea eliminar este comentario?", - "addCommentPlaceholder": "Agregar un comentario...", + "confirmDeleteComment": "¿Estás seguro de que quieres eliminar este comentario?", + "addCommentPlaceholder": "Añadir un comentario...", "cancel": "Cancelar", "commentButton": "Comentar", "attachFiles": "Adjuntar archivos", - "addMoreFiles": "Agregar más archivos", - "selectedFiles": "Archivos Seleccionados (Hasta 25MB, Máximo {count})", - "maxFilesError": "Solo puede subir un máximo de {count} archivos", + "addMoreFiles": "Añadir más archivos", + "selectedFiles": "Archivos seleccionados (Hasta 25MB, Máximo de {count})", + "maxFilesError": "Solo puedes subir un máximo de {count} archivos", "processFilesError": "Error al procesar archivos", - "addCommentError": "Por favor agregue un comentario o adjunte archivos", + "addCommentError": "Por favor añade un comentario o adjunta archivos", "createdBy": "Creado {{time}} por {{user}}", "updatedTime": "Actualizado {{time}}" }, "searchInputPlaceholder": "Buscar por nombre", - "pendingInvitation": "Invitación Pendiente" + "pendingInvitation": "Invitación pendiente" }, "taskTimeLogTab": { - "title": "Registro de Tiempo", + "title": "Registro de tiempo", "addTimeLog": "Añadir nuevo registro de tiempo", - "totalLogged": "Total Registrado", + "totalLogged": "Total registrado", "exportToExcel": "Exportar a Excel", "noTimeLogsFound": "No se encontraron registros de tiempo", "timeLogForm": { "date": "Fecha", - "startTime": "Hora de Inicio", - "endTime": "Hora de Fin", - "workDescription": "Descripción del Trabajo", - "descriptionPlaceholder": "Agregar una descripción", + "startTime": "Hora de inicio", + "endTime": "Hora de finalización", + "workDescription": "Descripción del trabajo", + "descriptionPlaceholder": "Añadir una descripción", "logTime": "Registrar tiempo", "updateTime": "Actualizar tiempo", "cancel": "Cancelar", - "selectDateError": "Por favor seleccione una fecha", - "selectStartTimeError": "Por favor seleccione la hora de inicio", - "selectEndTimeError": "Por favor seleccione la hora de fin", - "endTimeAfterStartError": "La hora de fin debe ser posterior a la hora de inicio" + "selectDateError": "Por favor selecciona una fecha", + "selectStartTimeError": "Por favor selecciona hora de inicio", + "selectEndTimeError": "Por favor selecciona hora de finalización", + "endTimeAfterStartError": "La hora de finalización debe ser posterior a la de inicio" } }, "taskActivityLogTab": { - "title": "Registro de Actividad", - "add": "AGREGAR", - "remove": "QUITAR", + "title": "Registro de actividad", + "add": "AÑADIR", + "remove": "ELIMINAR", "none": "Ninguno", "weight": "Peso", "createdTask": "creó la tarea." }, "taskProgress": { - "markAsDoneTitle": "¿Marcar Tarea como Completada?", + "markAsDoneTitle": "¿Marcar tarea como completada?", "confirmMarkAsDone": "Sí, marcar como completada", "cancelMarkAsDone": "No, mantener estado actual", - "markAsDoneDescription": "Ha establecido el progreso al 100%. ¿Le gustaría actualizar el estado de la tarea a \"Completada\"?" + "markAsDoneDescription": "Has establecido el progreso al 100%. ¿Te gustaría actualizar el estado de la tarea a \"Completada\"?" } } diff --git a/worklenz-frontend/public/locales/pt/task-drawer/task-drawer.json b/worklenz-frontend/public/locales/pt/task-drawer/task-drawer.json index c24e943e..a2fe12c3 100644 --- a/worklenz-frontend/public/locales/pt/task-drawer/task-drawer.json +++ b/worklenz-frontend/public/locales/pt/task-drawer/task-drawer.json @@ -1,33 +1,39 @@ { "taskHeader": { - "taskNamePlaceholder": "Digite sua Tarefa", - "deleteTask": "Deletar Tarefa" + "taskNamePlaceholder": "Digite sua tarefa", + "deleteTask": "Excluir tarefa", + "parentTask": "Tarefa principal", + "currentTask": "Tarefa atual", + "back": "Voltar", + "backToParent": "Voltar à tarefa principal", + "toParentTask": "à tarefa principal", + "loadingHierarchy": "Carregando hierarquia..." }, "taskInfoTab": { "title": "Informações", "details": { "title": "Detalhes", - "task-key": "Chave da Tarefa", + "task-key": "Chave da tarefa", "phase": "Fase", "assignees": "Responsáveis", - "due-date": "Data de Vencimento", - "time-estimation": "Estimativa de Tempo", + "due-date": "Data de vencimento", + "time-estimation": "Estimativa de tempo", "priority": "Prioridade", "labels": "Etiquetas", "billable": "Faturável", "notify": "Notificar", - "when-done-notify": "Quando concluído, notificar", - "start-date": "Data de Início", - "end-date": "Data de Fim", - "hide-start-date": "Ocultar Data de Início", - "show-start-date": "Mostrar Data de Início", + "when-done-notify": "Ao concluir, notificar", + "start-date": "Data de início", + "end-date": "Data de término", + "hide-start-date": "Ocultar data de início", + "show-start-date": "Mostrar data de início", "hours": "Horas", "minutes": "Minutos", - "progressValue": "Valor do Progresso", + "progressValue": "Valor do progresso", "progressValueTooltip": "Definir a porcentagem de progresso (0-100%)", "progressValueRequired": "Por favor, insira um valor de progresso", "progressValueRange": "O progresso deve estar entre 0 e 100", - "taskWeight": "Peso da Tarefa", + "taskWeight": "Peso da tarefa", "taskWeightTooltip": "Definir o peso desta subtarefa (porcentagem)", "taskWeightRequired": "Por favor, insira um peso da tarefa", "taskWeightRange": "O peso deve estar entre 0 e 100", @@ -39,17 +45,17 @@ }, "description": { "title": "Descrição", - "placeholder": "Adicionar uma descrição mais detalhada..." + "placeholder": "Adicione uma descrição mais detalhada..." }, "subTasks": { - "title": "Sub Tarefas", - "addSubTask": "Adicionar Sub Tarefa", + "title": "Subtarefas", + "addSubTask": "Adicionar subtarefa", "addSubTaskInputPlaceholder": "Digite sua tarefa e pressione enter", - "refreshSubTasks": "Atualizar Sub Tarefas", + "refreshSubTasks": "Atualizar subtarefas", "edit": "Editar", - "delete": "Deletar", - "confirmDeleteSubTask": "Tem certeza de que deseja deletar esta subtarefa?", - "deleteSubTask": "Deletar Sub Tarefa" + "delete": "Excluir", + "confirmDeleteSubTask": "Tem certeza de que deseja excluir esta subtarefa?", + "deleteSubTask": "Excluir subtarefa" }, "dependencies": { "title": "Dependências", @@ -57,57 +63,57 @@ "blockedBy": "Bloqueado por", "searchTask": "Digite para pesquisar tarefa", "noTasksFound": "Nenhuma tarefa encontrada", - "confirmDeleteDependency": "Tem certeza de que deseja deletar?" + "confirmDeleteDependency": "Tem certeza de que deseja excluir?" }, "attachments": { "title": "Anexos", - "chooseOrDropFileToUpload": "Escolha ou arraste um arquivo para upload", + "chooseOrDropFileToUpload": "Escolha ou arraste arquivo para enviar", "uploading": "Enviando..." }, "comments": { "title": "Comentários", "addComment": "+ Adicionar novo comentário", "noComments": "Ainda não há comentários. Seja o primeiro a comentar!", - "delete": "Deletar", - "confirmDeleteComment": "Tem certeza de que deseja deletar este comentário?", + "delete": "Excluir", + "confirmDeleteComment": "Tem certeza de que deseja excluir este comentário?", "addCommentPlaceholder": "Adicionar um comentário...", "cancel": "Cancelar", "commentButton": "Comentar", "attachFiles": "Anexar arquivos", "addMoreFiles": "Adicionar mais arquivos", - "selectedFiles": "Arquivos Selecionados (Até 25MB, Máximo {count})", - "maxFilesError": "Você pode fazer upload de no máximo {count} arquivos", + "selectedFiles": "Arquivos selecionados (Até 25MB, Máximo de {count})", + "maxFilesError": "Você pode enviar no máximo {count} arquivos", "processFilesError": "Falha ao processar arquivos", - "addCommentError": "Por favor adicione um comentário ou anexe arquivos", + "addCommentError": "Por favor, adicione um comentário ou anexe arquivos", "createdBy": "Criado {{time}} por {{user}}", "updatedTime": "Atualizado {{time}}" }, "searchInputPlaceholder": "Pesquisar por nome", - "pendingInvitation": "Convite Pendente" + "pendingInvitation": "Convite pendente" }, "taskTimeLogTab": { - "title": "Registro de Tempo", + "title": "Registro de tempo", "addTimeLog": "Adicionar novo registro de tempo", - "totalLogged": "Total Registrado", + "totalLogged": "Total registrado", "exportToExcel": "Exportar para Excel", "noTimeLogsFound": "Nenhum registro de tempo encontrado", "timeLogForm": { "date": "Data", - "startTime": "Hora de Início", - "endTime": "Hora de Fim", - "workDescription": "Descrição do Trabalho", + "startTime": "Hora de início", + "endTime": "Hora de término", + "workDescription": "Descrição do trabalho", "descriptionPlaceholder": "Adicionar uma descrição", "logTime": "Registrar tempo", "updateTime": "Atualizar tempo", "cancel": "Cancelar", - "selectDateError": "Por favor selecione uma data", - "selectStartTimeError": "Por favor selecione a hora de início", - "selectEndTimeError": "Por favor selecione a hora de fim", - "endTimeAfterStartError": "A hora de fim deve ser posterior à hora de início" + "selectDateError": "Por favor, selecione uma data", + "selectStartTimeError": "Por favor, selecione a hora de início", + "selectEndTimeError": "Por favor, selecione a hora de término", + "endTimeAfterStartError": "A hora de término deve ser posterior à hora de início" } }, "taskActivityLogTab": { - "title": "Registro de Atividade", + "title": "Registro de atividade", "add": "ADICIONAR", "remove": "REMOVER", "none": "Nenhum", @@ -115,7 +121,7 @@ "createdTask": "criou a tarefa." }, "taskProgress": { - "markAsDoneTitle": "Marcar Tarefa como Concluída?", + "markAsDoneTitle": "Marcar tarefa como concluída?", "confirmMarkAsDone": "Sim, marcar como concluída", "cancelMarkAsDone": "Não, manter status atual", "markAsDoneDescription": "Você definiu o progresso para 100%. Gostaria de atualizar o status da tarefa para \"Concluída\"?" diff --git a/worklenz-frontend/public/locales/zh/task-drawer/task-drawer.json b/worklenz-frontend/public/locales/zh/task-drawer/task-drawer.json index dfe304fe..868b2876 100644 --- a/worklenz-frontend/public/locales/zh/task-drawer/task-drawer.json +++ b/worklenz-frontend/public/locales/zh/task-drawer/task-drawer.json @@ -1,22 +1,28 @@ { "taskHeader": { "taskNamePlaceholder": "输入您的任务", - "deleteTask": "删除任务" + "deleteTask": "删除任务", + "parentTask": "父任务", + "currentTask": "当前任务", + "back": "返回", + "backToParent": "返回父任务", + "toParentTask": "到父任务", + "loadingHierarchy": "加载层次结构..." }, "taskInfoTab": { "title": "信息", "details": { - "title": "详情", + "title": "详细信息", "task-key": "任务键", "phase": "阶段", - "assignees": "受让人", + "assignees": "受理人", "due-date": "截止日期", "time-estimation": "时间估算", "priority": "优先级", "labels": "标签", "billable": "可计费", "notify": "通知", - "when-done-notify": "完成时,通知", + "when-done-notify": "完成时通知", "start-date": "开始日期", "end-date": "结束日期", "hide-start-date": "隐藏开始日期", @@ -24,18 +30,18 @@ "hours": "小时", "minutes": "分钟", "progressValue": "进度值", - "progressValueTooltip": "设置进度百分比(0-100%)", + "progressValueTooltip": "设置进度百分比 (0-100%)", "progressValueRequired": "请输入进度值", - "progressValueRange": "进度必须在0到100之间", + "progressValueRange": "进度必须在 0 到 100 之间", "taskWeight": "任务权重", - "taskWeightTooltip": "设置此子任务的权重(百分比)", + "taskWeightTooltip": "设置此子任务的权重 (百分比)", "taskWeightRequired": "请输入任务权重", - "taskWeightRange": "权重必须在0到100之间", + "taskWeightRange": "权重必须在 0 到 100 之间", "recurring": "重复" }, "labels": { "labelInputPlaceholder": "搜索或创建", - "labelsSelectorInputTip": "按回车创建" + "labelsSelectorInputTip": "按 Enter 键创建" }, "description": { "title": "描述", @@ -44,7 +50,7 @@ "subTasks": { "title": "子任务", "addSubTask": "添加子任务", - "addSubTaskInputPlaceholder": "输入您的任务并按回车", + "addSubTaskInputPlaceholder": "输入您的任务并按回车键", "refreshSubTasks": "刷新子任务", "edit": "编辑", "delete": "删除", @@ -52,10 +58,10 @@ "deleteSubTask": "删除子任务" }, "dependencies": { - "title": "依赖关系", - "addDependency": "+ 添加新依赖", + "title": "依赖项", + "addDependency": "+ 添加新依赖项", "blockedBy": "被阻止", - "searchTask": "输入搜索任务", + "searchTask": "输入以搜索任务", "noTasksFound": "未找到任务", "confirmDeleteDependency": "您确定要删除吗?" }, @@ -67,7 +73,7 @@ "comments": { "title": "评论", "addComment": "+ 添加新评论", - "noComments": "还没有评论。成为第一个评论的人!", + "noComments": "还没有评论。成为第一个评论者!", "delete": "删除", "confirmDeleteComment": "您确定要删除此评论吗?", "addCommentPlaceholder": "添加评论...", @@ -75,12 +81,12 @@ "commentButton": "评论", "attachFiles": "附加文件", "addMoreFiles": "添加更多文件", - "selectedFiles": "已选择的文件(最多25MB,最大{count}个)", - "maxFilesError": "您最多只能上传{count}个文件", + "selectedFiles": "选定文件 (最多 25MB,最多 {count} 个)", + "maxFilesError": "您最多只能上传 {count} 个文件", "processFilesError": "处理文件失败", "addCommentError": "请添加评论或附加文件", - "createdBy": "{{time}}由{{user}}创建", - "updatedTime": "更新于{{time}}" + "createdBy": "由 {{user}} 在 {{time}} 创建", + "updatedTime": "更新于 {{time}}" }, "searchInputPlaceholder": "按名称搜索", "pendingInvitation": "待处理邀请" @@ -88,8 +94,8 @@ "taskTimeLogTab": { "title": "时间日志", "addTimeLog": "添加新时间日志", - "totalLogged": "总记录时间", - "exportToExcel": "导出到Excel", + "totalLogged": "总计记录", + "exportToExcel": "导出到 Excel", "noTimeLogsFound": "未找到时间日志", "timeLogForm": { "date": "日期", @@ -103,7 +109,7 @@ "selectDateError": "请选择日期", "selectStartTimeError": "请选择开始时间", "selectEndTimeError": "请选择结束时间", - "endTimeAfterStartError": "结束时间必须在开始时间之后" + "endTimeAfterStartError": "结束时间必须晚于开始时间" } }, "taskActivityLogTab": { @@ -116,8 +122,8 @@ }, "taskProgress": { "markAsDoneTitle": "将任务标记为完成?", - "confirmMarkAsDone": "是的,标记为完成", - "cancelMarkAsDone": "不,保持当前状态", - "markAsDoneDescription": "您已将进度设置为100%。您想将任务状态更新为\"完成\"吗?" + "confirmMarkAsDone": "是,标记为完成", + "cancelMarkAsDone": "否,保持当前状态", + "markAsDoneDescription": "您已将进度设置为 100%。您想将任务状态更新为\"完成\"吗?" } } \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.css b/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.css index b2a7f42a..b7fe31a0 100644 --- a/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.css +++ b/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.css @@ -7,3 +7,28 @@ outline: 1px solid #d9d9d9; border-radius: 4px; } + +/* Task name display styles */ +.task-name-display { + margin: 0; + padding: 2px 8px; + font-size: 16px; + cursor: pointer; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-height: 20px; + line-height: 1.4; + display: flex; + align-items: center; +} + +.task-name-display:hover { + background-color: rgba(0, 0, 0, 0.02); + border-radius: 4px; +} + +[data-theme='dark'] .task-name-display:hover { + background-color: rgba(255, 255, 255, 0.05); +} diff --git a/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.tsx b/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.tsx index 8096a8e8..40de65e8 100644 --- a/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.tsx +++ b/worklenz-frontend/src/components/task-drawer/task-drawer-header/task-drawer-header.tsx @@ -1,4 +1,4 @@ -import { Button, Dropdown, Flex, Input, InputRef, MenuProps } from 'antd'; +import { Button, Dropdown, Flex, Input, InputRef, MenuProps, Tooltip } from 'antd'; import React, { ChangeEvent, useEffect, useRef, useState } from 'react'; import { EllipsisOutlined } from '@ant-design/icons'; import { TFunction } from 'i18next'; @@ -21,12 +21,19 @@ import { deleteBoardTask } from '@/features/board/board-slice'; import { deleteTask as deleteKanbanTask, updateEnhancedKanbanSubtask } from '@/features/enhanced-kanban/enhanced-kanban.slice'; import useTabSearchParam from '@/hooks/useTabSearchParam'; import { ITaskViewModel } from '@/types/tasks/task.types'; +import TaskHierarchyBreadcrumb from '../task-hierarchy-breadcrumb/task-hierarchy-breadcrumb'; type TaskDrawerHeaderProps = { inputRef: React.RefObject; t: TFunction; }; +// Utility function to truncate text +const truncateText = (text: string, maxLength: number = 50): string => { + if (!text || text.length <= maxLength) return text; + return `${text.substring(0, maxLength)}...`; +}; + const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => { const dispatch = useAppDispatch(); const { socket, connected } = useSocket(); @@ -38,6 +45,9 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => { const [taskName, setTaskName] = useState(taskFormViewModel?.task?.name ?? ''); const currentSession = useAuthService().getCurrentSession(); + // Check if current task is a sub-task + const isSubTask = taskFormViewModel?.task?.is_sub_task || !!taskFormViewModel?.task?.parent_task_id; + useEffect(() => { setTaskName(taskFormViewModel?.task?.name ?? ''); }, [taskFormViewModel?.task?.name]); @@ -126,54 +136,57 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => { // No need for local socket listeners that could interfere with global handlers }; + const displayTaskName = taskName || t('taskHeader.taskNamePlaceholder'); + const truncatedTaskName = truncateText(displayTaskName, 50); + const shouldShowTooltip = displayTaskName.length > 50; + return ( - - - {isEditing ? ( - onTaskNameChange(e)} - onBlur={handleInputBlur} - placeholder={t('taskHeader.taskNamePlaceholder')} - className="task-name-input" - style={{ - width: '100%', - border: 'none', - }} - showCount={true} - maxLength={250} - autoFocus - /> - ) : ( -

setIsEditing(true)} - style={{ - margin: 0, - padding: '4px 11px', - fontSize: '16px', - cursor: 'pointer', - wordWrap: 'break-word', - overflowWrap: 'break-word', - width: '100%', - }} - > - {taskName || t('taskHeader.taskNamePlaceholder')} -

- )} +
+ {/* Show breadcrumb for sub-tasks */} + {isSubTask && } + + + + {isEditing ? ( + onTaskNameChange(e)} + onBlur={handleInputBlur} + placeholder={t('taskHeader.taskNamePlaceholder')} + className="task-name-input" + style={{ + width: '100%', + border: 'none', + }} + showCount={true} + maxLength={250} + autoFocus + /> + ) : ( + +

setIsEditing(true)} + className="task-name-display" + > + {truncatedTaskName} +

+
+ )} +
+ + + + +
); }; diff --git a/worklenz-frontend/src/components/task-drawer/task-drawer.tsx b/worklenz-frontend/src/components/task-drawer/task-drawer.tsx index 335ad133..de80fb4f 100644 --- a/worklenz-frontend/src/components/task-drawer/task-drawer.tsx +++ b/worklenz-frontend/src/components/task-drawer/task-drawer.tsx @@ -3,7 +3,7 @@ import Drawer from 'antd/es/drawer'; import { InputRef } from 'antd/es/input'; import { useTranslation } from 'react-i18next'; import { useEffect, useRef, useState } from 'react'; -import { PlusOutlined } from '@ant-design/icons'; +import { PlusOutlined, CloseOutlined, ArrowLeftOutlined } from '@ant-design/icons'; import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppDispatch } from '@/hooks/useAppDispatch'; @@ -13,6 +13,7 @@ import { setTaskFormViewModel, setTaskSubscribers, setTimeLogEditing, + fetchTask, } from '@/features/task-drawer/task-drawer.slice'; import './task-drawer.css'; @@ -33,6 +34,7 @@ const TaskDrawer = () => { const { showTaskDrawer, timeLogEditing } = useAppSelector(state => state.taskDrawerReducer); const { taskFormViewModel, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer); + const { projectId } = useAppSelector(state => state.projectReducer); const taskNameInputRef = useRef(null); const isClosingManually = useRef(false); @@ -54,6 +56,17 @@ const TaskDrawer = () => { dispatch(setTaskSubscribers([])); }; + const handleBackToParent = () => { + if (taskFormViewModel?.task?.parent_task_id && projectId) { + // Navigate to parent task + dispatch(setSelectedTaskId(taskFormViewModel.task.parent_task_id)); + dispatch(fetchTask({ + taskId: taskFormViewModel.task.parent_task_id, + projectId + })); + } + }; + const handleOnClose = ( e?: React.MouseEvent | React.KeyboardEvent ) => { @@ -68,10 +81,8 @@ const TaskDrawer = () => { if (isClickOutsideDrawer || !taskFormViewModel?.task?.is_sub_task) { resetTaskState(); } else { - dispatch(setSelectedTaskId(null)); - dispatch(setTaskFormViewModel({})); - dispatch(setTaskSubscribers([])); - dispatch(setSelectedTaskId(taskFormViewModel?.task?.parent_task_id || null)); + // For sub-tasks, navigate to parent instead of closing + handleBackToParent(); } // Reset the flag after a short delay setTimeout(() => { @@ -205,6 +216,17 @@ const TaskDrawer = () => { }; }; + // Check if current task is a sub-task + const isSubTask = taskFormViewModel?.task?.is_sub_task || !!taskFormViewModel?.task?.parent_task_id; + + // Custom close icon based on whether it's a sub-task + const getCloseIcon = () => { + if (isSubTask) { + return ; + } + return ; + }; + const drawerProps = { open: showTaskDrawer, onClose: handleOnClose, @@ -215,6 +237,7 @@ const TaskDrawer = () => { footer: renderFooter(), bodyStyle: getBodyStyle(), footerStyle: getFooterStyle(), + closeIcon: getCloseIcon(), }; return ( diff --git a/worklenz-frontend/src/components/task-drawer/task-hierarchy-breadcrumb/task-hierarchy-breadcrumb.css b/worklenz-frontend/src/components/task-drawer/task-hierarchy-breadcrumb/task-hierarchy-breadcrumb.css new file mode 100644 index 00000000..58f85ab6 --- /dev/null +++ b/worklenz-frontend/src/components/task-drawer/task-hierarchy-breadcrumb/task-hierarchy-breadcrumb.css @@ -0,0 +1,88 @@ +.task-hierarchy-breadcrumb { + margin-bottom: 4px; +} + +.task-hierarchy-breadcrumb .ant-breadcrumb { + font-size: 14px; + line-height: 1; +} + +.task-hierarchy-breadcrumb .ant-breadcrumb-link { + color: inherit; +} + +.task-hierarchy-breadcrumb .ant-breadcrumb-separator { + color: #8c8c8c; + margin: 0 4px; +} + +/* Dark mode styles */ +[data-theme='dark'] .task-hierarchy-breadcrumb .ant-breadcrumb-separator { + color: #595959; +} + +/* Back button styles */ +.task-hierarchy-breadcrumb .ant-btn-link { + padding: 0; + height: auto; + font-size: 14px; + line-height: 1.3; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: flex; + align-items: center; +} + +.task-hierarchy-breadcrumb .ant-btn-link .anticon { + margin-right: 0; /* Remove default margin */ +} + +.task-hierarchy-breadcrumb .ant-btn-link:hover { + color: #40a9ff; +} + +[data-theme='dark'] .task-hierarchy-breadcrumb .ant-btn-link:hover { + color: #40a9ff; +} + +/* Current task name styles */ +.task-hierarchy-breadcrumb .current-task-name { + font-size: 14px; + color: #000000d9; + font-weight: 500; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + line-height: 1.3; +} + +[data-theme='dark'] .task-hierarchy-breadcrumb .current-task-name { + color: #ffffffd9; +} + +/* Breadcrumb item container */ +.task-hierarchy-breadcrumb .ant-breadcrumb-item { + max-width: 220px; + overflow: hidden; + display: flex; + align-items: center; +} + +/* Ensure breadcrumb items don't break the layout */ +.task-hierarchy-breadcrumb .ant-breadcrumb ol { + display: flex; + flex-wrap: wrap; + align-items: center; + margin: 0; + padding: 0; +} + +/* Better alignment for breadcrumb items */ +.task-hierarchy-breadcrumb .ant-breadcrumb-item .ant-breadcrumb-link { + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/worklenz-frontend/src/components/task-drawer/task-hierarchy-breadcrumb/task-hierarchy-breadcrumb.tsx b/worklenz-frontend/src/components/task-drawer/task-hierarchy-breadcrumb/task-hierarchy-breadcrumb.tsx new file mode 100644 index 00000000..792b4347 --- /dev/null +++ b/worklenz-frontend/src/components/task-drawer/task-hierarchy-breadcrumb/task-hierarchy-breadcrumb.tsx @@ -0,0 +1,189 @@ +import React, { useState, useEffect } from 'react'; +import { Breadcrumb, Button, Typography, Tooltip } from 'antd'; +import { HomeOutlined } from '@ant-design/icons'; +import { useAppSelector } from '@/hooks/useAppSelector'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { fetchTask, setSelectedTaskId } from '@/features/task-drawer/task-drawer.slice'; +import { tasksApiService } from '@/api/tasks/tasks.api.service'; +import { TFunction } from 'i18next'; +import './task-hierarchy-breadcrumb.css'; + +interface TaskHierarchyBreadcrumbProps { + t: TFunction; + onBackClick?: () => void; +} + +interface TaskHierarchyItem { + id: string; + name: string; + parent_task_id?: string; +} + +// Utility function to truncate text +const truncateText = (text: string, maxLength: number = 25): string => { + if (!text || text.length <= maxLength) return text; + return `${text.substring(0, maxLength)}...`; +}; + +const TaskHierarchyBreadcrumb: React.FC = ({ t, onBackClick }) => { + const dispatch = useAppDispatch(); + const { taskFormViewModel } = useAppSelector(state => state.taskDrawerReducer); + const { projectId } = useAppSelector(state => state.projectReducer); + const themeMode = useAppSelector(state => state.themeReducer.mode); + const [hierarchyPath, setHierarchyPath] = useState([]); + const [loading, setLoading] = useState(false); + + const task = taskFormViewModel?.task; + const isSubTask = task?.is_sub_task || !!task?.parent_task_id; + + // Recursively fetch the complete hierarchy path + const fetchHierarchyPath = async (currentTaskId: string): Promise => { + if (!projectId) return []; + + const path: TaskHierarchyItem[] = []; + let taskId = currentTaskId; + + // Traverse up the hierarchy until we reach the root + while (taskId) { + try { + const response = await tasksApiService.getFormViewModel(taskId, projectId); + if (response.done && response.body.task) { + const taskData = response.body.task; + path.unshift({ + id: taskData.id, + name: taskData.name || '', + parent_task_id: taskData.parent_task_id || undefined + }); + + // Move to parent task + taskId = taskData.parent_task_id || ''; + } else { + break; + } + } catch (error) { + console.error('Error fetching task in hierarchy:', error); + break; + } + } + + return path; + }; + + // Fetch the complete hierarchy when component mounts or task changes + useEffect(() => { + const loadHierarchy = async () => { + if (!isSubTask || !task?.parent_task_id || !projectId) { + setHierarchyPath([]); + return; + } + + setLoading(true); + try { + const path = await fetchHierarchyPath(task.parent_task_id); + setHierarchyPath(path); + } catch (error) { + console.error('Error loading task hierarchy:', error); + setHierarchyPath([]); + } finally { + setLoading(false); + } + }; + + loadHierarchy(); + }, [task?.parent_task_id, projectId, isSubTask]); + + const handleNavigateToTask = (taskId: string) => { + if (projectId) { + if (onBackClick) { + onBackClick(); + } + + // Navigate to the selected task + dispatch(setSelectedTaskId(taskId)); + dispatch(fetchTask({ taskId, projectId })); + } + }; + + if (!isSubTask || hierarchyPath.length === 0) { + return null; + } + + // Create breadcrumb items from the hierarchy path + const breadcrumbItems = [ + // Add all parent tasks in the hierarchy + ...hierarchyPath.map((hierarchyTask, index) => { + const truncatedName = truncateText(hierarchyTask.name, 25); + const shouldShowTooltip = hierarchyTask.name.length > 25; + + return { + title: ( + + + + ), + }; + }), + // Add the current task as the last item (non-clickable) + { + title: (() => { + const currentTaskName = task?.name || t('taskHeader.currentTask', 'Current Task'); + const truncatedCurrentName = truncateText(currentTaskName, 25); + const shouldShowCurrentTooltip = currentTaskName.length > 25; + + return ( + + + {truncatedCurrentName} + + + ); + })(), + }, + ]; + + return ( +
+ {loading ? ( + + {t('taskHeader.loadingHierarchy', 'Loading hierarchy...')} + + ) : ( + + )} +
+ ); +}; + +export default TaskHierarchyBreadcrumb; \ No newline at end of file