Migrate Home page to new macaw (#3737)

This commit is contained in:
Paweł Chyła 2023-07-18 12:01:15 +02:00 committed by GitHub
parent 1c7486818b
commit b66af99477
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 606 additions and 620 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": minor
---
Migrate Home page to new macaw components

View file

@ -427,10 +427,6 @@
"context": "section header", "context": "section header",
"string": "Refunded Amount" "string": "Refunded Amount"
}, },
"0opVvi": {
"context": "number of ordered products",
"string": "{amount, plural,one {One ordered}other {{amount} Ordered}}"
},
"0qg33z": { "0qg33z": {
"context": "table column header", "context": "table column header",
"string": "Return" "string": "Return"
@ -1053,9 +1049,6 @@
"context": "vat not included in order price", "context": "vat not included in order price",
"string": "does not apply" "string": "does not apply"
}, },
"5LRkEs": {
"string": "The new dashboard and the GraphQL API are preview-quality software."
},
"5ObBlW": { "5ObBlW": {
"string": "Dark Mode" "string": "Dark Mode"
}, },
@ -1198,10 +1191,6 @@
"context": "button", "context": "button",
"string": "Create page type" "string": "Create page type"
}, },
"6L6Fy2": {
"context": "header",
"string": "Disclaimer"
},
"6QjMei": { "6QjMei": {
"string": "Preorder end time needs to be set in the future" "string": "Preorder end time needs to be set in the future"
}, },
@ -2500,9 +2489,6 @@
"context": "tooltip", "context": "tooltip",
"string": "Checkout reservation time threshold is enabled in settings." "string": "Checkout reservation time threshold is enabled in settings."
}, },
"G7mu0y": {
"string": "The GraphQL API is beta quality. It is not fully optimized and some mutations or queries may be missing."
},
"GAmGog": { "GAmGog": {
"context": "value input label", "context": "value input label",
"string": "Discount value" "string": "Discount value"
@ -5859,6 +5845,10 @@
"context": "current balance filter label", "context": "current balance filter label",
"string": "Current balance" "string": "Current balance"
}, },
"e08xWz": {
"context": "header",
"string": "Top products"
},
"e0RKe+": { "e0RKe+": {
"context": "generate invoice button", "context": "generate invoice button",
"string": "Generate" "string": "Generate"
@ -7041,6 +7031,10 @@
"context": "order history message", "context": "order history message",
"string": "Fulfilled {quantity} items" "string": "Fulfilled {quantity} items"
}, },
"nII/qB": {
"context": "number of ordered products",
"string": "{amount, plural,one {One ordered}other {{amount} ordered}}"
},
"nIrjSR": { "nIrjSR": {
"context": "section header", "context": "section header",
"string": "Ongoing Installations" "string": "Ongoing Installations"
@ -7616,10 +7610,6 @@
"context": "order status", "context": "order status",
"string": "Ready to capture" "string": "Ready to capture"
}, },
"rr8fyf": {
"context": "header",
"string": "Top Products"
},
"rs815i": { "rs815i": {
"context": "text field label", "context": "text field label",
"string": "Group name" "string": "Group name"

View file

@ -4,7 +4,7 @@ import {
savebarHeight, savebarHeight,
} from "@dashboard/components/AppLayout/consts"; } from "@dashboard/components/AppLayout/consts";
import { Box, Sprinkles } from "@saleor/macaw-ui/next"; import { Box, Sprinkles } from "@saleor/macaw-ui/next";
import React from "react"; import React, { useMemo } from "react";
interface DetailPageLayoutProps { interface DetailPageLayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -19,17 +19,39 @@ export const RootLayout: React.FC<DetailPageLayoutProps> = ({
children, children,
gridTemplateColumns = 12, gridTemplateColumns = 12,
withSavebar = true, withSavebar = true,
}) => ( }) => {
<Box const gridTemplateColumnsValue =
display="grid" useMemo((): Sprinkles["gridTemplateColumns"] => {
margin="auto" if (gridTemplateColumns instanceof Object) {
gridTemplateColumns={gridTemplateColumns} return {
__gridTemplateRows="auto 1fr" mobile: gridTemplateColumns.mobile ?? 1,
__maxWidth={contentMaxWidth} ...gridTemplateColumns,
__height={ };
withSavebar ? contentWithSidebarHeight : contentWithoutSidebarHeight }
}
> return {
{children} mobile: 1,
</Box> desktop: gridTemplateColumns,
); };
}, [gridTemplateColumns]);
const heightValue = useMemo(() => {
return withSavebar ? contentWithSidebarHeight : contentWithoutSidebarHeight;
}, [withSavebar]);
return (
<Box
// TODO: Use custom value media query when it will be ready
// https://github.com/saleor/macaw-ui/issues/498
className="mobile-full-height"
display="grid"
margin="auto"
gridTemplateColumns={gridTemplateColumnsValue}
__gridTemplateRows="auto 1fr"
__maxWidth={contentMaxWidth}
__height={heightValue}
>
{children}
</Box>
);
};

View file

@ -1,104 +1,84 @@
// @ts-strict-ignore import { DashboardCard } from "@dashboard/components/Card";
import CardTitle from "@dashboard/components/CardTitle";
import { DateTime } from "@dashboard/components/Date"; import { DateTime } from "@dashboard/components/Date";
import Skeleton from "@dashboard/components/Skeleton"; import Skeleton from "@dashboard/components/Skeleton";
import { HomeQuery } from "@dashboard/graphql"; import { Activities } from "@dashboard/home/types";
import { RelayToFlat } from "@dashboard/types"; import { Box, List, Text, useTheme } from "@saleor/macaw-ui/next";
import {
Card,
CardContent,
List,
ListItem,
ListItemText,
Typography,
} from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { renderCollection } from "../../../misc"; import { renderCollection } from "../../../misc";
import { getActivityMessage } from "./activityMessages"; import { getActivityMessage } from "./activityMessages";
const useStyles = makeStyles(
{
loadingProducts: {
paddingBottom: "10px",
paddingTop: "10px",
},
noProducts: {
paddingBottom: "16px",
paddingTop: "16px",
},
listItem: {
paddingLeft: 0,
},
},
{ name: "HomeActivityCard" },
);
interface HomeActivityCardProps { interface HomeActivityCardProps {
activities: RelayToFlat<HomeQuery["activities"]>; activities: Activities;
testId?: string; testId?: string;
} }
const HomeActivityCard: React.FC<HomeActivityCardProps> = props => { export const HomeActivityCard = ({
const { activities, testId } = props; activities,
const classes = useStyles(props); testId,
}: HomeActivityCardProps) => {
const intl = useIntl(); const intl = useIntl();
const { themeValues } = useTheme();
return ( return (
<Card data-test-id={testId}> <DashboardCard data-test-id={testId}>
<CardTitle <DashboardCard.Title>
title={intl.formatMessage({ {intl.formatMessage({
id: "BXkF8Z", id: "BXkF8Z",
defaultMessage: "Activity", defaultMessage: "Activity",
description: "header", description: "header",
})} })}
/> </DashboardCard.Title>
<CardContent> <DashboardCard.Content>
<List dense> <List>
{renderCollection( {renderCollection(
activities, activities,
(activity, activityId) => ( (activity, activityId) => (
<ListItem key={activityId} className={classes.listItem}> <List.Item
key={activityId}
flexDirection="column"
alignItems="flex-start"
cursor="auto"
paddingY={1}
paddingX={6}
__marginLeft={"-" + themeValues.spacing[6]}
__marginRight={"-" + themeValues.spacing[6]}
marginBottom={3}
>
{activity ? ( {activity ? (
<ListItemText <>
primary={ <Text variant="body" size="small">
<Typography> {getActivityMessage(activity, intl)}
{getActivityMessage(activity, intl)} </Text>
</Typography> <Text
} variant="body"
secondary={<DateTime date={activity.date} plain />} size="small"
/> color="textNeutralSubdued"
>
<DateTime date={activity.date} plain />
</Text>
</>
) : ( ) : (
<ListItemText className={classes.loadingProducts}> <Box paddingY={4}>
<Typography> <Skeleton />
<Skeleton /> </Box>
</Typography>
</ListItemText>
)} )}
</ListItem> </List.Item>
), ),
() => ( () => (
<ListItem className={classes.noProducts}> <Box paddingY={4}>
<ListItemText <Text variant="body" size="small">
primary={ <FormattedMessage
<Typography> id="wWTUrM"
<FormattedMessage defaultMessage="No activities found"
id="wWTUrM" />
defaultMessage="No activities found" </Text>
/> </Box>
</Typography>
}
/>
</ListItem>
), ),
)} )}
</List> </List>
</CardContent> </DashboardCard.Content>
</Card> </DashboardCard>
); );
}; };
HomeActivityCard.displayName = "HomeActivityCard";
export default HomeActivityCard;

View file

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

View file

@ -8,33 +8,32 @@ interface HomeAnalyticsCardProps {
children?: React.ReactNode; children?: React.ReactNode;
} }
const HomeAnalyticsCard: React.FC<HomeAnalyticsCardProps> = props => { export const HomeAnalyticsCard = ({
const { children, title, testId } = props; children,
title,
return ( testId,
<Box }: HomeAnalyticsCardProps) => (
borderWidth={1} <Box
borderStyle="solid" borderWidth={1}
borderColor="neutralPlain" borderStyle="solid"
borderRadius={3} borderColor="neutralPlain"
padding={5} borderRadius={3}
display="flex" paddingX={3}
justifyContent="space-between" paddingY={5}
data-test-id={testId} display="flex"
> justifyContent="space-between"
<Box display="flex" flexDirection="column" gap={0.5}> data-test-id={testId}
<Text size="large" variant="body"> >
{title} <Box display="flex" flexDirection="column" gap={0.5}>
</Text> <Text size="large" variant="body">
<Text size="medium" variant="caption" color="iconNeutralPlain"> {title}
<FormattedMessage id="zWgbGg" defaultMessage="Today" /> </Text>
</Text> <Text size="medium" variant="caption" color="iconNeutralPlain">
</Box> <FormattedMessage id="zWgbGg" defaultMessage="Today" />
<Text as="h4" variant="heading">
{children}
</Text> </Text>
</Box> </Box>
); <Text as="h4" variant="heading">
}; {children}
HomeAnalyticsCard.displayName = "HomeAnalyticsCard"; </Text>
export default HomeAnalyticsCard; </Box>
);

View file

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

View file

@ -1,71 +1,46 @@
import Skeleton from "@dashboard/components/Skeleton"; import Skeleton from "@dashboard/components/Skeleton";
import { Typography } from "@material-ui/core"; import { Text } from "@saleor/macaw-ui/next";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
const useStyles = makeStyles( interface HomeHeaderProps {
theme => ({
headerContainer: {
alignItems: "flex-end",
display: "flex",
justifyContent: "space-between",
marginBottom: theme.spacing(6),
},
pageHeader: {
fontWeight: 600 as 600,
},
subtitle: {
color: theme.typography.caption.color,
},
}),
{ name: "HomeHeader" },
);
interface HomeOrdersCardProps {
userName: string; userName: string;
} }
const HomeOrdersCard: React.FC<HomeOrdersCardProps> = props => { export const HomeHeader: React.FC<HomeHeaderProps> = ({
const { userName } = props; userName,
}: HomeHeaderProps) => (
const classes = useStyles(props); <div data-test-id="home-header">
<Text
return ( variant="heading"
<div data-test-id="home-header"> lineHeight="captionSmall"
<div> size="small"
<Typography as="h4"
className={classes.pageHeader} data-test-id="welcome-header"
variant="h4" >
data-test-id="welcome-header" {userName ? (
> <FormattedMessage
{userName ? ( id="By5ZBp"
<FormattedMessage defaultMessage="Hello there, {userName}"
id="By5ZBp" description="header"
defaultMessage="Hello there, {userName}" values={{
description="header" userName,
values={{ }}
userName, />
}} ) : (
/> <Skeleton style={{ width: "10em" }} />
) : ( )}
<Skeleton style={{ width: "10em" }} /> </Text>
)} <Text variant="caption" size="large">
</Typography> {userName ? (
<Typography className={classes.subtitle}> <FormattedMessage
{userName ? ( id="aCX8rl"
<FormattedMessage defaultMessage="Here is some information we gathered about your store"
id="aCX8rl" description="subheader"
defaultMessage="Here is some information we gathered about your store" />
description="subheader" ) : (
/> <Skeleton style={{ width: "10em" }} />
) : ( )}
<Skeleton style={{ width: "10em" }} /> </Text>
)} </div>
</Typography> );
</div>
</div>
);
};
HomeOrdersCard.displayName = "HomeOrdersCard";
export default HomeOrdersCard;

View file

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

View file

@ -0,0 +1,78 @@
import RequirePermissions from "@dashboard/components/RequirePermissions";
import { PermissionEnum } from "@dashboard/graphql";
import { List } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
import { HomeNotificationListItem } from "./HomeNotificationListItem";
import { homeNotificationTableMessages as messages } from "./messages";
import {
getOrdersToCaptureText,
getOrderToFulfillText,
getProductsOutOfStockText,
} from "./utils";
interface HomeNotificationTableProps {
ordersToCapture: number;
ordersToFulfill: number;
productsOutOfStock: number;
createNewChannelHref: string;
ordersToFulfillHref: string;
ordersToCaptureHref: string;
productsOutOfStockHref: string;
noChannel: boolean;
}
export const HomeNotificationList = ({
createNewChannelHref,
ordersToFulfillHref,
ordersToCaptureHref,
productsOutOfStockHref,
ordersToCapture,
ordersToFulfill,
productsOutOfStock,
noChannel,
}: HomeNotificationTableProps) => {
const intl = useIntl();
return (
<List>
{noChannel && (
<RequirePermissions
requiredPermissions={[PermissionEnum.MANAGE_CHANNELS]}
>
<HomeNotificationListItem linkUrl={createNewChannelHref}>
{intl.formatMessage(messages.createNewChannel)}
</HomeNotificationListItem>
</RequirePermissions>
)}
<RequirePermissions requiredPermissions={[PermissionEnum.MANAGE_ORDERS]}>
<HomeNotificationListItem
linkUrl={ordersToFulfillHref}
dataTestId="orders-to-fulfill"
>
{getOrderToFulfillText(ordersToFulfill, intl)}
</HomeNotificationListItem>
<HomeNotificationListItem
linkUrl={ordersToCaptureHref}
dataTestId="orders-to-capture"
>
{getOrdersToCaptureText(ordersToCapture, intl)}
</HomeNotificationListItem>
</RequirePermissions>
<RequirePermissions
requiredPermissions={[PermissionEnum.MANAGE_PRODUCTS]}
>
<HomeNotificationListItem
linkUrl={productsOutOfStockHref}
dataTestId="products-out-of-stock"
>
{getProductsOutOfStockText(productsOutOfStock, intl)}
</HomeNotificationListItem>
</RequirePermissions>
</List>
);
};

View file

@ -0,0 +1,43 @@
import {
Box,
ChevronRightIcon,
List,
sprinkles,
Text,
} from "@saleor/macaw-ui/next";
import React, { ReactNode } from "react";
import { Link } from "react-router-dom";
interface HomeNotificationListItemProps {
dataTestId?: string;
linkUrl: string;
children: ReactNode;
}
export const HomeNotificationListItem = ({
dataTestId,
linkUrl,
children,
}: HomeNotificationListItemProps) => (
<List.Item
borderColor="neutralPlain"
borderWidth={1}
borderBottomStyle="solid"
data-test-id={dataTestId}
>
<Link
className={sprinkles({ width: "100%", paddingX: 3, paddingY: 4 })}
to={linkUrl}
>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
width="100%"
>
<Text size="small">{children}</Text>
<ChevronRightIcon />
</Box>
</Link>
</List.Item>
);

View file

@ -0,0 +1 @@
export * from "./HomeNotificationList";

View file

@ -0,0 +1,56 @@
import Skeleton from "@dashboard/components/Skeleton";
import React from "react";
import { IntlShape } from "react-intl";
import { homeNotificationTableMessages as messages } from "./messages";
export const getOrderToFulfillText = (
ordersToFulfill: number | undefined,
intl: IntlShape,
) => {
if (ordersToFulfill === undefined) {
return <Skeleton />;
}
if (ordersToFulfill === 0) {
return intl.formatMessage(messages.noOrders);
}
return intl.formatMessage(messages.orderReady, {
amount: <strong>{ordersToFulfill}</strong>,
});
};
export const getOrdersToCaptureText = (
ordersToCapture: number | undefined,
intl: IntlShape,
) => {
if (ordersToCapture === undefined) {
return <Skeleton />;
}
if (ordersToCapture === 0) {
return intl.formatMessage(messages.noPaymentWaiting);
}
return intl.formatMessage(messages.paymentCapture, {
amount: <strong>{ordersToCapture}</strong>,
});
};
export const getProductsOutOfStockText = (
productsOutOfStock: number | undefined,
intl: IntlShape,
) => {
if (productsOutOfStock === undefined) {
return <Skeleton />;
}
if (productsOutOfStock === 0) {
return intl.formatMessage(messages.noProductsOut);
}
return intl.formatMessage(messages.productOut, {
amount: <strong>{productsOutOfStock}</strong>,
});
};

View file

@ -1,172 +0,0 @@
import RequirePermissions from "@dashboard/components/RequirePermissions";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
import TableRowLink from "@dashboard/components/TableRowLink";
import { PermissionEnum } from "@dashboard/graphql";
import {
Card,
CardContent,
TableBody,
TableCell,
Typography,
} from "@material-ui/core";
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
import { makeStyles } from "@saleor/macaw-ui";
import { vars } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
import { homeNotificationTableMessages as messages } from "./messages";
const useStyles = makeStyles(
() => ({
arrowIcon: {
textAlign: "right",
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
},
tableCard: {
overflow: "hidden",
borderRadius: 0,
},
tableRow: {
cursor: "pointer",
/* Table to be replaced with Box */
"& .MuiTableCell-root": {
paddingLeft: `${vars.spacing[5]} !important`,
paddingRight: `${vars.spacing[5]} !important`,
},
},
cardContent: {
padding: 0,
},
}),
{ name: "HomeNotificationTable" },
);
interface HomeNotificationTableProps {
ordersToCapture: number;
ordersToFulfill: number;
productsOutOfStock: number;
createNewChannelHref: string;
ordersToFulfillHref: string;
ordersToCaptureHref: string;
productsOutOfStockHref: string;
noChannel: boolean;
}
const HomeNotificationTable: React.FC<HomeNotificationTableProps> = props => {
const {
createNewChannelHref,
ordersToFulfillHref,
ordersToCaptureHref,
productsOutOfStockHref,
ordersToCapture,
ordersToFulfill,
productsOutOfStock,
noChannel,
} = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Card className={classes.tableCard}>
<CardContent className={classes.cardContent}>
<ResponsiveTable>
<TableBody className={classes.tableRow}>
{noChannel && (
<RequirePermissions
requiredPermissions={[PermissionEnum.MANAGE_CHANNELS]}
>
<TableRowLink hover={true} href={createNewChannelHref}>
<TableCell>
<Typography>
{intl.formatMessage(messages.createNewChannel)}
</Typography>
</TableCell>
<TableCell className={classes.arrowIcon}>
<KeyboardArrowRight />
</TableCell>
</TableRowLink>
</RequirePermissions>
)}
<RequirePermissions
requiredPermissions={[PermissionEnum.MANAGE_ORDERS]}
>
<TableRowLink hover={true} href={ordersToFulfillHref}>
<TableCell data-test-id="orders-to-fulfill">
{ordersToFulfill === undefined ? (
<Skeleton />
) : ordersToFulfill === 0 ? (
<Typography>
{intl.formatMessage(messages.noOrders)}
</Typography>
) : (
<Typography>
{intl.formatMessage(messages.orderReady, {
amount: <strong>{ordersToFulfill}</strong>,
})}
</Typography>
)}
</TableCell>
<TableCell className={classes.arrowIcon}>
<KeyboardArrowRight />
</TableCell>
</TableRowLink>
<TableRowLink hover={true} href={ordersToCaptureHref}>
<TableCell data-test-id="orders-to-capture">
{ordersToCapture === undefined ? (
<Skeleton />
) : ordersToCapture === 0 ? (
<Typography>
{intl.formatMessage(messages.noPaymentWaiting)}
</Typography>
) : (
<Typography>
{intl.formatMessage(messages.paymentCapture, {
amount: <strong>{ordersToCapture}</strong>,
})}
</Typography>
)}
</TableCell>
<TableCell className={classes.arrowIcon}>
<KeyboardArrowRight />
</TableCell>
</TableRowLink>
</RequirePermissions>
<RequirePermissions
requiredPermissions={[PermissionEnum.MANAGE_PRODUCTS]}
>
<TableRowLink hover={true} href={productsOutOfStockHref}>
<TableCell data-test-id="products-out-of-stock">
{productsOutOfStock === undefined ? (
<Skeleton />
) : productsOutOfStock === 0 ? (
<Typography>
{intl.formatMessage(messages.noProductsOut)}
</Typography>
) : (
<Typography>
{intl.formatMessage(messages.productOut, {
amount: <strong>{productsOutOfStock}</strong>,
})}
</Typography>
)}
</TableCell>
<TableCell className={classes.arrowIcon}>
<KeyboardArrowRight />
</TableCell>
</TableRowLink>
</RequirePermissions>
</TableBody>
</ResponsiveTable>
</CardContent>
</Card>
);
};
HomeNotificationTable.displayName = "HomeNotificationTable";
export default HomeNotificationTable;

View file

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

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
import { TopNav } from "@dashboard/components/AppLayout/TopNav"; import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import CardSpacer from "@dashboard/components/CardSpacer"; import CardSpacer from "@dashboard/components/CardSpacer";
import { DetailPageLayout } from "@dashboard/components/Layouts"; import { DetailPageLayout } from "@dashboard/components/Layouts";
@ -6,26 +5,26 @@ import Money from "@dashboard/components/Money";
import RequirePermissions from "@dashboard/components/RequirePermissions"; import RequirePermissions from "@dashboard/components/RequirePermissions";
import Skeleton from "@dashboard/components/Skeleton"; import Skeleton from "@dashboard/components/Skeleton";
import { HomeQuery, PermissionEnum } from "@dashboard/graphql"; import { HomeQuery, PermissionEnum } from "@dashboard/graphql";
import { RelayToFlat } from "@dashboard/types"; import { Activities, ProductTopToday } from "@dashboard/home/types";
import { Box } from "@saleor/macaw-ui/next"; import { Box } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import HomeActivityCard from "../HomeActivityCard"; import { HomeActivityCard } from "../HomeActivityCard";
import HomeAnalyticsCard from "../HomeAnalyticsCard"; import { HomeAnalyticsCard } from "../HomeAnalyticsCard";
import HomeHeader from "../HomeHeader"; import { HomeHeader } from "../HomeHeader";
import HomeNotificationTable from "../HomeNotificationTable/HomeNotificationTable"; import { HomeNotificationList } from "../HomeNotificationList";
import HomeProductListCard from "../HomeProductListCard"; import { HomeProductList } from "../HomeProductList";
import { homePageMessages } from "./messages"; import { homePageMessages } from "./messages";
export interface HomePageProps { export interface HomePageProps {
activities: RelayToFlat<HomeQuery["activities"]>; activities: Activities;
orders: number | null; orders: number | null;
ordersToCapture: number | null; ordersToCapture: number | null;
ordersToFulfill: number | null; ordersToFulfill: number | null;
productsOutOfStock: number; productsOutOfStock: number;
sales: HomeQuery["salesToday"]["gross"]; sales: NonNullable<HomeQuery["salesToday"]>["gross"];
topProducts: RelayToFlat<HomeQuery["productTopToday"]> | null; topProducts: ProductTopToday | null;
userName: string; userName: string;
createNewChannelHref: string; createNewChannelHref: string;
ordersToFulfillHref: string; ordersToFulfillHref: string;
@ -93,13 +92,13 @@ const HomePage: React.FC<HomePageProps> = props => {
</HomeAnalyticsCard> </HomeAnalyticsCard>
</Box> </Box>
</RequirePermissions> </RequirePermissions>
<HomeNotificationTable <HomeNotificationList
createNewChannelHref={createNewChannelHref} createNewChannelHref={createNewChannelHref}
ordersToFulfillHref={ordersToFulfillHref} ordersToFulfillHref={ordersToFulfillHref}
ordersToCaptureHref={ordersToCaptureHref} ordersToCaptureHref={ordersToCaptureHref}
productsOutOfStockHref={productsOutOfStockHref} productsOutOfStockHref={productsOutOfStockHref}
ordersToCapture={ordersToCapture} ordersToCapture={ordersToCapture ?? 0}
ordersToFulfill={ordersToFulfill} ordersToFulfill={ordersToFulfill ?? 0}
productsOutOfStock={productsOutOfStock} productsOutOfStock={productsOutOfStock}
noChannel={noChannel} noChannel={noChannel}
/> />
@ -111,7 +110,7 @@ const HomePage: React.FC<HomePageProps> = props => {
PermissionEnum.MANAGE_PRODUCTS, PermissionEnum.MANAGE_PRODUCTS,
]} ]}
> >
<HomeProductListCard <HomeProductList
testId="top-products" testId="top-products"
topProducts={topProducts} topProducts={topProducts}
/> />

View file

@ -0,0 +1,118 @@
import Money from "@dashboard/components/Money";
import Skeleton from "@dashboard/components/Skeleton";
import { ProductTopToday } from "@dashboard/home/types";
import { productVariantEditUrl } from "@dashboard/products/urls";
import { Box, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { renderCollection } from "../../../misc";
import { HomeProductListItem } from "./HomeProductListItem";
interface HomeProductListProps {
testId?: string;
topProducts: ProductTopToday;
}
export const HomeProductList = ({
topProducts,
testId,
}: HomeProductListProps) => {
const intl = useIntl();
return (
<Box data-test-id={testId}>
<Text variant="heading" display="block" paddingTop={7} marginBottom={2}>
{intl.formatMessage({
id: "e08xWz",
defaultMessage: "Top products",
description: "header",
})}
</Text>
<Box>
{renderCollection(
topProducts,
variant => (
<HomeProductListItem
key={variant ? variant.id : "skeleton"}
linkUrl={
variant
? productVariantEditUrl(variant.product.id, variant.id)
: ""
}
>
{variant ? (
<>
<Box display="flex" gap={3} alignItems="center">
<Box
borderColor="neutralHighlight"
borderStyle="solid"
borderWidth={1}
borderRadius={3}
as="img"
width={16}
height={16}
padding={0.5}
alt={variant.product.name}
objectFit="scale-down"
src={variant.product.thumbnail?.url}
/>
<Box display="flex" flexDirection="column">
<Text size="small">{variant.product.name}</Text>
<Text size="small" color="textNeutralSubdued">
{variant.attributes
.map(attribute => attribute.values[0].name)
.join(" / ")}
</Text>
<Text size="small" color="textNeutralSubdued">
<FormattedMessage
id="nII/qB"
defaultMessage="{amount, plural,one {One ordered}other {{amount} ordered}}"
description="number of ordered products"
values={{
amount: variant.quantityOrdered,
}}
/>
</Text>
</Box>
</Box>
<Text textAlign="right">
{variant.revenue ? (
<Money money={variant.revenue.gross} />
) : (
"-"
)}
</Text>
</>
) : (
<Skeleton />
)}
</HomeProductListItem>
),
() => (
<Box
borderColor="neutralPlain"
borderWidth={1}
paddingY={5}
borderBottomStyle="solid"
>
<Text size="small">
<FormattedMessage
id="Q1Uzbb"
defaultMessage="No products found"
/>
</Text>
</Box>
),
)}
</Box>
</Box>
);
};
HomeProductList.displayName = "HomeProductList";
export default HomeProductList;

View file

@ -0,0 +1,33 @@
import { Box, List, sprinkles } from "@saleor/macaw-ui/next";
import React, { ReactNode } from "react";
import { Link } from "react-router-dom";
interface HomeNotificationListItemProps {
dataTestId?: string;
linkUrl: string;
children: ReactNode;
}
export const HomeProductListItem = ({
dataTestId,
linkUrl,
children,
}: HomeNotificationListItemProps) => (
<List.Item
borderColor="neutralPlain"
borderWidth={1}
borderBottomStyle="solid"
data-test-id={dataTestId}
>
<Link className={sprinkles({ width: "100%", padding: 3 })} to={linkUrl}>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
width="100%"
>
{children}
</Box>
</Link>
</List.Item>
);

View file

@ -0,0 +1 @@
export * from "./HomeProductList";

View file

@ -1,175 +0,0 @@
// @ts-strict-ignore
import CardTitle from "@dashboard/components/CardTitle";
import Money from "@dashboard/components/Money";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
import TableCellAvatar from "@dashboard/components/TableCellAvatar";
import TableRowLink from "@dashboard/components/TableRowLink";
import { HomeQuery } from "@dashboard/graphql";
import { productVariantEditUrl } from "@dashboard/products/urls";
import { RelayToFlat } from "@dashboard/types";
import {
Card,
CardContent,
TableBody,
TableCell,
Typography,
} from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import { vars } from "@saleor/macaw-ui/next";
import clsx from "clsx";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { maybe, renderCollection } from "../../../misc";
const useStyles = makeStyles(
theme => ({
avatarProps: {
height: 64,
width: 64,
},
colAvatar: {
paddingBottom: theme.spacing(2),
paddingRight: theme.spacing(),
paddingTop: theme.spacing(2),
width: 112,
},
colName: {
width: "auto",
},
label: {
paddingLeft: 0,
},
noProducts: {
paddingBottom: 20,
paddingTop: 20,
paddingLeft: "0 !important",
},
tableRow: {
cursor: "pointer",
/* Table to be replaced with Box */
"& .MuiTableCell-root": {
paddingLeft: `${vars.spacing[5]} !important`,
paddingRight: `${vars.spacing[5]} !important`,
},
},
cardContent: {
padding: 0,
},
cardTitle: {
padding: 0,
},
}),
{ name: "HomeProductListCard" },
);
interface HomeProductListProps {
testId?: string;
topProducts: RelayToFlat<HomeQuery["productTopToday"]>;
}
export const HomeProductList: React.FC<HomeProductListProps> = props => {
const { topProducts, testId } = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<Card data-test-id={testId}>
<CardTitle
className={classes.cardTitle}
title={intl.formatMessage({
id: "rr8fyf",
defaultMessage: "Top Products",
description: "header",
})}
/>
<CardContent className={classes.cardContent}>
<ResponsiveTable>
<colgroup>
<col className={classes.colAvatar} />
<col className={classes.colName} />
<col />
</colgroup>
<TableBody>
{renderCollection(
topProducts,
variant => (
<TableRowLink
key={variant ? variant.id : "skeleton"}
hover={!!variant}
className={clsx({
[classes.tableRow]: !!variant,
})}
href={productVariantEditUrl(variant.product.id, variant.id)}
>
<TableCellAvatar
className={classes.colAvatar}
thumbnail={maybe(() => variant.product.thumbnail.url)}
avatarProps={classes.avatarProps}
/>
<TableCell className={classes.label}>
{variant ? (
<>
<Typography color={"primary"}>
{variant.product.name}
</Typography>
<Typography color={"textSecondary"}>
{maybe(() =>
variant.attributes
.map(attribute => attribute.values[0].name)
.join(" / "),
)}
</Typography>
<Typography color={"textSecondary"}>
<FormattedMessage
id="0opVvi"
defaultMessage="{amount, plural,one {One ordered}other {{amount} Ordered}}"
description="number of ordered products"
values={{
amount: variant.quantityOrdered,
}}
/>
</Typography>
</>
) : (
<Skeleton />
)}
</TableCell>
<TableCell>
<Typography align={"right"}>
{maybe(
() => (
<Money money={variant.revenue.gross} />
),
<Skeleton />,
)}
</Typography>
</TableCell>
</TableRowLink>
),
() => (
<TableRowLink>
<TableCell colSpan={3} className={classes.noProducts}>
<Typography>
<FormattedMessage
id="Q1Uzbb"
defaultMessage="No products found"
/>
</Typography>
</TableCell>
</TableRowLink>
),
)}
</TableBody>
</ResponsiveTable>
</CardContent>
</Card>
);
};
HomeProductList.displayName = "HomeProductList";
export default HomeProductList;

View file

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

View file

@ -1,53 +0,0 @@
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import CardTitle from "@dashboard/components/CardTitle";
import { Card, CardContent, Typography } from "@material-ui/core";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
interface HomeScreenProps {
user: {
email: string;
};
}
export const HomeScreen: React.FC<HomeScreenProps> = ({ user }) => {
const intl = useIntl();
return (
<>
<TopNav
title={intl.formatMessage(
{
id: "By5ZBp",
defaultMessage: "Hello there, {userName}",
description: "header",
},
{ userName: user.email },
)}
/>
<Card>
<CardTitle
title={intl.formatMessage({
id: "6L6Fy2",
defaultMessage: "Disclaimer",
description: "header",
})}
/>
<CardContent>
<Typography>
<FormattedMessage
id="5LRkEs"
defaultMessage="The new dashboard and the GraphQL API are preview-quality software."
/>
</Typography>
<Typography>
<FormattedMessage
id="G7mu0y"
defaultMessage="The GraphQL API is beta quality. It is not fully optimized and some mutations or queries may be missing."
/>
</Typography>
</CardContent>
</Card>
</>
);
};

View file

@ -316,7 +316,7 @@ export const shop: (placeholderImage: string) => HomeQuery = (
{ {
__typename: "AttributeValue", __typename: "AttributeValue",
id: "QXR0cmlidXRlVmFsdWU6OTI=", id: "QXR0cmlidXRlVmFsdWU6OTI=",
name: "XS", name: "XL",
sortOrder: 0, sortOrder: 0,
}, },
], ],
@ -326,7 +326,83 @@ export const shop: (placeholderImage: string) => HomeQuery = (
product: { product: {
__typename: "Product", __typename: "Product",
id: "UHJvZHVjdDo4", id: "UHJvZHVjdDo4",
name: "Gardner-Martin", name: "Black Hoodie",
thumbnail: {
__typename: "Image",
url: placeholderImage,
},
},
quantityOrdered: 1,
revenue: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 37.65,
currency: "USD",
},
},
},
},
{
__typename: "ProductVariantCountableEdge",
node: {
__typename: "ProductVariant",
attributes: [
{
__typename: "SelectedAttribute",
values: [
{
__typename: "AttributeValue",
id: "QXR0cmlidXRlVmFsdWU6OTI2=",
name: "2l",
sortOrder: 0,
},
],
},
],
id: "UHJvZHVjdFZhcmlhbnQ6NDM=2",
product: {
__typename: "Product",
id: "UHJvZHVjdDo4",
name: "Bean Juice",
thumbnail: {
__typename: "Image",
url: placeholderImage,
},
},
quantityOrdered: 1,
revenue: {
__typename: "TaxedMoney",
gross: {
__typename: "Money",
amount: 37.65,
currency: "USD",
},
},
},
},
{
__typename: "ProductVariantCountableEdge",
node: {
__typename: "ProductVariant",
attributes: [
{
__typename: "SelectedAttribute",
values: [
{
__typename: "AttributeValue",
id: "QXR0cmlidXRlVmFsdWU6OTI=3",
name: "L",
sortOrder: 0,
},
],
},
],
id: "UHJvZHVjdFZhcmlhbnQ6NDM=3",
product: {
__typename: "Product",
id: "UHJvZHVjdDo4",
name: "Black Hoodie",
thumbnail: { thumbnail: {
__typename: "Image", __typename: "Image",
url: placeholderImage, url: placeholderImage,

7
src/home/types.ts Normal file
View file

@ -0,0 +1,7 @@
import { HomeQuery } from "@dashboard/graphql";
import { RelayToFlat } from "@dashboard/types";
export type Activities = RelayToFlat<NonNullable<HomeQuery["activities"]>>;
export type ProductTopToday = RelayToFlat<
NonNullable<HomeQuery["productTopToday"]>
>;

View file

@ -48,3 +48,13 @@ body {
[id*="ScrollableDialog"] .infinite-scroll-component { [id*="ScrollableDialog"] .infinite-scroll-component {
overflow: hidden !important; overflow: hidden !important;
} }
/*
TODO: Remove it when macaw will handle media queries in custom properties
https://github.com/saleor/macaw-ui/issues/498
*/
@media screen and (max-width: 1024px) {
.mobile-full-height {
height: auto !important;
}
}