feat(P2.2c): show per-user custom libs in the Library Manager + autocomplete

The Library Manager Installed tab + the velxio.json add-autocomplete now merge
the user's per-user custom uploads (getCustomLibraries -> GET /api/pro/libraries/
custom) with the shared global index list, so users can see and reuse their own
uploads (which live in the per-user store, not the global list). A custom lib's
button removes it via the per-user delete endpoint (not arduino-cli uninstall,
which would not find it). Degrades to [] for OSS/anon.
This commit is contained in:
David Montero 2026-06-07 19:58:13 +02:00
parent cc40bda3eb
commit 97f390719f
2 changed files with 80 additions and 4 deletions

View File

@ -4,6 +4,8 @@ import {
searchLibraries,
installLibrary,
getInstalledLibraries,
getCustomLibraries,
deleteCustomLibrary,
uninstallLibrary,
} from '../../services/libraryService';
import type { ArduinoLibrary, InstalledLibrary } from '../../services/libraryService';
@ -66,8 +68,16 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
const fetchInstalled = useCallback(async () => {
setLoadingInstalled(true);
try {
const libs = await getInstalledLibraries();
setInstalledLibraries(libs);
// P2.2c — the shared global list (index libs) PLUS the user's per-user
// custom uploads (which live in their per-user store, not the global
// list). Custom first so they're easy to find; de-duped by name.
const [libs, custom] = await Promise.all([getInstalledLibraries(), getCustomLibraries()]);
const customNames = new Set(custom.map((c) => (c.name || '').toLowerCase()));
const merged = [
...custom,
...libs.filter((l) => !customNames.has((l.library?.name || l.name || '').toLowerCase())),
];
setInstalledLibraries(merged);
} catch (e: unknown) {
setStatusMsg({
type: 'error',
@ -166,6 +176,25 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
}
};
// P2.2c — a CUSTOM lib lives in the user's per-user store, not the shared
// arduino-cli dir, so removing it hits the per-user delete endpoint.
const handleRemoveCustom = async (libName: string) => {
setUninstallingLib(libName);
setStatusMsg(null);
try {
const result = await deleteCustomLibrary(libName);
if (result.success) {
setStatusMsg({ type: 'success', text: `Removed your custom "${libName}".` });
removeFromManifest(libName);
fetchInstalled();
} else {
setStatusMsg({ type: 'error', text: result.error || `Failed to remove "${libName}"` });
}
} finally {
setUninstallingLib(null);
}
};
// ── Project manifest (velxio.json) editing ──────────────────────────────
const declared = manifestLibs ?? [];
@ -748,10 +777,19 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
)}
<button
className="lib-uninstall-btn"
onClick={() => handleUninstall(getInstalledName(lib))}
onClick={() =>
lib.custom
? handleRemoveCustom(getInstalledName(lib))
: handleUninstall(getInstalledName(lib))
}
disabled={uninstallingLib !== null}
title={lib.custom ? 'Remove your custom upload' : 'Uninstall library'}
>
{uninstallingLib === getInstalledName(lib) ? 'Uninstalling...' : 'UNINSTALL'}
{uninstallingLib === getInstalledName(lib)
? 'Removing...'
: lib.custom
? 'Remove'
: 'UNINSTALL'}
</button>
</div>
</div>

View File

@ -32,6 +32,8 @@ export interface InstalledLibrary {
version?: string;
author?: string;
sentence?: string;
/** P2.2c — true for the user's own per-user custom uploads (vs shared index libs). */
custom?: boolean;
}
export async function searchLibraries(query: string): Promise<ArduinoLibrary[]> {
@ -74,6 +76,42 @@ export async function getInstalledLibraries(): Promise<InstalledLibrary[]> {
return data.libraries || [];
}
/**
* P2.2c the signed-in user's per-user CUSTOM libraries (uploaded .zip),
* which live in their per-user store, not the shared global list. Merged into
* the Installed view + velxio.json autocomplete so they can see and reuse their
* uploads. Returns [] when unauthenticated or no pro overlay (401/404).
*/
export async function getCustomLibraries(): Promise<InstalledLibrary[]> {
try {
const res = await fetch('/api/pro/libraries/custom', { credentials: 'include' });
if (!res.ok) return [];
const data = await res.json();
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
/** P2.2c — remove one of the user's own custom uploads (per-user store). */
export async function deleteCustomLibrary(
name: string,
): Promise<{ success: boolean; error?: string }> {
try {
const res = await fetch(`/api/pro/libraries/custom/${encodeURIComponent(name)}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: 'Failed to remove' }));
return { success: false, error: err.detail || 'Failed to remove' };
}
return { success: true };
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : 'network error' };
}
}
/**
* Resolve a library name to "Name@X.Y.Z" using arduino-cli lib search.
* Returns null if the library is not found in the index.