Add command mode

This commit is contained in:
dominik-zeglen 2019-11-22 16:39:20 +01:00
parent 564aab26f7
commit e6dfd5c3e5
16 changed files with 213 additions and 61 deletions

View file

@ -4,9 +4,7 @@ import hotkeys from "hotkeys-js";
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 { getActions, getViews, hasActions, hasViews } from "./modes/utils";
import NavigatorInput from "./NavigatorInput";
import NavigatorSection from "./NavigatorSection";
import { QuickSearchAction } from "./types";
@ -18,7 +16,6 @@ const Navigator: React.FC = () => {
const [visible, setVisible] = React.useState(false);
const input = React.useRef(null);
const [query, mode, change, actions] = useQuickSearch(visible, input);
const navigate = useNavigator();
const intl = useIntl();
React.useEffect(() => {
@ -37,8 +34,8 @@ const Navigator: React.FC = () => {
<Downshift
itemToString={(item: QuickSearchAction) => (item ? item.label : "")}
onSelect={(item: QuickSearchAction) => {
navigate(item.url);
setVisible(false);
item.onClick();
}}
onInputValueChange={value =>
change({
@ -60,18 +57,6 @@ const Navigator: React.FC = () => {
})}
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({
@ -84,6 +69,18 @@ const Navigator: React.FC = () => {
offset={getActions(actions).length}
/>
)}
{hasActions(actions) && (
<NavigatorSection
label={intl.formatMessage({
defaultMessage: "Quick Actions",
description: "navigator section header"
})}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
items={getActions(actions)}
offset={0}
/>
)}
</div>
)}
</Downshift>

View file

@ -50,7 +50,11 @@ const NavigatorInput = React.forwardRef<HTMLInputElement, NavigatorInputProps>(
return (
<div className={classes.root}>
{mode === "orders" && <span className={classes.adornment}>#</span>}
{mode !== "default" && (
<span className={classes.adornment}>
{mode === "orders" ? "#" : ">"}
</span>
)}
<input
autoFocus
autoComplete="off"
@ -61,6 +65,11 @@ const NavigatorInput = React.forwardRef<HTMLInputElement, NavigatorInputProps>(
defaultMessage: "Order Number",
description: "navigator placeholder"
})
: mode === "commands"
? intl.formatMessage({
defaultMessage: "Type Command",
description: "navigator placeholder"
})
: intl.formatMessage({
defaultMessage: "Use Navigator to move through Saleor",
description: "navigator placeholder"

View file

@ -0,0 +1,75 @@
import { score } from "fuzzaldrin";
import { IntlShape } from "react-intl";
import { categoryAddUrl } from "@saleor/categories/urls";
import { collectionAddUrl } from "@saleor/collections/urls";
import { customerAddUrl } from "@saleor/customers/urls";
import { voucherAddUrl } from "@saleor/discounts/urls";
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 { QuickSearchAction } from "../../types";
import messages from "../messages";
const threshold = 0.05;
const maxActions = 5;
interface Command {
label: string;
onClick: () => void;
}
export function searchInCommands(
search: string,
intl: IntlShape,
navigate: UseNavigatorResult,
createOrder: MutationFunction<OrderDraftCreate, {}>
): QuickSearchAction[] {
const actions: Command[] = [
{
label: intl.formatMessage(messages.addCategory),
onClick: () => navigate(categoryAddUrl())
},
{
label: intl.formatMessage(messages.addCollection),
onClick: () => navigate(collectionAddUrl)
},
{
label: intl.formatMessage(messages.addProduct),
onClick: () => navigate(productAddUrl)
},
{
label: intl.formatMessage(messages.addCustomer),
onClick: () => navigate(customerAddUrl)
},
{
label: intl.formatMessage(messages.addVoucher),
onClick: () => navigate(voucherAddUrl)
},
{
label: intl.formatMessage(messages.createOrder),
onClick: createOrder
}
];
return actions.map(action => ({
label: action.label,
onClick: action.onClick,
score: score(action.label, search),
type: "action"
}));
}
function getCommandModeActions(
query: string,
intl: IntlShape,
navigate: UseNavigatorResult,
createOrder: MutationFunction<OrderDraftCreate, {}>
): QuickSearchAction[] {
return [...searchInCommands(query, intl, navigate, createOrder)]
.filter(action => action.score >= threshold)
.sort((a, b) => (a.score <= b.score ? 1 : -1))
.slice(0, maxActions);
}
export default getCommandModeActions;

View file

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

View file

@ -1,8 +0,0 @@
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

@ -1,16 +1,25 @@
import { MutationFunction } from "react-apollo";
import { IntlShape } from "react-intl";
import { UseNavigatorResult } from "@saleor/hooks/useNavigator";
import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate";
import { QuickSearchAction } from "../../types";
import { searchInCommands } from "../commands";
import searchInViews from "./views";
const threshold = 0.05;
const maxActions = 10;
const maxActions = 5;
function getDefaultModeActions(
query: string,
intl: IntlShape
intl: IntlShape,
navigate: UseNavigatorResult,
createOrder: MutationFunction<OrderDraftCreate, {}>
): QuickSearchAction[] {
return [...searchInViews(query, intl)]
return [
...searchInViews(query, intl, navigate),
...searchInCommands(query, intl, navigate, createOrder)
]
.filter(action => action.score >= threshold)
.sort((a, b) => (a.score <= b.score ? 1 : -1))
.slice(0, maxActions);

View file

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

View file

@ -6,6 +6,7 @@ import { categoryListUrl } from "@saleor/categories/urls";
import { collectionListUrl } from "@saleor/collections/urls";
import { customerListUrl } from "@saleor/customers/urls";
import { saleListUrl, voucherListUrl } from "@saleor/discounts/urls";
import { UseNavigatorResult } from "@saleor/hooks/useNavigator";
import { sectionNames } from "@saleor/intl";
import { menuListUrl } from "@saleor/navigation/urls";
import { orderDraftListUrl, orderListUrl } from "@saleor/orders/urls";
@ -26,7 +27,11 @@ interface View {
label: string;
url: string;
}
function searchInViews(search: string, intl: IntlShape): QuickSearchAction[] {
function searchInViews(
search: string,
intl: IntlShape,
navigate: UseNavigatorResult
): QuickSearchAction[] {
const views: View[] = [
{
label: intl.formatMessage(sectionNames.attributes),
@ -116,17 +121,10 @@ function searchInViews(search: string, intl: IntlShape): QuickSearchAction[] {
return views.map(view => ({
label: view.label,
onClick: () => navigate(view.url),
score: score(view.label, search),
type: "view",
url: view.url
type: "view"
}));
}
export function getViews(actions: QuickSearchAction[]): QuickSearchAction[] {
return actions.filter(action => action.type === "view");
}
export function hasViews(actions: QuickSearchAction[]): boolean {
return getViews(actions).length > 0;
}
export default searchInViews;

View file

@ -1,10 +1,10 @@
import { IntlShape } from "react-intl";
import {
CheckIfOrderExists,
CheckIfOrderExistsVariables
} from "../queries/types/CheckIfOrderExists";
import { UseNavigatorResult } from "@saleor/hooks/useNavigator";
import { OrderDraftCreate } from "@saleor/orders/types/OrderDraftCreate";
import { MutationFunction } from "react-apollo";
import { QuickSearchAction, QuickSearchMode } from "../types";
import getCommandModeActions from "./commands";
import getDefaultModeActions from "./default";
import getOrdersModeActions from "./orders";
import { ActionQueries } from "./types";
@ -13,13 +13,19 @@ function getModeActions(
mode: QuickSearchMode,
query: string,
intl: IntlShape,
queries: ActionQueries
queries: ActionQueries,
cbs: {
navigate: UseNavigatorResult;
createOrder: MutationFunction<OrderDraftCreate, {}>;
}
): QuickSearchAction[] {
switch (mode) {
case "commands":
return getCommandModeActions(query, intl, cbs.navigate, cbs.createOrder);
case "orders":
return getOrdersModeActions(query, intl, queries.order);
return getOrdersModeActions(query, intl, cbs.navigate, queries.order);
default:
return getDefaultModeActions(query, intl);
return getDefaultModeActions(query, intl, cbs.navigate, cbs.createOrder);
}
}

View file

@ -1,6 +1,30 @@
import { defineMessages } from "react-intl";
const messages = defineMessages({
addCategory: {
defaultMessage: "Add Category",
description: "button"
},
addCollection: {
defaultMessage: "Add Collection",
description: "button"
},
addCustomer: {
defaultMessage: "Add Customer",
description: "button"
},
addProduct: {
defaultMessage: "Add Product",
description: "button"
},
addVoucher: {
defaultMessage: "Add Voucher",
description: "button"
},
createOrder: {
defaultMessage: "Create Order",
description: "button"
},
goToOrder: {
defaultMessage: "Go to order #{orderNumber}",
description: "navigator action"

View file

@ -1,5 +1,6 @@
import { IntlShape } from "react-intl";
import { UseNavigatorResult } from "@saleor/hooks/useNavigator";
import { maybe } from "@saleor/misc";
import { orderUrl } from "@saleor/orders/urls";
import { CheckIfOrderExists_order } from "../queries/types/CheckIfOrderExists";
@ -17,6 +18,7 @@ export function getGqlOrderId(orderNumber: string): string {
function getOrdersModeActions(
query: string,
intl: IntlShape,
navigate: UseNavigatorResult,
order: CheckIfOrderExists_order
): QuickSearchAction[] {
const gqlId = getGqlOrderId(query);
@ -27,9 +29,9 @@ function getOrdersModeActions(
label: intl.formatMessage(messages.goToOrder, {
orderNumber: query
}),
onClick: () => navigate(orderUrl(gqlId)),
score: 1,
type: "action",
url: orderUrl(gqlId)
type: "action"
}
];
}

View file

@ -0,0 +1,15 @@
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;
}
export function getViews(actions: QuickSearchAction[]): QuickSearchAction[] {
return actions.filter(action => action.type === "view");
}
export function hasViews(actions: QuickSearchAction[]): boolean {
return getViews(actions).length > 0;
}

View file

@ -4,7 +4,7 @@ export interface QuickSearchAction {
label: string;
score: number;
type: QuickSearchActionType;
url: string;
onClick: () => void;
}
export type QuickSearchMode = "default" | "orders" | "customers";
export type QuickSearchMode = "default" | "commands" | "orders" | "customers";

View file

@ -3,7 +3,10 @@ import { useIntl } from "react-intl";
import { ChangeEvent, FormChange } from "@saleor/hooks/useForm";
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
import useNavigator from "@saleor/hooks/useNavigator";
import { maybe } from "@saleor/misc";
import { useOrderDraftCreateMutation } from "@saleor/orders/mutations";
import { orderUrl } from "@saleor/orders/urls";
import getModeActions from "./modes";
import { getGqlOrderId, isQueryValidOrderNumber } from "./modes/orders";
import useCheckIfOrderExists from "./queries/useCheckIfOrderExists";
@ -22,7 +25,15 @@ function useQuickSearch(
const [query, setQuery] = useState("");
const [mode, setMode] = useState<QuickSearchMode>("default");
const intl = useIntl();
const navigate = useNavigator();
const [{ data: orderData }, getOrderData] = useCheckIfOrderExists();
const [createOrder] = useOrderDraftCreateMutation({
onCompleted: result => {
if (result.draftOrderCreate.errors.length === 0) {
navigate(orderUrl(result.draftOrderCreate.order.id));
}
}
});
useModalDialogOpen(open, {
onClose: () => {
@ -58,8 +69,12 @@ function useQuickSearch(
if (mode === "default") {
switch (value) {
case "> ":
setMode("commands");
break;
case "# ":
setMode("orders");
break;
default:
setQuery(value);
}
@ -75,9 +90,18 @@ function useQuickSearch(
query,
mode,
change,
getModeActions(mode, query, intl, {
order: maybe(() => orderData.order)
})
getModeActions(
mode,
query,
intl,
{
order: maybe(() => orderData.order)
},
{
createOrder,
navigate
}
)
];
}

View file

@ -11,15 +11,15 @@ import { commonMessages } from "@saleor/intl";
import { maybe } from "@saleor/misc";
import useNotifier from "./useNotifier";
type UseMutation<TData, TVariables> = [
export type UseMutation<TData, TVariables> = [
MutationFunction<TData, TVariables>,
MutationResult<TData>
];
type UseMutationCbs<TData> = Partial<{
export type UseMutationCbs<TData> = Partial<{
onCompleted: (data: TData) => void;
onError: (error: ApolloError) => void;
}>;
type UseMutationHook<TData, TVariables> = (
export type UseMutationHook<TData, TVariables> = (
cbs: UseMutationCbs<TData>
) => UseMutation<TData, TVariables>;

View file

@ -1,5 +1,6 @@
import gql from "graphql-tag";
import makeMutation from "@saleor/hooks/makeMutation";
import { TypedMutation } from "../mutations";
import {
fragmentAddress,
@ -409,10 +410,9 @@ const orderDraftCreateMutation = gql`
}
}
`;
export const TypedOrderDraftCreateMutation = TypedMutation<
OrderDraftCreate,
{}
>(orderDraftCreateMutation);
export const useOrderDraftCreateMutation = makeMutation<OrderDraftCreate, {}>(
orderDraftCreateMutation
);
const orderLineDeleteMutation = gql`
${fragmentOrderDetails}