Add order search mode
This commit is contained in:
parent
8b36ca5308
commit
09dce08f3d
11 changed files with 224 additions and 51 deletions
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
outline: 0,
|
lineHeight: 1.33
|
||||||
padding: theme.spacing(2, 3),
|
};
|
||||||
width: "100%"
|
|
||||||
}
|
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"
|
name: "NavigatorInput"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const NavigatorInput: React.FC<
|
|
||||||
React.InputHTMLAttributes<HTMLInputElement>
|
|
||||||
> = props => {
|
|
||||||
const classes = useStyles(props);
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
return (
|
interface NavigatorInputProps
|
||||||
<input
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
autoFocus
|
mode: QuickSearchMode;
|
||||||
autoComplete="off"
|
}
|
||||||
className={classes.root}
|
|
||||||
placeholder={intl.formatMessage({
|
const NavigatorInput = React.forwardRef<HTMLInputElement, NavigatorInputProps>(
|
||||||
defaultMessage: "Use Navigator to move through Saleor",
|
(props, ref) => {
|
||||||
description: "navigator placeholder"
|
const { mode, ...rest } = props;
|
||||||
})}
|
const classes = useStyles(props);
|
||||||
{...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";
|
NavigatorInput.displayName = "NavigatorInput";
|
||||||
export default NavigatorInput;
|
export default NavigatorInput;
|
||||||
|
|
8
src/components/Navigator/modes/default/actions.ts
Normal file
8
src/components/Navigator/modes/default/actions.ts
Normal 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;
|
||||||
|
}
|
19
src/components/Navigator/modes/default/default.ts
Normal file
19
src/components/Navigator/modes/default/default.ts
Normal 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;
|
4
src/components/Navigator/modes/default/index.ts
Normal file
4
src/components/Navigator/modes/default/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from "./actions";
|
||||||
|
export * from "./default";
|
||||||
|
export { default } from "./default";
|
||||||
|
export * from "./views";
|
|
@ -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;
|
20
src/components/Navigator/modes/index.ts
Normal file
20
src/components/Navigator/modes/index.ts
Normal 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;
|
10
src/components/Navigator/modes/messages.ts
Normal file
10
src/components/Navigator/modes/messages.ts
Normal 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;
|
27
src/components/Navigator/modes/orders.ts
Normal file
27
src/components/Navigator/modes/orders.ts
Normal 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;
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
setQuery(value);
|
if (mode === "default") {
|
||||||
|
switch (value) {
|
||||||
|
case "# ":
|
||||||
|
setMode("orders");
|
||||||
|
default:
|
||||||
|
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;
|
||||||
|
|
Loading…
Reference in a new issue