feat(tickets): Markdown in Texten + Editier-Regeln für Rückfragen
- Ticket-Texte (Frage/Antwort/Nachricht/Changelog/Verlauf) rendern eine Markdown-Teilmenge: **fett**, `code`, Aufzählungen, Links, Tier-/Ticket-IDs - Offene Rückfrage (NeedsInfo): nicht bearbeitbar — die Züchterin soll antworten - Beantwortet (Answered): Antwort nachträglich änderbar, bevor die KI übernimmt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -433,7 +433,8 @@ export function seedDb(): MockDb {
|
|||||||
createdAt: '2026-06-14T08:00:00Z',
|
createdAt: '2026-06-14T08:00:00Z',
|
||||||
status: 'NeedsInfo',
|
status: 'NeedsInfo',
|
||||||
resolvedAt: null,
|
resolvedAt: null,
|
||||||
question: 'Welches Geburtsdatum ist korrekt — der 1. oder der 2. Juni?',
|
question:
|
||||||
|
'Welches Geburtsdatum ist korrekt — der **1.** oder der **2. Juni**?\n\nBitte wähle:\n- 1. Juni\n- 2. Juni',
|
||||||
answer: null,
|
answer: null,
|
||||||
answeredAt: null,
|
answeredAt: null,
|
||||||
fixNote: null,
|
fixNote: null,
|
||||||
|
|||||||
@@ -158,6 +158,21 @@ test.describe('Meine Tickets', () => {
|
|||||||
await expect(openFilter).toHaveClass(/tickets-filter--active/)
|
await expect(openFilter).toHaveClass(/tickets-filter--active/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Rückfrage rendert Markdown und ist (offen) nicht bearbeitbar', async ({ page }) => {
|
||||||
|
await page.goto('/hilfe/tickets')
|
||||||
|
await selectView(page, tt.filters.dialog)
|
||||||
|
const card = page.locator('.ticket-card').filter({ hasText: 'Wurf B' })
|
||||||
|
await expect(card).toBeVisible()
|
||||||
|
|
||||||
|
// Markdown: **fett** → <strong>, „- "-Aufzählung → <li>.
|
||||||
|
await expect(card.locator('.ticket-card__question-text strong').first()).toBeVisible()
|
||||||
|
await expect(card.locator('.ticket-card__question-text li')).toHaveCount(2)
|
||||||
|
|
||||||
|
// Offene Rückfrage (NeedsInfo): kein „Bearbeiten"-Button, dafür ein Antwort-Feld.
|
||||||
|
await expect(card.getByRole('button', { name: tt.edit })).toHaveCount(0)
|
||||||
|
await expect(card.locator('.ticket-card__answer-form textarea')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
test('Ticket löschen', async ({ page }) => {
|
test('Ticket löschen', async ({ page }) => {
|
||||||
await page.goto('/hilfe/tickets')
|
await page.goto('/hilfe/tickets')
|
||||||
await selectView(page, tt.filters.closed)
|
await selectView(page, tt.filters.closed)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* Rückfragen kommen hierher zurück), „Geschlossen" (Resolved, zeigt den Changelog/fixNote).
|
* Rückfragen kommen hierher zurück), „Geschlossen" (Resolved, zeigt den Changelog/fixNote).
|
||||||
* Der Frage/Antwort-Verlauf (thread) wird angezeigt; das interne `agentContext` NIEMALS.
|
* Der Frage/Antwort-Verlauf (thread) wird angezeigt; das interne `agentContext` NIEMALS.
|
||||||
*/
|
*/
|
||||||
import { createContext, Fragment, useContext, useEffect, useMemo, useState } from 'react'
|
import { createContext, Fragment, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||||
import { Link, useSearchParams } from 'react-router-dom'
|
import { Link, useSearchParams } from 'react-router-dom'
|
||||||
import { de } from '../strings/de'
|
import { de } from '../strings/de'
|
||||||
import { ApiError } from '../api/client'
|
import { ApiError } from '../api/client'
|
||||||
@@ -57,43 +57,38 @@ function entityHref(ticket: FeedbackTicket): string | null {
|
|||||||
|
|
||||||
const GUID_RE = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
|
const GUID_RE = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
|
||||||
|
|
||||||
type RichToken = { kind: 'text'; value: string } | { kind: 'link'; href: string; label: string }
|
type RichBlock = { type: 'p'; text: string } | { type: 'ul'; items: string[] }
|
||||||
|
|
||||||
/**
|
/** Block-Ebene: aufeinanderfolgende Aufzählungszeilen (-, *, •) zu einer Liste gruppieren. */
|
||||||
* Zerlegt Ticket-Text in Text- und Link-Token. Erkannt werden:
|
function parseBlocks(text: string): RichBlock[] {
|
||||||
* - Markdown-Links `[Label](/route)` → interner/externer Link,
|
const blocks: RichBlock[] = []
|
||||||
* - nackte GUIDs: gehört sie zu einem bekannten Ticket → Link auf dieses Ticket; sonst, wenn
|
let para: string | null = null
|
||||||
* sie einen Tier-Namen auflöst → Name + Link zur Rennmausakte (so kann die KI einfach die
|
let list: string[] | null = null
|
||||||
* Tier-ID in den Text schreiben). Unauflösbare GUIDs bleiben Text.
|
const flushPara = () => {
|
||||||
*/
|
if (para !== null) {
|
||||||
function tokenizeRich(
|
blocks.push({ type: 'p', text: para })
|
||||||
text: string,
|
para = null
|
||||||
ticketIds: Set<string>,
|
|
||||||
gerbilNames: Record<string, string | null>,
|
|
||||||
): RichToken[] {
|
|
||||||
const tokens: RichToken[] = []
|
|
||||||
const re = new RegExp(`\\[([^\\]]+)\\]\\(([^)]+)\\)|(${GUID_RE})`, 'g')
|
|
||||||
let last = 0
|
|
||||||
let m: RegExpExecArray | null
|
|
||||||
while ((m = re.exec(text)) !== null) {
|
|
||||||
if (m.index > last) tokens.push({ kind: 'text', value: text.slice(last, m.index) })
|
|
||||||
if (m[1] !== undefined && m[2] !== undefined) {
|
|
||||||
tokens.push({ kind: 'link', href: m[2], label: m[1] })
|
|
||||||
} else {
|
|
||||||
const guid = m[3]
|
|
||||||
const gerbilName = gerbilNames[guid]
|
|
||||||
if (ticketIds.has(guid)) {
|
|
||||||
tokens.push({ kind: 'link', href: `/hilfe/tickets?focus=${guid}`, label: t.ticketRefLabel })
|
|
||||||
} else if (gerbilName) {
|
|
||||||
tokens.push({ kind: 'link', href: `/rennmaeuse/${guid}`, label: gerbilName })
|
|
||||||
} else {
|
|
||||||
tokens.push({ kind: 'text', value: guid })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
last = re.lastIndex
|
|
||||||
}
|
}
|
||||||
if (last < text.length) tokens.push({ kind: 'text', value: text.slice(last) })
|
const flushList = () => {
|
||||||
return tokens
|
if (list) {
|
||||||
|
blocks.push({ type: 'ul', items: list })
|
||||||
|
list = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
const bullet = line.match(/^\s*[-*•]\s+(.*)$/)
|
||||||
|
if (bullet) {
|
||||||
|
flushPara()
|
||||||
|
;(list ??= []).push(bullet[1])
|
||||||
|
} else {
|
||||||
|
flushList()
|
||||||
|
para = para === null ? line : `${para}\n${line}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushPara()
|
||||||
|
flushList()
|
||||||
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Alle nackten GUIDs in einem Text, die KEINE Markdown-Link-Ziele sind (mögliche Tier-IDs). */
|
/** Alle nackten GUIDs in einem Text, die KEINE Markdown-Link-Ziele sind (mögliche Tier-IDs). */
|
||||||
@@ -117,28 +112,81 @@ function TextWithBreaks({ value }: { value: string }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rendert Ticket-Text mit klickbaren Tier-/Ticket-Links und Zeilenumbrüchen. */
|
/** Inline-Markdown: **fett**, `code`, [Label](url)-Links, Tier-/Ticket-IDs, Zeilenumbrüche. */
|
||||||
function RichText({ text }: { text: string }) {
|
function InlineMarkdown({ text }: { text: string }) {
|
||||||
const { ticketIds, gerbilNames } = useContext(TicketRefsContext)
|
const { ticketIds, gerbilNames } = useContext(TicketRefsContext)
|
||||||
const tokens = useMemo(
|
const re = new RegExp(
|
||||||
() => tokenizeRich(text, ticketIds, gerbilNames),
|
`\\[([^\\]]+)\\]\\(([^)]+)\\)|\\*\\*([^*]+)\\*\\*|\`([^\`]+)\`|(${GUID_RE})`,
|
||||||
[text, ticketIds, gerbilNames],
|
'g',
|
||||||
)
|
)
|
||||||
|
const nodes: ReactNode[] = []
|
||||||
|
let last = 0
|
||||||
|
let m: RegExpExecArray | null
|
||||||
|
let k = 0
|
||||||
|
while ((m = re.exec(text)) !== null) {
|
||||||
|
if (m.index > last) nodes.push(<TextWithBreaks key={k++} value={text.slice(last, m.index)} />)
|
||||||
|
if (m[1] !== undefined && m[2] !== undefined) {
|
||||||
|
const href = m[2]
|
||||||
|
nodes.push(
|
||||||
|
href.startsWith('/') ? (
|
||||||
|
<Link key={k++} to={href} className="ticket-link">
|
||||||
|
{m[1]}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<a key={k++} href={href} className="ticket-link" target="_blank" rel="noreferrer">
|
||||||
|
{m[1]}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else if (m[3] !== undefined) {
|
||||||
|
nodes.push(<strong key={k++}>{m[3]}</strong>)
|
||||||
|
} else if (m[4] !== undefined) {
|
||||||
|
nodes.push(
|
||||||
|
<code key={k++} className="ticket-code">
|
||||||
|
{m[4]}
|
||||||
|
</code>,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const guid = m[5]
|
||||||
|
const gn = gerbilNames[guid]
|
||||||
|
if (ticketIds.has(guid)) {
|
||||||
|
nodes.push(
|
||||||
|
<Link key={k++} to={`/hilfe/tickets?focus=${guid}`} className="ticket-link">
|
||||||
|
{t.ticketRefLabel}
|
||||||
|
</Link>,
|
||||||
|
)
|
||||||
|
} else if (gn) {
|
||||||
|
nodes.push(
|
||||||
|
<Link key={k++} to={`/rennmaeuse/${guid}`} className="ticket-link">
|
||||||
|
{gn}
|
||||||
|
</Link>,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
nodes.push(<TextWithBreaks key={k++} value={guid} />)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last = re.lastIndex
|
||||||
|
}
|
||||||
|
if (last < text.length) nodes.push(<TextWithBreaks key={k} value={text.slice(last)} />)
|
||||||
|
return <>{nodes}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rendert Ticket-Text als Markdown-Teilmenge (Absätze, Aufzählungen, Inline-Formatierung). */
|
||||||
|
function RichText({ text }: { text: string }) {
|
||||||
|
const blocks = useMemo(() => parseBlocks(text), [text])
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{tokens.map((tok, i) =>
|
{blocks.map((b, i) =>
|
||||||
tok.kind === 'link' ? (
|
b.type === 'ul' ? (
|
||||||
tok.href.startsWith('/') ? (
|
<ul key={i} className="ticket-md-list">
|
||||||
<Link key={i} to={tok.href} className="ticket-link">
|
{b.items.map((it, j) => (
|
||||||
{tok.label}
|
<li key={j}>
|
||||||
</Link>
|
<InlineMarkdown text={it} />
|
||||||
) : (
|
</li>
|
||||||
<a key={i} href={tok.href} className="ticket-link" target="_blank" rel="noreferrer">
|
))}
|
||||||
{tok.label}
|
</ul>
|
||||||
</a>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<TextWithBreaks key={i} value={tok.value} />
|
<InlineMarkdown key={i} text={b.text} />
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -408,9 +456,26 @@ function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete
|
|||||||
const entityLink = entityHref(ticket)
|
const entityLink = entityHref(ticket)
|
||||||
const resolved = ticket.status === 'Resolved'
|
const resolved = ticket.status === 'Resolved'
|
||||||
const badge = statusBadge(ticket.status)
|
const badge = statusBadge(ticket.status)
|
||||||
// Die Züchterin darf antworten, solange eine Rückfrage offen ist und noch keine
|
const [answerEditing, setAnswerEditing] = useState(false)
|
||||||
// Antwort vorliegt (NeedsInfo, oder eine Rückfrage ohne Antwort).
|
// Erstantwort: Rückfrage offen, noch keine Antwort.
|
||||||
const canAnswer = !ticket.answer && (ticket.status === 'NeedsInfo' || (!!ticket.question && !resolved))
|
const canAnswerNew = !ticket.answer && (ticket.status === 'NeedsInfo' || (!!ticket.question && !resolved))
|
||||||
|
// Nachträglich ändern: bereits beantwortet (Answered) — die Züchterin darf ihre letzte
|
||||||
|
// Info noch korrigieren, BEVOR die KI das Ticket bearbeitet.
|
||||||
|
const canAmend = !!ticket.answer && ticket.status === 'Answered'
|
||||||
|
// Antwort-Formular zeigen: neue Antwort, aktives Ändern, oder ein gepufferter Änderungs-Entwurf.
|
||||||
|
const showAnswerForm = canAnswerNew || answerEditing || (canAmend && !!answerDraft)
|
||||||
|
// Bearbeiten (Beschreibung) ist gesperrt, solange eine Rückfrage offen ist (NeedsInfo):
|
||||||
|
// dann soll die Züchterin ANTWORTEN, nicht den Text ändern.
|
||||||
|
const canEditMessage = ticket.status !== 'NeedsInfo'
|
||||||
|
|
||||||
|
function startAmend() {
|
||||||
|
if (!answerDraft) setAnswerDraft(ticket.answer ?? '')
|
||||||
|
setAnswerEditing(true)
|
||||||
|
}
|
||||||
|
function cancelAmend() {
|
||||||
|
setAnswerEditing(false)
|
||||||
|
setAnswerDraft('')
|
||||||
|
}
|
||||||
|
|
||||||
function startEdit() {
|
function startEdit() {
|
||||||
setDraft(ticket.message)
|
setDraft(ticket.message)
|
||||||
@@ -444,6 +509,7 @@ function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete
|
|||||||
try {
|
try {
|
||||||
await onAnswer(trimmed)
|
await onAnswer(trimmed)
|
||||||
setAnswerDraft('')
|
setAnswerDraft('')
|
||||||
|
setAnswerEditing(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof ApiError ? err.message : t.updateError)
|
toast.error(err instanceof ApiError ? err.message : t.updateError)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -490,9 +556,9 @@ function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="ticket-card__message">
|
<div className="ticket-card__message">
|
||||||
<RichText text={ticket.message} />
|
<RichText text={ticket.message} />
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
{ticket.thread.length > 0 && (
|
{ticket.thread.length > 0 && (
|
||||||
<div className="ticket-card__thread">
|
<div className="ticket-card__thread">
|
||||||
@@ -506,9 +572,9 @@ function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete
|
|||||||
<span className="ticket-card__thread-role">
|
<span className="ticket-card__thread-role">
|
||||||
{entry.role === 'maintainer' ? t.threadMaintainer : t.threadBreeder}
|
{entry.role === 'maintainer' ? t.threadMaintainer : t.threadBreeder}
|
||||||
</span>
|
</span>
|
||||||
<p className="ticket-card__thread-text">
|
<div className="ticket-card__thread-text">
|
||||||
<RichText text={entry.text} />
|
<RichText text={entry.text} />
|
||||||
</p>
|
</div>
|
||||||
{entry.at && (
|
{entry.at && (
|
||||||
<span className="ticket-card__thread-date">{formatDate(entry.at)}</span>
|
<span className="ticket-card__thread-date">{formatDate(entry.at)}</span>
|
||||||
)}
|
)}
|
||||||
@@ -521,25 +587,13 @@ function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete
|
|||||||
{ticket.question && (
|
{ticket.question && (
|
||||||
<div className="ticket-card__question">
|
<div className="ticket-card__question">
|
||||||
<span className="ticket-card__question-label">{t.questionLabel}</span>
|
<span className="ticket-card__question-label">{t.questionLabel}</span>
|
||||||
<p className="ticket-card__question-text">
|
<div className="ticket-card__question-text">
|
||||||
<RichText text={ticket.question} />
|
<RichText text={ticket.question} />
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ticket.answer ? (
|
{showAnswerForm ? (
|
||||||
<div className="ticket-card__answer">
|
|
||||||
<span className="ticket-card__answer-label">{t.answerLabel}</span>
|
|
||||||
<p className="ticket-card__answer-text">
|
|
||||||
<RichText text={ticket.answer} />
|
|
||||||
</p>
|
|
||||||
{ticket.answeredAt && (
|
|
||||||
<span className="ticket-card__answer-date">
|
|
||||||
{t.answeredOn}: {formatDate(ticket.answeredAt)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : canAnswer ? (
|
|
||||||
<div className="ticket-card__answer-form">
|
<div className="ticket-card__answer-form">
|
||||||
<label className="ticket-card__edit-label" htmlFor={`ticket-answer-${ticket.id}`}>
|
<label className="ticket-card__edit-label" htmlFor={`ticket-answer-${ticket.id}`}>
|
||||||
{t.answerLabel}
|
{t.answerLabel}
|
||||||
@@ -553,6 +607,11 @@ function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete
|
|||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<div className="ticket-card__actions">
|
<div className="ticket-card__actions">
|
||||||
|
{answerEditing && (
|
||||||
|
<button type="button" className="btn" onClick={cancelAmend} disabled={answering}>
|
||||||
|
{t.cancel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn--primary"
|
className="btn btn--primary"
|
||||||
@@ -563,14 +622,33 @@ function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : ticket.answer ? (
|
||||||
|
<div className="ticket-card__answer">
|
||||||
|
<span className="ticket-card__answer-label">{t.answerLabel}</span>
|
||||||
|
<div className="ticket-card__answer-text">
|
||||||
|
<RichText text={ticket.answer} />
|
||||||
|
</div>
|
||||||
|
{ticket.answeredAt && (
|
||||||
|
<span className="ticket-card__answer-date">
|
||||||
|
{t.answeredOn}: {formatDate(ticket.answeredAt)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{canAmend && (
|
||||||
|
<div className="ticket-card__actions">
|
||||||
|
<button type="button" className="btn" onClick={startAmend}>
|
||||||
|
{t.amendAnswer}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{resolved && ticket.fixNote && (
|
{resolved && ticket.fixNote && (
|
||||||
<div className="ticket-card__changelog">
|
<div className="ticket-card__changelog">
|
||||||
<span className="ticket-card__changelog-label">{t.changelogLabel}</span>
|
<span className="ticket-card__changelog-label">{t.changelogLabel}</span>
|
||||||
<p className="ticket-card__changelog-text">
|
<div className="ticket-card__changelog-text">
|
||||||
<RichText text={ticket.fixNote} />
|
<RichText text={ticket.fixNote} />
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -590,9 +668,11 @@ function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete
|
|||||||
<button type="button" className="btn" onClick={onToggleStatus}>
|
<button type="button" className="btn" onClick={onToggleStatus}>
|
||||||
{resolved ? t.reopen : t.markResolved}
|
{resolved ? t.reopen : t.markResolved}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn" onClick={startEdit}>
|
{canEditMessage && (
|
||||||
{t.edit}
|
<button type="button" className="btn" onClick={startEdit}>
|
||||||
</button>
|
{t.edit}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button type="button" className="btn btn--danger" onClick={onDelete}>
|
<button type="button" className="btn btn--danger" onClick={onDelete}>
|
||||||
{t.delete}
|
{t.delete}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -402,3 +402,21 @@
|
|||||||
box-shadow: 0 0 0 0 transparent;
|
box-shadow: 0 0 0 0 transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Markdown im Ticket-Text: Aufzählungen + Inline-Code. */
|
||||||
|
.ticket-md-list {
|
||||||
|
margin: 0.35rem 0;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-md-list li {
|
||||||
|
margin: 0.15rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.88em;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 0.05em 0.35em;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -926,6 +926,8 @@ export const de = {
|
|||||||
answerLabel: 'Deine Antwort',
|
answerLabel: 'Deine Antwort',
|
||||||
answerPlaceholder: 'Antwort auf die Rückfrage eingeben …',
|
answerPlaceholder: 'Antwort auf die Rückfrage eingeben …',
|
||||||
answerButton: 'Antworten',
|
answerButton: 'Antworten',
|
||||||
|
/** Button auf einem beantworteten Ticket, um die eigene Antwort noch zu ändern. */
|
||||||
|
amendAnswer: 'Antwort ändern',
|
||||||
answering: 'Wird gesendet …',
|
answering: 'Wird gesendet …',
|
||||||
answeredOn: 'Beantwortet am',
|
answeredOn: 'Beantwortet am',
|
||||||
answeredToast: 'Antwort gesendet.',
|
answeredToast: 'Antwort gesendet.',
|
||||||
|
|||||||
Reference in New Issue
Block a user