diff --git a/frontend/src/components/share/PublicReportDialog.jsx b/frontend/src/components/share/PublicReportDialog.jsx
index a5a7e8b..30d615f 100644
--- a/frontend/src/components/share/PublicReportDialog.jsx
+++ b/frontend/src/components/share/PublicReportDialog.jsx
@@ -49,6 +49,10 @@ export function PublicReportDialog({
const handleCreate = async (event) => {
event.preventDefault()
+ if (expiresAt && new Date(expiresAt) <= new Date()) {
+ toast.error(t("expiredDateError"))
+ return
+ }
try {
const payload = {
kind: defaultKind,
@@ -133,6 +137,7 @@ export function PublicReportDialog({
diff --git a/frontend/src/components/ui/context-panel.jsx b/frontend/src/components/ui/context-panel.jsx
index a2183c2..3252345 100644
--- a/frontend/src/components/ui/context-panel.jsx
+++ b/frontend/src/components/ui/context-panel.jsx
@@ -77,10 +77,12 @@ function ContextSection({ section }) {
function ContextRow({ row }) {
const Icon = row.icon
return (
-
- {Icon &&
}
-
{row.label}
-
{row.value}
+
+
+ {Icon && }
+
+
{row.label}
+
{row.value}
)
}
diff --git a/frontend/src/i18n/locales/ca/bugs.json b/frontend/src/i18n/locales/ca/bugs.json
index 1e41ef1..57d0ba8 100644
--- a/frontend/src/i18n/locales/ca/bugs.json
+++ b/frontend/src/i18n/locales/ca/bugs.json
@@ -24,7 +24,11 @@
"version": "Versió",
"assignee": "Assignat",
"tag": "Etiqueta",
- "any": "Qualsevol"
+ "any": "Qualsevol",
+ "sortLabel": "Sort by",
+ "sortNewest": "Newest first",
+ "sortSeverity": "Severity",
+ "sortCreated": "Creation date"
},
"severity": {
"critical": "Crítica",
@@ -48,7 +52,8 @@
"version": "Versió",
"environment": "Entorn",
"assignee": "Assignat",
- "externalUrl": "URL del tiquet extern"
+ "externalUrl": "URL del tiquet extern",
+ "notSet": "Not set"
},
"actions": {
"save": "Desa",
@@ -57,7 +62,8 @@
"changeStatus": "Canvia estat",
"addComment": "Afegeix comentari",
"edit": "Edita",
- "cancel": "Cancel·la"
+ "cancel": "Cancel·la",
+ "editTitle": "Click to edit title"
},
"tabs": {
"description": "Descripció",
@@ -166,7 +172,8 @@
"version": "Versió",
"externalTicket": "Tiquet extern",
"tags": "Etiquetes",
- "noTags": "Sense etiquetes"
+ "noTags": "Sense etiquetes",
+ "affectedVersion": "Affected version"
},
"links": {
"add": "Afegeix vincle",
@@ -205,11 +212,16 @@
"updateFailed": "No s'han pogut desar els canvis",
"commentFailed": "No s'ha pogut afegir el comentari",
"commentEditFailed": "No s'ha pogut desar el comentari",
- "commentDeleteFailed": "No s'ha pogut eliminar el comentari"
+ "commentDeleteFailed": "No s'ha pogut eliminar el comentari",
+ "severityChanged": "Severity updated",
+ "severityFailed": "Could not change severity"
},
"report": {
"dialogTitle": "Reporta bug",
"fromResult": "Des del resultat {{result}}",
"logHint": "L'extracte del log s'adjunta automàticament a la descripció."
+ },
+ "list": {
+ "updated": "updated"
}
}
diff --git a/frontend/src/i18n/locales/ca/publicReports.json b/frontend/src/i18n/locales/ca/publicReports.json
index 9667345..4668a03 100644
--- a/frontend/src/i18n/locales/ca/publicReports.json
+++ b/frontend/src/i18n/locales/ca/publicReports.json
@@ -30,5 +30,6 @@
"revoked": "Enllaç revocat",
"revokeFailed": "No s'ha pogut revocar l'enllaç",
"created": "Enllaç públic generat",
- "createFailed": "No s'ha pogut crear l'enllaç"
+ "createFailed": "No s'ha pogut crear l'enllaç",
+ "expiredDateError": "Expiration date must be in the future"
}
diff --git a/frontend/src/i18n/locales/en/bugs.json b/frontend/src/i18n/locales/en/bugs.json
index e0ae1db..31df574 100644
--- a/frontend/src/i18n/locales/en/bugs.json
+++ b/frontend/src/i18n/locales/en/bugs.json
@@ -36,7 +36,11 @@
"versionLabel": "Filter by version",
"anyStatus": "Any status",
"anySeverity": "Any severity",
- "anyVersion": "Any version"
+ "anyVersion": "Any version",
+ "sortLabel": "Sort by",
+ "sortNewest": "Newest first",
+ "sortSeverity": "Severity",
+ "sortCreated": "Creation date"
},
"severity": {
"critical": "Critical",
@@ -66,7 +70,8 @@
"fixedInVersionNone": "Not set",
"affectedVersion": "Affected version",
"affectedVersionPlaceholder": "Pick a version…",
- "affectedVersionNone": "Not set"
+ "affectedVersionNone": "Not set",
+ "notSet": "Not set"
},
"actions": {
"save": "Save",
@@ -75,7 +80,8 @@
"changeStatus": "Change status",
"addComment": "Add comment",
"edit": "Edit",
- "cancel": "Cancel"
+ "cancel": "Cancel",
+ "editTitle": "Click to edit title"
},
"tabs": {
"description": "Description",
@@ -188,7 +194,8 @@
"version": "Version",
"externalTicket": "External ticket",
"tags": "Tags",
- "noTags": "No tags"
+ "noTags": "No tags",
+ "affectedVersion": "Affected version"
},
"links": {
"add": "Add link",
@@ -227,11 +234,16 @@
"updateFailed": "Could not save changes",
"commentFailed": "Could not add comment",
"commentEditFailed": "Could not save comment",
- "commentDeleteFailed": "Could not delete comment"
+ "commentDeleteFailed": "Could not delete comment",
+ "severityChanged": "Severity updated",
+ "severityFailed": "Could not change severity"
},
"report": {
"dialogTitle": "Report bug",
"fromResult": "From result {{result}}",
"logHint": "Log excerpt attached to the description automatically."
+ },
+ "list": {
+ "updated": "updated"
}
}
diff --git a/frontend/src/i18n/locales/en/publicReports.json b/frontend/src/i18n/locales/en/publicReports.json
index 00a4cb6..1c49eb7 100644
--- a/frontend/src/i18n/locales/en/publicReports.json
+++ b/frontend/src/i18n/locales/en/publicReports.json
@@ -30,5 +30,6 @@
"revoked": "Link revoked",
"revokeFailed": "Could not revoke link",
"created": "Public link generated",
- "createFailed": "Could not create link"
+ "createFailed": "Could not create link",
+ "expiredDateError": "Expiration date must be in the future"
}
diff --git a/frontend/src/i18n/locales/es/bugs.json b/frontend/src/i18n/locales/es/bugs.json
index 001472c..05d617e 100644
--- a/frontend/src/i18n/locales/es/bugs.json
+++ b/frontend/src/i18n/locales/es/bugs.json
@@ -36,7 +36,11 @@
"versionLabel": "Filtrar por versión",
"anyStatus": "Cualquier estado",
"anySeverity": "Cualquier severidad",
- "anyVersion": "Cualquier versión"
+ "anyVersion": "Cualquier versión",
+ "sortLabel": "Ordenar por",
+ "sortNewest": "Más recientes",
+ "sortSeverity": "Severidad",
+ "sortCreated": "Fecha de creación"
},
"severity": {
"critical": "Crítica",
@@ -66,7 +70,8 @@
"fixedInVersionNone": "Sin establecer",
"affectedVersion": "Versión afectada",
"affectedVersionPlaceholder": "Elige una versión…",
- "affectedVersionNone": "Sin establecer"
+ "affectedVersionNone": "Sin establecer",
+ "notSet": "Sin asignar"
},
"actions": {
"save": "Guardar",
@@ -75,7 +80,8 @@
"changeStatus": "Cambiar estado",
"addComment": "Añadir comentario",
"edit": "Editar",
- "cancel": "Cancelar"
+ "cancel": "Cancelar",
+ "editTitle": "Clic para editar el título"
},
"tabs": {
"description": "Descripción",
@@ -188,7 +194,8 @@
"version": "Versión",
"externalTicket": "Ticket externo",
"tags": "Etiquetas",
- "noTags": "Sin etiquetas"
+ "noTags": "Sin etiquetas",
+ "affectedVersion": "Versión afectada"
},
"links": {
"add": "Añadir vínculo",
@@ -227,11 +234,16 @@
"updateFailed": "No se pudieron guardar los cambios",
"commentFailed": "No se pudo añadir el comentario",
"commentEditFailed": "No se pudo guardar el comentario",
- "commentDeleteFailed": "No se pudo eliminar el comentario"
+ "commentDeleteFailed": "No se pudo eliminar el comentario",
+ "severityChanged": "Severidad actualizada",
+ "severityFailed": "No se pudo cambiar la severidad"
},
"report": {
"dialogTitle": "Reportar bug",
"fromResult": "Desde el resultado {{result}}",
"logHint": "El extracto de log se adjunta automáticamente a la descripción."
+ },
+ "list": {
+ "updated": "actualizado"
}
}
diff --git a/frontend/src/i18n/locales/es/publicReports.json b/frontend/src/i18n/locales/es/publicReports.json
index c62a7a8..34f8a08 100644
--- a/frontend/src/i18n/locales/es/publicReports.json
+++ b/frontend/src/i18n/locales/es/publicReports.json
@@ -30,5 +30,6 @@
"revoked": "Enlace revocado",
"revokeFailed": "No se pudo revocar el enlace",
"created": "Enlace público generado",
- "createFailed": "No se pudo crear el enlace"
+ "createFailed": "No se pudo crear el enlace",
+ "expiredDateError": "La fecha de expiración debe ser futura"
}
diff --git a/frontend/src/i18n/locales/eu/bugs.json b/frontend/src/i18n/locales/eu/bugs.json
index 901e227..b385c6e 100644
--- a/frontend/src/i18n/locales/eu/bugs.json
+++ b/frontend/src/i18n/locales/eu/bugs.json
@@ -24,7 +24,11 @@
"version": "Bertsioa",
"assignee": "Esleitua",
"tag": "Etiketa",
- "any": "Edozein"
+ "any": "Edozein",
+ "sortLabel": "Sort by",
+ "sortNewest": "Newest first",
+ "sortSeverity": "Severity",
+ "sortCreated": "Creation date"
},
"severity": {
"critical": "Kritikoa",
@@ -48,7 +52,8 @@
"version": "Bertsioa",
"environment": "Ingurunea",
"assignee": "Esleitua",
- "externalUrl": "Kanpoko txartelaren URLa"
+ "externalUrl": "Kanpoko txartelaren URLa",
+ "notSet": "Not set"
},
"actions": {
"save": "Gorde",
@@ -57,7 +62,8 @@
"changeStatus": "Aldatu egoera",
"addComment": "Gehitu iruzkina",
"edit": "Editatu",
- "cancel": "Utzi"
+ "cancel": "Utzi",
+ "editTitle": "Click to edit title"
},
"tabs": {
"description": "Deskribapena",
@@ -166,7 +172,8 @@
"version": "Bertsioa",
"externalTicket": "Kanpoko txartela",
"tags": "Etiketak",
- "noTags": "Etiketarik ez"
+ "noTags": "Etiketarik ez",
+ "affectedVersion": "Affected version"
},
"links": {
"add": "Gehitu lotura",
@@ -205,11 +212,16 @@
"updateFailed": "Ezin izan dira aldaketak gorde",
"commentFailed": "Ezin izan da iruzkina gehitu",
"commentEditFailed": "Ezin izan da iruzkina gorde",
- "commentDeleteFailed": "Ezin izan da iruzkina ezabatu"
+ "commentDeleteFailed": "Ezin izan da iruzkina ezabatu",
+ "severityChanged": "Severity updated",
+ "severityFailed": "Could not change severity"
},
"report": {
"dialogTitle": "Eman akatsaren berri",
"fromResult": "Emaitzatik {{result}}",
"logHint": "Logaren laburpena automatikoki gehitzen zaio deskribapenari."
+ },
+ "list": {
+ "updated": "updated"
}
}
diff --git a/frontend/src/i18n/locales/eu/publicReports.json b/frontend/src/i18n/locales/eu/publicReports.json
index 3baff39..3f10cff 100644
--- a/frontend/src/i18n/locales/eu/publicReports.json
+++ b/frontend/src/i18n/locales/eu/publicReports.json
@@ -30,5 +30,6 @@
"revoked": "Esteka baliogabetuta",
"revokeFailed": "Ezin izan da esteka baliogabetu",
"created": "Esteka publikoa sortu da",
- "createFailed": "Ezin izan da esteka sortu"
+ "createFailed": "Ezin izan da esteka sortu",
+ "expiredDateError": "Expiration date must be in the future"
}
diff --git a/frontend/src/i18n/locales/gl/bugs.json b/frontend/src/i18n/locales/gl/bugs.json
index 130df2d..9e39784 100644
--- a/frontend/src/i18n/locales/gl/bugs.json
+++ b/frontend/src/i18n/locales/gl/bugs.json
@@ -24,7 +24,11 @@
"version": "Versión",
"assignee": "Asignado",
"tag": "Etiqueta",
- "any": "Calquera"
+ "any": "Calquera",
+ "sortLabel": "Sort by",
+ "sortNewest": "Newest first",
+ "sortSeverity": "Severity",
+ "sortCreated": "Creation date"
},
"severity": {
"critical": "Crítica",
@@ -48,7 +52,8 @@
"version": "Versión",
"environment": "Contorno",
"assignee": "Asignado",
- "externalUrl": "URL do tícket externo"
+ "externalUrl": "URL do tícket externo",
+ "notSet": "Not set"
},
"actions": {
"save": "Gardar",
@@ -57,7 +62,8 @@
"changeStatus": "Cambiar estado",
"addComment": "Engadir comentario",
"edit": "Editar",
- "cancel": "Cancelar"
+ "cancel": "Cancelar",
+ "editTitle": "Click to edit title"
},
"tabs": {
"description": "Descrición",
@@ -166,7 +172,8 @@
"version": "Versión",
"externalTicket": "Tícket externo",
"tags": "Etiquetas",
- "noTags": "Sen etiquetas"
+ "noTags": "Sen etiquetas",
+ "affectedVersion": "Affected version"
},
"links": {
"add": "Engadir vínculo",
@@ -205,11 +212,16 @@
"updateFailed": "Non se puideron gardar os cambios",
"commentFailed": "Non se puido engadir o comentario",
"commentEditFailed": "Non se puido gardar o comentario",
- "commentDeleteFailed": "Non se puido eliminar o comentario"
+ "commentDeleteFailed": "Non se puido eliminar o comentario",
+ "severityChanged": "Severity updated",
+ "severityFailed": "Could not change severity"
},
"report": {
"dialogTitle": "Reportar bug",
"fromResult": "Dende o resultado {{result}}",
"logHint": "O extracto do log adxúntase automaticamente á descrición."
+ },
+ "list": {
+ "updated": "updated"
}
}
diff --git a/frontend/src/i18n/locales/gl/publicReports.json b/frontend/src/i18n/locales/gl/publicReports.json
index c8ec1e8..6b796e1 100644
--- a/frontend/src/i18n/locales/gl/publicReports.json
+++ b/frontend/src/i18n/locales/gl/publicReports.json
@@ -30,5 +30,6 @@
"revoked": "Ligazón revogada",
"revokeFailed": "Non se puido revogar a ligazón",
"created": "Ligazón pública xerada",
- "createFailed": "Non se puido crear a ligazón"
+ "createFailed": "Non se puido crear a ligazón",
+ "expiredDateError": "Expiration date must be in the future"
}
diff --git a/frontend/src/lib/bugConfig.js b/frontend/src/lib/bugConfig.js
index b0aaa24..ef8ca65 100644
--- a/frontend/src/lib/bugConfig.js
+++ b/frontend/src/lib/bugConfig.js
@@ -1,3 +1,5 @@
+export const SEVERITIES = ["critical", "high", "medium", "low"]
+
export const SEVERITY_VARIANT = {
critical: "destructive",
high: "warning",
diff --git a/frontend/src/pages/BugDetailPage.jsx b/frontend/src/pages/BugDetailPage.jsx
index 2148bb6..aa5d7c7 100644
--- a/frontend/src/pages/BugDetailPage.jsx
+++ b/frontend/src/pages/BugDetailPage.jsx
@@ -59,7 +59,7 @@ import { BugTimeline } from "../components/bug/BugTimeline"
import { BugExternalLinksPanel } from "../components/integration/BugExternalLinksPanel"
import { CustomFieldsEditableBody } from "../components/customField/CustomFieldInput"
import { MdEditor, MdViewer } from "../components/MdEditor"
-import { SEVERITY_VARIANT, STATUS_VARIANT, STATUSES } from "../lib/bugConfig"
+import { SEVERITIES, SEVERITY_VARIANT, STATUS_VARIANT, STATUSES } from "../lib/bugConfig"
export function BugDetailPage() {
const { t } = useTranslation(["bugs", "nav", "customFields"])
@@ -124,6 +124,8 @@ export function BugDetailPage() {
const [editingBody, setEditingBody] = useState("")
const [editingDescription, setEditingDescription] = useState(false)
const [descriptionDraft, setDescriptionDraft] = useState("")
+ const [editingTitle, setEditingTitle] = useState(false)
+ const [titleDraft, setTitleDraft] = useState("")
const [deleteBugOpen, setDeleteBugOpen] = useState(false)
const [commentPendingDelete, setCommentPendingDelete] = useState(null)
const statusLabels = useMemo(
@@ -132,10 +134,23 @@ export function BugDetailPage() {
)
const historyCount = activity.length + comments.length
+ const affectedVersionPicker = bug ? (
+
+ updateBug.mutateAsync({ id: bug.id, data: { version_id: versionId } })
+ }
+ saving={updateBug.isPending}
+ />
+ ) : null
+
const fixedInVersionPicker = bug ? (
-
updateBug.mutateAsync({ id: bug.id, data: { fixed_in_version_id: versionId } })
}
@@ -158,14 +173,37 @@ export function BugDetailPage() {
),
} : null
+ const handleStatusChange = async (status) => {
+ if (!bug || status === bug.status) return
+ try {
+ await changeStatus.mutateAsync({ id: bug.id, status })
+ toast.success(t("toast.statusChanged"))
+ } catch (error) {
+ toast.error(error?.response?.data?.detail ?? t("toast.statusFailed"))
+ }
+ }
+
+ const handleSeverityChange = async (severity) => {
+ if (!bug || severity === bug.severity) return
+ try {
+ await updateBug.mutateAsync({ id: bug.id, data: { severity } })
+ toast.success(t("toast.severityChanged"))
+ } catch (error) {
+ toast.error(error?.response?.data?.detail ?? t("toast.severityFailed"))
+ }
+ }
+
const contextSections = useMemo(() => {
if (!bug) return []
return buildContextSections({
bug,
links,
linkContext,
+ affectedVersionPicker,
fixedInVersionPicker,
customFieldsSection,
+ onSeverityChange: handleSeverityChange,
+ onStatusChange: handleStatusChange,
onDeleteLink: canWrite
? (linkId) => removeLink.mutateAsync({ bugId: bug.id, linkId })
: null,
@@ -185,7 +223,7 @@ export function BugDetailPage() {
) : null,
t,
})
- }, [bug, links, linkContext, removeLink, fixedInVersionPicker, customFieldsSection, canWrite, t])
+ }, [bug, links, linkContext, removeLink, affectedVersionPicker, fixedInVersionPicker, customFieldsSection, canWrite, t])
if (isLoading || !bug) {
return (
@@ -198,14 +236,19 @@ export function BugDetailPage() {
)
}
- const handleStatusChange = async (status) => {
- if (status === bug.status) return
+ const handleTitleSave = async () => {
+ const trimmed = titleDraft.trim()
+ if (!trimmed || trimmed === bug.title) {
+ setEditingTitle(false)
+ return
+ }
try {
- await changeStatus.mutateAsync({ id: bug.id, status })
- toast.success(t("toast.statusChanged"))
+ await updateBug.mutateAsync({ id: bug.id, data: { title: trimmed } })
+ toast.success(t("toast.updated"))
} catch (error) {
- toast.error(error?.response?.data?.detail ?? t("toast.statusFailed"))
+ toast.error(error?.response?.data?.detail ?? t("toast.updateFailed"))
}
+ setEditingTitle(false)
}
const handleDelete = async () => {
@@ -296,16 +339,30 @@ export function BugDetailPage() {
#{bug.number}
- {bug.title}
+ {editingTitle ? (
+ setTitleDraft(event.target.value)}
+ onBlur={handleTitleSave}
+ onKeyDown={event => {
+ if (event.key === "Enter") handleTitleSave()
+ if (event.key === "Escape") setEditingTitle(false)
+ }}
+ />
+ ) : (
+ { setTitleDraft(bug.title); setEditingTitle(true) }}
+ title={t("actions.editTitle")}
+ >
+ {bug.title}
+
+ )}
-
- {t(`severity.${bug.severity}`)}
-
-
- {t(`statuses.${bug.status}`)}
-
{bug.environment && (
)}
@@ -346,18 +403,6 @@ export function BugDetailPage() {
{isWatching ? t("watch.watching") : t("watch.watch")}
{bug.watcher_count ?? watchers.length}
-
{canDelete && (