From 9f83d904f664669bf3b6073fd8b08f3d0a51c050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chy=C5=82a?= Date: Fri, 10 Nov 2023 09:51:42 +0100 Subject: [PATCH] Delay home queries to be non-blocking for the UI (#4405) Co-authored-by: wojteknowacki --- .changeset/funny-badgers-begin.md | 5 + cypress/support/customCommands/user/index.js | 4 +- cypress/support/pages/homePage.js | 18 +- locale/defaultMessages.json | 6 + src/graphql/hooks.generated.ts | 187 +++- src/graphql/types.generated.ts | 26 +- .../HomeActivityCard/HomeActivityCard.tsx | 53 +- .../HomeActivityCard/activityMessages.ts | 4 +- .../HomeNotificationList.tsx | 34 +- .../HomeNotificationListItem.tsx | 73 +- .../components/HomePage/HomePage.stories.tsx | 111 ++- src/home/components/HomePage/HomePage.tsx | 47 +- .../HomeProductList/HomeProductList.tsx | 65 +- src/home/fixtures.ts | 795 +++++++++--------- src/home/queries.ts | 102 ++- src/home/types.ts | 29 +- src/home/views/index.tsx | 95 ++- 17 files changed, 1045 insertions(+), 609 deletions(-) create mode 100644 .changeset/funny-badgers-begin.md diff --git a/.changeset/funny-badgers-begin.md b/.changeset/funny-badgers-begin.md new file mode 100644 index 00000000000..ed3c4b47d22 --- /dev/null +++ b/.changeset/funny-badgers-begin.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Delay home queries to be non-blocking for the UI diff --git a/cypress/support/customCommands/user/index.js b/cypress/support/customCommands/user/index.js index 0e62fd78cc6..18717a28c6d 100644 --- a/cypress/support/customCommands/user/index.js +++ b/cypress/support/customCommands/user/index.js @@ -19,10 +19,10 @@ Cypress.Commands.add("loginInShop", () => { }); Cypress.Commands.add("visitHomePageLoggedViaApi", user => { - cy.addAliasToGraphRequest("Home") + cy.addAliasToGraphRequest("UserDetails") .loginUserViaRequest("auth", user) .visit(urlList.homePage) - .waitForRequestAndCheckIfNoErrors("@Home"); + .waitForRequestAndCheckIfNoErrors("@UserDetails"); }); Cypress.Commands.add( diff --git a/cypress/support/pages/homePage.js b/cypress/support/pages/homePage.js index 09abf4a0cae..f501eafa5e6 100644 --- a/cypress/support/pages/homePage.js +++ b/cypress/support/pages/homePage.js @@ -4,11 +4,9 @@ import { HOMEPAGE_SELECTORS } from "../../elements/homePage/homePage-selectors"; export function changeChannel(channelName) { cy.get(HEADER_SELECTORS.channelSelect) .click() - .addAliasToGraphRequest("Home") .get(HEADER_SELECTORS.channelSelectList) .contains(channelName) - .click() - .wait("@Home"); + .click(); } export function expectWelcomeMessageIncludes(name) { @@ -21,37 +19,37 @@ export function expectWelcomeMessageIncludes(name) { export function getOrdersReadyToFulfillRegex( ordersReadyToFulfillBefore, - quantityOfNewOrders + quantityOfNewOrders, ) { const allOrdersReadyToFulfill = ordersReadyToFulfillBefore + quantityOfNewOrders; const notANumberRegex = "\\D*"; return new RegExp( - `${notANumberRegex}${allOrdersReadyToFulfill}${notANumberRegex}` + `${notANumberRegex}${allOrdersReadyToFulfill}${notANumberRegex}`, ); } export function getOrdersReadyForCaptureRegex( ordersReadyForCaptureBefore, - quantityOfNewOrders + quantityOfNewOrders, ) { const allOrdersReadyForCapture = ordersReadyForCaptureBefore + quantityOfNewOrders; const notANumberRegex = "\\D*"; return new RegExp( - `${notANumberRegex}${allOrdersReadyForCapture}${notANumberRegex}` + `${notANumberRegex}${allOrdersReadyForCapture}${notANumberRegex}`, ); } export function getProductsOutOfStockRegex( productsOutOfStockBefore, - quantityOfNewProducts + quantityOfNewProducts, ) { const allProductsOutOfStock = productsOutOfStockBefore + quantityOfNewProducts; const notANumberRegex = "\\D*"; return new RegExp( - `${notANumberRegex}${allProductsOutOfStock}${notANumberRegex}` + `${notANumberRegex}${allProductsOutOfStock}${notANumberRegex}`, ); } @@ -67,7 +65,7 @@ export function getSalesAmountRegex(salesAmountBefore, addedAmount) { const totalAmountWithSeparators = `${totalAmountIntegerWithThousandsSeparator}${decimalSeparator}${totalAmountDecimalValue}`; const notANumberRegex = "\\D*"; return new RegExp( - `${notANumberRegex}${totalAmountWithSeparators}${notANumberRegex}` + `${notANumberRegex}${totalAmountWithSeparators}${notANumberRegex}`, ); } diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 870ce9fbaa7..cebe2a2f46b 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -214,6 +214,9 @@ "context": "section description", "string": "Strategy defines the preference of warehouses for stock allocations and reservations." }, + "/Fa+RP": { + "string": "Couldn't load top products" + }, "/ILyIf": { "context": "tax classes menu header", "string": "Tax class label" @@ -241,6 +244,9 @@ "context": "page label", "string": "Hidden" }, + "/U8FUp": { + "string": "Couldn't load activities" + }, "/V7UOC": { "context": "unassign category from sale and save, button", "string": "Unassign and save" diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 63dd3823b4b..1cd20eac590 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -8843,8 +8843,8 @@ export function useCustomerGiftCardListLazyQuery(baseOptions?: ApolloReactHooks. export type CustomerGiftCardListQueryHookResult = ReturnType; export type CustomerGiftCardListLazyQueryHookResult = ReturnType; export type CustomerGiftCardListQueryResult = Apollo.QueryResult; -export const HomeDocument = gql` - query Home($channel: String!, $datePeriod: DateRangeInput!, $hasPermissionToManageProducts: Boolean!, $hasPermissionToManageOrders: Boolean!) { +export const HomeAnaliticsDocument = gql` + query HomeAnalitics($channel: String!, $datePeriod: DateRangeInput!, $hasPermissionToManageOrders: Boolean!) { salesToday: ordersTotal(period: TODAY, channel: $channel) @include(if: $hasPermissionToManageOrders) { gross { amount @@ -8854,18 +8854,93 @@ export const HomeDocument = gql` ordersToday: orders(filter: {created: $datePeriod}, channel: $channel) @include(if: $hasPermissionToManageOrders) { totalCount } - ordersToFulfill: orders(filter: {status: READY_TO_FULFILL}, channel: $channel) @include(if: $hasPermissionToManageOrders) { - totalCount - } - ordersToCapture: orders(filter: {status: READY_TO_CAPTURE}, channel: $channel) @include(if: $hasPermissionToManageOrders) { - totalCount - } - productsOutOfStock: products( - filter: {stockAvailability: OUT_OF_STOCK} - channel: $channel - ) { - totalCount +} + `; + +/** + * __useHomeAnaliticsQuery__ + * + * To run a query within a React component, call `useHomeAnaliticsQuery` and pass it any options that fit your needs. + * When your component renders, `useHomeAnaliticsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useHomeAnaliticsQuery({ + * variables: { + * channel: // value for 'channel' + * datePeriod: // value for 'datePeriod' + * hasPermissionToManageOrders: // value for 'hasPermissionToManageOrders' + * }, + * }); + */ +export function useHomeAnaliticsQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(HomeAnaliticsDocument, options); + } +export function useHomeAnaliticsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(HomeAnaliticsDocument, options); + } +export type HomeAnaliticsQueryHookResult = ReturnType; +export type HomeAnaliticsLazyQueryHookResult = ReturnType; +export type HomeAnaliticsQueryResult = Apollo.QueryResult; +export const HomeActivitiesDocument = gql` + query HomeActivities($hasPermissionToManageOrders: Boolean!) { + activities: homepageEvents(last: 10) @include(if: $hasPermissionToManageOrders) { + edges { + node { + amount + composedId + date + email + emailType + id + message + orderNumber + oversoldItems + quantity + type + user { + id + email + } + } + } } +} + `; + +/** + * __useHomeActivitiesQuery__ + * + * To run a query within a React component, call `useHomeActivitiesQuery` and pass it any options that fit your needs. + * When your component renders, `useHomeActivitiesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useHomeActivitiesQuery({ + * variables: { + * hasPermissionToManageOrders: // value for 'hasPermissionToManageOrders' + * }, + * }); + */ +export function useHomeActivitiesQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(HomeActivitiesDocument, options); + } +export function useHomeActivitiesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(HomeActivitiesDocument, options); + } +export type HomeActivitiesQueryHookResult = ReturnType; +export type HomeActivitiesLazyQueryHookResult = ReturnType; +export type HomeActivitiesQueryResult = Apollo.QueryResult; +export const HomeTopProductsDocument = gql` + query HomeTopProducts($channel: String!, $hasPermissionToManageProducts: Boolean!) { productTopToday: reportProductSales(period: TODAY, first: 5, channel: $channel) @include(if: $hasPermissionToManageProducts) { edges { node { @@ -8893,60 +8968,82 @@ export const HomeDocument = gql` } } } - activities: homepageEvents(last: 10) @include(if: $hasPermissionToManageOrders) { - edges { - node { - amount - composedId - date - email - emailType - id - message - orderNumber - oversoldItems - quantity - type - user { - id - email - } +} + `; + +/** + * __useHomeTopProductsQuery__ + * + * To run a query within a React component, call `useHomeTopProductsQuery` and pass it any options that fit your needs. + * When your component renders, `useHomeTopProductsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useHomeTopProductsQuery({ + * variables: { + * channel: // value for 'channel' + * hasPermissionToManageProducts: // value for 'hasPermissionToManageProducts' + * }, + * }); + */ +export function useHomeTopProductsQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(HomeTopProductsDocument, options); } - } +export function useHomeTopProductsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(HomeTopProductsDocument, options); + } +export type HomeTopProductsQueryHookResult = ReturnType; +export type HomeTopProductsLazyQueryHookResult = ReturnType; +export type HomeTopProductsQueryResult = Apollo.QueryResult; +export const HomeNotificationsDocument = gql` + query homeNotifications($channel: String!, $hasPermissionToManageOrders: Boolean!) { + ordersToFulfill: orders(filter: {status: READY_TO_FULFILL}, channel: $channel) @include(if: $hasPermissionToManageOrders) { + totalCount + } + ordersToCapture: orders(filter: {status: READY_TO_CAPTURE}, channel: $channel) @include(if: $hasPermissionToManageOrders) { + totalCount + } + productsOutOfStock: products( + filter: {stockAvailability: OUT_OF_STOCK} + channel: $channel + ) { + totalCount } } `; /** - * __useHomeQuery__ + * __useHomeNotificationsQuery__ * - * To run a query within a React component, call `useHomeQuery` and pass it any options that fit your needs. - * When your component renders, `useHomeQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useHomeNotificationsQuery` and pass it any options that fit your needs. + * When your component renders, `useHomeNotificationsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useHomeQuery({ + * const { data, loading, error } = useHomeNotificationsQuery({ * variables: { * channel: // value for 'channel' - * datePeriod: // value for 'datePeriod' - * hasPermissionToManageProducts: // value for 'hasPermissionToManageProducts' * hasPermissionToManageOrders: // value for 'hasPermissionToManageOrders' * }, * }); */ -export function useHomeQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { +export function useHomeNotificationsQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return ApolloReactHooks.useQuery(HomeDocument, options); + return ApolloReactHooks.useQuery(HomeNotificationsDocument, options); } -export function useHomeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { +export function useHomeNotificationsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return ApolloReactHooks.useLazyQuery(HomeDocument, options); + return ApolloReactHooks.useLazyQuery(HomeNotificationsDocument, options); } -export type HomeQueryHookResult = ReturnType; -export type HomeLazyQueryHookResult = ReturnType; -export type HomeQueryResult = Apollo.QueryResult; +export type HomeNotificationsQueryHookResult = ReturnType; +export type HomeNotificationsLazyQueryHookResult = ReturnType; +export type HomeNotificationsQueryResult = Apollo.QueryResult; export const MenuCreateDocument = gql` mutation MenuCreate($input: MenuCreateInput!) { menuCreate(input: $input) { diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index ee1084f3472..3799b06281c 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -10277,15 +10277,37 @@ export type CustomerGiftCardListQueryVariables = Exact<{ export type CustomerGiftCardListQuery = { __typename: 'Query', giftCards: { __typename: 'GiftCardCountableConnection', edges: Array<{ __typename: 'GiftCardCountableEdge', node: { __typename: 'GiftCard', id: string, last4CodeChars: string, expiryDate: any | null, isActive: boolean, currentBalance: { __typename: 'Money', amount: number, currency: string } } }> } | null }; -export type HomeQueryVariables = Exact<{ +export type HomeAnaliticsQueryVariables = Exact<{ channel: Scalars['String']; datePeriod: DateRangeInput; + hasPermissionToManageOrders: Scalars['Boolean']; +}>; + + +export type HomeAnaliticsQuery = { __typename: 'Query', salesToday: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } | null, ordersToday: { __typename: 'OrderCountableConnection', totalCount: number | null } | null }; + +export type HomeActivitiesQueryVariables = Exact<{ + hasPermissionToManageOrders: Scalars['Boolean']; +}>; + + +export type HomeActivitiesQuery = { __typename: 'Query', activities: { __typename: 'OrderEventCountableConnection', edges: Array<{ __typename: 'OrderEventCountableEdge', node: { __typename: 'OrderEvent', amount: number | null, composedId: string | null, date: any | null, email: string | null, emailType: OrderEventsEmailsEnum | null, id: string, message: string | null, orderNumber: string | null, oversoldItems: Array | null, quantity: number | null, type: OrderEventsEnum | null, user: { __typename: 'User', id: string, email: string } | null } }> } | null }; + +export type HomeTopProductsQueryVariables = Exact<{ + channel: Scalars['String']; hasPermissionToManageProducts: Scalars['Boolean']; +}>; + + +export type HomeTopProductsQuery = { __typename: 'Query', productTopToday: { __typename: 'ProductVariantCountableConnection', edges: Array<{ __typename: 'ProductVariantCountableEdge', node: { __typename: 'ProductVariant', id: string, quantityOrdered: number | null, revenue: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } | null, attributes: Array<{ __typename: 'SelectedAttribute', values: Array<{ __typename: 'AttributeValue', id: string, name: string | null }> }>, product: { __typename: 'Product', id: string, name: string, thumbnail: { __typename: 'Image', url: string } | null } } }> } | null }; + +export type HomeNotificationsQueryVariables = Exact<{ + channel: Scalars['String']; hasPermissionToManageOrders: Scalars['Boolean']; }>; -export type HomeQuery = { __typename: 'Query', salesToday: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } | null, ordersToday: { __typename: 'OrderCountableConnection', totalCount: number | null } | null, ordersToFulfill: { __typename: 'OrderCountableConnection', totalCount: number | null } | null, ordersToCapture: { __typename: 'OrderCountableConnection', totalCount: number | null } | null, productsOutOfStock: { __typename: 'ProductCountableConnection', totalCount: number | null } | null, productTopToday: { __typename: 'ProductVariantCountableConnection', edges: Array<{ __typename: 'ProductVariantCountableEdge', node: { __typename: 'ProductVariant', id: string, quantityOrdered: number | null, revenue: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } | null, attributes: Array<{ __typename: 'SelectedAttribute', values: Array<{ __typename: 'AttributeValue', id: string, name: string | null }> }>, product: { __typename: 'Product', id: string, name: string, thumbnail: { __typename: 'Image', url: string } | null } } }> } | null, activities: { __typename: 'OrderEventCountableConnection', edges: Array<{ __typename: 'OrderEventCountableEdge', node: { __typename: 'OrderEvent', amount: number | null, composedId: string | null, date: any | null, email: string | null, emailType: OrderEventsEmailsEnum | null, id: string, message: string | null, orderNumber: string | null, oversoldItems: Array | null, quantity: number | null, type: OrderEventsEnum | null, user: { __typename: 'User', id: string, email: string } | null } }> } | null }; +export type HomeNotificationsQuery = { __typename: 'Query', ordersToFulfill: { __typename: 'OrderCountableConnection', totalCount: number | null } | null, ordersToCapture: { __typename: 'OrderCountableConnection', totalCount: number | null } | null, productsOutOfStock: { __typename: 'ProductCountableConnection', totalCount: number | null } | null }; export type MenuCreateMutationVariables = Exact<{ input: MenuCreateInput; diff --git a/src/home/components/HomeActivityCard/HomeActivityCard.tsx b/src/home/components/HomeActivityCard/HomeActivityCard.tsx index 7877d1bd29c..1abfebf0499 100644 --- a/src/home/components/HomeActivityCard/HomeActivityCard.tsx +++ b/src/home/components/HomeActivityCard/HomeActivityCard.tsx @@ -1,8 +1,7 @@ import { DashboardCard } from "@dashboard/components/Card"; import { DateTime } from "@dashboard/components/Date"; -import Skeleton from "@dashboard/components/Skeleton"; -import { Activities } from "@dashboard/home/types"; -import { Box, List, Text, useTheme } from "@saleor/macaw-ui-next"; +import { Activities, HomeData } from "@dashboard/home/types"; +import { Box, List, Skeleton, Text, useTheme } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -10,7 +9,7 @@ import { renderCollection } from "../../../misc"; import { getActivityMessage } from "./activityMessages"; interface HomeActivityCardProps { - activities: Activities; + activities: HomeData; testId?: string; } @@ -20,20 +19,50 @@ export const HomeActivityCard = ({ }: HomeActivityCardProps) => { const intl = useIntl(); const { themeValues } = useTheme(); + const title = intl.formatMessage({ + id: "BXkF8Z", + defaultMessage: "Activity", + description: "header", + }); + + if (activities.hasError) { + return ( + + {title} + + + + + + + ); + } + + if (activities.loading) { + return ( + + {title} + + + + + + + + + ); + } return ( - - {intl.formatMessage({ - id: "BXkF8Z", - defaultMessage: "Activity", - description: "header", - })} - + {title} {renderCollection( - activities, + activities.data, (activity, activityId) => ( { switch (activity.type) { diff --git a/src/home/components/HomeNotificationList/HomeNotificationList.tsx b/src/home/components/HomeNotificationList/HomeNotificationList.tsx index 44caa9c31b6..61f239040b8 100644 --- a/src/home/components/HomeNotificationList/HomeNotificationList.tsx +++ b/src/home/components/HomeNotificationList/HomeNotificationList.tsx @@ -1,5 +1,6 @@ import RequirePermissions from "@dashboard/components/RequirePermissions"; import { PermissionEnum } from "@dashboard/graphql"; +import { HomeData, Notifications } from "@dashboard/home/types"; import { List } from "@saleor/macaw-ui-next"; import React from "react"; import { useIntl } from "react-intl"; @@ -13,9 +14,7 @@ import { } from "./utils"; interface HomeNotificationTableProps { - ordersToCapture: number; - ordersToFulfill: number; - productsOutOfStock: number; + notifications: HomeData; createNewChannelHref: string; ordersToFulfillHref: string; ordersToCaptureHref: string; @@ -24,24 +23,30 @@ interface HomeNotificationTableProps { } export const HomeNotificationList = ({ + notifications, createNewChannelHref, ordersToFulfillHref, ordersToCaptureHref, productsOutOfStockHref, - ordersToCapture, - ordersToFulfill, - productsOutOfStock, + noChannel, }: HomeNotificationTableProps) => { const intl = useIntl(); + if (notifications.hasError) { + return null; + } + return ( {noChannel && ( - + {intl.formatMessage(messages.createNewChannel)} @@ -49,17 +54,22 @@ export const HomeNotificationList = ({ - {getOrderToFulfillText(ordersToFulfill, intl)} + {getOrderToFulfillText(notifications.data.ordersToFulfill ?? 0, intl)} - {getOrdersToCaptureText(ordersToCapture, intl)} + {getOrdersToCaptureText( + notifications.data.ordersToCapture ?? 0, + intl, + )} @@ -67,10 +77,14 @@ export const HomeNotificationList = ({ requiredPermissions={[PermissionEnum.MANAGE_PRODUCTS]} > - {getProductsOutOfStockText(productsOutOfStock, intl)} + {getProductsOutOfStockText( + notifications.data.productsOutOfStock ?? 0, + intl, + )} diff --git a/src/home/components/HomeNotificationList/HomeNotificationListItem.tsx b/src/home/components/HomeNotificationList/HomeNotificationListItem.tsx index ac7a202e035..712eabe908e 100644 --- a/src/home/components/HomeNotificationList/HomeNotificationListItem.tsx +++ b/src/home/components/HomeNotificationList/HomeNotificationListItem.tsx @@ -2,6 +2,7 @@ import { Box, ChevronRightIcon, List, + Skeleton, sprinkles, Text, } from "@saleor/macaw-ui-next"; @@ -12,32 +13,60 @@ interface HomeNotificationListItemProps { dataTestId?: string; linkUrl: string; children: ReactNode; + loading: boolean; } export const HomeNotificationListItem = ({ dataTestId, linkUrl, children, -}: HomeNotificationListItemProps) => ( - - - { + if (loading) { + return ( + + + + + + ); + } + + return ( + + - {children} - - - - -); + + {children} + + + + + ); +}; + +function Listitem({ + children, + dataTestId, +}: { + children: ReactNode; + dataTestId?: string; +}) { + return ( + + {children} + + ); +} diff --git a/src/home/components/HomePage/HomePage.stories.tsx b/src/home/components/HomePage/HomePage.stories.tsx index 37a5bf7e8b9..598f2b8f1ec 100644 --- a/src/home/components/HomePage/HomePage.stories.tsx +++ b/src/home/components/HomePage/HomePage.stories.tsx @@ -2,28 +2,53 @@ import placeholderImage from "@assets/images/placeholder60x60.png"; import { adminUserPermissions } from "@dashboard/fixtures"; import { PermissionEnum } from "@dashboard/graphql"; -import { shop as shopFixture } from "@dashboard/home/fixtures"; +import { + activities, + analitics, + notifications, + topProducts as topProductsFixture, +} from "@dashboard/home/fixtures"; import { mapEdgesToItems } from "@dashboard/utils/maps"; import React from "react"; import { MockedUserProvider } from "../../../../.storybook/helpers"; import HomePageComponent, { HomePageProps } from "./HomePage"; -const shop = shopFixture(placeholderImage); +const productTopToday = topProductsFixture(placeholderImage); const homePageProps: Omit = { - activities: mapEdgesToItems(shop.activities), + activities: { + data: mapEdgesToItems(activities), + loading: false, + hasError: false, + }, + notifications: { + data: { + ordersToCapture: notifications.ordersToCapture.totalCount, + ordersToFulfill: notifications.ordersToFulfill.totalCount, + productsOutOfStock: notifications.productsOutOfStock.totalCount, + }, + loading: false, + hasError: false, + }, + analitics: { + data: { + orders: analitics.ordersToday.totalCount, + sales: analitics.salesToday.gross, + }, + loading: false, + hasError: false, + }, noChannel: false, createNewChannelHref: "", ordersToFulfillHref: "", ordersToCaptureHref: "", productsOutOfStockHref: "", - orders: shop.ordersToday.totalCount, - ordersToCapture: shop.ordersToCapture.totalCount, - ordersToFulfill: shop.ordersToFulfill.totalCount, - productsOutOfStock: shop.productsOutOfStock.totalCount, - sales: shop.salesToday.gross, - topProducts: mapEdgesToItems(shop.productTopToday), + topProducts: { + data: mapEdgesToItems(productTopToday), + loading: false, + hasError: false, + }, userName: "admin@example.com", }; @@ -46,19 +71,69 @@ export const Default = () => ; export const Loading = () => ( +); + +export const Error = () => ( + ); export const NoData = () => ( - + ); export const NoPermissions = () => ( diff --git a/src/home/components/HomePage/HomePage.tsx b/src/home/components/HomePage/HomePage.tsx index 6ac5b2a0821..56e8eb47ce2 100644 --- a/src/home/components/HomePage/HomePage.tsx +++ b/src/home/components/HomePage/HomePage.tsx @@ -3,10 +3,15 @@ import CardSpacer from "@dashboard/components/CardSpacer"; import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 { Activities, ProductTopToday } from "@dashboard/home/types"; -import { Box } from "@saleor/macaw-ui-next"; +import { PermissionEnum } from "@dashboard/graphql"; +import { + Activities, + Analitics, + HomeData, + Notifications, + ProductTopToday, +} from "@dashboard/home/types"; +import { Box, Skeleton } from "@saleor/macaw-ui-next"; import React from "react"; import { useIntl } from "react-intl"; @@ -18,13 +23,10 @@ import { HomeProductList } from "../HomeProductList"; import { homePageMessages } from "./messages"; export interface HomePageProps { - activities: Activities; - orders: number | null; - ordersToCapture: number | null; - ordersToFulfill: number | null; - productsOutOfStock: number; - sales: NonNullable["gross"]; - topProducts: ProductTopToday | null; + activities: HomeData; + analitics: HomeData; + topProducts: HomeData; + notifications: HomeData; userName: string; createNewChannelHref: string; ordersToFulfillHref: string; @@ -36,17 +38,14 @@ export interface HomePageProps { const HomePage: React.FC = props => { const { userName, - orders, - sales, + analitics, topProducts, activities, createNewChannelHref, ordersToFulfillHref, ordersToCaptureHref, productsOutOfStockHref, - ordersToCapture = 0, - ordersToFulfill = 0, - productsOutOfStock = 0, + notifications, noChannel, } = props; const intl = useIntl(); @@ -70,10 +69,10 @@ const HomePage: React.FC = props => { title={intl.formatMessage(homePageMessages.salesCardTitle)} testId="sales-analytics" > - {noChannel ? ( + {noChannel || analitics.hasError ? ( 0 - ) : sales ? ( - + ) : !analitics.loading ? ( + ) : ( )} @@ -82,10 +81,10 @@ const HomePage: React.FC = props => { title={intl.formatMessage(homePageMessages.ordersCardTitle)} testId="orders-analytics" > - {noChannel ? ( + {noChannel || analitics.hasError ? ( 0 - ) : orders !== undefined ? ( - orders + ) : !analitics.loading ? ( + analitics.data.orders ) : ( )} @@ -97,9 +96,7 @@ const HomePage: React.FC = props => { ordersToFulfillHref={ordersToFulfillHref} ordersToCaptureHref={ordersToCaptureHref} productsOutOfStockHref={productsOutOfStockHref} - ordersToCapture={ordersToCapture ?? 0} - ordersToFulfill={ordersToFulfill ?? 0} - productsOutOfStock={productsOutOfStock} + notifications={notifications} noChannel={noChannel} /> diff --git a/src/home/components/HomeProductList/HomeProductList.tsx b/src/home/components/HomeProductList/HomeProductList.tsx index d82a7c08060..263e96d6483 100644 --- a/src/home/components/HomeProductList/HomeProductList.tsx +++ b/src/home/components/HomeProductList/HomeProductList.tsx @@ -1,8 +1,7 @@ import Money from "@dashboard/components/Money"; -import Skeleton from "@dashboard/components/Skeleton"; -import { ProductTopToday } from "@dashboard/home/types"; +import { HomeData, ProductTopToday } from "@dashboard/home/types"; import { productVariantEditUrl } from "@dashboard/products/urls"; -import { Box, Text } from "@saleor/macaw-ui-next"; +import { Box, Skeleton, Text } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -12,7 +11,7 @@ import { generateAttributesInfo } from "./variant"; interface HomeProductListProps { testId?: string; - topProducts: ProductTopToday; + topProducts: HomeData; } export const HomeProductList = ({ @@ -20,19 +19,51 @@ export const HomeProductList = ({ testId, }: HomeProductListProps) => { const intl = useIntl(); + const title = intl.formatMessage({ + id: "e08xWz", + defaultMessage: "Top products", + description: "header", + }); + + if (topProducts.hasError) { + return ( + + + {title} + + + + + + ); + } + + if (topProducts.loading) { + return ( + + + {title} + + + + + + + + ); + } return ( - {intl.formatMessage({ - id: "e08xWz", - defaultMessage: "Top products", - description: "header", - })} + {title} {renderCollection( - topProducts, + topProducts.data, variant => ( + + + ); +} diff --git a/src/home/fixtures.ts b/src/home/fixtures.ts index d357a16010a..49836570245 100644 --- a/src/home/fixtures.ts +++ b/src/home/fixtures.ts @@ -1,430 +1,439 @@ // @ts-strict-ignore -import { HomeQuery, OrderEventsEnum } from "@dashboard/graphql"; +import { + HomeActivitiesQuery, + HomeAnaliticsQuery, + HomeNotificationsQuery, + HomeTopProductsQuery, + OrderEventsEnum, +} from "@dashboard/graphql"; -export const shop: (placeholderImage: string) => HomeQuery = ( - placeholderImage: string, -) => ({ +export const notifications: HomeNotificationsQuery = { __typename: "Query", - activities: { - __typename: "OrderEventCountableConnection", - edges: [ - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-09-14T16:10:27.137126+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDoxOA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + ordersToCapture: { + __typename: "OrderCountableConnection", + totalCount: 0, + }, + ordersToFulfill: { + __typename: "OrderCountableConnection", + totalCount: 1, + }, + + productsOutOfStock: { + __typename: "ProductCountableConnection", + totalCount: 0, + }, +}; + +export const activities: HomeActivitiesQuery["activities"] = { + __typename: "OrderEventCountableConnection", + edges: [ + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-09-14T16:10:27.137126+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDoxOA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-03T13:28:46.325279+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDozNQ==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-03T13:28:46.325279+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDozNQ==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-03T13:29:01.837496+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDozNw==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.ORDER_FULLY_PAID, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-03T13:29:01.837496+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDozNw==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.ORDER_FULLY_PAID, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-04T01:01:51.243723+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo1OA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-04T01:01:51.243723+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo1OA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-04T19:36:18.831561+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo2Nw==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-04T19:36:18.831561+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo2Nw==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-04T19:38:01.420365+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo2OA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-04T19:38:01.420365+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo2OA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-05T12:30:57.268592+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo3MQ==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-05T12:30:57.268592+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo3MQ==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-08T09:50:42.622253+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo3Mw==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-08T09:50:42.622253+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo3Mw==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-12T15:51:11.665838+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo3Nw==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-12T15:51:11.665838+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo3Nw==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-12T15:51:11.665838+00:00", + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-12T15:51:11.665838+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo3Nw==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", email: null, - emailType: null, - id: "T3JkZXJFdmVudDo3Nw==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: null, - id: "VXNlcjoyMQ==", - }, + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-25T11:25:58.843860+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo3OA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-25T11:25:58.843860+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo3OA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-26T09:34:57.580167+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo4MA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-26T09:34:57.580167+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo4MA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-26T09:38:02.440061+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo4Mg==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.ORDER_FULLY_PAID, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-26T09:38:02.440061+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo4Mg==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.ORDER_FULLY_PAID, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-26T09:38:02.467443+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo4NA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.ORDER_FULLY_PAID, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-26T09:38:02.467443+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo4NA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.ORDER_FULLY_PAID, + user: null, }, - ], - }, - ordersToCapture: { - __typename: "OrderCountableConnection", - totalCount: 0, - }, - ordersToFulfill: { - __typename: "OrderCountableConnection", - totalCount: 1, - }, - ordersToday: { - __typename: "OrderCountableConnection", - totalCount: 1, - }, - productTopToday: { - __typename: "ProductVariantCountableConnection", - edges: [ - { - __typename: "ProductVariantCountableEdge", - node: { - __typename: "ProductVariant", - attributes: [ - { - __typename: "SelectedAttribute", - values: [ - { - __typename: "AttributeValue", - id: "QXR0cmlidXRlVmFsdWU6OTI=", - name: "XL", - sortOrder: 0, - }, - ], - }, - ], - id: "UHJvZHVjdFZhcmlhbnQ6NDM=", - product: { - __typename: "Product", - id: "UHJvZHVjdDo4", - name: "Black Hoodie", - thumbnail: { - __typename: "Image", - url: placeholderImage, - }, + }, + ], +}; + +export const topProducts: ( + placeholderImage: string, +) => HomeTopProductsQuery["productTopToday"] = (placeholderImage: string) => ({ + __typename: "ProductVariantCountableConnection", + edges: [ + { + __typename: "ProductVariantCountableEdge", + node: { + __typename: "ProductVariant", + attributes: [ + { + __typename: "SelectedAttribute", + values: [ + { + __typename: "AttributeValue", + id: "QXR0cmlidXRlVmFsdWU6OTI=", + name: "XL", + sortOrder: 0, + }, + ], + }, + ], + id: "UHJvZHVjdFZhcmlhbnQ6NDM=", + product: { + __typename: "Product", + id: "UHJvZHVjdDo4", + name: "Black Hoodie", + thumbnail: { + __typename: "Image", + url: placeholderImage, }, - quantityOrdered: 1, - revenue: { - __typename: "TaxedMoney", - gross: { - __typename: "Money", - amount: 37.65, - currency: "USD", - }, + }, + 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, - }, + }, + { + __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", - }, + }, + 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, - }, + }, + { + __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, }, - quantityOrdered: 1, - revenue: { - __typename: "TaxedMoney", - gross: { - __typename: "Money", - amount: 37.65, - currency: "USD", - }, + }, + quantityOrdered: 1, + revenue: { + __typename: "TaxedMoney", + gross: { + __typename: "Money", + amount: 37.65, + currency: "USD", }, }, }, - ], - }, - productsOutOfStock: { - __typename: "ProductCountableConnection", - totalCount: 0, - }, + }, + ], +}); + +export const analitics: HomeAnaliticsQuery = { + __typename: "Query", salesToday: { __typename: "TaxedMoney", gross: { @@ -433,4 +442,8 @@ export const shop: (placeholderImage: string) => HomeQuery = ( currency: "USD", }, }, -}); + ordersToday: { + __typename: "OrderCountableConnection", + totalCount: 1, + }, +}; diff --git a/src/home/queries.ts b/src/home/queries.ts index de097bfa358..b831fc0226e 100644 --- a/src/home/queries.ts +++ b/src/home/queries.ts @@ -1,10 +1,9 @@ import { gql } from "@apollo/client"; -export const home = gql` - query Home( +export const homeAnalitics = gql` + query HomeAnalitics( $channel: String! $datePeriod: DateRangeInput! - $hasPermissionToManageProducts: Boolean! $hasPermissionToManageOrders: Boolean! ) { salesToday: ordersTotal(period: TODAY, channel: $channel) @@ -18,24 +17,41 @@ export const home = gql` @include(if: $hasPermissionToManageOrders) { totalCount } - ordersToFulfill: orders( - filter: { status: READY_TO_FULFILL } - channel: $channel - ) @include(if: $hasPermissionToManageOrders) { - totalCount - } - ordersToCapture: orders( - filter: { status: READY_TO_CAPTURE } - channel: $channel - ) @include(if: $hasPermissionToManageOrders) { - totalCount - } - productsOutOfStock: products( - filter: { stockAvailability: OUT_OF_STOCK } - channel: $channel - ) { - totalCount + } +`; + +export const homeActivities = gql` + query HomeActivities($hasPermissionToManageOrders: Boolean!) { + activities: homepageEvents(last: 10) + @include(if: $hasPermissionToManageOrders) { + edges { + node { + amount + composedId + date + email + emailType + id + message + orderNumber + oversoldItems + quantity + type + user { + id + email + } + } + } } + } +`; + +export const homeTopProducts = gql` + query HomeTopProducts( + $channel: String! + $hasPermissionToManageProducts: Boolean! + ) { productTopToday: reportProductSales( period: TODAY first: 5 @@ -67,27 +83,31 @@ export const home = gql` } } } - activities: homepageEvents(last: 10) - @include(if: $hasPermissionToManageOrders) { - edges { - node { - amount - composedId - date - email - emailType - id - message - orderNumber - oversoldItems - quantity - type - user { - id - email - } - } - } + } +`; + +export const homeNotifications = gql` + query homeNotifications( + $channel: String! + $hasPermissionToManageOrders: Boolean! + ) { + ordersToFulfill: orders( + filter: { status: READY_TO_FULFILL } + channel: $channel + ) @include(if: $hasPermissionToManageOrders) { + totalCount + } + ordersToCapture: orders( + filter: { status: READY_TO_CAPTURE } + channel: $channel + ) @include(if: $hasPermissionToManageOrders) { + totalCount + } + productsOutOfStock: products( + filter: { stockAvailability: OUT_OF_STOCK } + channel: $channel + ) { + totalCount } } `; diff --git a/src/home/types.ts b/src/home/types.ts index aca07b69469..56293d0df02 100644 --- a/src/home/types.ts +++ b/src/home/types.ts @@ -1,7 +1,30 @@ -import { HomeQuery } from "@dashboard/graphql"; +import { + HomeActivitiesQuery, + HomeAnaliticsQuery, + HomeTopProductsQuery, +} from "@dashboard/graphql"; import { RelayToFlat } from "@dashboard/types"; -export type Activities = RelayToFlat>; +export type Activities = RelayToFlat< + NonNullable +>; export type ProductTopToday = RelayToFlat< - NonNullable + NonNullable >; + +export interface Analitics { + orders: number | null; + sales: NonNullable["gross"]; +} + +export interface Notifications { + ordersToCapture: number | null; + ordersToFulfill: number | null; + productsOutOfStock: number; +} + +export interface HomeData { + data: T; + loading: boolean; + hasError: boolean; +} diff --git a/src/home/views/index.tsx b/src/home/views/index.tsx index 30e815a8b27..4d6e11b52ce 100644 --- a/src/home/views/index.tsx +++ b/src/home/views/index.tsx @@ -7,7 +7,10 @@ import { OrderStatusFilter, PermissionEnum, StockAvailability, - useHomeQuery, + useHomeActivitiesQuery, + useHomeAnaliticsQuery, + useHomeNotificationsQuery, + useHomeTopProductsQuery, } from "@dashboard/graphql"; import { mapEdgesToItems } from "@dashboard/utils/maps"; import React from "react"; @@ -24,28 +27,91 @@ const HomeSection = () => { const noChannel = !channel && typeof channel !== "undefined"; const userPermissions = user?.userPermissions || []; + const hasPermissionToManageOrders = hasPermissions(userPermissions, [ + PermissionEnum.MANAGE_ORDERS, + ]); + const hasPermissionToManageProducts = hasPermissions(userPermissions, [ + PermissionEnum.MANAGE_PRODUCTS, + ]); - const { data } = useHomeQuery({ - displayLoader: true, + const { + data: homeActivities, + loading: homeActivitiesLoading, + error: homeActivitiesError, + } = useHomeActivitiesQuery({ + skip: noChannel, + variables: { + hasPermissionToManageOrders, + }, + }); + + const { + data: homeTopProducts, + loading: homeTopProductsLoading, + error: homeTopProductsError, + } = useHomeTopProductsQuery({ + skip: noChannel, + variables: { + channel: channel?.slug, + hasPermissionToManageProducts, + }, + }); + + const { + data: homeNotificationsData, + loading: homeNotificationsLoaing, + error: homeNotificationsError, + } = useHomeNotificationsQuery({ + skip: noChannel, + variables: { + channel: channel?.slug, + hasPermissionToManageOrders, + }, + }); + + const { + data: homeAnaliticsData, + loading: homeAnaliticsLoading, + error: homeAnaliticsError, + } = useHomeAnaliticsQuery({ skip: noChannel, variables: { channel: channel?.slug, datePeriod: getDatePeriod(1), - hasPermissionToManageOrders: hasPermissions(userPermissions, [ - PermissionEnum.MANAGE_ORDERS, - ]), - hasPermissionToManageProducts: hasPermissions(userPermissions, [ - PermissionEnum.MANAGE_PRODUCTS, - ]), + hasPermissionToManageOrders, }, }); return ( { stockStatus: StockAvailability.OUT_OF_STOCK, channel: channel?.slug, })} - ordersToCapture={data?.ordersToCapture?.totalCount} - ordersToFulfill={data?.ordersToFulfill?.totalCount} - productsOutOfStock={data?.productsOutOfStock.totalCount} userName={getUserName(user, true)} noChannel={noChannel} />