Feat: Create Dropdown Button component (#840)

This PR introduces a new **DropdownButton** component that enables the
creation of interactive dropdown buttons in our application. The
dropdown button provides a user-friendly way to present multiple options
while maintaining a clean and compact interface.
This commit is contained in:
Lucas Oliveira 2023-08-31 11:42:27 -03:00 • committed by GitHub
commit fc96c1c728
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 355 additions and 271 deletions

View file

@ -43,7 +43,9 @@ def add_user(
db.refresh(new_user) db.refresh(new_user)
except IntegrityError as e: except IntegrityError as e:
db.rollback() db.rollback()
raise HTTPException(status_code=400, detail="This username is unavailable.") from e raise HTTPException(
status_code=400, detail="This username is unavailable."
) from e
return new_user return new_user

View file

@ -0,0 +1,67 @@
import { useState } from "react";
import { dropdownButtonPropsType } from "../../types/components";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
export default function DropdownButton({
firstButtonName,
onFirstBtnClick,
options,
}: dropdownButtonPropsType): JSX.Element {
const [showOptions, setShowOptions] = useState<boolean>(false);
return (
<div>
<DropdownMenu open={showOptions}>
<DropdownMenuTrigger asChild>
<Button
variant="primary"
className="relative pr-10"
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
onFirstBtnClick();
}}
>
{firstButtonName}
<div
className="absolute right-2 items-center text-muted-foreground"
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
setShowOptions(!showOptions);
}}
>
{!showOptions ? (
<IconComponent
name="ChevronDown"
/>
) : (
<IconComponent name="ChevronUp" />
)}
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
onPointerDownOutside={(event) => {
event.stopPropagation();
event.preventDefault();
setShowOptions(!showOptions);
}}
>
{options.map(({ name, onBtnClick }, index) => (
<DropdownMenuItem onClick={onBtnClick} key={index}>
{name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}

View file

@ -48,7 +48,7 @@ const TabsContextInitialValue: TabsContextType = {
downloadFlow: (flow: FlowType) => {}, downloadFlow: (flow: FlowType) => {},
downloadFlows: () => {}, downloadFlows: () => {},
uploadFlows: () => {}, uploadFlows: () => {},
uploadFlow: () => {}, uploadFlow: async () => "",
isBuilt: false, isBuilt: false,
setIsBuilt: (state: boolean) => {}, setIsBuilt: (state: boolean) => {},
hardReset: () => {}, hardReset: () => {},
@ -298,39 +298,38 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* If the file type is application/json, the file is read and parsed into a JSON object. * If the file type is application/json, the file is read and parsed into a JSON object.
* The resulting JSON object is passed to the addFlow function. * The resulting JSON object is passed to the addFlow function.
*/ */
function uploadFlow(newProject?: boolean, file?: File) { async function uploadFlow(
newProject?: boolean,
file?: File
): Promise<String | undefined> {
let id;
if (file) { if (file) {
file.text().then((text) => { let text = await file.text();
// parse the text into a JSON object // parse the text into a JSON object
let flow: FlowType = JSON.parse(text); let flow: FlowType = JSON.parse(text);
addFlow(flow, newProject); id = await addFlow(flow, newProject);
});
} else { } else {
// create a file input // create a file input
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "file"; input.type = "file";
input.accept = ".json"; input.accept = ".json";
// add a change event listener to the file input // add a change event listener to the file input
input.onchange = (e: Event) => { id = await new Promise(resolve => {
// check if the file type is application/json input.onchange = async (e: Event) => {
if ( if ((e.target as HTMLInputElement).files![0].type === "application/json") {
(e.target as HTMLInputElement).files![0].type === "application/json" const currentfile = (e.target as HTMLInputElement).files![0];
) { let text = await currentfile.text();
// get the file from the file input
const currentfile = (e.target as HTMLInputElement).files![0];
// read the file as text
currentfile.text().then((text) => {
// parse the text into a JSON object
let flow: FlowType = JSON.parse(text); let flow: FlowType = JSON.parse(text);
const flowId = await addFlow(flow, newProject);
addFlow(flow, newProject); resolve(flowId);
}); }
} };
}; // trigger the file input click event to open the file dialog
// trigger the file input click event to open the file dialog input.click();
input.click(); });
} }
return id;
} }
function uploadFlows() { function uploadFlows() {

View file

@ -4,6 +4,7 @@ import { useContext, useEffect, useRef, useState } from "react";
import PaginatorComponent from "../../components/PaginatorComponent"; import PaginatorComponent from "../../components/PaginatorComponent";
import ShadTooltip from "../../components/ShadTooltipComponent"; import ShadTooltip from "../../components/ShadTooltipComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox"; import { Checkbox } from "../../components/ui/checkbox";
import { Input } from "../../components/ui/input"; import { Input } from "../../components/ui/input";
@ -25,9 +26,8 @@ import {
} from "../../controllers/API"; } from "../../controllers/API";
import ConfirmationModal from "../../modals/ConfirmationModal"; import ConfirmationModal from "../../modals/ConfirmationModal";
import UserManagementModal from "../../modals/UserManagementModal"; import UserManagementModal from "../../modals/UserManagementModal";
import { UserInputType } from "../../types/components";
import Header from "../../components/headerComponent";
import { Users } from "../../types/api"; import { Users } from "../../types/api";
import { UserInputType } from "../../types/components";
export default function AdminPage() { export default function AdminPage() {
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
@ -89,7 +89,7 @@ export default function AdminPage() {
if (input === "") { if (input === "") {
setFilterUserList(userList.current); setFilterUserList(userList.current);
} else { } else {
const filteredList = userList.current.filter((user:Users) => const filteredList = userList.current.filter((user: Users) =>
user.username.toLowerCase().includes(input.toLowerCase()) user.username.toLowerCase().includes(input.toLowerCase())
); );
setFilterUserList(filteredList); setFilterUserList(filteredList);
@ -183,249 +183,254 @@ export default function AdminPage() {
return ( return (
<> <>
<div className="flex flex-col"> <div className="flex flex-col">
<Header /> <Header />
{userData && ( {userData && (
<div className="main-page-panel"> <div className="main-page-panel">
<div className="m-auto flex h-full flex-row justify-center"> <div className="m-auto flex h-full flex-row justify-center">
<div className="basis-5/6"> <div className="basis-5/6">
<div className="m-auto flex h-full flex-col space-y-8 p-8 "> <div className="m-auto flex h-full flex-col space-y-8 p-8 ">
<div className="flex items-center justify-between space-y-2"> <div className="flex items-center justify-between space-y-2">
<div> <div>
<h2 className="text-2xl font-bold tracking-tight"> <h2 className="text-2xl font-bold tracking-tight">
Welcome back! Welcome back!
</h2> </h2>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Navigate through this section to efficiently oversee all Navigate through this section to efficiently oversee all
application users. From here, you can seamlessly manage application users. From here, you can seamlessly manage
user accounts. user accounts.
</p> </p>
</div>
<div className="flex items-center space-x-2"></div>
</div> </div>
<div className="flex items-center space-x-2"></div>
</div>
{userList.current.length === 0 && !loadingUsers && ( {userList.current.length === 0 && !loadingUsers && (
<>
<div className="flex items-center justify-between">
<h2>There's no users registered :)</h2>
</div>
</>
)}
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2>There's no users registered :)</h2> <div className="flex flex-1 items-center space-x-2">
</div> <Input
</> value={inputValue}
)} placeholder="Filter users..."
<> className="h-8 w-[150px] lg:w-[250px]"
<div className="flex items-center justify-between"> onChange={(e) => handleFilterUsers(e.target.value)}
<div className="flex flex-1 items-center space-x-2"> />
<Input {inputValue.length > 0 && (
value={inputValue} <Button
placeholder="Filter users..." onClick={() => {
className="h-8 w-[150px] lg:w-[250px]" setInputValue("");
onChange={(e) => handleFilterUsers(e.target.value)} setFilterUserList(userList.current);
/> }}
{inputValue.length > 0 && ( variant="ghost"
<Button className="h-8 px-2 lg:px-3"
onClick={() => { >
setInputValue(""); Reset
setFilterUserList(userList.current); <X className="ml-2 h-4 w-4" />
</Button>
)}
</div>
<div>
<UserManagementModal
title="New User"
titleHeader={"Add a new user"}
cancelText="Cancel"
confirmationText="Save"
icon={"UserPlus2"}
onConfirm={(index, user) => {
handleNewUser(user);
}} }}
variant="ghost"
className="h-8 px-2 lg:px-3"
> >
Reset <Button>New User</Button>
<X className="ml-2 h-4 w-4" /> </UserManagementModal>
</Button> </div>
)}
</div> </div>
<div> {loadingUsers && (
<UserManagementModal <div>
title="New User" <strong>Loading...</strong>
titleHeader={"Add a new user"} </div>
cancelText="Cancel" )}
confirmationText="Save" <div
icon={"UserPlus2"} className={
onConfirm={(index, user) => { "max-h-[26rem] min-h-[26rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" +
handleNewUser(user); (loadingUsers ? " border-0" : "")
}} }
> >
<Button>New User</Button> <Table className={"table-fixed bg-muted outline-1"}>
</UserManagementModal> <TableHeader
</div> className={
</div> loadingUsers
{loadingUsers && ( ? "hidden"
<div> : "table-fixed bg-muted outline-1"
<strong>Loading...</strong> }
</div> >
)} <TableRow>
<div <TableHead className="h-10">Id</TableHead>
className={ <TableHead className="h-10">Username</TableHead>
"max-h-[26rem] min-h-[26rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" + <TableHead className="h-10">Active</TableHead>
(loadingUsers ? " border-0" : "") <TableHead className="h-10">Superuser</TableHead>
} <TableHead className="h-10">Created At</TableHead>
> <TableHead className="h-10">Updated At</TableHead>
<Table className={"table-fixed bg-muted outline-1"}> <TableHead className="h-10 w-[100px] text-right"></TableHead>
<TableHeader </TableRow>
className={ </TableHeader>
loadingUsers {!loadingUsers && (
? "hidden" <TableBody>
: "table-fixed bg-muted outline-1" {filterUserList.map(
} (user: UserInputType, index) => (
> <TableRow key={index}>
<TableRow> <TableCell className="truncate py-2 font-medium">
<TableHead className="h-10">Id</TableHead> <ShadTooltip content={user.id}>
<TableHead className="h-10">Username</TableHead> <span className="cursor-default">
<TableHead className="h-10">Active</TableHead> {user.id}
<TableHead className="h-10">Superuser</TableHead> </span>
<TableHead className="h-10">Created At</TableHead>
<TableHead className="h-10">Updated At</TableHead>
<TableHead className="h-10 w-[100px] text-right"></TableHead>
</TableRow>
</TableHeader>
{!loadingUsers && (
<TableBody>
{filterUserList.map((user:UserInputType, index) => (
<TableRow key={index}>
<TableCell className="truncate py-2 font-medium">
<ShadTooltip content={user.id}>
<span className="cursor-default">
{user.id}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="truncate py-2">
<ShadTooltip content={user.username}>
<span className="cursor-default">
{user.username}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
<ConfirmationModal
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleDisableUser(
user.is_active,
user.id,
user
);
}}
>
<Checkbox
id="is_active"
checked={user.is_active}
/>
</ConfirmationModal>
</TableCell>
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
<ConfirmationModal
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleSuperUserEdit(
user.is_superuser,
user.id,
user
);
}}
>
<Checkbox
id="is_superuser"
checked={user.is_superuser}
/>
</ConfirmationModal>
</TableCell>
<TableCell className="truncate py-2 ">
{
new Date(user.create_at!)
.toISOString()
.split("T")[0]
}
</TableCell>
<TableCell className="truncate py-2">
{
new Date(user.updated_at!)
.toISOString()
.split("T")[0]
}
</TableCell>
<TableCell className="flex w-[100px] py-2 text-right">
<div className="flex">
<UserManagementModal
title="Edit"
titleHeader={`${user.id}`}
cancelText="Cancel"
confirmationText="Save"
icon={"UserPlus2"}
data={user}
index={index}
onConfirm={(index, editUser) => {
handleEditUser(user.id, editUser);
}}
>
<ShadTooltip content="Edit" side="top">
<IconComponent
name="Pencil"
className="h-4 w-4 cursor-pointer"
/>
</ShadTooltip> </ShadTooltip>
</UserManagementModal> </TableCell>
<TableCell className="truncate py-2">
<ConfirmationModal <ShadTooltip content={user.username}>
title="Delete" <span className="cursor-default">
titleHeader="Delete User" {user.username}
modalContentTitle="Attention!" </span>
modalContent="Are you sure you want to delete this user? This action cannot be undone."
cancelText="Cancel"
confirmationText="Delete"
icon={"UserMinus2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleDeleteUser(user);
}}
>
<ShadTooltip content="Delete" side="top">
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip> </ShadTooltip>
</ConfirmationModal> </TableCell>
</div> <TableCell className="relative left-5 truncate py-2 text-align-last-left">
</TableCell> <ConfirmationModal
</TableRow> title="Edit"
))} titleHeader={`${user.username}`}
</TableBody> modalContentTitle="Attention!"
)} modalContent="Are you completely confident about the changes you are making to this user?"
</Table> cancelText="Cancel"
</div> confirmationText="Confirm"
icon={"UserCog2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleDisableUser(
user.is_active,
user.id,
user
);
}}
>
<Checkbox
id="is_active"
checked={user.is_active}
/>
</ConfirmationModal>
</TableCell>
<TableCell className="relative left-5 truncate py-2 text-align-last-left">
<ConfirmationModal
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
data={user}
index={index}
onConfirm={(index, user) => {
handleSuperUserEdit(
user.is_superuser,
user.id,
user
);
}}
>
<Checkbox
id="is_superuser"
checked={user.is_superuser}
/>
</ConfirmationModal>
</TableCell>
<TableCell className="truncate py-2 ">
{
new Date(user.create_at!)
.toISOString()
.split("T")[0]
}
</TableCell>
<TableCell className="truncate py-2">
{
new Date(user.updated_at!)
.toISOString()
.split("T")[0]
}
</TableCell>
<TableCell className="flex w-[100px] py-2 text-right">
<div className="flex">
<UserManagementModal
title="Edit"
titleHeader={`${user.id}`}
cancelText="Cancel"
confirmationText="Save"
icon={"UserPlus2"}
data={user}
index={index}
onConfirm={(index, editUser) => {
handleEditUser(user.id, editUser);
}}
>
<ShadTooltip content="Edit" side="top">
<IconComponent
name="Pencil"
className="h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</UserManagementModal>
<PaginatorComponent <ConfirmationModal
pageIndex={index} title="Delete"
pageSize={size} titleHeader="Delete User"
totalRowsCount={totalRowsCount} modalContentTitle="Attention!"
paginate={(pageIndex, pageSize) => { modalContent="Are you sure you want to delete this user? This action cannot be undone."
handleChangePagination(pageSize, pageIndex); cancelText="Cancel"
}} confirmationText="Delete"
></PaginatorComponent> icon={"UserMinus2"}
</> data={user}
index={index}
onConfirm={(index, user) => {
handleDeleteUser(user);
}}
>
<ShadTooltip
content="Delete"
side="top"
>
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</ConfirmationModal>
</div>
</TableCell>
</TableRow>
)
)}
</TableBody>
)}
</Table>
</div>
<PaginatorComponent
pageIndex={index}
pageSize={size}
totalRowsCount={totalRowsCount}
paginate={(pageIndex, pageSize) => {
handleChangePagination(pageSize, pageIndex);
}}
></PaginatorComponent>
</>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> )}
)}
</div> </div>
</> </>
); );

View file

@ -421,7 +421,7 @@ export default function Page({
zoomOnScroll={!view} zoomOnScroll={!view}
zoomOnPinch={!view} zoomOnPinch={!view}
panOnDrag={!view} panOnDrag={!view}
proOptions={{hideAttribution: true}} proOptions={{ hideAttribution: true }}
> >
<Background className="" /> <Background className="" />
{!view && ( {!view && (

View file

@ -7,6 +7,7 @@ import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { USER_PROJECTS_HEADER } from "../../constants/constants"; import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext"; import { TabsContext } from "../../contexts/tabsContext";
import DropdownButton from "../../components/DropdownButtonComponent";
export default function HomePage(): JSX.Element { export default function HomePage(): JSX.Element {
const { const {
flows, flows,
@ -14,9 +15,12 @@ export default function HomePage(): JSX.Element {
downloadFlows, downloadFlows,
uploadFlows, uploadFlows,
addFlow, addFlow,
removeFlow, removeFlow, uploadFlow,
isLoading, isLoading,
} = useContext(TabsContext); } = useContext(TabsContext);
const dropdownOptions = [{name: "Import from JSON", onBtnClick: () => uploadFlow(true).then((id) => {
navigate("/flow/" + id);
})}]
// Set a null id // Set a null id
useEffect(() => { useEffect(() => {
@ -57,21 +61,19 @@ export default function HomePage(): JSX.Element {
<IconComponent name="Upload" className="main-page-nav-button" /> <IconComponent name="Upload" className="main-page-nav-button" />
Upload Collection Upload Collection
</Button> </Button>
<Button <DropdownButton
variant="primary" firstButtonName="New Project"
onClick={() => { onFirstBtnClick={() => {
addFlow(null!, true).then((id) => { addFlow(null!, true).then((id) => {
navigate("/flow/" + id); navigate("/flow/" + id);
}); });
}} }}
> options={dropdownOptions}
<IconComponent name="Plus" className="main-page-nav-button" /> />
New Project
</Button>
</div> </div>
</div> </div>
<span className="main-page-description-text"> <span className="main-page-description-text">
Manage your personal projects. Download or upload your collection. Manage your personal projects. Download or upload your collection.
</span> </span>
<div className="main-page-flows-display"> <div className="main-page-flows-display">
{isLoading && flows.length == 0 ? ( {isLoading && flows.length == 0 ? (

View file

@ -19,7 +19,8 @@ export default function LoginPage(): JSX.Element {
useState<loginInputStateType>(CONTROL_LOGIN_STATE); useState<loginInputStateType>(CONTROL_LOGIN_STATE);
const { password, username } = inputState; const { password, username } = inputState;
const { login, getAuthentication, setUserData, setIsAdmin } = useContext(AuthContext); const { login, getAuthentication, setUserData, setIsAdmin } =
useContext(AuthContext);
const navigate = useNavigate(); const navigate = useNavigate();
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);

View file

@ -268,7 +268,7 @@ export type UserInputType = {
is_superuser?: boolean; is_superuser?: boolean;
id?: string; id?: string;
create_at?: string; create_at?: string;
updated_at?:string; updated_at?: string;
}; };
export type ApiKeyType = { export type ApiKeyType = {
@ -544,3 +544,9 @@ export type fetchErrorComponentType = {
message: string; message: string;
description: string; description: string;
}; };
export type dropdownButtonPropsType = {
firstButtonName: string;
onFirstBtnClick: () => void;
options: Array<{ name: string; onBtnClick: () => void; }>;
};

View file

@ -24,7 +24,7 @@ export type TabsContextType = {
uploadFlows: () => void; uploadFlows: () => void;
isBuilt: boolean; isBuilt: boolean;
setIsBuilt: (state: boolean) => void; setIsBuilt: (state: boolean) => void;
uploadFlow: (newFlow?: boolean, file?: File) => void; uploadFlow: (newFlow?: boolean, file?: File) => Promise<String | undefined>;
hardReset: () => void; hardReset: () => void;
getNodeId: (nodeType: string) => string; getNodeId: (nodeType: string) => string;
tabsState: TabsState; tabsState: TabsState;

View file

@ -5,6 +5,7 @@ import {
ChevronDown, ChevronDown,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
ChevronUp,
ChevronsLeft, ChevronsLeft,
ChevronsRight, ChevronsRight,
ChevronsUpDown, ChevronsUpDown,
@ -300,4 +301,5 @@ export const nodeIconsLucide: iconsType = {
UserCog2, UserCog2,
Key, Key,
Unplug, Unplug,
ChevronUp,
}; };