feat: Folders Design Uplift (#4122)
* move folders to a full page aside * new dropdown + styles * [autofix.ci] apply automated fixes * design updates * [autofix.ci] apply automated fixes * increase empty state width * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
parent
fdaaf19ba1
commit
e8e226c0dc
6 changed files with 198 additions and 133 deletions
|
|
@ -1,4 +1,10 @@
|
||||||
import ShadTooltip from "@/components/shadTooltipComponent";
|
import ShadTooltip from "@/components/shadTooltipComponent";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
} from "@/components/ui/select-custom";
|
||||||
import {
|
import {
|
||||||
usePatchFolders,
|
usePatchFolders,
|
||||||
usePostFolders,
|
usePostFolders,
|
||||||
|
|
@ -129,6 +135,7 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
link.download = `${data.folder_name}.json`;
|
link.download = `${data.folder_name}.json`;
|
||||||
|
|
||||||
link.click();
|
link.click();
|
||||||
|
track("Folder Exported", { folderId: id! });
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setErrorData({
|
setErrorData({
|
||||||
|
|
@ -245,13 +252,13 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
isDeletingFolder;
|
isDeletingFolder;
|
||||||
|
|
||||||
const HeaderButtons = () => (
|
const HeaderButtons = () => (
|
||||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
<div className="mt-4 flex shrink-0 items-center justify-between gap-2">
|
||||||
<div className="flex-1 self-start text-lg font-semibold">Folders</div>
|
<div className="text-md flex-1 font-semibold">Folders</div>
|
||||||
<AddFolderButton onClick={addNewFolder} disabled={isUpdatingFolder} />
|
|
||||||
<UploadFolderButton
|
<UploadFolderButton
|
||||||
onClick={handleUploadFlowsToFolder}
|
onClick={handleUploadFlowsToFolder}
|
||||||
disabled={isUpdatingFolder}
|
disabled={isUpdatingFolder}
|
||||||
/>
|
/>
|
||||||
|
<AddFolderButton onClick={addNewFolder} disabled={isUpdatingFolder} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -260,12 +267,12 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="px-2"
|
className="border-0"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
data-testid="add-folder-button"
|
data-testid="add-folder-button"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<IconComponent name="FolderPlus" className="w-4" />
|
<IconComponent name="Plus" className="w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
);
|
);
|
||||||
|
|
@ -275,7 +282,7 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="px-2"
|
className="border-0"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
data-testid="upload-folder-button"
|
data-testid="upload-folder-button"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
@ -285,11 +292,30 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const FolderSelectItem = ({ name, iconName }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
name === "Delete" ? "text-error" : "",
|
||||||
|
"flex items-center font-medium",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<IconComponent name={iconName} className="mr-2 w-4" />
|
||||||
|
<span>{name}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const handleDoubleClick = (event, item) => {
|
const handleDoubleClick = (event, item) => {
|
||||||
if (item.name === "My Projects") {
|
if (item.name === "My Projects") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
handleSelectFolderToRename(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectFolderToRename = (item) => {
|
||||||
if (!foldersNames[item.name]) {
|
if (!foldersNames[item.name]) {
|
||||||
setFoldersNames({ [item.name]: item.name });
|
setFoldersNames({ [item.name]: item.name });
|
||||||
}
|
}
|
||||||
|
|
@ -303,8 +329,6 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
});
|
});
|
||||||
setEditFolderName(newEditFolders);
|
setEditFolderName(newEditFolders);
|
||||||
takeSnapshot();
|
takeSnapshot();
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,8 +338,6 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
[item.name]: item.name,
|
[item.name]: item.name,
|
||||||
}));
|
}));
|
||||||
takeSnapshot();
|
takeSnapshot();
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDownFn = (e, item) => {
|
const handleKeyDownFn = (e, item) => {
|
||||||
|
|
@ -340,6 +362,20 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (option, folder) => {
|
||||||
|
switch (option) {
|
||||||
|
case "delete":
|
||||||
|
handleDeleteFolder!(folder);
|
||||||
|
break;
|
||||||
|
case "download":
|
||||||
|
handleDownloadFolder(folder.id!);
|
||||||
|
break;
|
||||||
|
case "rename":
|
||||||
|
handleSelectFolderToRename(folder);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderButtons />
|
<HeaderButtons />
|
||||||
|
|
@ -362,7 +398,7 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "ghost" }),
|
buttonVariants({ variant: "ghost" }),
|
||||||
checkPathName(item.id!)
|
checkPathName(item.id!)
|
||||||
? "border border-border bg-muted hover:bg-muted"
|
? "bg-muted hover:bg-muted"
|
||||||
: "border hover:bg-transparent lg:border-transparent lg:hover:border-border",
|
: "border hover:bg-transparent lg:border-transparent lg:hover:border-border",
|
||||||
"group flex w-full shrink-0 cursor-pointer gap-2 opacity-100 lg:min-w-full",
|
"group flex w-full shrink-0 cursor-pointer gap-2 opacity-100 lg:min-w-full",
|
||||||
folderIdDragging === item.id! ? "bg-border" : "",
|
folderIdDragging === item.id! ? "bg-border" : "",
|
||||||
|
|
@ -373,79 +409,104 @@ const SideBarFoldersButtonsComponent = ({
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
handleDoubleClick(event, item);
|
handleDoubleClick(event, item);
|
||||||
}}
|
}}
|
||||||
className="flex w-full items-center gap-2"
|
className="flex w-full items-center justify-between"
|
||||||
>
|
>
|
||||||
<IconComponent
|
<div className="flex items-center gap-2">
|
||||||
name={"folder"}
|
<IconComponent
|
||||||
className="mr-2 w-4 flex-shrink-0 justify-start stroke-[1.5] opacity-100"
|
name={"folder"}
|
||||||
/>
|
className="mr-2 w-4 flex-shrink-0 justify-start stroke-[1.5] opacity-100"
|
||||||
{editFolderName?.edit && !isUpdatingFolder ? (
|
/>
|
||||||
<div>
|
{editFolderName?.edit && !isUpdatingFolder ? (
|
||||||
<Input
|
<div>
|
||||||
className="w-36"
|
<Input
|
||||||
onChange={(e) => {
|
className="w-36"
|
||||||
handleEditFolderName(e, item.name);
|
onChange={(e) => {
|
||||||
}}
|
handleEditFolderName(e, item.name);
|
||||||
ref={refInput}
|
}}
|
||||||
onKeyDown={(e) => {
|
ref={refInput}
|
||||||
handleKeyDownFn(e, item);
|
onKeyDown={(e) => {
|
||||||
handleKeyDown(e, e.key, "");
|
handleKeyDownFn(e, item);
|
||||||
}}
|
handleKeyDown(e, e.key, "");
|
||||||
autoFocus={true}
|
}}
|
||||||
onBlur={() => {
|
autoFocus={true}
|
||||||
if (refInput.current?.value !== item.name) {
|
onBlur={(e) => {
|
||||||
handleEditNameFolder(item);
|
// fixes autofocus problem where cursor isn't present
|
||||||
} else {
|
if (
|
||||||
editFolderName.edit = false;
|
e.relatedTarget?.id ===
|
||||||
}
|
`options-trigger-${item.name}`
|
||||||
refInput.current?.blur();
|
) {
|
||||||
}}
|
refInput.current?.focus();
|
||||||
value={foldersNames[item.name]}
|
return;
|
||||||
id={`input-folder-${item.name}`}
|
}
|
||||||
data-testid={`input-folder`}
|
|
||||||
/>
|
if (refInput.current?.value !== item.name) {
|
||||||
</div>
|
handleEditNameFolder(item);
|
||||||
) : (
|
} else {
|
||||||
<span className="block w-full truncate opacity-100">
|
editFolderName.edit = false;
|
||||||
{item.name}
|
}
|
||||||
</span>
|
refInput.current?.blur();
|
||||||
)}
|
}}
|
||||||
{index > 0 && (
|
value={foldersNames[item.name]}
|
||||||
<Button
|
id={`input-folder-${item.name}`}
|
||||||
data-testid="btn-delete-folder"
|
data-testid={`input-folder`}
|
||||||
className="hidden p-0 hover:bg-primary group-hover:block"
|
/>
|
||||||
onClick={(e) => {
|
</div>
|
||||||
handleDeleteFolder!(item);
|
) : (
|
||||||
e.stopPropagation();
|
<span className="block w-full grow truncate opacity-100">
|
||||||
e.preventDefault();
|
{item.name}
|
||||||
}}
|
</span>
|
||||||
variant={"ghost"}
|
)}
|
||||||
size={"icon"}
|
</div>
|
||||||
disabled={isUpdatingFolder}
|
<Select
|
||||||
|
onValueChange={(value) => handleSelectChange(value, item)}
|
||||||
|
value=""
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className="w-fit"
|
||||||
|
id={`options-trigger-${item.name}`}
|
||||||
|
data-testid="more-options-button"
|
||||||
>
|
>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name={"trash"}
|
name={"MoreHorizontal"}
|
||||||
className="w-4 stroke-[1.5] p-0"
|
className="hidden w-4 stroke-[1.5] px-0 text-primary group-hover:block"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</SelectTrigger>
|
||||||
)}
|
<SelectContent
|
||||||
<Button
|
align="end"
|
||||||
className="hidden px-0 hover:bg-primary group-hover:block"
|
alignOffset={-16}
|
||||||
onClick={(e) => {
|
position="popper"
|
||||||
handleDownloadFolder(item.id!);
|
>
|
||||||
e.stopPropagation();
|
{item.name !== "My Projects" && (
|
||||||
e.preventDefault();
|
<SelectItem
|
||||||
track("Folder Exported", { folderId: item.id! });
|
id="rename-button"
|
||||||
}}
|
value="rename"
|
||||||
variant={"ghost"}
|
data-testid="btn-rename-folder"
|
||||||
size={"icon"}
|
>
|
||||||
disabled={isUpdatingFolder}
|
<FolderSelectItem
|
||||||
>
|
name="Rename"
|
||||||
<IconComponent
|
iconName="square-pen"
|
||||||
name={"Download"}
|
/>
|
||||||
className="w-4 stroke-[1.5] text-primary"
|
</SelectItem>
|
||||||
/>
|
)}
|
||||||
</Button>
|
<SelectItem
|
||||||
|
value="download"
|
||||||
|
data-testid="btn-download-folder"
|
||||||
|
>
|
||||||
|
<FolderSelectItem
|
||||||
|
name="Download Content"
|
||||||
|
iconName="download"
|
||||||
|
/>
|
||||||
|
</SelectItem>
|
||||||
|
{index > 0 && (
|
||||||
|
<SelectItem
|
||||||
|
value="delete"
|
||||||
|
data-testid="btn-delete-folder"
|
||||||
|
>
|
||||||
|
<FolderSelectItem name="Delete" iconName="trash" />
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export default function HorizontalScrollFadeComponent({
|
||||||
}, [divWidth, children]); // Depend on divWidth
|
}, [divWidth, children]); // Depend on divWidth
|
||||||
|
|
||||||
return isFolder ? (
|
return isFolder ? (
|
||||||
<div className="hidden w-full flex-col gap-2 lg:flex">{children}</div>
|
<div className="flex w-full flex-col gap-2">{children}</div>
|
||||||
) : (
|
) : (
|
||||||
<div ref={fadeContainerRef} className="fade-container flex">
|
<div ref={fadeContainerRef} className="fade-container flex">
|
||||||
<div ref={scrollContainerRef} className="scroll-container flex gap-2">
|
<div ref={scrollContainerRef} className="scroll-container flex gap-2">
|
||||||
|
|
|
||||||
|
|
@ -15,25 +15,27 @@ export default function PageLayout({
|
||||||
betaIcon?: boolean;
|
betaIcon?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col justify-between overflow-auto bg-background px-16 pt-6">
|
<div className="flex h-full w-full flex-col justify-between overflow-auto bg-background px-6 pt-10">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="mx-auto h-full w-full max-w-[1440px]">
|
||||||
<CustomBanner />
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex w-full items-center justify-between gap-4 space-y-0.5 py-2">
|
<CustomBanner />
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full items-center justify-between gap-4 space-y-0.5 py-2">
|
||||||
<h2
|
<div className="flex w-full flex-col">
|
||||||
className="text-2xl font-bold tracking-tight"
|
<h2
|
||||||
data-testid="mainpage_title"
|
className="text-2xl font-bold tracking-tight"
|
||||||
>
|
data-testid="mainpage_title"
|
||||||
{title}
|
>
|
||||||
{betaIcon && <span className="store-beta-icon">BETA</span>}
|
{title}
|
||||||
</h2>
|
{betaIcon && <span className="store-beta-icon">BETA</span>}
|
||||||
<p className="text-muted-foreground">{description}</p>
|
</h2>
|
||||||
|
<p className="text-muted-foreground">{description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0">{button && button}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">{button && button}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Separator className="my-6 flex" />
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-6 flex" />
|
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,43 +79,43 @@ export default function HomePage(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageLayout
|
<div className="flex h-full w-full space-y-8 md:flex-col lg:flex-row lg:space-y-0">
|
||||||
title={USER_PROJECTS_HEADER}
|
<aside className="hidden h-full w-fit flex-col space-y-6 border-r px-4 lg:flex">
|
||||||
description={MY_COLLECTION_DESC}
|
<FolderSidebarNav
|
||||||
button={
|
handleChangeFolder={(id: string) => {
|
||||||
<div className="flex gap-2">
|
navigate(`all/folder/${id}`);
|
||||||
<DropdownButton
|
}}
|
||||||
firstButtonName="New Project"
|
handleDeleteFolder={(item) => {
|
||||||
onFirstBtnClick={() => {
|
setFolderToEdit(item);
|
||||||
setOpenModal(true);
|
setOpenDeleteFolderModal(true);
|
||||||
track("New Project Button Clicked");
|
}}
|
||||||
}}
|
className="w-[20vw] max-w-[288px]"
|
||||||
options={dropdownOptions}
|
/>
|
||||||
plusButton={true}
|
</aside>
|
||||||
dropdownOptions={false}
|
<PageLayout
|
||||||
isFetchingFolders={isLoadingFolder}
|
title={USER_PROJECTS_HEADER}
|
||||||
/>
|
description={MY_COLLECTION_DESC}
|
||||||
</div>
|
button={
|
||||||
}
|
<div className="flex gap-2">
|
||||||
>
|
<DropdownButton
|
||||||
<div className="flex h-full w-full space-y-8 md:flex-col lg:flex-row lg:space-x-8 lg:space-y-0">
|
firstButtonName="New Project"
|
||||||
<aside className="flex h-fit w-fit flex-col space-y-6">
|
onFirstBtnClick={() => {
|
||||||
<FolderSidebarNav
|
setOpenModal(true);
|
||||||
handleChangeFolder={(id: string) => {
|
track("New Project Button Clicked");
|
||||||
navigate(`all/folder/${id}`);
|
}}
|
||||||
}}
|
options={dropdownOptions}
|
||||||
handleDeleteFolder={(item) => {
|
plusButton={true}
|
||||||
setFolderToEdit(item);
|
dropdownOptions={false}
|
||||||
setOpenDeleteFolderModal(true);
|
isFetchingFolders={isLoadingFolder}
|
||||||
}}
|
/>
|
||||||
className="w-[20vw]"
|
</div>
|
||||||
/>
|
}
|
||||||
</aside>
|
>
|
||||||
<div className="relative h-full w-full flex-1">
|
<div className="relative h-full w-full flex-1">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageLayout>
|
||||||
</PageLayout>
|
</div>
|
||||||
<ModalsComponent
|
<ModalsComponent
|
||||||
openModal={openModal}
|
openModal={openModal}
|
||||||
setOpenModal={setOpenModal}
|
setOpenModal={setOpenModal}
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,10 @@ test("CRUD folders", async ({ page }) => {
|
||||||
.last()
|
.last()
|
||||||
.hover()
|
.hover()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await page.getByTestId("btn-delete-folder").last().click();
|
await page.getByTestId("more-options-button").last().click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("btn-delete-folder").click();
|
||||||
await page.getByText("Delete").last().click();
|
await page.getByText("Delete").last().click();
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
await page.getByText("Folder deleted successfully").isVisible();
|
await page.getByText("Folder deleted successfully").isVisible();
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@ test("should be able to move flow from folder, rename it and be displayed on cor
|
||||||
while (countFolders > 1) {
|
while (countFolders > 1) {
|
||||||
await page.getByText("New Folder").first().hover();
|
await page.getByText("New Folder").first().hover();
|
||||||
|
|
||||||
await page.getByTestId("btn-delete-folder").first().click();
|
await page.getByTestId("more-options-button").first().click();
|
||||||
|
await page.getByTestId("btn-delete-folder").click();
|
||||||
await page.getByText("Delete").last().click();
|
await page.getByText("Delete").last().click();
|
||||||
countFolders--;
|
countFolders--;
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue