Remove the read-only "Originaltext" date field that confuses editors (#710) #712
@@ -302,7 +302,6 @@
|
|||||||
"date_season_summer": "Sommer",
|
"date_season_summer": "Sommer",
|
||||||
"date_season_autumn": "Herbst",
|
"date_season_autumn": "Herbst",
|
||||||
"date_season_winter": "Winter",
|
"date_season_winter": "Winter",
|
||||||
"date_original_label": "Originaltext:",
|
|
||||||
"date_unknown_icon_label": "Datum unbekannt",
|
"date_unknown_icon_label": "Datum unbekannt",
|
||||||
"form_label_date_precision": "Datumsgenauigkeit",
|
"form_label_date_precision": "Datumsgenauigkeit",
|
||||||
"form_label_date_end": "Enddatum",
|
"form_label_date_end": "Enddatum",
|
||||||
|
|||||||
@@ -302,7 +302,6 @@
|
|||||||
"date_season_summer": "Summer",
|
"date_season_summer": "Summer",
|
||||||
"date_season_autumn": "Autumn",
|
"date_season_autumn": "Autumn",
|
||||||
"date_season_winter": "Winter",
|
"date_season_winter": "Winter",
|
||||||
"date_original_label": "Original:",
|
|
||||||
"date_unknown_icon_label": "Date unknown",
|
"date_unknown_icon_label": "Date unknown",
|
||||||
"form_label_date_precision": "Date precision",
|
"form_label_date_precision": "Date precision",
|
||||||
"form_label_date_end": "End date",
|
"form_label_date_end": "End date",
|
||||||
|
|||||||
@@ -302,7 +302,6 @@
|
|||||||
"date_season_summer": "Verano",
|
"date_season_summer": "Verano",
|
||||||
"date_season_autumn": "Otoño",
|
"date_season_autumn": "Otoño",
|
||||||
"date_season_winter": "Invierno",
|
"date_season_winter": "Invierno",
|
||||||
"date_original_label": "Texto original:",
|
|
||||||
"date_unknown_icon_label": "Fecha desconocida",
|
"date_unknown_icon_label": "Fecha desconocida",
|
||||||
"form_label_date_precision": "Precisión de la fecha",
|
"form_label_date_precision": "Precisión de la fecha",
|
||||||
"form_label_date_end": "Fecha final",
|
"form_label_date_end": "Fecha final",
|
||||||
|
|||||||
@@ -1,30 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate';
|
import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate';
|
||||||
import { getLocale } from '$lib/paraglide/runtime.js';
|
import { getLocale } from '$lib/paraglide/runtime.js';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
iso?: string | null;
|
iso?: string | null;
|
||||||
precision?: DatePrecision | null;
|
precision?: DatePrecision | null;
|
||||||
end?: string | null;
|
end?: string | null;
|
||||||
|
/** Verbatim import cell — used only to derive the SEASON word, never displayed. */
|
||||||
raw?: string | null;
|
raw?: string | null;
|
||||||
/** Show the verbatim "Originaltext: …" secondary line when raw is present. */
|
|
||||||
showRaw?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let { iso = null, precision = null, end = null, raw = null, showRaw = true }: Props = $props();
|
let { iso = null, precision = null, end = null, raw = null }: Props = $props();
|
||||||
|
|
||||||
const effectivePrecision = $derived<DatePrecision>(precision ?? (iso ? 'DAY' : 'UNKNOWN'));
|
const effectivePrecision = $derived<DatePrecision>(precision ?? (iso ? 'DAY' : 'UNKNOWN'));
|
||||||
const label = $derived(formatDocumentDate(iso, effectivePrecision, end, raw, getLocale()));
|
const label = $derived(formatDocumentDate(iso, effectivePrecision, end, raw, getLocale()));
|
||||||
const isUnknown = $derived(effectivePrecision === 'UNKNOWN' || !iso);
|
const isUnknown = $derived(effectivePrecision === 'UNKNOWN' || !iso);
|
||||||
// Only show the verbatim raw line where it adds information the label can't: the
|
|
||||||
// season word's source, or the original cell behind an "unknown"/approx date.
|
|
||||||
const showRawLine = $derived(
|
|
||||||
showRaw &&
|
|
||||||
!!raw &&
|
|
||||||
raw.trim().length > 0 &&
|
|
||||||
(isUnknown || effectivePrecision === 'SEASON' || effectivePrecision === 'APPROX')
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="inline-flex flex-col">
|
<span class="inline-flex flex-col">
|
||||||
@@ -61,10 +51,4 @@ const showRawLine = $derived(
|
|||||||
{:else}
|
{:else}
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showRawLine}
|
|
||||||
<!-- Visible secondary line (WCAG 1.4.13 — not tooltip-only). raw is untrusted
|
|
||||||
verbatim spreadsheet text; rendered via default Svelte interpolation, which
|
|
||||||
HTML-escapes it (never {@html}; CWE-79). -->
|
|
||||||
<span class="font-sans text-xs text-ink-2">{m.date_original_label()} {raw}</span>
|
|
||||||
{/if}
|
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -17,19 +17,4 @@ describe('DocumentDate', () => {
|
|||||||
render(DocumentDate, { props: { iso: '1916-06-01', precision: 'MONTH', raw: 'Juni 1916' } });
|
render(DocumentDate, { props: { iso: '1916-06-01', precision: 'MONTH', raw: 'Juni 1916' } });
|
||||||
await expect.element(page.getByText('Juni 1916')).toBeInTheDocument();
|
await expect.element(page.getByText('Juni 1916')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the verbatim raw cell as a visible secondary line for UNKNOWN (not tooltip-only)', async () => {
|
|
||||||
render(DocumentDate, { props: { iso: null, precision: 'UNKNOWN', raw: 'Sommer?' } });
|
|
||||||
// Real, visible text — not hidden behind a title attribute.
|
|
||||||
await expect.element(page.getByText('Datum unbekannt')).toBeInTheDocument();
|
|
||||||
await expect.element(page.getByText(/Sommer\?/)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders a malicious raw value as inert escaped text (no element injected)', async () => {
|
|
||||||
const malicious = '<img src=x onerror="alert(1)">';
|
|
||||||
render(DocumentDate, { props: { iso: null, precision: 'UNKNOWN', raw: malicious } });
|
|
||||||
// The payload appears as literal text, and no <img> is created in the DOM.
|
|
||||||
await expect.element(page.getByText(/<img/)).toBeInTheDocument();
|
|
||||||
expect(document.querySelector('img')).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -209,7 +209,6 @@ async function handleReplaceFile(e: Event) {
|
|||||||
bind:dateIso={dateIso}
|
bind:dateIso={dateIso}
|
||||||
bind:precision={datePrecision}
|
bind:precision={datePrecision}
|
||||||
bind:endDateIso={dateEndIso}
|
bind:endDateIso={dateEndIso}
|
||||||
rawDate={doc.metaDateRaw ?? ''}
|
|
||||||
initialDateIso={doc.documentDate ?? ''}
|
initialDateIso={doc.documentDate ?? ''}
|
||||||
initialLocation={doc.location ?? ''}
|
initialLocation={doc.location ?? ''}
|
||||||
initialSenderName={doc.sender?.displayName ?? ''}
|
initialSenderName={doc.sender?.displayName ?? ''}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ function getFullName(person: Person): string {
|
|||||||
<div>
|
<div>
|
||||||
<dt class="font-sans text-xs font-medium text-ink-3">{m.doc_details_field_date()}</dt>
|
<dt class="font-sans text-xs font-medium text-ink-3">{m.doc_details_field_date()}</dt>
|
||||||
<dd class="text-ink">
|
<dd class="text-ink">
|
||||||
{#if documentDate || metaDateRaw}
|
{#if documentDate}
|
||||||
<DocumentDate
|
<DocumentDate
|
||||||
iso={documentDate}
|
iso={documentDate}
|
||||||
precision={metaDatePrecision}
|
precision={metaDatePrecision}
|
||||||
|
|||||||
@@ -58,6 +58,18 @@ describe('DocumentMetadataDrawer', () => {
|
|||||||
expect(dashTexts.length).toBeGreaterThan(0);
|
expect(dashTexts.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows an em-dash and never the raw cell for an undated, raw-only document', async () => {
|
||||||
|
render(DocumentMetadataDrawer, {
|
||||||
|
props: { ...baseProps, documentDate: null, metaDateRaw: 'Sommer 1916' }
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect.element(page.getByText('Sommer 1916')).not.toBeInTheDocument();
|
||||||
|
const dashTexts = Array.from(document.querySelectorAll('dd, p'))
|
||||||
|
.map((el) => el.textContent?.trim())
|
||||||
|
.filter((t) => t === '—');
|
||||||
|
expect(dashTexts.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('renders the no-persons placeholder when sender and receivers are empty', async () => {
|
it('renders the no-persons placeholder when sender and receivers are empty', async () => {
|
||||||
render(DocumentMetadataDrawer, { props: baseProps });
|
render(DocumentMetadataDrawer, { props: baseProps });
|
||||||
|
|
||||||
|
|||||||
@@ -164,15 +164,10 @@ function safeTagColor(color: string | null | undefined): string {
|
|||||||
<!-- Mobile-only metadata -->
|
<!-- Mobile-only metadata -->
|
||||||
<div class="mt-3 grid grid-cols-2 gap-x-4 gap-y-1 font-sans text-xs text-ink-2 sm:hidden">
|
<div class="mt-3 grid grid-cols-2 gap-x-4 gap-y-1 font-sans text-xs text-ink-2 sm:hidden">
|
||||||
<div>
|
<div>
|
||||||
<!-- Product decision (#666): raw provenance (meta_date_raw) is shown on the
|
|
||||||
document DETAIL page, never in list/search rows — list rows surface only the
|
|
||||||
honest label to keep scan-rows compact. showRaw={false} enforces this; the
|
|
||||||
DocumentListItem payload also intentionally omits metaDateRaw. -->
|
|
||||||
<DocumentDate
|
<DocumentDate
|
||||||
iso={doc.documentDate}
|
iso={doc.documentDate}
|
||||||
precision={doc.metaDatePrecision}
|
precision={doc.metaDatePrecision}
|
||||||
end={doc.metaDateEnd}
|
end={doc.metaDateEnd}
|
||||||
showRaw={false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
@@ -194,7 +189,6 @@ function safeTagColor(color: string | null | undefined): string {
|
|||||||
iso={doc.documentDate}
|
iso={doc.documentDate}
|
||||||
precision={doc.metaDatePrecision}
|
precision={doc.metaDatePrecision}
|
||||||
end={doc.metaDateEnd}
|
end={doc.metaDateEnd}
|
||||||
showRaw={false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ let {
|
|||||||
dateIso = $bindable(''),
|
dateIso = $bindable(''),
|
||||||
precision = $bindable<DatePrecision>('DAY'),
|
precision = $bindable<DatePrecision>('DAY'),
|
||||||
endDateIso = $bindable(''),
|
endDateIso = $bindable(''),
|
||||||
rawDate = '',
|
|
||||||
initialDateIso = '',
|
initialDateIso = '',
|
||||||
initialLocation = '',
|
initialLocation = '',
|
||||||
initialSenderName = '',
|
initialSenderName = '',
|
||||||
@@ -30,7 +29,6 @@ let {
|
|||||||
dateIso?: string;
|
dateIso?: string;
|
||||||
precision?: DatePrecision;
|
precision?: DatePrecision;
|
||||||
endDateIso?: string;
|
endDateIso?: string;
|
||||||
rawDate?: string;
|
|
||||||
initialDateIso?: string;
|
initialDateIso?: string;
|
||||||
initialLocation?: string;
|
initialLocation?: string;
|
||||||
initialSenderName?: string;
|
initialSenderName?: string;
|
||||||
@@ -179,15 +177,6 @@ $effect(() => {
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="metaDateEnd" value={showEndDate ? endDateIso : ''} />
|
<input type="hidden" name="metaDateEnd" value={showEndDate ? endDateIso : ''} />
|
||||||
|
|
||||||
<!-- Originaltext (read-only raw cell): labelled static text, not a disabled input. -->
|
|
||||||
{#if rawDate && rawDate.trim().length > 0}
|
|
||||||
<div data-testid="who-when-raw">
|
|
||||||
<p class="mb-1 block text-sm font-medium text-ink-2">{m.date_original_label()}</p>
|
|
||||||
<p class="font-sans text-sm text-ink">{rawDate}</p>
|
|
||||||
<input type="hidden" name="metaDateRaw" value={rawDate} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Absender (required in upload mode — row 1, col 2) -->
|
<!-- Absender (required in upload mode — row 1, col 2) -->
|
||||||
|
|||||||
@@ -93,13 +93,12 @@ describe('WhoWhenSection — precision controls', () => {
|
|||||||
expect(document.querySelector('input#metaDateEnd')).not.toBeNull();
|
expect(document.querySelector('input#metaDateEnd')).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the raw cell as static text (not an editable input) and escapes it', async () => {
|
it('never renders the raw cell, and never re-submits it via a hidden input', async () => {
|
||||||
render(WhoWhenSection, { rawDate: '<b>Sommer</b> 1916' });
|
render(WhoWhenSection, {});
|
||||||
const raw = document.querySelector('[data-testid="who-when-raw"]');
|
// The confusing "Originaltext" line is gone …
|
||||||
expect(raw).not.toBeNull();
|
expect(document.querySelector('[data-testid="who-when-raw"]')).toBeNull();
|
||||||
// Verbatim shown as escaped text; no injected <b> element.
|
// … and editing no longer round-trips metaDateRaw to the backend.
|
||||||
expect(raw?.textContent).toContain('<b>Sommer</b> 1916');
|
expect(document.querySelector('input[name="metaDateRaw"]')).toBeNull();
|
||||||
expect(raw?.querySelector('b')).toBeNull();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ export type DatePrecision = 'DAY' | 'MONTH' | 'SEASON' | 'YEAR' | 'RANGE' | 'APP
|
|||||||
* {@code DocumentTitleFormatter}: both are asserted against
|
* {@code DocumentTitleFormatter}: both are asserted against
|
||||||
* `docs/date-label-fixtures.json` so they cannot drift. The untrusted `raw`
|
* `docs/date-label-fixtures.json` so they cannot drift. The untrusted `raw`
|
||||||
* cell is only used to derive a season word (a known German season token) — it
|
* cell is only used to derive a season word (a known German season token) — it
|
||||||
* is otherwise rendered separately by the caller via Svelte default escaping,
|
* is never displayed and never interpolated into HTML here.
|
||||||
* never interpolated into HTML here.
|
|
||||||
*
|
*
|
||||||
* @param iso the sort/filter anchor day (`YYYY-MM-DD`), nullable for UNKNOWN rows
|
* @param iso the sort/filter anchor day (`YYYY-MM-DD`), nullable for UNKNOWN rows
|
||||||
* @param precision descriptive precision metadata
|
* @param precision descriptive precision metadata
|
||||||
@@ -82,8 +81,7 @@ function seasonLabel(
|
|||||||
): string {
|
): string {
|
||||||
const month = Number(iso.slice(5, 7));
|
const month = Number(iso.slice(5, 7));
|
||||||
// Prefer the season named in the raw cell; fall back to deriving it from the
|
// Prefer the season named in the raw cell; fall back to deriving it from the
|
||||||
// anchor month. Either way the WORD is localized (Decision 4) — the verbatim
|
// anchor month. Either way the WORD is localized (Decision 4).
|
||||||
// German raw cell is preserved separately as the visible secondary line.
|
|
||||||
const season = seasonFromRaw(raw) ?? seasonOfMonth(month);
|
const season = seasonFromRaw(raw) ?? seasonOfMonth(month);
|
||||||
return `${seasonWord(season, locale)} ${year}`;
|
return `${seasonWord(season, locale)} ${year}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user