Files
TREK/client/src/components/Files/fileListRules.test.ts
T
Maurice 973773e003 refactor(files): share the file filter, link list, note save and trash between desktop and phone
fileListRules now holds the type category of a file, the filter tab match
and the list of places and bookings a file is linked to (own column plus
extra links, transport or booking). useFileManager and FileManagerRow on
desktop and filesModel on the phone run on it. fileActions gains the field
save with its reload and assign error, which the desktop assign dialog and
the phone note edit both use, and the trash call with its toasts, used by
the desktop delete and the phone menu sheet. The phone sheet no longer
imports filesApi.

Differences that stay, each as an explicit option:
PHONE_FILE_FILTER_RULES sorts pdf, image and doc by type category (videos
count as images, doc is every file that is no PDF and no media, a pass
with a PDF mime type is a doc) and counts a note id of 0 as collab, while
desktop reads the mime type (images only, doc is Word, Excel and text).
DESKTOP_FILE_LINK_LIST_RULES skips an own place or booking column of 0,
the phone only a null one. PHONE_FILE_UPDATE_RULES waits for the reload
after a save, so a failed reload shows the assign error there; desktop
starts it and moves on. DESKTOP_FILE_TRASH_TOAST_RULES falls back to
English when the trash toast translation is empty.
2026-10-09 15:19:51 +02:00

153 lines
7.3 KiB
TypeScript

// FE-FILES-LIST-001 to FE-FILES-LIST-012: the filter tabs and the link list the desktop file
// manager and the phone's files tab share, with the type and id reading each view keeps.
import { describe, expect, it } from 'vitest';
import { buildPlace, buildReservation, buildTripFile } from '../../../tests/helpers/factories';
import {
DESKTOP_FILE_LINK_LIST_RULES,
PHONE_FILE_FILTER_RULES,
getFileTypeCategory,
linkedFileIds,
linkedFileTargets,
matchesFileFilter,
} from './fileListRules';
const TRANSPORT = new Set(['flight', 'train']);
describe('getFileTypeCategory', () => {
it('FE-FILES-LIST-001: a pass wins over its mime type, video counts as image, sheets are xls', () => {
expect(getFileTypeCategory({ mime_type: 'application/pdf', original_name: 'ticket.pkpass' })).toBe('pass');
expect(getFileTypeCategory({ mime_type: 'application/pdf', original_name: 'a.pdf' })).toBe('pdf');
expect(getFileTypeCategory({ mime_type: 'video/mp4', original_name: 'a.mp4' })).toBe('image');
expect(getFileTypeCategory({ mime_type: 'text/csv', original_name: 'export' })).toBe('xls');
expect(getFileTypeCategory({ mime_type: 'application/zip', original_name: 'a.zip' })).toBe('other');
});
});
describe('matchesFileFilter', () => {
const pdf = buildTripFile({ mime_type: 'application/pdf', original_name: 'a.pdf' });
const passAsPdf = buildTripFile({ mime_type: 'application/pdf', original_name: 'ticket.pkpass' });
const photo = buildTripFile({ mime_type: 'image/jpeg', original_name: 'a.jpg' });
const video = buildTripFile({ mime_type: 'video/mp4', original_name: 'a.mp4' });
const word = buildTripFile({ mime_type: 'application/msword', original_name: 'a.doc' });
const text = buildTripFile({ mime_type: 'text/plain', original_name: 'a.txt' });
const zip = buildTripFile({ mime_type: 'application/zip', original_name: 'a.zip' });
const all = [pdf, passAsPdf, photo, video, word, text, zip];
const ids = (filter: string, rules?: typeof PHONE_FILE_FILTER_RULES) =>
all.filter((f) => matchesFileFilter(f, filter, rules)).map((f) => f.id);
it('FE-FILES-LIST-002: desktop reads the mime type for pdf, image and doc', () => {
expect(ids('pdf')).toEqual([pdf.id, passAsPdf.id]);
expect(ids('image')).toEqual([photo.id]);
expect(ids('doc')).toEqual([word.id, text.id]);
});
it('FE-FILES-LIST-003: the phone partitions by type category', () => {
expect(ids('pdf', PHONE_FILE_FILTER_RULES)).toEqual([pdf.id]);
expect(ids('image', PHONE_FILE_FILTER_RULES)).toEqual([photo.id, video.id]);
expect(ids('doc', PHONE_FILE_FILTER_RULES)).toEqual([passAsPdf.id, word.id, text.id, zip.id]);
});
it('FE-FILES-LIST-004: all and an unknown filter show everything in both views', () => {
for (const rules of [undefined, PHONE_FILE_FILTER_RULES]) {
expect(ids('all', rules)).toEqual(all.map((f) => f.id));
expect(ids('bogus', rules)).toEqual(all.map((f) => f.id));
}
});
it('FE-FILES-LIST-005: starred follows the flag in both views', () => {
for (const rules of [undefined, PHONE_FILE_FILTER_RULES]) {
expect(matchesFileFilter(buildTripFile({ starred: 1 }), 'starred', rules)).toBe(true);
expect(matchesFileFilter(buildTripFile({ starred: 0 }), 'starred', rules)).toBe(false);
}
});
it('FE-FILES-LIST-006: a note id of 0 is collab on the phone only', () => {
const zeroNote = buildTripFile({ note_id: 0 });
expect(matchesFileFilter(zeroNote, 'collab')).toBe(false);
expect(matchesFileFilter(zeroNote, 'collab', PHONE_FILE_FILTER_RULES)).toBe(true);
for (const rules of [undefined, PHONE_FILE_FILTER_RULES]) {
expect(matchesFileFilter(buildTripFile({ note_id: 4 }), 'collab', rules)).toBe(true);
expect(matchesFileFilter(buildTripFile({ note_id: null }), 'collab', rules)).toBe(false);
}
});
});
describe('linkedFileTargets', () => {
const zeroPlace = buildPlace({ id: 0, name: 'Zero' });
const louvre = buildPlace({ id: 1, name: 'Louvre' });
const orsay = buildPlace({ id: 2, name: 'Orsay' });
const flight = buildReservation({ id: 10, type: 'flight', title: 'LH 123' });
const hotel = buildReservation({ id: 11, type: 'hotel', title: 'Hotel' });
const places = [zeroPlace, louvre, orsay];
const reservations = [flight, hotel];
it('FE-FILES-LIST-007: own column first, then each extra link once, unknown ids left out', () => {
const file = buildTripFile({
place_id: 2,
linked_place_ids: [1, 2, 99],
reservation_id: 11,
linked_reservation_ids: [10, 11],
});
for (const rules of [undefined, DESKTOP_FILE_LINK_LIST_RULES]) {
const linked = linkedFileTargets(file, places, reservations, TRANSPORT, rules);
expect(linked.places).toEqual([orsay, louvre]);
expect(linked.reservations).toEqual([
{ reservation: hotel, transport: false },
{ reservation: flight, transport: true },
]);
}
});
it('FE-FILES-LIST-008: an own column of 0 counts on the phone and not on desktop', () => {
const file = buildTripFile({ place_id: 0, linked_place_ids: [], reservation_id: null });
expect(linkedFileTargets(file, places, reservations, TRANSPORT).places).toEqual([zeroPlace]);
expect(linkedFileTargets(file, places, reservations, TRANSPORT, DESKTOP_FILE_LINK_LIST_RULES).places).toEqual([]);
});
it('FE-FILES-LIST-009: null entries in the extra links are skipped', () => {
const file = buildTripFile({
place_id: null,
linked_place_ids: [null as unknown as number, 1],
reservation_id: null,
linked_reservation_ids: [null as unknown as number],
});
const linked = linkedFileTargets(file, places, reservations, TRANSPORT, DESKTOP_FILE_LINK_LIST_RULES);
expect(linked.places).toEqual([louvre]);
expect(linked.reservations).toEqual([]);
});
it('FE-FILES-LIST-010: missing place and booking lists give empty links', () => {
const file = buildTripFile({ place_id: 1, reservation_id: 10 });
expect(linkedFileTargets(file, undefined, undefined, TRANSPORT)).toEqual({ places: [], reservations: [] });
});
it('FE-FILES-LIST-011: a booking column of 0 counts on the phone and not on desktop', () => {
const zeroBooking = buildReservation({ id: 0, type: 'hotel', title: 'Zero' });
const file = buildTripFile({ place_id: null, reservation_id: 0, linked_reservation_ids: [] });
expect(linkedFileTargets(file, places, [zeroBooking], TRANSPORT).reservations).toEqual([
{ reservation: zeroBooking, transport: false },
]);
expect(
linkedFileTargets(file, places, [zeroBooking], TRANSPORT, DESKTOP_FILE_LINK_LIST_RULES).reservations
).toEqual([]);
});
});
describe('linkedFileIds', () => {
it('FE-FILES-LIST-012: own column first, extras once, nulls skipped, 0 kept only on the phone', () => {
const file = buildTripFile({
place_id: 0,
linked_place_ids: [2, null as unknown as number, 0, 2],
reservation_id: 11,
linked_reservation_ids: [10, 11],
});
expect(linkedFileIds(file, 'place_id')).toEqual([0, 2]);
expect(linkedFileIds(file, 'place_id', DESKTOP_FILE_LINK_LIST_RULES)).toEqual([2, 0]);
expect(linkedFileIds(file, 'reservation_id')).toEqual([11, 10]);
expect(linkedFileIds(file, 'reservation_id', DESKTOP_FILE_LINK_LIST_RULES)).toEqual([11, 10]);
const empty = buildTripFile({ place_id: null, linked_place_ids: undefined });
expect(linkedFileIds(empty, 'place_id')).toEqual([]);
});
});