Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 8 additions & 41 deletions packages/web-app-files/src/components/Search/List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ import {
useClientService,
useFileListHeaderPosition,
useGetMatchingSpace,
getLastModifiedFilterOptions,
getMediaTypeFilterOptions,
SearchMediaTypeFilterOption,
useRoute,
useRouteQuery,
useRouter,
Expand All @@ -204,10 +207,6 @@ type Tag = {
id: string
label: string
}
type LastModifiedKeyword = {
id: string
label: string
}

const { searchResult = { totalResults: null, values: [] }, loading = false } = defineProps<{
searchResult?: SearchResult
Expand Down Expand Up @@ -298,49 +297,17 @@ onBeforeRouteLeave(() => {
eventBus.publish('app.search.term.clear')
})

// transifex hack b/c dynamically fetched values from backend will otherwise not be automatically translated
const lastModifiedTranslations: Record<string, string> = {
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 =
useTemplateRef<ComponentPublicInstance<typeof ItemFilter>>('lastModifiedFilter')
const availableLastModifiedValues = ref<LastModifiedKeyword[]>(
capabilityStore.searchLastMofifiedDate.keywords?.map((k: string) => ({
id: k,
label: lastModifiedTranslations[k]
})) || []
)

const mediaTypeMapping: Record<string, { label: string; icon: string }> = {
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 availableLastModifiedValues = computed(() => {
return getLastModifiedFilterOptions(capabilityStore.searchLastMofifiedDate.keywords, $gettext)
})

const availableMediaTypeValues = computed(() => {
return (
capabilityStore.searchMediaType.keywords?.filter((key) => mediaTypeMapping[key]) || []
).map((key) => ({ id: key, ...mediaTypeMapping[key] }))
return getMediaTypeFilterOptions(capabilityStore.searchMediaType.keywords, $gettext)
})

const getFakeResourceForIcon = (item: { label: string; icon: string }) => {
function getFakeResourceForIcon(item: SearchMediaTypeFilterOption) {
return { type: 'file', extension: item.icon, isFolder: item.icon == 'folder' } as Resource
}

Expand Down
107 changes: 103 additions & 4 deletions packages/web-app-files/src/views/Favorites.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,49 @@
<template>
<div class="flex">
<files-view-wrapper>
<app-bar :view-modes="viewModes" :has-bulk-actions="true" :breadcrumbs="breadcrumbs" />
<app-bar :view-modes="viewModes" :has-bulk-actions="true" :breadcrumbs="breadcrumbs">
<template #actions>
<div v-if="displayFilter" class="files-favorites-filter flex flex-wrap my-2">
<item-filter
v-if="availableMediaTypeValues.length"
ref="mediaTypeFilter"
:allow-multiple="true"
:filter-label="$gettext('Type')"
:filterable-attributes="['label']"
:items="availableMediaTypeValues"
class="mr-2"
display-name-attribute="label"
filter-name="mediaType"
>
<template #image="{ item }">
<div
class="flex items-center"
:data-test-id="`media-type-${item.id.toLowerCase()}`"
>
<resource-icon :resource="getFakeResourceForIcon(item)" />
<span class="ml-2">{{ item.label }}</span>
</div>
</template>
</item-filter>
<item-filter
v-if="availableLastModifiedValues.length"
ref="lastModifiedFilter"
:filter-label="$gettext('Last Modified')"
:filterable-attributes="['label']"
:items="availableLastModifiedValues"
:show-option-filter="false"
:close-on-click="true"
class="files-favorites-filter-last-modified mr-2"
display-name-attribute="label"
filter-name="lastModified"
>
<template #item="{ item }">
<span v-text="item.label" />
</template>
</item-filter>
</div>
</template>
</app-bar>
<app-loading-spinner v-if="areResourcesLoading" />
<template v-else>
<no-content-message
Expand All @@ -13,7 +55,7 @@
<span v-text="$gettext('Nothing marked as favorite, yet')" />
</template>
<template #callToAction>
<span v-text="$gettext('All your favorites will show up here')" />
<span v-text="emptyStateDescription" />
</template>
</no-content-message>
<component
Expand Down Expand Up @@ -54,7 +96,15 @@
</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, unref, watch } from 'vue'
import {
ComponentPublicInstance,
computed,
onBeforeUnmount,
onMounted,
unref,
useTemplateRef,
watch
} from 'vue'
import { isProjectSpaceResource, Resource } from '@opencloud-eu/web-client'
import {
useClientService,
Expand All @@ -70,7 +120,14 @@ import {
AppBar,
useGetMatchingSpace,
ContextActions,
useFileActions
useFileActions,
ItemFilter,
ResourceIcon,
useCapabilityStore,
useRouteQuery,
getLastModifiedFilterOptions,
getMediaTypeFilterOptions,
SearchMediaTypeFilterOption
} from '@opencloud-eu/web-pkg'
import QuickActions from '../components/FilesList/QuickActions.vue'
import ListInfo from '../components/FilesList/ListInfo.vue'
Expand All @@ -84,9 +141,43 @@ const { getMatchingSpace } = useGetMatchingSpace()
const { loadGraphPermissions } = useSpacesStore()
const clientService = useClientService()
const { $gettext } = useGettext()
const capabilityStore = useCapabilityStore()

const resourcesStore = useResourcesStore()

const mediaTypeFilter =
useTemplateRef<ComponentPublicInstance<typeof ItemFilter>>('mediaTypeFilter')
const lastModifiedFilter =
useTemplateRef<ComponentPublicInstance<typeof ItemFilter>>('lastModifiedFilter')

const lastModifiedParam = useRouteQuery('q_lastModified')
const mediaTypeParam = useRouteQuery('q_mediaType')

const availableLastModifiedValues = computed(() =>
getLastModifiedFilterOptions(capabilityStore.searchLastMofifiedDate.keywords, $gettext)
)

const availableMediaTypeValues = computed(() => {
return getMediaTypeFilterOptions(capabilityStore.searchMediaType.keywords, $gettext)
})

function getFakeResourceForIcon(item: SearchMediaTypeFilterOption) {
return { type: 'file', extension: item.icon, isFolder: item.icon == 'folder' } as Resource
}

const displayFilter = computed(() => {
return (
unref(availableLastModifiedValues).length || capabilityStore.searchMediaType.keywords?.length
)
})

const emptyStateDescription = computed(() => {
if (unref(lastModifiedParam) || unref(mediaTypeParam)) {
return $gettext('Try refining the search term or filters to get results')
}
return $gettext('All your favorites will show up here')
})

const {
paginatedResources,
selectedResources,
Expand Down Expand Up @@ -161,6 +252,14 @@ watch(selectedResourcesIds, async (ids) => {
})
})

watch(
[lastModifiedParam, mediaTypeParam],
async () => {
await loadResourcesTask.perform()
},
{ deep: true }
)

const isEmpty = computed(() => {
return unref(paginatedResources).length < 1
})
Expand Down
126 changes: 115 additions & 11 deletions packages/web-app-files/tests/unit/views/Favorites.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Favorites from '../../../src/views/Favorites.vue'
import { useResourcesViewDefaults } from '../../../src/composables'
import { useResourcesViewDefaultsMock } from '../../../tests/mocks/useResourcesViewDefaultsMock'
import { ref } from 'vue'
import { defineComponent, h, ref } from 'vue'
import { mockDeep, mock } from 'vitest-mock-extended'
import { Resource } from '@opencloud-eu/web-client'
import {
Expand All @@ -11,13 +11,23 @@ import {
defaultComponentMocks
} from '@opencloud-eu/web-test-helpers'
import { RouteLocation } from 'vue-router'
import { AppBar, ItemFilter, useRouteQuery } from '@opencloud-eu/web-pkg'
import { Capabilities } from '@opencloud-eu/web-client/ocs'

vi.mock('../../../src/composables')
vi.mock('@opencloud-eu/web-pkg', async (importOriginal) => ({
...(await importOriginal<any>()),
useFileActions: () => ({ triggerDefaultAction: vi.fn() })
useFileActions: () => ({ triggerDefaultAction: vi.fn() }),
useRouteQuery: vi.fn()
}))

const AppBarStub = defineComponent({
props: AppBar.props,
setup(_, { slots }) {
return () => h('app-bar-stub', {}, [slots.actions?.()])
}
})

describe('Favorites view', () => {
it('appBar always present', () => {
const { wrapper } = getMountedWrapper()
Expand All @@ -42,21 +52,110 @@ describe('Favorites view', () => {
expect(wrapper.find('.no-content-message').exists()).toBeFalsy()
expect(wrapper.find('resource-table-stub').exists()).toBeTruthy()
})
it('shows default empty-state text when no filter is active', () => {
const { wrapper } = getMountedWrapper()
expect((wrapper.vm as any).emptyStateDescription).toBe('All your favorites will show up here')
})
it('shows refine hint in empty-state when a filter is active', () => {
const { wrapper } = getMountedWrapper({
lastModifiedParam: ref('today')
})
expect((wrapper.vm as any).emptyStateDescription).toBe(
'Try refining the search term or filters to get results'
)
})
})
describe('filter', () => {
it('does not show filter if no values are available', () => {
const { wrapper } = getMountedWrapper()
expect(wrapper.find('.files-favorites-filter').exists()).toBeFalsy()
})
it('shows available last modified values from capabilities', () => {
const { wrapper } = getMountedWrapper({
capabilities: {
search: {
property: {
mtime: { keywords: ['today', 'last week'] }
}
}
}
})

const filterItems = wrapper
.findComponent<typeof ItemFilter>('.files-favorites-filter-last-modified')
.props('items')

expect(wrapper.find('.files-favorites-filter-last-modified').exists()).toBeTruthy()
expect(filterItems).toEqual([
{ id: 'today', label: 'today' },
{ id: 'last week', label: 'last week' }
])
})
it('shows available media types from capabilities and ignores unknown values', () => {
const { wrapper } = getMountedWrapper({
capabilities: {
search: {
property: {
mediatype: { keywords: ['image', 'folder', 'unknown'] }
}
}
}
})

const mediaTypeFilter = wrapper
.findAllComponents<typeof ItemFilter>('item-filter-stub')
.find((component) => component.props('filterName') === 'mediaType')

expect(wrapper.find('.files-favorites-filter').exists()).toBeTruthy()
expect(mediaTypeFilter).toBeDefined()
expect(mediaTypeFilter.props('items')).toEqual([
{ id: 'image', label: 'Image', icon: 'jpg' },
{ id: 'folder', label: 'Folder', icon: 'folder' }
])
})
it('loads resources again when filter query changes', async () => {
const lastModifiedParam = ref<string | null>(null)
const { resourcesViewDefaults } = getMountedWrapper({
lastModifiedParam
})

expect(resourcesViewDefaults.loadResourcesTask.perform).toHaveBeenCalledTimes(1)

lastModifiedParam.value = 'today'
await Promise.resolve()

expect(resourcesViewDefaults.loadResourcesTask.perform).toHaveBeenCalledTimes(2)
})
})
})

function getMountedWrapper({
mocks = {},
files = [],
loading = false
}: { mocks?: Record<string, unknown>; files?: Resource[]; loading?: boolean } = {}) {
const plugins = defaultPlugins()
loading = false,
capabilities = {},
lastModifiedParam = ref<string | null>(null),
mediaTypeParam = ref<string | null>(null)
}: {
mocks?: Record<string, unknown>
files?: Resource[]
loading?: boolean
capabilities?: Partial<Capabilities['capabilities']>
lastModifiedParam?: ReturnType<typeof ref<string | null>>
mediaTypeParam?: ReturnType<typeof ref<string | null>>
} = {}) {
const plugins = defaultPlugins({ piniaOptions: { capabilityState: { capabilities } } })

vi.mocked(useRouteQuery).mockImplementationOnce(() => lastModifiedParam)
vi.mocked(useRouteQuery).mockImplementationOnce(() => mediaTypeParam)

const resourcesViewDefaults = useResourcesViewDefaultsMock({
paginatedResources: ref(files),
areResourcesLoading: ref(loading)
})

vi.mocked(useResourcesViewDefaults).mockImplementation(() => {
return useResourcesViewDefaultsMock({
paginatedResources: ref(files),
areResourcesLoading: ref(loading)
})
return resourcesViewDefaults
})

const defaultMocks = {
Expand All @@ -72,8 +171,13 @@ function getMountedWrapper({
plugins,
mocks: defaultMocks,
provide: defaultMocks,
stubs: defaultStubs
stubs: {
...defaultStubs,
'app-bar': AppBarStub,
ItemFilter: true
}
}
})
}),
resourcesViewDefaults
}
}
Loading