Add gift card expired status (#1412)

* Add gift card expiry status to gift card details provider WIP

* Add gift card expiry handling in gift card update page header

* Add gift card expiry handling to gift card update details tag section & refactor

* Add gift card expiry handling to gift card update details expiry date section

* Add gift card expiry handling to gift card update metadata fields, add disabled prop to metadata cards

* Add comment

* Add gift card expiry handling to gift card list & refactor

* Change back some parts because of change of requirements

* Extract messages

* Remove comments, fix wrong expiry label being displayed in gift card list
This commit is contained in:
Magdalena Markusik 2021-09-20 15:41:18 +02:00 committed by GitHub
parent ffe44be733
commit 705df796d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 229 additions and 152 deletions

View file

@ -3546,10 +3546,18 @@
"context": "GiftCardUpdateDetailsCard set balance button label",
"string": "set balance"
},
"src_dot_giftCards_dot_GiftCardUpdate_dot_GiftCardUpdateDetailsCard_dot_tagInputLabel": {
"context": "GiftCardTagInput tag label",
"string": "Card Tag"
},
"src_dot_giftCards_dot_GiftCardUpdate_dot_GiftCardUpdateDetailsCard_dot_title": {
"context": "GiftCardUpdateDetailsCard title",
"string": "Details"
},
"src_dot_giftCards_dot_GiftCardUpdate_dot_GiftCardUpdateExpirySelect_dot_expiredOnLabel": {
"context": "GiftCarUpdateDetailsExpirySection expired on label",
"string": "Expired on {date}"
},
"src_dot_giftCards_dot_GiftCardUpdate_dot_GiftCardUpdateExpirySelect_dot_expiryDateCheckboxLabel": {
"context": "GiftCarUpdateDetailsExpirySection expiry date checkbox label",
"string": "Gift card expires"
@ -3590,6 +3598,14 @@
"context": "GiftCardUpdateInfoCard used by label",
"string": "Used by"
},
"src_dot_giftCards_dot_GiftCardUpdate_dot_GiftCardUpdatePageHeader_dot_disabledStatusLabel": {
"context": "giftCardUpdatePageHeader disabled status label",
"string": "Disabled"
},
"src_dot_giftCards_dot_GiftCardUpdate_dot_GiftCardUpdatePageHeader_dot_expiredStatusLabel": {
"context": "giftCardUpdatePageHeader expired status label",
"string": "Expired"
},
"src_dot_giftCards_dot_GiftCardUpdate_dot_GiftCardUpdatePageHeader_dot_resendButtonLabel": {
"context": "giftCardUpdatePageHeader resendButtonLabel",
"string": "Resend code"
@ -3654,10 +3670,6 @@
"context": "GiftCardsListHeader menu item settings",
"string": "Export card codes"
},
"src_dot_giftCards_dot_GiftCardsList_dot_giftCardDisabledLabel": {
"context": "GiftCardsListTable disabled label",
"string": "Disabled"
},
"src_dot_giftCards_dot_GiftCardsList_dot_giftCardProduct": {
"context": "GiftCardsListHeader alert",
"string": "gift card product"
@ -3754,10 +3766,6 @@
"context": "checkbox label",
"string": "Set gift card expiration period"
},
"src_dot_giftCards_dot_components_dot_GiftCardTagInput_dot_label": {
"context": "GiftCardTagInput tag label",
"string": "Card Tag"
},
"src_dot_giftCards_dot_components_dot_GiftCardTagInput_dot_placeholder": {
"context": "GiftCardTagInput tag placeholder",
"string": "Tag"

View file

@ -120,6 +120,7 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
/>
)}
</Typography>
<Typography color="textSecondary">
<FormattedMessage
defaultMessage="Use the button below to add new metadata field"

View file

@ -33,7 +33,7 @@ const GiftCardUpdateDetailsBalanceSection: React.FC = () => {
<HorizontalSpacer />
/
<HorizontalSpacer />
<Money money={initialBalance} />
<Money className={classes.balanceTotal} money={initialBalance} />
</div>
</div>
<CardSpacer />

View file

@ -1,4 +1,11 @@
import { Button, Card, CardContent, Divider } from "@material-ui/core";
import {
Button,
Card,
CardContent,
Divider,
Typography
} from "@material-ui/core";
import VerticalSpacer from "@saleor/apps/components/VerticalSpacer";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import Skeleton from "@saleor/components/Skeleton";
@ -16,9 +23,8 @@ import { giftCardUpdateDetailsCardMessages as messages } from "./messages";
const GiftCardUpdateDetailsCard: React.FC = () => {
const intl = useIntl();
const { loading } = useGiftCardDetails();
const { loading, giftCard } = useGiftCardDetails();
const { openSetBalanceDialog } = useGiftCardUpdateDialogs();
const {
change,
data: { tag },
@ -30,13 +36,16 @@ const GiftCardUpdateDetailsCard: React.FC = () => {
<CardTitle
title={intl.formatMessage(messages.title)}
toolbar={
<Button
data-test-id="set-balance-button"
color="primary"
onClick={openSetBalanceDialog}
>
{intl.formatMessage(messages.setBalanceButtonLabel)}
</Button>
!loading &&
!giftCard?.isExpired && (
<Button
data-test-id="set-balance-button"
color="primary"
onClick={openSetBalanceDialog}
>
{intl.formatMessage(messages.setBalanceButtonLabel)}
</Button>
)
}
/>
<CardContent>
@ -47,10 +56,13 @@ const GiftCardUpdateDetailsCard: React.FC = () => {
<CardSpacer />
<Divider />
<CardSpacer />
<Typography>
{intl.formatMessage(messages.tagInputLabel)}
</Typography>
<VerticalSpacer />
<GiftCardTagInput
error={formErrors?.tag}
name="tag"
withTopLabel
value={tag}
change={change}
/>

View file

@ -12,5 +12,9 @@ export const giftCardUpdateDetailsCardMessages = defineMessages({
cardBalanceLabel: {
defaultMessage: "Card Balance",
description: "GiftCardUpdateDetailsCard card balance label"
},
tagInputLabel: {
defaultMessage: "Card Tag",
description: "GiftCardTagInput tag label"
}
});

View file

@ -22,6 +22,9 @@ export const useGiftCardDetailsBalanceStyles = makeStyles(
height: 28,
borderRadius: 14,
backgroundColor: theme.palette.primary.light
},
balanceTotal: {
color: theme.palette.text.secondary
}
}),
{ name: "GiftCardUpdateDetailsBalanceSection" }

View file

@ -8,5 +8,9 @@ export const giftCardExpirySelectMessages = defineMessages({
expiryDateLabel: {
defaultMessage: "Expiration date",
description: "GiftCarUpdateDetailsExpirySection expiry date label"
},
expiredOnLabel: {
defaultMessage: "Expired on {date}",
description: "GiftCarUpdateDetailsExpirySection expired on label"
}
});

View file

@ -16,10 +16,9 @@ import React from "react";
import { MessageDescriptor, useIntl } from "react-intl";
import useGiftCardDetails from "../providers/GiftCardDetailsProvider/hooks/useGiftCardDetails";
import { PLACEHOLDER } from "../types";
import { giftCardUpdateInfoCardMessages as messages } from "./messages";
const PLACEHOLDER = "-";
const GiftCardUpdateInfoCardContent: React.FC = () => {
const intl = useIntl();
const localizeDate = useDateLocalize();

View file

@ -20,9 +20,13 @@ const GiftCardEnableDisableSection: React.FC = () => {
const intl = useIntl();
const {
giftCard: { id, isActive }
giftCard: { id, isActive, isExpired }
} = useGiftCardDetails();
if (isExpired) {
return null;
}
const onActivateCompleted = (data: GiftCardActivate) => {
const errors = data?.giftCardActivate?.errors;

View file

@ -25,42 +25,53 @@ const GiftCardUpdatePageHeader: React.FC = () => {
const { openResendCodeDialog } = useGiftCardUpdateDialogs();
const { displayCode, isActive } = giftCard;
const { displayCode, isActive, isExpired } = giftCard;
const title = intl.formatMessage(tableMessages.codeEndingWithLabel, {
displayCode
});
const getPageTitle = () => {
if (isExpired) {
return (
<PageTitleWithStatusChip
title={title}
statusLabel={intl.formatMessage(messages.expiredStatusLabel)}
statusType={StatusType.NEUTRAL}
/>
);
}
if (!isActive) {
return (
<PageTitleWithStatusChip
title={title}
statusLabel={intl.formatMessage(messages.disabledStatusLabel)}
statusType={StatusType.ERROR}
/>
);
}
return title;
};
return (
<>
<Backlink onClick={navigateBack}>
{intl.formatMessage(sectionNames.giftCards)}
</Backlink>
<PageHeader
inline
title={
isActive ? (
title
) : (
<PageTitleWithStatusChip
title={title}
statusLabel={intl.formatMessage(
tableMessages.giftCardDisabledLabel
)}
statusType={StatusType.ERROR}
/>
)
}
>
<PageHeader inline title={getPageTitle()}>
<GiftCardEnableDisableSection />
<HorizontalSpacer />
<Button
color="primary"
variant="contained"
onClick={openResendCodeDialog}
>
{intl.formatMessage(messages.resendButtonLabel)}
</Button>
{!isExpired && (
<Button
color="primary"
variant="contained"
onClick={openResendCodeDialog}
>
{intl.formatMessage(messages.resendButtonLabel)}
</Button>
)}
</PageHeader>
</>
);

View file

@ -15,5 +15,13 @@ export const giftCardUpdatePageHeaderMessages = defineMessages({
resendButtonLabel: {
defaultMessage: "Resend code",
description: "giftCardUpdatePageHeader resendButtonLabel"
},
expiredStatusLabel: {
defaultMessage: "Expired",
description: "giftCardUpdatePageHeader expired status label"
},
disabledStatusLabel: {
defaultMessage: "Disabled",
description: "giftCardUpdatePageHeader disabled status label"
}
});

View file

@ -2,6 +2,8 @@ import React, { createContext } from "react";
import { useGiftCardDetailsQuery } from "../../queries";
import { GiftCardDetails_giftCard } from "../../types/GiftCardDetails";
import { ExtendedGiftCard } from "./types";
import { getExtendedGiftCard } from "./utils";
interface GiftCardDetailsProviderProps {
children: React.ReactNode;
@ -9,7 +11,7 @@ interface GiftCardDetailsProviderProps {
}
export interface GiftCardDetailsConsumerProps {
giftCard: GiftCardDetails_giftCard;
giftCard: ExtendedGiftCard<GiftCardDetails_giftCard> | undefined;
loading: boolean;
}
@ -27,7 +29,7 @@ const GiftCardDetailsProvider: React.FC<GiftCardDetailsProviderProps> = ({
});
const providerValues: GiftCardDetailsConsumerProps = {
giftCard: data?.giftCard,
giftCard: getExtendedGiftCard(data?.giftCard),
loading
};

View file

@ -0,0 +1,7 @@
export type ExtendedGiftCard<T extends GiftCardBase> = T & {
isExpired: boolean;
};
export interface GiftCardBase {
expiryDate: string | undefined;
}

View file

@ -0,0 +1,26 @@
import moment from "moment";
import { ExtendedGiftCard, GiftCardBase } from "./types";
export function isGiftCardExpired<T extends GiftCardBase>(
giftCard: T
): boolean {
if (!giftCard?.expiryDate) {
return false;
}
return moment(giftCard?.expiryDate).isBefore(moment());
}
export function getExtendedGiftCard<T extends GiftCardBase>(
giftCard?: T
): ExtendedGiftCard<T> {
if (!giftCard) {
return undefined;
}
return {
...giftCard,
isExpired: isGiftCardExpired(giftCard)
};
}

View file

@ -9,3 +9,5 @@ export enum GiftCardUpdatePageActionParamsEnum {
export type GiftCardUpdatePageUrlQueryParams = Dialog<
GiftCardUpdatePageActionParamsEnum
>;
export const PLACEHOLDER = "-";

View file

@ -14,6 +14,7 @@ import Skeleton from "@saleor/components/Skeleton";
import StatusChip from "@saleor/components/StatusChip";
import { StatusType } from "@saleor/components/StatusChip/types";
import { customerUrl } from "@saleor/customers/urls";
import { PLACEHOLDER } from "@saleor/giftCards/GiftCardUpdate/types";
import { giftCardUrl } from "@saleor/giftCards/urls";
import useNavigator from "@saleor/hooks/useNavigator";
import { renderCollection } from "@saleor/misc";
@ -21,6 +22,7 @@ import { productUrl } from "@saleor/products/urls";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { giftCardUpdatePageHeaderMessages as giftCardStatusChipMessages } from "../../GiftCardUpdate/GiftCardUpdatePageHeader/messages";
import { giftCardsListTableMessages as messages } from "../messages";
import useGiftCardListDialogs from "../providers/GiftCardListDialogsProvider/hooks/useGiftCardListDialogs";
import useGiftCardList from "../providers/GiftCardListProvider/hooks/useGiftCardList";
@ -29,8 +31,6 @@ import { useTableStyles as useStyles } from "../styles";
import GiftCardsListTableFooter from "./GiftCardsListTableFooter";
import GiftCardsListTableHeader from "./GiftCardsListTableHeader";
const PLACEHOLDER = "-";
const GiftCardsListTable: React.FC = () => {
const intl = useIntl();
const classes = useStyles({});
@ -43,6 +43,38 @@ const GiftCardsListTable: React.FC = () => {
const redirectToGiftCardUpdate = (id: string) => () =>
navigate(giftCardUrl(id));
const selectGiftCardStatusChip = ({
isActive,
isExpired
}: {
isActive: boolean;
isExpired: boolean;
}) => {
if (isExpired) {
return (
<StatusChip
size="md"
status={StatusType.NEUTRAL}
label={intl.formatMessage(
giftCardStatusChipMessages.expiredStatusLabel
)}
/>
);
}
if (!isActive) {
return (
<StatusChip
size="md"
status={StatusType.ERROR}
label={intl.formatMessage(
giftCardStatusChipMessages.disabledStatusLabel
)}
/>
);
}
};
return (
<Card>
<ResponsiveTable>
@ -59,7 +91,8 @@ const GiftCardsListTable: React.FC = () => {
tag,
isActive,
product,
currentBalance
currentBalance,
isExpired
}) => (
<TableRow
onClick={redirectToGiftCardUpdate(id)}
@ -80,18 +113,10 @@ const GiftCardsListTable: React.FC = () => {
displayCode
})}
</Typography>
{!isActive && (
<>
<HorizontalSpacer spacing={2} />
<StatusChip
size="md"
status={StatusType.ERROR}
label={intl.formatMessage(
messages.giftCardDisabledLabel
)}
/>
</>
)}
<>
<HorizontalSpacer spacing={2} />
{selectGiftCardStatusChip({ isActive, isExpired })}
</>
</div>
</TableCell>
<TableCell>

View file

@ -75,10 +75,6 @@ export const giftCardsListTableMessages = defineMessages({
defaultMessage: "Code ending with {displayCode}",
description: "GiftCardsListTable code ending with label"
},
giftCardDisabledLabel: {
defaultMessage: "Disabled",
description: "GiftCardsListTable disabled label"
},
noGiftCardsFound: {
defaultMessage: "No gift cards found",
description: "GiftCardsListTable no cards found title"

View file

@ -1,3 +1,5 @@
import { ExtendedGiftCard } from "@saleor/giftCards/GiftCardUpdate/providers/GiftCardDetailsProvider/types";
import { getExtendedGiftCard } from "@saleor/giftCards/GiftCardUpdate/providers/GiftCardDetailsProvider/utils";
import useBulkActions, {
UseBulkActionsProps
} from "@saleor/hooks/useBulkActions";
@ -28,7 +30,7 @@ interface GiftCardsListProviderProps {
}
export interface GiftCardListDataProps {
giftCards: GiftCardList_giftCards_edges_node[];
giftCards: Array<ExtendedGiftCard<GiftCardList_giftCards_edges_node>>;
pageInfo: PageInfo;
loading: boolean;
params: GiftCardListUrlQueryParams;
@ -73,8 +75,11 @@ export const GiftCardsListProvider: React.FC<GiftCardsListProviderProps> = ({
variables: queryVariables
});
const giftCards =
mapEdgesToItems(data?.giftCards)?.map(getExtendedGiftCard) || [];
const providerValues: GiftCardsListConsumerProps = {
giftCards: mapEdgesToItems(data?.giftCards) || [],
giftCards,
loading,
isSelected,
listElements,

View file

@ -3,19 +3,32 @@ import { fragmentMoney } from "@saleor/fragments/products";
import makeQuery from "@saleor/hooks/makeQuery";
import gql from "graphql-tag";
import { giftCardDataFragment } from "../GiftCardUpdate/queries";
import { GiftCardList, GiftCardListVariables } from "./types/GiftCardList";
import { GiftCardProductsCount } from "./types/GiftCardProductsCount";
export const giftCardList = gql`
${fragmentUserBase}
${fragmentMoney}
${giftCardDataFragment}
query GiftCardList($first: Int, $after: String, $last: Int, $before: String) {
giftCards(first: $first, after: $after, before: $before, last: $last) {
edges {
node {
...GiftCardData
id
usedByEmail
displayCode
isActive
expiryDate
product {
id
name
}
tag
usedBy {
...UserBase
}
currentBalance {
...Money
}
}
}
pageInfo {
@ -27,6 +40,7 @@ export const giftCardList = gql`
}
}
`;
export const useGiftCardListQuery = makeQuery<
GiftCardList,
GiftCardListVariables
@ -42,6 +56,7 @@ export const giftCardProductsCount = gql`
}
}
`;
export const useGiftCardProductsCountQuery = makeQuery<
GiftCardProductsCount,
never

View file

@ -7,25 +7,6 @@
// GraphQL query operation: GiftCardList
// ====================================================
export interface GiftCardList_giftCards_edges_node_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface GiftCardList_giftCards_edges_node_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface GiftCardList_giftCards_edges_node_createdBy {
__typename: "User";
id: string;
firstName: string;
lastName: string;
}
export interface GiftCardList_giftCards_edges_node_product {
__typename: "Product";
id: string;
@ -39,18 +20,6 @@ export interface GiftCardList_giftCards_edges_node_usedBy {
lastName: string;
}
export interface GiftCardList_giftCards_edges_node_app {
__typename: "App";
id: string;
name: string | null;
}
export interface GiftCardList_giftCards_edges_node_initialBalance {
__typename: "Money";
amount: number;
currency: string;
}
export interface GiftCardList_giftCards_edges_node_currentBalance {
__typename: "Money";
amount: number;
@ -59,24 +28,15 @@ export interface GiftCardList_giftCards_edges_node_currentBalance {
export interface GiftCardList_giftCards_edges_node {
__typename: "GiftCard";
metadata: (GiftCardList_giftCards_edges_node_metadata | null)[];
privateMetadata: (GiftCardList_giftCards_edges_node_privateMetadata | null)[];
displayCode: string;
boughtInChannel: string | null;
createdBy: GiftCardList_giftCards_edges_node_createdBy | null;
product: GiftCardList_giftCards_edges_node_product | null;
usedBy: GiftCardList_giftCards_edges_node_usedBy | null;
usedByEmail: string | null;
createdByEmail: string | null;
app: GiftCardList_giftCards_edges_node_app | null;
created: any;
expiryDate: any | null;
lastUsedOn: any | null;
isActive: boolean;
initialBalance: GiftCardList_giftCards_edges_node_initialBalance | null;
currentBalance: GiftCardList_giftCards_edges_node_currentBalance | null;
id: string;
usedByEmail: string | null;
displayCode: string;
isActive: boolean;
expiryDate: any | null;
product: GiftCardList_giftCards_edges_node_product | null;
tag: string | null;
usedBy: GiftCardList_giftCards_edges_node_usedBy | null;
currentBalance: GiftCardList_giftCards_edges_node_currentBalance | null;
}
export interface GiftCardList_giftCards_edges {

View file

@ -1,5 +1,3 @@
import { Typography } from "@material-ui/core";
import VerticalSpacer from "@saleor/apps/components/VerticalSpacer";
import SingleAutocompleteSelectField, {
SingleAutocompleteSelectFieldProps
} from "@saleor/components/SingleAutocompleteSelectField";
@ -24,12 +22,10 @@ interface GiftCardTagInputProps
extends Pick<SingleAutocompleteSelectFieldProps, "name"> {
change: FormChange;
value: string;
withTopLabel?: boolean;
error: GiftCardError;
}
const GiftCardTagInput: React.FC<GiftCardTagInputProps> = ({
withTopLabel = false,
change,
name,
value,
@ -47,30 +43,22 @@ const GiftCardTagInput: React.FC<GiftCardTagInputProps> = ({
);
return (
<>
{withTopLabel && (
<>
<Typography>{intl.formatMessage(messages.label)}</Typography>
<VerticalSpacer />
</>
)}
<SingleAutocompleteSelectField
error={!!error}
helperText={getGiftCardErrorMessage(error, intl)}
allowCustomValues
name={name || "giftCardTag"}
label={`${intl.formatMessage(
messages.placeholder
)} *${intl.formatMessage(commonMessages.optionalField)}`}
data-test-id="gift-card-tag-select-field"
value={value}
displayValue={value}
choices={choices}
fetchChoices={search}
onChange={change}
onFetchMore={loadMore}
/>
</>
<SingleAutocompleteSelectField
error={!!error}
helperText={getGiftCardErrorMessage(error, intl)}
allowCustomValues
name={name || "giftCardTag"}
label={`${intl.formatMessage(messages.placeholder)} *${intl.formatMessage(
commonMessages.optionalField
)}`}
data-test-id="gift-card-tag-select-field"
value={value}
displayValue={value}
choices={choices}
fetchChoices={search}
onChange={change}
onFetchMore={loadMore}
/>
);
};

View file

@ -1,10 +1,6 @@
import { defineMessages } from "react-intl";
export const giftCardTagInputMessages = defineMessages({
label: {
defaultMessage: "Card Tag",
description: "GiftCardTagInput tag label"
},
placeholder: {
defaultMessage: "Tag",
description: "GiftCardTagInput tag placeholder"

View file

@ -44,6 +44,7 @@ export function mapNodeToChoice<
T extends ExtendedNode | Node,
K extends ChoiceValue
>(nodes: T[], getterFn: (node: T) => K): Array<SingleAutocompleteChoiceType<K>>;
export function mapNodeToChoice<T extends ExtendedNode>(
nodes: T[],
getterFn?: (node: T) => any