refactor: queries loading order and wrapper order (#3603)

* Added loading page

* Removed unused loadings and changed loading to LoadingPage

* Refactored ComponentsComponent to receive info from parent

* refactored headerTabsComponent to receive loading from parent

* Added loading of folders into MyCollectionComponent

* removed unused loading and folderSelected

* updated get config api call to update everything

* Make app wait for autoLogin to be set to execute everything else

* changed API type to not contain params if its undefined

* Updated get autologin to do all logic regarding autologin

* Updated other queries with the new useQueryFunctionType type

* Updated App.tsx with new gets and configurations and added a loader before loading the router

* Made ProtectedRoute refresh on authentication change

* Fixed order of wrappers in order for Auth and  API context to have access to router

* Made loading only exist in one place

* 📝 (folders.spec.ts): remove unused test for adding folder by drag and drop to improve test suite cleanliness and maintainability.

* Fixed flow dropping to another folder

*  (folders.spec.ts): add test for adding folder by drag and drop functionality
🔧 (auto-save-off.spec.ts): add click event for "Save And Exit" button
🔧 (dragAndDrop.spec.ts): change dispatchEvent to getByTestId and add assertions for specific text visibility
🔧 (store-shard-3.spec.ts): increase timeout for page.waitForTimeout to improve test reliability

*  (folders.spec.ts): update test description to be more descriptive and accurate

* test: improve timeout for page.waitForSelector in auto-save-off.spec.ts

* feat: add replace button functionality to main page

The code changes include adding the functionality for the replace button on the main page. This allows users to replace a flow or a component. The replace button is now visible on the page, and clicking on it triggers the appropriate action.

Recent user commits:
- test: improve timeout for page.waitForSelector in auto-save-off.spec.ts
-  (folders.spec.ts): update test description to be more descriptive and accurate
-  (folders.spec.ts): add test for adding folder by drag and drop functionality
- 🔧 (auto-save-off.spec.ts): add click event for "Save And Exit" button
- 🔧 (dragAndDrop.spec.ts): change dispatchEvent to getByTestId and add assertions for specific text visibility
- 🔧 (store-shard-3.spec.ts): increase timeout for page.waitForTimeout to improve test reliability

Recent repository commits:
- test: improve timeout for page.waitForSelector in auto-save-off.spec.ts
-  (folders.spec.ts): update test description to be more descriptive and accurate
-  (folders.spec.ts): add test for adding folder by drag and drop functionality
- 🔧 (auto-save-off.spec.ts): add click event for "Save And Exit" button
- 🔧 (dragAndDrop.spec.ts): change dispatchEvent to getByTestId and add assertions for specific text visibility
- 🔧 (store-shard-3.spec.ts): increase timeout for page.waitForTimeout to improve test reliability
- Fixed flow dropping to another folder
- 📝 (folders.spec.ts): remove unused test for adding folder by drag and drop to improve test suite cleanliness and maintainability.
- Made loading only exist in one place
- Fixed order of wrappers in order for Auth and API context to have access to router
- Made ProtectedRoute refresh on authentication change
- Updated App.tsx with new gets and configurations and added a loader before loading the router
- Updated other queries with the new useQueryFunctionType type

---------

Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
Lucas Oliveira 2024-08-29 09:10:26 -03:00 committed by GitHub
commit af052285ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 393 additions and 371 deletions

View file

@ -1,90 +1,12 @@
import { Suspense, useContext, useEffect } from "react";
import { Cookies } from "react-cookie";
import { Suspense } from "react";
import { RouterProvider } from "react-router-dom";
import "reactflow/dist/style.css";
import LoadingComponent from "./components/loadingComponent";
import { AuthContext } from "./contexts/authContext";
import {
useAutoLogin,
useRefreshAccessToken,
} from "./controllers/API/queries/auth";
import { useGetVersionQuery } from "./controllers/API/queries/version";
import useSaveConfig from "./hooks/use-save-config";
import { LoadingPage } from "./pages/LoadingPage";
import router from "./routes";
import useAlertStore from "./stores/alertStore";
import useAuthStore from "./stores/authStore";
import { useDarkStore } from "./stores/darkStore";
import useFlowsManagerStore from "./stores/flowsManagerStore";
export default function App() {
const { login, setUserData, getUser } = useContext(AuthContext);
const setAutoLogin = useAuthStore((state) => state.setAutoLogin);
const setLoading = useAlertStore((state) => state.setLoading);
const refreshStars = useDarkStore((state) => state.refreshStars);
const dark = useDarkStore((state) => state.dark);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const cookies = new Cookies();
const logout = useAuthStore((state) => state.logout);
const refreshToken = cookies.get("refresh_token");
const { mutate: mutateAutoLogin } = useAutoLogin();
const { mutate: mutateRefresh } = useRefreshAccessToken();
const isLoginPage = location.pathname.includes("login");
useEffect(() => {
if (!dark) {
document.getElementById("body")!.classList.remove("dark");
} else {
document.getElementById("body")!.classList.add("dark");
}
}, [dark]);
useEffect(() => {
mutateAutoLogin(undefined, {
onSuccess: async (user) => {
if (user && user["access_token"]) {
user["refresh_token"] = "auto";
login(user["access_token"], "auto");
setUserData(user);
setAutoLogin(true);
refreshStars();
// mutateRefresh({ refresh_token: refreshToken });
}
},
onError: (error) => {
if (error.name !== "CanceledError") {
setAutoLogin(false);
if (!isLoginPage) {
if (!isAuthenticated) {
setLoading(false);
useFlowsManagerStore.setState({ isLoading: false });
logout();
} else {
mutateRefresh({ refresh_token: refreshToken });
refreshStars();
getUser();
}
}
}
},
});
}, []);
useGetVersionQuery();
useSaveConfig();
return (
//need parent component with width and height
<Suspense
fallback={
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
}
>
<Suspense fallback={<LoadingPage />}>
<RouterProvider router={router} />
</Suspense>
);

View file

@ -1,8 +1,8 @@
import { LoadingPage } from "@/pages/LoadingPage";
import useAuthStore from "@/stores/authStore";
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import { AuthContext } from "../../contexts/authContext";
import LoadingComponent from "../loadingComponent";
export const ProtectedAdminRoute = ({ children }) => {
const { userData } = useContext(AuthContext);
@ -11,11 +11,7 @@ export const ProtectedAdminRoute = ({ children }) => {
const isAdmin = useAuthStore((state) => state.isAdmin);
if (!isAuthenticated) {
return (
<div className="flex h-screen w-screen items-center justify-center">
<LoadingComponent remSize={30} />
</div>
);
return <LoadingPage />;
} else if ((userData && !isAdmin) || autoLogin) {
return <Navigate to="/" replace />;
} else {

View file

@ -25,14 +25,17 @@ export const ProtectedRoute = ({ children }) => {
? automaticRefreshTime
: envRefreshTime;
const intervalId = setInterval(() => {
const intervalFunction = () => {
if (isAuthenticated) {
mutateRefresh({ refresh_token: refreshToken });
}
}, accessTokenTimer * 1000);
};
const intervalId = setInterval(intervalFunction, accessTokenTimer * 1000);
intervalFunction();
return () => clearInterval(intervalId);
}, []);
}, [isAuthenticated]);
if (!isAuthenticated && hasToken) {
logout();

View file

@ -19,12 +19,12 @@ const useFileDrop = (folderId: string) => {
const flows = useFlowsManagerStore((state) => state.flows);
const saveFlow = useSaveFlow();
const { mutate: uploadFlowToFolder } = usePostUploadFlowToFolder();
const handleFileDrop = async (e) => {
const handleFileDrop = async (e, folderId) => {
if (e.dataTransfer.types.some((type) => type === "Files")) {
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const firstFile = e.dataTransfer.files[0];
if (firstFile.type === "application/json") {
uploadFormData(firstFile);
uploadFormData(firstFile, folderId);
} else {
setErrorData({
title: WRONG_FILE_ERROR_ALERT,
@ -94,7 +94,7 @@ const useFileDrop = (folderId: string) => {
}
e.preventDefault();
handleFileDrop(e);
handleFileDrop(e, folderId);
};
const uploadFromDragCard = (flowId, folderId) => {
@ -115,7 +115,7 @@ const useFileDrop = (folderId: string) => {
saveFlow(updatedFlow);
};
const uploadFormData = (data) => {
const uploadFormData = (data, folderId) => {
const formData = new FormData();
formData.append("file", data);
setFolderDragging(false);

View file

@ -8,7 +8,6 @@ import { useGetUserData } from "@/controllers/API/queries/auth";
import useAuthStore from "@/stores/authStore";
import { createContext, useEffect, useState } from "react";
import Cookies from "universal-cookie";
import useAlertStore from "../stores/alertStore";
import { useStoreStore } from "../stores/storeStore";
import { Users } from "../types/api";
import { AuthContextType } from "../types/contexts/auth";
@ -33,7 +32,6 @@ export function AuthProvider({ children }): React.ReactElement {
cookies.get(LANGFLOW_ACCESS_TOKEN) ?? null,
);
const [userData, setUserData] = useState<Users | null>(null);
const setLoading = useAlertStore((state) => state.setLoading);
const [apiKey, setApiKey] = useState<string | null>(
cookies.get(LANGFLOW_API_TOKEN),
);
@ -71,7 +69,6 @@ export function AuthProvider({ children }): React.ReactElement {
},
onError: () => {
setUserData(null);
setLoading(false);
},
},
);

View file

@ -1,4 +1,3 @@
import { useDarkStore } from "@/stores/darkStore";
import { useQueryFunctionType } from "@/types/api";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
@ -24,7 +23,7 @@ interface IApiQueryResponse {
export const useGetApiKeysQuery: useQueryFunctionType<
undefined,
IApiQueryResponse
> = (_, options) => {
> = (options) => {
const { query } = UseRequestProcessor();
const getApiKeysFn = async () => {

View file

@ -1,24 +1,57 @@
import { UseMutationResult } from "@tanstack/react-query";
import { useMutationFunctionType } from "../../../../types/api";
import { AuthContext } from "@/contexts/authContext";
import useAuthStore from "@/stores/authStore";
import { AxiosError } from "axios";
import { useContext } from "react";
import { useQueryFunctionType, Users } from "../../../../types/api";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor";
export const useAutoLogin: useMutationFunctionType<undefined, any> = (
options?,
export interface AutoLoginResponse {
frontend_timeout: number;
auto_saving: boolean;
auto_saving_interval: number;
health_check_max_retries: number;
}
export const useGetAutoLogin: useQueryFunctionType<undefined, undefined> = (
options,
) => {
const { mutate } = UseRequestProcessor();
const { query } = UseRequestProcessor();
const { login, setUserData, getUser } = useContext(AuthContext);
const setAutoLogin = useAuthStore((state) => state.setAutoLogin);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const logout = useAuthStore((state) => state.logout);
const isLoginPage = location.pathname.includes("login");
const autoLoginFn = async (): Promise<any> => {
const res = await api.get(`${getURL("AUTOLOGIN")}`);
return res.data;
};
async function getAutoLoginFn(): Promise<null> {
try {
const response = await api.get<Users>(`${getURL("AUTOLOGIN")}`);
const user = response.data;
if (user && user["access_token"]) {
user["refresh_token"] = "auto";
login(user["access_token"], "auto");
setUserData(user);
setAutoLogin(true);
}
} catch (e) {
const error = e as AxiosError;
if (error.name !== "CanceledError") {
setAutoLogin(false);
if (!isLoginPage) {
if (!isAuthenticated) {
await logout();
throw new Error("Unauthorized");
} else {
getUser();
}
}
}
}
return null;
}
const mutation: UseMutationResult = mutate(
["useAutoLogin"],
autoLoginFn,
options,
);
const queryResult = query(["useGetAutoLogin"], getAutoLoginFn, options);
return mutation;
return queryResult;
};

View file

@ -1,3 +1,5 @@
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import axios from "axios";
import { useQueryFunctionType } from "../../../../types/api";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
@ -10,18 +12,35 @@ export interface ConfigResponse {
health_check_max_retries: number;
}
export const useGetConfigQuery: useQueryFunctionType<
undefined,
ConfigResponse
> = (options) => {
export const useGetConfig: useQueryFunctionType<undefined, undefined> = (
options,
) => {
const setAutoSaving = useFlowsManagerStore((state) => state.setAutoSaving);
const setAutoSavingInterval = useFlowsManagerStore(
(state) => state.setAutoSavingInterval,
);
const setHealthCheckMaxRetries = useFlowsManagerStore(
(state) => state.setHealthCheckMaxRetries,
);
const { query } = UseRequestProcessor();
const getConfigFn = async () => {
const response = await api.get<ConfigResponse>(`${getURL("CONFIG")}`);
return response["data"];
const data = response["data"];
if (data) {
const timeoutInMilliseconds = data.frontend_timeout
? data.frontend_timeout * 1000
: 30000;
axios.defaults.baseURL = "";
axios.defaults.timeout = timeoutInMilliseconds;
setAutoSaving(data.auto_saving);
setAutoSavingInterval(data.auto_saving_interval);
setHealthCheckMaxRetries(data.health_check_max_retries);
}
};
const queryResult = query(["useGetConfigQuery"], getConfigFn, options);
const queryResult = query(["useGetConfig"], getConfigFn, options);
return queryResult;
};

View file

@ -5,7 +5,7 @@ import {
import { useUtilityStore } from "@/stores/utilityStore";
import { createNewError503 } from "@/types/factory/axios-error-503";
import { keepPreviousData } from "@tanstack/react-query";
import { AxiosError, AxiosHeaders } from "axios";
import { AxiosError } from "axios";
import { useQueryFunctionType } from "../../../../types/api";
import { api } from "../../api";
import { UseRequestProcessor } from "../../services/request-processor";
@ -21,7 +21,7 @@ interface getHealthResponse {
export const useGetHealthQuery: useQueryFunctionType<
undefined,
getHealthResponse
> = (_, options) => {
> = (options) => {
const { query } = UseRequestProcessor();
const setHealthCheckTimeout = useUtilityStore(
(state) => state.setHealthCheckTimeout,

View file

@ -13,7 +13,7 @@ type tagsQueryResponse = Array<ITagsDataArray>;
export const useGetTagsQuery: useQueryFunctionType<
undefined,
tagsQueryResponse
> = (_, options) => {
> = (options) => {
const { query } = UseRequestProcessor();
const getTagsFn = async () => {

View file

@ -12,7 +12,7 @@ interface versionQueryResponse {
export const useGetVersionQuery: useQueryFunctionType<
undefined,
versionQueryResponse
> = (_, options) => {
> = (options) => {
const { query } = UseRequestProcessor();
const getVersionFn = async () => {

View file

@ -1,30 +0,0 @@
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import axios from "axios";
import { useEffect } from "react";
import { useGetConfigQuery } from "../controllers/API/queries/config/use-get-config";
function useSaveConfig() {
const { data } = useGetConfigQuery();
const setAutoSaving = useFlowsManagerStore((state) => state.setAutoSaving);
const setAutoSavingInterval = useFlowsManagerStore(
(state) => state.setAutoSavingInterval,
);
const setHealthCheckMaxRetries = useFlowsManagerStore(
(state) => state.setHealthCheckMaxRetries,
);
useEffect(() => {
if (data) {
const timeoutInMilliseconds = data.frontend_timeout
? data.frontend_timeout * 1000
: 30000;
axios.defaults.baseURL = "";
axios.defaults.timeout = timeoutInMilliseconds;
setAutoSaving(data.auto_saving);
setAutoSavingInterval(data.auto_saving_interval);
setHealthCheckMaxRetries(data.health_check_max_retries);
}
}, [data]);
}
export default useSaveConfig;

View file

@ -1,5 +1,4 @@
import ReactDOM from "react-dom/client";
import ContextWrapper from "./contexts";
import reportWebVitals from "./reportWebVitals";
import "./style/classes.css";
@ -16,9 +15,5 @@ const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,
);
root.render(
<ContextWrapper>
<App />
</ContextWrapper>,
);
root.render(<App />);
reportWebVitals();

View file

@ -1,5 +1,4 @@
import { useLoginUser } from "@/controllers/API/queries/auth";
import { useFolderStore } from "@/stores/foldersStore";
import { useContext, useState } from "react";
import { Button } from "../../../components/ui/button";
import { Input } from "../../../components/ui/input";
@ -17,8 +16,6 @@ export default function LoginAdminPage() {
const [inputState, setInputState] =
useState<loginInputStateType>(CONTROL_LOGIN_STATE);
const { login } = useContext(AuthContext);
const setLoading = useAlertStore((state) => state.setLoading);
const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder);
const { password, username } = inputState;
const setErrorData = useAlertStore((state) => state.setErrorData);
@ -38,9 +35,6 @@ export default function LoginAdminPage() {
mutate(user, {
onSuccess: (res) => {
setSelectedFolder(null);
setLoading(true);
login(res.access_token, "login", res.refresh_token);
},
onError: (error) => {

View file

@ -0,0 +1,40 @@
import { useGetAutoLogin } from "@/controllers/API/queries/auth";
import { useGetConfig } from "@/controllers/API/queries/config/use-get-config";
import { useGetVersionQuery } from "@/controllers/API/queries/version";
import { useDarkStore } from "@/stores/darkStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { useEffect } from "react";
import { Outlet } from "react-router-dom";
import { LoadingPage } from "../LoadingPage";
export function AppInitPage() {
const dark = useDarkStore((state) => state.dark);
const refreshStars = useDarkStore((state) => state.refreshStars);
const isLoading = useFlowsManagerStore((state) => state.isLoading);
const { isFetched } = useGetAutoLogin();
useGetVersionQuery({ enabled: isFetched });
useGetConfig({ enabled: isFetched });
useEffect(() => {
if (isFetched) {
refreshStars();
}
}, [isFetched]);
useEffect(() => {
if (!dark) {
document.getElementById("body")!.classList.remove("dark");
} else {
document.getElementById("body")!.classList.add("dark");
}
}, [dark]);
return (
//need parent component with width and height
<>
{(isLoading || !isFetched) && <LoadingPage overlay />}
{isFetched && <Outlet />}
</>
);
}

View file

@ -1,7 +1,6 @@
import AlertDisplayArea from "@/alerts/displayArea";
import CrashErrorComponent from "@/components/crashErrorComponent";
import FetchErrorComponent from "@/components/fetchErrorComponent";
import LoadingComponent from "@/components/loadingComponent";
import TimeoutErrorComponent from "@/components/timeoutErrorComponent";
import {
FETCH_ERROR_DESCRIPION,
@ -12,15 +11,12 @@ import {
import { useGetHealthQuery } from "@/controllers/API/queries/health";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { useUtilityStore } from "@/stores/utilityStore";
import { cn } from "@/utils/utils";
import { AxiosError } from "axios";
import { useEffect, useMemo, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { Outlet } from "react-router-dom";
export function AppWrapperPage() {
const isLoading = useFlowsManagerStore((state) => state.isLoading);
const healthCheckMaxRetries = useFlowsManagerStore(
(state) => state.healthCheckMaxRetries,
);
@ -108,15 +104,6 @@ export function AppWrapperPage() {
>
<>
{modalErrorComponent}
<div
className={cn(
"loading-page-panel absolute left-0 top-0 z-[999]",
isLoading ? "" : "hidden",
)}
>
<LoadingComponent remSize={50} />
</div>
<Outlet />
</>
</ErrorBoundary>

View file

@ -0,0 +1,15 @@
import LoadingComponent from "@/components/loadingComponent";
import { cn } from "@/utils/utils";
export function LoadingPage({ overlay = false }: { overlay?: boolean }) {
return (
<div
className={cn(
"flex h-screen w-screen items-center justify-center bg-background",
overlay && "fixed left-0 top-0 z-[999]",
)}
>
<LoadingComponent remSize={50} />
</div>
);
}

View file

@ -1,5 +1,4 @@
import { useLoginUser } from "@/controllers/API/queries/auth";
import { useFolderStore } from "@/stores/foldersStore";
import * as Form from "@radix-ui/react-form";
import { useContext, useState } from "react";
import { Link } from "react-router-dom";
@ -23,7 +22,6 @@ export default function LoginPage(): JSX.Element {
const { password, username } = inputState;
const { login } = useContext(AuthContext);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder);
function handleInput({
target: { name, value },
@ -41,8 +39,6 @@ export default function LoginPage(): JSX.Element {
mutate(user, {
onSuccess: (data) => {
setSelectedFolder(null);
login(data.access_token, "login", data.refresh_token);
},
onError: (error) => {

View file

@ -1,6 +1,4 @@
import { usePostDownloadMultipleFlows } from "@/controllers/API/queries/flows";
import { useGetFolderQuery } from "@/controllers/API/queries/folders/use-get-folder";
import { useGetFoldersQuery } from "@/controllers/API/queries/folders/use-get-folders";
import useDeleteFlow from "@/hooks/flows/use-delete-flow";
import { useEffect, useMemo, useState } from "react";
import { FormProvider, useForm, useWatch } from "react-hook-form";
@ -13,6 +11,7 @@ import useAlertStore from "../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
import { FlowType } from "../../../../types/flow";
import { FolderType } from "../../entities";
import useFileDrop from "../../hooks/use-on-file-drop";
import { getNameByType } from "../../utils/get-name-by-type";
import { sortFlows } from "../../utils/sort-flows";
@ -28,11 +27,13 @@ import useSelectedFlows from "./hooks/use-selected-flows";
export default function ComponentsComponent({
type = "all",
currentFolder,
isLoading,
}: {
type?: string;
currentFolder?: FolderType;
isLoading: boolean;
}) {
const isLoading = useFlowsManagerStore((state) => state.isLoading);
const { folderId } = useParams();
const setSuccessData = useAlertStore((state) => state.setSuccessData);
@ -51,10 +52,6 @@ export default function ComponentsComponent({
);
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const { data: currentFolder, isLoading: isLoadingCurrentFolder } =
useGetFolderQuery({
id: folderId ?? myCollectionId ?? "",
});
const flowsFromFolder = currentFolder?.flows ?? [];
const [filteredFlows, setFilteredFlows] =
@ -71,10 +68,6 @@ export default function ComponentsComponent({
const name = getNameByType(type);
const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder);
const { isLoading: isLoadingFolders } = useGetFoldersQuery();
const [shouldSelectAll, setShouldSelectAll] = useState(true);
const cardTypes = useMemo(() => {
@ -188,7 +181,6 @@ export default function ComponentsComponent({
const handleDeleteMultiple = () => {
deleteFlow({ id: selectedFlowsComponentsCards })
.then(() => {
setSelectedFolder(null);
resetFilter();
setSelectedFlowsComponentsCards([]);
handleSelectAll(false);
@ -218,12 +210,7 @@ export default function ComponentsComponent({
<>
<div className="flex w-full gap-4 pb-5">
<HeaderComponent
disabled={
isLoading ||
isLoadingFolders ||
isLoadingCurrentFolder ||
data?.length === 0
}
disabled={isLoading || data?.length === 0}
shouldSelectAll={shouldSelectAll}
setShouldSelectAll={setShouldSelectAll}
handleDelete={() => handleSelectOptionsChange("delete")}
@ -243,16 +230,11 @@ export default function ComponentsComponent({
data-testid="cards-wrapper"
>
<div className="flex w-full flex-col gap-4">
{!isLoading &&
!isLoadingFolders &&
!isLoadingCurrentFolder &&
data?.length === 0 ? (
{!isLoading && data?.length === 0 ? (
<EmptyComponent />
) : (
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-2">
{data?.length > 0 &&
isLoadingFolders === false &&
isLoadingCurrentFolder === false ? (
{data?.length > 0 && isLoading === false ? (
<>
{data?.map((item) => (
<FormProvider {...methods} key={item.id}>

View file

@ -3,10 +3,13 @@ import useFlowsManagerStore from "../../../../../../stores/flowsManagerStore";
import InputSearchComponent from "../inputSearchComponent";
import TabsSearchComponent from "../tabsComponent";
type HeaderTabsSearchComponentProps = {};
type HeaderTabsSearchComponentProps = {
loading: boolean;
};
const HeaderTabsSearchComponent = ({}: HeaderTabsSearchComponentProps) => {
const isLoading = useFlowsManagerStore((state) => state.isLoading);
const HeaderTabsSearchComponent = ({
loading,
}: HeaderTabsSearchComponentProps) => {
const [tabActive, setTabActive] = useState("Flows");
const [inputValue, setInputValue] = useState("");
@ -18,7 +21,7 @@ const HeaderTabsSearchComponent = ({}: HeaderTabsSearchComponentProps) => {
<>
<div className="relative flex items-end gap-4">
<InputSearchComponent
loading={isLoading}
loading={loading}
value={inputValue}
onChange={(e) => {
setSearchFlowsComponents(e.target.value);
@ -33,7 +36,7 @@ const HeaderTabsSearchComponent = ({}: HeaderTabsSearchComponentProps) => {
<TabsSearchComponent
tabsOptions={["All", "Flows", "Components"]}
setActiveTab={setTabActive}
loading={isLoading}
loading={loading}
tabActive={tabActive}
/>
</div>

View file

@ -1,3 +1,7 @@
import { useGetFolderQuery } from "@/controllers/API/queries/folders/use-get-folder";
import { useGetFoldersQuery } from "@/controllers/API/queries/folders/use-get-folders";
import { useFolderStore } from "@/stores/foldersStore";
import { useParams } from "react-router-dom";
import ComponentsComponent from "../componentsComponent";
import HeaderTabsSearchComponent from "./components/headerTabsSearchComponent";
@ -6,11 +10,24 @@ type MyCollectionComponentProps = {
};
const MyCollectionComponent = ({ type }: MyCollectionComponentProps) => {
const { folderId } = useParams();
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const { data, isLoading } = useGetFolderQuery({
id: folderId ?? myCollectionId ?? "",
});
const { isLoading: isLoadingFolders } = useGetFoldersQuery();
return (
<>
<HeaderTabsSearchComponent />
<HeaderTabsSearchComponent loading={isLoading || isLoadingFolders} />
<div className="mt-5 flex h-full flex-col">
<ComponentsComponent key={type} type={type} />
<ComponentsComponent
key={type}
type={type}
currentFolder={data}
isLoading={isLoading || isLoadingFolders}
/>
</div>
</>
);

View file

@ -4,7 +4,6 @@ import { useStoreStore } from "@/stores/storeStore";
import { useTypesStore } from "@/stores/typesStore";
import { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import LoadingComponent from "../../components/loadingComponent";
import { getComponent } from "../../controllers/API";
import IOModal from "../../modals/IOModal";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
@ -59,11 +58,7 @@ export default function PlaygroundPage() {
return (
<div className="flex h-full w-full flex-col items-center justify-center align-middle">
{!currentSavedFlow ? (
<div>
<LoadingComponent remSize={24}></LoadingComponent>
</div>
) : (
{currentSavedFlow && (
<IOModal open={true} setOpen={() => {}} isPlayground>
<></>
</IOModal>

View file

@ -11,6 +11,8 @@ import { ProtectedRoute } from "./components/authGuard";
import { ProtectedLoginRoute } from "./components/authLoginGuard";
import { AuthSettingsGuard } from "./components/authSettingsGuard";
import { StoreGuard } from "./components/storeGuard";
import ContextWrapper from "./contexts";
import { AppInitPage } from "./pages/AppInitPage";
import { AppWrapperPage } from "./pages/AppWrapperPage";
import { DashboardWrapperPage } from "./pages/DashboardWrapperPage";
import FlowPage from "./pages/FlowPage";
@ -35,129 +37,146 @@ const PlaygroundPage = lazy(() => import("./pages/Playground"));
const SignUp = lazy(() => import("./pages/SignUpPage"));
const router = createBrowserRouter(
createRoutesFromElements([
<Route path="/" element={<AppWrapperPage />}>
<Route
path=""
element={
<ProtectedRoute>
<Outlet />
</ProtectedRoute>
}
>
<Route path="" element={<DashboardWrapperPage />}>
<Route path="" element={<HomePage />}>
<Route index element={<Navigate replace to={"all"} />} />
<Route
path="flows/"
element={<MyCollectionComponent key="flows" type="flow" />}
>
<Route
path="/"
element={
<ContextWrapper>
<Outlet />
</ContextWrapper>
}
>
<Route path="" element={<AppInitPage />}>
<Route path="" element={<AppWrapperPage />}>
<Route
path=""
element={
<ProtectedRoute>
<Outlet />
</ProtectedRoute>
}
>
<Route path="" element={<DashboardWrapperPage />}>
<Route path="" element={<HomePage />}>
<Route index element={<Navigate replace to={"all"} />} />
<Route
path="flows/"
element={<MyCollectionComponent key="flows" type="flow" />}
>
<Route
path="folder/:folderId"
element={<MyCollectionComponent key="flows" type="flow" />}
/>
</Route>
<Route
path="components/"
element={
<MyCollectionComponent key="components" type="component" />
}
>
<Route
path="folder/:folderId"
element={
<MyCollectionComponent
key="components"
type="component"
/>
}
/>
</Route>
<Route
path="all/"
element={<MyCollectionComponent key="all" type="all" />}
>
<Route
path="folder/:folderId"
element={<MyCollectionComponent key="all" type="all" />}
/>
</Route>
</Route>
<Route path="/settings" element={<SettingsPage />}>
<Route index element={<Navigate replace to={"general"} />} />
<Route
path="global-variables"
element={<GlobalVariablesPage />}
/>
<Route path="api-keys" element={<ApiKeysPage />} />
<Route
path="general/:scrollId?"
element={
<AuthSettingsGuard>
<GeneralPage />
</AuthSettingsGuard>
}
/>
<Route path="shortcuts" element={<ShortcutsPage />} />
<Route path="messages" element={<MessagesPage />} />
</Route>
<Route
path="folder/:folderId"
element={<MyCollectionComponent key="flows" type="flow" />}
/>
</Route>
<Route
path="components/"
element={
<MyCollectionComponent key="components" type="component" />
}
>
<Route
path="folder/:folderId"
path="/store"
element={
<MyCollectionComponent key="components" type="component" />
<StoreGuard>
<StorePage />
</StoreGuard>
}
/>
<Route
path="/store/:id/"
element={
<StoreGuard>
<StorePage />
</StoreGuard>
}
/>
<Route path="/account">
<Route path="delete" element={<DeleteAccountPage />}></Route>
</Route>
<Route
path="/admin"
element={
<ProtectedAdminRoute>
<AdminPage />
</ProtectedAdminRoute>
}
/>
</Route>
<Route
path="all/"
element={<MyCollectionComponent key="all" type="all" />}
>
<Route
path="folder/:folderId"
element={<MyCollectionComponent key="all" type="all" />}
/>
<Route path="/flow/:id/">
<Route path="" element={<DashboardWrapperPage />}>
<Route path="folder/:folderId/" element={<FlowPage />} />
<Route path="" element={<FlowPage />} />
</Route>
<Route path="view" element={<ViewPage />} />
</Route>
<Route path="/playground/:id/">
<Route path="" element={<PlaygroundPage />} />
</Route>
</Route>
<Route path="/settings" element={<SettingsPage />}>
<Route index element={<Navigate replace to={"general"} />} />
<Route path="global-variables" element={<GlobalVariablesPage />} />
<Route path="api-keys" element={<ApiKeysPage />} />
<Route
path="general/:scrollId?"
element={
<AuthSettingsGuard>
<GeneralPage />
</AuthSettingsGuard>
}
/>
<Route path="shortcuts" element={<ShortcutsPage />} />
<Route path="messages" element={<MessagesPage />} />
</Route>
<Route
path="/store"
path="/login"
element={
<StoreGuard>
<StorePage />
</StoreGuard>
<ProtectedLoginRoute>
<LoginPage />
</ProtectedLoginRoute>
}
/>
<Route
path="/store/:id/"
path="/signup"
element={
<StoreGuard>
<StorePage />
</StoreGuard>
<ProtectedLoginRoute>
<SignUp />
</ProtectedLoginRoute>
}
/>
<Route path="/account">
<Route path="delete" element={<DeleteAccountPage />}></Route>
</Route>
<Route
path="/admin"
path="/login/admin"
element={
<ProtectedAdminRoute>
<AdminPage />
</ProtectedAdminRoute>
<ProtectedLoginRoute>
<LoginAdminPage />
</ProtectedLoginRoute>
}
/>
</Route>
<Route path="/flow/:id/">
<Route path="" element={<DashboardWrapperPage />}>
<Route path="folder/:folderId/" element={<FlowPage />} />
<Route path="" element={<FlowPage />} />
</Route>
<Route path="view" element={<ViewPage />} />
</Route>
<Route path="/playground/:id/">
<Route path="" element={<PlaygroundPage />} />
<Route path="*" element={<Navigate replace to="/" />} />
</Route>
</Route>
<Route
path="/login"
element={
<ProtectedLoginRoute>
<LoginPage />
</ProtectedLoginRoute>
}
/>
<Route
path="/signup"
element={
<ProtectedLoginRoute>
<SignUp />
</ProtectedLoginRoute>
}
/>
<Route
path="/login/admin"
element={
<ProtectedLoginRoute>
<LoginAdminPage />
</ProtectedLoginRoute>
}
/>
<Route path="*" element={<Navigate replace to="/" />} />
</Route>,
]),
);

View file

@ -19,7 +19,6 @@ const useAlertStore = create<AlertStoreType>((set, get) => ({
notificationCenter: false,
notificationList: [],
tempNotificationList: [],
loading: true,
setErrorData: (newState: { title: string; list?: Array<string> }) => {
if (newState.title && newState.title !== "") {
set({
@ -151,9 +150,6 @@ const useAlertStore = create<AlertStoreType>((set, get) => ({
),
});
},
setLoading: (newState: boolean) => {
set({ loading: newState });
},
clearTempNotificationList: () => {
set({ tempNotificationList: [] });
},

View file

@ -10,7 +10,7 @@ const useAuthStore = create<AuthStoreType>((set, get) => ({
isAuthenticated: !!cookies.get(LANGFLOW_ACCESS_TOKEN),
accessToken: cookies.get(LANGFLOW_ACCESS_TOKEN) ?? null,
userData: null,
autoLogin: false,
autoLogin: null,
apiKey: cookies.get("apikey_tkn_lflw"),
authenticationErrorCount: 0,

View file

@ -2,8 +2,6 @@ import { create } from "zustand";
import { FoldersStoreType } from "../types/zustand/folders";
export const useFolderStore = create<FoldersStoreType>((set, get) => ({
selectedFolder: null,
setSelectedFolder: (folder) => set(() => ({ selectedFolder: folder })),
loadingById: false,
setMyCollectionId: (myCollectionId) => {
set({ myCollectionId });

View file

@ -248,7 +248,6 @@ export type ResponseErrorDetailAPI = {
};
export type useQueryFunctionType<T = undefined, R = any> = T extends undefined
? (
params?: T,
options?: Omit<UseQueryOptions, "queryFn" | "queryKey">,
) => UseQueryResult<R>
: (

View file

@ -15,6 +15,4 @@ export type AlertStoreType = {
removeFromTempNotificationList: (index: string) => void;
clearNotificationList: () => void;
removeFromNotificationList: (index: string) => void;
loading: boolean;
setLoading: (newState: boolean) => void;
};

View file

@ -5,7 +5,7 @@ export interface AuthStoreType {
isAuthenticated: boolean;
accessToken: string | null;
userData: Users | null;
autoLogin: boolean;
autoLogin: boolean | null;
apiKey: string | null;
authenticationErrorCount: number;

View file

@ -1,8 +1,6 @@
import { FolderType } from "../../../pages/MainPage/entities";
export type FoldersStoreType = {
selectedFolder: FolderType | null;
setSelectedFolder: (folder: FolderType | null) => void;
myCollectionId: string | null;
setMyCollectionId: (value: string) => void;
folderToEdit: FolderType | null;

View file

@ -1,4 +1,4 @@
import { test } from "@playwright/test";
import { expect, test } from "@playwright/test";
import { readFileSync } from "fs";
test("CRUD folders", async ({ page }) => {
@ -71,7 +71,7 @@ test("CRUD folders", async ({ page }) => {
await page.getByText("Folder deleted successfully").isVisible();
});
test("add folder by drag and drop", async ({ page }) => {
test("add a flow into a folder by drag and drop", async ({ page }) => {
await page.goto("/");
await page.waitForSelector("text=my collection", {
@ -84,10 +84,10 @@ test("add folder by drag and drop", async ({ page }) => {
);
// Wait for the target element to be available before evaluation
await page.waitForSelector(
'//*[@id="root"]/div/div[2]/div[2]/div[3]/aside/nav/div/div[2]',
);
await page.waitForSelector('[data-testid="sidebar-nav-My Projects"]', {
timeout: 100000,
});
// Create the DataTransfer and File
const dataTransfer = await page.evaluateHandle((data) => {
const dt = new DataTransfer();
@ -100,15 +100,34 @@ test("add folder by drag and drop", async ({ page }) => {
}, jsonContent);
// Now dispatch
await page.dispatchEvent(
'//*[@id="root"]/div/div[2]/div[2]/div[3]/aside/nav/div/div[2]',
"drop",
{
dataTransfer,
},
);
await page.getByTestId("sidebar-nav-My Projects").dispatchEvent("drop", {
dataTransfer,
});
await page.getByText("Getting Started").first().isVisible();
await page.waitForTimeout(3000);
const genericNode = page.getByTestId("div-generic-node");
const elementCount = await genericNode?.count();
if (elementCount > 0) {
expect(true).toBeTruthy();
}
await page.getByTestId("sidebar-nav-My Projects").click();
await page.waitForTimeout(1000);
expect(
await page.locator("text=Getting Started:").last().isVisible(),
).toBeTruthy();
expect(
await page.locator("text=Inquisitive Pike").last().isVisible(),
).toBeTruthy();
expect(
await page.locator("text=Dreamy Bassi").last().isVisible(),
).toBeTruthy();
expect(
await page.locator("text=Furious Faraday").last().isVisible(),
).toBeTruthy();
});
test("change flow folder", async ({ page }) => {

View file

@ -162,6 +162,12 @@ test("user should be able to duplicate a flow or a component", async ({
await page.getByText("Exit", { exact: true }).click();
}
const replaceButton = await page.getByTestId("replace-button").isVisible();
if (replaceButton) {
await page.getByTestId("replace-button").click();
}
await page.getByTestId("icon-ChevronLeft").last().click();
await page.getByRole("checkbox").nth(1).click();

View file

@ -22,11 +22,11 @@ test("user should be able to manually save a flow when the auto_save is off", as
await page.locator("span").filter({ hasText: "My Collection" }).isVisible();
await page.waitForSelector('[data-testid="mainpage_title"]', {
timeout: 30000,
timeout: 5000,
});
await page.waitForSelector('[id="new-project-btn"]', {
timeout: 30000,
timeout: 5000,
});
let modalCount = 0;
@ -46,12 +46,12 @@ test("user should be able to manually save a flow when the auto_save is off", as
}
await page.waitForSelector('[data-testid="blank-flow"]', {
timeout: 30000,
timeout: 5000,
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
timeout: 5000,
});
await page.getByPlaceholder("Search").click();
@ -66,7 +66,7 @@ test("user should be able to manually save a flow when the auto_save is off", as
await page.mouse.down();
await page.waitForSelector('[title="fit view"]', {
timeout: 100000,
timeout: 5000,
});
await page.getByTitle("fit view").click();
@ -77,7 +77,7 @@ test("user should be able to manually save a flow when the auto_save is off", as
await page.waitForSelector("text=loading", {
state: "hidden",
timeout: 100000,
timeout: 5000,
});
await page.getByTestId("icon-ChevronLeft").last().click();
@ -93,7 +93,7 @@ test("user should be able to manually save a flow when the auto_save is off", as
await page.getByText("Untitled document").first().click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 100000,
timeout: 5000,
});
expect(await page.getByText("NVIDIA").isVisible()).toBeFalsy();
@ -110,7 +110,7 @@ test("user should be able to manually save a flow when the auto_save is off", as
await page.mouse.down();
await page.waitForSelector('[title="fit view"]', {
timeout: 100000,
timeout: 5000,
});
await page.getByTitle("fit view").click();
@ -123,7 +123,7 @@ test("user should be able to manually save a flow when the auto_save is off", as
await page.waitForSelector("text=loading", {
state: "hidden",
timeout: 100000,
timeout: 5000,
});
await page.waitForTimeout(5000);
@ -142,7 +142,7 @@ test("user should be able to manually save a flow when the auto_save is off", as
await page.mouse.down();
await page.waitForSelector('[title="fit view"]', {
timeout: 100000,
timeout: 5000,
});
await page.getByTitle("fit view").click();
@ -150,10 +150,25 @@ test("user should be able to manually save a flow when the auto_save is off", as
await page.getByTestId("save-flow-button").click();
await page.getByTestId("icon-ChevronLeft").last().click();
const replaceButton = await page.getByTestId("replace-button").isVisible();
if (replaceButton) {
await page.getByTestId("replace-button").click();
}
const saveExitButton = await page
.getByText("Save And Exit", { exact: true })
.last()
.isVisible();
if (saveExitButton) {
await page.getByText("Save And Exit", { exact: true }).last().click();
}
await page.getByText("Untitled document").first().click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 100000,
timeout: 5000,
});
await page.waitForTimeout(5000);

View file

@ -42,18 +42,29 @@ test.describe("drag and drop test", () => {
}, jsonContent);
// Now dispatch
await page.dispatchEvent(
'//*[@id="root"]/div/div[2]/div[2]/div[3]/div',
"drop",
{
dataTransfer,
},
);
await page.getByTestId("cards-wrapper").dispatchEvent("drop", {
dataTransfer,
});
await page.waitForTimeout(3000);
const genericNode = page.getByTestId("div-generic-node");
const elementCount = await genericNode?.count();
if (elementCount > 0) {
expect(true).toBeTruthy();
}
expect(
await page.locator("text=Getting Started:").last().isVisible(),
).toBeTruthy();
expect(
await page.locator("text=Inquisitive Pike").last().isVisible(),
).toBeTruthy();
expect(
await page.locator("text=Dreamy Bassi").last().isVisible(),
).toBeTruthy();
expect(
await page.locator("text=Furious Faraday").last().isVisible(),
).toBeTruthy();
});
});

View file

@ -96,12 +96,12 @@ test("should filter by type", async ({ page }) => {
expect(toyBrick).not.toBe(0);
await page.getByTestId("all-button-store").click();
await page.waitForTimeout(8000);
await page.waitForTimeout(10000);
let iconGroupAllCount = await page.getByTestId("icon-Group")?.count();
await page.waitForTimeout(1000);
await page.waitForTimeout(5000);
let toyBrickAllCount = await page.getByTestId("icon-ToyBrick")?.count();
await page.waitForTimeout(1000);
await page.waitForTimeout(5000);
if (iconGroupAllCount === 0 || toyBrickAllCount === 0) {
expect(false).toBe(true);