feat(sidebar): show empty folders and create folders from the sidebar (#999)

Empty folders (tracked in the scan-recorded all_folders list, same source
the move/download destination picker uses) can now be surfaced in the
folder sidebar via a view-options toggle, dimmed when their subtree holds
no models. Folders can be created directly from the sidebar through a new
POST /api/lm/{prefix}/create-folder endpoint with library-root
containment checks; the scanner records the new directory incrementally
so the tree reflects it without a rescan.

The sidebar header moves its view toggles (tree/list, recursive, empty
folders) into a "..." menu to fit the new create-folder button.
This commit is contained in:
Will Miao
2026-09-15 15:14:56 +08:00
parent 2ceb1e2850
commit 9734df15b4
24 changed files with 1260 additions and 173 deletions
+5
View File
@@ -22,6 +22,7 @@ export const MODEL_CONFIG = {
supportsLetterFilter: true,
supportsBulkOperations: true,
supportsMove: true,
supportsFolderManagement: true,
templateName: 'loras.html'
},
[MODEL_TYPES.CHECKPOINT]: {
@@ -31,6 +32,7 @@ export const MODEL_CONFIG = {
supportsLetterFilter: false,
supportsBulkOperations: true,
supportsMove: true,
supportsFolderManagement: true,
templateName: 'checkpoints.html'
},
[MODEL_TYPES.EMBEDDING]: {
@@ -40,6 +42,7 @@ export const MODEL_CONFIG = {
supportsLetterFilter: true,
supportsBulkOperations: true,
supportsMove: true,
supportsFolderManagement: true,
templateName: 'embeddings.html'
},
[MODEL_TYPES.OTHER]: {
@@ -49,6 +52,7 @@ export const MODEL_CONFIG = {
supportsLetterFilter: false,
supportsBulkOperations: true,
supportsMove: true,
supportsFolderManagement: true,
templateName: 'other.html'
}
};
@@ -79,6 +83,7 @@ export function getApiEndpoints(modelType) {
// Move operations (now common for all model types that support move)
moveModel: `/api/lm/${modelType}/move_model`,
moveBulk: `/api/lm/${modelType}/move_models_bulk`,
createFolder: `/api/lm/${modelType}/create-folder`,
// CivitAI integration
fetchCivitai: `/api/lm/${modelType}/fetch-civitai`,
+24 -2
View File
@@ -1295,9 +1295,13 @@ export class BaseModelApiClient {
}
}
async fetchModelFolders() {
async fetchModelFolders(options = {}) {
try {
const response = await fetch(this.apiConfig.endpoints.folders);
const { includeEmpty = false } = options || {};
const url = includeEmpty
? `${this.apiConfig.endpoints.folders}?include_empty=1`
: this.apiConfig.endpoints.folders;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${this.apiConfig.config.displayName} folders`);
}
@@ -1308,6 +1312,24 @@ export class BaseModelApiClient {
}
}
async createFolder(folderPath) {
const response = await fetch(this.apiConfig.endpoints.createFolder, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ folder_path: folderPath })
});
const result = await response.json().catch(() => ({}));
if (!response.ok || result.success === false) {
throw new Error(result.error || `Failed to create folder`);
}
return result;
}
async fetchUnifiedFolderTree(options = {}) {
try {
const { includeEmpty = false } = options;