diff --git a/.drone.env b/.drone.env
index c8fe09bb93b..103265ab8e4 100644
--- a/.drone.env
+++ b/.drone.env
@@ -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
diff --git a/changelog/unreleased/enhancement-add-file-media-filter-chip b/changelog/unreleased/enhancement-add-file-media-filter-chip
new file mode 100644
index 00000000000..dbc36a64187
--- /dev/null
+++ b/changelog/unreleased/enhancement-add-file-media-filter-chip
@@ -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
diff --git a/packages/web-app-files/src/components/Search/List.vue b/packages/web-app-files/src/components/Search/List.vue
index 026469247f5..73cdb38e4c5 100644
--- a/packages/web-app-files/src/components/Search/List.vue
+++ b/packages/web-app-files/src/components/Search/List.vue
@@ -7,6 +7,29 @@
+
+
+
+
+ {{ item.label }}
+
+
+
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'
@@ -181,6 +208,11 @@ import {
const visibilityObserver = new VisibilityObserver()
+type FileCategoryKeyword = {
+ id: string
+ label: string
+ icon: string
+}
type Tag = {
id: string
label: string
@@ -227,6 +259,7 @@ export default defineComponent({
const hasTags = useCapabilityFilesTags()
const fullTextSearchEnabled = useCapabilityFilesFullTextSearch()
const modifiedDateCapability = useCapabilitySearchModifiedDate()
+ const mediaTypeCapability = useCapabilitySearchMediaType()
const { getMatchingSpace } = useGetMatchingSpace()
const searchTermQuery = useRouteQuery('term')
@@ -234,7 +267,6 @@ export default defineComponent({
const doUseScope = useRouteQuery('useScope')
const resourcesView = useResourcesViewDefaults()
-
const keyActions = useKeyboardActions()
useKeyboardTableNavigation(keyActions, resourcesView.paginatedResources, resourcesView.viewMode)
useKeyboardTableMouseActions(keyActions, resourcesView.viewMode)
@@ -246,12 +278,18 @@ export default defineComponent({
const availableTags = ref([])
const tagFilter = ref()
+ const mediaTypeFilter = ref()
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* () {
@@ -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 = {}
@@ -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.
@@ -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
}
@@ -416,7 +491,10 @@ export default defineComponent({
breadcrumbs,
displayFilter,
availableLastModifiedValues,
- lastModifiedFilter
+ lastModifiedFilter,
+ mediaTypeFilter,
+ availableMediaTypeValues,
+ getFakeResourceForIcon
}
},
computed: {
diff --git a/packages/web-client/src/ocs/capabilities.ts b/packages/web-client/src/ocs/capabilities.ts
index 0c53ebaeb82..73e9abc541f 100644
--- a/packages/web-client/src/ocs/capabilities.ts
+++ b/packages/web-client/src/ocs/capabilities.ts
@@ -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: {
diff --git a/packages/web-pkg/src/composables/capability/useCapability.ts b/packages/web-pkg/src/composables/capability/useCapability.ts
index 15abde5c3db..332e3ab4df1 100644
--- a/packages/web-pkg/src/composables/capability/useCapability.ts
+++ b/packages/web-pkg/src/composables/capability/useCapability.ts
@@ -5,6 +5,7 @@ import { useStore } from '../store'
import {
AppProviderCapability,
LastModifiedFilterCapability,
+ MediaTypeCapability,
PasswordPolicyCapability
} from '@ownclouders/web-client/src/ocs/capabilities'
@@ -146,3 +147,8 @@ export const useCapabilityPasswordPolicy = createCapabilityComposable('search.property.mtime', {})
+
+export const useCapabilitySearchMediaType = createCapabilityComposable(
+ 'search.property.mediatype',
+ {}
+)
diff --git a/packages/web-pkg/src/router/common.ts b/packages/web-pkg/src/router/common.ts
index 00d11c257b2..d37dfe5033e 100644
--- a/packages/web-pkg/src/router/common.ts
+++ b/packages/web-pkg/src/router/common.ts
@@ -33,6 +33,7 @@ export const buildRoutes = (components: RouteComponents): RouteRecordRaw[] => [
'q_tags',
'q_lastModified',
'q_fullText',
+ 'q_mediaType',
'scope',
'useScope'
]
diff --git a/tests/e2e/cucumber/features/smoke/search.feature b/tests/e2e/cucumber/features/smoke/search.feature
index 929fffe36f6..4439079a7f1 100644
--- a/tests/e2e/cucumber/features/smoke/search.feature
+++ b/tests/e2e/cucumber/features/smoke/search.feature
@@ -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
diff --git a/tests/e2e/cucumber/steps/ui/search.ts b/tests/e2e/cucumber/steps/ui/search.ts
index 8c0df6d0aec..562d0d4f570 100644
--- a/tests/e2e/cucumber/steps/ui/search.ts
+++ b/tests/e2e/cucumber/steps/ui/search.ts
@@ -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 {
+ 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 {
+ const { page } = this.actorsEnvironment.getActor({ key: stepUser })
+ const searchObject = new objects.applicationFiles.Search({ page })
+ await searchObject.clearMediaTypeFilter()
+ }
+)
diff --git a/tests/e2e/support/objects/app-files/search/actions.ts b/tests/e2e/support/objects/app-files/search/actions.ts
index 8316eaa0a19..3eb2eb2c4fa 100644
--- a/tests/e2e/support/objects/app-files/search/actions.ts
+++ b/tests/e2e/support/objects/app-files/search/actions.ts
@@ -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 =
@@ -31,6 +36,22 @@ export const clearTagFilter = async ({ page }: { page: Page }): Promise =>
await page.locator(clearTagFilterSelector).click()
}
+export const selectMediaTypeFilter = async ({
+ mediaType,
+ page
+}: {
+ mediaType: string
+ page: Page
+}): Promise => {
+ 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 => {
+ await page.locator(clearMediaTypeFilterSelector).click()
+}
+
export const toggleSearchInFileContent = async ({
enableOrDisable,
page
diff --git a/tests/e2e/support/objects/app-files/search/index.ts b/tests/e2e/support/objects/app-files/search/index.ts
index 879f329c330..5a7844ce899 100644
--- a/tests/e2e/support/objects/app-files/search/index.ts
+++ b/tests/e2e/support/objects/app-files/search/index.ts
@@ -16,10 +16,18 @@ export class Search {
await po.selectTagFilter({ tag: string, page: this.#page })
}
+ async selectMediaTypeFilter({ mediaType: string }): Promise {
+ await po.selectMediaTypeFilter({ mediaType: string, page: this.#page })
+ }
+
async clearTagFilter(): Promise {
await po.clearTagFilter({ page: this.#page })
}
+ async clearMediaTypeFilter(): Promise {
+ await po.clearMediaTypeFilter({ page: this.#page })
+ }
+
async toggleSearchInFileContent({ enableOrDisable: string }): Promise {
await po.toggleSearchInFileContent({ enableOrDisable: string, page: this.#page })
}