Migrate app details view (settings view) to new-apps (#3167)

* Migrate app details view to new-apps

* Fix data privacy card
This commit is contained in:
Dawid 2023-03-01 15:04:53 +01:00 committed by GitHub
parent 3c082b3386
commit dfb5f167d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 627 additions and 497 deletions

View file

@ -31,6 +31,10 @@
"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}"
@ -908,10 +912,6 @@
"context": "content", "context": "content",
"string": "Saleor couldnt fetch crucial information regarding installation. Without those System cant install the app in your Saleor. Please use the button below to get back to systems dashboard." "string": "Saleor couldnt fetch crucial information regarding installation. Without those System cant install the app in your Saleor. Please use the button below to get back to systems dashboard."
}, },
"5+Xcrz": {
"context": "app deactivated",
"string": "Deactivated"
},
"51HE+Q": { "51HE+Q": {
"string": "No sales found" "string": "No sales found"
}, },
@ -1192,10 +1192,6 @@
"context": "tax classes name input placeholder", "context": "tax classes name input placeholder",
"string": "Tax rate name" "string": "Tax rate name"
}, },
"73RU3R": {
"context": "deactivate app",
"string": "Are you sure you want to disable this app? Your data will be kept until you reactivate the app. You will be still billed for the app."
},
"74Cxe8": { "74Cxe8": {
"context": "table header product label", "context": "table header product label",
"string": "Product" "string": "Product"
@ -1234,10 +1230,6 @@
"context": "custom search delete, dialog header", "context": "custom search delete, dialog header",
"string": "Delete Search" "string": "Delete Search"
}, },
"7O2EsY": {
"context": "deactivate local app",
"string": "Are you sure you want to disable this app? Your data will be kept until you reactivate the app."
},
"7Oorx5": { "7Oorx5": {
"context": "navigator section header", "context": "navigator section header",
"string": "Search in Catalog" "string": "Search in Catalog"
@ -1922,10 +1914,6 @@
"D2qihU": { "D2qihU": {
"string": "Permission is invalid" "string": "Permission is invalid"
}, },
"D3E2b5": {
"context": "button label",
"string": "Activate"
},
"D3WUc/": { "D3WUc/": {
"context": "order history message", "context": "order history message",
"string": "Order was refunded by {refundedBy}" "string": "Order was refunded by {refundedBy}"
@ -2465,10 +2453,6 @@
"context": "header", "context": "header",
"string": "Create New App" "string": "Create New App"
}, },
"Gjb6eq": {
"context": "link",
"string": "Get Support"
},
"Gkip05": { "Gkip05": {
"context": "button", "context": "button",
"string": "Unassign" "string": "Unassign"
@ -2632,10 +2616,6 @@
"context": "create gift card product alert message", "context": "create gift card product alert message",
"string": "Create a gift card product" "string": "Create a gift card product"
}, },
"HtfL5/": {
"context": "button",
"string": "Open App"
},
"HvJPcU": { "HvJPcU": {
"string": "Category deleted" "string": "Category deleted"
}, },
@ -3143,10 +3123,6 @@
"context": "button", "context": "button",
"string": "Create order" "string": "Create order"
}, },
"LtqrM8": {
"context": "delete custom app",
"string": "Deleting {name}, you will delete all the data and webhooks regarding this app. Are you sure you want to do that?"
},
"Lx1ima": { "Lx1ima": {
"context": "button", "context": "button",
"string": "Upload image" "string": "Upload image"
@ -3395,14 +3371,14 @@
"context": "header field name, header", "context": "header field name, header",
"string": "Name" "string": "Name"
}, },
"Np7J92": {
"context": "deactivate local named app",
"string": "Are you sure you want to disable {name}? Your data will be kept until you reactivate the app."
},
"NqxvFh": { "NqxvFh": {
"context": "navigator placeholder", "context": "navigator placeholder",
"string": "Type Command" "string": "Type Command"
}, },
"Nsk5WL": {
"context": "link",
"string": "Get support"
},
"NtFVFS": { "NtFVFS": {
"context": "weight", "context": "weight",
"string": "{value} {unit}" "string": "{value} {unit}"
@ -4500,10 +4476,6 @@
"VyzsWZ": { "VyzsWZ": {
"string": "Default Billing Address" "string": "Default Billing Address"
}, },
"W+AFZY": {
"context": "button label",
"string": "Deactivate"
},
"W/Es0H": { "W/Es0H": {
"string": "Order successfully cancelled" "string": "Order successfully cancelled"
}, },
@ -6891,6 +6863,10 @@
"context": "search label", "context": "search label",
"string": "Search Countries" "string": "Search Countries"
}, },
"pGwvpX": {
"context": "status",
"string": "Deactivated"
},
"pNrF72": { "pNrF72": {
"context": "tab name", "context": "tab name",
"string": "All Vouchers" "string": "All Vouchers"
@ -7810,10 +7786,6 @@
"context": "section header", "context": "section header",
"string": "Plugin Information and Status" "string": "Plugin Information and Status"
}, },
"w6Gau0": {
"context": "deactivate named app",
"string": "Are you sure you want to disable {name}? Your data will be kept until you reactivate the app. You will be still billed for the app."
},
"w9xgN9": { "w9xgN9": {
"context": "see error log label in notification", "context": "see error log label in notification",
"string": "See error log" "string": "See error log"

View file

@ -1,65 +0,0 @@
import ActionDialog from "@dashboard/components/ActionDialog";
import { getStringOrPlaceholder } from "@dashboard/misc";
import { DialogContentText } from "@material-ui/core";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
export interface AppActivateDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
name: string;
onClose: () => void;
onConfirm: () => void;
}
const AppActivateDialog: React.FC<AppActivateDialogProps> = ({
confirmButtonState,
open,
name,
onClose,
onConfirm,
}) => {
const intl = useIntl();
return (
<ActionDialog
confirmButtonLabel={intl.formatMessage({
id: "D3E2b5",
defaultMessage: "Activate",
description: "button label",
})}
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={onConfirm}
title={intl.formatMessage({
id: "YHNozE",
defaultMessage: "Activate App",
description: "dialog header",
})}
variant="default"
>
<DialogContentText>
{["", null].includes(name) ? (
<FormattedMessage
id="Q47ovw"
defaultMessage="Are you sure you want to activate this app? Activating will start gathering events."
description="activate app"
/>
) : (
<FormattedMessage
id="JbUg2b"
defaultMessage="Are you sure you want to activate {name}? Activating will start gathering events."
description="activate app"
values={{
name: <strong>{getStringOrPlaceholder(name)}</strong>,
}}
/>
)}
</DialogContentText>
</ActionDialog>
);
};
AppActivateDialog.displayName = "AppActivateDialog";
export default AppActivateDialog;

View file

@ -1,2 +0,0 @@
export * from "./AppActivateDialog";
export { default } from "./AppActivateDialog";

View file

@ -1,67 +0,0 @@
import ActionDialog from "@dashboard/components/ActionDialog";
import { getStringOrPlaceholder } from "@dashboard/misc";
import { DialogContentText } from "@material-ui/core";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import msgs from "./messages";
export interface AppDeactivateDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
name: string;
thirdParty?: boolean;
onClose: () => void;
onConfirm: () => void;
}
const AppDeactivateDialog: React.FC<AppDeactivateDialogProps> = ({
confirmButtonState,
open,
name,
thirdParty = true,
onClose,
onConfirm,
}) => {
const intl = useIntl();
return (
<ActionDialog
confirmButtonLabel={intl.formatMessage({
id: "W+AFZY",
defaultMessage: "Deactivate",
description: "button label",
})}
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={onConfirm}
title={intl.formatMessage({
id: "yMi8I8",
defaultMessage: "Dectivate App",
description: "dialog header",
})}
variant="delete"
>
<DialogContentText>
{["", null].includes(name) ? (
<FormattedMessage
{...(thirdParty ? msgs.deactivateApp : msgs.deactivateLocalApp)}
/>
) : (
<FormattedMessage
{...(thirdParty
? msgs.deactivateNamedApp
: msgs.deactivateLocalNamedApp)}
values={{
name: <strong>{getStringOrPlaceholder(name)}</strong>,
}}
/>
)}
</DialogContentText>
</ActionDialog>
);
};
AppDeactivateDialog.displayName = "AppDeactivateDialog";
export default AppDeactivateDialog;

View file

@ -1,2 +0,0 @@
export * from "./AppDeactivateDialog";
export { default } from "./AppDeactivateDialog";

View file

@ -1,28 +0,0 @@
import { defineMessages } from "react-intl";
export default defineMessages({
deactivateApp: {
id: "73RU3R",
defaultMessage:
"Are you sure you want to disable this app? Your data will be kept until you reactivate the app. You will be still billed for the app.",
description: "deactivate app",
},
deactivateNamedApp: {
id: "w6Gau0",
defaultMessage:
"Are you sure you want to disable {name}? Your data will be kept until you reactivate the app. You will be still billed for the app.",
description: "deactivate named app",
},
deactivateLocalApp: {
id: "7O2EsY",
defaultMessage:
"Are you sure you want to disable this app? Your data will be kept until you reactivate the app.",
description: "deactivate local app",
},
deactivateLocalNamedApp: {
id: "Np7J92",
defaultMessage:
"Are you sure you want to disable {name}? Your data will be kept until you reactivate the app.",
description: "deactivate local named app",
},
});

View file

@ -1,71 +0,0 @@
import ActionDialog from "@dashboard/components/ActionDialog";
import { getStringOrPlaceholder } from "@dashboard/misc";
import { DialogContentText } from "@material-ui/core";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
export interface AppDeleteDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
name: string;
onClose: () => void;
onConfirm: () => void;
type: "CUSTOM" | "EXTERNAL";
}
const AppDeleteDialog: React.FC<AppDeleteDialogProps> = ({
confirmButtonState,
open,
name,
onClose,
onConfirm,
type,
}) => {
const intl = useIntl();
return (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={onConfirm}
title={intl.formatMessage({
id: "zQX6xO",
defaultMessage: "Delete App",
description: "dialog header",
})}
variant="delete"
>
<DialogContentText>
{["", null].includes(name) ? (
<FormattedMessage
id="6hLZNA"
defaultMessage="Are you sure you want to delete this app?"
description="delete app"
/>
) : type === "EXTERNAL" ? (
<FormattedMessage
id="EWD/wU"
defaultMessage="Deleting {name}, you will remove installation of the app. If you are paying for app subscription, remember to unsubscribe from the app in Saleor Marketplace. Are you sure you want to delete the app?"
description="delete app"
values={{
name: <strong>{getStringOrPlaceholder(name)}</strong>,
}}
/>
) : (
<FormattedMessage
id="LtqrM8"
defaultMessage="Deleting {name}, you will delete all the data and webhooks regarding this app. Are you sure you want to do that?"
description="delete custom app"
values={{
name: <strong>{getStringOrPlaceholder(name)}</strong>,
}}
/>
)}
</DialogContentText>
</ActionDialog>
);
};
AppDeleteDialog.displayName = "AppDeleteDialog";
export default AppDeleteDialog;

View file

@ -1,2 +0,0 @@
export * from "./AppDeleteDialog";
export { default } from "./AppDeleteDialog";

View file

@ -1,186 +0,0 @@
import { appsListPath } from "@dashboard/apps/urls";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import CardSpacer from "@dashboard/components/CardSpacer";
import CardTitle from "@dashboard/components/CardTitle";
import ExternalLink from "@dashboard/components/ExternalLink";
import Skeleton from "@dashboard/components/Skeleton";
import { AppQuery } from "@dashboard/graphql";
import { buttonMessages } from "@dashboard/intl";
import { ButtonBase, Card, CardContent, Typography } from "@material-ui/core";
import { Box, Button } from "@saleor/macaw-ui/next";
import React from "react";
import SVG from "react-inlinesvg";
import { FormattedMessage, useIntl } from "react-intl";
import ReactMarkdown from "react-markdown";
import activateIcon from "../../../../assets/images/activate-icon.svg";
import deleteIcon from "../../../../assets/images/delete.svg";
import supportIcon from "../../../../assets/images/support-icon.svg";
import { useStyles } from "../../styles";
import DeactivatedText from "../DeactivatedText";
export interface AppDetailsPageProps {
loading: boolean;
data: AppQuery["app"];
navigateToApp: () => void;
onAppActivateOpen: () => void;
onAppDeactivateOpen: () => void;
onAppDeleteOpen: () => void;
}
export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
data,
loading,
navigateToApp,
onAppActivateOpen,
onAppDeactivateOpen,
onAppDeleteOpen,
}) => {
const intl = useIntl();
const classes = useStyles({});
return (
<>
<TopNav
href={appsListPath}
title={
<>
{data?.name} {!data?.isActive && <DeactivatedText />}
</>
}
>
<Button onClick={navigateToApp} variant="primary" data-tc="open-app">
<FormattedMessage
id="HtfL5/"
defaultMessage="Open App"
description="button"
/>
</Button>
</TopNav>
<Box marginX={10}>
{data ? (
<div className={classes.appHeaderLinks}>
<ExternalLink
className={classes.headerLinkContainer}
href={data.supportUrl || ""}
target="_blank"
>
<SVG src={supportIcon} />
<FormattedMessage
id="Gjb6eq"
defaultMessage="Get Support"
description="link"
/>
</ExternalLink>
<ButtonBase
className={classes.headerLinkContainer}
disableRipple
onClick={data.isActive ? onAppDeactivateOpen : onAppActivateOpen}
>
<SVG src={activateIcon} />
{data?.isActive ? (
<FormattedMessage {...buttonMessages.deactivate} />
) : (
<FormattedMessage {...buttonMessages.activate} />
)}
</ButtonBase>
<ButtonBase
className={classes.headerLinkContainer}
disableRipple
onClick={onAppDeleteOpen}
>
<SVG src={deleteIcon} />
<FormattedMessage {...buttonMessages.delete} />
</ButtonBase>
</div>
) : (
<Skeleton />
)}
<div className={classes.hr} />
</Box>
<Card>
<CardTitle
title={intl.formatMessage({
id: "jDIRQV",
defaultMessage: "About this app",
description: "section header",
})}
/>
<CardContent>
{!loading ? (
<ReactMarkdown source={data?.aboutApp ?? ""} />
) : (
<Skeleton />
)}
</CardContent>
</Card>
<CardSpacer />
<Card>
<CardTitle
title={intl.formatMessage({
id: "VsGcdP",
defaultMessage: "App permissions",
description: "section header",
})}
/>
<CardContent>
{!loading ? (
<>
<Typography>
<FormattedMessage
id="7oQUMG"
defaultMessage="This app has permissions to:"
description="apps about permissions"
/>
</Typography>
{!!data?.permissions?.length && (
<ul className={classes.permissionsContainer}>
{data?.permissions?.map(perm => (
<li key={perm.code}>{perm.name}</li>
))}
</ul>
)}
</>
) : (
<Skeleton />
)}
</CardContent>
</Card>
<CardSpacer />
{data?.dataPrivacyUrl && (
<Card>
<CardTitle
title={intl.formatMessage({
id: "a55zOn",
defaultMessage: "Data privacy",
description: "section header",
})}
/>
<CardContent>
{!loading ? (
<ExternalLink
className={classes.linkContainer}
href={data.dataPrivacyUrl}
target="_blank"
>
<FormattedMessage
id="Go50v2"
defaultMessage="View this apps privacy policy"
description="app privacy policy link"
/>
</ExternalLink>
) : (
<Skeleton />
)}
</CardContent>
</Card>
)}
<CardSpacer />
</>
);
};
AppDetailsPage.displayName = "AppDetailsPage";
export default AppDetailsPage;

View file

@ -7,28 +7,14 @@ import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { WindowTitle } from "../components/WindowTitle"; import { WindowTitle } from "../components/WindowTitle";
import { import {
appDetailsPath,
AppDetailsUrlQueryParams,
appInstallPath, appInstallPath,
AppInstallUrlQueryParams, AppInstallUrlQueryParams,
AppListUrlQueryParams, AppListUrlQueryParams,
appsListPath, appsListPath,
} from "./urls"; } from "./urls";
import AppDetailsView from "./views/AppDetails";
import AppInstallView from "./views/AppInstall"; import AppInstallView from "./views/AppInstall";
import AppsListView from "./views/AppsList"; import AppsListView from "./views/AppsList";
const AppDetails: React.FC<RouteComponentProps<{ id: string }>> = ({
match,
}) => {
const qs = parseQs(location.search.substr(1));
const params: AppDetailsUrlQueryParams = qs;
return (
<AppDetailsView id={decodeURIComponent(match.params.id)} params={params} />
);
};
const AppInstall: React.FC<RouteComponentProps> = props => { const AppInstall: React.FC<RouteComponentProps> = props => {
const qs = parseQs(location.search.substr(1)); const qs = parseQs(location.search.substr(1));
const params: AppInstallUrlQueryParams = qs; const params: AppInstallUrlQueryParams = qs;
@ -51,7 +37,6 @@ const Component = () => {
<Switch> <Switch>
<Route exact path={appsListPath} component={AppsList} /> <Route exact path={appsListPath} component={AppsList} />
<Route exact path={appInstallPath} component={AppInstall} /> <Route exact path={appInstallPath} component={AppInstall} />
<Route exact path={appDetailsPath(":id")} component={AppDetails} />
<WebhooksRoutes /> <WebhooksRoutes />
</Switch> </Switch>
</> </>

View file

@ -14,8 +14,8 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import AppsSkeleton from "../../../apps/components/AppsSkeleton"; import AppsSkeleton from "../../../apps/components/AppsSkeleton";
import DeactivatedText from "../../../apps/components/DeactivatedText";
import { useStyles } from "../../../apps/styles"; import { useStyles } from "../../../apps/styles";
import DeactivatedText from "../../../new-apps/components/DeactivatedText";
export interface CustomAppListPageProps { export interface CustomAppListPageProps {
appsList: AppListItemFragment[]; appsList: AppListItemFragment[];

View file

@ -1,5 +1,3 @@
import AppActivateDialog from "@dashboard/apps/components/AppActivateDialog";
import AppDeactivateDialog from "@dashboard/apps/components/AppDeactivateDialog";
import { appMessages } from "@dashboard/apps/messages"; import { appMessages } from "@dashboard/apps/messages";
import NotFoundPage from "@dashboard/components/NotFoundPage"; import NotFoundPage from "@dashboard/components/NotFoundPage";
import { WindowTitle } from "@dashboard/components/WindowTitle"; import { WindowTitle } from "@dashboard/components/WindowTitle";
@ -26,6 +24,8 @@ import useNotifier from "@dashboard/hooks/useNotifier";
import useShop from "@dashboard/hooks/useShop"; import useShop from "@dashboard/hooks/useShop";
import { commonMessages } from "@dashboard/intl"; import { commonMessages } from "@dashboard/intl";
import { extractMutationErrors, getStringOrPlaceholder } from "@dashboard/misc"; import { extractMutationErrors, getStringOrPlaceholder } from "@dashboard/misc";
import AppActivateDialog from "@dashboard/new-apps/components/AppActivateDialog";
import AppDeactivateDialog from "@dashboard/new-apps/components/AppDeactivateDialog";
import getAppErrorMessage from "@dashboard/utils/errors/app"; import getAppErrorMessage from "@dashboard/utils/errors/app";
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
import React from "react"; import React from "react";

View file

@ -1,5 +1,4 @@
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import AppDeleteDialog from "@dashboard/apps/components/AppDeleteDialog";
import { EXTENSION_LIST_QUERY } from "@dashboard/apps/queries"; import { EXTENSION_LIST_QUERY } from "@dashboard/apps/queries";
import { WindowTitle } from "@dashboard/components/WindowTitle"; import { WindowTitle } from "@dashboard/components/WindowTitle";
import { import {
@ -13,6 +12,7 @@ import useNavigator from "@dashboard/hooks/useNavigator";
import useNotifier from "@dashboard/hooks/useNotifier"; import useNotifier from "@dashboard/hooks/useNotifier";
import { sectionNames } from "@dashboard/intl"; import { sectionNames } from "@dashboard/intl";
import { findById } from "@dashboard/misc"; import { findById } from "@dashboard/misc";
import AppDeleteDialog from "@dashboard/new-apps/components/AppDeleteDialog";
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
import { mapEdgesToItems } from "@dashboard/utils/maps"; import { mapEdgesToItems } from "@dashboard/utils/maps";
import React from "react"; import React from "react";

View file

@ -562,6 +562,11 @@ export const commonStatusMessages = defineMessages({
id: "tthToS", id: "tthToS",
defaultMessage: "Disabled", defaultMessage: "Disabled",
}, },
deactivated: {
id: "pGwvpX",
defaultMessage: "Deactivated",
description: "status",
},
}); });
export const orderStatusMessages = defineMessages({ export const orderStatusMessages = defineMessages({

View file

@ -0,0 +1,27 @@
import CardTitle from "@dashboard/components/CardTitle";
import Skeleton from "@dashboard/components/Skeleton";
import { Card, CardContent } from "@material-ui/core";
import React from "react";
import { useIntl } from "react-intl";
import ReactMarkdown from "react-markdown";
import messages from "./messages";
interface AboutCardProps {
aboutApp?: string | null;
loading: boolean;
}
const AboutCard: React.FC<AboutCardProps> = ({ aboutApp, loading }) => {
const intl = useIntl();
return (
<Card>
<CardTitle title={intl.formatMessage(messages.aboutAppTitle)} />
<CardContent>
{!loading ? <ReactMarkdown source={aboutApp ?? ""} /> : <Skeleton />}
</CardContent>
</Card>
);
};
export default AboutCard;

View file

@ -0,0 +1,79 @@
import { render } from "@testing-library/react";
import React from "react";
import { appDetails } from "../../fixtures";
import AppDetailsPage from "./AppDetailsPage";
const mockHeader = jest.fn();
jest.mock("./Header", () => props => {
mockHeader(props);
return <></>;
});
const mockAboutCard = jest.fn();
jest.mock("./AboutCard", () => props => {
mockAboutCard(props);
return <></>;
});
const mockPermissionsCard = jest.fn();
jest.mock("./PermissionsCard", () => props => {
mockPermissionsCard(props);
return <></>;
});
const mockDataPrivacyCard = jest.fn();
jest.mock("./DataPrivacyCard", () => props => {
mockDataPrivacyCard(props);
return <></>;
});
beforeEach(() => {
mockHeader.mockClear();
mockAboutCard.mockClear();
mockPermissionsCard.mockClear();
mockDataPrivacyCard.mockClear();
});
describe("Apps AppDetailsPage", () => {
it("displays app details when app data passed", () => {
// Arrange
const navigateToApp = jest.fn();
const onAppActivateOpen = jest.fn();
const onAppDeactivateOpen = jest.fn();
const onAppDeleteOpen = jest.fn();
// Act
render(
<AppDetailsPage
data={appDetails}
loading={false}
navigateToApp={navigateToApp}
onAppActivateOpen={onAppActivateOpen}
onAppDeactivateOpen={onAppDeactivateOpen}
onAppDeleteOpen={onAppDeleteOpen}
/>,
);
// Assert
expect(mockHeader).toHaveBeenCalledWith({
data: appDetails,
navigateToApp,
onAppActivateOpen,
onAppDeactivateOpen,
onAppDeleteOpen,
});
expect(mockAboutCard).toHaveBeenCalledWith({
aboutApp: appDetails.aboutApp,
loading: false,
});
expect(mockPermissionsCard).toHaveBeenCalledWith({
permissions: appDetails.permissions,
loading: false,
});
expect(mockDataPrivacyCard).toHaveBeenCalledWith({
dataPrivacyUrl: appDetails.dataPrivacyUrl,
loading: false,
});
});
});

View file

@ -0,0 +1,45 @@
import CardSpacer from "@dashboard/components/CardSpacer";
import { AppQuery } from "@dashboard/graphql";
import React from "react";
import AboutCard from "./AboutCard";
import DataPrivacyCard from "./DataPrivacyCard";
import Header from "./Header";
import PermissionsCard from "./PermissionsCard";
export interface AppDetailsPageProps {
loading: boolean;
data: AppQuery["app"];
navigateToApp: () => void;
onAppActivateOpen: () => void;
onAppDeactivateOpen: () => void;
onAppDeleteOpen: () => void;
}
export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
data,
loading,
navigateToApp,
onAppActivateOpen,
onAppDeactivateOpen,
onAppDeleteOpen,
}) => (
<>
<Header
data={data}
navigateToApp={navigateToApp}
onAppActivateOpen={onAppActivateOpen}
onAppDeactivateOpen={onAppDeactivateOpen}
onAppDeleteOpen={onAppDeleteOpen}
/>
<AboutCard aboutApp={data?.aboutApp} loading={loading} />
<CardSpacer />
<PermissionsCard permissions={data?.permissions} loading={loading} />
<CardSpacer />
<DataPrivacyCard dataPrivacyUrl={data?.dataPrivacyUrl} loading={loading} />
<CardSpacer />
</>
);
AppDetailsPage.displayName = "AppDetailsPage";
export default AppDetailsPage;

View file

@ -0,0 +1,46 @@
import CardTitle from "@dashboard/components/CardTitle";
import ExternalLink from "@dashboard/components/ExternalLink";
import Skeleton from "@dashboard/components/Skeleton";
import { Card, CardContent } from "@material-ui/core";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import messages from "./messages";
import { useStyles } from "./styles";
interface DataPrivacyCardProps {
dataPrivacyUrl?: string | null;
loading: boolean;
}
const DataPrivacyCard: React.FC<DataPrivacyCardProps> = ({
dataPrivacyUrl,
loading,
}) => {
const classes = useStyles();
const intl = useIntl();
if (!dataPrivacyUrl && !loading) {
return null;
}
return (
<Card>
<CardTitle title={intl.formatMessage(messages.dataPrivacyTitle)} />
<CardContent>
{!loading ? (
<ExternalLink
className={classes.linkContainer}
href={dataPrivacyUrl}
target="_blank"
>
<FormattedMessage {...messages.dataPrivacyDescription} />
</ExternalLink>
) : (
<Skeleton />
)}
</CardContent>
</Card>
);
};
export default DataPrivacyCard;

View file

@ -0,0 +1,112 @@
import { appDetails } from "@dashboard/new-apps/fixtures";
import { render } from "@testing-library/react";
import React from "react";
import Header from "./Header";
const mockHeaderOptions = jest.fn();
const mockTopNav = jest.fn();
const mockButton = jest.fn();
jest.mock("@dashboard/components/AppLayout/TopNav", () => ({
TopNav: props => {
mockTopNav(props);
return <>{props.children}</>;
},
}));
jest.mock("@saleor/macaw-ui/next", () => ({
Button: props => {
mockButton(props);
return <>{props.children}</>;
},
}));
jest.mock("../DeactivatedText", () => () => "deactivated");
jest.mock("react-intl", () => ({
useIntl: jest.fn(() => ({
formatMessage: jest.fn(x => x.defaultMessage),
})),
defineMessages: jest.fn(x => x),
FormattedMessage: ({ defaultMessage }) => <>{defaultMessage}</>,
}));
jest.mock("./HeaderOptions", () => props => {
mockHeaderOptions(props);
return <></>;
});
beforeEach(() => {
mockHeaderOptions.mockClear();
mockTopNav.mockClear();
mockButton.mockClear();
});
describe("Apps AppDetailsPage Header", () => {
it("displays app details options when active app data passed", () => {
// Arrange
const navigateToApp = jest.fn();
const onAppActivateOpen = jest.fn();
const onAppDeactivateOpen = jest.fn();
const onAppDeleteOpen = jest.fn();
// Act
render(
<Header
data={appDetails}
navigateToApp={navigateToApp}
onAppActivateOpen={onAppActivateOpen}
onAppDeactivateOpen={onAppDeactivateOpen}
onAppDeleteOpen={onAppDeleteOpen}
/>,
);
const title = render(mockTopNav.mock.calls[0][0].title);
// Assert
expect(mockHeaderOptions).toHaveBeenCalledWith({
data: appDetails,
onAppActivateOpen,
onAppDeactivateOpen,
onAppDeleteOpen,
});
expect(mockButton).toHaveBeenCalledWith(
expect.objectContaining({
onClick: navigateToApp,
}),
);
expect(mockTopNav).toHaveBeenCalled();
expect(title.container).toHaveTextContent(appDetails.name as string);
});
it("displays app details options when inactive app data passed", () => {
// Arrange
const navigateToApp = jest.fn();
const onAppActivateOpen = jest.fn();
const onAppDeactivateOpen = jest.fn();
const onAppDeleteOpen = jest.fn();
// Act
render(
<Header
data={{ ...appDetails, isActive: false }}
navigateToApp={navigateToApp}
onAppActivateOpen={onAppActivateOpen}
onAppDeactivateOpen={onAppDeactivateOpen}
onAppDeleteOpen={onAppDeleteOpen}
/>,
);
const title = render(mockTopNav.mock.calls[0][0].title);
// Assert
expect(mockHeaderOptions).toHaveBeenCalledWith({
data: { ...appDetails, isActive: false },
onAppActivateOpen,
onAppDeactivateOpen,
onAppDeleteOpen,
});
expect(mockButton).toHaveBeenCalledWith(
expect.objectContaining({
onClick: navigateToApp,
}),
);
expect(mockTopNav).toHaveBeenCalled();
expect(title.container).toHaveTextContent(`${appDetails.name} deactivated`);
});
});

View file

@ -0,0 +1,48 @@
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { AppQuery } from "@dashboard/graphql";
import { AppPaths } from "@dashboard/new-apps/urls";
import { Button } from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage } from "react-intl";
import DeactivatedText from "../DeactivatedText";
import HeaderOptions from "./HeaderOptions";
import messages from "./messages";
interface HeaderProps {
data: AppQuery["app"];
navigateToApp: () => void;
onAppActivateOpen: () => void;
onAppDeactivateOpen: () => void;
onAppDeleteOpen: () => void;
}
const Header: React.FC<HeaderProps> = ({
data,
navigateToApp,
onAppActivateOpen,
onAppDeactivateOpen,
onAppDeleteOpen,
}) => (
<>
<TopNav
href={AppPaths.appListPath}
title={
<>
{data?.name} {!data?.isActive && <DeactivatedText />}
</>
}
>
<Button onClick={navigateToApp} variant="primary" data-tc="open-app">
<FormattedMessage {...messages.openApp} />
</Button>
</TopNav>
<HeaderOptions
data={data}
onAppActivateOpen={onAppActivateOpen}
onAppDeactivateOpen={onAppDeactivateOpen}
onAppDeleteOpen={onAppDeleteOpen}
/>
</>
);
export default Header;

View file

@ -0,0 +1,77 @@
import ExternalLink from "@dashboard/components/ExternalLink";
import Skeleton from "@dashboard/components/Skeleton";
import { AppQuery } from "@dashboard/graphql";
import { buttonMessages } from "@dashboard/intl";
import { ButtonBase } from "@material-ui/core";
import { Box } from "@saleor/macaw-ui/next";
import React from "react";
import SVG from "react-inlinesvg";
import { FormattedMessage } from "react-intl";
import activateIcon from "../../../../assets/images/activate-icon.svg";
import deleteIcon from "../../../../assets/images/delete.svg";
import supportIcon from "../../../../assets/images/support-icon.svg";
import messages from "./messages";
import { useStyles } from "./styles";
interface HeaderOptionsProps {
data: AppQuery["app"];
onAppActivateOpen: () => void;
onAppDeactivateOpen: () => void;
onAppDeleteOpen: () => void;
}
const HeaderOptions: React.FC<HeaderOptionsProps> = ({
data,
onAppActivateOpen,
onAppDeactivateOpen,
onAppDeleteOpen,
}) => {
const classes = useStyles();
if (!data) {
return (
<Box marginX={10}>
<Skeleton />
<div className={classes.hr} />
</Box>
);
}
return (
<Box marginX={10}>
<div className={classes.appHeaderLinks}>
<ExternalLink
className={classes.headerLinkContainer}
href={data.supportUrl || ""}
target="_blank"
>
<SVG src={supportIcon} />
<FormattedMessage {...messages.supportLink} />
</ExternalLink>
<ButtonBase
className={classes.headerLinkContainer}
disableRipple
onClick={data.isActive ? onAppDeactivateOpen : onAppActivateOpen}
>
<SVG src={activateIcon} />
{data?.isActive ? (
<FormattedMessage {...buttonMessages.deactivate} />
) : (
<FormattedMessage {...buttonMessages.activate} />
)}
</ButtonBase>
<ButtonBase
className={classes.headerLinkContainer}
disableRipple
onClick={onAppDeleteOpen}
>
<SVG src={deleteIcon} />
<FormattedMessage {...buttonMessages.delete} />
</ButtonBase>
</div>
<div className={classes.hr} />
</Box>
);
};
export default HeaderOptions;

View file

@ -0,0 +1,47 @@
import CardTitle from "@dashboard/components/CardTitle";
import Skeleton from "@dashboard/components/Skeleton";
import { AppQuery } from "@dashboard/graphql";
import { Card, CardContent, Typography } from "@material-ui/core";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import messages from "./messages";
import { useStyles } from "./styles";
interface PermissionsCardProps {
permissions?: AppQuery["app"]["permissions"];
loading: boolean;
}
const PermissionsCard: React.FC<PermissionsCardProps> = ({
permissions,
loading,
}) => {
const classes = useStyles();
const intl = useIntl();
return (
<Card>
<CardTitle title={intl.formatMessage(messages.appPermissionsTitle)} />
<CardContent>
{!loading ? (
<>
<Typography>
<FormattedMessage {...messages.appPermissionsDescription} />
</Typography>
{!!permissions?.length && (
<ul className={classes.permissionsContainer}>
{permissions?.map(perm => (
<li key={perm.code}>{perm.name}</li>
))}
</ul>
)}
</>
) : (
<Skeleton />
)}
</CardContent>
</Card>
);
};
export default PermissionsCard;

View file

@ -0,0 +1,39 @@
import { defineMessages } from "react-intl";
export default defineMessages({
openApp: {
id: "+FWlRD",
defaultMessage: "Open app",
description: "button",
},
supportLink: {
id: "Nsk5WL",
defaultMessage: "Get support",
description: "link",
},
aboutAppTitle: {
id: "jDIRQV",
defaultMessage: "About this app",
description: "section header",
},
appPermissionsTitle: {
id: "VsGcdP",
defaultMessage: "App permissions",
description: "section header",
},
appPermissionsDescription: {
id: "7oQUMG",
defaultMessage: "This app has permissions to:",
description: "apps about permissions",
},
dataPrivacyTitle: {
id: "a55zOn",
defaultMessage: "Data privacy",
description: "section header",
},
dataPrivacyDescription: {
id: "Go50v2",
defaultMessage: "View this apps privacy policy",
description: "app privacy policy link",
},
});

View file

@ -0,0 +1,65 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
appHeader: {
marginBottom: theme.spacing(3),
},
appHeaderLinks: {
"& img": {
marginRight: theme.spacing(1),
},
alignItems: "center",
display: "flex",
padding: theme.spacing(2, 0),
},
headerLinkContainer: {
"& svg": {
marginRight: theme.spacing(),
},
"& span": {
fontWeight: 500,
},
alignItems: "center",
color: theme.palette.text.primary,
display: "flex",
fontSize: theme.spacing(2),
fontWeight: 500,
lineHeight: 1.2,
marginRight: theme.spacing(3),
padding: 0,
textTransform: "none",
},
hr: {
border: "none",
borderTop: `1px solid ${theme.palette.divider}`,
height: 0,
marginBottom: 0,
marginTop: 0,
width: "100%",
},
linkContainer: {
fontWeight: 500,
marginTop: theme.spacing(1.5),
},
marketplaceContent: {
"& button": {
marginTop: theme.spacing(1),
},
"&:last-child": {
padding: theme.spacing(2, 3, 2, 3),
},
padding: theme.spacing(1),
},
permissionsContainer: {
"& li": {
"&:last-child": {
marginBottom: 0,
},
marginBottom: theme.spacing(1),
},
paddingLeft: theme.spacing(2),
},
}),
{ name: "AppDetailsPage" },
);

View file

@ -1,3 +1,4 @@
import { commonStatusMessages } from "@dashboard/intl";
import { Typography } from "@material-ui/core"; import { Typography } from "@material-ui/core";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
@ -8,11 +9,7 @@ export const DeactivatedText: React.FC<{}> = () => {
const classes = useStyles({}); const classes = useStyles({});
return ( return (
<Typography className={classes.root}> <Typography className={classes.root}>
<FormattedMessage <FormattedMessage {...commonStatusMessages.deactivated} />
id="5+Xcrz"
defaultMessage="Deactivated"
description="app deactivated"
/>
</Typography> </Typography>
); );
}; };

View file

@ -2,7 +2,6 @@ import {
AppDetailsUrlQueryParams, AppDetailsUrlQueryParams,
AppInstallUrlQueryParams, AppInstallUrlQueryParams,
} from "@dashboard/apps/urls"; } from "@dashboard/apps/urls";
import AppDetailsView from "@dashboard/apps/views/AppDetails";
import AppInstallView from "@dashboard/apps/views/AppInstall"; import AppInstallView from "@dashboard/apps/views/AppInstall";
import { sectionNames } from "@dashboard/intl"; import { sectionNames } from "@dashboard/intl";
import { parse as parseQs } from "qs"; import { parse as parseQs } from "qs";
@ -12,6 +11,7 @@ import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { WindowTitle } from "../components/WindowTitle"; import { WindowTitle } from "../components/WindowTitle";
import { AppListUrlQueryParams, AppPaths } from "./urls"; import { AppListUrlQueryParams, AppPaths } from "./urls";
import AppDetailsView from "./views/AppDetails";
import AppListView from "./views/AppList"; import AppListView from "./views/AppList";
import AppView from "./views/AppView"; import AppView from "./views/AppView";

View file

@ -34,6 +34,16 @@ export const appMessages = defineMessages({
defaultMessage: "Failed to fetch app settings", defaultMessage: "Failed to fetch app settings",
description: "app settings error", description: "app settings error",
}, },
appActivated: {
id: "D/+84n",
defaultMessage: "App activated",
description: "snackbar text",
},
appDeactivated: {
id: "USO8PB",
defaultMessage: "App deactivated",
description: "snackbar text",
},
}); });
export const appInstallationStatusMessages = defineMessages({ export const appInstallationStatusMessages = defineMessages({

View file

@ -1,6 +1,4 @@
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import AppDeleteDialog from "@dashboard/apps/components/AppDeleteDialog";
import { appMessages } from "@dashboard/apps/messages";
import { EXTENSION_LIST_QUERY } from "@dashboard/apps/queries"; import { EXTENSION_LIST_QUERY } from "@dashboard/apps/queries";
import NotFoundPage from "@dashboard/components/NotFoundPage"; import NotFoundPage from "@dashboard/components/NotFoundPage";
import { import {
@ -11,6 +9,8 @@ import {
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import useNavigator from "@dashboard/hooks/useNavigator"; import useNavigator from "@dashboard/hooks/useNavigator";
import useNotifier from "@dashboard/hooks/useNotifier"; import useNotifier from "@dashboard/hooks/useNotifier";
import AppDeleteDialog from "@dashboard/new-apps/components/AppDeleteDialog";
import { appMessages } from "@dashboard/new-apps/messages";
import getAppErrorMessage from "@dashboard/utils/errors/app"; import getAppErrorMessage from "@dashboard/utils/errors/app";
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
import React from "react"; import React from "react";
@ -20,11 +20,10 @@ import AppActivateDialog from "../../components/AppActivateDialog";
import AppDeactivateDialog from "../../components/AppDeactivateDialog"; import AppDeactivateDialog from "../../components/AppDeactivateDialog";
import AppDetailsPage from "../../components/AppDetailsPage"; import AppDetailsPage from "../../components/AppDetailsPage";
import { import {
appDetailsUrl,
AppDetailsUrlDialog, AppDetailsUrlDialog,
AppDetailsUrlQueryParams, AppDetailsUrlQueryParams,
appsListPath, AppPaths,
appUrl, AppUrls,
} from "../../urls"; } from "../../urls";
import { messages } from "./messages"; import { messages } from "./messages";
@ -110,7 +109,7 @@ export const AppDetails: React.FC<AppDetailsProps> = ({ id, params }) => {
refetch(); refetch();
refetchExtensionList(); refetchExtensionList();
removeAppNotify(); removeAppNotify();
navigate(appsListPath); navigate(AppPaths.appListPath);
} }
}, },
}); });
@ -118,14 +117,14 @@ export const AppDetails: React.FC<AppDetailsProps> = ({ id, params }) => {
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
AppDetailsUrlDialog, AppDetailsUrlDialog,
AppDetailsUrlQueryParams AppDetailsUrlQueryParams
>(navigate, params => appDetailsUrl(id, params), params); >(navigate, params => AppUrls.resolveAppDetailsUrl(id, params), params);
const handleActivateConfirm = () => activateApp(mutationOpts); const handleActivateConfirm = () => activateApp(mutationOpts);
const handleDeactivateConfirm = () => deactivateApp(mutationOpts); const handleDeactivateConfirm = () => deactivateApp(mutationOpts);
const handleRemoveConfirm = () => deleteApp(mutationOpts); const handleRemoveConfirm = () => deleteApp(mutationOpts);
if (!appExists) { if (!appExists) {
return <NotFoundPage backHref={appsListPath} />; return <NotFoundPage backHref={AppPaths.appListPath} />;
} }
return ( return (
@ -155,7 +154,7 @@ export const AppDetails: React.FC<AppDetailsProps> = ({ id, params }) => {
<AppDetailsPage <AppDetailsPage
data={data?.app || null} data={data?.app || null}
loading={loading} loading={loading}
navigateToApp={() => navigate(appUrl(id))} 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")}