fix: heap convert dialog supports nested subfolders + visiblePhotoIds loop guard
- HeapConvertDialog: switch the target picker from sourceFolders.list (top-level source roots only) to useFolderTreeQuery, flattened depth-first into a list with depth info. Each option is indented with non-breaking spaces so nested subfolders read as a tree in the native dropdown. Backend already accepts any Folder id, so no server change needed. - photoStore.setVisiblePhotoIds: short-circuit when the new id list matches the existing one element-for-element. Avoids feedback loops if a publisher fires from an effect on a render where the contents haven't actually changed (which was triggering React error #185). - Timeline: pull setVisiblePhotoIds via a focused selector instead of the wholesale destructure so the publisher subscription doesn't re-render Timeline on unrelated photo store changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,37 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { X, Folder, AlertCircle } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { sourceFolders, heaps as heapsApi, type Heap } from '../../services/api'
|
||||
import {
|
||||
heaps as heapsApi,
|
||||
type Heap,
|
||||
type FolderTreeNode,
|
||||
} from '../../services/api'
|
||||
import { HEAPS_QUERY_KEY } from '../../hooks/useHeapsQuery'
|
||||
import { useFolderTreeQuery } from '../../hooks/useFolderTreeQuery'
|
||||
import { toast } from '../ToastContainer'
|
||||
|
||||
interface FlatFolder {
|
||||
id: string
|
||||
name: string
|
||||
path: string
|
||||
depth: number
|
||||
}
|
||||
|
||||
/** Walk the folder tree depth-first into a flat list with depth info so
|
||||
* the picker can render every node — including nested subfolders — as
|
||||
* one indented option. */
|
||||
function flattenTree(nodes: FolderTreeNode[], depth = 0): FlatFolder[] {
|
||||
const out: FlatFolder[] = []
|
||||
for (const n of nodes) {
|
||||
out.push({ id: n.id, name: n.name, path: n.path, depth })
|
||||
if (n.children.length > 0) {
|
||||
out.push(...flattenTree(n.children, depth + 1))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
interface HeapConvertDialogProps {
|
||||
heap: Heap | null
|
||||
onClose: () => void
|
||||
@@ -23,12 +49,10 @@ export function HeapConvertDialog({ heap, onClose }: HeapConvertDialogProps) {
|
||||
const [deleteHeap, setDeleteHeap] = useState(false)
|
||||
const [subfolderName, setSubfolderName] = useState('')
|
||||
|
||||
const { data: foldersData } = useQuery({
|
||||
queryKey: ['folders'],
|
||||
queryFn: sourceFolders.list,
|
||||
enabled: !!heap,
|
||||
})
|
||||
const folders = foldersData?.folders ?? []
|
||||
// Use the recursive folder tree, not the flat source-root list, so the
|
||||
// user can pick a sub-folder at any depth as the target.
|
||||
const { data: tree = [] } = useFolderTreeQuery()
|
||||
const folders = useMemo<FlatFolder[]>(() => flattenTree(tree), [tree])
|
||||
|
||||
// Default to the first folder when the dialog opens or folders load.
|
||||
useEffect(() => {
|
||||
@@ -77,7 +101,7 @@ export function HeapConvertDialog({ heap, onClose }: HeapConvertDialogProps) {
|
||||
|
||||
if (!heap) return null
|
||||
|
||||
const targetFolder = folders.find((f: any) => f.id === targetId)
|
||||
const targetFolder = folders.find((f) => f.id === targetId)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
@@ -110,9 +134,11 @@ export function HeapConvertDialog({ heap, onClose }: HeapConvertDialogProps) {
|
||||
onChange={(e) => setTargetId(e.target.value)}
|
||||
className="w-full rounded border border-border bg-bg px-2 py-1.5 text-sm text-text focus:border-primary focus:outline-none"
|
||||
>
|
||||
{folders.map((f: any) => (
|
||||
{folders.map((f) => (
|
||||
<option key={f.id} value={f.id}>
|
||||
{f.name}
|
||||
{/* Two non-breaking spaces per depth so nested
|
||||
* subfolders read as a tree in the native dropdown. */}
|
||||
{'\u00A0\u00A0'.repeat(f.depth) + f.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user