diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index ff4677d0d..b55495938 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -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" diff --git a/src/attributes/utils/data.ts b/src/attributes/utils/data.ts index ee2a64f86..273c4fabe 100644 --- a/src/attributes/utils/data.ts +++ b/src/attributes/utils/data.ts @@ -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 +) => { + const attribute = attributes.find(attribute => attribute.id === attributeId); + + return attribute.value + ? [...attribute.value, ...attributeValues] + : attributeValues; +}; + export const getFileValuesToUploadFromAttributes = ( attributesWithNewFileValue: FormsetData ) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value); @@ -149,3 +166,53 @@ export const getAttributesAfterFileAttributesUpdate = ( return uploadedFileAttributes.concat(removedFileAttributes); }; + +export const getFileAttributeDisplayData = ( + attribute: AttributeInput, + attributesWithNewFileValue: FormsetData +) => { + 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, + 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; + }); diff --git a/src/components/AssignAttributeValueDialog/AssignAttributeValueDialog.tsx b/src/components/AssignAttributeValueDialog/AssignAttributeValueDialog.tsx new file mode 100644 index 000000000..ff2711eeb --- /dev/null +++ b/src/components/AssignAttributeValueDialog/AssignAttributeValueDialog.tsx @@ -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 = ({ + attributeValues, + ...rest +}) => { + const intl = useIntl(); + + return ( + ({ + 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; diff --git a/src/components/AssignAttributeValueDialog/index.ts b/src/components/AssignAttributeValueDialog/index.ts new file mode 100644 index 000000000..8dce80ff9 --- /dev/null +++ b/src/components/AssignAttributeValueDialog/index.ts @@ -0,0 +1,2 @@ +export { default } from "./AssignAttributeValueDialog"; +export * from "./AssignAttributeValueDialog"; diff --git a/src/components/Attributes/Attributes.stories.tsx b/src/components/Attributes/Attributes.stories.tsx index ba294cbd9..679184415 100644 --- a/src/components/Attributes/Attributes.stories.tsx +++ b/src/components/Attributes/Attributes.stories.tsx @@ -13,8 +13,8 @@ const props: AttributesProps = { onChange: () => undefined, onFileChange: () => undefined, onMultiChange: () => undefined, - onReferencesChange: () => undefined, - onReferencesChangeClick: () => undefined, + onReferencesAddClick: () => undefined, + onReferencesRemove: () => undefined, onReferencesReorder: () => undefined }; diff --git a/src/components/Attributes/Attributes.tsx b/src/components/Attributes/Attributes.tsx index 87a9077bf..4a366ceb3 100644 --- a/src/components/Attributes/Attributes.tsx +++ b/src/components/Attributes/Attributes.tsx @@ -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; export type AttributeFileInput = FormsetAtomicData; @@ -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 = ({ onChange, onMultiChange, onFileChange, - onReferencesChange, - onReferencesChangeClick, + onReferencesRemove, + onReferencesAddClick, onReferencesReorder }) => { const intl = useIntl(); @@ -272,13 +305,13 @@ const Attributes: React.FC = ({ defaultMessage: "Assign references", description: "button label" })} - onSelect={onReferencesChangeClick} + onSelect={() => onReferencesAddClick(attribute)} disabled={disabled} > - onReferencesChange( + onReferencesRemove( attribute.id, attribute.value?.filter(id => id !== value) ) diff --git a/src/components/Attributes/fixtures.ts b/src/components/Attributes/fixtures.ts index e6aff799d..ef11df402 100644 --- a/src/components/Attributes/fixtures.ts +++ b/src/components/Attributes/fixtures.ts @@ -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 ] } ]; diff --git a/src/components/SortableChipsField/SortableChipsField.tsx b/src/components/SortableChipsField/SortableChipsField.tsx index ef44b8b4c..a2282d441 100644 --- a/src/components/SortableChipsField/SortableChipsField.tsx +++ b/src/components/SortableChipsField/SortableChipsField.tsx @@ -20,9 +20,9 @@ const useStyles = makeStyles( } ); -interface SortableChipsFieldValueType { +export interface SortableChipsFieldValueType { label: string; - value: any; + value: string; } export interface SortableChipsFieldProps extends SortableContainerProps { diff --git a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx index 809ba0037..6b6dadc5a 100644 --- a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx +++ b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx @@ -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 = ({ @@ -42,20 +52,49 @@ const PageDetailsPage: React.FC = ({ 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 ( - + {({ change, data, pageType, handlers, hasChanged, submit }) => ( @@ -107,6 +146,8 @@ const PageDetailsPage: React.FC = ({ onChange={handlers.selectAttribute} onMultiChange={handlers.selectAttributeMulti} onFileChange={handlers.selectAttributeFile} + onReferencesRemove={handlers.selectAttributeReference} + onReferencesAddClick={onAssignReferencesClick} /> )} @@ -161,6 +202,24 @@ const PageDetailsPage: React.FC = ({ onDelete={page === null ? undefined : onRemove} onSave={submit} /> + {canOpenAssignReferencesAttributeDialog && ( + + handleAssignReferenceAttribute(attributeValues, data, handlers) + } + /> + )} )} diff --git a/src/pages/components/PageDetailsPage/form.tsx b/src/pages/components/PageDetailsPage/form.tsx index 36e83b9de..91024107d 100644 --- a/src/pages/components/PageDetailsPage/form.tsx +++ b/src/pages/components/PageDetailsPage/form.tsx @@ -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; selectAttributeMulti: FormsetChange; + selectAttributeReference: FormsetChange; selectAttributeFile: FormsetChange; } 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 = ({ children, page, + referencePages, pageTypes, onSubmit }) => { - const props = usePageForm(page, onSubmit, pageTypes); + const props = usePageForm(page, onSubmit, referencePages, pageTypes); return
{children(props)}
; }; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index cf98c14ae..11bbcdb84 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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> = ({ location }) => { return ; }; +const PageCreate: React.FC> = ({ match }) => { + const qs = parseQs(location.search.substr(1)); + const params: PageUrlQueryParams = qs; + + return ( + + ); +}; + const PageDetails: React.FC> = ({ match }) => { const qs = parseQs(location.search.substr(1)); const params: PageUrlQueryParams = qs; diff --git a/src/pages/urls.ts b/src/pages/urls.ts index ff6e228f9..748153f58 100644 --- a/src/pages/urls.ts +++ b/src/pages/urls.ts @@ -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; +export type PageUrlDialog = "remove" | "assign-attribute-value"; +export type PageUrlQueryParams = Dialog & 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); diff --git a/src/pages/utils/data.ts b/src/pages/utils/data.ts index e6dffacf4..df6ecb031 100644 --- a/src/pages/utils/data.ts +++ b/src/pages/utils/data.ts @@ -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 -) => - 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 attribute; - }); + return ( + referencePages?.filter( + value => + !attribute?.value?.some(selectedValue => selectedValue === value.id) + ) || [] + ); +}; diff --git a/src/pages/views/PageCreate.tsx b/src/pages/views/PageCreate.tsx index 39b50aa83..a77a11edc 100644 --- a/src/pages/views/PageCreate.tsx +++ b/src/pages/views/PageCreate.tsx @@ -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 = () => { +export const PageCreate: React.FC = ({ params }) => { const navigate = useNavigator(); const notify = useNotifier(); const intl = useIntl(); @@ -42,6 +50,14 @@ export const PageCreate: React.FC = () => { 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 = () => { } }; + 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 ( {(pageCreate, pageCreateOpts) => { @@ -119,11 +155,17 @@ export const PageCreate: React.FC = () => { 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())} /> ); diff --git a/src/pages/views/PageDetails.tsx b/src/pages/views/PageDetails.tsx index 65841c5c8..2b3174c64 100644 --- a/src/pages/views/PageDetails.tsx +++ b/src/pages/views/PageDetails.tsx @@ -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 = ({ 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 = ({ 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 ( <> pageDetails.data.page.title)} /> @@ -161,6 +186,16 @@ export const PageDetails: React.FC = ({ 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))} /> = ({ 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} diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index a5f85f6d2..595f0a714 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -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; 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 = ({ header, initial, productTypes: productTypeChoiceList, + referencePages, saveButtonBarState, warehouses, taxTypes, @@ -89,7 +101,12 @@ export const ProductCreatePage: React.FC = ({ onSubmit, onChannelsChange, onWarehouseConfigure, - openChannelsModal + openChannelsModal, + assignReferencesAttributeId, + onAssignReferencesClick, + fetchReferencePages, + fetchMoreReferencePages, + onCloseDialog }: ProductCreatePageProps) => { const intl = useIntl(); @@ -115,6 +132,24 @@ export const ProductCreatePage: React.FC = ({ value: taxType.taxCode })) || []; + const canOpenAssignReferencesAttributeDialog = !!assignReferencesAttributeId; + + const handleAssignReferenceAttribute = ( + attributeValues: string[], + data: ProductCreateData, + handlers: ProductCreateHandlers + ) => { + handlers.selectAttributeReference( + assignReferencesAttributeId, + mergeAttributeValues( + assignReferencesAttributeId, + attributeValues, + data.attributes + ) + ); + onCloseDialog(); + }; + return ( = ({ categories={categories} collections={collections} productTypes={productTypeChoiceList} + referencePages={referencePages} selectedCollections={selectedCollections} setSelectedCategory={setSelectedCategory} setSelectedCollections={setSelectedCollections} @@ -168,6 +204,8 @@ export const ProductCreatePage: React.FC = ({ onChange={handlers.selectAttribute} onMultiChange={handlers.selectAttributeMultiple} onFileChange={handlers.selectAttributeFile} + onReferencesRemove={handlers.selectAttributeReference} + onReferencesAddClick={onAssignReferencesClick} /> )} @@ -283,6 +321,28 @@ export const ProductCreatePage: React.FC = ({ state={saveButtonBarState} disabled={loading || !onSubmit || formDisabled || !hasChanged} /> + {canOpenAssignReferencesAttributeDialog && ( + + handleAssignReferenceAttribute( + attributeValues, + data, + handlers + ) + } + /> + )} ); }} diff --git a/src/products/components/ProductCreatePage/form.tsx b/src/products/components/ProductCreatePage/form.tsx index 4377c96ab..682a7775f 100644 --- a/src/products/components/ProductCreatePage/form.tsx +++ b/src/products/components/ProductCreatePage/form.tsx @@ -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 ) => void >, + Record<"selectAttributeReference", FormsetChange>, Record<"selectAttributeFile", FormsetChange>, 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, diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx index bc872118d..09913fbfb 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.test.tsx @@ -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, diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index 225aa4aca..1afe7b8e8 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -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 = ({ variants, warehouses, taxTypes, + referencePages, onBack, onDelete, onImageDelete, @@ -144,7 +158,12 @@ export const ProductUpdatePage: React.FC = ({ selectedChannelId, toggle, toggleAll, - toolbar + toolbar, + assignReferencesAttributeId, + onAssignReferencesClick, + fetchReferencePages, + fetchMoreReferencePages, + onCloseDialog }) => { const intl = useIntl(); @@ -169,6 +188,24 @@ export const ProductUpdatePage: React.FC = ({ value: taxType.taxCode })) || []; + const canOpenAssignReferencesAttributeDialog = !!assignReferencesAttributeId; + + const handleAssignReferenceAttribute = ( + attributeValues: string[], + data: ProductUpdateData, + handlers: ProductUpdateHandlers + ) => { + handlers.selectAttributeReference( + assignReferencesAttributeId, + mergeAttributeValues( + assignReferencesAttributeId, + attributeValues, + data.attributes + ) + ); + onCloseDialog(); + }; + return ( = ({ warehouses={warehouses} currentChannels={currentChannels} hasVariants={hasVariants} + referencePages={referencePages} > {({ change, @@ -227,6 +265,8 @@ export const ProductUpdatePage: React.FC = ({ onChange={handlers.selectAttribute} onMultiChange={handlers.selectAttributeMultiple} onFileChange={handlers.selectAttributeFile} + onReferencesRemove={handlers.selectAttributeReference} + onReferencesAddClick={onAssignReferencesClick} /> )} @@ -362,6 +402,28 @@ export const ProductUpdatePage: React.FC = ({ disabled || formDisabled || (!hasChanged && !hasChannelChanged) } /> + {canOpenAssignReferencesAttributeDialog && ( + + handleAssignReferenceAttribute( + attributeValues, + data, + handlers + ) + } + /> + )} )} diff --git a/src/products/components/ProductUpdatePage/form.tsx b/src/products/components/ProductUpdatePage/form.tsx index bdb7b8018..b271bf8a8 100644 --- a/src/products/components/ProductUpdatePage/form.tsx +++ b/src/products/components/ProductUpdatePage/form.tsx @@ -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 ) => void >, + Record<"selectAttributeReference", FormsetChange>, Record<"selectAttributeFile", FormsetChange>, 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 diff --git a/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx b/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx index 757640156..4354d9ddd 100644 --- a/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx +++ b/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx @@ -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 = ({ @@ -70,20 +84,45 @@ const ProductVariantCreatePage: React.FC = ({ 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 ( {({ change, @@ -138,6 +177,8 @@ const ProductVariantCreatePage: React.FC = ({ onChange={handlers.selectAttribute} onMultiChange={handlers.selectAttributeMultiple} onFileChange={handlers.selectAttributeFile} + onReferencesRemove={handlers.selectAttributeReference} + onReferencesAddClick={onAssignReferencesClick} /> = ({ onCancel={onBack} onSave={submit} /> + {canOpenAssignReferencesAttributeDialog && ( + + handleAssignReferenceAttribute(attributeValues, data, handlers) + } + /> + )} )} diff --git a/src/products/components/ProductVariantCreatePage/form.tsx b/src/products/components/ProductVariantCreatePage/form.tsx index 7373be6ad..c804d055b 100644 --- a/src/products/components/ProductVariantCreatePage/form.tsx +++ b/src/products/components/ProductVariantCreatePage/form.tsx @@ -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>, Record<"selectAttributeFile", FormsetChange>, 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 diff --git a/src/products/components/ProductVariantPage/ProductVariantPage.tsx b/src/products/components/ProductVariantPage/ProductVariantPage.tsx index 113e4bc32..960f64cf8 100644 --- a/src/products/components/ProductVariantPage/ProductVariantPage.tsx +++ b/src/products/components/ProductVariantPage/ProductVariantPage.tsx @@ -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 = ({ saveButtonBarState, variant, warehouses, + referencePages, onAdd, onBack, onDelete, @@ -105,7 +118,12 @@ const ProductVariantPage: React.FC = ({ onVariantClick, onVariantReorder, onSetDefaultVariant, - onWarehouseConfigure + onWarehouseConfigure, + assignReferencesAttributeId, + onAssignReferencesClick, + fetchReferencePages, + fetchMoreReferencePages, + onCloseDialog }) => { const intl = useIntl(); @@ -120,6 +138,24 @@ const ProductVariantPage: React.FC = ({ ?.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 ( <> @@ -136,6 +172,7 @@ const ProductVariantPage: React.FC = ({ onSubmit={onSubmit} warehouses={warehouses} currentChannels={channels} + referencePages={referencePages} > {({ change, @@ -178,6 +215,8 @@ const ProductVariantPage: React.FC = ({ onChange={handlers.selectAttribute} onMultiChange={handlers.selectAttributeMultiple} onFileChange={handlers.selectAttributeFile} + onReferencesRemove={handlers.selectAttributeReference} + onReferencesAddClick={onAssignReferencesClick} /> = ({ onChange={handlers.selectAttribute} onMultiChange={handlers.selectAttributeMultiple} onFileChange={handlers.selectAttributeFile} + onReferencesRemove={handlers.selectAttributeReference} + onReferencesAddClick={onAssignReferencesClick} /> = ({ onDelete={onDelete} onSave={submit} /> + {canOpenAssignReferencesAttributeDialog && ( + + handleAssignReferenceAttribute( + attributeValues, + data, + handlers + ) + } + /> + )} )} diff --git a/src/products/components/ProductVariantPage/form.tsx b/src/products/components/ProductVariantPage/form.tsx index b6a9bbeac..35e7c0845 100644 --- a/src/products/components/ProductVariantPage/form.tsx +++ b/src/products/components/ProductVariantPage/form.tsx @@ -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>, Record<"selectAttributeFile", FormsetChange>, 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 diff --git a/src/products/index.tsx b/src/products/index.tsx index 8663604c3..58134544a 100644 --- a/src/products/index.tsx +++ b/src/products/index.tsx @@ -18,6 +18,7 @@ import { productPath, ProductUrlQueryParams, productVariantAddPath, + ProductVariantAddUrlQueryParams, productVariantCreatorPath, productVariantEditPath, ProductVariantEditUrlQueryParams @@ -99,11 +100,17 @@ const ProductImage: React.FC> = ({ const ProductVariantCreate: React.FC> = ({ match -}) => ( - -); +}) => { + const qs = parseQs(location.search.substr(1)); + const params: ProductVariantAddUrlQueryParams = qs; + + return ( + + ); +}; const ProductVariantCreator: React.FC productListPath + "?" + stringifyQs(params); export const productPath = (id: string) => urlJoin(productSection + id); -export type ProductUrlDialog = "remove" | "remove-variants" | ChannelsAction; -export type ProductUrlQueryParams = BulkAction & Dialog; -export type ProductCreateUrlQueryParams = Dialog; +export type ProductUrlDialog = + | "remove" + | "remove-variants" + | "assign-attribute-value" + | ChannelsAction; +export type ProductUrlQueryParams = BulkAction & + Dialog & + SingleAction; +export type ProductCreateUrlDialog = "assign-attribute-value" | ChannelsAction; +export type ProductCreateUrlQueryParams = Dialog & + 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); diff --git a/src/products/utils/data.ts b/src/products/utils/data.ts index 98717ec8c..08f28e058 100644 --- a/src/products/utils/data.ts +++ b/src/products/utils/data.ts @@ -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,18 +116,22 @@ export function getAttributeInputFromSelectedAttributes( variantAttributes: SelectedVariantAttributeFragment[], variantAttributeScope: VariantAttributeScope ): AttributeInput[] { - return variantAttributes?.map(attribute => ({ - data: { - inputType: attribute.attribute.inputType, - isRequired: attribute.attribute.valueRequired, - selectedValues: attribute.values, - values: attribute.attribute.values, - variantAttributeScope - }, - id: attribute.attribute.id, - label: attribute.attribute.name, - value: [(attribute.values.length && attribute.values[0]?.slug) || null] - })); + return variantAttributes?.map(attribute => { + const value = attribute.values.length > 0 && attribute.values[0]?.slug; + + return { + data: { + inputType: attribute.attribute.inputType, + isRequired: attribute.attribute.valueRequired, + selectedValues: attribute.values, + values: attribute.attribute.values, + variantAttributeScope + }, + id: attribute.attribute.id, + label: attribute.attribute.name, + value: value ? [value] : undefined + }; + }); } export function getAttributeInputFromVariant( @@ -217,26 +221,6 @@ export function getChoices(nodes: Node[]): SingleAutocompleteChoiceType[] { ); } -export const getAttributesDisplayData = ( - attributes: AttributeInput[], - attributesWithNewFileValue: FormsetData -) => - 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; diff --git a/src/products/utils/handlers.ts b/src/products/utils/handlers.ts index a07f440d5..40e79b80c 100644 --- a/src/products/utils/handlers.ts +++ b/src/products/utils/handlers.ts @@ -124,6 +124,16 @@ export function createAttributeMultiChangeHandler( }; } +export function createAttributeReferenceChangeHandler( + changeAttributeData: FormsetChange, + triggerChange: () => void +): FormsetChange { + return (attributeId: string, values: string[]) => { + changeAttributeData(attributeId, values); + triggerChange(); + }; +} + export function createAttributeFileChangeHandler( changeAttributeData: FormsetChange, attributesWithNewFileValue: FormsetData>, diff --git a/src/products/views/ProductCreate/ProductCreate.tsx b/src/products/views/ProductCreate/ProductCreate.tsx index a8e49afd4..03150579e 100644 --- a/src/products/views/ProductCreate/ProductCreate.tsx +++ b/src/products/views/ProductCreate/ProductCreate.tsx @@ -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 = ({ params }) => { ); const [openModal, closeModal] = createDialogActionHandlers< - ChannelsAction, + ProductCreateUrlDialog, ProductCreateUrlQueryParams >(navigate, params => productAddUrl(params), params); @@ -80,6 +82,13 @@ export const ProductCreateView: React.FC = ({ 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 = ({ 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 = ({ 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 ( <> = ({ 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 = ({ 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 = ({ 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())} /> ); diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index de1553425..9bca2a110 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 ( <> @@ -413,19 +446,21 @@ export const ProductUpdate: React.FC = ({ 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))} /> = ({ 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 ( <> @@ -303,6 +328,18 @@ export const ProductVariant: React.FC = ({ 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)) + } /> = ({ - productId + productId, + params }) => { const navigate = useNavigator(); const notify = useNotifier(); @@ -172,6 +183,28 @@ export const ProductVariant: React.FC = ({ 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 = ({ 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))} /> ); diff --git a/src/storybook/stories/pages/PageDetailsPage.tsx b/src/storybook/stories/pages/PageDetailsPage.tsx index 35f876fdc..8e70f05ab 100644 --- a/src/storybook/stories/pages/PageDetailsPage.tsx +++ b/src/storybook/stories/pages/PageDetailsPage.tsx @@ -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" }; diff --git a/src/storybook/stories/products/ProductCreatePage.tsx b/src/storybook/stories/products/ProductCreatePage.tsx index 3bf2c845e..ba6b9cc4b 100644 --- a/src/storybook/stories/products/ProductCreatePage.tsx +++ b/src/storybook/stories/products/ProductCreatePage.tsx @@ -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} /> )); diff --git a/src/storybook/stories/products/ProductUpdatePage.tsx b/src/storybook/stories/products/ProductUpdatePage.tsx index 2259779ae..da6134cb2 100644 --- a/src/storybook/stories/products/ProductUpdatePage.tsx +++ b/src/storybook/stories/products/ProductUpdatePage.tsx @@ -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, diff --git a/src/storybook/stories/products/ProductVariantCreatePage.tsx b/src/storybook/stories/products/ProductVariantCreatePage.tsx index 25998faba..e75232693 100644 --- a/src/storybook/stories/products/ProductVariantCreatePage.tsx +++ b/src/storybook/stories/products/ProductVariantCreatePage.tsx @@ -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} /> )); diff --git a/src/storybook/stories/products/ProductVariantPage.tsx b/src/storybook/stories/products/ProductVariantPage.tsx index 86722a224..0b0f50086 100644 --- a/src/storybook/stories/products/ProductVariantPage.tsx +++ b/src/storybook/stories/products/ProductVariantPage.tsx @@ -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} /> ));