feat: Add searching of sub actions in command menu (#2845)
This commit is contained in:
@@ -102,20 +102,12 @@ export function actionToKBar(
|
|||||||
name: resolvedName,
|
name: resolvedName,
|
||||||
section: resolvedSection,
|
section: resolvedSection,
|
||||||
placeholder: resolvedPlaceholder,
|
placeholder: resolvedPlaceholder,
|
||||||
keywords: `${action.keywords || ""} ${children
|
keywords: `${action.keywords}`,
|
||||||
.filter((c) => !!c.keywords)
|
|
||||||
.map((c) => c.keywords)
|
|
||||||
.join(" ")}`,
|
|
||||||
shortcut: action.shortcut || [],
|
shortcut: action.shortcut || [],
|
||||||
icon: resolvedIcon
|
icon: resolvedIcon,
|
||||||
? React.cloneElement(resolvedIcon, {
|
|
||||||
color: "currentColor",
|
|
||||||
})
|
|
||||||
: undefined,
|
|
||||||
perform: action.perform
|
perform: action.perform
|
||||||
? () => action.perform && action.perform(context)
|
? () => action.perform && action.perform(context)
|
||||||
: undefined,
|
: undefined,
|
||||||
children: children.length ? children.map((a) => a.id) : undefined,
|
|
||||||
},
|
},
|
||||||
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
|
||||||
].concat(children.map((child) => ({ ...child, parent: action.id })));
|
].concat(children.map((child) => ({ ...child, parent: action.id })));
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ function CommandBar() {
|
|||||||
|
|
||||||
const { rootAction } = useKBar((state) => ({
|
const { rootAction } = useKBar((state) => ({
|
||||||
rootAction: state.currentRootActionId
|
rootAction: state.currentRootActionId
|
||||||
? (state.actions[state.currentRootActionId] as CommandBarAction)
|
? ((state.actions[
|
||||||
|
state.currentRootActionId
|
||||||
|
] as unknown) as CommandBarAction)
|
||||||
: undefined,
|
: undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,55 @@
|
|||||||
import { BackIcon } from "outline-icons";
|
import { ActionImpl } from "kbar";
|
||||||
|
import { ArrowIcon, BackIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled, { css } from "styled-components";
|
||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
import Key from "~/components/Key";
|
import Key from "~/components/Key";
|
||||||
import { CommandBarAction } from "~/types";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
action: CommandBarAction;
|
action: ActionImpl;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
currentRootActionId: string | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
function CommandBarItem(
|
function CommandBarItem(
|
||||||
{ action, active }: Props,
|
{ action, active, currentRootActionId }: Props,
|
||||||
ref: React.RefObject<HTMLDivElement>
|
ref: React.RefObject<HTMLDivElement>
|
||||||
) {
|
) {
|
||||||
|
const ancestors = React.useMemo(() => {
|
||||||
|
if (!currentRootActionId) {
|
||||||
|
return action.ancestors;
|
||||||
|
}
|
||||||
|
const index = action.ancestors.findIndex(
|
||||||
|
(ancestor) => ancestor.id === currentRootActionId
|
||||||
|
);
|
||||||
|
|
||||||
|
// +1 removes the currentRootAction; e.g. if we are on the "Set theme"
|
||||||
|
// parent action, the UI should not display "Set theme… > Dark" but rather
|
||||||
|
// just "Dark"
|
||||||
|
return action.ancestors.slice(index + 1);
|
||||||
|
}, [action.ancestors, currentRootActionId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Item active={active} ref={ref}>
|
<Item active={active} ref={ref}>
|
||||||
<Text align="center" gap={8}>
|
<Text align="center" gap={8}>
|
||||||
<Icon>
|
<Icon>
|
||||||
{action.icon ? (
|
{action.icon ? (
|
||||||
|
// @ts-expect-error no icon on ActionImpl
|
||||||
React.cloneElement(action.icon, {
|
React.cloneElement(action.icon, {
|
||||||
size: 22,
|
size: 22,
|
||||||
|
color: "currentColor",
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<ForwardIcon color="currentColor" size={22} />
|
<ArrowIcon color="currentColor" />
|
||||||
)}
|
)}
|
||||||
</Icon>
|
</Icon>
|
||||||
|
|
||||||
|
{ancestors.map((ancestor) => (
|
||||||
|
<React.Fragment key={ancestor.id}>
|
||||||
|
<Ancestor>{ancestor.name}</Ancestor>
|
||||||
|
<ForwardIcon color="currentColor" size={20} />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
{action.name}
|
{action.name}
|
||||||
{action.children?.length ? "…" : ""}
|
{action.children?.length ? "…" : ""}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -46,9 +70,16 @@ function CommandBarItem(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Icon = styled.div`
|
const Icon = styled(Flex)`
|
||||||
width: 22px;
|
align-items: center;
|
||||||
height: 22px;
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
color: ${(props) => props.theme.textSecondary};
|
||||||
|
flex-shrink: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Ancestor = styled.span`
|
||||||
color: ${(props) => props.theme.textSecondary};
|
color: ${(props) => props.theme.textSecondary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -60,7 +91,7 @@ const Text = styled(Flex)`
|
|||||||
|
|
||||||
const Item = styled.div<{ active?: boolean }>`
|
const Item = styled.div<{ active?: boolean }>`
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding: 12px 16px;
|
padding: 10px 16px;
|
||||||
background: ${(props) =>
|
background: ${(props) =>
|
||||||
props.active ? props.theme.menuItemSelected : "none"};
|
props.active ? props.theme.menuItemSelected : "none"};
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -72,10 +103,19 @@ const Item = styled.div<{ active?: boolean }>`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.active &&
|
||||||
|
css`
|
||||||
|
${Icon} {
|
||||||
|
color: ${props.theme.text};
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ForwardIcon = styled(BackIcon)`
|
const ForwardIcon = styled(BackIcon)`
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
|
flex-shrink: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default React.forwardRef<HTMLDivElement, Props>(CommandBarItem);
|
export default React.forwardRef<HTMLDivElement, Props>(CommandBarItem);
|
||||||
|
|||||||
@@ -1,33 +1,24 @@
|
|||||||
import { useMatches, KBarResults, Action } from "kbar";
|
import { useMatches, KBarResults } from "kbar";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import CommandBarItem from "~/components/CommandBarItem";
|
import CommandBarItem from "~/components/CommandBarItem";
|
||||||
import { CommandBarAction } from "~/types";
|
|
||||||
|
|
||||||
export default function CommandBarResults() {
|
export default function CommandBarResults() {
|
||||||
const matches = useMatches();
|
const { results, rootActionId } = useMatches();
|
||||||
const items = React.useMemo(
|
|
||||||
() =>
|
|
||||||
matches
|
|
||||||
.reduce((acc, curr) => {
|
|
||||||
const { actions, name } = curr;
|
|
||||||
acc.push(name);
|
|
||||||
acc.push(...actions);
|
|
||||||
return acc;
|
|
||||||
}, [] as (Action | string)[])
|
|
||||||
.filter((i) => i !== "none"),
|
|
||||||
[matches]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KBarResults
|
<KBarResults
|
||||||
items={items}
|
items={results}
|
||||||
maxHeight={400}
|
maxHeight={400}
|
||||||
onRender={({ item, active }) =>
|
onRender={({ item, active }) =>
|
||||||
typeof item === "string" ? (
|
typeof item === "string" ? (
|
||||||
<Header>{item}</Header>
|
<Header>{item}</Header>
|
||||||
) : (
|
) : (
|
||||||
<CommandBarItem action={item as CommandBarAction} active={active} />
|
<CommandBarItem
|
||||||
|
action={item}
|
||||||
|
active={active}
|
||||||
|
currentRootActionId={rootActionId}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
BookmarkedIcon,
|
BookmarkedIcon,
|
||||||
|
BicycleIcon,
|
||||||
CollectionIcon,
|
CollectionIcon,
|
||||||
CoinsIcon,
|
CoinsIcon,
|
||||||
AcademicCapIcon,
|
AcademicCapIcon,
|
||||||
@@ -26,6 +27,7 @@ import {
|
|||||||
SportIcon,
|
SportIcon,
|
||||||
SunIcon,
|
SunIcon,
|
||||||
TargetIcon,
|
TargetIcon,
|
||||||
|
TerminalIcon,
|
||||||
ToolsIcon,
|
ToolsIcon,
|
||||||
VehicleIcon,
|
VehicleIcon,
|
||||||
WarningIcon,
|
WarningIcon,
|
||||||
@@ -74,6 +76,10 @@ export const icons = {
|
|||||||
component: AcademicCapIcon,
|
component: AcademicCapIcon,
|
||||||
keywords: "learn teach lesson guide tutorial onboarding training",
|
keywords: "learn teach lesson guide tutorial onboarding training",
|
||||||
},
|
},
|
||||||
|
bicycle: {
|
||||||
|
component: BicycleIcon,
|
||||||
|
keywords: "bicycle bike cycle",
|
||||||
|
},
|
||||||
beaker: {
|
beaker: {
|
||||||
component: BeakerIcon,
|
component: BeakerIcon,
|
||||||
keywords: "lab research experiment test",
|
keywords: "lab research experiment test",
|
||||||
@@ -162,6 +168,10 @@ export const icons = {
|
|||||||
component: TargetIcon,
|
component: TargetIcon,
|
||||||
keywords: "target goal sales",
|
keywords: "target goal sales",
|
||||||
},
|
},
|
||||||
|
terminal: {
|
||||||
|
component: TerminalIcon,
|
||||||
|
keywords: "terminal code",
|
||||||
|
},
|
||||||
tools: {
|
tools: {
|
||||||
component: ToolsIcon,
|
component: ToolsIcon,
|
||||||
keywords: "tool settings",
|
keywords: "tool settings",
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
"json-loader": "0.5.4",
|
"json-loader": "0.5.4",
|
||||||
"jsonwebtoken": "^8.5.0",
|
"jsonwebtoken": "^8.5.0",
|
||||||
"jszip": "^3.7.1",
|
"jszip": "^3.7.1",
|
||||||
"kbar": "^0.1.0-beta.16",
|
"kbar": "^0.1.0-beta.23",
|
||||||
"koa": "^2.10.0",
|
"koa": "^2.10.0",
|
||||||
"koa-body": "^4.2.0",
|
"koa-body": "^4.2.0",
|
||||||
"koa-compress": "2.0.0",
|
"koa-compress": "2.0.0",
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
"mobx-react": "^6.3.1",
|
"mobx-react": "^6.3.1",
|
||||||
"natural-sort": "^1.0.0",
|
"natural-sort": "^1.0.0",
|
||||||
"nodemailer": "^6.6.1",
|
"nodemailer": "^6.6.1",
|
||||||
"outline-icons": "^1.35.0",
|
"outline-icons": "^1.37.0",
|
||||||
"oy-vey": "^0.10.0",
|
"oy-vey": "^0.10.0",
|
||||||
"passport": "^0.4.1",
|
"passport": "^0.4.1",
|
||||||
"passport-google-oauth2": "^0.2.0",
|
"passport-google-oauth2": "^0.2.0",
|
||||||
|
|||||||
25
yarn.lock
25
yarn.lock
@@ -9682,15 +9682,16 @@ jws@^3.2.2:
|
|||||||
jwa "^1.4.1"
|
jwa "^1.4.1"
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
kbar@^0.1.0-beta.16:
|
kbar@^0.1.0-beta.23:
|
||||||
version "0.1.0-beta.16"
|
version "0.1.0-beta.23"
|
||||||
resolved "https://registry.yarnpkg.com/kbar/-/kbar-0.1.0-beta.16.tgz#ed8517c6f3ec1994dc965bff288a0df25b64bfde"
|
resolved "https://registry.yarnpkg.com/kbar/-/kbar-0.1.0-beta.23.tgz#90f0b1280fdec6790a11447049fbb66fb6840fe6"
|
||||||
integrity sha512-e/jOX5XZRoACgMMQylXdmOko2+2gvKNRC7H6xqZ/6qP9uJfPoh7Tg66RIzGlmms0OVcYkgMHy9Z0a66SNby3Ig==
|
integrity sha512-incORLzU/VwmSLiRnvTk3Figxs0iAXoeuBU8ul0457AlMZtqqCu3KrKLiQwmtFe/wooy6Br6MiYOBGXA44aAJQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@reach/portal" "^0.16.0"
|
"@reach/portal" "^0.16.0"
|
||||||
fast-equals "^2.0.3"
|
fast-equals "^2.0.3"
|
||||||
match-sorter "^6.3.0"
|
match-sorter "^6.3.0"
|
||||||
react-virtual "^2.8.2"
|
react-virtual "^2.8.2"
|
||||||
|
tiny-invariant "^1.2.0"
|
||||||
|
|
||||||
keygrip@~1.1.0:
|
keygrip@~1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
@@ -11183,10 +11184,10 @@ os-browserify@^0.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
||||||
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
|
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
|
||||||
|
|
||||||
outline-icons@^1.26.1, outline-icons@^1.35.0:
|
outline-icons@^1.26.1, outline-icons@^1.37.0:
|
||||||
version "1.35.0"
|
version "1.37.0"
|
||||||
resolved "https://registry.yarnpkg.com/outline-icons/-/outline-icons-1.35.0.tgz#3249d8e8a05b491e8820ca182175336240d1cfa5"
|
resolved "https://registry.yarnpkg.com/outline-icons/-/outline-icons-1.37.0.tgz#d189a07519d10da913fd4fc2942dd49db804fe82"
|
||||||
integrity sha512-QWPrft2WKy6eIGBbzeaO/0sYXHuLvbQoBa7NrGQM5j7R9/AoxItnJdolYWKiZJAH8tUxfp2yuBdg+H9/z1Zf+g==
|
integrity sha512-rWgGbPIrAtRSPHx81NkuR9dKWO1yKbrvT0WcFq5XgetX+Jsri8wQxwt3unSVWVQlkK1AwJR3o5b/FT/b8ifj9w==
|
||||||
|
|
||||||
oy-vey@^0.10.0:
|
oy-vey@^0.10.0:
|
||||||
version "0.10.0"
|
version "0.10.0"
|
||||||
@@ -14372,10 +14373,10 @@ tiny-emitter@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
|
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
|
||||||
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
|
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
|
||||||
|
|
||||||
tiny-invariant@^1.0.1, tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
|
tiny-invariant@^1.0.1, tiny-invariant@^1.0.2, tiny-invariant@^1.0.6, tiny-invariant@^1.2.0:
|
||||||
version "1.1.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
|
||||||
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
|
||||||
|
|
||||||
tiny-warning@^0.0.3:
|
tiny-warning@^0.0.3:
|
||||||
version "0.0.3"
|
version "0.0.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user