From d1efd7f199568a94b8205c49e26c75bebfb6d25d Mon Sep 17 00:00:00 2001 From: akash-codemonk <67054171+akash-codemonk@users.noreply.github.com> Date: Mon, 23 Jan 2023 15:28:45 +0530 Subject: [PATCH] fix: viewing forkable apps routing to login for non signed in users (#19966) --- app/client/src/pages/AppViewer/PageMenu.tsx | 4 +- .../src/pages/AppViewer/PrimaryCTA.test.tsx | 166 ++++++++++++++++++ app/client/src/pages/AppViewer/PrimaryCTA.tsx | 17 +- .../Applications/ForkApplicationModal.tsx | 45 +++-- 4 files changed, 211 insertions(+), 21 deletions(-) create mode 100644 app/client/src/pages/AppViewer/PrimaryCTA.test.tsx diff --git a/app/client/src/pages/AppViewer/PageMenu.tsx b/app/client/src/pages/AppViewer/PageMenu.tsx index 5365e9f58e..a51812ec32 100644 --- a/app/client/src/pages/AppViewer/PageMenu.tsx +++ b/app/client/src/pages/AppViewer/PageMenu.tsx @@ -140,7 +140,9 @@ export function PageMenu(props: AppViewerHeaderProps) { workspaceId={workspaceID} /> )} - + {isOpen && ( + + )} {!hideWatermark && ( ({ + ...jest.requireActual("react-router"), + useLocation: () => ({ + pathname: "/app/test-3/page1-63cccd44463c535b9fbc297c", + search: "?fork=true", + }), +})); + +const mockDispatch = jest.fn(); +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useDispatch: () => mockDispatch, +})); + +export const initialState: any = { + entities: { + pageList: { + applicationId: 1, + currentPageId: 1, + pages: [ + { + pageId: 1, + slug: "pageSlug", + }, + ], + }, + datasources: { + list: [], + }, + actions: [], + canvasWidgets: { + main_component: {}, + }, + }, + ui: { + appTheming: { + selectedTheme: { + properties: { + colors: { + primaryColor: "", + }, + borderRadius: { + appBorderRadius: "", + }, + }, + }, + }, + applications: { + currentApplication: { + id: "605c435a91dea93f0eaf91b8", + name: "My Application", + slug: "my-application", + workspaceId: "", + evaluationVersion: 1, + appIsExample: false, + gitApplicationMetadata: undefined, + applicationVersion: 2, + forkingEnabled: true, + isPublic: true, + }, + userWorkspaces: [], + }, + theme: { + theme: { + colors: { + applications: { + iconColor: "#f2f2f2", + }, + success: { + main: "#e2e2e2", + }, + }, + }, + }, + users: { + currentUser: undefined, + }, + }, +}; +const state = JSON.parse(JSON.stringify(initialState)); +const mockStore = configureStore(); + +export function getStore(action?: string) { + switch (action) { + case "SET_CURRENT_USER_DETAILS": + state.ui.users.currentUser = { + username: "test", + }; + break; + } + return mockStore(state); +} + +export const fetchApplicationMockResponse = { + responseMeta: { + status: 200, + success: true, + }, + data: { + application: { + id: "605c435a91dea93f0eaf91b8", + name: "My Application", + slug: "my-application", + workspaceId: "", + evaluationVersion: 1, + appIsExample: false, + gitApplicationMetadata: undefined, + applicationVersion: 2, + forkingEnabled: true, + isPublic: true, + }, + pages: [ + { + id: "605c435a91dea93f0eaf91ba", + name: "Page1", + isDefault: true, + slug: "page-1", + }, + { + id: "605c435a91dea93f0eaf91bc", + name: "Page2", + isDefault: false, + slug: "page-2", + }, + ], + workspaceId: "", + }, +}; + +describe("App viewer fork button", () => { + afterEach(cleanup); + + it("Fork modal trigger should not be displayed until user details are fetched", () => { + render( + + + + + , + ); + expect(screen.queryAllByTestId("fork-modal-trigger").length).toEqual(0); + expect(mockDispatch).toHaveBeenCalledTimes(0); + mockDispatch.mockClear(); + }); + it("Fork modal trigger should be displayed when user details are defined and user is not anonymous", () => { + render( + + + + + , + ); + expect(screen.queryAllByTestId("fork-modal-trigger").length).toEqual(1); + expect(mockDispatch).toHaveBeenCalledTimes(1); + mockDispatch.mockClear(); + }); +}); diff --git a/app/client/src/pages/AppViewer/PrimaryCTA.tsx b/app/client/src/pages/AppViewer/PrimaryCTA.tsx index 996e3019a0..cc43603b2b 100644 --- a/app/client/src/pages/AppViewer/PrimaryCTA.tsx +++ b/app/client/src/pages/AppViewer/PrimaryCTA.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import Button from "./AppViewerButton"; import { AUTH_LOGIN_URL } from "constants/routes"; @@ -21,7 +21,6 @@ import { import { getCurrentUser } from "selectors/usersSelectors"; import { ANONYMOUS_USERNAME } from "constants/userConstants"; import ForkApplicationModal from "pages/Applications/ForkApplicationModal"; -import { getAllApplications } from "actions/applicationActions"; import { viewerURL } from "RouteBuilder"; import { useHistory } from "react-router"; import { useHref } from "pages/Editor/utils"; @@ -44,7 +43,6 @@ type Props = { function PrimaryCTA(props: Props) { const { className, url } = props; - const dispatch = useDispatch(); const currentUser = useSelector(getCurrentUser); const currentPageID = useSelector(getCurrentPageId); const selectedTheme = useSelector(getSelectedAppTheme); @@ -56,7 +54,9 @@ function PrimaryCTA(props: Props) { const appViewerURL = useHref(viewerURL, { pageId: currentPageID, - suffix: "fork", + params: { + fork: "true", + }, }); // get the fork url @@ -94,6 +94,7 @@ function PrimaryCTA(props: Props) { ); } + if (!currentUser) return; if ( currentApplication?.forkingEnabled && currentApplication?.isPublic && @@ -104,7 +105,7 @@ function PrimaryCTA(props: Props) { borderRadius={selectedTheme.properties.borderRadius.appBorderRadius} buttonColor={selectedTheme.properties.colors.primaryColor} buttonVariant="PRIMARY" - className="t--fork-app" + className={`t--fork-app w-full md:w-auto ${className}`} icon="fork" onClick={() => { history.push(forkURL); @@ -126,9 +127,9 @@ function PrimaryCTA(props: Props) { } buttonColor={selectedTheme.properties.colors.primaryColor} buttonVariant="PRIMARY" - className="t--fork-app" + className={`t--fork-app w-full md:w-auto ${className}`} + data-testid="fork-modal-trigger" icon="fork" - onClick={() => dispatch(getAllApplications())} text={createMessage(FORK_APP)} /> } @@ -157,6 +158,8 @@ function PrimaryCTA(props: Props) { }, [ url, canEdit, + forkURL, + currentUser?.username, selectedTheme.properties.colors.primaryColor, selectedTheme.properties.borderRadius.appBorderRadius, ]); diff --git a/app/client/src/pages/Applications/ForkApplicationModal.tsx b/app/client/src/pages/Applications/ForkApplicationModal.tsx index b20647ca98..698312907c 100644 --- a/app/client/src/pages/Applications/ForkApplicationModal.tsx +++ b/app/client/src/pages/Applications/ForkApplicationModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useEffect } from "react"; +import React, { useState, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { getUserApplicationsWorkspaces } from "selectors/applicationSelectors"; import { hasCreateNewAppPermission } from "@appsmith/utils/permissionHelpers"; @@ -15,7 +15,6 @@ import { import { StyledDialog, ButtonWrapper, SpinnerWrapper } from "./ForkModalStyles"; import { getIsFetchingApplications } from "selectors/applicationSelectors"; import { useLocation } from "react-router"; -import { matchViewerForkPath } from "constants/routes"; import { Colors } from "constants/Colors"; import { CANCEL, @@ -26,6 +25,7 @@ import { FORK_APP_MODAL_SUCCESS_TITLE, } from "@appsmith/constants/messages"; import { getAllApplications } from "actions/applicationActions"; +import history from "utils/history"; type ForkApplicationModalProps = { applicationId: string; @@ -48,16 +48,9 @@ function ForkApplicationModal(props: ForkApplicationModalProps) { (state: AppState) => state.ui.applications.forkingApplication, ); - useEffect(() => { - if (!userWorkspaces.length) { - dispatch(getAllApplications()); - } - }, [userWorkspaces.length]); - const isFetchingApplications = useSelector(getIsFetchingApplications); - const { pathname } = useLocation(); - - const showBasedOnURL = matchViewerForkPath(pathname); + const location = useLocation(); + const queryParams = new URLSearchParams(location.search); const forkApplication = () => { dispatch({ @@ -98,12 +91,35 @@ function ForkApplicationModal(props: ForkApplicationModalProps) { ? createMessage(FORK_APP_MODAL_EMPTY_TITLE) : createMessage(FORK_APP_MODAL_SUCCESS_TITLE); + const handleClose = () => { + if (!props.setModalClose) { + const url = new URL(window.location.href); + if (url.searchParams.has("fork")) { + url.searchParams.delete("fork"); + history.push(url.toString().slice(url.origin.length)); + } + } + }; + + const handleOpen = () => { + if (!props.setModalClose) { + const url = new URL(window.location.href); + if (!url.searchParams.has("fork")) { + url.searchParams.append("fork", "true"); + history.push(url.toString().slice(url.origin.length)); + } + dispatch(getAllApplications()); + } + }; + return ( setModalClose && setModalClose(false)} + onClick={() => { + setModalClose && setModalClose(false); + handleClose(); + }} size={Size.large} tag="button" text={createMessage(CANCEL)}