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:
commit
fc96c1c728
10 changed files with 355 additions and 271 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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; }>;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue