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:
Mike Fortman 2024-10-18 12:17:49 -05:00 • committed by GitHub
commit e8e226c0dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 198 additions and 133 deletions

View file

@ -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>
); );

View file

@ -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">

View file

@ -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>
); );
} }

View file

@ -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}

View file

@ -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();

View file

@ -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);