Migrate Home page to new macaw (#3737)
This commit is contained in:
parent
1c7486818b
commit
b66af99477
26 changed files with 606 additions and 620 deletions
5
.changeset/shiny-moons-wave.md
Normal file
5
.changeset/shiny-moons-wave.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"saleor-dashboard": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Migrate Home page to new macaw components
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
}) => (
|
}) => {
|
||||||
|
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
|
<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"
|
display="grid"
|
||||||
margin="auto"
|
margin="auto"
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
gridTemplateColumns={gridTemplateColumnsValue}
|
||||||
__gridTemplateRows="auto 1fr"
|
__gridTemplateRows="auto 1fr"
|
||||||
__maxWidth={contentMaxWidth}
|
__maxWidth={contentMaxWidth}
|
||||||
__height={
|
__height={heightValue}
|
||||||
withSavebar ? contentWithSidebarHeight : contentWithoutSidebarHeight
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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)}
|
||||||
</Typography>
|
</Text>
|
||||||
}
|
<Text
|
||||||
secondary={<DateTime date={activity.date} plain />}
|
variant="body"
|
||||||
/>
|
size="small"
|
||||||
|
color="textNeutralSubdued"
|
||||||
|
>
|
||||||
|
<DateTime date={activity.date} plain />
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<ListItemText className={classes.loadingProducts}>
|
<Box paddingY={4}>
|
||||||
<Typography>
|
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
</Typography>
|
</Box>
|
||||||
</ListItemText>
|
|
||||||
)}
|
)}
|
||||||
</ListItem>
|
</List.Item>
|
||||||
),
|
),
|
||||||
() => (
|
() => (
|
||||||
<ListItem className={classes.noProducts}>
|
<Box paddingY={4}>
|
||||||
<ListItemText
|
<Text variant="body" size="small">
|
||||||
primary={
|
|
||||||
<Typography>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="wWTUrM"
|
id="wWTUrM"
|
||||||
defaultMessage="No activities found"
|
defaultMessage="No activities found"
|
||||||
/>
|
/>
|
||||||
</Typography>
|
</Text>
|
||||||
}
|
</Box>
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
</CardContent>
|
</DashboardCard.Content>
|
||||||
</Card>
|
</DashboardCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
HomeActivityCard.displayName = "HomeActivityCard";
|
|
||||||
export default HomeActivityCard;
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
export { default } from "./HomeActivityCard";
|
|
||||||
export * from "./HomeActivityCard";
|
export * from "./HomeActivityCard";
|
||||||
|
|
|
@ -8,16 +8,18 @@ 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,
|
||||||
|
}: HomeAnalyticsCardProps) => (
|
||||||
<Box
|
<Box
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderStyle="solid"
|
borderStyle="solid"
|
||||||
borderColor="neutralPlain"
|
borderColor="neutralPlain"
|
||||||
borderRadius={3}
|
borderRadius={3}
|
||||||
padding={5}
|
paddingX={3}
|
||||||
|
paddingY={5}
|
||||||
display="flex"
|
display="flex"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
data-test-id={testId}
|
data-test-id={testId}
|
||||||
|
@ -35,6 +37,3 @@ const HomeAnalyticsCard: React.FC<HomeAnalyticsCardProps> = props => {
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
HomeAnalyticsCard.displayName = "HomeAnalyticsCard";
|
|
||||||
export default HomeAnalyticsCard;
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
export { default } from "./HomeAnalyticsCard";
|
|
||||||
export * from "./HomeAnalyticsCard";
|
export * from "./HomeAnalyticsCard";
|
||||||
|
|
|
@ -1,42 +1,21 @@
|
||||||
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);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-test-id="home-header">
|
<div data-test-id="home-header">
|
||||||
<div>
|
<Text
|
||||||
<Typography
|
variant="heading"
|
||||||
className={classes.pageHeader}
|
lineHeight="captionSmall"
|
||||||
variant="h4"
|
size="small"
|
||||||
|
as="h4"
|
||||||
data-test-id="welcome-header"
|
data-test-id="welcome-header"
|
||||||
>
|
>
|
||||||
{userName ? (
|
{userName ? (
|
||||||
|
@ -51,8 +30,8 @@ const HomeOrdersCard: React.FC<HomeOrdersCardProps> = props => {
|
||||||
) : (
|
) : (
|
||||||
<Skeleton style={{ width: "10em" }} />
|
<Skeleton style={{ width: "10em" }} />
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Text>
|
||||||
<Typography className={classes.subtitle}>
|
<Text variant="caption" size="large">
|
||||||
{userName ? (
|
{userName ? (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="aCX8rl"
|
id="aCX8rl"
|
||||||
|
@ -62,10 +41,6 @@ const HomeOrdersCard: React.FC<HomeOrdersCardProps> = props => {
|
||||||
) : (
|
) : (
|
||||||
<Skeleton style={{ width: "10em" }} />
|
<Skeleton style={{ width: "10em" }} />
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Text>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
HomeOrdersCard.displayName = "HomeOrdersCard";
|
|
||||||
export default HomeOrdersCard;
|
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
export { default } from "./HomeHeader";
|
|
||||||
export * from "./HomeHeader";
|
export * from "./HomeHeader";
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
1
src/home/components/HomeNotificationList/index.ts
Normal file
1
src/home/components/HomeNotificationList/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./HomeNotificationList";
|
56
src/home/components/HomeNotificationList/utils.tsx
Normal file
56
src/home/components/HomeNotificationList/utils.tsx
Normal 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>,
|
||||||
|
});
|
||||||
|
};
|
|
@ -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;
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { default } from "./HomeNotificationTable";
|
|
||||||
export * from "./HomeNotificationTable";
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
118
src/home/components/HomeProductList/HomeProductList.tsx
Normal file
118
src/home/components/HomeProductList/HomeProductList.tsx
Normal 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;
|
33
src/home/components/HomeProductList/HomeProductListItem.tsx
Normal file
33
src/home/components/HomeProductList/HomeProductListItem.tsx
Normal 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>
|
||||||
|
);
|
1
src/home/components/HomeProductList/index.ts
Normal file
1
src/home/components/HomeProductList/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./HomeProductList";
|
|
@ -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;
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { default } from "./HomeProductListCard";
|
|
||||||
export * from "./HomeProductListCard";
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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
7
src/home/types.ts
Normal 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"]>
|
||||||
|
>;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue