diff --git a/.changeset/orange-falcons-judge.md b/.changeset/orange-falcons-judge.md new file mode 100644 index 000000000..95a0bd102 --- /dev/null +++ b/.changeset/orange-falcons-judge.md @@ -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. diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index b76235a71..92a9d33ab 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -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" diff --git a/src/apps/components/AppDetailsPage/AboutCard.tsx b/src/apps/components/AppDetailsPage/AboutCard.tsx index e61d91a84..86b1537f0 100644 --- a/src/apps/components/AppDetailsPage/AboutCard.tsx +++ b/src/apps/components/AppDetailsPage/AboutCard.tsx @@ -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 = ({ aboutApp, loading }) => { +export const AboutCard: React.FC = ({ + aboutApp, + loading, + ...boxProps +}) => { const intl = useIntl(); + const renderContent = () => { + if (loading) { + return ; + } + + if (aboutApp) { + return ; + } + + if (!aboutApp) { + return {intl.formatMessage(messages.noAboutApp)}; + } + + throw new Error('Leaking "if" statement, should never happen'); + }; + return ( - - - - {!loading ? : } - - + + + {intl.formatMessage(messages.aboutAppTitle)} + + {renderContent()} + ); }; -export default AboutCard; diff --git a/src/apps/components/AppDetailsPage/AppDetailsPage.stories.tsx b/src/apps/components/AppDetailsPage/AppDetailsPage.stories.tsx index 5033092a1..548a1caf2 100644 --- a/src/apps/components/AppDetailsPage/AppDetailsPage.stories.tsx +++ b/src/apps/components/AppDetailsPage/AppDetailsPage.stories.tsx @@ -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, diff --git a/src/apps/components/AppDetailsPage/AppDetailsPage.test.tsx b/src/apps/components/AppDetailsPage/AppDetailsPage.test.tsx index 50e0a22e0..585e3b12d 100644 --- a/src/apps/components/AppDetailsPage/AppDetailsPage.test.tsx +++ b/src/apps/components/AppDetailsPage/AppDetailsPage.test.tsx @@ -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, + }), + ); }); }); diff --git a/src/apps/components/AppDetailsPage/AppDetailsPage.tsx b/src/apps/components/AppDetailsPage/AppDetailsPage.tsx index e94c15f7c..765a600d7 100644 --- a/src/apps/components/AppDetailsPage/AppDetailsPage.tsx +++ b/src/apps/components/AppDetailsPage/AppDetailsPage.tsx @@ -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 = ({ } return ( - <> + ( + + Error, please refresh the page + + )} + >
- - - - + + - - + ); }; AppDetailsPage.displayName = "AppDetailsPage"; -export default AppDetailsPage; diff --git a/src/apps/components/AppDetailsPage/DataPrivacyCard.tsx b/src/apps/components/AppDetailsPage/DataPrivacyCard.tsx index 428c84038..e0dc4a2fa 100644 --- a/src/apps/components/AppDetailsPage/DataPrivacyCard.tsx +++ b/src/apps/components/AppDetailsPage/DataPrivacyCard.tsx @@ -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 = ({ +export const DataPrivacyCard: React.FC = ({ dataPrivacyUrl, loading, + ...boxProps }) => { - const classes = useStyles(); const intl = useIntl(); if (!dataPrivacyUrl && !loading) { return null; } + const renderContent = () => { + if (loading) { + return ; + } + + if (dataPrivacyUrl) { + return ( + + + + ); + } + + if (!dataPrivacyUrl) { + return {intl.formatMessage(messages.noDataPrivacyPage)}; + } + + throw new Error('Leaking "if" statement, should never happen'); + }; + return ( - - - - {!loading ? ( - - - - ) : ( - - )} - - + + + {intl.formatMessage(messages.dataPrivacyTitle)} + + {renderContent()} + ); }; -export default DataPrivacyCard; diff --git a/src/apps/components/AppDetailsPage/PermissionsCard.tsx b/src/apps/components/AppDetailsPage/PermissionsCard.tsx index 488f06ed8..bd875ddcf 100644 --- a/src/apps/components/AppDetailsPage/PermissionsCard.tsx +++ b/src/apps/components/AppDetailsPage/PermissionsCard.tsx @@ -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 = ({ +export const PermissionsCard: React.FC = ({ permissions, loading, + ...boxProps }) => { - const classes = useStyles(); const intl = useIntl(); + const renderContent = () => { + if (loading) { + return ; + } + + if (permissions && permissions.length === 0) { + return {intl.formatMessage(messages.appNoPermissions)}; + } + + if (permissions && permissions.length > 0) { + return ( + <> + + + + + {permissions?.map(perm => ( + + {perm.name} + + ))} + + + ); + } + + throw new Error('Leaking "if" statement, should never happen'); + }; + return ( - - - - {!loading ? ( - <> - - - - {!!permissions?.length && ( -
    - {permissions?.map(perm => ( -
  • {perm.name}
  • - ))} -
- )} - - ) : ( - - )} -
-
+ + + {intl.formatMessage(messages.appPermissionsTitle)} + + {renderContent()} + ); }; -export default PermissionsCard; diff --git a/src/apps/components/AppDetailsPage/index.ts b/src/apps/components/AppDetailsPage/index.ts index 1e11990da..6d198b40b 100644 --- a/src/apps/components/AppDetailsPage/index.ts +++ b/src/apps/components/AppDetailsPage/index.ts @@ -1,2 +1 @@ export * from "./AppDetailsPage"; -export { default } from "./AppDetailsPage"; diff --git a/src/apps/components/AppDetailsPage/messages.ts b/src/apps/components/AppDetailsPage/messages.ts index 585bc361e..8a8c411b1 100644 --- a/src/apps/components/AppDetailsPage/messages.ts +++ b/src/apps/components/AppDetailsPage/messages.ts @@ -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.", + }, }); diff --git a/src/apps/components/AppDetailsPage/styles.ts b/src/apps/components/AppDetailsPage/styles.ts index c850307fa..00d8164af 100644 --- a/src/apps/components/AppDetailsPage/styles.ts +++ b/src/apps/components/AppDetailsPage/styles.ts @@ -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" }, ); diff --git a/src/apps/views/AppManageView/AppManageView.tsx b/src/apps/views/AppManageView/AppManageView.tsx index c106a0606..59f122aae 100644 --- a/src/apps/views/AppManageView/AppManageView.tsx +++ b/src/apps/views/AppManageView/AppManageView.tsx @@ -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,