feat(import): bring the account's own mood vocabulary along

A full backup carries the account's own mood tags, rated factors and
tag groups, and the JSON import left them out: a mood entry imported
into another account landed without every link to a tag the person had
made.

Before the entries are written, every key they or the file name is
resolved once. A catalogue tag or one the account owns is used as it
is; an own tag whose label the file states differently is kept, never
overwritten, and named in keptMoodTagKeys (keptMoodCategoryKeys for a
group). A tag the file defines and the account lacks is created in its
group, the group created the same way, within the account's caps of 50
tags and 12 groups. A key another account of the server holds comes
over under a key derived from the account and the file's key, so a
second import of the same file finds the copy instead of minting
another. The links are written through that resolution, own rated
factors included, inside the entry's transaction; anything unresolved
is still dropped and named. The import card names kept keys in all
nine locales.

The field-coverage guard now holds the backup's customMoodTags and
customMoodTagCategories to the import schema. The round trip moves a
backup to another server (the same seeded catalogue, none of the
account's rows), imports it into a fresh account and compares the
re-export, own tags, group and rated factor included; a second case
imports into another account of the same server, and a third keeps a
renamed own tag.
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 22:41:36 +02:00
1 parent d1545cf554
commit 2357807706
25 files changed
+995 -67

No files matched your search

@@ -38,9 +38,22 @@ describe("<JsonImportResultView>", () => {
expect(html).toContain("Imported 3 measurements and 2 mood entries");
});
it("names the account's own tags it kept rather than renamed", () => {
const html = render({
measurements: 0,
moodEntries: 2,
skipped: 0,
keptMoodTagKeys: ["custom:walk"],
keptMoodCategoryKeys: ["customcat:outdoors"],
});
expect(html).toContain('data-testid="import-json-kept-labels"');
expect(html).toContain("custom:walk, customcat:outdoors");
});
it("says nothing about links when every one landed", () => {
const html = render({ measurements: 3, moodEntries: 2, skipped: 0 });
expect(html).not.toContain("import-json-dropped-links");
expect(html).not.toContain("import-json-kept-labels");
});
it("reads a missing count as none", () => {
@@ -49,6 +62,7 @@ describe("<JsonImportResultView>", () => {
moodEntries: 0,
skipped: 0,
droppedLinkKeys: [],
keptLabelKeys: [],
});
});
});
@@ -28,6 +28,11 @@ export interface JsonImportResult {
* those links, which the count alone would not say.
*/
droppedLinkKeys: string[];
/**
* Keys of the account's own tags and groups the file names differently.
* The account's names were kept, never overwritten.
*/
keptLabelKeys: string[];
}
/** The route's `data`, read field by field; a missing count reads as none. */
@@ -38,6 +43,8 @@ export function readJsonImportResult(
skipped?: number;
droppedTagKeys?: string[];
droppedFactorKeys?: string[];
keptMoodTagKeys?: string[];
keptMoodCategoryKeys?: string[];
} | null,
): JsonImportResult {
return {
@@ -48,6 +55,10 @@ export function readJsonImportResult(
...(data?.droppedTagKeys ?? []),
...(data?.droppedFactorKeys ?? []),
],
keptLabelKeys: [
...(data?.keptMoodTagKeys ?? []),
...(data?.keptMoodCategoryKeys ?? []),
],
};
}
@@ -96,6 +107,16 @@ export function JsonImportResultView({ result }: { result: JsonImportResult }) {
})}
</p>
)}
{result.keptLabelKeys.length > 0 && (
<p
className="text-muted-foreground text-xs"
data-testid="import-json-kept-labels"
>
{t("settings.sections.export.import.json.keptLabels", {
keys: result.keptLabelKeys.join(", "),
})}
</p>
)}
</>
);
}