Add help mode

This commit is contained in:
dominik-zeglen 2019-11-26 15:14:21 +01:00
parent 19021040f0
commit 6faffa24ff
15 changed files with 369 additions and 139 deletions

View file

@ -1,4 +1,8 @@
import Dialog from "@material-ui/core/Dialog"; import Fade from "@material-ui/core/Fade";
import Modal from "@material-ui/core/Modal";
import Paper from "@material-ui/core/Paper";
import makeStyles from "@material-ui/core/styles/makeStyles";
import useTheme from "@material-ui/core/styles/useTheme";
import Downshift from "downshift"; import Downshift from "downshift";
import hotkeys from "hotkeys-js"; import hotkeys from "hotkeys-js";
import React from "react"; import React from "react";
@ -33,6 +37,29 @@ function getItemOffset(
return cbs.reduce((acc, cb) => cb(actions).length + acc, 0); return cbs.reduce((acc, cb) => cb(actions).length + acc, 0);
} }
const useStyles = makeStyles(
theme => ({
modal: {
alignItems: "center",
display: "flex",
justifyContent: "center",
padding: theme.spacing(3)
},
paper: {
overflow: "hidden"
},
root: {
height: 500,
maxWidth: 600,
outline: 0,
width: "100%"
}
}),
{
name: "Navigator"
}
);
const Navigator: React.FC = () => { const Navigator: React.FC = () => {
const [visible, setVisible] = React.useState(false); const [visible, setVisible] = React.useState(false);
const input = React.useRef(null); const input = React.useRef(null);
@ -43,6 +70,8 @@ const Navigator: React.FC = () => {
navigatorNotificationStorageKey, navigatorNotificationStorageKey,
false false
); );
const classes = useStyles({});
const theme = useTheme();
React.useEffect(() => { React.useEffect(() => {
hotkeys(navigatorHotkey, event => { hotkeys(navigatorHotkey, event => {
@ -78,90 +107,99 @@ const Navigator: React.FC = () => {
}, []); }, []);
return ( return (
<Dialog <Modal
className={classes.modal}
open={visible} open={visible}
onClose={() => setVisible(false)} onClose={() => setVisible(false)}
fullWidth
maxWidth="sm"
> >
<Downshift <Fade appear in={visible} timeout={theme.transitions.duration.short}>
itemToString={(item: QuickSearchAction) => (item ? item.label : "")} <div className={classes.root}>
onSelect={(item: QuickSearchAction) => { <Paper className={classes.paper}>
setVisible(false); <Downshift
item.onClick(); itemToString={(item: QuickSearchAction) =>
}} item ? item.label : ""
onInputValueChange={value => }
change({ onSelect={(item: QuickSearchAction) => {
target: { const shouldRemainVisible = item.onClick();
name: "query", if (!shouldRemainVisible) {
value setVisible(false);
} }
}) }}
} onInputValueChange={value =>
defaultHighlightedIndex={0} change({
> target: {
{({ getInputProps, getItemProps, highlightedIndex }) => ( name: "query",
<div> value
<NavigatorInput }
mode={mode} })
value={query} }
{...getInputProps({ defaultHighlightedIndex={0}
value: query >
})} {({ getInputProps, getItemProps, highlightedIndex }) => (
ref={input} <div>
/> <NavigatorInput
{hasViews(actions) && ( mode={mode}
<NavigatorSection value={query}
label={intl.formatMessage({ {...getInputProps({
defaultMessage: "Navigate to", value: query
description: "navigator section header" })}
})} ref={input}
getItemProps={getItemProps} />
highlightedIndex={highlightedIndex} {hasViews(actions) && (
items={getViews(actions)} <NavigatorSection
offset={0} label={intl.formatMessage({
/> defaultMessage: "Navigate to",
)} description: "navigator section header"
{hasActions(actions) && ( })}
<NavigatorSection getItemProps={getItemProps}
label={intl.formatMessage({ highlightedIndex={highlightedIndex}
defaultMessage: "Quick Actions", items={getViews(actions)}
description: "navigator section header" offset={0}
})} />
getItemProps={getItemProps} )}
highlightedIndex={highlightedIndex} {hasActions(actions) && (
items={getActions(actions)} <NavigatorSection
offset={getItemOffset(actions, [getViews])} label={intl.formatMessage({
/> defaultMessage: "Quick Actions",
)} description: "navigator section header"
{hasCustomers(actions) && ( })}
<NavigatorSection getItemProps={getItemProps}
label={intl.formatMessage({ highlightedIndex={highlightedIndex}
defaultMessage: "Search in Customers", items={getActions(actions)}
description: "navigator section header" offset={getItemOffset(actions, [getViews])}
})} />
getItemProps={getItemProps} )}
highlightedIndex={highlightedIndex} {hasCustomers(actions) && (
items={getCustomers(actions)} <NavigatorSection
offset={getItemOffset(actions, [getViews, getActions])} label={intl.formatMessage({
/> defaultMessage: "Search in Customers",
)} description: "navigator section header"
{hasCatalog(actions) && ( })}
<NavigatorSection getItemProps={getItemProps}
label={intl.formatMessage({ highlightedIndex={highlightedIndex}
defaultMessage: "Search in Catalog", items={getCustomers(actions)}
description: "navigator section header" offset={getItemOffset(actions, [getViews, getActions])}
})} />
getItemProps={getItemProps} )}
highlightedIndex={highlightedIndex} {hasCatalog(actions) && (
items={getCatalog(actions)} <NavigatorSection
offset={0} label={intl.formatMessage({
/> defaultMessage: "Search in Catalog",
)} description: "navigator section header"
</div> })}
)} getItemProps={getItemProps}
</Downshift> highlightedIndex={highlightedIndex}
</Dialog> items={getCatalog(actions)}
offset={0}
/>
)}
</div>
)}
</Downshift>
</Paper>
</div>
</Fade>
</Modal>
); );
}; };

View file

@ -58,6 +58,8 @@ const NavigatorInput = React.forwardRef<HTMLInputElement, NavigatorInputProps>(
? "@" ? "@"
: mode === "catalog" : mode === "catalog"
? "$" ? "$"
: mode === "help"
? "?"
: ">"} : ">"}
</span> </span>
)} )}

View file

@ -33,14 +33,19 @@ const useStyles = makeStyles(
textTransform: "uppercase" textTransform: "uppercase"
}, },
root: { root: {
"&:not(:last-child)": { "&:last-child": {
marginBottom: theme.spacing(3) marginBottom: 0
}, },
margin: theme.spacing(2, 0), margin: theme.spacing(2, 0),
padding: theme.spacing(0, 1) padding: theme.spacing(0, 1)
}, },
spacer: { spacer: {
flex: 1 flex: 1
},
symbol: {
display: "inline-block",
fontWeight: 600,
width: theme.spacing(4)
} }
}), }),
{ {
@ -78,6 +83,9 @@ const NavigatorSection: React.FC<NavigatorSectionProps> = props => {
key={[item.label, item.type].join(":")} key={[item.label, item.type].join(":")}
> >
<span className={classes.itemLabel}> <span className={classes.itemLabel}>
{item.symbol && (
<span className={classes.symbol}>{item.symbol}</span>
)}
<span>{item.label}</span> <span>{item.label}</span>
{item.caption && ( {item.caption && (
<Typography variant="caption">{item.caption}</Typography> <Typography variant="caption">{item.caption}</Typography>

View file

@ -26,7 +26,10 @@ export function searchInCatalog(
.map<QuickSearchActionInput>(category => ({ .map<QuickSearchActionInput>(category => ({
caption: intl.formatMessage(messages.category), caption: intl.formatMessage(messages.category),
label: category.name, label: category.name,
onClick: () => navigate(categoryUrl(category.id)), onClick: () => {
navigate(categoryUrl(category.id));
return false;
},
score: score(category.name, search), score: score(category.name, search),
text: category.name, text: category.name,
type: "catalog" type: "catalog"
@ -45,7 +48,10 @@ export function searchInCatalog(
: messages.collectionUnpublished : messages.collectionUnpublished
), ),
label: collection.name, label: collection.name,
onClick: () => navigate(collectionUrl(collection.id)), onClick: () => {
navigate(collectionUrl(collection.id));
return false;
},
score: score(collection.name, search), score: score(collection.name, search),
text: collection.name, text: collection.name,
type: "catalog" type: "catalog"
@ -60,7 +66,10 @@ export function searchInCatalog(
caption: intl.formatMessage(messages.product), caption: intl.formatMessage(messages.product),
extraInfo: product.category.name, extraInfo: product.category.name,
label: product.name, label: product.name,
onClick: () => navigate(productUrl(product.id)), onClick: () => {
navigate(productUrl(product.id));
return false;
},
score: score(product.name, search), score: score(product.name, search),
text: product.name, text: product.name,
type: "catalog" type: "catalog"

View file

@ -9,7 +9,7 @@ import { UseNavigatorResult } from "@saleor/hooks/useNavigator";
import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate"; import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate";
import { productAddUrl } from "@saleor/products/urls"; import { productAddUrl } from "@saleor/products/urls";
import { MutationFunction } from "react-apollo"; import { MutationFunction } from "react-apollo";
import { QuickSearchActionInput } from "../../types"; import { QuickSearchActionInput, QuickSearchMode } from "../../types";
import messages from "../messages"; import messages from "../messages";
import { sortScores } from "../utils"; import { sortScores } from "../utils";
@ -18,38 +18,64 @@ const maxActions = 5;
interface Command { interface Command {
label: string; label: string;
onClick: () => void; onClick: () => boolean;
} }
export function searchInCommands( export function searchInCommands(
search: string, search: string,
intl: IntlShape, intl: IntlShape,
navigate: UseNavigatorResult, navigate: UseNavigatorResult,
createOrder: MutationFunction<OrderDraftCreate, {}> createOrder: MutationFunction<OrderDraftCreate, {}>,
setMode: (mode: QuickSearchMode) => void
): QuickSearchActionInput[] { ): QuickSearchActionInput[] {
const actions: Command[] = [ const actions: Command[] = [
{ {
label: intl.formatMessage(messages.addCategory), label: intl.formatMessage(messages.addCategory),
onClick: () => navigate(categoryAddUrl()) onClick: () => {
navigate(categoryAddUrl());
return false;
}
}, },
{ {
label: intl.formatMessage(messages.addCollection), label: intl.formatMessage(messages.addCollection),
onClick: () => navigate(collectionAddUrl) onClick: () => {
navigate(collectionAddUrl);
return false;
}
}, },
{ {
label: intl.formatMessage(messages.addProduct), label: intl.formatMessage(messages.addProduct),
onClick: () => navigate(productAddUrl) onClick: () => {
navigate(productAddUrl);
return false;
}
}, },
{ {
label: intl.formatMessage(messages.addCustomer), label: intl.formatMessage(messages.addCustomer),
onClick: () => navigate(customerAddUrl) onClick: () => {
navigate(customerAddUrl);
return false;
}
}, },
{ {
label: intl.formatMessage(messages.addVoucher), label: intl.formatMessage(messages.addVoucher),
onClick: () => navigate(voucherAddUrl) onClick: () => {
navigate(voucherAddUrl);
return false;
}
}, },
{ {
label: intl.formatMessage(messages.createOrder), label: intl.formatMessage(messages.createOrder),
onClick: createOrder onClick: () => {
createOrder();
return false;
}
},
{
label: intl.formatMessage(messages.helpMode),
onClick: () => {
setMode("help");
return true;
}
} }
]; ];
@ -66,9 +92,10 @@ function getCommandModeActions(
query: string, query: string,
intl: IntlShape, intl: IntlShape,
navigate: UseNavigatorResult, navigate: UseNavigatorResult,
createOrder: MutationFunction<OrderDraftCreate, {}> createOrder: MutationFunction<OrderDraftCreate, {}>,
setMode: (mode: QuickSearchMode) => void
): QuickSearchActionInput[] { ): QuickSearchActionInput[] {
return [...searchInCommands(query, intl, navigate, createOrder)] return [...searchInCommands(query, intl, navigate, createOrder, setMode)]
.filter(action => action.score >= threshold) .filter(action => action.score >= threshold)
.sort(sortScores) .sort(sortScores)
.slice(0, maxActions); .slice(0, maxActions);

View file

@ -20,7 +20,10 @@ export function searchInCustomers(
lastName: customer.lastName lastName: customer.lastName
}) })
: customer.email, : customer.email,
onClick: () => navigate(customerUrl(customer.id)), onClick: () => {
navigate(customerUrl(customer.id));
return false;
},
score: 1, score: 1,
type: "customer" type: "customer"
})); }));

View file

@ -3,7 +3,7 @@ import { IntlShape } from "react-intl";
import { UseNavigatorResult } from "@saleor/hooks/useNavigator"; import { UseNavigatorResult } from "@saleor/hooks/useNavigator";
import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate"; import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate";
import { QuickSearchAction } from "../../types"; import { QuickSearchAction, QuickSearchMode } from "../../types";
import { searchInCommands } from "../commands"; import { searchInCommands } from "../commands";
import { sortScores } from "../utils"; import { sortScores } from "../utils";
import searchInViews from "./views"; import searchInViews from "./views";
@ -15,11 +15,12 @@ function getDefaultModeActions(
query: string, query: string,
intl: IntlShape, intl: IntlShape,
navigate: UseNavigatorResult, navigate: UseNavigatorResult,
createOrder: MutationFunction<OrderDraftCreate, {}> createOrder: MutationFunction<OrderDraftCreate, {}>,
setMode: (mode: QuickSearchMode) => void
): QuickSearchAction[] { ): QuickSearchAction[] {
return [ return [
...searchInViews(query, intl, navigate), ...searchInViews(query, intl, navigate),
...searchInCommands(query, intl, navigate, createOrder) ...searchInCommands(query, intl, navigate, createOrder, setMode)
] ]
.filter(action => action.score >= threshold) .filter(action => action.score >= threshold)
.sort(sortScores) .sort(sortScores)

View file

@ -121,7 +121,10 @@ function searchInViews(
return views.map(view => ({ return views.map(view => ({
label: view.label, label: view.label,
onClick: () => navigate(view.url), onClick: () => {
navigate(view.url);
return false;
},
score: score(view.label, search), score: score(view.label, search),
text: view.label, text: view.label,
type: "view" type: "view"

View file

@ -0,0 +1,79 @@
import { IntlShape } from "react-intl";
import { QuickSearchAction, QuickSearchMode } from "../types";
import messages from "./messages";
function getHelpModeActions(
query: string,
intl: IntlShape,
setMode: (mode: QuickSearchMode) => void
): QuickSearchAction[] {
if (query !== "") {
return [
{
label: intl.formatMessage(messages.noResults),
onClick: () => true,
type: "action"
}
];
}
return [
{
label: intl.formatMessage(messages.helpDefaultMode),
onClick: () => {
setMode("default");
return true;
},
symbol: "...",
type: "action"
},
{
label: intl.formatMessage(messages.helpCommandsMode),
onClick: () => {
setMode("commands");
return true;
},
symbol: ">",
type: "action"
},
{
label: intl.formatMessage(messages.helpOrdersMode),
onClick: () => {
setMode("orders");
return true;
},
symbol: "#",
type: "action"
},
{
label: intl.formatMessage(messages.helpCustomersMode),
onClick: () => {
setMode("customers");
return true;
},
symbol: "@",
type: "action"
},
{
label: intl.formatMessage(messages.helpCatalogMode),
onClick: () => {
setMode("catalog");
return true;
},
symbol: "$",
type: "action"
},
{
label: intl.formatMessage(messages.helpMode),
onClick: () => {
setMode("help");
return true;
},
symbol: "?",
type: "action"
}
];
}
export default getHelpModeActions;

View file

@ -8,6 +8,7 @@ import getCatalogModeActions from "./catalog";
import getCommandModeActions from "./commands"; import getCommandModeActions from "./commands";
import getCustomersModeActions from "./customers"; import getCustomersModeActions from "./customers";
import getDefaultModeActions from "./default"; import getDefaultModeActions from "./default";
import getHelpModeActions from "./help";
import getOrdersModeActions from "./orders"; import getOrdersModeActions from "./orders";
import { ActionQueries } from "./types"; import { ActionQueries } from "./types";
@ -17,21 +18,36 @@ function getModeActions(
intl: IntlShape, intl: IntlShape,
queries: ActionQueries, queries: ActionQueries,
cbs: { cbs: {
navigate: UseNavigatorResult;
createOrder: MutationFunction<OrderDraftCreate, {}>; createOrder: MutationFunction<OrderDraftCreate, {}>;
navigate: UseNavigatorResult;
setMode: (mode: QuickSearchMode) => void;
} }
): QuickSearchAction[] { ): QuickSearchAction[] {
switch (mode) { switch (mode) {
case "catalog": case "catalog":
return getCatalogModeActions(query, intl, cbs.navigate, queries.catalog); return getCatalogModeActions(query, intl, cbs.navigate, queries.catalog);
case "commands": case "commands":
return getCommandModeActions(query, intl, cbs.navigate, cbs.createOrder); return getCommandModeActions(
query,
intl,
cbs.navigate,
cbs.createOrder,
cbs.setMode
);
case "customers": case "customers":
return getCustomersModeActions(intl, cbs.navigate, queries.customers); return getCustomersModeActions(intl, cbs.navigate, queries.customers);
case "help":
return getHelpModeActions(query, intl, cbs.setMode);
case "orders": case "orders":
return getOrdersModeActions(query, intl, cbs.navigate, queries.order); return getOrdersModeActions(query, intl, cbs.navigate, queries.order);
default: default:
return getDefaultModeActions(query, intl, cbs.navigate, cbs.createOrder); return getDefaultModeActions(
query,
intl,
cbs.navigate,
cbs.createOrder,
cbs.setMode
);
} }
} }

View file

@ -48,6 +48,33 @@ const messages = defineMessages({
defaultMessage: "Go to order #{orderNumber}", defaultMessage: "Go to order #{orderNumber}",
description: "navigator action" description: "navigator action"
}, },
helpCatalogMode: {
defaultMessage: "Search in Catalog",
description: "navigator catalog mode description"
},
helpCommandsMode: {
defaultMessage: "Search Command",
description: "navigator command mode description"
},
helpCustomersMode: {
defaultMessage: "Search Customers",
description: "navigator customer mode description"
},
helpDefaultMode: {
defaultMessage: "Search Views and Actions",
description: "navigator default mode description"
},
helpMode: {
defaultMessage: "Display Help",
description: "navigator help mode description"
},
helpOrdersMode: {
defaultMessage: "Search Orders",
description: "navigator order mode description"
},
noResults: {
defaultMessage: "No Results"
},
product: { product: {
defaultMessage: "Product", defaultMessage: "Product",
description: "catalog item type" description: "catalog item type"

View file

@ -30,7 +30,10 @@ function getOrdersModeActions(
label: intl.formatMessage(messages.goToOrder, { label: intl.formatMessage(messages.goToOrder, {
orderNumber: query orderNumber: query
}), }),
onClick: () => navigate(orderUrl(gqlId)), onClick: () => {
navigate(orderUrl(gqlId));
return false;
},
type: "action" type: "action"
} }
]; ];

View file

@ -1,4 +1,8 @@
import { QuickSearchAction, QuickSearchActionInput } from "../types"; import {
QuickSearchAction,
QuickSearchActionInput,
QuickSearchMode
} from "../types";
export function getActions(actions: QuickSearchAction[]): QuickSearchAction[] { export function getActions(actions: QuickSearchAction[]): QuickSearchAction[] {
return actions.filter(action => action.type === "action"); return actions.filter(action => action.type === "action");
@ -36,3 +40,21 @@ export function sortScores(
) { ) {
return a.score <= b.score ? 1 : -1; return a.score <= b.score ? 1 : -1;
} }
export function getMode(command: string): QuickSearchMode {
switch (command) {
case ">":
return "commands";
case "@":
return "customers";
case "#":
return "orders";
case "$":
return "catalog";
case "?":
return "help";
default:
return null;
}
}

View file

@ -5,8 +5,9 @@ export interface QuickSearchAction {
extraInfo?: string; extraInfo?: string;
label: string; label: string;
price?: number; price?: number;
symbol?: string;
type: QuickSearchActionType; type: QuickSearchActionType;
onClick: () => void; onClick: () => boolean;
} }
export interface QuickSearchActionInput extends QuickSearchAction { export interface QuickSearchActionInput extends QuickSearchAction {
@ -18,5 +19,6 @@ export type QuickSearchMode =
| "default" | "default"
| "catalog" | "catalog"
| "commands" | "commands"
| "orders" | "customers"
| "customers"; | "help"
| "orders";

View file

@ -11,6 +11,7 @@ import { orderUrl } from "@saleor/orders/urls";
import useCustomerSearch from "@saleor/searches/useCustomerSearch"; import useCustomerSearch from "@saleor/searches/useCustomerSearch";
import getModeActions from "./modes"; import getModeActions from "./modes";
import { getGqlOrderId, isQueryValidOrderNumber } from "./modes/orders"; import { getGqlOrderId, isQueryValidOrderNumber } from "./modes/orders";
import { getMode } from "./modes/utils";
import useSearchCatalog from "./queries/useCatalogSearch"; import useSearchCatalog from "./queries/useCatalogSearch";
import useCheckIfOrderExists from "./queries/useCheckIfOrderExists"; import useCheckIfOrderExists from "./queries/useCheckIfOrderExists";
import { QuickSearchAction, QuickSearchMode } from "./types"; import { QuickSearchAction, QuickSearchMode } from "./types";
@ -77,35 +78,23 @@ function useQuickSearch(
const change = (event: ChangeEvent) => { const change = (event: ChangeEvent) => {
const value = event.target.value; const value = event.target.value;
if (mode === "default") { if (mode === "default" || mode === "help") {
switch (value) { const newMode = getMode(value);
case "> ": if (newMode) {
setMode("commands"); setMode(newMode);
break;
case "@ ":
setMode("customers");
break;
case "# ":
setMode("orders");
break;
case "$ ":
setMode("catalog");
break;
default:
setQuery(value);
} }
} else {
if (mode === "orders" && isQueryValidOrderNumber(value)) {
getOrderData(getGqlOrderId(value));
}
if (mode === "catalog") {
searchCatalog(value);
}
if (mode === "customers") {
searchCustomers(value);
}
setQuery(value);
} }
if (mode === "orders" && isQueryValidOrderNumber(value)) {
getOrderData(getGqlOrderId(value));
}
if (mode === "catalog") {
searchCatalog(value);
}
if (mode === "customers") {
searchCustomers(value);
}
setQuery(value);
}; };
return [ return [
@ -126,7 +115,8 @@ function useQuickSearch(
}, },
{ {
createOrder, createOrder,
navigate navigate,
setMode
} }
) )
]; ];