fix(nextcloud): NULL parent_id on cross-source-root child folders + spinner

The previous fix NULLed parent_id only for folders within the
SourceRoot being deleted, but folder rows under a *different*
SourceRoot whose path nests inside this one (e.g. 'Leóns 1st Year' at
.../Taco and Muli - 2024 onward/Leóns 1st Year) still pointed into
our delete set. folders_parent_id_fkey kept tripping. Widen the UPDATE
to NULL parent_id for any folder whose parent_id is in folder_ids,
regardless of source_root_id.

UI: trash button on a Nextcloud library now swaps to a spinning
Loader2 while the delete is in flight (only the row being deleted —
others stay as trash icons but disabled). Title updates to flag
that a cascade through every photo + folder can take a few seconds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claudio
2026-05-10 22:37:12 +02:00
parent 89f99d220a
commit 76551d898b
2 changed files with 20 additions and 8 deletions

View File

@@ -333,14 +333,18 @@ async def delete_nextcloud_source_root(
)
)
# Folders have a self-referential parent_id FK with no
# ON DELETE rule. A flat DELETE of the whole subtree trips
# `folders_parent_id_fkey` because postgres checks the
# constraint per row, regardless of insertion / list order.
# NULL the parent_id on every folder we're about to delete
# first so the chain breaks cleanly.
# ON DELETE rule. NULL parent_id on every folder that points
# *into* our delete set — that includes children within this
# source root AND any folder under a different SourceRoot whose
# path happens to nest inside this one (e.g. a 'Leóns 1st Year'
# SourceRoot at `.../Taco and Muli - 2024 onward/Leóns 1st Year`
# has folder rows whose parent_id points at folder rows under
# 'Taco and Muli - 2024 onward'). Without this, deleting the
# outer SourceRoot trips folders_parent_id_fkey from the inner
# SourceRoot's still-live rows.
await db.execute(
update(Folder)
.where(Folder.id.in_(folder_ids))
.where(Folder.parent_id.in_(folder_ids))
.values(parent_id=None)
)
await db.execute(delete(Folder).where(Folder.id.in_(folder_ids)))

View File

@@ -1594,9 +1594,17 @@ function NextcloudIntegrationCard() {
variant="ghost"
disabled={removeRoot.isPending}
onClick={() => removeRoot.mutate(r.id)}
title="Remove this library"
title={
removeRoot.isPending && removeRoot.variables === r.id
? 'Removing — cascades through every photo + folder, may take a few seconds'
: 'Remove this library'
}
>
<Trash2 className="h-3 w-3" />
{removeRoot.isPending && removeRoot.variables === r.id ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Trash2 className="h-3 w-3" />
)}
</Button>
</li>
))}