1992 - Add attribute references select dialog (#931)

* Add attribute references select dialog

* Add reference attribute support to products and variants

* Fix attribute select dialog selection

* Refactor reference attribute handlers

* Refactor reference attribute handlers

* Refactor searching for reference pages
This commit is contained in:
Dawid Tarasiuk 2021-01-12 12:13:02 +01:00 committed by Jakub Majorek
parent 8b0fe986b2
commit 5303d9f714
38 changed files with 1008 additions and 167 deletions

View file

@ -1517,6 +1517,18 @@
"src_dot_components_dot_AssignAttributeDialog_dot_902296540": { "src_dot_components_dot_AssignAttributeDialog_dot_902296540": {
"string": "Search Attributes" "string": "Search Attributes"
}, },
"src_dot_components_dot_AssignAttributeValueDialog_dot_header": {
"context": "dialog header",
"string": "Assign Attribute Value"
},
"src_dot_components_dot_AssignAttributeValueDialog_dot_searchLabel": {
"context": "label",
"string": "Search Attribute Value"
},
"src_dot_components_dot_AssignAttributeValueDialog_dot_searchPlaceholder": {
"context": "placeholder",
"string": "Search by value name, etc..."
},
"src_dot_components_dot_AssignCategoryDialog_dot_3125506097": { "src_dot_components_dot_AssignCategoryDialog_dot_3125506097": {
"context": "dialog header", "context": "dialog header",
"string": "Assign Category" "string": "Assign Category"

View file

@ -1,9 +1,14 @@
import {
AttributeInput,
AttributeInputData
} from "@saleor/components/Attributes";
import { FileUpload } from "@saleor/files/types/FileUpload"; import { FileUpload } from "@saleor/files/types/FileUpload";
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
import { SelectedVariantAttributeFragment } from "@saleor/fragments/types/SelectedVariantAttributeFragment"; import { SelectedVariantAttributeFragment } from "@saleor/fragments/types/SelectedVariantAttributeFragment";
import { UploadErrorFragment } from "@saleor/fragments/types/UploadErrorFragment"; import { UploadErrorFragment } from "@saleor/fragments/types/UploadErrorFragment";
import { FormsetData } from "@saleor/hooks/useFormset"; import { FormsetData } from "@saleor/hooks/useFormset";
import { PageDetails_page_attributes } from "@saleor/pages/types/PageDetails"; import { PageDetails_page_attributes } from "@saleor/pages/types/PageDetails";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { import {
AttributeInputTypeEnum, AttributeInputTypeEnum,
AttributeValueInput AttributeValueInput
@ -99,6 +104,18 @@ export const mergeAttributeValueDeleteErrors = (
return errors; return errors;
}, []); }, []);
export const mergeAttributeValues = (
attributeId: string,
attributeValues: string[],
attributes: FormsetData<AttributeInputData, string[]>
) => {
const attribute = attributes.find(attribute => attribute.id === attributeId);
return attribute.value
? [...attribute.value, ...attributeValues]
: attributeValues;
};
export const getFileValuesToUploadFromAttributes = ( export const getFileValuesToUploadFromAttributes = (
attributesWithNewFileValue: FormsetData<null, File> attributesWithNewFileValue: FormsetData<null, File>
) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value); ) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value);
@ -149,3 +166,53 @@ export const getAttributesAfterFileAttributesUpdate = (
return uploadedFileAttributes.concat(removedFileAttributes); return uploadedFileAttributes.concat(removedFileAttributes);
}; };
export const getFileAttributeDisplayData = (
attribute: AttributeInput,
attributesWithNewFileValue: FormsetData<null, File>
) => {
const attributeWithNewFileValue = attributesWithNewFileValue.find(
attributeWithNewFile => attribute.id === attributeWithNewFile.id
);
if (attributeWithNewFileValue) {
return {
...attribute,
value: attributeWithNewFileValue?.value?.name
? [attributeWithNewFileValue.value.name]
: []
};
}
return attribute;
};
export const getReferenceAttributeDisplayData = (
attribute: AttributeInput,
referencePages: SearchPages_search_edges_node[]
) => ({
...attribute,
data: {
...attribute.data,
references:
referencePages &&
attribute.value?.map(value =>
referencePages.find(reference => reference.id === value)
)
}
});
export const getAttributesDisplayData = (
attributes: AttributeInput[],
attributesWithNewFileValue: FormsetData<null, File>,
referencePages: SearchPages_search_edges_node[]
) =>
attributes.map(attribute => {
if (attribute.data.inputType === AttributeInputTypeEnum.REFERENCE) {
return getReferenceAttributeDisplayData(attribute, referencePages);
}
if (attribute.data.inputType === AttributeInputTypeEnum.FILE) {
return getFileAttributeDisplayData(attribute, attributesWithNewFileValue);
}
return attribute;
});

View file

@ -0,0 +1,56 @@
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
import AssignContainerDialog, {
AssignContainerDialogProps
} from "../AssignContainerDialog";
const messages = defineMessages({
header: {
defaultMessage: "Assign Attribute Value",
description: "dialog header"
},
searchLabel: {
defaultMessage: "Search Attribute Value",
description: "label"
},
searchPlaceholder: {
defaultMessage: "Search by value name, etc...",
description: "placeholder"
}
});
interface AssignAttributeValueDialogProps
extends Omit<
AssignContainerDialogProps,
"containers" | "title" | "search" | "confirmButtonState"
> {
attributeValues: SearchPages_search_edges_node[];
}
const AssignAttributeValueDialog: React.FC<AssignAttributeValueDialogProps> = ({
attributeValues,
...rest
}) => {
const intl = useIntl();
return (
<AssignContainerDialog
containers={attributeValues.map(value => ({
id: value.id,
name: value.title
}))}
search={{
label: intl.formatMessage(messages.searchLabel),
placeholder: intl.formatMessage(messages.searchPlaceholder)
}}
title={intl.formatMessage(messages.header)}
confirmButtonState="default"
{...rest}
/>
);
};
AssignAttributeValueDialog.displayName = "AssignAttributeValueDialog";
export default AssignAttributeValueDialog;

View file

@ -0,0 +1,2 @@
export { default } from "./AssignAttributeValueDialog";
export * from "./AssignAttributeValueDialog";

View file

@ -13,8 +13,8 @@ const props: AttributesProps = {
onChange: () => undefined, onChange: () => undefined,
onFileChange: () => undefined, onFileChange: () => undefined,
onMultiChange: () => undefined, onMultiChange: () => undefined,
onReferencesChange: () => undefined, onReferencesAddClick: () => undefined,
onReferencesChangeClick: () => undefined, onReferencesRemove: () => undefined,
onReferencesReorder: () => undefined onReferencesReorder: () => undefined
}; };

View file

@ -16,6 +16,7 @@ import { AttributeValueFragment } from "@saleor/fragments/types/AttributeValueFr
import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErrorWithAttributesFragment"; import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErrorWithAttributesFragment";
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment"; import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset"; import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { ReorderAction } from "@saleor/types"; import { ReorderAction } from "@saleor/types";
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
import { getProductErrorMessage } from "@saleor/utils/errors"; import { getProductErrorMessage } from "@saleor/utils/errors";
@ -30,7 +31,9 @@ import {
} from "react-intl"; } from "react-intl";
import FileUploadField, { FileChoiceType } from "../FileUploadField"; import FileUploadField, { FileChoiceType } from "../FileUploadField";
import SortableChipsField from "../SortableChipsField"; import SortableChipsField, {
SortableChipsFieldValueType
} from "../SortableChipsField";
import BasicAttributeRow from "./BasicAttributeRow"; import BasicAttributeRow from "./BasicAttributeRow";
import ExtendedAttributeRow from "./ExtendedAttributeRow"; import ExtendedAttributeRow from "./ExtendedAttributeRow";
import { VariantAttributeScope } from "./types"; import { VariantAttributeScope } from "./types";
@ -41,6 +44,7 @@ export interface AttributeInputData {
isRequired: boolean; isRequired: boolean;
values: AttributeValueFragment[]; values: AttributeValueFragment[];
selectedValues?: AttributeValueFragment[]; selectedValues?: AttributeValueFragment[];
references?: SearchPages_search_edges_node[];
} }
export type AttributeInput = FormsetAtomicData<AttributeInputData, string[]>; export type AttributeInput = FormsetAtomicData<AttributeInputData, string[]>;
export type AttributeFileInput = FormsetAtomicData<AttributeInputData, File[]>; export type AttributeFileInput = FormsetAtomicData<AttributeInputData, File[]>;
@ -55,8 +59,8 @@ export interface AttributesProps {
onChange: FormsetChange; onChange: FormsetChange;
onMultiChange: FormsetChange; onMultiChange: FormsetChange;
onFileChange: FormsetChange; onFileChange: FormsetChange;
onReferencesChange?: FormsetChange; // TODO: temporairy optional, should be changed to required, after all pages implement it onReferencesRemove?: FormsetChange; // TODO: temporairy optional, should be changed to required, after all pages implement it
onReferencesChangeClick?: () => void; // TODO: temporairy optional, should be changed to required, after all pages implement it onReferencesAddClick?: (attribute: AttributeInput) => void; // TODO: temporairy optional, should be changed to required, after all pages implement it
onReferencesReorder?: ReorderAction; // TODO: temporairy optional, should be changed to required, after all pages implement it onReferencesReorder?: ReorderAction; // TODO: temporairy optional, should be changed to required, after all pages implement it
} }
@ -129,6 +133,10 @@ function getMultiChoices(
function getMultiDisplayValue( function getMultiDisplayValue(
attribute: AttributeInput attribute: AttributeInput
): MultiAutocompleteChoiceType[] { ): MultiAutocompleteChoiceType[] {
if (!attribute.value) {
return [];
}
return attribute.value.map(attributeValue => { return attribute.value.map(attributeValue => {
const definedAttributeValue = attribute.data.values.find( const definedAttributeValue = attribute.data.values.find(
definedValue => definedValue.slug === attributeValue definedValue => definedValue.slug === attributeValue
@ -147,6 +155,31 @@ function getMultiDisplayValue(
}); });
} }
function getReferenceDisplayValue(
attribute: AttributeInput
): SortableChipsFieldValueType[] {
if (!attribute.value) {
return [];
}
return attribute.value.map(attributeValue => {
const definedAttributeValue = attribute.data.references?.find(
reference => reference.id === attributeValue
);
if (!!definedAttributeValue) {
return {
label: definedAttributeValue.title,
value: definedAttributeValue.id
};
}
return {
label: attributeValue,
value: attributeValue
};
});
}
function getSingleChoices( function getSingleChoices(
values: AttributeValueFragment[] values: AttributeValueFragment[]
): SingleAutocompleteChoiceType[] { ): SingleAutocompleteChoiceType[] {
@ -157,7 +190,7 @@ function getSingleChoices(
} }
function getFileChoice(attribute: AttributeInput): FileChoiceType { function getFileChoice(attribute: AttributeInput): FileChoiceType {
const attributeValue = attribute.value[0]; const attributeValue = attribute.value?.length > 0 && attribute.value[0];
const definedAttributeValue = attribute.data.values.find( const definedAttributeValue = attribute.data.values.find(
definedValue => definedValue.slug === attributeValue definedValue => definedValue.slug === attributeValue
@ -217,8 +250,8 @@ const Attributes: React.FC<AttributesProps> = ({
onChange, onChange,
onMultiChange, onMultiChange,
onFileChange, onFileChange,
onReferencesChange, onReferencesRemove,
onReferencesChangeClick, onReferencesAddClick,
onReferencesReorder onReferencesReorder
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@ -272,13 +305,13 @@ const Attributes: React.FC<AttributesProps> = ({
defaultMessage: "Assign references", defaultMessage: "Assign references",
description: "button label" description: "button label"
})} })}
onSelect={onReferencesChangeClick} onSelect={() => onReferencesAddClick(attribute)}
disabled={disabled} disabled={disabled}
> >
<SortableChipsField <SortableChipsField
values={getMultiDisplayValue(attribute)} values={getReferenceDisplayValue(attribute)}
onValueDelete={value => onValueDelete={value =>
onReferencesChange( onReferencesRemove(
attribute.id, attribute.id,
attribute.value?.filter(id => id !== value) attribute.value?.filter(id => id !== value)
) )

View file

@ -88,6 +88,23 @@ const REFERENCE_ATTRIBUTE: AttributeInput = {
data: { data: {
inputType: AttributeInputTypeEnum.REFERENCE, inputType: AttributeInputTypeEnum.REFERENCE,
isRequired: true, isRequired: true,
references: [
{
__typename: "Page",
id: "vbnhgcvjhbvhj",
title: "References First Value"
},
{
__typename: "Page",
id: "gucngdfdfvdvd",
title: "References Second Value"
},
{
__typename: "Page",
id: "dfdfdsfdsfdse",
title: "References Third Value"
}
],
values: [ values: [
{ {
__typename: "AttributeValue", __typename: "AttributeValue",
@ -143,9 +160,9 @@ export const ATTRIBUTES_SELECTED: AttributeInput[] = [
{ {
...REFERENCE_ATTRIBUTE, ...REFERENCE_ATTRIBUTE,
value: [ value: [
REFERENCE_ATTRIBUTE.data.values[0].slug, REFERENCE_ATTRIBUTE.data.values[0].id,
REFERENCE_ATTRIBUTE.data.values[1].slug, REFERENCE_ATTRIBUTE.data.values[1].id,
REFERENCE_ATTRIBUTE.data.values[2].slug REFERENCE_ATTRIBUTE.data.values[2].id
] ]
} }
]; ];

View file

@ -20,9 +20,9 @@ const useStyles = makeStyles(
} }
); );
interface SortableChipsFieldValueType { export interface SortableChipsFieldValueType {
label: string; label: string;
value: any; value: string;
} }
export interface SortableChipsFieldProps extends SortableContainerProps { export interface SortableChipsFieldProps extends SortableContainerProps {

View file

@ -1,5 +1,7 @@
import { mergeAttributeValues } from "@saleor/attributes/utils/data";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import Attributes from "@saleor/components/Attributes"; import AssignAttributeValueDialog from "@saleor/components/AssignAttributeValueDialog";
import Attributes, { AttributeInput } from "@saleor/components/Attributes";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
@ -13,6 +15,8 @@ import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErr
import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useDateLocalize from "@saleor/hooks/useDateLocalize";
import { SubmitPromise } from "@saleor/hooks/useForm"; import { SubmitPromise } from "@saleor/hooks/useForm";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { getAttributeValuesFromReferences } from "@saleor/pages/utils/data";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { SearchPageTypes_search_edges_node } from "@saleor/searches/types/SearchPageTypes"; import { SearchPageTypes_search_edges_node } from "@saleor/searches/types/SearchPageTypes";
import { FetchMoreProps } from "@saleor/types"; import { FetchMoreProps } from "@saleor/types";
import React from "react"; import React from "react";
@ -21,13 +25,14 @@ import { useIntl } from "react-intl";
import { PageDetails_page } from "../../types/PageDetails"; import { PageDetails_page } from "../../types/PageDetails";
import PageInfo from "../PageInfo"; import PageInfo from "../PageInfo";
import PageOrganizeContent from "../PageOrganizeContent"; import PageOrganizeContent from "../PageOrganizeContent";
import PageForm, { PageData } from "./form"; import PageForm, { PageData, PageUpdateHandlers } from "./form";
export interface PageDetailsPageProps { export interface PageDetailsPageProps {
loading: boolean; loading: boolean;
errors: PageErrorWithAttributesFragment[]; errors: PageErrorWithAttributesFragment[];
page: PageDetails_page; page: PageDetails_page;
pageTypes?: SearchPageTypes_search_edges_node[]; pageTypes?: SearchPageTypes_search_edges_node[];
referencePages: SearchPages_search_edges_node[];
allowEmptySlug?: boolean; allowEmptySlug?: boolean;
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void; onBack: () => void;
@ -35,6 +40,11 @@ export interface PageDetailsPageProps {
onSubmit: (data: PageData) => SubmitPromise; onSubmit: (data: PageData) => SubmitPromise;
fetchPageTypes?: (data: string) => void; fetchPageTypes?: (data: string) => void;
fetchMorePageTypes?: FetchMoreProps; fetchMorePageTypes?: FetchMoreProps;
assignReferencesAttributeId?: string;
onAssignReferencesClick: (attribute: AttributeInput) => void;
fetchReferencePages?: (data: string) => void;
fetchMoreReferencePages?: FetchMoreProps;
onCloseDialog: () => void;
} }
const PageDetailsPage: React.FC<PageDetailsPageProps> = ({ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
@ -42,20 +52,49 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
errors, errors,
page, page,
pageTypes, pageTypes,
referencePages,
saveButtonBarState, saveButtonBarState,
onBack, onBack,
onRemove, onRemove,
onSubmit, onSubmit,
fetchPageTypes, fetchPageTypes,
fetchMorePageTypes fetchMorePageTypes,
assignReferencesAttributeId,
onAssignReferencesClick,
fetchReferencePages,
fetchMoreReferencePages,
onCloseDialog
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const localizeDate = useDateLocalize(); const localizeDate = useDateLocalize();
const pageExists = page !== null; const pageExists = page !== null;
const canOpenAssignReferencesAttributeDialog = !!assignReferencesAttributeId;
const handleAssignReferenceAttribute = (
attributeValues: string[],
data: PageData,
handlers: PageUpdateHandlers
) => {
handlers.selectAttributeReference(
assignReferencesAttributeId,
mergeAttributeValues(
assignReferencesAttributeId,
attributeValues,
data.attributes
)
);
onCloseDialog();
};
return ( return (
<PageForm page={page} pageTypes={pageTypes} onSubmit={onSubmit}> <PageForm
page={page}
pageTypes={pageTypes}
referencePages={referencePages}
onSubmit={onSubmit}
>
{({ change, data, pageType, handlers, hasChanged, submit }) => ( {({ change, data, pageType, handlers, hasChanged, submit }) => (
<Container> <Container>
<AppHeader onBack={onBack}> <AppHeader onBack={onBack}>
@ -107,6 +146,8 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
onChange={handlers.selectAttribute} onChange={handlers.selectAttribute}
onMultiChange={handlers.selectAttributeMulti} onMultiChange={handlers.selectAttributeMulti}
onFileChange={handlers.selectAttributeFile} onFileChange={handlers.selectAttributeFile}
onReferencesRemove={handlers.selectAttributeReference}
onReferencesAddClick={onAssignReferencesClick}
/> />
)} )}
<CardSpacer /> <CardSpacer />
@ -161,6 +202,24 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
onDelete={page === null ? undefined : onRemove} onDelete={page === null ? undefined : onRemove}
onSave={submit} onSave={submit}
/> />
{canOpenAssignReferencesAttributeDialog && (
<AssignAttributeValueDialog
attributeValues={getAttributeValuesFromReferences(
assignReferencesAttributeId,
data.attributes,
referencePages
)}
hasMore={fetchMoreReferencePages?.hasMore}
open={canOpenAssignReferencesAttributeDialog}
onFetch={fetchReferencePages}
onFetchMore={fetchMoreReferencePages?.onFetchMore}
loading={fetchMoreReferencePages?.loading}
onClose={onCloseDialog}
onSubmit={attributeValues =>
handleAssignReferenceAttribute(attributeValues, data, handlers)
}
/>
)}
</Container> </Container>
)} )}
</PageForm> </PageForm>

View file

@ -1,4 +1,5 @@
import { OutputData } from "@editorjs/editorjs"; import { OutputData } from "@editorjs/editorjs";
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
import { AttributeInput } from "@saleor/components/Attributes"; import { AttributeInput } from "@saleor/components/Attributes";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
import { RichTextEditorChange } from "@saleor/components/RichTextEditor"; import { RichTextEditorChange } from "@saleor/components/RichTextEditor";
@ -13,16 +14,15 @@ import {
PageDetails_page, PageDetails_page,
PageDetails_page_pageType PageDetails_page_pageType
} from "@saleor/pages/types/PageDetails"; } from "@saleor/pages/types/PageDetails";
import { import { getAttributeInputFromPage } from "@saleor/pages/utils/data";
getAttributeInputFromPage,
getAttributesDisplayData
} from "@saleor/pages/utils/data";
import { createPageTypeSelectHandler } from "@saleor/pages/utils/handlers"; import { createPageTypeSelectHandler } from "@saleor/pages/utils/handlers";
import { import {
createAttributeChangeHandler, createAttributeChangeHandler,
createAttributeFileChangeHandler, createAttributeFileChangeHandler,
createAttributeMultiChangeHandler createAttributeMultiChangeHandler,
createAttributeReferenceChangeHandler
} from "@saleor/products/utils/handlers"; } from "@saleor/products/utils/handlers";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import getPublicationData from "@saleor/utils/data/getPublicationData"; import getPublicationData from "@saleor/utils/data/getPublicationData";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
@ -51,12 +51,13 @@ export interface PageSubmitData extends PageFormData {
content: OutputData; content: OutputData;
} }
interface PageUpdateHandlers { export interface PageUpdateHandlers {
changeMetadata: FormChange; changeMetadata: FormChange;
changeContent: RichTextEditorChange; changeContent: RichTextEditorChange;
selectPageType: FormChange; selectPageType: FormChange;
selectAttribute: FormsetChange<string>; selectAttribute: FormsetChange<string>;
selectAttributeMulti: FormsetChange<string>; selectAttributeMulti: FormsetChange<string>;
selectAttributeReference: FormsetChange<string[]>;
selectAttributeFile: FormsetChange<File>; selectAttributeFile: FormsetChange<File>;
} }
export interface UsePageUpdateFormResult { export interface UsePageUpdateFormResult {
@ -72,12 +73,14 @@ export interface PageFormProps {
children: (props: UsePageUpdateFormResult) => React.ReactNode; children: (props: UsePageUpdateFormResult) => React.ReactNode;
page: PageDetails_page; page: PageDetails_page;
pageTypes?: PageDetails_page_pageType[]; pageTypes?: PageDetails_page_pageType[];
referencePages: SearchPages_search_edges_node[];
onSubmit: (data: PageData) => SubmitPromise; onSubmit: (data: PageData) => SubmitPromise;
} }
function usePageForm( function usePageForm(
page: PageDetails_page, page: PageDetails_page,
onSubmit: (data: PageData) => SubmitPromise, onSubmit: (data: PageData) => SubmitPromise,
referencePages: SearchPages_search_edges_node[],
pageTypes?: PageDetails_page_pageType[] pageTypes?: PageDetails_page_pageType[]
): UsePageUpdateFormResult { ): UsePageUpdateFormResult {
const [changed, setChanged] = React.useState(false); const [changed, setChanged] = React.useState(false);
@ -136,6 +139,10 @@ function usePageForm(
attributes.data, attributes.data,
triggerChange triggerChange
); );
const handleAttributeReferenceChange = createAttributeReferenceChangeHandler(
attributes.change,
triggerChange
);
const handleAttributeFileChange = createAttributeFileChangeHandler( const handleAttributeFileChange = createAttributeFileChangeHandler(
attributes.change, attributes.change,
attributesWithNewFileValue.data, attributesWithNewFileValue.data,
@ -149,7 +156,8 @@ function usePageForm(
...form.data, ...form.data,
attributes: getAttributesDisplayData( attributes: getAttributesDisplayData(
attributes.data, attributes.data,
attributesWithNewFileValue.data attributesWithNewFileValue.data,
referencePages
), ),
content: content.current content: content.current
}); });
@ -185,6 +193,7 @@ function usePageForm(
selectAttribute: handleAttributeChange, selectAttribute: handleAttributeChange,
selectAttributeFile: handleAttributeFileChange, selectAttributeFile: handleAttributeFileChange,
selectAttributeMulti: handleAttributeMultiChange, selectAttributeMulti: handleAttributeMultiChange,
selectAttributeReference: handleAttributeReferenceChange,
selectPageType selectPageType
}, },
hasChanged: changed, hasChanged: changed,
@ -196,10 +205,11 @@ function usePageForm(
const PageForm: React.FC<PageFormProps> = ({ const PageForm: React.FC<PageFormProps> = ({
children, children,
page, page,
referencePages,
pageTypes, pageTypes,
onSubmit onSubmit
}) => { }) => {
const props = usePageForm(page, onSubmit, pageTypes); const props = usePageForm(page, onSubmit, referencePages, pageTypes);
return <form onSubmit={props.submit}>{children(props)}</form>; return <form onSubmit={props.submit}>{children(props)}</form>;
}; };

View file

@ -14,7 +14,7 @@ import {
pagePath, pagePath,
PageUrlQueryParams PageUrlQueryParams
} from "./urls"; } from "./urls";
import PageCreate from "./views/PageCreate"; import PageCreateComponent from "./views/PageCreate";
import PageDetailsComponent from "./views/PageDetails"; import PageDetailsComponent from "./views/PageDetails";
import PageListComponent from "./views/PageList"; import PageListComponent from "./views/PageList";
@ -28,6 +28,18 @@ const PageList: React.FC<RouteComponentProps<any>> = ({ location }) => {
return <PageListComponent params={params} />; return <PageListComponent params={params} />;
}; };
const PageCreate: React.FC<RouteComponentProps<any>> = ({ match }) => {
const qs = parseQs(location.search.substr(1));
const params: PageUrlQueryParams = qs;
return (
<PageCreateComponent
id={decodeURIComponent(match.params.id)}
params={params}
/>
);
};
const PageDetails: React.FC<RouteComponentProps<any>> = ({ match }) => { const PageDetails: React.FC<RouteComponentProps<any>> = ({ match }) => {
const qs = parseQs(location.search.substr(1)); const qs = parseQs(location.search.substr(1));
const params: PageUrlQueryParams = qs; const params: PageUrlQueryParams = qs;

View file

@ -1,7 +1,7 @@
import { stringify as stringifyQs } from "qs"; import { stringify as stringifyQs } from "qs";
import urlJoin from "url-join"; import urlJoin from "url-join";
import { BulkAction, Dialog, Pagination, Sort } from "../types"; import { BulkAction, Dialog, Pagination, SingleAction, Sort } from "../types";
export const pagesSection = "/pages/"; export const pagesSection = "/pages/";
@ -21,10 +21,11 @@ export const pageListUrl = (params?: PageListUrlQueryParams) =>
pageListPath + "?" + stringifyQs(params); pageListPath + "?" + stringifyQs(params);
export const pagePath = (id: string) => urlJoin(pagesSection, id); export const pagePath = (id: string) => urlJoin(pagesSection, id);
export type PageUrlDialog = "remove"; export type PageUrlDialog = "remove" | "assign-attribute-value";
export type PageUrlQueryParams = Dialog<PageUrlDialog>; export type PageUrlQueryParams = Dialog<PageUrlDialog> & SingleAction;
export const pageUrl = (id: string, params?: PageUrlQueryParams) => export const pageUrl = (id: string, params?: PageUrlQueryParams) =>
pagePath(encodeURIComponent(id)) + "?" + stringifyQs(params); pagePath(encodeURIComponent(id)) + "?" + stringifyQs(params);
export const pageCreatePath = urlJoin(pagesSection, "add"); export const pageCreatePath = urlJoin(pagesSection, "add");
export const pageCreateUrl = pageCreatePath; export const pageCreateUrl = (params?: PageUrlQueryParams) =>
pageCreatePath + "?" + stringifyQs(params);

View file

@ -1,5 +1,5 @@
import { AttributeInput } from "@saleor/components/Attributes"; import { AttributeInput } from "@saleor/components/Attributes";
import { FormsetData } from "@saleor/hooks/useFormset"; import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { import {
PageDetails_page, PageDetails_page,
@ -37,22 +37,17 @@ export function getAttributeInputFromPageType(
})); }));
} }
export const getAttributesDisplayData = ( export const getAttributeValuesFromReferences = (
attributeId: string,
attributes: AttributeInput[], attributes: AttributeInput[],
attributesWithNewFileValue: FormsetData<null, File> referencePages: SearchPages_search_edges_node[]
) => ) => {
attributes.map(attribute => { const attribute = attributes?.find(attribute => attribute.id === attributeId);
const attributeWithNewFileValue = attributesWithNewFileValue.find(
attributeWithNewFile => attribute.id === attributeWithNewFile.id
);
if (attributeWithNewFileValue) { return (
return { referencePages?.filter(
...attribute, value =>
value: attributeWithNewFileValue?.value?.name !attribute?.value?.some(selectedValue => selectedValue === value.id)
? [attributeWithNewFileValue.value.name] ) || []
: [] );
}; };
}
return attribute;
});

View file

@ -3,11 +3,13 @@ import {
handleUploadMultipleFiles, handleUploadMultipleFiles,
prepareAttributesInput prepareAttributesInput
} from "@saleor/attributes/utils/handlers"; } from "@saleor/attributes/utils/handlers";
import { AttributeInput } from "@saleor/components/Attributes";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config"; import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import { useFileUploadMutation } from "@saleor/files/mutations"; import { useFileUploadMutation } from "@saleor/files/mutations";
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 usePageSearch from "@saleor/searches/usePageSearch";
import usePageTypeSearch from "@saleor/searches/usePageTypeSearch"; import usePageTypeSearch from "@saleor/searches/usePageTypeSearch";
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
import { import {
@ -21,13 +23,19 @@ import PageDetailsPage from "../components/PageDetailsPage";
import { PageSubmitData } from "../components/PageDetailsPage/form"; import { PageSubmitData } from "../components/PageDetailsPage/form";
import { TypedPageCreate } from "../mutations"; import { TypedPageCreate } from "../mutations";
import { PageCreate as PageCreateData } from "../types/PageCreate"; import { PageCreate as PageCreateData } from "../types/PageCreate";
import { pageListUrl, pageUrl } from "../urls"; import {
pageCreateUrl,
pageListUrl,
pageUrl,
PageUrlQueryParams
} from "../urls";
export interface PageCreateProps { export interface PageCreateProps {
id: string; id: string;
params: PageUrlQueryParams;
} }
export const PageCreate: React.FC<PageCreateProps> = () => { export const PageCreate: React.FC<PageCreateProps> = ({ params }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl(); const intl = useIntl();
@ -42,6 +50,14 @@ export const PageCreate: React.FC<PageCreateProps> = () => {
variables: DEFAULT_INITIAL_SEARCH_DATA variables: DEFAULT_INITIAL_SEARCH_DATA
}); });
const {
loadMore: loadMorePages,
search: searchPages,
result: searchPagesOpts
} = usePageSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const [uploadFile, uploadFileOpts] = useFileUploadMutation({}); const [uploadFile, uploadFileOpts] = useFileUploadMutation({});
const handlePageCreate = (data: PageCreateData) => { const handlePageCreate = (data: PageCreateData) => {
@ -56,6 +72,26 @@ export const PageCreate: React.FC<PageCreateProps> = () => {
} }
}; };
const handleAssignAttributeReferenceClick = (attribute: AttributeInput) =>
navigate(
pageCreateUrl({
action: "assign-attribute-value",
id: attribute.id
})
);
const fetchMorePageTypes = {
hasMore: searchPageTypesOpts.data?.search.pageInfo.hasNextPage,
loading: searchPageTypesOpts.loading,
onFetchMore: loadMorePageTypes
};
const fetchMoreReferencePages = {
hasMore: searchPagesOpts.data?.search.pageInfo.hasNextPage,
loading: searchPagesOpts.loading,
onFetchMore: loadMorePages
};
return ( return (
<TypedPageCreate onCompleted={handlePageCreate}> <TypedPageCreate onCompleted={handlePageCreate}>
{(pageCreate, pageCreateOpts) => { {(pageCreate, pageCreateOpts) => {
@ -119,11 +155,17 @@ export const PageCreate: React.FC<PageCreateProps> = () => {
onRemove={() => undefined} onRemove={() => undefined}
onSubmit={handleSubmit} onSubmit={handleSubmit}
fetchPageTypes={searchPageTypes} fetchPageTypes={searchPageTypes}
fetchMorePageTypes={{ fetchMorePageTypes={fetchMorePageTypes}
hasMore: searchPageTypesOpts.data?.search.pageInfo.hasNextPage, assignReferencesAttributeId={
loading: searchPageTypesOpts.loading, params.action === "assign-attribute-value" && params.id
onFetchMore: loadMorePageTypes }
}} onAssignReferencesClick={handleAssignAttributeReferenceClick}
referencePages={searchPagesOpts.data?.search.edges.map(
edge => edge.node
)}
fetchReferencePages={searchPages}
fetchMoreReferencePages={fetchMoreReferencePages}
onCloseDialog={() => navigate(pageCreateUrl())}
/> />
</> </>
); );

View file

@ -11,7 +11,9 @@ import {
prepareAttributesInput prepareAttributesInput
} from "@saleor/attributes/utils/handlers"; } from "@saleor/attributes/utils/handlers";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import { AttributeInput } from "@saleor/components/Attributes";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import { useFileUploadMutation } from "@saleor/files/mutations"; import { useFileUploadMutation } from "@saleor/files/mutations";
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment"; import { PageErrorFragment } from "@saleor/fragments/types/PageErrorFragment";
@ -19,6 +21,7 @@ import { UploadErrorFragment } from "@saleor/fragments/types/UploadErrorFragment
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 { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import usePageSearch from "@saleor/searches/usePageSearch";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import { import {
useMetadataUpdate, useMetadataUpdate,
@ -94,6 +97,14 @@ export const PageDetails: React.FC<PageDetailsProps> = ({ id, params }) => {
} }
}); });
const handleAssignAttributeReferenceClick = (attribute: AttributeInput) =>
navigate(
pageUrl(id, {
action: "assign-attribute-value",
id: attribute.id
})
);
const handleUpdate = async (data: PageSubmitData) => { const handleUpdate = async (data: PageSubmitData) => {
let errors: Array< let errors: Array<
AttributeErrorFragment | UploadErrorFragment | PageErrorFragment AttributeErrorFragment | UploadErrorFragment | PageErrorFragment
@ -139,6 +150,20 @@ export const PageDetails: React.FC<PageDetailsProps> = ({ id, params }) => {
variables => updatePrivateMetadata({ variables }) variables => updatePrivateMetadata({ variables })
); );
const {
loadMore: loadMorePages,
search: searchPages,
result: searchPagesOpts
} = usePageSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const fetchMoreReferencePages = {
hasMore: searchPagesOpts.data?.search.pageInfo.hasNextPage,
loading: searchPagesOpts.loading,
onFetchMore: loadMorePages
};
return ( return (
<> <>
<WindowTitle title={maybe(() => pageDetails.data.page.title)} /> <WindowTitle title={maybe(() => pageDetails.data.page.title)} />
@ -161,6 +186,16 @@ export const PageDetails: React.FC<PageDetailsProps> = ({ id, params }) => {
) )
} }
onSubmit={handleSubmit} onSubmit={handleSubmit}
assignReferencesAttributeId={
params.action === "assign-attribute-value" && params.id
}
onAssignReferencesClick={handleAssignAttributeReferenceClick}
referencePages={searchPagesOpts.data?.search.edges.map(
edge => edge.node
)}
fetchReferencePages={searchPages}
fetchMoreReferencePages={fetchMoreReferencePages}
onCloseDialog={() => navigate(pageUrl(id))}
/> />
<ActionDialog <ActionDialog
open={params.action === "remove"} open={params.action === "remove"}

View file

@ -116,7 +116,7 @@ export const PageList: React.FC<PageListProps> = ({ params }) => {
settings={settings} settings={settings}
pages={maybe(() => data.pages.edges.map(edge => edge.node))} pages={maybe(() => data.pages.edges.map(edge => edge.node))}
pageInfo={pageInfo} pageInfo={pageInfo}
onAdd={() => navigate(pageCreateUrl)} onAdd={() => navigate(pageCreateUrl())}
onBack={() => navigate(configurationMenuUrl)} onBack={() => navigate(configurationMenuUrl)}
onNextPage={loadNextPage} onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}

View file

@ -1,6 +1,8 @@
import { mergeAttributeValues } from "@saleor/attributes/utils/data";
import { ChannelData } from "@saleor/channels/utils"; import { ChannelData } from "@saleor/channels/utils";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import Attributes from "@saleor/components/Attributes"; import AssignAttributeValueDialog from "@saleor/components/AssignAttributeValueDialog";
import Attributes, { AttributeInput } from "@saleor/components/Attributes";
import AvailabilityCard from "@saleor/components/AvailabilityCard"; import AvailabilityCard from "@saleor/components/AvailabilityCard";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
@ -16,10 +18,12 @@ import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/Prod
import { TaxTypeFragment } from "@saleor/fragments/types/TaxTypeFragment"; import { TaxTypeFragment } from "@saleor/fragments/types/TaxTypeFragment";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { getAttributeValuesFromReferences } from "@saleor/pages/utils/data";
import ProductVariantPrice from "@saleor/products/components/ProductVariantPrice"; import ProductVariantPrice from "@saleor/products/components/ProductVariantPrice";
import { getChoices } from "@saleor/products/utils/data"; import { getChoices } from "@saleor/products/utils/data";
import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories"; import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories";
import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { SearchProductTypes_search_edges_node } from "@saleor/searches/types/SearchProductTypes"; import { SearchProductTypes_search_edges_node } from "@saleor/searches/types/SearchProductTypes";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import React from "react"; import React from "react";
@ -33,7 +37,8 @@ import ProductStocks from "../ProductStocks";
import ProductTaxes from "../ProductTaxes"; import ProductTaxes from "../ProductTaxes";
import ProductCreateForm, { import ProductCreateForm, {
ProductCreateData, ProductCreateData,
ProductCreateFormData ProductCreateFormData,
ProductCreateHandlers
} from "./form"; } from "./form";
interface ProductCreatePageProps { interface ProductCreatePageProps {
@ -49,6 +54,7 @@ interface ProductCreatePageProps {
fetchMoreProductTypes: FetchMoreProps; fetchMoreProductTypes: FetchMoreProps;
initial?: Partial<ProductCreateFormData>; initial?: Partial<ProductCreateFormData>;
productTypes?: SearchProductTypes_search_edges_node[]; productTypes?: SearchProductTypes_search_edges_node[];
referencePages: SearchPages_search_edges_node[];
header: string; header: string;
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
weightUnit: string; weightUnit: string;
@ -60,6 +66,11 @@ interface ProductCreatePageProps {
onWarehouseConfigure: () => void; onWarehouseConfigure: () => void;
openChannelsModal: () => void; openChannelsModal: () => void;
onChannelsChange: (data: ChannelData[]) => void; onChannelsChange: (data: ChannelData[]) => void;
assignReferencesAttributeId?: string;
onAssignReferencesClick: (attribute: AttributeInput) => void;
fetchReferencePages?: (data: string) => void;
fetchMoreReferencePages?: FetchMoreProps;
onCloseDialog: () => void;
onBack?(); onBack?();
onSubmit?(data: ProductCreateData); onSubmit?(data: ProductCreateData);
} }
@ -80,6 +91,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
header, header,
initial, initial,
productTypes: productTypeChoiceList, productTypes: productTypeChoiceList,
referencePages,
saveButtonBarState, saveButtonBarState,
warehouses, warehouses,
taxTypes, taxTypes,
@ -89,7 +101,12 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
onSubmit, onSubmit,
onChannelsChange, onChannelsChange,
onWarehouseConfigure, onWarehouseConfigure,
openChannelsModal openChannelsModal,
assignReferencesAttributeId,
onAssignReferencesClick,
fetchReferencePages,
fetchMoreReferencePages,
onCloseDialog
}: ProductCreatePageProps) => { }: ProductCreatePageProps) => {
const intl = useIntl(); const intl = useIntl();
@ -115,6 +132,24 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
value: taxType.taxCode value: taxType.taxCode
})) || []; })) || [];
const canOpenAssignReferencesAttributeDialog = !!assignReferencesAttributeId;
const handleAssignReferenceAttribute = (
attributeValues: string[],
data: ProductCreateData,
handlers: ProductCreateHandlers
) => {
handlers.selectAttributeReference(
assignReferencesAttributeId,
mergeAttributeValues(
assignReferencesAttributeId,
attributeValues,
data.attributes
)
);
onCloseDialog();
};
return ( return (
<ProductCreateForm <ProductCreateForm
onSubmit={onSubmit} onSubmit={onSubmit}
@ -122,6 +157,7 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
categories={categories} categories={categories}
collections={collections} collections={collections}
productTypes={productTypeChoiceList} productTypes={productTypeChoiceList}
referencePages={referencePages}
selectedCollections={selectedCollections} selectedCollections={selectedCollections}
setSelectedCategory={setSelectedCategory} setSelectedCategory={setSelectedCategory}
setSelectedCollections={setSelectedCollections} setSelectedCollections={setSelectedCollections}
@ -168,6 +204,8 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
onChange={handlers.selectAttribute} onChange={handlers.selectAttribute}
onMultiChange={handlers.selectAttributeMultiple} onMultiChange={handlers.selectAttributeMultiple}
onFileChange={handlers.selectAttributeFile} onFileChange={handlers.selectAttributeFile}
onReferencesRemove={handlers.selectAttributeReference}
onReferencesAddClick={onAssignReferencesClick}
/> />
)} )}
<CardSpacer /> <CardSpacer />
@ -283,6 +321,28 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
state={saveButtonBarState} state={saveButtonBarState}
disabled={loading || !onSubmit || formDisabled || !hasChanged} disabled={loading || !onSubmit || formDisabled || !hasChanged}
/> />
{canOpenAssignReferencesAttributeDialog && (
<AssignAttributeValueDialog
attributeValues={getAttributeValuesFromReferences(
assignReferencesAttributeId,
data.attributes,
referencePages
)}
hasMore={fetchMoreReferencePages?.hasMore}
open={canOpenAssignReferencesAttributeDialog}
onFetch={fetchReferencePages}
onFetchMore={fetchMoreReferencePages?.onFetchMore}
loading={fetchMoreReferencePages?.loading}
onClose={onCloseDialog}
onSubmit={attributeValues =>
handleAssignReferenceAttribute(
attributeValues,
data,
handlers
)
}
/>
)}
</Container> </Container>
); );
}} }}

View file

@ -1,4 +1,5 @@
import { OutputData } from "@editorjs/editorjs"; import { OutputData } from "@editorjs/editorjs";
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
import { ChannelData, ChannelPriceArgs } from "@saleor/channels/utils"; import { ChannelData, ChannelPriceArgs } from "@saleor/channels/utils";
import { import {
AttributeInput, AttributeInput,
@ -16,13 +17,13 @@ import useFormset, {
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { import {
getAttributeInputFromProductType, getAttributeInputFromProductType,
getAttributesDisplayData,
ProductType ProductType
} from "@saleor/products/utils/data"; } from "@saleor/products/utils/data";
import { import {
createAttributeChangeHandler, createAttributeChangeHandler,
createAttributeFileChangeHandler, createAttributeFileChangeHandler,
createAttributeMultiChangeHandler, createAttributeMultiChangeHandler,
createAttributeReferenceChangeHandler,
createChannelsChangeHandler, createChannelsChangeHandler,
createChannelsPriceChangeHandler, createChannelsPriceChangeHandler,
createProductTypeSelectHandler createProductTypeSelectHandler
@ -31,6 +32,7 @@ import {
validateCostPrice, validateCostPrice,
validatePrice validatePrice
} from "@saleor/products/utils/validation"; } from "@saleor/products/utils/validation";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { SearchProductTypes_search_edges_node } from "@saleor/searches/types/SearchProductTypes"; import { SearchProductTypes_search_edges_node } from "@saleor/searches/types/SearchProductTypes";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
@ -67,7 +69,7 @@ export interface ProductCreateData extends ProductCreateFormData {
stocks: ProductStockInput[]; stocks: ProductStockInput[];
} }
interface ProductCreateHandlers export interface ProductCreateHandlers
extends Record< extends Record<
| "changeMetadata" | "changeMetadata"
| "selectCategory" | "selectCategory"
@ -88,6 +90,7 @@ interface ProductCreateHandlers
data: Omit<ChannelData, "name" | "price" | "currency" | "id"> data: Omit<ChannelData, "name" | "price" | "currency" | "id">
) => void ) => void
>, >,
Record<"selectAttributeReference", FormsetChange<string[]>>,
Record<"selectAttributeFile", FormsetChange<File>>, Record<"selectAttributeFile", FormsetChange<File>>,
Record<"addStock" | "deleteStock", (id: string) => void> { Record<"addStock" | "deleteStock", (id: string) => void> {
changeDescription: RichTextEditorChange; changeDescription: RichTextEditorChange;
@ -117,6 +120,7 @@ export interface UseProductCreateFormOpts
warehouses: SearchWarehouses_search_edges_node[]; warehouses: SearchWarehouses_search_edges_node[];
currentChannels: ChannelData[]; currentChannels: ChannelData[];
productTypeChoiceList: SearchProductTypes_search_edges_node[]; productTypeChoiceList: SearchProductTypes_search_edges_node[];
referencePages: SearchPages_search_edges_node[];
} }
export interface ProductCreateFormProps extends UseProductCreateFormOpts { export interface ProductCreateFormProps extends UseProductCreateFormOpts {
@ -209,6 +213,10 @@ function useProductCreateForm(
attributes.data, attributes.data,
triggerChange triggerChange
); );
const handleAttributeReferenceChange = createAttributeReferenceChangeHandler(
attributes.change,
triggerChange
);
const handleAttributeFileChange = createAttributeFileChangeHandler( const handleAttributeFileChange = createAttributeFileChangeHandler(
attributes.change, attributes.change,
attributesWithNewFileValue.data, attributesWithNewFileValue.data,
@ -262,7 +270,8 @@ function useProductCreateForm(
...form.data, ...form.data,
attributes: getAttributesDisplayData( attributes: getAttributesDisplayData(
attributes.data, attributes.data,
attributesWithNewFileValue.data attributesWithNewFileValue.data,
opts.referencePages
), ),
attributesWithNewFileValue: attributesWithNewFileValue.data, attributesWithNewFileValue: attributesWithNewFileValue.data,
description: description.current, description: description.current,
@ -299,6 +308,7 @@ function useProductCreateForm(
selectAttribute: handleAttributeChange, selectAttribute: handleAttributeChange,
selectAttributeFile: handleAttributeFileChange, selectAttributeFile: handleAttributeFileChange,
selectAttributeMultiple: handleAttributeMultiChange, selectAttributeMultiple: handleAttributeMultiChange,
selectAttributeReference: handleAttributeReferenceChange,
selectCategory: handleCategorySelect, selectCategory: handleCategorySelect,
selectCollection: handleCollectionSelect, selectCollection: handleCollectionSelect,
selectProductType: handleProductTypeSelect, selectProductType: handleProductTypeSelect,

View file

@ -42,8 +42,10 @@ const props: ProductUpdatePageProps = {
hasChannelChanged: false, hasChannelChanged: false,
header: product.name, header: product.name,
images: product.images, images: product.images,
onAssignReferencesClick: () => undefined,
onBack: () => undefined, onBack: () => undefined,
onChannelsChange: () => undefined, onChannelsChange: () => undefined,
onCloseDialog: () => undefined,
onDelete: () => undefined, onDelete: () => undefined,
onImageDelete: () => undefined, onImageDelete: () => undefined,
onImageUpload: () => undefined, onImageUpload: () => undefined,
@ -57,6 +59,7 @@ const props: ProductUpdatePageProps = {
openChannelsModal: () => undefined, openChannelsModal: () => undefined,
placeholderImage, placeholderImage,
product, product,
referencePages: [],
saveButtonBarState: "default", saveButtonBarState: "default",
selectedChannelId: "123", selectedChannelId: "123",
taxTypes, taxTypes,

View file

@ -1,6 +1,8 @@
import { OutputData } from "@editorjs/editorjs"; import { OutputData } from "@editorjs/editorjs";
import { mergeAttributeValues } from "@saleor/attributes/utils/data";
import { ChannelData } from "@saleor/channels/utils"; import { ChannelData } from "@saleor/channels/utils";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import AssignAttributeValueDialog from "@saleor/components/AssignAttributeValueDialog";
import Attributes, { AttributeInput } from "@saleor/components/Attributes"; import Attributes, { AttributeInput } from "@saleor/components/Attributes";
import AvailabilityCard from "@saleor/components/AvailabilityCard"; import AvailabilityCard from "@saleor/components/AvailabilityCard";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
@ -21,9 +23,11 @@ import { FormsetData } from "@saleor/hooks/useFormset";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { getAttributeValuesFromReferences } from "@saleor/pages/utils/data";
import ProductVariantPrice from "@saleor/products/components/ProductVariantPrice"; import ProductVariantPrice from "@saleor/products/components/ProductVariantPrice";
import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories"; import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories";
import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { import {
ChannelProps, ChannelProps,
FetchMoreProps, FetchMoreProps,
@ -46,7 +50,10 @@ import ProductShipping from "../ProductShipping/ProductShipping";
import ProductStocks, { ProductStockInput } from "../ProductStocks"; import ProductStocks, { ProductStockInput } from "../ProductStocks";
import ProductTaxes from "../ProductTaxes"; import ProductTaxes from "../ProductTaxes";
import ProductVariants from "../ProductVariants"; import ProductVariants from "../ProductVariants";
import ProductUpdateForm from "./form"; import ProductUpdateForm, {
ProductUpdateData,
ProductUpdateHandlers
} from "./form";
export interface ProductUpdatePageProps extends ListActions, ChannelProps { export interface ProductUpdatePageProps extends ListActions, ChannelProps {
defaultWeightUnit: string; defaultWeightUnit: string;
@ -69,8 +76,14 @@ export interface ProductUpdatePageProps extends ListActions, ChannelProps {
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
warehouses: WarehouseFragment[]; warehouses: WarehouseFragment[];
taxTypes: TaxTypeFragment[]; taxTypes: TaxTypeFragment[];
referencePages: SearchPages_search_edges_node[];
assignReferencesAttributeId?: string;
fetchMoreReferencePages?: FetchMoreProps;
fetchCategories: (query: string) => void; fetchCategories: (query: string) => void;
fetchCollections: (query: string) => void; fetchCollections: (query: string) => void;
fetchReferencePages?: (data: string) => void;
onAssignReferencesClick: (attribute: AttributeInput) => void;
onCloseDialog: () => void;
onVariantsAdd: () => void; onVariantsAdd: () => void;
onVariantShow: (id: string) => () => void; onVariantShow: (id: string) => () => void;
onVariantReorder: ReorderAction; onVariantReorder: ReorderAction;
@ -123,6 +136,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
variants, variants,
warehouses, warehouses,
taxTypes, taxTypes,
referencePages,
onBack, onBack,
onDelete, onDelete,
onImageDelete, onImageDelete,
@ -144,7 +158,12 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
selectedChannelId, selectedChannelId,
toggle, toggle,
toggleAll, toggleAll,
toolbar toolbar,
assignReferencesAttributeId,
onAssignReferencesClick,
fetchReferencePages,
fetchMoreReferencePages,
onCloseDialog
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@ -169,6 +188,24 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
value: taxType.taxCode value: taxType.taxCode
})) || []; })) || [];
const canOpenAssignReferencesAttributeDialog = !!assignReferencesAttributeId;
const handleAssignReferenceAttribute = (
attributeValues: string[],
data: ProductUpdateData,
handlers: ProductUpdateHandlers
) => {
handlers.selectAttributeReference(
assignReferencesAttributeId,
mergeAttributeValues(
assignReferencesAttributeId,
attributeValues,
data.attributes
)
);
onCloseDialog();
};
return ( return (
<ProductUpdateForm <ProductUpdateForm
onSubmit={onSubmit} onSubmit={onSubmit}
@ -184,6 +221,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
warehouses={warehouses} warehouses={warehouses}
currentChannels={currentChannels} currentChannels={currentChannels}
hasVariants={hasVariants} hasVariants={hasVariants}
referencePages={referencePages}
> >
{({ {({
change, change,
@ -227,6 +265,8 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
onChange={handlers.selectAttribute} onChange={handlers.selectAttribute}
onMultiChange={handlers.selectAttributeMultiple} onMultiChange={handlers.selectAttributeMultiple}
onFileChange={handlers.selectAttributeFile} onFileChange={handlers.selectAttributeFile}
onReferencesRemove={handlers.selectAttributeReference}
onReferencesAddClick={onAssignReferencesClick}
/> />
)} )}
<CardSpacer /> <CardSpacer />
@ -362,6 +402,28 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
disabled || formDisabled || (!hasChanged && !hasChannelChanged) disabled || formDisabled || (!hasChanged && !hasChannelChanged)
} }
/> />
{canOpenAssignReferencesAttributeDialog && (
<AssignAttributeValueDialog
attributeValues={getAttributeValuesFromReferences(
assignReferencesAttributeId,
data.attributes,
referencePages
)}
hasMore={fetchMoreReferencePages?.hasMore}
open={canOpenAssignReferencesAttributeDialog}
onFetch={fetchReferencePages}
onFetchMore={fetchMoreReferencePages?.onFetchMore}
loading={fetchMoreReferencePages?.loading}
onClose={onCloseDialog}
onSubmit={attributeValues =>
handleAssignReferenceAttribute(
attributeValues,
data,
handlers
)
}
/>
)}
</Container> </Container>
</> </>
)} )}

View file

@ -1,4 +1,5 @@
import { OutputData } from "@editorjs/editorjs"; import { OutputData } from "@editorjs/editorjs";
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
import { ChannelData, ChannelPriceArgs } from "@saleor/channels/utils"; import { ChannelData, ChannelPriceArgs } from "@saleor/channels/utils";
import { AttributeInput } from "@saleor/components/Attributes"; import { AttributeInput } from "@saleor/components/Attributes";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
@ -14,7 +15,6 @@ import useFormset, {
import { ProductDetails_product } from "@saleor/products/types/ProductDetails"; import { ProductDetails_product } from "@saleor/products/types/ProductDetails";
import { import {
getAttributeInputFromProduct, getAttributeInputFromProduct,
getAttributesDisplayData,
getProductUpdatePageFormData, getProductUpdatePageFormData,
getStockInputFromProduct getStockInputFromProduct
} from "@saleor/products/utils/data"; } from "@saleor/products/utils/data";
@ -22,6 +22,7 @@ import {
createAttributeChangeHandler, createAttributeChangeHandler,
createAttributeFileChangeHandler, createAttributeFileChangeHandler,
createAttributeMultiChangeHandler, createAttributeMultiChangeHandler,
createAttributeReferenceChangeHandler,
createChannelsChangeHandler, createChannelsChangeHandler,
createChannelsPriceChangeHandler createChannelsPriceChangeHandler
} from "@saleor/products/utils/handlers"; } from "@saleor/products/utils/handlers";
@ -29,6 +30,7 @@ import {
validateCostPrice, validateCostPrice,
validatePrice validatePrice
} from "@saleor/products/utils/validation"; } from "@saleor/products/utils/validation";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
@ -85,7 +87,7 @@ export interface ProductUpdateSubmitData extends ProductUpdateFormData {
removeStocks: string[]; removeStocks: string[];
} }
interface ProductUpdateHandlers export interface ProductUpdateHandlers
extends Record< extends Record<
| "changeMetadata" | "changeMetadata"
| "selectCategory" | "selectCategory"
@ -105,6 +107,7 @@ interface ProductUpdateHandlers
data: Omit<ChannelData, "name" | "price" | "currency" | "id"> data: Omit<ChannelData, "name" | "price" | "currency" | "id">
) => void ) => void
>, >,
Record<"selectAttributeReference", FormsetChange<string[]>>,
Record<"selectAttributeFile", FormsetChange<File>>, Record<"selectAttributeFile", FormsetChange<File>>,
Record<"addStock" | "deleteStock", (id: string) => void> { Record<"addStock" | "deleteStock", (id: string) => void> {
changeDescription: RichTextEditorChange; changeDescription: RichTextEditorChange;
@ -134,6 +137,7 @@ export interface UseProductUpdateFormOpts
warehouses: SearchWarehouses_search_edges_node[]; warehouses: SearchWarehouses_search_edges_node[];
currentChannels: ChannelData[]; currentChannels: ChannelData[];
hasVariants: boolean; hasVariants: boolean;
referencePages: SearchPages_search_edges_node[];
} }
export interface ProductUpdateFormProps extends UseProductUpdateFormOpts { export interface ProductUpdateFormProps extends UseProductUpdateFormOpts {
@ -219,6 +223,10 @@ function useProductUpdateForm(
attributes.data, attributes.data,
triggerChange triggerChange
); );
const handleAttributeReferenceChange = createAttributeReferenceChangeHandler(
attributes.change,
triggerChange
);
const handleAttributeFileChange = createAttributeFileChangeHandler( const handleAttributeFileChange = createAttributeFileChangeHandler(
attributes.change, attributes.change,
attributesWithNewFileValue.data, attributesWithNewFileValue.data,
@ -264,7 +272,8 @@ function useProductUpdateForm(
...form.data, ...form.data,
attributes: getAttributesDisplayData( attributes: getAttributesDisplayData(
attributes.data, attributes.data,
attributesWithNewFileValue.data attributesWithNewFileValue.data,
opts.referencePages
), ),
description: description.current, description: description.current,
stocks: stocks.data stocks: stocks.data
@ -316,6 +325,7 @@ function useProductUpdateForm(
selectAttribute: handleAttributeChange, selectAttribute: handleAttributeChange,
selectAttributeFile: handleAttributeFileChange, selectAttributeFile: handleAttributeFileChange,
selectAttributeMultiple: handleAttributeMultiChange, selectAttributeMultiple: handleAttributeMultiChange,
selectAttributeReference: handleAttributeReferenceChange,
selectCategory: handleCategorySelect, selectCategory: handleCategorySelect,
selectCollection: handleCollectionSelect, selectCollection: handleCollectionSelect,
selectTaxRate: handleTaxTypeSelect selectTaxRate: handleTaxTypeSelect

View file

@ -1,6 +1,9 @@
import { mergeAttributeValues } from "@saleor/attributes/utils/data";
import { ChannelPriceData } from "@saleor/channels/utils"; import { ChannelPriceData } from "@saleor/channels/utils";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import AssignAttributeValueDialog from "@saleor/components/AssignAttributeValueDialog";
import Attributes, { import Attributes, {
AttributeInput,
VariantAttributeScope VariantAttributeScope
} from "@saleor/components/Attributes"; } from "@saleor/components/Attributes";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
@ -12,8 +15,10 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { ProductChannelListingErrorFragment } from "@saleor/fragments/types/ProductChannelListingErrorFragment"; import { ProductChannelListingErrorFragment } from "@saleor/fragments/types/ProductChannelListingErrorFragment";
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment"; import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
import { getAttributeValuesFromReferences } from "@saleor/pages/utils/data";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { ReorderAction } from "@saleor/types"; import { FetchMoreProps, ReorderAction } from "@saleor/types";
import React from "react"; import React from "react";
import { defineMessages, useIntl } from "react-intl"; import { defineMessages, useIntl } from "react-intl";
@ -22,7 +27,10 @@ import ProductShipping from "../ProductShipping/ProductShipping";
import ProductStocks from "../ProductStocks"; import ProductStocks from "../ProductStocks";
import ProductVariantNavigation from "../ProductVariantNavigation"; import ProductVariantNavigation from "../ProductVariantNavigation";
import ProductVariantPrice from "../ProductVariantPrice"; import ProductVariantPrice from "../ProductVariantPrice";
import ProductVariantCreateForm, { ProductVariantCreateData } from "./form"; import ProductVariantCreateForm, {
ProductVariantCreateData,
ProductVariantCreateHandlers
} from "./form";
const messages = defineMessages({ const messages = defineMessages({
attributesHeader: { attributesHeader: {
@ -53,11 +61,17 @@ interface ProductVariantCreatePageProps {
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
warehouses: SearchWarehouses_search_edges_node[]; warehouses: SearchWarehouses_search_edges_node[];
weightUnit: string; weightUnit: string;
referencePages: SearchPages_search_edges_node[];
onBack: () => void; onBack: () => void;
onSubmit: (data: ProductVariantCreateData) => void; onSubmit: (data: ProductVariantCreateData) => void;
onVariantClick: (variantId: string) => void; onVariantClick: (variantId: string) => void;
onVariantReorder: ReorderAction; onVariantReorder: ReorderAction;
onWarehouseConfigure: () => void; onWarehouseConfigure: () => void;
assignReferencesAttributeId?: string;
onAssignReferencesClick: (attribute: AttributeInput) => void;
fetchReferencePages?: (data: string) => void;
fetchMoreReferencePages?: FetchMoreProps;
onCloseDialog: () => void;
} }
const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
@ -70,20 +84,45 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
saveButtonBarState, saveButtonBarState,
warehouses, warehouses,
weightUnit, weightUnit,
referencePages,
onBack, onBack,
onSubmit, onSubmit,
onVariantClick, onVariantClick,
onVariantReorder, onVariantReorder,
onWarehouseConfigure onWarehouseConfigure,
assignReferencesAttributeId,
onAssignReferencesClick,
fetchReferencePages,
fetchMoreReferencePages,
onCloseDialog
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const canOpenAssignReferencesAttributeDialog = !!assignReferencesAttributeId;
const handleAssignReferenceAttribute = (
attributeValues: string[],
data: ProductVariantCreateData,
handlers: ProductVariantCreateHandlers
) => {
handlers.selectAttributeReference(
assignReferencesAttributeId,
mergeAttributeValues(
assignReferencesAttributeId,
attributeValues,
data.attributes
)
);
onCloseDialog();
};
return ( return (
<ProductVariantCreateForm <ProductVariantCreateForm
product={product} product={product}
onSubmit={onSubmit} onSubmit={onSubmit}
warehouses={warehouses} warehouses={warehouses}
currentChannels={channels} currentChannels={channels}
referencePages={referencePages}
> >
{({ {({
change, change,
@ -138,6 +177,8 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
onChange={handlers.selectAttribute} onChange={handlers.selectAttribute}
onMultiChange={handlers.selectAttributeMultiple} onMultiChange={handlers.selectAttributeMultiple}
onFileChange={handlers.selectAttributeFile} onFileChange={handlers.selectAttributeFile}
onReferencesRemove={handlers.selectAttributeReference}
onReferencesAddClick={onAssignReferencesClick}
/> />
<CardSpacer /> <CardSpacer />
<ProductShipping <ProductShipping
@ -187,6 +228,24 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
onCancel={onBack} onCancel={onBack}
onSave={submit} onSave={submit}
/> />
{canOpenAssignReferencesAttributeDialog && (
<AssignAttributeValueDialog
attributeValues={getAttributeValuesFromReferences(
assignReferencesAttributeId,
data.attributes,
referencePages
)}
hasMore={fetchMoreReferencePages?.hasMore}
open={canOpenAssignReferencesAttributeDialog}
onFetch={fetchReferencePages}
onFetchMore={fetchMoreReferencePages?.onFetchMore}
loading={fetchMoreReferencePages?.loading}
onClose={onCloseDialog}
onSubmit={attributeValues =>
handleAssignReferenceAttribute(attributeValues, data, handlers)
}
/>
)}
</Container> </Container>
)} )}
</ProductVariantCreateForm> </ProductVariantCreateForm>

View file

@ -1,3 +1,4 @@
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
import { ChannelPriceData, IChannelPriceArgs } from "@saleor/channels/utils"; import { ChannelPriceData, IChannelPriceArgs } from "@saleor/channels/utils";
import { AttributeInput } from "@saleor/components/Attributes"; import { AttributeInput } from "@saleor/components/Attributes";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
@ -7,12 +8,10 @@ import useFormset, {
FormsetData FormsetData
} from "@saleor/hooks/useFormset"; } from "@saleor/hooks/useFormset";
import { ProductVariantCreateData_product } from "@saleor/products/types/ProductVariantCreateData"; import { ProductVariantCreateData_product } from "@saleor/products/types/ProductVariantCreateData";
import { import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data";
getAttributesDisplayData,
getVariantAttributeInputFromProduct
} from "@saleor/products/utils/data";
import { import {
createAttributeFileChangeHandler, createAttributeFileChangeHandler,
createAttributeReferenceChangeHandler,
getChannelsInput getChannelsInput
} from "@saleor/products/utils/handlers"; } from "@saleor/products/utils/handlers";
import { import {
@ -23,6 +22,7 @@ import {
validateCostPrice, validateCostPrice,
validatePrice validatePrice
} from "@saleor/products/utils/validation"; } from "@saleor/products/utils/validation";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger";
import React from "react"; import React from "react";
@ -44,9 +44,10 @@ export interface ProductVariantCreateData extends ProductVariantCreateFormData {
export interface UseProductVariantCreateFormOpts { export interface UseProductVariantCreateFormOpts {
warehouses: SearchWarehouses_search_edges_node[]; warehouses: SearchWarehouses_search_edges_node[];
currentChannels: ChannelPriceData[]; currentChannels: ChannelPriceData[];
referencePages: SearchPages_search_edges_node[];
} }
interface ProductVariantCreateHandlers export interface ProductVariantCreateHandlers
extends Record< extends Record<
| "changeStock" | "changeStock"
| "selectAttribute" | "selectAttribute"
@ -54,6 +55,7 @@ interface ProductVariantCreateHandlers
| "changeChannels", | "changeChannels",
FormsetChange FormsetChange
>, >,
Record<"selectAttributeReference", FormsetChange<string[]>>,
Record<"selectAttributeFile", FormsetChange<File>>, Record<"selectAttributeFile", FormsetChange<File>>,
Record<"addStock" | "deleteStock", (id: string) => void> { Record<"addStock" | "deleteStock", (id: string) => void> {
changeMetadata: FormChange; changeMetadata: FormChange;
@ -118,6 +120,10 @@ function useProductVariantCreateForm(
attributes.data, attributes.data,
triggerChange triggerChange
); );
const handleAttributeReferenceChange = createAttributeReferenceChangeHandler(
attributes.change,
triggerChange
);
const handleAttributeFileChange = createAttributeFileChangeHandler( const handleAttributeFileChange = createAttributeFileChangeHandler(
attributes.change, attributes.change,
attributesWithNewFileValue.data, attributesWithNewFileValue.data,
@ -157,7 +163,8 @@ function useProductVariantCreateForm(
...form.data, ...form.data,
attributes: getAttributesDisplayData( attributes: getAttributesDisplayData(
attributes.data, attributes.data,
attributesWithNewFileValue.data attributesWithNewFileValue.data,
opts.referencePages
), ),
attributesWithNewFileValue: attributesWithNewFileValue.data, attributesWithNewFileValue: attributesWithNewFileValue.data,
channelListings: channels.data, channelListings: channels.data,
@ -178,7 +185,8 @@ function useProductVariantCreateForm(
deleteStock: handleStockDelete, deleteStock: handleStockDelete,
selectAttribute: handleAttributeChange, selectAttribute: handleAttributeChange,
selectAttributeFile: handleAttributeFileChange, selectAttributeFile: handleAttributeFileChange,
selectAttributeMultiple: handleAttributeMultiChange selectAttributeMultiple: handleAttributeMultiChange,
selectAttributeReference: handleAttributeReferenceChange
}, },
hasChanged: changed, hasChanged: changed,
submit submit

View file

@ -1,5 +1,7 @@
import { mergeAttributeValues } from "@saleor/attributes/utils/data";
import { ChannelPriceData } from "@saleor/channels/utils"; import { ChannelPriceData } from "@saleor/channels/utils";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import AssignAttributeValueDialog from "@saleor/components/AssignAttributeValueDialog";
import Attributes, { import Attributes, {
AttributeInput, AttributeInput,
VariantAttributeScope VariantAttributeScope
@ -16,8 +18,10 @@ import { ProductChannelListingErrorFragment } from "@saleor/fragments/types/Prod
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment"; import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
import { ProductVariant } from "@saleor/fragments/types/ProductVariant"; import { ProductVariant } from "@saleor/fragments/types/ProductVariant";
import { WarehouseFragment } from "@saleor/fragments/types/WarehouseFragment"; import { WarehouseFragment } from "@saleor/fragments/types/WarehouseFragment";
import { getAttributeValuesFromReferences } from "@saleor/pages/utils/data";
import { VariantUpdate_productVariantUpdate_errors } from "@saleor/products/types/VariantUpdate"; import { VariantUpdate_productVariantUpdate_errors } from "@saleor/products/types/VariantUpdate";
import { ReorderAction } from "@saleor/types"; import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { FetchMoreProps, ReorderAction } from "@saleor/types";
import React from "react"; import React from "react";
import { defineMessages, useIntl } from "react-intl"; import { defineMessages, useIntl } from "react-intl";
@ -30,6 +34,8 @@ import ProductVariantNavigation from "../ProductVariantNavigation";
import ProductVariantPrice from "../ProductVariantPrice"; import ProductVariantPrice from "../ProductVariantPrice";
import ProductVariantSetDefault from "../ProductVariantSetDefault"; import ProductVariantSetDefault from "../ProductVariantSetDefault";
import ProductVariantUpdateForm, { import ProductVariantUpdateForm, {
ProductVariantUpdateData,
ProductVariantUpdateHandlers,
ProductVariantUpdateSubmitData ProductVariantUpdateSubmitData
} from "./form"; } from "./form";
@ -61,6 +67,7 @@ export interface ProductVariantPageSubmitData
} }
interface ProductVariantPageProps { interface ProductVariantPageProps {
assignReferencesAttributeId?: string;
defaultVariantId?: string; defaultVariantId?: string;
defaultWeightUnit: string; defaultWeightUnit: string;
errors: errors:
@ -74,6 +81,11 @@ interface ProductVariantPageProps {
saveButtonBarState: ConfirmButtonTransitionState; saveButtonBarState: ConfirmButtonTransitionState;
variant?: ProductVariant; variant?: ProductVariant;
warehouses: WarehouseFragment[]; warehouses: WarehouseFragment[];
referencePages: SearchPages_search_edges_node[];
fetchMoreReferencePages?: FetchMoreProps;
fetchReferencePages?: (data: string) => void;
onAssignReferencesClick: (attribute: AttributeInput) => void;
onCloseDialog: () => void;
onVariantReorder: ReorderAction; onVariantReorder: ReorderAction;
onAdd(); onAdd();
onBack(); onBack();
@ -97,6 +109,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
saveButtonBarState, saveButtonBarState,
variant, variant,
warehouses, warehouses,
referencePages,
onAdd, onAdd,
onBack, onBack,
onDelete, onDelete,
@ -105,7 +118,12 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
onVariantClick, onVariantClick,
onVariantReorder, onVariantReorder,
onSetDefaultVariant, onSetDefaultVariant,
onWarehouseConfigure onWarehouseConfigure,
assignReferencesAttributeId,
onAssignReferencesClick,
fetchReferencePages,
fetchMoreReferencePages,
onCloseDialog
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
@ -120,6 +138,24 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
?.filter(image => variantImages.indexOf(image.id) !== -1) ?.filter(image => variantImages.indexOf(image.id) !== -1)
.sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1)); .sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1));
const canOpenAssignReferencesAttributeDialog = !!assignReferencesAttributeId;
const handleAssignReferenceAttribute = (
attributeValues: string[],
data: ProductVariantUpdateData,
handlers: ProductVariantUpdateHandlers
) => {
handlers.selectAttributeReference(
assignReferencesAttributeId,
mergeAttributeValues(
assignReferencesAttributeId,
attributeValues,
data.attributes
)
);
onCloseDialog();
};
return ( return (
<> <>
<Container> <Container>
@ -136,6 +172,7 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
onSubmit={onSubmit} onSubmit={onSubmit}
warehouses={warehouses} warehouses={warehouses}
currentChannels={channels} currentChannels={channels}
referencePages={referencePages}
> >
{({ {({
change, change,
@ -178,6 +215,8 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
onChange={handlers.selectAttribute} onChange={handlers.selectAttribute}
onMultiChange={handlers.selectAttributeMultiple} onMultiChange={handlers.selectAttributeMultiple}
onFileChange={handlers.selectAttributeFile} onFileChange={handlers.selectAttributeFile}
onReferencesRemove={handlers.selectAttributeReference}
onReferencesAddClick={onAssignReferencesClick}
/> />
<CardSpacer /> <CardSpacer />
<Attributes <Attributes
@ -195,6 +234,8 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
onChange={handlers.selectAttribute} onChange={handlers.selectAttribute}
onMultiChange={handlers.selectAttributeMultiple} onMultiChange={handlers.selectAttributeMultiple}
onFileChange={handlers.selectAttributeFile} onFileChange={handlers.selectAttributeFile}
onReferencesRemove={handlers.selectAttributeReference}
onReferencesAddClick={onAssignReferencesClick}
/> />
<CardSpacer /> <CardSpacer />
<ProductVariantImages <ProductVariantImages
@ -248,6 +289,28 @@ const ProductVariantPage: React.FC<ProductVariantPageProps> = ({
onDelete={onDelete} onDelete={onDelete}
onSave={submit} onSave={submit}
/> />
{canOpenAssignReferencesAttributeDialog && (
<AssignAttributeValueDialog
attributeValues={getAttributeValuesFromReferences(
assignReferencesAttributeId,
data.attributes,
referencePages
)}
hasMore={fetchMoreReferencePages?.hasMore}
open={canOpenAssignReferencesAttributeDialog}
onFetch={fetchReferencePages}
onFetchMore={fetchMoreReferencePages?.onFetchMore}
loading={fetchMoreReferencePages?.loading}
onClose={onCloseDialog}
onSubmit={attributeValues =>
handleAssignReferenceAttribute(
attributeValues,
data,
handlers
)
}
/>
)}
</> </>
)} )}
</ProductVariantUpdateForm> </ProductVariantUpdateForm>

View file

@ -1,3 +1,4 @@
import { getAttributesDisplayData } from "@saleor/attributes/utils/data";
import { ChannelPriceData, IChannelPriceArgs } from "@saleor/channels/utils"; import { ChannelPriceData, IChannelPriceArgs } from "@saleor/channels/utils";
import { AttributeInput } from "@saleor/components/Attributes"; import { AttributeInput } from "@saleor/components/Attributes";
import { MetadataFormData } from "@saleor/components/Metadata"; import { MetadataFormData } from "@saleor/components/Metadata";
@ -9,11 +10,11 @@ import useFormset, {
} from "@saleor/hooks/useFormset"; } from "@saleor/hooks/useFormset";
import { import {
getAttributeInputFromVariant, getAttributeInputFromVariant,
getAttributesDisplayData,
getStockInputFromVariant getStockInputFromVariant
} from "@saleor/products/utils/data"; } from "@saleor/products/utils/data";
import { import {
createAttributeFileChangeHandler, createAttributeFileChangeHandler,
createAttributeReferenceChangeHandler,
getChannelsInput getChannelsInput
} from "@saleor/products/utils/handlers"; } from "@saleor/products/utils/handlers";
import { import {
@ -24,6 +25,7 @@ import {
validateCostPrice, validateCostPrice,
validatePrice validatePrice
} from "@saleor/products/utils/validation"; } from "@saleor/products/utils/validation";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { mapMetadataItemToInput } from "@saleor/utils/maps"; import { mapMetadataItemToInput } from "@saleor/utils/maps";
import getMetadata from "@saleor/utils/metadata/getMetadata"; import getMetadata from "@saleor/utils/metadata/getMetadata";
@ -57,9 +59,10 @@ export interface ProductVariantUpdateSubmitData
export interface UseProductVariantUpdateFormOpts { export interface UseProductVariantUpdateFormOpts {
warehouses: SearchWarehouses_search_edges_node[]; warehouses: SearchWarehouses_search_edges_node[];
currentChannels: ChannelPriceData[]; currentChannels: ChannelPriceData[];
referencePages: SearchPages_search_edges_node[];
} }
interface ProductVariantUpdateHandlers export interface ProductVariantUpdateHandlers
extends Record< extends Record<
| "changeStock" | "changeStock"
| "selectAttribute" | "selectAttribute"
@ -67,6 +70,7 @@ interface ProductVariantUpdateHandlers
| "changeChannels", | "changeChannels",
FormsetChange FormsetChange
>, >,
Record<"selectAttributeReference", FormsetChange<string[]>>,
Record<"selectAttributeFile", FormsetChange<File>>, Record<"selectAttributeFile", FormsetChange<File>>,
Record<"addStock" | "deleteStock", (id: string) => void> { Record<"addStock" | "deleteStock", (id: string) => void> {
changeMetadata: FormChange; changeMetadata: FormChange;
@ -133,6 +137,10 @@ function useProductVariantUpdateForm(
attributes.data, attributes.data,
triggerChange triggerChange
); );
const handleAttributeReferenceChange = createAttributeReferenceChangeHandler(
attributes.change,
triggerChange
);
const handleAttributeFileChange = createAttributeFileChangeHandler( const handleAttributeFileChange = createAttributeFileChangeHandler(
attributes.change, attributes.change,
attributesWithNewFileValue.data, attributesWithNewFileValue.data,
@ -186,7 +194,8 @@ function useProductVariantUpdateForm(
...form.data, ...form.data,
attributes: getAttributesDisplayData( attributes: getAttributesDisplayData(
attributes.data, attributes.data,
attributesWithNewFileValue.data attributesWithNewFileValue.data,
opts.referencePages
), ),
channelListings: channels.data, channelListings: channels.data,
stocks: stocks.data stocks: stocks.data
@ -226,7 +235,8 @@ function useProductVariantUpdateForm(
deleteStock: handleStockDelete, deleteStock: handleStockDelete,
selectAttribute: handleAttributeChange, selectAttribute: handleAttributeChange,
selectAttributeFile: handleAttributeFileChange, selectAttributeFile: handleAttributeFileChange,
selectAttributeMultiple: handleAttributeMultiChange selectAttributeMultiple: handleAttributeMultiChange,
selectAttributeReference: handleAttributeReferenceChange
}, },
hasChanged: changed, hasChanged: changed,
submit submit

View file

@ -18,6 +18,7 @@ import {
productPath, productPath,
ProductUrlQueryParams, ProductUrlQueryParams,
productVariantAddPath, productVariantAddPath,
ProductVariantAddUrlQueryParams,
productVariantCreatorPath, productVariantCreatorPath,
productVariantEditPath, productVariantEditPath,
ProductVariantEditUrlQueryParams ProductVariantEditUrlQueryParams
@ -99,11 +100,17 @@ const ProductImage: React.FC<RouteComponentProps<any>> = ({
const ProductVariantCreate: React.FC<RouteComponentProps<any>> = ({ const ProductVariantCreate: React.FC<RouteComponentProps<any>> = ({
match match
}) => ( }) => {
<ProductVariantCreateComponent const qs = parseQs(location.search.substr(1));
productId={decodeURIComponent(match.params.id)} const params: ProductVariantAddUrlQueryParams = qs;
/>
); return (
<ProductVariantCreateComponent
productId={decodeURIComponent(match.params.id)}
params={params}
/>
);
};
const ProductVariantCreator: React.FC<RouteComponentProps<{ const ProductVariantCreator: React.FC<RouteComponentProps<{
id: string; id: string;

View file

@ -9,6 +9,7 @@ import {
FiltersAsDictWithMultipleValues, FiltersAsDictWithMultipleValues,
FiltersWithMultipleValues, FiltersWithMultipleValues,
Pagination, Pagination,
SingleAction,
Sort, Sort,
TabActionDialog TabActionDialog
} from "../types"; } from "../types";
@ -63,18 +64,27 @@ export const productListUrl = (params?: ProductListUrlQueryParams): string =>
productListPath + "?" + stringifyQs(params); productListPath + "?" + stringifyQs(params);
export const productPath = (id: string) => urlJoin(productSection + id); export const productPath = (id: string) => urlJoin(productSection + id);
export type ProductUrlDialog = "remove" | "remove-variants" | ChannelsAction; export type ProductUrlDialog =
export type ProductUrlQueryParams = BulkAction & Dialog<ProductUrlDialog>; | "remove"
export type ProductCreateUrlQueryParams = Dialog<ChannelsAction>; | "remove-variants"
| "assign-attribute-value"
| ChannelsAction;
export type ProductUrlQueryParams = BulkAction &
Dialog<ProductUrlDialog> &
SingleAction;
export type ProductCreateUrlDialog = "assign-attribute-value" | ChannelsAction;
export type ProductCreateUrlQueryParams = Dialog<ProductCreateUrlDialog> &
SingleAction;
export const productUrl = (id: string, params?: ProductUrlQueryParams) => export const productUrl = (id: string, params?: ProductUrlQueryParams) =>
productPath(encodeURIComponent(id)) + "?" + stringifyQs(params); productPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
export const productVariantEditPath = (productId: string, variantId: string) => export const productVariantEditPath = (productId: string, variantId: string) =>
urlJoin(productSection, productId, "variant", variantId); urlJoin(productSection, productId, "variant", variantId);
export type ProductVariantEditUrlDialog = "remove"; export type ProductVariantEditUrlDialog = "remove" | "assign-attribute-value";
export type ProductVariantEditUrlQueryParams = Dialog< export type ProductVariantEditUrlQueryParams = Dialog<
ProductVariantEditUrlDialog ProductVariantEditUrlDialog
>; > &
SingleAction;
export const productVariantEditUrl = ( export const productVariantEditUrl = (
productId: string, productId: string,
variantId: string, variantId: string,
@ -92,10 +102,20 @@ export const productVariantCreatorPath = (productId: string) =>
export const productVariantCreatorUrl = (productId: string) => export const productVariantCreatorUrl = (productId: string) =>
productVariantCreatorPath(encodeURIComponent(productId)); productVariantCreatorPath(encodeURIComponent(productId));
export type ProductVariantAddUrlDialog = "assign-attribute-value";
export type ProductVariantAddUrlQueryParams = Dialog<
ProductVariantAddUrlDialog
> &
SingleAction;
export const productVariantAddPath = (productId: string) => export const productVariantAddPath = (productId: string) =>
urlJoin(productSection, productId, "variant/add"); urlJoin(productSection, productId, "variant/add");
export const productVariantAddUrl = (productId: string): string => export const productVariantAddUrl = (
productVariantAddPath(encodeURIComponent(productId)); productId: string,
params?: ProductVariantAddUrlQueryParams
): string =>
productVariantAddPath(encodeURIComponent(productId)) +
"?" +
stringifyQs(params);
export const productImagePath = (productId: string, imageId: string) => export const productImagePath = (productId: string, imageId: string) =>
urlJoin(productSection, productId, "image", imageId); urlJoin(productSection, productId, "image", imageId);

View file

@ -9,7 +9,7 @@ import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompl
import { ProductVariant } from "@saleor/fragments/types/ProductVariant"; import { ProductVariant } from "@saleor/fragments/types/ProductVariant";
import { SelectedVariantAttributeFragment } from "@saleor/fragments/types/SelectedVariantAttributeFragment"; import { SelectedVariantAttributeFragment } from "@saleor/fragments/types/SelectedVariantAttributeFragment";
import { VariantAttributeFragment } from "@saleor/fragments/types/VariantAttributeFragment"; import { VariantAttributeFragment } from "@saleor/fragments/types/VariantAttributeFragment";
import { FormsetAtomicData, FormsetData } from "@saleor/hooks/useFormset"; import { FormsetAtomicData } from "@saleor/hooks/useFormset";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { import {
ProductDetails_product, ProductDetails_product,
@ -116,18 +116,22 @@ export function getAttributeInputFromSelectedAttributes(
variantAttributes: SelectedVariantAttributeFragment[], variantAttributes: SelectedVariantAttributeFragment[],
variantAttributeScope: VariantAttributeScope variantAttributeScope: VariantAttributeScope
): AttributeInput[] { ): AttributeInput[] {
return variantAttributes?.map(attribute => ({ return variantAttributes?.map(attribute => {
data: { const value = attribute.values.length > 0 && attribute.values[0]?.slug;
inputType: attribute.attribute.inputType,
isRequired: attribute.attribute.valueRequired, return {
selectedValues: attribute.values, data: {
values: attribute.attribute.values, inputType: attribute.attribute.inputType,
variantAttributeScope isRequired: attribute.attribute.valueRequired,
}, selectedValues: attribute.values,
id: attribute.attribute.id, values: attribute.attribute.values,
label: attribute.attribute.name, variantAttributeScope
value: [(attribute.values.length && attribute.values[0]?.slug) || null] },
})); id: attribute.attribute.id,
label: attribute.attribute.name,
value: value ? [value] : undefined
};
});
} }
export function getAttributeInputFromVariant( export function getAttributeInputFromVariant(
@ -217,26 +221,6 @@ export function getChoices(nodes: Node[]): SingleAutocompleteChoiceType[] {
); );
} }
export const getAttributesDisplayData = (
attributes: AttributeInput[],
attributesWithNewFileValue: FormsetData<null, File>
) =>
attributes.map(attribute => {
const attributeWithNewFileValue = attributesWithNewFileValue.find(
attributeWithNewFile => attribute.id === attributeWithNewFile.id
);
if (attributeWithNewFileValue) {
return {
...attribute,
value: attributeWithNewFileValue?.value?.name
? [attributeWithNewFileValue.value.name]
: []
};
}
return attribute;
});
export interface ProductUpdatePageFormData extends MetadataFormData { export interface ProductUpdatePageFormData extends MetadataFormData {
category: string | null; category: string | null;
changeTaxCode: boolean; changeTaxCode: boolean;

View file

@ -124,6 +124,16 @@ export function createAttributeMultiChangeHandler(
}; };
} }
export function createAttributeReferenceChangeHandler(
changeAttributeData: FormsetChange<string[]>,
triggerChange: () => void
): FormsetChange<string[]> {
return (attributeId: string, values: string[]) => {
changeAttributeData(attributeId, values);
triggerChange();
};
}
export function createAttributeFileChangeHandler( export function createAttributeFileChangeHandler(
changeAttributeData: FormsetChange<string[]>, changeAttributeData: FormsetChange<string[]>,
attributesWithNewFileValue: FormsetData<FormsetData<null, File>>, attributesWithNewFileValue: FormsetData<FormsetData<null, File>>,

View file

@ -1,6 +1,6 @@
import { useChannelsList } from "@saleor/channels/queries"; import { useChannelsList } from "@saleor/channels/queries";
import { ChannelsAction } from "@saleor/channels/urls";
import { ChannelData, createSortedChannelsData } from "@saleor/channels/utils"; import { ChannelData, createSortedChannelsData } from "@saleor/channels/utils";
import { AttributeInput } from "@saleor/components/Attributes";
import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog"; import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config"; import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
@ -19,12 +19,14 @@ import {
import { useProductCreateMutation } from "@saleor/products/mutations"; import { useProductCreateMutation } from "@saleor/products/mutations";
import { import {
productAddUrl, productAddUrl,
ProductCreateUrlDialog,
ProductCreateUrlQueryParams, ProductCreateUrlQueryParams,
productListUrl, productListUrl,
productUrl productUrl
} from "@saleor/products/urls"; } from "@saleor/products/urls";
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 usePageSearch from "@saleor/searches/usePageSearch";
import useProductTypeSearch from "@saleor/searches/useProductTypeSearch"; import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
import { useTaxTypeList } from "@saleor/taxes/queries"; import { useTaxTypeList } from "@saleor/taxes/queries";
import { getProductErrorMessage } from "@saleor/utils/errors"; import { getProductErrorMessage } from "@saleor/utils/errors";
@ -55,7 +57,7 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
); );
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
ChannelsAction, ProductCreateUrlDialog,
ProductCreateUrlQueryParams ProductCreateUrlQueryParams
>(navigate, params => productAddUrl(params), params); >(navigate, params => productAddUrl(params), params);
@ -80,6 +82,13 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
} = useProductTypeSearch({ } = useProductTypeSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA variables: DEFAULT_INITIAL_SEARCH_DATA
}); });
const {
loadMore: loadMorePages,
search: searchPages,
result: searchPagesOpts
} = usePageSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const warehouses = useWarehouseList({ const warehouses = useWarehouseList({
displayLoader: true, displayLoader: true,
variables: { variables: {
@ -176,6 +185,14 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
} }
}; };
const handleAssignAttributeReferenceClick = (attribute: AttributeInput) =>
navigate(
productAddUrl({
action: "assign-attribute-value",
id: attribute.id
})
);
React.useEffect(() => { React.useEffect(() => {
const productId = productCreateOpts.data?.productCreate?.product?.id; const productId = productCreateOpts.data?.productCreate?.product?.id;
@ -184,6 +201,34 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
} }
}, [productCreateComplete]); }, [productCreateComplete]);
const fetchMoreProductTypes = {
hasMore: searchProductTypesOpts.data?.search.pageInfo.hasNextPage,
loading: searchProductTypesOpts.loading,
onFetchMore: loadMoreProductTypes
};
const fetchMoreCollections = {
hasMore: searchCollectionOpts.data?.search.pageInfo.hasNextPage,
loading: searchCollectionOpts.loading,
onFetchMore: loadMoreCollections
};
const fetchMoreCategories = {
hasMore: searchCategoryOpts.data?.search.pageInfo.hasNextPage,
loading: searchCategoryOpts.loading,
onFetchMore: loadMoreCategories
};
const fetchMoreReferencePages = {
hasMore: searchPagesOpts.data?.search.pageInfo.hasNextPage,
loading: searchPagesOpts.loading,
onFetchMore: loadMorePages
};
const loading =
uploadFileOpts.loading ||
productCreateOpts.loading ||
productVariantCreateOpts.loading ||
updateChannelsOpts.loading ||
updateVariantChannelsOpts.loading;
return ( return (
<> <>
<WindowTitle <WindowTitle
@ -218,13 +263,7 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
collections={(searchCollectionOpts?.data?.search?.edges || []).map( collections={(searchCollectionOpts?.data?.search?.edges || []).map(
edge => edge.node edge => edge.node
)} )}
loading={ loading={loading}
uploadFileOpts.loading ||
productCreateOpts.loading ||
productVariantCreateOpts.loading ||
updateChannelsOpts.loading ||
updateVariantChannelsOpts.loading
}
channelsErrors={ channelsErrors={
updateVariantChannelsOpts.data?.productVariantChannelListingUpdate updateVariantChannelsOpts.data?.productVariantChannelListingUpdate
?.errors ?.errors
@ -245,21 +284,9 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
onSubmit={handleSubmit} onSubmit={handleSubmit}
onWarehouseConfigure={() => navigate(warehouseAddPath)} onWarehouseConfigure={() => navigate(warehouseAddPath)}
saveButtonBarState={productCreateOpts.status} saveButtonBarState={productCreateOpts.status}
fetchMoreCategories={{ fetchMoreCategories={fetchMoreCategories}
hasMore: searchCategoryOpts.data?.search.pageInfo.hasNextPage, fetchMoreCollections={fetchMoreCollections}
loading: searchCategoryOpts.loading, fetchMoreProductTypes={fetchMoreProductTypes}
onFetchMore: loadMoreCategories
}}
fetchMoreCollections={{
hasMore: searchCollectionOpts.data?.search.pageInfo.hasNextPage,
loading: searchCollectionOpts.loading,
onFetchMore: loadMoreCollections
}}
fetchMoreProductTypes={{
hasMore: searchProductTypesOpts.data?.search.pageInfo.hasNextPage,
loading: searchProductTypesOpts.loading,
onFetchMore: loadMoreProductTypes
}}
warehouses={ warehouses={
warehouses.data?.warehouses.edges.map(edge => edge.node) || [] warehouses.data?.warehouses.edges.map(edge => edge.node) || []
} }
@ -267,6 +294,16 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
weightUnit={shop?.defaultWeightUnit} weightUnit={shop?.defaultWeightUnit}
openChannelsModal={handleChannelsModalOpen} openChannelsModal={handleChannelsModalOpen}
onChannelsChange={setCurrentChannels} onChannelsChange={setCurrentChannels}
assignReferencesAttributeId={
params.action === "assign-attribute-value" && params.id
}
onAssignReferencesClick={handleAssignAttributeReferenceClick}
referencePages={searchPagesOpts.data?.search.edges.map(
edge => edge.node
)}
fetchReferencePages={searchPages}
fetchMoreReferencePages={fetchMoreReferencePages}
onCloseDialog={() => navigate(productAddUrl())}
/> />
</> </>
); );

View file

@ -11,6 +11,7 @@ import {
} from "@saleor/channels/utils"; } from "@saleor/channels/utils";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import useAppChannel from "@saleor/components/AppLayout/AppChannelContext"; import useAppChannel from "@saleor/components/AppLayout/AppChannelContext";
import { AttributeInput } from "@saleor/components/Attributes";
import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog"; import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog";
import NotFoundPage from "@saleor/components/NotFoundPage"; import NotFoundPage from "@saleor/components/NotFoundPage";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
@ -38,6 +39,7 @@ import {
} from "@saleor/products/mutations"; } from "@saleor/products/mutations";
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 usePageSearch from "@saleor/searches/usePageSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import { import {
@ -97,6 +99,13 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
} = useCollectionSearch({ } = useCollectionSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA variables: DEFAULT_INITIAL_SEARCH_DATA
}); });
const {
loadMore: loadMorePages,
search: searchPages,
result: searchPagesOpts
} = usePageSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const warehouses = useWarehouseList({ const warehouses = useWarehouseList({
displayLoader: true, displayLoader: true,
variables: { variables: {
@ -296,6 +305,14 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
reorderProductVariants({ variables }) reorderProductVariants({ variables })
); );
const handleAssignAttributeReferenceClick = (attribute: AttributeInput) =>
navigate(
productUrl(id, {
action: "assign-attribute-value",
id: attribute.id
})
);
const disableFormSave = const disableFormSave =
uploadFileOpts.loading || uploadFileOpts.loading ||
createProductImageOpts.loading || createProductImageOpts.loading ||
@ -338,6 +355,22 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
?.errors || []) ?.errors || [])
]; ];
const fetchMoreCollections = {
hasMore: searchCollectionsOpts.data?.search.pageInfo.hasNextPage,
loading: searchCollectionsOpts.loading,
onFetchMore: loadMoreCollections
};
const fetchMoreCategories = {
hasMore: searchCategoriesOpts.data?.search.pageInfo.hasNextPage,
loading: searchCategoriesOpts.loading,
onFetchMore: loadMoreCategories
};
const fetchMoreReferencePages = {
hasMore: searchPagesOpts.data?.search.pageInfo.hasNextPage,
loading: searchPagesOpts.loading,
onFetchMore: loadMorePages
};
return ( return (
<> <>
<WindowTitle title={data?.product?.name} /> <WindowTitle title={data?.product?.name} />
@ -413,19 +446,21 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
selected={listElements.length} selected={listElements.length}
toggle={toggle} toggle={toggle}
toggleAll={toggleAll} toggleAll={toggleAll}
fetchMoreCategories={{ fetchMoreCategories={fetchMoreCategories}
hasMore: searchCategoriesOpts?.data?.search?.pageInfo?.hasNextPage, fetchMoreCollections={fetchMoreCollections}
loading: searchCategoriesOpts.loading,
onFetchMore: loadMoreCategories
}}
fetchMoreCollections={{
hasMore: searchCollectionsOpts?.data?.search?.pageInfo?.hasNextPage,
loading: searchCollectionsOpts.loading,
onFetchMore: loadMoreCollections
}}
selectedChannelId={channel?.id} selectedChannelId={channel?.id}
openChannelsModal={handleChannelsModalOpen} openChannelsModal={handleChannelsModalOpen}
onChannelsChange={setCurrentChannels} onChannelsChange={setCurrentChannels}
assignReferencesAttributeId={
params.action === "assign-attribute-value" && params.id
}
onAssignReferencesClick={handleAssignAttributeReferenceClick}
referencePages={searchPagesOpts.data?.search.edges.map(
edge => edge.node
)}
fetchReferencePages={searchPages}
fetchMoreReferencePages={fetchMoreReferencePages}
onCloseDialog={() => navigate(productUrl(id))}
/> />
<ActionDialog <ActionDialog
open={params.action === "remove"} open={params.action === "remove"}

View file

@ -11,8 +11,10 @@ import {
prepareAttributesInput prepareAttributesInput
} from "@saleor/attributes/utils/handlers"; } from "@saleor/attributes/utils/handlers";
import { createVariantChannels } from "@saleor/channels/utils"; import { createVariantChannels } from "@saleor/channels/utils";
import { AttributeInput } from "@saleor/components/Attributes";
import NotFoundPage from "@saleor/components/NotFoundPage"; import NotFoundPage from "@saleor/components/NotFoundPage";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import { useFileUploadMutation } from "@saleor/files/mutations"; import { useFileUploadMutation } from "@saleor/files/mutations";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
@ -21,6 +23,7 @@ import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { useProductVariantChannelListingUpdate } from "@saleor/products/mutations"; import { useProductVariantChannelListingUpdate } from "@saleor/products/mutations";
import { ProductVariantDetails_productVariant } from "@saleor/products/types/ProductVariantDetails"; import { ProductVariantDetails_productVariant } from "@saleor/products/types/ProductVariantDetails";
import usePageSearch from "@saleor/searches/usePageSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler"; import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import { import {
@ -269,6 +272,28 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
variables => updatePrivateMetadata({ variables }) variables => updatePrivateMetadata({ variables })
); );
const handleAssignAttributeReferenceClick = (attribute: AttributeInput) =>
navigate(
productVariantEditUrl(productId, variantId, {
action: "assign-attribute-value",
id: attribute.id
})
);
const {
loadMore: loadMorePages,
search: searchPages,
result: searchPagesOpts
} = usePageSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const fetchMoreReferencePages = {
hasMore: searchPagesOpts.data?.search.pageInfo.hasNextPage,
loading: searchPagesOpts.loading,
onFetchMore: loadMorePages
};
return ( return (
<> <>
<WindowTitle title={data?.productVariant?.name} /> <WindowTitle title={data?.productVariant?.name} />
@ -303,6 +328,18 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
navigate(productVariantEditUrl(productId, variantId)); navigate(productVariantEditUrl(productId, variantId));
}} }}
onVariantReorder={handleVariantReorder} onVariantReorder={handleVariantReorder}
assignReferencesAttributeId={
params.action === "assign-attribute-value" && params.id
}
onAssignReferencesClick={handleAssignAttributeReferenceClick}
referencePages={searchPagesOpts.data?.search.edges.map(
edge => edge.node
)}
fetchReferencePages={searchPages}
fetchMoreReferencePages={fetchMoreReferencePages}
onCloseDialog={() =>
navigate(productVariantEditUrl(productId, variantId))
}
/> />
<ProductVariantDeleteDialog <ProductVariantDeleteDialog
confirmButtonState={deleteVariantOpts.status} confirmButtonState={deleteVariantOpts.status}

View file

@ -4,8 +4,10 @@ import {
prepareAttributesInput prepareAttributesInput
} from "@saleor/attributes/utils/handlers"; } from "@saleor/attributes/utils/handlers";
import { ChannelPriceData } from "@saleor/channels/utils"; import { ChannelPriceData } from "@saleor/channels/utils";
import { AttributeInput } from "@saleor/components/Attributes";
import NotFoundPage from "@saleor/components/NotFoundPage"; import NotFoundPage from "@saleor/components/NotFoundPage";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import { useFileUploadMutation } from "@saleor/files/mutations"; import { useFileUploadMutation } from "@saleor/files/mutations";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
@ -13,6 +15,7 @@ import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { useProductVariantChannelListingUpdate } from "@saleor/products/mutations"; import { useProductVariantChannelListingUpdate } from "@saleor/products/mutations";
import { ProductVariantChannelListingUpdate } from "@saleor/products/types/ProductVariantChannelListingUpdate"; import { ProductVariantChannelListingUpdate } from "@saleor/products/types/ProductVariantChannelListingUpdate";
import usePageSearch from "@saleor/searches/usePageSearch";
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
import { import {
useMetadataUpdate, useMetadataUpdate,
@ -31,15 +34,23 @@ import {
useVariantCreateMutation useVariantCreateMutation
} from "../mutations"; } from "../mutations";
import { useProductVariantCreateQuery } from "../queries"; import { useProductVariantCreateQuery } from "../queries";
import { productListUrl, productUrl, productVariantEditUrl } from "../urls"; import {
productListUrl,
productUrl,
productVariantAddUrl,
ProductVariantAddUrlQueryParams,
productVariantEditUrl
} from "../urls";
import { createVariantReorderHandler } from "./ProductUpdate/handlers"; import { createVariantReorderHandler } from "./ProductUpdate/handlers";
interface ProductVariantCreateProps { interface ProductVariantCreateProps {
productId: string; productId: string;
params: ProductVariantAddUrlQueryParams;
} }
export const ProductVariant: React.FC<ProductVariantCreateProps> = ({ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
productId productId,
params
}) => { }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
@ -172,6 +183,28 @@ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
const handleVariantClick = (id: string) => const handleVariantClick = (id: string) =>
navigate(productVariantEditUrl(productId, id)); navigate(productVariantEditUrl(productId, id));
const handleAssignAttributeReferenceClick = (attribute: AttributeInput) =>
navigate(
productVariantAddUrl(productId, {
action: "assign-attribute-value",
id: attribute.id
})
);
const {
loadMore: loadMorePages,
search: searchPages,
result: searchPagesOpts
} = usePageSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const fetchMoreReferencePages = {
hasMore: searchPagesOpts.data?.search.pageInfo.hasNextPage,
loading: searchPagesOpts.loading,
onFetchMore: loadMorePages
};
const disableForm = const disableForm =
productLoading || productLoading ||
uploadFileOpts.loading || uploadFileOpts.loading ||
@ -208,6 +241,16 @@ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
warehouses.data?.warehouses.edges.map(edge => edge.node) || [] warehouses.data?.warehouses.edges.map(edge => edge.node) || []
} }
weightUnit={shop?.defaultWeightUnit} weightUnit={shop?.defaultWeightUnit}
assignReferencesAttributeId={
params.action === "assign-attribute-value" && params.id
}
onAssignReferencesClick={handleAssignAttributeReferenceClick}
referencePages={searchPagesOpts.data?.search.edges.map(
edge => edge.node
)}
fetchReferencePages={searchPages}
fetchMoreReferencePages={fetchMoreReferencePages}
onCloseDialog={() => navigate(productVariantAddUrl(productId))}
/> />
</> </>
); );

View file

@ -12,10 +12,13 @@ import Decorator from "../../Decorator";
const props: PageDetailsPageProps = { const props: PageDetailsPageProps = {
errors: [], errors: [],
loading: false, loading: false,
onAssignReferencesClick: () => undefined,
onBack: () => undefined, onBack: () => undefined,
onCloseDialog: () => undefined,
onRemove: () => undefined, onRemove: () => undefined,
onSubmit: () => undefined, onSubmit: () => undefined,
page, page,
referencePages: [],
saveButtonBarState: "default" saveButtonBarState: "default"
}; };

View file

@ -45,6 +45,9 @@ storiesOf("Views / Products / Create product", module)
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
taxTypes={taxTypes} taxTypes={taxTypes}
weightUnit="kg" weightUnit="kg"
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)) ))
.add("When loading", () => ( .add("When loading", () => (
@ -73,6 +76,9 @@ storiesOf("Views / Products / Create product", module)
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
taxTypes={taxTypes} taxTypes={taxTypes}
weightUnit="kg" weightUnit="kg"
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)) ))
.add("form errors", () => ( .add("form errors", () => (
@ -118,5 +124,8 @@ storiesOf("Views / Products / Create product", module)
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
taxTypes={taxTypes} taxTypes={taxTypes}
weightUnit="kg" weightUnit="kg"
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)); ));

View file

@ -42,8 +42,10 @@ const props: ProductUpdatePageProps = {
hasChannelChanged: false, hasChannelChanged: false,
header: product.name, header: product.name,
images: product.images, images: product.images,
onAssignReferencesClick: () => undefined,
onBack: () => undefined, onBack: () => undefined,
onChannelsChange: () => undefined, onChannelsChange: () => undefined,
onCloseDialog: () => undefined,
onDelete: () => undefined, onDelete: () => undefined,
onImageDelete: () => undefined, onImageDelete: () => undefined,
onImageUpload: () => undefined, onImageUpload: () => undefined,
@ -57,6 +59,7 @@ const props: ProductUpdatePageProps = {
openChannelsModal: () => undefined, openChannelsModal: () => undefined,
placeholderImage, placeholderImage,
product, product,
referencePages: [],
saveButtonBarState: "default", saveButtonBarState: "default",
selectedChannelId: "123", selectedChannelId: "123",
taxTypes, taxTypes,

View file

@ -35,6 +35,9 @@ storiesOf("Views / Products / Create product variant", module)
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)) ))
.add("with errors", () => ( .add("with errors", () => (
@ -72,6 +75,9 @@ storiesOf("Views / Products / Create product variant", module)
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)) ))
.add("when loading data", () => ( .add("when loading data", () => (
@ -90,6 +96,9 @@ storiesOf("Views / Products / Create product variant", module)
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)) ))
.add("add first variant", () => ( .add("add first variant", () => (
@ -111,6 +120,9 @@ storiesOf("Views / Products / Create product variant", module)
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)) ))
.add("no warehouses", () => ( .add("no warehouses", () => (
@ -129,5 +141,8 @@ storiesOf("Views / Products / Create product variant", module)
saveButtonBarState="default" saveButtonBarState="default"
warehouses={[]} warehouses={[]}
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)); ));

View file

@ -33,6 +33,9 @@ storiesOf("Views / Products / Product variant details", module)
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)) ))
.add("when loading data", () => ( .add("when loading data", () => (
@ -55,6 +58,9 @@ storiesOf("Views / Products / Product variant details", module)
saveButtonBarState="default" saveButtonBarState="default"
warehouses={warehouseList} warehouses={warehouseList}
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)) ))
.add("no warehouses", () => ( .add("no warehouses", () => (
@ -76,6 +82,9 @@ storiesOf("Views / Products / Product variant details", module)
saveButtonBarState="default" saveButtonBarState="default"
warehouses={[]} warehouses={[]}
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)) ))
.add("attribute errors", () => ( .add("attribute errors", () => (
@ -125,5 +134,8 @@ storiesOf("Views / Products / Product variant details", module)
]} ]}
warehouses={warehouseList} warehouses={warehouseList}
onWarehouseConfigure={() => undefined} onWarehouseConfigure={() => undefined}
referencePages={[]}
onAssignReferencesClick={() => undefined}
onCloseDialog={() => undefined}
/> />
)); ));