@@ -25,7 +25,6 @@ import { useParams, useRouter } from 'next/navigation'
2525import { useQueryStates } from 'nuqs'
2626import { usePostHog } from 'posthog-js/react'
2727import { getDocumentIcon } from '@/components/icons/document-icons'
28- import { PinButton } from '@/components/pin-button'
2928import { useLimitUpgradeToast } from '@/lib/billing/client'
3029import { captureEvent } from '@/lib/posthog/client'
3130import { triggerArchiveDownload , triggerFileDownload } from '@/lib/uploads/client/download'
@@ -85,7 +84,7 @@ import {
8584} from '@/app/workspace/[workspaceId]/files/search-params'
8685import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
8786import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
88- import { usePinnedIds } from '@/hooks/queries/pinned-items'
87+ import { usePinItem , usePinnedIds , useUnpinItem } from '@/hooks/queries/pinned-items'
8988import { useWorkspaceMembersQuery , type WorkspaceMember } from '@/hooks/queries/workspace'
9089import {
9190 useBulkArchiveWorkspaceFileItems ,
@@ -215,6 +214,10 @@ export function Files() {
215214 const { data : folders = EMPTY_WORKSPACE_FILE_FOLDERS } = useWorkspaceFileFolders ( workspaceId )
216215 const { data : members } = useWorkspaceMembersQuery ( workspaceId )
217216 const pinnedFileIds = usePinnedIds ( workspaceId , 'file' )
217+ // Folders pin under their own resource type, so their pinned set is a separate query.
218+ const pinnedFolderIds = usePinnedIds ( workspaceId , 'folder' )
219+ const pinItem = usePinItem ( )
220+ const unpinItem = useUnpinItem ( )
218221 const membersById = useMemo ( ( ) => {
219222 const map = new Map < string , WorkspaceMember > ( )
220223 for ( const member of members ?? [ ] ) map . set ( member . userId , member )
@@ -398,6 +401,12 @@ export function Files() {
398401 const col = activeSort ?. column ?? 'name'
399402 const dir = activeSort ?. direction ?? 'asc'
400403 return [ ...searched ] . sort ( ( a , b ) => {
404+ // Pinned folders float to the top of every sort/direction — pinning is a
405+ // user-declared priority, not another sort key to be inverted by `desc`.
406+ const aPinned = pinnedFolderIds . has ( a . id )
407+ const bPinned = pinnedFolderIds . has ( b . id )
408+ if ( aPinned !== bPinned ) return aPinned ? - 1 : 1
409+
401410 let cmp = 0
402411 if ( col === 'updated' ) {
403412 cmp = new Date ( a . updatedAt ) . getTime ( ) - new Date ( b . updatedAt ) . getTime ( )
@@ -408,7 +417,7 @@ export function Files() {
408417 }
409418 return dir === 'asc' ? cmp : - cmp
410419 } )
411- } , [ folders , currentFolderId , debouncedSearchTerm , activeSort ] )
420+ } , [ folders , currentFolderId , debouncedSearchTerm , activeSort , pinnedFolderIds ] )
412421
413422 const filteredFiles = useMemo ( ( ) => {
414423 const needle = debouncedSearchTerm . trim ( ) . toLowerCase ( )
@@ -521,15 +530,6 @@ export function Files() {
521530 name : {
522531 icon : < Icon className = 'size-[14px]' /> ,
523532 label : file . name ,
524- endAdornment : (
525- < PinButton
526- workspaceId = { workspaceId }
527- resourceType = 'file'
528- resourceId = { file . id }
529- pinned = { pinnedFileIds . has ( file . id ) }
530- className = 'ml-2'
531- />
532- ) ,
533533 } ,
534534 size : {
535535 label : formatFileSize ( file . size , { includeBytes : true } ) ,
@@ -547,7 +547,7 @@ export function Files() {
547547 } )
548548
549549 return [ ...folderRows , ...fileRows ]
550- } , [ visibleFolders , filteredFiles , membersById , folderSizeMap , workspaceId , pinnedFileIds ] )
550+ } , [ visibleFolders , filteredFiles , membersById , folderSizeMap ] )
551551
552552 const rows : ResourceRow [ ] = useMemo ( ( ) => {
553553 if ( ! listRename . editingId ) return baseRows
@@ -1365,6 +1365,17 @@ export function Files() {
13651365 closeContextMenu ( )
13661366 } , [ selectedRowIds , handleBulkDelete , closeContextMenu ] )
13671367
1368+ const handleContextMenuTogglePin = useCallback ( ( ) => {
1369+ const item = contextMenuItemRef . current
1370+ if ( ! item ) return
1371+ const resourceType = item . kind === 'folder' ? 'folder' : 'file'
1372+ const pinned =
1373+ item . kind === 'folder' ? pinnedFolderIds . has ( item . id ) : pinnedFileIds . has ( item . id )
1374+ const mutation = pinned ? unpinItem : pinItem
1375+ mutation . mutate ( { workspaceId, resourceType, resourceId : item . id } )
1376+ closeContextMenu ( )
1377+ } , [ workspaceId , pinnedFolderIds , pinnedFileIds , closeContextMenu ] )
1378+
13681379 const handleContextMenuMove = useCallback (
13691380 async ( optionValue : string ) => {
13701381 const targetFolderId = optionValue === '__root__' ? null : optionValue
@@ -1985,6 +1996,16 @@ export function Files() {
19851996 )
19861997 }
19871998
1999+ /**
2000+ * Read off the same ref the context-menu handlers use, so the menu's Pin/Unpin label
2001+ * describes the row that was right-clicked. Opening the menu is a state change, so
2002+ * this re-reads on the render that shows it.
2003+ */
2004+ const contextMenuItem = contextMenuItemRef . current
2005+ const isContextMenuItemPinned = contextMenuItem
2006+ ? ( contextMenuItem . kind === 'folder' ? pinnedFolderIds : pinnedFileIds ) . has ( contextMenuItem . id )
2007+ : false
2008+
19882009 return (
19892010 < div
19902011 className = 'relative flex h-full flex-col overflow-hidden'
@@ -2064,11 +2085,9 @@ export function Files() {
20642085 onRename = { handleContextMenuRename }
20652086 onDelete = { handleContextMenuDelete }
20662087 onMove = { handleContextMenuMove }
2067- onShare = {
2068- canEdit && contextMenuItemRef . current ?. kind === 'file'
2069- ? handleContextMenuShare
2070- : undefined
2071- }
2088+ onShare = { canEdit && contextMenuItem ?. kind === 'file' ? handleContextMenuShare : undefined }
2089+ onTogglePin = { handleContextMenuTogglePin }
2090+ pinned = { isContextMenuItemPinned }
20722091 moveOptions = { contextMenuMoveOptions }
20732092 canEdit = { canEdit }
20742093 selectedCount = { selectedRowIds . size }
0 commit comments