mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #3778 from omnivore-app/fix/filter
add tests for filters
This commit is contained in:
commit
f8538d2543
6 changed files with 168 additions and 21 deletions
140
packages/api/test/services/library_item.test.ts
Normal file
140
packages/api/test/services/library_item.test.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { expect } from 'chai'
|
||||
import 'mocha'
|
||||
import { filterItemEvents } from '../../src/services/library_item'
|
||||
import { parseSearchQuery } from '../../src/utils/search'
|
||||
|
||||
describe('filterItemEvents', () => {
|
||||
it('returns events if there are quotation marks in the subscription name', () => {
|
||||
const query = 'subscription:"Best \\\"Omnivore\\\""'
|
||||
const ast = parseSearchQuery(query)
|
||||
const events = [
|
||||
{
|
||||
subscription: 'Best "Omnivore"',
|
||||
},
|
||||
]
|
||||
const result = filterItemEvents(ast, events)
|
||||
expect(result).to.eql(events)
|
||||
})
|
||||
|
||||
it('returns events if subscription name equals ignore case', () => {
|
||||
const query = 'subscription:substack'
|
||||
const ast = parseSearchQuery(query)
|
||||
const events = [
|
||||
{
|
||||
subscription: 'Substack',
|
||||
},
|
||||
]
|
||||
const result = filterItemEvents(ast, events)
|
||||
expect(result).to.eql(events)
|
||||
})
|
||||
|
||||
it('returns events if site name equals ignore case', () => {
|
||||
const query = 'site:youtube'
|
||||
const ast = parseSearchQuery(query)
|
||||
const events = [
|
||||
{
|
||||
siteName: 'YouTube',
|
||||
},
|
||||
]
|
||||
const result = filterItemEvents(ast, events)
|
||||
expect(result).to.eql(events)
|
||||
})
|
||||
|
||||
it('returns events if site name contains the search query', () => {
|
||||
const query = 'site:standard'
|
||||
const ast = parseSearchQuery(query)
|
||||
const events = [
|
||||
{
|
||||
siteName: 'Der Standard',
|
||||
},
|
||||
]
|
||||
const result = filterItemEvents(ast, events)
|
||||
expect(result).to.eql(events)
|
||||
})
|
||||
|
||||
it('returns events if domain name contains the search query', () => {
|
||||
const query = 'site:stackoverflow.com'
|
||||
const ast = parseSearchQuery(query)
|
||||
const events = [
|
||||
{
|
||||
siteName: 'Stack Overflow',
|
||||
originalUrl: 'https://stackoverflow.com/questions/123',
|
||||
},
|
||||
]
|
||||
const result = filterItemEvents(ast, events)
|
||||
expect(result).to.eql(events)
|
||||
})
|
||||
|
||||
it('returns events if top level domain matches', () => {
|
||||
const query = 'site:".com"'
|
||||
const ast = parseSearchQuery(query)
|
||||
const events = [
|
||||
{
|
||||
siteName: 'Stack Overflow',
|
||||
originalUrl: 'https://stackoverflow.com/questions/123',
|
||||
},
|
||||
]
|
||||
const result = filterItemEvents(ast, events)
|
||||
expect(result).to.eql(events)
|
||||
})
|
||||
|
||||
it('returns events if labels match the search query', () => {
|
||||
const query = 'label:foo'
|
||||
const ast = parseSearchQuery(query)
|
||||
const events = [
|
||||
{
|
||||
labelNames: ['foo'],
|
||||
},
|
||||
]
|
||||
const result = filterItemEvents(ast, events)
|
||||
expect(result).to.eql(events)
|
||||
})
|
||||
|
||||
it('returns events if labels contain quotation marks', () => {
|
||||
const query = 'label:"foo \\\"bar\\\""'
|
||||
const ast = parseSearchQuery(query)
|
||||
const events = [
|
||||
{
|
||||
labelNames: ['foo "bar"'],
|
||||
},
|
||||
]
|
||||
const result = filterItemEvents(ast, events)
|
||||
expect(result).to.eql(events)
|
||||
})
|
||||
|
||||
it('returns events if labels contain space', () => {
|
||||
const query = 'label:"foo bar"'
|
||||
const ast = parseSearchQuery(query)
|
||||
const events = [
|
||||
{
|
||||
labelNames: ['foo bar'],
|
||||
},
|
||||
]
|
||||
const result = filterItemEvents(ast, events)
|
||||
expect(result).to.eql(events)
|
||||
})
|
||||
|
||||
it('returns events if labels match the search query ignore case', () => {
|
||||
const query = 'label:Foo'
|
||||
const ast = parseSearchQuery(query)
|
||||
const events = [
|
||||
{
|
||||
labelNames: ['foo'],
|
||||
},
|
||||
]
|
||||
const result = filterItemEvents(ast, events)
|
||||
expect(result).to.eql(events)
|
||||
})
|
||||
|
||||
it('returns events if labels match the search query with multiple labels', () => {
|
||||
const query = 'label:foo,bar'
|
||||
const ast = parseSearchQuery(query)
|
||||
const events = [
|
||||
{
|
||||
labelNames: ['foo', 'bar'],
|
||||
},
|
||||
]
|
||||
const result = filterItemEvents(ast, events)
|
||||
expect(result).to.eql(events)
|
||||
})
|
||||
})
|
||||
|
|
@ -20,6 +20,7 @@ import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon'
|
|||
import Link from 'next/link'
|
||||
import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
|
||||
import { NavMenuFooter } from './Footer'
|
||||
import { escapeQuotes } from '../../../utils/helper'
|
||||
|
||||
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
|
||||
|
||||
|
|
@ -255,7 +256,7 @@ function Subscriptions(
|
|||
name: name,
|
||||
keywords: '*' + name,
|
||||
perform: () => {
|
||||
props.applySearchQuery(`subscription:\"${name}\"`)
|
||||
props.applySearchQuery(`subscription:\"${escapeQuotes(name)}\"`)
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
|
@ -291,7 +292,9 @@ function Subscriptions(
|
|||
return (
|
||||
<FilterButton
|
||||
key={item.id}
|
||||
filterTerm={`in:inbox subscription:\"${item.name}\"`}
|
||||
filterTerm={`in:inbox subscription:\"${escapeQuotes(
|
||||
item.name
|
||||
)}\"`}
|
||||
text={item.name}
|
||||
{...props}
|
||||
/>
|
||||
|
|
@ -507,7 +510,7 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
|
|||
const checkboxRef = useRef<HTMLInputElement | null>(null)
|
||||
const state = useMemo(() => {
|
||||
const term = props.searchTerm ?? ''
|
||||
if (term.indexOf(`label:\"${props.label.name}\"`) >= 0) {
|
||||
if (term.indexOf(`label:\"${escapeQuotes(props.label.name)}\"`) >= 0) {
|
||||
return 'on'
|
||||
}
|
||||
return 'off'
|
||||
|
|
@ -557,7 +560,7 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
|
|||
props.applySearchQuery(query.trim())
|
||||
} else {
|
||||
props.applySearchQuery(
|
||||
`${query.trim()} label:\"${props.label.name}\"`
|
||||
`${query.trim()} label:\"${escapeQuotes(props.label.name)}\"`
|
||||
)
|
||||
}
|
||||
}}
|
||||
|
|
@ -576,16 +579,14 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
|
|||
type="checkbox"
|
||||
checked={state === 'on'}
|
||||
onChange={(e) => {
|
||||
const escapedName = escapeQuotes(props.label.name)
|
||||
if (e.target.checked) {
|
||||
props.applySearchQuery(
|
||||
`${props.searchTerm ?? ''} label:\"${props.label.name}\"`
|
||||
`${props.searchTerm ?? ''} label:\"${escapedName}\"`
|
||||
)
|
||||
} else {
|
||||
const query =
|
||||
props.searchTerm?.replace(
|
||||
`label:\"${props.label.name}\"`,
|
||||
''
|
||||
) ?? ''
|
||||
props.searchTerm?.replace(`label:\"${escapedName}\"`, '') ?? ''
|
||||
props.applySearchQuery(query)
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { NewsletterIcon } from '../../elements/icons/NewsletterIcon'
|
|||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
import { useRouter } from 'next/router'
|
||||
import { DiscoverIcon } from "../../elements/icons/DiscoverIcon"
|
||||
import { escapeQuotes } from "../../../utils/helper"
|
||||
|
||||
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
|
||||
|
||||
|
|
@ -221,7 +222,7 @@ const LibraryNav = (props: LibraryFilterMenuProps): JSX.Element => {
|
|||
<NavRedirectButton
|
||||
{...props}
|
||||
text="Discover"
|
||||
redirectLocation={"/discover"}
|
||||
redirectLocation={'/discover'}
|
||||
icon={<DiscoverIcon color={theme.colors.discover.toString()} />}
|
||||
/>
|
||||
</VStack>
|
||||
|
|
@ -545,7 +546,7 @@ function Subscriptions(
|
|||
name: name,
|
||||
keywords: '*' + name,
|
||||
perform: () => {
|
||||
props.applySearchQuery(`subscription:\"${name}\"`)
|
||||
props.applySearchQuery(`subscription:\"${escapeQuotes(name)}\"`)
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
|
@ -581,7 +582,9 @@ function Subscriptions(
|
|||
return (
|
||||
<FilterButton
|
||||
key={item.id}
|
||||
filterTerm={`in:inbox subscription:\"${item.name}\"`}
|
||||
filterTerm={`in:inbox subscription:\"${escapeQuotes(
|
||||
item.name
|
||||
)}\"`}
|
||||
text={item.name}
|
||||
{...props}
|
||||
/>
|
||||
|
|
@ -735,7 +738,7 @@ type NavButtonRedirectProps = {
|
|||
}
|
||||
|
||||
function NavRedirectButton(props: NavButtonRedirectProps): JSX.Element {
|
||||
const [selected, setSelected] = useState(false);
|
||||
const [selected, setSelected] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -932,7 +935,7 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
|
|||
const checkboxRef = useRef<HTMLInputElement | null>(null)
|
||||
const state = useMemo(() => {
|
||||
const term = props.searchTerm ?? ''
|
||||
if (term.indexOf(`label:\"${props.label.name}\"`) >= 0) {
|
||||
if (term.indexOf(`label:\"${escapeQuotes(props.label.name)}\"`) >= 0) {
|
||||
return 'on'
|
||||
}
|
||||
return 'off'
|
||||
|
|
@ -982,7 +985,7 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
|
|||
props.applySearchQuery(query.trim())
|
||||
} else {
|
||||
props.applySearchQuery(
|
||||
`${query.trim()} label:\"${props.label.name}\"`
|
||||
`${query.trim()} label:\"${escapeQuotes(props.label.name)}\"`
|
||||
)
|
||||
}
|
||||
}}
|
||||
|
|
@ -1001,14 +1004,15 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
|
|||
type="checkbox"
|
||||
checked={state === 'on'}
|
||||
onChange={(e) => {
|
||||
const escapedLabelName = escapeQuotes(props.label.name)
|
||||
if (e.target.checked) {
|
||||
props.applySearchQuery(
|
||||
`${props.searchTerm ?? ''} label:\"${props.label.name}\"`
|
||||
`${props.searchTerm ?? ''} label:\"${escapedLabelName}\"`
|
||||
)
|
||||
} else {
|
||||
const query =
|
||||
props.searchTerm?.replace(
|
||||
`label:\"${props.label.name}\"`,
|
||||
`label:\"${escapedLabelName}\"`,
|
||||
''
|
||||
) ?? ''
|
||||
props.applySearchQuery(query)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { Label } from '../../lib/networking/fragments/labelFragment'
|
|||
import { CheckSquare, Circle, Square } from 'phosphor-react'
|
||||
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
|
||||
import { usePersistedState } from '../../lib/hooks/usePersistedState'
|
||||
import { escapeQuotes } from '../../utils/helper'
|
||||
|
||||
export type PinnedSearch = {
|
||||
type: 'saved-search' | 'label'
|
||||
|
|
@ -282,7 +283,7 @@ function LabelButton(props: LabelButtonProps): JSX.Element {
|
|||
type: 'label',
|
||||
itemId: props.label.id,
|
||||
name: props.label.name,
|
||||
search: `label:\"${props.label.name}\"`,
|
||||
search: `label:\"${escapeQuotes(props.label.name)}\"`,
|
||||
}}
|
||||
listAction={props.listAction}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import { CheckSquare, Square } from 'phosphor-react'
|
|||
import { Button } from '../../components/elements/Button'
|
||||
import { styled } from '@stitches/react'
|
||||
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
|
||||
|
||||
import { escapeQuotes } from '../../utils/helper'
|
||||
type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
|
||||
|
||||
const SHORTCUTS_KEY = 'library-shortcuts'
|
||||
|
|
@ -365,7 +365,7 @@ const AvailableItems = (props: ListProps): JSX.Element => {
|
|||
type: 'label',
|
||||
label: label,
|
||||
name: label.name,
|
||||
filter: `label:\"${label.name}\"`,
|
||||
filter: `label:\"${escapeQuotes(label.name)}\"`,
|
||||
}
|
||||
props.dispatchList({
|
||||
item,
|
||||
|
|
@ -416,7 +416,7 @@ const AvailableItems = (props: ListProps): JSX.Element => {
|
|||
: 'feed',
|
||||
filter:
|
||||
subscription.type == SubscriptionType.NEWSLETTER
|
||||
? `subscription:\"${subscription.name}\"`
|
||||
? `subscription:\"${escapeQuotes(subscription.name)}\"`
|
||||
: `rss:\"${subscription.url}\"`,
|
||||
}
|
||||
props.dispatchList({
|
||||
|
|
|
|||
1
packages/web/utils/helper.ts
Normal file
1
packages/web/utils/helper.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const escapeQuotes = (str: string) => str.replace(/"/g, '\\"')
|
||||
Loading…
Reference in a new issue