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 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 }) => (
<div>
<NavigatorInput
mode={mode}
value={query}
{...getInputProps({
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) && (
<NavigatorSection
label={intl.formatMessage({
@ -65,7 +81,7 @@ const Navigator: React.FC = () => {
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
items={getViews(actions)}
offset={0}
offset={getActions(actions).length}
/>
)}
</div>

View file

@ -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<HTMLInputElement>
> = props => {
const classes = useStyles(props);
const intl = useIntl();
return (
<input
autoFocus
autoComplete="off"
className={classes.root}
placeholder={intl.formatMessage({
defaultMessage: "Use Navigator to move through Saleor",
description: "navigator placeholder"
})}
{...props}
/>
);
};
interface NavigatorInputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
mode: QuickSearchMode;
}
const NavigatorInput = React.forwardRef<HTMLInputElement, NavigatorInputProps>(
(props, ref) => {
const { mode, ...rest } = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<div className={classes.root}>
{mode === "orders" && <span className={classes.adornment}>#</span>}
<input
autoFocus
autoComplete="off"
className={classes.input}
placeholder={
mode === "orders"
? intl.formatMessage({
defaultMessage: "Order Number",
description: "navigator placeholder"
})
: intl.formatMessage({
defaultMessage: "Use Navigator to move through Saleor",
description: "navigator placeholder"
})
}
ref={ref}
{...rest}
/>
</div>
);
}
);
NavigatorInput.displayName = "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 { languageListUrl } from "@saleor/translations/urls";
import { webhooksListUrl } from "@saleor/webhooks/urls";
import { QuickSearchAction } from "./types";
import { QuickSearchAction } from "../../types";
interface View {
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 {
label: string;
@ -6,3 +6,5 @@ export interface QuickSearchAction {
type: QuickSearchActionType;
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 { 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<HTMLInputElement>
): UseQuickSearch {
const [query, setQuery] = useState("");
const [mode, setMode] = useState<QuickSearchMode>("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;