diff --git a/app/actions/definitions/navigation.tsx b/app/actions/definitions/navigation.tsx
index d566737d4..3fbab8362 100644
--- a/app/actions/definitions/navigation.tsx
+++ b/app/actions/definitions/navigation.tsx
@@ -19,14 +19,19 @@ import {
githubIssuesUrl,
} from "@shared/utils/urlHelpers";
import stores from "~/stores";
+import SearchQuery from "~/models/SearchQuery";
import KeyboardShortcuts from "~/scenes/KeyboardShortcuts";
import { createAction } from "~/actions";
-import { NavigationSection } from "~/actions/sections";
+import {
+ NavigationSection,
+ NoSection,
+ RecentSearchesSection,
+} from "~/actions/sections";
import history from "~/utils/history";
import {
settingsPath,
homePath,
- searchUrl,
+ searchPath,
draftsPath,
templatesPath,
archivePath,
@@ -42,14 +47,24 @@ export const navigateToHome = createAction({
visible: ({ location }) => location.pathname !== homePath(),
});
-export const navigateToSearch = createAction({
- name: ({ t }) => t("Search"),
- section: NavigationSection,
- shortcut: ["/"],
- icon: ,
- perform: () => history.push(searchUrl()),
- visible: ({ location }) => location.pathname !== searchUrl(),
-});
+export const navigateToRecentSearchQuery = (searchQuery: SearchQuery) =>
+ createAction({
+ section: RecentSearchesSection,
+ name: searchQuery.query,
+ icon: ,
+ perform: () => history.push(searchPath(searchQuery.query)),
+ });
+
+export const navigateToSearchQuery = (searchQuery: string) =>
+ createAction({
+ id: "search",
+ section: NoSection,
+ name: ({ t }) =>
+ t(`Search documents for "{{searchQuery}}"`, { searchQuery }),
+ icon: ,
+ perform: () => history.push(searchPath(searchQuery)),
+ visible: ({ location }) => location.pathname !== searchPath(),
+ });
export const navigateToDrafts = createAction({
name: ({ t }) => t("Drafts"),
@@ -70,6 +85,7 @@ export const navigateToTemplates = createAction({
export const navigateToArchive = createAction({
name: ({ t }) => t("Archive"),
section: NavigationSection,
+ shortcut: ["g", "a"],
icon: ,
perform: () => history.push(archivePath()),
visible: ({ location }) => location.pathname !== archivePath(),
@@ -145,7 +161,6 @@ export const logout = createAction({
export const rootNavigationActions = [
navigateToHome,
- navigateToSearch,
navigateToDrafts,
navigateToTemplates,
navigateToArchive,
diff --git a/app/actions/index.ts b/app/actions/index.ts
index 2084eeb25..3d271a98e 100644
--- a/app/actions/index.ts
+++ b/app/actions/index.ts
@@ -1,6 +1,6 @@
import { flattenDeep } from "lodash";
import * as React from "react";
-import { $Diff } from "utility-types";
+import { Optional } from "utility-types";
import { v4 as uuidv4 } from "uuid";
import {
Action,
@@ -10,17 +10,10 @@ import {
MenuItemWithChildren,
} from "~/types";
-export function createAction(
- definition: $Diff<
- Action,
- {
- id?: string;
- }
- >
-): Action {
+export function createAction(definition: Optional): Action {
return {
- id: uuidv4(),
...definition,
+ id: uuidv4(),
};
}
diff --git a/app/actions/sections.ts b/app/actions/sections.ts
index e8a6a6320..a387d23a9 100644
--- a/app/actions/sections.ts
+++ b/app/actions/sections.ts
@@ -11,3 +11,8 @@ export const SettingsSection = ({ t }: ActionContext) => t("Settings");
export const NavigationSection = ({ t }: ActionContext) => t("Navigation");
export const UserSection = ({ t }: ActionContext) => t("People");
+
+export const RecentSearchesSection = ({ t }: ActionContext) =>
+ t("Recent searches");
+
+export const NoSection = "";
diff --git a/app/components/AuthenticatedLayout.tsx b/app/components/AuthenticatedLayout.tsx
index 36219e605..12c31551e 100644
--- a/app/components/AuthenticatedLayout.tsx
+++ b/app/components/AuthenticatedLayout.tsx
@@ -11,7 +11,7 @@ import Sidebar from "~/components/Sidebar";
import SettingsSidebar from "~/components/Sidebar/Settings";
import history from "~/utils/history";
import {
- searchUrl,
+ searchPath,
matchDocumentSlug as slug,
newDocumentPath,
settingsPath,
@@ -49,7 +49,7 @@ class AuthenticatedLayout extends React.Component {
if (!ev.metaKey && !ev.ctrlKey) {
ev.preventDefault();
ev.stopPropagation();
- history.push(searchUrl());
+ history.push(searchPath());
}
};
diff --git a/app/components/CommandBar.tsx b/app/components/CommandBar.tsx
index 1e182ef8c..3239ea59e 100644
--- a/app/components/CommandBar.tsx
+++ b/app/components/CommandBar.tsx
@@ -1,24 +1,24 @@
import { useKBar, KBarPositioner, KBarAnimator, KBarSearch } from "kbar";
import { observer } from "mobx-react";
+import { QuestionMarkIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Portal } from "react-portal";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import CommandBarResults from "~/components/CommandBarResults";
+import SearchActions from "~/components/SearchActions";
import rootActions from "~/actions/root";
import useCommandBarActions from "~/hooks/useCommandBarActions";
+import useStores from "~/hooks/useStores";
import { CommandBarAction } from "~/types";
-
-export const CommandBarOptions = {
- animations: {
- enterMs: 250,
- exitMs: 200,
- },
-};
+import { metaDisplay } from "~/utils/keyboard";
+import Text from "./Text";
function CommandBar() {
const { t } = useTranslation();
+ const { ui } = useStores();
+
useCommandBarActions(rootActions);
const { rootAction } = useKBar((state) => ({
@@ -30,20 +30,34 @@ function CommandBar() {
}));
return (
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+ {ui.showModKHint && (
+
+
+ {t(
+ "Open search from anywhere with the {{ shortcut }} shortcut",
+ {
+ shortcut: `${metaDisplay} + k`,
+ }
+ )}
+
+ )}
+
+
+
+ >
);
}
@@ -59,6 +73,20 @@ function KBarPortal({ children }: { children: React.ReactNode }) {
return {children};
}
+const Hint = styled(Text)`
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background: ${(props) => props.theme.secondaryBackground};
+ border-top: 1px solid ${(props) => props.theme.background};
+ margin: 1px 0 0;
+ padding: 6px 16px;
+ width: 100%;
+
+ position: absolute;
+ bottom: 0;
+`;
+
const Positioner = styled(KBarPositioner)`
z-index: ${(props) => props.theme.depths.commandBar};
`;
diff --git a/app/components/CommandBarResults.tsx b/app/components/CommandBarResults.tsx
index 7c2b59ee1..ad184b705 100644
--- a/app/components/CommandBarResults.tsx
+++ b/app/components/CommandBarResults.tsx
@@ -1,14 +1,18 @@
import { useMatches, KBarResults } from "kbar";
+import { orderBy } from "lodash";
import * as React from "react";
import styled from "styled-components";
import CommandBarItem from "~/components/CommandBarItem";
+import { NoSection } from "~/actions/sections";
export default function CommandBarResults() {
const { results, rootActionId } = useMatches();
return (
+ typeof item !== "string" && item.section === NoSection ? -1 : 1
+ )}
maxHeight={400}
onRender={({ item, active }) =>
typeof item === "string" ? (
diff --git a/app/components/InputSearchPage.tsx b/app/components/InputSearchPage.tsx
index a5d75be87..6fec91daf 100644
--- a/app/components/InputSearchPage.tsx
+++ b/app/components/InputSearchPage.tsx
@@ -7,7 +7,7 @@ import styled, { useTheme } from "styled-components";
import useBoolean from "~/hooks/useBoolean";
import useKeyDown from "~/hooks/useKeyDown";
import { isModKey } from "~/utils/keyboard";
-import { searchUrl } from "~/utils/routeHelpers";
+import { searchPath } from "~/utils/routeHelpers";
import Input from "./Input";
type Props = {
@@ -51,7 +51,7 @@ function InputSearchPage({
if (ev.key === "Enter") {
ev.preventDefault();
history.push(
- searchUrl(ev.currentTarget.value, {
+ searchPath(ev.currentTarget.value, {
collectionId,
ref: source,
})
diff --git a/app/components/SearchActions.ts b/app/components/SearchActions.ts
new file mode 100644
index 000000000..b18620bce
--- /dev/null
+++ b/app/components/SearchActions.ts
@@ -0,0 +1,26 @@
+import { useKBar } from "kbar";
+import * as React from "react";
+import {
+ navigateToRecentSearchQuery,
+ navigateToSearchQuery,
+} from "~/actions/definitions/navigation";
+import useCommandBarActions from "~/hooks/useCommandBarActions";
+import useStores from "~/hooks/useStores";
+
+export default function SearchActions() {
+ const { searches } = useStores();
+
+ React.useEffect(() => {
+ searches.fetchPage({});
+ }, [searches]);
+
+ const { searchQuery } = useKBar((state) => ({
+ searchQuery: state.searchQuery,
+ }));
+
+ useCommandBarActions(searchQuery ? [navigateToSearchQuery(searchQuery)] : []);
+
+ useCommandBarActions(searches.recent.map(navigateToRecentSearchQuery));
+
+ return null;
+}
diff --git a/app/components/Sidebar/Main.tsx b/app/components/Sidebar/Main.tsx
index 9b2de6a65..9a60ee347 100644
--- a/app/components/Sidebar/Main.tsx
+++ b/app/components/Sidebar/Main.tsx
@@ -1,3 +1,4 @@
+import { useKBar } from "kbar";
import { observer } from "mobx-react";
import {
EditIcon,
@@ -10,6 +11,7 @@ import * as React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useTranslation } from "react-i18next";
+import { useHistory, useLocation } from "react-router-dom";
import styled from "styled-components";
import Bubble from "~/components/Bubble";
import Flex from "~/components/Flex";
@@ -21,10 +23,10 @@ import useStores from "~/hooks/useStores";
import AccountMenu from "~/menus/AccountMenu";
import {
homePath,
- searchUrl,
draftsPath,
templatesPath,
settingsPath,
+ searchPath,
} from "~/utils/routeHelpers";
import Sidebar from "./Sidebar";
import ArchiveLink from "./components/ArchiveLink";
@@ -38,9 +40,12 @@ import TrashLink from "./components/TrashLink";
function MainSidebar() {
const { t } = useTranslation();
- const { policies, documents } = useStores();
+ const { ui, policies, documents } = useStores();
const team = useCurrentTeam();
const user = useCurrentUser();
+ const { query } = useKBar();
+ const location = useLocation();
+ const history = useHistory();
React.useEffect(() => {
documents.fetchDrafts();
@@ -57,6 +62,16 @@ function MainSidebar() {
);
const can = policies.abilities(team.id);
+ const handleSearch = React.useCallback(() => {
+ const isSearching = location.pathname.startsWith(searchPath());
+ if (isSearching) {
+ history.push(searchPath());
+ } else {
+ ui.enableModKHint();
+ query.toggle();
+ }
+ }, [ui, location, history, query]);
+
return (
{dndArea && (
@@ -81,12 +96,7 @@ function MainSidebar() {
label={t("Home")}
/>
}
label={t("Search")}
exact={false}
diff --git a/app/index.tsx b/app/index.tsx
index 3b7758ad4..4bf59515a 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -8,7 +8,6 @@ import { Router } from "react-router-dom";
import { initI18n } from "@shared/i18n";
import stores from "~/stores";
import Analytics from "~/components/Analytics";
-import { CommandBarOptions } from "~/components/CommandBar";
import Dialogs from "~/components/Dialogs";
import ErrorBoundary from "~/components/ErrorBoundary";
import PageTheme from "~/components/PageTheme";
@@ -53,6 +52,17 @@ if ("serviceWorker" in window.navigator) {
// Make sure to return the specific export containing the feature bundle.
const loadFeatures = () => import("./utils/motion").then((res) => res.default);
+const commandBarOptions = {
+ animations: {
+ enterMs: 250,
+ exitMs: 200,
+ },
+ callbacks: {
+ onClose: () => stores.ui.disableModKHint(),
+ onQueryChange: () => stores.ui.disableModKHint(),
+ },
+};
+
if (element) {
const App = () => (
@@ -60,7 +70,7 @@ if (element) {
-
+
<>
diff --git a/app/scenes/Search/Search.tsx b/app/scenes/Search/Search.tsx
index 9b21bdc1d..5e7cfcf0e 100644
--- a/app/scenes/Search/Search.tsx
+++ b/app/scenes/Search/Search.tsx
@@ -23,7 +23,7 @@ import RegisterKeyDown from "~/components/RegisterKeyDown";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import withStores from "~/components/withStores";
-import { searchUrl } from "~/utils/routeHelpers";
+import { searchPath } from "~/utils/routeHelpers";
import { decodeURIComponentSafe } from "~/utils/urls";
import CollectionFilter from "./components/CollectionFilter";
import DateFilter from "./components/DateFilter";
@@ -247,7 +247,7 @@ class Search extends React.Component {
updateLocation = (query: string) => {
this.props.history.replace({
- pathname: searchUrl(query),
+ pathname: searchPath(query),
search: this.props.location.search,
});
};
diff --git a/app/scenes/Search/components/RecentSearches.tsx b/app/scenes/Search/components/RecentSearches.tsx
index d34937629..a88ea0a82 100644
--- a/app/scenes/Search/components/RecentSearches.tsx
+++ b/app/scenes/Search/components/RecentSearches.tsx
@@ -9,7 +9,7 @@ import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
-import { searchUrl } from "~/utils/routeHelpers";
+import { searchPath } from "~/utils/routeHelpers";
function RecentSearches() {
const { searches } = useStores();
@@ -26,7 +26,7 @@ function RecentSearches() {
{searches.recent.map((searchQuery) => (
-
+
{searchQuery.query}
{
+ this.showModKHint = true;
+ };
+
+ @action
+ disableModKHint = () => {
+ this.showModKHint = false;
+ };
+
@action
hideMobileSidebar = () => {
this.mobileSidebarVisible = false;
diff --git a/app/utils/routeHelpers.ts b/app/utils/routeHelpers.ts
index c19c6dcaa..3b5b779f2 100644
--- a/app/utils/routeHelpers.ts
+++ b/app/utils/routeHelpers.ts
@@ -91,7 +91,7 @@ export function newDocumentPath(
return `/collection/${collectionId}/new?${queryString.stringify(params)}`;
}
-export function searchUrl(
+export function searchPath(
query?: string,
params: {
collectionId?: string;
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index 762618d16..3c02aae5c 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -26,6 +26,7 @@
"Create template": "Create template",
"Home": "Home",
"Search": "Search",
+ "Search documents for \"{{searchQuery}}\"": "Search documents for \"{{searchQuery}}\"",
"Drafts": "Drafts",
"Templates": "Templates",
"Archive": "Archive",
@@ -49,6 +50,7 @@
"Document": "Document",
"Navigation": "Navigation",
"People": "People",
+ "Recent searches": "Recent searches",
"currently editing": "currently editing",
"currently viewing": "currently viewing",
"previously edited": "previously edited",
@@ -59,6 +61,7 @@
"Collapse": "Collapse",
"Expand": "Expand",
"Type a command or search": "Type a command or search",
+ "Open search from anywhere with the {{ shortcut }} shortcut": "Open search from anywhere with the {{ shortcut }} shortcut",
"Server connection lost": "Server connection lost",
"Edits you make will sync once you’re online": "Edits you make will sync once you’re online",
"Submenu": "Submenu",
@@ -364,11 +367,8 @@
"Add groups to {{ collectionName }}": "Add groups to {{ collectionName }}",
"Add people to {{ collectionName }}": "Add people to {{ collectionName }}",
"Document updated by {{userName}}": "Document updated by {{userName}}",
- "This template will be permanently deleted in <2>2> unless restored.": "This template will be permanently deleted in <2>2> unless restored.",
- "This document will be permanently deleted in <2>2> unless restored.": "This document will be permanently deleted in <2>2> unless restored.",
"You have unsaved changes.\nAre you sure you want to discard them?": "You have unsaved changes.\nAre you sure you want to discard them?",
"Images are still uploading.\nAre you sure you want to discard them?": "Images are still uploading.\nAre you sure you want to discard them?",
- "Untitled template": "Untitled template",
"Type '/' to insert, or start writing…": "Type '/' to insert, or start writing…",
"Hide contents": "Hide contents",
"Show contents": "Show contents",
@@ -382,7 +382,10 @@
"Publish": "Publish",
"Publishing": "Publishing",
"Sorry, it looks like you don’t have permission to access the document": "Sorry, it looks like you don’t have permission to access the document",
- "You’re editing a template. Highlight some text and use the <2>2> control to add placeholders that can be filled out when creating new documents from this template.": "You’re editing a template. Highlight some text and use the <2>2> control to add placeholders that can be filled out when creating new documents from this template.",
+ "This template will be permanently deleted in <2>2> unless restored.": "This template will be permanently deleted in <2>2> unless restored.",
+ "This document will be permanently deleted in <2>2> unless restored.": "This document will be permanently deleted in <2>2> unless restored.",
+ "Highlight some text and use the <2>2> control to add placeholders that can be filled out when creating new documents": "Highlight some text and use the <2>2> control to add placeholders that can be filled out when creating new documents",
+ "You’re editing a template": "You’re editing a template",
"Archived by {{userName}}": "Archived by {{userName}}",
"Deleted by {{userName}}": "Deleted by {{userName}}",
"Observing {{ userName }}": "Observing {{ userName }}",
@@ -506,7 +509,6 @@
"Past week": "Past week",
"Past month": "Past month",
"Past year": "Past year",
- "Recent searches": "Recent searches",
"Remove search": "Remove search",
"Active documents": "Active documents",
"Documents in collections you are able to access": "Documents in collections you are able to access",