Skip to content

Commit

Permalink
Modified Chip Filter (#9831)
Browse files Browse the repository at this point in the history
* mvp, wip

* added unit test

* added loading from capabilities + adjusted tests accordingly

* clean up, added changelog

* added route query param to persistence list
  • Loading branch information
grimmoc authored Oct 25, 2023
1 parent 4949e29 commit 7e5340a
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 8 deletions.
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-last-modified-filter-chips
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Last modified filter chips

We've added a "last modified" filter chip in search to narrow down results based on last modified date.

https://github.com/owncloud/web/pull/9831
https://github.com/owncloud/web/issues/9779
69 changes: 66 additions & 3 deletions packages/web-app-files/src/components/Search/List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@
<span v-text="item.label" />
</template>
</item-filter>
<item-filter
v-if="availableLastModifiedValues.length"
ref="lastModifiedFilter"
:filter-label="$gettext('Last Modified')"
:filterable-attributes="['label']"
:items="availableLastModifiedValues"
:option-filter-label="$gettext('Filter by last modified date')"
:show-option-filter="true"
:close-on-click="true"
class="files-search-filter-last-modified oc-mr-s"
display-name-attribute="label"
filter-name="lastModified"
>
<template #item="{ item }">
<span v-text="item.label" />
</template>
</item-filter>

<item-filter-toggle
v-if="fullTextSearchEnabled"
:filter-label="$gettext('Search only in content')"
Expand Down Expand Up @@ -108,7 +126,7 @@

<script lang="ts">
import { useResourcesViewDefaults } from '../../composables'
import { AppLoadingSpinner } from '@ownclouders/web-pkg'
import { AppLoadingSpinner, useCapabilitySearchModifiedDate } from '@ownclouders/web-pkg'
import { VisibilityObserver } from '@ownclouders/web-pkg'
import { ImageType, ImageDimension } from '@ownclouders/web-pkg'
import { NoContentMessage } from '@ownclouders/web-pkg'
Expand Down Expand Up @@ -157,6 +175,10 @@ type Tag = {
id: string
label: string
}
type LastModifiedKeyword = {
id: string
label: string
}
export default defineComponent({
components: {
Expand Down Expand Up @@ -194,6 +216,7 @@ export default defineComponent({
const clientService = useClientService()
const hasTags = useCapabilityFilesTags()
const fullTextSearchEnabled = useCapabilityFilesFullTextSearch()
const modifiedDateCapability = useCapabilitySearchModifiedDate()
const { getMatchingSpace } = useGetMatchingSpace()
const searchTermQuery = useRouteQuery('term')
Expand All @@ -214,6 +237,7 @@ export default defineComponent({
const availableTags = ref<Tag[]>([])
const tagFilter = ref<VNodeRef>()
const tagParam = useRouteQuery('q_tags')
const lastModifiedParam = useRouteQuery('q_lastModified')
const fullTextParam = useRouteQuery('q_fullText')
const displayFilter = computed(() => {
Expand All @@ -231,6 +255,28 @@ export default defineComponent({
eventBus.publish('app.search.term.clear')
})
// transifex hack b/c dynamically fetched values from backend will otherwise not be automatically translated
const lastModifiedTranslations = {
today: $gettext('today'),
yesterday: $gettext('yesterday'),
'this week': $gettext('this week'),
'last week': $gettext('last week'),
'last 7 days': $gettext('last 7 days'),
'this month': $gettext('this month'),
'last month': $gettext('last month'),
'last 30 days': $gettext('last 30 days'),
'this year': $gettext('this year'),
'last year': $gettext('last year')
}
const lastModifiedFilter = ref<VNodeRef>()
const availableLastModifiedValues = ref<LastModifiedKeyword[]>(
unref(modifiedDateCapability).keywords?.map((k: string) => ({
id: k,
label: lastModifiedTranslations[k]
})) || []
)
const buildSearchTerm = (manuallyUpdateFilterChip = false) => {
let query = ''
const add = (k: string, v: string) => {
Expand Down Expand Up @@ -267,6 +313,21 @@ export default defineComponent({
}
}
const lastModifiedParams = queryItemAsString(unref(lastModifiedParam))
if (lastModifiedParams) {
add('mtime', `"${lastModifiedParams}"`)
if (manuallyUpdateFilterChip && unref(lastModifiedFilter)) {
/**
* Handles edge cases where a filter is not being applied via the filter directly,
* e.g. when clicking on a tag in the files list.
* We need to manually update the selected items in the ItemFilter component because normally
* it only does this on mount or when interacting with the filter directly.
*/
;(unref(lastModifiedFilter) as any).setSelectedItemsBasedOnQuery()
}
}
return query
}
Expand All @@ -290,7 +351,7 @@ export default defineComponent({
watch(
() => unref(route).query,
(newVal, oldVal) => {
const filters = ['q_fullText', 'q_tags', 'useScope']
const filters = ['q_fullText', 'q_tags', 'q_lastModified', 'useScope']
const isChange =
newVal?.term !== oldVal?.term ||
filters.some((f) => newVal[f] ?? undefined !== oldVal[f] ?? undefined)
Expand All @@ -311,7 +372,9 @@ export default defineComponent({
availableTags,
tagFilter,
breadcrumbs,
displayFilter
displayFilter,
availableLastModifiedValues,
lastModifiedFilter
}
},
computed: {
Expand Down
70 changes: 66 additions & 4 deletions packages/web-app-files/tests/unit/components/Search/List.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,24 @@ import {
defaultStoreMockOptions,
mockAxiosResolve
} from 'web-test-helpers/src'
import { queryItemAsString } from '@ownclouders/web-pkg'
import { ref } from 'vue'
import { queryItemAsString, useCapabilitySearchModifiedDate } from '@ownclouders/web-pkg'
import { computed, ref } from 'vue'
import { Resource } from '@ownclouders/web-client/src'
import { mock } from 'jest-mock-extended'

jest.mock('web-app-files/src/composables')
jest.mock('@ownclouders/web-pkg', () => ({
...jest.requireActual('@ownclouders/web-pkg'),
queryItemAsString: jest.fn(),
useAppDefaults: jest.fn()
useAppDefaults: jest.fn(),
useCapabilitySearchModifiedDate: jest.fn()
}))

const selectors = {
noContentMessageStub: 'no-content-message-stub',
resourceTableStub: 'resource-table-stub',
tagFilter: '.files-search-filter-tags',
lastModifiedFilter: '.files-search-filter-last-modified',
fullTextFilter: '.files-search-filter-full-text',
filter: '.files-search-result-filter'
}
Expand Down Expand Up @@ -89,6 +91,60 @@ describe('List component', () => {
)
})
})

describe('last modified', () => {
it('should show available last modified values', async () => {
const expectation = [
{ label: 'today', id: 'today' },
{ label: 'yesterday', id: 'yesterday' },
{ label: 'this week', id: 'this week' },
{ label: 'last week', id: 'last week' },
{ label: 'last 7 days', id: 'last 7 days' },
{ label: 'this month', id: 'this month' },
{ label: 'last month', id: 'last month' },
{ label: 'last 30 days', id: 'last 30 days' },
{ label: 'this year', id: 'this year' },
{ label: 'last year', id: 'last year' }
]
const lastModifiedValues = {
keywords: [
'today',
'yesterday',
'this week',
'last week',
'last 7 days',
'this month',
'last month',
'last 30 days',
'this year',
'last year'
]
}
const { wrapper } = getWrapper({
availableLastModifiedValues: lastModifiedValues,
availableTags: ['tag']
})
await wrapper.vm.loadAvailableTagsTask.last

expect(wrapper.find(selectors.lastModifiedFilter).exists()).toBeTruthy()
expect(wrapper.findComponent<any>(selectors.lastModifiedFilter).props('items')).toEqual(
expectation
)
})
it('should set initial filter when last modified is given via query param', async () => {
const searchTerm = 'Screenshot'
const lastModifiedFilterQuery = 'today'
const { wrapper } = getWrapper({
searchTerm,
lastModifiedFilterQuery
})
await wrapper.vm.loadAvailableTagsTask.last
expect(wrapper.emitted('search')[0][0]).toEqual(
`name:"*${searchTerm}*" mtime:"${lastModifiedFilterQuery}"`
)
})
})

describe('fullText', () => {
it('should render filter if enabled via capabilities', () => {
const { wrapper } = getWrapper({ fullTextSearchEnabled: true })
Expand All @@ -114,11 +170,17 @@ function getWrapper({
searchTerm = '',
tagFilterQuery = null,
fullTextFilterQuery = null,
fullTextSearchEnabled = false
fullTextSearchEnabled = false,
availableLastModifiedValues = {},
lastModifiedFilterQuery = null
} = {}) {
jest.mocked(queryItemAsString).mockImplementationOnce(() => searchTerm)
jest.mocked(queryItemAsString).mockImplementationOnce(() => fullTextFilterQuery)
jest.mocked(queryItemAsString).mockImplementationOnce(() => tagFilterQuery)
jest.mocked(queryItemAsString).mockImplementationOnce(() => lastModifiedFilterQuery)
jest
.mocked(useCapabilitySearchModifiedDate)
.mockReturnValue(computed(() => availableLastModifiedValues as any))

const resourcesViewDetailsMock = useResourcesViewDefaultsMock({
paginatedResources: ref(resources)
Expand Down
9 changes: 9 additions & 0 deletions packages/web-client/src/ocs/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@ export interface PasswordPolicyCapability {
min_special_characters?: number
}

export interface LastModifiedFilterCapability {
keywords?: string[]
enabled?: boolean
}
export interface Capabilities {
capabilities: {
password_policy?: PasswordPolicyCapability
search: {
property: {
mtime: LastModifiedFilterCapability
}
}
notifications: {
ocs_endpoints: string[]
}
Expand Down
4 changes: 4 additions & 0 deletions packages/web-pkg/src/composables/capability/useCapability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { computed, ComputedRef } from 'vue'
import { useStore } from '../store'
import {
AppProviderCapability,
LastModifiedFilterCapability,
PasswordPolicyCapability
} from '@ownclouders/web-client/src/ocs/capabilities'

Expand Down Expand Up @@ -142,3 +143,6 @@ export const useCapabilityPasswordPolicy = createCapabilityComposable<PasswordPo
'password_policy',
{}
)

export const useCapabilitySearchModifiedDate =
createCapabilityComposable<LastModifiedFilterCapability>('search.property.mtime', {})
10 changes: 9 additions & 1 deletion packages/web-pkg/src/router/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ export const buildRoutes = (components: RouteComponents): RouteRecordRaw[] => [
meta: {
authContext: 'user',
title: $gettext('Search results'),
contextQueryItems: ['term', 'provider', 'q_tags', 'q_fullText', 'scope', 'useScope']
contextQueryItems: [
'term',
'provider',
'q_tags',
'q_lastModified',
'q_fullText',
'scope',
'useScope'
]
}
}
]
Expand Down

0 comments on commit 7e5340a

Please sign in to comment.