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
This commit is contained in:
Marek Choiński 2021-01-12 15:44:11 +01:00 committed by GitHub
parent 9d55dc920b
commit 3f5cacb8a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 212 additions and 86 deletions

View file

@ -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"
},

View file

@ -12,6 +12,7 @@ const props: ChannelDeleteDialogProps = {
label: channel.name,
value: channel.id
})),
hasOrders: true,
confirmButtonState: "default",
onBack: () => undefined,
onClose: () => undefined,

View file

@ -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<ChannelDeleteDialogProps> = ({
channelsChoices = [],
hasOrders,
confirmButtonState,
open,
onBack,
@ -37,55 +65,48 @@ const ChannelDeleteDialog: React.FC<ChannelDeleteDialogProps> = ({
);
const hasChannels = !!channelsChoices?.length;
const canBeDeleted = hasChannels || !hasOrders;
return (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={() => (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"}
>
<div>
{hasChannels ? (
<>
{hasOrders ? (
hasChannels ? (
<>
<Typography>
{intl.formatMessage(messages.needToBeMoved)}
</Typography>
<div className={classes.select}>
<SingleSelectField
choices={channelsChoices}
name="channels"
label={intl.formatMessage(messages.selectChannel)}
value={choice}
onChange={e => setChoice(e.target.value)}
/>
</div>
<Typography>
{intl.formatMessage(messages.deletingAllProductData)}
</Typography>
</>
) : (
<Typography>
<FormattedMessage
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"
/>
{intl.formatMessage(messages.noAvailableChannel)}
</Typography>
<div className={classes.select}>
<SingleSelectField
choices={channelsChoices}
name="channels"
label={intl.formatMessage({
defaultMessage: "Select Channel",
description: "dialog header"
})}
value={choice}
onChange={e => setChoice(e.target.value)}
/>
</div>
<Typography>
<FormattedMessage
defaultMessage="Deleting channel will delete all product data regarding this channel. Are you sure you want to delete this channel?"
description="delete channel"
/>
</Typography>
</>
)
) : (
<Typography>
<FormattedMessage
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"
/>
{intl.formatMessage(messages.deletingAllProductData)}
</Typography>
)}
</div>

View file

@ -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",

View file

@ -17,9 +17,16 @@ import ChannelCreateComponent from "./views/ChannelCreate";
import ChannelDetailsComponent from "./views/ChannelDetails";
import ChannelsListComponent from "./views/ChannelsList";
const ChannelDetails: React.FC<RouteComponentProps<{ id: string }>> = ({
match
}) => <ChannelDetailsComponent id={decodeURIComponent(match.params.id)} />;
const ChannelDetails: React.FC<RouteComponentProps<any>> = ({ match }) => {
const params = parseQs(location.search.substr(1));
return (
<ChannelDetailsComponent
id={decodeURIComponent(match.params.id)}
params={params}
/>
);
};
const ChannelsList: React.FC<RouteComponentProps> = ({ location }) => {
const qs = parseQs(location.search.substr(1));

View file

@ -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
}

View file

@ -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<ChannelDetailsPageProps> = ({
errors,
onBack,
onSubmit,
onDelete,
saveButtonBarState,
updateChannelStatus
}) => {
@ -79,6 +81,7 @@ export const ChannelDetailsPage: React.FC<ChannelDetailsPageProps> = ({
<SaveButtonBar
onCancel={onBack}
onSave={submit}
onDelete={onDelete}
state={saveButtonBarState}
disabled={disabled || formDisabled || !onSubmit || !hasChanged}
/>

View file

@ -13,6 +13,7 @@ export interface Channel_channel {
name: string;
slug: string;
currencyCode: string;
hasOrders: boolean;
}
export interface Channel {

View file

@ -15,6 +15,7 @@ export interface ChannelActivate_channelActivate_channel {
name: string;
slug: string;
currencyCode: string;
hasOrders: boolean;
}
export interface ChannelActivate_channelActivate_errors {

View file

@ -15,6 +15,7 @@ export interface ChannelCreate_channelCreate_channel {
name: string;
slug: string;
currencyCode: string;
hasOrders: boolean;
}
export interface ChannelCreate_channelCreate_errors {

View file

@ -15,6 +15,7 @@ export interface ChannelDeactivate_channelDeactivate_channel {
name: string;
slug: string;
currencyCode: string;
hasOrders: boolean;
}
export interface ChannelDeactivate_channelDeactivate_errors {

View file

@ -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;
}

View file

@ -15,6 +15,7 @@ export interface ChannelUpdate_channelUpdate_channel {
name: string;
slug: string;
currencyCode: string;
hasOrders: boolean;
}
export interface ChannelUpdate_channelUpdate_errors {

View file

@ -13,6 +13,7 @@ export interface Channels_channels {
name: string;
slug: string;
currencyCode: string;
hasOrders: boolean;
}
export interface Channels {

View file

@ -11,6 +11,8 @@ export enum ChannelsListUrlSortField {
}
export type ChannelsListUrlSort = Sort<ChannelsListUrlSortField>;
export type ChannelsListUrlFilters = Filters<ChannelsListUrlFiltersEnum>;
export type ChannelUrlDialog = "remove";
export type ChannelUrlQueryParams = Dialog<ChannelUrlDialog>;
export type ChannelsListUrlDialog = "remove";
export type ChannelsListUrlQueryParams = Dialog<ChannelsListUrlDialog> &
ChannelsListUrlFilters &

View file

@ -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
)
)
: [];

View file

@ -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<ChannelDetailsProps> = ({ id }) => {
export const ChannelDetails: React.FC<ChannelDetailsProps> = ({
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<ChannelDetailsProps> = ({ 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 (
<>
<WindowTitle
@ -109,6 +168,7 @@ export const ChannelDetails: React.FC<ChannelDetailsProps> = ({ 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<ChannelDetailsProps> = ({ id }) => {
saveButtonBarState={updateChannelOpts.status}
/>
</Container>
<ChannelDeleteDialog
channelsChoices={channelsChoices}
hasOrders={data?.channel?.hasOrders}
open={params.action === "remove"}
confirmButtonState={deleteChannelOpts.status}
onBack={() => navigate(channelsListUrl())}
onClose={closeModal}
onConfirm={handleRemoveConfirm}
/>
</>
);
};

View file

@ -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<ChannelsListProps> = ({ 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<ChannelsListProps> = ({ params }) => {
{!!selectedChannel && (
<ChannelDeleteDialog
channelsChoices={channelsChoices}
hasOrders={selectedChannel.hasOrders}
open={params.action === "remove"}
confirmButtonState={deleteChannelOpts.status}
onBack={() => navigate(channelsListUrl())}

View file

@ -15,5 +15,6 @@ export const channelDetailsFragment = gql`
name
slug
currencyCode
hasOrders
}
`;

View file

@ -13,4 +13,5 @@ export interface ChannelDetailsFragment {
name: string;
slug: string;
currencyCode: string;
hasOrders: boolean;
}