Feature Enhancements and Bug Fixes (#2099)

#### Overview:
This PR introduces several new features and fixes to improve the user
experience and functionality of the application. Key updates include
enhanced interactivity, export functionalities, UI improvements, and bug
fixes.

#### New Features:
1. **Play on Hover**:
- Added functionality to play media when hovering over the entire node
for better interactivity.

2. **Export Handler**:
   - Implemented an export handler on the main page.
- `handleExport` function added as a parameter to the header, along with
an export button for easy data export.

3. **Upload Flow**:
- Introduced an "Upload Flow" button on the main page for streamlined
file uploads.

4. **User Menu**:
- Added a user option in the header menu for better user profile
management.

#### Bug Fixes and UI Improvements:
1. **Naming and Position Fixes**:
- Corrected naming issues and positioning of alerts in the export modal
for clarity.

2. **Button Styling**:
- Updated buttons in the components section header to utilize ShadCN
button styles for consistency.

3. **Select All Classes**:
- Resolved issues with the "Select All" functionality for improved batch
operations.

4. **Import Function**:
   - Fixed the import function to ensure reliable data import processes.

5. **Error Naming**:
- Standardized error naming conventions for better error handling and
debugging.

#### Commits:
1. **Added play on hover of entire node** - Enhances interactivity by
enabling play on hover.
2. **Added Export handler on Main Page** - Implements core export
functionality.
3. **Added handleExport as parameter of header and added button to
export** - Integrates export control into the header.
4. **Fixed naming of errors** - Improves error handling by fixing naming
issues.
5. **Fixed buttons not using ShadCN buttons on header of components
section** - Ensures consistent button styling.
6. **Fixed naming and position of alert on Export modal** - Corrects
alert display issues in export modal.
7. **Fixed select all classes** - Resolves issues with the "Select All"
feature.
8. **Added Upload Flow button to main page** - Introduces an upload
button for file handling.
9. **Fixed import function** - Ensures reliable import operations.
10. **Added user on header menu** - Adds user options to the header for
profile management.

These updates collectively enhance the application's functionality, user
interface, and overall user experience.
This commit is contained in:
Cristhian Zanforlin Lousa 2024-06-06 21:25:37 -03:00 • committed by GitHub
commit 106ce9e64b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 321 additions and 184 deletions

View file

@ -480,7 +480,6 @@
}, },
"node_modules/@clack/prompts/node_modules/is-unicode-supported": { "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0", "version": "1.3.0",
"extraneous": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {

View file

@ -56,14 +56,14 @@ export default function GenericNode({
const [nodeName, setNodeName] = useState(data.node!.display_name); const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false); const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState( const [nodeDescription, setNodeDescription] = useState(
data.node?.description! data.node?.description!,
); );
const [isOutdated, setIsOutdated] = useState(false); const [isOutdated, setIsOutdated] = useState(false);
const buildStatus = useFlowStore( const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.status (state) => state.flowBuildStatus[data.id]?.status,
); );
const lastRunTime = useFlowStore( const lastRunTime = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.timestamp (state) => state.flowBuildStatus[data.id]?.timestamp,
); );
const [validationStatus, setValidationStatus] = const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null); useState<validationStatusType | null>(null);
@ -116,7 +116,7 @@ export default function GenericNode({
updateNodeInternals(data.id); updateNodeInternals(data.id);
}, },
[data.id, data.node, setNode, setIsOutdated] [data.id, data.node, setNode, setIsOutdated],
); );
if (!data.node!.template) { if (!data.node!.template) {
@ -255,8 +255,17 @@ export default function GenericNode({
const isDark = useDarkStore((state) => state.dark); const isDark = useDarkStore((state) => state.dark);
const renderIconStatus = ( const renderIconStatus = (
buildStatus: BuildStatus | undefined, buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null validationStatus: validationStatusType | null,
) => { ) => {
const conditionSuccess = validationStatus && validationStatus.valid;
const conditionInactive =
validationStatus &&
!validationStatus.valid &&
buildStatus === BuildStatus.INACTIVE;
const conditionError =
buildStatus === BuildStatus.ERROR ||
(validationStatus && !validationStatus.valid);
if (buildStatus === BuildStatus.BUILDING) { if (buildStatus === BuildStatus.BUILDING) {
return <Loading className="text-medium-indigo" />; return <Loading className="text-medium-indigo" />;
} else { } else {
@ -264,31 +273,30 @@ export default function GenericNode({
<> <>
<IconComponent <IconComponent
name="Play" name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-medium-indigo opacity-0 transition-all group-hover:opacity-100" className={cn(
!conditionSuccess && !conditionInactive && !conditionError
? "opacity-100"
: "opacity-0",
"absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground transition-all group-hover:text-medium-indigo group-hover/node:opacity-100",
)}
/> />
{validationStatus && validationStatus.valid ? ( {conditionSuccess ? (
<Checkmark <Checkmark
className="absolute ml-0.5 h-5 stroke-2 text-status-green opacity-100 transition-all group-hover:opacity-0" className="absolute ml-0.5 h-5 stroke-2 text-status-green opacity-100 transition-all group-hover/node:opacity-0"
isVisible={true} isVisible={true}
/> />
) : validationStatus && ) : conditionInactive ? (
!validationStatus.valid &&
buildStatus === BuildStatus.INACTIVE ? (
<IconComponent <IconComponent
name="Play" name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0" className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-gray opacity-30 transition-all group-hover/node:opacity-0"
/> />
) : buildStatus === BuildStatus.ERROR || ) : conditionError ? (
(validationStatus && !validationStatus.valid) ? (
<Xmark <Xmark
isVisible={true} isVisible={true}
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0" className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover/node:opacity-0"
/> />
) : ( ) : (
<IconComponent <></>
name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground opacity-100 transition-all group-hover:opacity-0"
/>
)} )}
</> </>
); );
@ -296,7 +304,7 @@ export default function GenericNode({
}; };
const getSpecificClassFromBuildStatus = ( const getSpecificClassFromBuildStatus = (
buildStatus: BuildStatus | undefined, buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null validationStatus: validationStatusType | null,
) => { ) => {
let isInvalid = validationStatus && !validationStatus.valid; let isInvalid = validationStatus && !validationStatus.valid;
@ -320,11 +328,11 @@ export default function GenericNode({
selected: boolean, selected: boolean,
showNode: boolean, showNode: boolean,
buildStatus: BuildStatus | undefined, buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null validationStatus: validationStatusType | null,
) => { ) => {
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus( const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
buildStatus, buildStatus,
validationStatus validationStatus,
); );
const baseBorderClass = getBaseBorderClass(selected); const baseBorderClass = getBaseBorderClass(selected);
@ -332,8 +340,8 @@ export default function GenericNode({
const names = classNames( const names = classNames(
baseBorderClass, baseBorderClass,
nodeSizeClass, nodeSizeClass,
"generic-node-div", "generic-node-div group/node",
specificClassFromBuildStatus specificClassFromBuildStatus,
); );
return names; return names;
}; };
@ -393,7 +401,7 @@ export default function GenericNode({
selected, selected,
showNode, showNode,
buildStatus, buildStatus,
validationStatus validationStatus,
)} )}
> >
{data.node?.beta && showNode && ( {data.node?.beta && showNode && (
@ -524,7 +532,7 @@ export default function GenericNode({
} }
title={getFieldTitle( title={getFieldTitle(
data.node?.template!, data.node?.template!,
templateField templateField,
)} )}
info={data.node?.template[templateField].info} info={data.node?.template[templateField].info}
name={templateField} name={templateField}
@ -552,7 +560,7 @@ export default function GenericNode({
proxy={data.node?.template[templateField].proxy} proxy={data.node?.template[templateField].proxy}
showNode={showNode} showNode={showNode}
/> />
) ),
)} )}
<ParameterComponent <ParameterComponent
key={scapedJSONStringfy({ key={scapedJSONStringfy({
@ -709,7 +717,7 @@ export default function GenericNode({
!data.node?.description) && !data.node?.description) &&
nameEditable nameEditable
? "font-light italic" ? "font-light italic"
: "" : "",
)} )}
onClick={(e) => { onClick={(e) => {
setInputDescription(true); setInputDescription(true);
@ -771,13 +779,13 @@ export default function GenericNode({
} }
title={getFieldTitle( title={getFieldTitle(
data.node?.template!, data.node?.template!,
templateField templateField,
)} )}
info={data.node?.template[templateField].info} info={data.node?.template[templateField].info}
name={templateField} name={templateField}
tooltipTitle={ tooltipTitle={
data.node?.template[templateField].input_types?.join( data.node?.template[templateField].input_types?.join(
"\n" "\n",
) ?? data.node?.template[templateField].type ) ?? data.node?.template[templateField].type
} }
required={data.node!.template[templateField].required} required={data.node!.template[templateField].required}
@ -804,7 +812,7 @@ export default function GenericNode({
<div <div
className={classNames( className={classNames(
Object.keys(data.node!.template).length < 1 ? "hidden" : "", Object.keys(data.node!.template).length < 1 ? "hidden" : "",
"flex-max-width justify-center" "flex-max-width justify-center",
)} )}
> >
{" "} {" "}

View file

@ -51,13 +51,15 @@ export default function ErrorAlert({
/> />
</div> </div>
<div className="ml-3"> <div className="ml-3">
<h3 className="error-build-foreground">{title}</h3> <h3 className="error-build-foreground line-clamp-2">{title}</h3>
{list?.length !== 0 && {list?.length !== 0 &&
list?.some((item) => item !== null && item !== undefined) ? ( list?.some((item) => item !== null && item !== undefined) ? (
<div className="error-build-message-div"> <div className="error-build-message-div">
<ul className="error-build-message-list"> <ul className="error-build-message-list">
{list.map((item, index) => ( {list.map((item, index) => (
<li key={index}>{item}</li> <li key={index} className="line-clamp-5">
{item}
</li>
))} ))}
</ul> </ul>
</div> </div>

View file

@ -47,7 +47,7 @@ export default function NoticeAlert({
/> />
</div> </div>
<div className="ml-3 flex-1 md:flex md:justify-between"> <div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-info-foreground word-break-break-word"> <p className="line-clamp-2 text-sm text-info-foreground word-break-break-word">
{title} {title}
</p> </p>
<p className="mt-3 text-sm md:ml-6 md:mt-0"> <p className="mt-3 text-sm md:ml-6 md:mt-0">

View file

@ -45,7 +45,7 @@ export default function SuccessAlert({
/> />
</div> </div>
<div className="ml-3"> <div className="ml-3">
<p className="success-alert-message">{title}</p> <p className="success-alert-message line-clamp-2">{title}</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -56,7 +56,7 @@ export default function Header(): JSX.Element {
const lastFlowVisitedIndex = routeHistory const lastFlowVisitedIndex = routeHistory
.reverse() .reverse()
.findIndex( .findIndex(
(path) => path.includes("/flow/") && path !== location.pathname (path) => path.includes("/flow/") && path !== location.pathname,
); );
const lastFlowVisited = routeHistory[lastFlowVisitedIndex]; const lastFlowVisited = routeHistory[lastFlowVisitedIndex];
@ -200,6 +200,28 @@ export default function Header(): JSX.Element {
/> />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
{!autoLogin && (
<>
<DropdownMenuLabel>
<div className="flex items-center gap-3">
<div
className={
"h-5 w-5 rounded-full focus-visible:outline-0 " +
(userData?.profile_image ??
(userData?.id
? gradients[
parseInt(userData?.id ?? "", 30) %
gradients.length
]
: "bg-gray-500"))
}
/>
{userData?.username ?? "User"}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuLabel>General</DropdownMenuLabel> <DropdownMenuLabel>General</DropdownMenuLabel>
<DropdownMenuItem <DropdownMenuItem
className="cursor-pointer" className="cursor-pointer"

View file

@ -13,6 +13,7 @@ import IconComponent, {
import { Button, buttonVariants } from "../../../ui/button"; import { Button, buttonVariants } from "../../../ui/button";
import { Input } from "../../../ui/input"; import { Input } from "../../../ui/input";
import useFileDrop from "../../hooks/use-on-file-drop"; import useFileDrop from "../../hooks/use-on-file-drop";
import useAlertStore from "../../../../stores/alertStore";
type SideBarFoldersButtonsComponentProps = { type SideBarFoldersButtonsComponentProps = {
folders: FolderType[]; folders: FolderType[];
@ -33,7 +34,7 @@ const SideBarFoldersButtonsComponent = ({
const [foldersNames, setFoldersNames] = useState({}); const [foldersNames, setFoldersNames] = useState({});
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [editFolders, setEditFolderName] = useState( const [editFolders, setEditFolderName] = useState(
folders.map((obj) => ({ name: obj.name, edit: false })) folders.map((obj) => ({ name: obj.name, edit: false })),
); );
const uploadFolder = useFolderStore((state) => state.uploadFolder); const uploadFolder = useFolderStore((state) => state.uploadFolder);
const currentFolder = pathname.split("/"); const currentFolder = pathname.split("/");
@ -51,6 +52,7 @@ const SideBarFoldersButtonsComponent = ({
const location = useLocation(); const location = useLocation();
const folderId = location?.state?.folderId ?? myCollectionId; const folderId = location?.state?.folderId ?? myCollectionId;
const getFolderById = useFolderStore((state) => state.getFolderById); const getFolderById = useFolderStore((state) => state.getFolderById);
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleFolderChange = (folderId: string) => { const handleFolderChange = (folderId: string) => {
getFolderById(folderId); getFolderById(folderId);
@ -58,11 +60,21 @@ const SideBarFoldersButtonsComponent = ({
const { dragOver, dragEnter, dragLeave, onDrop } = useFileDrop( const { dragOver, dragEnter, dragLeave, onDrop } = useFileDrop(
folderId, folderId,
handleFolderChange handleFolderChange,
); );
const handleUploadFlowsToFolder = () => { const handleUploadFlowsToFolder = () => {
uploadFolder(folderId); uploadFolder(folderId)
.then(() => {
getFolderById(folderId);
})
.catch((err) => {
console.log(err);
setErrorData({
title: `Error on upload`,
list: [err["response"]["data"]],
});
});
}; };
const handleDownloadFolder = (id: string) => { const handleDownloadFolder = (id: string) => {
@ -73,7 +85,7 @@ const SideBarFoldersButtonsComponent = ({
addFolder({ name: "New Folder", parent_id: null, description: "" }).then( addFolder({ name: "New Folder", parent_id: null, description: "" }).then(
(res) => { (res) => {
getFoldersApi(true); getFoldersApi(true);
} },
); );
} }
@ -119,7 +131,7 @@ const SideBarFoldersButtonsComponent = ({
<> <>
{folders.map((item, index) => { {folders.map((item, index) => {
const editFolderName = editFolders?.filter( const editFolderName = editFolders?.filter(
(folder) => folder.name === item.name (folder) => folder.name === item.name,
)[0]; )[0];
return ( return (
<div <div
@ -135,7 +147,7 @@ const SideBarFoldersButtonsComponent = ({
? "border border-border bg-muted hover:bg-muted" ? "border border-border 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" : "",
)} )}
onClick={() => handleChangeFolder!(item.id!)} onClick={() => handleChangeFolder!(item.id!)}
> >
@ -205,7 +217,7 @@ const SideBarFoldersButtonsComponent = ({
folders.map((obj) => ({ folders.map((obj) => ({
name: obj.name, name: obj.name,
edit: false, edit: false,
})) })),
); );
} }
if (e.key === "Enter") { if (e.key === "Enter") {
@ -238,10 +250,10 @@ const SideBarFoldersButtonsComponent = ({
}; };
const updatedFolder = await updateFolder( const updatedFolder = await updateFolder(
body, body,
item.id! item.id!,
); );
const updateFolders = folders.filter( const updateFolders = folders.filter(
(f) => f.name !== item.name (f) => f.name !== item.name,
); );
setFolders([...updateFolders, updatedFolder]); setFolders([...updateFolders, updatedFolder]);
setFoldersNames({}); setFoldersNames({});
@ -249,7 +261,7 @@ const SideBarFoldersButtonsComponent = ({
folders.map((obj) => ({ folders.map((obj) => ({
name: obj.name, name: obj.name,
edit: false, edit: false,
})) })),
); );
} else { } else {
setFoldersNames((old) => ({ setFoldersNames((old) => ({

View file

@ -668,7 +668,7 @@ export const ZERO_NOTIFICATIONS = "No new notifications";
export const SUCCESS_BUILD = "Built sucessfully ✨"; export const SUCCESS_BUILD = "Built sucessfully ✨";
export const ALERT_SAVE_WITH_API = export const ALERT_SAVE_WITH_API =
"Caution: Uncheck this box only removes API keys from fields specifically designated for API keys."; "Caution: Unchecking this box only removes API keys from fields specifically designated for API keys.";
export const SAVE_WITH_API_CHECKBOX = "Save with my API keys"; export const SAVE_WITH_API_CHECKBOX = "Save with my API keys";
export const EDIT_TEXT_MODAL_TITLE = "Edit Text"; export const EDIT_TEXT_MODAL_TITLE = "Edit Text";

View file

@ -45,7 +45,7 @@ const ExportModal = forwardRef(
is_component: false, is_component: false,
}, },
name!, name!,
description description,
); );
setNoticeData({ setNoticeData({
title: API_WARNING_NOTICE_ALERT, title: API_WARNING_NOTICE_ALERT,
@ -61,7 +61,7 @@ const ExportModal = forwardRef(
is_component: false, is_component: false,
}), }),
name!, name!,
description description,
); );
setOpen(false); setOpen(false);
}} }}
@ -94,7 +94,7 @@ const ExportModal = forwardRef(
{SAVE_WITH_API_CHECKBOX} {SAVE_WITH_API_CHECKBOX}
</label> </label>
</div> </div>
<span className=" text-xs text-destructive "> <span className="mt-1 text-xs text-destructive ">
{ALERT_SAVE_WITH_API} {ALERT_SAVE_WITH_API}
</span> </span>
</BaseModal.Content> </BaseModal.Content>
@ -102,6 +102,6 @@ const ExportModal = forwardRef(
<BaseModal.Footer submit={{ label: "Download Flow" }} /> <BaseModal.Footer submit={{ label: "Download Flow" }} />
</BaseModal> </BaseModal>
); );
} },
); );
export default ExportModal; export default ExportModal;

View file

@ -4,7 +4,9 @@ import { FormProvider, useForm, useWatch } from "react-hook-form";
import { Link, useLocation, useNavigate } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import CollectionCardComponent from "../../../../components/cardComponent"; import CollectionCardComponent from "../../../../components/cardComponent";
import CardsWrapComponent from "../../../../components/cardsWrapComponent"; import CardsWrapComponent from "../../../../components/cardsWrapComponent";
import IconComponent from "../../../../components/genericIconComponent"; import IconComponent, {
ForwardedIconComponent,
} from "../../../../components/genericIconComponent";
import PaginatorComponent from "../../../../components/paginatorComponent"; import PaginatorComponent from "../../../../components/paginatorComponent";
import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent"; import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent";
import { Button } from "../../../../components/ui/button"; import { Button } from "../../../../components/ui/button";
@ -18,6 +20,9 @@ import { getNameByType } from "../../utils/get-name-by-type";
import { sortFlows } from "../../utils/sort-flows"; import { sortFlows } from "../../utils/sort-flows";
import EmptyComponent from "../emptyComponent"; import EmptyComponent from "../emptyComponent";
import HeaderComponent from "../headerComponent"; import HeaderComponent from "../headerComponent";
import { downloadFlow, removeApiKeys } from "../../../../utils/reactflowUtils";
import { useDarkStore } from "../../../../stores/darkStore";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
export default function ComponentsComponent({ export default function ComponentsComponent({
type = "all", type = "all",
@ -31,22 +36,22 @@ export default function ComponentsComponent({
const allFlows = useFlowsManagerStore((state) => state.allFlows); const allFlows = useFlowsManagerStore((state) => state.allFlows);
const flowsFromFolder = useFolderStore( const flowsFromFolder = useFolderStore(
(state) => state.selectedFolder?.flows (state) => state.selectedFolder?.flows,
); );
const setSuccessData = useAlertStore((state) => state.setSuccessData); const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const [openDelete, setOpenDelete] = useState(false); const [openDelete, setOpenDelete] = useState(false);
const searchFlowsComponents = useFlowsManagerStore( const searchFlowsComponents = useFlowsManagerStore(
(state) => state.searchFlowsComponents (state) => state.searchFlowsComponents,
); );
const setSelectedFlowsComponentsCards = useFlowsManagerStore( const setSelectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.setSelectedFlowsComponentsCards (state) => state.setSelectedFlowsComponentsCards,
); );
const selectedFlowsComponentsCards = useFlowsManagerStore( const selectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.selectedFlowsComponentsCards (state) => state.selectedFlowsComponentsCards,
); );
const [handleFileDrop] = useFileDrop(uploadFlow, type)!; const [handleFileDrop] = useFileDrop(uploadFlow, type)!;
@ -82,7 +87,7 @@ export default function ComponentsComponent({
f.name.toLowerCase().includes(searchFlowsComponents.toLowerCase()) || f.name.toLowerCase().includes(searchFlowsComponents.toLowerCase()) ||
f.description f.description
.toLowerCase() .toLowerCase()
.includes(searchFlowsComponents.toLowerCase()) .includes(searchFlowsComponents.toLowerCase()),
); );
if (searchFlowsComponents === "") { if (searchFlowsComponents === "") {
@ -129,6 +134,8 @@ export default function ComponentsComponent({
setOpenDelete(true); setOpenDelete(true);
} else if (action === "duplicate") { } else if (action === "duplicate") {
handleDuplicate(); handleDuplicate();
} else if (action === "export") {
handleExport();
} }
}; };
@ -137,9 +144,9 @@ export default function ComponentsComponent({
selectedFlowsComponentsCards.map((selectedFlow) => selectedFlowsComponentsCards.map((selectedFlow) =>
addFlow( addFlow(
true, true,
allFlows.find((flow) => flow.id === selectedFlow) allFlows.find((flow) => flow.id === selectedFlow),
) ),
) ),
).then(() => { ).then(() => {
resetFilter(); resetFilter();
getFoldersApi(true); getFoldersApi(true);
@ -152,6 +159,47 @@ export default function ComponentsComponent({
}); });
}; };
const handleImport = () => {
uploadFlow({ newProject: true, isComponent: false })
.then(() => {
resetFilter();
getFoldersApi(true);
if (!folderId || folderId === myCollectionId) {
getFolderById(folderId ? folderId : myCollectionId);
}
setSelectedFlowsComponentsCards([]);
setSuccessData({ title: "Flows imported successfully" });
})
.catch((error) => {
setErrorData({
title: UPLOAD_ERROR_ALERT,
list: [error],
});
});
};
const version = useDarkStore((state) => state.version);
const handleExport = () => {
selectedFlowsComponentsCards.map((selectedFlowId) => {
const selectedFlow = allFlows.find((flow) => flow.id === selectedFlowId);
downloadFlow(
removeApiKeys({
id: selectedFlow!.id,
data: selectedFlow!.data!,
description: selectedFlow!.description,
name: selectedFlow!.name,
last_tested_version: version,
is_component: false,
}),
selectedFlow!.name,
selectedFlow!.description,
);
});
setSuccessData({ title: "Flows exported successfully" });
};
const handleDeleteMultiple = () => { const handleDeleteMultiple = () => {
removeFlow(selectedFlowsComponentsCards) removeFlow(selectedFlowsComponentsCards)
.then(() => { .then(() => {
@ -161,7 +209,7 @@ export default function ComponentsComponent({
getFolderById(folderId ? folderId : myCollectionId); getFolderById(folderId ? folderId : myCollectionId);
} }
setSuccessData({ setSuccessData({
title: "Selected items deleted successfully!", title: "Selected items deleted successfully",
}); });
}) })
.catch(() => { .catch(() => {
@ -180,7 +228,7 @@ export default function ComponentsComponent({
return true; return true;
} }
return false; return false;
} },
); );
setSelectedFlowsComponentsCards(selectedFlows); setSelectedFlowsComponentsCards(selectedFlows);
@ -215,20 +263,23 @@ export default function ComponentsComponent({
if (type === "all") return allFlows?.length; if (type === "all") return allFlows?.length;
return allFlows?.filter( return allFlows?.filter(
(f) => (f.is_component ?? false) === (type === "component") (f) => (f.is_component ?? false) === (type === "component"),
)?.length; )?.length;
}; };
return ( return (
<> <>
{allFlows?.length > 0 && ( <div className="flex w-full gap-4 pb-5">
<HeaderComponent {allFlows?.length > 0 && (
handleDelete={() => handleSelectOptionsChange("delete")} <HeaderComponent
handleSelectAll={handleSelectAll} handleDelete={() => handleSelectOptionsChange("delete")}
handleDuplicate={() => handleSelectOptionsChange("duplicate")} handleSelectAll={handleSelectAll}
disableFunctions={!(selectedFlowsComponentsCards?.length > 0)} handleDuplicate={() => handleSelectOptionsChange("duplicate")}
/> handleExport={() => handleSelectOptionsChange("export")}
)} disableFunctions={!(selectedFlowsComponentsCards?.length > 0)}
/>
)}
</div>
<CardsWrapComponent <CardsWrapComponent
onFileDrop={handleFileDrop} onFileDrop={handleFileDrop}

View file

@ -13,7 +13,7 @@ const EmptyComponent = ({}: EmptyComponentProps) => {
return ( return (
<> <>
<div className="mt-6 flex w-full items-center justify-center text-center"> <div className="mt-2 flex w-full items-center justify-center text-center">
<div className="flex-max-width h-full flex-col"> <div className="flex-max-width h-full flex-col">
<div className="align-center flex w-full justify-center gap-1 "> <div className="align-center flex w-full justify-center gap-1 ">
<span className="text-muted-foreground"> <span className="text-muted-foreground">

View file

@ -1,13 +1,17 @@
import { useState } from "react"; import { useState } from "react";
import IconComponent from "../../../../components/genericIconComponent"; import IconComponent, {
ForwardedIconComponent,
} from "../../../../components/genericIconComponent";
import ShadTooltip from "../../../../components/shadTooltipComponent"; import ShadTooltip from "../../../../components/shadTooltipComponent";
import { Checkbox } from "../../../../components/ui/checkbox"; import { Checkbox } from "../../../../components/ui/checkbox";
import { cn } from "../../../../utils/utils"; import { cn } from "../../../../utils/utils";
import { Button } from "../../../../components/ui/button";
type HeaderComponentProps = { type HeaderComponentProps = {
handleSelectAll: (select) => void; handleSelectAll: (select) => void;
handleDelete: () => void; handleDelete: () => void;
handleDuplicate: () => void; handleDuplicate: () => void;
handleExport: () => void;
disableFunctions: boolean; disableFunctions: boolean;
}; };
@ -15,6 +19,7 @@ const HeaderComponent = ({
handleSelectAll, handleSelectAll,
handleDelete, handleDelete,
handleDuplicate, handleDuplicate,
handleExport,
disableFunctions, disableFunctions,
}: HeaderComponentProps) => { }: HeaderComponentProps) => {
const [shouldSelectAll, setShouldSelectAll] = useState(true); const [shouldSelectAll, setShouldSelectAll] = useState(true);
@ -26,29 +31,46 @@ const HeaderComponent = ({
return ( return (
<> <>
<div className="grid grid-cols-3 pb-5"> <div className="flex w-full items-center justify-between gap-4">
<div className="col-auto grid-cols-1 self-center justify-self-start"> <div className="flex items-center justify-self-start">
<a onClick={handleClick} className="text-sm"> <Button
<div className="header-menu-bar-display "> variant="none"
<div size="none"
className="header-menu-flow-name" onClick={handleClick}
data-testid="select_all_collection" className="text-sm"
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox checked={!shouldSelectAll} id="terms" /> <Checkbox checked={!shouldSelectAll} id="terms" />
<label <span className="label text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
onClick={handleClick} {shouldSelectAll ? "Select All" : "Unselect All"}
htmlFor="terms" </span>
className="label cursor-pointer text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{shouldSelectAll ? "Select All" : "Unselect All"}
</label>
</div>
</div>
</div> </div>
</a> </Button>
</div> </div>
<div className="col-span-2 flex grid-cols-1 gap-2 justify-self-end"> <div className="flex items-center gap-2">
<div>
<ShadTooltip
content={
disableFunctions ? (
<span>Select items to export</span>
) : (
<span>Export selected items</span>
)
}
>
<Button
variant="none"
size="none"
onClick={handleExport}
disabled={disableFunctions}
>
<IconComponent
name="FileDown"
className={cn("h-5 w-5 text-primary transition-all")}
/>
</Button>
</ShadTooltip>
</div>
<div> <div>
<ShadTooltip <ShadTooltip
content={ content={
@ -59,12 +81,17 @@ const HeaderComponent = ({
) )
} }
> >
<button onClick={handleDuplicate} disabled={disableFunctions}> <Button
variant="none"
size="none"
onClick={handleDuplicate}
disabled={disableFunctions}
>
<IconComponent <IconComponent
name="Copy" name="Copy"
className={cn("h-5 w-5 text-primary transition-all")} className={cn("h-5 w-5 text-primary transition-all")}
/> />
</button> </Button>
</ShadTooltip> </ShadTooltip>
</div> </div>
<div> <div>
@ -77,15 +104,20 @@ const HeaderComponent = ({
) )
} }
> >
<button onClick={handleDelete} disabled={disableFunctions}> <Button
variant="none"
size="none"
onClick={handleDelete}
disabled={disableFunctions}
>
<IconComponent <IconComponent
name="Trash2" name="Trash2"
className={cn( className={cn(
"h-5 w-5 text-primary transition-all", "h-5 w-5 text-primary transition-all",
disableFunctions ? "" : "hover:text-destructive" disableFunctions ? "" : "hover:text-destructive",
)} )}
/> />
</button> </Button>
</ShadTooltip> </ShadTooltip>
</div> </div>
</div> </div>

View file

@ -8,7 +8,7 @@ import useFlowsManagerStore from "../../stores/flowsManagerStore";
export default function SettingsPage(): JSX.Element { export default function SettingsPage(): JSX.Element {
const pathname = location.pathname; const pathname = location.pathname;
const setCurrentFlowId = useFlowsManagerStore( const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId (state) => state.setCurrentFlowId,
); );
useEffect(() => { useEffect(() => {
setCurrentFlowId(""); setCurrentFlowId("");
@ -59,7 +59,10 @@ export default function SettingsPage(): JSX.Element {
title: "Messages", title: "Messages",
href: "/settings/messages", href: "/settings/messages",
icon: ( icon: (
<ForwardedIconComponent name="Keyboard" className="w-5 stroke-[1.5]" /> <ForwardedIconComponent
name="MessagesSquare"
className="w-4 flex-shrink-0 justify-start stroke-[1.5]"
/>
), ),
}, },
]; ];
@ -72,8 +75,10 @@ export default function SettingsPage(): JSX.Element {
<aside className="flex h-full shrink-0 flex-col space-y-6 lg:w-[20vw]"> <aside className="flex h-full shrink-0 flex-col space-y-6 lg:w-[20vw]">
<SidebarNav items={sidebarNavItems} /> <SidebarNav items={sidebarNavItems} />
</aside> </aside>
<div className="h-full w-full flex-1 pb-8"> <div className="flex h-full w-full flex-1 flex-col">
<Outlet /> <div className="flex-1 pb-8">
<Outlet />
</div>
</div> </div>
</div> </div>
</PageLayout> </PageLayout>

View file

@ -52,23 +52,17 @@ export default function ApiKeysPage() {
/> />
<div className="flex h-full w-full flex-col justify-between"> <div className="flex h-full w-full flex-col justify-between">
<Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4"> <TableComponent
<CardContent className="h-full"> overlayNoRowsTemplate="No data available"
<TableComponent onSelectionChanged={(event: SelectionChangedEvent) => {
overlayNoRowsTemplate="No data available" setSelectedRows(event.api.getSelectedRows().map((row) => row.id));
onSelectionChanged={(event: SelectionChangedEvent) => { }}
setSelectedRows( rowSelection="multiple"
event.api.getSelectedRows().map((row) => row.id), suppressRowClickSelection={true}
); pagination={true}
}} columnDefs={columnDefs}
rowSelection="multiple" rowData={keysList.current}
suppressRowClickSelection={true} />
pagination={true}
columnDefs={columnDefs}
rowData={keysList.current}
/>
</CardContent>
</Card>
</div> </div>
</div> </div>
); );

View file

@ -16,13 +16,13 @@ import { cn } from "../../../../utils/utils";
export default function GlobalVariablesPage() { export default function GlobalVariablesPage() {
const globalVariablesEntries = useGlobalVariablesStore( const globalVariablesEntries = useGlobalVariablesStore(
(state) => state.globalVariablesEntries (state) => state.globalVariablesEntries,
); );
const removeGlobalVariable = useGlobalVariablesStore( const removeGlobalVariable = useGlobalVariablesStore(
(state) => state.removeGlobalVariable (state) => state.removeGlobalVariable,
); );
const globalVariables = useGlobalVariablesStore( const globalVariables = useGlobalVariablesStore(
(state) => state.globalVariables (state) => state.globalVariables,
); );
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const getVariableId = useGlobalVariablesStore((state) => state.getVariableId); const getVariableId = useGlobalVariablesStore((state) => state.getVariableId);
@ -154,7 +154,7 @@ export default function GlobalVariablesPage() {
<IconComponent <IconComponent
name="Trash2" name="Trash2"
className={cn( className={cn(
"h-5 w-5 text-destructive group-disabled:text-primary" "h-5 w-5 text-destructive group-disabled:text-primary",
)} )}
/> />
</Button> </Button>
@ -168,23 +168,17 @@ export default function GlobalVariablesPage() {
</div> </div>
<div className="flex h-full w-full flex-col justify-between"> <div className="flex h-full w-full flex-col justify-between">
<Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4"> <TableComponent
<CardContent className="h-full"> overlayNoRowsTemplate="No data available"
<TableComponent onSelectionChanged={(event: SelectionChangedEvent) => {
overlayNoRowsTemplate="No data available" setSelectedRows(event.api.getSelectedRows().map((row) => row.name));
onSelectionChanged={(event: SelectionChangedEvent) => { }}
setSelectedRows( rowSelection="multiple"
event.api.getSelectedRows().map((row) => row.name) suppressRowClickSelection={true}
); pagination={true}
}} columnDefs={colDefs}
rowSelection="multiple" rowData={rowData}
suppressRowClickSelection={true} />
pagination={true}
columnDefs={colDefs}
rowData={rowData}
/>
</CardContent>
</Card>
</div> </div>
</div> </div>
); );

View file

@ -115,15 +115,11 @@ export default function ShortcutsPage() {
</div> </div>
</div> </div>
<div className="flex h-full w-full flex-col justify-between"> <div className="flex h-full w-full flex-col justify-between">
<Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4"> <TableComponent
<CardContent className="h-full"> pagination={false}
<TableComponent columnDefs={colDefs}
pagination={false} rowData={nodesRowData}
columnDefs={colDefs} />
rowData={nodesRowData}
/>
</CardContent>
</Card>
</div> </div>
</div> </div>
); );

View file

@ -27,7 +27,7 @@ export default function MessagesPage() {
setSelectedRows, setSelectedRows,
setSuccessData, setSuccessData,
setErrorData, setErrorData,
selectedRows selectedRows,
); );
const { handleUpdate } = useUpdateMessage(setSuccessData, setErrorData); const { handleUpdate } = useUpdateMessage(setSuccessData, setErrorData);
@ -52,29 +52,25 @@ export default function MessagesPage() {
handleRemoveMessages={handleRemoveMessages} handleRemoveMessages={handleRemoveMessages}
/> />
<div className="flex h-full w-full flex-col justify-between pb-8"> <div className="flex h-full w-full flex-col justify-between">
<Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4"> <TableComponent
<CardContent className="h-full"> readOnlyEdit
<TableComponent onCellEditRequest={(event) => {
readOnlyEdit handleUpdateMessage(event);
onCellEditRequest={(event) => { }}
handleUpdateMessage(event); editable={["Sender Name", "Message"]}
}} overlayNoRowsTemplate="No data available"
editable={["Sender Name", "Message"]} onSelectionChanged={(event: SelectionChangedEvent) => {
overlayNoRowsTemplate="No data available" setSelectedRows(
onSelectionChanged={(event: SelectionChangedEvent) => { event.api.getSelectedRows().map((row) => row.index),
setSelectedRows( );
event.api.getSelectedRows().map((row) => row.index) }}
); rowSelection="multiple"
}} suppressRowClickSelection={true}
rowSelection="multiple" pagination={true}
suppressRowClickSelection={true} columnDefs={columns}
pagination={true} rowData={messages}
columnDefs={columns} />
rowData={messages}
/>
</CardContent>
</Card>
</div> </div>
</div> </div>
); );

View file

@ -7,6 +7,7 @@ import {
} from "../pages/MainPage/services"; } from "../pages/MainPage/services";
import { FoldersStoreType } from "../types/zustand/folders"; import { FoldersStoreType } from "../types/zustand/folders";
import useFlowsManagerStore from "./flowsManagerStore"; import useFlowsManagerStore from "./flowsManagerStore";
import { uploadFlowsToDatabase } from "../controllers/API";
export const useFolderStore = create<FoldersStoreType>((set, get) => ({ export const useFolderStore = create<FoldersStoreType>((set, get) => ({
folders: [], folders: [],
@ -17,18 +18,18 @@ export const useFolderStore = create<FoldersStoreType>((set, get) => ({
getFolders().then( getFolders().then(
(res) => { (res) => {
const foldersWithoutStarterProjects = res.filter( const foldersWithoutStarterProjects = res.filter(
(folder) => folder.name !== STARTER_FOLDER_NAME (folder) => folder.name !== STARTER_FOLDER_NAME,
); );
const starterProjects = res.find( const starterProjects = res.find(
(folder) => folder.name === STARTER_FOLDER_NAME (folder) => folder.name === STARTER_FOLDER_NAME,
); );
set({ starterProjectId: starterProjects!.id ?? "" }); set({ starterProjectId: starterProjects!.id ?? "" });
set({ folders: foldersWithoutStarterProjects }); set({ folders: foldersWithoutStarterProjects });
const myCollectionId = res?.find( const myCollectionId = res?.find(
(f) => f.name === DEFAULT_FOLDER (f) => f.name === DEFAULT_FOLDER,
)?.id; )?.id;
set({ myCollectionId }); set({ myCollectionId });
@ -45,7 +46,7 @@ export const useFolderStore = create<FoldersStoreType>((set, get) => ({
set({ folders: [] }); set({ folders: [] });
get().setLoading(false); get().setLoading(false);
reject(); reject();
} },
); );
} }
}); });
@ -65,7 +66,7 @@ export const useFolderStore = create<FoldersStoreType>((set, get) => ({
}, },
() => { () => {
get().setLoadingById(false); get().setLoadingById(false);
} },
); );
} }
}, },
@ -100,7 +101,7 @@ export const useFolderStore = create<FoldersStoreType>((set, get) => ({
folderIdDragging: "", folderIdDragging: "",
setFolderIdDragging: (id) => set(() => ({ folderIdDragging: id })), setFolderIdDragging: (id) => set(() => ({ folderIdDragging: id })),
uploadFolder: () => { uploadFolder: () => {
return new Promise<void>(() => { return new Promise<void>((resolve, reject) => {
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "file"; input.type = "file";
input.onchange = (event: Event) => { input.onchange = (event: Event) => {
@ -111,8 +112,31 @@ export const useFolderStore = create<FoldersStoreType>((set, get) => ({
const file = (event.target as HTMLInputElement).files![0]; const file = (event.target as HTMLInputElement).files![0];
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
uploadFlowsFromFolders(formData).then(() => { file.text().then((text) => {
get().getFoldersApi(true); const data = JSON.parse(text);
if (data.data?.nodes) {
useFlowsManagerStore
.getState()
.addFlow(true, data)
.then(() => {
resolve();
})
.catch((error) => {
reject(error);
});
} else {
uploadFlowsFromFolders(formData)
.then(() => {
get()
.getFoldersApi(true)
.then(() => {
resolve();
});
})
.catch((error) => {
reject(error);
});
}
}); });
} }
}; };

View file

@ -20,7 +20,7 @@ export type FoldersStoreType = {
setFolderUrl: (folderUrl: string) => void; setFolderUrl: (folderUrl: string) => void;
folderDragging: boolean; folderDragging: boolean;
setFolderDragging: (set: boolean) => void; setFolderDragging: (set: boolean) => void;
uploadFolder: (folderId: string) => void; uploadFolder: (folderId: string) => Promise<void>;
folderIdDragging: string; folderIdDragging: string;
setFolderIdDragging: (id: string) => void; setFolderIdDragging: (id: string) => void;
starterProjectId: string; starterProjectId: string;

View file

@ -56,6 +56,7 @@ import {
FolderIcon, FolderIcon,
FolderPlus, FolderPlus,
FolderPlusIcon, FolderPlusIcon,
FolderUp,
FormInput, FormInput,
Forward, Forward,
Gift, Gift,
@ -431,6 +432,7 @@ export const nodeIconsLucide: iconsType = {
ChevronLeft, ChevronLeft,
SlidersHorizontal, SlidersHorizontal,
Palette, Palette,
FolderUp,
Blocks, Blocks,
ChevronDown, ChevronDown,
ArrowLeft, ArrowLeft,