Merge pull request #267 from Worklenz/feature/show-multiples-levels-of-subtaks-in-task-list

Feature/show multiples levels of subtaks in task list
This commit is contained in:
Chamika J
2025-07-15 13:31:10 +05:30
committed by GitHub
17 changed files with 705 additions and 281 deletions

View File

@@ -1,37 +1,43 @@
{ {
"taskHeader": { "taskHeader": {
"taskNamePlaceholder": "Shkruani Detyrën tuaj", "taskNamePlaceholder": "Shkruani detyrën tuaj",
"deleteTask": "Fshi Detyrën" "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": { "taskInfoTab": {
"title": "Informacioni", "title": "Informacioni",
"details": { "details": {
"title": "Detajet", "title": "Detajet",
"task-key": "Çelësi i Detyrës", "task-key": "Çelësi i detyrës",
"phase": "Faza", "phase": "Faza",
"assignees": "Të Caktuar", "assignees": "Të caktuarit",
"due-date": "Data e Përfundimit", "due-date": "Data e përfundimit",
"time-estimation": "Vlerësimi i Kohës", "time-estimation": "Vlerësimi i kohës",
"priority": "Prioriteti", "priority": "Prioriteti",
"labels": "Etiketat", "labels": "Etiketat",
"billable": "E Faturueshme", "billable": "I faturueshëm",
"notify": "Njofto", "notify": "Njofto",
"when-done-notify": "Kur përfundon, njofto", "when-done-notify": "Kur përfundon, njofto",
"start-date": "Data e Fillimit", "start-date": "Data e fillimit",
"end-date": "Data e Përfundimit", "end-date": "Data e përfundimit",
"hide-start-date": "Fshih Datën e Fillimit", "hide-start-date": "Fshih datën e fillimit",
"show-start-date": "Shfaq Datën e Fillimit", "show-start-date": "Shfaq datën e fillimit",
"hours": "Orë", "hours": "Orë",
"minutes": "Minuta", "minutes": "Minuta",
"progressValue": "Vlera e Progresit", "progressValue": "Vlera e progresit",
"progressValueTooltip": "Vendosni përqindjen e progresit (0-100%)", "progressValueTooltip": "Vendos përqindjen e progresit (0-100%)",
"progressValueRequired": "Ju lutemi vendosni një vlerë progresi", "progressValueRequired": "Ju lutemi vendosni një vlerë progresi",
"progressValueRange": "Progresi duhet të jetë midis 0 dhe 100", "progressValueRange": "Progresi duhet të jetë midis 0 dhe 100",
"taskWeight": "Pesha e Detyrës", "taskWeight": "Pesha e detyrës",
"taskWeightTooltip": "Vendosni peshën e kësaj nëndetyre (përqindje)", "taskWeightTooltip": "Vendos peshën e kësaj nëndetyre (përqindje)",
"taskWeightRequired": "Ju lutemi vendosni një peshë detyre", "taskWeightRequired": "Ju lutemi vendosni një peshë detyre",
"taskWeightRange": "Pesha duhet të jetë midis 0 dhe 100", "taskWeightRange": "Pesha duhet të jetë midis 0 dhe 100",
"recurring": "E Përsëritur" "recurring": "Përsëritëse"
}, },
"labels": { "labels": {
"labelInputPlaceholder": "Kërko ose krijo", "labelInputPlaceholder": "Kërko ose krijo",
@@ -43,71 +49,71 @@
}, },
"subTasks": { "subTasks": {
"title": "Nëndetyrat", "title": "Nëndetyrat",
"addSubTask": "Shto Nëndetyrë", "addSubTask": "Shto nëndetyrë",
"addSubTaskInputPlaceholder": "Shkruani detyrën tuaj dhe shtypni enter", "addSubTaskInputPlaceholder": "Shkruani detyrën tuaj dhe shtypni enter",
"refreshSubTasks": "Rifresko Nëndetyrat", "refreshSubTasks": "Rifresko nëndetyrat",
"edit": "Modifiko", "edit": "Redakto",
"delete": "Fshi", "delete": "Fshi",
"confirmDeleteSubTask": "Jeni i sigurt që doni të fshini këtë nëndetyrë?", "confirmDeleteSubTask": "Jeni i sigurt që dëshironi ta fshini këtë nëndetyrë?",
"deleteSubTask": "Fshi Nëndetyrën" "deleteSubTask": "Fshi nëndetyrën"
}, },
"dependencies": { "dependencies": {
"title": "Varësitë", "title": "Varësitë",
"addDependency": "+ Shto varësi të re", "addDependency": "+ Shto varësi të re",
"blockedBy": "Bllokuar nga", "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", "noTasksFound": "Nuk u gjetën detyra",
"confirmDeleteDependency": "Jeni i sigurt që doni të fshini?" "confirmDeleteDependency": "Jeni i sigurt që dëshironi ta fshini?"
}, },
"attachments": { "attachments": {
"title": "Bashkëngjitjet", "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..." "uploading": "Duke ngarkuar..."
}, },
"comments": { "comments": {
"title": "Komentet", "title": "Komentet",
"addComment": "+ Shto koment të ri", "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", "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...", "addCommentPlaceholder": "Shto një koment...",
"cancel": "Anulo", "cancel": "Anulo",
"commentButton": "Komento", "commentButton": "Komento",
"attachFiles": "Bashkëngjit skedarë", "attachFiles": "Bashkëngjit skedarë",
"addMoreFiles": "Shto më shumë skedarë", "addMoreFiles": "Shto më shumë skedarë",
"selectedFiles": "Skedarët e Zgjedhur (Deri në 25MB, Maksimumi {count})", "selectedFiles": "Skedarët e zgjedhur (Deri në 25MB, Maksimumi {count})",
"maxFilesError": "Mund të ngarkoni maksimum {count} skedarë", "maxFilesError": "Mund të ngarkoni maksimumi {count} skedarë",
"processFilesError": "Dështoi përpunimi i skedarëve", "processFilesError": "Dështoi përpunimin e skedarëve",
"addCommentError": "Ju lutemi shtoni një koment ose bashkëngjitni skedarë", "addCommentError": "Ju lutemi shtoni një koment ose bashkëngjitni skedarë",
"createdBy": "Krijuar {{time}} nga {{user}}", "createdBy": "Krijuar {{time}} nga {{user}}",
"updatedTime": "Përditësuar {{time}}" "updatedTime": "Përditësuar {{time}}"
}, },
"searchInputPlaceholder": "Kërko sipas emrit", "searchInputPlaceholder": "Kërko sipas emrit",
"pendingInvitation": "Ftesë në Pritje" "pendingInvitation": "Ftesë në pritje"
}, },
"taskTimeLogTab": { "taskTimeLogTab": {
"title": "Regjistri i Kohës", "title": "Regjistri i kohës",
"addTimeLog": "Shto regjistrim të ri kohe", "addTimeLog": "Shto regjistër të ri kohe",
"totalLogged": "Totali i Regjistruar", "totalLogged": "Totali i regjistruar",
"exportToExcel": "Eksporto në Excel", "exportToExcel": "Eksporto në Excel",
"noTimeLogsFound": "Nuk u gjetën regjistra kohe", "noTimeLogsFound": "Nuk u gjetën regjistrime kohe",
"timeLogForm": { "timeLogForm": {
"date": "Data", "date": "Data",
"startTime": "Koha e Fillimit", "startTime": "Ora e fillimit",
"endTime": "Koha e Përfundimit", "endTime": "Ora e përfundimit",
"workDescription": "Përshkrimi i Punës", "workDescription": "Përshkrimi i punës",
"descriptionPlaceholder": "Shto një përshkrim", "descriptionPlaceholder": "Shto një përshkrim",
"logTime": "Regjistro kohën", "logTime": "Regjistro kohën",
"updateTime": "Përditëso kohën", "updateTime": "Përditëso kohën",
"cancel": "Anulo", "cancel": "Anulo",
"selectDateError": "Ju lutemi zgjidhni një datë", "selectDateError": "Ju lutemi zgjidhni një datë",
"selectStartTimeError": "Ju lutemi zgjidhni kohën e fillimit", "selectStartTimeError": "Ju lutemi zgjidhni orën e fillimit",
"selectEndTimeError": "Ju lutemi zgjidhni kohën e përfundimit", "selectEndTimeError": "Ju lutemi zgjidhni orën e përfundimit",
"endTimeAfterStartError": "Koha e përfundimit duhet të jetë pas kohës së fillimit" "endTimeAfterStartError": "Ora e përfundimit duhet të jetë pas orës së fillimit"
} }
}, },
"taskActivityLogTab": { "taskActivityLogTab": {
"title": "Regjistri i Aktivitetit", "title": "Regjistri i aktivitetit",
"add": "SHTO", "add": "SHTO",
"remove": "HIQE", "remove": "HIQE",
"none": "Asnjë", "none": "Asnjë",
@@ -115,9 +121,9 @@
"createdTask": "krijoi detyrën." "createdTask": "krijoi detyrën."
}, },
"taskProgress": { "taskProgress": {
"markAsDoneTitle": "Shëno Detyrën si të Kryer?", "markAsDoneTitle": "Shëno detyrën si të përfunduar?",
"confirmMarkAsDone": "Po, shëno si të kryer", "confirmMarkAsDone": "Po, shënoje si të përfunduar",
"cancelMarkAsDone": "Jo, mbaj statusin aktual", "cancelMarkAsDone": "Jo, mbaj gjendjen aktuale",
"markAsDoneDescription": "Keni vendosur progresin në 100%. Doni të përditësoni statusin e detyrës në \"Kryer\"?" "markAsDoneDescription": "Keni vendosur progresin në 100%. Dëshironi ta përditësoni gjendjen e detyrës në \"Përfunduar\"?"
} }
} }

View File

@@ -1,22 +1,28 @@
{ {
"taskHeader": { "taskHeader": {
"taskNamePlaceholder": "Geben Sie Ihre Aufgabe ein", "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": { "taskInfoTab": {
"title": "Info", "title": "Info",
"details": { "details": {
"title": "Details", "title": "Details",
"task-key": "Aufgaben-Schlüssel", "task-key": "Aufgabenschlüssel",
"phase": "Phase", "phase": "Phase",
"assignees": "Beauftragte", "assignees": "Zugewiesene",
"due-date": "Fälligkeitsdatum", "due-date": "Fälligkeitsdatum",
"time-estimation": "Zeitschätzung", "time-estimation": "Zeitschätzung",
"priority": "Priorität", "priority": "Priorität",
"labels": "Labels", "labels": "Labels",
"billable": "Abrechenbar", "billable": "Abrechenbar",
"notify": "Benachrichtigen", "notify": "Benachrichtigen",
"when-done-notify": "Bei Abschluss benachrichtigen", "when-done-notify": "Bei Fertigstellung benachrichtigen",
"start-date": "Startdatum", "start-date": "Startdatum",
"end-date": "Enddatum", "end-date": "Enddatum",
"hide-start-date": "Startdatum ausblenden", "hide-start-date": "Startdatum ausblenden",
@@ -24,50 +30,50 @@
"hours": "Stunden", "hours": "Stunden",
"minutes": "Minuten", "minutes": "Minuten",
"progressValue": "Fortschrittswert", "progressValue": "Fortschrittswert",
"progressValueTooltip": "Fortschritt in Prozent einstellen (0-100%)", "progressValueTooltip": "Setzen Sie den Fortschrittsprozentsatz (0-100%)",
"progressValueRequired": "Bitte geben Sie einen Fortschrittswert ein", "progressValueRequired": "Bitte geben Sie einen Fortschrittswert ein",
"progressValueRange": "Fortschritt muss zwischen 0 und 100 liegen", "progressValueRange": "Fortschritt muss zwischen 0 und 100 liegen",
"taskWeight": "Aufgabengewicht", "taskWeight": "Aufgabengewicht",
"taskWeightTooltip": "Gewicht dieser Teilaufgabe festlegen (Prozent)", "taskWeightTooltip": "Setzen Sie das Gewicht dieser Unteraufgabe (Prozentsatz)",
"taskWeightRequired": "Bitte geben Sie ein Aufgabengewicht ein", "taskWeightRequired": "Bitte geben Sie ein Aufgabengewicht ein",
"taskWeightRange": "Gewicht muss zwischen 0 und 100 liegen", "taskWeightRange": "Gewicht muss zwischen 0 und 100 liegen",
"recurring": "Wiederkehrend" "recurring": "Wiederkehrend"
}, },
"labels": { "labels": {
"labelInputPlaceholder": "Suchen oder erstellen", "labelInputPlaceholder": "Suchen oder erstellen",
"labelsSelectorInputTip": "Enter drücken zum Erstellen" "labelsSelectorInputTip": "Drücken Sie Enter zum Erstellen"
}, },
"description": { "description": {
"title": "Beschreibung", "title": "Beschreibung",
"placeholder": "Detailliertere Beschreibung hinzufügen..." "placeholder": "Fügen Sie eine detailliertere Beschreibung hinzu..."
}, },
"subTasks": { "subTasks": {
"title": "Teilaufgaben", "title": "Unteraufgaben",
"addSubTask": "Teilaufgabe hinzufügen", "addSubTask": "Unteraufgabe hinzufügen",
"addSubTaskInputPlaceholder": "Geben Sie Ihre Aufgabe ein und drücken Sie Enter", "addSubTaskInputPlaceholder": "Geben Sie Ihre Aufgabe ein und drücken Sie Enter",
"refreshSubTasks": "Teilaufgaben aktualisieren", "refreshSubTasks": "Unteraufgaben aktualisieren",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"delete": "Löschen", "delete": "Löschen",
"confirmDeleteSubTask": "Sind Sie sicher, dass Sie diese Teilaufgabe löschen möchten?", "confirmDeleteSubTask": "Sind Sie sicher, dass Sie diese Unteraufgabe löschen möchten?",
"deleteSubTask": "Teilaufgabe löschen" "deleteSubTask": "Unteraufgabe löschen"
}, },
"dependencies": { "dependencies": {
"title": "Abhängigkeiten", "title": "Abhängigkeiten",
"addDependency": "+ Neue Abhängigkeit hinzufügen", "addDependency": "+ Neue Abhängigkeit hinzufügen",
"blockedBy": "Blockiert von", "blockedBy": "Blockiert von",
"searchTask": "Aufgabe suchen", "searchTask": "Zum Suchen der Aufgabe eingeben",
"noTasksFound": "Keine Aufgaben gefunden", "noTasksFound": "Keine Aufgaben gefunden",
"confirmDeleteDependency": "Sind Sie sicher, dass Sie löschen möchten?" "confirmDeleteDependency": "Sind Sie sicher, dass Sie löschen möchten?"
}, },
"attachments": { "attachments": {
"title": "Anhänge", "title": "Anhänge",
"chooseOrDropFileToUpload": "Datei zum Hochladen wählen oder ablegen", "chooseOrDropFileToUpload": "Datei zum Hochladen auswählen oder ablegen",
"uploading": "Wird hochgeladen..." "uploading": "Wird hochgeladen..."
}, },
"comments": { "comments": {
"title": "Kommentare", "title": "Kommentare",
"addComment": "+ Neuen Kommentar hinzufügen", "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", "delete": "Löschen",
"confirmDeleteComment": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?", "confirmDeleteComment": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?",
"addCommentPlaceholder": "Kommentar hinzufügen...", "addCommentPlaceholder": "Kommentar hinzufügen...",
@@ -75,9 +81,9 @@
"commentButton": "Kommentieren", "commentButton": "Kommentieren",
"attachFiles": "Dateien anhängen", "attachFiles": "Dateien anhängen",
"addMoreFiles": "Weitere Dateien hinzufügen", "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", "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", "addCommentError": "Bitte fügen Sie einen Kommentar hinzu oder hängen Sie Dateien an",
"createdBy": "Erstellt {{time}} von {{user}}", "createdBy": "Erstellt {{time}} von {{user}}",
"updatedTime": "Aktualisiert {{time}}" "updatedTime": "Aktualisiert {{time}}"
@@ -86,18 +92,18 @@
"pendingInvitation": "Ausstehende Einladung" "pendingInvitation": "Ausstehende Einladung"
}, },
"taskTimeLogTab": { "taskTimeLogTab": {
"title": "Zeiterfassung", "title": "Zeitprotokoll",
"addTimeLog": "Neuen Zeiteintrag hinzufügen", "addTimeLog": "Neues Zeitprotokoll hinzufügen",
"totalLogged": "Gesamt erfasst", "totalLogged": "Gesamt protokolliert",
"exportToExcel": "Nach Excel exportieren", "exportToExcel": "Nach Excel exportieren",
"noTimeLogsFound": "Keine Zeiteinträge gefunden", "noTimeLogsFound": "Keine Zeitprotokolle gefunden",
"timeLogForm": { "timeLogForm": {
"date": "Datum", "date": "Datum",
"startTime": "Startzeit", "startTime": "Startzeit",
"endTime": "Endzeit", "endTime": "Endzeit",
"workDescription": "Arbeitsbeschreibung", "workDescription": "Arbeitsbeschreibung",
"descriptionPlaceholder": "Beschreibung hinzufügen", "descriptionPlaceholder": "Beschreibung hinzufügen",
"logTime": "Zeit erfassen", "logTime": "Zeit protokollieren",
"updateTime": "Zeit aktualisieren", "updateTime": "Zeit aktualisieren",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"selectDateError": "Bitte wählen Sie ein Datum", "selectDateError": "Bitte wählen Sie ein Datum",

View File

@@ -1,7 +1,13 @@
{ {
"taskHeader": { "taskHeader": {
"taskNamePlaceholder": "Type your Task", "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": { "taskInfoTab": {
"title": "Info", "title": "Info",

View File

@@ -1,35 +1,41 @@
{ {
"taskHeader": { "taskHeader": {
"taskNamePlaceholder": "Escriba su Tarea", "taskNamePlaceholder": "Escribe tu tarea",
"deleteTask": "Eliminar 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": { "taskInfoTab": {
"title": "Información", "title": "Información",
"details": { "details": {
"title": "Detalles", "title": "Detalles",
"task-key": "Clave de Tarea", "task-key": "Clave de tarea",
"phase": "Fase", "phase": "Fase",
"assignees": "Asignados", "assignees": "Asignados",
"due-date": "Fecha de Vencimiento", "due-date": "Fecha de vencimiento",
"time-estimation": "Estimación de Tiempo", "time-estimation": "Estimación de tiempo",
"priority": "Prioridad", "priority": "Prioridad",
"labels": "Etiquetas", "labels": "Etiquetas",
"billable": "Facturable", "billable": "Facturable",
"notify": "Notificar", "notify": "Notificar",
"when-done-notify": "Al terminar, notificar", "when-done-notify": "Al finalizar, notificar",
"start-date": "Fecha de Inicio", "start-date": "Fecha de inicio",
"end-date": "Fecha de Fin", "end-date": "Fecha de finalización",
"hide-start-date": "Ocultar Fecha de Inicio", "hide-start-date": "Ocultar fecha de inicio",
"show-start-date": "Mostrar Fecha de Inicio", "show-start-date": "Mostrar fecha de inicio",
"hours": "Horas", "hours": "Horas",
"minutes": "Minutos", "minutes": "Minutos",
"progressValue": "Valor de Progreso", "progressValue": "Valor de progreso",
"progressValueTooltip": "Establecer el porcentaje de progreso (0-100%)", "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", "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)", "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", "taskWeightRange": "El peso debe estar entre 0 y 100",
"recurring": "Recurrente" "recurring": "Recurrente"
}, },
@@ -39,85 +45,85 @@
}, },
"description": { "description": {
"title": "Descripción", "title": "Descripción",
"placeholder": "Añadir una descripción más detallada..." "placeholder": "Añade una descripción más detallada..."
}, },
"subTasks": { "subTasks": {
"title": "Sub Tareas", "title": "Subtareas",
"addSubTask": "Agregar Sub Tarea", "addSubTask": "Añadir subtarea",
"addSubTaskInputPlaceholder": "Escriba su tarea y presione enter", "addSubTaskInputPlaceholder": "Escribe tu tarea y presiona enter",
"refreshSubTasks": "Actualizar Sub Tareas", "refreshSubTasks": "Actualizar subtareas",
"edit": "Editar", "edit": "Editar",
"delete": "Eliminar", "delete": "Eliminar",
"confirmDeleteSubTask": "¿Está seguro de que desea eliminar esta subtarea?", "confirmDeleteSubTask": "¿Estás seguro de que quieres eliminar esta subtarea?",
"deleteSubTask": "Eliminar Sub Tarea" "deleteSubTask": "Eliminar subtarea"
}, },
"dependencies": { "dependencies": {
"title": "Dependencias", "title": "Dependencias",
"addDependency": "+ Agregar nueva dependencia", "addDependency": "+ Añadir nueva dependencia",
"blockedBy": "Bloqueado por", "blockedBy": "Bloqueado por",
"searchTask": "Escribir para buscar tarea", "searchTask": "Escribe para buscar tarea",
"noTasksFound": "No se encontraron tareas", "noTasksFound": "No se encontraron tareas",
"confirmDeleteDependency": "¿Está seguro de que desea eliminar?" "confirmDeleteDependency": "¿Estás seguro de que quieres eliminar?"
}, },
"attachments": { "attachments": {
"title": "Adjuntos", "title": "Adjuntos",
"chooseOrDropFileToUpload": "Elija o arrastre un archivo para subir", "chooseOrDropFileToUpload": "Elige o arrastra archivo para subir",
"uploading": "Subiendo..." "uploading": "Subiendo..."
}, },
"comments": { "comments": {
"title": "Comentarios", "title": "Comentarios",
"addComment": "+ Agregar nuevo comentario", "addComment": "+ Añadir nuevo comentario",
"noComments": "Aún no hay comentarios. ¡Sé el primero en comentar!", "noComments": "Aún no hay comentarios. ¡Sé el primero en comentar!",
"delete": "Eliminar", "delete": "Eliminar",
"confirmDeleteComment": "¿Está seguro de que desea eliminar este comentario?", "confirmDeleteComment": "¿Estás seguro de que quieres eliminar este comentario?",
"addCommentPlaceholder": "Agregar un comentario...", "addCommentPlaceholder": "Añadir un comentario...",
"cancel": "Cancelar", "cancel": "Cancelar",
"commentButton": "Comentar", "commentButton": "Comentar",
"attachFiles": "Adjuntar archivos", "attachFiles": "Adjuntar archivos",
"addMoreFiles": "Agregar más archivos", "addMoreFiles": "Añadir más archivos",
"selectedFiles": "Archivos Seleccionados (Hasta 25MB, Máximo {count})", "selectedFiles": "Archivos seleccionados (Hasta 25MB, Máximo de {count})",
"maxFilesError": "Solo puede subir un máximo de {count} archivos", "maxFilesError": "Solo puedes subir un máximo de {count} archivos",
"processFilesError": "Error al procesar 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}}", "createdBy": "Creado {{time}} por {{user}}",
"updatedTime": "Actualizado {{time}}" "updatedTime": "Actualizado {{time}}"
}, },
"searchInputPlaceholder": "Buscar por nombre", "searchInputPlaceholder": "Buscar por nombre",
"pendingInvitation": "Invitación Pendiente" "pendingInvitation": "Invitación pendiente"
}, },
"taskTimeLogTab": { "taskTimeLogTab": {
"title": "Registro de Tiempo", "title": "Registro de tiempo",
"addTimeLog": "Añadir nuevo registro de tiempo", "addTimeLog": "Añadir nuevo registro de tiempo",
"totalLogged": "Total Registrado", "totalLogged": "Total registrado",
"exportToExcel": "Exportar a Excel", "exportToExcel": "Exportar a Excel",
"noTimeLogsFound": "No se encontraron registros de tiempo", "noTimeLogsFound": "No se encontraron registros de tiempo",
"timeLogForm": { "timeLogForm": {
"date": "Fecha", "date": "Fecha",
"startTime": "Hora de Inicio", "startTime": "Hora de inicio",
"endTime": "Hora de Fin", "endTime": "Hora de finalización",
"workDescription": "Descripción del Trabajo", "workDescription": "Descripción del trabajo",
"descriptionPlaceholder": "Agregar una descripción", "descriptionPlaceholder": "Añadir una descripción",
"logTime": "Registrar tiempo", "logTime": "Registrar tiempo",
"updateTime": "Actualizar tiempo", "updateTime": "Actualizar tiempo",
"cancel": "Cancelar", "cancel": "Cancelar",
"selectDateError": "Por favor seleccione una fecha", "selectDateError": "Por favor selecciona una fecha",
"selectStartTimeError": "Por favor seleccione la hora de inicio", "selectStartTimeError": "Por favor selecciona hora de inicio",
"selectEndTimeError": "Por favor seleccione la hora de fin", "selectEndTimeError": "Por favor selecciona hora de finalización",
"endTimeAfterStartError": "La hora de fin debe ser posterior a la hora de inicio" "endTimeAfterStartError": "La hora de finalización debe ser posterior a la de inicio"
} }
}, },
"taskActivityLogTab": { "taskActivityLogTab": {
"title": "Registro de Actividad", "title": "Registro de actividad",
"add": "AGREGAR", "add": "AÑADIR",
"remove": "QUITAR", "remove": "ELIMINAR",
"none": "Ninguno", "none": "Ninguno",
"weight": "Peso", "weight": "Peso",
"createdTask": "creó la tarea." "createdTask": "creó la tarea."
}, },
"taskProgress": { "taskProgress": {
"markAsDoneTitle": "¿Marcar Tarea como Completada?", "markAsDoneTitle": "¿Marcar tarea como completada?",
"confirmMarkAsDone": "Sí, marcar como completada", "confirmMarkAsDone": "Sí, marcar como completada",
"cancelMarkAsDone": "No, mantener estado actual", "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\"?"
} }
} }

View File

@@ -1,33 +1,39 @@
{ {
"taskHeader": { "taskHeader": {
"taskNamePlaceholder": "Digite sua Tarefa", "taskNamePlaceholder": "Digite sua tarefa",
"deleteTask": "Deletar Tarefa" "deleteTask": "Excluir tarefa",
"parentTask": "Tarefa principal",
"currentTask": "Tarefa atual",
"back": "Voltar",
"backToParent": "Voltar à tarefa principal",
"toParentTask": "à tarefa principal",
"loadingHierarchy": "Carregando hierarquia..."
}, },
"taskInfoTab": { "taskInfoTab": {
"title": "Informações", "title": "Informações",
"details": { "details": {
"title": "Detalhes", "title": "Detalhes",
"task-key": "Chave da Tarefa", "task-key": "Chave da tarefa",
"phase": "Fase", "phase": "Fase",
"assignees": "Responsáveis", "assignees": "Responsáveis",
"due-date": "Data de Vencimento", "due-date": "Data de vencimento",
"time-estimation": "Estimativa de Tempo", "time-estimation": "Estimativa de tempo",
"priority": "Prioridade", "priority": "Prioridade",
"labels": "Etiquetas", "labels": "Etiquetas",
"billable": "Faturável", "billable": "Faturável",
"notify": "Notificar", "notify": "Notificar",
"when-done-notify": "Quando concluído, notificar", "when-done-notify": "Ao concluir, notificar",
"start-date": "Data de Início", "start-date": "Data de início",
"end-date": "Data de Fim", "end-date": "Data de término",
"hide-start-date": "Ocultar Data de Início", "hide-start-date": "Ocultar data de início",
"show-start-date": "Mostrar Data de Início", "show-start-date": "Mostrar data de início",
"hours": "Horas", "hours": "Horas",
"minutes": "Minutos", "minutes": "Minutos",
"progressValue": "Valor do Progresso", "progressValue": "Valor do progresso",
"progressValueTooltip": "Definir a porcentagem de progresso (0-100%)", "progressValueTooltip": "Definir a porcentagem de progresso (0-100%)",
"progressValueRequired": "Por favor, insira um valor de progresso", "progressValueRequired": "Por favor, insira um valor de progresso",
"progressValueRange": "O progresso deve estar entre 0 e 100", "progressValueRange": "O progresso deve estar entre 0 e 100",
"taskWeight": "Peso da Tarefa", "taskWeight": "Peso da tarefa",
"taskWeightTooltip": "Definir o peso desta subtarefa (porcentagem)", "taskWeightTooltip": "Definir o peso desta subtarefa (porcentagem)",
"taskWeightRequired": "Por favor, insira um peso da tarefa", "taskWeightRequired": "Por favor, insira um peso da tarefa",
"taskWeightRange": "O peso deve estar entre 0 e 100", "taskWeightRange": "O peso deve estar entre 0 e 100",
@@ -39,17 +45,17 @@
}, },
"description": { "description": {
"title": "Descrição", "title": "Descrição",
"placeholder": "Adicionar uma descrição mais detalhada..." "placeholder": "Adicione uma descrição mais detalhada..."
}, },
"subTasks": { "subTasks": {
"title": "Sub Tarefas", "title": "Subtarefas",
"addSubTask": "Adicionar Sub Tarefa", "addSubTask": "Adicionar subtarefa",
"addSubTaskInputPlaceholder": "Digite sua tarefa e pressione enter", "addSubTaskInputPlaceholder": "Digite sua tarefa e pressione enter",
"refreshSubTasks": "Atualizar Sub Tarefas", "refreshSubTasks": "Atualizar subtarefas",
"edit": "Editar", "edit": "Editar",
"delete": "Deletar", "delete": "Excluir",
"confirmDeleteSubTask": "Tem certeza de que deseja deletar esta subtarefa?", "confirmDeleteSubTask": "Tem certeza de que deseja excluir esta subtarefa?",
"deleteSubTask": "Deletar Sub Tarefa" "deleteSubTask": "Excluir subtarefa"
}, },
"dependencies": { "dependencies": {
"title": "Dependências", "title": "Dependências",
@@ -57,57 +63,57 @@
"blockedBy": "Bloqueado por", "blockedBy": "Bloqueado por",
"searchTask": "Digite para pesquisar tarefa", "searchTask": "Digite para pesquisar tarefa",
"noTasksFound": "Nenhuma tarefa encontrada", "noTasksFound": "Nenhuma tarefa encontrada",
"confirmDeleteDependency": "Tem certeza de que deseja deletar?" "confirmDeleteDependency": "Tem certeza de que deseja excluir?"
}, },
"attachments": { "attachments": {
"title": "Anexos", "title": "Anexos",
"chooseOrDropFileToUpload": "Escolha ou arraste um arquivo para upload", "chooseOrDropFileToUpload": "Escolha ou arraste arquivo para enviar",
"uploading": "Enviando..." "uploading": "Enviando..."
}, },
"comments": { "comments": {
"title": "Comentários", "title": "Comentários",
"addComment": "+ Adicionar novo comentário", "addComment": "+ Adicionar novo comentário",
"noComments": "Ainda não há comentários. Seja o primeiro a comentar!", "noComments": "Ainda não há comentários. Seja o primeiro a comentar!",
"delete": "Deletar", "delete": "Excluir",
"confirmDeleteComment": "Tem certeza de que deseja deletar este comentário?", "confirmDeleteComment": "Tem certeza de que deseja excluir este comentário?",
"addCommentPlaceholder": "Adicionar um comentário...", "addCommentPlaceholder": "Adicionar um comentário...",
"cancel": "Cancelar", "cancel": "Cancelar",
"commentButton": "Comentar", "commentButton": "Comentar",
"attachFiles": "Anexar arquivos", "attachFiles": "Anexar arquivos",
"addMoreFiles": "Adicionar mais arquivos", "addMoreFiles": "Adicionar mais arquivos",
"selectedFiles": "Arquivos Selecionados (Até 25MB, Máximo {count})", "selectedFiles": "Arquivos selecionados (Até 25MB, Máximo de {count})",
"maxFilesError": "Você pode fazer upload de no máximo {count} arquivos", "maxFilesError": "Você pode enviar no máximo {count} arquivos",
"processFilesError": "Falha ao processar 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}}", "createdBy": "Criado {{time}} por {{user}}",
"updatedTime": "Atualizado {{time}}" "updatedTime": "Atualizado {{time}}"
}, },
"searchInputPlaceholder": "Pesquisar por nome", "searchInputPlaceholder": "Pesquisar por nome",
"pendingInvitation": "Convite Pendente" "pendingInvitation": "Convite pendente"
}, },
"taskTimeLogTab": { "taskTimeLogTab": {
"title": "Registro de Tempo", "title": "Registro de tempo",
"addTimeLog": "Adicionar novo registro de tempo", "addTimeLog": "Adicionar novo registro de tempo",
"totalLogged": "Total Registrado", "totalLogged": "Total registrado",
"exportToExcel": "Exportar para Excel", "exportToExcel": "Exportar para Excel",
"noTimeLogsFound": "Nenhum registro de tempo encontrado", "noTimeLogsFound": "Nenhum registro de tempo encontrado",
"timeLogForm": { "timeLogForm": {
"date": "Data", "date": "Data",
"startTime": "Hora de Início", "startTime": "Hora de início",
"endTime": "Hora de Fim", "endTime": "Hora de término",
"workDescription": "Descrição do Trabalho", "workDescription": "Descrição do trabalho",
"descriptionPlaceholder": "Adicionar uma descrição", "descriptionPlaceholder": "Adicionar uma descrição",
"logTime": "Registrar tempo", "logTime": "Registrar tempo",
"updateTime": "Atualizar tempo", "updateTime": "Atualizar tempo",
"cancel": "Cancelar", "cancel": "Cancelar",
"selectDateError": "Por favor selecione uma data", "selectDateError": "Por favor, selecione uma data",
"selectStartTimeError": "Por favor selecione a hora de início", "selectStartTimeError": "Por favor, selecione a hora de início",
"selectEndTimeError": "Por favor selecione a hora de fim", "selectEndTimeError": "Por favor, selecione a hora de término",
"endTimeAfterStartError": "A hora de fim deve ser posterior à hora de início" "endTimeAfterStartError": "A hora de término deve ser posterior à hora de início"
} }
}, },
"taskActivityLogTab": { "taskActivityLogTab": {
"title": "Registro de Atividade", "title": "Registro de atividade",
"add": "ADICIONAR", "add": "ADICIONAR",
"remove": "REMOVER", "remove": "REMOVER",
"none": "Nenhum", "none": "Nenhum",
@@ -115,7 +121,7 @@
"createdTask": "criou a tarefa." "createdTask": "criou a tarefa."
}, },
"taskProgress": { "taskProgress": {
"markAsDoneTitle": "Marcar Tarefa como Concluída?", "markAsDoneTitle": "Marcar tarefa como concluída?",
"confirmMarkAsDone": "Sim, marcar como concluída", "confirmMarkAsDone": "Sim, marcar como concluída",
"cancelMarkAsDone": "Não, manter status atual", "cancelMarkAsDone": "Não, manter status atual",
"markAsDoneDescription": "Você definiu o progresso para 100%. Gostaria de atualizar o status da tarefa para \"Concluída\"?" "markAsDoneDescription": "Você definiu o progresso para 100%. Gostaria de atualizar o status da tarefa para \"Concluída\"?"

View File

@@ -1,22 +1,28 @@
{ {
"taskHeader": { "taskHeader": {
"taskNamePlaceholder": "输入您的任务", "taskNamePlaceholder": "输入您的任务",
"deleteTask": "删除任务" "deleteTask": "删除任务",
"parentTask": "父任务",
"currentTask": "当前任务",
"back": "返回",
"backToParent": "返回父任务",
"toParentTask": "到父任务",
"loadingHierarchy": "加载层次结构..."
}, },
"taskInfoTab": { "taskInfoTab": {
"title": "信息", "title": "信息",
"details": { "details": {
"title": "详", "title": "详细信息",
"task-key": "任务键", "task-key": "任务键",
"phase": "阶段", "phase": "阶段",
"assignees": "受人", "assignees": "受人",
"due-date": "截止日期", "due-date": "截止日期",
"time-estimation": "时间估算", "time-estimation": "时间估算",
"priority": "优先级", "priority": "优先级",
"labels": "标签", "labels": "标签",
"billable": "可计费", "billable": "可计费",
"notify": "通知", "notify": "通知",
"when-done-notify": "完成时通知", "when-done-notify": "完成时通知",
"start-date": "开始日期", "start-date": "开始日期",
"end-date": "结束日期", "end-date": "结束日期",
"hide-start-date": "隐藏开始日期", "hide-start-date": "隐藏开始日期",
@@ -24,18 +30,18 @@
"hours": "小时", "hours": "小时",
"minutes": "分钟", "minutes": "分钟",
"progressValue": "进度值", "progressValue": "进度值",
"progressValueTooltip": "设置进度百分比0-100%", "progressValueTooltip": "设置进度百分比 (0-100%)",
"progressValueRequired": "请输入进度值", "progressValueRequired": "请输入进度值",
"progressValueRange": "进度必须在0到100之间", "progressValueRange": "进度必须在 0 到 100 之间",
"taskWeight": "任务权重", "taskWeight": "任务权重",
"taskWeightTooltip": "设置此子任务的权重百分比", "taskWeightTooltip": "设置此子任务的权重 (百分比)",
"taskWeightRequired": "请输入任务权重", "taskWeightRequired": "请输入任务权重",
"taskWeightRange": "权重必须在0到100之间", "taskWeightRange": "权重必须在 0 到 100 之间",
"recurring": "重复" "recurring": "重复"
}, },
"labels": { "labels": {
"labelInputPlaceholder": "搜索或创建", "labelInputPlaceholder": "搜索或创建",
"labelsSelectorInputTip": "按回车创建" "labelsSelectorInputTip": "按 Enter 键创建"
}, },
"description": { "description": {
"title": "描述", "title": "描述",
@@ -44,7 +50,7 @@
"subTasks": { "subTasks": {
"title": "子任务", "title": "子任务",
"addSubTask": "添加子任务", "addSubTask": "添加子任务",
"addSubTaskInputPlaceholder": "输入您的任务并按回车", "addSubTaskInputPlaceholder": "输入您的任务并按回车",
"refreshSubTasks": "刷新子任务", "refreshSubTasks": "刷新子任务",
"edit": "编辑", "edit": "编辑",
"delete": "删除", "delete": "删除",
@@ -52,10 +58,10 @@
"deleteSubTask": "删除子任务" "deleteSubTask": "删除子任务"
}, },
"dependencies": { "dependencies": {
"title": "依赖关系", "title": "依赖",
"addDependency": "+ 添加新依赖", "addDependency": "+ 添加新依赖",
"blockedBy": "被阻止", "blockedBy": "被阻止",
"searchTask": "输入搜索任务", "searchTask": "输入搜索任务",
"noTasksFound": "未找到任务", "noTasksFound": "未找到任务",
"confirmDeleteDependency": "您确定要删除吗?" "confirmDeleteDependency": "您确定要删除吗?"
}, },
@@ -67,7 +73,7 @@
"comments": { "comments": {
"title": "评论", "title": "评论",
"addComment": "+ 添加新评论", "addComment": "+ 添加新评论",
"noComments": "还没有评论。成为第一个评论的人", "noComments": "还没有评论。成为第一个评论",
"delete": "删除", "delete": "删除",
"confirmDeleteComment": "您确定要删除此评论吗?", "confirmDeleteComment": "您确定要删除此评论吗?",
"addCommentPlaceholder": "添加评论...", "addCommentPlaceholder": "添加评论...",
@@ -75,12 +81,12 @@
"commentButton": "评论", "commentButton": "评论",
"attachFiles": "附加文件", "attachFiles": "附加文件",
"addMoreFiles": "添加更多文件", "addMoreFiles": "添加更多文件",
"selectedFiles": "已选择的文件(最多25MB{count}个)", "selectedFiles": "选定文件 (最多 25MB{count} 个)",
"maxFilesError": "您最多只能上传{count}个文件", "maxFilesError": "您最多只能上传 {count} 个文件",
"processFilesError": "处理文件失败", "processFilesError": "处理文件失败",
"addCommentError": "请添加评论或附加文件", "addCommentError": "请添加评论或附加文件",
"createdBy": "{{time}}由{{user}}创建", "createdBy": "由 {{user}} 在 {{time}} 创建",
"updatedTime": "更新于{{time}}" "updatedTime": "更新于 {{time}}"
}, },
"searchInputPlaceholder": "按名称搜索", "searchInputPlaceholder": "按名称搜索",
"pendingInvitation": "待处理邀请" "pendingInvitation": "待处理邀请"
@@ -88,8 +94,8 @@
"taskTimeLogTab": { "taskTimeLogTab": {
"title": "时间日志", "title": "时间日志",
"addTimeLog": "添加新时间日志", "addTimeLog": "添加新时间日志",
"totalLogged": "总记录时间", "totalLogged": "总记录",
"exportToExcel": "导出到Excel", "exportToExcel": "导出到 Excel",
"noTimeLogsFound": "未找到时间日志", "noTimeLogsFound": "未找到时间日志",
"timeLogForm": { "timeLogForm": {
"date": "日期", "date": "日期",
@@ -103,7 +109,7 @@
"selectDateError": "请选择日期", "selectDateError": "请选择日期",
"selectStartTimeError": "请选择开始时间", "selectStartTimeError": "请选择开始时间",
"selectEndTimeError": "请选择结束时间", "selectEndTimeError": "请选择结束时间",
"endTimeAfterStartError": "结束时间必须开始时间之后" "endTimeAfterStartError": "结束时间必须晚于开始时间"
} }
}, },
"taskActivityLogTab": { "taskActivityLogTab": {
@@ -116,8 +122,8 @@
}, },
"taskProgress": { "taskProgress": {
"markAsDoneTitle": "将任务标记为完成?", "markAsDoneTitle": "将任务标记为完成?",
"confirmMarkAsDone": "是,标记为完成", "confirmMarkAsDone": "是,标记为完成",
"cancelMarkAsDone": ",保持当前状态", "cancelMarkAsDone": ",保持当前状态",
"markAsDoneDescription": "您已将进度设置为100%。您想将任务状态更新为\"完成\"吗?" "markAsDoneDescription": "您已将进度设置为 100%。您想将任务状态更新为\"完成\"吗?"
} }
} }

View File

@@ -1,6 +1,5 @@
import { useSocket } from '@/socket/socketContext'; import { useSocket } from '@/socket/socketContext';
import { ITaskPhase } from '@/types/tasks/taskPhase.types'; import { ITaskPhase } from '@/types/tasks/taskPhase.types';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { Select } from 'antd'; import { Select } from 'antd';
import { Form } from 'antd'; import { Form } from 'antd';
@@ -27,12 +26,6 @@ const TaskDrawerPhaseSelector = ({ phases, task }: TaskDrawerPhaseSelectorProps)
phase_id: value, phase_id: value,
parent_task: task.parent_task_id || null, parent_task: task.parent_task_id || null,
}); });
// socket?.once(SocketEvents.TASK_PHASE_CHANGE.toString(), () => {
// if(list.getCurrentGroup().value === this.list.GROUP_BY_PHASE_VALUE && this.list.isSubtasksIncluded) {
// this.list.emitRefreshSubtasksIncluded();
// }
// });
}; };
return ( return (
@@ -41,8 +34,11 @@ const TaskDrawerPhaseSelector = ({ phases, task }: TaskDrawerPhaseSelectorProps)
allowClear allowClear
placeholder="Select Phase" placeholder="Select Phase"
options={phaseMenuItems} options={phaseMenuItems}
style={{ width: 'fit-content' }} styles={{
dropdownStyle={{ width: 'fit-content' }} root: {
width: 'fit-content',
},
}}
onChange={handlePhaseChange} onChange={handlePhaseChange}
/> />
</Form.Item> </Form.Item>

View File

@@ -7,3 +7,28 @@
outline: 1px solid #d9d9d9; outline: 1px solid #d9d9d9;
border-radius: 4px; 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);
}

View File

@@ -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 React, { ChangeEvent, useEffect, useRef, useState } from 'react';
import { EllipsisOutlined } from '@ant-design/icons'; import { EllipsisOutlined } from '@ant-design/icons';
import { TFunction } from 'i18next'; 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 { deleteTask as deleteKanbanTask, updateEnhancedKanbanSubtask } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import useTabSearchParam from '@/hooks/useTabSearchParam'; import useTabSearchParam from '@/hooks/useTabSearchParam';
import { ITaskViewModel } from '@/types/tasks/task.types'; import { ITaskViewModel } from '@/types/tasks/task.types';
import TaskHierarchyBreadcrumb from '../task-hierarchy-breadcrumb/task-hierarchy-breadcrumb';
type TaskDrawerHeaderProps = { type TaskDrawerHeaderProps = {
inputRef: React.RefObject<InputRef | null>; inputRef: React.RefObject<InputRef | null>;
t: TFunction; 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 TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { socket, connected } = useSocket(); const { socket, connected } = useSocket();
@@ -38,6 +45,9 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
const [taskName, setTaskName] = useState<string>(taskFormViewModel?.task?.name ?? ''); const [taskName, setTaskName] = useState<string>(taskFormViewModel?.task?.name ?? '');
const currentSession = useAuthService().getCurrentSession(); 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(() => { useEffect(() => {
setTaskName(taskFormViewModel?.task?.name ?? ''); setTaskName(taskFormViewModel?.task?.name ?? '');
}, [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 // 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 ( return (
<Flex gap={12} align="center" style={{ marginBlockEnd: 6 }}> <div>
<Flex style={{ position: 'relative', width: '100%' }}> {/* Show breadcrumb for sub-tasks */}
{isEditing ? ( {isSubTask && <TaskHierarchyBreadcrumb t={t} />}
<Input
ref={inputRef} <Flex gap={8} align="center" style={{ marginBlockEnd: 2 }}>
size="large" <Flex style={{ position: 'relative', width: '100%', alignItems: 'center' }}>
value={taskName} {isEditing ? (
onChange={e => onTaskNameChange(e)} <Input
onBlur={handleInputBlur} ref={inputRef}
placeholder={t('taskHeader.taskNamePlaceholder')} size="large"
className="task-name-input" value={taskName}
style={{ onChange={e => onTaskNameChange(e)}
width: '100%', onBlur={handleInputBlur}
border: 'none', placeholder={t('taskHeader.taskNamePlaceholder')}
}} className="task-name-input"
showCount={true} style={{
maxLength={250} width: '100%',
autoFocus border: 'none',
/> }}
) : ( showCount={true}
<p maxLength={250}
onClick={() => setIsEditing(true)} autoFocus
style={{ />
margin: 0, ) : (
padding: '4px 11px', <Tooltip title={shouldShowTooltip ? displayTaskName : ''} trigger="hover">
fontSize: '16px', <p
cursor: 'pointer', onClick={() => setIsEditing(true)}
wordWrap: 'break-word', className="task-name-display"
overflowWrap: 'break-word', >
width: '100%', {truncatedTaskName}
}} </p>
> </Tooltip>
{taskName || t('taskHeader.taskNamePlaceholder')} )}
</p> </Flex>
)}
<TaskDrawerStatusDropdown
statuses={taskFormViewModel?.statuses ?? []}
task={taskFormViewModel?.task ?? ({} as ITaskViewModel)}
teamId={currentSession?.team_id ?? ''}
/>
<Dropdown overlayClassName={'delete-task-dropdown'} menu={{ items: deletTaskDropdownItems }}>
<Button type="text" icon={<EllipsisOutlined />} />
</Dropdown>
</Flex> </Flex>
</div>
<TaskDrawerStatusDropdown
statuses={taskFormViewModel?.statuses ?? []}
task={taskFormViewModel?.task ?? ({} as ITaskViewModel)}
teamId={currentSession?.team_id ?? ''}
/>
<Dropdown overlayClassName={'delete-task-dropdown'} menu={{ items: deletTaskDropdownItems }}>
<Button type="text" icon={<EllipsisOutlined />} />
</Dropdown>
</Flex>
); );
}; };

View File

@@ -3,7 +3,7 @@ import Drawer from 'antd/es/drawer';
import { InputRef } from 'antd/es/input'; import { InputRef } from 'antd/es/input';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useEffect, useRef, useState } from 'react'; 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 { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
@@ -13,6 +13,7 @@ import {
setTaskFormViewModel, setTaskFormViewModel,
setTaskSubscribers, setTaskSubscribers,
setTimeLogEditing, setTimeLogEditing,
fetchTask,
} from '@/features/task-drawer/task-drawer.slice'; } from '@/features/task-drawer/task-drawer.slice';
import './task-drawer.css'; import './task-drawer.css';
@@ -33,6 +34,7 @@ const TaskDrawer = () => {
const { showTaskDrawer, timeLogEditing } = useAppSelector(state => state.taskDrawerReducer); const { showTaskDrawer, timeLogEditing } = useAppSelector(state => state.taskDrawerReducer);
const { taskFormViewModel, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer); const { taskFormViewModel, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer);
const { projectId } = useAppSelector(state => state.projectReducer);
const taskNameInputRef = useRef<InputRef>(null); const taskNameInputRef = useRef<InputRef>(null);
const isClosingManually = useRef(false); const isClosingManually = useRef(false);
@@ -54,6 +56,17 @@ const TaskDrawer = () => {
dispatch(setTaskSubscribers([])); 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 = ( const handleOnClose = (
e?: React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element> e?: React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element>
) => { ) => {
@@ -68,10 +81,8 @@ const TaskDrawer = () => {
if (isClickOutsideDrawer || !taskFormViewModel?.task?.is_sub_task) { if (isClickOutsideDrawer || !taskFormViewModel?.task?.is_sub_task) {
resetTaskState(); resetTaskState();
} else { } else {
dispatch(setSelectedTaskId(null)); // For sub-tasks, navigate to parent instead of closing
dispatch(setTaskFormViewModel({})); handleBackToParent();
dispatch(setTaskSubscribers([]));
dispatch(setSelectedTaskId(taskFormViewModel?.task?.parent_task_id || null));
} }
// Reset the flag after a short delay // Reset the flag after a short delay
setTimeout(() => { 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 <ArrowLeftOutlined />;
}
return <CloseOutlined />;
};
const drawerProps = { const drawerProps = {
open: showTaskDrawer, open: showTaskDrawer,
onClose: handleOnClose, onClose: handleOnClose,
@@ -215,6 +237,7 @@ const TaskDrawer = () => {
footer: renderFooter(), footer: renderFooter(),
bodyStyle: getBodyStyle(), bodyStyle: getBodyStyle(),
footerStyle: getFooterStyle(), footerStyle: getFooterStyle(),
closeIcon: getCloseIcon(),
}; };
return ( return (

View File

@@ -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;
}

View File

@@ -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<TaskHierarchyBreadcrumbProps> = ({ 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<TaskHierarchyItem[]>([]);
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<TaskHierarchyItem[]> => {
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: (
<Tooltip title={shouldShowTooltip ? hierarchyTask.name : ''} trigger="hover">
<Button
type="link"
icon={index === 0 ? <HomeOutlined /> : undefined}
onClick={() => handleNavigateToTask(hierarchyTask.id)}
style={{
padding: 0,
height: 'auto',
color: themeMode === 'dark' ? '#1890ff' : '#1890ff',
fontSize: '14px',
marginRight: '0px',
maxWidth: '200px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'flex',
alignItems: 'center',
gap: index === 0 ? '6px' : '0px', // Add gap between icon and text for root task
}}
>
{truncatedName}
</Button>
</Tooltip>
),
};
}),
// 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 (
<Tooltip title={shouldShowCurrentTooltip ? currentTaskName : ''} trigger="hover">
<Typography.Text
className="current-task-name"
style={{
color: themeMode === 'dark' ? '#ffffffd9' : '#000000d9',
fontSize: '14px',
fontWeight: 500,
maxWidth: '200px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'inline-block',
}}
>
{truncatedCurrentName}
</Typography.Text>
</Tooltip>
);
})(),
},
];
return (
<div className="task-hierarchy-breadcrumb">
{loading ? (
<Typography.Text style={{ color: themeMode === 'dark' ? '#ffffffd9' : '#000000d9' }}>
{t('taskHeader.loadingHierarchy', 'Loading hierarchy...')}
</Typography.Text>
) : (
<Breadcrumb items={breadcrumbItems} />
)}
</div>
);
};
export default TaskHierarchyBreadcrumb;

View File

@@ -24,6 +24,7 @@ interface TaskRowProps {
isSubtask?: boolean; isSubtask?: boolean;
isFirstInGroup?: boolean; isFirstInGroup?: boolean;
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void; updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
depth?: number;
} }
const TaskRow: React.FC<TaskRowProps> = memo(({ const TaskRow: React.FC<TaskRowProps> = memo(({
@@ -32,7 +33,8 @@ const TaskRow: React.FC<TaskRowProps> = memo(({
visibleColumns, visibleColumns,
isSubtask = false, isSubtask = false,
isFirstInGroup = false, isFirstInGroup = false,
updateTaskCustomColumnValue updateTaskCustomColumnValue,
depth = 0
}) => { }) => {
// Get task data and selection state from Redux // Get task data and selection state from Redux
const task = useAppSelector(state => selectTaskById(state, taskId)); const task = useAppSelector(state => selectTaskById(state, taskId));
@@ -107,6 +109,7 @@ const TaskRow: React.FC<TaskRowProps> = memo(({
handleTaskNameEdit, handleTaskNameEdit,
attributes, attributes,
listeners, listeners,
depth,
}); });
// Memoize style object to prevent unnecessary re-renders // Memoize style object to prevent unnecessary re-renders

View File

@@ -22,6 +22,8 @@ interface TaskRowWithSubtasksProps {
}>; }>;
isFirstInGroup?: boolean; isFirstInGroup?: boolean;
updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void; updateTaskCustomColumnValue?: (taskId: string, columnKey: string, value: string) => void;
depth?: number; // Add depth prop to track nesting level
maxDepth?: number; // Add maxDepth prop to limit nesting
} }
interface AddSubtaskRowProps { interface AddSubtaskRowProps {
@@ -32,14 +34,15 @@ interface AddSubtaskRowProps {
width: string; width: string;
isSticky?: boolean; isSticky?: boolean;
}>; }>;
onSubtaskAdded: () => void; // Simplified - no rowId needed onSubtaskAdded: () => void;
rowId: string; // Unique identifier for this add subtask row rowId: string;
autoFocus?: boolean; // Whether this row should auto-focus on mount autoFocus?: boolean;
isActive?: boolean; // Whether this row should show the input/button isActive?: boolean;
onActivate?: () => void; // Simplified - no rowId needed onActivate?: () => void;
depth?: number; // Add depth prop for proper indentation
} }
const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
parentTaskId, parentTaskId,
projectId, projectId,
visibleColumns, visibleColumns,
@@ -47,25 +50,20 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
rowId, rowId,
autoFocus = false, autoFocus = false,
isActive = true, isActive = true,
onActivate onActivate,
depth = 0
}) => { }) => {
const [isAdding, setIsAdding] = useState(autoFocus); const { t } = useTranslation('task-list-table');
const [isAdding, setIsAdding] = useState(false);
const [subtaskName, setSubtaskName] = useState(''); const [subtaskName, setSubtaskName] = useState('');
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
const { socket, connected } = useSocket();
const { t } = useTranslation('task-list-table');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { socket, connected } = useSocket();
// Get session data for reporter_id and team_id
const currentSession = useAuthService().getCurrentSession(); const currentSession = useAuthService().getCurrentSession();
// Auto-focus when autoFocus prop is true
useEffect(() => { useEffect(() => {
if (autoFocus && inputRef.current) { if (autoFocus && inputRef.current) {
setIsAdding(true); inputRef.current.focus();
setTimeout(() => {
inputRef.current?.focus();
}, 100);
} }
}, [autoFocus]); }, [autoFocus]);
@@ -141,11 +139,15 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
return ( return (
<div className="flex items-center h-full" style={baseStyle}> <div className="flex items-center h-full" style={baseStyle}>
<div className="flex items-center w-full h-full"> <div className="flex items-center w-full h-full">
{/* Match subtask indentation pattern - tighter spacing */} {/* Match subtask indentation pattern - reduced spacing for level 1 */}
<div className="w-4" />
<div className="w-2" /> <div className="w-2" />
{/* Add additional indentation for deeper levels - increased spacing for level 2+ */}
{Array.from({ length: depth }).map((_, i) => (
<div key={i} className="w-6" />
))}
<div className="w-1" />
{isActive ? ( {isActive ? (
!isAdding ? ( !isAdding ? (
<button <button
onClick={() => { onClick={() => {
@@ -188,7 +190,7 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
default: default:
return <div style={baseStyle} />; return <div style={baseStyle} />;
} }
}, [isAdding, subtaskName, handleAddSubtask, handleCancel, handleBlur, handleKeyDown, t, isActive, onActivate]); }, [isAdding, subtaskName, handleAddSubtask, handleCancel, handleBlur, handleKeyDown, t, isActive, onActivate, depth]);
return ( return (
<div className="flex items-center min-w-max px-1 py-0.5 hover:bg-gray-50 dark:hover:bg-gray-800 min-h-[36px] border-b border-gray-200 dark:border-gray-700"> <div className="flex items-center min-w-max px-1 py-0.5 hover:bg-gray-50 dark:hover:bg-gray-800 min-h-[36px] border-b border-gray-200 dark:border-gray-700">
@@ -203,12 +205,42 @@ const AddSubtaskRow: React.FC<AddSubtaskRowProps> = memo(({
AddSubtaskRow.displayName = 'AddSubtaskRow'; AddSubtaskRow.displayName = 'AddSubtaskRow';
// Helper function to get background color based on depth
const getSubtaskBackgroundColor = (depth: number) => {
switch (depth) {
case 1:
return 'bg-gray-50 dark:bg-gray-800/50';
case 2:
return 'bg-blue-50 dark:bg-blue-900/20';
case 3:
return 'bg-green-50 dark:bg-green-900/20';
default:
return 'bg-gray-50 dark:bg-gray-800/50';
}
};
// Helper function to get border color based on depth
const getBorderColor = (depth: number) => {
switch (depth) {
case 1:
return 'border-blue-200 dark:border-blue-700';
case 2:
return 'border-green-200 dark:border-green-700';
case 3:
return 'border-purple-200 dark:border-purple-700';
default:
return 'border-blue-200 dark:border-blue-700';
}
};
const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
taskId, taskId,
projectId, projectId,
visibleColumns, visibleColumns,
isFirstInGroup = false, isFirstInGroup = false,
updateTaskCustomColumnValue updateTaskCustomColumnValue,
depth = 0,
maxDepth = 3
}) => { }) => {
const task = useAppSelector(state => selectTaskById(state, taskId)); const task = useAppSelector(state => selectTaskById(state, taskId));
const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId)); const isLoadingSubtasks = useAppSelector(state => selectSubtaskLoading(state, taskId));
@@ -223,6 +255,9 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
return null; return null;
} }
// Don't render subtasks if we've reached the maximum depth
const canHaveSubtasks = depth < maxDepth;
return ( return (
<> <>
{/* Main task row */} {/* Main task row */}
@@ -232,10 +267,12 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
visibleColumns={visibleColumns} visibleColumns={visibleColumns}
isFirstInGroup={isFirstInGroup} isFirstInGroup={isFirstInGroup}
updateTaskCustomColumnValue={updateTaskCustomColumnValue} updateTaskCustomColumnValue={updateTaskCustomColumnValue}
isSubtask={depth > 0}
depth={depth}
/> />
{/* Subtasks and add subtask row when expanded */} {/* Subtasks and add subtask row when expanded */}
{task.show_sub_tasks && ( {canHaveSubtasks && task.show_sub_tasks && (
<> <>
{/* Show loading skeleton while fetching subtasks */} {/* Show loading skeleton while fetching subtasks */}
{isLoadingSubtasks && ( {isLoadingSubtasks && (
@@ -244,22 +281,23 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
</> </>
)} )}
{/* Render existing subtasks when not loading */} {/* Render existing subtasks when not loading - RECURSIVELY */}
{!isLoadingSubtasks && task.sub_tasks?.map((subtask: Task) => ( {!isLoadingSubtasks && task.sub_tasks?.map((subtask: Task) => (
<div key={subtask.id} className="bg-gray-50 dark:bg-gray-800/50 border-l-2 border-blue-200 dark:border-blue-700"> <div key={subtask.id} className={`${getSubtaskBackgroundColor(depth + 1)} border-l-2 ${getBorderColor(depth + 1)}`}>
<TaskRow <TaskRowWithSubtasks
taskId={subtask.id} taskId={subtask.id}
projectId={projectId} projectId={projectId}
visibleColumns={visibleColumns} visibleColumns={visibleColumns}
isSubtask={true}
updateTaskCustomColumnValue={updateTaskCustomColumnValue} updateTaskCustomColumnValue={updateTaskCustomColumnValue}
depth={depth + 1}
maxDepth={maxDepth}
/> />
</div> </div>
))} ))}
{/* Add subtask row - only show when not loading */} {/* Add subtask row - only show when not loading */}
{!isLoadingSubtasks && ( {!isLoadingSubtasks && (
<div className="bg-gray-50 dark:bg-gray-800/50 border-l-2 border-blue-200 dark:border-blue-700"> <div className={`${getSubtaskBackgroundColor(depth + 1)} border-l-2 ${getBorderColor(depth + 1)}`}>
<AddSubtaskRow <AddSubtaskRow
parentTaskId={taskId} parentTaskId={taskId}
projectId={projectId} projectId={projectId}
@@ -267,8 +305,9 @@ const TaskRowWithSubtasks: React.FC<TaskRowWithSubtasksProps> = memo(({
onSubtaskAdded={handleSubtaskAdded} onSubtaskAdded={handleSubtaskAdded}
rowId={`add-subtask-${taskId}`} rowId={`add-subtask-${taskId}`}
autoFocus={false} autoFocus={false}
isActive={true} // Always show the add subtask row isActive={true}
onActivate={undefined} // Not needed anymore onActivate={undefined}
depth={depth + 1}
/> />
</div> </div>
)} )}

View File

@@ -24,6 +24,7 @@ interface TitleColumnProps {
onEditTaskName: (editing: boolean) => void; onEditTaskName: (editing: boolean) => void;
onTaskNameChange: (name: string) => void; onTaskNameChange: (name: string) => void;
onTaskNameSave: () => void; onTaskNameSave: () => void;
depth?: number;
} }
export const TitleColumn: React.FC<TitleColumnProps> = memo(({ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
@@ -36,7 +37,8 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
taskName, taskName,
onEditTaskName, onEditTaskName,
onTaskNameChange, onTaskNameChange,
onTaskNameSave onTaskNameSave,
depth = 0
}) => { }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { socket, connected } = useSocket(); const { socket, connected } = useSocket();
@@ -150,11 +152,16 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
/* Normal layout when not editing */ /* Normal layout when not editing */
<> <>
<div className="flex items-center flex-1 min-w-0"> <div className="flex items-center flex-1 min-w-0">
{/* Indentation for subtasks - tighter spacing */} {/* Indentation for subtasks - reduced spacing for level 1 */}
{isSubtask && <div className="w-4 flex-shrink-0" />} {isSubtask && <div className="w-2 flex-shrink-0" />}
{/* Expand/Collapse button - only show for parent tasks */} {/* Additional indentation for deeper levels - increased spacing for level 2+ */}
{!isSubtask && ( {Array.from({ length: depth }).map((_, i) => (
<div key={i} className="w-6 flex-shrink-0" />
))}
{/* Expand/Collapse button - show for any task that can have sub-tasks */}
{depth < 2 && ( // Only show if not at maximum depth (can still have children)
<button <button
onClick={handleToggleExpansion} onClick={handleToggleExpansion}
className={`flex h-4 w-4 items-center justify-center rounded-sm text-xs mr-1 hover:border hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:scale-110 transition-all duration-300 ease-out flex-shrink-0 ${ className={`flex h-4 w-4 items-center justify-center rounded-sm text-xs mr-1 hover:border hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:scale-110 transition-all duration-300 ease-out flex-shrink-0 ${
@@ -175,8 +182,8 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
</button> </button>
)} )}
{/* Additional indentation for subtasks after the expand button space */} {/* Additional indentation for subtasks after the expand button space - reduced for level 1 */}
{isSubtask && <div className="w-2 flex-shrink-0" />} {isSubtask && <div className="w-1 flex-shrink-0" />}
<div className="flex items-center gap-2 flex-1 min-w-0"> <div className="flex items-center gap-2 flex-1 min-w-0">
{/* Task name with dynamic width */} {/* Task name with dynamic width */}
@@ -202,8 +209,8 @@ export const TitleColumn: React.FC<TitleColumnProps> = memo(({
{/* Indicators container - flex-shrink-0 to prevent compression */} {/* Indicators container - flex-shrink-0 to prevent compression */}
<div className="flex items-center gap-1 flex-shrink-0"> <div className="flex items-center gap-1 flex-shrink-0">
{/* Subtask count indicator - only show if count > 0 */} {/* Subtask count indicator - show for any task that can have sub-tasks */}
{!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count > 0 && ( {depth < 2 && task.sub_tasks_count != null && task.sub_tasks_count > 0 && (
<Tooltip title={t(`indicators.tooltips.subtasks${task.sub_tasks_count === 1 ? '' : '_plural'}`, { count: task.sub_tasks_count })}> <Tooltip title={t(`indicators.tooltips.subtasks${task.sub_tasks_count === 1 ? '' : '_plural'}`, { count: task.sub_tasks_count })}>
<div className="flex items-center gap-1 px-1.5 py-0.5 bg-blue-50 dark:bg-blue-900/20 rounded text-xs"> <div className="flex items-center gap-1 px-1.5 py-0.5 bg-blue-50 dark:bg-blue-900/20 rounded text-xs">
<span className="text-blue-600 dark:text-blue-400 font-medium"> <span className="text-blue-600 dark:text-blue-400 font-medium">

View File

@@ -58,6 +58,9 @@ interface UseTaskRowColumnsProps {
// Drag and drop // Drag and drop
attributes: any; attributes: any;
listeners: any; listeners: any;
// Depth for nested subtasks
depth?: number;
} }
export const useTaskRowColumns = ({ export const useTaskRowColumns = ({
@@ -84,6 +87,7 @@ export const useTaskRowColumns = ({
handleTaskNameEdit, handleTaskNameEdit,
attributes, attributes,
listeners, listeners,
depth = 0,
}: UseTaskRowColumnsProps) => { }: UseTaskRowColumnsProps) => {
const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => { const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => {
@@ -128,6 +132,7 @@ export const useTaskRowColumns = ({
onEditTaskName={setEditTaskName} onEditTaskName={setEditTaskName}
onTaskNameChange={setTaskName} onTaskNameChange={setTaskName}
onTaskNameSave={handleTaskNameSave} onTaskNameSave={handleTaskNameSave}
depth={depth}
/> />
); );

View File

@@ -1029,7 +1029,7 @@ const taskManagementSlice = createSlice({
order: subtask.sort_order || subtask.order || 0, order: subtask.sort_order || subtask.order || 0,
parent_task_id: parentTaskId, parent_task_id: parentTaskId,
is_sub_task: true, is_sub_task: true,
sub_tasks_count: 0, sub_tasks_count: subtask.sub_tasks_count || 0, // Use actual count from backend
show_sub_tasks: false, show_sub_tasks: false,
})); }));