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

View file

@ -4,7 +4,7 @@ import {
savebarHeight,
} from "@dashboard/components/AppLayout/consts";
import { Box, Sprinkles } from "@saleor/macaw-ui/next";
import React from "react";
import React, { useMemo } from "react";
interface DetailPageLayoutProps {
children: React.ReactNode;
@ -19,17 +19,39 @@ export const RootLayout: React.FC<DetailPageLayoutProps> = ({
children,
gridTemplateColumns = 12,
withSavebar = true,
}) => (
<Box
display="grid"
margin="auto"
gridTemplateColumns={gridTemplateColumns}
__gridTemplateRows="auto 1fr"
__maxWidth={contentMaxWidth}
__height={
withSavebar ? contentWithSidebarHeight : contentWithoutSidebarHeight
}
>
{children}
</Box>
);
}) => {
const gridTemplateColumnsValue =
useMemo((): Sprinkles["gridTemplateColumns"] => {
if (gridTemplateColumns instanceof Object) {
return {
mobile: gridTemplateColumns.mobile ?? 1,
...gridTemplateColumns,
};
}
return {
mobile: 1,
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 CardTitle from "@dashboard/components/CardTitle";
import { DashboardCard } from "@dashboard/components/Card";
import { DateTime } from "@dashboard/components/Date";
import Skeleton from "@dashboard/components/Skeleton";
import { HomeQuery } from "@dashboard/graphql";
import { RelayToFlat } from "@dashboard/types";
import {
Card,
CardContent,
List,
ListItem,
ListItemText,
Typography,
} from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import { Activities } from "@dashboard/home/types";
import { Box, List, Text, useTheme } from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { renderCollection } from "../../../misc";
import { getActivityMessage } from "./activityMessages";
const useStyles = makeStyles(
{
loadingProducts: {
paddingBottom: "10px",
paddingTop: "10px",
},
noProducts: {
paddingBottom: "16px",
paddingTop: "16px",
},
listItem: {
paddingLeft: 0,
},
},
{ name: "HomeActivityCard" },
);
interface HomeActivityCardProps {
activities: RelayToFlat<HomeQuery["activities"]>;
activities: Activities;
testId?: string;
}
const HomeActivityCard: React.FC<HomeActivityCardProps> = props => {
const { activities, testId } = props;
const classes = useStyles(props);
export const HomeActivityCard = ({
activities,
testId,
}: HomeActivityCardProps) => {
const intl = useIntl();
const { themeValues } = useTheme();
return (
<Card data-test-id={testId}>
<CardTitle
title={intl.formatMessage({
<DashboardCard data-test-id={testId}>
<DashboardCard.Title>
{intl.formatMessage({
id: "BXkF8Z",
defaultMessage: "Activity",
description: "header",
})}
/>
<CardContent>
<List dense>
</DashboardCard.Title>
<DashboardCard.Content>
<List>
{renderCollection(
activities,
(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 ? (
<ListItemText
primary={
<Typography>
{getActivityMessage(activity, intl)}
</Typography>
}
secondary={<DateTime date={activity.date} plain />}
/>
<>
<Text variant="body" size="small">
{getActivityMessage(activity, intl)}
</Text>
<Text
variant="body"
size="small"
color="textNeutralSubdued"
>
<DateTime date={activity.date} plain />
</Text>
</>
) : (
<ListItemText className={classes.loadingProducts}>
<Typography>
<Skeleton />
</Typography>
</ListItemText>
<Box paddingY={4}>
<Skeleton />
</Box>
)}
</ListItem>
</List.Item>
),
() => (
<ListItem className={classes.noProducts}>
<ListItemText
primary={
<Typography>
<FormattedMessage
id="wWTUrM"
defaultMessage="No activities found"
/>
</Typography>
}
/>
</ListItem>
<Box paddingY={4}>
<Text variant="body" size="small">
<FormattedMessage
id="wWTUrM"
defaultMessage="No activities found"
/>
</Text>
</Box>
),
)}
</List>
</CardContent>
</Card>
</DashboardCard.Content>
</DashboardCard>
);
};
HomeActivityCard.displayName = "HomeActivityCard";
export default HomeActivityCard;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +1 @@
export { default } 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 CardSpacer from "@dashboard/components/CardSpacer";
import { DetailPageLayout } from "@dashboard/components/Layouts";
@ -6,26 +5,26 @@ import Money from "@dashboard/components/Money";
import RequirePermissions from "@dashboard/components/RequirePermissions";
import Skeleton from "@dashboard/components/Skeleton";
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 React from "react";
import { useIntl } from "react-intl";
import HomeActivityCard from "../HomeActivityCard";
import HomeAnalyticsCard from "../HomeAnalyticsCard";
import HomeHeader from "../HomeHeader";
import HomeNotificationTable from "../HomeNotificationTable/HomeNotificationTable";
import HomeProductListCard from "../HomeProductListCard";
import { HomeActivityCard } from "../HomeActivityCard";
import { HomeAnalyticsCard } from "../HomeAnalyticsCard";
import { HomeHeader } from "../HomeHeader";
import { HomeNotificationList } from "../HomeNotificationList";
import { HomeProductList } from "../HomeProductList";
import { homePageMessages } from "./messages";
export interface HomePageProps {
activities: RelayToFlat<HomeQuery["activities"]>;
activities: Activities;
orders: number | null;
ordersToCapture: number | null;
ordersToFulfill: number | null;
productsOutOfStock: number;
sales: HomeQuery["salesToday"]["gross"];
topProducts: RelayToFlat<HomeQuery["productTopToday"]> | null;
sales: NonNullable<HomeQuery["salesToday"]>["gross"];
topProducts: ProductTopToday | null;
userName: string;
createNewChannelHref: string;
ordersToFulfillHref: string;
@ -93,13 +92,13 @@ const HomePage: React.FC<HomePageProps> = props => {
</HomeAnalyticsCard>
</Box>
</RequirePermissions>
<HomeNotificationTable
<HomeNotificationList
createNewChannelHref={createNewChannelHref}
ordersToFulfillHref={ordersToFulfillHref}
ordersToCaptureHref={ordersToCaptureHref}
productsOutOfStockHref={productsOutOfStockHref}
ordersToCapture={ordersToCapture}
ordersToFulfill={ordersToFulfill}
ordersToCapture={ordersToCapture ?? 0}
ordersToFulfill={ordersToFulfill ?? 0}
productsOutOfStock={productsOutOfStock}
noChannel={noChannel}
/>
@ -111,7 +110,7 @@ const HomePage: React.FC<HomePageProps> = props => {
PermissionEnum.MANAGE_PRODUCTS,
]}
>
<HomeProductListCard
<HomeProductList
testId="top-products"
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",
id: "QXR0cmlidXRlVmFsdWU6OTI=",
name: "XS",
name: "XL",
sortOrder: 0,
},
],
@ -326,7 +326,83 @@ export const shop: (placeholderImage: string) => HomeQuery = (
product: {
__typename: "Product",
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: {
__typename: "Image",
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 {
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;
}
}