feat(library-manager): read-only libraries.json file + drop Uninstall for shared index libs

(1) The explorer's per-board manifest entry is renamed velxio.json -> libraries.json
and clicking it now opens a READ-ONLY JSON view of that board's declared libraries
(board.libraries) in the editor, instead of the modal. New editor state
manifestViewBoardId: when set, CodeEditor renders a read-only Monaco showing
{libraries:[...]} live; opening/activating any real file clears it. No file is
added to the workspace, so nothing touches compile or save. Library actions are
done in the Library Manager modal (toolbar button).

(2) Drop the 'Uninstall' button for shared index/cache libraries — you can't
uninstall a copy everyone shares (content-addressed cache). Only your own custom
.zip uploads keep a 'Remove' (per-user store). Index libs: just Add to / In project.
This commit is contained in:
David Montero 2026-06-09 15:49:14 +02:00
parent 02c8ad756d
commit c580a7e418
4 changed files with 62 additions and 32 deletions

View File

@ -1,5 +1,6 @@
import Editor from '@monaco-editor/react';
import { useEditorStore } from '../../store/useEditorStore';
import { useSimulatorStore } from '../../store/useSimulatorStore';
import { registerRetroAsm, LANGUAGE_ID as RETRO_ASM_ID } from './retroAsmLanguage';
function getLanguage(filename: string): string {
@ -14,9 +15,40 @@ function getLanguage(filename: string): string {
}
export const CodeEditor = () => {
const { files, activeFileId, setFileContent, theme, fontSize } = useEditorStore();
const { files, activeFileId, setFileContent, theme, fontSize, manifestViewBoardId } =
useEditorStore();
const boards = useSimulatorStore((s) => s.boards);
const activeFile = files.find((f) => f.id === activeFileId);
// READ-ONLY libraries.json view (the file explorer's libraries.json entry).
// Shows the active board's declared library manifest as plain-text JSON, live.
// It is read-only on purpose: adding/removing libraries is done from the
// Library Manager modal, which edits board.libraries (this just reflects it).
if (manifestViewBoardId) {
const b = boards.find((x) => x.id === manifestViewBoardId);
const content = JSON.stringify({ libraries: b?.libraries ?? [] }, null, 2);
return (
<div style={{ height: '100%', width: '100%' }}>
<Editor
key="__libraries_json__"
height="100%"
language="json"
theme={theme}
value={content}
options={{
readOnly: true,
domReadOnly: true,
minimap: { enabled: false },
fontSize,
automaticLayout: true,
scrollBeyondLastLine: false,
wordWrap: 'on',
}}
/>
</div>
);
}
return (
<div style={{ height: '100%', width: '100%' }}>
<Editor

View File

@ -298,6 +298,8 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
deleteFile,
renameFile,
setActiveGroup,
manifestViewBoardId,
setManifestView,
} = useEditorStore();
const boards = useSimulatorStore((s) => s.boards);
const activeBoardId = useSimulatorStore((s) => s.activeBoardId);
@ -741,17 +743,21 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
Clicking switches to the board and opens the Library
Manager on its list. */}
<div
className="file-explorer-item fe-file-item"
className={`file-explorer-item fe-file-item${
manifestViewBoardId === board.id ? ' file-explorer-item-active' : ''
}`}
onClick={() => {
switchToBoard(board.id, groupId);
window.dispatchEvent(new CustomEvent('velxio-open-library-manager'));
// Open the READ-ONLY libraries.json view (not the modal).
// Library actions happen in the Library Manager modal.
setManifestView(board.id);
}}
title={`Libraries for ${boardDisplayName(board)} — click to manage (compile scope)`}
title={`libraries.json — ${boardDisplayName(board)}'s declared libraries (read-only; manage from the Library Manager)`}
>
<span className="file-explorer-icon" style={{ color: '#ffd60a' }}>
<FileIcon name="velxio.json" />
<FileIcon name="libraries.json" />
</span>
<span className="file-explorer-name">velxio.json</span>
<span className="file-explorer-name">libraries.json</span>
<span
style={{
marginLeft: 'auto',

View File

@ -6,7 +6,6 @@ import {
getInstalledLibraries,
getCustomLibraries,
deleteCustomLibrary,
uninstallLibrary,
} from '../../services/libraryService';
import type { ArduinoLibrary, InstalledLibrary } from '../../services/libraryService';
import { trackInstallLibrary } from '../../utils/analytics';
@ -218,27 +217,6 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
[install, addToManifest, activeBoard],
);
const uninstall = useCallback(
async (name: string) => {
setBusyLib(name);
setStatusMsg(null);
try {
const result = await uninstallLibrary(name);
if (result.success) {
setStatusMsg({ type: 'success', text: `"${name}" uninstalled.` });
fetchInstalled();
} else {
setStatusMsg({ type: 'error', text: result.error || `Failed to uninstall "${name}"` });
}
} catch (e: unknown) {
setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Uninstall failed' });
} finally {
setBusyLib(null);
}
},
[fetchInstalled],
);
// A CUSTOM lib lives in the user's per-user store, so removing it hits the
// per-user delete endpoint and also drops it from the manifest.
const removeCustom = useCallback(
@ -446,14 +424,18 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
{busy ? '…' : '+ Add to project'}
</button>
)}
{row.installed && (
{/* Only CUSTOM uploads can be removed (per-user store). Index
libraries live in the shared content-addressed cache you
add/remove them from THIS project, but never "uninstall" a
copy everyone shares, so no Uninstall button for them. */}
{row.custom && (
<button
className="lib-uninstall-btn"
onClick={() => (row.custom ? removeCustom(row.name) : uninstall(row.name))}
onClick={() => removeCustom(row.name)}
disabled={busy}
title={row.custom ? 'Remove your custom upload' : 'Uninstall (free the cache)'}
title="Remove your custom upload"
>
{busy ? '…' : row.custom ? 'Remove' : 'Uninstall'}
{busy ? '…' : 'Remove'}
</button>
)}
</div>

View File

@ -104,6 +104,12 @@ interface EditorState {
files: WorkspaceFile[];
activeFileId: string;
openFileIds: string[];
/** When set, the editor shows a READ-ONLY `libraries.json` view of this
* board's library manifest (board.libraries) instead of the active file.
* Cleared whenever a real file is opened/activated. Managed by the explorer's
* libraries.json entry; the Library Manager modal is what edits the manifest. */
manifestViewBoardId: string | null;
setManifestView: (boardId: string | null) => void;
theme: 'vs-dark' | 'light';
fontSize: number;
viewMode: EditorViewMode;
@ -159,6 +165,8 @@ export const useEditorStore = create<EditorState>((set, get) => ({
files: [DEFAULT_FILE],
activeFileId: MAIN_ID,
openFileIds: [MAIN_ID],
manifestViewBoardId: null,
setManifestView: (boardId: string | null) => set({ manifestViewBoardId: boardId }),
theme: 'vs-dark',
fontSize: 14,
viewMode: 'both',
@ -266,6 +274,7 @@ export const useEditorStore = create<EditorState>((set, get) => ({
return {
openFileIds: s.openFileIds.includes(id) ? s.openFileIds : [...s.openFileIds, id],
activeFileId: id,
manifestViewBoardId: null, // opening a real file exits the libraries.json view
openGroupFileIds: {
...s.openGroupFileIds,
[groupId]: groupOpenIds.includes(id) ? groupOpenIds : [...groupOpenIds, id],
@ -299,6 +308,7 @@ export const useEditorStore = create<EditorState>((set, get) => ({
const groupId = s.activeGroupId;
return {
activeFileId: id,
manifestViewBoardId: null, // activating a real file exits the libraries.json view
activeGroupFileId: { ...s.activeGroupFileId, [groupId]: id },
};
});