Skip to content

Commit

Permalink
Merge pull request #973 from nextcloud-libraries/feat/files-settings
Browse files Browse the repository at this point in the history
Get files app config and use it for the file picker
  • Loading branch information
susnux authored Oct 17, 2023
2 parents 8928a43 + 82d6637 commit 9324082
Show file tree
Hide file tree
Showing 16 changed files with 1,144 additions and 176 deletions.
51 changes: 35 additions & 16 deletions lib/components/FilePicker/FileList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,17 @@
*
*/

import { mount } from '@vue/test-utils'
import { mount, shallowMount } from '@vue/test-utils'
import { beforeAll, describe, expect, it, vi } from 'vitest'

import FileList from './FileList.vue'
import { File, Folder } from '@nextcloud/files'
import { nextTick } from 'vue'

// mock OC.MimeType
window.OC = {
MimeType: {
getIconUrl: (mime: string) => `icon/${mime}`,
},
} as never
const axios = vi.hoisted(() => ({
get: vi.fn(() => new Promise(() => {})),
}))
vi.mock('@nextcloud/axios', () => ({ default: axios }))

const exampleNodes = [
new File({
Expand Down Expand Up @@ -79,8 +78,9 @@ describe('FilePicker FileList', () => {
const consoleError = vi.spyOn(console, 'error')
const consoleWarning = vi.spyOn(console, 'warn')

const wrapper = mount(FileList, {
const wrapper = shallowMount(FileList, {
propsData: {
currentView: 'files',
multiselect: false,
allowPickDirectory: false,
loading: false,
Expand All @@ -96,8 +96,9 @@ describe('FilePicker FileList', () => {
})

it('header checkbox is not shown if multiselect is `false`', () => {
const wrapper = mount(FileList, {
const wrapper = shallowMount(FileList, {
propsData: {
currentView: 'files',
multiselect: false,
allowPickDirectory: false,
loading: false,
Expand All @@ -110,8 +111,9 @@ describe('FilePicker FileList', () => {
})

it('header checkbox is shown if multiselect is `true`', () => {
const wrapper = mount(FileList, {
const wrapper = shallowMount(FileList, {
propsData: {
currentView: 'files',
multiselect: true,
allowPickDirectory: false,
loading: false,
Expand All @@ -130,8 +132,9 @@ describe('FilePicker FileList', () => {

it('header checkbox is checked when all nodes are selected', async () => {
const nodes = [...exampleNodes]
const wrapper = mount(FileList, {
const wrapper = shallowMount(FileList, {
propsData: {
currentView: 'files',
multiselect: true,
allowPickDirectory: false,
loading: false,
Expand All @@ -150,48 +153,64 @@ describe('FilePicker FileList', () => {
const nodes = [...exampleNodes]
const wrapper = mount(FileList, {
propsData: {
currentView: 'files',
multiselect: true,
allowPickDirectory: false,
loading: false,
files: nodes,
selectedFiles: [],
path: '/',
},
stubs: {
FilePreview: true,
},
})

await nextTick()

const rows = wrapper.findAll('[data-testid="file-list-row"]')
// all nodes are shown
expect(rows.length).toBe(nodes.length)
// folder are sorted first
expect(rows.at(0).attributes('data-filename')).toBe('directory')
// by default favorites are sorted before other files
expect(rows.at(1).attributes('data-filename')).toBe('favorite.txt')
// other files are ascending
expect(rows.at(1).attributes('data-filename')).toBe('a-file.txt')
expect(rows.at(2).attributes('data-filename')).toBe('b-file.txt')
expect(rows.at(3).attributes('data-filename')).toBe('favorite.txt')
expect(rows.at(2).attributes('data-filename')).toBe('a-file.txt')
expect(rows.at(3).attributes('data-filename')).toBe('b-file.txt')
})

it('can sort descending by name', async () => {
const nodes = [...exampleNodes]
const wrapper = mount(FileList, {
propsData: {
currentView: 'files',
multiselect: true,
allowPickDirectory: false,
loading: false,
files: nodes,
selectedFiles: [],
path: '/',
},
stubs: {
FilePreview: true,
},
})

await wrapper.find('[data-test="file-picker_sort-name"]').trigger('click')
await nextTick()

wrapper.find('[data-test="file-picker_sort-name"]').trigger('click')

await nextTick()

const rows = wrapper.findAll('.file-picker__row')
// all nodes are shown
expect(rows.length).toBe(nodes.length)
// folder are sorted first
expect(rows.at(0).attributes('data-filename')).toBe('directory')
// other files are descending
// by default favorites are sorted before other files
expect(rows.at(1).attributes('data-filename')).toBe('favorite.txt')
// other files are descending
expect(rows.at(2).attributes('data-filename')).toBe('b-file.txt')
expect(rows.at(3).attributes('data-filename')).toBe('a-file.txt')
})
Expand Down
110 changes: 67 additions & 43 deletions lib/components/FilePicker/FileList.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<template>
<div class="file-picker__files" ref="fileContainer">
<div ref="fileContainer" class="file-picker__files">
<table>
<thead>
<tr>
<th class="row-checkbox" v-if="multiselect">
<th v-if="multiselect" class="row-checkbox">
<span class="hidden-visually">
{{ t('Select entry') }}
</span>
Expand All @@ -16,11 +16,10 @@
<th :aria-sort="sortByName" class="row-name">
<div class="header-wrapper">
<span class="file-picker__header-preview" />
<NcButton
:wide="true"
<NcButton :wide="true"
type="tertiary"
data-test="file-picker_sort-name"
@click="toggleSortByName">
@click="toggleSorting('basename')">
<template #icon>
<IconSortAscending v-if="sortByName === 'ascending'" :size="20" />
<IconSortDescending v-else-if="sortByName === 'descending'" :size="20" />
Expand All @@ -31,7 +30,7 @@
</div>
</th>
<th :aria-sort="sortBySize" class="row-size">
<NcButton :wide="true" type="tertiary" @click="toggleSortBySize">
<NcButton :wide="true" type="tertiary" @click="toggleSorting('size')">
<template #icon>
<IconSortAscending v-if="sortBySize === 'ascending'" :size="20" />
<IconSortDescending v-else-if="sortBySize === 'descending'" :size="20" />
Expand All @@ -41,7 +40,7 @@
</NcButton>
</th>
<th :aria-sort="sortByModified" class="row-modified">
<NcButton :wide="true" type="tertiary" @click="toggleSortByModified">
<NcButton :wide="true" type="tertiary" @click="toggleSorting('mtime')">
<template #icon>
<IconSortAscending v-if="sortByModified === 'ascending'" :size="20" />
<IconSortDescending v-else-if="sortByModified === 'descending'" :size="20" />
Expand All @@ -54,7 +53,7 @@
</thead>
<tbody>
<template v-if="loading">
<LoadingTableRow v-for="index in skeletonNumber" :key="index" :show-checkbox="multiselect"/>
<LoadingTableRow v-for="index in skeletonNumber" :key="index" :show-checkbox="multiselect" />
</template>
<template v-else>
<FileListRow v-for="file in sortedFiles"
Expand All @@ -73,20 +72,24 @@
</template>

<script setup lang="ts">
import { FileType, type Node } from '@nextcloud/files'
import type { Node } from '@nextcloud/files'
import type { FileListViews } from '../../composables/filesSettings'
import { FileType } from '@nextcloud/files'
import { getCanonicalLocale } from '@nextcloud/l10n'
import { NcButton, NcCheckboxRadioSwitch } from '@nextcloud/vue'
import { join } from 'path'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useFilesSettings, useFilesViews } from '../../composables/filesSettings'
import { t } from '../../utils/l10n'
import { computed, nextTick, onMounted, onUnmounted, ref, type Ref } from 'vue'
import IconSortAscending from 'vue-material-design-icons/MenuUp.vue'
import IconSortDescending from 'vue-material-design-icons/MenuDown.vue'
import LoadingTableRow from './LoadingTableRow.vue'
import FileListRow from './FileListRow.vue'
const props = defineProps<{
currentView: FileListViews,
multiselect: boolean
allowPickDirectory: boolean
loading: boolean
Expand All @@ -100,52 +103,65 @@ const emit = defineEmits<{
(e: 'update:selectedFiles', nodes: Node[]): void
}>()
type ISortingOptions = 'ascending' | 'descending' | undefined
/// sorting related stuff
const sortByName = ref<ISortingOptions>('ascending')
const sortBySize = ref<ISortingOptions>(undefined)
const sortByModified = ref<ISortingOptions>(undefined)
type ISortingAttributes = 'basename' | 'size' | 'mtime'
type ISortingOrder = 'ascending' | 'descending' | 'none'
const ordering = {
ascending: <T>(a: T, b: T, fn: (a: T, b: T) => number) => fn(a, b),
descending: <T>(a: T, b: T, fn: (a: T, b: T) => number) => fn(b, a),
none: <T>(a: T, b: T, fn: (a: T, b: T) => number) => 0,
}
const byName = (a: Node, b: Node) => (a.attributes?.displayName || a.basename).localeCompare(b.attributes?.displayName || b.basename, getCanonicalLocale())
const bySize = (a: Node, b: Node) => (b.size || 0) - (a.size || 0)
const byDate = (a: Node, b: Node) => (a.mtime?.getTime() || 0) - (b.mtime?.getTime() || 0)
/** Override files app sorting */
const customSortingConfig = ref<{ sortBy: ISortingAttributes, order: ISortingOrder }>()
/** The current sorting of the files app */
const { currentConfig: filesAppSorting } = useFilesViews(props.currentView)
/** Wrapper that uses custom sorting, but fallsback to the files app */
const sortingConfig = computed(() => customSortingConfig.value ?? filesAppSorting.value)
const toggleSorting = (variable: Ref<ISortingOptions>) => {
const old = variable.value
// reset
sortByModified.value = sortBySize.value = sortByName.value = undefined
// Some helpers for the template
const sortByName = computed(() => sortingConfig.value.sortBy === 'basename' ? (sortingConfig.value.order === 'none' ? undefined : sortingConfig.value.order) : undefined)
const sortBySize = computed(() => sortingConfig.value.sortBy === 'size' ? (sortingConfig.value.order === 'none' ? undefined : sortingConfig.value.order) : undefined)
const sortByModified = computed(() => sortingConfig.value.sortBy === 'mtime' ? (sortingConfig.value.order === 'none' ? undefined : sortingConfig.value.order) : undefined)
if (old === 'ascending') {
variable.value = 'descending'
const toggleSorting = (sortBy: ISortingAttributes) => {
if (sortingConfig.value.sortBy === sortBy) {
if (sortingConfig.value.order === 'ascending') {
customSortingConfig.value = { sortBy: sortingConfig.value.sortBy, order: 'descending' }
} else {
customSortingConfig.value = { sortBy: sortingConfig.value.sortBy, order: 'ascending' }
}
} else {
variable.value = 'ascending'
customSortingConfig.value = { sortBy, order: 'ascending' }
}
}
const toggleSortByName = () => toggleSorting(sortByName)
const toggleSortBySize = () => toggleSorting(sortBySize)
const toggleSortByModified = () => toggleSorting(sortByModified)
const { sortFavoritesFirst } = useFilesSettings()
/**
* Files sorted by columns
*/
const sortedFiles = computed(() => [...props.files].sort(
const sortedFiles = computed(() => {
const ordering = {
ascending: <T, >(a: T, b: T, fn: (a: T, b: T) => number) => fn(a, b),
descending: <T, >(a: T, b: T, fn: (a: T, b: T) => number) => fn(b, a),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
none: <T, >(_a: T, _b: T, _fn: (a: T, b: T) => number) => 0,
}
const sorting = {
basename: (a: Node, b: Node) => (a.attributes?.displayName || a.basename).localeCompare(b.attributes?.displayName || b.basename, getCanonicalLocale()),
size: (a: Node, b: Node) => (a.size || 0) - (b.size || 0),
// reverted because "young" is smaller than "old"
mtime: (a: Node, b: Node) => (b.mtime?.getTime?.() || 0) - (a.mtime?.getTime?.() || 0),
}
return [...props.files].sort(
(a, b) =>
// Folders always come above the files
(b.type === FileType.Folder ? 1 : 0) - (a.type === FileType.Folder ? 1 : 0) ||
// Favorites above other files
// (b.attributes?.favorite || false) - (a.attributes?.favorite || false) ||
// then sort by name / size / modified
ordering[sortByName.value || 'none'](a, b, byName) ||
ordering[sortBySize.value || 'none'](a, b, bySize) ||
ordering[sortByModified.value || 'none'](a, b, byDate)
// Folders always come above the files
(b.type === FileType.Folder ? 1 : 0) - (a.type === FileType.Folder ? 1 : 0)
// Favorites above other files
|| (sortFavoritesFirst ? ((b.attributes.favorite ? 1 : 0) - (a.attributes.favorite ? 1 : 0)) : 0)
// then sort by name / size / modified
|| ordering[sortingConfig.value.order](a, b, sorting[sortingConfig.value.sortBy]),
)
},
)
/**
Expand All @@ -171,6 +187,10 @@ function onSelectAll() {
}
}
/**
* Handle selecting a node on the files list
* @param file the selected node
*/
function onNodeSelected(file: Node) {
if (props.selectedFiles.includes(file)) {
emit('update:selectedFiles', props.selectedFiles.filter((f) => f.path !== file.path))
Expand All @@ -184,6 +204,10 @@ function onNodeSelected(file: Node) {
}
}
/**
* Emit the new current path
* @param dir The directory that is entered
*/
function onChangeDirectory(dir: Node) {
emit('update:path', join(props.path, dir.basename))
}
Expand All @@ -197,7 +221,7 @@ const fileContainer = ref<HTMLDivElement>()
const resize = () => nextTick(() => {
const nodes = fileContainer.value?.parentElement?.children || []
let height = fileContainer.value?.parentElement?.clientHeight || 450
for(let index = 0; index < nodes.length; index++) {
for (let index = 0; index < nodes.length; index++) {
if (!fileContainer.value?.isSameNode(nodes[index])) {
height -= nodes[index].clientHeight
}
Expand Down
Loading

0 comments on commit 9324082

Please sign in to comment.