]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat(files): implement sorting per view
authorJohn Molakvoæ <skjnldsv@protonmail.com>
Fri, 24 Mar 2023 08:41:40 +0000 (09:41 +0100)
committerJohn Molakvoæ <skjnldsv@protonmail.com>
Thu, 6 Apr 2023 12:49:31 +0000 (14:49 +0200)
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
17 files changed:
apps/files/js/app.js
apps/files/js/filelist.js
apps/files/lib/Controller/ApiController.php
apps/files/lib/Controller/ViewController.php
apps/files/src/components/FileEntry.vue
apps/files/src/components/FilesListHeader.vue
apps/files/src/components/FilesListHeaderButton.vue [new file with mode: 0644]
apps/files/src/components/FilesListVirtual.vue
apps/files/src/mixins/fileslist-row.scss
apps/files/src/services/Navigation.ts
apps/files/src/store/sorting.ts
apps/files/src/views/FilesList.vue
apps/files/tests/Controller/ApiControllerTest.php
apps/files/tests/Controller/ViewControllerTest.php
apps/files_trashbin/src/main.ts
apps/files_trashbin/tests/Controller/PreviewControllerTest.php
package.json

index 8ebd506c1a3eb417a15ac487f3dfc2c1f2931875..1252bd5796cb3d73758165f2ed609c8827da1d4d 100644 (file)
                                                },
                                        ],
                                        sorting: {
-                                               mode: $('#defaultFileSorting').val(),
+                                               mode: $('#defaultFileSorting').val() === 'basename'
+                                                       ? 'name'
+                                                       : $('#defaultFileSorting').val(),
                                                direction: $('#defaultFileSortingDirection').val()
                                        },
                                        config: this._filesConfig,
index 2d93ced71006c20c4c287a2fa425a3031813d994..e3052ea9fe85b88cf36f8f98706cff9156120d2e 100644 (file)
 
                        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'
                                });
                        }
                },
index c7da9b2c11815081627bcdc27fb163d010890bc9..808f0d555d0e74b08905d1ffc08ed02c0bd9a759 100644 (file)
@@ -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,
+               ]);
        }
 
        /**
index 6047ad81808ff49005cf8eb7884895e55ee9d114..cb41dfb300b4d0e93a55a05c3c3f4c0082a4733d 100644 (file)
@@ -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;
index ea9615af596400f4c8bf6e612b4f4f19818a2929..29e9895757e9daea5da983b0c2299319d0dd6ada 100644 (file)
@@ -50,7 +50,7 @@
                                </span>
 
                                <!-- File name -->
-                               {{ displayName }}
+                               <span>{{ displayName }}</span>
                        </a>
                </td>
 
                </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)
index 1fe6d230a205b7ed12ebe5456c7d6c0b982f883f..0ee7298ee95b8269812b3eb333dda92c76d730ef 100644 (file)
   -->
 <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 -->
 
                <!-- 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 (file)
index 0000000..8a07dd7
--- /dev/null
@@ -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>
index 3f055f8b87886e7859ebef377965b992b12f6183..eafac678310a84603a89918767aa7569187aa982 100644 (file)
@@ -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);
+                       }
                }
        }
 }
index 9ad821eb860666b581e7e5bfac17b536dc110e68..06b6637b6f2da10ef7f92ff929f53bfbf701a7de 100644 (file)
  * 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);
 }
index 40881b0e73ca6262e1055349585872656ac4c581..4ca4588dff5d0a99bf49117e3ed441d861c70f85 100644 (file)
@@ -74,6 +74,12 @@ export interface Navigation {
        /** This view has children and is expanded or not */
        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.
@@ -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
 }
 
index b153301b76b0356fe33278a77f4f5f85359da7be..8e7c87b12b30865c8a7af282c45a83ed3f3bd697 100644 (file)
@@ -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)
                }
        }
 })
index d09d3c619f23424cc9f8732a346f2b2afd722e69..03b0076a4359727805a086cb92a19b202d999a6d 100644 (file)
@@ -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',
+                       )
                },
 
                /**
index 6df3f46c5a96a51415a6637dde168814aa13b733..2f4daa989013d7adf203a1a54a34fc8dcf98c3a3 100644 (file)
@@ -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);
        }
index 08ec7d17a1d05cc11bb7d409fbde99b1a3e4186f..58b70f8b0fa3343855af2148b7fb914deb900482 100644 (file)
@@ -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,
index 7cd6cf850f86a55a57e404996178b6ec879ccd68..13b37836774eb265b29f7f7bc1e067e3030b9126 100644 (file)
@@ -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)
index 441222bea1979d9dc9b642fa7eacf9750a10b4ea..4db3d4f613c5bf9fb0db74095b139ab75b51b945 100644 (file)
@@ -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);
 
index c83e376da142955d770397fd1047ee21850596af..8df2826e368389c35803df1e94383679a8389635 100644 (file)
@@ -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",