Add order search mode

This commit is contained in:
dominik-zeglen 2019-11-21 13:13:41 +01:00
parent 8b36ca5308
commit 09dce08f3d
11 changed files with 224 additions and 51 deletions

View file

@ -5,17 +5,19 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import { getActions, hasActions } from "./modes/default";
import { getViews, hasViews } from "./modes/default/views";
import NavigatorInput from "./NavigatorInput"; import NavigatorInput from "./NavigatorInput";
import NavigatorSection from "./NavigatorSection"; import NavigatorSection from "./NavigatorSection";
import { QuickSearchAction } from "./types"; import { QuickSearchAction } from "./types";
import useQuickSearch from "./useQuickSearch"; import useQuickSearch from "./useQuickSearch";
import { getViews, hasViews } from "./views";
const navigatorHotkey = "ctrl+m, command+m"; const navigatorHotkey = "ctrl+m, command+m";
const Navigator: React.FC = () => { const Navigator: React.FC = () => {
const [visible, setVisible] = React.useState(false); 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 navigate = useNavigator();
const intl = useIntl(); const intl = useIntl();
@ -51,11 +53,25 @@ const Navigator: React.FC = () => {
{({ getInputProps, getItemProps, highlightedIndex }) => ( {({ getInputProps, getItemProps, highlightedIndex }) => (
<div> <div>
<NavigatorInput <NavigatorInput
mode={mode}
value={query} value={query}
{...getInputProps({ {...getInputProps({
value: query value: query
})} })}
ref={input}
/> />
{hasActions(actions) && (
<NavigatorSection
label={intl.formatMessage({
defaultMessage: "Quick Actions",
description: "navigator section header"
})}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
items={getActions(actions)}
offset={0}
/>
)}
{hasViews(actions) && ( {hasViews(actions) && (
<NavigatorSection <NavigatorSection
label={intl.formatMessage({ label={intl.formatMessage({
@ -65,7 +81,7 @@ const Navigator: React.FC = () => {
getItemProps={getItemProps} getItemProps={getItemProps}
highlightedIndex={highlightedIndex} highlightedIndex={highlightedIndex}
items={getViews(actions)} items={getViews(actions)}
offset={0} offset={getActions(actions).length}
/> />
)} )}
</div> </div>

View file

@ -1,42 +1,78 @@
import makeStyles from "@material-ui/core/styles/makeStyles"; import makeStyles from "@material-ui/core/styles/makeStyles";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { QuickSearchMode } from "./types";
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => {
root: { const typography = {
background: theme.palette.background.default,
border: "none",
color: theme.palette.text.primary, color: theme.palette.text.primary,
fontSize: 24, fontSize: 24,
lineHeight: 1.33
};
return {
adornment: {
...typography,
color: theme.palette.text.secondary,
paddingRight: theme.spacing(1)
},
input: {
...typography,
background: "transparent",
border: "none",
outline: 0, outline: 0,
padding: theme.spacing(2, 3), padding: 0,
width: "100%" width: "100%"
},
root: {
background: theme.palette.background.default,
display: "flex",
padding: theme.spacing(2, 3)
} }
}), };
},
{ {
name: "NavigatorInput" name: "NavigatorInput"
} }
); );
const NavigatorInput: React.FC<
React.InputHTMLAttributes<HTMLInputElement> interface NavigatorInputProps
> = props => { extends React.InputHTMLAttributes<HTMLInputElement> {
mode: QuickSearchMode;
}
const NavigatorInput = React.forwardRef<HTMLInputElement, NavigatorInputProps>(
(props, ref) => {
const { mode, ...rest } = props;
const classes = useStyles(props); const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
return ( return (
<div className={classes.root}>
{mode === "orders" && <span className={classes.adornment}>#</span>}
<input <input
autoFocus autoFocus
autoComplete="off" autoComplete="off"
className={classes.root} className={classes.input}
placeholder={intl.formatMessage({ placeholder={
mode === "orders"
? intl.formatMessage({
defaultMessage: "Order Number",
description: "navigator placeholder"
})
: intl.formatMessage({
defaultMessage: "Use Navigator to move through Saleor", defaultMessage: "Use Navigator to move through Saleor",
description: "navigator placeholder" description: "navigator placeholder"
})} })
{...props} }
ref={ref}
{...rest}
/> />
</div>
); );
}; }
);
NavigatorInput.displayName = "NavigatorInput"; NavigatorInput.displayName = "NavigatorInput";
export default NavigatorInput; export default NavigatorInput;

View file

@ -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;
}

View file

@ -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;

View file

@ -0,0 +1,4 @@
export * from "./actions";
export * from "./default";
export { default } from "./default";
export * from "./views";

View file

@ -20,7 +20,7 @@ import { staffListUrl } from "@saleor/staff/urls";
import { countryListUrl } from "@saleor/taxes/urls"; import { countryListUrl } from "@saleor/taxes/urls";
import { languageListUrl } from "@saleor/translations/urls"; import { languageListUrl } from "@saleor/translations/urls";
import { webhooksListUrl } from "@saleor/webhooks/urls"; import { webhooksListUrl } from "@saleor/webhooks/urls";
import { QuickSearchAction } from "./types"; import { QuickSearchAction } from "../../types";
interface View { interface View {
label: string; label: string;

View file

@ -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;

View file

@ -0,0 +1,10 @@
import { defineMessages } from "react-intl";
const messages = defineMessages({
goToOrder: {
defaultMessage: "Go to order #{orderNumber}",
description: "navigator action"
}
});
export default messages;

View file

@ -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;

View file

@ -1,4 +1,4 @@
export type QuickSearchActionType = "view"; export type QuickSearchActionType = "action" | "view";
export interface QuickSearchAction { export interface QuickSearchAction {
label: string; label: string;
@ -6,3 +6,5 @@ export interface QuickSearchAction {
type: QuickSearchActionType; type: QuickSearchActionType;
url: string; url: string;
} }
export type QuickSearchMode = "default" | "orders" | "customers";

View file

@ -1,39 +1,70 @@
import { useState } from "react"; import { RefObject, useEffect, useState } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { ChangeEvent, FormChange } from "@saleor/hooks/useForm"; import { ChangeEvent, FormChange } from "@saleor/hooks/useForm";
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
import { QuickSearchAction } from "./types"; import getModeActions from "./modes";
import searchInViews from "./views"; import { QuickSearchAction, QuickSearchMode } from "./types";
export type QuickSearchMode = "default" | "orders" | "customers"; type UseQuickSearch = [
const threshold = 0.05; string,
const maxActions = 10; QuickSearchMode,
FormChange,
type UseQuickSearch = [string, FormChange, QuickSearchAction[]]; QuickSearchAction[]
function useQuickSearch(open: boolean): UseQuickSearch { ];
function useQuickSearch(
open: boolean,
input: RefObject<HTMLInputElement>
): UseQuickSearch {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [mode, setMode] = useState<QuickSearchMode>("default"); const [mode, setMode] = useState<QuickSearchMode>("default");
const intl = useIntl(); const intl = useIntl();
useModalDialogOpen(open, { 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 change = (event: ChangeEvent) => {
const value = event.target.value; const value = event.target.value;
if (mode === "default") {
switch (value) {
case "# ":
setMode("orders");
default:
setQuery(value); setQuery(value);
}
} else {
setQuery(value);
}
}; };
return [ return [query, mode, change, getModeActions(mode, query, intl)];
query,
change,
[...searchInViews(query, intl)]
.filter(action => action.score >= threshold)
.sort((a, b) => (a.score <= b.score ? 1 : -1))
.slice(0, maxActions)
];
} }
export default useQuickSearch; export default useQuickSearch;