Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(command-bar): add commands from existing palette #18355

Merged
merged 29 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ef46acc
scaffold
thmsobrmlr Nov 2, 2023
f9a5ee7
separate out action result
thmsobrmlr Nov 2, 2023
2db8426
cleanup
thmsobrmlr Nov 2, 2023
20f3b26
allow executing result
thmsobrmlr Nov 2, 2023
efcc85f
Update UI snapshots for `chromium` (2)
github-actions[bot] Nov 8, 2023
645d072
Update UI snapshots for `chromium` (2)
github-actions[bot] Nov 8, 2023
2469fb9
remove isFirst
thmsobrmlr Nov 8, 2023
cd0c8c4
move event listener to logic
thmsobrmlr Nov 8, 2023
49fbad7
add scope to name util
thmsobrmlr Nov 8, 2023
04d35c0
wip
thmsobrmlr Nov 9, 2023
e083cba
rename index to CommandPalette
thmsobrmlr Nov 10, 2023
ab9b9fd
minor changes
thmsobrmlr Nov 10, 2023
5570519
wip
thmsobrmlr Nov 10, 2023
5ac9203
wip
thmsobrmlr Nov 10, 2023
8e2f3db
keyboard commands
thmsobrmlr Nov 10, 2023
de0dc9f
cleanup
thmsobrmlr Nov 10, 2023
dc47a89
Update UI snapshots for `webkit` (2)
github-actions[bot] Nov 10, 2023
e506066
move useEventListener into commandBarLogic
thmsobrmlr Nov 10, 2023
44688a9
Update UI snapshots for `webkit` (2)
github-actions[bot] Nov 10, 2023
6299368
hide dashboard search from commands
thmsobrmlr Nov 10, 2023
fe27ef0
add comments
thmsobrmlr Nov 10, 2023
099fb53
add mouse hover interaction
thmsobrmlr Nov 11, 2023
0655da4
Update UI snapshots for `chromium` (2)
github-actions[bot] Nov 11, 2023
c443637
Update UI snapshots for `webkit` (2)
github-actions[bot] Nov 11, 2023
9678f07
Update UI snapshots for `webkit` (2)
github-actions[bot] Nov 11, 2023
76b07e9
Update UI snapshots for `chromium` (2)
github-actions[bot] Nov 11, 2023
df66174
Update UI snapshots for `chromium` (2)
github-actions[bot] Nov 11, 2023
202834b
Update UI snapshots for `chromium` (1)
github-actions[bot] Nov 11, 2023
fc94c51
Update UI snapshots for `chromium` (1)
github-actions[bot] Nov 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The placeholder didn't render when doing CMD+K (it did with CMD+SHIFT+K)
placeholder

thmsobrmlr marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't clear what this action does (I assume it opens the toolbar). Do we need some kind of description?
Screenshot 2023-11-13 at 09 57 04

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blank space below the options looked weird to me. Maybe it should be the same color as the options Screenshot 2023-11-13 at 09 56 23

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/src/layout/navigation-3000/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CommandPalette } from 'lib/components/CommandPalette'
import { CommandPalette } from 'lib/components/CommandPalette/CommandPalette'
import { useMountedLogic, useValues } from 'kea'
import { ReactNode, useEffect } from 'react'
import { Breadcrumbs } from './components/Breadcrumbs'
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/layout/navigation/TopBar/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { SitePopover } from './SitePopover'
import { Announcement } from './Announcement'
import { navigationLogic } from '../navigationLogic'
import { HelpButton } from 'lib/components/HelpButton/HelpButton'
import { CommandPalette } from 'lib/components/CommandPalette'
import { CommandPalette } from 'lib/components/CommandPalette/CommandPalette'
import { CreateOrganizationModal } from 'scenes/organization/CreateOrganizationModal'
import { InviteModal } from 'scenes/organization/Settings/InviteModal'
import { Link } from 'lib/lemon-ui/Link'
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/lib/components/CommandBar/ActionBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useValues } from 'kea'

import { actionBarLogic } from './actionBarLogic'

import ActionInput from './ActionInput'
import ActionResults from './ActionResults'

const ActionBar = (): JSX.Element => {
const { activeFlow } = useValues(actionBarLogic)

return (
<div className="flex flex-col h-full">
{(!activeFlow || activeFlow.instruction) && <ActionInput />}
{<ActionResults />}
</div>
)
}

export default ActionBar
43 changes: 43 additions & 0 deletions frontend/src/lib/components/CommandBar/ActionInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react'
import { useActions, useValues } from 'kea'

import { LemonInput } from '@posthog/lemon-ui'
import { KeyboardShortcut } from '~/layout/navigation-3000/components/KeyboardShortcut'

import { actionBarLogic } from './actionBarLogic'
import { IconChevronRight, IconEdit } from 'lib/lemon-ui/icons'
import { CommandFlow } from 'lib/components/CommandPalette/commandPaletteLogic'

type PrefixIconProps = {
activeFlow: CommandFlow | null
}
const PrefixIcon = ({ activeFlow }: PrefixIconProps): React.ReactElement | null => {
if (activeFlow) {
return <activeFlow.icon className="palette__icon" /> ?? <IconEdit className="palette__icon" />
} else {
return <IconChevronRight className="palette__icon" />
}
}

const ActionInput = (): JSX.Element => {
const { input, activeFlow } = useValues(actionBarLogic)
const { setInput } = useActions(actionBarLogic)

return (
<div className="border-b">
<LemonInput
size="small"
className="CommandBar__input"
fullWidth
prefix={<PrefixIcon activeFlow={activeFlow} />}
suffix={<KeyboardShortcut escape muted />}
placeholder={activeFlow?.instruction ?? 'What would you like to do? Try some suggestions…'}
autoFocus
value={input}
onChange={setInput}
/>
</div>
)
}

export default ActionInput
55 changes: 55 additions & 0 deletions frontend/src/lib/components/CommandBar/ActionResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect, useRef } from 'react'
import { useActions } from 'kea'

import { actionBarLogic } from './actionBarLogic'
import { getNameFromActionScope } from './utils'
import { CommandResultDisplayable } from '../CommandPalette/commandPaletteLogic'

type SearchResultProps = {
result: CommandResultDisplayable
focused: boolean
}

const ActionResult = ({ result, focused }: SearchResultProps): JSX.Element => {
const { executeResult, onMouseEnterResult, onMouseLeaveResult } = useActions(actionBarLogic)

const ref = useRef<HTMLDivElement | null>(null)
const isExecutable = !!result.executor

useEffect(() => {
if (focused) {
ref.current?.scrollIntoView()
}
}, [focused])

return (
<div className={`border-l-4 ${isExecutable ? 'border-primary' : ''}`}>
<div
className={`w-full pl-3 pr-2 ${
focused ? 'bg-secondary-3000-hover' : 'bg-secondary-3000'
} border-b cursor-pointer`}
onMouseEnter={() => {
onMouseEnterResult(result.index)
}}
onMouseLeave={() => {
onMouseLeaveResult()
}}
onClick={() => {
if (isExecutable) {
executeResult(result)
}
}}
ref={ref}
>
<div className="px-2 py-3 w-full space-y-0.5 flex flex-col items-start">
{result.source.scope && (
<span className="text-muted-3000 text-xs">{getNameFromActionScope(result.source.scope)}</span>
)}
<span className="text-text-3000">{result.display}</span>
</div>
</div>
</div>
)
}

export default ActionResult
42 changes: 42 additions & 0 deletions frontend/src/lib/components/CommandBar/ActionResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useValues } from 'kea'

import { CommandResultDisplayable } from '../CommandPalette/commandPaletteLogic'

import { actionBarLogic } from './actionBarLogic'
import ActionResult from './ActionResult'
import { getNameFromActionScope } from 'lib/components/CommandBar/utils'

type ResultsGroupProps = {
scope: string
results: CommandResultDisplayable[]
activeResultIndex: number
}

const ResultsGroup = ({ scope, results, activeResultIndex }: ResultsGroupProps): JSX.Element => {
return (
<>
<div className="border-b pl-3 pr-2 pt-1 pb-1 bg-bg-3000-light">{getNameFromActionScope(scope)}</div>
{results.map((result) => (
<ActionResult
key={`command_result_${result.index}`}
result={result}
focused={result.index === activeResultIndex}
/>
))}
</>
)
}

const ActionResults = (): JSX.Element => {
const { commandSearchResultsGrouped, activeResultIndex } = useValues(actionBarLogic)

return (
<div className="grow overscroll-none overflow-y-auto">
{commandSearchResultsGrouped.map(([scope, results]) => (
<ResultsGroup key={scope} scope={scope} results={results} activeResultIndex={activeResultIndex} />
))}
</div>
)
}

export default ActionResults
thmsobrmlr marked this conversation as resolved.
Show resolved Hide resolved
36 changes: 6 additions & 30 deletions frontend/src/lib/components/CommandBar/CommandBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useRef, forwardRef } from 'react'
import { useRef } from 'react'
import { useActions, useValues } from 'kea'

import { useEventListener } from 'lib/hooks/useEventListener'
import { useOutsideClickHandler } from 'lib/hooks/useOutsideClickHandler'

import { commandBarLogic } from './commandBarLogic'
Expand All @@ -10,43 +9,20 @@ import { BarStatus } from './types'
import './index.scss'
import SearchBar from './SearchBar'
import { LemonModal } from '@posthog/lemon-ui'

const CommandBarContainer = forwardRef<HTMLDivElement, { children?: React.ReactNode }>(function CommandBarContainer(
{ children },
ref
): JSX.Element {
return (
<div className="w-full h-160 max-w-lg bg-bg-3000 rounded overflow-hidden flex flex-col" ref={ref}>
{children}
</div>
)
})
import ActionBar from './ActionBar'

function CommandBar(): JSX.Element | null {
const containerRef = useRef<HTMLDivElement | null>(null)
const { barStatus } = useValues(commandBarLogic)
const { toggleSearchBar, toggleActionsBar, hideCommandBar } = useActions(commandBarLogic)

useEventListener('keydown', (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault()
if (event.shiftKey) {
toggleActionsBar()
} else {
toggleSearchBar()
}
} else if (event.key === 'Escape') {
hideCommandBar()
}
})
const { hideCommandBar } = useActions(commandBarLogic)

useOutsideClickHandler(containerRef, hideCommandBar, [])

return (
<LemonModal isOpen={barStatus !== BarStatus.HIDDEN} simple closable={false} width={800}>
<CommandBarContainer ref={containerRef}>
<SearchBar />
</CommandBarContainer>
<div className="w-full h-160 max-w-lg bg-bg-3000 rounded overflow-hidden flex flex-col" ref={containerRef}>
{barStatus === BarStatus.SHOW_SEARCH ? <SearchBar /> : <ActionBar />}
</div>
</LemonModal>
)
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/components/CommandBar/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import SearchResults from './SearchResults'
import SearchTabs from './SearchTabs'

const SearchBar = (): JSX.Element => {
useMountedLogic(searchBarLogic)
useMountedLogic(searchBarLogic) // load initial results

return (
<div className="flex flex-col h-full">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/components/CommandBar/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const SearchInput = (): JSX.Element => {
<LemonInput
type="search"
size="small"
className="CommandBar__search-input"
className="CommandBar__input"
fullWidth
suffix={<KeyboardShortcut escape muted />}
autoFocus
Expand Down
24 changes: 3 additions & 21 deletions frontend/src/lib/components/CommandBar/SearchResults.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,13 @@
import { useActions, useValues } from 'kea'
import { useEventListener } from 'lib/hooks/useEventListener'
import { useValues } from 'kea'

import { DetectiveHog } from '../hedgehogs'

import { searchBarLogic } from './searchBarLogic'
import SearchResult, { SearchResultSkeleton } from './SearchResult'

const SearchResults = (): JSX.Element => {
const { filterSearchResults, searchResponseLoading, activeResultIndex, keyboardResultIndex, maxIndex } =
const { filterSearchResults, searchResponseLoading, activeResultIndex, keyboardResultIndex } =
useValues(searchBarLogic)
const { onArrowUp, onArrowDown, openResult } = useActions(searchBarLogic)

useEventListener('keydown', (event) => {
if (!filterSearchResults) {
return
}

if (event.key === 'Enter') {
event.preventDefault()
openResult(activeResultIndex)
} else if (event.key === 'ArrowDown') {
event.preventDefault()
onArrowDown(activeResultIndex, maxIndex)
} else if (event.key === 'ArrowUp') {
event.preventDefault()
onArrowUp(activeResultIndex, maxIndex)
}
})

return (
<div className="grow overscroll-none overflow-y-auto">
Expand Down
97 changes: 97 additions & 0 deletions frontend/src/lib/components/CommandBar/actionBarLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { kea, path, listeners, connect, afterMount, beforeUnmount } from 'kea'

import { commandPaletteLogic } from '../CommandPalette/commandPaletteLogic'
import { commandBarLogic } from './commandBarLogic'

import { BarStatus } from './types'

import type { actionBarLogicType } from './actionBarLogicType'

export const actionBarLogic = kea<actionBarLogicType>([
path(['lib', 'components', 'CommandBar', 'actionBarLogic']),
connect({
actions: [
commandBarLogic,
['hideCommandBar', 'setCommandBar'],
commandPaletteLogic,
[
'showPalette',
'hidePalette',
'setInput',
'executeResult',
'backFlow',
'onArrowUp',
'onArrowDown',
'onMouseEnterResult',
'onMouseLeaveResult',
],
],
values: [
commandPaletteLogic,
[
'input',
'activeResultIndex',
'commandRegistrations',
'commandSearchResults',
'commandSearchResultsGrouped',
'activeFlow',
],
],
}),
listeners(({ actions }) => ({
hidePalette: () => {
// listen on hide action from legacy palette, and hide command bar
actions.hideCommandBar()
},
})),
afterMount(({ actions, values, cache }) => {
// trigger show action from legacy palette
actions.showPalette()

// register keyboard shortcuts
cache.onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && values.commandSearchResults.length) {
// execute result
const result = values.commandSearchResults[values.activeResultIndex]
const isExecutable = !!result.executor
if (isExecutable) {
actions.executeResult(result)
}
} else if (event.key === 'ArrowDown') {
// navigate to next result
event.preventDefault()
actions.onArrowDown(values.commandSearchResults.length - 1)
} else if (event.key === 'ArrowUp') {
// navigate to previous result
event.preventDefault()
actions.onArrowUp()
} else if (event.key === 'Escape') {
event.preventDefault()

if (values.activeFlow) {
// return to previous flow
actions.backFlow()
} else if (values.input) {
// or erase input
actions.setInput('')
} else {
// or hide palette
actions.hidePalette()
}
} else if (event.key === 'Backspace') {
if (values.input.length === 0) {
// transition to search when pressing backspace with empty input
actions.setCommandBar(BarStatus.SHOW_SEARCH)
}
}
}
window.addEventListener('keydown', cache.onKeyDown)
}),
beforeUnmount(({ actions, cache }) => {
// trigger hide action from legacy palette
actions.hidePalette()

// unregister keyboard shortcuts
window.removeEventListener('keydown', cache.onKeyDown)
}),
])
Loading
Loading