Refactor AppDetailsPage to Macaw Next (#3818)
* Refactor AppDetailsPage to Macaw Next * Update locales * CR fixes * Add error boundary
This commit is contained in:
parent
c12a42932d
commit
a1482cc453
12 changed files with 203 additions and 128 deletions
5
.changeset/orange-falcons-judge.md
Normal file
5
.changeset/orange-falcons-judge.md
Normal 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.
|
|
@ -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 app’s 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 app’s 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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 => {
|
||||
mockAboutCard(props);
|
||||
return <></>;
|
||||
});
|
||||
jest.mock("./AboutCard", () => ({
|
||||
AboutCard: props => {
|
||||
mockAboutCard(props);
|
||||
return <></>;
|
||||
},
|
||||
}));
|
||||
|
||||
const mockPermissionsCard = jest.fn();
|
||||
jest.mock("./PermissionsCard", () => props => {
|
||||
mockPermissionsCard(props);
|
||||
return <></>;
|
||||
});
|
||||
|
||||
jest.mock("./PermissionsCard", () => ({
|
||||
PermissionsCard: props => {
|
||||
mockPermissionsCard(props);
|
||||
return <></>;
|
||||
},
|
||||
}));
|
||||
|
||||
const mockDataPrivacyCard = jest.fn();
|
||||
jest.mock("./DataPrivacyCard", () => props => {
|
||||
mockDataPrivacyCard(props);
|
||||
return <></>;
|
||||
});
|
||||
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({
|
||||
aboutApp: appDetails.aboutApp,
|
||||
loading: false,
|
||||
});
|
||||
expect(mockPermissionsCard).toHaveBeenCalledWith({
|
||||
permissions: appDetails.permissions,
|
||||
loading: false,
|
||||
});
|
||||
expect(mockDataPrivacyCard).toHaveBeenCalledWith({
|
||||
dataPrivacyUrl: appDetails.dataPrivacyUrl,
|
||||
loading: false,
|
||||
});
|
||||
expect(mockAboutCard).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
aboutApp: appDetails.aboutApp,
|
||||
loading: false,
|
||||
}),
|
||||
);
|
||||
expect(mockPermissionsCard).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
permissions: appDetails.permissions,
|
||||
loading: false,
|
||||
}),
|
||||
);
|
||||
expect(mockDataPrivacyCard).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dataPrivacyUrl: appDetails.dataPrivacyUrl,
|
||||
loading: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
<ExternalLink href={dataPrivacyUrl} target="_blank">
|
||||
<FormattedMessage {...messages.dataPrivacyDescription} />
|
||||
</ExternalLink>
|
||||
);
|
||||
}
|
||||
|
||||
if (!dataPrivacyUrl) {
|
||||
return <Text>{intl.formatMessage(messages.noDataPrivacyPage)}</Text>;
|
||||
}
|
||||
|
||||
throw new Error('Leaking "if" statement, should never happen');
|
||||
};
|
||||
|
||||
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>
|
||||
<Box {...boxProps}>
|
||||
<Text variant={"heading"} marginBottom={4} as={"h2"}>
|
||||
{intl.formatMessage(messages.dataPrivacyTitle)}
|
||||
</Text>
|
||||
<Box>{renderContent()}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default DataPrivacyCard;
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<Text as={"p"} marginBottom={4}>
|
||||
<FormattedMessage {...messages.appPermissionsDescription} />
|
||||
</Text>
|
||||
<Box as={"ul"}>
|
||||
{permissions?.map(perm => (
|
||||
<Box as={"li"} paddingX={4} paddingY={2} key={perm.code}>
|
||||
<Text>{perm.name}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('Leaking "if" statement, should never happen');
|
||||
};
|
||||
|
||||
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>
|
||||
<Box {...boxProps}>
|
||||
<Text variant={"heading"} marginBottom={4} as={"h2"}>
|
||||
{intl.formatMessage(messages.appPermissionsTitle)}
|
||||
</Text>
|
||||
<Box>{renderContent()}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default PermissionsCard;
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export * from "./AppDetailsPage";
|
||||
export { default } from "./AppDetailsPage";
|
||||
|
|
|
@ -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 app’s privacy policy",
|
||||
id: "CHoZ8S",
|
||||
defaultMessage: "View this app’s 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.",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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" },
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue