= props => {
+ const { getItemProps, highlightedIndex, label, items, offset } = props;
+
+ const classes = useStyles(props);
+
+ return (
+
+
+ {label}
+
+
+ {items.map((item, itemIndex) => {
+ const index = offset + itemIndex;
+ const itemProps = getItemProps({
+ index,
+ item
+ });
+
+ return (
+
+ );
+ })}
+
+ );
+};
+
+NavigatorSection.displayName = "NavigatorSection";
+export default NavigatorSection;
diff --git a/src/components/Navigator/index.ts b/src/components/Navigator/index.ts
new file mode 100644
index 000000000..8816cc013
--- /dev/null
+++ b/src/components/Navigator/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./Navigator";
+export * from "./Navigator";
diff --git a/src/components/Navigator/types.ts b/src/components/Navigator/types.ts
new file mode 100644
index 000000000..4c15f60a9
--- /dev/null
+++ b/src/components/Navigator/types.ts
@@ -0,0 +1,8 @@
+export type QuickSearchActionType = "view";
+
+export interface QuickSearchAction {
+ label: string;
+ score: number;
+ type: QuickSearchActionType;
+ url: string;
+}
diff --git a/src/components/Navigator/useQuickSearch.ts b/src/components/Navigator/useQuickSearch.ts
new file mode 100644
index 000000000..dc51a9967
--- /dev/null
+++ b/src/components/Navigator/useQuickSearch.ts
@@ -0,0 +1,39 @@
+import { useState } from "react";
+import { useIntl } from "react-intl";
+
+import { ChangeEvent, FormChange } from "@saleor/hooks/useForm";
+import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
+import { QuickSearchAction } from "./types";
+import searchInViews from "./views";
+
+export type QuickSearchMode = "default" | "orders" | "customers";
+const threshold = 0.05;
+const maxActions = 10;
+
+type UseQuickSearch = [string, FormChange, QuickSearchAction[]];
+function useQuickSearch(open: boolean): UseQuickSearch {
+ const [query, setQuery] = useState("");
+ const [mode, setMode] = useState("default");
+ const intl = useIntl();
+
+ useModalDialogOpen(open, {
+ onClose: () => setQuery("")
+ });
+
+ const change = (event: ChangeEvent) => {
+ const value = event.target.value;
+
+ setQuery(value);
+ };
+
+ return [
+ 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;
diff --git a/src/components/Navigator/views.ts b/src/components/Navigator/views.ts
new file mode 100644
index 000000000..000adeaa2
--- /dev/null
+++ b/src/components/Navigator/views.ts
@@ -0,0 +1,132 @@
+import { score } from "fuzzaldrin";
+import { IntlShape } from "react-intl";
+
+import { attributeListUrl } from "@saleor/attributes/urls";
+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 { sectionNames } from "@saleor/intl";
+import { menuListUrl } from "@saleor/navigation/urls";
+import { orderDraftListUrl, orderListUrl } from "@saleor/orders/urls";
+import { pageListUrl } from "@saleor/pages/urls";
+import { pluginsListUrl } from "@saleor/plugins/urls";
+import { productListUrl } from "@saleor/products/urls";
+import { productTypeListUrl } from "@saleor/productTypes/urls";
+import { serviceListUrl } from "@saleor/services/urls";
+import { shippingZonesListUrl } from "@saleor/shipping/urls";
+import { siteSettingsUrl } from "@saleor/siteSettings/urls";
+import { staffListUrl } from "@saleor/staff/urls";
+import { countryListUrl } from "@saleor/taxes/urls";
+import { languageListUrl } from "@saleor/translations/urls";
+import { webhooksListUrl } from "@saleor/webhooks/urls";
+import { QuickSearchAction } from "./types";
+
+interface View {
+ label: string;
+ url: string;
+}
+function searchInViews(search: string, intl: IntlShape): QuickSearchAction[] {
+ const views: View[] = [
+ {
+ label: intl.formatMessage(sectionNames.attributes),
+ url: attributeListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.categories),
+ url: categoryListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.collections),
+ url: collectionListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.customers),
+ url: customerListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.draftOrders),
+ url: orderDraftListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.home),
+ url: "/"
+ },
+ {
+ label: intl.formatMessage(sectionNames.navigation),
+ url: menuListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.orders),
+ url: orderListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.pages),
+ url: pageListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.plugins),
+ url: pluginsListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.productTypes),
+ url: productTypeListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.products),
+ url: productListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.sales),
+ url: saleListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.serviceAccounts),
+ url: serviceListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.shipping),
+ url: shippingZonesListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.siteSettings),
+ url: siteSettingsUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.staff),
+ url: staffListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.taxes),
+ url: countryListUrl
+ },
+ {
+ label: intl.formatMessage(sectionNames.translations),
+ url: languageListUrl
+ },
+ {
+ label: intl.formatMessage(sectionNames.vouchers),
+ url: voucherListUrl()
+ },
+ {
+ label: intl.formatMessage(sectionNames.webhooks),
+ url: webhooksListUrl()
+ }
+ ];
+
+ return views.map(view => ({
+ label: view.label,
+ score: score(view.label, search),
+ type: "view",
+ url: view.url
+ }));
+}
+
+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;
diff --git a/src/index.tsx b/src/index.tsx
index b229ec5d1..915593649 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -12,6 +12,7 @@ import ErrorBoundary from "react-error-boundary";
import { useIntl } from "react-intl";
import { BrowserRouter, Route, Switch } from "react-router-dom";
+import Navigator from "@saleor/components/Navigator";
import useAppState from "@saleor/hooks/useAppState";
import AttributeSection from "./attributes";
import { attributeSection } from "./attributes/urls";
@@ -153,6 +154,7 @@ const Routes: React.FC = () => {
}) =>
isAuthenticated && !tokenAuthLoading && !tokenVerifyLoading ? (
+
dispatchAppState({