Rebuild App Settings to be nested inside app screen (#3676)

* Rebuild App Settings to be nested inside app screen

* Remove memoization

* Update src/apps/components/AppPage/AppPageNav.tsx

Co-authored-by: Michał Droń <droniu@droniu.dev>

---------

Co-authored-by: Michał Droń <droniu@droniu.dev>
This commit is contained in:
Lukasz Ostrowski 2023-05-22 08:35:35 +02:00 committed by GitHub
parent ea5b6a80a5
commit 8f58efcd50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 60 additions and 135 deletions

View file

@ -35,10 +35,6 @@
"context": "order shipping method name", "context": "order shipping method name",
"string": "Shipping" "string": "Shipping"
}, },
"+FWlRD": {
"context": "button",
"string": "Open app"
},
"+HuipK": { "+HuipK": {
"context": "variant sku", "context": "variant sku",
"string": "SKU {sku}" "string": "SKU {sku}"
@ -1812,10 +1808,6 @@
"context": "header", "context": "header",
"string": "Activity" "string": "Activity"
}, },
"BYGJ/j": {
"context": "button label",
"string": "Settings"
},
"BYTvv/": { "BYTvv/": {
"context": "Dry run events unavailable", "context": "Dry run events unavailable",
"string": "The following events from provided query are currently not available for dry run:" "string": "The following events from provided query are currently not available for dry run:"
@ -3298,6 +3290,10 @@
"context": "button", "context": "button",
"string": "Create order" "string": "Create order"
}, },
"LwX0Ug": {
"context": "Button with Manage app label",
"string": "Manage app"
},
"Lx1ima": { "Lx1ima": {
"context": "button", "context": "button",
"string": "Upload image" "string": "Upload image"

View file

@ -6,7 +6,6 @@ import AppDetailsPage, { AppDetailsPageProps } from "./AppDetailsPage";
const props: AppDetailsPageProps = { const props: AppDetailsPageProps = {
data: appDetails, data: appDetails,
loading: false, loading: false,
navigateToApp: () => undefined,
onAppActivateOpen: () => undefined, onAppActivateOpen: () => undefined,
onAppDeactivateOpen: () => undefined, onAppDeactivateOpen: () => undefined,
onAppDeleteOpen: () => undefined, onAppDeleteOpen: () => undefined,

View file

@ -38,7 +38,6 @@ beforeEach(() => {
describe("Apps AppDetailsPage", () => { describe("Apps AppDetailsPage", () => {
it("displays app details when app data passed", () => { it("displays app details when app data passed", () => {
// Arrange // Arrange
const navigateToApp = jest.fn();
const onAppActivateOpen = jest.fn(); const onAppActivateOpen = jest.fn();
const onAppDeactivateOpen = jest.fn(); const onAppDeactivateOpen = jest.fn();
const onAppDeleteOpen = jest.fn(); const onAppDeleteOpen = jest.fn();
@ -48,7 +47,6 @@ describe("Apps AppDetailsPage", () => {
<AppDetailsPage <AppDetailsPage
data={appDetails} data={appDetails}
loading={false} loading={false}
navigateToApp={navigateToApp}
onAppActivateOpen={onAppActivateOpen} onAppActivateOpen={onAppActivateOpen}
onAppDeactivateOpen={onAppDeactivateOpen} onAppDeactivateOpen={onAppDeactivateOpen}
onAppDeleteOpen={onAppDeleteOpen} onAppDeleteOpen={onAppDeleteOpen}
@ -58,7 +56,6 @@ describe("Apps AppDetailsPage", () => {
// Assert // Assert
expect(mockHeader).toHaveBeenCalledWith({ expect(mockHeader).toHaveBeenCalledWith({
data: appDetails, data: appDetails,
navigateToApp,
onAppActivateOpen, onAppActivateOpen,
onAppDeactivateOpen, onAppDeactivateOpen,
onAppDeleteOpen, onAppDeleteOpen,

View file

@ -10,7 +10,6 @@ import PermissionsCard from "./PermissionsCard";
export interface AppDetailsPageProps { export interface AppDetailsPageProps {
loading: boolean; loading: boolean;
data: AppQuery["app"]; data: AppQuery["app"];
navigateToApp: () => void;
onAppActivateOpen: () => void; onAppActivateOpen: () => void;
onAppDeactivateOpen: () => void; onAppDeactivateOpen: () => void;
onAppDeleteOpen: () => void; onAppDeleteOpen: () => void;
@ -19,7 +18,6 @@ export interface AppDetailsPageProps {
export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({ export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
data, data,
loading, loading,
navigateToApp,
onAppActivateOpen, onAppActivateOpen,
onAppDeactivateOpen, onAppDeactivateOpen,
onAppDeleteOpen, onAppDeleteOpen,
@ -27,7 +25,6 @@ export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
<> <>
<Header <Header
data={data} data={data}
navigateToApp={navigateToApp}
onAppActivateOpen={onAppActivateOpen} onAppActivateOpen={onAppActivateOpen}
onAppDeactivateOpen={onAppDeactivateOpen} onAppDeactivateOpen={onAppDeactivateOpen}
onAppDeleteOpen={onAppDeleteOpen} onAppDeleteOpen={onAppDeleteOpen}

View file

@ -6,7 +6,6 @@ import Header from "./Header";
const mockHeaderOptions = jest.fn(); const mockHeaderOptions = jest.fn();
const mockTopNav = jest.fn(); const mockTopNav = jest.fn();
const mockButton = jest.fn();
jest.mock("@dashboard/components/AppLayout/TopNav", () => ({ jest.mock("@dashboard/components/AppLayout/TopNav", () => ({
TopNav: props => { TopNav: props => {
@ -14,12 +13,6 @@ jest.mock("@dashboard/components/AppLayout/TopNav", () => ({
return <>{props.children}</>; return <>{props.children}</>;
}, },
})); }));
jest.mock("@saleor/macaw-ui/next", () => ({
Button: props => {
mockButton(props);
return <>{props.children}</>;
},
}));
jest.mock("../DeactivatedText", () => () => "deactivated"); jest.mock("../DeactivatedText", () => () => "deactivated");
jest.mock("react-intl", () => ({ jest.mock("react-intl", () => ({
useIntl: jest.fn(() => ({ useIntl: jest.fn(() => ({
@ -36,13 +29,11 @@ jest.mock("./HeaderOptions", () => props => {
beforeEach(() => { beforeEach(() => {
mockHeaderOptions.mockClear(); mockHeaderOptions.mockClear();
mockTopNav.mockClear(); mockTopNav.mockClear();
mockButton.mockClear();
}); });
describe("Apps AppDetailsPage Header", () => { describe("Apps AppDetailsPage Header", () => {
it("displays app details options when active app data passed", () => { it("displays app details options when active app data passed", () => {
// Arrange // Arrange
const navigateToApp = jest.fn();
const onAppActivateOpen = jest.fn(); const onAppActivateOpen = jest.fn();
const onAppDeactivateOpen = jest.fn(); const onAppDeactivateOpen = jest.fn();
const onAppDeleteOpen = jest.fn(); const onAppDeleteOpen = jest.fn();
@ -51,7 +42,6 @@ describe("Apps AppDetailsPage Header", () => {
render( render(
<Header <Header
data={appDetails} data={appDetails}
navigateToApp={navigateToApp}
onAppActivateOpen={onAppActivateOpen} onAppActivateOpen={onAppActivateOpen}
onAppDeactivateOpen={onAppDeactivateOpen} onAppDeactivateOpen={onAppDeactivateOpen}
onAppDeleteOpen={onAppDeleteOpen} onAppDeleteOpen={onAppDeleteOpen}
@ -66,18 +56,12 @@ describe("Apps AppDetailsPage Header", () => {
onAppDeactivateOpen, onAppDeactivateOpen,
onAppDeleteOpen, onAppDeleteOpen,
}); });
expect(mockButton).toHaveBeenCalledWith(
expect.objectContaining({
onClick: navigateToApp,
}),
);
expect(mockTopNav).toHaveBeenCalled(); expect(mockTopNav).toHaveBeenCalled();
expect(title.container).toHaveTextContent(appDetails.name as string); expect(title.container).toHaveTextContent(appDetails.name as string);
}); });
it("displays app details options when inactive app data passed", () => { it("displays app details options when inactive app data passed", () => {
// Arrange // Arrange
const navigateToApp = jest.fn();
const onAppActivateOpen = jest.fn(); const onAppActivateOpen = jest.fn();
const onAppDeactivateOpen = jest.fn(); const onAppDeactivateOpen = jest.fn();
const onAppDeleteOpen = jest.fn(); const onAppDeleteOpen = jest.fn();
@ -86,7 +70,6 @@ describe("Apps AppDetailsPage Header", () => {
render( render(
<Header <Header
data={{ ...appDetails, isActive: false }} data={{ ...appDetails, isActive: false }}
navigateToApp={navigateToApp}
onAppActivateOpen={onAppActivateOpen} onAppActivateOpen={onAppActivateOpen}
onAppDeactivateOpen={onAppDeactivateOpen} onAppDeactivateOpen={onAppDeactivateOpen}
onAppDeleteOpen={onAppDeleteOpen} onAppDeleteOpen={onAppDeleteOpen}
@ -101,11 +84,6 @@ describe("Apps AppDetailsPage Header", () => {
onAppDeactivateOpen, onAppDeactivateOpen,
onAppDeleteOpen, onAppDeleteOpen,
}); });
expect(mockButton).toHaveBeenCalledWith(
expect.objectContaining({
onClick: navigateToApp,
}),
);
expect(mockTopNav).toHaveBeenCalled(); expect(mockTopNav).toHaveBeenCalled();
expect(title.container).toHaveTextContent(`${appDetails.name} deactivated`); expect(title.container).toHaveTextContent(`${appDetails.name} deactivated`);
}); });

View file

@ -1,17 +1,13 @@
import { AppPaths } from "@dashboard/apps/urls"; import { AppUrls } from "@dashboard/apps/urls";
import { TopNav } from "@dashboard/components/AppLayout/TopNav"; import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { AppQuery } from "@dashboard/graphql"; import { AppQuery } from "@dashboard/graphql";
import { Button } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl";
import DeactivatedText from "../DeactivatedText"; import DeactivatedText from "../DeactivatedText";
import HeaderOptions from "./HeaderOptions"; import HeaderOptions from "./HeaderOptions";
import messages from "./messages";
interface HeaderProps { interface HeaderProps {
data: AppQuery["app"]; data: AppQuery["app"];
navigateToApp: () => void;
onAppActivateOpen: () => void; onAppActivateOpen: () => void;
onAppDeactivateOpen: () => void; onAppDeactivateOpen: () => void;
onAppDeleteOpen: () => void; onAppDeleteOpen: () => void;
@ -19,30 +15,32 @@ interface HeaderProps {
const Header: React.FC<HeaderProps> = ({ const Header: React.FC<HeaderProps> = ({
data, data,
navigateToApp,
onAppActivateOpen, onAppActivateOpen,
onAppDeactivateOpen, onAppDeactivateOpen,
onAppDeleteOpen, onAppDeleteOpen,
}) => ( }) => {
<> /**
<TopNav * App is null with first render so fallback with HTML-safe fallback
href={AppPaths.appListPath} */
title={ const backButtonTarget = data?.id ? AppUrls.resolveAppUrl(data.id) : "#";
<>
{data?.name} {!data?.isActive && <DeactivatedText />} return (
</> <>
} <TopNav
> href={backButtonTarget}
<Button onClick={navigateToApp} variant="primary" data-tc="open-app"> title={
<FormattedMessage {...messages.openApp} /> <>
</Button> {data?.name} {!data?.isActive && <DeactivatedText />}
</TopNav> </>
<HeaderOptions }
data={data} />
onAppActivateOpen={onAppActivateOpen} <HeaderOptions
onAppDeactivateOpen={onAppDeactivateOpen} data={data}
onAppDeleteOpen={onAppDeleteOpen} onAppActivateOpen={onAppActivateOpen}
/> onAppDeactivateOpen={onAppDeactivateOpen}
</> onAppDeleteOpen={onAppDeleteOpen}
); />
</>
);
};
export default Header; export default Header;

View file

@ -1,11 +1,6 @@
import { defineMessages } from "react-intl"; import { defineMessages } from "react-intl";
export default defineMessages({ export default defineMessages({
openApp: {
id: "+FWlRD",
defaultMessage: "Open app",
description: "button",
},
supportLink: { supportLink: {
id: "Nsk5WL", id: "Nsk5WL",
defaultMessage: "Get support", defaultMessage: "Get support",

View file

@ -25,6 +25,7 @@ export const AppPage: React.FC<AppPageProps> = ({
}) => ( }) => (
<DetailPageLayout gridTemplateColumns={1} withSavebar={false}> <DetailPageLayout gridTemplateColumns={1} withSavebar={false}>
<AppPageNav <AppPageNav
appId={data.id}
name={data?.name} name={data?.name}
supportUrl={data?.supportUrl} supportUrl={data?.supportUrl}
homepageUrl={data?.homepageUrl} homepageUrl={data?.homepageUrl}

View file

@ -2,6 +2,7 @@ import { AppAvatar } from "@dashboard/apps/components/AppAvatar/AppAvatar";
import { AppUrls } from "@dashboard/apps/urls"; import { AppUrls } from "@dashboard/apps/urls";
import { TopNavLink, TopNavWrapper } from "@dashboard/components/AppLayout"; import { TopNavLink, TopNavWrapper } from "@dashboard/components/AppLayout";
import { LinkState } from "@dashboard/components/Link"; import { LinkState } from "@dashboard/components/Link";
import useNavigator from "@dashboard/hooks/useNavigator";
import { Box, Button, Text } from "@saleor/macaw-ui/next"; import { Box, Button, Text } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
@ -12,6 +13,7 @@ interface AppPageNavProps {
supportUrl: string | undefined | null; supportUrl: string | undefined | null;
homepageUrl: string | undefined | null; homepageUrl: string | undefined | null;
author: string | undefined | null; author: string | undefined | null;
appId: string;
} }
export const AppPageNav: React.FC<AppPageNavProps> = ({ export const AppPageNav: React.FC<AppPageNavProps> = ({
@ -19,9 +21,15 @@ export const AppPageNav: React.FC<AppPageNavProps> = ({
supportUrl, supportUrl,
homepageUrl, homepageUrl,
author, author,
appId,
}) => { }) => {
const location = useLocation<LinkState>(); const location = useLocation<LinkState>();
const goBackLink = location.state?.from ?? AppUrls.resolveAppListUrl(); const goBackLink = location.state?.from ?? AppUrls.resolveAppListUrl();
const navigate = useNavigator();
const navigateToManageAppScreen = () => {
navigate(AppUrls.resolveAppDetailsUrl(appId));
};
return ( return (
<TopNavWrapper> <TopNavWrapper>
@ -55,6 +63,18 @@ export const AppPageNav: React.FC<AppPageNavProps> = ({
</Box> </Box>
</Box> </Box>
<Box display="flex" gap={4}> <Box display="flex" gap={4}>
<Button
whiteSpace="nowrap"
variant="secondary"
onClick={navigateToManageAppScreen}
data-test-id="app-settings-button"
>
<FormattedMessage
defaultMessage="Manage app"
id="LwX0Ug"
description="Button with Manage app label"
/>
</Button>
{supportUrl && ( {supportUrl && (
<Button <Button
variant="secondary" variant="secondary"

View file

@ -4,7 +4,6 @@ import { InstalledApp } from "@dashboard/apps/types";
import { getAppsConfig } from "@dashboard/config"; import { getAppsConfig } from "@dashboard/config";
import Wrapper from "@test/wrapper"; import Wrapper from "@test/wrapper";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react"; import React from "react";
import { MemoryRouter as Router } from "react-router-dom"; import { MemoryRouter as Router } from "react-router-dom";
@ -29,7 +28,6 @@ const Component = ({
describe("Apps InstalledAppListRow", () => { describe("Apps InstalledAppListRow", () => {
it("displays app details when basic app data passed", () => { it("displays app details when basic app data passed", () => {
// Arrange // Arrange
const openAppSettings = jest.fn();
const removeAppInstallation = jest.fn(); const removeAppInstallation = jest.fn();
const retryAppInstallation = jest.fn(); const retryAppInstallation = jest.fn();
render( render(
@ -39,7 +37,6 @@ describe("Apps InstalledAppListRow", () => {
isExternal: false, isExternal: false,
}} }}
context={{ context={{
openAppSettings,
removeAppInstallation, removeAppInstallation,
retryAppInstallation, retryAppInstallation,
}} }}
@ -67,9 +64,9 @@ describe("Apps InstalledAppListRow", () => {
it("displays external label when app is external", () => { it("displays external label when app is external", () => {
// Arrange // Arrange
const openAppSettings = jest.fn();
const removeAppInstallation = jest.fn(); const removeAppInstallation = jest.fn();
const retryAppInstallation = jest.fn(); const retryAppInstallation = jest.fn();
render( render(
<Component <Component
data={{ data={{
@ -77,7 +74,6 @@ describe("Apps InstalledAppListRow", () => {
isExternal: true, isExternal: true,
}} }}
context={{ context={{
openAppSettings,
removeAppInstallation, removeAppInstallation,
retryAppInstallation, retryAppInstallation,
}} }}
@ -91,10 +87,10 @@ describe("Apps InstalledAppListRow", () => {
it("displays tunnnel label when app is served via tunnnel", () => { it("displays tunnnel label when app is served via tunnnel", () => {
// Arrange // Arrange
const openAppSettings = jest.fn();
const removeAppInstallation = jest.fn(); const removeAppInstallation = jest.fn();
const retryAppInstallation = jest.fn(); const retryAppInstallation = jest.fn();
const AppsConfig = getAppsConfig(); const AppsConfig = getAppsConfig();
render( render(
<Component <Component
data={{ data={{
@ -106,7 +102,6 @@ describe("Apps InstalledAppListRow", () => {
isExternal: false, isExternal: false,
}} }}
context={{ context={{
openAppSettings,
removeAppInstallation, removeAppInstallation,
retryAppInstallation, retryAppInstallation,
}} }}
@ -117,33 +112,4 @@ describe("Apps InstalledAppListRow", () => {
// Assert // Assert
expect(tunnelLabel).toBeTruthy(); expect(tunnelLabel).toBeTruthy();
}); });
it("calls handlers when app data passed and buttons clicked", async () => {
// Arrange
const openAppSettings = jest.fn();
const removeAppInstallation = jest.fn();
const retryAppInstallation = jest.fn();
render(
<Component
data={{
app: activeApp,
isExternal: false,
}}
context={{
openAppSettings,
removeAppInstallation,
retryAppInstallation,
}}
/>,
);
const user = userEvent.setup();
const settingsButton = screen.getByTestId("app-settings-button");
// Act
await user.click(settingsButton);
// Assert
expect(openAppSettings).toHaveBeenCalledWith(activeApp.id);
expect(openAppSettings).toHaveBeenCalledTimes(1);
});
}); });

View file

@ -1,18 +1,9 @@
import { useAppListContext } from "@dashboard/apps/context";
import { appsMessages } from "@dashboard/apps/messages"; import { appsMessages } from "@dashboard/apps/messages";
import { InstalledApp } from "@dashboard/apps/types"; import { InstalledApp } from "@dashboard/apps/types";
import { AppUrls } from "@dashboard/apps/urls"; import { AppUrls } from "@dashboard/apps/urls";
import { isAppInTunnel } from "@dashboard/apps/utils"; import { isAppInTunnel } from "@dashboard/apps/utils";
import Link from "@dashboard/components/Link"; import Link from "@dashboard/components/Link";
import { TableButtonWrapper } from "@dashboard/components/TableButtonWrapper/TableButtonWrapper"; import { Box, Chip, List, sprinkles, Text } from "@saleor/macaw-ui/next";
import {
Box,
Button,
Chip,
List,
sprinkles,
Text,
} from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useLocation } from "react-router"; import { useLocation } from "react-router";
@ -25,7 +16,7 @@ import { messages } from "./messages";
export const InstalledAppListRow: React.FC<InstalledApp> = props => { export const InstalledAppListRow: React.FC<InstalledApp> = props => {
const { app, isExternal, logo } = props; const { app, isExternal, logo } = props;
const intl = useIntl(); const intl = useIntl();
const { openAppSettings } = useAppListContext();
const location = useLocation(); const location = useLocation();
return ( return (
@ -44,7 +35,11 @@ export const InstalledAppListRow: React.FC<InstalledApp> = props => {
justifyContent="space-between" justifyContent="space-between"
flexDirection="row" flexDirection="row"
flexWrap="wrap" flexWrap="wrap"
backgroundColor={!app.isActive ? "surfaceNeutralSubdued" : undefined} transition={"ease"}
backgroundColor={{
default: !app.isActive ? "surfaceNeutralSubdued" : undefined,
hover: "surfaceNeutralSubdued",
}}
cursor={app.isActive ? "pointer" : "not-allowed"} cursor={app.isActive ? "pointer" : "not-allowed"}
> >
<Box <Box
@ -102,15 +97,6 @@ export const InstalledAppListRow: React.FC<InstalledApp> = props => {
)} )}
<AppPermissions permissions={app.permissions} /> <AppPermissions permissions={app.permissions} />
</Box> </Box>
<TableButtonWrapper>
<Button
variant="secondary"
onClick={() => openAppSettings(app.id)}
data-test-id="app-settings-button"
>
<FormattedMessage {...messages.settings} />
</Button>
</TableButtonWrapper>
</Box> </Box>
</List.Item> </List.Item>
</Link> </Link>

View file

@ -6,11 +6,6 @@ export const messages = defineMessages({
defaultMessage: "Tunnel - development", defaultMessage: "Tunnel - development",
description: "label", description: "label",
}, },
settings: {
id: "BYGJ/j",
defaultMessage: "Settings",
description: "button label",
},
appDisabled: { appDisabled: {
id: "7u9Ep7", id: "7u9Ep7",
defaultMessage: "Disabled", defaultMessage: "Disabled",

View file

@ -4,7 +4,6 @@ import React from "react";
export interface AppListContextValues { export interface AppListContextValues {
removeAppInstallation: (installationId: string) => void; removeAppInstallation: (installationId: string) => void;
retryAppInstallation: (installationId: string) => void; retryAppInstallation: (installationId: string) => void;
openAppSettings: (appId: string) => void;
} }
export const AppListContext = React.createContext< export const AppListContext = React.createContext<

View file

@ -154,7 +154,6 @@ export const AppDetails: React.FC<AppDetailsProps> = ({ id, params }) => {
<AppDetailsPage <AppDetailsPage
data={data?.app || null} data={data?.app || null}
loading={loading} loading={loading}
navigateToApp={() => navigate(AppUrls.resolveAppUrl(id))}
onAppActivateOpen={() => openModal("app-activate")} onAppActivateOpen={() => openModal("app-activate")}
onAppDeactivateOpen={() => openModal("app-deactivate")} onAppDeactivateOpen={() => openModal("app-deactivate")}
onAppDeleteOpen={() => openModal("app-delete")} onAppDeleteOpen={() => openModal("app-delete")}

View file

@ -135,7 +135,6 @@ export const AppsList: React.FC<AppsListProps> = ({ params }) => {
() => ({ () => ({
retryAppInstallation: handleAppInstallRetry, retryAppInstallation: handleAppInstallRetry,
removeAppInstallation: id => openModal("app-installation-remove", { id }), removeAppInstallation: id => openModal("app-installation-remove", { id }),
openAppSettings: id => navigate(AppUrls.resolveAppDetailsUrl(id)),
}), }),
[navigate, openModal], [navigate, openModal],
); );