Skip to content

Commit 0d5455d

Browse files
committed
Release v3.10.1
Fix file rename extension bug (#67): Renaming a checked-out file via the client UI no longer removes the file extension when only the base name is changed.
1 parent 81e0f9d commit 0d5455d

File tree

6 files changed

+180
-41
lines changed

6 files changed

+180
-41
lines changed

.cursor/plans/extension-system-architecture-agents.plan.md

Lines changed: 150 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -40,61 +40,74 @@
4040
flowchart TB
4141
subgraph BluePLMSite["blueplm-site repo (Blue Robotics hosted)"]
4242
subgraph Store["Extension Store (marketplace.blueplm.io)"]
43-
StoreAPI[Store API]
44-
StoreDB[(Store Supabase)]
4543
StoreFE[Marketplace Frontend]
44+
StoreAPI["Store API (Hono on Workers)"]
45+
StoreDB[("Store Supabase: publishers, extensions, versions, reports")]
4646
end
47-
MainSite[Main Website]
4847
end
4948
50-
subgraph BluePLMRepo["bluePLM repo (Self-hosted by orgs)"]
51-
subgraph App["BluePLM App (Electron)"]
52-
subgraph MainProc["Main Process"]
53-
ExtManager[Extension Manager]
54-
Watchdog[Watchdog]
55-
IPCHub[IPC Hub]
49+
subgraph BluePLMRepo["bluePLM repo (Self-hosted by organizations)"]
50+
subgraph ElectronApp["BluePLM Desktop App"]
51+
subgraph MainProcess["Main Process"]
52+
ExtRegistry["Extension Registry (lifecycle, discovery)"]
53+
ExtProcessMgr["Process Manager (spawn, terminate)"]
54+
Watchdog["Watchdog (heartbeat, memory, CPU)"]
55+
IPCHub["IPC Hub (message routing)"]
56+
NativeLoader["Native Extension Loader (verified only)"]
5657
end
5758
58-
subgraph ExtProcesses["Extension Processes (one per extension)"]
59-
ExtProcA["Process: Source Files"]
60-
ExtProcB["Process: Change Control"]
61-
ExtProcC["Process: Google Drive"]
59+
subgraph SandboxedProcs["Sandboxed Extension Processes"]
60+
ExtProc1["Process: Extension A (API Bridge + Sandbox)"]
61+
ExtProc2["Process: Extension B (API Bridge + Sandbox)"]
62+
ExtProc3["Process: Extension C (API Bridge + Sandbox)"]
6263
end
6364
64-
Renderer[App Renderer]
65+
subgraph NativeExts["Native Extensions (Main Process)"]
66+
NativeExt1["SolidWorks Integration"]
67+
end
68+
69+
Renderer["App Renderer (React UI)"]
6570
end
6671
6772
subgraph OrgAPI["Organization API Server"]
6873
Router[Request Router]
74+
SchemaMgr["Schema Manager (CREATE/DROP schema)"]
75+
SecretMgr["Secret Manager (AES-256 + audit)"]
6976
70-
subgraph IsolatePools["Per-Extension Isolate Pools"]
71-
PoolA["Pool: Source Files"]
72-
PoolB["Pool: Change Control"]
73-
PoolC["Pool: Google Drive"]
77+
subgraph IsolatePools["V8 Isolate Pools (per-extension)"]
78+
Pool1["Pool: ext-a (2 isolates, 100 req/min)"]
79+
Pool2["Pool: ext-b (2 isolates, 100 req/min)"]
7480
end
7581
76-
subgraph OrgDB["Org Supabase (Modular Schemas)"]
77-
CoreSchema["core schema"]
78-
ExtSFSchema["ext_source_files schema"]
79-
ExtCCSchema["ext_change_control schema"]
82+
subgraph OrgDB["Org Supabase"]
83+
CoreSchema["core schema: orgs, users, installed_extensions"]
84+
ExtSchema1["ext_a schema: extension tables"]
85+
ExtSchema2["ext_b schema: extension tables"]
8086
end
8187
end
8288
end
8389
84-
External[External APIs: Odoo, Google, etc.]
90+
External["External APIs: Google, Odoo, etc."]
8591
86-
StoreFE -->|API calls| StoreAPI
92+
StoreFE -->|"browse/search"| StoreAPI
8793
StoreAPI --> StoreDB
88-
Store -->|Download .bpx| MainProc
89-
IPCHub <-->|"child_process IPC"| ExtProcA
90-
IPCHub <-->|"child_process IPC"| ExtProcB
91-
IPCHub <-->|"child_process IPC"| ExtProcC
92-
MainProc <-->|Electron IPC| Renderer
93-
Watchdog -->|monitor| ExtProcesses
94-
ExtProcesses -->|callOrgApi| OrgAPI
94+
StoreAPI -->|"download .bpx"| ExtRegistry
95+
96+
ExtRegistry -->|"spawn"| ExtProcessMgr
97+
ExtProcessMgr -->|"fork()"| SandboxedProcs
98+
Watchdog -->|"ping/monitor"| SandboxedProcs
99+
IPCHub <-->|"process.send/on"| SandboxedProcs
100+
NativeLoader -->|"require()"| NativeExts
101+
102+
Renderer <-->|"Electron IPC"| MainProcess
103+
104+
SandboxedProcs -->|"callOrgApi"| Router
95105
Router --> IsolatePools
96106
IsolatePools --> OrgDB
97-
IsolatePools --> External
107+
IsolatePools -->|"http.fetch"| External
108+
109+
SchemaMgr -->|"CREATE SCHEMA"| OrgDB
110+
SecretMgr --> CoreSchema
98111
```
99112

100113
> **Repository Split:** The marketplace (Store API, Store Database, Marketplace Frontend) lives in the `blueplm-site` repository, maintained by Blue Robotics. The BluePLM application and Org API live in the `bluePLM` repository, self-hosted by organizations.
@@ -103,6 +116,111 @@ flowchart TB
103116
104117
> **Database Isolation:** Each extension has its own Postgres schema (e.g., `ext_source_files`, `ext_change_control`). Core tables live in the `core` schema. Extensions can reference core tables and optionally enhance other extensions via nullable foreign keys.
105118
119+
### Miro-Friendly Component List
120+
121+
Copy the components below into Miro as shapes/sticky notes. Use the connections section to draw arrows.
122+
123+
```
124+
=== BLUEPLM-SITE REPO (Blue Robotics Hosted) ===
125+
126+
[Marketplace Frontend]
127+
React SPA on Cloudflare Pages
128+
Browse, search, submit extensions
129+
130+
[Store API]
131+
Hono on Cloudflare Workers
132+
REST endpoints for marketplace
133+
134+
[Store Supabase]
135+
publishers, extensions, extension_versions
136+
extension_reports, extension_deprecations
137+
138+
---
139+
140+
=== BLUEPLM REPO (Self-hosted by Organizations) ===
141+
142+
-- ELECTRON MAIN PROCESS --
143+
144+
[Extension Registry]
145+
Singleton managing all extension lifecycle
146+
Discovery, install, activate, deactivate, update, rollback
147+
148+
[Process Manager]
149+
Spawns child_process.fork() per sandboxed extension
150+
Tracks running processes, handles termination
151+
152+
[Watchdog Service]
153+
External monitoring of all extension processes
154+
Heartbeat (5s ping), memory limits, CPU tracking
155+
Kills unresponsive processes
156+
157+
[IPC Hub]
158+
Routes messages between Main and Extension Processes
159+
Request/response correlation, event broadcasting
160+
161+
[Native Extension Loader]
162+
Loads verified-only native extensions directly in Main
163+
For SolidWorks, system integrations
164+
165+
-- EXTENSION PROCESSES (one per sandboxed extension) --
166+
167+
[Extension Process]
168+
Isolated Node.js child_process
169+
Contains: IPC Layer, Heartbeat Responder, API Bridge, Extension Code
170+
Cannot access other extensions or main process memory
171+
172+
-- RENDERER --
173+
174+
[App Renderer]
175+
React UI with Zustand state
176+
Communicates with Main via Electron IPC
177+
178+
-- ORG API SERVER --
179+
180+
[Request Router]
181+
Routes /extensions/:id/* to correct isolate pool
182+
183+
[Schema Manager]
184+
Creates/migrates extension Postgres schemas
185+
Handles soft-disable and hard-delete
186+
187+
[Secret Manager]
188+
AES-256-GCM encrypted secrets
189+
Access audit logging
190+
191+
[V8 Isolate Pool]
192+
Per-extension pool of warm isolates
193+
Independent rate limits, memory limits
194+
195+
-- ORG DATABASE --
196+
197+
[core schema]
198+
organizations, users, teams, permissions
199+
org_installed_extensions, extension_secrets
200+
201+
[ext_{name} schema]
202+
Extension-specific tables
203+
Created on install, preserved on soft-disable
204+
205+
---
206+
207+
=== CONNECTIONS ===
208+
209+
Marketplace Frontend -> Store API: API calls
210+
Store API -> Store Supabase: Database queries
211+
Store API -> Extension Registry: Download .bpx
212+
Extension Registry -> Process Manager: Spawn/terminate
213+
Process Manager -> Extension Process: fork()
214+
Watchdog -> Extension Process: ping/pong heartbeat
215+
IPC Hub <-> Extension Process: process.send/on messages
216+
Renderer <-> Main Process: Electron IPC
217+
Extension Process -> Org API: callOrgApi()
218+
Request Router -> Isolate Pool: Route by extensionId
219+
Isolate Pool -> Org DB: Query extension schema
220+
Isolate Pool -> External APIs: http.fetch (logged)
221+
Schema Manager -> Org DB: CREATE/DROP SCHEMA
222+
```
223+
106224
---
107225

108226
## Extension Database Architecture

CHANGELOG.md

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

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

5+
## [3.10.1] - 2026-01-21
6+
7+
### Fixed
8+
- **File rename drops extension**: Fixed issue where renaming a checked-out file via the client UI would remove the file extension if the user only typed a new base name. Now the original extension is automatically preserved when no extension is provided in the new name (e.g., renaming "PartA.sldprt" to "PartB" now correctly results in "PartB.sldprt")
9+
10+
---
11+
512
## [3.10.0] - 2026-01-16
613

714
### Added

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.10.0",
3+
"version": "3.10.1",
44
"description": "Open-source Product Lifecycle Management",
55
"main": "dist-electron/main.js",
66
"scripts": {

src/components/layout/ActivityBar/ActivityBar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,11 @@ export function ActivityBar() {
177177
<ExpandedContext.Provider value={isExpanded}>
178178
<SidebarRectContext.Provider value={sidebarRect}>
179179
{/* Container with relative positioning for the overlay */}
180-
<div className={`relative flex-shrink-0 transition-[width] duration-200 ${containerWidth}`}>
180+
<div className={`relative flex-shrink-0 transition-[width] duration-200 bg-plm-activitybar ${containerWidth}`}>
181181
{/* Actual activity bar - expands on hover, overlays content */}
182182
<div
183183
ref={sidebarRef}
184-
className={`absolute inset-y-0 left-0 bg-plm-activitybar flex flex-col border-r border-plm-border z-40 transition-[width,box-shadow] duration-200 ease-out ${
184+
className={`absolute inset-y-0 left-0 bg-plm-activitybar flex flex-col z-40 transition-[width,box-shadow] duration-200 ease-out ${
185185
isExpanded ? 'w-64' : 'w-[53px]'
186186
} ${activityBarMode === 'hover' && isExpanded ? 'shadow-xl' : ''}`}
187187
onMouseEnter={() => setIsHovering(true)}

src/components/layout/Sidebar/Sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ export function Sidebar() {
305305

306306
return (
307307
<div
308-
className="bg-plm-sidebar flex flex-col overflow-hidden"
308+
className="bg-plm-sidebar flex flex-col overflow-hidden border-l border-plm-border"
309309
style={{ width: effectiveWidth }}
310310
>
311311
{/* Sidebar header - compact uppercase style for all views */}

src/lib/commands/handlers/fileOps.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import type { Command, RenameParams, MoveParams, CopyParams, NewFolderParams, CommandResult, LocalFile } from '../types'
99
import { ProgressTracker } from '../executor'
1010
import { updateFilePath, updateFolderPath } from '../../supabase'
11+
import { getExtension } from '../../utils/path'
1112

1213
/**
1314
* Rename Command - Rename a file or folder
@@ -39,11 +40,24 @@ export const renameCommand: Command<RenameParams> = {
3940

4041
async execute({ file, newName }, ctx): Promise<CommandResult> {
4142
try {
43+
// Preserve the original file extension if not provided in the new name
44+
// Only applies to files, not directories
45+
let finalName = newName
46+
if (!file.isDirectory) {
47+
const originalExt = getExtension(file.name)
48+
const newNameExt = getExtension(newName)
49+
50+
// If original file had an extension but new name doesn't, append the original extension
51+
if (originalExt && !newNameExt) {
52+
finalName = newName + originalExt
53+
}
54+
}
55+
4256
// Rename locally first
4357
const oldPath = file.path
4458
const sep = file.path.includes('\\') ? '\\' : '/'
4559
const parentDir = oldPath.substring(0, oldPath.lastIndexOf(sep))
46-
const newPath = `${parentDir}${sep}${newName}`
60+
const newPath = `${parentDir}${sep}${finalName}`
4761

4862
const renameResult = await window.electronAPI?.renameItem(oldPath, newPath)
4963
if (!renameResult?.success) {
@@ -59,7 +73,7 @@ export const renameCommand: Command<RenameParams> = {
5973
// Compute new relative path
6074
const oldRelPath = file.relativePath
6175
const relParentDir = oldRelPath.substring(0, oldRelPath.lastIndexOf('/'))
62-
const newRelPath = relParentDir ? `${relParentDir}/${newName}` : newName
76+
const newRelPath = relParentDir ? `${relParentDir}/${finalName}` : finalName
6377

6478
// For synced files, update server path
6579
if (file.pdmData?.id) {
@@ -71,14 +85,14 @@ export const renameCommand: Command<RenameParams> = {
7185
}
7286

7387
// Optimistic UI update: rename in store immediately
74-
ctx.renameFileInStore(oldPath, newPath, newName, false)
88+
ctx.renameFileInStore(oldPath, newPath, finalName, false)
7589

76-
ctx.addToast('success', `Renamed to ${newName}`)
90+
ctx.addToast('success', `Renamed to ${finalName}`)
7791
// No onRefresh needed - UI updates instantly via renameFileInStore
7892

7993
return {
8094
success: true,
81-
message: `Renamed to ${newName}`,
95+
message: `Renamed to ${finalName}`,
8296
total: 1,
8397
succeeded: 1,
8498
failed: 0

0 commit comments

Comments
 (0)