Skip to content

Commit

Permalink
Add "File type" filter Chip (#9912)
Browse files Browse the repository at this point in the history
* Implement basic ui filter construct

* Implement doctype icons

* Add changelog

* Refactor using capabilities for filetypes

* Make filter work

* Add check for capability

* Fix typescript error

* Address PR issues

* rename to mediatype

* Update changelog

* add missing mediatype renames

* Fix renaming issue

* WIP e2e

* WIP e2e

* Implement basic mediaType filter e2e test

* Add more e2e steps

* logout user before finishing test scenario

Co-authored-by: Jannik Stehle <[email protected]>

* Update ocis commit id

* Adjust ocis commit id

* Adjust commit id

* Update changelog

---------

Co-authored-by: Jan <[email protected]>
Co-authored-by: Jannik Stehle <[email protected]>
  • Loading branch information
3 people authored Nov 14, 2023
1 parent fd18c2f commit ed8f2fd
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .drone.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# The version of OCIS to use in pipelines that test against OCIS
OCIS_COMMITID=a9366caa0cb97de0c4a68a43f05156cc584bee16
OCIS_COMMITID=ef1b18aeacea86be2a6918827c84f0ed99043b1e
OCIS_BRANCH=master
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-add-file-media-filter-chip
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Add media type filter chip

We've added a new filter option in the search list to filter by media type.

https://github.com/owncloud/web/pull/9912
https://github.com/owncloud/web/issues/9780
92 changes: 85 additions & 7 deletions packages/web-app-files/src/components/Search/List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@
<oc-icon name="filter-2" class="oc-mr-xs" />
<span v-text="$gettext('Filter:')" />
</div>
<item-filter
v-if="availableMediaTypeValues.length"
ref="mediaTypeFilter"
:allow-multiple="true"
:filter-label="$gettext('Media type')"
:filterable-attributes="['label']"
:items="availableMediaTypeValues"
:option-filter-label="$gettext('Filter media type')"
:show-option-filter="true"
class="files-search-filter-file-type oc-mr-s"
display-name-attribute="label"
filter-name="mediaType"
>
<template #image="{ item }">
<div
class="file-category-option-wrapper oc-flex oc-flex-middle"
:data-test-id="`media-type-${item.id.toLowerCase()}`"
>
<oc-resource-icon :resource="getFakeResourceForIcon(item)" />
<span class="oc-ml-s">{{ item.label }}</span>
</div>
</template>
</item-filter>
<item-filter
v-if="availableTags.length"
ref="tagFilter"
Expand Down Expand Up @@ -126,7 +149,11 @@

<script lang="ts">
import { useResourcesViewDefaults } from '../../composables'
import { AppLoadingSpinner, useCapabilitySearchModifiedDate } from '@ownclouders/web-pkg'
import {
AppLoadingSpinner,
useCapabilitySearchMediaType,
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 @@ -181,6 +208,11 @@ import {
const visibilityObserver = new VisibilityObserver()
type FileCategoryKeyword = {
id: string
label: string
icon: string
}
type Tag = {
id: string
label: string
Expand Down Expand Up @@ -227,14 +259,14 @@ export default defineComponent({
const hasTags = useCapabilityFilesTags()
const fullTextSearchEnabled = useCapabilityFilesFullTextSearch()
const modifiedDateCapability = useCapabilitySearchModifiedDate()
const mediaTypeCapability = useCapabilitySearchMediaType()
const { getMatchingSpace } = useGetMatchingSpace()
const searchTermQuery = useRouteQuery('term')
const scopeQuery = useRouteQuery('scope')
const doUseScope = useRouteQuery('useScope')
const resourcesView = useResourcesViewDefaults<Resource, any, any[]>()
const keyActions = useKeyboardActions()
useKeyboardTableNavigation(keyActions, resourcesView.paginatedResources, resourcesView.viewMode)
useKeyboardTableMouseActions(keyActions, resourcesView.viewMode)
Expand All @@ -246,12 +278,18 @@ export default defineComponent({
const availableTags = ref<Tag[]>([])
const tagFilter = ref<VNodeRef>()
const mediaTypeFilter = ref<VNodeRef>()
const tagParam = useRouteQuery('q_tags')
const lastModifiedParam = useRouteQuery('q_lastModified')
const mediaTypeParam = useRouteQuery('q_mediaType')
const fullTextParam = useRouteQuery('q_fullText')
const displayFilter = computed(() => {
return unref(fullTextSearchEnabled) || unref(availableTags).length
return (
unref(fullTextSearchEnabled) ||
unref(availableTags).length ||
(unref(modifiedDateCapability) && unref(modifiedDateCapability).enabled)
)
})
const loadAvailableTagsTask = useTask(function* () {
Expand Down Expand Up @@ -287,6 +325,33 @@ export default defineComponent({
})) || []
)
const mediaTypeMapping = {
file: { label: $gettext('File'), icon: 'txt' },
folder: { label: $gettext('Folder'), icon: 'folder' },
document: { label: $gettext('Document'), icon: 'doc' },
spreadsheet: { label: $gettext('Spreadsheet'), icon: 'xls' },
presentation: { label: $gettext('Presentation'), icon: 'ppt' },
pdf: { label: $gettext('PDF'), icon: 'pdf' },
image: { label: $gettext('Image'), icon: 'jpg' },
video: { label: $gettext('Video'), icon: 'mp4' },
audio: { label: $gettext('Audio'), icon: 'mp3' },
archive: { label: $gettext('Archive'), icon: 'zip' }
}
const availableMediaTypeValues: FileCategoryKeyword[] = []
unref(mediaTypeCapability).keywords?.forEach((key: string) => {
if (!mediaTypeMapping[key]) {
return
}
availableMediaTypeValues.push({
id: key,
...mediaTypeMapping[key]
})
})
const getFakeResourceForIcon = (item) => {
return { type: 'file', extension: item.icon, isFolder: item.icon == 'folder' } as Resource
}
const buildSearchTerm = (manuallyUpdateFilterChip = false) => {
const query = {}
Expand Down Expand Up @@ -329,6 +394,12 @@ export default defineComponent({
updateFilter(lastModifiedFilter)
}
const mediaTypeParams = queryItemAsString(unref(mediaTypeParam))
if (mediaTypeParams) {
query['mediatype'] = mediaTypeParams.split('+').map((t) => `"${t}"`)
updateFilter(mediaTypeFilter)
}
return (
// By definition (KQL spec) OR, AND or (GROUP) is implicit for simple cases where
// different or identical keys are part of the query.
Expand Down Expand Up @@ -391,9 +462,13 @@ export default defineComponent({
// return early if the search term or filter has not changed, no search needed
{
const isSameTerm = newVal?.term === oldVal?.term
const isSameFilter = ['q_fullText', 'q_tags', 'q_lastModified', 'useScope'].every(
(key) => newVal[key] === oldVal[key]
)
const isSameFilter = [
'q_fullText',
'q_tags',
'q_lastModified',
'q_mediaType',
'useScope'
].every((key) => newVal[key] === oldVal[key])
if (isSameTerm && isSameFilter) {
return
}
Expand All @@ -416,7 +491,10 @@ export default defineComponent({
breadcrumbs,
displayFilter,
availableLastModifiedValues,
lastModifiedFilter
lastModifiedFilter,
mediaTypeFilter,
availableMediaTypeValues,
getFakeResourceForIcon
}
},
computed: {
Expand Down
7 changes: 7 additions & 0 deletions packages/web-client/src/ocs/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@ export interface LastModifiedFilterCapability {
keywords?: string[]
enabled?: boolean
}

export interface MediaTypeCapability {
keywords?: string[]
enabled?: boolean
}

export interface Capabilities {
capabilities: {
password_policy?: PasswordPolicyCapability
search: {
property: {
mtime: LastModifiedFilterCapability
mimetype: MediaTypeCapability
}
}
notifications: {
Expand Down
6 changes: 6 additions & 0 deletions packages/web-pkg/src/composables/capability/useCapability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useStore } from '../store'
import {
AppProviderCapability,
LastModifiedFilterCapability,
MediaTypeCapability,
PasswordPolicyCapability
} from '@ownclouders/web-client/src/ocs/capabilities'

Expand Down Expand Up @@ -146,3 +147,8 @@ export const useCapabilityPasswordPolicy = createCapabilityComposable<PasswordPo

export const useCapabilitySearchModifiedDate =
createCapabilityComposable<LastModifiedFilterCapability>('search.property.mtime', {})

export const useCapabilitySearchMediaType = createCapabilityComposable<MediaTypeCapability>(
'search.property.mediatype',
{}
)
1 change: 1 addition & 0 deletions packages/web-pkg/src/router/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const buildRoutes = (components: RouteComponents): RouteRecordRaw[] => [
'q_tags',
'q_lastModified',
'q_fullText',
'q_mediaType',
'scope',
'useScope'
]
Expand Down
34 changes: 34 additions & 0 deletions tests/e2e/cucumber/features/smoke/search.feature
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,37 @@ Feature: Search
| resource |
| exampleInsideThePersonalSpace.txt |
And "Alice" logs out

Scenario: Search using mediaType filter
Given "Admin" creates following users using API
| id |
| Alice |
And "Alice" logs in
And "Alice" creates the following files into personal space using API
| pathToFile | content |
| mediaTest.txt | I'm a Document |
| mediaTest.pdf | I'm a PDF |
| mediaTest.mp3 | I'm a Audio |
| mediaTest.zip | I'm a Archive |
And "Alice" opens the "files" app
And "Alice" searches "mediaTest" using the global search and the "everywhere" filter and presses enter
And "Alice" selects mediaType "Document" from the search result filter chip
Then following resources should be displayed in the files list for user "Alice"
| resource |
| mediaTest.txt |
And "Alice" clears mediaType filter
And "Alice" selects mediaType "PDF" from the search result filter chip
Then following resources should be displayed in the files list for user "Alice"
| resource |
| mediaTest.pdf |
And "Alice" clears mediaType filter
And "Alice" selects mediaType "Audio" from the search result filter chip
Then following resources should be displayed in the files list for user "Alice"
| resource |
| mediaTest.mp3 |
And "Alice" clears mediaType filter
And "Alice" selects mediaType "Archive" from the search result filter chip
Then following resources should be displayed in the files list for user "Alice"
| resource |
| mediaTest.zip |
And "Alice" logs out
16 changes: 16 additions & 0 deletions tests/e2e/cucumber/steps/ui/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,19 @@ When(
await searchObject.toggleSearchInFileContent({ enableOrDisable })
}
)
When(
'{string} selects mediaType {string} from the search result filter chip',
async function (this: World, stepUser: string, mediaType: string): Promise<void> {
const { page } = this.actorsEnvironment.getActor({ key: stepUser })
const searchObject = new objects.applicationFiles.Search({ page })
await searchObject.selectMediaTypeFilter({ mediaType })
}
)
When(
'{string} clears mediaType filter',
async function (this: World, stepUser: string): Promise<void> {
const { page } = this.actorsEnvironment.getActor({ key: stepUser })
const searchObject = new objects.applicationFiles.Search({ page })
await searchObject.clearMediaTypeFilter()
}
)
21 changes: 21 additions & 0 deletions tests/e2e/support/objects/app-files/search/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ const searchResultMessageSelector = '//p[@class="oc-text-muted"]'
const selectTagDropdownSelector =
'//div[contains(@class,"files-search-filter-tags")]//button[contains(@class,"oc-filter-chip-button")]'
const tagFilterChipSelector = '//button[contains(@data-test-value,"%s")]'
const mediaTypeFilterSelector = '.item-filter-mediaType'
const mediaTypeFilterItem = '[data-test-id="media-type-%s"]'
const mediaTypeOutside = '.files-search-result-filter'
const clearTagFilterSelector =
'//div[contains(@class,"files-search-filter-tags")]//button[contains(@class,"oc-filter-chip-clear")]'
const clearMediaTypeFilterSelector =
'//div[contains(@class,"item-filter-mediaType")]//button[contains(@class,"oc-filter-chip-clear")]'
const enableSearchInFileContentSelector =
'//div[contains(@class,"files-search-filter-full-text")]//button[contains(@class,"oc-filter-chip-button")]'
const disableSearchInFileContentSelector =
Expand All @@ -31,6 +36,22 @@ export const clearTagFilter = async ({ page }: { page: Page }): Promise<void> =>
await page.locator(clearTagFilterSelector).click()
}

export const selectMediaTypeFilter = async ({
mediaType,
page
}: {
mediaType: string
page: Page
}): Promise<void> => {
await page.locator(mediaTypeFilterSelector).click()
await page.locator(util.format(mediaTypeFilterItem, mediaType.toLowerCase())).click()
await page.locator(mediaTypeOutside).click()
}

export const clearMediaTypeFilter = async ({ page }: { page: Page }): Promise<void> => {
await page.locator(clearMediaTypeFilterSelector).click()
}

export const toggleSearchInFileContent = async ({
enableOrDisable,
page
Expand Down
8 changes: 8 additions & 0 deletions tests/e2e/support/objects/app-files/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@ export class Search {
await po.selectTagFilter({ tag: string, page: this.#page })
}

async selectMediaTypeFilter({ mediaType: string }): Promise<void> {
await po.selectMediaTypeFilter({ mediaType: string, page: this.#page })
}

async clearTagFilter(): Promise<void> {
await po.clearTagFilter({ page: this.#page })
}

async clearMediaTypeFilter(): Promise<void> {
await po.clearMediaTypeFilter({ page: this.#page })
}

async toggleSearchInFileContent({ enableOrDisable: string }): Promise<void> {
await po.toggleSearchInFileContent({ enableOrDisable: string, page: this.#page })
}
Expand Down

0 comments on commit ed8f2fd

Please sign in to comment.