Add command mode
This commit is contained in:
parent
564aab26f7
commit
e6dfd5c3e5
16 changed files with 213 additions and 61 deletions
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
75
src/components/Navigator/modes/commands/actions.ts
Normal file
75
src/components/Navigator/modes/commands/actions.ts
Normal 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;
|
2
src/components/Navigator/modes/commands/index.ts
Normal file
2
src/components/Navigator/modes/commands/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./actions";
|
||||
export { default } from "./actions";
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
export * from "./actions";
|
||||
export * from "./default";
|
||||
export { default } from "./default";
|
||||
export * from "./views";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
|
15
src/components/Navigator/modes/utils.ts
Normal file
15
src/components/Navigator/modes/utils.ts
Normal 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;
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue