From 82d15f444114fb1cf77662b4ce33ef91f5c4567d Mon Sep 17 00:00:00 2001 From: Jakub Neander Date: Thu, 23 Mar 2023 15:53:48 +0100 Subject: [PATCH] Copy filters automatically to GraphiQL playground (#3385) --- CHANGELOG.md | 1 + locale/defaultMessages.json | 4 + src/components/AppLayout/AppLayout.tsx | 31 ++++++- src/components/AppLayout/util.test.ts | 25 +++++ src/components/AppLayout/util.ts | 23 +++++ .../DevModePanel/useDevModeKeyTrigger.ts | 10 +- src/graphql/hooks.generated.ts | 93 +++++++++++++++++++ src/graphql/types.generated.ts | 15 +++ .../OrderListPage/OrderListPage.stories.tsx | 1 + .../OrderListPage/OrderListPage.tsx | 36 ++++++- src/orders/queries.ts | 51 +++++++--- src/orders/views/OrderList/OrderList.tsx | 18 ++-- src/orders/views/OrderList/filters.ts | 24 ++--- 13 files changed, 287 insertions(+), 45 deletions(-) create mode 100644 src/components/AppLayout/util.test.ts create mode 100644 src/components/AppLayout/util.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e4aaae64e..2a78fd1e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ All notable, unreleased changes to this project will be documented in this file. - Add DevMode panel (trigger: CMD+') - #3333 by @zaiste - Migrate to `es2020` for TypeScript - #3386 by @zaiste - Fix styling for GraphiQL on the webhook page - #3389 by @zaiste +- Copy filters automatically to GraphiQL playground - #3385 by @zaiste ## 3.4 diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index e7b225aa4..5a8624870 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -8010,6 +8010,10 @@ "vEYtiq": { "string": "Category Name" }, + "vEwjub": { + "context": "button", + "string": "Open in GraphiQL" + }, "vM9quW": { "context": "order payment", "string": "Paid with Gift Card" diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index e5f7fe597..724d615dd 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -1,8 +1,11 @@ import useAppState from "@dashboard/hooks/useAppState"; +import { DevModeQuery } from "@dashboard/orders/queries"; +import { getFilterVariables } from "@dashboard/orders/views/OrderList/filters"; import { LinearProgress } from "@material-ui/core"; import { useActionBar } from "@saleor/macaw-ui"; import { Box } from "@saleor/macaw-ui/next"; import React, { useState } from "react"; +import { useLocation } from "react-router"; import { DevModePanel } from "../DevModePanel/DevModePanel"; import { useDevModeContext } from "../DevModePanel/hooks"; @@ -11,6 +14,7 @@ import Navigator from "../Navigator"; import { Sidebar } from "../Sidebar"; import { contentMaxWidth } from "./consts"; import { useStyles } from "./styles"; +import { extractQueryParams } from "./util"; interface AppLayoutProps { children: React.ReactNode; @@ -23,9 +27,32 @@ const AppLayout: React.FC = ({ children }) => { const [appState] = useAppState(); const [isNavigatorVisible, setNavigatorVisibility] = useState(false); - const { isDevModeVisible, setDevModeVisibility } = useDevModeContext(); + const { + isDevModeVisible, + setDevModeVisibility, + setDevModeContent, + setVariables, + } = useDevModeContext(); - useDevModeKeyTrigger(() => setDevModeVisibility(!isDevModeVisible)); + const params = extractQueryParams(useLocation().search); + + useDevModeKeyTrigger(({ shift }) => { + if (shift) { + setDevModeContent(DevModeQuery); + const variables = JSON.stringify( + { + filter: getFilterVariables(params), + }, + null, + 2, + ); + setVariables(variables); + } else { + setDevModeContent(""); + setVariables(""); + } + setDevModeVisibility(!isDevModeVisible); + }); return ( <> diff --git a/src/components/AppLayout/util.test.ts b/src/components/AppLayout/util.test.ts new file mode 100644 index 000000000..f1758cb8d --- /dev/null +++ b/src/components/AppLayout/util.test.ts @@ -0,0 +1,25 @@ +import { extractQueryParams } from "./util"; + +describe("extractQueryParams", () => { + test("parses a query string with an explicitly indexed list into an object of key-value pairs", () => { + const queryString = + "q=apple&color[0]=red&color[1]=green&shape[]=round&shape[]=oval"; + const expected = { + q: "apple", + color: ["red", "green"], + shape: ["round", "oval"], + }; + expect(extractQueryParams(queryString)).toEqual(expected); + }); + + test("parses a query string into an object of key-value pairs (overwrites non array elements!)", () => { + const queryString = + "q=apple&color=red&color=green&shape[]=round&shape[]=oval"; + const expected = { + q: "apple", + color: "green", + shape: ["round", "oval"], + }; + expect(extractQueryParams(queryString)).toEqual(expected); + }); +}); diff --git a/src/components/AppLayout/util.ts b/src/components/AppLayout/util.ts new file mode 100644 index 000000000..109bcca09 --- /dev/null +++ b/src/components/AppLayout/util.ts @@ -0,0 +1,23 @@ +export const extractQueryParams = (queryString: string) => { + const urlSearchParams = new URLSearchParams(queryString); + const queryParams = {}; + + urlSearchParams.forEach((value, key) => { + const arrayKeyRegex = /^(.+)\[\d*\]$/; + const match = key.match(arrayKeyRegex); + + if (match) { + const arrayKey = match[1]; + + if (!queryParams.hasOwnProperty(arrayKey)) { + queryParams[arrayKey] = []; + } + + queryParams[arrayKey].push(value); + } else { + queryParams[key] = value; + } + }); + + return queryParams; +}; diff --git a/src/components/DevModePanel/useDevModeKeyTrigger.ts b/src/components/DevModePanel/useDevModeKeyTrigger.ts index 639342fd3..2eb74d2fb 100644 --- a/src/components/DevModePanel/useDevModeKeyTrigger.ts +++ b/src/components/DevModePanel/useDevModeKeyTrigger.ts @@ -1,10 +1,14 @@ import { useEffect } from "react"; -export const useDevModeKeyTrigger = (callback?: () => void) => { +type DevModeKeyTriggerCallback = ({ shift }: { shift: boolean }) => void; + +export const useDevModeKeyTrigger = (callback?: DevModeKeyTriggerCallback) => { useEffect(() => { const handler = (event: KeyboardEvent) => { - if (event.metaKey && event.code === "Quote") { - callback(); + if (event.shiftKey && event.metaKey && event.code === "Quote") { + callback({ shift: true }); + } else if (event.metaKey && event.code === "Quote") { + callback({ shift: false }); } }; diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 90f370c48..f7405935c 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -9993,6 +9993,99 @@ export function useChannelUsabilityDataLazyQuery(baseOptions?: ApolloReactHooks. export type ChannelUsabilityDataQueryHookResult = ReturnType; export type ChannelUsabilityDataLazyQueryHookResult = ReturnType; export type ChannelUsabilityDataQueryResult = Apollo.QueryResult; +export const OrderDetailsGraphiQlDocument = gql` + query OrderDetailsGraphiQL($id: ID!) { + order(id: $id) { + id + number + status + isShippingRequired + canFinalize + created + customerNote + paymentStatus + userEmail + isPaid + } +} + `; + +/** + * __useOrderDetailsGraphiQlQuery__ + * + * To run a query within a React component, call `useOrderDetailsGraphiQlQuery` and pass it any options that fit your needs. + * When your component renders, `useOrderDetailsGraphiQlQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useOrderDetailsGraphiQlQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useOrderDetailsGraphiQlQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(OrderDetailsGraphiQlDocument, options); + } +export function useOrderDetailsGraphiQlLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(OrderDetailsGraphiQlDocument, options); + } +export type OrderDetailsGraphiQlQueryHookResult = ReturnType; +export type OrderDetailsGraphiQlLazyQueryHookResult = ReturnType; +export type OrderDetailsGraphiQlQueryResult = Apollo.QueryResult; +export const DevModeRunDocument = gql` + query DevModeRun($filter: OrderFilterInput, $sortBy: OrderSortingInput) { + orders(first: 10, filter: $filter, sortBy: $sortBy) { + edges { + node { + id + number + status + isShippingRequired + canFinalize + created + customerNote + paymentStatus + userEmail + isPaid + } + } + } +} + `; + +/** + * __useDevModeRunQuery__ + * + * To run a query within a React component, call `useDevModeRunQuery` and pass it any options that fit your needs. + * When your component renders, `useDevModeRunQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useDevModeRunQuery({ + * variables: { + * filter: // value for 'filter' + * sortBy: // value for 'sortBy' + * }, + * }); + */ +export function useDevModeRunQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(DevModeRunDocument, options); + } +export function useDevModeRunLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(DevModeRunDocument, options); + } +export type DevModeRunQueryHookResult = ReturnType; +export type DevModeRunLazyQueryHookResult = ReturnType; +export type DevModeRunQueryResult = Apollo.QueryResult; export const PageTypeUpdateDocument = gql` mutation PageTypeUpdate($id: ID!, $input: PageTypeUpdateInput!) { pageTypeUpdate(id: $id, input: $input) { diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index d395a00cf..043b7ca85 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -8640,6 +8640,21 @@ export type ChannelUsabilityDataQueryVariables = Exact<{ export type ChannelUsabilityDataQuery = { __typename: 'Query', products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null }; +export type OrderDetailsGraphiQlQueryVariables = Exact<{ + id: Scalars['ID']; +}>; + + +export type OrderDetailsGraphiQlQuery = { __typename: 'Query', order: { __typename: 'Order', id: string, number: string, status: OrderStatus, isShippingRequired: boolean, canFinalize: boolean, created: any, customerNote: string, paymentStatus: PaymentChargeStatusEnum, userEmail: string | null, isPaid: boolean } | null }; + +export type DevModeRunQueryVariables = Exact<{ + filter?: InputMaybe; + sortBy?: InputMaybe; +}>; + + +export type DevModeRunQuery = { __typename: 'Query', orders: { __typename: 'OrderCountableConnection', edges: Array<{ __typename: 'OrderCountableEdge', node: { __typename: 'Order', id: string, number: string, status: OrderStatus, isShippingRequired: boolean, canFinalize: boolean, created: any, customerNote: string, paymentStatus: PaymentChargeStatusEnum, userEmail: string | null, isPaid: boolean } }> } | null }; + export type PageTypeUpdateMutationVariables = Exact<{ id: Scalars['ID']; input: PageTypeUpdateInput; diff --git a/src/orders/components/OrderListPage/OrderListPage.stories.tsx b/src/orders/components/OrderListPage/OrderListPage.stories.tsx index 34993532a..ec17228c9 100644 --- a/src/orders/components/OrderListPage/OrderListPage.stories.tsx +++ b/src/orders/components/OrderListPage/OrderListPage.stories.tsx @@ -78,6 +78,7 @@ const props: OrderListPageProps = { ...sortPageProps.sort, sort: OrderListUrlSortField.number, }, + params: {}, }; storiesOf("Orders / Order list", module) diff --git a/src/orders/components/OrderListPage/OrderListPage.tsx b/src/orders/components/OrderListPage/OrderListPage.tsx index e54754d94..62f8ae523 100644 --- a/src/orders/components/OrderListPage/OrderListPage.tsx +++ b/src/orders/components/OrderListPage/OrderListPage.tsx @@ -7,11 +7,17 @@ import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo"; import { TopNav } from "@dashboard/components/AppLayout/TopNav"; import { ButtonWithSelect } from "@dashboard/components/ButtonWithSelect"; import CardMenu from "@dashboard/components/CardMenu"; +import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks"; import FilterBar from "@dashboard/components/FilterBar"; import { ListPageLayout } from "@dashboard/components/Layouts"; import { OrderListQuery, RefreshLimitsQuery } from "@dashboard/graphql"; import { sectionNames } from "@dashboard/intl"; -import { OrderListUrlSortField } from "@dashboard/orders/urls"; +import { DevModeQuery } from "@dashboard/orders/queries"; +import { + OrderListUrlQueryParams, + OrderListUrlSortField, +} from "@dashboard/orders/urls"; +import { getFilterVariables } from "@dashboard/orders/views/OrderList/filters"; import { FilterPageProps, PageListProps, @@ -40,6 +46,7 @@ export interface OrderListPageProps orders: RelayToFlat; onSettingsOpen: () => void; onAdd: () => void; + params: OrderListUrlQueryParams; } const useStyles = makeStyles( @@ -65,6 +72,7 @@ const OrderListPage: React.FC = ({ onTabChange, onTabDelete, onTabSave, + params, ...listProps }) => { const intl = useIntl(); @@ -78,6 +86,24 @@ const OrderListPage: React.FC = ({ const extensionMenuItems = mapToMenuItems(ORDER_OVERVIEW_MORE_ACTIONS); const extensionCreateButtonItems = mapToMenuItems(ORDER_OVERVIEW_CREATE); + const context = useDevModeContext(); + + const openPlaygroundURL = () => { + context.setDevModeContent(DevModeQuery); + const variables = JSON.stringify( + { + filter: getFilterVariables(params), + // TODO add sorting: Issue #3409 + // strange error when uncommenting this line + // sortBy: getSortQueryVariables(params) + }, + null, + 2, + ); + context.setVariables(variables); + context.setDevModeVisibility(true); + }; + return ( @@ -85,6 +111,14 @@ const OrderListPage: React.FC = ({ = ({ params }) => { const currentTab = getFiltersCurrentTab(params, tabs); - const [ - changeFilters, - resetFilters, - handleSearchChange, - ] = createFilterHandlers({ - createUrl: orderListUrl, - getFilterQueryParam, - navigate, - params, - }); + const [changeFilters, resetFilters, handleSearchChange] = + createFilterHandlers({ + createUrl: orderListUrl, + getFilterQueryParam, + navigate, + params, + }); const [openModal, closeModal] = createDialogActionHandlers< OrderListUrlDialog, @@ -178,6 +175,7 @@ export const OrderList: React.FC = ({ params }) => { tabs={getFilterTabs().map(tab => tab.name)} onAll={resetFilters} onSettingsOpen={() => navigate(orderSettingsPath)} + params={params} /> (ORDER_FILTERS_KEY); +export const { deleteFilterTab, getFilterTabs, saveFilterTab } = + createFilterTabUtils(ORDER_FILTERS_KEY); -export const { - areFiltersApplied, - getActiveFilters, - getFiltersCurrentTab, -} = createFilterUtils({ - ...OrderListUrlFiltersEnum, - ...OrderListUrlFiltersWithMultipleValues, - ...OrderListFitersWithKeyValueValues, -}); +export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } = + createFilterUtils({ + ...OrderListUrlFiltersEnum, + ...OrderListUrlFiltersWithMultipleValues, + ...OrderListFitersWithKeyValueValues, + });