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 hotkeys from "hotkeys-js";
import React from "react";
@ -33,6 +37,29 @@ function getItemOffset(
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 [visible, setVisible] = React.useState(false);
const input = React.useRef(null);
@ -43,6 +70,8 @@ const Navigator: React.FC = () => {
navigatorNotificationStorageKey,
false
);
const classes = useStyles({});
const theme = useTheme();
React.useEffect(() => {
hotkeys(navigatorHotkey, event => {
@ -78,90 +107,99 @@ const Navigator: React.FC = () => {
}, []);
return (
<Dialog
<Modal
className={classes.modal}
open={visible}
onClose={() => setVisible(false)}
fullWidth
maxWidth="sm"
>
<Downshift
itemToString={(item: QuickSearchAction) => (item ? item.label : "")}
onSelect={(item: QuickSearchAction) => {
setVisible(false);
item.onClick();
}}
onInputValueChange={value =>
change({
target: {
name: "query",
value
}
})
}
defaultHighlightedIndex={0}
>
{({ getInputProps, getItemProps, highlightedIndex }) => (
<div>
<NavigatorInput
mode={mode}
value={query}
{...getInputProps({
value: query
})}
ref={input}
/>
{hasViews(actions) && (
<NavigatorSection
label={intl.formatMessage({
defaultMessage: "Navigate to",
description: "navigator section header"
})}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
items={getViews(actions)}
offset={0}
/>
)}
{hasActions(actions) && (
<NavigatorSection
label={intl.formatMessage({
defaultMessage: "Quick Actions",
description: "navigator section header"
})}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
items={getActions(actions)}
offset={getItemOffset(actions, [getViews])}
/>
)}
{hasCustomers(actions) && (
<NavigatorSection
label={intl.formatMessage({
defaultMessage: "Search in Customers",
description: "navigator section header"
})}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
items={getCustomers(actions)}
offset={getItemOffset(actions, [getViews, getActions])}
/>
)}
{hasCatalog(actions) && (
<NavigatorSection
label={intl.formatMessage({
defaultMessage: "Search in Catalog",
description: "navigator section header"
})}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
items={getCatalog(actions)}
offset={0}
/>
)}
</div>
)}
</Downshift>
</Dialog>
<Fade appear in={visible} timeout={theme.transitions.duration.short}>
<div className={classes.root}>
<Paper className={classes.paper}>
<Downshift
itemToString={(item: QuickSearchAction) =>
item ? item.label : ""
}
onSelect={(item: QuickSearchAction) => {
const shouldRemainVisible = item.onClick();
if (!shouldRemainVisible) {
setVisible(false);
}
}}
onInputValueChange={value =>
change({
target: {
name: "query",
value
}
})
}
defaultHighlightedIndex={0}
>
{({ getInputProps, getItemProps, highlightedIndex }) => (
<div>
<NavigatorInput
mode={mode}
value={query}
{...getInputProps({
value: query
})}
ref={input}
/>
{hasViews(actions) && (
<NavigatorSection
label={intl.formatMessage({
defaultMessage: "Navigate to",
description: "navigator section header"
})}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
items={getViews(actions)}
offset={0}
/>
)}
{hasActions(actions) && (
<NavigatorSection
label={intl.formatMessage({
defaultMessage: "Quick Actions",
description: "navigator section header"
})}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
items={getActions(actions)}
offset={getItemOffset(actions, [getViews])}
/>
)}
{hasCustomers(actions) && (
<NavigatorSection
label={intl.formatMessage({
defaultMessage: "Search in Customers",
description: "navigator section header"
})}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
items={getCustomers(actions)}
offset={getItemOffset(actions, [getViews, getActions])}
/>
)}
{hasCatalog(actions) && (
<NavigatorSection
label={intl.formatMessage({
defaultMessage: "Search in Catalog",
description: "navigator section header"
})}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
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 === "help"
? "?"
: ">"}
</span>
)}

View file

@ -33,14 +33,19 @@ const useStyles = makeStyles(
textTransform: "uppercase"
},
root: {
"&:not(:last-child)": {
marginBottom: theme.spacing(3)
"&:last-child": {
marginBottom: 0
},
margin: theme.spacing(2, 0),
padding: theme.spacing(0, 1)
},
spacer: {
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(":")}
>
<span className={classes.itemLabel}>
{item.symbol && (
<span className={classes.symbol}>{item.symbol}</span>
)}
<span>{item.label}</span>
{item.caption && (
<Typography variant="caption">{item.caption}</Typography>

View file

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

View file

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

View file

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

View file

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

View file

@ -121,7 +121,10 @@ function searchInViews(
return views.map(view => ({
label: view.label,
onClick: () => navigate(view.url),
onClick: () => {
navigate(view.url);
return false;
},
score: score(view.label, search),
text: view.label,
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 getCustomersModeActions from "./customers";
import getDefaultModeActions from "./default";
import getHelpModeActions from "./help";
import getOrdersModeActions from "./orders";
import { ActionQueries } from "./types";
@ -17,21 +18,36 @@ function getModeActions(
intl: IntlShape,
queries: ActionQueries,
cbs: {
navigate: UseNavigatorResult;
createOrder: MutationFunction<OrderDraftCreate, {}>;
navigate: UseNavigatorResult;
setMode: (mode: QuickSearchMode) => void;
}
): QuickSearchAction[] {
switch (mode) {
case "catalog":
return getCatalogModeActions(query, intl, cbs.navigate, queries.catalog);
case "commands":
return getCommandModeActions(query, intl, cbs.navigate, cbs.createOrder);
return getCommandModeActions(
query,
intl,
cbs.navigate,
cbs.createOrder,
cbs.setMode
);
case "customers":
return getCustomersModeActions(intl, cbs.navigate, queries.customers);
case "help":
return getHelpModeActions(query, intl, cbs.setMode);
case "orders":
return getOrdersModeActions(query, intl, cbs.navigate, queries.order);
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}",
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: {
defaultMessage: "Product",
description: "catalog item type"

View file

@ -30,7 +30,10 @@ function getOrdersModeActions(
label: intl.formatMessage(messages.goToOrder, {
orderNumber: query
}),
onClick: () => navigate(orderUrl(gqlId)),
onClick: () => {
navigate(orderUrl(gqlId));
return false;
},
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[] {
return actions.filter(action => action.type === "action");
@ -36,3 +40,21 @@ export function sortScores(
) {
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;
label: string;
price?: number;
symbol?: string;
type: QuickSearchActionType;
onClick: () => void;
onClick: () => boolean;
}
export interface QuickSearchActionInput extends QuickSearchAction {
@ -18,5 +19,6 @@ export type QuickSearchMode =
| "default"
| "catalog"
| "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 getModeActions from "./modes";
import { getGqlOrderId, isQueryValidOrderNumber } from "./modes/orders";
import { getMode } from "./modes/utils";
import useSearchCatalog from "./queries/useCatalogSearch";
import useCheckIfOrderExists from "./queries/useCheckIfOrderExists";
import { QuickSearchAction, QuickSearchMode } from "./types";
@ -77,35 +78,23 @@ function useQuickSearch(
const change = (event: ChangeEvent) => {
const value = event.target.value;
if (mode === "default") {
switch (value) {
case "> ":
setMode("commands");
break;
case "@ ":
setMode("customers");
break;
case "# ":
setMode("orders");
break;
case "$ ":
setMode("catalog");
break;
default:
setQuery(value);
if (mode === "default" || mode === "help") {
const newMode = getMode(value);
if (newMode) {
setMode(newMode);
}
} 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 [
@ -126,7 +115,8 @@ function useQuickSearch(
},
{
createOrder,
navigate
navigate,
setMode
}
)
];