Skip to content

Commit 23599eb

Browse files
v3.13.5 - copy file name: context menu + slow double-click highlight
- Add Copy Name to right-click context menu (works for all file states including cloud-only, supports multi-select) - Slow double-click on checked-in files highlights name as selectable text for copying Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 679550e commit 23599eb

File tree

9 files changed

+144
-12
lines changed

9 files changed

+144
-12
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to BluePLM will be documented in this file.
44

5+
## [3.13.5] - 2026-02-09
6+
7+
### Added
8+
- **Copy file name from context menu**: Right-click any file and choose "Copy Name" to copy just the filename to clipboard. Works for all file states including cloud-only files, and supports multi-select
9+
- **Slow double-click highlights name for copying**: On checked-in files that can't be renamed, a slow double-click now shows the filename as selectable text so you can highlight and Ctrl+C to copy. Useful for toolbox parts with long names where the new part is very similar
10+
11+
---
12+
513
## [3.13.4] - 2026-02-09
614

715
### Fixed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "blue-plm",
3-
"version": "3.13.4",
3+
"version": "3.13.5",
44
"description": "Open-source Product Lifecycle Management",
55
"main": "dist-electron/main.js",
66
"scripts": {

src/features/source/browser/FilePane.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,11 +268,14 @@ export function FilePane({ onRefresh, onRefreshFolder }: FilePaneProps) {
268268
folderConflictDialog, setFolderConflictDialog
269269
} = useDialogState()
270270

271-
// Rename and inline editing state (file rename, new folder, cell editing)
271+
// Rename and inline editing state (file rename, new folder, cell editing, name highlighting)
272272
const {
273273
renamingFile, setRenamingFile,
274274
renameValue, setRenameValue,
275275
renameInputRef,
276+
highlightingFile, setHighlightingFile,
277+
highlightInputRef,
278+
startHighlight,
276279
isCreatingFolder, setIsCreatingFolder,
277280
newFolderName, setNewFolderName,
278281
newFolderInputRef,
@@ -837,8 +840,10 @@ export function FilePane({ onRefresh, onRefreshFolder }: FilePaneProps) {
837840
})
838841

839842
// Slow double-click to rename (Windows Explorer-style)
843+
// For non-renamable files (checked in), highlights name for copying instead
840844
const { handleSlowDoubleClick, resetSlowDoubleClick } = useSlowDoubleClick({
841845
onRename: startRenaming,
846+
onHighlight: startHighlight,
842847
canRename: (file) => {
843848
// Can rename if: not synced OR checked out by current user
844849
const isSynced = !!file.pdmData
@@ -1328,6 +1333,9 @@ export function FilePane({ onRefresh, onRefreshFolder }: FilePaneProps) {
13281333
renameValue,
13291334
setRenameValue,
13301335
renameInputRef,
1336+
highlightingFile,
1337+
setHighlightingFile,
1338+
highlightInputRef,
13311339
isCreatingFolder,
13321340
setIsCreatingFolder,
13331341
newFolderName,

src/features/source/browser/components/ContextMenu/FileContextMenu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ export function FileContextMenu({
206206
}
207207
}
208208

209-
// Check if we have items for the File Actions submenu
210-
const hasFileSystemActions = !allCloudOnly
209+
// Check if we have items for the File Actions submenu (always true - Copy Name works for all files)
210+
const hasFileSystemActions = true
211211

212212
// Check if we have export actions (any SolidWorks file in selection)
213213
const swExtensions = ['.sldprt', '.sldasm', '.slddrw']

src/features/source/browser/components/ContextMenu/actions/FileSystemActions.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,27 @@ export function FileSystemActions({
3535
const allCloudOnly = contextFiles.every(f => f.diffStatus === 'cloud')
3636
const isFolder = firstFile.isDirectory
3737

38-
// Don't show for cloud-only files
38+
// Copy Name is always available regardless of file status
39+
const copyNameItem = (
40+
<div
41+
className="context-menu-item"
42+
onClick={async () => {
43+
const names = contextFiles.map(f => f.name).join('\n')
44+
const result = await copyToClipboard(names)
45+
if (result.success) {
46+
addToast('success', `Copied ${contextFiles.length > 1 ? contextFiles.length + ' names' : 'name'} to clipboard`)
47+
}
48+
onClose()
49+
}}
50+
>
51+
<Copy size={14} />
52+
Copy Name{multiSelect ? 's' : ''}
53+
</div>
54+
)
55+
56+
// For cloud-only files, only show Copy Name
3957
if (allCloudOnly) {
40-
return null
58+
return copyNameItem
4159
}
4260

4361
// Check rename eligibility
@@ -74,6 +92,9 @@ export function FileSystemActions({
7492
{platform === 'darwin' ? 'Reveal in Finder' : 'Show in Explorer'}
7593
</div>
7694

95+
{/* Copy Name(s) - always available */}
96+
{copyNameItem}
97+
7798
{/* Copy Path(s) */}
7899
<div
79100
className="context-menu-item"

src/features/source/browser/components/FileList/cells/NameCell.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* - useFilePaneContext() for UI state
66
* - useFilePaneHandlers() for action handlers
77
*/
8+
import { useEffect } from 'react'
89
import {
910
ChevronDown,
1011
ChevronRight,
@@ -40,6 +41,9 @@ export function NameCell({ file }: CellRendererBaseProps): React.ReactNode {
4041
renameInputRef,
4142
setRenameValue,
4243
setRenamingFile,
44+
highlightingFile,
45+
setHighlightingFile,
46+
highlightInputRef,
4347
expandedConfigFiles,
4448
loadingConfigs,
4549
folderMetrics,
@@ -79,10 +83,19 @@ export function NameCell({ file }: CellRendererBaseProps): React.ReactNode {
7983

8084
const isSynced = !!file.pdmData
8185
const isBeingRenamed = renamingFile?.path === file.path
86+
const isBeingHighlighted = highlightingFile?.path === file.path
8287

8388
// Icon size scales with row size, but has a minimum of 16
8489
const iconSize = Math.max(16, listRowSize - 8)
8590

91+
// Auto-select text when entering highlight mode
92+
useEffect(() => {
93+
if (isBeingHighlighted && highlightInputRef?.current) {
94+
highlightInputRef.current.focus()
95+
highlightInputRef.current.select()
96+
}
97+
}, [isBeingHighlighted, highlightInputRef])
98+
8699
// Rename mode
87100
if (isBeingRenamed) {
88101
const renameIconSize = Math.max(16, listRowSize - 8)
@@ -119,6 +132,39 @@ export function NameCell({ file }: CellRendererBaseProps): React.ReactNode {
119132
)
120133
}
121134

135+
// Highlight mode - read-only name selection for copying (shown on slow double-click of non-renamable files)
136+
if (isBeingHighlighted) {
137+
const highlightIconSize = Math.max(16, listRowSize - 8)
138+
return (
139+
<div className="flex items-center gap-2" style={{ minHeight: listRowSize }}>
140+
<ListRowIcon
141+
file={file}
142+
size={highlightIconSize}
143+
folderCheckoutStatus={file.isDirectory ? getFolderCheckoutStatus(file.relativePath) : undefined}
144+
isFolderSynced={file.isDirectory ? isFolderSynced(file.relativePath) : undefined}
145+
/>
146+
<input
147+
ref={highlightInputRef}
148+
type="text"
149+
readOnly
150+
value={file.name}
151+
onKeyDown={(e) => {
152+
if (e.key === 'Escape') {
153+
setHighlightingFile(null)
154+
}
155+
e.stopPropagation()
156+
}}
157+
onBlur={() => setHighlightingFile(null)}
158+
onClick={(e) => e.stopPropagation()}
159+
onMouseDown={(e) => e.stopPropagation()}
160+
onDragStart={(e) => e.preventDefault()}
161+
draggable={false}
162+
className="flex-1 bg-plm-bg border border-plm-border rounded px-2 py-0.5 text-sm text-plm-fg focus:outline-none focus:ring-1 focus:ring-plm-border select-text cursor-text"
163+
/>
164+
</div>
165+
)
166+
}
167+
122168
const fileStatusColumnVisible = columns.find(c => c.id === 'fileStatus')?.visible
123169

124170
// Format filename with lowercase extension if setting is on

src/features/source/browser/context/FilePaneContext.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ export interface FilePaneContextValue {
6262
renameValue: string
6363
setRenameValue: (value: string) => void
6464

65+
// Highlight state (read-only name selection for copying)
66+
highlightingFile: LocalFile | null
67+
setHighlightingFile: (file: LocalFile | null) => void
68+
6569
// Delete state
6670
deleteConfirm: LocalFile | null
6771
setDeleteConfirm: (file: LocalFile | null) => void
@@ -134,6 +138,7 @@ export interface FilePaneContextValue {
134138
tableRef: React.RefObject<HTMLDivElement | null>
135139
contextMenuRef: React.RefObject<HTMLDivElement | null>
136140
renameInputRef: React.RefObject<HTMLInputElement | null>
141+
highlightInputRef: React.RefObject<HTMLInputElement | null>
137142
newFolderInputRef: React.RefObject<HTMLInputElement | null>
138143
inlineEditInputRef: React.RefObject<HTMLInputElement | null>
139144

@@ -155,6 +160,9 @@ export interface FilePaneProviderProps {
155160
renameValue: string
156161
setRenameValue: (value: string) => void
157162
renameInputRef: React.RefObject<HTMLInputElement | null>
163+
highlightingFile: LocalFile | null
164+
setHighlightingFile: (file: LocalFile | null) => void
165+
highlightInputRef: React.RefObject<HTMLInputElement | null>
158166
isCreatingFolder: boolean
159167
setIsCreatingFolder: (creating: boolean) => void
160168
newFolderName: string
@@ -230,11 +238,13 @@ export function FilePaneProvider({
230238
// This avoids duplicate state between useRenameState hook and context
231239
const [localRenamingFile, setLocalRenamingFile] = useState<LocalFile | null>(null)
232240
const [localRenameValue, setLocalRenameValue] = useState('')
241+
const [localHighlightingFile, setLocalHighlightingFile] = useState<LocalFile | null>(null)
233242
const [localIsCreatingFolder, setLocalIsCreatingFolder] = useState(false)
234243
const [localNewFolderName, setLocalNewFolderName] = useState('')
235244
const [localEditingCell, setLocalEditingCell] = useState<{ path: string; column: string } | null>(null)
236245
const [localEditValue, setLocalEditValue] = useState('')
237246
const localRenameInputRef = useRef<HTMLInputElement>(null)
247+
const localHighlightInputRef = useRef<HTMLInputElement>(null)
238248
const localNewFolderInputRef = useRef<HTMLInputElement>(null)
239249
const localInlineEditInputRef = useRef<HTMLInputElement>(null)
240250

@@ -243,6 +253,8 @@ export function FilePaneProvider({
243253
const setRenamingFile = renameState?.setRenamingFile ?? setLocalRenamingFile
244254
const renameValue = renameState?.renameValue ?? localRenameValue
245255
const setRenameValue = renameState?.setRenameValue ?? setLocalRenameValue
256+
const highlightingFile = renameState?.highlightingFile ?? localHighlightingFile
257+
const setHighlightingFile = renameState?.setHighlightingFile ?? setLocalHighlightingFile
246258
const isCreatingFolder = renameState?.isCreatingFolder ?? localIsCreatingFolder
247259
const setIsCreatingFolder = renameState?.setIsCreatingFolder ?? setLocalIsCreatingFolder
248260
const newFolderName = renameState?.newFolderName ?? localNewFolderName
@@ -252,6 +264,7 @@ export function FilePaneProvider({
252264
const editValue = renameState?.editValue ?? localEditValue
253265
const setEditValue = renameState?.setEditValue ?? setLocalEditValue
254266
const renameInputRef = renameState?.renameInputRef ?? localRenameInputRef
267+
const highlightInputRef = renameState?.highlightInputRef ?? localHighlightInputRef
255268
const newFolderInputRef = renameState?.newFolderInputRef ?? localNewFolderInputRef
256269
const inlineEditInputRef = renameState?.inlineEditInputRef ?? localInlineEditInputRef
257270

@@ -341,6 +354,9 @@ export function FilePaneProvider({
341354
renamingFile, setRenamingFile,
342355
renameValue, setRenameValue,
343356

357+
// Highlight (read-only name selection)
358+
highlightingFile, setHighlightingFile,
359+
344360
// Delete
345361
deleteConfirm, setDeleteConfirm,
346362
deleteEverywhere, setDeleteEverywhere,
@@ -394,6 +410,7 @@ export function FilePaneProvider({
394410
tableRef,
395411
contextMenuRef,
396412
renameInputRef,
413+
highlightInputRef,
397414
newFolderInputRef,
398415
inlineEditInputRef,
399416

@@ -405,7 +422,7 @@ export function FilePaneProvider({
405422
contextMenu, emptyContextMenu, columnContextMenu, configContextMenu,
406423
isDraggingOver, isExternalDrag, dragOverFolder, draggedFiles,
407424
selectionBox, lastClickedIndex,
408-
renamingFile, renameValue,
425+
renamingFile, renameValue, highlightingFile,
409426
deleteConfirm, deleteEverywhere,
410427
customConfirm, deleteLocalCheckoutConfirm, conflictDialog,
411428
resizingColumn, draggingColumn, dragOverColumn,

src/features/source/browser/hooks/useRenameState.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export interface UseRenameStateReturn {
99
setRenameValue: (value: string) => void
1010
renameInputRef: React.RefObject<HTMLInputElement | null>
1111

12+
// Highlighting file name (read-only selection for copying)
13+
highlightingFile: LocalFile | null
14+
setHighlightingFile: (file: LocalFile | null) => void
15+
highlightInputRef: React.RefObject<HTMLInputElement | null>
16+
1217
// New folder creation
1318
isCreatingFolder: boolean
1419
setIsCreatingFolder: (creating: boolean) => void
@@ -26,6 +31,8 @@ export interface UseRenameStateReturn {
2631
// Helper functions
2732
startRename: (file: LocalFile) => void
2833
cancelRename: () => void
34+
startHighlight: (file: LocalFile) => void
35+
cancelHighlight: () => void
2936
startNewFolder: () => void
3037
cancelNewFolder: () => void
3138
startCellEdit: (path: string, column: string, currentValue: string) => void
@@ -41,6 +48,10 @@ export function useRenameState(): UseRenameStateReturn {
4148
const [renameValue, setRenameValue] = useState('')
4249
const renameInputRef = useRef<HTMLInputElement | null>(null)
4350

51+
// Highlighting file name (read-only selection for copying)
52+
const [highlightingFile, setHighlightingFile] = useState<LocalFile | null>(null)
53+
const highlightInputRef = useRef<HTMLInputElement | null>(null)
54+
4455
// New folder creation
4556
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
4657
const [newFolderName, setNewFolderName] = useState('')
@@ -65,6 +76,14 @@ export function useRenameState(): UseRenameStateReturn {
6576
setRenameValue('')
6677
}, [])
6778

79+
const startHighlight = useCallback((file: LocalFile) => {
80+
setHighlightingFile(file)
81+
}, [])
82+
83+
const cancelHighlight = useCallback(() => {
84+
setHighlightingFile(null)
85+
}, [])
86+
6887
const startNewFolder = useCallback(() => {
6988
setIsCreatingFolder(true)
7089
setNewFolderName('')
@@ -91,6 +110,9 @@ export function useRenameState(): UseRenameStateReturn {
91110
renameValue,
92111
setRenameValue,
93112
renameInputRef,
113+
highlightingFile,
114+
setHighlightingFile,
115+
highlightInputRef,
94116
isCreatingFolder,
95117
setIsCreatingFolder,
96118
newFolderName,
@@ -103,6 +125,8 @@ export function useRenameState(): UseRenameStateReturn {
103125
inlineEditInputRef,
104126
startRename,
105127
cancelRename,
128+
startHighlight,
129+
cancelHighlight,
106130
startNewFolder,
107131
cancelNewFolder,
108132
startCellEdit,

src/hooks/useSlowDoubleClick.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export const SLOW_DOUBLE_CLICK_MAX_MS = 1500 // Maximum time between clicks
3232
export interface UseSlowDoubleClickOptions {
3333
/** Callback when slow double-click triggers rename */
3434
onRename: (file: LocalFile) => void
35+
/** Callback when slow double-click detected but file can't be renamed (e.g., highlight name for copying) */
36+
onHighlight?: (file: LocalFile) => void
3537
/** Check if file can be renamed (e.g., not locked by another user) */
3638
canRename?: (file: LocalFile) => boolean
3739
/** Minimum time between clicks in ms (default: 400) */
@@ -56,6 +58,7 @@ export interface UseSlowDoubleClickReturn {
5658
*/
5759
export function useSlowDoubleClick({
5860
onRename,
61+
onHighlight,
5962
canRename,
6063
minDelay = SLOW_DOUBLE_CLICK_MIN_MS,
6164
maxDelay = SLOW_DOUBLE_CLICK_MAX_MS,
@@ -91,16 +94,21 @@ export function useSlowDoubleClick({
9194
// Check if file can be renamed
9295
const fileCanRename = canRename ? canRename(file) : true
9396

94-
// Detect slow double-click: same file, within timing window, and can be renamed
95-
if (isSameFile && timeDiff >= minDelay && timeDiff <= maxDelay && fileCanRename) {
97+
// Detect slow double-click: same file, within timing window
98+
if (isSameFile && timeDiff >= minDelay && timeDiff <= maxDelay) {
9699
// Clear any pending timeout
97100
if (timeoutRef.current) {
98101
clearTimeout(timeoutRef.current)
99102
timeoutRef.current = null
100103
}
101104

102-
// Trigger rename
103-
onRename(file)
105+
if (fileCanRename) {
106+
// Trigger rename
107+
onRename(file)
108+
} else if (onHighlight) {
109+
// Can't rename - highlight name for copying instead
110+
onHighlight(file)
111+
}
104112

105113
// Reset state
106114
setLastClickTime(0)
@@ -120,7 +128,7 @@ export function useSlowDoubleClick({
120128
timeoutRef.current = null
121129
}, maxDelay + 100) // Small buffer
122130
}
123-
}, [lastClickTime, lastClickPath, onRename, canRename, minDelay, maxDelay, allowDirectories])
131+
}, [lastClickTime, lastClickPath, onRename, onHighlight, canRename, minDelay, maxDelay, allowDirectories])
124132

125133
const resetSlowDoubleClick = useCallback(() => {
126134
// Clear any pending timeout

0 commit comments

Comments
 (0)