mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(tasks): make mobile and desktop searches agree (#28580)
Use one token-based task search implementation on desktop and mobile. Preserve explicit issue/PR qualifiers, distinguish quoted search text, and follow shared reexports in mounted mobile recordings. Refs #10950 and #11121. Co-authored-by: MumuTW <42820974+MumuTW@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('./mobile-tasks-dependencies', () => ({ colors: {} }))
|
||||
|
||||
import {
|
||||
githubKindFromQuery,
|
||||
scopeGitHubTaskSearch,
|
||||
normalizeGitHubPreset,
|
||||
getTaskPresetQuery
|
||||
} from './mobile-tasks-item-mapping'
|
||||
|
||||
describe('mobile task search parity', () => {
|
||||
it('lets an explicit issue query override a saved PR preset', () => {
|
||||
for (const preset of ['prs', 'my-prs', 'review'] as const) {
|
||||
expect(githubKindFromQuery('is:issue is:open', preset)).toBe('issues')
|
||||
expect(githubKindFromQuery('is:"issue" is:open', preset)).toBe('issues')
|
||||
expect(scopeGitHubTaskSearch('is:issue is:open', 'prs')).toBe('is:issue is:open')
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves the pull-request alias while browsing issues', () => {
|
||||
expect(scopeGitHubTaskSearch('is:pull-request is:open', 'issues')).toBe(
|
||||
'is:pull-request is:open'
|
||||
)
|
||||
expect(githubKindFromQuery('is:pull-request is:open', 'issues')).toBe('prs')
|
||||
})
|
||||
|
||||
it('opens the PR tab for draft and review searches', () => {
|
||||
for (const query of ['is:draft', 'reviewed-by:@me', 'review-requested:@me', 'is:merged']) {
|
||||
expect(githubKindFromQuery(query, 'issues')).toBe('prs')
|
||||
expect(scopeGitHubTaskSearch(query, 'issues')).toBe(`is:pr ${query}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('does not read scope qualifiers out of labels or quoted search text', () => {
|
||||
expect(scopeGitHubTaskSearch('label:is:pr', 'issues')).toBe('is:issue label:is:pr')
|
||||
expect(scopeGitHubTaskSearch('"is:pull-request"', 'issues')).toBe('is:issue "is:pull-request"')
|
||||
expect(githubKindFromQuery('"is:pr"', 'issues')).toBe('issues')
|
||||
expect(scopeGitHubTaskSearch('is:"pr"', 'issues')).toBe('is:"pr"')
|
||||
})
|
||||
|
||||
it('uses the current split-tab fallback for a legacy all preset', () => {
|
||||
const preset = normalizeGitHubPreset('all')
|
||||
expect(preset).toBe('issues')
|
||||
expect(getTaskPresetQuery(preset)).toBe('is:issue is:open')
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,6 @@
|
||||
import { getGitHubTaskKind } from '../../../src/shared/task-github-search'
|
||||
export { scopeGitHubTaskSearch } from '../../../src/shared/task-github-search'
|
||||
export { getTaskPresetQuery } from '../../../src/shared/task-preset-query'
|
||||
import {
|
||||
type RpcSuccess,
|
||||
type TaskProvider,
|
||||
@@ -44,22 +47,6 @@ export function formatUpdatedAt(value: string): string {
|
||||
return `${Math.floor(hours / 24)}d`
|
||||
}
|
||||
|
||||
export function getTaskPresetQuery(preset: GitHubPreset): string {
|
||||
switch (preset) {
|
||||
case 'my-issues':
|
||||
return 'assignee:@me is:issue is:open'
|
||||
case 'prs':
|
||||
return 'is:pr is:open'
|
||||
case 'my-prs':
|
||||
return 'author:@me is:pr is:open'
|
||||
case 'review':
|
||||
return 'review-requested:@me is:pr is:open'
|
||||
case 'issues':
|
||||
default:
|
||||
return 'is:issue is:open'
|
||||
}
|
||||
}
|
||||
|
||||
export function isTaskProvider(value: unknown): value is TaskProvider {
|
||||
return value === 'github' || value === 'gitlab' || value === 'linear'
|
||||
}
|
||||
@@ -81,15 +68,7 @@ export function normalizeLinearFilter(value: unknown): LinearFilter {
|
||||
}
|
||||
|
||||
export function githubKindFromQuery(query: string, fallbackPreset: GitHubPreset): GitHubTaskKind {
|
||||
if (/\bis:pr\b/i.test(query)) {
|
||||
return 'prs'
|
||||
}
|
||||
if (/\bis:issue\b/i.test(query)) {
|
||||
return 'issues'
|
||||
}
|
||||
return fallbackPreset === 'prs' || fallbackPreset === 'my-prs' || fallbackPreset === 'review'
|
||||
? 'prs'
|
||||
: 'issues'
|
||||
return getGitHubTaskKind(fallbackPreset, query)
|
||||
}
|
||||
|
||||
export function projectRowType(row: GitHubProjectRow): 'issue' | 'pr' | null {
|
||||
@@ -164,17 +143,6 @@ export function projectRowStatusLabel(row: GitHubProjectRow): string {
|
||||
return 'Open'
|
||||
}
|
||||
|
||||
export function scopeGitHubTaskSearch(query: string, kind: GitHubTaskKind): string {
|
||||
const trimmed = query.trim()
|
||||
if (!trimmed) {
|
||||
return getTaskPresetQuery(kind === 'prs' ? 'prs' : 'issues')
|
||||
}
|
||||
if (/\bis:(?:issue|pr)\b/i.test(trimmed)) {
|
||||
return trimmed
|
||||
}
|
||||
return `${kind === 'prs' ? 'is:pr' : 'is:issue'} ${trimmed}`
|
||||
}
|
||||
|
||||
export function gitHubStatusLabel(item: GitHubWorkItem): string {
|
||||
if (item.state === 'merged') {
|
||||
return 'Merged'
|
||||
|
||||
@@ -32,6 +32,21 @@ afterAll(() => {
|
||||
})
|
||||
|
||||
describe('the mounted module loader', () => {
|
||||
it('follows named reexports reached through the task foundation barrel', () => {
|
||||
const modules = loaderOver({
|
||||
'mobile-tasks-legacy-foundation.tsx': `export * from './mapping'`,
|
||||
'mapping.ts': `export { run as search } from './operation'; export type { Missing } from './missing'`,
|
||||
'operation.ts': `export const run = (query: string) => 'is:pr ' + query`
|
||||
})
|
||||
const foundation = modules.load<{ search: (query: string) => string }>(
|
||||
'mobile/src/mobile-tasks-legacy-foundation.tsx'
|
||||
)
|
||||
expect(foundation.search('is:open')).toBe('is:pr is:open')
|
||||
expect(() => modules.load('mobile/src/mobile-tasks-legacy-foundation.tsx').Missing).toThrow(
|
||||
'Unmapped barrel export: Missing'
|
||||
)
|
||||
})
|
||||
|
||||
it('reuses code without sharing exports, module state, mounting dependencies or effects', () => {
|
||||
const root = sourceTree({
|
||||
'state.ts': `
|
||||
|
||||
@@ -106,7 +106,19 @@ export function operationModuleLoader(
|
||||
if (
|
||||
new RegExp(`export (?:async )?(?:function|const|class) ${String(key)}\\b`).test(
|
||||
text
|
||||
)
|
||||
) ||
|
||||
ts
|
||||
.createSourceFile(target, text, ts.ScriptTarget.Latest, true)
|
||||
.statements.some(
|
||||
(entry) =>
|
||||
ts.isExportDeclaration(entry) &&
|
||||
!entry.isTypeOnly &&
|
||||
entry.exportClause &&
|
||||
ts.isNamedExports(entry.exportClause) &&
|
||||
entry.exportClause.elements.some(
|
||||
(binding) => binding.name.text === key && !binding.isTypeOnly
|
||||
)
|
||||
)
|
||||
) {
|
||||
return load(target)[String(key)]
|
||||
}
|
||||
|
||||
@@ -67,6 +67,14 @@ describe('normalizeGitHubTaskPreset', () => {
|
||||
})
|
||||
|
||||
describe('getGitHubTaskKind', () => {
|
||||
it('lets an explicit issue query override a PR-focused preset', () => {
|
||||
for (const preset of ['prs', 'my-prs', 'review'] as const) {
|
||||
expect(getGitHubTaskKind(preset, 'is:issue is:open')).toBe('issues')
|
||||
expect(getGitHubTaskKind(preset, 'is:"issue"')).toBe('issues')
|
||||
expect(getGitHubTaskKind(preset, 'label:is:issue')).toBe('prs')
|
||||
}
|
||||
})
|
||||
|
||||
it('reports prs when the preset is PR-focused', () => {
|
||||
expect(getGitHubTaskKind('review', '')).toBe('prs')
|
||||
})
|
||||
@@ -125,7 +133,7 @@ describe('scopeGitHubTaskSearch', () => {
|
||||
it('lets a parsed issue scope override the caller kind', () => {
|
||||
// characterization: quoting hides `is:issue` from the literal-scope regex but
|
||||
// not from the tokenizer, so the parsed issue scope beats the prs caller kind.
|
||||
expect(scopeGitHubTaskSearch('is:"issue"', 'prs')).toBe('is:issue is:"issue"')
|
||||
expect(scopeGitHubTaskSearch('is:"issue"', 'prs')).toBe('is:"issue"')
|
||||
})
|
||||
|
||||
it('re-prefixes a merged-state query with the caller kind', () => {
|
||||
|
||||
@@ -1,47 +1,7 @@
|
||||
import { getTaskPresetQuery } from '../../../shared/task-preset-query'
|
||||
import { parseTaskQuery } from '../../../shared/task-query'
|
||||
import type { TaskViewPresetId } from '../../../shared/ui-chrome-types'
|
||||
import type { GitHubTaskKind } from '@/components/task-page-localized-options'
|
||||
|
||||
export function isPRFocusedTaskView(preset: TaskViewPresetId | null, query: string): boolean {
|
||||
if (preset === 'prs' || preset === 'my-prs' || preset === 'review') {
|
||||
return true
|
||||
}
|
||||
const parsed = parseTaskQuery(query)
|
||||
return (
|
||||
parsed.scope === 'pr' ||
|
||||
parsed.state === 'merged' ||
|
||||
parsed.draft ||
|
||||
parsed.reviewRequested !== null ||
|
||||
parsed.reviewedBy !== null
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeGitHubTaskPreset(
|
||||
preset: TaskViewPresetId | null | undefined
|
||||
): TaskViewPresetId {
|
||||
// Why: the split Issues/PRs tabs dropped the mixed "All" view, so legacy saved defaults land on the first tab instead of mixing rows.
|
||||
return !preset || preset === 'all' ? 'issues' : preset
|
||||
}
|
||||
|
||||
export function getGitHubTaskKind(preset: TaskViewPresetId | null, query: string): GitHubTaskKind {
|
||||
return isPRFocusedTaskView(preset, query) ? 'prs' : 'issues'
|
||||
}
|
||||
|
||||
export function getDefaultPresetForGitHubTaskKind(kind: GitHubTaskKind): TaskViewPresetId {
|
||||
return kind === 'prs' ? 'prs' : 'issues'
|
||||
}
|
||||
|
||||
export function scopeGitHubTaskSearch(query: string, kind: GitHubTaskKind): string {
|
||||
const trimmed = query.trim()
|
||||
if (!trimmed) {
|
||||
return getTaskPresetQuery(getDefaultPresetForGitHubTaskKind(kind))
|
||||
}
|
||||
if (/\bis:(?:issue|pr|pull-request)\b/i.test(trimmed)) {
|
||||
return trimmed
|
||||
}
|
||||
const parsed = parseTaskQuery(trimmed)
|
||||
// Why: the issue arm still fires for quoted forms like is:"issue" that the literal regex above misses.
|
||||
const inferredKind = parsed.scope === 'pr' ? 'prs' : parsed.scope === 'issue' ? 'issues' : kind
|
||||
return `${inferredKind === 'prs' ? 'is:pr' : 'is:issue'} ${trimmed}`
|
||||
}
|
||||
export {
|
||||
getDefaultPresetForGitHubTaskKind,
|
||||
getGitHubTaskKind,
|
||||
isPRFocusedTaskView,
|
||||
normalizeGitHubTaskPreset,
|
||||
scopeGitHubTaskSearch
|
||||
} from '../../../shared/task-github-search'
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { getTaskPresetQuery } from './task-preset-query'
|
||||
import { hasExplicitTaskScope, parseTaskQuery } from './task-query'
|
||||
import type { TaskViewPresetId } from './ui-chrome-types'
|
||||
export type GitHubTaskKind = 'issues' | 'prs'
|
||||
|
||||
export function isPRFocusedTaskView(preset: TaskViewPresetId | null, query: string): boolean {
|
||||
const parsed = parseTaskQuery(query)
|
||||
if (parsed.scope === 'issue') {
|
||||
return false
|
||||
}
|
||||
if (preset === 'prs' || preset === 'my-prs' || preset === 'review') {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
parsed.scope === 'pr' ||
|
||||
parsed.state === 'merged' ||
|
||||
parsed.draft ||
|
||||
parsed.reviewRequested !== null ||
|
||||
parsed.reviewedBy !== null
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeGitHubTaskPreset(
|
||||
preset: TaskViewPresetId | null | undefined
|
||||
): TaskViewPresetId {
|
||||
// Why: the split Issues/PRs tabs dropped the mixed "All" view, so legacy saved defaults land on the first tab instead of mixing rows.
|
||||
return !preset || preset === 'all' ? 'issues' : preset
|
||||
}
|
||||
|
||||
export function getGitHubTaskKind(preset: TaskViewPresetId | null, query: string): GitHubTaskKind {
|
||||
return isPRFocusedTaskView(preset, query) ? 'prs' : 'issues'
|
||||
}
|
||||
|
||||
export function getDefaultPresetForGitHubTaskKind(kind: GitHubTaskKind): TaskViewPresetId {
|
||||
return kind === 'prs' ? 'prs' : 'issues'
|
||||
}
|
||||
|
||||
export function scopeGitHubTaskSearch(query: string, kind: GitHubTaskKind): string {
|
||||
const trimmed = query.trim()
|
||||
if (!trimmed) {
|
||||
return getTaskPresetQuery(getDefaultPresetForGitHubTaskKind(kind))
|
||||
}
|
||||
if (hasExplicitTaskScope(trimmed)) {
|
||||
return trimmed
|
||||
}
|
||||
const parsed = parseTaskQuery(trimmed)
|
||||
const inferredKind = parsed.scope === 'pr' ? 'prs' : parsed.scope === 'issue' ? 'issues' : kind
|
||||
return `${inferredKind === 'prs' ? 'is:pr' : 'is:issue'} ${trimmed}`
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
hasExplicitTaskScope,
|
||||
parseTaskQuery,
|
||||
serializeTaskQuery,
|
||||
stripRepoQualifiers,
|
||||
@@ -36,6 +37,21 @@ describe('tokenizeSearchQuery', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasExplicitTaskScope', () => {
|
||||
it('recognizes scope qualifiers and their quoted values', () => {
|
||||
for (const query of ['is:issue', 'IS:PR', 'is:pull-request', 'is:"pr"']) {
|
||||
expect(hasExplicitTaskScope(query)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps quoted phrases and values of other qualifiers as search text', () => {
|
||||
for (const query of ['"is:pr"', "'is:issue'", 'label:is:pr', 'title:"is:pull-request"']) {
|
||||
expect(hasExplicitTaskScope(query)).toBe(false)
|
||||
expect(parseTaskQuery(query).scope).toBe('all')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseTaskQuery', () => {
|
||||
it('returns defaults for an empty query', () => {
|
||||
const parsed = parseTaskQuery('')
|
||||
|
||||
@@ -54,6 +54,12 @@ export function tokenizeSearchQuery(rawQuery: string): string[] {
|
||||
return tokenizeSearchQueryWithRaw(rawQuery).map((token) => token.value)
|
||||
}
|
||||
|
||||
export function hasExplicitTaskScope(rawQuery: string): boolean {
|
||||
return tokenizeSearchQueryWithRaw(rawQuery).some(
|
||||
({ value, raw }) => /^is:/i.test(raw) && /^is:(?:issue|pr|pull-request)$/i.test(value)
|
||||
)
|
||||
}
|
||||
|
||||
export function parseTaskQuery(rawQuery: string): ParsedTaskQuery {
|
||||
const query: ParsedTaskQuery = {
|
||||
scope: 'all',
|
||||
@@ -71,6 +77,10 @@ export function parseTaskQuery(rawQuery: string): ParsedTaskQuery {
|
||||
let sawIssueScope = false
|
||||
let sawPRScope = false
|
||||
for (const { value: token, raw } of tokenizeSearchQueryWithRaw(rawQuery.trim())) {
|
||||
if (raw.startsWith('"') || raw.startsWith("'")) {
|
||||
freeTextTokens.push(raw)
|
||||
continue
|
||||
}
|
||||
const normalized = token.toLowerCase()
|
||||
if (normalized === 'is:issue') {
|
||||
sawIssueScope = true
|
||||
|
||||
Reference in New Issue
Block a user