feat: Trigger cmd+k from sidebar (#3149)

* feat: Trigger cmd+k from sidebar

* Add hint when opening command bar from sidebar
This commit is contained in:
Tom Moor
2022-02-22 20:13:56 -08:00
committed by GitHub
parent d75af27267
commit 63265b49ea
15 changed files with 173 additions and 67 deletions

View File

@@ -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: <SearchIcon />,
perform: () => history.push(searchUrl()),
visible: ({ location }) => location.pathname !== searchUrl(),
});
export const navigateToRecentSearchQuery = (searchQuery: SearchQuery) =>
createAction({
section: RecentSearchesSection,
name: searchQuery.query,
icon: <SearchIcon />,
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: <SearchIcon />,
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: <ArchiveIcon />,
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,

View File

@@ -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, "id">): Action {
return {
id: uuidv4(),
...definition,
id: uuidv4(),
};
}

View File

@@ -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 = "";

View File

@@ -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<Props> {
if (!ev.metaKey && !ev.ctrlKey) {
ev.preventDefault();
ev.stopPropagation();
history.push(searchUrl());
history.push(searchPath());
}
};

View File

@@ -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 (
<KBarPortal>
<Positioner>
<Animator>
<SearchInput
placeholder={`${
rootAction?.placeholder ||
rootAction?.name ||
t("Type a command or search")
}`}
/>
<CommandBarResults />
</Animator>
</Positioner>
</KBarPortal>
<>
<SearchActions />
<KBarPortal>
<Positioner>
<Animator>
<SearchInput
placeholder={`${
rootAction?.placeholder ||
rootAction?.name ||
t("Type a command or search")
}`}
/>
<CommandBarResults />
{ui.showModKHint && (
<Hint size="small" type="tertiary">
<QuestionMarkIcon size={18} color="currentColor" />
{t(
"Open search from anywhere with the {{ shortcut }} shortcut",
{
shortcut: `${metaDisplay} + k`,
}
)}
</Hint>
)}
</Animator>
</Positioner>
</KBarPortal>
</>
);
}
@@ -59,6 +73,20 @@ function KBarPortal({ children }: { children: React.ReactNode }) {
return <Portal>{children}</Portal>;
}
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};
`;

View File

@@ -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 (
<KBarResults
items={results}
items={orderBy(results, (item) =>
typeof item !== "string" && item.section === NoSection ? -1 : 1
)}
maxHeight={400}
onRender={({ item, active }) =>
typeof item === "string" ? (

View File

@@ -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,
})

View File

@@ -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;
}

View File

@@ -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 (
<Sidebar ref={handleSidebarRef}>
{dndArea && (
@@ -81,12 +96,7 @@ function MainSidebar() {
label={t("Home")}
/>
<SidebarLink
to={{
pathname: searchUrl(),
state: {
fromMenu: true,
},
}}
onClick={handleSearch}
icon={<SearchIcon color="currentColor" />}
label={t("Search")}
exact={false}

View File

@@ -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 = () => (
<React.StrictMode>
@@ -60,7 +70,7 @@ if (element) {
<Analytics>
<Theme>
<ErrorBoundary>
<KBarProvider actions={[]} options={CommandBarOptions}>
<KBarProvider actions={[]} options={commandBarOptions}>
<LazyMotion features={loadFeatures}>
<Router history={history}>
<>

View File

@@ -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<Props> {
updateLocation = (query: string) => {
this.props.history.replace({
pathname: searchUrl(query),
pathname: searchPath(query),
search: this.props.location.search,
});
};

View File

@@ -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() {
<List>
{searches.recent.map((searchQuery) => (
<ListItem key={searchQuery.id}>
<RecentSearch to={searchUrl(searchQuery.query)}>
<RecentSearch to={searchPath(searchQuery.query)}>
{searchQuery.query}
<Tooltip tooltip={t("Remove search")} delay={150}>
<RemoveButton

View File

@@ -39,6 +39,9 @@ class UiStore {
@observable
observingUserId: string | undefined;
@observable
showModKHint = false;
@observable
progressBarVisible = false;
@@ -211,6 +214,16 @@ class UiStore {
this.mobileSidebarVisible = !this.mobileSidebarVisible;
};
@action
enableModKHint = () => {
this.showModKHint = true;
};
@action
disableModKHint = () => {
this.showModKHint = false;
};
@action
hideMobileSidebar = () => {
this.mobileSidebarVisible = false;

View File

@@ -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;

View File

@@ -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 youre online": "Edits you make will sync once youre 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 dont have permission to access the document": "Sorry, it looks like you dont have permission to access the document",
"Youre 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.": "Youre 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",
"Youre editing a template": "Youre 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",