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"
) {
// get the file from the file input
const currentfile = (e.target as HTMLInputElement).files![0]; const currentfile = (e.target as HTMLInputElement).files![0];
// read the file as text let text = await currentfile.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("");
@ -280,7 +280,8 @@ export default function AdminPage() {
</TableHeader> </TableHeader>
{!loadingUsers && ( {!loadingUsers && (
<TableBody> <TableBody>
{filterUserList.map((user:UserInputType, index) => ( {filterUserList.map(
(user: UserInputType, index) => (
<TableRow key={index}> <TableRow key={index}>
<TableCell className="truncate py-2 font-medium"> <TableCell className="truncate py-2 font-medium">
<ShadTooltip content={user.id}> <ShadTooltip content={user.id}>
@ -396,7 +397,10 @@ export default function AdminPage() {
handleDeleteUser(user); handleDeleteUser(user);
}} }}
> >
<ShadTooltip content="Delete" side="top"> <ShadTooltip
content="Delete"
side="top"
>
<IconComponent <IconComponent
name="Trash2" name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer" className="ml-2 h-4 w-4 cursor-pointer"
@ -406,7 +410,8 @@ export default function AdminPage() {
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} )
)}
</TableBody> </TableBody>
)} )}
</Table> </Table>

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,17 +61,15 @@ 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">

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

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