enh: create subfolder

Co-Authored-By: Colin Chen <1207878+silenceroom@users.noreply.github.com>
This commit is contained in:
Timothy Jaeryang Baek
2026-03-07 19:45:43 -06:00
parent 80b5896b70
commit 8913f37c3d
8 changed files with 76 additions and 12 deletions

View File

@@ -71,6 +71,7 @@ class FolderForm(BaseModel):
name: str
data: Optional[dict] = None
meta: Optional[dict] = None
parent_id: Optional[str] = None
model_config = ConfigDict(extra="allow")

View File

@@ -119,7 +119,7 @@ def create_folder(
db: Session = Depends(get_session),
):
folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
None, user.id, form_data.name, db=db
form_data.parent_id, user.id, form_data.name, db=db
)
if folder:
@@ -129,7 +129,7 @@ def create_folder(
)
try:
folder = Folders.insert_new_folder(user.id, form_data, db=db)
folder = Folders.insert_new_folder(user.id, form_data, form_data.parent_id, db=db)
return folder
except Exception as e:
log.exception(e)
@@ -317,7 +317,7 @@ async def delete_folder_by_id(
folder = folders.pop()
if folder:
try:
folder_ids = Folders.delete_folder_by_id_and_user_id(id, user.id, db=db)
folder_ids = Folders.delete_folder_by_id_and_user_id(folder.id, user.id, db=db)
for folder_id in folder_ids:
if delete_contents:

View File

@@ -4,6 +4,7 @@ type FolderForm = {
name?: string;
data?: Record<string, any>;
meta?: Record<string, any>;
parent_id?: string | null;
};
export const createNewFolder = async (token: string, folderForm: FolderForm) => {

View File

@@ -142,19 +142,24 @@
}
};
const createFolder = async ({ name, data }) => {
const createFolder = async ({ name, data, parent_id }) => {
name = name?.trim();
if (!name) {
toast.error($i18n.t('Folder name cannot be empty.'));
return;
}
const rootFolders = Object.values(folders).filter((folder) => folder.parent_id === null);
if (rootFolders.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) {
// Check for duplicate names in the same parent
const siblings = Object.values(folders).filter(
(folder) => folder.parent_id === parent_id
);
if (siblings.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) {
// If a folder with the same name already exists, append a number to the name
let i = 1;
while (
rootFolders.find((folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase())
siblings.find(
(folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase()
)
) {
i++;
}
@@ -166,9 +171,10 @@
const tempId = uuidv4();
folders = {
...folders,
tempId: {
[tempId]: {
id: tempId,
name: name,
parent_id: parent_id,
created_at: Date.now(),
updated_at: Date.now()
}
@@ -176,7 +182,8 @@
const res = await createNewFolder(localStorage.token, {
name,
data
data,
parent_id
}).catch((error) => {
toast.error(`${error}`);
return null;

View File

@@ -11,11 +11,13 @@
import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Download from '$lib/components/icons/Download.svelte';
import Folder from '$lib/components/icons/Folder.svelte';
export let align: 'start' | 'end' = 'start';
export let onEdit = () => {};
export let onExport = () => {};
export let onDelete = () => {};
export let onCreateSub = () => {};
let show = false;
</script>
@@ -47,6 +49,18 @@
{align}
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm select-none cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
onCreateSub();
}}
>
<Folder />
<div class="flex items-center">{$i18n.t('Create Folder')}</div>
</DropdownMenu.Item>
<hr class="border-gray-50/30 dark:border-gray-800/30 my-1" />
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm select-none cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {

View File

@@ -19,6 +19,7 @@
export let onSubmit: Function = (e) => {};
export let folderId = null;
export let parentId = null;
export let edit = false;
let folder = null;
@@ -55,7 +56,8 @@
await onSubmit({
name,
meta,
data
data,
parent_id: edit ? undefined : parentId
});
show = false;
loading = false;

View File

@@ -18,7 +18,8 @@
updateFolderIsExpandedById,
updateFolderById,
updateFolderParentIdById,
getFolderById
getFolderById,
createNewFolder
} from '$lib/apis/folders';
import {
getChatById,
@@ -64,6 +65,9 @@
let showFolderModal = false;
let edit = false;
let showCreateSubFolderModal = false;
let createSubFolderParentId = null;
let draggedOver = false;
let dragged = false;
@@ -414,6 +418,30 @@
saveAs(blob, `folder-${folders[folderId].name}-export-${Date.now()}.json`);
};
const createSubFolderHandler = async ({ name, meta, data, parent_id }) => {
if (name === '') {
toast.error($i18n.t('Folder name cannot be empty.'));
return;
}
name = name.trim();
const res = await createNewFolder(localStorage.token, {
name,
data,
meta,
parent_id
}).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
toast.success($i18n.t('Folder created successfully'));
dispatch('update');
}
};
</script>
<DeleteConfirmDialog
@@ -444,6 +472,12 @@
<FolderModal bind:show={showFolderModal} edit={true} {folderId} onSubmit={updateHandler} />
<FolderModal
bind:show={showCreateSubFolderModal}
parentId={createSubFolderParentId}
onSubmit={createSubFolderHandler}
/>
{#if dragged && x && y}
<DragGhost {x} {y}>
<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
@@ -593,6 +627,10 @@
onExport={() => {
exportHandler();
}}
onCreateSub={() => {
createSubFolderParentId = folderId;
showCreateSubFolderModal = true;
}}
>
<div class="p-1 dark:hover:bg-gray-850 rounded-lg touch-auto">
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />

View File

@@ -897,6 +897,7 @@
"Focus Chat Input": "",
"Folder": "",
"Folder Background Image": "",
"Folder created successfully": "",
"Folder deleted successfully": "",
"Folder Max File Count": "",
"Folder name": "",
@@ -2135,4 +2136,4 @@
"YouTube": "",
"Youtube Language": "",
"Youtube Proxy URL": ""
}
}