Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement filter for Project Spaces #9649

Merged
merged 13 commits into from
Sep 5, 2023
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-add-project-space-filter
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Bugfix: Add project space filter

We've added the option to search for spaces in the project space overview.

https://github.com/owncloud/web/pull/9649
https://github.com/owncloud/web/issues/9650
107 changes: 100 additions & 7 deletions packages/web-app-files/src/views/spaces/Projects.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,18 @@
</template>
</no-content-message>
<div v-else class="spaces-list oc-mt-l">
<oc-text-input
id="spaces-filter"
v-model="filterTerm"
class="oc-ml-m oc-my-m"
:label="$gettext('Search')"
autocomplete="off"
/>
<resource-tiles
v-if="viewMode === ViewModeConstants.tilesView.name"
v-model:selectedIds="selectedResourcesIds"
class="oc-px-m"
:data="spaces"
:data="paginatedItems"
:resizable="true"
:sort-fields="sortFields"
:sort-by="sortBy"
Expand Down Expand Up @@ -64,11 +71,18 @@
:action-options="{ resources: [resource] as SpaceResource[] }"
/>
</template>
<template #footer>
<pagination :pages="totalPages" :current-page="currentPage" />
<div class="oc-text-nowrap oc-text-center oc-width-1-1 oc-my-s">
<p class="oc-text-muted">{{ footerTextTotal }}</p>
<p v-if="filterTerm" class="oc-text-muted">{{ footerTextFilter }}</p>
</div>
</template>
</resource-tiles>
<resource-table
v-else
v-model:selectedIds="selectedResourcesIds"
:resources="spaces"
:resources="paginatedItems"
class="spaces-table"
:class="{ 'spaces-table-squashed': sideBarOpen }"
:sticky="false"
Expand Down Expand Up @@ -118,6 +132,13 @@
/>
<oc-resource-icon v-else class="oc-mr-s" :resource="resource" />
</template>
<template #footer>
<pagination :pages="totalPages" :current-page="currentPage" />
<div class="oc-text-nowrap oc-text-center oc-width-1-1 oc-my-s">
<p class="oc-text-muted">{{ footerTextTotal }}</p>
<p v-if="filterTerm" class="oc-text-muted">{{ footerTextFilter }}</p>
</div>
</template>
</resource-table>
</div>
</template>
Expand All @@ -127,9 +148,11 @@
</template>

<script lang="ts">
import { onMounted, computed, defineComponent, unref } from 'vue'
import { onMounted, computed, defineComponent, unref, ref, watch, nextTick } from 'vue'
import { useTask } from 'vue-concurrency'
import { mapMutations, mapGetters } from 'vuex'
import Mark from 'mark.js'
import Fuse from 'fuse.js'

import NoContentMessage from 'web-pkg/src/components/NoContentMessage.vue'
import AppLoadingSpinner from 'web-pkg/src/components/AppLoadingSpinner.vue'
Expand All @@ -143,11 +166,16 @@ import {
useRouteQueryPersisted,
useSort,
useStore,
useRouteName
useRouteName,
SortDir,
usePagination,
useRouter,
useRoute
} from 'web-pkg/src/composables'
import { ImageDimension } from 'web-pkg/src/constants'
import Pagination from 'web-pkg/src/components/Pagination.vue'
import SpaceContextActions from '../../components/Spaces/SpaceContextActions.vue'
import { isProjectSpaceResource, SpaceResource } from 'web-client/src/helpers'
import { isProjectSpaceResource, Resource, SpaceResource } from 'web-client/src/helpers'
import SideBar from '../../components/SideBar/SideBar.vue'
import FilesViewWrapper from '../../components/FilesViewWrapper.vue'
import ResourceTiles from '../../components/FilesList/ResourceTiles.vue'
Expand All @@ -158,7 +186,7 @@ import { WebDAV } from 'web-client/src/webdav'
import { useScrollTo } from 'web-app-files/src/composables/scrollTo'
import { useSelectedResources } from 'web-app-files/src/composables'
import { sortFields as availableSortFields } from '../../helpers/ui/resourceTiles'
import { formatFileSize } from 'web-pkg/src'
import { defaultFuseOptions, formatFileSize } from 'web-pkg/src'
import { useGettext } from 'vue3-gettext'
import { spaceRoleEditor, spaceRoleManager, spaceRoleViewer } from 'web-client/src/helpers/share'
import { useKeyboardActions } from 'web-pkg/src/composables/keyboardActions'
Expand All @@ -167,6 +195,7 @@ import {
useKeyboardTableMouseActions,
useKeyboardTableActions
} from 'web-app-files/src/composables/keyboardActions'
import { orderBy } from 'lodash-es'

export default defineComponent({
components: {
Expand All @@ -175,17 +204,22 @@ export default defineComponent({
CreateSpace,
FilesViewWrapper,
NoContentMessage,
Pagination,
ResourceTiles,
ResourceTable,
SideBar,
SpaceContextActions
},
setup() {
const store = useStore()
const router = useRouter()
const route = useRoute()
const clientService = useClientService()
const { selectedResourcesIds } = useSelectedResources({ store })
const { can } = useAbility()
const { current: currentLanguage, $gettext } = useGettext()
const filterTerm = ref('')
const markInstance = ref(undefined)

const runtimeSpaces = computed((): SpaceResource[] => {
return store.getters['runtime/spaces/spaces'].filter((s) => isProjectSpaceResource(s)) || []
Expand Down Expand Up @@ -213,6 +247,44 @@ export default defineComponent({
items: runtimeSpaces,
fields: sortFields
})
const filter = (spaces: Array<Resource>, filterTerm: string) => {
if (!(filterTerm || '').trim()) {
return spaces
}
const searchEngine = new Fuse(spaces, { ...defaultFuseOptions, keys: ['name'] })
return searchEngine.search(filterTerm).map((r) => r.item)
}
const items = computed(() =>
orderBy(
filter(unref(spaces), unref(filterTerm)),
unref(sortBy),
unref(sortDir) === SortDir.Desc
)
)

const {
items: paginatedItems,
page: currentPage,
total: totalPages
} = usePagination({
items,
perPageDefault: '50',
perPageStoragePrefix: 'spaces-list'
})

watch(filterTerm, async () => {
const instance = unref(markInstance)
if (!instance) {
return
}
await router.push({ ...unref(route), query: { ...unref(route).query, page: '1' } })
instance.unmark()
instance.mark(unref(filterTerm), {
element: 'span',
className: 'highlight-mark',
exclude: ['th *', 'tfoot *']
})
})

const { scrollToResourceFromRoute } = useScrollTo()

Expand Down Expand Up @@ -284,6 +356,20 @@ export default defineComponent({
onMounted(async () => {
await loadResourcesTask.perform()
scrollToResourceFromRoute(unref(spaces))
nextTick(() => {
markInstance.value = new Mark('.spaces-table')
})
})

const footerTextTotal = computed(() => {
return $gettext('%{spaceCount} spaces in total', {
spaceCount: unref(spaces).length.toString()
})
})
const footerTextFilter = computed(() => {
return $gettext('%{spaceCount} matching spaces', {
spaceCount: unref(items).length.toString()
})
})

return {
Expand All @@ -306,7 +392,14 @@ export default defineComponent({
getTotalQuota,
getUsedQuota,
getRemainingQuota,
getMemberCount
getMemberCount,
paginatedItems,
filterTerm,
totalPages,
currentPage,
footerTextTotal,
footerTextFilter,
items
}
},
data: function () {
Expand Down
16 changes: 16 additions & 0 deletions packages/web-app-files/tests/unit/views/spaces/Projects.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Projects from '../../../../src/views/spaces/Projects.vue'
import { mock } from 'jest-mock-extended'
import { nextTick } from 'vue'
import { queryItemAsString } from 'web-pkg'

import {
createStore,
Expand All @@ -11,6 +13,12 @@ import {
RouteLocation
} from 'web-test-helpers'

jest.mock('web-pkg/src/helpers', () => ({
...jest.requireActual('web-pkg/src/helpers'),
displayPositionedDropdown: jest.fn()
}))
jest.mock('web-pkg/src/composables/appDefaults')

const spacesResources = [
{
id: '1',
Expand Down Expand Up @@ -64,6 +72,12 @@ describe('Projects view', () => {
expect(wrapper.find('.no-content-message').exists()).toBeFalsy()
expect(wrapper.find('.spaces-list').exists()).toBeTruthy()
})
it('shows only filtered spaces if filter applied', async () => {
const { wrapper } = getMountedWrapper({ spaces: spacesResources })
wrapper.vm.filterTerm = 'Some other space'
await nextTick()
expect(wrapper.vm.items).toEqual([spacesResources[1]])
})
})
it('should display the "Create Space"-button when permission given', () => {
const { wrapper } = getMountedWrapper({
Expand All @@ -75,6 +89,8 @@ describe('Projects view', () => {
})

function getMountedWrapper({ mocks = {}, spaces = [], abilities = [], stubAppBar = true } = {}) {
jest.mocked(queryItemAsString).mockImplementationOnce(() => '1')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is needed for pagination

jest.mocked(queryItemAsString).mockImplementationOnce(() => '100')
const defaultMocks = {
...defaultComponentMocks({
currentRoute: mock<RouteLocation>({ name: 'files-spaces-projects' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ exports[`Projects view different files view states lists all available project s
<div id="files-view">
<app-bar-stub breadcrumbs="[object Object]" breadcrumbscontextactionsitems="" hasbulkactions="true" hasfileextensions="false" hashiddenfiles="false" haspagination="false" hassharesnavigation="false" hassidebartoggle="true" hasviewoptions="true" showactionsonselection="true" sidebaropen="false" viewmodedefault="resource-tiles" viewmodes="[object Object],[object Object]"></app-bar-stub>
<div class="spaces-list oc-mt-l">
<resource-table-stub arepathsdisplayed="false" areresourcesclickable="true" arethumbnailsdisplayed="true" class="spaces-table" dragdrop="false" fieldsdisplayed="image,name,manager,members,totalQuota,usedQuota,remainingQuota,status,mdate" hasactions="true" headerposition="0" hover="true" isselectable="true" paddingx="small" resourcedomselector="[Function]" resources="[object Object],[object Object]" selectedids="" sort-fields="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" sortby="[Function]" sortdir="desc" sticky="false" viewmode="resource-table"></resource-table-stub>
<div class="oc-ml-m oc-my-m">
<label class="oc-label" for="spaces-filter">Search</label>
<div class="oc-position-relative">
<!--v-if-->
<input aria-invalid="false" autocomplete="off" class="oc-text-input oc-input oc-rounded" id="spaces-filter" type="text">
<!--v-if-->
</div>
<!--v-if-->
</div>
<resource-table-stub arepathsdisplayed="false" areresourcesclickable="true" arethumbnailsdisplayed="true" class="spaces-table" dragdrop="false" fieldsdisplayed="image,name,manager,members,totalQuota,usedQuota,remainingQuota,status,mdate" hasactions="true" headerposition="0" hover="true" isselectable="true" paddingx="small" resourcedomselector="[Function]" resources="" selectedids="" sort-fields="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" sortby="[Function]" sortdir="desc" sticky="false" viewmode="resource-table"></resource-table-stub>
</div>
</div>
</div>
Expand Down