Refactor AppDetailsPage to Macaw Next (#3818)

* Refactor AppDetailsPage to Macaw Next

* Update locales

* CR fixes

* Add error boundary
This commit is contained in:
Lukasz Ostrowski 2023-06-29 15:50:13 +02:00 committed by GitHub
parent c12a42932d
commit a1482cc453
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 203 additions and 128 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---
Refactored Manage App screen to use Macaw/next. Added missing empty-state messages, like missing permissions or app description.

View file

@ -1927,6 +1927,10 @@
"context": "dialog content",
"string": "Which address would you like to use as shipping address for selected customer:"
},
"CHoZ8S": {
"context": "app privacy policy link",
"string": "View this apps privacy policy."
},
"CJEIRC": {
"string": "Product export has finished and was sent to your email address."
},
@ -2597,10 +2601,6 @@
"context": "button",
"string": "Unassign"
},
"Go50v2": {
"context": "app privacy policy link",
"string": "View this apps privacy policy"
},
"GpqEl5": {
"context": "shipping method description",
"string": "Description"
@ -5444,6 +5444,9 @@
"b+jcaN": {
"string": "There are still fulfillments created for this order. Cancel the fulfillments first before you cancel the order."
},
"b088Xv": {
"string": "App doesn't provide a description."
},
"b1t9bM": {
"context": "empty headers text",
"string": "No custom request headers created for this webhook. Use the button below to add new custom request header."
@ -5946,6 +5949,9 @@
"context": "attributes, section header",
"string": "Variant Attributes"
},
"f3hf+w": {
"string": "App doesn't provide a privacy policy."
},
"f91E8b": {
"context": "app repository",
"string": "Repository"
@ -8231,6 +8237,9 @@
"context": "order refund amount",
"string": "Proposed refund amount"
},
"wDYozn": {
"string": "App doesn't have any permissions granted."
},
"wHdMAX": {
"context": "sale value, header",
"string": "Value"

View file

@ -1,27 +1,45 @@
import CardTitle from "@dashboard/components/CardTitle";
import Skeleton from "@dashboard/components/Skeleton";
import { Card, CardContent } from "@material-ui/core";
import { Box, BoxProps, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
import ReactMarkdown from "react-markdown";
import messages from "./messages";
interface AboutCardProps {
type AboutCardProps = {
aboutApp?: string | null;
loading: boolean;
}
} & BoxProps;
const AboutCard: React.FC<AboutCardProps> = ({ aboutApp, loading }) => {
export const AboutCard: React.FC<AboutCardProps> = ({
aboutApp,
loading,
...boxProps
}) => {
const intl = useIntl();
const renderContent = () => {
if (loading) {
return <Skeleton />;
}
if (aboutApp) {
return <ReactMarkdown source={aboutApp} />;
}
if (!aboutApp) {
return <Text>{intl.formatMessage(messages.noAboutApp)}</Text>;
}
throw new Error('Leaking "if" statement, should never happen');
};
return (
<Card>
<CardTitle title={intl.formatMessage(messages.aboutAppTitle)} />
<CardContent>
{!loading ? <ReactMarkdown source={aboutApp ?? ""} /> : <Skeleton />}
</CardContent>
</Card>
<Box {...boxProps}>
<Text variant={"heading"} as={"h2"} marginBottom={4}>
{intl.formatMessage(messages.aboutAppTitle)}
</Text>
<Box>{renderContent()}</Box>
</Box>
);
};
export default AboutCard;

View file

@ -1,7 +1,7 @@
import { Meta, StoryObj } from "@storybook/react";
import { appDetails } from "../../fixtures";
import AppDetailsPage, { AppDetailsPageProps } from "./AppDetailsPage";
import { AppDetailsPage, AppDetailsPageProps } from "./AppDetailsPage";
const props: AppDetailsPageProps = {
data: appDetails,

View file

@ -3,7 +3,7 @@ import { render } from "@testing-library/react";
import React from "react";
import { appDetails } from "../../fixtures";
import AppDetailsPage from "./AppDetailsPage";
import { AppDetailsPage } from "./AppDetailsPage";
const mockHeader = jest.fn();
jest.mock("./Header", () => props => {
@ -12,22 +12,29 @@ jest.mock("./Header", () => props => {
});
const mockAboutCard = jest.fn();
jest.mock("./AboutCard", () => props => {
jest.mock("./AboutCard", () => ({
AboutCard: props => {
mockAboutCard(props);
return <></>;
});
},
}));
const mockPermissionsCard = jest.fn();
jest.mock("./PermissionsCard", () => props => {
jest.mock("./PermissionsCard", () => ({
PermissionsCard: props => {
mockPermissionsCard(props);
return <></>;
});
},
}));
const mockDataPrivacyCard = jest.fn();
jest.mock("./DataPrivacyCard", () => props => {
jest.mock("./DataPrivacyCard", () => ({
DataPrivacyCard: props => {
mockDataPrivacyCard(props);
return <></>;
});
},
}));
beforeEach(() => {
mockHeader.mockClear();
@ -36,6 +43,9 @@ beforeEach(() => {
mockDataPrivacyCard.mockClear();
});
/**
* TODO Rewrite tests to actually render the tree
*/
describe("Apps AppDetailsPage", () => {
it("displays app details when app data passed", () => {
// Arrange
@ -61,17 +71,23 @@ describe("Apps AppDetailsPage", () => {
onAppDeactivateOpen,
onAppDeleteOpen,
});
expect(mockAboutCard).toHaveBeenCalledWith({
expect(mockAboutCard).toHaveBeenCalledWith(
expect.objectContaining({
aboutApp: appDetails.aboutApp,
loading: false,
});
expect(mockPermissionsCard).toHaveBeenCalledWith({
}),
);
expect(mockPermissionsCard).toHaveBeenCalledWith(
expect.objectContaining({
permissions: appDetails.permissions,
loading: false,
});
expect(mockDataPrivacyCard).toHaveBeenCalledWith({
}),
);
expect(mockDataPrivacyCard).toHaveBeenCalledWith(
expect.objectContaining({
dataPrivacyUrl: appDetails.dataPrivacyUrl,
loading: false,
});
}),
);
});
});

View file

@ -1,11 +1,13 @@
import CardSpacer from "@dashboard/components/CardSpacer";
import { AppQuery } from "@dashboard/graphql";
import errorTracker from "@dashboard/services/errorTracking";
import { Box, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { ErrorBoundary } from "react-error-boundary";
import AboutCard from "./AboutCard";
import DataPrivacyCard from "./DataPrivacyCard";
import { AboutCard } from "./AboutCard";
import { DataPrivacyCard } from "./DataPrivacyCard";
import Header from "./Header";
import PermissionsCard from "./PermissionsCard";
import { PermissionsCard } from "./PermissionsCard";
export interface AppDetailsPageProps {
loading: boolean;
@ -27,25 +29,33 @@ export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
}
return (
<>
<ErrorBoundary
onError={errorTracker.captureException}
fallbackRender={() => (
<Box padding={4}>
<Text>Error, please refresh the page</Text>
</Box>
)}
>
<Header
data={data}
onAppActivateOpen={onAppActivateOpen}
onAppDeactivateOpen={onAppDeactivateOpen}
onAppDeleteOpen={onAppDeleteOpen}
/>
<AboutCard aboutApp={data?.aboutApp} loading={loading} />
<CardSpacer />
<PermissionsCard permissions={data?.permissions} loading={loading} />
<CardSpacer />
<AboutCard margin={6} aboutApp={data?.aboutApp} loading={loading} />
<PermissionsCard
margin={6}
permissions={data?.permissions}
loading={loading}
/>
<DataPrivacyCard
margin={6}
dataPrivacyUrl={data?.dataPrivacyUrl}
loading={loading}
/>
<CardSpacer />
</>
</ErrorBoundary>
);
};
AppDetailsPage.displayName = "AppDetailsPage";
export default AppDetailsPage;

View file

@ -1,47 +1,53 @@
// @ts-strict-ignore
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 { Box, BoxProps, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import messages from "./messages";
import { useStyles } from "./styles";
interface DataPrivacyCardProps {
type DataPrivacyCardProps = {
dataPrivacyUrl?: string | null;
loading: boolean;
}
} & BoxProps;
const DataPrivacyCard: React.FC<DataPrivacyCardProps> = ({
export const DataPrivacyCard: React.FC<DataPrivacyCardProps> = ({
dataPrivacyUrl,
loading,
...boxProps
}) => {
const classes = useStyles();
const intl = useIntl();
if (!dataPrivacyUrl && !loading) {
return null;
}
const renderContent = () => {
if (loading) {
return <Skeleton />;
}
if (dataPrivacyUrl) {
return (
<Card>
<CardTitle title={intl.formatMessage(messages.dataPrivacyTitle)} />
<CardContent>
{!loading ? (
<ExternalLink
className={classes.linkContainer}
href={dataPrivacyUrl}
target="_blank"
>
<ExternalLink href={dataPrivacyUrl} target="_blank">
<FormattedMessage {...messages.dataPrivacyDescription} />
</ExternalLink>
) : (
<Skeleton />
)}
</CardContent>
</Card>
);
}
if (!dataPrivacyUrl) {
return <Text>{intl.formatMessage(messages.noDataPrivacyPage)}</Text>;
}
throw new Error('Leaking "if" statement, should never happen');
};
return (
<Box {...boxProps}>
<Text variant={"heading"} marginBottom={4} as={"h2"}>
{intl.formatMessage(messages.dataPrivacyTitle)}
</Text>
<Box>{renderContent()}</Box>
</Box>
);
};
export default DataPrivacyCard;

View file

@ -1,48 +1,61 @@
// @ts-strict-ignore
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 { PermissionEnum } from "@dashboard/graphql";
import { Box, BoxProps, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import messages from "./messages";
import { useStyles } from "./styles";
interface PermissionsCardProps {
permissions?: AppQuery["app"]["permissions"];
type PermissionsCardProps = {
permissions: Array<{
name: string;
code: PermissionEnum;
}> | null;
loading: boolean;
}
} & BoxProps;
const PermissionsCard: React.FC<PermissionsCardProps> = ({
export const PermissionsCard: React.FC<PermissionsCardProps> = ({
permissions,
loading,
...boxProps
}) => {
const classes = useStyles();
const intl = useIntl();
const renderContent = () => {
if (loading) {
return <Skeleton />;
}
if (permissions && permissions.length === 0) {
return <Text>{intl.formatMessage(messages.appNoPermissions)}</Text>;
}
if (permissions && permissions.length > 0) {
return (
<Card>
<CardTitle title={intl.formatMessage(messages.appPermissionsTitle)} />
<CardContent>
{!loading ? (
<>
<Typography>
<Text as={"p"} marginBottom={4}>
<FormattedMessage {...messages.appPermissionsDescription} />
</Typography>
{!!permissions?.length && (
<ul className={classes.permissionsContainer}>
</Text>
<Box as={"ul"}>
{permissions?.map(perm => (
<li key={perm.code}>{perm.name}</li>
<Box as={"li"} paddingX={4} paddingY={2} key={perm.code}>
<Text>{perm.name}</Text>
</Box>
))}
</ul>
)}
</Box>
</>
) : (
<Skeleton />
)}
</CardContent>
</Card>
);
}
throw new Error('Leaking "if" statement, should never happen');
};
return (
<Box {...boxProps}>
<Text variant={"heading"} marginBottom={4} as={"h2"}>
{intl.formatMessage(messages.appPermissionsTitle)}
</Text>
<Box>{renderContent()}</Box>
</Box>
);
};
export default PermissionsCard;

View file

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

View file

@ -16,6 +16,10 @@ export default defineMessages({
defaultMessage: "App permissions",
description: "section header",
},
appNoPermissions: {
defaultMessage: "App doesn't have any permissions granted.",
id: "wDYozn",
},
appPermissionsDescription: {
id: "7oQUMG",
defaultMessage: "This app has permissions to:",
@ -27,8 +31,16 @@ export default defineMessages({
description: "section header",
},
dataPrivacyDescription: {
id: "Go50v2",
defaultMessage: "View this apps privacy policy",
id: "CHoZ8S",
defaultMessage: "View this apps privacy policy.",
description: "app privacy policy link",
},
noDataPrivacyPage: {
id: "f3hf+w",
defaultMessage: "App doesn't provide a privacy policy.",
},
noAboutApp: {
id: "b088Xv",
defaultMessage: "App doesn't provide a description.",
},
});

View file

@ -38,10 +38,6 @@ export const useStyles = makeStyles(
marginTop: 0,
width: "100%",
},
linkContainer: {
fontWeight: 500,
marginTop: theme.spacing(1.5),
},
marketplaceContent: {
"& button": {
marginTop: theme.spacing(1),
@ -51,15 +47,6 @@ export const useStyles = makeStyles(
},
padding: theme.spacing(1),
},
permissionsContainer: {
"& li": {
"&:last-child": {
marginBottom: 0,
},
marginBottom: theme.spacing(1),
},
paddingLeft: theme.spacing(2),
},
}),
{ name: "AppDetailsPage" },
);

View file

@ -18,7 +18,7 @@ import { useIntl } from "react-intl";
import AppActivateDialog from "../../components/AppActivateDialog";
import AppDeactivateDialog from "../../components/AppDeactivateDialog";
import AppDetailsPage from "../../components/AppDetailsPage";
import { AppDetailsPage } from "../../components/AppDetailsPage";
import {
AppDetailsUrlDialog,
AppDetailsUrlQueryParams,