feat(collections): import a file into a list that already exists

The import dialog always made a new list. It now asks where the places go:
a new list, as before and still the default, or a list that is already there.
The picker offers the lists the person may edit, which is their own plus a
shared one where they are an editor or an admin, and starts on the list they
have open.

Adding to a list only ever adds. A place the list already holds is left
exactly as it is, with its status, notes, rating and labels, and counted as
already there in what the import reports; it is recognised the way a single
save recognises one, by provider id, then name, then position. The list keeps
its own name, colour, icon and description whatever the file says, a label
the file brings is created only when the list has none of that name, and new
places are appended after the ones already there so a manual order survives.

The route is POST /api/addons/collections/:id/import, 200 because it adds to
a list rather than making one, and both imports now write their places through
the same helper.
This commit is contained in:
Maurice
2026-09-19 23:12:25 +02:00
parent 7cfec6452d
commit d803efd39d
36 changed files with 978 additions and 81 deletions
@@ -172,8 +172,8 @@ export type CollectionFile = z.infer<typeof collectionFileSchema>;
/**
* The import request: the file itself, plus the one thing the importer may
* decide rather than the file. A list always arrives as a NEW list, so there
* is no target id here and no way for a file to write into an existing one.
* decide rather than the file. This one always makes a NEW list, so it carries
* no target id; the list to add to is named in the URL of the route below.
*/
export const collectionImportRequestSchema = z.object({
file: collectionFileSchema,
@@ -182,12 +182,29 @@ export const collectionImportRequestSchema = z.object({
});
export type CollectionImportRequest = z.infer<typeof collectionImportRequestSchema>;
/**
* The same file read into a list that already exists.
*
* Only the file: what the list is called, how it looks and what it says stay
* the list's own. Nothing in the request can overwrite a place either, so the
* worst a file can do to a list somebody else shares is add to it.
*/
export const collectionImportIntoRequestSchema = z.object({
file: collectionFileSchema,
});
export type CollectionImportIntoRequest = z.infer<typeof collectionImportIntoRequestSchema>;
export interface CollectionImportResult {
/** The list as created, so the client can select it without a refetch. */
/** The list as created or added to, so the client can select it without a refetch. */
collection: unknown;
imported: number;
/** Places the file carried that the contract refused. */
skipped: number;
/**
* Places the list already had, left exactly as they were. Only an import
* into an existing list can report any: a new list starts empty.
*/
duplicates?: number;
}
/*