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": {
"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": {
"context": "dialog header",
"string": "Assign Category"

View file

@ -1,9 +1,14 @@
import {
AttributeInput,
AttributeInputData
} from "@saleor/components/Attributes";
import { FileUpload } from "@saleor/files/types/FileUpload";
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
import { SelectedVariantAttributeFragment } from "@saleor/fragments/types/SelectedVariantAttributeFragment";
import { UploadErrorFragment } from "@saleor/fragments/types/UploadErrorFragment";
import { FormsetData } from "@saleor/hooks/useFormset";
import { PageDetails_page_attributes } from "@saleor/pages/types/PageDetails";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import {
AttributeInputTypeEnum,
AttributeValueInput
@ -99,6 +104,18 @@ export const mergeAttributeValueDeleteErrors = (
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 = (
attributesWithNewFileValue: FormsetData<null, File>
) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value);
@ -149,3 +166,53 @@ export const getAttributesAfterFileAttributesUpdate = (
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,
onFileChange: () => undefined,
onMultiChange: () => undefined,
onReferencesChange: () => undefined,
onReferencesChangeClick: () => undefined,
onReferencesAddClick: () => undefined,
onReferencesRemove: () => undefined,
onReferencesReorder: () => undefined
};

View file

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

View file

@ -88,6 +88,23 @@ const REFERENCE_ATTRIBUTE: AttributeInput = {
data: {
inputType: AttributeInputTypeEnum.REFERENCE,
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: [
{
__typename: "AttributeValue",
@ -143,9 +160,9 @@ export const ATTRIBUTES_SELECTED: AttributeInput[] = [
{
...REFERENCE_ATTRIBUTE,
value: [
REFERENCE_ATTRIBUTE.data.values[0].slug,
REFERENCE_ATTRIBUTE.data.values[1].slug,
REFERENCE_ATTRIBUTE.data.values[2].slug
REFERENCE_ATTRIBUTE.data.values[0].id,
REFERENCE_ATTRIBUTE.data.values[1].id,
REFERENCE_ATTRIBUTE.data.values[2].id
]
}
];

View file

@ -20,9 +20,9 @@ const useStyles = makeStyles(
}
);
interface SortableChipsFieldValueType {
export interface SortableChipsFieldValueType {
label: string;
value: any;
value: string;
}
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 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 { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container";
@ -13,6 +15,8 @@ import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErr
import useDateLocalize from "@saleor/hooks/useDateLocalize";
import { SubmitPromise } from "@saleor/hooks/useForm";
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 { FetchMoreProps } from "@saleor/types";
import React from "react";
@ -21,13 +25,14 @@ import { useIntl } from "react-intl";
import { PageDetails_page } from "../../types/PageDetails";
import PageInfo from "../PageInfo";
import PageOrganizeContent from "../PageOrganizeContent";
import PageForm, { PageData } from "./form";
import PageForm, { PageData, PageUpdateHandlers } from "./form";
export interface PageDetailsPageProps {
loading: boolean;
errors: PageErrorWithAttributesFragment[];
page: PageDetails_page;
pageTypes?: SearchPageTypes_search_edges_node[];
referencePages: SearchPages_search_edges_node[];
allowEmptySlug?: boolean;
saveButtonBarState: ConfirmButtonTransitionState;
onBack: () => void;
@ -35,6 +40,11 @@ export interface PageDetailsPageProps {
onSubmit: (data: PageData) => SubmitPromise;
fetchPageTypes?: (data: string) => void;
fetchMorePageTypes?: FetchMoreProps;
assignReferencesAttributeId?: string;
onAssignReferencesClick: (attribute: AttributeInput) => void;
fetchReferencePages?: (data: string) => void;
fetchMoreReferencePages?: FetchMoreProps;
onCloseDialog: () => void;
}
const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
@ -42,20 +52,49 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
errors,
page,
pageTypes,
referencePages,
saveButtonBarState,
onBack,
onRemove,
onSubmit,
fetchPageTypes,
fetchMorePageTypes
fetchMorePageTypes,
assignReferencesAttributeId,
onAssignReferencesClick,
fetchReferencePages,
fetchMoreReferencePages,
onCloseDialog
}) => {
const intl = useIntl();
const localizeDate = useDateLocalize();
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 (
<PageForm page={page} pageTypes={pageTypes} onSubmit={onSubmit}>
<PageForm
page={page}
pageTypes={pageTypes}
referencePages={referencePages}
onSubmit={onSubmit}
>
{({ change, data, pageType, handlers, hasChanged, submit }) => (
<Container>
<AppHeader onBack={onBack}>
@ -107,6 +146,8 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
onChange={handlers.selectAttribute}
onMultiChange={handlers.selectAttributeMulti}
onFileChange={handlers.selectAttributeFile}
onReferencesRemove={handlers.selectAttributeReference}
onReferencesAddClick={onAssignReferencesClick}
/>
)}
<CardSpacer />
@ -161,6 +202,24 @@ const PageDetailsPage: React.FC<PageDetailsPageProps> = ({
onDelete={page === null ? undefined : onRemove}
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>
)}
</PageForm>

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { stringify as stringifyQs } from "qs";
import urlJoin from "url-join";
import { BulkAction, Dialog, Pagination, Sort } from "../types";
import { BulkAction, Dialog, Pagination, SingleAction, Sort } from "../types";
export const pagesSection = "/pages/";
@ -21,10 +21,11 @@ export const pageListUrl = (params?: PageListUrlQueryParams) =>
pageListPath + "?" + stringifyQs(params);
export const pagePath = (id: string) => urlJoin(pagesSection, id);
export type PageUrlDialog = "remove";
export type PageUrlQueryParams = Dialog<PageUrlDialog>;
export type PageUrlDialog = "remove" | "assign-attribute-value";
export type PageUrlQueryParams = Dialog<PageUrlDialog> & SingleAction;
export const pageUrl = (id: string, params?: PageUrlQueryParams) =>
pagePath(encodeURIComponent(id)) + "?" + stringifyQs(params);
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 { FormsetData } from "@saleor/hooks/useFormset";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import {
PageDetails_page,
@ -37,22 +37,17 @@ export function getAttributeInputFromPageType(
}));
}
export const getAttributesDisplayData = (
export const getAttributeValuesFromReferences = (
attributeId: string,
attributes: AttributeInput[],
attributesWithNewFileValue: FormsetData<null, File>
) =>
attributes.map(attribute => {
const attributeWithNewFileValue = attributesWithNewFileValue.find(
attributeWithNewFile => attribute.id === attributeWithNewFile.id
);
referencePages: SearchPages_search_edges_node[]
) => {
const attribute = attributes?.find(attribute => attribute.id === attributeId);
if (attributeWithNewFileValue) {
return {
...attribute,
value: attributeWithNewFileValue?.value?.name
? [attributeWithNewFileValue.value.name]
: []
return (
referencePages?.filter(
value =>
!attribute?.value?.some(selectedValue => selectedValue === value.id)
) || []
);
};
}
return attribute;
});

View file

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

View file

@ -11,7 +11,9 @@ import {
prepareAttributesInput
} from "@saleor/attributes/utils/handlers";
import ActionDialog from "@saleor/components/ActionDialog";
import { AttributeInput } from "@saleor/components/Attributes";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import { useFileUploadMutation } from "@saleor/files/mutations";
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
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 useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl";
import usePageSearch from "@saleor/searches/usePageSearch";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import {
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) => {
let errors: Array<
AttributeErrorFragment | UploadErrorFragment | PageErrorFragment
@ -139,6 +150,20 @@ export const PageDetails: React.FC<PageDetailsProps> = ({ id, params }) => {
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 (
<>
<WindowTitle title={maybe(() => pageDetails.data.page.title)} />
@ -161,6 +186,16 @@ export const PageDetails: React.FC<PageDetailsProps> = ({ id, params }) => {
)
}
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
open={params.action === "remove"}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ import {
FiltersAsDictWithMultipleValues,
FiltersWithMultipleValues,
Pagination,
SingleAction,
Sort,
TabActionDialog
} from "../types";
@ -63,18 +64,27 @@ export const productListUrl = (params?: ProductListUrlQueryParams): string =>
productListPath + "?" + stringifyQs(params);
export const productPath = (id: string) => urlJoin(productSection + id);
export type ProductUrlDialog = "remove" | "remove-variants" | ChannelsAction;
export type ProductUrlQueryParams = BulkAction & Dialog<ProductUrlDialog>;
export type ProductCreateUrlQueryParams = Dialog<ChannelsAction>;
export type ProductUrlDialog =
| "remove"
| "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) =>
productPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
export const productVariantEditPath = (productId: string, variantId: string) =>
urlJoin(productSection, productId, "variant", variantId);
export type ProductVariantEditUrlDialog = "remove";
export type ProductVariantEditUrlDialog = "remove" | "assign-attribute-value";
export type ProductVariantEditUrlQueryParams = Dialog<
ProductVariantEditUrlDialog
>;
> &
SingleAction;
export const productVariantEditUrl = (
productId: string,
variantId: string,
@ -92,10 +102,20 @@ export const productVariantCreatorPath = (productId: string) =>
export const productVariantCreatorUrl = (productId: string) =>
productVariantCreatorPath(encodeURIComponent(productId));
export type ProductVariantAddUrlDialog = "assign-attribute-value";
export type ProductVariantAddUrlQueryParams = Dialog<
ProductVariantAddUrlDialog
> &
SingleAction;
export const productVariantAddPath = (productId: string) =>
urlJoin(productSection, productId, "variant/add");
export const productVariantAddUrl = (productId: string): string =>
productVariantAddPath(encodeURIComponent(productId));
export const productVariantAddUrl = (
productId: string,
params?: ProductVariantAddUrlQueryParams
): string =>
productVariantAddPath(encodeURIComponent(productId)) +
"?" +
stringifyQs(params);
export const productImagePath = (productId: string, imageId: string) =>
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 { SelectedVariantAttributeFragment } from "@saleor/fragments/types/SelectedVariantAttributeFragment";
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 {
ProductDetails_product,
@ -116,7 +116,10 @@ export function getAttributeInputFromSelectedAttributes(
variantAttributes: SelectedVariantAttributeFragment[],
variantAttributeScope: VariantAttributeScope
): AttributeInput[] {
return variantAttributes?.map(attribute => ({
return variantAttributes?.map(attribute => {
const value = attribute.values.length > 0 && attribute.values[0]?.slug;
return {
data: {
inputType: attribute.attribute.inputType,
isRequired: attribute.attribute.valueRequired,
@ -126,8 +129,9 @@ export function getAttributeInputFromSelectedAttributes(
},
id: attribute.attribute.id,
label: attribute.attribute.name,
value: [(attribute.values.length && attribute.values[0]?.slug) || null]
}));
value: value ? [value] : undefined
};
});
}
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 {
category: string | null;
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(
changeAttributeData: FormsetChange<string[]>,
attributesWithNewFileValue: FormsetData<FormsetData<null, File>>,

View file

@ -1,6 +1,6 @@
import { useChannelsList } from "@saleor/channels/queries";
import { ChannelsAction } from "@saleor/channels/urls";
import { ChannelData, createSortedChannelsData } from "@saleor/channels/utils";
import { AttributeInput } from "@saleor/components/Attributes";
import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
@ -19,12 +19,14 @@ import {
import { useProductCreateMutation } from "@saleor/products/mutations";
import {
productAddUrl,
ProductCreateUrlDialog,
ProductCreateUrlQueryParams,
productListUrl,
productUrl
} from "@saleor/products/urls";
import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import usePageSearch from "@saleor/searches/usePageSearch";
import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
import { useTaxTypeList } from "@saleor/taxes/queries";
import { getProductErrorMessage } from "@saleor/utils/errors";
@ -55,7 +57,7 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
);
const [openModal, closeModal] = createDialogActionHandlers<
ChannelsAction,
ProductCreateUrlDialog,
ProductCreateUrlQueryParams
>(navigate, params => productAddUrl(params), params);
@ -80,6 +82,13 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
} = useProductTypeSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const {
loadMore: loadMorePages,
search: searchPages,
result: searchPagesOpts
} = usePageSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const warehouses = useWarehouseList({
displayLoader: true,
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(() => {
const productId = productCreateOpts.data?.productCreate?.product?.id;
@ -184,6 +201,34 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
}
}, [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 (
<>
<WindowTitle
@ -218,13 +263,7 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
collections={(searchCollectionOpts?.data?.search?.edges || []).map(
edge => edge.node
)}
loading={
uploadFileOpts.loading ||
productCreateOpts.loading ||
productVariantCreateOpts.loading ||
updateChannelsOpts.loading ||
updateVariantChannelsOpts.loading
}
loading={loading}
channelsErrors={
updateVariantChannelsOpts.data?.productVariantChannelListingUpdate
?.errors
@ -245,21 +284,9 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
onSubmit={handleSubmit}
onWarehouseConfigure={() => navigate(warehouseAddPath)}
saveButtonBarState={productCreateOpts.status}
fetchMoreCategories={{
hasMore: searchCategoryOpts.data?.search.pageInfo.hasNextPage,
loading: searchCategoryOpts.loading,
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
}}
fetchMoreCategories={fetchMoreCategories}
fetchMoreCollections={fetchMoreCollections}
fetchMoreProductTypes={fetchMoreProductTypes}
warehouses={
warehouses.data?.warehouses.edges.map(edge => edge.node) || []
}
@ -267,6 +294,16 @@ export const ProductCreateView: React.FC<ProductCreateProps> = ({ params }) => {
weightUnit={shop?.defaultWeightUnit}
openChannelsModal={handleChannelsModalOpen}
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";
import ActionDialog from "@saleor/components/ActionDialog";
import useAppChannel from "@saleor/components/AppLayout/AppChannelContext";
import { AttributeInput } from "@saleor/components/Attributes";
import ChannelsAvailabilityDialog from "@saleor/components/ChannelsAvailabilityDialog";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { WindowTitle } from "@saleor/components/WindowTitle";
@ -38,6 +39,7 @@ import {
} from "@saleor/products/mutations";
import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import usePageSearch from "@saleor/searches/usePageSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import {
@ -97,6 +99,13 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
} = useCollectionSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const {
loadMore: loadMorePages,
search: searchPages,
result: searchPagesOpts
} = usePageSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const warehouses = useWarehouseList({
displayLoader: true,
variables: {
@ -296,6 +305,14 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
reorderProductVariants({ variables })
);
const handleAssignAttributeReferenceClick = (attribute: AttributeInput) =>
navigate(
productUrl(id, {
action: "assign-attribute-value",
id: attribute.id
})
);
const disableFormSave =
uploadFileOpts.loading ||
createProductImageOpts.loading ||
@ -338,6 +355,22 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
?.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 (
<>
<WindowTitle title={data?.product?.name} />
@ -413,19 +446,21 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
fetchMoreCategories={{
hasMore: searchCategoriesOpts?.data?.search?.pageInfo?.hasNextPage,
loading: searchCategoriesOpts.loading,
onFetchMore: loadMoreCategories
}}
fetchMoreCollections={{
hasMore: searchCollectionsOpts?.data?.search?.pageInfo?.hasNextPage,
loading: searchCollectionsOpts.loading,
onFetchMore: loadMoreCollections
}}
fetchMoreCategories={fetchMoreCategories}
fetchMoreCollections={fetchMoreCollections}
selectedChannelId={channel?.id}
openChannelsModal={handleChannelsModalOpen}
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
open={params.action === "remove"}

View file

@ -11,8 +11,10 @@ import {
prepareAttributesInput
} from "@saleor/attributes/utils/handlers";
import { createVariantChannels } from "@saleor/channels/utils";
import { AttributeInput } from "@saleor/components/Attributes";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import { useFileUploadMutation } from "@saleor/files/mutations";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
@ -21,6 +23,7 @@ import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl";
import { useProductVariantChannelListingUpdate } from "@saleor/products/mutations";
import { ProductVariantDetails_productVariant } from "@saleor/products/types/ProductVariantDetails";
import usePageSearch from "@saleor/searches/usePageSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataUpdateHandler from "@saleor/utils/handlers/metadataUpdateHandler";
import {
@ -269,6 +272,28 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
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 (
<>
<WindowTitle title={data?.productVariant?.name} />
@ -303,6 +328,18 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
navigate(productVariantEditUrl(productId, variantId));
}}
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
confirmButtonState={deleteVariantOpts.status}

View file

@ -4,8 +4,10 @@ import {
prepareAttributesInput
} from "@saleor/attributes/utils/handlers";
import { ChannelPriceData } from "@saleor/channels/utils";
import { AttributeInput } from "@saleor/components/Attributes";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import { useFileUploadMutation } from "@saleor/files/mutations";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
@ -13,6 +15,7 @@ import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl";
import { useProductVariantChannelListingUpdate } from "@saleor/products/mutations";
import { ProductVariantChannelListingUpdate } from "@saleor/products/types/ProductVariantChannelListingUpdate";
import usePageSearch from "@saleor/searches/usePageSearch";
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
import {
useMetadataUpdate,
@ -31,15 +34,23 @@ import {
useVariantCreateMutation
} from "../mutations";
import { useProductVariantCreateQuery } from "../queries";
import { productListUrl, productUrl, productVariantEditUrl } from "../urls";
import {
productListUrl,
productUrl,
productVariantAddUrl,
ProductVariantAddUrlQueryParams,
productVariantEditUrl
} from "../urls";
import { createVariantReorderHandler } from "./ProductUpdate/handlers";
interface ProductVariantCreateProps {
productId: string;
params: ProductVariantAddUrlQueryParams;
}
export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
productId
productId,
params
}) => {
const navigate = useNavigator();
const notify = useNotifier();
@ -172,6 +183,28 @@ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
const handleVariantClick = (id: string) =>
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 =
productLoading ||
uploadFileOpts.loading ||
@ -208,6 +241,16 @@ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
warehouses.data?.warehouses.edges.map(edge => edge.node) || []
}
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 = {
errors: [],
loading: false,
onAssignReferencesClick: () => undefined,
onBack: () => undefined,
onCloseDialog: () => undefined,
onRemove: () => undefined,
onSubmit: () => undefined,
page,
referencePages: [],
saveButtonBarState: "default"
};

View file

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

View file

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

View file

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

View file

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