Skip to content

Commit 8645136

Browse files
committed
fix(files): close retention races and preserve browser navigation
1 parent 6643909 commit 8645136

10 files changed

Lines changed: 530 additions & 419 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/files/browser.tsx‎

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import {
1111
import { Files } from '@/app/workspace/[workspaceId]/files/files'
1212
import FilesLoading from '@/app/workspace/[workspaceId]/files/loading'
1313
import { ProjectFiles } from '@/app/workspace/[workspaceId]/files/project-files'
14-
import { fileOwnerParsers } from '@/app/workspace/[workspaceId]/files/search-params'
14+
import {
15+
fileOwnerParsers,
16+
filesFilterParsers,
17+
filesFilterUrlKeys,
18+
filesParsers,
19+
projectFilesScopeParsers,
20+
} from '@/app/workspace/[workspaceId]/files/search-params'
1521
import { useWorkspaceProject } from '@/hooks/queries/project-files'
1622

1723
interface FilesBrowserProps {
@@ -75,20 +81,44 @@ interface FileOwnerTabsProps {
7581

7682
function FileOwnerTabs({ owner, projectId, workspaceId }: FileOwnerTabsProps) {
7783
const { navigate } = useFileNavigation(owner)
84+
const { fileId } = useParams<{ fileId?: string }>()
85+
const [, setLocation] = useQueryStates({
86+
...fileOwnerParsers,
87+
...filesParsers,
88+
...projectFilesScopeParsers,
89+
})
90+
const [, setFilters] = useQueryStates(filesFilterParsers, filesFilterUrlKeys)
7891
return (
7992
<TabStrip
8093
variant='underline'
8194
tabs={[
8295
{ id: 'project', title: 'Project', active: owner.entityType === 'project' },
8396
{ id: 'workspace', title: 'Environment', active: owner.entityType === 'workspace' },
8497
]}
85-
onSelect={(next) =>
86-
navigate(
87-
next === 'project'
88-
? `/workspace/${encodeURIComponent(workspaceId)}/files?owner=project&projectId=${encodeURIComponent(projectId)}`
89-
: `/workspace/${encodeURIComponent(workspaceId)}/files`
98+
onSelect={(next) => {
99+
if (next === owner.entityType || (next !== 'project' && next !== 'workspace')) return
100+
if (fileId) {
101+
navigate(
102+
next === 'project'
103+
? `/workspace/${encodeURIComponent(workspaceId)}/files?owner=project&projectId=${encodeURIComponent(projectId)}`
104+
: `/workspace/${encodeURIComponent(workspaceId)}/files`
105+
)
106+
return
107+
}
108+
void setFilters({ uploadedBy: null })
109+
void setLocation(
110+
{
111+
owner: next,
112+
projectId: next === 'project' ? projectId : null,
113+
folderId: null,
114+
shareFileId: null,
115+
historyFileId: null,
116+
new: null,
117+
scope: null,
118+
},
119+
{ history: 'push' }
90120
)
91-
}
121+
}}
92122
/>
93123
)
94124
}

‎apps/sim/app/workspace/[workspaceId]/files/components/file-filter-controls.tsx‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { Button, ChipCombobox, type ComboboxOption } from '@sim/emcn'
3+
import { Chip, ChipCombobox, type ComboboxOption } from '@sim/emcn'
44
import { FILE_BROWSER_SIZE_OPTIONS, FILE_BROWSER_TYPE_OPTIONS } from '@/lib/workspace-files/browser'
55
import {
66
ResourceFilterPanel,
@@ -94,13 +94,9 @@ export function FileFilterControls({
9494
</ResourceFilterSection>
9595
)}
9696
{(types.length > 0 || sizes.length > 0 || creatorIds.length > 0) && (
97-
<Button
98-
variant='ghost'
99-
onClick={onClear}
100-
className='h-[32px] w-full text-caption hover-hover:bg-[var(--surface-active)]'
101-
>
97+
<Chip onClick={onClear} className='w-full'>
10298
Clear all filters
103-
</Button>
99+
</Chip>
104100
)}
105101
</ResourceFilterPanel>
106102
)

‎apps/sim/app/workspace/[workspaceId]/files/project-files.tsx‎

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,10 @@ import { useContextMenu } from '@/hooks/use-context-menu'
7878
import { useDebouncedSearchSetter } from '@/hooks/use-debounced-search-setter'
7979
import { useFileListRoom } from '@/hooks/use-file-list-room'
8080
import { useInlineRename } from '@/hooks/use-inline-rename'
81+
import { useSearchFilterValue } from '@/hooks/use-search-filter-value'
8182
import { useUrlSort } from '@/hooks/use-url-sort'
8283

84+
const FILES_SEARCH_DEBOUNCE_MS = 200
8385
const COLUMNS: ResourceColumn[] = [...FILE_BROWSER_COLUMNS]
8486

8587
interface ProjectFilesProps {
@@ -118,10 +120,11 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
118120
const sort = useUrlSort(filesSortParams, filesFilterUrlKeys)
119121
const [{ scope }, setScope] = useQueryStates(projectFilesScopeParsers)
120122
const archived = scope === 'archived'
123+
const searchFilter = useSearchFilterValue(search, FILES_SEARCH_DEBOUNCE_MS)
121124
const list = useProjectFiles(project.id, {
122125
scope,
123-
folderId: search || archived ? undefined : (folderId ?? undefined),
124-
search: search || undefined,
126+
folderId: searchFilter || archived ? undefined : (folderId ?? undefined),
127+
search: searchFilter || undefined,
125128
types,
126129
sizes,
127130
creatorIds,
@@ -132,6 +135,7 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
132135
const folders = useProjectFileFolders(project.id, scope)
133136
const detail = useProjectFile(project.id, fileId)
134137
const historyFile = useProjectFile(project.id, historyFileId ?? undefined)
138+
const sharedFile = useProjectFile(project.id, shareFileId ?? undefined)
135139
const [copySource, setCopySource] = useState<FileCopySource | null>(null)
136140
const createFile = useCreateProjectFile(project.id)
137141
const createFolder = useCreateProjectFileFolder(project.id)
@@ -162,11 +166,7 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
162166
const filesById = new Map(files.map((file) => [file.id, file]))
163167
const items = list.data?.pages.flatMap((page) => page.items) ?? []
164168
const creators = list.data?.pages[0]?.creators ?? []
165-
const shareFile = shareFileId
166-
? file?.id === shareFileId
167-
? file
168-
: files.find((item) => item.id === shareFileId)
169-
: undefined
169+
const shareFile = sharedFile.isError ? undefined : sharedFile.data?.file
170170
const allFolders = folders.data?.folders ?? []
171171
const folderById = useMemo(
172172
() => new Map(allFolders.map((folder) => [folder.id, folder])),
@@ -188,9 +188,19 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
188188
(value, options) => {
189189
void setFilters({ search: value }, options)
190190
},
191-
{ debounceMs: 200 }
191+
{ debounceMs: FILES_SEARCH_DEBOUNCE_MS }
192192
)
193193

194+
function navigateToFolder(nextFolderId: string | null) {
195+
if (fileId) {
196+
const folderQuery = nextFolderId ? `&folderId=${encodeURIComponent(nextFolderId)}` : ''
197+
navigation.navigate(`${base}?${ownerQuery}${folderQuery}`)
198+
return
199+
}
200+
searchSetter('')
201+
void setNavigation({ folderId: nextFolderId, new: null })
202+
}
203+
194204
function openFile(id: string) {
195205
const target = filesById.get(id)
196206
if (target && isArchiveFileName(target.name) && canWrite && !archived) {
@@ -267,7 +277,7 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
267277
async function addFolder() {
268278
try {
269279
const result = await createFolder.mutateAsync({ name: 'New Folder', parentId: folderId })
270-
void setNavigation({ folderId: result.folder.id })
280+
navigateToFolder(result.folder.id)
271281
} catch (error) {
272282
toast.error(getErrorMessage(error, 'Unable to create this folder'))
273283
}
@@ -321,8 +331,8 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
321331
}
322332

323333
const breadcrumbs: BreadcrumbItem[] = [
324-
{ label: project.name, onClick: () => navigation.navigate(`${base}?${ownerQuery}`) },
325-
{ label: 'Files', folderId: null, onClick: () => navigation.navigate(`${base}?${ownerQuery}`) },
334+
{ label: project.name, onClick: () => navigateToFolder(null) },
335+
{ label: 'Files', folderId: null, onClick: () => navigateToFolder(null) },
326336
]
327337
const ancestors: BreadcrumbItem[] = []
328338
const visited = new Set<string>()
@@ -333,8 +343,7 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
333343
ancestors.unshift({
334344
label: ancestor.name,
335345
folderId: id,
336-
onClick: () =>
337-
navigation.navigate(`${base}?${ownerQuery}&folderId=${encodeURIComponent(id)}`),
346+
onClick: () => navigateToFolder(id),
338347
})
339348
ancestor = ancestor.parentId ? folderById.get(ancestor.parentId) : undefined
340349
}
@@ -435,7 +444,13 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
435444
}
436445
})
437446

438-
const visibleRowIds = useMemo(() => rows.map((row) => row.id), [rows])
447+
const visibleRowIds = useMemo(
448+
() =>
449+
list.data?.pages.flatMap((page) =>
450+
page.items.map((item) => (item.kind === 'folder' ? `folder:${item.id}` : item.id))
451+
) ?? [],
452+
[list.data?.pages]
453+
)
439454
const { selectedRowIds, selectable, clearSelection, replaceSelection } = useResourceRowSelection({
440455
visibleRowIds,
441456
isKeyboardBlocked: () => Boolean(fileId || rename.editingId || deleteTarget || archived),
@@ -475,7 +490,7 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
475490
selection: { selectedRowIds, visibleRowIds, replaceSelection },
476491
onSpringOpenFolder: (folderId, options) => void setNavigation({ folderId }, options),
477492
currentFolderId: folderId,
478-
bodyDropFolderId: search ? undefined : folderId,
493+
bodyDropFolderId: searchFilter ? undefined : folderId,
479494
externalDrop: {
480495
matches: hasExternalFiles,
481496
onDropIntoFolder: (dataTransfer, targetFolderId) => {
@@ -619,7 +634,7 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
619634
archived
620635
? undefined
621636
: id.startsWith('folder:')
622-
? void setNavigation({ folderId: id.slice('folder:'.length) })
637+
? navigateToFolder(id.slice('folder:'.length))
623638
: openFile(id)
624639
}
625640
onRowContextMenu={
@@ -686,7 +701,7 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
686701
position={menu.position}
687702
onClose={menu.closeMenu}
688703
onOpen={() => {
689-
if (contextItem?.kind === 'folder') void setNavigation({ folderId: contextItem.id })
704+
if (contextItem?.kind === 'folder') navigateToFolder(contextItem.id)
690705
else if (contextItem) openFile(contextItem.id)
691706
}}
692707
onShare={

‎apps/sim/app/workspace/[workspaceId]/lib/seed-workspace-files.test.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { emcnMock } from '@sim/testing/mocks/emcn.mock'
21
import { describe, expect, it, vi } from 'vitest'
32
import { workspaceFilesKeys } from '@/hooks/queries/utils/workspace-file-query'
43

@@ -10,9 +9,6 @@ vi.mock('@/lib/workspace-files/queries', () => ({
109
listWorkspaceFilesWithShares: mockListWorkspaceFilesWithShares,
1110
}))
1211

13-
/** The key factory lives in a `'use client'` module that pulls emcn's CSS at import. */
14-
vi.mock('@sim/emcn', () => emcnMock)
15-
1612
import { seedWorkspaceFiles } from '@/app/workspace/[workspaceId]/lib/seed-workspace-files'
1713

1814
const WORKSPACE_ID = 'ws-123'

0 commit comments

Comments
 (0)