summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Molakvoæ <skjnldsv@protonmail.com>2023-03-24 09:41:40 +0100
committerJohn Molakvoæ <skjnldsv@protonmail.com>2023-04-06 14:49:31 +0200
commit3c3050c76f86c7a8cc2f217f9305cb1051e0eca0 (patch)
treed9656a549b1db4c7f3d37549713a6c96da616464
parent0b4da6117fff4d999cb492503a8b6fc04eb75f9d (diff)
downloadnextcloud-server-3c3050c76f86c7a8cc2f217f9305cb1051e0eca0.tar.gz
nextcloud-server-3c3050c76f86c7a8cc2f217f9305cb1051e0eca0.zip
feat(files): implement sorting per view
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
-rw-r--r--apps/files/js/app.js4
-rw-r--r--apps/files/js/filelist.js6
-rw-r--r--apps/files/lib/Controller/ApiController.php29
-rw-r--r--apps/files/lib/Controller/ViewController.php10
-rw-r--r--apps/files/src/components/FileEntry.vue10
-rw-r--r--apps/files/src/components/FilesListHeader.vue90
-rw-r--r--apps/files/src/components/FilesListHeaderButton.vue160
-rw-r--r--apps/files/src/components/FilesListVirtual.vue6
-rw-r--r--apps/files/src/mixins/fileslist-row.scss27
-rw-r--r--apps/files/src/services/Navigation.ts10
-rw-r--r--apps/files/src/store/sorting.ts50
-rw-r--r--apps/files/src/views/FilesList.vue46
-rw-r--r--apps/files/tests/Controller/ApiControllerTest.php31
-rw-r--r--apps/files/tests/Controller/ViewControllerTest.php19
-rw-r--r--apps/files_trashbin/src/main.ts7
-rw-r--r--apps/files_trashbin/tests/Controller/PreviewControllerTest.php2
-rw-r--r--package.json1
17 files changed, 385 insertions, 123 deletions
diff --git a/apps/files/js/app.js b/apps/files/js/app.js
index 8ebd506c1a3..1252bd5796c 100644
--- a/apps/files/js/app.js
+++ b/apps/files/js/app.js
@@ -116,7 +116,9 @@
},
],
sorting: {
- mode: $('#defaultFileSorting').val(),
+ mode: $('#defaultFileSorting').val() === 'basename'
+ ? 'name'
+ : $('#defaultFileSorting').val(),
direction: $('#defaultFileSortingDirection').val()
},
config: this._filesConfig,
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index 2d93ced7100..e3052ea9fe8 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -2181,8 +2181,10 @@
if (persist && OC.getCurrentUser().uid) {
$.post(OC.generateUrl('/apps/files/api/v1/sorting'), {
- mode: sort,
- direction: direction
+ // Compatibility with new files-to-vue API
+ mode: sort === 'name' ? 'basename' : sort,
+ direction: direction,
+ view: 'files'
});
}
},
diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php
index c7da9b2c118..808f0d555d0 100644
--- a/apps/files/lib/Controller/ApiController.php
+++ b/apps/files/lib/Controller/ApiController.php
@@ -281,20 +281,29 @@ class ApiController extends Controller {
*
* @param string $mode
* @param string $direction
- * @return Response
+ * @return JSONResponse
* @throws \OCP\PreConditionNotMetException
*/
- public function updateFileSorting($mode, $direction) {
- $allowedMode = ['basename', 'size', 'mtime'];
+ public function updateFileSorting($mode, string $direction = 'asc', string $view = 'files'): JSONResponse {
$allowedDirection = ['asc', 'desc'];
- if (!in_array($mode, $allowedMode) || !in_array($direction, $allowedDirection)) {
- $response = new Response();
- $response->setStatus(Http::STATUS_UNPROCESSABLE_ENTITY);
- return $response;
+ if (!in_array($direction, $allowedDirection)) {
+ return new JSONResponse(['message' => 'Invalid direction parameter'], Http::STATUS_UNPROCESSABLE_ENTITY);
}
- $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'file_sorting', $mode);
- $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'file_sorting_direction', $direction);
- return new Response();
+
+ $userId = $this->userSession->getUser()->getUID();
+
+ $sortingJson = $this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}');
+ $sortingConfig = json_decode($sortingJson, true) ?: [];
+ $sortingConfig[$view] = [
+ 'mode' => $mode,
+ 'direction' => $direction,
+ ];
+
+ $this->config->setUserValue($userId, 'files', 'files_sorting_configs', json_encode($sortingConfig));
+ return new JSONResponse([
+ 'message' => 'ok',
+ 'data' => $sortingConfig,
+ ]);
}
/**
diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php
index 6047ad81808..cb41dfb300b 100644
--- a/apps/files/lib/Controller/ViewController.php
+++ b/apps/files/lib/Controller/ViewController.php
@@ -250,10 +250,8 @@ class ViewController extends Controller {
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
// File sorting user config
- $defaultFileSorting = $this->config->getUserValue($userId, 'files', 'file_sorting', 'basename');
- $defaultFileSortingDirection = $this->config->getUserValue($userId, 'files', 'file_sorting_direction', 'asc');
- $this->initialState->provideInitialState('defaultFileSorting', $defaultFileSorting === 'name' ? 'basename' : $defaultFileSorting);
- $this->initialState->provideInitialState('defaultFileSortingDirection', $defaultFileSortingDirection === 'desc' ? 'desc' : 'asc');
+ $filesSortingConfig = json_decode($this->config->getUserValue($userId, 'files', 'files_sorting_configs', '{}'), true);
+ $this->initialState->provideInitialState('filesSortingConfig', $filesSortingConfig);
// render the container content for every navigation item
foreach ($navItems as $item) {
@@ -298,8 +296,8 @@ class ViewController extends Controller {
$params['ownerDisplayName'] = $storageInfo['ownerDisplayName'] ?? '';
$params['isPublic'] = false;
$params['allowShareWithLink'] = $this->shareManager->shareApiAllowLinks() ? 'yes' : 'no';
- $params['defaultFileSorting'] = $this->config->getUserValue($userId, 'files', 'file_sorting', 'name');
- $params['defaultFileSortingDirection'] = $this->config->getUserValue($userId, 'files', 'file_sorting_direction', 'asc');
+ $params['defaultFileSorting'] = $filesSortingConfig['files']['mode'] ?? 'basename';
+ $params['defaultFileSortingDirection'] = $filesSortingConfig['files']['direction'] ?? 'asc';
$params['showgridview'] = $this->config->getUserValue($userId, 'files', 'show_grid', false);
$showHidden = (bool) $this->config->getUserValue($userId, 'files', 'show_hidden', false);
$params['showHiddenFiles'] = $showHidden ? 1 : 0;
diff --git a/apps/files/src/components/FileEntry.vue b/apps/files/src/components/FileEntry.vue
index ea9615af596..29e9895757e 100644
--- a/apps/files/src/components/FileEntry.vue
+++ b/apps/files/src/components/FileEntry.vue
@@ -50,7 +50,7 @@
</span>
<!-- File name -->
- {{ displayName }}
+ <span>{{ displayName }}</span>
</a>
</td>
@@ -89,17 +89,17 @@
</td>
<!-- Size -->
- <th v-if="isSizeAvailable"
+ <td v-if="isSizeAvailable"
:style="{ opacity: sizeOpacity }"
class="files-list__row-size">
<span>{{ size }}</span>
- </th>
+ </td>
<!-- View columns -->
<td v-for="column in columns"
:key="column.id"
:class="`files-list__row-${currentView?.id}-${column.id}`"
- class="files-list__row-column--custom">
+ class="files-list__row-column-custom">
<CustomElementRender :element="column.render(source)" />
</td>
</Fragment>
@@ -207,7 +207,7 @@ export default Vue.extend({
},
size() {
const size = parseInt(this.source.size, 10) || 0
- if (!size || size < 0) {
+ if (typeof size !== 'number' || size < 0) {
return this.t('files', 'Pending')
}
return formatFileSize(size, true)
diff --git a/apps/files/src/components/FilesListHeader.vue b/apps/files/src/components/FilesListHeader.vue
index 1fe6d230a20..0ee7298ee95 100644
--- a/apps/files/src/components/FilesListHeader.vue
+++ b/apps/files/src/components/FilesListHeader.vue
@@ -21,22 +21,18 @@
-->
<template>
<tr>
- <th class="files-list__row-checkbox">
+ <th class="files-list__column files-list__row-checkbox">
<NcCheckboxRadioSwitch v-bind="selectAllBind" @update:checked="onToggleAll" />
</th>
<!-- Link to file -->
- <th class="files-list__row-name files-list__row--sortable"
- @click="toggleSortBy('basename')">
+ <th class="files-list__column files-list__row-name files-list__column--sortable"
+ @click.exact.stop="toggleSortBy('basename')">
<!-- Icon or preview -->
<span class="files-list__row-icon" />
<!-- Name -->
- {{ t('files', 'Name') }}
- <template v-if="defaultFileSorting === 'basename'">
- <MenuUp v-if="defaultFileSortingDirection === 'asc'" />
- <MenuDown v-else />
- </template>
+ <FilesListHeaderButton :name="t('files', 'Name')" mode="basename" />
</th>
<!-- Actions -->
@@ -44,20 +40,19 @@
<!-- Size -->
<th v-if="isSizeAvailable"
- class="files-list__row-size"
- @click="toggleSortBy('size')">
- {{ t('files', 'Size') }}
- <template v-if="defaultFileSorting === 'size'">
- <MenuUp v-if="defaultFileSortingDirection === 'asc'" />
- <MenuDown v-else />
- </template>
+ :class="{'files-list__column--sortable': isSizeAvailable}"
+ class="files-list__column files-list__row-size">
+ <FilesListHeaderButton :name="t('files', 'Size')" mode="size" />
</th>
<!-- Custom views columns -->
<th v-for="column in columns"
:key="column.id"
- :class="`files-list__row-column--custom files-list__row-${currentView.id}-${column.id}`">
- {{ column.title }}
+ :class="classForColumn(column)">
+ <FilesListHeaderButton v-if="!!column.sort" :name="column.title" :mode="column.id" />
+ <span v-else>
+ {{ column.title }}
+ </span>
</th>
</tr>
</template>
@@ -67,6 +62,7 @@ import { mapState } from 'pinia'
import { translate } from '@nextcloud/l10n'
import MenuDown from 'vue-material-design-icons/MenuDown.vue'
import MenuUp from 'vue-material-design-icons/MenuUp.vue'
+import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import Vue from 'vue'
@@ -75,15 +71,13 @@ import { useSelectionStore } from '../store/selection'
import { useSortingStore } from '../store/sorting'
import logger from '../logger.js'
import Navigation from '../services/Navigation'
-
-Vue.config.performance = true
+import FilesListHeaderButton from './FilesListHeaderButton.vue'
export default Vue.extend({
name: 'FilesListHeader',
components: {
- MenuDown,
- MenuUp,
+ FilesListHeaderButton,
NcCheckboxRadioSwitch,
},
@@ -110,7 +104,7 @@ export default Vue.extend({
},
computed: {
- ...mapState(useSortingStore, ['defaultFileSorting', 'defaultFileSortingDirection']),
+ ...mapState(useSortingStore, ['filesSortingConfig']),
/** @return {Navigation} */
currentView() {
@@ -153,9 +147,37 @@ export default Vue.extend({
selectedFiles() {
return this.selectionStore.selected
},
+
+ sortingMode() {
+ return this.sortingStore.getSortingMode(this.currentView.id)
+ || this.currentView.defaultSortKey
+ || 'basename'
+ },
+ isAscSorting() {
+ return this.sortingStore.isAscSorting(this.currentView.id) === true
+ },
},
methods: {
+ classForColumn(column) {
+ return {
+ 'files-list__column': true,
+ 'files-list__column--sortable': !!column.sort,
+ 'files-list__row-column-custom': true,
+ [`files-list__row-${this.currentView.id}-${column.id}`]: true,
+ }
+ },
+
+ sortAriaLabel(column) {
+ const direction = this.isAscSorting
+ ? this.t('files', 'ascending')
+ : this.t('files', 'descending')
+ return this.t('files', 'Sort list by {column} ({direction})', {
+ column,
+ direction,
+ })
+ },
+
onToggleAll(selected) {
if (selected) {
const selection = this.nodes.map(node => node.attributes.fileid.toString())
@@ -169,12 +191,19 @@ export default Vue.extend({
toggleSortBy(key) {
// If we're already sorting by this key, flip the direction
- if (this.defaultFileSorting === key) {
- this.sortingStore.toggleSortingDirection()
+ if (this.sortingMode === key) {
+ this.sortingStore.toggleSortingDirection(this.currentView.id)
return
}
// else sort ASC by this new key
- this.sortingStore.setFileSorting(key)
+ this.sortingStore.setSortingBy(key, this.currentView.id)
+ },
+
+ toggleSortByCustomColumn(column) {
+ if (!column.sort) {
+ return
+ }
+ this.toggleSortBy(column.id)
},
t: translate,
@@ -183,6 +212,15 @@ export default Vue.extend({
</script>
<style scoped lang="scss">
-@import '../mixins/fileslist-row.scss'
+@import '../mixins/fileslist-row.scss';
+.files-list__column {
+ user-select: none;
+ // Make sure the cell colors don't apply to column headers
+ color: var(--color-text-maxcontrast) !important;
+
+ &--sortable {
+ cursor: pointer;
+ }
+}
</style>
diff --git a/apps/files/src/components/FilesListHeaderButton.vue b/apps/files/src/components/FilesListHeaderButton.vue
new file mode 100644
index 00000000000..8a07dd71395
--- /dev/null
+++ b/apps/files/src/components/FilesListHeaderButton.vue
@@ -0,0 +1,160 @@
+<!--
+ - @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
+ -
+ - @author Gary Kim <gary@garykim.dev>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+<template>
+ <NcButton :aria-label="sortAriaLabel(name)"
+ :class="{'files-list__column-sort-button--active': sortingMode === mode}"
+ class="files-list__column-sort-button"
+ type="tertiary"
+ @click="toggleSortBy(mode)">
+ <!-- Sort icon before text as size is align right -->
+ <MenuUp v-if="sortingMode !== mode || isAscSorting" slot="icon" />
+ <MenuDown v-else slot="icon" />
+ {{ name }}
+ </NcButton>
+</template>
+
+<script lang="ts">
+import { mapState } from 'pinia'
+import { translate } from '@nextcloud/l10n'
+import MenuDown from 'vue-material-design-icons/MenuDown.vue'
+import MenuUp from 'vue-material-design-icons/MenuUp.vue'
+import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import Vue from 'vue'
+
+import { useSortingStore } from '../store/sorting'
+
+Vue.config.performance = true
+
+export default Vue.extend({
+ name: 'FilesListHeaderButton',
+
+ components: {
+ MenuDown,
+ MenuUp,
+ NcButton,
+ },
+
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ mode: {
+ type: String,
+ required: true,
+ },
+ },
+
+ setup() {
+ const sortingStore = useSortingStore()
+ return {
+ sortingStore,
+ }
+ },
+
+ computed: {
+ ...mapState(useSortingStore, ['filesSortingConfig']),
+
+ currentView() {
+ return this.$navigation.active
+ },
+
+ sortingMode() {
+ return this.sortingStore.getSortingMode(this.currentView.id)
+ || this.currentView.defaultSortKey
+ || 'basename'
+ },
+ isAscSorting() {
+ return this.sortingStore.isAscSorting(this.currentView.id) === true
+ },
+ },
+
+ methods: {
+ sortAriaLabel(column) {
+ const direction = this.isAscSorting
+ ? this.t('files', 'ascending')
+ : this.t('files', 'descending')
+ return this.t('files', 'Sort list by {column} ({direction})', {
+ column,
+ direction,
+ })
+ },
+
+ toggleSortBy(key) {
+ // If we're already sorting by this key, flip the direction
+ if (this.sortingMode === key) {
+ this.sortingStore.toggleSortingDirection(this.currentView.id)
+ return
+ }
+ // else sort ASC by this new key
+ this.sortingStore.setSortingBy(key, this.currentView.id)
+ },
+
+ toggleSortByCustomColumn(column) {
+ if (!column.sort) {
+ return
+ }
+ this.toggleSortBy(column.id)
+ },
+
+ t: translate,
+ },
+})
+</script>
+
+<style lang="scss">
+.files-list__column-sort-button {
+ // Compensate for cells margin
+ margin: 0 calc(var(--cell-margin) * -1);
+ // Reverse padding
+ padding: 0 4px 0 16px !important;
+
+ // Icon after text
+ .button-vue__wrapper {
+ flex-direction: row-reverse;
+ // Take max inner width for text overflow ellipsis
+ width: 100%;
+ }
+
+ .button-vue__icon {
+ transition-timing-function: linear;
+ transition-duration: .1s;
+ transition-property: opacity;
+ opacity: 0;
+ }
+
+ .button-vue__text {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ &--active,
+ &:hover,
+ &:focus,
+ &:active {
+ .button-vue__icon {
+ opacity: 1 !important;
+ }
+ }
+}
+</style>
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index 3f055f8b878..eafac678310 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -151,6 +151,12 @@ export default Vue.extend({
align-items: center;
width: 100%;
border-bottom: 1px solid var(--color-border);
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: var(--color-background-dark);
+ }
}
}
}
diff --git a/apps/files/src/mixins/fileslist-row.scss b/apps/files/src/mixins/fileslist-row.scss
index 9ad821eb860..06b6637b6f2 100644
--- a/apps/files/src/mixins/fileslist-row.scss
+++ b/apps/files/src/mixins/fileslist-row.scss
@@ -19,6 +19,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
+
+/**
+ * This file is for every column styling that must be
+ * shared between the files list and the list header.
+ */
td, th {
display: flex;
align-items: center;
@@ -31,6 +36,9 @@ td, th {
color: var(--color-text-maxcontrast);
border: none;
+ // Columns should try to add any text
+ // node wrapped in a span. That should help
+ // with the ellipsis on overflow.
span {
overflow: hidden;
white-space: nowrap;
@@ -38,12 +46,6 @@ td, th {
}
}
-.files-list__row {
- &--sortable {
- cursor: pointer;
- }
-}
-
.files-list__row-checkbox {
justify-content: center;
&::v-deep .checkbox-radio-switch {
@@ -122,12 +124,21 @@ td, th {
}
.files-list__row-size {
- justify-content: right;
+ // Right align text
+ justify-content: flex-end;
width: calc(var(--row-height) * 1.5);
// opacity varies with the size
color: var(--color-main-text);
+
+ // Icon is before text since size is right aligned
+ ::v-deep .files-list__column-sort-button {
+ padding: 0 16px 0 4px !important;
+ .button-vue__wrapper {
+ flex-direction: row;
+ }
+ }
}
-.files-list__row-column--custom {
+.files-list__row-column-custom {
width: calc(var(--row-height) * 2);
}
diff --git a/apps/files/src/services/Navigation.ts b/apps/files/src/services/Navigation.ts
index 40881b0e73c..4ca4588dff5 100644
--- a/apps/files/src/services/Navigation.ts
+++ b/apps/files/src/services/Navigation.ts
@@ -75,6 +75,12 @@ export interface Navigation {
expanded?: boolean
/**
+ * Will be used as default if the user
+ * haven't customized their sorting column
+ * */
+ defaultSortKey?: string
+
+ /**
* This view is sticky a legacy view.
* Here until all the views are migrated to Vue.
* @deprecated It will be removed in a near future
@@ -195,6 +201,10 @@ const isValidNavigation = function(view: Navigation): boolean {
throw new Error('Navigation expanded must be a boolean')
}
+ if (view.defaultSortKey && typeof view.defaultSortKey !== 'string') {
+ throw new Error('Navigation defaultSortKey must be a string')
+ }
+
return true
}
diff --git a/apps/files/src/store/sorting.ts b/apps/files/src/store/sorting.ts
index b153301b76b..8e7c87b12b3 100644
--- a/apps/files/src/store/sorting.ts
+++ b/apps/files/src/store/sorting.ts
@@ -28,24 +28,34 @@ import axios from '@nextcloud/axios'
type direction = 'asc' | 'desc'
-const saveUserConfig = (key: string, direction: direction) => {
+interface SortingConfig {
+ mode: string
+ direction: direction
+}
+
+interface SortingStore {
+ [key: string]: SortingConfig
+}
+
+const saveUserConfig = (mode: string, direction: direction, view: string) => {
return axios.post(generateUrl('/apps/files/api/v1/sorting'), {
- mode: key,
- direction: direction as string,
+ mode,
+ direction,
+ view,
})
}
-const defaultFileSorting = loadState('files', 'defaultFileSorting', 'basename')
-const defaultFileSortingDirection = loadState('files', 'defaultFileSortingDirection', 'asc') as direction
+const filesSortingConfig = loadState('files', 'filesSortingConfig', {}) as SortingStore
+console.debug('filesSortingConfig', filesSortingConfig)
export const useSortingStore = defineStore('sorting', {
state: () => ({
- defaultFileSorting,
- defaultFileSortingDirection,
+ filesSortingConfig,
}),
getters: {
- isAscSorting: (state) => state.defaultFileSortingDirection === 'asc',
+ isAscSorting: (state) => (view: string = 'files') => state.filesSortingConfig[view]?.direction !== 'desc',
+ getSortingMode: (state) => (view: string = 'files') => state.filesSortingConfig[view]?.mode,
},
actions: {
@@ -54,19 +64,27 @@ export const useSortingStore = defineStore('sorting', {
* The key param must be a valid key of a File object
* If not found, will be searched within the File attributes
*/
- setSortingBy(key: string) {
- Vue.set(this, 'defaultFileSorting', key)
- Vue.set(this, 'defaultFileSortingDirection', 'asc')
- saveUserConfig(key, 'asc')
+ setSortingBy(key: string = 'basename', view: string = 'files') {
+ const config = this.filesSortingConfig[view] || {}
+ config.mode = key
+ config.direction = 'asc'
+
+ // Save new config
+ Vue.set(this.filesSortingConfig, view, config)
+ saveUserConfig(config.mode, config.direction, view)
},
/**
* Toggle the sorting direction
*/
- toggleSortingDirection() {
- const newDirection = this.defaultFileSortingDirection === 'asc' ? 'desc' : 'asc'
- Vue.set(this, 'defaultFileSortingDirection', newDirection)
- saveUserConfig(this.defaultFileSorting, newDirection)
+ toggleSortingDirection(view: string = 'files') {
+ const config = this.filesSortingConfig[view] || { 'direction': 'asc' }
+ const newDirection = config.direction === 'asc' ? 'desc' : 'asc'
+ config.direction = newDirection
+
+ // Save new config
+ Vue.set(this.filesSortingConfig, view, config)
+ saveUserConfig(config.mode, config.direction, view)
}
}
})
diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue
index d09d3c619f2..03b0076a435 100644
--- a/apps/files/src/views/FilesList.vue
+++ b/apps/files/src/views/FilesList.vue
@@ -63,6 +63,7 @@
<script lang="ts">
import { Folder } from '@nextcloud/files'
import { join } from 'path'
+import { compare, orderBy } from 'natural-orderby'
import { translate } from '@nextcloud/l10n'
import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
@@ -151,26 +152,39 @@ export default Vue.extend({
* @return {Node[]}
*/
dirContents() {
- const sortAsc = this.sortingStore.isAscSorting === true
- const sortKey = this.sortingStore.defaultFileSorting || 'basename'
+ if (!this.currentView) {
+ return []
+ }
- return [...(this.currentFolder?.children || []).map(this.getNode)]
- .sort((a, b) => {
- // Sort folders first
- if (a.type === 'folder' && b.type !== 'folder') {
- return sortAsc ? -1 : 1
- }
+ const sortAsc = this.sortingStore.isAscSorting(this.currentView.id) === true
+ const sortKey = this.sortingStore.getSortingMode(this.currentView.id)
+ || this.currentView.defaultSortKey
+ || 'basename'
- if (a.type !== 'folder' && b.type === 'folder') {
- return sortAsc ? 1 : -1
- }
+ const customColumn = this.currentView.columns
+ .find(column => column.id === sortKey)
- if (typeof a[sortKey] === 'number' && typeof b[sortKey] === 'number') {
- return (a[sortKey] - b[sortKey]) * (sortAsc ? 1 : -1)
- }
+ // Custom column must provide their own sorting methods
+ if (customColumn?.sort && typeof customColumn.sort === 'function') {
+ if (sortAsc) {
+ return [...(this.currentFolder?.children || []).map(this.getNode)]
+ .sort(customColumn.sort)
+ }
+ return [...(this.currentFolder?.children || []).map(this.getNode)]
+ .sort(customColumn.sort)
+ .reverse()
+ }
- return (a[sortKey] || a.basename).localeCompare(b[sortKey] || b.basename) * (sortAsc ? 1 : -1)
- })
+ return orderBy(
+ [...(this.currentFolder?.children || []).map(this.getNode)],
+ [
+ // Sort folders first if sorting by name
+ ...sortKey === 'basename' ? [v => v.type !== 'folder'] : [],
+ v => v[sortKey],
+ v => v.basename,
+ ],
+ sortAsc ? 'asc' : 'desc',
+ )
},
/**
diff --git a/apps/files/tests/Controller/ApiControllerTest.php b/apps/files/tests/Controller/ApiControllerTest.php
index 6df3f46c5a9..2f4daa98901 100644
--- a/apps/files/tests/Controller/ApiControllerTest.php
+++ b/apps/files/tests/Controller/ApiControllerTest.php
@@ -206,37 +206,44 @@ class ApiControllerTest extends TestCase {
$mode = 'mtime';
$direction = 'desc';
- $this->config->expects($this->exactly(2))
+ $sortingConfig = [];
+ $sortingConfig['files'] = [
+ 'mode' => $mode,
+ 'direction' => $direction,
+ ];
+
+ $this->config->expects($this->once())
->method('setUserValue')
- ->withConsecutive(
- [$this->user->getUID(), 'files', 'file_sorting', $mode],
- [$this->user->getUID(), 'files', 'file_sorting_direction', $direction],
- );
+ ->with($this->user->getUID(), 'files', 'files_sorting_configs', json_encode($sortingConfig));
- $expected = new HTTP\Response();
+ $expected = new HTTP\JSONResponse([
+ 'message' => 'ok',
+ 'data' => $sortingConfig
+ ]);
$actual = $this->apiController->updateFileSorting($mode, $direction);
$this->assertEquals($expected, $actual);
}
public function invalidSortingModeData() {
return [
- ['color', 'asc'],
- ['name', 'size'],
- ['foo', 'bar']
+ ['size'],
+ ['bar']
];
}
/**
* @dataProvider invalidSortingModeData
*/
- public function testUpdateInvalidFileSorting($mode, $direction) {
+ public function testUpdateInvalidFileSorting($direction) {
$this->config->expects($this->never())
->method('setUserValue');
- $expected = new Http\Response(null);
+ $expected = new Http\JSONResponse([
+ 'message' => 'Invalid direction parameter'
+ ]);
$expected->setStatus(Http::STATUS_UNPROCESSABLE_ENTITY);
- $result = $this->apiController->updateFileSorting($mode, $direction);
+ $result = $this->apiController->updateFileSorting('basename', $direction);
$this->assertEquals($expected, $result);
}
diff --git a/apps/files/tests/Controller/ViewControllerTest.php b/apps/files/tests/Controller/ViewControllerTest.php
index 08ec7d17a1d..58b70f8b0fa 100644
--- a/apps/files/tests/Controller/ViewControllerTest.php
+++ b/apps/files/tests/Controller/ViewControllerTest.php
@@ -266,19 +266,6 @@ class ViewControllerTest extends TestCase {
'expanded' => false,
'unread' => 0,
],
- 'trashbin' => [
- 'id' => 'trashbin',
- 'appname' => 'files_trashbin',
- 'script' => 'list.php',
- 'order' => 50,
- 'name' => \OC::$server->getL10N('files_trashbin')->t('Deleted files'),
- 'active' => false,
- 'icon' => '',
- 'type' => 'link',
- 'classes' => 'pinned',
- 'expanded' => false,
- 'unread' => 0,
- ],
'shareoverview' => [
'id' => 'shareoverview',
'appname' => 'files_sharing',
@@ -339,7 +326,7 @@ class ViewControllerTest extends TestCase {
'owner' => 'MyName',
'ownerDisplayName' => 'MyDisplayName',
'isPublic' => false,
- 'defaultFileSorting' => 'name',
+ 'defaultFileSorting' => 'basename',
'defaultFileSortingDirection' => 'asc',
'showHiddenFiles' => 0,
'cropImagePreviews' => 1,
@@ -363,10 +350,6 @@ class ViewControllerTest extends TestCase {
'id' => 'systemtagsfilter',
'content' => null,
],
- 'trashbin' => [
- 'id' => 'trashbin',
- 'content' => null,
- ],
'sharingout' => [
'id' => 'sharingout',
'content' => null,
diff --git a/apps/files_trashbin/src/main.ts b/apps/files_trashbin/src/main.ts
index 7cd6cf850f8..13b37836774 100644
--- a/apps/files_trashbin/src/main.ts
+++ b/apps/files_trashbin/src/main.ts
@@ -20,6 +20,7 @@
*
*/
import type NavigationService from '../../files/src/services/Navigation'
+import type { Navigation } from '../../files/src/services/Navigation'
import { translate as t, translate } from '@nextcloud/l10n'
import DeleteSvg from '@mdi/svg/svg/delete.svg?raw'
@@ -39,6 +40,8 @@ Navigation.register({
order: 50,
sticky: true,
+ defaultSortKey: 'deleted',
+
columns: [
{
id: 'deleted',
@@ -57,10 +60,10 @@ Navigation.register({
sort(nodeA, nodeB) {
const deletionTimeA = nodeA.attributes?.['trashbin-deletion-time'] || nodeA?.mtime || 0
const deletionTimeB = nodeB.attributes?.['trashbin-deletion-time'] || nodeB?.mtime || 0
- return deletionTimeA - deletionTimeB
+ return deletionTimeB - deletionTimeA
},
},
],
getContents,
-})
+} as Navigation)
diff --git a/apps/files_trashbin/tests/Controller/PreviewControllerTest.php b/apps/files_trashbin/tests/Controller/PreviewControllerTest.php
index 441222bea19..4db3d4f613c 100644
--- a/apps/files_trashbin/tests/Controller/PreviewControllerTest.php
+++ b/apps/files_trashbin/tests/Controller/PreviewControllerTest.php
@@ -157,7 +157,7 @@ class PreviewControllerTest extends TestCase {
$this->overwriteService(ITimeFactory::class, $this->time);
- $res = $this->controller->getPreview(42, 10, 10);
+ $res = $this->controller->getPreview(42, 10, 10, true);
$expected = new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => 'previewMime']);
$expected->cacheFor(3600 * 24);
diff --git a/package.json b/package.json
index c83e376da14..8df2826e368 100644
--- a/package.json
+++ b/package.json
@@ -81,6 +81,7 @@
"marked": "^4.0.14",
"moment": "^2.29.4",
"moment-timezone": "^0.5.38",
+ "natural-orderby": "^3.0.2",
"nextcloud-vue-collections": "^0.10.0",
"node-vibrant": "^3.1.6",
"p-limit": "^4.0.0",