mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 08:02:52 +00:00
Improve GitHub list search
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# GitHub List Search
|
||||
|
||||
## What the code does today
|
||||
|
||||
- `TaskPage` has GitHub Items search input with Enter-to-apply and X-to-clear; it does not claim Cmd/Ctrl+F.
|
||||
- `ProjectViewWrapper` has a local input (`ProjectSearchInput`) with Enter/blur apply and Esc restore+blur.
|
||||
- Project X button is **reset to saved view filter**, not clear-to-empty.
|
||||
- `queryOverride` semantics are:
|
||||
- `undefined`: use `selectedView.filter`
|
||||
- `''`: explicit empty filter for this fetch
|
||||
- any other string: override filter for this fetch
|
||||
- Shared type docs currently claim `''` and `undefined` are equivalent; that is incorrect.
|
||||
|
||||
## Critical corrections
|
||||
|
||||
1. Shortcut behavior is missing, not partially implemented.
|
||||
- Add explicit Cmd/Ctrl+F capture in both GitHub Items and Projects modes.
|
||||
- Use platform check (`navigator.userAgent.includes('Mac') ? metaKey : ctrlKey`) and never hardcode `metaKey`.
|
||||
- Do not capture while dialogs/modals or editable overlays are active.
|
||||
|
||||
2. Project clear behavior in this doc was wrong.
|
||||
- Current code resets to saved filter; GitHub-like clear should apply `queryOverride: ''`.
|
||||
- Keep Esc as the “revert to last applied” interaction.
|
||||
|
||||
3. `queryOverride` contract in shared types is wrong.
|
||||
- Update `GetProjectViewTableArgs.queryOverride` docs to match main-process behavior and cache key behavior.
|
||||
|
||||
## Edge cases this must explicitly handle
|
||||
|
||||
- IME composition: keep Enter suppression for composing text in GitHub Items and Projects.
|
||||
- Repeated Cmd/Ctrl+F: always focus the active search box and select all text.
|
||||
- Mode gating: never steal find when task source is not GitHub, or GitHub mode is not the visible mode.
|
||||
- View switch race: do not let late responses overwrite the visible search state for a newer view.
|
||||
- External mutations and multi-window drift: local optimistic edits and per-window cache are non-authoritative; refresh must remain available and predictable.
|
||||
|
||||
## Consistency and concurrency notes
|
||||
|
||||
- Renderer cache TTL for project tables is 60s; user-initiated apply already forces fetch (`force: true`), which is correct.
|
||||
- Inflight dedupe is per-renderer process only. Separate windows can race and show temporarily different snapshots.
|
||||
- No push invalidation exists for GitHub-side external changes; consistency is pull-based (refresh/fetch).
|
||||
- `queryOverride` is part of cache keys; this is required so saved-filter and empty-filter results do not clobber each other.
|
||||
|
||||
## Feasibility check
|
||||
|
||||
- “One call” claims are not valid for most flows:
|
||||
- `getProjectViewTable` can require multiple GraphQL pages for views, fields, and items.
|
||||
- Discovery and view resolution are paginated.
|
||||
- “Free” claims are not valid:
|
||||
- Every apply/refresh consumes GitHub API budget; forced fetch intentionally bypasses fresh cache.
|
||||
|
||||
## Required doc-aligned implementation outcomes
|
||||
|
||||
1. Cmd/Ctrl+F focuses/selects GitHub Items search when GitHub Items mode is visible.
|
||||
2. Cmd/Ctrl+F focuses/selects Project search when Project view mode is visible.
|
||||
3. Project clear button applies empty override (`''`) immediately.
|
||||
4. Esc in Project search restores last applied query and blurs.
|
||||
5. Shared type comment for `queryOverride` matches runtime semantics.
|
||||
@@ -869,6 +869,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
|
||||
const [taskSearchInput, setTaskSearchInput] = useState(initialTaskQuery)
|
||||
const [appliedTaskSearch, setAppliedTaskSearch] = useState(initialTaskQuery)
|
||||
const taskSearchInputRef = useRef<HTMLInputElement>(null)
|
||||
const [activeTaskPreset, setActiveTaskPreset] = useState<TaskViewPresetId | null>(
|
||||
defaultTaskViewPreset
|
||||
)
|
||||
@@ -1645,6 +1646,59 @@ export default function TaskPage(): React.JSX.Element {
|
||||
[handleApplyTaskSearch]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
taskSource !== 'github' ||
|
||||
githubMode !== 'items' ||
|
||||
dialogWorkItem ||
|
||||
drawerLinearIssue ||
|
||||
newIssueOpen ||
|
||||
newLinearIssueOpen ||
|
||||
activeModal !== 'none'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent): void => {
|
||||
const isMac = navigator.userAgent.includes('Mac')
|
||||
const modifierPressed = isMac ? event.metaKey : event.ctrlKey
|
||||
if (!modifierPressed || event.altKey || event.shiftKey || event.key.toLowerCase() !== 'f') {
|
||||
return
|
||||
}
|
||||
|
||||
const input = taskSearchInputRef.current
|
||||
if (!input) {
|
||||
return
|
||||
}
|
||||
const target = event.target
|
||||
if (
|
||||
target instanceof HTMLElement &&
|
||||
target !== input &&
|
||||
(target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
target.isContentEditable)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
input.focus()
|
||||
input.select()
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown, { capture: true })
|
||||
return () => window.removeEventListener('keydown', onKeyDown, { capture: true })
|
||||
}, [
|
||||
activeModal,
|
||||
dialogWorkItem,
|
||||
drawerLinearIssue,
|
||||
githubMode,
|
||||
newIssueOpen,
|
||||
newLinearIssueOpen,
|
||||
taskSource
|
||||
])
|
||||
|
||||
const openComposerForItem = useCallback(
|
||||
(item: GitHubWorkItem): void => {
|
||||
const linkedWorkItem: LinkedWorkItemSummary = {
|
||||
@@ -2285,6 +2339,8 @@ export default function TaskPage(): React.JSX.Element {
|
||||
<div className="relative min-w-[320px] flex-1">
|
||||
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
ref={taskSearchInputRef}
|
||||
data-github-items-search-input
|
||||
value={taskSearchInput}
|
||||
onChange={handleTaskSearchChange}
|
||||
onKeyDown={handleTaskSearchKeyDown}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Why: top-level container for Project mode. Handles the picker, header,
|
||||
// filter label, count pill, Open-in-GitHub, and all Interaction States
|
||||
// documented in the design doc.
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
ExternalLink,
|
||||
Loader,
|
||||
@@ -799,6 +799,7 @@ function ProjectSearchInput({
|
||||
}): React.JSX.Element {
|
||||
const initial = appliedOverride !== undefined ? appliedOverride : viewFilter
|
||||
const [value, setValue] = useState<string>(initial)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const applied = appliedOverride !== undefined ? appliedOverride : viewFilter
|
||||
const dirty = value !== applied
|
||||
|
||||
@@ -808,14 +809,55 @@ function ProjectSearchInput({
|
||||
onApply(next === viewFilter ? undefined : next)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent): void => {
|
||||
const isMac = navigator.userAgent.includes('Mac')
|
||||
const modifierPressed = isMac ? event.metaKey : event.ctrlKey
|
||||
if (!modifierPressed || event.altKey || event.shiftKey || event.key.toLowerCase() !== 'f') {
|
||||
return
|
||||
}
|
||||
if (document.querySelector('[role="dialog"]')) {
|
||||
return
|
||||
}
|
||||
|
||||
const input = inputRef.current
|
||||
if (!input) {
|
||||
return
|
||||
}
|
||||
const target = event.target
|
||||
if (
|
||||
target instanceof HTMLElement &&
|
||||
target !== input &&
|
||||
(target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
target.isContentEditable)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
input.focus()
|
||||
input.select()
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown, { capture: true })
|
||||
return () => window.removeEventListener('keydown', onKeyDown, { capture: true })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative min-w-[280px] flex-1 max-w-xl">
|
||||
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
ref={inputRef}
|
||||
data-github-project-search-input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.nativeEvent.isComposing) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
apply(value)
|
||||
} else if (e.key === 'Escape') {
|
||||
@@ -835,13 +877,14 @@ function ProjectSearchInput({
|
||||
dirty && 'border-amber-500/50'
|
||||
)}
|
||||
/>
|
||||
{value && value !== viewFilter ? (
|
||||
{value ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Reset to view filter"
|
||||
aria-label="Clear search"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => {
|
||||
setValue(viewFilter)
|
||||
apply(viewFilter)
|
||||
setValue('')
|
||||
apply('')
|
||||
}}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground transition hover:text-foreground"
|
||||
>
|
||||
|
||||
@@ -338,7 +338,8 @@ export type GetProjectViewTableArgs = {
|
||||
viewName?: string
|
||||
/** Ephemeral GitHub-search-syntax query that replaces the view's filter for
|
||||
* this fetch only. The view's stored filter on GitHub is not modified.
|
||||
* Empty string and undefined both mean "use the view's filter as-is". */
|
||||
* `undefined` uses the view's saved filter; `''` explicitly clears it for
|
||||
* this fetch and gets a distinct renderer cache key. */
|
||||
queryOverride?: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user