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:
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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};
|
||||
`;
|
||||
|
||||
@@ -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" ? (
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
26
app/components/SearchActions.ts
Normal file
26
app/components/SearchActions.ts
Normal 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;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
<>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user