From 09dce08f3d50d056f266dcdbd73c27c08105becd Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 21 Nov 2019 13:13:41 +0100 Subject: [PATCH] Add order search mode --- src/components/Navigator/Navigator.tsx | 22 ++++- src/components/Navigator/NavigatorInput.tsx | 90 +++++++++++++------ .../Navigator/modes/default/actions.ts | 8 ++ .../Navigator/modes/default/default.ts | 19 ++++ .../Navigator/modes/default/index.ts | 4 + .../Navigator/{ => modes/default}/views.ts | 2 +- src/components/Navigator/modes/index.ts | 20 +++++ src/components/Navigator/modes/messages.ts | 10 +++ src/components/Navigator/modes/orders.ts | 27 ++++++ src/components/Navigator/types.ts | 4 +- src/components/Navigator/useQuickSearch.ts | 69 ++++++++++---- 11 files changed, 224 insertions(+), 51 deletions(-) create mode 100644 src/components/Navigator/modes/default/actions.ts create mode 100644 src/components/Navigator/modes/default/default.ts create mode 100644 src/components/Navigator/modes/default/index.ts rename src/components/Navigator/{ => modes/default}/views.ts (98%) create mode 100644 src/components/Navigator/modes/index.ts create mode 100644 src/components/Navigator/modes/messages.ts create mode 100644 src/components/Navigator/modes/orders.ts diff --git a/src/components/Navigator/Navigator.tsx b/src/components/Navigator/Navigator.tsx index 1f9adbe28..d58a0cd1b 100644 --- a/src/components/Navigator/Navigator.tsx +++ b/src/components/Navigator/Navigator.tsx @@ -5,17 +5,19 @@ 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 NavigatorInput from "./NavigatorInput"; import NavigatorSection from "./NavigatorSection"; import { QuickSearchAction } from "./types"; import useQuickSearch from "./useQuickSearch"; -import { getViews, hasViews } from "./views"; const navigatorHotkey = "ctrl+m, command+m"; const Navigator: React.FC = () => { const [visible, setVisible] = React.useState(false); - const [query, change, actions] = useQuickSearch(visible); + const input = React.useRef(null); + const [query, mode, change, actions] = useQuickSearch(visible, input); const navigate = useNavigator(); const intl = useIntl(); @@ -51,11 +53,25 @@ const Navigator: React.FC = () => { {({ getInputProps, getItemProps, highlightedIndex }) => (
+ {hasActions(actions) && ( + + )} {hasViews(actions) && ( { getItemProps={getItemProps} highlightedIndex={highlightedIndex} items={getViews(actions)} - offset={0} + offset={getActions(actions).length} /> )}
diff --git a/src/components/Navigator/NavigatorInput.tsx b/src/components/Navigator/NavigatorInput.tsx index af78b3a93..fcb7907f9 100644 --- a/src/components/Navigator/NavigatorInput.tsx +++ b/src/components/Navigator/NavigatorInput.tsx @@ -1,42 +1,78 @@ import makeStyles from "@material-ui/core/styles/makeStyles"; import React from "react"; import { useIntl } from "react-intl"; +import { QuickSearchMode } from "./types"; const useStyles = makeStyles( - theme => ({ - root: { - background: theme.palette.background.default, - border: "none", + theme => { + const typography = { color: theme.palette.text.primary, fontSize: 24, - outline: 0, - padding: theme.spacing(2, 3), - width: "100%" - } - }), + lineHeight: 1.33 + }; + + return { + adornment: { + ...typography, + color: theme.palette.text.secondary, + paddingRight: theme.spacing(1) + }, + input: { + ...typography, + background: "transparent", + border: "none", + outline: 0, + padding: 0, + width: "100%" + }, + root: { + background: theme.palette.background.default, + display: "flex", + padding: theme.spacing(2, 3) + } + }; + }, { name: "NavigatorInput" } ); -const NavigatorInput: React.FC< - React.InputHTMLAttributes -> = props => { - const classes = useStyles(props); - const intl = useIntl(); - return ( - - ); -}; +interface NavigatorInputProps + extends React.InputHTMLAttributes { + mode: QuickSearchMode; +} + +const NavigatorInput = React.forwardRef( + (props, ref) => { + const { mode, ...rest } = props; + const classes = useStyles(props); + const intl = useIntl(); + + return ( +
+ {mode === "orders" && #} + +
+ ); + } +); NavigatorInput.displayName = "NavigatorInput"; export default NavigatorInput; diff --git a/src/components/Navigator/modes/default/actions.ts b/src/components/Navigator/modes/default/actions.ts new file mode 100644 index 000000000..ee7c489bd --- /dev/null +++ b/src/components/Navigator/modes/default/actions.ts @@ -0,0 +1,8 @@ +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 new file mode 100644 index 000000000..41f1c3e09 --- /dev/null +++ b/src/components/Navigator/modes/default/default.ts @@ -0,0 +1,19 @@ +import { IntlShape } from "react-intl"; + +import { QuickSearchAction } from "../../types"; +import searchInViews from "./views"; + +const threshold = 0.05; +const maxActions = 10; + +function getDefaultModeActions( + query: string, + intl: IntlShape +): QuickSearchAction[] { + return [...searchInViews(query, intl)] + .filter(action => action.score >= threshold) + .sort((a, b) => (a.score <= b.score ? 1 : -1)) + .slice(0, maxActions); +} + +export default getDefaultModeActions; diff --git a/src/components/Navigator/modes/default/index.ts b/src/components/Navigator/modes/default/index.ts new file mode 100644 index 000000000..445c4ca3d --- /dev/null +++ b/src/components/Navigator/modes/default/index.ts @@ -0,0 +1,4 @@ +export * from "./actions"; +export * from "./default"; +export { default } from "./default"; +export * from "./views"; diff --git a/src/components/Navigator/views.ts b/src/components/Navigator/modes/default/views.ts similarity index 98% rename from src/components/Navigator/views.ts rename to src/components/Navigator/modes/default/views.ts index 000adeaa2..8b6819967 100644 --- a/src/components/Navigator/views.ts +++ b/src/components/Navigator/modes/default/views.ts @@ -20,7 +20,7 @@ import { staffListUrl } from "@saleor/staff/urls"; import { countryListUrl } from "@saleor/taxes/urls"; import { languageListUrl } from "@saleor/translations/urls"; import { webhooksListUrl } from "@saleor/webhooks/urls"; -import { QuickSearchAction } from "./types"; +import { QuickSearchAction } from "../../types"; interface View { label: string; diff --git a/src/components/Navigator/modes/index.ts b/src/components/Navigator/modes/index.ts new file mode 100644 index 000000000..2856aaeab --- /dev/null +++ b/src/components/Navigator/modes/index.ts @@ -0,0 +1,20 @@ +import { IntlShape } from "react-intl"; + +import { QuickSearchAction, QuickSearchMode } from "../types"; +import getDefaultModeActions from "./default"; +import getOrdersModeActions from "./orders"; + +function getModeActions( + mode: QuickSearchMode, + query: string, + intl: IntlShape +): QuickSearchAction[] { + switch (mode) { + case "orders": + return getOrdersModeActions(query, intl); + default: + return getDefaultModeActions(query, intl); + } +} + +export default getModeActions; diff --git a/src/components/Navigator/modes/messages.ts b/src/components/Navigator/modes/messages.ts new file mode 100644 index 000000000..0fce6090f --- /dev/null +++ b/src/components/Navigator/modes/messages.ts @@ -0,0 +1,10 @@ +import { defineMessages } from "react-intl"; + +const messages = defineMessages({ + goToOrder: { + defaultMessage: "Go to order #{orderNumber}", + description: "navigator action" + } +}); + +export default messages; diff --git a/src/components/Navigator/modes/orders.ts b/src/components/Navigator/modes/orders.ts new file mode 100644 index 000000000..09a6a8f79 --- /dev/null +++ b/src/components/Navigator/modes/orders.ts @@ -0,0 +1,27 @@ +import { IntlShape } from "react-intl"; + +import { orderUrl } from "@saleor/orders/urls"; +import { QuickSearchAction } from "../types"; +import messages from "./messages"; + +function getOrdersModeActions( + query: string, + intl: IntlShape +): QuickSearchAction[] { + if (query === parseInt(query, 0).toString()) { + return [ + { + label: intl.formatMessage(messages.goToOrder, { + orderNumber: query + }), + score: 1, + type: "action", + url: orderUrl(btoa(`Order:${query}`)) + } + ]; + } + + return []; +} + +export default getOrdersModeActions; diff --git a/src/components/Navigator/types.ts b/src/components/Navigator/types.ts index 4c15f60a9..3646a7ce9 100644 --- a/src/components/Navigator/types.ts +++ b/src/components/Navigator/types.ts @@ -1,4 +1,4 @@ -export type QuickSearchActionType = "view"; +export type QuickSearchActionType = "action" | "view"; export interface QuickSearchAction { label: string; @@ -6,3 +6,5 @@ export interface QuickSearchAction { type: QuickSearchActionType; url: string; } + +export type QuickSearchMode = "default" | "orders" | "customers"; diff --git a/src/components/Navigator/useQuickSearch.ts b/src/components/Navigator/useQuickSearch.ts index dc51a9967..3a9798341 100644 --- a/src/components/Navigator/useQuickSearch.ts +++ b/src/components/Navigator/useQuickSearch.ts @@ -1,39 +1,70 @@ -import { useState } from "react"; +import { RefObject, useEffect, useState } from "react"; import { useIntl } from "react-intl"; import { ChangeEvent, FormChange } from "@saleor/hooks/useForm"; import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; -import { QuickSearchAction } from "./types"; -import searchInViews from "./views"; +import getModeActions from "./modes"; +import { QuickSearchAction, QuickSearchMode } from "./types"; -export type QuickSearchMode = "default" | "orders" | "customers"; -const threshold = 0.05; -const maxActions = 10; - -type UseQuickSearch = [string, FormChange, QuickSearchAction[]]; -function useQuickSearch(open: boolean): UseQuickSearch { +type UseQuickSearch = [ + string, + QuickSearchMode, + FormChange, + QuickSearchAction[] +]; +function useQuickSearch( + open: boolean, + input: RefObject +): UseQuickSearch { const [query, setQuery] = useState(""); const [mode, setMode] = useState("default"); const intl = useIntl(); useModalDialogOpen(open, { - onClose: () => setQuery("") + onClose: () => { + setMode("default"); + setQuery(""); + } }); + const handleBack = (event: KeyboardEvent) => { + // `any` type because of poorly typed `KeyboardEvent.EventTarget` which + // has no `value` key. Which it would have if `KeyboardEvent` and + // `EventTarget` would be generic types accepting HTMLDOM element types. + if ((event.target as any).value === "" && event.keyCode === 8) { + setMode("default"); + } + }; + + useEffect(() => { + setQuery(""); + if (mode !== "default" && input.current) { + input.current.addEventListener("keyup", handleBack); + + return () => { + if (input.current) { + input.current.removeEventListener("keyup", handleBack); + } + }; + } + }, [mode, open]); + const change = (event: ChangeEvent) => { const value = event.target.value; - setQuery(value); + if (mode === "default") { + switch (value) { + case "# ": + setMode("orders"); + default: + setQuery(value); + } + } else { + setQuery(value); + } }; - return [ - query, - change, - [...searchInViews(query, intl)] - .filter(action => action.score >= threshold) - .sort((a, b) => (a.score <= b.score ? 1 : -1)) - .slice(0, maxActions) - ]; + return [query, mode, change, getModeActions(mode, query, intl)]; } export default useQuickSearch;