From 3f5cacb8a0df16ec6ee144c4f569fd32221adbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Choi=C5=84ski?= Date: Tue, 12 Jan 2021 15:44:11 +0100 Subject: [PATCH] Saleor 1906 user should be able to delete a channel if there are no orders associated with it (#901) * Update type of channelDetailsFragment * Add hasOrder recognition * Update types * Add delete channel on details page * Refactor fixtures * Update locale * Refactor * Remove redundant assignment * Move messages to defineMessages * Simplify variables * Create util getChannelsCurrencyChoices * Add message * Update locale * Refactor to mapNodeToChoice --- locale/defaultMessages.json | 25 ++--- .../ChannelDeleteDialog.stories.tsx | 1 + .../ChannelDeleteDialog.tsx | 95 +++++++++++-------- src/channels/fixtures.ts | 8 ++ src/channels/index.tsx | 13 ++- src/channels/mutations.ts | 6 +- .../ChannelDetailsPage/ChannelDetailsPage.tsx | 3 + src/channels/types/Channel.ts | 1 + src/channels/types/ChannelActivate.ts | 1 + src/channels/types/ChannelCreate.ts | 1 + src/channels/types/ChannelDeactivate.ts | 1 + src/channels/types/ChannelDelete.ts | 12 +-- src/channels/types/ChannelUpdate.ts | 1 + src/channels/types/Channels.ts | 1 + src/channels/urls.ts | 2 + src/channels/utils.ts | 16 ++++ .../views/ChannelDetails/ChannelDetails.tsx | 75 ++++++++++++++- .../views/ChannelsList/ChannelsList.tsx | 34 +++---- src/fragments/channels.ts | 1 + src/fragments/types/ChannelDetailsFragment.ts | 1 + 20 files changed, 212 insertions(+), 86 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index d80faf95c..659a46a5c 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1165,26 +1165,26 @@ "context": "channels section name", "string": "Channels" }, - "src_dot_channels_dot_components_dot_ChannelDeleteDialog_dot_2257704694": { - "context": "delete channel", - "string": "All order information from this channel need to be moved to a different channel. Please select channel orders need to be moved to:." - }, - "src_dot_channels_dot_components_dot_ChannelDeleteDialog_dot_2342703674": { - "context": "dialog header", - "string": "Select Channel" - }, - "src_dot_channels_dot_components_dot_ChannelDeleteDialog_dot_2405647976": { + "src_dot_channels_dot_components_dot_ChannelDeleteDialog_dot_deleteChannel": { "context": "dialog header", "string": "Delete Channel" }, - "src_dot_channels_dot_components_dot_ChannelDeleteDialog_dot_777038435": { + "src_dot_channels_dot_components_dot_ChannelDeleteDialog_dot_deletingAllProductData": { "context": "delete channel", "string": "Deleting channel will delete all product data regarding this channel. Are you sure you want to delete this channel?" }, - "src_dot_channels_dot_components_dot_ChannelDeleteDialog_dot_863901": { + "src_dot_channels_dot_components_dot_ChannelDeleteDialog_dot_needToBeMoved": { + "context": "delete channel", + "string": "All order information from this channel need to be moved to a different channel. Please select channel orders need to be moved to:." + }, + "src_dot_channels_dot_components_dot_ChannelDeleteDialog_dot_noAvailableChannel": { "context": "currency channel", "string": "There is no available channel to move order information to. Please create a channel with same currency so that information can be moved to it." }, + "src_dot_channels_dot_components_dot_ChannelDeleteDialog_dot_selectChannel": { + "context": "dialog header", + "string": "Select Channel" + }, "src_dot_channels_dot_components_dot_ChannelForm_dot_1223259680": { "context": "channel settings", "string": "Channel Settings" @@ -1276,6 +1276,9 @@ "context": "window title", "string": "Channel details" }, + "src_dot_channels_dot_views_dot_ChannelDetails_dot_3499322424": { + "string": "Channel deleted" + }, "src_dot_channels_dot_views_dot_ChannelsList_dot_3499322424": { "string": "Channel deleted" }, diff --git a/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.stories.tsx b/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.stories.tsx index efcd253e8..fa14df20e 100644 --- a/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.stories.tsx +++ b/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.stories.tsx @@ -12,6 +12,7 @@ const props: ChannelDeleteDialogProps = { label: channel.name, value: channel.id })), + hasOrders: true, confirmButtonState: "default", onBack: () => undefined, onClose: () => undefined, diff --git a/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.tsx b/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.tsx index 58c228100..c97df56eb 100644 --- a/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.tsx +++ b/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.tsx @@ -8,12 +8,39 @@ import { import useStateFromProps from "@saleor/hooks/useStateFromProps"; import { buttonMessages } from "@saleor/intl"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { defineMessages, useIntl } from "react-intl"; import { useStyles } from "../styles"; +const messages = defineMessages({ + needToBeMoved: { + defaultMessage: + "All order information from this channel need to be moved to a different channel. Please select channel orders need to be moved to:.", + description: "delete channel" + }, + deletingAllProductData: { + defaultMessage: + "Deleting channel will delete all product data regarding this channel. Are you sure you want to delete this channel?", + description: "delete channel" + }, + noAvailableChannel: { + defaultMessage: + "There is no available channel to move order information to. Please create a channel with same currency so that information can be moved to it.", + description: "currency channel" + }, + selectChannel: { + defaultMessage: "Select Channel", + description: "dialog header" + }, + deleteChannel: { + defaultMessage: "Delete Channel", + description: "dialog header" + } +}); + export interface ChannelDeleteDialogProps { channelsChoices: Choices; + hasOrders: boolean; confirmButtonState: ConfirmButtonTransitionState; open: boolean; onBack: () => void; @@ -23,6 +50,7 @@ export interface ChannelDeleteDialogProps { const ChannelDeleteDialog: React.FC = ({ channelsChoices = [], + hasOrders, confirmButtonState, open, onBack, @@ -37,55 +65,48 @@ const ChannelDeleteDialog: React.FC = ({ ); const hasChannels = !!channelsChoices?.length; + const canBeDeleted = hasChannels || !hasOrders; + return ( (hasChannels ? onConfirm(choice) : onBack())} - title={intl.formatMessage({ - defaultMessage: "Delete Channel", - description: "dialog header" - })} + onConfirm={() => (canBeDeleted ? onConfirm(choice) : onBack())} + title={intl.formatMessage(messages.deleteChannel)} confirmButtonLabel={intl.formatMessage( - hasChannels ? buttonMessages.delete : buttonMessages.ok + canBeDeleted ? buttonMessages.delete : buttonMessages.ok )} - variant={hasChannels ? "delete" : "default"} + variant={canBeDeleted ? "delete" : "default"} >
- {hasChannels ? ( - <> + {hasOrders ? ( + hasChannels ? ( + <> + + {intl.formatMessage(messages.needToBeMoved)} + +
+ setChoice(e.target.value)} + /> +
+ + {intl.formatMessage(messages.deletingAllProductData)} + + + ) : ( - + {intl.formatMessage(messages.noAvailableChannel)} -
- setChoice(e.target.value)} - /> -
- - - - + ) ) : ( - + {intl.formatMessage(messages.deletingAllProductData)} )}
diff --git a/src/channels/fixtures.ts b/src/channels/fixtures.ts index a471fc1eb..f040c41cf 100644 --- a/src/channels/fixtures.ts +++ b/src/channels/fixtures.ts @@ -18,6 +18,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + hasOrders: false, id: "Q2hhbm5lcDoy", isActive: true, name: "Test", @@ -26,6 +27,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + hasOrders: false, id: "Q2hhbm7lbDoy213", isActive: true, name: "Channel", @@ -34,6 +36,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + hasOrders: false, id: "Q2hhbn5lbDoytr", isActive: true, name: "Channel test", @@ -42,6 +45,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + hasOrders: false, id: "Q2hhbm5lbDo5bot", isActive: true, name: "Channel USD", @@ -50,6 +54,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + hasOrders: false, id: "Q2hhbm7lbDoyr0tr", isActive: true, name: "Channel", @@ -58,6 +63,7 @@ export const channelsList: Channels_channels[] = [ { __typename: "Channel", currencyCode: "euro", + hasOrders: false, id: "Q2hhbn5lbDoyya", isActive: true, name: "Channel test", @@ -67,6 +73,7 @@ export const channelsList: Channels_channels[] = [ __typename: "Channel", currencyCode: "euro", id: "Q2hhbm5lbDo5w0z", + hasOrders: false, isActive: true, name: "Channel USD", slug: "channel-usd1" @@ -76,6 +83,7 @@ export const channelsList: Channels_channels[] = [ export const channel: Channel_channel = { __typename: "Channel", currencyCode: "zl", + hasOrders: false, id: "Q2hhbm5lbDov78", isActive: true, name: "Test", diff --git a/src/channels/index.tsx b/src/channels/index.tsx index 209f41531..0aebcd7ca 100644 --- a/src/channels/index.tsx +++ b/src/channels/index.tsx @@ -17,9 +17,16 @@ import ChannelCreateComponent from "./views/ChannelCreate"; import ChannelDetailsComponent from "./views/ChannelDetails"; import ChannelsListComponent from "./views/ChannelsList"; -const ChannelDetails: React.FC> = ({ - match -}) => ; +const ChannelDetails: React.FC> = ({ match }) => { + const params = parseQs(location.search.substr(1)); + + return ( + + ); +}; const ChannelsList: React.FC = ({ location }) => { const qs = parseQs(location.search.substr(1)); diff --git a/src/channels/mutations.ts b/src/channels/mutations.ts index 5f80ebc06..8ac075f08 100644 --- a/src/channels/mutations.ts +++ b/src/channels/mutations.ts @@ -49,12 +49,8 @@ export const channelUpdateMutation = gql` export const channelDeleteMutation = gql` ${channelErrorFragment} - ${channelDetailsFragment} - mutation ChannelDelete($id: ID!, $input: ChannelDeleteInput!) { + mutation ChannelDelete($id: ID!, $input: ChannelDeleteInput) { channelDelete(id: $id, input: $input) { - channel { - ...ChannelDetailsFragment - } errors: channelErrors { ...ChannelErrorFragment } diff --git a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx index 377382f02..4a668a4e5 100644 --- a/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx +++ b/src/channels/pages/ChannelDetailsPage/ChannelDetailsPage.tsx @@ -19,6 +19,7 @@ export interface ChannelDetailsPageProps { errors: ChannelErrorFragment[]; saveButtonBarState: ConfirmButtonTransitionState; onBack?: () => void; + onDelete?: () => void; onSubmit?: (data: FormData) => void; updateChannelStatus?: () => void; } @@ -37,6 +38,7 @@ export const ChannelDetailsPage: React.FC = ({ errors, onBack, onSubmit, + onDelete, saveButtonBarState, updateChannelStatus }) => { @@ -79,6 +81,7 @@ export const ChannelDetailsPage: React.FC = ({ diff --git a/src/channels/types/Channel.ts b/src/channels/types/Channel.ts index e0cbc55c6..e5614b134 100644 --- a/src/channels/types/Channel.ts +++ b/src/channels/types/Channel.ts @@ -13,6 +13,7 @@ export interface Channel_channel { name: string; slug: string; currencyCode: string; + hasOrders: boolean; } export interface Channel { diff --git a/src/channels/types/ChannelActivate.ts b/src/channels/types/ChannelActivate.ts index d011227a9..4de493d03 100644 --- a/src/channels/types/ChannelActivate.ts +++ b/src/channels/types/ChannelActivate.ts @@ -15,6 +15,7 @@ export interface ChannelActivate_channelActivate_channel { name: string; slug: string; currencyCode: string; + hasOrders: boolean; } export interface ChannelActivate_channelActivate_errors { diff --git a/src/channels/types/ChannelCreate.ts b/src/channels/types/ChannelCreate.ts index 5437a1d29..64ba7b111 100644 --- a/src/channels/types/ChannelCreate.ts +++ b/src/channels/types/ChannelCreate.ts @@ -15,6 +15,7 @@ export interface ChannelCreate_channelCreate_channel { name: string; slug: string; currencyCode: string; + hasOrders: boolean; } export interface ChannelCreate_channelCreate_errors { diff --git a/src/channels/types/ChannelDeactivate.ts b/src/channels/types/ChannelDeactivate.ts index d4bee0c93..d7f58e281 100644 --- a/src/channels/types/ChannelDeactivate.ts +++ b/src/channels/types/ChannelDeactivate.ts @@ -15,6 +15,7 @@ export interface ChannelDeactivate_channelDeactivate_channel { name: string; slug: string; currencyCode: string; + hasOrders: boolean; } export interface ChannelDeactivate_channelDeactivate_errors { diff --git a/src/channels/types/ChannelDelete.ts b/src/channels/types/ChannelDelete.ts index 7dbcef156..6e52a7130 100644 --- a/src/channels/types/ChannelDelete.ts +++ b/src/channels/types/ChannelDelete.ts @@ -8,15 +8,6 @@ import { ChannelDeleteInput, ChannelErrorCode } from "./../../types/globalTypes" // GraphQL mutation operation: ChannelDelete // ==================================================== -export interface ChannelDelete_channelDelete_channel { - __typename: "Channel"; - id: string; - isActive: boolean; - name: string; - slug: string; - currencyCode: string; -} - export interface ChannelDelete_channelDelete_errors { __typename: "ChannelError"; code: ChannelErrorCode; @@ -26,7 +17,6 @@ export interface ChannelDelete_channelDelete_errors { export interface ChannelDelete_channelDelete { __typename: "ChannelDelete"; - channel: ChannelDelete_channelDelete_channel | null; errors: ChannelDelete_channelDelete_errors[]; } @@ -36,5 +26,5 @@ export interface ChannelDelete { export interface ChannelDeleteVariables { id: string; - input: ChannelDeleteInput; + input?: ChannelDeleteInput | null; } diff --git a/src/channels/types/ChannelUpdate.ts b/src/channels/types/ChannelUpdate.ts index 26ef58efd..911ba7565 100644 --- a/src/channels/types/ChannelUpdate.ts +++ b/src/channels/types/ChannelUpdate.ts @@ -15,6 +15,7 @@ export interface ChannelUpdate_channelUpdate_channel { name: string; slug: string; currencyCode: string; + hasOrders: boolean; } export interface ChannelUpdate_channelUpdate_errors { diff --git a/src/channels/types/Channels.ts b/src/channels/types/Channels.ts index eb551cf26..6b231deff 100644 --- a/src/channels/types/Channels.ts +++ b/src/channels/types/Channels.ts @@ -13,6 +13,7 @@ export interface Channels_channels { name: string; slug: string; currencyCode: string; + hasOrders: boolean; } export interface Channels { diff --git a/src/channels/urls.ts b/src/channels/urls.ts index 735d00333..7629cf0e5 100644 --- a/src/channels/urls.ts +++ b/src/channels/urls.ts @@ -11,6 +11,8 @@ export enum ChannelsListUrlSortField { } export type ChannelsListUrlSort = Sort; export type ChannelsListUrlFilters = Filters; +export type ChannelUrlDialog = "remove"; +export type ChannelUrlQueryParams = Dialog; export type ChannelsListUrlDialog = "remove"; export type ChannelsListUrlQueryParams = Dialog & ChannelsListUrlFilters & diff --git a/src/channels/utils.ts b/src/channels/utils.ts index 8961b0a66..120c1cd45 100644 --- a/src/channels/utils.ts +++ b/src/channels/utils.ts @@ -6,6 +6,7 @@ import { RequireOnlyOne } from "@saleor/misc"; import { ProductDetails_product } from "@saleor/products/types/ProductDetails"; import { ProductVariantDetails_productVariant } from "@saleor/products/types/ProductVariantDetails"; import { ShippingZone_shippingZone_shippingMethods_channelListings } from "@saleor/shipping/types/ShippingZone"; +import { mapNodeToChoice } from "@saleor/utils/maps"; import uniqBy from "lodash-es/uniqBy"; export interface Channel { @@ -304,3 +305,18 @@ export const createSortedChannelsDataFromSale = (data?: SaleDetails_sale) => createChannelsDataFromSale(data)?.sort((channel, nextChannel) => channel.name.localeCompare(nextChannel.name) ); + +export const getChannelsCurrencyChoices = ( + id: string, + selectedChannel: Channels_channels, + channelsList: Channels_channels[] +) => + id + ? mapNodeToChoice( + channelsList?.filter( + channel => + channel.id !== id && + channel.currencyCode === selectedChannel?.currencyCode + ) + ) + : []; diff --git a/src/channels/views/ChannelDetails/ChannelDetails.tsx b/src/channels/views/ChannelDetails/ChannelDetails.tsx index 89b67e228..6e2bde8ef 100644 --- a/src/channels/views/ChannelDetails/ChannelDetails.tsx +++ b/src/channels/views/ChannelDetails/ChannelDetails.tsx @@ -1,3 +1,6 @@ +import ChannelDeleteDialog from "@saleor/channels/components/ChannelDeleteDialog"; +import { ChannelDelete } from "@saleor/channels/types/ChannelDelete"; +import { getChannelsCurrencyChoices } from "@saleor/channels/utils"; import AppHeader from "@saleor/components/AppHeader"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; @@ -8,6 +11,7 @@ import useNotifier from "@saleor/hooks/useNotifier"; import { commonMessages } from "@saleor/intl"; import { sectionNames } from "@saleor/intl"; import getChannelsErrorMessage from "@saleor/utils/errors/channels"; +import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import React from "react"; import { useIntl } from "react-intl"; @@ -15,24 +19,41 @@ import { ChannelUpdateInput } from "../../../types/globalTypes"; import { useChannelActivateMutation, useChannelDeactivateMutation, + useChannelDeleteMutation, useChannelUpdateMutation } from "../../mutations"; import ChannelDetailsPage from "../../pages/ChannelDetailsPage"; -import { useChannelDetails } from "../../queries"; +import { useChannelDetails, useChannelsList } from "../../queries"; import { ChannelUpdate } from "../../types/ChannelUpdate"; -import { channelsListUrl } from "../../urls"; +import { + channelsListUrl, + channelUrl, + ChannelUrlDialog, + ChannelUrlQueryParams +} from "../../urls"; interface ChannelDetailsProps { id: string; + params: ChannelUrlQueryParams; } -export const ChannelDetails: React.FC = ({ id }) => { +export const ChannelDetails: React.FC = ({ + id, + params +}) => { const navigate = useNavigator(); const notify = useNotifier(); const intl = useIntl(); const handleBack = () => navigate(channelsListUrl()); + const channelsListData = useChannelsList({ displayLoader: true }); + + const [openModal, closeModal] = createDialogActionHandlers< + ChannelUrlDialog, + ChannelUrlQueryParams + >(navigate, params => channelUrl(id, params), params); + const onSubmit = (data: ChannelUpdate) => { if (!data.channelUpdate.errors.length) { notify({ @@ -87,6 +108,44 @@ export const ChannelDetails: React.FC = ({ id }) => { } }); + const onCompleted = (data: ChannelDelete) => { + const errors = data.channelDelete.errors; + if (errors.length === 0) { + notify({ + status: "success", + text: intl.formatMessage({ + defaultMessage: "Channel deleted" + }) + }); + closeModal(); + navigate(channelsListUrl()); + } else { + errors.map(error => + notify({ + status: "error", + text: getChannelsErrorMessage(error, intl) + }) + ); + } + }; + + const [deleteChannel, deleteChannelOpts] = useChannelDeleteMutation({ + onCompleted + }); + + const channelsChoices = getChannelsCurrencyChoices( + id, + data?.channel, + channelsListData?.data?.channels + ); + + const handleRemoveConfirm = (targetChannelId?: string) => { + const data = targetChannelId + ? { id, input: { targetChannel: targetChannelId } } + : { id }; + deleteChannel({ variables: data }); + }; + return ( <> = ({ id }) => { errors={updateChannelOpts?.data?.channelUpdate?.errors || []} onSubmit={handleSubmit} onBack={handleBack} + onDelete={() => openModal("remove")} updateChannelStatus={() => data?.channel?.isActive ? deactivateChannel({ variables: { id } }) @@ -117,6 +177,15 @@ export const ChannelDetails: React.FC = ({ id }) => { saveButtonBarState={updateChannelOpts.status} /> + navigate(channelsListUrl())} + onClose={closeModal} + onConfirm={handleRemoveConfirm} + /> ); }; diff --git a/src/channels/views/ChannelsList/ChannelsList.tsx b/src/channels/views/ChannelsList/ChannelsList.tsx index 52b7fcf87..1a658640b 100644 --- a/src/channels/views/ChannelsList/ChannelsList.tsx +++ b/src/channels/views/ChannelsList/ChannelsList.tsx @@ -1,3 +1,4 @@ +import { getChannelsCurrencyChoices } from "@saleor/channels/utils"; import { configurationMenuUrl } from "@saleor/configuration"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; @@ -64,25 +65,25 @@ export const ChannelsList: React.FC = ({ params }) => { onCompleted }); - const channelsChoices = params.id - ? data?.channels - ?.filter( - channel => - channel.id !== params.id && - channel.currencyCode === selectedChannel.currencyCode - ) - .map(channel => ({ - label: channel.name, - value: channel.id - })) - : []; + const channelsChoices = getChannelsCurrencyChoices( + params.id, + selectedChannel, + data?.channels + ); const navigateToChannelCreate = () => navigate(channelAddUrl); - const handleRemoveConfirm = (id: string) => - deleteChannel({ - variables: { id: params.id, input: { targetChannel: id } } - }); + const handleRemoveConfirm = (targetChannelId?: string) => { + if (targetChannelId) { + deleteChannel({ + variables: { id: params.id, input: { targetChannel: targetChannelId } } + }); + } else { + deleteChannel({ + variables: { id: params.id } + }); + } + }; return ( <> @@ -101,6 +102,7 @@ export const ChannelsList: React.FC = ({ params }) => { {!!selectedChannel && ( navigate(channelsListUrl())} diff --git a/src/fragments/channels.ts b/src/fragments/channels.ts index 1aebf0016..20b468ded 100644 --- a/src/fragments/channels.ts +++ b/src/fragments/channels.ts @@ -15,5 +15,6 @@ export const channelDetailsFragment = gql` name slug currencyCode + hasOrders } `; diff --git a/src/fragments/types/ChannelDetailsFragment.ts b/src/fragments/types/ChannelDetailsFragment.ts index 01999b257..2c9f7dcab 100644 --- a/src/fragments/types/ChannelDetailsFragment.ts +++ b/src/fragments/types/ChannelDetailsFragment.ts @@ -13,4 +13,5 @@ export interface ChannelDetailsFragment { name: string; slug: string; currencyCode: string; + hasOrders: boolean; }