mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
Move expand/collapse to header bar, dim tree lines, increase gap
- Lift expandedVersionIds state to DeletedFilesContext (shared between DeletedFilesList and FileExplorerHeader) - Move 'Expand all' / 'Collapse all' buttons from the table header into the FileExplorerHeader top bar with outline button variant - Change tree-connector line color from bg-foreground-muted to bg-border-stronger for a subtler appearance - Increase gap between horizontal tree branch and version title text (pl-[22px] → pl-[36px], w-[11px] → w-[8px]) - Apply same tree line changes to standalone Trash page (TrashList) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
This commit is contained in:
4 files changed
+69
-43
No files matched your search
@@ -1,5 +1,13 @@
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { createContext, useCallback, useContext, useState, type ReactNode } from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
} from 'react'
|
||||
|
||||
import {
|
||||
type DeletedObjectVersion,
|
||||
@@ -24,6 +32,8 @@ interface DeletedFilesContextValue {
|
||||
lastToggledId: string | null
|
||||
setLastToggledId: (id: string | null) => void
|
||||
clearDeletedSelection: () => void
|
||||
expandedVersionIds: Set<string>
|
||||
setExpandedVersionIds: Dispatch<SetStateAction<Set<string>>>
|
||||
}
|
||||
|
||||
const DeletedFilesContext = createContext<DeletedFilesContextValue>({
|
||||
@@ -39,6 +49,8 @@ const DeletedFilesContext = createContext<DeletedFilesContextValue>({
|
||||
lastToggledId: null,
|
||||
setLastToggledId: () => {},
|
||||
clearDeletedSelection: () => {},
|
||||
expandedVersionIds: new Set(),
|
||||
setExpandedVersionIds: () => {},
|
||||
})
|
||||
|
||||
export const useDeletedFilesContext = () => useContext(DeletedFilesContext)
|
||||
@@ -59,6 +71,7 @@ export const DeletedFilesProvider = ({ enabled, children }: DeletedFilesProvider
|
||||
>()
|
||||
const [selectedDeletedIds, setSelectedDeletedIds] = useState<string[]>([])
|
||||
const [lastToggledId, setLastToggledId] = useState<string | null>(null)
|
||||
const [expandedVersionIds, setExpandedVersionIds] = useState<Set<string>>(new Set())
|
||||
|
||||
const clearDeletedSelection = useCallback(() => {
|
||||
setSelectedDeletedIds([])
|
||||
@@ -80,6 +93,8 @@ export const DeletedFilesProvider = ({ enabled, children }: DeletedFilesProvider
|
||||
lastToggledId,
|
||||
setLastToggledId,
|
||||
clearDeletedSelection,
|
||||
expandedVersionIds,
|
||||
setExpandedVersionIds,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -59,6 +59,8 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
|
||||
setSelectedDeletedIds,
|
||||
lastToggledId,
|
||||
setLastToggledId,
|
||||
expandedVersionIds,
|
||||
setExpandedVersionIds,
|
||||
} = useDeletedFilesContext()
|
||||
const { can: canUpdateFiles } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*')
|
||||
|
||||
@@ -67,8 +69,6 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
|
||||
parentObject: TrashObject
|
||||
version: DeletedObjectVersion
|
||||
}>()
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
|
||||
|
||||
const {
|
||||
data: objects,
|
||||
isPending,
|
||||
@@ -173,7 +173,7 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
|
||||
|
||||
const orderedIds = filtered.flatMap((o) => {
|
||||
const ids = [o.id]
|
||||
if (expandedIds.has(o.id) && o.noncurrentVersions) {
|
||||
if (expandedVersionIds.has(o.id) && o.noncurrentVersions) {
|
||||
for (const v of o.noncurrentVersions) {
|
||||
ids.push(versionKey(o.id, v.versionId))
|
||||
}
|
||||
@@ -223,15 +223,8 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
|
||||
setVersionToDelete({ parentObject: parent, version })
|
||||
}
|
||||
|
||||
// ── Expand / collapse all ───────────────────────────────────────────
|
||||
|
||||
const expandableIds = filtered
|
||||
.filter((o) => o.noncurrentVersions && o.noncurrentVersions.length > 0)
|
||||
.map((o) => o.id)
|
||||
const isAllExpanded = expandableIds.length > 0 && expandableIds.every((id) => expandedIds.has(id))
|
||||
|
||||
const toggleExpanded = (id: string) => {
|
||||
setExpandedIds((prev) => {
|
||||
setExpandedVersionIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) {
|
||||
next.delete(id)
|
||||
@@ -242,9 +235,6 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
|
||||
})
|
||||
}
|
||||
|
||||
const expandAll = () => setExpandedIds(new Set(expandableIds))
|
||||
const collapseAll = () => setExpandedIds(new Set())
|
||||
|
||||
const totalVersionCount = (object: TrashObject) => {
|
||||
return 1 + (object.noncurrentVersions?.length ?? 0)
|
||||
}
|
||||
@@ -278,25 +268,7 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
|
||||
<TableHead>Original location</TableHead>
|
||||
<TableHead>Deleted</TableHead>
|
||||
<TableHead className="text-right">Size</TableHead>
|
||||
<TableHead className="text-right">
|
||||
{expandableIds.length > 0 && (
|
||||
<div className="flex items-center justify-end gap-x-2 text-xs font-normal">
|
||||
<button
|
||||
className="text-foreground-lighter hover:text-foreground transition-colors"
|
||||
onClick={expandAll}
|
||||
>
|
||||
Expand all
|
||||
</button>
|
||||
<span className="text-border-stronger">·</span>
|
||||
<button
|
||||
className="text-foreground-lighter hover:text-foreground transition-colors"
|
||||
onClick={collapseAll}
|
||||
>
|
||||
Collapse all
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</TableHead>
|
||||
<TableHead />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -306,7 +278,7 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
|
||||
selectedDeletedFile?.id === object.id && selectedDeletedVersion === undefined
|
||||
const hasVersions =
|
||||
object.noncurrentVersions !== undefined && object.noncurrentVersions.length > 0
|
||||
const isExpanded = expandedIds.has(object.id)
|
||||
const isExpanded = expandedVersionIds.has(object.id)
|
||||
|
||||
return (
|
||||
<Fragment key={object.id}>
|
||||
@@ -540,14 +512,14 @@ const NoncurrentVersionRow = ({
|
||||
{/* Tree connector: vertical line */}
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-[23px] w-px bg-foreground-muted pointer-events-none',
|
||||
'absolute left-[23px] w-px bg-border-stronger pointer-events-none',
|
||||
isLast ? 'top-0 h-1/2' : 'inset-y-0'
|
||||
)}
|
||||
/>
|
||||
{/* Tree connector: horizontal branch */}
|
||||
<div className="absolute left-[23px] top-1/2 h-px w-[11px] -translate-y-px bg-foreground-muted pointer-events-none" />
|
||||
{/* Content aligned with parent row text */}
|
||||
<div className="flex items-center gap-x-2 pl-[22px]">
|
||||
<div className="absolute left-[23px] top-1/2 h-px w-[8px] -translate-y-px bg-border-stronger pointer-events-none" />
|
||||
{/* Content aligned after horizontal branch with gap */}
|
||||
<div className="flex items-center gap-x-2 pl-[36px]">
|
||||
<span className="text-foreground-lighter font-mono text-xs">{shortId}</span>
|
||||
<span className="text-foreground-muted text-xs">({version.action})</span>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { compact, isEqual, noop } from 'lodash'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Check,
|
||||
ChevronsDownUp,
|
||||
ChevronsUpDown,
|
||||
Columns,
|
||||
Edit2,
|
||||
FolderPlus,
|
||||
@@ -50,6 +52,8 @@ import { useTrack } from '@/lib/telemetry/track'
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
import { useStorageExplorerStateSnapshot } from '@/state/storage-explorer'
|
||||
|
||||
import { useBucketTrashQuery } from '@/data/storage/protection/bucket-trash-query'
|
||||
|
||||
import { useSelectedBucket } from '../FilesBuckets/useSelectedBucket'
|
||||
import { STORAGE_SORT_BY, STORAGE_SORT_BY_ORDER, STORAGE_VIEWS } from '../Storage.constants'
|
||||
import { hasVersioningHistory } from '../StorageProtection.constants'
|
||||
@@ -293,7 +297,19 @@ export const FileExplorerHeader = ({
|
||||
|
||||
const { data: bucket } = useSelectedBucket()
|
||||
const isVersioned = hasVersioningHistory(bucket?.id)
|
||||
const { isShowingDeleted, setIsShowingDeleted } = useDeletedFilesContext()
|
||||
const { isShowingDeleted, setIsShowingDeleted, expandedVersionIds, setExpandedVersionIds } =
|
||||
useDeletedFilesContext()
|
||||
|
||||
const { data: trashObjects } = useBucketTrashQuery({
|
||||
projectRef: projectRef ?? undefined,
|
||||
bucketId: bucket?.id,
|
||||
})
|
||||
const expandableIds = (trashObjects ?? [])
|
||||
.filter((o) => o.noncurrentVersions && o.noncurrentVersions.length > 0)
|
||||
.map((o) => o.id)
|
||||
const hasExpandableVersions = expandableIds.length > 0
|
||||
const isAllExpanded =
|
||||
hasExpandableVersions && expandableIds.every((id) => expandedVersionIds.has(id))
|
||||
|
||||
const refreshData = async () => {
|
||||
await refreshAll()
|
||||
@@ -454,6 +470,29 @@ export const FileExplorerHeader = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isShowingDeleted && hasExpandableVersions && (
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="outline"
|
||||
icon={<ChevronsUpDown size={14} />}
|
||||
onClick={() => setExpandedVersionIds(new Set(expandableIds))}
|
||||
disabled={isAllExpanded}
|
||||
>
|
||||
Expand all
|
||||
</Button>
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="outline"
|
||||
icon={<ChevronsDownUp size={14} />}
|
||||
onClick={() => setExpandedVersionIds(new Set())}
|
||||
disabled={expandedVersionIds.size === 0}
|
||||
>
|
||||
Collapse all
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isShowingDeleted && (
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<div className="hidden">
|
||||
|
||||
@@ -231,14 +231,14 @@ const NoncurrentVersionRow = ({
|
||||
{/* Tree connector: vertical line */}
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-[23px] w-px bg-foreground-muted pointer-events-none',
|
||||
'absolute left-[23px] w-px bg-border-stronger pointer-events-none',
|
||||
isLast ? 'top-0 h-1/2' : 'inset-y-0'
|
||||
)}
|
||||
/>
|
||||
{/* Tree connector: horizontal branch */}
|
||||
<div className="absolute left-[23px] top-1/2 h-px w-[11px] -translate-y-px bg-foreground-muted pointer-events-none" />
|
||||
<div className="absolute left-[23px] top-1/2 h-px w-[8px] -translate-y-px bg-border-stronger pointer-events-none" />
|
||||
{/* Content aligned with parent row text */}
|
||||
<div className="flex items-center gap-x-2 pl-[22px]">
|
||||
<div className="flex items-center gap-x-2 pl-[36px]">
|
||||
<span className="text-foreground-lighter font-mono text-xs">{shortId}</span>
|
||||
<span className="text-foreground-muted text-xs">({version.action})</span>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user