Skip to content

Commit

Permalink
feat(files_sharing): Add file list filter to filter by owner / sharee
Browse files Browse the repository at this point in the history
Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux committed Jul 25, 2024
1 parent d0c9047 commit 1160fc4
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 20 deletions.
28 changes: 8 additions & 20 deletions apps/files_sharing/src/actions/sharingStatusAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,19 @@
*/
import { Node, View, registerFileAction, FileAction, Permission } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { Type } from '@nextcloud/sharing'
import { ShareType } from '@nextcloud/sharing'

import AccountGroupSvg from '@mdi/svg/svg/account-group.svg?raw'
import AccountPlusSvg from '@mdi/svg/svg/account-plus.svg?raw'
import LinkSvg from '@mdi/svg/svg/link.svg?raw'
import CircleSvg from '../../../../core/img/apps/circles.svg?raw'

import { action as sidebarAction } from '../../../files/src/actions/sidebarAction'
import { generateUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import { action as sidebarAction } from '../../../files/src/actions/sidebarAction'
import { generateAvatarSvg } from '../utils/AccountIcon'

import './sharingStatusAction.scss'

const isDarkMode = window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches === true
|| document.querySelector('[data-themes*=dark]') !== null

const generateAvatarSvg = (userId: string, isGuest = false) => {
const url = isDarkMode ? '/avatar/{userId}/32/dark' : '/avatar/{userId}/32'
const avatarUrl = generateUrl(isGuest ? url : url + '?guestFallback=true', { userId })
return `<svg width="32" height="32" viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg" class="sharing-status__avatar">
<image href="${avatarUrl}" height="32" width="32" />
</svg>`
}

const isExternal = (node: Node) => {
return node.attributes.remote_id !== undefined
}
Expand Down Expand Up @@ -75,19 +63,19 @@ export const action = new FileAction({
}

// Link shares
if (shareTypes.includes(Type.SHARE_TYPE_LINK)
|| shareTypes.includes(Type.SHARE_TYPE_EMAIL)) {
if (shareTypes.includes(ShareType.Link)
|| shareTypes.includes(ShareType.Email)) {
return LinkSvg
}

// Group shares
if (shareTypes.includes(Type.SHARE_TYPE_GROUP)
|| shareTypes.includes(Type.SHARE_TYPE_REMOTE_GROUP)) {
if (shareTypes.includes(ShareType.Grup)
|| shareTypes.includes(ShareType.RemoteGroup)) {
return AccountGroupSvg
}

// Circle shares
if (shareTypes.includes(Type.SHARE_TYPE_CIRCLE)) {
if (shareTypes.includes(ShareType.Team)) {
return CircleSvg
}

Expand Down
116 changes: 116 additions & 0 deletions apps/files_sharing/src/components/FileListFilterAccount.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcSelect v-model="selectedAccounts"
:aria-label-combobox="t('files_sharing', 'Accounts')"
class="file-list-filter-accounts"
multiple
no-wrap
:options="availableAccounts"
:placeholder="t('files_sharing', 'Accounts')"
user-select />
</template>

<script setup lang="ts">
import type { IAccountData } from '../filters/AccountFilter.ts'
import { translate as t } from '@nextcloud/l10n'
import { useBrowserLocation } from '@vueuse/core'
import { ref, watch, watchEffect } from 'vue'
import { useNavigation } from '../../../files/src/composables/useNavigation.ts'
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
interface IUserSelectData {
id: string
user: string
displayName: string
}
const emit = defineEmits<{
(event: 'update:accounts', value: IAccountData[]): void
}>()
const { currentView } = useNavigation()
const currentLocation = useBrowserLocation()
const availableAccounts = ref<IUserSelectData[]>([])
const selectedAccounts = ref<IUserSelectData[]>([])
// Watch selected account, on change we emit the new account data to the filter instance
watch(selectedAccounts, () => {
// Emit selected accounts as account data
const accounts = selectedAccounts.value.map(({ id: uid, displayName }) => ({ uid, displayName }))
emit('update:accounts', accounts)
})
/**
* Update the accounts owning nodes or have nodes shared to them
* @param path The path inside the current view to load for accounts
*/
async function updateAvailableAccounts(path: string = '/') {
availableAccounts.value = []
if (!currentView.value) {
return
}
const { contents } = await currentView.value.getContents(path)
const available = new Map<string, IUserSelectData>()
for (const node of contents) {
const owner = node.owner ?? node.attributes['owner-id']
if (owner && !available.has(owner)) {
available.set(owner, {
id: owner,
user: owner,
displayName: node.attributes['owner-display-name'] ?? node.owner,
})
}
const sharees = node.attributes.sharees?.sharee
if (sharees) {
// ensure sharees is an array (if only one share then it is just an object)
for (const sharee of [sharees].flat()) {
// Skip link shares and other without user
if (sharee.id === '') {
continue
}
// Add if not already added
if (!available.has(sharee.id)) {
available.set(sharee.id, {
id: sharee.id,
user: sharee.id,
displayName: sharee['display-name'],
})
}
}
}
}
availableAccounts.value = [...available.values()]
}
/**
* Reset this filter
*/
function resetFilter() {
selectedAccounts.value = []
}
defineExpose({ resetFilter })
// When the current view changes or the current directory,
// then we need to rebuild the available accounts
watchEffect(() => {
if (currentView.value) {
// we have no access to the files router here...
const path = (currentLocation.value.search ?? '?dir=/').match(/(?<=&|\?)dir=([^&#]+)/)?.[1]
selectedAccounts.value = []
updateAvailableAccounts(decodeURIComponent(path ?? '/'))
}
})
</script>

<style scoped lang="scss">
.file-list-filter-accounts {
max-width: 300px;
}
</style>
79 changes: 79 additions & 0 deletions apps/files_sharing/src/filters/AccountFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { INode } from '@nextcloud/files'

import { FileListFilter, registerFileListFilter } from '@nextcloud/files'
import Vue from 'vue'
import FileListFilterAccount from '../components/FileListFilterAccount.vue'

export interface IAccountData {
uid: string
displayName: string
}

/**
* File list filter to filter by owner / sharee
*/
class AccountFilter extends FileListFilter {

private currentInstance?: Vue
private filterAccounts?: IAccountData[]

constructor() {
super('files_sharing:account', 100)
}

public mount(el: HTMLElement) {
if (this.currentInstance) {
this.currentInstance.$destroy()
}

const View = Vue.extend(FileListFilterAccount as never)
this.currentInstance = new View({
el,
})
.$on('update:accounts', this.setAccounts.bind(this))
.$mount()
}

public filter(nodes: INode[]): INode[] {
if (!this.filterAccounts || this.filterAccounts.length === 0) {
return nodes
}

const userIds = this.filterAccounts.map(({ uid }) => uid)
// Filter if the owner of the node is in the list of filtered accounts
return nodes.filter((node) => {
const sharees = node.attributes.sharees?.sharee as { id: string }[] | undefined
// If the node provides no information lets keep it
if (!node.owner && !sharees) {
return true
}
// if the owner matches
if (node.owner && userIds.includes(node.owner)) {
return true
}
// Or any of the sharees (if only one share this will be an object, otherwise an array. So using `.flat()` to make it always an array)
if (sharees && [sharees].flat().some(({ id }) => userIds.includes(id))) {
return true
}
// Not a valid node for the current filter
return false
})
}

public setAccounts(accounts?: IAccountData[]) {
this.filterAccounts = accounts
this.filterUpdated()
}

}

/**
* Register the file list filter by owner or sharees
*/
export function registerAccountFilter() {
registerFileListFilter(new AccountFilter())
}
4 changes: 4 additions & 0 deletions apps/files_sharing/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import './actions/openInFilesAction'
import './actions/rejectShareAction'
import './actions/restoreShareAction'
import './actions/sharingStatusAction'
import { registerAccountFilter } from './filters/AccountFilter'

registerSharingViews()

addNewFileMenuEntry(newFileRequest)

registerDavProperty('nc:sharees', { nc: 'http://nextcloud.org/ns' })
registerDavProperty('nc:share-attributes', { nc: 'http://nextcloud.org/ns' })
registerDavProperty('oc:share-types', { oc: 'http://owncloud.org/ns' })
registerDavProperty('ocs:share-permissions', { ocs: 'http://open-collaboration-services.org/ns' })

registerAccountFilter()
17 changes: 17 additions & 0 deletions apps/files_sharing/src/utils/AccountIcon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*!
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { generateUrl } from '@nextcloud/router'

const isDarkMode = window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches === true
|| document.querySelector('[data-themes*=dark]') !== null

export const generateAvatarSvg = (userId: string, isGuest = false) => {
const url = isDarkMode ? '/avatar/{userId}/32/dark' : '/avatar/{userId}/32'
const avatarUrl = generateUrl(isGuest ? url : url + '?guestFallback=true', { userId })
return `<svg width="32" height="32" viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg" class="sharing-status__avatar">
<image href="${avatarUrl}" height="32" width="32" />
</svg>`
}

0 comments on commit 1160fc4

Please sign in to comment.