Compare commits

..

5 Commits

Author SHA1 Message Date
Marcel
18cad798fc fix(documents): preserve archiveBox + archiveFolder in markForReview; drop documentLocation
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m1s
CI / OCR Service Tests (push) Successful in 30s
CI / Backend Unit Tests (push) Failing after 2m53s
2026-04-25 20:25:08 +02:00
Marcel
0ddf43947b refactor(documents): drop documentLocation binding from edit layouts; wire archive fields 2026-04-25 20:23:37 +02:00
Marcel
45f7642f8d feat(documents): replace documentLocation with archiveBox/archiveFolder in edit form 2026-04-25 20:11:30 +02:00
Marcel
5a13e61357 feat(documents): wire archiveBox + archiveFolder through DTO and service update 2026-04-25 20:08:21 +02:00
Marcel
a91ee1f26d refactor(documents): unify count + action links into one row
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m7s
CI / OCR Service Tests (push) Successful in 32s
CI / Backend Unit Tests (push) Failing after 2m56s
Move result count, bulk-edit button, and new-document link into a shared
flex row so they appear on the same line. Adds an edit icon to the
bulk-edit button to visually match the existing plus icon on the add link.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:41:24 +02:00
11 changed files with 125 additions and 114 deletions

View File

@@ -13,6 +13,8 @@ public class DocumentUpdateDTO {
private LocalDate documentDate;
private String location;
private String documentLocation;
private String archiveBox;
private String archiveFolder;
private String transcription;
private String summary;
private UUID senderId;

View File

@@ -271,6 +271,8 @@ public class DocumentService {
doc.setTranscription(dto.getTranscription());
doc.setSummary(dto.getSummary());
doc.setDocumentLocation(dto.getDocumentLocation());
doc.setArchiveBox(dto.getArchiveBox());
doc.setArchiveFolder(dto.getArchiveFolder());
List<String> tags = new ArrayList<>();
if (dto.getTags() != null && !dto.getTags().isBlank()) {

View File

@@ -121,6 +121,23 @@ class DocumentServiceTest {
.isInstanceOf(DomainException.class);
}
@Test
void updateDocument_setsArchiveBoxAndFolder() throws Exception {
UUID id = UUID.randomUUID();
Document doc = Document.builder().id(id).receivers(new HashSet<>()).tags(new HashSet<>()).build();
when(documentRepository.findById(id)).thenReturn(Optional.of(doc));
when(documentRepository.save(any())).thenReturn(doc);
DocumentUpdateDTO dto = new DocumentUpdateDTO();
dto.setArchiveBox("K-03");
dto.setArchiveFolder("Mappe B");
documentService.updateDocument(id, dto, null, null);
assertThat(doc.getArchiveBox()).isEqualTo("K-03");
assertThat(doc.getArchiveFolder()).isEqualTo("Mappe B");
}
// ─── deleteTagCascading ───────────────────────────────────────────────────
@Test

View File

@@ -67,7 +67,6 @@ let selectedReceivers = $state<Person[]>(untrack(() => initialReceivers));
let dateIso = $state('');
let tags = $state<Tag[]>([]);
// Bulk-edit only — replace-on-non-blank semantics.
let documentLocation = $state('');
let archiveBox = $state('');
let archiveFolder = $state('');
@@ -231,7 +230,6 @@ async function saveBulkEdit() {
tagNames: tags.map((t) => t.name),
senderId: senderId || null,
receiverIds: selectedReceivers.map((r) => r.id),
documentLocation: documentLocation || null,
archiveBox: archiveBox || null,
archiveFolder: archiveFolder || null
};
@@ -442,7 +440,6 @@ async function retrySave() {
/>
<DescriptionSection
bind:tags={tags}
bind:documentLocation={documentLocation}
bind:archiveBox={archiveBox}
bind:archiveFolder={archiveFolder}
hideTitle
@@ -494,7 +491,6 @@ async function retrySave() {
/>
<DescriptionSection
bind:tags={tags}
bind:documentLocation={documentLocation}
bind:archiveBox={archiveBox}
bind:archiveFolder={archiveFolder}
hideTitle

View File

@@ -397,8 +397,8 @@ describe('BulkDocumentEditLayout — mode="edit"', () => {
initialEditEntries: [editEntry(1)]
});
const replaceBadges = container.querySelectorAll('[data-testid="field-label-badge-replace"]');
// sender + documentLocation + archiveBox + archiveFolder = 4
expect(replaceBadges.length).toBeGreaterThanOrEqual(4);
// sender + archiveBox + archiveFolder = 3
expect(replaceBadges.length).toBeGreaterThanOrEqual(3);
});
it('topbar reads "Massenbearbeitung" + "{count} werden bearbeitet" in edit mode', async () => {

View File

@@ -11,7 +11,8 @@ let {
archiveBox = $bindable(''),
archiveFolder = $bindable(''),
initialTitle = '',
initialDocumentLocation = '',
initialArchiveBox = '',
initialArchiveFolder = '',
initialSummary = '',
titleRequired = false,
suggestedTitle = '',
@@ -24,7 +25,8 @@ let {
archiveBox?: string;
archiveFolder?: string;
initialTitle?: string;
initialDocumentLocation?: string;
initialArchiveBox?: string;
initialArchiveFolder?: string;
initialSummary?: string;
titleRequired?: boolean;
suggestedTitle?: string;
@@ -41,7 +43,8 @@ let {
let titleDirty = $state(false);
onMount(() => {
if (!currentTitle && initialTitle) currentTitle = initialTitle;
if (!documentLocation && initialDocumentLocation) documentLocation = initialDocumentLocation;
if (!archiveBox && initialArchiveBox) archiveBox = initialArchiveBox;
if (!archiveFolder && initialArchiveFolder) archiveFolder = initialArchiveFolder;
});
const titleValue = $derived(titleDirty ? currentTitle : suggestedTitle || currentTitle);
</script>
@@ -110,55 +113,36 @@ const titleValue = $derived(titleDirty ? currentTitle : suggestedTitle || curren
</div>
{/if}
<!-- Aufbewahrungsort (optional) -->
<div data-testid="description-document-location">
<label for="documentLocation" class="mb-1 block text-sm font-medium text-ink-2"
>{m.form_label_archive_location()}
<!-- Karton -->
<div data-testid="description-archive-box">
<label for="archiveBox" class="mb-1 block text-sm font-medium text-ink-2">
{m.form_label_archive_box()}
{#if editMode}<FieldLabelBadge variant="replace" />{/if}
</label>
<input
id="documentLocation"
id="archiveBox"
type="text"
name="documentLocation"
bind:value={documentLocation}
placeholder={m.form_placeholder_archive_location()}
name="archiveBox"
bind:value={archiveBox}
class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
/>
<p class="mt-1 text-xs text-ink-3">{m.form_helper_archive_location()}</p>
<p class="mt-1 text-xs text-ink-3">{m.form_helper_archive_box()}</p>
</div>
{#if editMode}
<!-- Karton (only in editMode — bulk-editable replace) -->
<div data-testid="description-archive-box">
<label for="archiveBox" class="mb-1 block text-sm font-medium text-ink-2">
{m.form_label_archive_box()}
<FieldLabelBadge variant="replace" />
</label>
<input
id="archiveBox"
type="text"
name="archiveBox"
bind:value={archiveBox}
class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
/>
<p class="mt-1 text-xs text-ink-3">{m.form_helper_archive_box()}</p>
</div>
<!-- Mappe (only in editMode — bulk-editable replace) -->
<div data-testid="description-archive-folder">
<label for="archiveFolder" class="mb-1 block text-sm font-medium text-ink-2">
{m.form_label_archive_folder()}
<FieldLabelBadge variant="replace" />
</label>
<input
id="archiveFolder"
type="text"
name="archiveFolder"
bind:value={archiveFolder}
class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
/>
<p class="mt-1 text-xs text-ink-3">{m.form_helper_archive_folder()}</p>
</div>
{/if}
<!-- Mappe -->
<div data-testid="description-archive-folder">
<label for="archiveFolder" class="mb-1 block text-sm font-medium text-ink-2">
{m.form_label_archive_folder()}
{#if editMode}<FieldLabelBadge variant="replace" />{/if}
</label>
<input
id="archiveFolder"
type="text"
name="archiveFolder"
bind:value={archiveFolder}
class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
/>
<p class="mt-1 text-xs text-ink-3">{m.form_helper_archive_folder()}</p>
</div>
</div>
</div>

View File

@@ -21,19 +21,10 @@ describe('DescriptionSection — onMount seeding (Felix B1/B2 fix regression fen
expect(titleInput.value).toBe('Parent Title');
});
it('pre-fills the documentLocation input from initialDocumentLocation', async () => {
render(DescriptionSection, { initialDocumentLocation: 'Schrank 3, Mappe B' });
const locationInput = document.querySelector('input#documentLocation') as HTMLInputElement;
expect(locationInput.value).toBe('Schrank 3, Mappe B');
});
it('does not stomp a parent-bound documentLocation that is already non-empty', async () => {
render(DescriptionSection, {
documentLocation: 'Bound Value',
initialDocumentLocation: 'Should Not Win'
});
const locationInput = document.querySelector('input#documentLocation') as HTMLInputElement;
expect(locationInput.value).toBe('Bound Value');
it('always renders archiveBox + archiveFolder fields regardless of editMode', async () => {
render(DescriptionSection, { editMode: false });
expect(document.querySelector('[data-testid="description-archive-box"]')).not.toBeNull();
expect(document.querySelector('[data-testid="description-archive-folder"]')).not.toBeNull();
});
it('renders the editMode-only archiveBox + archiveFolder fields when editMode=true', async () => {
@@ -42,9 +33,25 @@ describe('DescriptionSection — onMount seeding (Felix B1/B2 fix regression fen
expect(document.querySelector('[data-testid="description-archive-folder"]')).not.toBeNull();
});
it('hides the editMode-only archive fields when editMode=false', async () => {
render(DescriptionSection, { editMode: false });
expect(document.querySelector('[data-testid="description-archive-box"]')).toBeNull();
expect(document.querySelector('[data-testid="description-archive-folder"]')).toBeNull();
it('pre-fills archiveBox from initialArchiveBox when archiveBox is empty', async () => {
render(DescriptionSection, { initialArchiveBox: 'K-03', hideTitle: true });
const input = document.querySelector('input#archiveBox') as HTMLInputElement;
expect(input.value).toBe('K-03');
});
it('pre-fills archiveFolder from initialArchiveFolder when archiveFolder is empty', async () => {
render(DescriptionSection, { initialArchiveFolder: 'Mappe B', hideTitle: true });
const input = document.querySelector('input#archiveFolder') as HTMLInputElement;
expect(input.value).toBe('Mappe B');
});
it('does not stomp a parent-bound archiveBox that is already non-empty', async () => {
render(DescriptionSection, {
archiveBox: 'Parent Value',
initialArchiveBox: 'Should Not Win',
hideTitle: true
});
const input = document.querySelector('input#archiveBox') as HTMLInputElement;
expect(input.value).toBe('Parent Value');
});
});

View File

@@ -207,7 +207,8 @@ async function handleReplaceFile(e: Event) {
bind:tags={tags}
bind:currentTitle={currentTitle}
initialTitle={doc.title ?? ''}
initialDocumentLocation={doc.documentLocation ?? ''}
initialArchiveBox={doc.archiveBox ?? ''}
initialArchiveFolder={doc.archiveFolder ?? ''}
initialSummary={doc.summary ?? ''}
titleRequired={true}
/>

View File

@@ -13,14 +13,12 @@ let {
items,
canWrite,
error,
total = 0,
q = '',
sort = 'DATE'
}: {
items: DocumentSearchItem[];
canWrite: boolean;
error?: string | null;
total?: number;
q?: string;
sort?: SortMode;
} = $props();
@@ -71,29 +69,6 @@ function groupByReceiver(docItems: DocumentSearchItem[]) {
}
</script>
<!-- DOCUMENT LIST HEADER -->
<div class="mb-2 flex justify-end">
{#if canWrite}
<a
href="/documents/new"
class="inline-flex items-center gap-1 text-sm font-medium text-ink-2 transition-colors hover:text-ink"
>
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Add/Add-General-MD.svg"
alt=""
aria-hidden="true"
class="h-4 w-4"
/>
{m.docs_btn_new()}
</a>
{/if}
</div>
<!-- RESULT COUNT -->
{#if total > 0}
<p class="mb-3 font-sans text-base text-ink-2">{m.docs_result_count({ count: total })}</p>
{/if}
<!-- ERROR -->
{#if error}
<div class="border border-line bg-surface shadow-sm">

View File

@@ -234,28 +234,54 @@ $effect(() => {
onblur={() => (qFocused = false)}
/>
{#if data.canWrite && data.totalElements > 0}
<div class="mb-2 flex flex-col items-end gap-1">
<button
type="button"
onclick={editAllMatching}
disabled={editingAll}
class="inline-flex items-center gap-1 text-sm font-medium text-ink-2 transition-colors hover:text-ink disabled:opacity-50"
data-testid="bulk-edit-all-x"
>
{m.bulk_edit_all_x({ count: data.totalElements })}
</button>
{#if editAllError}
<p role="alert" class="text-xs text-danger" data-testid="bulk-edit-all-x-error">
{editAllError}
</p>
{/if}
</div>
{/if}
<div class="mb-3 flex items-center justify-between gap-4">
<p class="font-sans text-base text-ink-2">
{#if data.totalElements > 0}{m.docs_result_count({ count: data.totalElements })}{/if}
</p>
{#if data.canWrite}
<div class="flex flex-col items-end gap-1">
<div class="flex items-center gap-4">
{#if data.totalElements > 0}
<button
type="button"
onclick={editAllMatching}
disabled={editingAll}
class="inline-flex cursor-pointer items-center gap-1 text-sm font-medium text-ink-2 transition-colors hover:text-ink disabled:opacity-50"
data-testid="bulk-edit-all-x"
>
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg"
alt=""
aria-hidden="true"
class="h-4 w-4"
/>
{m.bulk_edit_all_x({ count: data.totalElements })}
</button>
{/if}
<a
href="/documents/new"
class="inline-flex items-center gap-1 text-sm font-medium text-ink-2 transition-colors hover:text-ink"
>
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Add/Add-General-MD.svg"
alt=""
aria-hidden="true"
class="h-4 w-4"
/>
{m.docs_btn_new()}
</a>
</div>
{#if editAllError}
<p role="alert" class="text-xs text-danger" data-testid="bulk-edit-all-x-error">
{editAllError}
</p>
{/if}
</div>
{/if}
</div>
<DocumentList
items={data.items}
total={data.totalElements}
q={data.q}
canWrite={data.canWrite}
error={data.error}

View File

@@ -85,7 +85,8 @@ export const actions = {
if (doc.title) formData.set('title', doc.title);
if (doc.documentDate) formData.set('documentDate', doc.documentDate);
if (doc.location) formData.set('location', doc.location);
if (doc.documentLocation) formData.set('documentLocation', doc.documentLocation);
if (doc.archiveBox) formData.set('archiveBox', doc.archiveBox);
if (doc.archiveFolder) formData.set('archiveFolder', doc.archiveFolder);
if (doc.transcription) formData.set('transcription', doc.transcription);
if (doc.summary) formData.set('summary', doc.summary);
if (doc.sender?.id) formData.set('senderId', doc.sender.id);