diff --git a/src/components/Navigator/Navigator.tsx b/src/components/Navigator/Navigator.tsx index d58a0cd1b..ea2b8510c 100644 --- a/src/components/Navigator/Navigator.tsx +++ b/src/components/Navigator/Navigator.tsx @@ -4,9 +4,7 @@ import hotkeys from "hotkeys-js"; import React from "react"; import { useIntl } from "react-intl"; -import useNavigator from "@saleor/hooks/useNavigator"; -import { getActions, hasActions } from "./modes/default"; -import { getViews, hasViews } from "./modes/default/views"; +import { getActions, getViews, hasActions, hasViews } from "./modes/utils"; import NavigatorInput from "./NavigatorInput"; import NavigatorSection from "./NavigatorSection"; import { QuickSearchAction } from "./types"; @@ -18,7 +16,6 @@ const Navigator: React.FC = () => { const [visible, setVisible] = React.useState(false); const input = React.useRef(null); const [query, mode, change, actions] = useQuickSearch(visible, input); - const navigate = useNavigator(); const intl = useIntl(); React.useEffect(() => { @@ -37,8 +34,8 @@ const Navigator: React.FC = () => { (item ? item.label : "")} onSelect={(item: QuickSearchAction) => { - navigate(item.url); setVisible(false); + item.onClick(); }} onInputValueChange={value => change({ @@ -60,18 +57,6 @@ const Navigator: React.FC = () => { })} ref={input} /> - {hasActions(actions) && ( - - )} {hasViews(actions) && ( { offset={getActions(actions).length} /> )} + {hasActions(actions) && ( + + )} )} diff --git a/src/components/Navigator/NavigatorInput.tsx b/src/components/Navigator/NavigatorInput.tsx index fcb7907f9..f32c90f9b 100644 --- a/src/components/Navigator/NavigatorInput.tsx +++ b/src/components/Navigator/NavigatorInput.tsx @@ -50,7 +50,11 @@ const NavigatorInput = React.forwardRef( return (
- {mode === "orders" && #} + {mode !== "default" && ( + + {mode === "orders" ? "#" : ">"} + + )} ( defaultMessage: "Order Number", description: "navigator placeholder" }) + : mode === "commands" + ? intl.formatMessage({ + defaultMessage: "Type Command", + description: "navigator placeholder" + }) : intl.formatMessage({ defaultMessage: "Use Navigator to move through Saleor", description: "navigator placeholder" diff --git a/src/components/Navigator/modes/commands/actions.ts b/src/components/Navigator/modes/commands/actions.ts new file mode 100644 index 000000000..2d00ab69d --- /dev/null +++ b/src/components/Navigator/modes/commands/actions.ts @@ -0,0 +1,75 @@ +import { score } from "fuzzaldrin"; +import { IntlShape } from "react-intl"; + +import { categoryAddUrl } from "@saleor/categories/urls"; +import { collectionAddUrl } from "@saleor/collections/urls"; +import { customerAddUrl } from "@saleor/customers/urls"; +import { voucherAddUrl } from "@saleor/discounts/urls"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate"; +import { productAddUrl } from "@saleor/products/urls"; +import { MutationFunction } from "react-apollo"; +import { QuickSearchAction } from "../../types"; +import messages from "../messages"; + +const threshold = 0.05; +const maxActions = 5; + +interface Command { + label: string; + onClick: () => void; +} +export function searchInCommands( + search: string, + intl: IntlShape, + navigate: UseNavigatorResult, + createOrder: MutationFunction +): QuickSearchAction[] { + const actions: Command[] = [ + { + label: intl.formatMessage(messages.addCategory), + onClick: () => navigate(categoryAddUrl()) + }, + { + label: intl.formatMessage(messages.addCollection), + onClick: () => navigate(collectionAddUrl) + }, + { + label: intl.formatMessage(messages.addProduct), + onClick: () => navigate(productAddUrl) + }, + { + label: intl.formatMessage(messages.addCustomer), + onClick: () => navigate(customerAddUrl) + }, + { + label: intl.formatMessage(messages.addVoucher), + onClick: () => navigate(voucherAddUrl) + }, + { + label: intl.formatMessage(messages.createOrder), + onClick: createOrder + } + ]; + + return actions.map(action => ({ + label: action.label, + onClick: action.onClick, + score: score(action.label, search), + type: "action" + })); +} + +function getCommandModeActions( + query: string, + intl: IntlShape, + navigate: UseNavigatorResult, + createOrder: MutationFunction +): QuickSearchAction[] { + return [...searchInCommands(query, intl, navigate, createOrder)] + .filter(action => action.score >= threshold) + .sort((a, b) => (a.score <= b.score ? 1 : -1)) + .slice(0, maxActions); +} + +export default getCommandModeActions; diff --git a/src/components/Navigator/modes/commands/index.ts b/src/components/Navigator/modes/commands/index.ts new file mode 100644 index 000000000..9600b6455 --- /dev/null +++ b/src/components/Navigator/modes/commands/index.ts @@ -0,0 +1,2 @@ +export * from "./actions"; +export { default } from "./actions"; diff --git a/src/components/Navigator/modes/default/actions.ts b/src/components/Navigator/modes/default/actions.ts deleted file mode 100644 index ee7c489bd..000000000 --- a/src/components/Navigator/modes/default/actions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { QuickSearchAction } from "../../types"; - -export function getActions(actions: QuickSearchAction[]): QuickSearchAction[] { - return actions.filter(action => action.type === "action"); -} -export function hasActions(actions: QuickSearchAction[]): boolean { - return getActions(actions).length > 0; -} diff --git a/src/components/Navigator/modes/default/default.ts b/src/components/Navigator/modes/default/default.ts index 41f1c3e09..d56a44e79 100644 --- a/src/components/Navigator/modes/default/default.ts +++ b/src/components/Navigator/modes/default/default.ts @@ -1,16 +1,25 @@ +import { MutationFunction } from "react-apollo"; import { IntlShape } from "react-intl"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate"; import { QuickSearchAction } from "../../types"; +import { searchInCommands } from "../commands"; import searchInViews from "./views"; const threshold = 0.05; -const maxActions = 10; +const maxActions = 5; function getDefaultModeActions( query: string, - intl: IntlShape + intl: IntlShape, + navigate: UseNavigatorResult, + createOrder: MutationFunction ): QuickSearchAction[] { - return [...searchInViews(query, intl)] + return [ + ...searchInViews(query, intl, navigate), + ...searchInCommands(query, intl, navigate, createOrder) + ] .filter(action => action.score >= threshold) .sort((a, b) => (a.score <= b.score ? 1 : -1)) .slice(0, maxActions); diff --git a/src/components/Navigator/modes/default/index.ts b/src/components/Navigator/modes/default/index.ts index 445c4ca3d..86fa704d4 100644 --- a/src/components/Navigator/modes/default/index.ts +++ b/src/components/Navigator/modes/default/index.ts @@ -1,4 +1,3 @@ -export * from "./actions"; export * from "./default"; export { default } from "./default"; export * from "./views"; diff --git a/src/components/Navigator/modes/default/views.ts b/src/components/Navigator/modes/default/views.ts index 8b6819967..6ae6980f0 100644 --- a/src/components/Navigator/modes/default/views.ts +++ b/src/components/Navigator/modes/default/views.ts @@ -6,6 +6,7 @@ import { categoryListUrl } from "@saleor/categories/urls"; import { collectionListUrl } from "@saleor/collections/urls"; import { customerListUrl } from "@saleor/customers/urls"; import { saleListUrl, voucherListUrl } from "@saleor/discounts/urls"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; import { sectionNames } from "@saleor/intl"; import { menuListUrl } from "@saleor/navigation/urls"; import { orderDraftListUrl, orderListUrl } from "@saleor/orders/urls"; @@ -26,7 +27,11 @@ interface View { label: string; url: string; } -function searchInViews(search: string, intl: IntlShape): QuickSearchAction[] { +function searchInViews( + search: string, + intl: IntlShape, + navigate: UseNavigatorResult +): QuickSearchAction[] { const views: View[] = [ { label: intl.formatMessage(sectionNames.attributes), @@ -116,17 +121,10 @@ function searchInViews(search: string, intl: IntlShape): QuickSearchAction[] { return views.map(view => ({ label: view.label, + onClick: () => navigate(view.url), score: score(view.label, search), - type: "view", - url: view.url + type: "view" })); } -export function getViews(actions: QuickSearchAction[]): QuickSearchAction[] { - return actions.filter(action => action.type === "view"); -} -export function hasViews(actions: QuickSearchAction[]): boolean { - return getViews(actions).length > 0; -} - export default searchInViews; diff --git a/src/components/Navigator/modes/index.ts b/src/components/Navigator/modes/index.ts index 65164eb7d..4bd08f23d 100644 --- a/src/components/Navigator/modes/index.ts +++ b/src/components/Navigator/modes/index.ts @@ -1,10 +1,10 @@ import { IntlShape } from "react-intl"; -import { - CheckIfOrderExists, - CheckIfOrderExistsVariables -} from "../queries/types/CheckIfOrderExists"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; +import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate"; +import { MutationFunction } from "react-apollo"; import { QuickSearchAction, QuickSearchMode } from "../types"; +import getCommandModeActions from "./commands"; import getDefaultModeActions from "./default"; import getOrdersModeActions from "./orders"; import { ActionQueries } from "./types"; @@ -13,13 +13,19 @@ function getModeActions( mode: QuickSearchMode, query: string, intl: IntlShape, - queries: ActionQueries + queries: ActionQueries, + cbs: { + navigate: UseNavigatorResult; + createOrder: MutationFunction; + } ): QuickSearchAction[] { switch (mode) { + case "commands": + return getCommandModeActions(query, intl, cbs.navigate, cbs.createOrder); case "orders": - return getOrdersModeActions(query, intl, queries.order); + return getOrdersModeActions(query, intl, cbs.navigate, queries.order); default: - return getDefaultModeActions(query, intl); + return getDefaultModeActions(query, intl, cbs.navigate, cbs.createOrder); } } diff --git a/src/components/Navigator/modes/messages.ts b/src/components/Navigator/modes/messages.ts index 0fce6090f..07c6df3d0 100644 --- a/src/components/Navigator/modes/messages.ts +++ b/src/components/Navigator/modes/messages.ts @@ -1,6 +1,30 @@ import { defineMessages } from "react-intl"; const messages = defineMessages({ + addCategory: { + defaultMessage: "Add Category", + description: "button" + }, + addCollection: { + defaultMessage: "Add Collection", + description: "button" + }, + addCustomer: { + defaultMessage: "Add Customer", + description: "button" + }, + addProduct: { + defaultMessage: "Add Product", + description: "button" + }, + addVoucher: { + defaultMessage: "Add Voucher", + description: "button" + }, + createOrder: { + defaultMessage: "Create Order", + description: "button" + }, goToOrder: { defaultMessage: "Go to order #{orderNumber}", description: "navigator action" diff --git a/src/components/Navigator/modes/orders.ts b/src/components/Navigator/modes/orders.ts index 4525b72f0..0156d68c6 100644 --- a/src/components/Navigator/modes/orders.ts +++ b/src/components/Navigator/modes/orders.ts @@ -1,5 +1,6 @@ import { IntlShape } from "react-intl"; +import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; import { maybe } from "@saleor/misc"; import { orderUrl } from "@saleor/orders/urls"; import { CheckIfOrderExists_order } from "../queries/types/CheckIfOrderExists"; @@ -17,6 +18,7 @@ export function getGqlOrderId(orderNumber: string): string { function getOrdersModeActions( query: string, intl: IntlShape, + navigate: UseNavigatorResult, order: CheckIfOrderExists_order ): QuickSearchAction[] { const gqlId = getGqlOrderId(query); @@ -27,9 +29,9 @@ function getOrdersModeActions( label: intl.formatMessage(messages.goToOrder, { orderNumber: query }), + onClick: () => navigate(orderUrl(gqlId)), score: 1, - type: "action", - url: orderUrl(gqlId) + type: "action" } ]; } diff --git a/src/components/Navigator/modes/utils.ts b/src/components/Navigator/modes/utils.ts new file mode 100644 index 000000000..ceabc81ca --- /dev/null +++ b/src/components/Navigator/modes/utils.ts @@ -0,0 +1,15 @@ +import { QuickSearchAction } from "../types"; + +export function getActions(actions: QuickSearchAction[]): QuickSearchAction[] { + return actions.filter(action => action.type === "action"); +} +export function hasActions(actions: QuickSearchAction[]): boolean { + return getActions(actions).length > 0; +} + +export function getViews(actions: QuickSearchAction[]): QuickSearchAction[] { + return actions.filter(action => action.type === "view"); +} +export function hasViews(actions: QuickSearchAction[]): boolean { + return getViews(actions).length > 0; +} diff --git a/src/components/Navigator/types.ts b/src/components/Navigator/types.ts index 3646a7ce9..12279daf5 100644 --- a/src/components/Navigator/types.ts +++ b/src/components/Navigator/types.ts @@ -4,7 +4,7 @@ export interface QuickSearchAction { label: string; score: number; type: QuickSearchActionType; - url: string; + onClick: () => void; } -export type QuickSearchMode = "default" | "orders" | "customers"; +export type QuickSearchMode = "default" | "commands" | "orders" | "customers"; diff --git a/src/components/Navigator/useQuickSearch.ts b/src/components/Navigator/useQuickSearch.ts index f1b333e1a..fee527cf5 100644 --- a/src/components/Navigator/useQuickSearch.ts +++ b/src/components/Navigator/useQuickSearch.ts @@ -3,7 +3,10 @@ import { useIntl } from "react-intl"; import { ChangeEvent, FormChange } from "@saleor/hooks/useForm"; import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; +import useNavigator from "@saleor/hooks/useNavigator"; import { maybe } from "@saleor/misc"; +import { useOrderDraftCreateMutation } from "@saleor/orders/mutations"; +import { orderUrl } from "@saleor/orders/urls"; import getModeActions from "./modes"; import { getGqlOrderId, isQueryValidOrderNumber } from "./modes/orders"; import useCheckIfOrderExists from "./queries/useCheckIfOrderExists"; @@ -22,7 +25,15 @@ function useQuickSearch( const [query, setQuery] = useState(""); const [mode, setMode] = useState("default"); const intl = useIntl(); + const navigate = useNavigator(); const [{ data: orderData }, getOrderData] = useCheckIfOrderExists(); + const [createOrder] = useOrderDraftCreateMutation({ + onCompleted: result => { + if (result.draftOrderCreate.errors.length === 0) { + navigate(orderUrl(result.draftOrderCreate.order.id)); + } + } + }); useModalDialogOpen(open, { onClose: () => { @@ -58,8 +69,12 @@ function useQuickSearch( if (mode === "default") { switch (value) { + case "> ": + setMode("commands"); + break; case "# ": setMode("orders"); + break; default: setQuery(value); } @@ -75,9 +90,18 @@ function useQuickSearch( query, mode, change, - getModeActions(mode, query, intl, { - order: maybe(() => orderData.order) - }) + getModeActions( + mode, + query, + intl, + { + order: maybe(() => orderData.order) + }, + { + createOrder, + navigate + } + ) ]; } diff --git a/src/hooks/makeMutation.ts b/src/hooks/makeMutation.ts index b6cfd4a91..0ec08736c 100644 --- a/src/hooks/makeMutation.ts +++ b/src/hooks/makeMutation.ts @@ -11,15 +11,15 @@ import { commonMessages } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import useNotifier from "./useNotifier"; -type UseMutation = [ +export type UseMutation = [ MutationFunction, MutationResult ]; -type UseMutationCbs = Partial<{ +export type UseMutationCbs = Partial<{ onCompleted: (data: TData) => void; onError: (error: ApolloError) => void; }>; -type UseMutationHook = ( +export type UseMutationHook = ( cbs: UseMutationCbs ) => UseMutation; diff --git a/src/orders/mutations.ts b/src/orders/mutations.ts index 8ab8f659b..4a68ab6ba 100644 --- a/src/orders/mutations.ts +++ b/src/orders/mutations.ts @@ -1,5 +1,6 @@ import gql from "graphql-tag"; +import makeMutation from "@saleor/hooks/makeMutation"; import { TypedMutation } from "../mutations"; import { fragmentAddress, @@ -409,10 +410,9 @@ const orderDraftCreateMutation = gql` } } `; -export const TypedOrderDraftCreateMutation = TypedMutation< - OrderDraftCreate, - {} ->(orderDraftCreateMutation); +export const useOrderDraftCreateMutation = makeMutation( + orderDraftCreateMutation +); const orderLineDeleteMutation = gql` ${fragmentOrderDetails}