-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(files_sharing): Add file list filter to filter by owner / sharee
Signed-off-by: Ferdinand Thiessen <[email protected]>
- Loading branch information
Showing
5 changed files
with
224 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 116 additions & 0 deletions
116
apps/files_sharing/src/components/FileListFilterAccount.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>` | ||
} |