From bb441ea11a932c3df94ed67ef47be622ddcd6da3 Mon Sep 17 00:00:00 2001 From: mmarkusik Date: Tue, 11 May 2021 14:26:17 +0200 Subject: [PATCH] Add product / page delete warning (#1095) * Add Delete button component * Add product / page type delete warning dialog * Replace old product types delete dialog with new one, add products total count query * Update schema, types and queries for pages, add use page count query and add warning delete dialog to page types * Move type delete warning dialog data to proper hooks, refactor * Remove unused components and stories * Add plural forms to messages for product / page type delete warning, refactor * Add type delete warning dialog stories * Move type delete hooks to proper directiories, fix imports * Fix imports * Remove countallproducts query and instead use useproductcountquery * Remove unnecessary types and imports --- schema.graphql | 1 + .../ChannelsAvailabilityCardWrapper.tsx | 2 +- src/components/DeleteButton/DeleteButton.tsx | 47 +++++++ src/components/DeleteButton/index.tsx | 2 + .../TypeDeleteWarningDialog.stories.tsx | 40 ++++++ .../TypeDeleteWarningDialog.tsx | 132 ++++++++++++++++++ .../TypeDeleteWarningDialogContent.tsx | 95 +++++++++++++ .../TypeDeleteWarningDialog/index.tsx | 2 + .../TypeDeleteWarningDialog/styles.ts | 23 +++ .../TypeDeleteWarningDialog/types.ts | 10 ++ .../OrderDiscountCommonModal/ModalTitle.tsx | 25 +++- .../PageTypeDeleteDialog.tsx | 61 -------- .../components/PageTypeDeleteDialog/index.ts | 2 - .../hooks/usePageTypeDelete/index.ts | 2 + .../hooks/usePageTypeDelete/messages.ts | 59 ++++++++ .../hooks/usePageTypeDelete/types.ts | 16 +++ .../usePageTypeDelete/usePageTypeDelete.tsx | 64 +++++++++ src/pageTypes/views/PageTypeDetails.tsx | 99 +++++++------ .../views/PageTypeList/PageTypeList.tsx | 40 +++--- .../views/PageTypeUpdate/PageTypeUpdate.tsx | 0 src/pages/queries.ts | 13 ++ src/pages/types/PageCount.ts | 23 +++ src/pages/urls.ts | 18 ++- .../ProductTypeDeleteDialog.tsx | 48 ------- .../ProductTypeDeleteDialog/index.ts | 2 - .../hooks/useProductTypeDelete/index.ts | 2 + .../hooks/useProductTypeDelete/messages.ts | 60 ++++++++ .../useProductTypeDelete.tsx | 68 +++++++++ .../views/ProductTypeList/ProductTypeList.tsx | 63 ++++----- .../views/ProductTypeUpdate/index.tsx | 121 ++++++++-------- src/products/queries.ts | 16 ++- .../{CountAllProducts.ts => ProductCount.ts} | 14 +- .../views/ProductList/ProductList.tsx | 4 +- src/storybook/config.js | 1 - .../productTypes/ProductTypeDeleteDialog.tsx | 19 --- src/types.ts | 2 + src/types/globalTypes.ts | 6 + 37 files changed, 900 insertions(+), 302 deletions(-) create mode 100644 src/components/DeleteButton/DeleteButton.tsx create mode 100644 src/components/DeleteButton/index.tsx create mode 100644 src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialog.stories.tsx create mode 100644 src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialog.tsx create mode 100644 src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialogContent.tsx create mode 100644 src/components/TypeDeleteWarningDialog/index.tsx create mode 100644 src/components/TypeDeleteWarningDialog/styles.ts create mode 100644 src/components/TypeDeleteWarningDialog/types.ts delete mode 100644 src/pageTypes/components/PageTypeDeleteDialog/PageTypeDeleteDialog.tsx delete mode 100644 src/pageTypes/components/PageTypeDeleteDialog/index.ts create mode 100644 src/pageTypes/hooks/usePageTypeDelete/index.ts create mode 100644 src/pageTypes/hooks/usePageTypeDelete/messages.ts create mode 100644 src/pageTypes/hooks/usePageTypeDelete/types.ts create mode 100644 src/pageTypes/hooks/usePageTypeDelete/usePageTypeDelete.tsx delete mode 100644 src/pageTypes/views/PageTypeUpdate/PageTypeUpdate.tsx create mode 100644 src/pages/types/PageCount.ts delete mode 100644 src/productTypes/components/ProductTypeDeleteDialog/ProductTypeDeleteDialog.tsx delete mode 100644 src/productTypes/components/ProductTypeDeleteDialog/index.ts create mode 100644 src/productTypes/hooks/useProductTypeDelete/index.ts create mode 100644 src/productTypes/hooks/useProductTypeDelete/messages.ts create mode 100644 src/productTypes/hooks/useProductTypeDelete/useProductTypeDelete.tsx rename src/products/types/{CountAllProducts.ts => ProductCount.ts} (50%) delete mode 100644 src/storybook/stories/productTypes/ProductTypeDeleteDialog.tsx diff --git a/schema.graphql b/schema.graphql index 928c5a8c6..5943a4045 100644 --- a/schema.graphql +++ b/schema.graphql @@ -3579,6 +3579,7 @@ enum PageErrorCode { } input PageFilterInput { + pageTypes: [ID!] search: String metadata: [MetadataInput] } diff --git a/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCardWrapper.tsx b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCardWrapper.tsx index 1436d598e..fdd84e410 100644 --- a/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCardWrapper.tsx +++ b/src/components/ChannelsAvailabilityCard/ChannelsAvailabilityCardWrapper.tsx @@ -53,7 +53,7 @@ export const ChannelsAvailabilityWrapper: React.FC + ); +}; + +export default DeleteButton; diff --git a/src/components/DeleteButton/index.tsx b/src/components/DeleteButton/index.tsx new file mode 100644 index 000000000..185d2bc63 --- /dev/null +++ b/src/components/DeleteButton/index.tsx @@ -0,0 +1,2 @@ +export * from "./DeleteButton"; +export { default } from "./DeleteButton"; diff --git a/src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialog.stories.tsx b/src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialog.stories.tsx new file mode 100644 index 000000000..129a9fa17 --- /dev/null +++ b/src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialog.stories.tsx @@ -0,0 +1,40 @@ +import CentralPlacementDecorator from "@saleor/storybook/CentralPlacementDecorator"; +import CommonDecorator from "@saleor/storybook/Decorator"; +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import * as messages from "../../pageTypes/hooks/usePageTypeDelete/messages"; +import TypeDeleteWarningDialog, { + TypeBaseData, + TypeDeleteWarningDialogProps +} from "./TypeDeleteWarningDialog"; + +const props: TypeDeleteWarningDialogProps = { + ...messages, + isOpen: true, + onClose: () => undefined, + onDelete: () => undefined, + typesData: [{ id: "id-1", name: "Interesting Pages" }], + isLoading: false, + assignedItemsCount: 4, + typesToDelete: ["id-1"], + viewAssignedItemsUrl: "some-url", + deleteButtonState: "default" +}; + +storiesOf("TypeDeleteWarningDialog.stories", module) + .addDecorator(CommonDecorator) + .addDecorator(CentralPlacementDecorator) + .add("loading", () => ) + .add("single type no assigned items", () => ( + + )) + .add("single type some assigned items", () => ( + + )) + .add("multiple type no assigned items", () => ( + + )) + .add("multiple types some assigned items", () => ( + + )); diff --git a/src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialog.tsx b/src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialog.tsx new file mode 100644 index 000000000..59b582e0c --- /dev/null +++ b/src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialog.tsx @@ -0,0 +1,132 @@ +import { CardContent } from "@material-ui/core"; +import Card from "@material-ui/core/Card"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import Modal from "@material-ui/core/Modal"; +import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; +import ModalTitle from "@saleor/orders/components/OrderDiscountCommonModal/ModalTitle"; +import { getById } from "@saleor/orders/components/OrderReturnPage/utils"; +import React from "react"; +import { useIntl } from "react-intl"; + +import { useTypeDeleteWarningDialogStyles as useStyles } from "./styles"; +import ProductTypeDeleteWarningDialogContent from "./TypeDeleteWarningDialogContent"; +import { + CommonTypeDeleteWarningMessages, + TypeDeleteWarningMessages +} from "./types"; + +export interface TypeBaseData { + id: string; + name: string; +} + +export interface TypeDeleteMessages { + baseMessages: CommonTypeDeleteWarningMessages; + singleWithItemsMessages: TypeDeleteWarningMessages; + singleWithoutItemsMessages: TypeDeleteWarningMessages; + multipleWithItemsMessages: TypeDeleteWarningMessages; + multipleWithoutItemsMessages: TypeDeleteWarningMessages; +} + +export interface TypeDeleteWarningDialogProps + extends TypeDeleteMessages { + isOpen: boolean; + deleteButtonState: ConfirmButtonTransitionState; + onClose: () => void; + onDelete: () => void; + viewAssignedItemsUrl: string; + typesToDelete: string[]; + assignedItemsCount: number | undefined; + isLoading?: boolean; + typesData: T[]; + // temporary, until we add filters to pages list - SALEOR-3279 + showViewAssignedItemsButton?: boolean; +} + +function TypeDeleteWarningDialog({ + isLoading = false, + isOpen, + baseMessages, + singleWithItemsMessages, + singleWithoutItemsMessages, + multipleWithItemsMessages, + multipleWithoutItemsMessages, + onClose, + onDelete, + assignedItemsCount, + viewAssignedItemsUrl, + typesToDelete, + typesData, + showViewAssignedItemsButton = true +}: TypeDeleteWarningDialogProps) { + const intl = useIntl(); + const classes = useStyles({}); + + const showMultiple = typesToDelete.length > 1; + + const hasAssignedItems = !!assignedItemsCount; + + const selectMessages = () => { + if (showMultiple) { + const multipleMessages = hasAssignedItems + ? multipleWithItemsMessages + : multipleWithoutItemsMessages; + + return { + ...multipleMessages + }; + } + + const singleMessages = hasAssignedItems + ? singleWithItemsMessages + : singleWithoutItemsMessages; + + return { + ...singleMessages + }; + }; + + const { description, consentLabel } = selectMessages(); + + const singleItemSelectedId = typesToDelete[0]; + + const singleItemSelectedName = typesData.find(getById(singleItemSelectedId)) + ?.name; + + return ( + +
+ + + {isLoading ? ( + + + + ) : ( + + )} + +
+
+ ); +} + +export default TypeDeleteWarningDialog; diff --git a/src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialogContent.tsx b/src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialogContent.tsx new file mode 100644 index 000000000..6c2b19a6c --- /dev/null +++ b/src/components/TypeDeleteWarningDialog/TypeDeleteWarningDialogContent.tsx @@ -0,0 +1,95 @@ +import CardContent from "@material-ui/core/CardContent"; +import Typography from "@material-ui/core/Typography"; +import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer"; +import CardSpacer from "@saleor/components/CardSpacer"; +import ConfirmButton from "@saleor/components/ConfirmButton"; +import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; +import DeleteButton from "@saleor/components/DeleteButton"; +import useNavigator from "@saleor/hooks/useNavigator"; +import React, { ChangeEvent, useState } from "react"; +import { MessageDescriptor, useIntl } from "react-intl"; + +import { useTypeDeleteWarningDialogStyles as useStyles } from "./styles"; + +interface TypeDeleteWarningDialogContentProps { + singleItemSelectedName?: string; + viewAssignedItemsButtonLabel: MessageDescriptor; + description: MessageDescriptor; + consentLabel: MessageDescriptor; + viewAssignedItemsUrl: string; + hasAssignedItems: boolean; + assignedItemsCount: number | undefined; + onDelete: () => void; + // temporary, until we add filters to pages list - SALEOR-3279 + showViewAssignedItemsButton?: boolean; +} + +const TypeDeleteWarningDialogContent: React.FC = ({ + description, + consentLabel, + viewAssignedItemsUrl, + viewAssignedItemsButtonLabel, + singleItemSelectedName, + hasAssignedItems, + assignedItemsCount, + onDelete, + showViewAssignedItemsButton +}) => { + const classes = useStyles({}); + const intl = useIntl(); + const navigate = useNavigator(); + + const [isConsentChecked, setIsConsentChecked] = useState(false); + + const handleConsentChange = ({ target }: ChangeEvent) => + setIsConsentChecked(target.value); + + const handleViewAssignedItems = () => navigate(viewAssignedItemsUrl); + + const isDisbled = hasAssignedItems ? !isConsentChecked : false; + + const shouldShowViewAssignedItemsButton = + showViewAssignedItemsButton && hasAssignedItems; + + return ( + + + {intl.formatMessage(description, { + typeName: singleItemSelectedName, + assignedItemsCount, + b: (...chunks) => {chunks} + })} + + + {consentLabel && ( + + {intl.formatMessage(consentLabel)} + + } + /> + )} + +
+ {shouldShowViewAssignedItemsButton && ( + <> + + {intl.formatMessage(viewAssignedItemsButtonLabel)} + + + + )} + +
+
+ ); +}; + +export default TypeDeleteWarningDialogContent; diff --git a/src/components/TypeDeleteWarningDialog/index.tsx b/src/components/TypeDeleteWarningDialog/index.tsx new file mode 100644 index 000000000..73ce09808 --- /dev/null +++ b/src/components/TypeDeleteWarningDialog/index.tsx @@ -0,0 +1,2 @@ +export * from "./TypeDeleteWarningDialog"; +export { default } from "./TypeDeleteWarningDialog"; diff --git a/src/components/TypeDeleteWarningDialog/styles.ts b/src/components/TypeDeleteWarningDialog/styles.ts new file mode 100644 index 000000000..0e5e83c14 --- /dev/null +++ b/src/components/TypeDeleteWarningDialog/styles.ts @@ -0,0 +1,23 @@ +import { makeStyles } from "@saleor/theme"; + +export const useTypeDeleteWarningDialogStyles = makeStyles( + theme => ({ + centerContainer: { + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "100%" + }, + content: { + width: 600 + }, + consentLabel: { + color: theme.palette.primary.main + }, + buttonsSection: { + display: "flex", + justifyContent: "flex-end" + } + }), + { name: "ProductTypeDeleteWarningDialog" } +); diff --git a/src/components/TypeDeleteWarningDialog/types.ts b/src/components/TypeDeleteWarningDialog/types.ts new file mode 100644 index 000000000..d5c8cce8b --- /dev/null +++ b/src/components/TypeDeleteWarningDialog/types.ts @@ -0,0 +1,10 @@ +import { MessageDescriptor } from "react-intl"; + +export type CommonTypeDeleteWarningMessages = Record< + "title" | "viewAssignedItemsButtonLabel", + MessageDescriptor +>; + +export type TypeDeleteWarningMessages = Partial< + Record<"description" | "consentLabel", MessageDescriptor> +>; diff --git a/src/orders/components/OrderDiscountCommonModal/ModalTitle.tsx b/src/orders/components/OrderDiscountCommonModal/ModalTitle.tsx index 99da31e29..cec6f6f6d 100644 --- a/src/orders/components/OrderDiscountCommonModal/ModalTitle.tsx +++ b/src/orders/components/OrderDiscountCommonModal/ModalTitle.tsx @@ -1,5 +1,7 @@ +import Divider from "@material-ui/core/Divider"; import Typography from "@material-ui/core/Typography"; import CloseIcon from "@material-ui/icons/Close"; +import CardSpacer from "@saleor/components/CardSpacer"; import { makeStyles } from "@saleor/theme"; import React from "react"; @@ -19,16 +21,29 @@ const useStyles = makeStyles( interface ModalTitleProps { title: string; onClose: () => void; + withBorder?: boolean; } -const ModalTitle: React.FC = ({ title, onClose }) => { +const ModalTitle: React.FC = ({ + title, + onClose, + withBorder = false +}) => { const classes = useStyles({}); return ( -
- {title} - -
+ <> +
+ {title} + +
+ {withBorder && ( + <> + + + + )} + ); }; diff --git a/src/pageTypes/components/PageTypeDeleteDialog/PageTypeDeleteDialog.tsx b/src/pageTypes/components/PageTypeDeleteDialog/PageTypeDeleteDialog.tsx deleted file mode 100644 index 8cef40620..000000000 --- a/src/pageTypes/components/PageTypeDeleteDialog/PageTypeDeleteDialog.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import DialogContentText from "@material-ui/core/DialogContentText"; -import ActionDialog from "@saleor/components/ActionDialog"; -import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; -import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; - -export interface PageTypeDeleteDialogProps { - confirmButtonState: ConfirmButtonTransitionState; - open: boolean; - name: string; - hasPages: boolean; - onClose: () => void; - onConfirm: () => void; -} - -const PageTypeDeleteDialog: React.FC = ({ - confirmButtonState, - open, - name, - hasPages, - onClose, - onConfirm -}) => { - const intl = useIntl(); - - return ( - - - {hasPages ? ( - {name} - }} - /> - ) : ( - {name} - }} - /> - )} - - - ); -}; -PageTypeDeleteDialog.displayName = "PageTypeDeleteDialog"; -export default PageTypeDeleteDialog; diff --git a/src/pageTypes/components/PageTypeDeleteDialog/index.ts b/src/pageTypes/components/PageTypeDeleteDialog/index.ts deleted file mode 100644 index 24fe8502b..000000000 --- a/src/pageTypes/components/PageTypeDeleteDialog/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./PageTypeDeleteDialog"; -export * from "./PageTypeDeleteDialog"; diff --git a/src/pageTypes/hooks/usePageTypeDelete/index.ts b/src/pageTypes/hooks/usePageTypeDelete/index.ts new file mode 100644 index 000000000..ecb78f329 --- /dev/null +++ b/src/pageTypes/hooks/usePageTypeDelete/index.ts @@ -0,0 +1,2 @@ +export * from "./usePageTypeDelete"; +export { default } from "./usePageTypeDelete"; diff --git a/src/pageTypes/hooks/usePageTypeDelete/messages.ts b/src/pageTypes/hooks/usePageTypeDelete/messages.ts new file mode 100644 index 000000000..46de4491d --- /dev/null +++ b/src/pageTypes/hooks/usePageTypeDelete/messages.ts @@ -0,0 +1,59 @@ +import { defineMessages } from "react-intl"; + +export const baseMessages = defineMessages({ + title: { + defaultMessage: + "Delete page {selectedTypesCount,plural,one{type} other{types}}", + description: "ProductTypeDeleteWarningDialog title" + }, + viewAssignedItemsButtonLabel: { + defaultMessage: "View pages", + description: + "ProductTypeDeleteWarningDialog single assigned items button label" + } +}); + +export const singleWithItemsMessages = defineMessages({ + description: { + defaultMessage: + "You are about to delete page type {typeName}. It is assigned to {assignedItemsCount} {assignedItemsCount,plural,one{page} other{pages}}. Deleting this page type will also delete those pages. Are you sure you want to do this?", + description: + "ProductTypeDeleteWarningDialog single assigned items description" + }, + consentLabel: { + defaultMessage: "Yes, I want to delete this page type and assigned pages", + description: "ProductTypeDeleteWarningDialog single consent label" + } +}); + +export const multipleWithItemsMessages = defineMessages({ + description: { + defaultMessage: + "You are about to delete multiple page types. Some of them are assigned to pages. Deleting those page types will also delete those pages", + description: + "ProductTypeDeleteWarningDialog with items multiple description" + }, + consentLabel: { + defaultMessage: + "Yes, I want to delete those pages types and assigned pages", + description: "ProductTypeDeleteWarningDialog multiple consent label" + } +}); + +export const singleWithoutItemsMessages = defineMessages({ + description: { + defaultMessage: + "Are you sure you want to delete {typeName}? If you remove it you won’t be able to assign it to created pages.", + description: + "ProductTypeDeleteWarningDialog single assigned items description" + } +}); + +export const multipleWithoutItemsMessages = defineMessages({ + description: { + defaultMessage: + "Are you sure you want to delete selected page types? If you remove them you won’t be able to assign them to created pages.", + description: + "ProductTypeDeleteWarningDialog single assigned items description" + } +}); diff --git a/src/pageTypes/hooks/usePageTypeDelete/types.ts b/src/pageTypes/hooks/usePageTypeDelete/types.ts new file mode 100644 index 000000000..a89455cd4 --- /dev/null +++ b/src/pageTypes/hooks/usePageTypeDelete/types.ts @@ -0,0 +1,16 @@ +import { TypeDeleteMessages } from "@saleor/components/TypeDeleteWarningDialog"; +import { Ids } from "@saleor/types"; + +export interface UseTypeDeleteData extends TypeDeleteMessages { + isOpen: boolean; + assignedItemsCount: number | undefined; + viewAssignedItemsUrl: string; + isLoading: boolean | undefined; + typesToDelete: Ids; +} + +export interface UseTypeDeleteProps { + params: T; + selectedTypes?: Ids; + singleId?: string; +} diff --git a/src/pageTypes/hooks/usePageTypeDelete/usePageTypeDelete.tsx b/src/pageTypes/hooks/usePageTypeDelete/usePageTypeDelete.tsx new file mode 100644 index 000000000..9849aa51b --- /dev/null +++ b/src/pageTypes/hooks/usePageTypeDelete/usePageTypeDelete.tsx @@ -0,0 +1,64 @@ +import { usePageCountQuery } from "@saleor/pages/queries"; +import { PageCountVariables } from "@saleor/pages/types/PageCount"; +import { pageListUrl } from "@saleor/pages/urls"; +import { + PageTypeListUrlQueryParams, + PageTypeUrlQueryParams +} from "@saleor/pageTypes/urls"; +import React from "react"; + +import * as messages from "./messages"; +import { UseTypeDeleteData, UseTypeDeleteProps } from "./types"; + +type UsePageTypeDeleteProps< + T = PageTypeListUrlQueryParams | PageTypeUrlQueryParams +> = UseTypeDeleteProps; + +function usePageTypeDelete({ + singleId, + params, + selectedTypes +}: UsePageTypeDeleteProps): UseTypeDeleteData { + const pageTypes = selectedTypes || [singleId]; + + const isDeleteDialogOpen = params.action === "remove"; + + const pagesAssignedToSelectedTypesQueryVars = React.useMemo< + PageCountVariables + >( + () => ({ + filter: { + pageTypes + } + }), + [pageTypes] + ); + + const shouldSkipPageListQuery = !pageTypes.length || !isDeleteDialogOpen; + + const { + data: pagesAssignedToSelectedTypesData, + loading: loadingPagesAssignedToSelectedTypes + } = usePageCountQuery({ + variables: pagesAssignedToSelectedTypesQueryVars, + skip: shouldSkipPageListQuery + }); + + const selectedPagesAssignedToDeleteUrl = pageListUrl({ + pageTypes + }); + + const assignedItemsCount = + pagesAssignedToSelectedTypesData?.pages?.totalCount; + + return { + ...messages, + isOpen: isDeleteDialogOpen, + assignedItemsCount, + viewAssignedItemsUrl: selectedPagesAssignedToDeleteUrl, + isLoading: loadingPagesAssignedToSelectedTypes, + typesToDelete: pageTypes + }; +} + +export default usePageTypeDelete; diff --git a/src/pageTypes/views/PageTypeDetails.tsx b/src/pageTypes/views/PageTypeDetails.tsx index 7d2c0c32d..1574dc353 100644 --- a/src/pageTypes/views/PageTypeDetails.tsx +++ b/src/pageTypes/views/PageTypeDetails.tsx @@ -4,6 +4,7 @@ import AssignAttributeDialog from "@saleor/components/AssignAttributeDialog"; import AttributeUnassignDialog from "@saleor/components/AttributeUnassignDialog"; import BulkAttributeUnassignDialog from "@saleor/components/BulkAttributeUnassignDialog"; import NotFoundPage from "@saleor/components/NotFoundPage"; +import TypeDeleteWarningDialog from "@saleor/components/TypeDeleteWarningDialog"; import { WindowTitle } from "@saleor/components/WindowTitle"; import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config"; import useBulkActions from "@saleor/hooks/useBulkActions"; @@ -11,7 +12,6 @@ import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import { commonMessages } from "@saleor/intl"; import { getStringOrPlaceholder } from "@saleor/misc"; -import PageTypeDeleteDialog from "@saleor/pageTypes/components/PageTypeDeleteDialog"; import { useAssignPageAttributeMutation, usePageTypeAttributeReorderMutation, @@ -33,6 +33,7 @@ import PageTypeDetailsPage, { PageTypeForm } from "../components/PageTypeDetailsPage"; import useAvailablePageAttributeSearch from "../hooks/useAvailablePageAttributeSearch"; +import usePageTypeDelete from "../hooks/usePageTypeDelete"; import { usePageTypeDetailsQuery } from "../queries"; import { pageTypeListUrl, pageTypeUrl, PageTypeUrlQueryParams } from "../urls"; @@ -184,6 +185,11 @@ export const PageTypeDetails: React.FC = ({ const loading = updatePageTypeOpts.loading || dataLoading; + const pageTypeDeleteData = usePageTypeDelete({ + singleId: id, + params + }); + return ( <> @@ -245,50 +251,55 @@ export const PageTypeDetails: React.FC = ({ ) }} /> - navigate(pageTypeUrl(id))} - onConfirm={handlePageTypeDelete} - /> + {!dataLoading && ( - edge.node - )} - confirmButtonState={assignAttributeOpts.status} - errors={ - assignAttributeOpts.data?.pageAttributeAssign.errors - ? assignAttributeOpts.data.pageAttributeAssign.errors.map(err => - getPageErrorMessage(err, intl) - ) - : [] - } - loading={result.loading} - onClose={closeModal} - onSubmit={handleAssignAttribute} - onFetch={search} - onFetchMore={loadMore} - onOpen={result.refetch} - hasMore={ - !!result.data?.pageType.availableAttributes.pageInfo.hasNextPage - } - open={params.action === "assign-attribute"} - selected={params.ids || []} - onToggle={attributeId => { - const ids = params.ids || []; - navigate( - pageTypeUrl(id, { - ...params, - ids: ids.includes(attributeId) - ? params.ids.filter(selectedId => selectedId !== attributeId) - : [...ids, attributeId] - }) - ); - }} - /> + <> + + edge.node + )} + confirmButtonState={assignAttributeOpts.status} + errors={ + assignAttributeOpts.data?.pageAttributeAssign.errors + ? assignAttributeOpts.data.pageAttributeAssign.errors.map(err => + getPageErrorMessage(err, intl) + ) + : [] + } + loading={result.loading} + onClose={closeModal} + onSubmit={handleAssignAttribute} + onFetch={search} + onFetchMore={loadMore} + onOpen={result.refetch} + hasMore={ + !!result.data?.pageType.availableAttributes.pageInfo.hasNextPage + } + open={params.action === "assign-attribute"} + selected={params.ids || []} + onToggle={attributeId => { + const ids = params.ids || []; + navigate( + pageTypeUrl(id, { + ...params, + ids: ids.includes(attributeId) + ? params.ids.filter( + selectedId => selectedId !== attributeId + ) + : [...ids, attributeId] + }) + ); + }} + /> + )} = ({ params }) => { const navigate = useNavigator(); const paginate = usePaginator(); const notify = useNotifier(); - const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( - params.ids - ); + const { + isSelected, + listElements: selectedPageTypes, + reset, + toggle, + toggleAll + } = useBulkActions(params.ids); const intl = useIntl(); const { settings } = useListSettings(ListViews.PAGES_LIST); @@ -155,10 +160,12 @@ export const PageTypeList: React.FC = ({ params }) => { } }); - const selectedPageTypesHasPages = data?.pageTypes.edges.some( - pageType => - pageType.node.hasPages && params.ids?.some(id => id === pageType.node.id) - ); + const pageTypeDeleteData = usePageTypeDelete({ + selectedTypes: selectedPageTypes, + params + }); + + const pageTypesData = data?.pageTypes?.edges.map(edge => edge.node) || []; return ( <> @@ -181,7 +188,7 @@ export const PageTypeList: React.FC = ({ params }) => { onRowClick={id => () => navigate(pageTypeUrl(id))} onSort={handleSort} isChecked={isSelected} - selected={listElements.length} + selected={selectedPageTypes.length} sort={getSortParams(params)} toggle={toggle} toggleAll={toggleAll} @@ -190,7 +197,7 @@ export const PageTypeList: React.FC = ({ params }) => { color="primary" onClick={() => openModal("remove", { - ids: listElements + ids: selectedPageTypes }) } > @@ -198,13 +205,14 @@ export const PageTypeList: React.FC = ({ params }) => { } /> - ( pageDetails ); + +const pageCountQuery = gql` + query PageCount($filter: PageFilterInput) { + pages(filter: $filter) { + totalCount + } + } +`; + +export const usePageCountQuery = makeQuery( + pageCountQuery +); diff --git a/src/pages/types/PageCount.ts b/src/pages/types/PageCount.ts new file mode 100644 index 000000000..19bb4d4e3 --- /dev/null +++ b/src/pages/types/PageCount.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { PageFilterInput } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL query operation: PageCount +// ==================================================== + +export interface PageCount_pages { + __typename: "PageCountableConnection"; + totalCount: number | null; +} + +export interface PageCount { + pages: PageCount_pages | null; +} + +export interface PageCountVariables { + filter?: PageFilterInput | null; +} diff --git a/src/pages/urls.ts b/src/pages/urls.ts index 748153f58..5474e5a57 100644 --- a/src/pages/urls.ts +++ b/src/pages/urls.ts @@ -1,7 +1,14 @@ import { stringify as stringifyQs } from "qs"; import urlJoin from "url-join"; -import { BulkAction, Dialog, Pagination, SingleAction, Sort } from "../types"; +import { + BulkAction, + Dialog, + FiltersWithMultipleValues, + Pagination, + SingleAction, + Sort +} from "../types"; export const pagesSection = "/pages/"; @@ -12,8 +19,17 @@ export enum PageListUrlSortField { slug = "slug", visible = "visible" } + +export enum PageListUrlFiltersWithMultipleValues { + pageTypes = "pageTypes" +} + +export type PageListUrlFilters = FiltersWithMultipleValues< + PageListUrlFiltersWithMultipleValues +>; export type PageListUrlSort = Sort; export type PageListUrlQueryParams = BulkAction & + PageListUrlFilters & Dialog & PageListUrlSort & Pagination; diff --git a/src/productTypes/components/ProductTypeDeleteDialog/ProductTypeDeleteDialog.tsx b/src/productTypes/components/ProductTypeDeleteDialog/ProductTypeDeleteDialog.tsx deleted file mode 100644 index 482f3ba98..000000000 --- a/src/productTypes/components/ProductTypeDeleteDialog/ProductTypeDeleteDialog.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import DialogContentText from "@material-ui/core/DialogContentText"; -import ActionDialog from "@saleor/components/ActionDialog"; -import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; -import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; - -export interface ProductTypeDeleteDialogProps { - confirmButtonState: ConfirmButtonTransitionState; - open: boolean; - name: string; - onClose: () => void; - onConfirm: () => void; -} - -const ProductTypeDeleteDialog: React.FC = ({ - confirmButtonState, - open, - name, - onClose, - onConfirm -}) => { - const intl = useIntl(); - - return ( - - - {name} - }} - /> - - - ); -}; -ProductTypeDeleteDialog.displayName = "ProductTypeDeleteDialog"; -export default ProductTypeDeleteDialog; diff --git a/src/productTypes/components/ProductTypeDeleteDialog/index.ts b/src/productTypes/components/ProductTypeDeleteDialog/index.ts deleted file mode 100644 index 70a362a77..000000000 --- a/src/productTypes/components/ProductTypeDeleteDialog/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./ProductTypeDeleteDialog"; -export * from "./ProductTypeDeleteDialog"; diff --git a/src/productTypes/hooks/useProductTypeDelete/index.ts b/src/productTypes/hooks/useProductTypeDelete/index.ts new file mode 100644 index 000000000..407501ce8 --- /dev/null +++ b/src/productTypes/hooks/useProductTypeDelete/index.ts @@ -0,0 +1,2 @@ +export * from "./useProductTypeDelete"; +export { default } from "./useProductTypeDelete"; diff --git a/src/productTypes/hooks/useProductTypeDelete/messages.ts b/src/productTypes/hooks/useProductTypeDelete/messages.ts new file mode 100644 index 000000000..adde90214 --- /dev/null +++ b/src/productTypes/hooks/useProductTypeDelete/messages.ts @@ -0,0 +1,60 @@ +import { defineMessages } from "react-intl"; + +export const baseMessages = defineMessages({ + title: { + defaultMessage: + "Delete product {selectedTypesCount,plural,one{type} other{types}}", + description: "ProductTypeDeleteWarningDialog title" + }, + viewAssignedItemsButtonLabel: { + defaultMessage: "View products", + description: + "ProductTypeDeleteWarningDialog single assigned items button label" + } +}); + +export const singleWithItemsMessages = defineMessages({ + description: { + defaultMessage: + "You are about to delete product type {typeName}. It is assigned to {assignedItemsCount} {assignedItemsCount,plural,one{product} other{products}}. Deleting this product type will also delete those products. Are you sure you want to do this?", + description: + "ProductTypeDeleteWarningDialog single assigned items description" + }, + consentLabel: { + defaultMessage: + "Yes, I want to delete this product type and assigned products", + description: "ProductTypeDeleteWarningDialog single consent label" + } +}); + +export const multipleWithItemsMessages = defineMessages({ + description: { + defaultMessage: + "You are about to delete multiple product types. Some of them are assigned to products. Deleting those product types will also delete those products", + description: + "ProductTypeDeleteWarningDialog with items multiple description" + }, + consentLabel: { + defaultMessage: + "Yes, I want to delete those products types and assigned products", + description: "ProductTypeDeleteWarningDialog multiple consent label" + } +}); + +export const singleWithoutItemsMessages = defineMessages({ + description: { + defaultMessage: + "Are you sure you want to delete {typeName}? If you remove it you won’t be able to assign it to created products.", + description: + "ProductTypeDeleteWarningDialog single assigned items description" + } +}); + +export const multipleWithoutItemsMessages = defineMessages({ + description: { + defaultMessage: + "Are you sure you want to delete selected product types? If you remove them you won’t be able to assign them to created products.", + description: + "ProductTypeDeleteWarningDialog single assigned items description" + } +}); diff --git a/src/productTypes/hooks/useProductTypeDelete/useProductTypeDelete.tsx b/src/productTypes/hooks/useProductTypeDelete/useProductTypeDelete.tsx new file mode 100644 index 000000000..b8b43dc38 --- /dev/null +++ b/src/productTypes/hooks/useProductTypeDelete/useProductTypeDelete.tsx @@ -0,0 +1,68 @@ +import { + UseTypeDeleteData, + UseTypeDeleteProps +} from "@saleor/pageTypes/hooks/usePageTypeDelete/types"; +import { useProductCountQuery } from "@saleor/products/queries"; +import { ProductCountVariables } from "@saleor/products/types/ProductCount"; +import { productListUrl } from "@saleor/products/urls"; +import { + ProductTypeListUrlQueryParams, + ProductTypeUrlQueryParams +} from "@saleor/productTypes/urls"; +import React from "react"; + +import * as messages from "./messages"; + +type UseProductTypeDeleteProps< + T = ProductTypeListUrlQueryParams | ProductTypeUrlQueryParams +> = UseTypeDeleteProps; + +function useProductTypeDelete({ + params, + singleId, + selectedTypes +}: UseProductTypeDeleteProps): UseTypeDeleteData { + const productTypes = selectedTypes || [singleId]; + + const isDeleteDialogOpen = params.action === "remove"; + + const productsAssignedToSelectedTypesQueryVars = React.useMemo< + ProductCountVariables + >( + () => ({ + filter: { + productTypes + } + }), + [productTypes] + ); + + const shouldSkipProductListQuery = + !productTypes.length || !isDeleteDialogOpen; + + const { + data: productsAssignedToSelectedTypesData, + loading: loadingProductsAssignedToSelectedTypes + } = useProductCountQuery({ + variables: productsAssignedToSelectedTypesQueryVars, + skip: shouldSkipProductListQuery + }); + + const selectedProductsAssignedToDeleteUrl = productListUrl({ + productTypes + }); + + const assignedItemsCount = + productsAssignedToSelectedTypesData?.products?.totalCount; + + return { + ...messages, + isOpen: isDeleteDialogOpen, + assignedItemsCount, + viewAssignedItemsUrl: selectedProductsAssignedToDeleteUrl, + isLoading: loadingProductsAssignedToSelectedTypes, + typesToDelete: productTypes + }; +} + +export default useProductTypeDelete; diff --git a/src/productTypes/views/ProductTypeList/ProductTypeList.tsx b/src/productTypes/views/ProductTypeList/ProductTypeList.tsx index e308af9ea..7e4654255 100644 --- a/src/productTypes/views/ProductTypeList/ProductTypeList.tsx +++ b/src/productTypes/views/ProductTypeList/ProductTypeList.tsx @@ -1,7 +1,5 @@ -import DialogContentText from "@material-ui/core/DialogContentText"; import IconButton from "@material-ui/core/IconButton"; import DeleteIcon from "@material-ui/icons/Delete"; -import ActionDialog from "@saleor/components/ActionDialog"; import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; import SaveFilterTabDialog, { SaveFilterTabDialogFormData @@ -14,14 +12,16 @@ import usePaginator, { createPaginationState } from "@saleor/hooks/usePaginator"; import { commonMessages } from "@saleor/intl"; +import useProductTypeDelete from "@saleor/productTypes/hooks/useProductTypeDelete"; import { ListViews } from "@saleor/types"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createFilterHandlers from "@saleor/utils/handlers/filterHandlers"; import createSortHandler from "@saleor/utils/handlers/sortHandler"; import { getSortParams } from "@saleor/utils/sort"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; +import TypeDeleteWarningDialog from "../../../components/TypeDeleteWarningDialog/TypeDeleteWarningDialog"; import { configurationMenuUrl } from "../../../configuration"; import { maybe } from "../../../misc"; import ProductTypeListPage from "../../components/ProductTypeListPage"; @@ -55,9 +55,14 @@ export const ProductTypeList: React.FC = ({ params }) => { const navigate = useNavigator(); const notify = useNotifier(); const paginate = usePaginator(); - const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( - params.ids - ); + const { + isSelected, + listElements: selectedProductTypes, + reset, + toggle, + toggleAll + } = useBulkActions(params.ids); + const { settings } = useListSettings(ListViews.PRODUCT_LIST); const intl = useIntl(); @@ -148,6 +153,14 @@ export const ProductTypeList: React.FC = ({ params }) => { const handleSort = createSortHandler(navigate, productTypeListUrl, params); + const productTypeDeleteData = useProductTypeDelete({ + selectedTypes: selectedProductTypes, + params + }); + + const productTypesData = + data?.productTypes?.edges.map(edge => edge.node) || []; + return ( = ({ params }) => { onTabSave={() => openModal("save-search")} tabs={tabs.map(tab => tab.name)} disabled={loading} - productTypes={maybe(() => - data.productTypes.edges.map(edge => edge.node) - )} + productTypes={productTypesData} pageInfo={pageInfo} onAdd={() => navigate(productTypeAddUrl)} onBack={() => navigate(configurationMenuUrl)} @@ -185,7 +196,7 @@ export const ProductTypeList: React.FC = ({ params }) => { onRowClick={id => () => navigate(productTypeUrl(id))} onSort={handleSort} isChecked={isSelected} - selected={listElements.length} + selected={selectedProductTypes.length} sort={getSortParams(params)} toggle={toggle} toggleAll={toggleAll} @@ -194,7 +205,7 @@ export const ProductTypeList: React.FC = ({ params }) => { color="primary" onClick={() => openModal("remove", { - ids: listElements + ids: selectedProductTypes }) } > @@ -202,30 +213,14 @@ export const ProductTypeList: React.FC = ({ params }) => { } /> - - - params.ids.length), - displayQuantity: ( - {maybe(() => params.ids.length)} - ) - }} - /> - - + onDelete={onProductTypeBulkDelete} + deleteButtonState={productTypeBulkDeleteOpts.status} + /> = ({ return result.data.productTypeUpdate.errors; }; + const productTypeDeleteData = useProductTypeDelete({ + singleId: id, + params + }); + return ( {({ data, loading: dataLoading }) => { @@ -335,62 +341,67 @@ export const ProductTypeUpdate: React.FC = ({ ) }} /> - {!dataLoading && - Object.keys(ProductAttributeType).map(key => ( - - result.data.productType.availableAttributes.edges.map( - edge => edge.node - ) - )} - confirmButtonState={assignAttribute.opts.status} - errors={maybe( - () => - assignAttribute.opts.data.productAttributeAssign.errors.map( - err => err.message - ), - [] - )} - loading={result.loading} + {!dataLoading && ( + <> + {Object.keys(ProductAttributeType).map(key => ( + + result.data.productType.availableAttributes.edges.map( + edge => edge.node + ) + )} + confirmButtonState={assignAttribute.opts.status} + errors={maybe( + () => + assignAttribute.opts.data.productAttributeAssign.errors.map( + err => err.message + ), + [] + )} + loading={result.loading} + onClose={closeModal} + onSubmit={handleAssignAttribute} + onFetch={search} + onFetchMore={loadMore} + onOpen={result.refetch} + hasMore={maybe( + () => + result.data.productType.availableAttributes + .pageInfo.hasNextPage, + false + )} + open={ + params.action === "assign-attribute" && + params.type === ProductAttributeType[key] + } + selected={maybe(() => params.ids, [])} + onToggle={attributeId => { + const ids = maybe(() => params.ids, []); + navigate( + productTypeUrl(id, { + ...params, + ids: ids.includes(attributeId) + ? params.ids.filter( + selectedId => selectedId !== attributeId + ) + : [...ids, attributeId] + }) + ); + }} + key={key} + /> + ))} + - result.data.productType.availableAttributes.pageInfo - .hasNextPage, - false - )} - open={ - params.action === "assign-attribute" && - params.type === ProductAttributeType[key] - } - selected={maybe(() => params.ids, [])} - onToggle={attributeId => { - const ids = maybe(() => params.ids, []); - navigate( - productTypeUrl(id, { - ...params, - ids: ids.includes(attributeId) - ? params.ids.filter( - selectedId => selectedId !== attributeId - ) - : [...ids, attributeId] - }) - ); - }} - key={key} + onDelete={handleProductTypeDelete} + deleteButtonState={deleteProductType.opts.status} /> - ))} - data.productType.name, "...")} - open={params.action === "remove"} - onClose={() => navigate(productTypeUrl(id))} - onConfirm={handleProductTypeDelete} - /> + + )} + ( productListQuery ); -const countAllProductsQuery = gql` - query CountAllProducts { - products { +const productCountQuery = gql` + query ProductCount($filter: ProductFilterInput) { + products(filter: $filter) { totalCount } } `; -export const useCountAllProducts = makeQuery( - countAllProductsQuery -); + +export const useProductCountQuery = makeQuery< + ProductCount, + ProductCountVariables +>(productCountQuery); const productDetailsQuery = gql` ${productFragmentDetails} diff --git a/src/products/types/CountAllProducts.ts b/src/products/types/ProductCount.ts similarity index 50% rename from src/products/types/CountAllProducts.ts rename to src/products/types/ProductCount.ts index 71d7a60ce..ccef6bf33 100644 --- a/src/products/types/CountAllProducts.ts +++ b/src/products/types/ProductCount.ts @@ -3,15 +3,21 @@ // @generated // This file was automatically generated and should not be edited. +import { ProductFilterInput } from "./../../types/globalTypes"; + // ==================================================== -// GraphQL query operation: CountAllProducts +// GraphQL query operation: ProductCount // ==================================================== -export interface CountAllProducts_products { +export interface ProductCount_products { __typename: "ProductCountableConnection"; totalCount: number | null; } -export interface CountAllProducts { - products: CountAllProducts_products | null; +export interface ProductCount { + products: ProductCount_products | null; +} + +export interface ProductCountVariables { + filter?: ProductFilterInput | null; } diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index bf83ac5a7..a66a0c943 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -32,11 +32,11 @@ import { } from "@saleor/products/components/ProductListPage/utils"; import { useAvailableInGridAttributesQuery, - useCountAllProducts, useInitialProductFilterAttributesQuery, useInitialProductFilterCategoriesQuery, useInitialProductFilterCollectionsQuery, useInitialProductFilterProductTypesQuery, + useProductCountQuery, useProductListQuery } from "@saleor/products/queries"; import { ProductListVariables } from "@saleor/products/types/ProductList"; @@ -186,7 +186,7 @@ export const ProductList: React.FC = ({ params }) => { : 0 : parseInt(params.activeTab, 0); - const countAllProducts = useCountAllProducts({ + const countAllProducts = useProductCountQuery({ skip: params.action !== "export" }); diff --git a/src/storybook/config.js b/src/storybook/config.js index 42ada3b25..1575d9e22 100644 --- a/src/storybook/config.js +++ b/src/storybook/config.js @@ -130,7 +130,6 @@ function loadStories() { // Product types require("./stories/productTypes/ProductTypeCreatePage"); - require("./stories/productTypes/ProductTypeDeleteDialog"); require("./stories/productTypes/ProductTypeDetailsPage"); require("./stories/productTypes/ProductTypeListPage"); diff --git a/src/storybook/stories/productTypes/ProductTypeDeleteDialog.tsx b/src/storybook/stories/productTypes/ProductTypeDeleteDialog.tsx deleted file mode 100644 index 6ffe808fe..000000000 --- a/src/storybook/stories/productTypes/ProductTypeDeleteDialog.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { storiesOf } from "@storybook/react"; -import React from "react"; - -import ProductTypeDeleteDialog, { - ProductTypeDeleteDialogProps -} from "../../../productTypes/components/ProductTypeDeleteDialog"; -import Decorator from "../../Decorator"; - -const props: ProductTypeDeleteDialogProps = { - confirmButtonState: "default", - name: "Shoes", - onClose: () => undefined, - onConfirm: () => undefined, - open: true -}; - -storiesOf("Product types / ProductTypeDeleteDialog", module) - .addDecorator(Decorator) - .add("default", () => ); diff --git a/src/types.ts b/src/types.ts index 6d2ad5939..069a800a9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -203,3 +203,5 @@ export interface AutocompleteFilterOpts choices: MultiAutocompleteChoiceType[]; displayValues: MultiAutocompleteChoiceType[]; } + +export type Ids = string[]; diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 325ec55a2..ddef892e6 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -1528,6 +1528,12 @@ export interface PageCreateInput { pageType: string; } +export interface PageFilterInput { + pageTypes?: string[] | null; + search?: string | null; + metadata?: (MetadataInput | null)[] | null; +} + export interface PageInput { slug?: string | null; title?: string | null;