Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor AppDetailsPage to Macaw Next #3818

Merged
merged 4 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/orange-falcons-judge.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 13 additions & 4 deletions locale/defaultMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
},
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
42 changes: 30 additions & 12 deletions src/apps/components/AppDetailsPage/AboutCard.tsx
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
66 changes: 41 additions & 25 deletions src/apps/components/AppDetailsPage/AppDetailsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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();
Expand All @@ -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
Expand All @@ -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,
}),
);
});
});
34 changes: 22 additions & 12 deletions src/apps/components/AppDetailsPage/AppDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
56 changes: 31 additions & 25 deletions src/apps/components/AppDetailsPage/DataPrivacyCard.tsx
Original file line number Diff line number Diff line change
@@ -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;
Loading