Add metadata (#670)

* Add metadata editor component

* Add tests

* Fix plurals

* Use pascal case in selectors

* Update product metadata

* Add metadata handler decorator

* Update snapshots

* wip

* Remove operation provider component

* Add metadata to collections

* Add metadata editor to variant

* Add metadata editor to categories

* Add metadata to product types

* Simplify code

* Add metadata to attributes

* Drop maybe

* Rename Metadata to MetadataFragment

* Update changelog and snapshots
This commit is contained in:
Dominik Żegleń 2020-08-28 14:45:11 +02:00 committed by GitHub
parent 265c8bc0ca
commit 7770ae34df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
96 changed files with 12334 additions and 2002 deletions

View file

@ -29,6 +29,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Fix navigator button rendering on safari browser - #656 by @dominik-zeglen - Fix navigator button rendering on safari browser - #656 by @dominik-zeglen
- Use hooks instead of containers with render props in product mutations - #667 by @dominik-zeglen - Use hooks instead of containers with render props in product mutations - #667 by @dominik-zeglen
- Add apps and permission groups to navigator - #678 by @dominik-zeglen - Add apps and permission groups to navigator - #678 by @dominik-zeglen
- Add metadata - #670 by @dominik-zeglen
## 2.10.1 ## 2.10.1

View file

@ -0,0 +1,7 @@
<svg width="91" height="88" viewBox="0 0 91 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 68.4054L43.7676 47.1814V1L1 22.224V68.4054Z" fill="#F1F6F6" stroke="#28234A" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M60.038 41.9077L87.9299 28.1653V10.2969L60.038 24.0393V41.9077Z" fill="#F1F6F6" stroke="#28234A" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M62.8279 86.998L90.2549 73.1248V45.625L62.8279 59.4981V86.998Z" fill="#F1F6F6" stroke="#28234A" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M46.5574 55.3909L60.0385 48.8828" stroke="#06847B" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M53.5302 43.7656V60.9656" stroke="#06847B" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 909 B

View file

@ -1549,6 +1549,46 @@
"context": "button", "context": "button",
"string": "Add" "string": "Add"
}, },
"src_dot_components_dot_Metadata_dot_1148029984": {
"context": "metadata field value, header",
"string": "Value"
},
"src_dot_components_dot_Metadata_dot_1509432322": {
"context": "header",
"string": "Metadata"
},
"src_dot_components_dot_Metadata_dot_1535223586": {
"context": "header",
"string": "Private Metadata"
},
"src_dot_components_dot_Metadata_dot_2024779015": {
"context": "empty metadata text",
"string": "There is no private metadata created for this element."
},
"src_dot_components_dot_Metadata_dot_2087058956": {
"context": "empty metadata text",
"string": "There is no metadata created for this element."
},
"src_dot_components_dot_Metadata_dot_2537046678": {
"context": "metadata field name, header",
"string": "Field"
},
"src_dot_components_dot_Metadata_dot_3181473584": {
"context": "add metadata field,button",
"string": "Add Field"
},
"src_dot_components_dot_Metadata_dot_4190792473": {
"context": "table action",
"string": "Actions"
},
"src_dot_components_dot_Metadata_dot_549104951": {
"context": "number of metadata fields in model",
"string": "{number,plural,one{{number} Field} other{{number} Fields}}"
},
"src_dot_components_dot_Metadata_dot_553451245": {
"context": "empty metadata text",
"string": "Use the button below to add new metadata field"
},
"src_dot_components_dot_MoneyRange_dot_12301532": { "src_dot_components_dot_MoneyRange_dot_12301532": {
"context": "money", "context": "money",
"string": "to {money}" "string": "to {money}"

View file

@ -4,6 +4,8 @@ import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Metadata from "@saleor/components/Metadata/Metadata";
import { MetadataFormData } from "@saleor/components/Metadata/types";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { import {
@ -15,6 +17,8 @@ import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { ReorderAction } from "@saleor/types"; import { ReorderAction } from "@saleor/types";
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import slugify from "slugify"; import slugify from "slugify";
@ -38,7 +42,7 @@ export interface AttributePageProps {
onValueUpdate: (id: string) => void; onValueUpdate: (id: string) => void;
} }
export interface AttributePageFormData { export interface AttributePageFormData extends MetadataFormData {
availableInGrid: boolean; availableInGrid: boolean;
filterableInDashboard: boolean; filterableInDashboard: boolean;
inputType: AttributeInputTypeEnum; inputType: AttributeInputTypeEnum;
@ -65,6 +69,12 @@ const AttributePage: React.FC<AttributePageProps> = ({
onValueUpdate onValueUpdate
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const initialForm: AttributePageFormData = const initialForm: AttributePageFormData =
attribute === null attribute === null
? { ? {
@ -72,7 +82,9 @@ const AttributePage: React.FC<AttributePageProps> = ({
filterableInDashboard: true, filterableInDashboard: true,
filterableInStorefront: true, filterableInStorefront: true,
inputType: AttributeInputTypeEnum.DROPDOWN, inputType: AttributeInputTypeEnum.DROPDOWN,
metadata: undefined,
name: "", name: "",
privateMetadata: undefined,
slug: "", slug: "",
storefrontSearchPosition: "", storefrontSearchPosition: "",
valueRequired: true, valueRequired: true,
@ -92,7 +104,11 @@ const AttributePage: React.FC<AttributePageProps> = ({
() => attribute.inputType, () => attribute.inputType,
AttributeInputTypeEnum.DROPDOWN AttributeInputTypeEnum.DROPDOWN
), ),
metadata: attribute?.metadata?.map(mapMetadataItemToInput),
name: maybe(() => attribute.name, ""), name: maybe(() => attribute.name, ""),
privateMetadata: attribute?.privateMetadata?.map(
mapMetadataItemToInput
),
slug: maybe(() => attribute.slug, ""), slug: maybe(() => attribute.slug, ""),
storefrontSearchPosition: maybe( storefrontSearchPosition: maybe(
() => attribute.storefrontSearchPosition.toString(), () => attribute.storefrontSearchPosition.toString(),
@ -102,66 +118,84 @@ const AttributePage: React.FC<AttributePageProps> = ({
visibleInStorefront: maybe(() => attribute.visibleInStorefront, true) visibleInStorefront: maybe(() => attribute.visibleInStorefront, true)
}; };
const handleSubmit = (data: AttributePageFormData) => const handleSubmit = (data: AttributePageFormData) => {
const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
onSubmit({ onSubmit({
...data, ...data,
metadata,
privateMetadata,
slug: data.slug || slugify(data.name).toLowerCase() slug: data.slug || slugify(data.name).toLowerCase()
}); });
};
return ( return (
<Form initial={initialForm} onSubmit={handleSubmit}> <Form initial={initialForm} onSubmit={handleSubmit}>
{({ change, data, submit }) => ( {({ change, data, submit }) => {
<Container> const changeMetadata = makeMetadataChangeHandler(change);
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.attributes)} return (
</AppHeader> <Container>
<PageHeader <AppHeader onBack={onBack}>
title={ {intl.formatMessage(sectionNames.attributes)}
attribute === null </AppHeader>
? intl.formatMessage({ <PageHeader
defaultMessage: "Create New Attribute", title={
description: "page title" attribute === null
}) ? intl.formatMessage({
: maybe(() => attribute.name) defaultMessage: "Create New Attribute",
} description: "page title"
/> })
<Grid> : maybe(() => attribute.name)
<div> }
<AttributeDetails />
canChangeType={attribute === null} <Grid>
data={data} <div>
disabled={disabled} <AttributeDetails
errors={errors} canChangeType={attribute === null}
onChange={change} data={data}
/> disabled={disabled}
<CardSpacer /> errors={errors}
<AttributeValues onChange={change}
disabled={disabled} />
values={values} <CardSpacer />
onValueAdd={onValueAdd} <AttributeValues
onValueDelete={onValueDelete} disabled={disabled}
onValueReorder={onValueReorder} values={values}
onValueUpdate={onValueUpdate} onValueAdd={onValueAdd}
/> onValueDelete={onValueDelete}
</div> onValueReorder={onValueReorder}
<div> onValueUpdate={onValueUpdate}
<AttributeProperties />
data={data} {!!attribute && (
errors={errors} <>
disabled={disabled} <CardSpacer />
onChange={change} <Metadata data={data} onChange={changeMetadata} />
/> </>
</div> )}
</Grid> </div>
<SaveButtonBar <div>
disabled={disabled} <AttributeProperties
state={saveButtonBarState} data={data}
onCancel={onBack} errors={errors}
onSave={submit} disabled={disabled}
onDelete={attribute === null ? undefined : onDelete} onChange={change}
/> />
</Container> </div>
)} </Grid>
<SaveButtonBar
disabled={disabled}
state={saveButtonBarState}
onCancel={onBack}
onSave={submit}
onDelete={attribute === null ? undefined : onDelete}
/>
</Container>
);
}}
</Form> </Form>
); );
}; };

View file

@ -14,7 +14,15 @@ export const attribute: AttributeDetailsFragment = {
filterableInStorefront: true, filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZTo5", id: "UHJvZHVjdEF0dHJpYnV0ZTo5",
inputType: AttributeInputTypeEnum.DROPDOWN, inputType: AttributeInputTypeEnum.DROPDOWN,
metadata: [
{
__typename: "MetadataItem",
key: "integration.id",
value: "100023123"
}
],
name: "Author", name: "Author",
privateMetadata: [],
slug: "author", slug: "author",
storefrontSearchPosition: 2, storefrontSearchPosition: 2,
valueRequired: true, valueRequired: true,

View file

@ -1,6 +1,6 @@
import { attributeDetailsFragment } from "@saleor/fragments/attributes"; import { attributeDetailsFragment } from "@saleor/fragments/attributes";
import { productErrorFragment } from "@saleor/fragments/errors"; import { productErrorFragment } from "@saleor/fragments/errors";
import { TypedMutation } from "@saleor/mutations"; import makeMutation from "@saleor/hooks/makeMutation";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { import {
@ -46,7 +46,7 @@ const attributeBulkDelete = gql`
} }
} }
`; `;
export const AttributeBulkDeleteMutation = TypedMutation< export const useAttributeBulkDeleteMutation = makeMutation<
AttributeBulkDelete, AttributeBulkDelete,
AttributeBulkDeleteVariables AttributeBulkDeleteVariables
>(attributeBulkDelete); >(attributeBulkDelete);
@ -61,7 +61,7 @@ const attributeDelete = gql`
} }
} }
`; `;
export const AttributeDeleteMutation = TypedMutation< export const useAttributeDeleteMutation = makeMutation<
AttributeDelete, AttributeDelete,
AttributeDeleteVariables AttributeDeleteVariables
>(attributeDelete); >(attributeDelete);
@ -80,7 +80,7 @@ export const attributeUpdateMutation = gql`
} }
} }
`; `;
export const AttributeUpdateMutation = TypedMutation< export const useAttributeUpdateMutation = makeMutation<
AttributeUpdate, AttributeUpdate,
AttributeUpdateVariables AttributeUpdateVariables
>(attributeUpdateMutation); >(attributeUpdateMutation);
@ -99,7 +99,7 @@ const attributeValueDelete = gql`
} }
} }
`; `;
export const AttributeValueDeleteMutation = TypedMutation< export const useAttributeValueDeleteMutation = makeMutation<
AttributeValueDelete, AttributeValueDelete,
AttributeValueDeleteVariables AttributeValueDeleteVariables
>(attributeValueDelete); >(attributeValueDelete);
@ -118,7 +118,7 @@ export const attributeValueUpdateMutation = gql`
} }
} }
`; `;
export const AttributeValueUpdateMutation = TypedMutation< export const useAttributeValueUpdateMutation = makeMutation<
AttributeValueUpdate, AttributeValueUpdate,
AttributeValueUpdateVariables AttributeValueUpdateVariables
>(attributeValueUpdateMutation); >(attributeValueUpdateMutation);
@ -137,7 +137,7 @@ export const attributeValueCreateMutation = gql`
} }
} }
`; `;
export const AttributeValueCreateMutation = TypedMutation< export const useAttributeValueCreateMutation = makeMutation<
AttributeValueCreate, AttributeValueCreate,
AttributeValueCreateVariables AttributeValueCreateVariables
>(attributeValueCreateMutation); >(attributeValueCreateMutation);
@ -156,7 +156,7 @@ export const attributeCreateMutation = gql`
} }
} }
`; `;
export const AttributeCreateMutation = TypedMutation< export const useAttributeCreateMutation = makeMutation<
AttributeCreate, AttributeCreate,
AttributeCreateVariables AttributeCreateVariables
>(attributeCreateMutation); >(attributeCreateMutation);
@ -177,7 +177,7 @@ const attributeValueReorderMutation = gql`
} }
} }
`; `;
export const AttributeValueReorderMutation = TypedMutation< export const useAttributeValueReorderMutation = makeMutation<
AttributeValueReorder, AttributeValueReorder,
AttributeValueReorderVariables AttributeValueReorderVariables
>(attributeValueReorderMutation); >(attributeValueReorderMutation);

View file

@ -6,7 +6,6 @@ import { pageInfoFragment } from "@saleor/fragments/pageInfo";
import makeQuery from "@saleor/hooks/makeQuery"; import makeQuery from "@saleor/hooks/makeQuery";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { TypedQuery } from "../queries";
import { import {
AttributeDetails, AttributeDetails,
AttributeDetailsVariables AttributeDetailsVariables
@ -21,7 +20,7 @@ const attributeDetails = gql`
} }
} }
`; `;
export const AttributeDetailsQuery = TypedQuery< export const useAttributeDetailsQuery = makeQuery<
AttributeDetails, AttributeDetails,
AttributeDetailsVariables AttributeDetailsVariables
>(attributeDetails); >(attributeDetails);

View file

@ -8,6 +8,18 @@ import { AttributeCreateInput, AttributeInputTypeEnum, AttributeValueType, Produ
// GraphQL mutation operation: AttributeCreate // GraphQL mutation operation: AttributeCreate
// ==================================================== // ====================================================
export interface AttributeCreate_attributeCreate_attribute_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeCreate_attributeCreate_attribute_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeCreate_attributeCreate_attribute_values { export interface AttributeCreate_attributeCreate_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -24,6 +36,8 @@ export interface AttributeCreate_attributeCreate_attribute {
visibleInStorefront: boolean; visibleInStorefront: boolean;
filterableInDashboard: boolean; filterableInDashboard: boolean;
filterableInStorefront: boolean; filterableInStorefront: boolean;
metadata: (AttributeCreate_attributeCreate_attribute_metadata | null)[];
privateMetadata: (AttributeCreate_attributeCreate_attribute_privateMetadata | null)[];
availableInGrid: boolean; availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null; inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number; storefrontSearchPosition: number;

View file

@ -8,6 +8,18 @@ import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/global
// GraphQL query operation: AttributeDetails // GraphQL query operation: AttributeDetails
// ==================================================== // ====================================================
export interface AttributeDetails_attribute_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeDetails_attribute_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeDetails_attribute_values { export interface AttributeDetails_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -24,6 +36,8 @@ export interface AttributeDetails_attribute {
visibleInStorefront: boolean; visibleInStorefront: boolean;
filterableInDashboard: boolean; filterableInDashboard: boolean;
filterableInStorefront: boolean; filterableInStorefront: boolean;
metadata: (AttributeDetails_attribute_metadata | null)[];
privateMetadata: (AttributeDetails_attribute_privateMetadata | null)[];
availableInGrid: boolean; availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null; inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number; storefrontSearchPosition: number;

View file

@ -8,6 +8,18 @@ import { AttributeUpdateInput, AttributeInputTypeEnum, AttributeValueType, Produ
// GraphQL mutation operation: AttributeUpdate // GraphQL mutation operation: AttributeUpdate
// ==================================================== // ====================================================
export interface AttributeUpdate_attributeUpdate_attribute_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeUpdate_attributeUpdate_attribute_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeUpdate_attributeUpdate_attribute_values { export interface AttributeUpdate_attributeUpdate_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -24,6 +36,8 @@ export interface AttributeUpdate_attributeUpdate_attribute {
visibleInStorefront: boolean; visibleInStorefront: boolean;
filterableInDashboard: boolean; filterableInDashboard: boolean;
filterableInStorefront: boolean; filterableInStorefront: boolean;
metadata: (AttributeUpdate_attributeUpdate_attribute_metadata | null)[];
privateMetadata: (AttributeUpdate_attributeUpdate_attribute_privateMetadata | null)[];
availableInGrid: boolean; availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null; inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number; storefrontSearchPosition: number;

View file

@ -8,6 +8,18 @@ import { AttributeValueCreateInput, AttributeInputTypeEnum, AttributeValueType,
// GraphQL mutation operation: AttributeValueCreate // GraphQL mutation operation: AttributeValueCreate
// ==================================================== // ====================================================
export interface AttributeValueCreate_attributeValueCreate_attribute_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeValueCreate_attributeValueCreate_attribute_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeValueCreate_attributeValueCreate_attribute_values { export interface AttributeValueCreate_attributeValueCreate_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -24,6 +36,8 @@ export interface AttributeValueCreate_attributeValueCreate_attribute {
visibleInStorefront: boolean; visibleInStorefront: boolean;
filterableInDashboard: boolean; filterableInDashboard: boolean;
filterableInStorefront: boolean; filterableInStorefront: boolean;
metadata: (AttributeValueCreate_attributeValueCreate_attribute_metadata | null)[];
privateMetadata: (AttributeValueCreate_attributeValueCreate_attribute_privateMetadata | null)[];
availableInGrid: boolean; availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null; inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number; storefrontSearchPosition: number;

View file

@ -8,6 +8,18 @@ import { AttributeInputTypeEnum, AttributeValueType, ProductErrorCode } from "./
// GraphQL mutation operation: AttributeValueDelete // GraphQL mutation operation: AttributeValueDelete
// ==================================================== // ====================================================
export interface AttributeValueDelete_attributeValueDelete_attribute_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeValueDelete_attributeValueDelete_attribute_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeValueDelete_attributeValueDelete_attribute_values { export interface AttributeValueDelete_attributeValueDelete_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -24,6 +36,8 @@ export interface AttributeValueDelete_attributeValueDelete_attribute {
visibleInStorefront: boolean; visibleInStorefront: boolean;
filterableInDashboard: boolean; filterableInDashboard: boolean;
filterableInStorefront: boolean; filterableInStorefront: boolean;
metadata: (AttributeValueDelete_attributeValueDelete_attribute_metadata | null)[];
privateMetadata: (AttributeValueDelete_attributeValueDelete_attribute_privateMetadata | null)[];
availableInGrid: boolean; availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null; inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number; storefrontSearchPosition: number;

View file

@ -8,6 +8,18 @@ import { AttributeValueCreateInput, AttributeInputTypeEnum, AttributeValueType,
// GraphQL mutation operation: AttributeValueUpdate // GraphQL mutation operation: AttributeValueUpdate
// ==================================================== // ====================================================
export interface AttributeValueUpdate_attributeValueUpdate_attribute_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeValueUpdate_attributeValueUpdate_attribute_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeValueUpdate_attributeValueUpdate_attribute_values { export interface AttributeValueUpdate_attributeValueUpdate_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -24,6 +36,8 @@ export interface AttributeValueUpdate_attributeValueUpdate_attribute {
visibleInStorefront: boolean; visibleInStorefront: boolean;
filterableInDashboard: boolean; filterableInDashboard: boolean;
filterableInStorefront: boolean; filterableInStorefront: boolean;
metadata: (AttributeValueUpdate_attributeValueUpdate_attribute_metadata | null)[];
privateMetadata: (AttributeValueUpdate_attributeValueUpdate_attribute_privateMetadata | null)[];
availableInGrid: boolean; availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null; inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number; storefrontSearchPosition: number;

View file

@ -1,7 +1,7 @@
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { maybe } from "@saleor/misc"; import { getStringOrPlaceholder } from "@saleor/misc";
import { ReorderEvent } from "@saleor/types"; import { ReorderEvent } from "@saleor/types";
import { ProductErrorCode } from "@saleor/types/globalTypes"; import { ProductErrorCode } from "@saleor/types/globalTypes";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
@ -21,8 +21,7 @@ import AttributeValueDeleteDialog from "../../components/AttributeValueDeleteDia
import AttributeValueEditDialog, { import AttributeValueEditDialog, {
AttributeValueEditDialogFormData AttributeValueEditDialogFormData
} from "../../components/AttributeValueEditDialog"; } from "../../components/AttributeValueEditDialog";
import { AttributeCreateMutation } from "../../mutations"; import { useAttributeCreateMutation } from "../../mutations";
import { AttributeCreate } from "../../types/AttributeCreate";
import { import {
attributeAddUrl, attributeAddUrl,
AttributeAddUrlDialog, AttributeAddUrlDialog,
@ -60,6 +59,20 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
[] []
); );
const [attributeCreate, attributeCreateOpts] = useAttributeCreateMutation({
onCompleted: data => {
if (data.attributeCreate.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Successfully created attribute"
})
});
navigate(attributeUrl(data.attributeCreate.attribute.id));
}
}
});
const id = params.id ? parseInt(params.id, 0) : undefined; const id = params.id ? parseInt(params.id, 0) : undefined;
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
@ -73,17 +86,6 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
setValues(remove(values[params.id], values, areValuesEqual)); setValues(remove(values[params.id], values, areValuesEqual));
closeModal(); closeModal();
}; };
const handleCreate = (data: AttributeCreate) => {
if (data.attributeCreate.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Successfully created attribute"
})
});
navigate(attributeUrl(data.attributeCreate.attribute.id));
}
};
const handleValueUpdate = (input: AttributeValueEditDialogFormData) => { const handleValueUpdate = (input: AttributeValueEditDialogFormData) => {
if (isSelected(input, values, areValuesEqual)) { if (isSelected(input, values, areValuesEqual)) {
setValueErrors([attributeValueAlreadyExistsError]); setValueErrors([attributeValueAlreadyExistsError]);
@ -104,87 +106,83 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
setValues(move(values[oldIndex], values, areValuesEqual, newIndex)); setValues(move(values[oldIndex], values, areValuesEqual, newIndex));
return ( return (
<AttributeCreateMutation onCompleted={handleCreate}> <>
{(attributeCreate, attributeCreateOpts) => ( <AttributePage
attribute={null}
disabled={attributeCreateOpts.loading}
errors={attributeCreateOpts.data?.attributeCreate.errors || []}
onBack={() => navigate(attributeListUrl())}
onDelete={undefined}
onSubmit={input =>
attributeCreate({
variables: {
input: {
...input,
storefrontSearchPosition: parseInt(
input.storefrontSearchPosition,
0
),
values: values.map(value => ({
name: value.name
}))
}
}
})
}
onValueAdd={() => openModal("add-value")}
onValueDelete={id =>
openModal("remove-value", {
id
})
}
onValueReorder={handleValueReorder}
onValueUpdate={id =>
openModal("edit-value", {
id
})
}
saveButtonBarState={attributeCreateOpts.status}
values={values.map((value, valueIndex) => ({
__typename: "AttributeValue" as "AttributeValue",
id: valueIndex.toString(),
slug: slugify(value.name).toLowerCase(),
sortOrder: valueIndex,
type: null,
value: null,
...value
}))}
/>
<AttributeValueEditDialog
attributeValue={null}
confirmButtonState="default"
disabled={false}
errors={valueErrors}
open={params.action === "add-value"}
onClose={closeModal}
onSubmit={handleValueCreate}
/>
{values.length > 0 && (
<> <>
<AttributePage <AttributeValueDeleteDialog
attribute={null} attributeName={undefined}
disabled={false} open={params.action === "remove-value"}
errors={attributeCreateOpts.data?.attributeCreate.errors || []} name={getStringOrPlaceholder(values[id].name)}
onBack={() => navigate(attributeListUrl())} confirmButtonState="default"
onDelete={undefined} onClose={closeModal}
onSubmit={input => onConfirm={handleValueDelete}
attributeCreate({
variables: {
input: {
...input,
storefrontSearchPosition: parseInt(
input.storefrontSearchPosition,
0
),
values: values.map(value => ({
name: value.name
}))
}
}
})
}
onValueAdd={() => openModal("add-value")}
onValueDelete={id =>
openModal("remove-value", {
id
})
}
onValueReorder={handleValueReorder}
onValueUpdate={id =>
openModal("edit-value", {
id
})
}
saveButtonBarState={attributeCreateOpts.status}
values={values.map((value, valueIndex) => ({
__typename: "AttributeValue" as "AttributeValue",
id: valueIndex.toString(),
slug: slugify(value.name).toLowerCase(),
sortOrder: valueIndex,
type: null,
value: null,
...value
}))}
/> />
<AttributeValueEditDialog <AttributeValueEditDialog
attributeValue={null} attributeValue={values[params.id]}
confirmButtonState="default" confirmButtonState="default"
disabled={false} disabled={false}
errors={valueErrors} errors={valueErrors}
open={params.action === "add-value"} open={params.action === "edit-value"}
onClose={closeModal} onClose={closeModal}
onSubmit={handleValueCreate} onSubmit={handleValueUpdate}
/> />
{values.length > 0 && (
<>
<AttributeValueDeleteDialog
attributeName={undefined}
open={params.action === "remove-value"}
name={maybe(() => values[id].name, "...")}
confirmButtonState="default"
onClose={closeModal}
onConfirm={handleValueDelete}
/>
<AttributeValueEditDialog
attributeValue={maybe(() => values[params.id])}
confirmButtonState="default"
disabled={false}
errors={valueErrors}
open={params.action === "edit-value"}
onClose={closeModal}
onSubmit={handleValueUpdate}
/>
</>
)}
</> </>
)} )}
</AttributeCreateMutation> </>
); );
}; };
AttributeDetails.displayName = "AttributeDetails"; AttributeDetails.displayName = "AttributeDetails";

View file

@ -5,29 +5,30 @@ import { maybe } from "@saleor/misc";
import { ReorderEvent } from "@saleor/types"; import { ReorderEvent } from "@saleor/types";
import { getProductErrorMessage } from "@saleor/utils/errors"; import { getProductErrorMessage } from "@saleor/utils/errors";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import { move } from "@saleor/utils/lists"; import { move } from "@saleor/utils/lists";
import {
useMetadataUpdate,
usePrivateMetadataUpdate
} from "@saleor/utils/metadata/updateMetadata";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import AttributeDeleteDialog from "../../components/AttributeDeleteDialog"; import AttributeDeleteDialog from "../../components/AttributeDeleteDialog";
import AttributePage from "../../components/AttributePage"; import AttributePage, {
AttributePageFormData
} from "../../components/AttributePage";
import AttributeValueDeleteDialog from "../../components/AttributeValueDeleteDialog"; import AttributeValueDeleteDialog from "../../components/AttributeValueDeleteDialog";
import AttributeValueEditDialog from "../../components/AttributeValueEditDialog"; import AttributeValueEditDialog from "../../components/AttributeValueEditDialog";
import { import {
AttributeDeleteMutation, useAttributeDeleteMutation,
AttributeUpdateMutation, useAttributeUpdateMutation,
AttributeValueCreateMutation, useAttributeValueCreateMutation,
AttributeValueDeleteMutation, useAttributeValueDeleteMutation,
AttributeValueReorderMutation, useAttributeValueReorderMutation,
AttributeValueUpdateMutation useAttributeValueUpdateMutation
} from "../../mutations"; } from "../../mutations";
import { AttributeDetailsQuery } from "../../queries"; import { useAttributeDetailsQuery } from "../../queries";
import { AttributeDelete } from "../../types/AttributeDelete";
import { AttributeUpdate } from "../../types/AttributeUpdate";
import { AttributeValueCreate } from "../../types/AttributeValueCreate";
import { AttributeValueDelete } from "../../types/AttributeValueDelete";
import { AttributeValueReorder } from "../../types/AttributeValueReorder";
import { AttributeValueUpdate } from "../../types/AttributeValueUpdate";
import { import {
attributeListUrl, attributeListUrl,
attributeUrl, attributeUrl,
@ -44,285 +45,258 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl(); const intl = useIntl();
const [updateMetadata] = useMetadataUpdate({});
const [updatePrivateMetadata] = usePrivateMetadataUpdate({});
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
AttributeUrlDialog, AttributeUrlDialog,
AttributeUrlQueryParams AttributeUrlQueryParams
>(navigate, params => attributeUrl(id, params), params); >(navigate, params => attributeUrl(id, params), params);
const handleDelete = (data: AttributeDelete) => { const { data, loading } = useAttributeDetailsQuery({
if (data.attributeDelete.errors.length === 0) { variables: {
notify({ id
status: "success",
text: intl.formatMessage({
defaultMessage: "Attribute deleted"
})
});
navigate(attributeListUrl());
} }
}; });
const handleValueDelete = (data: AttributeValueDelete) => {
if (data.attributeValueDelete.errors.length === 0) { const [attributeDelete, attributeDeleteOpts] = useAttributeDeleteMutation({
notify({ onCompleted: data => {
status: "success", if (data.attributeDelete.errors.length === 0) {
text: intl.formatMessage({ notify({
defaultMessage: "Value deleted", status: "success",
description: "attribute value deleted" text: intl.formatMessage({
}) defaultMessage: "Attribute deleted"
}); })
closeModal(); });
navigate(attributeListUrl());
}
} }
}; });
const handleUpdate = (data: AttributeUpdate) => {
if (data.attributeUpdate.errors.length === 0) { const [
notify({ attributeValueDelete,
status: "success", attributeValueDeleteOpts
text: intl.formatMessage(commonMessages.savedChanges) ] = useAttributeValueDeleteMutation({
}); onCompleted: data => {
if (data.attributeValueDelete.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Value deleted",
description: "attribute value deleted"
})
});
closeModal();
}
} }
}; });
const handleValueUpdate = (data: AttributeValueUpdate) => {
if (data.attributeValueUpdate.errors.length === 0) { const [
notify({ attributeValueUpdate,
status: "success", attributeValueUpdateOpts
text: intl.formatMessage(commonMessages.savedChanges) ] = useAttributeValueUpdateMutation({
}); onCompleted: data => {
closeModal(); if (data.attributeValueUpdate.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
closeModal();
}
} }
}; });
const handleValueCreate = (data: AttributeValueCreate) => {
if (data.attributeValueCreate.errors.length === 0) { const [attributeUpdate, attributeUpdateOpts] = useAttributeUpdateMutation({
notify({ onCompleted: data => {
status: "success", if (data.attributeUpdate.errors.length === 0) {
text: intl.formatMessage({ notify({
defaultMessage: "Added new value", status: "success",
description: "added new attribute value" text: intl.formatMessage(commonMessages.savedChanges)
}) });
}); }
closeModal();
} }
}; });
const handleValueReorderMutation = (data: AttributeValueReorder) => {
if (data.attributeReorderValues.errors.length !== 0) { const [
notify({ attributeValueCreate,
status: "error", attributeValueCreateOpts
text: getProductErrorMessage( ] = useAttributeValueCreateMutation({
data.attributeReorderValues.errors[0], onCompleted: data => {
intl if (data.attributeValueCreate.errors.length === 0) {
) notify({
}); status: "success",
text: intl.formatMessage({
defaultMessage: "Added new value",
description: "added new attribute value"
})
});
closeModal();
}
} }
});
const [attributeValueReorder] = useAttributeValueReorderMutation({
onCompleted: data => {
if (data.attributeReorderValues.errors.length !== 0) {
notify({
status: "error",
text: getProductErrorMessage(
data.attributeReorderValues.errors[0],
intl
)
});
}
}
});
const handleValueReorder = ({ newIndex, oldIndex }: ReorderEvent) =>
attributeValueReorder({
optimisticResponse: {
attributeReorderValues: {
__typename: "AttributeReorderValues",
attribute: {
...data.attribute,
values: move(
data.attribute.values[oldIndex],
data.attribute.values,
(a, b) => a.id === b.id,
newIndex
)
},
errors: []
}
},
variables: {
id,
move: {
id: data.attribute.values[oldIndex].id,
sortOrder: newIndex - oldIndex
}
}
});
const handleUpdate = async (data: AttributePageFormData) => {
const input = {
...data,
inputType: undefined,
metadata: undefined,
privateMetadata: undefined,
storefrontSearchPosition: parseInt(data.storefrontSearchPosition, 0)
};
const result = await attributeUpdate({
variables: {
id,
input
}
});
return result.data.attributeUpdate.errors;
}; };
const handleSubmit = createMetadataUpdateHandler(
data?.attribute,
handleUpdate,
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
);
return ( return (
<AttributeDetailsQuery variables={{ id }}> <>
{({ data, loading }) => ( <AttributePage
<AttributeDeleteMutation onCompleted={handleDelete}> attribute={maybe(() => data.attribute)}
{(attributeDelete, attributeDeleteOpts) => ( disabled={loading}
<AttributeValueDeleteMutation onCompleted={handleValueDelete}> errors={attributeUpdateOpts.data?.attributeUpdate.errors || []}
{(attributeValueDelete, attributeValueDeleteOpts) => ( onBack={() => navigate(attributeListUrl())}
<AttributeUpdateMutation onCompleted={handleUpdate}> onDelete={() => openModal("remove")}
{(attributeUpdate, attributeUpdateOpts) => ( onSubmit={handleSubmit}
<AttributeValueUpdateMutation onValueAdd={() => openModal("add-value")}
onCompleted={handleValueUpdate} onValueDelete={id =>
> openModal("remove-value", {
{(attributeValueUpdate, attributeValueUpdateOpts) => ( id
<AttributeValueCreateMutation })
onCompleted={handleValueCreate} }
> onValueReorder={handleValueReorder}
{(attributeValueCreate, attributeValueCreateOpts) => ( onValueUpdate={id =>
<AttributeValueReorderMutation openModal("edit-value", {
onCompleted={handleValueReorderMutation} id
> })
{attributeValueReorder => { }
const handleValueReorder = ({ saveButtonBarState={attributeUpdateOpts.status}
newIndex, values={maybe(() => data.attribute.values)}
oldIndex />
}: ReorderEvent) => <AttributeDeleteDialog
attributeValueReorder({ open={params.action === "remove"}
optimisticResponse: { name={maybe(() => data.attribute.name, "...")}
attributeReorderValues: { confirmButtonState={attributeDeleteOpts.status}
__typename: "AttributeReorderValues", onClose={closeModal}
attribute: { onConfirm={() =>
...data.attribute, attributeDelete({
values: move( variables: {
data.attribute.values[oldIndex], id
data.attribute.values, }
(a, b) => a.id === b.id, })
newIndex }
) />
}, <AttributeValueDeleteDialog
errors: [] attributeName={maybe(() => data.attribute.name, "...")}
} open={params.action === "remove-value"}
}, name={maybe(
variables: { () =>
id, data.attribute.values.find(value => params.id === value.id).name,
move: { "..."
id: data.attribute.values[oldIndex].id, )}
sortOrder: newIndex - oldIndex useName={true}
} confirmButtonState={attributeValueDeleteOpts.status}
} onClose={closeModal}
}); onConfirm={() =>
attributeValueDelete({
return ( variables: {
<> id: params.id
<AttributePage }
attribute={maybe(() => data.attribute)} })
disabled={loading} }
errors={ />
attributeUpdateOpts.data <AttributeValueEditDialog
?.attributeUpdate.errors || [] attributeValue={null}
} confirmButtonState={attributeValueCreateOpts.status}
onBack={() => disabled={loading}
navigate(attributeListUrl()) errors={
} attributeValueCreateOpts.data?.attributeValueCreate.errors || []
onDelete={() => openModal("remove")} }
onSubmit={data => { open={params.action === "add-value"}
const input = { onClose={closeModal}
...data, onSubmit={input =>
inputType: undefined attributeValueCreate({
}; variables: {
id,
attributeUpdate({ input
variables: { }
id, })
input: { }
...input, />
storefrontSearchPosition: parseInt( <AttributeValueEditDialog
input.storefrontSearchPosition, attributeValue={maybe(() =>
0 data.attribute.values.find(value => params.id === value.id)
) )}
} confirmButtonState={attributeValueUpdateOpts.status}
} disabled={loading}
}); errors={
}} attributeValueUpdateOpts.data?.attributeValueUpdate.errors || []
onValueAdd={() => openModal("add-value")} }
onValueDelete={id => open={params.action === "edit-value"}
openModal("remove-value", { onClose={closeModal}
id onSubmit={input =>
}) attributeValueUpdate({
} variables: {
onValueReorder={handleValueReorder} id: data.attribute.values.find(value => params.id === value.id)
onValueUpdate={id => .id,
openModal("edit-value", { input
id }
}) })
} }
saveButtonBarState={ />
attributeUpdateOpts.status </>
}
values={maybe(
() => data.attribute.values
)}
/>
<AttributeDeleteDialog
open={params.action === "remove"}
name={maybe(
() => data.attribute.name,
"..."
)}
confirmButtonState={
attributeDeleteOpts.status
}
onClose={closeModal}
onConfirm={() =>
attributeDelete({
variables: {
id
}
})
}
/>
<AttributeValueDeleteDialog
attributeName={maybe(
() => data.attribute.name,
"..."
)}
open={params.action === "remove-value"}
name={maybe(
() =>
data.attribute.values.find(
value => params.id === value.id
).name,
"..."
)}
useName={true}
confirmButtonState={
attributeValueDeleteOpts.status
}
onClose={closeModal}
onConfirm={() =>
attributeValueDelete({
variables: {
id: params.id
}
})
}
/>
<AttributeValueEditDialog
attributeValue={null}
confirmButtonState={
attributeValueCreateOpts.status
}
disabled={loading}
errors={
attributeValueCreateOpts.data
?.attributeValueCreate.errors || []
}
open={params.action === "add-value"}
onClose={closeModal}
onSubmit={input =>
attributeValueCreate({
variables: {
id,
input
}
})
}
/>
<AttributeValueEditDialog
attributeValue={maybe(() =>
data.attribute.values.find(
value => params.id === value.id
)
)}
confirmButtonState={
attributeValueUpdateOpts.status
}
disabled={loading}
errors={
attributeValueUpdateOpts.data
?.attributeValueUpdate.errors || []
}
open={params.action === "edit-value"}
onClose={closeModal}
onSubmit={input =>
attributeValueUpdate({
variables: {
id: data.attribute.values.find(
value => params.id === value.id
).id,
input
}
})
}
/>
</>
);
}}
</AttributeValueReorderMutation>
)}
</AttributeValueCreateMutation>
)}
</AttributeValueUpdateMutation>
)}
</AttributeUpdateMutation>
)}
</AttributeValueDeleteMutation>
)}
</AttributeDeleteMutation>
)}
</AttributeDetailsQuery>
); );
}; };
AttributeDetails.displayName = "AttributeDetails"; AttributeDetails.displayName = "AttributeDetails";

View file

@ -32,9 +32,8 @@ import useBulkActions from "../../../hooks/useBulkActions";
import { maybe } from "../../../misc"; import { maybe } from "../../../misc";
import AttributeBulkDeleteDialog from "../../components/AttributeBulkDeleteDialog"; import AttributeBulkDeleteDialog from "../../components/AttributeBulkDeleteDialog";
import AttributeListPage from "../../components/AttributeListPage"; import AttributeListPage from "../../components/AttributeListPage";
import { AttributeBulkDeleteMutation } from "../../mutations"; import { useAttributeBulkDeleteMutation } from "../../mutations";
import { useAttributeListQuery } from "../../queries"; import { useAttributeListQuery } from "../../queries";
import { AttributeBulkDelete } from "../../types/AttributeBulkDelete";
import { import {
attributeAddUrl, attributeAddUrl,
attributeListUrl, attributeListUrl,
@ -72,6 +71,26 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
variables: queryVariables variables: queryVariables
}); });
const [
attributeBulkDelete,
attributeBulkDeleteOpts
] = useAttributeBulkDeleteMutation({
onCompleted: data => {
if (data.attributeBulkDelete.errors.length === 0) {
closeModal();
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Attributes successfully delete",
description: "deleted multiple attributes"
})
});
reset();
refetch();
}
}
});
const tabs = getFilterTabs(); const tabs = getFilterTabs();
const currentTab = const currentTab =
@ -125,96 +144,73 @@ const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
params params
); );
const handleBulkDelete = (data: AttributeBulkDelete) => {
if (data.attributeBulkDelete.errors.length === 0) {
closeModal();
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Attributes successfully delete",
description: "deleted multiple attributes"
})
});
reset();
refetch();
}
};
const handleSort = createSortHandler(navigate, attributeListUrl, params); const handleSort = createSortHandler(navigate, attributeListUrl, params);
const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
return ( return (
<AttributeBulkDeleteMutation onCompleted={handleBulkDelete}> <>
{(attributeBulkDelete, attributeBulkDeleteOpts) => ( <AttributeListPage
<> attributes={maybe(() => data.attributes.edges.map(edge => edge.node))}
<AttributeListPage currencySymbol={currencySymbol}
attributes={maybe(() => currentTab={currentTab}
data.attributes.edges.map(edge => edge.node) disabled={loading || attributeBulkDeleteOpts.loading}
)} filterOpts={getFilterOpts(params)}
currencySymbol={currencySymbol} initialSearch={params.query || ""}
currentTab={currentTab} isChecked={isSelected}
disabled={loading || attributeBulkDeleteOpts.loading} onAdd={() => navigate(attributeAddUrl())}
filterOpts={getFilterOpts(params)} onAll={resetFilters}
initialSearch={params.query || ""} onBack={() => navigate(configurationMenuUrl)}
isChecked={isSelected} onFilterChange={changeFilters}
onAdd={() => navigate(attributeAddUrl())} onNextPage={loadNextPage}
onAll={resetFilters} onPreviousPage={loadPreviousPage}
onBack={() => navigate(configurationMenuUrl)} onRowClick={id => () => navigate(attributeUrl(id))}
onFilterChange={changeFilters} onSearchChange={handleSearchChange}
onNextPage={loadNextPage} onSort={handleSort}
onPreviousPage={loadPreviousPage} onTabChange={handleTabChange}
onRowClick={id => () => navigate(attributeUrl(id))} onTabDelete={() => openModal("delete-search")}
onSearchChange={handleSearchChange} onTabSave={() => openModal("save-search")}
onSort={handleSort} pageInfo={pageInfo}
onTabChange={handleTabChange} selected={listElements.length}
onTabDelete={() => openModal("delete-search")} sort={getSortParams(params)}
onTabSave={() => openModal("save-search")} tabs={tabs.map(tab => tab.name)}
pageInfo={pageInfo} toggle={toggle}
selected={listElements.length} toggleAll={toggleAll}
sort={getSortParams(params)} toolbar={
tabs={tabs.map(tab => tab.name)} <IconButton
toggle={toggle} color="primary"
toggleAll={toggleAll} onClick={() =>
toolbar={ openModal("remove", {
<IconButton ids: listElements
color="primary" })
onClick={() =>
openModal("remove", {
ids: listElements
})
}
>
<DeleteIcon />
</IconButton>
} }
/> >
<AttributeBulkDeleteDialog <DeleteIcon />
confirmButtonState={attributeBulkDeleteOpts.status} </IconButton>
open={ }
params.action === "remove" && maybe(() => params.ids.length > 0) />
} <AttributeBulkDeleteDialog
onConfirm={() => confirmButtonState={attributeBulkDeleteOpts.status}
attributeBulkDelete({ variables: { ids: params.ids } }) open={params.action === "remove" && maybe(() => params.ids.length > 0)}
} onConfirm={() =>
onClose={closeModal} attributeBulkDelete({ variables: { ids: params.ids } })
quantity={maybe(() => params.ids.length)} }
/> onClose={closeModal}
<SaveFilterTabDialog quantity={maybe(() => params.ids.length)}
open={params.action === "save-search"} />
confirmButtonState="default" <SaveFilterTabDialog
onClose={closeModal} open={params.action === "save-search"}
onSubmit={handleTabSave} confirmButtonState="default"
/> onClose={closeModal}
<DeleteFilterTabDialog onSubmit={handleTabSave}
open={params.action === "delete-search"} />
confirmButtonState="default" <DeleteFilterTabDialog
onClose={closeModal} open={params.action === "delete-search"}
onSubmit={handleTabDelete} confirmButtonState="default"
tabName={maybe(() => tabs[currentTab - 1].name, "...")} onClose={closeModal}
/> onSubmit={handleTabDelete}
</> tabName={maybe(() => tabs[currentTab - 1].name, "...")}
)} />
</AttributeBulkDeleteMutation> </>
); );
}; };
AttributeList.displayName = "AttributeList"; AttributeList.displayName = "AttributeList";

View file

@ -6,12 +6,16 @@ import CardTitle from "@saleor/components/CardTitle";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import Metadata from "@saleor/components/Metadata/Metadata";
import { MetadataFormData } from "@saleor/components/Metadata/types";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm"; import SeoForm from "@saleor/components/SeoForm";
import { Tab, TabContainer } from "@saleor/components/Tab"; import { Tab, TabContainer } from "@saleor/components/Tab";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { RawDraftContentState } from "draft-js"; import { RawDraftContentState } from "draft-js";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -28,7 +32,7 @@ import {
import CategoryBackground from "../CategoryBackground"; import CategoryBackground from "../CategoryBackground";
import CategoryProducts from "../CategoryProducts"; import CategoryProducts from "../CategoryProducts";
export interface FormData { export interface FormData extends MetadataFormData {
backgroundImageAlt: string; backgroundImageAlt: string;
description: RawDraftContentState; description: RawDraftContentState;
name: string; name: string;
@ -100,144 +104,174 @@ export const CategoryUpdatePage: React.FC<CategoryUpdatePageProps> = ({
toggleAll toggleAll
}: CategoryUpdatePageProps) => { }: CategoryUpdatePageProps) => {
const intl = useIntl(); const intl = useIntl();
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const initialData: FormData = category const initialData: FormData = category
? { ? {
backgroundImageAlt: maybe(() => category.backgroundImage.alt, ""), backgroundImageAlt: maybe(() => category.backgroundImage.alt, ""),
description: maybe(() => JSON.parse(category.descriptionJson)), description: maybe(() => JSON.parse(category.descriptionJson)),
metadata: category?.metadata?.map(mapMetadataItemToInput),
name: category.name || "", name: category.name || "",
privateMetadata: category?.privateMetadata?.map(mapMetadataItemToInput),
seoDescription: category.seoDescription || "", seoDescription: category.seoDescription || "",
seoTitle: category.seoTitle || "" seoTitle: category.seoTitle || ""
} }
: { : {
backgroundImageAlt: "", backgroundImageAlt: "",
description: "", description: "",
metadata: undefined,
name: "", name: "",
privateMetadata: undefined,
seoDescription: "", seoDescription: "",
seoTitle: "" seoTitle: ""
}; };
const handleSubmit = (data: FormData) => {
const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
onSubmit({
...data,
metadata,
privateMetadata
});
};
return ( return (
<Form onSubmit={onSubmit} initial={initialData} confirmLeave> <Form onSubmit={handleSubmit} initial={initialData} confirmLeave>
{({ data, change, submit, hasChanged }) => ( {({ data, change, submit, hasChanged }) => {
<Container> const changeMetadata = makeMetadataChangeHandler(change);
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.categories)} return (
</AppHeader> <Container>
<PageHeader title={category ? category.name : undefined} /> <AppHeader onBack={onBack}>
<CategoryDetailsForm {intl.formatMessage(sectionNames.categories)}
category={category} </AppHeader>
data={data} <PageHeader title={category ? category.name : undefined} />
disabled={disabled} <CategoryDetailsForm
errors={errors} category={category}
onChange={change} data={data}
/> disabled={disabled}
<CardSpacer /> errors={errors}
<CategoryBackground onChange={change}
data={data} />
onImageUpload={onImageUpload} <CardSpacer />
onImageDelete={onImageDelete} <CategoryBackground
image={maybe(() => category.backgroundImage)} data={data}
onChange={change} onImageUpload={onImageUpload}
/> onImageDelete={onImageDelete}
<CardSpacer /> image={maybe(() => category.backgroundImage)}
<SeoForm onChange={change}
helperText={intl.formatMessage({ />
defaultMessage: <CardSpacer />
"Add search engine title and description to make this category easier to find" <SeoForm
})} helperText={intl.formatMessage({
title={data.seoTitle} defaultMessage:
titlePlaceholder={data.name} "Add search engine title and description to make this category easier to find"
description={data.seoDescription} })}
descriptionPlaceholder={data.name} title={data.seoTitle}
loading={!category} titlePlaceholder={data.name}
onChange={change} description={data.seoDescription}
disabled={disabled} descriptionPlaceholder={data.name}
/> loading={!category}
<CardSpacer /> onChange={change}
<TabContainer> disabled={disabled}
<CategoriesTab />
isActive={currentTab === CategoryPageTab.categories} <CardSpacer />
changeTab={changeTab} <Metadata data={data} onChange={changeMetadata} />
> <CardSpacer />
<FormattedMessage <TabContainer>
defaultMessage="Subcategories" <CategoriesTab
description="number of subcategories in category" isActive={currentTab === CategoryPageTab.categories}
/> changeTab={changeTab}
</CategoriesTab> >
<ProductsTab <FormattedMessage
isActive={currentTab === CategoryPageTab.products} defaultMessage="Subcategories"
changeTab={changeTab} description="number of subcategories in category"
> />
<FormattedMessage </CategoriesTab>
defaultMessage="Products" <ProductsTab
description="number of products in category" isActive={currentTab === CategoryPageTab.products}
/> changeTab={changeTab}
</ProductsTab> >
</TabContainer> <FormattedMessage
<CardSpacer /> defaultMessage="Products"
{currentTab === CategoryPageTab.categories && ( description="number of products in category"
<Card> />
<CardTitle </ProductsTab>
title={intl.formatMessage({ </TabContainer>
defaultMessage: "All Subcategories", <CardSpacer />
description: "section header" {currentTab === CategoryPageTab.categories && (
})} <Card>
toolbar={ <CardTitle
<Button title={intl.formatMessage({
color="primary" defaultMessage: "All Subcategories",
variant="text" description: "section header"
onClick={onAddCategory} })}
> toolbar={
<FormattedMessage <Button
defaultMessage="Create subcategory" color="primary"
description="button" variant="text"
/> onClick={onAddCategory}
</Button> >
} <FormattedMessage
/> defaultMessage="Create subcategory"
<CategoryList description="button"
categories={subcategories} />
</Button>
}
/>
<CategoryList
categories={subcategories}
disabled={disabled}
isChecked={isChecked}
isRoot={false}
pageInfo={pageInfo}
selected={selected}
sort={undefined}
toggle={toggle}
toggleAll={toggleAll}
toolbar={subcategoryListToolbar}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
onRowClick={onCategoryClick}
onSort={() => undefined}
/>
</Card>
)}
{currentTab === CategoryPageTab.products && (
<CategoryProducts
categoryName={maybe(() => category.name)}
products={products}
disabled={disabled} disabled={disabled}
isChecked={isChecked}
isRoot={false}
pageInfo={pageInfo} pageInfo={pageInfo}
selected={selected}
sort={undefined}
toggle={toggle}
toggleAll={toggleAll}
toolbar={subcategoryListToolbar}
onNextPage={onNextPage} onNextPage={onNextPage}
onPreviousPage={onPreviousPage} onPreviousPage={onPreviousPage}
onRowClick={onCategoryClick} onRowClick={onProductClick}
onSort={() => undefined} onAdd={onAddProduct}
toggle={toggle}
toggleAll={toggleAll}
selected={selected}
isChecked={isChecked}
toolbar={productListToolbar}
/> />
</Card> )}
)} <SaveButtonBar
{currentTab === CategoryPageTab.products && ( onCancel={onBack}
<CategoryProducts onDelete={onDelete}
categoryName={maybe(() => category.name)} onSave={submit}
products={products} state={saveButtonBarState}
disabled={disabled} disabled={disabled || !hasChanged}
pageInfo={pageInfo}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
onRowClick={onProductClick}
onAdd={onAddProduct}
toggle={toggle}
toggleAll={toggleAll}
selected={selected}
isChecked={isChecked}
toolbar={productListToolbar}
/> />
)} </Container>
<SaveButtonBar );
onCancel={onBack} }}
onDelete={onDelete}
onSave={submit}
state={saveButtonBarState}
disabled={disabled || !hasChanged}
/>
</Container>
)}
</Form> </Form>
); );
}; };

View file

@ -105,11 +105,19 @@ export const category: (
}, },
descriptionJson: JSON.stringify(content), descriptionJson: JSON.stringify(content),
id: "Q2F0ZWdvcnk6NA==", id: "Q2F0ZWdvcnk6NA==",
metadata: [
{
__typename: "MetadataItem",
key: "integration.id",
value: "100023123"
}
],
name: "Coffees", name: "Coffees",
parent: { parent: {
__typename: "Category", __typename: "Category",
id: "Q2F0ZWdvcnk6Mw==" id: "Q2F0ZWdvcnk6Mw=="
}, },
privateMetadata: [],
products: { products: {
__typename: "ProductCountableConnection", __typename: "ProductCountableConnection",
edges: [ edges: [

View file

@ -8,6 +8,18 @@ import { CategoryInput, ProductErrorCode } from "./../../types/globalTypes";
// GraphQL mutation operation: CategoryCreate // GraphQL mutation operation: CategoryCreate
// ==================================================== // ====================================================
export interface CategoryCreate_categoryCreate_category_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CategoryCreate_categoryCreate_category_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CategoryCreate_categoryCreate_category_backgroundImage { export interface CategoryCreate_categoryCreate_category_backgroundImage {
__typename: "Image"; __typename: "Image";
alt: string | null; alt: string | null;
@ -22,6 +34,8 @@ export interface CategoryCreate_categoryCreate_category_parent {
export interface CategoryCreate_categoryCreate_category { export interface CategoryCreate_categoryCreate_category {
__typename: "Category"; __typename: "Category";
id: string; id: string;
metadata: (CategoryCreate_categoryCreate_category_metadata | null)[];
privateMetadata: (CategoryCreate_categoryCreate_category_privateMetadata | null)[];
backgroundImage: CategoryCreate_categoryCreate_category_backgroundImage | null; backgroundImage: CategoryCreate_categoryCreate_category_backgroundImage | null;
name: string; name: string;
descriptionJson: any; descriptionJson: any;

View file

@ -6,6 +6,18 @@
// GraphQL query operation: CategoryDetails // GraphQL query operation: CategoryDetails
// ==================================================== // ====================================================
export interface CategoryDetails_category_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CategoryDetails_category_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CategoryDetails_category_backgroundImage { export interface CategoryDetails_category_backgroundImage {
__typename: "Image"; __typename: "Image";
alt: string | null; alt: string | null;
@ -131,6 +143,8 @@ export interface CategoryDetails_category_products {
export interface CategoryDetails_category { export interface CategoryDetails_category {
__typename: "Category"; __typename: "Category";
id: string; id: string;
metadata: (CategoryDetails_category_metadata | null)[];
privateMetadata: (CategoryDetails_category_privateMetadata | null)[];
backgroundImage: CategoryDetails_category_backgroundImage | null; backgroundImage: CategoryDetails_category_backgroundImage | null;
name: string; name: string;
descriptionJson: any; descriptionJson: any;

View file

@ -8,6 +8,18 @@ import { CategoryInput, ProductErrorCode } from "./../../types/globalTypes";
// GraphQL mutation operation: CategoryUpdate // GraphQL mutation operation: CategoryUpdate
// ==================================================== // ====================================================
export interface CategoryUpdate_categoryUpdate_category_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CategoryUpdate_categoryUpdate_category_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CategoryUpdate_categoryUpdate_category_backgroundImage { export interface CategoryUpdate_categoryUpdate_category_backgroundImage {
__typename: "Image"; __typename: "Image";
alt: string | null; alt: string | null;
@ -22,6 +34,8 @@ export interface CategoryUpdate_categoryUpdate_category_parent {
export interface CategoryUpdate_categoryUpdate_category { export interface CategoryUpdate_categoryUpdate_category {
__typename: "Category"; __typename: "Category";
id: string; id: string;
metadata: (CategoryUpdate_categoryUpdate_category_metadata | null)[];
privateMetadata: (CategoryUpdate_categoryUpdate_category_privateMetadata | null)[];
backgroundImage: CategoryUpdate_categoryUpdate_category_backgroundImage | null; backgroundImage: CategoryUpdate_categoryUpdate_category_backgroundImage | null;
name: string; name: string;
descriptionJson: any; descriptionJson: any;

View file

@ -12,6 +12,11 @@ import usePaginator, {
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import {
useMetadataUpdate,
usePrivateMetadataUpdate
} from "@saleor/utils/metadata/updateMetadata";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -22,7 +27,8 @@ import { productAddUrl, productUrl } from "../../products/urls";
import { CategoryInput } from "../../types/globalTypes"; import { CategoryInput } from "../../types/globalTypes";
import { import {
CategoryPageTab, CategoryPageTab,
CategoryUpdatePage CategoryUpdatePage,
FormData
} from "../components/CategoryUpdatePage/CategoryUpdatePage"; } from "../components/CategoryUpdatePage/CategoryUpdatePage";
import { import {
useCategoryBulkDeleteMutation, useCategoryBulkDeleteMutation,
@ -63,6 +69,8 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
params.ids params.ids
); );
const intl = useIntl(); const intl = useIntl();
const [updateMetadata] = useMetadataUpdate({});
const [updatePrivateMetadata] = usePrivateMetadataUpdate({});
const paginationState = createPaginationState(PAGINATE_BY, params); const paginationState = createPaginationState(PAGINATE_BY, params);
const { data, loading, refetch } = useCategoryDetailsQuery({ const { data, loading, refetch } = useCategoryDetailsQuery({
@ -167,6 +175,31 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
params params
); );
const handleUpdate = async (formData: FormData) => {
const result = await updateCategory({
variables: {
id,
input: {
backgroundImageAlt: formData.backgroundImageAlt,
descriptionJson: JSON.stringify(formData.description),
name: formData.name,
seo: {
description: formData.seoDescription,
title: formData.seoTitle
}
}
}
});
return result.data.categoryUpdate.errors;
};
const handleSubmit = createMetadataUpdateHandler(
data?.category,
handleUpdate,
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
);
return ( return (
<> <>
<WindowTitle title={maybe(() => data.category.name)} /> <WindowTitle title={maybe(() => data.category.name)} />
@ -209,22 +242,7 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}
pageInfo={pageInfo} pageInfo={pageInfo}
onProductClick={id => () => navigate(productUrl(id))} onProductClick={id => () => navigate(productUrl(id))}
onSubmit={formData => onSubmit={handleSubmit}
updateCategory({
variables: {
id,
input: {
backgroundImageAlt: formData.backgroundImageAlt,
descriptionJson: JSON.stringify(formData.description),
name: formData.name,
seo: {
description: formData.seoDescription,
title: formData.seoTitle
}
}
}
})
}
products={maybe(() => products={maybe(() =>
data.category.products.edges.map(edge => edge.node) data.category.products.edges.map(edge => edge.node)
)} )}

View file

@ -7,6 +7,8 @@ import Form from "@saleor/components/Form";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Hr from "@saleor/components/Hr"; import Hr from "@saleor/components/Hr";
import Metadata from "@saleor/components/Metadata/Metadata";
import { MetadataFormData } from "@saleor/components/Metadata/types";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm"; import SeoForm from "@saleor/components/SeoForm";
@ -14,6 +16,8 @@ import VisibilityCard from "@saleor/components/VisibilityCard";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment"; import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useDateLocalize from "@saleor/hooks/useDateLocalize";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { RawDraftContentState } from "draft-js"; import { RawDraftContentState } from "draft-js";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -25,7 +29,7 @@ import CollectionDetails from "../CollectionDetails/CollectionDetails";
import { CollectionImage } from "../CollectionImage/CollectionImage"; import { CollectionImage } from "../CollectionImage/CollectionImage";
import CollectionProducts from "../CollectionProducts/CollectionProducts"; import CollectionProducts from "../CollectionProducts/CollectionProducts";
export interface CollectionDetailsPageFormData { export interface CollectionDetailsPageFormData extends MetadataFormData {
backgroundImageAlt: string; backgroundImageAlt: string;
description: RawDraftContentState; description: RawDraftContentState;
name: string; name: string;
@ -64,6 +68,24 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
}: CollectionDetailsPageProps) => { }: CollectionDetailsPageProps) => {
const intl = useIntl(); const intl = useIntl();
const localizeDate = useDateLocalize(); const localizeDate = useDateLocalize();
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const handleSubmit = (data: CollectionDetailsPageFormData) => {
const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
onSubmit({
...data,
metadata,
privateMetadata
});
};
return ( return (
<Form <Form
@ -72,108 +94,118 @@ const CollectionDetailsPage: React.FC<CollectionDetailsPageProps> = ({
description: maybe(() => JSON.parse(collection.descriptionJson)), description: maybe(() => JSON.parse(collection.descriptionJson)),
isFeatured, isFeatured,
isPublished: maybe(() => collection.isPublished, false), isPublished: maybe(() => collection.isPublished, false),
metadata: collection?.metadata?.map(mapMetadataItemToInput),
name: maybe(() => collection.name, ""), name: maybe(() => collection.name, ""),
privateMetadata: collection?.privateMetadata?.map(
mapMetadataItemToInput
),
publicationDate: maybe(() => collection.publicationDate, ""), publicationDate: maybe(() => collection.publicationDate, ""),
seoDescription: maybe(() => collection.seoDescription, ""), seoDescription: maybe(() => collection.seoDescription, ""),
seoTitle: maybe(() => collection.seoTitle, "") seoTitle: maybe(() => collection.seoTitle, "")
}} }}
onSubmit={onSubmit} onSubmit={handleSubmit}
confirmLeave confirmLeave
> >
{({ change, data, hasChanged, submit }) => ( {({ change, data, hasChanged, submit }) => {
<Container> const changeMetadata = makeMetadataChangeHandler(change);
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.collections)} return (
</AppHeader> <Container>
<PageHeader title={maybe(() => collection.name)} /> <AppHeader onBack={onBack}>
<Grid> {intl.formatMessage(sectionNames.collections)}
<div> </AppHeader>
<CollectionDetails <PageHeader title={maybe(() => collection.name)} />
collection={collection} <Grid>
data={data}
disabled={disabled}
errors={errors}
onChange={change}
/>
<CardSpacer />
<CollectionImage
data={data}
image={maybe(() => collection.backgroundImage)}
onImageDelete={onImageDelete}
onImageUpload={onImageUpload}
onChange={change}
/>
<CardSpacer />
<CollectionProducts
disabled={disabled}
collection={collection}
{...collectionProductsProps}
/>
<CardSpacer />
<SeoForm
description={data.seoDescription}
disabled={disabled}
descriptionPlaceholder=""
helperText={intl.formatMessage({
defaultMessage:
"Add search engine title and description to make this collection easier to find"
})}
title={data.seoTitle}
titlePlaceholder={maybe(() => collection.name)}
onChange={change}
/>
</div>
<div>
<div> <div>
<VisibilityCard <CollectionDetails
collection={collection}
data={data} data={data}
errors={errors}
disabled={disabled} disabled={disabled}
hiddenMessage={intl.formatMessage( errors={errors}
{
defaultMessage: "will be visible from {date}",
description: "collection"
},
{
date: localizeDate(data.publicationDate)
}
)}
onChange={change} onChange={change}
visibleMessage={intl.formatMessage( />
{ <CardSpacer />
defaultMessage: "since {date}", <CollectionImage
description: "collection" data={data}
}, image={maybe(() => collection.backgroundImage)}
{ onImageDelete={onImageDelete}
date: localizeDate(data.publicationDate) onImageUpload={onImageUpload}
} onChange={change}
)} />
> <CardSpacer />
<FormSpacer /> <Metadata data={data} onChange={changeMetadata} />
<Hr /> <CardSpacer />
<ControlledCheckbox <CollectionProducts
name={"isFeatured" as keyof CollectionDetailsPageFormData} disabled={disabled}
label={intl.formatMessage({ collection={collection}
defaultMessage: "Feature on Homepage", {...collectionProductsProps}
description: "switch button" />
})} <CardSpacer />
checked={data.isFeatured} <SeoForm
onChange={change} description={data.seoDescription}
disabled={disabled} disabled={disabled}
/> descriptionPlaceholder=""
</VisibilityCard> helperText={intl.formatMessage({
defaultMessage:
"Add search engine title and description to make this collection easier to find"
})}
title={data.seoTitle}
titlePlaceholder={maybe(() => collection.name)}
onChange={change}
/>
</div> </div>
</div> <div>
</Grid> <div>
<SaveButtonBar <VisibilityCard
state={saveButtonBarState} data={data}
disabled={disabled || !hasChanged} errors={errors}
onCancel={onBack} disabled={disabled}
onDelete={onCollectionRemove} hiddenMessage={intl.formatMessage(
onSave={submit} {
/> defaultMessage: "will be visible from {date}",
</Container> description: "collection"
)} },
{
date: localizeDate(data.publicationDate)
}
)}
onChange={change}
visibleMessage={intl.formatMessage(
{
defaultMessage: "since {date}",
description: "collection"
},
{
date: localizeDate(data.publicationDate)
}
)}
>
<FormSpacer />
<Hr />
<ControlledCheckbox
name={"isFeatured" as keyof CollectionDetailsPageFormData}
label={intl.formatMessage({
defaultMessage: "Feature on Homepage",
description: "switch button"
})}
checked={data.isFeatured}
onChange={change}
disabled={disabled}
/>
</VisibilityCard>
</div>
</div>
</Grid>
<SaveButtonBar
state={saveButtonBarState}
disabled={disabled || !hasChanged}
onCancel={onBack}
onDelete={onCollectionRemove}
onSave={submit}
/>
</Container>
);
}}
</Form> </Form>
); );
}; };

View file

@ -1,113 +0,0 @@
import React from "react";
import { getMutationProviderData } from "../../misc";
import { PartialMutationProviderOutput } from "../../types";
import {
TypedCollectionAssignProductMutation,
TypedCollectionRemoveMutation,
TypedCollectionUpdateMutation,
TypedCollectionUpdateWithHomepageMutation,
TypedUnassignCollectionProductMutation
} from "../mutations";
import {
CollectionAssignProduct,
CollectionAssignProductVariables
} from "../types/CollectionAssignProduct";
import {
CollectionUpdate,
CollectionUpdateVariables
} from "../types/CollectionUpdate";
import {
CollectionUpdateWithHomepage,
CollectionUpdateWithHomepageVariables
} from "../types/CollectionUpdateWithHomepage";
import {
RemoveCollection,
RemoveCollectionVariables
} from "../types/RemoveCollection";
import {
UnassignCollectionProduct,
UnassignCollectionProductVariables
} from "../types/UnassignCollectionProduct";
interface CollectionUpdateOperationsProps {
children: (props: {
updateCollectionWithHomepage: PartialMutationProviderOutput<
CollectionUpdateWithHomepage,
CollectionUpdateWithHomepageVariables
>;
assignProduct: PartialMutationProviderOutput<
CollectionAssignProduct,
CollectionAssignProductVariables
>;
unassignProduct: PartialMutationProviderOutput<
UnassignCollectionProduct,
UnassignCollectionProductVariables
>;
updateCollection: PartialMutationProviderOutput<
CollectionUpdate,
CollectionUpdateVariables
>;
removeCollection: PartialMutationProviderOutput<
RemoveCollection,
RemoveCollectionVariables
>;
}) => React.ReactNode;
onUpdate: (data: CollectionUpdate) => void;
onUpdateWithCollection: (data: CollectionUpdateWithHomepage) => void;
onProductAssign: (data: CollectionAssignProduct) => void;
onProductUnassign: (data: UnassignCollectionProduct) => void;
onRemove: (data: RemoveCollection) => void;
}
const CollectionOperations: React.FC<CollectionUpdateOperationsProps> = ({
children,
onUpdate,
onUpdateWithCollection,
onProductAssign,
onProductUnassign,
onRemove
}) => (
<TypedCollectionUpdateMutation onCompleted={onUpdate}>
{(...updateCollection) => (
<TypedCollectionRemoveMutation onCompleted={onRemove}>
{(...removeCollection) => (
<TypedCollectionAssignProductMutation onCompleted={onProductAssign}>
{(...assignProduct) => (
<TypedCollectionUpdateWithHomepageMutation
onCompleted={onUpdateWithCollection}
>
{(...updateWithHomepage) => (
<TypedUnassignCollectionProductMutation
onCompleted={onProductUnassign}
>
{(...unassignProduct) =>
children({
assignProduct: getMutationProviderData(
...assignProduct
),
removeCollection: getMutationProviderData(
...removeCollection
),
unassignProduct: getMutationProviderData(
...unassignProduct
),
updateCollection: getMutationProviderData(
...updateCollection
),
updateCollectionWithHomepage: getMutationProviderData(
...updateWithHomepage
)
})
}
</TypedUnassignCollectionProductMutation>
)}
</TypedCollectionUpdateWithHomepageMutation>
)}
</TypedCollectionAssignProductMutation>
)}
</TypedCollectionRemoveMutation>
)}
</TypedCollectionUpdateMutation>
);
export default CollectionOperations;

View file

@ -80,7 +80,15 @@ export const collection: (
descriptionJson: JSON.stringify(content), descriptionJson: JSON.stringify(content),
id: "Q29sbGVjdGlvbjox", id: "Q29sbGVjdGlvbjox",
isPublished: true, isPublished: true,
metadata: [
{
__typename: "MetadataItem",
key: "integration.id",
value: "100023123"
}
],
name: "Summer collection", name: "Summer collection",
privateMetadata: [],
products: { products: {
__typename: "ProductCountableConnection", __typename: "ProductCountableConnection",
edges: [ edges: [

View file

@ -6,9 +6,9 @@ import {
productErrorFragment, productErrorFragment,
shopErrorFragment shopErrorFragment
} from "@saleor/fragments/errors"; } from "@saleor/fragments/errors";
import makeMutation from "@saleor/hooks/makeMutation";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { TypedMutation } from "../mutations";
import { import {
CollectionAssignProduct, CollectionAssignProduct,
CollectionAssignProductVariables CollectionAssignProductVariables
@ -56,7 +56,7 @@ const collectionUpdate = gql`
} }
} }
`; `;
export const TypedCollectionUpdateMutation = TypedMutation< export const useCollectionUpdateMutation = makeMutation<
CollectionUpdate, CollectionUpdate,
CollectionUpdateVariables CollectionUpdateVariables
>(collectionUpdate); >(collectionUpdate);
@ -90,7 +90,7 @@ const collectionUpdateWithHomepage = gql`
} }
} }
`; `;
export const TypedCollectionUpdateWithHomepageMutation = TypedMutation< export const useCollectionUpdateWithHomepageMutation = makeMutation<
CollectionUpdateWithHomepage, CollectionUpdateWithHomepage,
CollectionUpdateWithHomepageVariables CollectionUpdateWithHomepageVariables
>(collectionUpdateWithHomepage); >(collectionUpdateWithHomepage);
@ -129,7 +129,7 @@ const assignCollectionProduct = gql`
} }
} }
`; `;
export const TypedCollectionAssignProductMutation = TypedMutation< export const useCollectionAssignProductMutation = makeMutation<
CollectionAssignProduct, CollectionAssignProduct,
CollectionAssignProductVariables CollectionAssignProductVariables
>(assignCollectionProduct); >(assignCollectionProduct);
@ -148,7 +148,7 @@ const createCollection = gql`
} }
} }
`; `;
export const TypedCollectionCreateMutation = TypedMutation< export const useCollectionCreateMutation = makeMutation<
CreateCollection, CreateCollection,
CreateCollectionVariables CreateCollectionVariables
>(createCollection); >(createCollection);
@ -163,7 +163,7 @@ const removeCollection = gql`
} }
} }
`; `;
export const TypedCollectionRemoveMutation = TypedMutation< export const useCollectionRemoveMutation = makeMutation<
RemoveCollection, RemoveCollection,
RemoveCollectionVariables RemoveCollectionVariables
>(removeCollection); >(removeCollection);
@ -213,7 +213,7 @@ const unassignCollectionProduct = gql`
} }
} }
`; `;
export const TypedUnassignCollectionProductMutation = TypedMutation< export const useUnassignCollectionProductMutation = makeMutation<
UnassignCollectionProduct, UnassignCollectionProduct,
UnassignCollectionProductVariables UnassignCollectionProductVariables
>(unassignCollectionProduct); >(unassignCollectionProduct);
@ -228,7 +228,7 @@ const collectionBulkDelete = gql`
} }
} }
`; `;
export const TypedCollectionBulkDelete = TypedMutation< export const useCollectionBulkDelete = makeMutation<
CollectionBulkDelete, CollectionBulkDelete,
CollectionBulkDeleteVariables CollectionBulkDeleteVariables
>(collectionBulkDelete); >(collectionBulkDelete);
@ -243,7 +243,7 @@ const collectionBulkPublish = gql`
} }
} }
`; `;
export const TypedCollectionBulkPublish = TypedMutation< export const useCollectionBulkPublish = makeMutation<
CollectionBulkPublish, CollectionBulkPublish,
CollectionBulkPublishVariables CollectionBulkPublishVariables
>(collectionBulkPublish); >(collectionBulkPublish);

View file

@ -6,6 +6,18 @@
// GraphQL query operation: CollectionDetails // GraphQL query operation: CollectionDetails
// ==================================================== // ====================================================
export interface CollectionDetails_collection_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CollectionDetails_collection_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CollectionDetails_collection_backgroundImage { export interface CollectionDetails_collection_backgroundImage {
__typename: "Image"; __typename: "Image";
alt: string | null; alt: string | null;
@ -56,6 +68,8 @@ export interface CollectionDetails_collection {
id: string; id: string;
isPublished: boolean; isPublished: boolean;
name: string; name: string;
metadata: (CollectionDetails_collection_metadata | null)[];
privateMetadata: (CollectionDetails_collection_privateMetadata | null)[];
backgroundImage: CollectionDetails_collection_backgroundImage | null; backgroundImage: CollectionDetails_collection_backgroundImage | null;
descriptionJson: any; descriptionJson: any;
publicationDate: any | null; publicationDate: any | null;

View file

@ -8,6 +8,18 @@ import { CollectionInput, ProductErrorCode } from "./../../types/globalTypes";
// GraphQL mutation operation: CollectionUpdate // GraphQL mutation operation: CollectionUpdate
// ==================================================== // ====================================================
export interface CollectionUpdate_collectionUpdate_collection_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CollectionUpdate_collectionUpdate_collection_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CollectionUpdate_collectionUpdate_collection_backgroundImage { export interface CollectionUpdate_collectionUpdate_collection_backgroundImage {
__typename: "Image"; __typename: "Image";
alt: string | null; alt: string | null;
@ -19,6 +31,8 @@ export interface CollectionUpdate_collectionUpdate_collection {
id: string; id: string;
isPublished: boolean; isPublished: boolean;
name: string; name: string;
metadata: (CollectionUpdate_collectionUpdate_collection_metadata | null)[];
privateMetadata: (CollectionUpdate_collectionUpdate_collection_privateMetadata | null)[];
backgroundImage: CollectionUpdate_collectionUpdate_collection_backgroundImage | null; backgroundImage: CollectionUpdate_collectionUpdate_collection_backgroundImage | null;
descriptionJson: any; descriptionJson: any;
publicationDate: any | null; publicationDate: any | null;

View file

@ -30,6 +30,18 @@ export interface CollectionUpdateWithHomepage_homepageCollectionUpdate {
shop: CollectionUpdateWithHomepage_homepageCollectionUpdate_shop | null; shop: CollectionUpdateWithHomepage_homepageCollectionUpdate_shop | null;
} }
export interface CollectionUpdateWithHomepage_collectionUpdate_collection_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CollectionUpdateWithHomepage_collectionUpdate_collection_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CollectionUpdateWithHomepage_collectionUpdate_collection_backgroundImage { export interface CollectionUpdateWithHomepage_collectionUpdate_collection_backgroundImage {
__typename: "Image"; __typename: "Image";
alt: string | null; alt: string | null;
@ -41,6 +53,8 @@ export interface CollectionUpdateWithHomepage_collectionUpdate_collection {
id: string; id: string;
isPublished: boolean; isPublished: boolean;
name: string; name: string;
metadata: (CollectionUpdateWithHomepage_collectionUpdate_collection_metadata | null)[];
privateMetadata: (CollectionUpdateWithHomepage_collectionUpdate_collection_privateMetadata | null)[];
backgroundImage: CollectionUpdateWithHomepage_collectionUpdate_collection_backgroundImage | null; backgroundImage: CollectionUpdateWithHomepage_collectionUpdate_collection_backgroundImage | null;
descriptionJson: any; descriptionJson: any;
publicationDate: any | null; publicationDate: any | null;

View file

@ -8,6 +8,18 @@ import { CollectionCreateInput, ProductErrorCode } from "./../../types/globalTyp
// GraphQL mutation operation: CreateCollection // GraphQL mutation operation: CreateCollection
// ==================================================== // ====================================================
export interface CreateCollection_collectionCreate_collection_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CreateCollection_collectionCreate_collection_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CreateCollection_collectionCreate_collection_backgroundImage { export interface CreateCollection_collectionCreate_collection_backgroundImage {
__typename: "Image"; __typename: "Image";
alt: string | null; alt: string | null;
@ -19,6 +31,8 @@ export interface CreateCollection_collectionCreate_collection {
id: string; id: string;
isPublished: boolean; isPublished: boolean;
name: string; name: string;
metadata: (CreateCollection_collectionCreate_collection_metadata | null)[];
privateMetadata: (CreateCollection_collectionCreate_collection_privateMetadata | null)[];
backgroundImage: CreateCollection_collectionCreate_collection_backgroundImage | null; backgroundImage: CreateCollection_collectionCreate_collection_backgroundImage | null;
descriptionJson: any; descriptionJson: any;
publicationDate: any | null; publicationDate: any | null;

View file

@ -7,8 +7,7 @@ import { useIntl } from "react-intl";
import { CollectionCreateInput } from "../../types/globalTypes"; import { CollectionCreateInput } from "../../types/globalTypes";
import CollectionCreatePage from "../components/CollectionCreatePage/CollectionCreatePage"; import CollectionCreatePage from "../components/CollectionCreatePage/CollectionCreatePage";
import { TypedCollectionCreateMutation } from "../mutations"; import { useCollectionCreateMutation } from "../mutations";
import { CreateCollection } from "../types/CreateCollection";
import { collectionListUrl, collectionUrl } from "../urls"; import { collectionListUrl, collectionUrl } from "../urls";
export const CollectionCreate: React.FC = () => { export const CollectionCreate: React.FC = () => {
@ -16,62 +15,61 @@ export const CollectionCreate: React.FC = () => {
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl(); const intl = useIntl();
const handleCollectionCreateSuccess = (data: CreateCollection) => { const [createCollection, createCollectionOpts] = useCollectionCreateMutation({
if (data.collectionCreate.errors.length === 0) { onCompleted: data => {
notify({ if (data.collectionCreate.errors.length === 0) {
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
navigate(collectionUrl(data.collectionCreate.collection.id));
} else {
const backgroundImageError = data.collectionCreate.errors.find(
error =>
error.field === ("backgroundImage" as keyof CollectionCreateInput)
);
if (backgroundImageError) {
notify({ notify({
status: "error", status: "success",
text: intl.formatMessage(commonMessages.somethingWentWrong) text: intl.formatMessage(commonMessages.savedChanges)
}); });
navigate(collectionUrl(data.collectionCreate.collection.id));
} else {
const backgroundImageError = data.collectionCreate.errors.find(
error =>
error.field === ("backgroundImage" as keyof CollectionCreateInput)
);
if (backgroundImageError) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
} }
} }
}; });
return ( return (
<TypedCollectionCreateMutation onCompleted={handleCollectionCreateSuccess}> <>
{(createCollection, createCollectionOpts) => ( <WindowTitle
<> title={intl.formatMessage({
<WindowTitle defaultMessage: "Create collection",
title={intl.formatMessage({ description: "window title"
defaultMessage: "Create collection", })}
description: "window title" />
})} <CollectionCreatePage
/> errors={createCollectionOpts.data?.collectionCreate.errors || []}
<CollectionCreatePage onBack={() => navigate(collectionListUrl())}
errors={createCollectionOpts.data?.collectionCreate.errors || []} disabled={createCollectionOpts.loading}
onBack={() => navigate(collectionListUrl())} onSubmit={formData =>
disabled={createCollectionOpts.loading} createCollection({
onSubmit={formData => variables: {
createCollection({ input: {
variables: { backgroundImage: formData.backgroundImage.value,
input: { backgroundImageAlt: formData.backgroundImageAlt,
backgroundImage: formData.backgroundImage.value, descriptionJson: JSON.stringify(formData.description),
backgroundImageAlt: formData.backgroundImageAlt, isPublished: formData.isPublished,
descriptionJson: JSON.stringify(formData.description), name: formData.name,
isPublished: formData.isPublished, seo: {
name: formData.name, description: formData.seoDescription,
seo: { title: formData.seoTitle
description: formData.seoDescription,
title: formData.seoTitle
}
}
} }
}) }
} }
saveButtonBarState={createCollectionOpts.status} })
/> }
</> saveButtonBarState={createCollectionOpts.status}
)} />
</TypedCollectionCreateMutation> </>
); );
}; };
export default CollectionCreate; export default CollectionCreate;

View file

@ -14,6 +14,11 @@ import usePaginator, {
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import useProductSearch from "@saleor/searches/useProductSearch"; import useProductSearch from "@saleor/searches/useProductSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import {
useMetadataUpdate,
usePrivateMetadataUpdate
} from "@saleor/utils/metadata/updateMetadata";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -23,13 +28,15 @@ import { CollectionInput } from "../../types/globalTypes";
import CollectionDetailsPage, { import CollectionDetailsPage, {
CollectionDetailsPageFormData CollectionDetailsPageFormData
} from "../components/CollectionDetailsPage/CollectionDetailsPage"; } from "../components/CollectionDetailsPage/CollectionDetailsPage";
import CollectionOperations from "../containers/CollectionOperations"; import {
useCollectionAssignProductMutation,
useCollectionRemoveMutation,
useCollectionUpdateMutation,
useCollectionUpdateWithHomepageMutation,
useUnassignCollectionProductMutation
} from "../mutations";
import { TypedCollectionDetailsQuery } from "../queries"; import { TypedCollectionDetailsQuery } from "../queries";
import { CollectionAssignProduct } from "../types/CollectionAssignProduct";
import { CollectionUpdate } from "../types/CollectionUpdate"; import { CollectionUpdate } from "../types/CollectionUpdate";
import { CollectionUpdateWithHomepage } from "../types/CollectionUpdateWithHomepage";
import { RemoveCollection } from "../types/RemoveCollection";
import { UnassignCollectionProduct } from "../types/UnassignCollectionProduct";
import { import {
collectionListUrl, collectionListUrl,
collectionUrl, collectionUrl,
@ -56,6 +63,90 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
const { search, result } = useProductSearch({ const { search, result } = useProductSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA variables: DEFAULT_INITIAL_SEARCH_DATA
}); });
const [updateMetadata] = useMetadataUpdate({});
const [updatePrivateMetadata] = usePrivateMetadataUpdate({});
const handleCollectionUpdate = (data: CollectionUpdate) => {
if (data.collectionUpdate.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
navigate(collectionUrl(id));
} else {
const backgroundImageError = data.collectionUpdate.errors.find(
error => error.field === ("backgroundImage" as keyof CollectionInput)
);
if (backgroundImageError) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
}
};
const [updateCollection, updateCollectionOpts] = useCollectionUpdateMutation({
onCompleted: handleCollectionUpdate
});
const [
updateCollectionWithHomepage,
updateCollectionWithHomepageOpts
] = useCollectionUpdateWithHomepageMutation({
onCompleted: data => {
if (data.homepageCollectionUpdate.errors.length === 0) {
handleCollectionUpdate(data);
}
}
});
const [assignProduct, assignProductOpts] = useCollectionAssignProductMutation(
{
onCompleted: data => {
if (data.collectionAddProducts.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Added product to collection"
})
});
navigate(collectionUrl(id), true);
}
}
}
);
const [
unassignProduct,
unassignProductOpts
] = useUnassignCollectionProductMutation({
onCompleted: data => {
if (data.collectionRemoveProducts.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Deleted product from collection"
})
});
reset();
closeModal();
}
}
});
const [removeCollection, removeCollectionOpts] = useCollectionRemoveMutation({
onCompleted: data => {
if (data.collectionDelete.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Deleted collection"
})
});
navigate(collectionListUrl());
}
}
});
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
CollectionUrlDialog, CollectionUrlDialog,
@ -76,298 +167,236 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
if (collection === null) { if (collection === null) {
return <NotFoundPage onBack={handleBack} />; return <NotFoundPage onBack={handleBack} />;
} }
const handleCollectionUpdate = (data: CollectionUpdate) => {
if (data.collectionUpdate.errors.length === 0) { const handleUpdate = async (
notify({ formData: CollectionDetailsPageFormData
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
navigate(collectionUrl(id));
} else {
const backgroundImageError = data.collectionUpdate.errors.find(
error =>
error.field === ("backgroundImage" as keyof CollectionInput)
);
if (backgroundImageError) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
}
};
const handleCollectioUpdateWithHomepage = (
data: CollectionUpdateWithHomepage
) => { ) => {
if (data.homepageCollectionUpdate.errors.length === 0) { const input: CollectionInput = {
handleCollectionUpdate(data); backgroundImageAlt: formData.backgroundImageAlt,
} descriptionJson: JSON.stringify(formData.description),
}; isPublished: formData.isPublished,
name: formData.name,
publicationDate: formData.publicationDate,
seo: {
description: formData.seoDescription,
title: formData.seoTitle
}
};
const isFeatured = data.shop.homepageCollection
? data.shop.homepageCollection.id === data.collection.id
: false;
const handleProductAssign = (data: CollectionAssignProduct) => { if (formData.isFeatured !== isFeatured) {
if (data.collectionAddProducts.errors.length === 0) { const result = await updateCollectionWithHomepage({
notify({ variables: {
status: "success", homepageId: formData.isFeatured ? id : null,
text: intl.formatMessage({ id,
defaultMessage: "Added product to collection" input
}) }
});
return [
...result.data.collectionUpdate.errors,
...result.data.homepageCollectionUpdate.errors
];
} else {
const result = await updateCollection({
variables: {
id,
input
}
}); });
navigate(collectionUrl(id), true);
}
};
const handleProductUnassign = (data: UnassignCollectionProduct) => { return result.data.collectionUpdate.errors;
if (data.collectionRemoveProducts.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Deleted product from collection"
})
});
reset();
closeModal();
} }
}; };
const handleSubmit = createMetadataUpdateHandler(
data?.collection,
handleUpdate,
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
);
const formTransitionState = getMutationState(
updateCollectionOpts.called ||
updateCollectionWithHomepageOpts.called,
updateCollectionOpts.loading ||
updateCollectionWithHomepageOpts.loading,
updateCollectionOpts.data?.collectionUpdate.errors,
updateCollectionWithHomepageOpts.data?.collectionUpdate.errors,
updateCollectionWithHomepageOpts.data?.homepageCollectionUpdate.errors
);
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
data?.collection?.products?.pageInfo,
paginationState,
params
);
const handleCollectionRemove = (data: RemoveCollection) => {
if (data.collectionDelete.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Deleted collection"
})
});
navigate(collectionListUrl());
}
};
return ( return (
<CollectionOperations <>
onUpdate={handleCollectionUpdate} <WindowTitle title={maybe(() => data.collection.name)} />
onUpdateWithCollection={handleCollectioUpdateWithHomepage} <CollectionDetailsPage
onProductAssign={handleProductAssign} onAdd={() => openModal("assign")}
onProductUnassign={handleProductUnassign} onBack={handleBack}
onRemove={handleCollectionRemove} disabled={loading}
> collection={data?.collection}
{({ errors={updateCollectionOpts?.data?.collectionUpdate.errors || []}
updateCollection, isFeatured={maybe(
updateCollectionWithHomepage, () => data.shop.homepageCollection.id === data.collection.id,
assignProduct, false
unassignProduct, )}
removeCollection onCollectionRemove={() => openModal("remove")}
}) => { onImageDelete={() => openModal("removeImage")}
const handleSubmit = ( onImageUpload={file =>
formData: CollectionDetailsPageFormData updateCollection({
) => { variables: {
const input: CollectionInput = { id,
backgroundImageAlt: formData.backgroundImageAlt, input: {
descriptionJson: JSON.stringify(formData.description), backgroundImage: file
isPublished: formData.isPublished, }
name: formData.name,
publicationDate: formData.publicationDate,
seo: {
description: formData.seoDescription,
title: formData.seoTitle
} }
}; })
const isFeatured = data.shop.homepageCollection }
? data.shop.homepageCollection.id === data.collection.id onSubmit={handleSubmit}
: false; onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
if (formData.isFeatured !== isFeatured) { pageInfo={pageInfo}
updateCollectionWithHomepage.mutate({ onProductUnassign={(productId, event) => {
homepageId: formData.isFeatured ? id : null, event.stopPropagation();
id, unassignProduct({
input variables: {
}); collectionId: id,
} else { productIds: [productId],
updateCollection.mutate({ ...paginationState
id, }
input });
}); }}
} onRowClick={id => () => navigate(productUrl(id))}
}; saveButtonBarState={formTransitionState}
toolbar={
const formTransitionState = getMutationState( <Button
updateCollection.opts.called || color="primary"
updateCollectionWithHomepage.opts.called, onClick={() =>
updateCollection.opts.loading || openModal("unassign", {
updateCollectionWithHomepage.opts.loading, ids: listElements
maybe(() => updateCollection.opts.data.collectionUpdate.errors), })
maybe( }
() => >
updateCollectionWithHomepage.opts.data.collectionUpdate <FormattedMessage
.errors defaultMessage="Unassign"
), description="unassign product from collection, button"
maybe(
() =>
updateCollectionWithHomepage.opts.data
.homepageCollectionUpdate.errors
)
);
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
maybe(() => data.collection.products.pageInfo),
paginationState,
params
);
return (
<>
<WindowTitle title={maybe(() => data.collection.name)} />
<CollectionDetailsPage
onAdd={() => openModal("assign")}
onBack={handleBack}
disabled={loading}
collection={data?.collection}
errors={
updateCollection.opts?.data?.collectionUpdate.errors || []
}
isFeatured={maybe(
() =>
data.shop.homepageCollection.id === data.collection.id,
false
)}
onCollectionRemove={() => openModal("remove")}
onImageDelete={() => openModal("removeImage")}
onImageUpload={file =>
updateCollection.mutate({
id,
input: {
backgroundImage: file
}
})
}
onSubmit={handleSubmit}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
pageInfo={pageInfo}
onProductUnassign={(productId, event) => {
event.stopPropagation();
unassignProduct.mutate({
collectionId: id,
productIds: [productId],
...paginationState
});
}}
onRowClick={id => () => navigate(productUrl(id))}
saveButtonBarState={formTransitionState}
toolbar={
<Button
color="primary"
onClick={() =>
openModal("unassign", {
ids: listElements
})
}
>
<FormattedMessage
defaultMessage="Unassign"
description="unassign product from collection, button"
/>
</Button>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
/> />
<AssignProductDialog </Button>
confirmButtonState={assignProduct.opts.status} }
open={params.action === "assign"} isChecked={isSelected}
onFetch={search} selected={listElements.length}
loading={result.loading} toggle={toggle}
onClose={closeModal} toggleAll={toggleAll}
onSubmit={products => />
assignProduct.mutate({ <AssignProductDialog
...paginationState, confirmButtonState={assignProductOpts.status}
collectionId: id, open={params.action === "assign"}
productIds: products.map(product => product.id) onFetch={search}
}) loading={result.loading}
onClose={closeModal}
onSubmit={products =>
assignProduct({
variables: {
...paginationState,
collectionId: id,
productIds: products.map(product => product.id)
}
})
}
products={maybe(() =>
result.data.search.edges
.map(edge => edge.node)
.filter(suggestedProduct => suggestedProduct.id)
)}
/>
<ActionDialog
confirmButtonState={removeCollectionOpts.status}
onClose={closeModal}
onConfirm={() =>
removeCollection({
variables: { id }
})
}
open={params.action === "remove"}
title={intl.formatMessage({
defaultMessage: "Delete Collection",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {collectionName}?"
values={{
collectionName: (
<strong>
{maybe(() => data.collection.name, "...")}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
confirmButtonState={unassignProductOpts.status}
onClose={closeModal}
onConfirm={() =>
unassignProduct({
variables: {
...paginationState,
collectionId: id,
productIds: params.ids
}
})
}
open={params.action === "unassign"}
title={intl.formatMessage({
defaultMessage: "Unassign products from collection",
description: "dialog title"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to unassign this product?} other{Are you sure you want to unassign {displayQuantity} products?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
confirmButtonState={updateCollectionOpts.status}
onClose={closeModal}
onConfirm={() =>
updateCollection({
variables: {
id,
input: {
backgroundImage: null
} }
products={maybe(() => }
result.data.search.edges })
.map(edge => edge.node) }
.filter(suggestedProduct => suggestedProduct.id) open={params.action === "removeImage"}
)} title={intl.formatMessage({
/> defaultMessage: "Delete image",
<ActionDialog description: "dialog title"
confirmButtonState={removeCollection.opts.status} })}
onClose={closeModal} variant="delete"
onConfirm={() => removeCollection.mutate({ id })} >
open={params.action === "remove"} <DialogContentText>
title={intl.formatMessage({ <FormattedMessage defaultMessage="Are you sure you want to delete collection's image?" />
defaultMessage: "Delete Collection", </DialogContentText>
description: "dialog title" </ActionDialog>
})} </>
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {collectionName}?"
values={{
collectionName: (
<strong>
{maybe(() => data.collection.name, "...")}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
confirmButtonState={unassignProduct.opts.status}
onClose={closeModal}
onConfirm={() =>
unassignProduct.mutate({
...paginationState,
collectionId: id,
productIds: params.ids
})
}
open={params.action === "unassign"}
title={intl.formatMessage({
defaultMessage: "Unassign products from collection",
description: "dialog title"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to unassign this product?} other{Are you sure you want to unassign {displayQuantity} products?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
confirmButtonState={updateCollection.opts.status}
onClose={closeModal}
onConfirm={() =>
updateCollection.mutate({
id,
input: {
backgroundImage: null
}
})
}
open={params.action === "removeImage"}
title={intl.formatMessage({
defaultMessage: "Delete image",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage defaultMessage="Are you sure you want to delete collection's image?" />
</DialogContentText>
</ActionDialog>
</>
);
}}
</CollectionOperations>
); );
}} }}
</TypedCollectionDetailsQuery> </TypedCollectionDetailsQuery>

View file

@ -27,12 +27,10 @@ import { FormattedMessage, useIntl } from "react-intl";
import CollectionListPage from "../../components/CollectionListPage/CollectionListPage"; import CollectionListPage from "../../components/CollectionListPage/CollectionListPage";
import { import {
TypedCollectionBulkDelete, useCollectionBulkDelete,
TypedCollectionBulkPublish useCollectionBulkPublish
} from "../../mutations"; } from "../../mutations";
import { useCollectionListQuery } from "../../queries"; import { useCollectionListQuery } from "../../queries";
import { CollectionBulkDelete } from "../../types/CollectionBulkDelete";
import { CollectionBulkPublish } from "../../types/CollectionBulkPublish";
import { import {
collectionAddUrl, collectionAddUrl,
collectionListUrl, collectionListUrl,
@ -83,6 +81,40 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
variables: queryVariables variables: queryVariables
}); });
const [
collectionBulkDelete,
collectionBulkDeleteOpts
] = useCollectionBulkDelete({
onCompleted: data => {
if (data.collectionBulkDelete.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
refetch();
reset();
closeModal();
}
}
});
const [
collectionBulkPublish,
collectionBulkPublishOpts
] = useCollectionBulkPublish({
onCompleted: data => {
if (data.collectionBulkPublish.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
refetch();
reset();
closeModal();
}
}
});
const tabs = getFilterTabs(); const tabs = getFilterTabs();
const currentTab = const currentTab =
@ -136,225 +168,178 @@ export const CollectionList: React.FC<CollectionListProps> = ({ params }) => {
params params
); );
const handleCollectionBulkDelete = (data: CollectionBulkDelete) => {
if (data.collectionBulkDelete.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
refetch();
reset();
closeModal();
}
};
const handleCollectionBulkPublish = (data: CollectionBulkPublish) => {
if (data.collectionBulkPublish.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
refetch();
reset();
closeModal();
}
};
const handleSort = createSortHandler(navigate, collectionListUrl, params); const handleSort = createSortHandler(navigate, collectionListUrl, params);
const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); const currencySymbol = maybe(() => shop.defaultCurrency, "USD");
return ( return (
<TypedCollectionBulkDelete onCompleted={handleCollectionBulkDelete}> <>
{(collectionBulkDelete, collectionBulkDeleteOpts) => ( <CollectionListPage
<TypedCollectionBulkPublish onCompleted={handleCollectionBulkPublish}> currencySymbol={currencySymbol}
{(collectionBulkPublish, collectionBulkPublishOpts) => ( currentTab={currentTab}
<> filterOpts={getFilterOpts(params)}
<CollectionListPage initialSearch={params.query || ""}
currencySymbol={currencySymbol} onSearchChange={handleSearchChange}
currentTab={currentTab} onFilterChange={changeFilters}
filterOpts={getFilterOpts(params)} onAdd={() => navigate(collectionAddUrl)}
initialSearch={params.query || ""} onAll={resetFilters}
onSearchChange={handleSearchChange} onTabChange={handleTabChange}
onFilterChange={changeFilters} onTabDelete={() => openModal("delete-search")}
onAdd={() => navigate(collectionAddUrl)} onTabSave={() => openModal("save-search")}
onAll={resetFilters} tabs={tabs.map(tab => tab.name)}
onTabChange={handleTabChange} disabled={loading}
onTabDelete={() => openModal("delete-search")} collections={maybe(() => data.collections.edges.map(edge => edge.node))}
onTabSave={() => openModal("save-search")} settings={settings}
tabs={tabs.map(tab => tab.name)} onNextPage={loadNextPage}
disabled={loading} onPreviousPage={loadPreviousPage}
collections={maybe(() => onSort={handleSort}
data.collections.edges.map(edge => edge.node) onUpdateListSettings={updateListSettings}
)} pageInfo={pageInfo}
settings={settings} sort={getSortParams(params)}
onNextPage={loadNextPage} onRowClick={id => () => navigate(collectionUrl(id))}
onPreviousPage={loadPreviousPage} toolbar={
onSort={handleSort} <>
onUpdateListSettings={updateListSettings} <Button
pageInfo={pageInfo} color="primary"
sort={getSortParams(params)} onClick={() =>
onRowClick={id => () => navigate(collectionUrl(id))} openModal("unpublish", {
toolbar={ ids: listElements
<> })
<Button }
color="primary" >
onClick={() => <FormattedMessage
openModal("unpublish", { defaultMessage="Unpublish"
ids: listElements description="unpublish collections"
})
}
>
<FormattedMessage
defaultMessage="Unpublish"
description="unpublish collections"
/>
</Button>
<Button
color="primary"
onClick={() =>
openModal("publish", {
ids: listElements
})
}
>
<FormattedMessage
defaultMessage="Publish"
description="publish collections"
/>
</Button>
<IconButton
color="primary"
onClick={() =>
openModal("remove", {
ids: listElements
})
}
>
<DeleteIcon />
</IconButton>
</>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
/> />
<ActionDialog </Button>
open={ <Button
params.action === "publish" && color="primary"
maybe(() => params.ids.length > 0) onClick={() =>
} openModal("publish", {
onClose={closeModal} ids: listElements
confirmButtonState={collectionBulkPublishOpts.status} })
onConfirm={() => }
collectionBulkPublish({ >
variables: { <FormattedMessage
ids: params.ids, defaultMessage="Publish"
isPublished: true description="publish collections"
}
})
}
variant="default"
title={intl.formatMessage({
defaultMessage: "Publish collections",
description: "dialog title"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to publish this collection?} other{Are you sure you want to publish {displayQuantity} collections?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={
params.action === "unpublish" &&
maybe(() => params.ids.length > 0)
}
onClose={closeModal}
confirmButtonState={collectionBulkPublishOpts.status}
onConfirm={() =>
collectionBulkPublish({
variables: {
ids: params.ids,
isPublished: false
}
})
}
variant="default"
title={intl.formatMessage({
defaultMessage: "Unpublish collections",
description: "dialog title"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to unpublish this collection?} other{Are you sure you want to unpublish {displayQuantity} collections?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={
params.action === "remove" &&
maybe(() => params.ids.length > 0)
}
onClose={closeModal}
confirmButtonState={collectionBulkDeleteOpts.status}
onConfirm={() =>
collectionBulkDelete({
variables: {
ids: params.ids
}
})
}
variant="delete"
title={intl.formatMessage({
defaultMessage: "Delete collections",
description: "dialog title"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to delete this collection?} other{Are you sure you want to delete {displayQuantity} collections?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabSave}
/> />
<DeleteFilterTabDialog </Button>
open={params.action === "delete-search"} <IconButton
confirmButtonState="default" color="primary"
onClose={closeModal} onClick={() =>
onSubmit={handleTabDelete} openModal("remove", {
tabName={maybe(() => tabs[currentTab - 1].name, "...")} ids: listElements
/> })
</> }
)} >
</TypedCollectionBulkPublish> <DeleteIcon />
)} </IconButton>
</TypedCollectionBulkDelete> </>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
/>
<ActionDialog
open={params.action === "publish" && maybe(() => params.ids.length > 0)}
onClose={closeModal}
confirmButtonState={collectionBulkPublishOpts.status}
onConfirm={() =>
collectionBulkPublish({
variables: {
ids: params.ids,
isPublished: true
}
})
}
variant="default"
title={intl.formatMessage({
defaultMessage: "Publish collections",
description: "dialog title"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to publish this collection?} other{Are you sure you want to publish {displayQuantity} collections?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={
params.action === "unpublish" && maybe(() => params.ids.length > 0)
}
onClose={closeModal}
confirmButtonState={collectionBulkPublishOpts.status}
onConfirm={() =>
collectionBulkPublish({
variables: {
ids: params.ids,
isPublished: false
}
})
}
variant="default"
title={intl.formatMessage({
defaultMessage: "Unpublish collections",
description: "dialog title"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to unpublish this collection?} other{Are you sure you want to unpublish {displayQuantity} collections?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "remove" && maybe(() => params.ids.length > 0)}
onClose={closeModal}
confirmButtonState={collectionBulkDeleteOpts.status}
onConfirm={() =>
collectionBulkDelete({
variables: {
ids: params.ids
}
})
}
variant="delete"
title={intl.formatMessage({
defaultMessage: "Delete collections",
description: "dialog title"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to delete this collection?} other{Are you sure you want to delete {displayQuantity} collections?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
/>
</>
); );
}; };
export default CollectionList; export default CollectionList;

View file

@ -0,0 +1,19 @@
import useForm from "@saleor/hooks/useForm";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import { props } from "./fixtures";
import Metadata from "./Metadata";
const InteractiveStory: React.FC = () => {
const { change, data } = useForm(props.data, () => undefined);
return <Metadata data={data} onChange={change} />;
};
storiesOf("Generics / Metadata", module)
.addDecorator(Decorator)
.add("default", () => <Metadata {...props} />)
.add("loading", () => <Metadata {...props} data={undefined} />)
.add("interactive", () => <InteractiveStory />);

View file

@ -0,0 +1,162 @@
import useForm from "@saleor/hooks/useForm";
import Wrapper from "@test/wrapper";
import { configure } from "enzyme";
import { mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import React from "react";
import { props } from "./fixtures";
import Metadata from "./Metadata";
configure({ adapter: new Adapter() });
const expandButton = 'data-test="expand"';
const Component: React.FC = () => {
const { change, data } = useForm(props.data, jest.fn());
return (
<Wrapper>
<Metadata data={data} onChange={change} />
</Wrapper>
);
};
describe("Metadata editor", () => {
it("can expand field", () => {
const wrapper = mount(<Component />);
const expandDataEl = "data-test-expanded";
expect(
wrapper
.find(`[${expandDataEl}]`)
.first()
.prop(expandDataEl)
).toEqual(false);
wrapper
.find(`[${expandButton}]`)
.first()
.simulate("click");
expect(
wrapper
.find(`[${expandDataEl}]`)
.first()
.prop(expandDataEl)
).toEqual(true);
});
it("can edit field name", () => {
const wrapper = mount(<Component />);
const inputNameSelector = '[name="name:1"] input';
// Expand to reveal fields
wrapper
.find(`[${expandButton}]`)
.first()
.simulate("click");
expect(
wrapper
.find(inputNameSelector)
.first()
.prop("value")
).toEqual(props.data.metadata[1].key);
wrapper
.find(inputNameSelector)
.first()
.simulate("change", { target: { name: "name:1", value: "x" } });
expect(
wrapper
.find(inputNameSelector)
.first()
.prop("value")
).toEqual("x");
});
it("can edit field value", () => {
const wrapper = mount(<Component />);
const inputNameSelector = '[name="value:1"] textarea';
// Expand to reveal fields
wrapper
.find(`[${expandButton}]`)
.first()
.simulate("click");
expect(
wrapper
.find(inputNameSelector)
.first()
.prop("value")
).toEqual(props.data.metadata[1].value);
wrapper
.find(inputNameSelector)
.first()
.simulate("change", { target: { name: "value:1", value: "x" } });
expect(
wrapper
.find(inputNameSelector)
.first()
.prop("value")
).toEqual("x");
});
it("can delete field", () => {
const wrapper = mount(<Component />);
const fieldSelector = 'tr[data-test="field"]';
const deleteButtonSelector = '[data-test="deleteField"]';
// Expand to reveal fields
wrapper
.find(`[${expandButton}]`)
.first()
.simulate("click");
expect(wrapper.find(fieldSelector).length).toEqual(
props.data.metadata.length
);
wrapper
.find(deleteButtonSelector)
.first()
.simulate("click");
expect(wrapper.find(fieldSelector).length).toEqual(
props.data.metadata.length - 1
);
});
it("can add field", () => {
const wrapper = mount(<Component />);
const fieldSelector = 'tr[data-test="field"]';
const addButtonSelector = '[data-test="addField"]';
// Expand to reveal fields
wrapper
.find(`[${expandButton}]`)
.first()
.simulate("click");
expect(wrapper.find(fieldSelector).length).toEqual(
props.data.metadata.length
);
wrapper
.find(addButtonSelector)
.first()
.simulate("click");
expect(wrapper.find(fieldSelector).length).toEqual(
props.data.metadata.length + 1
);
});
});

View file

@ -0,0 +1,73 @@
import { ChangeEvent } from "@saleor/hooks/useForm";
import { MetadataInput } from "@saleor/types/globalTypes";
import { removeAtIndex, updateAtIndex } from "@saleor/utils/lists";
import React from "react";
import CardSpacer from "../CardSpacer";
import MetadataCard, { MetadataCardProps } from "./MetadataCard";
import { EventDataAction, EventDataField } from "./types";
import { getDataKey, parseEventData } from "./utils";
export interface MetadataProps
extends Omit<MetadataCardProps, "data" | "isPrivate"> {
data: Record<"metadata" | "privateMetadata", MetadataInput[]>;
}
const Metadata: React.FC<MetadataProps> = ({ data, onChange }) => {
const change = (event: ChangeEvent, isPrivate: boolean) => {
const { action, field, fieldIndex, value } = parseEventData(event);
const key = getDataKey(isPrivate);
const dataToUpdate = data[key];
onChange({
target: {
name: key,
value:
action === EventDataAction.update
? updateAtIndex(
{
...dataToUpdate[fieldIndex],
key:
field === EventDataField.name
? value
: dataToUpdate[fieldIndex].key,
value:
field === EventDataField.value
? value
: dataToUpdate[fieldIndex].value
},
dataToUpdate,
fieldIndex
)
: action === EventDataAction.add
? [
...dataToUpdate,
{
key: "",
value: ""
}
]
: removeAtIndex(dataToUpdate, fieldIndex)
}
});
};
return (
<>
<MetadataCard
data={data?.metadata}
isPrivate={false}
onChange={event => change(event, false)}
/>
<CardSpacer />
<MetadataCard
data={data?.privateMetadata}
isPrivate={true}
onChange={event => change(event, true)}
/>
</>
);
};
Metadata.displayName = "Metadata";
export default Metadata;

View file

@ -0,0 +1,232 @@
import emptyMetadata from "@assets/images/empty-metadata.svg";
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardActions from "@material-ui/core/CardActions";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import ToggleIcon from "@material-ui/icons/ArrowDropDown";
import DeleteIcon from "@material-ui/icons/Delete";
import { FormChange } from "@saleor/hooks/useForm";
import { MetadataInput } from "@saleor/types/globalTypes";
import React, { useEffect } from "react";
import SVG from "react-inlinesvg";
import { useIntl } from "react-intl";
import { FormattedMessage } from "react-intl";
import CardTitle from "../CardTitle";
import Skeleton from "../Skeleton";
import useStyles from "./styles";
import { EventDataAction, EventDataField } from "./types";
export interface MetadataCardProps {
data: MetadataInput[];
isPrivate: boolean;
onChange: FormChange;
}
export const nameSeparator = ":";
export const nameInputPrefix = EventDataField.name;
export const valueInputPrefix = EventDataField.value;
const MetadataCard: React.FC<MetadataCardProps> = ({
data,
isPrivate,
onChange
}) => {
const intl = useIntl();
const loaded = React.useRef(false);
const [expanded, setExpanded] = React.useState(true);
const classes = useStyles({});
useEffect(() => {
if (data !== undefined) {
loaded.current = true;
if (data.length > 0) {
setExpanded(false);
}
}
}, [data === undefined]);
return (
<Card
data-test="metadataEditor"
data-test-is-private={isPrivate}
data-test-expanded={expanded}
>
<CardTitle
title={
isPrivate
? intl.formatMessage({
defaultMessage: "Private Metadata",
description: "header"
})
: intl.formatMessage({
defaultMessage: "Metadata",
description: "header"
})
}
/>
{data === undefined ? (
<CardContent>
<Skeleton />
</CardContent>
) : (
<>
<CardContent className={classes.content}>
{data.length > 0 && (
<div className={classes.togglable}>
<Typography color="textSecondary" variant="body2">
<FormattedMessage
defaultMessage="{number,plural,one{{number} Field} other{{number} Fields}}"
description="number of metadata fields in model"
values={{
number: data.length
}}
/>
</Typography>
<IconButton
data-test="expand"
onClick={() => setExpanded(!expanded)}
>
<ToggleIcon />
</IconButton>
</div>
)}
</CardContent>
{expanded && (
<>
{data.length === 0 ? (
<div className={classes.emptyContainer}>
<SVG className={classes.emptyImage} src={emptyMetadata} />
<Typography color="textSecondary">
{isPrivate ? (
<FormattedMessage
defaultMessage="There is no private metadata created for this element."
description="empty metadata text"
/>
) : (
<FormattedMessage
defaultMessage="There is no metadata created for this element."
description="empty metadata text"
/>
)}
</Typography>
<Typography color="textSecondary">
<FormattedMessage
defaultMessage="Use the button below to add new metadata field"
description="empty metadata text"
/>
</Typography>
</div>
) : (
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell className={classes.colNameHeader}>
<FormattedMessage
defaultMessage="Field"
description="metadata field name, header"
/>
</TableCell>
<TableCell className={classes.colValue}>
<FormattedMessage
defaultMessage="Value"
description="metadata field value, header"
/>
</TableCell>
<TableCell className={classes.colActionHeader}>
<FormattedMessage
defaultMessage="Actions"
description="table action"
/>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.map((field, fieldIndex) => (
<TableRow data-test="field" key={fieldIndex}>
<TableCell className={classes.colName}>
<TextField
InputProps={{
classes: {
input: classes.nameInput
}
}}
name={`${nameInputPrefix}${nameSeparator}${fieldIndex}`}
fullWidth
onChange={onChange}
value={field.key}
/>
</TableCell>
<TableCell className={classes.colValue}>
<TextField
InputProps={{
classes: {
root: classes.input
}
}}
multiline
name={`${valueInputPrefix}${nameSeparator}${fieldIndex}`}
fullWidth
onChange={onChange}
value={field.value}
/>
</TableCell>
<TableCell className={classes.colAction}>
<IconButton
color="primary"
data-test="deleteField"
data-test-id={fieldIndex}
onClick={() =>
onChange({
target: {
name: EventDataAction.delete,
value: fieldIndex
}
})
}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
<CardActions>
<Button
color="primary"
data-test="addField"
onClick={() =>
onChange({
target: {
name: EventDataAction.add,
value: null
}
})
}
>
<FormattedMessage
defaultMessage="Add Field"
description="add metadata field,button"
/>
</Button>
</CardActions>
</>
)}
</>
)}
</Card>
);
};
MetadataCard.displayName = "MetadataCard";
export default MetadataCard;

View file

@ -0,0 +1,22 @@
import { MetadataProps } from "./Metadata";
export const props: MetadataProps = {
data: {
metadata: [
{
key: "key",
value: "value"
},
{
key: "key2",
value: '{\n "jsonValue": "some-value"\n}'
},
{
key: "key3",
value: "some-value"
}
],
privateMetadata: []
},
onChange: () => undefined
};

View file

@ -0,0 +1,5 @@
export * from "./Metadata";
export * from "./MetadataCard";
export * from "./types";
export { default } from "./Metadata";
export { default as MetadataCard } from "./MetadataCard";

View file

@ -0,0 +1,66 @@
import makeStyles from "@material-ui/core/styles/makeStyles";
const useStyles = makeStyles(
theme => {
const colAction: React.CSSProperties = {
textAlign: "right",
width: 130
};
const colName: React.CSSProperties = {
width: 220
};
return {
colAction: {
"&:last-child": {
...colAction,
paddingRight: theme.spacing()
}
},
colActionHeader: {
...colAction
},
colName: {
...colName,
verticalAlign: "top"
},
colNameHeader: {
...colName
},
colValue: {},
content: {
paddingBottom: 0,
paddingTop: theme.spacing()
},
emptyContainer: {
borderBottom: `1px solid ${theme.palette.divider}`,
paddingBottom: theme.spacing(4),
paddingTop: theme.spacing(3),
textAlign: "center"
},
emptyImage: {
display: "block",
marginBottom: theme.spacing(2)
},
input: {
padding: theme.spacing(0.5, 2)
},
nameInput: {
padding: `13px 16px`
},
table: {
tableLayout: "fixed"
},
togglable: {
alignItems: "center",
display: "flex",
justifyContent: "space-between"
}
};
},
{
name: "Metadata"
}
);
export default useStyles;

View file

@ -0,0 +1,21 @@
import { MetadataInput } from "@saleor/types/globalTypes";
export enum EventDataAction {
add = "add",
delete = "delete",
update = "update"
}
export enum EventDataField {
name = "name",
value = "value"
}
export interface EventData {
action: EventDataAction;
field: EventDataField | null;
fieldIndex: number | null;
value: string;
}
export interface MetadataFormData {
metadata: MetadataInput[];
privateMetadata: MetadataInput[];
}

View file

@ -0,0 +1,42 @@
import { ChangeEvent } from "@saleor/hooks/useForm";
import { nameSeparator } from "./MetadataCard";
import { EventData, EventDataAction, EventDataField } from "./types";
export function parseEventData(event: ChangeEvent): EventData {
let action: EventDataAction;
let field: EventDataField = null;
let fieldIndex: number = null;
let value: string = null;
if (event.target.name.includes(EventDataField.name)) {
action = EventDataAction.update;
field = EventDataField.name;
fieldIndex = parseInt(event.target.name.split(nameSeparator)[1], 0);
value = event.target.value;
}
if (event.target.name.includes(EventDataField.value)) {
action = EventDataAction.update;
field = EventDataField.value;
fieldIndex = parseInt(event.target.name.split(nameSeparator)[1], 0);
value = event.target.value;
}
if (event.target.name === EventDataAction.add) {
action = EventDataAction.add;
}
if (event.target.name === EventDataAction.delete) {
action = EventDataAction.delete;
fieldIndex = event.target.value;
}
return {
action,
field,
fieldIndex,
value
};
}
export function getDataKey(isPrivate: boolean) {
return isPrivate ? "privateMetadata" : "metadata";
}

View file

@ -1,5 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import { metadataFragment } from "./metadata";
export const attributeFragment = gql` export const attributeFragment = gql`
fragment AttributeFragment on Attribute { fragment AttributeFragment on Attribute {
id id
@ -13,8 +15,10 @@ export const attributeFragment = gql`
export const attributeDetailsFragment = gql` export const attributeDetailsFragment = gql`
${attributeFragment} ${attributeFragment}
${metadataFragment}
fragment AttributeDetailsFragment on Attribute { fragment AttributeDetailsFragment on Attribute {
...AttributeFragment ...AttributeFragment
...MetadataFragment
availableInGrid availableInGrid
inputType inputType
storefrontSearchPosition storefrontSearchPosition

View file

@ -1,5 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import { metadataFragment } from "./metadata";
export const categoryFragment = gql` export const categoryFragment = gql`
fragment CategoryFragment on Category { fragment CategoryFragment on Category {
id id
@ -13,8 +15,10 @@ export const categoryFragment = gql`
} }
`; `;
export const categoryDetailsFragment = gql` export const categoryDetailsFragment = gql`
${metadataFragment}
fragment CategoryDetailsFragment on Category { fragment CategoryDetailsFragment on Category {
id id
...MetadataFragment
backgroundImage { backgroundImage {
alt alt
url url

View file

@ -1,5 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import { metadataFragment } from "./metadata";
export const collectionFragment = gql` export const collectionFragment = gql`
fragment CollectionFragment on Collection { fragment CollectionFragment on Collection {
id id
@ -10,8 +12,10 @@ export const collectionFragment = gql`
export const collectionDetailsFragment = gql` export const collectionDetailsFragment = gql`
${collectionFragment} ${collectionFragment}
${metadataFragment}
fragment CollectionDetailsFragment on Collection { fragment CollectionDetailsFragment on Collection {
...CollectionFragment ...CollectionFragment
...MetadataFragment
backgroundImage { backgroundImage {
alt alt
url url

View file

@ -134,3 +134,10 @@ export const pluginErrorFragment = gql`
field field
} }
`; `;
export const metadataErrorFragment = gql`
fragment MetadataErrorFragment on MetadataError {
code
field
}
`;

View file

@ -5,7 +5,7 @@ export const metadataFragment = gql`
key key
value value
} }
fragment Metadata on ObjectWithMetadata { fragment MetadataFragment on ObjectWithMetadata {
metadata { metadata {
...MetadataItem ...MetadataItem
} }

View file

@ -1,6 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import { attributeFragment } from "./attributes"; import { attributeFragment } from "./attributes";
import { metadataFragment } from "./metadata";
export const productTypeFragment = gql` export const productTypeFragment = gql`
fragment ProductTypeFragment on ProductType { fragment ProductTypeFragment on ProductType {
@ -18,8 +19,10 @@ export const productTypeFragment = gql`
export const productTypeDetailsFragment = gql` export const productTypeDetailsFragment = gql`
${attributeFragment} ${attributeFragment}
${productTypeFragment} ${productTypeFragment}
${metadataFragment}
fragment ProductTypeDetailsFragment on ProductType { fragment ProductTypeDetailsFragment on ProductType {
...ProductTypeFragment ...ProductTypeFragment
...MetadataFragment
productAttributes { productAttributes {
...AttributeFragment ...AttributeFragment
} }

View file

@ -1,5 +1,6 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import { metadataFragment } from "./metadata";
import { weightFragment } from "./weight"; import { weightFragment } from "./weight";
export const stockFragment = gql` export const stockFragment = gql`
@ -105,8 +106,10 @@ export const productFragmentDetails = gql`
${productVariantAttributesFragment} ${productVariantAttributesFragment}
${stockFragment} ${stockFragment}
${weightFragment} ${weightFragment}
${metadataFragment}
fragment Product on Product { fragment Product on Product {
...ProductVariantAttributesFragment ...ProductVariantAttributesFragment
...MetadataFragment
name name
descriptionJson descriptionJson
seoTitle seoTitle
@ -181,8 +184,10 @@ export const fragmentVariant = gql`
${fragmentProductImage} ${fragmentProductImage}
${stockFragment} ${stockFragment}
${weightFragment} ${weightFragment}
${metadataFragment}
fragment ProductVariant on ProductVariant { fragment ProductVariant on ProductVariant {
id id
...MetadataFragment
attributes { attributes {
attribute { attribute {
id id

View file

@ -8,6 +8,18 @@ import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/global
// GraphQL fragment: AttributeDetailsFragment // GraphQL fragment: AttributeDetailsFragment
// ==================================================== // ====================================================
export interface AttributeDetailsFragment_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeDetailsFragment_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AttributeDetailsFragment_values { export interface AttributeDetailsFragment_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -24,6 +36,8 @@ export interface AttributeDetailsFragment {
visibleInStorefront: boolean; visibleInStorefront: boolean;
filterableInDashboard: boolean; filterableInDashboard: boolean;
filterableInStorefront: boolean; filterableInStorefront: boolean;
metadata: (AttributeDetailsFragment_metadata | null)[];
privateMetadata: (AttributeDetailsFragment_privateMetadata | null)[];
availableInGrid: boolean; availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null; inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number; storefrontSearchPosition: number;

View file

@ -6,6 +6,18 @@
// GraphQL fragment: CategoryDetailsFragment // GraphQL fragment: CategoryDetailsFragment
// ==================================================== // ====================================================
export interface CategoryDetailsFragment_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CategoryDetailsFragment_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CategoryDetailsFragment_backgroundImage { export interface CategoryDetailsFragment_backgroundImage {
__typename: "Image"; __typename: "Image";
alt: string | null; alt: string | null;
@ -20,6 +32,8 @@ export interface CategoryDetailsFragment_parent {
export interface CategoryDetailsFragment { export interface CategoryDetailsFragment {
__typename: "Category"; __typename: "Category";
id: string; id: string;
metadata: (CategoryDetailsFragment_metadata | null)[];
privateMetadata: (CategoryDetailsFragment_privateMetadata | null)[];
backgroundImage: CategoryDetailsFragment_backgroundImage | null; backgroundImage: CategoryDetailsFragment_backgroundImage | null;
name: string; name: string;
descriptionJson: any; descriptionJson: any;

View file

@ -6,6 +6,18 @@
// GraphQL fragment: CollectionDetailsFragment // GraphQL fragment: CollectionDetailsFragment
// ==================================================== // ====================================================
export interface CollectionDetailsFragment_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CollectionDetailsFragment_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface CollectionDetailsFragment_backgroundImage { export interface CollectionDetailsFragment_backgroundImage {
__typename: "Image"; __typename: "Image";
alt: string | null; alt: string | null;
@ -17,6 +29,8 @@ export interface CollectionDetailsFragment {
id: string; id: string;
isPublished: boolean; isPublished: boolean;
name: string; name: string;
metadata: (CollectionDetailsFragment_metadata | null)[];
privateMetadata: (CollectionDetailsFragment_privateMetadata | null)[];
backgroundImage: CollectionDetailsFragment_backgroundImage | null; backgroundImage: CollectionDetailsFragment_backgroundImage | null;
descriptionJson: any; descriptionJson: any;
publicationDate: any | null; publicationDate: any | null;

View file

@ -0,0 +1,15 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { MetadataErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: MetadataErrorFragment
// ====================================================
export interface MetadataErrorFragment {
__typename: "MetadataError";
code: MetadataErrorCode;
field: string | null;
}

View file

@ -0,0 +1,25 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL fragment: MetadataFragment
// ====================================================
export interface MetadataFragment_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface MetadataFragment_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface MetadataFragment {
__typename: "ServiceAccount" | "App" | "Product" | "ProductType" | "Attribute" | "Category" | "ProductVariant" | "DigitalContent" | "Collection" | "User" | "Checkout" | "Order" | "Fulfillment" | "Invoice";
metadata: (MetadataFragment_metadata | null)[];
privateMetadata: (MetadataFragment_privateMetadata | null)[];
}

View file

@ -93,6 +93,18 @@ export interface Product_pricing {
priceRangeUndiscounted: Product_pricing_priceRangeUndiscounted | null; priceRangeUndiscounted: Product_pricing_priceRangeUndiscounted | null;
} }
export interface Product_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface Product_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface Product_category { export interface Product_category {
__typename: "Category"; __typename: "Category";
id: string; id: string;
@ -180,6 +192,8 @@ export interface Product {
attributes: Product_attributes[]; attributes: Product_attributes[];
productType: Product_productType; productType: Product_productType;
pricing: Product_pricing | null; pricing: Product_pricing | null;
metadata: (Product_metadata | null)[];
privateMetadata: (Product_privateMetadata | null)[];
name: string; name: string;
descriptionJson: any; descriptionJson: any;
seoTitle: string | null; seoTitle: string | null;

View file

@ -14,6 +14,18 @@ export interface ProductTypeDetailsFragment_taxType {
taxCode: string | null; taxCode: string | null;
} }
export interface ProductTypeDetailsFragment_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductTypeDetailsFragment_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductTypeDetailsFragment_productAttributes { export interface ProductTypeDetailsFragment_productAttributes {
__typename: "Attribute"; __typename: "Attribute";
id: string; id: string;
@ -47,6 +59,8 @@ export interface ProductTypeDetailsFragment {
hasVariants: boolean; hasVariants: boolean;
isShippingRequired: boolean; isShippingRequired: boolean;
taxType: ProductTypeDetailsFragment_taxType | null; taxType: ProductTypeDetailsFragment_taxType | null;
metadata: (ProductTypeDetailsFragment_metadata | null)[];
privateMetadata: (ProductTypeDetailsFragment_privateMetadata | null)[];
productAttributes: (ProductTypeDetailsFragment_productAttributes | null)[] | null; productAttributes: (ProductTypeDetailsFragment_productAttributes | null)[] | null;
variantAttributes: (ProductTypeDetailsFragment_variantAttributes | null)[] | null; variantAttributes: (ProductTypeDetailsFragment_variantAttributes | null)[] | null;
weight: ProductTypeDetailsFragment_weight | null; weight: ProductTypeDetailsFragment_weight | null;

View file

@ -8,6 +8,18 @@ import { WeightUnitsEnum } from "./../../types/globalTypes";
// GraphQL fragment: ProductVariant // GraphQL fragment: ProductVariant
// ==================================================== // ====================================================
export interface ProductVariant_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductVariant_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductVariant_attributes_attribute_values { export interface ProductVariant_attributes_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -114,6 +126,8 @@ export interface ProductVariant_weight {
export interface ProductVariant { export interface ProductVariant {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
metadata: (ProductVariant_metadata | null)[];
privateMetadata: (ProductVariant_privateMetadata | null)[];
attributes: ProductVariant_attributes[]; attributes: ProductVariant_attributes[];
costPrice: ProductVariant_costPrice | null; costPrice: ProductVariant_costPrice | null;
images: (ProductVariant_images | null)[] | null; images: (ProductVariant_images | null)[] | null;

View file

@ -5,6 +5,8 @@ import Container from "@saleor/components/Container";
import ControlledSwitch from "@saleor/components/ControlledSwitch"; import ControlledSwitch from "@saleor/components/ControlledSwitch";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Metadata from "@saleor/components/Metadata/Metadata";
import { MetadataFormData } from "@saleor/components/Metadata/types";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ChangeEvent, FormChange } from "@saleor/hooks/useForm"; import { ChangeEvent, FormChange } from "@saleor/hooks/useForm";
@ -13,6 +15,8 @@ import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { ListActions, ReorderEvent, UserError } from "@saleor/types"; import { ListActions, ReorderEvent, UserError } from "@saleor/types";
import { AttributeTypeEnum, WeightUnitsEnum } from "@saleor/types/globalTypes"; import { AttributeTypeEnum, WeightUnitsEnum } from "@saleor/types/globalTypes";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -30,7 +34,7 @@ interface ChoiceType {
value: string; value: string;
} }
export interface ProductTypeForm { export interface ProductTypeForm extends MetadataFormData {
name: string; name: string;
hasVariants: boolean; hasVariants: boolean;
isShippingRequired: boolean; isShippingRequired: boolean;
@ -92,6 +96,12 @@ const ProductTypeDetailsPage: React.FC<ProductTypeDetailsPageProps> = ({
onSubmit onSubmit
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const [taxTypeDisplayName, setTaxTypeDisplayName] = useStateFromProps( const [taxTypeDisplayName, setTaxTypeDisplayName] = useStateFromProps(
maybe(() => productType.taxType.description, "") maybe(() => productType.taxType.description, "")
); );
@ -104,7 +114,9 @@ const ProductTypeDetailsPage: React.FC<ProductTypeDetailsPageProps> = ({
maybe(() => productType.isShippingRequired) !== undefined maybe(() => productType.isShippingRequired) !== undefined
? productType.isShippingRequired ? productType.isShippingRequired
: false, : false,
metadata: productType?.metadata?.map(mapMetadataItemToInput),
name: maybe(() => productType.name) !== undefined ? productType.name : "", name: maybe(() => productType.name) !== undefined ? productType.name : "",
privateMetadata: productType?.privateMetadata?.map(mapMetadataItemToInput),
productAttributes: productAttributes:
maybe(() => productType.productAttributes) !== undefined maybe(() => productType.productAttributes) !== undefined
? productType.productAttributes.map(attribute => ({ ? productType.productAttributes.map(attribute => ({
@ -122,97 +134,117 @@ const ProductTypeDetailsPage: React.FC<ProductTypeDetailsPageProps> = ({
: [], : [],
weight: maybe(() => productType.weight.value) weight: maybe(() => productType.weight.value)
}; };
const handleSubmit = (data: ProductTypeForm) => {
const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
onSubmit({
...data,
metadata,
privateMetadata
});
};
return ( return (
<Form initial={formInitialData} onSubmit={onSubmit} confirmLeave> <Form initial={formInitialData} onSubmit={handleSubmit} confirmLeave>
{({ change, data, hasChanged, submit }) => ( {({ change, data, hasChanged, submit }) => {
<Container> const changeMetadata = makeMetadataChangeHandler(change);
<AppHeader onBack={onBack}>
{intl.formatMessage(sectionNames.productTypes)} return (
</AppHeader> <Container>
<PageHeader title={pageTitle} /> <AppHeader onBack={onBack}>
<Grid> {intl.formatMessage(sectionNames.productTypes)}
<div> </AppHeader>
<ProductTypeDetails <PageHeader title={pageTitle} />
data={data} <Grid>
disabled={disabled} <div>
errors={errors} <ProductTypeDetails
onChange={change} data={data}
/> disabled={disabled}
<CardSpacer /> errors={errors}
<ProductTypeTaxes onChange={change}
disabled={disabled} />
data={data} <CardSpacer />
taxTypes={taxTypes} <ProductTypeTaxes
taxTypeDisplayName={taxTypeDisplayName} disabled={disabled}
onChange={event => data={data}
handleTaxTypeChange( taxTypes={taxTypes}
event, taxTypeDisplayName={taxTypeDisplayName}
taxTypes, onChange={event =>
change, handleTaxTypeChange(
setTaxTypeDisplayName event,
) taxTypes,
} change,
/> setTaxTypeDisplayName
<CardSpacer /> )
<ProductTypeAttributes }
attributes={maybe(() => productType.productAttributes)} />
disabled={disabled} <CardSpacer />
type={AttributeTypeEnum.PRODUCT} <ProductTypeAttributes
onAttributeAssign={onAttributeAdd} attributes={maybe(() => productType.productAttributes)}
onAttributeClick={onAttributeClick} disabled={disabled}
onAttributeReorder={(event: ReorderEvent) => type={AttributeTypeEnum.PRODUCT}
onAttributeReorder(event, AttributeTypeEnum.PRODUCT) onAttributeAssign={onAttributeAdd}
} onAttributeClick={onAttributeClick}
onAttributeUnassign={onAttributeUnassign} onAttributeReorder={(event: ReorderEvent) =>
{...productAttributeList} onAttributeReorder(event, AttributeTypeEnum.PRODUCT)
/> }
<CardSpacer /> onAttributeUnassign={onAttributeUnassign}
<ControlledSwitch {...productAttributeList}
checked={data.hasVariants} />
disabled={disabled} <CardSpacer />
label={intl.formatMessage({ <ControlledSwitch
defaultMessage: "Product type uses Variant Attributes", checked={data.hasVariants}
description: "switch button" disabled={disabled}
})} label={intl.formatMessage({
name="hasVariants" defaultMessage: "Product type uses Variant Attributes",
onChange={event => onHasVariantsToggle(event.target.value)} description: "switch button"
/> })}
{data.hasVariants && ( name="hasVariants"
<> onChange={event => onHasVariantsToggle(event.target.value)}
<CardSpacer /> />
<ProductTypeAttributes {data.hasVariants && (
attributes={maybe(() => productType.variantAttributes)} <>
disabled={disabled} <CardSpacer />
type={AttributeTypeEnum.VARIANT} <ProductTypeAttributes
onAttributeAssign={onAttributeAdd} attributes={maybe(() => productType.variantAttributes)}
onAttributeClick={onAttributeClick} disabled={disabled}
onAttributeReorder={(event: ReorderEvent) => type={AttributeTypeEnum.VARIANT}
onAttributeReorder(event, AttributeTypeEnum.VARIANT) onAttributeAssign={onAttributeAdd}
} onAttributeClick={onAttributeClick}
onAttributeUnassign={onAttributeUnassign} onAttributeReorder={(event: ReorderEvent) =>
{...variantAttributeList} onAttributeReorder(event, AttributeTypeEnum.VARIANT)
/> }
</> onAttributeUnassign={onAttributeUnassign}
)} {...variantAttributeList}
</div> />
<div> </>
<ProductTypeShipping )}
disabled={disabled} <CardSpacer />
data={data} <Metadata data={data} onChange={changeMetadata} />
weightUnit={productType?.weight?.unit || defaultWeightUnit} </div>
onChange={change} <div>
/> <ProductTypeShipping
</div> disabled={disabled}
</Grid> data={data}
<SaveButtonBar weightUnit={productType?.weight?.unit || defaultWeightUnit}
onCancel={onBack} onChange={change}
onDelete={onDelete} />
onSave={submit} </div>
disabled={disabled || !hasChanged} </Grid>
state={saveButtonBarState} <SaveButtonBar
/> onCancel={onBack}
</Container> onDelete={onDelete}
)} onSave={submit}
disabled={disabled || !hasChanged}
state={saveButtonBarState}
/>
</Container>
);
}}
</Form> </Form>
); );
}; };

View file

@ -12,7 +12,6 @@ import {
ProductTypeAttributeReorderMutation, ProductTypeAttributeReorderMutation,
TypedAssignAttributeMutation, TypedAssignAttributeMutation,
TypedProductTypeDeleteMutation, TypedProductTypeDeleteMutation,
TypedProductTypeUpdateMutation,
TypedUnassignAttributeMutation TypedUnassignAttributeMutation
} from "../mutations"; } from "../mutations";
import { import {
@ -27,10 +26,6 @@ import {
ProductTypeDelete, ProductTypeDelete,
ProductTypeDeleteVariables ProductTypeDeleteVariables
} from "../types/ProductTypeDelete"; } from "../types/ProductTypeDelete";
import {
ProductTypeUpdate,
ProductTypeUpdateVariables
} from "../types/ProductTypeUpdate";
import { import {
UnassignAttribute, UnassignAttribute,
UnassignAttributeVariables UnassignAttributeVariables
@ -75,17 +70,12 @@ interface ProductTypeOperationsProps {
ProductTypeAttributeReorder, ProductTypeAttributeReorder,
ProductTypeAttributeReorderVariables ProductTypeAttributeReorderVariables
>; >;
updateProductType: PartialMutationProviderOutput<
ProductTypeUpdate,
ProductTypeUpdateVariables
>;
}) => React.ReactNode; }) => React.ReactNode;
productType: ProductTypeDetailsFragment; productType: ProductTypeDetailsFragment;
onAssignAttribute: (data: AssignAttribute) => void; onAssignAttribute: (data: AssignAttribute) => void;
onUnassignAttribute: (data: UnassignAttribute) => void; onUnassignAttribute: (data: UnassignAttribute) => void;
onProductTypeAttributeReorder: (data: ProductTypeAttributeReorder) => void; onProductTypeAttributeReorder: (data: ProductTypeAttributeReorder) => void;
onProductTypeDelete: (data: ProductTypeDelete) => void; onProductTypeDelete: (data: ProductTypeDelete) => void;
onProductTypeUpdate: (data: ProductTypeUpdate) => void;
} }
const ProductTypeOperations: React.FC<ProductTypeOperationsProps> = ({ const ProductTypeOperations: React.FC<ProductTypeOperationsProps> = ({
@ -94,85 +84,72 @@ const ProductTypeOperations: React.FC<ProductTypeOperationsProps> = ({
onAssignAttribute, onAssignAttribute,
onUnassignAttribute, onUnassignAttribute,
onProductTypeAttributeReorder, onProductTypeAttributeReorder,
onProductTypeDelete, onProductTypeDelete
onProductTypeUpdate
}) => ( }) => (
<TypedProductTypeDeleteMutation onCompleted={onProductTypeDelete}> <TypedProductTypeDeleteMutation onCompleted={onProductTypeDelete}>
{(...deleteProductType) => ( {(...deleteProductType) => (
<TypedProductTypeUpdateMutation onCompleted={onProductTypeUpdate}> <TypedAssignAttributeMutation onCompleted={onAssignAttribute}>
{(...updateProductType) => ( {(...assignAttribute) => (
<TypedAssignAttributeMutation onCompleted={onAssignAttribute}> <TypedUnassignAttributeMutation onCompleted={onUnassignAttribute}>
{(...assignAttribute) => ( {(...unassignAttribute) => (
<TypedUnassignAttributeMutation onCompleted={onUnassignAttribute}> <ProductTypeAttributeReorderMutation
{(...unassignAttribute) => ( onCompleted={onProductTypeAttributeReorder}
<ProductTypeAttributeReorderMutation >
onCompleted={onProductTypeAttributeReorder} {(reorderAttributeMutation, reorderAttributeMutationResult) => {
> const reorderAttributeMutationFn: MutationFunction<
{( ProductTypeAttributeReorder,
reorderAttributeMutation, ProductTypeAttributeReorderVariables
reorderAttributeMutationResult > = opts => {
) => { const optimisticResponse: ProductTypeAttributeReorder = {
const reorderAttributeMutationFn: MutationFunction< productTypeReorderAttributes: {
ProductTypeAttributeReorder, __typename: "ProductTypeReorderAttributes" as "ProductTypeReorderAttributes",
ProductTypeAttributeReorderVariables errors: [],
> = opts => { productType: {
const optimisticResponse: ProductTypeAttributeReorder = { ...productType,
productTypeReorderAttributes: { productAttributes:
__typename: "ProductTypeReorderAttributes" as "ProductTypeReorderAttributes", opts.variables.type === AttributeTypeEnum.PRODUCT
errors: [], ? moveAttribute(
productType: { productType.productAttributes,
...productType, opts.variables.move
productAttributes: )
opts.variables.type === : productType.productAttributes,
AttributeTypeEnum.PRODUCT variantAttributes:
? moveAttribute( opts.variables.type === AttributeTypeEnum.VARIANT
productType.productAttributes, ? moveAttribute(
opts.variables.move productType.variantAttributes,
) opts.variables.move
: productType.productAttributes, )
variantAttributes: : productType.variantAttributes
opts.variables.type === }
AttributeTypeEnum.VARIANT }
? moveAttribute( };
productType.variantAttributes, return reorderAttributeMutation({
opts.variables.move ...opts,
) optimisticResponse
: productType.variantAttributes });
} };
}
};
return reorderAttributeMutation({
...opts,
optimisticResponse
});
};
return children({ return children({
assignAttribute: getMutationProviderData( assignAttribute: getMutationProviderData(
...assignAttribute ...assignAttribute
), ),
deleteProductType: getMutationProviderData( deleteProductType: getMutationProviderData(
...deleteProductType ...deleteProductType
), ),
reorderAttribute: getMutationProviderData( reorderAttribute: getMutationProviderData(
reorderAttributeMutationFn, reorderAttributeMutationFn,
reorderAttributeMutationResult reorderAttributeMutationResult
), ),
unassignAttribute: getMutationProviderData( unassignAttribute: getMutationProviderData(
...unassignAttribute ...unassignAttribute
), )
updateProductType: getMutationProviderData( });
...updateProductType }}
) </ProductTypeAttributeReorderMutation>
});
}}
</ProductTypeAttributeReorderMutation>
)}
</TypedUnassignAttributeMutation>
)} )}
</TypedAssignAttributeMutation> </TypedUnassignAttributeMutation>
)} )}
</TypedProductTypeUpdateMutation> </TypedAssignAttributeMutation>
)} )}
</TypedProductTypeDeleteMutation> </TypedProductTypeDeleteMutation>
); );

View file

@ -550,7 +550,15 @@ export const productType: ProductTypeDetails_productType = {
hasVariants: false, hasVariants: false,
id: "UHJvZHVjdFR5cGU6NQ==", id: "UHJvZHVjdFR5cGU6NQ==",
isShippingRequired: false, isShippingRequired: false,
metadata: [
{
__typename: "MetadataItem",
key: "integration.id",
value: "100023123"
}
],
name: "E-books", name: "E-books",
privateMetadata: [],
productAttributes: [ productAttributes: [
{ {
__typename: "Attribute" as "Attribute", __typename: "Attribute" as "Attribute",

View file

@ -1,4 +1,5 @@
import { productTypeDetailsFragment } from "@saleor/fragments/productTypes"; import { productTypeDetailsFragment } from "@saleor/fragments/productTypes";
import makeMutation from "@saleor/hooks/makeMutation";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { TypedMutation } from "../mutations"; import { TypedMutation } from "../mutations";
@ -78,7 +79,7 @@ export const productTypeUpdateMutation = gql`
} }
} }
`; `;
export const TypedProductTypeUpdateMutation = TypedMutation< export const useProductTypeUpdateMutation = makeMutation<
ProductTypeUpdate, ProductTypeUpdate,
ProductTypeUpdateVariables ProductTypeUpdateVariables
>(productTypeUpdateMutation); >(productTypeUpdateMutation);

View file

@ -20,6 +20,18 @@ export interface AssignAttribute_attributeAssign_productType_taxType {
taxCode: string | null; taxCode: string | null;
} }
export interface AssignAttribute_attributeAssign_productType_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AssignAttribute_attributeAssign_productType_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface AssignAttribute_attributeAssign_productType_productAttributes { export interface AssignAttribute_attributeAssign_productType_productAttributes {
__typename: "Attribute"; __typename: "Attribute";
id: string; id: string;
@ -53,6 +65,8 @@ export interface AssignAttribute_attributeAssign_productType {
hasVariants: boolean; hasVariants: boolean;
isShippingRequired: boolean; isShippingRequired: boolean;
taxType: AssignAttribute_attributeAssign_productType_taxType | null; taxType: AssignAttribute_attributeAssign_productType_taxType | null;
metadata: (AssignAttribute_attributeAssign_productType_metadata | null)[];
privateMetadata: (AssignAttribute_attributeAssign_productType_privateMetadata | null)[];
productAttributes: (AssignAttribute_attributeAssign_productType_productAttributes | null)[] | null; productAttributes: (AssignAttribute_attributeAssign_productType_productAttributes | null)[] | null;
variantAttributes: (AssignAttribute_attributeAssign_productType_variantAttributes | null)[] | null; variantAttributes: (AssignAttribute_attributeAssign_productType_variantAttributes | null)[] | null;
weight: AssignAttribute_attributeAssign_productType_weight | null; weight: AssignAttribute_attributeAssign_productType_weight | null;

View file

@ -20,6 +20,18 @@ export interface ProductTypeAttributeReorder_productTypeReorderAttributes_produc
taxCode: string | null; taxCode: string | null;
} }
export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType_productAttributes { export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType_productAttributes {
__typename: "Attribute"; __typename: "Attribute";
id: string; id: string;
@ -53,6 +65,8 @@ export interface ProductTypeAttributeReorder_productTypeReorderAttributes_produc
hasVariants: boolean; hasVariants: boolean;
isShippingRequired: boolean; isShippingRequired: boolean;
taxType: ProductTypeAttributeReorder_productTypeReorderAttributes_productType_taxType | null; taxType: ProductTypeAttributeReorder_productTypeReorderAttributes_productType_taxType | null;
metadata: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_metadata | null)[];
privateMetadata: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_privateMetadata | null)[];
productAttributes: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_productAttributes | null)[] | null; productAttributes: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_productAttributes | null)[] | null;
variantAttributes: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_variantAttributes | null)[] | null; variantAttributes: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_variantAttributes | null)[] | null;
weight: ProductTypeAttributeReorder_productTypeReorderAttributes_productType_weight | null; weight: ProductTypeAttributeReorder_productTypeReorderAttributes_productType_weight | null;

View file

@ -20,6 +20,18 @@ export interface ProductTypeCreate_productTypeCreate_productType_taxType {
taxCode: string | null; taxCode: string | null;
} }
export interface ProductTypeCreate_productTypeCreate_productType_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductTypeCreate_productTypeCreate_productType_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductTypeCreate_productTypeCreate_productType_productAttributes { export interface ProductTypeCreate_productTypeCreate_productType_productAttributes {
__typename: "Attribute"; __typename: "Attribute";
id: string; id: string;
@ -53,6 +65,8 @@ export interface ProductTypeCreate_productTypeCreate_productType {
hasVariants: boolean; hasVariants: boolean;
isShippingRequired: boolean; isShippingRequired: boolean;
taxType: ProductTypeCreate_productTypeCreate_productType_taxType | null; taxType: ProductTypeCreate_productTypeCreate_productType_taxType | null;
metadata: (ProductTypeCreate_productTypeCreate_productType_metadata | null)[];
privateMetadata: (ProductTypeCreate_productTypeCreate_productType_privateMetadata | null)[];
productAttributes: (ProductTypeCreate_productTypeCreate_productType_productAttributes | null)[] | null; productAttributes: (ProductTypeCreate_productTypeCreate_productType_productAttributes | null)[] | null;
variantAttributes: (ProductTypeCreate_productTypeCreate_productType_variantAttributes | null)[] | null; variantAttributes: (ProductTypeCreate_productTypeCreate_productType_variantAttributes | null)[] | null;
weight: ProductTypeCreate_productTypeCreate_productType_weight | null; weight: ProductTypeCreate_productTypeCreate_productType_weight | null;

View file

@ -14,6 +14,18 @@ export interface ProductTypeDetails_productType_taxType {
taxCode: string | null; taxCode: string | null;
} }
export interface ProductTypeDetails_productType_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductTypeDetails_productType_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductTypeDetails_productType_productAttributes { export interface ProductTypeDetails_productType_productAttributes {
__typename: "Attribute"; __typename: "Attribute";
id: string; id: string;
@ -47,6 +59,8 @@ export interface ProductTypeDetails_productType {
hasVariants: boolean; hasVariants: boolean;
isShippingRequired: boolean; isShippingRequired: boolean;
taxType: ProductTypeDetails_productType_taxType | null; taxType: ProductTypeDetails_productType_taxType | null;
metadata: (ProductTypeDetails_productType_metadata | null)[];
privateMetadata: (ProductTypeDetails_productType_privateMetadata | null)[];
productAttributes: (ProductTypeDetails_productType_productAttributes | null)[] | null; productAttributes: (ProductTypeDetails_productType_productAttributes | null)[] | null;
variantAttributes: (ProductTypeDetails_productType_variantAttributes | null)[] | null; variantAttributes: (ProductTypeDetails_productType_variantAttributes | null)[] | null;
weight: ProductTypeDetails_productType_weight | null; weight: ProductTypeDetails_productType_weight | null;

View file

@ -20,6 +20,18 @@ export interface ProductTypeUpdate_productTypeUpdate_productType_taxType {
taxCode: string | null; taxCode: string | null;
} }
export interface ProductTypeUpdate_productTypeUpdate_productType_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductTypeUpdate_productTypeUpdate_productType_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductTypeUpdate_productTypeUpdate_productType_productAttributes { export interface ProductTypeUpdate_productTypeUpdate_productType_productAttributes {
__typename: "Attribute"; __typename: "Attribute";
id: string; id: string;
@ -53,6 +65,8 @@ export interface ProductTypeUpdate_productTypeUpdate_productType {
hasVariants: boolean; hasVariants: boolean;
isShippingRequired: boolean; isShippingRequired: boolean;
taxType: ProductTypeUpdate_productTypeUpdate_productType_taxType | null; taxType: ProductTypeUpdate_productTypeUpdate_productType_taxType | null;
metadata: (ProductTypeUpdate_productTypeUpdate_productType_metadata | null)[];
privateMetadata: (ProductTypeUpdate_productTypeUpdate_productType_privateMetadata | null)[];
productAttributes: (ProductTypeUpdate_productTypeUpdate_productType_productAttributes | null)[] | null; productAttributes: (ProductTypeUpdate_productTypeUpdate_productType_productAttributes | null)[] | null;
variantAttributes: (ProductTypeUpdate_productTypeUpdate_productType_variantAttributes | null)[] | null; variantAttributes: (ProductTypeUpdate_productTypeUpdate_productType_variantAttributes | null)[] | null;
weight: ProductTypeUpdate_productTypeUpdate_productType_weight | null; weight: ProductTypeUpdate_productTypeUpdate_productType_weight | null;

View file

@ -20,6 +20,18 @@ export interface UnassignAttribute_attributeUnassign_productType_taxType {
taxCode: string | null; taxCode: string | null;
} }
export interface UnassignAttribute_attributeUnassign_productType_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface UnassignAttribute_attributeUnassign_productType_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface UnassignAttribute_attributeUnassign_productType_productAttributes { export interface UnassignAttribute_attributeUnassign_productType_productAttributes {
__typename: "Attribute"; __typename: "Attribute";
id: string; id: string;
@ -53,6 +65,8 @@ export interface UnassignAttribute_attributeUnassign_productType {
hasVariants: boolean; hasVariants: boolean;
isShippingRequired: boolean; isShippingRequired: boolean;
taxType: UnassignAttribute_attributeUnassign_productType_taxType | null; taxType: UnassignAttribute_attributeUnassign_productType_taxType | null;
metadata: (UnassignAttribute_attributeUnassign_productType_metadata | null)[];
privateMetadata: (UnassignAttribute_attributeUnassign_productType_privateMetadata | null)[];
productAttributes: (UnassignAttribute_attributeUnassign_productType_productAttributes | null)[] | null; productAttributes: (UnassignAttribute_attributeUnassign_productType_productAttributes | null)[] | null;
variantAttributes: (UnassignAttribute_attributeUnassign_productType_variantAttributes | null)[] | null; variantAttributes: (UnassignAttribute_attributeUnassign_productType_variantAttributes | null)[] | null;
weight: UnassignAttribute_attributeUnassign_productType_weight | null; weight: UnassignAttribute_attributeUnassign_productType_weight | null;

View file

@ -1,44 +0,0 @@
import React from "react";
import { UserError } from "../../../types";
interface ProductTypeUpdateErrorsState {
addAttributeErrors: UserError[];
editAttributeErrors: UserError[];
formErrors: UserError[];
}
interface ProductTypeUpdateErrorsProps {
children: (props: {
errors: ProductTypeUpdateErrorsState;
set: {
addAttributeErrors: (errors: UserError[]) => void;
editAttributeErrors: (errors: UserError[]) => void;
formErrors: (errors: UserError[]) => void;
};
}) => React.ReactNode;
}
export class ProductTypeUpdateErrors extends React.Component<
ProductTypeUpdateErrorsProps,
ProductTypeUpdateErrorsState
> {
state: ProductTypeUpdateErrorsState = {
addAttributeErrors: [],
editAttributeErrors: [],
formErrors: []
};
render() {
return this.props.children({
errors: this.state,
set: {
addAttributeErrors: (addAttributeErrors: UserError[]) =>
this.setState({ addAttributeErrors }),
editAttributeErrors: (editAttributeErrors: UserError[]) =>
this.setState({ editAttributeErrors }),
formErrors: (formErrors: UserError[]) => this.setState({ formErrors })
}
});
}
}
export default ProductTypeUpdateErrors;

View file

@ -9,8 +9,14 @@ import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import AssignAttributeDialog from "@saleor/productTypes/components/AssignAttributeDialog"; import AssignAttributeDialog from "@saleor/productTypes/components/AssignAttributeDialog";
import { useProductTypeUpdateMutation } from "@saleor/productTypes/mutations";
import { ReorderEvent } from "@saleor/types"; import { ReorderEvent } from "@saleor/types";
import { AttributeTypeEnum } from "@saleor/types/globalTypes"; import { AttributeTypeEnum } from "@saleor/types/globalTypes";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import {
useMetadataUpdate,
usePrivateMetadataUpdate
} from "@saleor/utils/metadata/updateMetadata";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -25,14 +31,12 @@ import useAvailableAttributeSearch from "../../hooks/useAvailableAttributeSearch
import { TypedProductTypeDetailsQuery } from "../../queries"; import { TypedProductTypeDetailsQuery } from "../../queries";
import { AssignAttribute } from "../../types/AssignAttribute"; import { AssignAttribute } from "../../types/AssignAttribute";
import { ProductTypeDelete } from "../../types/ProductTypeDelete"; import { ProductTypeDelete } from "../../types/ProductTypeDelete";
import { ProductTypeUpdate as ProductTypeUpdateMutation } from "../../types/ProductTypeUpdate";
import { UnassignAttribute } from "../../types/UnassignAttribute"; import { UnassignAttribute } from "../../types/UnassignAttribute";
import { import {
productTypeListUrl, productTypeListUrl,
productTypeUrl, productTypeUrl,
ProductTypeUrlQueryParams ProductTypeUrlQueryParams
} from "../../urls"; } from "../../urls";
import { ProductTypeUpdateErrors } from "./errors";
interface ProductTypeUpdateProps { interface ProductTypeUpdateProps {
id: string; id: string;
@ -54,361 +58,367 @@ export const ProductTypeUpdate: React.FC<ProductTypeUpdateProps> = ({
id id
} }
}); });
const [errors, setErrors] = React.useState({
addAttributeErrors: [],
editAttributeErrors: [],
formErrors: []
});
const [
updateProductType,
updateProductTypeOpts
] = useProductTypeUpdateMutation({
onCompleted: updateData => {
if (
!updateData.productTypeUpdate.errors ||
updateData.productTypeUpdate.errors.length === 0
) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
} else if (
updateData.productTypeUpdate.errors !== null &&
updateData.productTypeUpdate.errors.length > 0
) {
setErrors(prevErrors => ({
...prevErrors,
formErrors: updateData.productTypeUpdate.errors
}));
}
}
});
const [updateMetadata] = useMetadataUpdate({});
const [updatePrivateMetadata] = usePrivateMetadataUpdate({});
const handleBack = () => navigate(productTypeListUrl()); const handleBack = () => navigate(productTypeListUrl());
const handleProductTypeUpdate = async (formData: ProductTypeForm) => {
const result = await updateProductType({
variables: {
id,
input: {
hasVariants: formData.hasVariants,
isShippingRequired: formData.isShippingRequired,
name: formData.name,
productAttributes: formData.productAttributes.map(
choice => choice.value
),
taxCode: formData.taxType,
variantAttributes: formData.variantAttributes.map(
choice => choice.value
),
weight: formData.weight
}
}
});
return result.data.productTypeUpdate.errors;
};
return ( return (
<ProductTypeUpdateErrors> <TypedProductTypeDetailsQuery displayLoader variables={{ id }}>
{({ errors, set: setErrors }) => ( {({ data, loading: dataLoading }) => {
<TypedProductTypeDetailsQuery displayLoader variables={{ id }}> const productType = data?.productType;
{({ data, loading: dataLoading }) => {
const productType = data?.productType;
if (productType === null) { if (productType === null) {
return <NotFoundPage onBack={handleBack} />; return <NotFoundPage onBack={handleBack} />;
} }
const closeModal = () => navigate(productTypeUrl(id), true); const closeModal = () => navigate(productTypeUrl(id), true);
const handleAttributeAssignSuccess = (data: AssignAttribute) => { const handleAttributeAssignSuccess = (data: AssignAttribute) => {
if (data.attributeAssign.errors.length === 0) { if (data.attributeAssign.errors.length === 0) {
notify({ notify({
status: "success", status: "success",
text: intl.formatMessage(commonMessages.savedChanges) text: intl.formatMessage(commonMessages.savedChanges)
});
closeModal();
} else if (
data.attributeAssign.errors !== null &&
data.attributeAssign.errors.length > 0
) {
setErrors(prevErrors => ({
...prevErrors,
addAttributeErrors: data.attributeAssign.errors
}));
}
};
const handleAttributeUnassignSuccess = (data: UnassignAttribute) => {
if (data.attributeUnassign.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
closeModal();
productAttributeListActions.reset();
variantAttributeListActions.reset();
}
};
const handleProductTypeDeleteSuccess = (
deleteData: ProductTypeDelete
) => {
if (deleteData.productTypeDelete.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Product type deleted"
})
});
navigate(productTypeListUrl(), true);
}
};
const handleSubmit = createMetadataUpdateHandler(
data?.productType,
handleProductTypeUpdate,
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
);
return (
<ProductTypeOperations
productType={maybe(() => data.productType)}
onAssignAttribute={handleAttributeAssignSuccess}
onUnassignAttribute={handleAttributeUnassignSuccess}
onProductTypeDelete={handleProductTypeDeleteSuccess}
onProductTypeAttributeReorder={() => undefined}
>
{({
assignAttribute,
deleteProductType,
unassignAttribute,
reorderAttribute
}) => {
const handleProductTypeDelete = () =>
deleteProductType.mutate({ id });
const handleProductTypeVariantsToggle = (hasVariants: boolean) =>
updateProductType({
variables: {
id,
input: {
hasVariants
}
}
}); });
closeModal(); const handleAssignAttribute = () =>
} else if ( assignAttribute.mutate({
data.attributeAssign.errors !== null && id,
data.attributeAssign.errors.length > 0 operations: params.ids.map(id => ({
) { id,
setErrors.addAttributeErrors(data.attributeAssign.errors); type: AttributeTypeEnum[params.type]
} }))
};
const handleAttributeUnassignSuccess = (
data: UnassignAttribute
) => {
if (data.attributeUnassign.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
}); });
closeModal();
productAttributeListActions.reset(); const handleAttributeUnassign = () =>
variantAttributeListActions.reset(); unassignAttribute.mutate({
} id,
}; ids: [params.id]
const handleProductTypeDeleteSuccess = (
deleteData: ProductTypeDelete
) => {
if (deleteData.productTypeDelete.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Product type deleted"
})
}); });
navigate(productTypeListUrl(), true);
} const handleBulkAttributeUnassign = () =>
}; unassignAttribute.mutate({
const handleProductTypeUpdateSuccess = ( id,
updateData: ProductTypeUpdateMutation ids: params.ids
) => {
if (
!updateData.productTypeUpdate.errors ||
updateData.productTypeUpdate.errors.length === 0
) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
}); });
} else if (
updateData.productTypeUpdate.errors !== null &&
updateData.productTypeUpdate.errors.length > 0
) {
setErrors.formErrors(updateData.productTypeUpdate.errors);
}
};
return ( const loading = updateProductTypeOpts.loading || dataLoading;
<ProductTypeOperations
productType={maybe(() => data.productType)}
onAssignAttribute={handleAttributeAssignSuccess}
onUnassignAttribute={handleAttributeUnassignSuccess}
onProductTypeDelete={handleProductTypeDeleteSuccess}
onProductTypeUpdate={handleProductTypeUpdateSuccess}
onProductTypeAttributeReorder={() => undefined}
>
{({
assignAttribute,
deleteProductType,
unassignAttribute,
updateProductType,
reorderAttribute
}) => {
const handleProductTypeDelete = () =>
deleteProductType.mutate({ id });
const handleProductTypeUpdate = (
formData: ProductTypeForm
) => {
updateProductType.mutate({
id,
input: {
hasVariants: formData.hasVariants,
isShippingRequired: formData.isShippingRequired,
name: formData.name,
productAttributes: formData.productAttributes.map(
choice => choice.value
),
taxCode: formData.taxType,
variantAttributes: formData.variantAttributes.map(
choice => choice.value
),
weight: formData.weight
}
});
};
const handleProductTypeVariantsToggle = (
hasVariants: boolean
) =>
updateProductType.mutate({
id,
input: {
hasVariants
}
});
const handleAssignAttribute = () =>
assignAttribute.mutate({
id,
operations: params.ids.map(id => ({
id,
type: AttributeTypeEnum[params.type]
}))
});
const handleAttributeUnassign = () => const handleAttributeReorder = (
unassignAttribute.mutate({ event: ReorderEvent,
id, type: AttributeTypeEnum
ids: [params.id] ) => {
}); const attributes =
type === AttributeTypeEnum.PRODUCT
? data.productType.productAttributes
: data.productType.variantAttributes;
const handleBulkAttributeUnassign = () => reorderAttribute.mutate({
unassignAttribute.mutate({ move: {
id, id: attributes[event.oldIndex].id,
ids: params.ids sortOrder: event.newIndex - event.oldIndex
}); },
productTypeId: id,
type
});
};
const loading = updateProductType.opts.loading || dataLoading; return (
<>
const handleAttributeReorder = ( <WindowTitle title={maybe(() => data.productType.name)} />
event: ReorderEvent, <ProductTypeDetailsPage
type: AttributeTypeEnum defaultWeightUnit={maybe(() => data.shop.defaultWeightUnit)}
) => { disabled={loading}
const attributes = errors={errors.formErrors}
type === AttributeTypeEnum.PRODUCT pageTitle={maybe(() => data.productType.name)}
? data.productType.productAttributes productType={maybe(() => data.productType)}
: data.productType.variantAttributes; saveButtonBarState={updateProductTypeOpts.status}
taxTypes={maybe(() => data.taxTypes, [])}
reorderAttribute.mutate({ onAttributeAdd={type =>
move: { navigate(
id: attributes[event.oldIndex].id, productTypeUrl(id, {
sortOrder: event.newIndex - event.oldIndex action: "assign-attribute",
}, type
productTypeId: id, })
type )
}); }
}; onAttributeClick={attributeId =>
navigate(attributeUrl(attributeId))
return ( }
<> onAttributeReorder={handleAttributeReorder}
<WindowTitle title={maybe(() => data.productType.name)} /> onAttributeUnassign={attributeId =>
<ProductTypeDetailsPage navigate(
defaultWeightUnit={maybe( productTypeUrl(id, {
() => data.shop.defaultWeightUnit action: "unassign-attribute",
)} id: attributeId
disabled={loading} })
errors={errors.formErrors} )
pageTitle={maybe(() => data.productType.name)} }
productType={maybe(() => data.productType)} onBack={handleBack}
saveButtonBarState={updateProductType.opts.status} onDelete={() =>
taxTypes={maybe(() => data.taxTypes, [])} navigate(
onAttributeAdd={type => productTypeUrl(id, {
navigate( action: "remove"
productTypeUrl(id, { })
action: "assign-attribute", )
type }
}) onHasVariantsToggle={handleProductTypeVariantsToggle}
) onSubmit={handleSubmit}
} productAttributeList={{
onAttributeClick={attributeId => isChecked: productAttributeListActions.isSelected,
navigate(attributeUrl(attributeId)) selected: productAttributeListActions.listElements.length,
} toggle: productAttributeListActions.toggle,
onAttributeReorder={handleAttributeReorder} toggleAll: productAttributeListActions.toggleAll,
onAttributeUnassign={attributeId => toolbar: (
navigate( <Button
productTypeUrl(id, { color="primary"
action: "unassign-attribute", onClick={() =>
id: attributeId navigate(
}) productTypeUrl(id, {
) action: "unassign-attributes",
} ids: productAttributeListActions.listElements
onBack={handleBack} })
onDelete={() => )
navigate( }
productTypeUrl(id, { >
action: "remove" <FormattedMessage
}) defaultMessage="Unassign"
) description="unassign attribute from product type, button"
}
onHasVariantsToggle={handleProductTypeVariantsToggle}
onSubmit={handleProductTypeUpdate}
productAttributeList={{
isChecked: productAttributeListActions.isSelected,
selected:
productAttributeListActions.listElements.length,
toggle: productAttributeListActions.toggle,
toggleAll: productAttributeListActions.toggleAll,
toolbar: (
<Button
color="primary"
onClick={() =>
navigate(
productTypeUrl(id, {
action: "unassign-attributes",
ids:
productAttributeListActions.listElements
})
)
}
>
<FormattedMessage
defaultMessage="Unassign"
description="unassign attribute from product type, button"
/>
</Button>
)
}}
variantAttributeList={{
isChecked: variantAttributeListActions.isSelected,
selected:
variantAttributeListActions.listElements.length,
toggle: variantAttributeListActions.toggle,
toggleAll: variantAttributeListActions.toggleAll,
toolbar: (
<Button
color="primary"
onClick={() =>
navigate(
productTypeUrl(id, {
action: "unassign-attributes",
ids:
variantAttributeListActions.listElements
})
)
}
>
<FormattedMessage
defaultMessage="Unassign"
description="unassign attribute from product type, button"
/>
</Button>
)
}}
/>
{!dataLoading &&
Object.keys(AttributeTypeEnum).map(key => (
<AssignAttributeDialog
attributes={maybe(() =>
result.data.productType.availableAttributes.edges.map(
edge => edge.node
)
)}
confirmButtonState={assignAttribute.opts.status}
errors={maybe(
() =>
assignAttribute.opts.data.attributeAssign.errors.map(
err => err.message
),
[]
)}
loading={result.loading}
onClose={closeModal}
onSubmit={handleAssignAttribute}
onFetch={search}
onFetchMore={loadMore}
onOpen={result.refetch}
hasMore={maybe(
() =>
result.data.productType.availableAttributes
.pageInfo.hasNextPage,
false
)}
open={
params.action === "assign-attribute" &&
params.type === AttributeTypeEnum[key]
}
selected={maybe(() => params.ids, [])}
onToggle={attributeId => {
const ids = maybe(() => params.ids, []);
navigate(
productTypeUrl(id, {
...params,
ids: ids.includes(attributeId)
? params.ids.filter(
selectedId => selectedId !== attributeId
)
: [...ids, attributeId]
})
);
}}
key={key}
/> />
))} </Button>
<ProductTypeDeleteDialog )
confirmButtonState={deleteProductType.opts.status} }}
name={maybe(() => data.productType.name, "...")} variantAttributeList={{
open={params.action === "remove"} isChecked: variantAttributeListActions.isSelected,
onClose={() => navigate(productTypeUrl(id))} selected: variantAttributeListActions.listElements.length,
onConfirm={handleProductTypeDelete} toggle: variantAttributeListActions.toggle,
/> toggleAll: variantAttributeListActions.toggleAll,
<ProductTypeBulkAttributeUnassignDialog toolbar: (
attributeQuantity={maybe(() => params.ids.length)} <Button
confirmButtonState={unassignAttribute.opts.status} color="primary"
onClose={closeModal} onClick={() =>
onConfirm={handleBulkAttributeUnassign} navigate(
open={params.action === "unassign-attributes"} productTypeUrl(id, {
productTypeName={maybe( action: "unassign-attributes",
() => data.productType.name, ids: variantAttributeListActions.listElements
"..." })
)
}
>
<FormattedMessage
defaultMessage="Unassign"
description="unassign attribute from product type, button"
/>
</Button>
)
}}
/>
{!dataLoading &&
Object.keys(AttributeTypeEnum).map(key => (
<AssignAttributeDialog
attributes={maybe(() =>
result.data.productType.availableAttributes.edges.map(
edge => edge.node
)
)} )}
/> confirmButtonState={assignAttribute.opts.status}
<ProductTypeAttributeUnassignDialog errors={maybe(
attributeName={maybe(
() => () =>
[ assignAttribute.opts.data.attributeAssign.errors.map(
...data.productType.productAttributes, err => err.message
...data.productType.variantAttributes ),
].find(attribute => attribute.id === params.id) []
.name,
"..."
)} )}
confirmButtonState={unassignAttribute.opts.status} loading={result.loading}
onClose={closeModal} onClose={closeModal}
onConfirm={handleAttributeUnassign} onSubmit={handleAssignAttribute}
open={params.action === "unassign-attribute"} onFetch={search}
productTypeName={maybe( onFetchMore={loadMore}
() => data.productType.name, onOpen={result.refetch}
"..." hasMore={maybe(
() =>
result.data.productType.availableAttributes.pageInfo
.hasNextPage,
false
)} )}
open={
params.action === "assign-attribute" &&
params.type === AttributeTypeEnum[key]
}
selected={maybe(() => params.ids, [])}
onToggle={attributeId => {
const ids = maybe(() => params.ids, []);
navigate(
productTypeUrl(id, {
...params,
ids: ids.includes(attributeId)
? params.ids.filter(
selectedId => selectedId !== attributeId
)
: [...ids, attributeId]
})
);
}}
key={key}
/> />
</> ))}
); <ProductTypeDeleteDialog
}} confirmButtonState={deleteProductType.opts.status}
</ProductTypeOperations> name={maybe(() => data.productType.name, "...")}
); open={params.action === "remove"}
}} onClose={() => navigate(productTypeUrl(id))}
</TypedProductTypeDetailsQuery> onConfirm={handleProductTypeDelete}
)} />
</ProductTypeUpdateErrors> <ProductTypeBulkAttributeUnassignDialog
attributeQuantity={maybe(() => params.ids.length)}
confirmButtonState={unassignAttribute.opts.status}
onClose={closeModal}
onConfirm={handleBulkAttributeUnassign}
open={params.action === "unassign-attributes"}
productTypeName={maybe(() => data.productType.name, "...")}
/>
<ProductTypeAttributeUnassignDialog
attributeName={maybe(
() =>
[
...data.productType.productAttributes,
...data.productType.variantAttributes
].find(attribute => attribute.id === params.id).name,
"..."
)}
confirmButtonState={unassignAttribute.opts.status}
onClose={closeModal}
onConfirm={handleAttributeUnassign}
open={params.action === "unassign-attribute"}
productTypeName={maybe(() => data.productType.name, "...")}
/>
</>
);
}}
</ProductTypeOperations>
);
}}
</TypedProductTypeDetailsQuery>
); );
}; };
export default ProductTypeUpdate; export default ProductTypeUpdate;

View file

@ -4,6 +4,7 @@ import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import Metadata from "@saleor/components/Metadata/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm"; import SeoForm from "@saleor/components/SeoForm";
@ -20,6 +21,7 @@ import { SearchCollections_search_edges_node } from "@saleor/searches/types/Sear
import { FetchMoreProps, ListActions } from "@saleor/types"; import { FetchMoreProps, ListActions } from "@saleor/types";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { convertFromRaw, RawDraftContentState } from "draft-js"; import { convertFromRaw, RawDraftContentState } from "draft-js";
import { diff } from "fast-array-diff"; import { diff } from "fast-array-diff";
import React from "react"; import React from "react";
@ -155,6 +157,12 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
getChoices(maybe(() => product.collections, [])) getChoices(maybe(() => product.collections, []))
); );
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const initialData = getProductUpdatePageFormData(product, variants); const initialData = getProductUpdatePageFormData(product, variants);
const initialDescription = maybe<RawDraftContentState>(() => const initialDescription = maybe<RawDraftContentState>(() =>
JSON.parse(product.descriptionJson) JSON.parse(product.descriptionJson)
@ -167,11 +175,18 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
const hasVariants = maybe(() => product.productType.hasVariants, false); const hasVariants = maybe(() => product.productType.hasVariants, false);
const handleSubmit = (data: ProductUpdatePageFormData) => { const handleSubmit = (data: ProductUpdatePageFormData) => {
const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
if (product.productType.hasVariants) { if (product.productType.hasVariants) {
onSubmit({ onSubmit({
...data, ...data,
addStocks: [], addStocks: [],
attributes, attributes,
metadata,
privateMetadata,
removeStocks: [], removeStocks: [],
updateStocks: [] updateStocks: []
}); });
@ -188,6 +203,8 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
stockDiff.added.some(addedStock => addedStock === stock.id) stockDiff.added.some(addedStock => addedStock === stock.id)
), ),
attributes, attributes,
metadata,
privateMetadata,
removeStocks: stockDiff.removed, removeStocks: stockDiff.removed,
updateStocks: stocks.filter( updateStocks: stocks.filter(
stock => !stockDiff.added.some(addedStock => addedStock === stock.id) stock => !stockDiff.added.some(addedStock => addedStock === stock.id)
@ -224,6 +241,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
attributes, attributes,
triggerChange triggerChange
); );
const changeMetadata = makeMetadataChangeHandler(change);
return ( return (
<> <>
@ -347,6 +365,8 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
"Add search engine title and description to make this product easier to find" "Add search engine title and description to make this product easier to find"
})} })}
/> />
<CardSpacer />
<Metadata data={data} onChange={changeMetadata} />
</div> </div>
<div> <div>
<ProductOrganization <ProductOrganization

View file

@ -4,6 +4,8 @@ import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import { MetadataFormData } from "@saleor/components/Metadata";
import Metadata from "@saleor/components/Metadata/Metadata";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ProductVariant } from "@saleor/fragments/types/ProductVariant"; import { ProductVariant } from "@saleor/fragments/types/ProductVariant";
@ -17,6 +19,8 @@ import {
getAttributeInputFromVariant, getAttributeInputFromVariant,
getStockInputFromVariant getStockInputFromVariant
} from "@saleor/products/utils/data"; } from "@saleor/products/utils/data";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import { diff } from "fast-array-diff"; import { diff } from "fast-array-diff";
import React from "react"; import React from "react";
@ -31,7 +35,7 @@ import ProductVariantImageSelectDialog from "../ProductVariantImageSelectDialog"
import ProductVariantNavigation from "../ProductVariantNavigation"; import ProductVariantNavigation from "../ProductVariantNavigation";
import ProductVariantPrice from "../ProductVariantPrice"; import ProductVariantPrice from "../ProductVariantPrice";
export interface ProductVariantPageFormData { export interface ProductVariantPageFormData extends MetadataFormData {
costPrice: string; costPrice: string;
price: string; price: string;
sku: string; sku: string;
@ -100,6 +104,12 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
const [isModalOpened, setModalStatus] = React.useState(false); const [isModalOpened, setModalStatus] = React.useState(false);
const toggleModal = () => setModalStatus(!isModalOpened); const toggleModal = () => setModalStatus(!isModalOpened);
const {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler: makeMetadataChangeHandler
} = useMetadataChangeTrigger();
const variantImages = maybe(() => variant.images.map(image => image.id), []); const variantImages = maybe(() => variant.images.map(image => image.id), []);
const productImages = maybe(() => const productImages = maybe(() =>
variant.product.images.sort((prev, next) => variant.product.images.sort((prev, next) =>
@ -114,7 +124,9 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
const initialForm: ProductVariantPageFormData = { const initialForm: ProductVariantPageFormData = {
costPrice: maybe(() => variant.costPrice.amount.toString(), ""), costPrice: maybe(() => variant.costPrice.amount.toString(), ""),
metadata: variant?.metadata?.map(mapMetadataItemToInput),
price: maybe(() => variant.price.amount.toString(), ""), price: maybe(() => variant.price.amount.toString(), ""),
privateMetadata: variant?.privateMetadata?.map(mapMetadataItemToInput),
sku: maybe(() => variant.sku, ""), sku: maybe(() => variant.sku, ""),
trackInventory: variant?.trackInventory, trackInventory: variant?.trackInventory,
weight: variant?.weight?.value.toString() || "" weight: variant?.weight?.value.toString() || ""
@ -124,6 +136,10 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
const dataStocks = stocks.map(stock => stock.id); const dataStocks = stocks.map(stock => stock.id);
const variantStocks = variant.stocks.map(stock => stock.warehouse.id); const variantStocks = variant.stocks.map(stock => stock.warehouse.id);
const stockDiff = diff(variantStocks, dataStocks); const stockDiff = diff(variantStocks, dataStocks);
const metadata = isMetadataModified ? data.metadata : undefined;
const privateMetadata = isPrivateMetadataModified
? data.privateMetadata
: undefined;
onSubmit({ onSubmit({
...data, ...data,
@ -131,6 +147,8 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
stockDiff.added.some(addedStock => addedStock === stock.id) stockDiff.added.some(addedStock => addedStock === stock.id)
), ),
attributes, attributes,
metadata,
privateMetadata,
removeStocks: stockDiff.removed, removeStocks: stockDiff.removed,
updateStocks: stocks.filter( updateStocks: stocks.filter(
stock => !stockDiff.added.some(addedStock => addedStock === stock.id) stock => !stockDiff.added.some(addedStock => addedStock === stock.id)
@ -152,6 +170,8 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
triggerChange(); triggerChange();
}; };
const changeMetadata = makeMetadataChangeHandler(change);
return ( return (
<> <>
<Grid variant="inverted"> <Grid variant="inverted">
@ -235,6 +255,8 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
removeStock(id); removeStock(id);
}} }}
/> />
<CardSpacer />
<Metadata data={data} onChange={changeMetadata} />
</div> </div>
</Grid> </Grid>
<SaveButtonBar <SaveButtonBar

View file

@ -153,6 +153,13 @@ export const product: (
isFeatured: false, isFeatured: false,
isPublished: true, isPublished: true,
margin: { __typename: "Margin", start: 2, stop: 7 }, margin: { __typename: "Margin", start: 2, stop: 7 },
metadata: [
{
__typename: "MetadataItem",
key: "integration.id",
value: "100023123"
}
],
name: "Ergonomic Plastic Bacon", name: "Ergonomic Plastic Bacon",
pricing: { pricing: {
__typename: "ProductPricingInfo", __typename: "ProductPricingInfo",
@ -186,6 +193,7 @@ export const product: (
} }
} }
}, },
privateMetadata: [],
productType: { productType: {
__typename: "ProductType", __typename: "ProductType",
hasVariants: true, hasVariants: true,
@ -1516,12 +1524,20 @@ export const variant = (placeholderImage: string): ProductVariant => ({
url: placeholderImage url: placeholderImage
} }
], ],
metadata: [
{
__typename: "MetadataItem",
key: "integration.id",
value: "100023123"
}
],
name: "Extended Hard", name: "Extended Hard",
price: { price: {
__typename: "Money", __typename: "Money",
amount: 100, amount: 100,
currency: "USD" currency: "USD"
}, },
privateMetadata: [],
product: { product: {
__typename: "Product" as "Product", __typename: "Product" as "Product",
id: "prod1", id: "prod1",

View file

@ -99,6 +99,18 @@ export interface ProductCreate_productCreate_product_pricing {
priceRangeUndiscounted: ProductCreate_productCreate_product_pricing_priceRangeUndiscounted | null; priceRangeUndiscounted: ProductCreate_productCreate_product_pricing_priceRangeUndiscounted | null;
} }
export interface ProductCreate_productCreate_product_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductCreate_productCreate_product_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductCreate_productCreate_product_category { export interface ProductCreate_productCreate_product_category {
__typename: "Category"; __typename: "Category";
id: string; id: string;
@ -186,6 +198,8 @@ export interface ProductCreate_productCreate_product {
attributes: ProductCreate_productCreate_product_attributes[]; attributes: ProductCreate_productCreate_product_attributes[];
productType: ProductCreate_productCreate_product_productType; productType: ProductCreate_productCreate_product_productType;
pricing: ProductCreate_productCreate_product_pricing | null; pricing: ProductCreate_productCreate_product_pricing | null;
metadata: (ProductCreate_productCreate_product_metadata | null)[];
privateMetadata: (ProductCreate_productCreate_product_privateMetadata | null)[];
name: string; name: string;
descriptionJson: any; descriptionJson: any;
seoTitle: string | null; seoTitle: string | null;

View file

@ -93,6 +93,18 @@ export interface ProductDetails_product_pricing {
priceRangeUndiscounted: ProductDetails_product_pricing_priceRangeUndiscounted | null; priceRangeUndiscounted: ProductDetails_product_pricing_priceRangeUndiscounted | null;
} }
export interface ProductDetails_product_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductDetails_product_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductDetails_product_category { export interface ProductDetails_product_category {
__typename: "Category"; __typename: "Category";
id: string; id: string;
@ -180,6 +192,8 @@ export interface ProductDetails_product {
attributes: ProductDetails_product_attributes[]; attributes: ProductDetails_product_attributes[];
productType: ProductDetails_product_productType; productType: ProductDetails_product_productType;
pricing: ProductDetails_product_pricing | null; pricing: ProductDetails_product_pricing | null;
metadata: (ProductDetails_product_metadata | null)[];
privateMetadata: (ProductDetails_product_privateMetadata | null)[];
name: string; name: string;
descriptionJson: any; descriptionJson: any;
seoTitle: string | null; seoTitle: string | null;

View file

@ -99,6 +99,18 @@ export interface ProductImageCreate_productImageCreate_product_pricing {
priceRangeUndiscounted: ProductImageCreate_productImageCreate_product_pricing_priceRangeUndiscounted | null; priceRangeUndiscounted: ProductImageCreate_productImageCreate_product_pricing_priceRangeUndiscounted | null;
} }
export interface ProductImageCreate_productImageCreate_product_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductImageCreate_productImageCreate_product_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductImageCreate_productImageCreate_product_category { export interface ProductImageCreate_productImageCreate_product_category {
__typename: "Category"; __typename: "Category";
id: string; id: string;
@ -186,6 +198,8 @@ export interface ProductImageCreate_productImageCreate_product {
attributes: ProductImageCreate_productImageCreate_product_attributes[]; attributes: ProductImageCreate_productImageCreate_product_attributes[];
productType: ProductImageCreate_productImageCreate_product_productType; productType: ProductImageCreate_productImageCreate_product_productType;
pricing: ProductImageCreate_productImageCreate_product_pricing | null; pricing: ProductImageCreate_productImageCreate_product_pricing | null;
metadata: (ProductImageCreate_productImageCreate_product_metadata | null)[];
privateMetadata: (ProductImageCreate_productImageCreate_product_privateMetadata | null)[];
name: string; name: string;
descriptionJson: any; descriptionJson: any;
seoTitle: string | null; seoTitle: string | null;

View file

@ -99,6 +99,18 @@ export interface ProductImageUpdate_productImageUpdate_product_pricing {
priceRangeUndiscounted: ProductImageUpdate_productImageUpdate_product_pricing_priceRangeUndiscounted | null; priceRangeUndiscounted: ProductImageUpdate_productImageUpdate_product_pricing_priceRangeUndiscounted | null;
} }
export interface ProductImageUpdate_productImageUpdate_product_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductImageUpdate_productImageUpdate_product_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductImageUpdate_productImageUpdate_product_category { export interface ProductImageUpdate_productImageUpdate_product_category {
__typename: "Category"; __typename: "Category";
id: string; id: string;
@ -186,6 +198,8 @@ export interface ProductImageUpdate_productImageUpdate_product {
attributes: ProductImageUpdate_productImageUpdate_product_attributes[]; attributes: ProductImageUpdate_productImageUpdate_product_attributes[];
productType: ProductImageUpdate_productImageUpdate_product_productType; productType: ProductImageUpdate_productImageUpdate_product_productType;
pricing: ProductImageUpdate_productImageUpdate_product_pricing | null; pricing: ProductImageUpdate_productImageUpdate_product_pricing | null;
metadata: (ProductImageUpdate_productImageUpdate_product_metadata | null)[];
privateMetadata: (ProductImageUpdate_productImageUpdate_product_privateMetadata | null)[];
name: string; name: string;
descriptionJson: any; descriptionJson: any;
seoTitle: string | null; seoTitle: string | null;

View file

@ -99,6 +99,18 @@ export interface ProductUpdate_productUpdate_product_pricing {
priceRangeUndiscounted: ProductUpdate_productUpdate_product_pricing_priceRangeUndiscounted | null; priceRangeUndiscounted: ProductUpdate_productUpdate_product_pricing_priceRangeUndiscounted | null;
} }
export interface ProductUpdate_productUpdate_product_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductUpdate_productUpdate_product_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductUpdate_productUpdate_product_category { export interface ProductUpdate_productUpdate_product_category {
__typename: "Category"; __typename: "Category";
id: string; id: string;
@ -186,6 +198,8 @@ export interface ProductUpdate_productUpdate_product {
attributes: ProductUpdate_productUpdate_product_attributes[]; attributes: ProductUpdate_productUpdate_product_attributes[];
productType: ProductUpdate_productUpdate_product_productType; productType: ProductUpdate_productUpdate_product_productType;
pricing: ProductUpdate_productUpdate_product_pricing | null; pricing: ProductUpdate_productUpdate_product_pricing | null;
metadata: (ProductUpdate_productUpdate_product_metadata | null)[];
privateMetadata: (ProductUpdate_productUpdate_product_privateMetadata | null)[];
name: string; name: string;
descriptionJson: any; descriptionJson: any;
seoTitle: string | null; seoTitle: string | null;

View file

@ -8,6 +8,18 @@ import { WeightUnitsEnum } from "./../../types/globalTypes";
// GraphQL query operation: ProductVariantDetails // GraphQL query operation: ProductVariantDetails
// ==================================================== // ====================================================
export interface ProductVariantDetails_productVariant_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductVariantDetails_productVariant_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface ProductVariantDetails_productVariant_attributes_attribute_values { export interface ProductVariantDetails_productVariant_attributes_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -114,6 +126,8 @@ export interface ProductVariantDetails_productVariant_weight {
export interface ProductVariantDetails_productVariant { export interface ProductVariantDetails_productVariant {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
metadata: (ProductVariantDetails_productVariant_metadata | null)[];
privateMetadata: (ProductVariantDetails_productVariant_privateMetadata | null)[];
attributes: ProductVariantDetails_productVariant_attributes[]; attributes: ProductVariantDetails_productVariant_attributes[];
costPrice: ProductVariantDetails_productVariant_costPrice | null; costPrice: ProductVariantDetails_productVariant_costPrice | null;
images: (ProductVariantDetails_productVariant_images | null)[] | null; images: (ProductVariantDetails_productVariant_images | null)[] | null;

View file

@ -99,6 +99,18 @@ export interface SimpleProductUpdate_productUpdate_product_pricing {
priceRangeUndiscounted: SimpleProductUpdate_productUpdate_product_pricing_priceRangeUndiscounted | null; priceRangeUndiscounted: SimpleProductUpdate_productUpdate_product_pricing_priceRangeUndiscounted | null;
} }
export interface SimpleProductUpdate_productUpdate_product_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface SimpleProductUpdate_productUpdate_product_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface SimpleProductUpdate_productUpdate_product_category { export interface SimpleProductUpdate_productUpdate_product_category {
__typename: "Category"; __typename: "Category";
id: string; id: string;
@ -186,6 +198,8 @@ export interface SimpleProductUpdate_productUpdate_product {
attributes: SimpleProductUpdate_productUpdate_product_attributes[]; attributes: SimpleProductUpdate_productUpdate_product_attributes[];
productType: SimpleProductUpdate_productUpdate_product_productType; productType: SimpleProductUpdate_productUpdate_product_productType;
pricing: SimpleProductUpdate_productUpdate_product_pricing | null; pricing: SimpleProductUpdate_productUpdate_product_pricing | null;
metadata: (SimpleProductUpdate_productUpdate_product_metadata | null)[];
privateMetadata: (SimpleProductUpdate_productUpdate_product_privateMetadata | null)[];
name: string; name: string;
descriptionJson: any; descriptionJson: any;
seoTitle: string | null; seoTitle: string | null;
@ -215,6 +229,18 @@ export interface SimpleProductUpdate_productVariantUpdate_errors {
field: string | null; field: string | null;
} }
export interface SimpleProductUpdate_productVariantUpdate_productVariant_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface SimpleProductUpdate_productVariantUpdate_productVariant_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface SimpleProductUpdate_productVariantUpdate_productVariant_attributes_attribute_values { export interface SimpleProductUpdate_productVariantUpdate_productVariant_attributes_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -321,6 +347,8 @@ export interface SimpleProductUpdate_productVariantUpdate_productVariant_weight
export interface SimpleProductUpdate_productVariantUpdate_productVariant { export interface SimpleProductUpdate_productVariantUpdate_productVariant {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
metadata: (SimpleProductUpdate_productVariantUpdate_productVariant_metadata | null)[];
privateMetadata: (SimpleProductUpdate_productVariantUpdate_productVariant_privateMetadata | null)[];
attributes: SimpleProductUpdate_productVariantUpdate_productVariant_attributes[]; attributes: SimpleProductUpdate_productVariantUpdate_productVariant_attributes[];
costPrice: SimpleProductUpdate_productVariantUpdate_productVariant_costPrice | null; costPrice: SimpleProductUpdate_productVariantUpdate_productVariant_costPrice | null;
images: (SimpleProductUpdate_productVariantUpdate_productVariant_images | null)[] | null; images: (SimpleProductUpdate_productVariantUpdate_productVariant_images | null)[] | null;
@ -346,6 +374,18 @@ export interface SimpleProductUpdate_productVariantStocksCreate_errors {
index: number | null; index: number | null;
} }
export interface SimpleProductUpdate_productVariantStocksCreate_productVariant_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface SimpleProductUpdate_productVariantStocksCreate_productVariant_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface SimpleProductUpdate_productVariantStocksCreate_productVariant_attributes_attribute_values { export interface SimpleProductUpdate_productVariantStocksCreate_productVariant_attributes_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -452,6 +492,8 @@ export interface SimpleProductUpdate_productVariantStocksCreate_productVariant_w
export interface SimpleProductUpdate_productVariantStocksCreate_productVariant { export interface SimpleProductUpdate_productVariantStocksCreate_productVariant {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
metadata: (SimpleProductUpdate_productVariantStocksCreate_productVariant_metadata | null)[];
privateMetadata: (SimpleProductUpdate_productVariantStocksCreate_productVariant_privateMetadata | null)[];
attributes: SimpleProductUpdate_productVariantStocksCreate_productVariant_attributes[]; attributes: SimpleProductUpdate_productVariantStocksCreate_productVariant_attributes[];
costPrice: SimpleProductUpdate_productVariantStocksCreate_productVariant_costPrice | null; costPrice: SimpleProductUpdate_productVariantStocksCreate_productVariant_costPrice | null;
images: (SimpleProductUpdate_productVariantStocksCreate_productVariant_images | null)[] | null; images: (SimpleProductUpdate_productVariantStocksCreate_productVariant_images | null)[] | null;
@ -476,6 +518,18 @@ export interface SimpleProductUpdate_productVariantStocksDelete_errors {
field: string | null; field: string | null;
} }
export interface SimpleProductUpdate_productVariantStocksDelete_productVariant_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface SimpleProductUpdate_productVariantStocksDelete_productVariant_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface SimpleProductUpdate_productVariantStocksDelete_productVariant_attributes_attribute_values { export interface SimpleProductUpdate_productVariantStocksDelete_productVariant_attributes_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -582,6 +636,8 @@ export interface SimpleProductUpdate_productVariantStocksDelete_productVariant_w
export interface SimpleProductUpdate_productVariantStocksDelete_productVariant { export interface SimpleProductUpdate_productVariantStocksDelete_productVariant {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
metadata: (SimpleProductUpdate_productVariantStocksDelete_productVariant_metadata | null)[];
privateMetadata: (SimpleProductUpdate_productVariantStocksDelete_productVariant_privateMetadata | null)[];
attributes: SimpleProductUpdate_productVariantStocksDelete_productVariant_attributes[]; attributes: SimpleProductUpdate_productVariantStocksDelete_productVariant_attributes[];
costPrice: SimpleProductUpdate_productVariantStocksDelete_productVariant_costPrice | null; costPrice: SimpleProductUpdate_productVariantStocksDelete_productVariant_costPrice | null;
images: (SimpleProductUpdate_productVariantStocksDelete_productVariant_images | null)[] | null; images: (SimpleProductUpdate_productVariantStocksDelete_productVariant_images | null)[] | null;
@ -607,6 +663,18 @@ export interface SimpleProductUpdate_productVariantStocksUpdate_errors {
index: number | null; index: number | null;
} }
export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant_attributes_attribute_values { export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant_attributes_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -713,6 +781,8 @@ export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant_w
export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant { export interface SimpleProductUpdate_productVariantStocksUpdate_productVariant {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
metadata: (SimpleProductUpdate_productVariantStocksUpdate_productVariant_metadata | null)[];
privateMetadata: (SimpleProductUpdate_productVariantStocksUpdate_productVariant_privateMetadata | null)[];
attributes: SimpleProductUpdate_productVariantStocksUpdate_productVariant_attributes[]; attributes: SimpleProductUpdate_productVariantStocksUpdate_productVariant_attributes[];
costPrice: SimpleProductUpdate_productVariantStocksUpdate_productVariant_costPrice | null; costPrice: SimpleProductUpdate_productVariantStocksUpdate_productVariant_costPrice | null;
images: (SimpleProductUpdate_productVariantStocksUpdate_productVariant_images | null)[] | null; images: (SimpleProductUpdate_productVariantStocksUpdate_productVariant_images | null)[] | null;

View file

@ -14,6 +14,18 @@ export interface VariantCreate_productVariantCreate_errors {
field: string | null; field: string | null;
} }
export interface VariantCreate_productVariantCreate_productVariant_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface VariantCreate_productVariantCreate_productVariant_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface VariantCreate_productVariantCreate_productVariant_attributes_attribute_values { export interface VariantCreate_productVariantCreate_productVariant_attributes_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -120,6 +132,8 @@ export interface VariantCreate_productVariantCreate_productVariant_weight {
export interface VariantCreate_productVariantCreate_productVariant { export interface VariantCreate_productVariantCreate_productVariant {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
metadata: (VariantCreate_productVariantCreate_productVariant_metadata | null)[];
privateMetadata: (VariantCreate_productVariantCreate_productVariant_privateMetadata | null)[];
attributes: VariantCreate_productVariantCreate_productVariant_attributes[]; attributes: VariantCreate_productVariantCreate_productVariant_attributes[];
costPrice: VariantCreate_productVariantCreate_productVariant_costPrice | null; costPrice: VariantCreate_productVariantCreate_productVariant_costPrice | null;
images: (VariantCreate_productVariantCreate_productVariant_images | null)[] | null; images: (VariantCreate_productVariantCreate_productVariant_images | null)[] | null;

View file

@ -14,6 +14,18 @@ export interface VariantImageAssign_variantImageAssign_errors {
field: string | null; field: string | null;
} }
export interface VariantImageAssign_variantImageAssign_productVariant_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface VariantImageAssign_variantImageAssign_productVariant_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface VariantImageAssign_variantImageAssign_productVariant_attributes_attribute_values { export interface VariantImageAssign_variantImageAssign_productVariant_attributes_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -120,6 +132,8 @@ export interface VariantImageAssign_variantImageAssign_productVariant_weight {
export interface VariantImageAssign_variantImageAssign_productVariant { export interface VariantImageAssign_variantImageAssign_productVariant {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
metadata: (VariantImageAssign_variantImageAssign_productVariant_metadata | null)[];
privateMetadata: (VariantImageAssign_variantImageAssign_productVariant_privateMetadata | null)[];
attributes: VariantImageAssign_variantImageAssign_productVariant_attributes[]; attributes: VariantImageAssign_variantImageAssign_productVariant_attributes[];
costPrice: VariantImageAssign_variantImageAssign_productVariant_costPrice | null; costPrice: VariantImageAssign_variantImageAssign_productVariant_costPrice | null;
images: (VariantImageAssign_variantImageAssign_productVariant_images | null)[] | null; images: (VariantImageAssign_variantImageAssign_productVariant_images | null)[] | null;

View file

@ -14,6 +14,18 @@ export interface VariantImageUnassign_variantImageUnassign_errors {
field: string | null; field: string | null;
} }
export interface VariantImageUnassign_variantImageUnassign_productVariant_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface VariantImageUnassign_variantImageUnassign_productVariant_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface VariantImageUnassign_variantImageUnassign_productVariant_attributes_attribute_values { export interface VariantImageUnassign_variantImageUnassign_productVariant_attributes_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -120,6 +132,8 @@ export interface VariantImageUnassign_variantImageUnassign_productVariant_weight
export interface VariantImageUnassign_variantImageUnassign_productVariant { export interface VariantImageUnassign_variantImageUnassign_productVariant {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
metadata: (VariantImageUnassign_variantImageUnassign_productVariant_metadata | null)[];
privateMetadata: (VariantImageUnassign_variantImageUnassign_productVariant_privateMetadata | null)[];
attributes: VariantImageUnassign_variantImageUnassign_productVariant_attributes[]; attributes: VariantImageUnassign_variantImageUnassign_productVariant_attributes[];
costPrice: VariantImageUnassign_variantImageUnassign_productVariant_costPrice | null; costPrice: VariantImageUnassign_variantImageUnassign_productVariant_costPrice | null;
images: (VariantImageUnassign_variantImageUnassign_productVariant_images | null)[] | null; images: (VariantImageUnassign_variantImageUnassign_productVariant_images | null)[] | null;

View file

@ -14,6 +14,18 @@ export interface VariantUpdate_productVariantUpdate_errors {
field: string | null; field: string | null;
} }
export interface VariantUpdate_productVariantUpdate_productVariant_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface VariantUpdate_productVariantUpdate_productVariant_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface VariantUpdate_productVariantUpdate_productVariant_attributes_attribute_values { export interface VariantUpdate_productVariantUpdate_productVariant_attributes_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -120,6 +132,8 @@ export interface VariantUpdate_productVariantUpdate_productVariant_weight {
export interface VariantUpdate_productVariantUpdate_productVariant { export interface VariantUpdate_productVariantUpdate_productVariant {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
metadata: (VariantUpdate_productVariantUpdate_productVariant_metadata | null)[];
privateMetadata: (VariantUpdate_productVariantUpdate_productVariant_privateMetadata | null)[];
attributes: VariantUpdate_productVariantUpdate_productVariant_attributes[]; attributes: VariantUpdate_productVariantUpdate_productVariant_attributes[];
costPrice: VariantUpdate_productVariantUpdate_productVariant_costPrice | null; costPrice: VariantUpdate_productVariantUpdate_productVariant_costPrice | null;
images: (VariantUpdate_productVariantUpdate_productVariant_images | null)[] | null; images: (VariantUpdate_productVariantUpdate_productVariant_images | null)[] | null;
@ -145,6 +159,18 @@ export interface VariantUpdate_productVariantStocksUpdate_errors {
index: number | null; index: number | null;
} }
export interface VariantUpdate_productVariantStocksUpdate_productVariant_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface VariantUpdate_productVariantStocksUpdate_productVariant_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface VariantUpdate_productVariantStocksUpdate_productVariant_attributes_attribute_values { export interface VariantUpdate_productVariantStocksUpdate_productVariant_attributes_attribute_values {
__typename: "AttributeValue"; __typename: "AttributeValue";
id: string; id: string;
@ -251,6 +277,8 @@ export interface VariantUpdate_productVariantStocksUpdate_productVariant_weight
export interface VariantUpdate_productVariantStocksUpdate_productVariant { export interface VariantUpdate_productVariantStocksUpdate_productVariant {
__typename: "ProductVariant"; __typename: "ProductVariant";
id: string; id: string;
metadata: (VariantUpdate_productVariantStocksUpdate_productVariant_metadata | null)[];
privateMetadata: (VariantUpdate_productVariantStocksUpdate_productVariant_privateMetadata | null)[];
attributes: VariantUpdate_productVariantStocksUpdate_productVariant_attributes[]; attributes: VariantUpdate_productVariantStocksUpdate_productVariant_attributes[];
costPrice: VariantUpdate_productVariantStocksUpdate_productVariant_costPrice | null; costPrice: VariantUpdate_productVariantStocksUpdate_productVariant_costPrice | null;
images: (VariantUpdate_productVariantStocksUpdate_productVariant_images | null)[] | null; images: (VariantUpdate_productVariantStocksUpdate_productVariant_images | null)[] | null;

View file

@ -1,3 +1,4 @@
import { MetadataFormData } from "@saleor/components/Metadata/types";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { ProductVariant } from "@saleor/fragments/types/ProductVariant"; import { ProductVariant } from "@saleor/fragments/types/ProductVariant";
@ -10,6 +11,7 @@ import {
} from "@saleor/products/types/ProductDetails"; } from "@saleor/products/types/ProductDetails";
import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/searches/types/SearchProductTypes"; import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/searches/types/SearchProductTypes";
import { StockInput } from "@saleor/types/globalTypes"; import { StockInput } from "@saleor/types/globalTypes";
import { mapMetadataItemToInput } from "@saleor/utils/maps";
import { RawDraftContentState } from "draft-js"; import { RawDraftContentState } from "draft-js";
import { ProductAttributeInput } from "../components/ProductAttributes"; import { ProductAttributeInput } from "../components/ProductAttributes";
@ -168,7 +170,7 @@ export function getChoices(nodes: Node[]): SingleAutocompleteChoiceType[] {
); );
} }
export interface ProductUpdatePageFormData { export interface ProductUpdatePageFormData extends MetadataFormData {
basePrice: number; basePrice: number;
category: string | null; category: string | null;
collections: string[]; collections: string[];
@ -198,7 +200,9 @@ export function getProductUpdatePageFormData(
), ),
description: maybe(() => JSON.parse(product.descriptionJson)), description: maybe(() => JSON.parse(product.descriptionJson)),
isPublished: maybe(() => product.isPublished, false), isPublished: maybe(() => product.isPublished, false),
metadata: product?.metadata?.map(mapMetadataItemToInput),
name: maybe(() => product.name, ""), name: maybe(() => product.name, ""),
privateMetadata: product?.privateMetadata?.map(mapMetadataItemToInput),
publicationDate: maybe(() => product.publicationDate, ""), publicationDate: maybe(() => product.publicationDate, ""),
seoDescription: maybe(() => product.seoDescription, ""), seoDescription: maybe(() => product.seoDescription, ""),
seoTitle: maybe(() => product.seoTitle, ""), seoTitle: maybe(() => product.seoTitle, ""),

View file

@ -23,6 +23,11 @@ import {
import useCategorySearch from "@saleor/searches/useCategorySearch"; import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch"; import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import {
useMetadataUpdate,
usePrivateMetadataUpdate
} from "@saleor/utils/metadata/updateMetadata";
import { useWarehouseList } from "@saleor/warehouses/queries"; import { useWarehouseList } from "@saleor/warehouses/queries";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -81,6 +86,8 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
} }
}); });
const shop = useShop(); const shop = useShop();
const [updateMetadata] = useMetadataUpdate({});
const [updatePrivateMetadata] = usePrivateMetadataUpdate({});
const { data, loading, refetch } = useProductDetails({ const { data, loading, refetch } = useProductDetails({
displayLoader: true, displayLoader: true,
@ -181,10 +188,15 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
deleteProductImage({ variables: { id } }); deleteProductImage({ variables: { id } });
const handleImageEdit = (imageId: string) => () => const handleImageEdit = (imageId: string) => () =>
navigate(productImageUrl(id, imageId)); navigate(productImageUrl(id, imageId));
const handleSubmit = createUpdateHandler( const handleSubmit = createMetadataUpdateHandler(
product, product,
variables => updateProduct({ variables }), createUpdateHandler(
variables => updateSimpleProduct({ variables }) product,
variables => updateProduct({ variables }),
variables => updateSimpleProduct({ variables })
),
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
); );
const handleImageUpload = createImageUploadHandler(id, variables => const handleImageUpload = createImageUploadHandler(id, variables =>
createProductImage({ variables }) createProductImage({ variables })

View file

@ -1,20 +1,34 @@
import { BulkStockErrorFragment } from "@saleor/fragments/types/BulkStockErrorFragment";
import { ProductErrorFragment } from "@saleor/fragments/types/ProductErrorFragment";
import { StockErrorFragment } from "@saleor/fragments/types/StockErrorFragment";
import { decimal, weight } from "@saleor/misc"; import { decimal, weight } from "@saleor/misc";
import { ProductUpdatePageSubmitData } from "@saleor/products/components/ProductUpdatePage"; import { ProductUpdatePageSubmitData } from "@saleor/products/components/ProductUpdatePage";
import { ProductDetails_product } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product } from "@saleor/products/types/ProductDetails";
import { ProductImageCreateVariables } from "@saleor/products/types/ProductImageCreate"; import { ProductImageCreateVariables } from "@saleor/products/types/ProductImageCreate";
import { ProductImageReorderVariables } from "@saleor/products/types/ProductImageReorder"; import { ProductImageReorderVariables } from "@saleor/products/types/ProductImageReorder";
import { ProductUpdateVariables } from "@saleor/products/types/ProductUpdate"; import {
import { SimpleProductUpdateVariables } from "@saleor/products/types/SimpleProductUpdate"; ProductUpdate,
ProductUpdateVariables
} from "@saleor/products/types/ProductUpdate";
import {
SimpleProductUpdate,
SimpleProductUpdateVariables
} from "@saleor/products/types/SimpleProductUpdate";
import { mapFormsetStockToStockInput } from "@saleor/products/utils/data"; import { mapFormsetStockToStockInput } from "@saleor/products/utils/data";
import { ReorderEvent } from "@saleor/types"; import { ReorderEvent } from "@saleor/types";
import { MutationFetchResult } from "react-apollo";
import { arrayMove } from "react-sortable-hoc"; import { arrayMove } from "react-sortable-hoc";
export function createUpdateHandler( export function createUpdateHandler(
product: ProductDetails_product, product: ProductDetails_product,
updateProduct: (variables: ProductUpdateVariables) => void, updateProduct: (
updateSimpleProduct: (variables: SimpleProductUpdateVariables) => void variables: ProductUpdateVariables
) => Promise<MutationFetchResult<ProductUpdate>>,
updateSimpleProduct: (
variables: SimpleProductUpdateVariables
) => Promise<MutationFetchResult<SimpleProductUpdate>>
) { ) {
return (data: ProductUpdatePageSubmitData) => { return async (data: ProductUpdatePageSubmitData) => {
const productVariables: ProductUpdateVariables = { const productVariables: ProductUpdateVariables = {
attributes: data.attributes.map(attribute => ({ attributes: data.attributes.map(attribute => ({
id: attribute.id, id: attribute.id,
@ -36,10 +50,15 @@ export function createUpdateHandler(
} }
}; };
let errors: Array<
ProductErrorFragment | StockErrorFragment | BulkStockErrorFragment
>;
if (product.productType.hasVariants) { if (product.productType.hasVariants) {
updateProduct(productVariables); const result = await updateProduct(productVariables);
errors = result.data.productUpdate.errors;
} else { } else {
updateSimpleProduct({ const result = await updateSimpleProduct({
...productVariables, ...productVariables,
addStocks: data.addStocks.map(mapFormsetStockToStockInput), addStocks: data.addStocks.map(mapFormsetStockToStockInput),
deleteStocks: data.removeStocks, deleteStocks: data.removeStocks,
@ -51,7 +70,16 @@ export function createUpdateHandler(
updateStocks: data.updateStocks.map(mapFormsetStockToStockInput), updateStocks: data.updateStocks.map(mapFormsetStockToStockInput),
weight: weight(data.weight) weight: weight(data.weight)
}); });
errors = [
...result.data.productUpdate.errors,
...result.data.productVariantStocksCreate.errors,
...result.data.productVariantStocksDelete.errors,
...result.data.productVariantStocksUpdate.errors,
...result.data.productVariantUpdate.errors
];
} }
return errors;
}; };
} }

View file

@ -6,6 +6,11 @@ import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import {
useMetadataUpdate,
usePrivateMetadataUpdate
} from "@saleor/utils/metadata/updateMetadata";
import { useWarehouseList } from "@saleor/warehouses/queries"; import { useWarehouseList } from "@saleor/warehouses/queries";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -67,6 +72,8 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
id: variantId id: variantId
} }
}); });
const [updateMetadata] = useMetadataUpdate({});
const [updatePrivateMetadata] = usePrivateMetadataUpdate({});
const [openModal] = createDialogActionHandlers< const [openModal] = createDialogActionHandlers<
ProductVariantEditUrlDialog, ProductVariantEditUrlDialog,
@ -140,6 +147,39 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
} }
}; };
const handleUpdate = async (data: ProductVariantPageSubmitData) => {
const result = await updateVariant({
variables: {
addStocks: data.addStocks.map(mapFormsetStockToStockInput),
attributes: data.attributes.map(attribute => ({
id: attribute.id,
values: [attribute.value]
})),
costPrice: decimal(data.costPrice),
id: variantId,
price: decimal(data.price),
removeStocks: data.removeStocks,
sku: data.sku,
stocks: data.updateStocks.map(mapFormsetStockToStockInput),
trackInventory: data.trackInventory,
weight: weight(data.weight)
}
});
return [
...result.data?.productVariantStocksCreate.errors,
...result.data?.productVariantStocksDelete.errors,
...result.data?.productVariantStocksUpdate.errors,
...result.data?.productVariantUpdate.errors
];
};
const handleSubmit = createMetadataUpdateHandler(
data?.productVariant,
handleUpdate,
variables => updateMetadata({ variables }),
variables => updatePrivateMetadata({ variables })
);
return ( return (
<> <>
<WindowTitle title={data?.productVariant?.name} /> <WindowTitle title={data?.productVariant?.name} />
@ -158,25 +198,7 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
onBack={handleBack} onBack={handleBack}
onDelete={() => openModal("remove")} onDelete={() => openModal("remove")}
onImageSelect={handleImageSelect} onImageSelect={handleImageSelect}
onSubmit={(data: ProductVariantPageSubmitData) => onSubmit={handleSubmit}
updateVariant({
variables: {
addStocks: data.addStocks.map(mapFormsetStockToStockInput),
attributes: data.attributes.map(attribute => ({
id: attribute.id,
values: [attribute.value]
})),
costPrice: decimal(data.costPrice),
id: variantId,
price: decimal(data.price),
removeStocks: data.removeStocks,
sku: data.sku,
stocks: data.updateStocks.map(mapFormsetStockToStockInput),
trackInventory: data.trackInventory,
weight: weight(data.weight)
}
})
}
onVariantClick={variantId => { onVariantClick={variantId => {
navigate(productVariantEditUrl(productId, variantId)); navigate(productVariantEditUrl(productId, variantId));
}} }}

File diff suppressed because it is too large Load diff

View file

@ -506,6 +506,12 @@ export enum MenuSortField {
NAME = "NAME", NAME = "NAME",
} }
export enum MetadataErrorCode {
GRAPHQL_ERROR = "GRAPHQL_ERROR",
INVALID = "INVALID",
NOT_FOUND = "NOT_FOUND",
}
export enum OrderAction { export enum OrderAction {
CAPTURE = "CAPTURE", CAPTURE = "CAPTURE",
MARK_AS_PAID = "MARK_AS_PAID", MARK_AS_PAID = "MARK_AS_PAID",
@ -1186,6 +1192,11 @@ export interface MenuSortingInput {
field: MenuSortField; field: MenuSortField;
} }
export interface MetadataInput {
key: string;
value: string;
}
export interface NameTranslationInput { export interface NameTranslationInput {
name?: string | null; name?: string | null;
} }

View file

@ -0,0 +1,88 @@
import { MetadataFormData } from "@saleor/components/Metadata/types";
import { MetadataInput } from "@saleor/types/globalTypes";
import { diff } from "fast-array-diff";
import { MutationFetchResult } from "react-apollo";
import {
UpdateMetadata,
UpdateMetadataVariables
} from "../metadata/types/UpdateMetadata";
import {
UpdatePrivateMetadata,
UpdatePrivateMetadataVariables
} from "../metadata/types/UpdatePrivateMetadata";
interface ObjectWithMetadata {
id: string;
metadata: MetadataInput[];
privateMetadata: MetadataInput[];
}
function createMetadataUpdateHandler<TData extends MetadataFormData, TError>(
initial: ObjectWithMetadata,
update: (data: TData) => Promise<TError[]>,
updateMetadata: (
variables: UpdateMetadataVariables
) => Promise<MutationFetchResult<UpdateMetadata>>,
updatePrivateMetadata: (
variables: UpdatePrivateMetadataVariables
) => Promise<MutationFetchResult<UpdatePrivateMetadata>>
) {
return async (data: TData) => {
const errors = await update(data);
if (errors.length > 0) {
return errors;
}
if (errors.length === 0) {
if (data.metadata) {
const metaDiff = diff(
initial.metadata,
data.metadata,
(a, b) => a.key === b.key
);
const updateMetaResult = await updateMetadata({
id: initial.id,
input: data.metadata,
keysToDelete: metaDiff.removed.map(meta => meta.key)
});
const updateMetaErrors = [
...(updateMetaResult.data.deleteMetadata.errors || []),
...(updateMetaResult.data.updateMetadata.errors || [])
];
if (updateMetaErrors.length > 0) {
return updateMetaErrors;
}
}
if (data.privateMetadata) {
const privateMetaDiff = diff(
initial.privateMetadata,
data.privateMetadata,
(a, b) => a.key === b.key
);
const updatePrivateMetaResult = await updatePrivateMetadata({
id: initial.id,
input: data.privateMetadata,
keysToDelete: privateMetaDiff.removed.map(meta => meta.key)
});
const updatePrivateMetaErrors = [
...(updatePrivateMetaResult.data.deletePrivateMetadata.errors || []),
...(updatePrivateMetaResult.data.updatePrivateMetadata.errors || [])
];
if (updatePrivateMetaErrors.length > 0) {
return updatePrivateMetaErrors;
}
}
}
return [];
};
}
export default createMetadataUpdateHandler;

View file

@ -1,7 +1,9 @@
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo"; import { ShopInfo_shop_countries } from "@saleor/components/Shop/types/ShopInfo";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { MetadataItem } from "@saleor/fragments/types/MetadataItem";
import { Node } from "@saleor/types"; import { Node } from "@saleor/types";
import { MetadataInput } from "@saleor/types/globalTypes";
export function mapCountriesToChoices( export function mapCountriesToChoices(
countries: ShopInfo_shop_countries[] countries: ShopInfo_shop_countries[]
@ -20,3 +22,10 @@ export function mapNodeToChoice(
value: node.id value: node.id
})); }));
} }
export function mapMetadataItemToInput(item: MetadataItem): MetadataInput {
return {
key: item.key,
value: item.value
};
}

View file

@ -0,0 +1,62 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { MetadataInput, MetadataErrorCode } from "./../../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: UpdateMetadata
// ====================================================
export interface UpdateMetadata_updateMetadata_errors {
__typename: "MetadataError";
code: MetadataErrorCode;
field: string | null;
}
export interface UpdateMetadata_updateMetadata {
__typename: "UpdateMetadata";
errors: UpdateMetadata_updateMetadata_errors[];
}
export interface UpdateMetadata_deleteMetadata_errors {
__typename: "MetadataError";
code: MetadataErrorCode;
field: string | null;
}
export interface UpdateMetadata_deleteMetadata_item_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface UpdateMetadata_deleteMetadata_item_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface UpdateMetadata_deleteMetadata_item {
__typename: "ServiceAccount" | "App" | "Product" | "ProductType" | "Attribute" | "Category" | "ProductVariant" | "DigitalContent" | "Collection" | "User" | "Checkout" | "Order" | "Fulfillment" | "Invoice";
metadata: (UpdateMetadata_deleteMetadata_item_metadata | null)[];
privateMetadata: (UpdateMetadata_deleteMetadata_item_privateMetadata | null)[];
id: string;
}
export interface UpdateMetadata_deleteMetadata {
__typename: "DeleteMetadata";
errors: UpdateMetadata_deleteMetadata_errors[];
item: UpdateMetadata_deleteMetadata_item | null;
}
export interface UpdateMetadata {
updateMetadata: UpdateMetadata_updateMetadata | null;
deleteMetadata: UpdateMetadata_deleteMetadata | null;
}
export interface UpdateMetadataVariables {
id: string;
input: MetadataInput[];
keysToDelete: string[];
}

View file

@ -0,0 +1,62 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { MetadataInput, MetadataErrorCode } from "./../../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: UpdatePrivateMetadata
// ====================================================
export interface UpdatePrivateMetadata_updatePrivateMetadata_errors {
__typename: "MetadataError";
code: MetadataErrorCode;
field: string | null;
}
export interface UpdatePrivateMetadata_updatePrivateMetadata {
__typename: "UpdatePrivateMetadata";
errors: UpdatePrivateMetadata_updatePrivateMetadata_errors[];
}
export interface UpdatePrivateMetadata_deletePrivateMetadata_errors {
__typename: "MetadataError";
code: MetadataErrorCode;
field: string | null;
}
export interface UpdatePrivateMetadata_deletePrivateMetadata_item_metadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface UpdatePrivateMetadata_deletePrivateMetadata_item_privateMetadata {
__typename: "MetadataItem";
key: string;
value: string;
}
export interface UpdatePrivateMetadata_deletePrivateMetadata_item {
__typename: "ServiceAccount" | "App" | "Product" | "ProductType" | "Attribute" | "Category" | "ProductVariant" | "DigitalContent" | "Collection" | "User" | "Checkout" | "Order" | "Fulfillment" | "Invoice";
metadata: (UpdatePrivateMetadata_deletePrivateMetadata_item_metadata | null)[];
privateMetadata: (UpdatePrivateMetadata_deletePrivateMetadata_item_privateMetadata | null)[];
id: string;
}
export interface UpdatePrivateMetadata_deletePrivateMetadata {
__typename: "DeletePrivateMetadata";
errors: UpdatePrivateMetadata_deletePrivateMetadata_errors[];
item: UpdatePrivateMetadata_deletePrivateMetadata_item | null;
}
export interface UpdatePrivateMetadata {
updatePrivateMetadata: UpdatePrivateMetadata_updatePrivateMetadata | null;
deletePrivateMetadata: UpdatePrivateMetadata_deletePrivateMetadata | null;
}
export interface UpdatePrivateMetadataVariables {
id: string;
input: MetadataInput[];
keysToDelete: string[];
}

View file

@ -0,0 +1,75 @@
import { metadataErrorFragment } from "@saleor/fragments/errors";
import makeMutation from "@saleor/hooks/makeMutation";
import gql from "graphql-tag";
import { metadataFragment } from "../../fragments/metadata";
import {
UpdateMetadata,
UpdateMetadataVariables
} from "./types/UpdateMetadata";
import {
UpdatePrivateMetadata,
UpdatePrivateMetadataVariables
} from "./types/UpdatePrivateMetadata";
const updateMetadata = gql`
${metadataFragment}
${metadataErrorFragment}
mutation UpdateMetadata(
$id: ID!
$input: [MetadataInput!]!
$keysToDelete: [String!]!
) {
updateMetadata(id: $id, input: $input) {
errors: metadataErrors {
...MetadataErrorFragment
}
}
deleteMetadata(id: $id, keys: $keysToDelete) {
errors: metadataErrors {
...MetadataErrorFragment
}
item {
...MetadataFragment
... on Node {
id
}
}
}
}
`;
export const useMetadataUpdate = makeMutation<
UpdateMetadata,
UpdateMetadataVariables
>(updateMetadata);
const updatePrivateMetadata = gql`
${metadataFragment}
${metadataErrorFragment}
mutation UpdatePrivateMetadata(
$id: ID!
$input: [MetadataInput!]!
$keysToDelete: [String!]!
) {
updatePrivateMetadata(id: $id, input: $input) {
errors: metadataErrors {
...MetadataErrorFragment
}
}
deletePrivateMetadata(id: $id, keys: $keysToDelete) {
errors: metadataErrors {
...MetadataErrorFragment
}
item {
...MetadataFragment
... on Node {
id
}
}
}
}
`;
export const usePrivateMetadataUpdate = makeMutation<
UpdatePrivateMetadata,
UpdatePrivateMetadataVariables
>(updatePrivateMetadata);

View file

@ -0,0 +1,28 @@
import { FormChange } from "@saleor/hooks/useForm";
import { useState } from "react";
function useMetadataChangeTrigger() {
const [isMetadataModified, setMetadataModified] = useState(false);
const [isPrivateMetadataModified, setPrivateMetadataModified] = useState(
false
);
const makeChangeHandler: (
onChange: FormChange
) => FormChange = onChange => event => {
if (event.target.name === "metadata") {
setMetadataModified(true);
} else {
setPrivateMetadataModified(true);
}
onChange(event);
};
return {
isMetadataModified,
isPrivateMetadataModified,
makeChangeHandler
};
}
export default useMetadataChangeTrigger;

25
testUtils/wrapper.tsx Normal file
View file

@ -0,0 +1,25 @@
import { Provider as DateProvider } from "@saleor/components/Date/DateContext";
import { Locale, RawLocaleProvider } from "@saleor/components/Locale";
import ThemeProvider from "@saleor/components/Theme";
import { TimezoneProvider } from "@saleor/components/Timezone";
import React from "react";
import { IntlProvider } from "react-intl";
const Wrapper: React.FC = ({ children }) => (
<IntlProvider defaultLocale={Locale.EN} locale={Locale.EN}>
<RawLocaleProvider
value={{
locale: Locale.EN,
setLocale: () => undefined
}}
>
<DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
<TimezoneProvider value="America/New_York">
<ThemeProvider isDefaultDark={false}>{children}</ThemeProvider>
</TimezoneProvider>
</DateProvider>
</RawLocaleProvider>
</IntlProvider>
);
export default Wrapper;