feat(persons): add sort toggle to person document list (issue #24)
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 1m55s
CI / Backend Unit Tests (pull_request) Successful in 2m9s
CI / E2E Tests (pull_request) Successful in 18m8s
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 1m55s
CI / Backend Unit Tests (pull_request) Successful in 2m9s
CI / E2E Tests (pull_request) Successful in 18m8s
Extracted sortDocumentsByDate utility with full Vitest coverage (6 tests), wired it into the person detail page with a DESC/ASC toggle button, and added an E2E smoke test for the toggle interaction. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,25 @@ test.describe('New person', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Person detail — sort toggle', () => {
|
||||||
|
test('sort toggle changes the button label when person has documents', async ({ page }) => {
|
||||||
|
await page.goto('/persons');
|
||||||
|
const firstPerson = page.locator('a[href^="/persons/"]').first();
|
||||||
|
await firstPerson.click();
|
||||||
|
await page.waitForSelector('[data-hydrated]');
|
||||||
|
|
||||||
|
const sortBtn = page.getByRole('button', { name: /Neueste zuerst|Älteste zuerst/i });
|
||||||
|
if (await sortBtn.isVisible()) {
|
||||||
|
await expect(sortBtn).toContainText('Neueste zuerst');
|
||||||
|
await sortBtn.click();
|
||||||
|
await expect(sortBtn).toContainText('Älteste zuerst');
|
||||||
|
await sortBtn.click();
|
||||||
|
await expect(sortBtn).toContainText('Neueste zuerst');
|
||||||
|
await page.screenshot({ path: 'test-results/e2e/person-sort-toggle.png' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test.describe('Conversations', () => {
|
test.describe('Conversations', () => {
|
||||||
test('shows the empty state when no persons are selected', async ({ page }) => {
|
test('shows the empty state when no persons are selected', async ({ page }) => {
|
||||||
await page.goto('/conversations');
|
await page.goto('/conversations');
|
||||||
|
|||||||
44
frontend/src/lib/utils/sort.spec.ts
Normal file
44
frontend/src/lib/utils/sort.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { sortDocumentsByDate } from './sort';
|
||||||
|
|
||||||
|
const doc = (id: string, documentDate: string | null) =>
|
||||||
|
({ id, documentDate } as { id: string; documentDate: string | null });
|
||||||
|
|
||||||
|
describe('sortDocumentsByDate', () => {
|
||||||
|
it('sorts DESC by default — newest first', () => {
|
||||||
|
const docs = [doc('a', '1920-01-01'), doc('b', '1950-06-15'), doc('c', '1935-03-10')];
|
||||||
|
const result = sortDocumentsByDate(docs, 'DESC');
|
||||||
|
expect(result.map((d) => d.id)).toEqual(['b', 'c', 'a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts ASC — oldest first', () => {
|
||||||
|
const docs = [doc('a', '1920-01-01'), doc('b', '1950-06-15'), doc('c', '1935-03-10')];
|
||||||
|
const result = sortDocumentsByDate(docs, 'ASC');
|
||||||
|
expect(result.map((d) => d.id)).toEqual(['a', 'c', 'b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('places documents without a date last in DESC', () => {
|
||||||
|
const docs = [doc('a', null), doc('b', '1940-01-01'), doc('c', null)];
|
||||||
|
const result = sortDocumentsByDate(docs, 'DESC');
|
||||||
|
expect(result[0].id).toBe('b');
|
||||||
|
expect(result.slice(1).map((d) => d.id)).toContain('a');
|
||||||
|
expect(result.slice(1).map((d) => d.id)).toContain('c');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('places documents without a date last in ASC', () => {
|
||||||
|
const docs = [doc('a', null), doc('b', '1940-01-01'), doc('c', null)];
|
||||||
|
const result = sortDocumentsByDate(docs, 'ASC');
|
||||||
|
expect(result[0].id).toBe('b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not mutate the original array', () => {
|
||||||
|
const docs = [doc('a', '1950-01-01'), doc('b', '1920-01-01')];
|
||||||
|
const original = [...docs];
|
||||||
|
sortDocumentsByDate(docs, 'ASC');
|
||||||
|
expect(docs).toEqual(original);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty array unchanged', () => {
|
||||||
|
expect(sortDocumentsByDate([], 'DESC')).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
19
frontend/src/lib/utils/sort.ts
Normal file
19
frontend/src/lib/utils/sort.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export type SortDir = 'ASC' | 'DESC';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new array of documents sorted by documentDate.
|
||||||
|
* Documents without a date are always placed last, regardless of direction.
|
||||||
|
*/
|
||||||
|
export function sortDocumentsByDate<T extends { documentDate?: string | null }>(
|
||||||
|
docs: T[],
|
||||||
|
dir: SortDir
|
||||||
|
): T[] {
|
||||||
|
return [...docs].sort((a, b) => {
|
||||||
|
const da = a.documentDate ?? '';
|
||||||
|
const db = b.documentDate ?? '';
|
||||||
|
if (!da && !db) return 0;
|
||||||
|
if (!da) return 1;
|
||||||
|
if (!db) return -1;
|
||||||
|
return dir === 'DESC' ? db.localeCompare(da) : da.localeCompare(db);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,12 +2,16 @@
|
|||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
import { sortDocumentsByDate, type SortDir } from '$lib/utils/sort';
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
const person = $derived(data.person);
|
const person = $derived(data.person);
|
||||||
const documents = $derived(data.documents);
|
const documents = $derived(data.documents);
|
||||||
|
|
||||||
|
let sortDir = $state<SortDir>('DESC');
|
||||||
|
const sortedDocuments = $derived(sortDocumentsByDate(documents, sortDir));
|
||||||
|
|
||||||
let editMode = $state(false);
|
let editMode = $state(false);
|
||||||
let mergeTargetId = $state('');
|
let mergeTargetId = $state('');
|
||||||
let showMergeConfirm = $state(false);
|
let showMergeConfirm = $state(false);
|
||||||
@@ -200,10 +204,21 @@
|
|||||||
<!-- Linked Documents Section -->
|
<!-- Linked Documents Section -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-6 border-b border-brand-navy/10 pb-2">
|
<div class="flex items-center justify-between mb-6 border-b border-brand-navy/10 pb-2">
|
||||||
<h2 class="text-xl font-serif text-brand-navy">{m.person_docs_heading()}</h2>
|
<div class="flex items-center gap-3">
|
||||||
<span class="bg-brand-navy text-white text-xs font-bold px-2 py-1 rounded-full">
|
<h2 class="text-xl font-serif text-brand-navy">{m.person_docs_heading()}</h2>
|
||||||
{documents.length}
|
<span class="bg-brand-navy text-white text-xs font-bold px-2 py-1 rounded-full">
|
||||||
</span>
|
{documents.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{#if documents.length > 0}
|
||||||
|
<button
|
||||||
|
onclick={() => (sortDir = sortDir === 'DESC' ? 'ASC' : 'DESC')}
|
||||||
|
class="text-xs font-bold uppercase tracking-widest text-gray-400 hover:text-brand-navy transition-colors"
|
||||||
|
aria-label={m.conv_sort_label()}
|
||||||
|
>
|
||||||
|
{sortDir === 'DESC' ? m.conv_sort_newest() : m.conv_sort_oldest()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if documents.length === 0}
|
{#if documents.length === 0}
|
||||||
@@ -212,7 +227,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="space-y-3">
|
<ul class="space-y-3">
|
||||||
{#each documents as doc}
|
{#each sortedDocuments as doc}
|
||||||
<li class="group">
|
<li class="group">
|
||||||
<a href="/documents/{doc.id}" class="block bg-white border border-brand-sand p-4 hover:border-brand-navy hover:shadow-md transition-all duration-200">
|
<a href="/documents/{doc.id}" class="block bg-white border border-brand-sand p-4 hover:border-brand-navy hover:shadow-md transition-all duration-200">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user