]> source.dussan.org Git - nextcloud-server.git/commitdiff
chore(files): add Headers, remove legacy methods and cleanup
authorJohn Molakvoæ <skjnldsv@protonmail.com>
Wed, 9 Aug 2023 12:59:35 +0000 (14:59 +0200)
committerJohn Molakvoæ <skjnldsv@protonmail.com>
Thu, 17 Aug 2023 16:56:37 +0000 (18:56 +0200)
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
34 files changed:
apps/files/lib/Controller/ViewController.php
apps/files/lib/Event/LoadAdditionalScriptsEvent.php
apps/files/src/components/FilesListFooter.vue [deleted file]
apps/files/src/components/FilesListHeader.vue
apps/files/src/components/FilesListHeaderActions.vue [deleted file]
apps/files/src/components/FilesListHeaderButton.vue [deleted file]
apps/files/src/components/FilesListTableFooter.vue [new file with mode: 0644]
apps/files/src/components/FilesListTableHeader.vue [new file with mode: 0644]
apps/files/src/components/FilesListTableHeaderActions.vue [new file with mode: 0644]
apps/files/src/components/FilesListTableHeaderButton.vue [new file with mode: 0644]
apps/files/src/components/FilesListVirtual.vue
apps/files/src/components/NavigationQuota.vue
apps/files/src/legacy/navigationMapper.js [deleted file]
apps/files/src/main.ts
apps/files/src/mixins/filesSorting.ts
apps/files/src/router/router.js [deleted file]
apps/files/src/router/router.ts [new file with mode: 0644]
apps/files/src/services/Navigation.ts
apps/files/src/views/FilesList.vue
apps/files/src/views/Navigation.cy.ts
apps/files/src/views/Navigation.vue
apps/files/src/views/Sidebar.vue
apps/files/src/views/favorites.spec.ts
apps/files/src/views/favorites.ts
apps/files/src/views/files.ts
apps/files/src/views/recent.ts
apps/files/templates/index.php
apps/files_external/src/main.ts
apps/files_sharing/src/views/shares.spec.ts
apps/files_sharing/src/views/shares.ts
apps/files_trashbin/src/main.ts
core/src/systemtags/systemtagmodel.js
package-lock.json
package.json

index 01f85a7c93971999d5486d654bf5741fb1858f8d..24f236a0893953bcbf5c1f6eba753d601563f1db 100644 (file)
@@ -187,8 +187,6 @@ class ViewController extends Controller {
                        }
                }
 
-               $nav = new \OCP\Template('files', 'appnavigation', '');
-
                // Load the files we need
                \OCP\Util::addStyle('files', 'merged');
                \OCP\Util::addScript('files', 'merged-index', 'files');
@@ -203,15 +201,6 @@ class ViewController extends Controller {
                        $favElements['folders'] = [];
                }
 
-               $navItems = \OCA\Files\App::getNavigationManager()->getAll();
-
-               // parse every menu and add the expanded user value
-               foreach ($navItems as $key => $item) {
-                       $navItems[$key]['expanded'] = $this->config->getUserValue($userId, 'files', 'show_' . $item['id'], '0') === '1';
-               }
-
-               $nav->assign('navigationItems', $navItems);
-
                $contentItems = [];
 
                try {
@@ -222,7 +211,6 @@ class ViewController extends Controller {
                }
 
                $this->initialState->provideInitialState('storageStats', $storageInfo);
-               $this->initialState->provideInitialState('navigation', $navItems);
                $this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
                $this->initialState->provideInitialState('viewConfigs', $this->viewConfig->getConfigs());
                $this->initialState->provideInitialState('favoriteFolders', $favElements['folders'] ?? []);
@@ -231,34 +219,9 @@ class ViewController extends Controller {
                $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) {
-                       $content = '';
-                       if (isset($item['script'])) {
-                               $content = $this->renderScript($item['appname'], $item['script']);
-                       }
-                       // parse submenus
-                       if (isset($item['sublist'])) {
-                               foreach ($item['sublist'] as $subitem) {
-                                       $subcontent = '';
-                                       if (isset($subitem['script'])) {
-                                               $subcontent = $this->renderScript($subitem['appname'], $subitem['script']);
-                                       }
-                                       $contentItems[$subitem['id']] = [
-                                               'id' => $subitem['id'],
-                                               'content' => $subcontent
-                                       ];
-                               }
-                       }
-                       $contentItems[$item['id']] = [
-                               'id' => $item['id'],
-                               'content' => $content
-                       ];
-               }
-
-               $this->eventDispatcher->dispatchTyped(new ResourcesLoadAdditionalScriptsEvent());
                $event = new LoadAdditionalScriptsEvent();
                $this->eventDispatcher->dispatchTyped($event);
+               $this->eventDispatcher->dispatchTyped(new ResourcesLoadAdditionalScriptsEvent());
                $this->eventDispatcher->dispatchTyped(new LoadSidebar());
                // Load Viewer scripts
                if (class_exists(LoadViewer::class)) {
@@ -268,23 +231,9 @@ class ViewController extends Controller {
                $this->initialState->provideInitialState('templates_path', $this->templateManager->hasTemplateDirectory() ? $this->templateManager->getTemplatePath() : false);
                $this->initialState->provideInitialState('templates', $this->templateManager->listCreators());
 
-               $params = [];
-               $params['usedSpacePercent'] = (int) $storageInfo['relative'];
-               $params['owner'] = $storageInfo['owner'] ?? '';
-               $params['ownerDisplayName'] = $storageInfo['ownerDisplayName'] ?? '';
-               $params['isPublic'] = false;
-               $params['allowShareWithLink'] = $this->shareManager->shareApiAllowLinks() ? 'yes' : 'no';
-               $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;
-               $cropImagePreviews = (bool) $this->config->getUserValue($userId, 'files', 'crop_image_previews', true);
-               $params['cropImagePreviews'] = $cropImagePreviews ? 1 : 0;
-               $params['fileNotFound'] = $fileNotFound ? 1 : 0;
-               $params['appNavigation'] = $nav;
-               $params['appContents'] = $contentItems;
-               $params['hiddenFields'] = $event->getHiddenFields();
+               $params = [
+                       'fileNotFound' => $fileNotFound ? 1 : 0
+               ];
 
                $response = new TemplateResponse(
                        Application::APP_ID,
index 5291a776e81bdf56e9226b7fbb290d14edbe1886..1e2080622f460b2b9f99f524c89eacdcdc16577c 100644 (file)
@@ -31,18 +31,7 @@ use OCP\EventDispatcher\Event;
 
 /**
  * This event is triggered when the files app is rendered.
- * It can be used to add additional scripts to the files app.
  *
  * @since 17.0.0
  */
-class LoadAdditionalScriptsEvent extends Event {
-       private $hiddenFields = [];
-
-       public function addHiddenField(string $name, string $value): void {
-               $this->hiddenFields[$name] = $value;
-       }
-
-       public function getHiddenFields(): array {
-               return $this->hiddenFields;
-       }
-}
+class LoadAdditionalScriptsEvent extends Event {}
\ No newline at end of file
diff --git a/apps/files/src/components/FilesListFooter.vue b/apps/files/src/components/FilesListFooter.vue
deleted file mode 100644 (file)
index b4a2d7e..0000000
+++ /dev/null
@@ -1,175 +0,0 @@
-<!--
-  - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
-  -
-  - @author John Molakvoæ <skjnldsv@protonmail.com>
-  -
-  - @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>
-       <tr>
-               <th class="files-list__row-checkbox">
-                       <span class="hidden-visually">{{ t('files', 'Total rows summary') }}</span>
-               </th>
-
-               <!-- Link to file -->
-               <td class="files-list__row-name">
-                       <!-- Icon or preview -->
-                       <span class="files-list__row-icon" />
-
-                       <!-- Summary -->
-                       <span>{{ summary }}</span>
-               </td>
-
-               <!-- Actions -->
-               <td class="files-list__row-actions" />
-
-               <!-- Size -->
-               <td v-if="isSizeAvailable"
-                       class="files-list__column files-list__row-size">
-                       <span>{{ totalSize }}</span>
-               </td>
-
-               <!-- Mtime -->
-               <td v-if="isMtimeAvailable"
-                       class="files-list__column files-list__row-mtime" />
-
-               <!-- Custom views columns -->
-               <th v-for="column in columns"
-                       :key="column.id"
-                       :class="classForColumn(column)">
-                       <span>{{ column.summary?.(nodes, currentView) }}</span>
-               </th>
-       </tr>
-</template>
-
-<script lang="ts">
-import { formatFileSize } from '@nextcloud/files'
-import { translate } from '@nextcloud/l10n'
-import Vue from 'vue'
-
-import { useFilesStore } from '../store/files.ts'
-import { usePathsStore } from '../store/paths.ts'
-
-export default Vue.extend({
-       name: 'FilesListFooter',
-
-       components: {
-       },
-
-       props: {
-               isMtimeAvailable: {
-                       type: Boolean,
-                       default: false,
-               },
-               isSizeAvailable: {
-                       type: Boolean,
-                       default: false,
-               },
-               nodes: {
-                       type: Array,
-                       required: true,
-               },
-               summary: {
-                       type: String,
-                       default: '',
-               },
-               filesListWidth: {
-                       type: Number,
-                       default: 0,
-               },
-       },
-
-       setup() {
-               const pathsStore = usePathsStore()
-               const filesStore = useFilesStore()
-               return {
-                       filesStore,
-                       pathsStore,
-               }
-       },
-
-       computed: {
-               currentView() {
-                       return this.$navigation.active
-               },
-
-               dir() {
-                       // Remove any trailing slash but leave root slash
-                       return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
-               },
-
-               currentFolder() {
-                       if (!this.currentView?.id) {
-                               return
-                       }
-
-                       if (this.dir === '/') {
-                               return this.filesStore.getRoot(this.currentView.id)
-                       }
-                       const fileId = this.pathsStore.getPath(this.currentView.id, this.dir)
-                       return this.filesStore.getNode(fileId)
-               },
-
-               columns() {
-                       // Hide columns if the list is too small
-                       if (this.filesListWidth < 512) {
-                               return []
-                       }
-                       return this.currentView?.columns || []
-               },
-
-               totalSize() {
-                       // If we have the size already, let's use it
-                       if (this.currentFolder?.size) {
-                               return formatFileSize(this.currentFolder.size, true)
-                       }
-
-                       // Otherwise let's compute it
-                       return formatFileSize(this.nodes.reduce((total, node) => total + node.size || 0, 0), true)
-               },
-       },
-
-       methods: {
-               classForColumn(column) {
-                       return {
-                               'files-list__row-column-custom': true,
-                               [`files-list__row-${this.currentView.id}-${column.id}`]: true,
-                       }
-               },
-
-               t: translate,
-       },
-})
-</script>
-
-<style scoped lang="scss">
-// Scoped row
-tr {
-       padding-bottom: 300px;
-       border-top: 1px solid var(--color-border);
-       // Prevent hover effect on the whole row
-       background-color: transparent !important;
-       border-bottom: none !important;
-}
-
-td {
-       user-select: none;
-       // Make sure the cell colors don't apply to column headers
-       color: var(--color-text-maxcontrast) !important;
-}
-
-</style>
index d36c9dd46a6dc0f9c6c020c015b1799a8d1d537a..74dc224a39b7be02353bb8f8c8bb5b61ebd42a17 100644 (file)
   -
   -->
 <template>
-       <tr>
-               <th class="files-list__column files-list__row-checkbox">
-                       <NcCheckboxRadioSwitch v-bind="selectAllBind" @update:checked="onToggleAll" />
-               </th>
-
-               <!-- Actions multiple if some are selected -->
-               <FilesListHeaderActions v-if="!isNoneSelected"
-                       :current-view="currentView"
-                       :selected-nodes="selectedNodes" />
-
-               <!-- Columns display -->
-               <template v-else>
-                       <!-- Link to file -->
-                       <th class="files-list__column files-list__row-name files-list__column--sortable"
-                               @click.stop.prevent="toggleSortBy('basename')">
-                               <!-- Icon or preview -->
-                               <span class="files-list__row-icon" />
-
-                               <!-- Name -->
-                               <FilesListHeaderButton :name="t('files', 'Name')" mode="basename" />
-                       </th>
-
-                       <!-- Actions -->
-                       <th class="files-list__row-actions" />
-
-                       <!-- Size -->
-                       <th v-if="isSizeAvailable"
-                               :class="{'files-list__column--sortable': isSizeAvailable}"
-                               class="files-list__column files-list__row-size">
-                               <FilesListHeaderButton :name="t('files', 'Size')" mode="size" />
-                       </th>
-
-                       <!-- Mtime -->
-                       <th v-if="isMtimeAvailable"
-                               :class="{'files-list__column--sortable': isMtimeAvailable}"
-                               class="files-list__column files-list__row-mtime">
-                               <FilesListHeaderButton :name="t('files', 'Modified')" mode="mtime" />
-                       </th>
-
-                       <!-- Custom views columns -->
-                       <th v-for="column in columns"
-                               :key="column.id"
-                               :class="classForColumn(column)">
-                               <FilesListHeaderButton v-if="!!column.sort" :name="column.title" :mode="column.id" />
-                               <span v-else>
-                                       {{ column.title }}
-                               </span>
-                       </th>
-               </template>
-       </tr>
+       <div v-show="enabled" :class="`files-list__header-${header.id}`">
+               <span ref="mount" />
+       </div>
 </template>
 
 <script lang="ts">
-import { translate } from '@nextcloud/l10n'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import Vue from 'vue'
-
-import { useFilesStore } from '../store/files.ts'
-import { useSelectionStore } from '../store/selection.ts'
-import FilesListHeaderActions from './FilesListHeaderActions.vue'
-import FilesListHeaderButton from './FilesListHeaderButton.vue'
-import filesSortingMixin from '../mixins/filesSorting.ts'
-import logger from '../logger.js'
-
-export default Vue.extend({
+/**
+ * This component is used to render custom
+ * elements provided by an API. Vue doesn't allow
+ * to directly render an HTMLElement, so we can do
+ * this magic here.
+ */
+export default {
        name: 'FilesListHeader',
-
-       components: {
-               FilesListHeaderButton,
-               NcCheckboxRadioSwitch,
-               FilesListHeaderActions,
-       },
-
-       mixins: [
-               filesSortingMixin,
-       ],
-
        props: {
-               isMtimeAvailable: {
-                       type: Boolean,
-                       default: false,
-               },
-               isSizeAvailable: {
-                       type: Boolean,
-                       default: false,
+               header: {
+                       type: Object,
+                       required: true,
                },
-               nodes: {
-                       type: Array,
+               currentFolder: {
+                       type: Object,
                        required: true,
                },
-               filesListWidth: {
-                       type: Number,
-                       default: 0,
+               currentView: {
+                       type: Object,
+                       required: true,
                },
        },
-
-       setup() {
-               const filesStore = useFilesStore()
-               const selectionStore = useSelectionStore()
-               return {
-                       filesStore,
-                       selectionStore,
-               }
-       },
-
        computed: {
-               currentView() {
-                       return this.$navigation.active
-               },
-
-               columns() {
-                       // Hide columns if the list is too small
-                       if (this.filesListWidth < 512) {
-                               return []
-                       }
-                       return this.currentView?.columns || []
-               },
-
-               dir() {
-                       // Remove any trailing slash but leave root slash
-                       return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
-               },
-
-               selectAllBind() {
-                       const label = this.isNoneSelected || this.isSomeSelected
-                               ? this.t('files', 'Select all')
-                               : this.t('files', 'Unselect all')
-                       return {
-                               'aria-label': label,
-                               checked: this.isAllSelected,
-                               indeterminate: this.isSomeSelected,
-                               title: label,
-                       }
-               },
-
-               selectedNodes() {
-                       return this.selectionStore.selected
-               },
-
-               isAllSelected() {
-                       return this.selectedNodes.length === this.nodes.length
-               },
-
-               isNoneSelected() {
-                       return this.selectedNodes.length === 0
-               },
-
-               isSomeSelected() {
-                       return !this.isAllSelected && !this.isNoneSelected
+               enabled() {
+                       console.debug('Enabled', this.header.id)
+                       return this.header.enabled(this.currentFolder, this.currentView)
                },
        },
-
-       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,
-                       }
-               },
-
-               onToggleAll(selected) {
-                       if (selected) {
-                               const selection = this.nodes.map(node => node.fileid.toString())
-                               logger.debug('Added all nodes to selection', { selection })
-                               this.selectionStore.setLastIndex(null)
-                               this.selectionStore.set(selection)
-                       } else {
-                               logger.debug('Cleared selection')
-                               this.selectionStore.reset()
+       watch: {
+               enabled(enabled) {
+                       if (!enabled) {
+                               return
                        }
+                       this.header.updated(this.currentFolder, this.currentView)
                },
-
-               t: translate,
        },
-})
-</script>
-
-<style scoped lang="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;
-       }
+       mounted() {
+               console.debug('Mounted', this.header.id)
+               this.header.render(this.$refs.mount, this.currentFolder, this.currentView)
+       },
 }
-
-</style>
+</script>
diff --git a/apps/files/src/components/FilesListHeaderActions.vue b/apps/files/src/components/FilesListHeaderActions.vue
deleted file mode 100644 (file)
index e419c8e..0000000
+++ /dev/null
@@ -1,226 +0,0 @@
-<!--
-  - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
-  -
-  - @author John Molakvoæ <skjnldsv@protonmail.com>
-  -
-  - @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>
-       <th class="files-list__column files-list__row-actions-batch" colspan="2">
-               <NcActions ref="actionsMenu"
-                       :disabled="!!loading || areSomeNodesLoading"
-                       :force-name="true"
-                       :inline="inlineActions"
-                       :menu-name="inlineActions <= 1 ? t('files', 'Actions') : null"
-                       :open.sync="openedMenu">
-                       <NcActionButton v-for="action in enabledActions"
-                               :key="action.id"
-                               :class="'files-list__row-actions-batch-' + action.id"
-                               @click="onActionClick(action)">
-                               <template #icon>
-                                       <NcLoadingIcon v-if="loading === action.id" :size="18" />
-                                       <CustomSvgIconRender v-else :svg="action.iconSvgInline(nodes, currentView)" />
-                               </template>
-                               {{ action.displayName(nodes, currentView) }}
-                       </NcActionButton>
-               </NcActions>
-       </th>
-</template>
-
-<script lang="ts">
-import { showError, showSuccess } from '@nextcloud/dialogs'
-import { translate } from '@nextcloud/l10n'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-import Vue from 'vue'
-
-import { getFileActions } from '../services/FileAction.ts'
-import { useActionsMenuStore } from '../store/actionsmenu.ts'
-import { useFilesStore } from '../store/files.ts'
-import { useSelectionStore } from '../store/selection.ts'
-import filesListWidthMixin from '../mixins/filesListWidth.ts'
-import CustomSvgIconRender from './CustomSvgIconRender.vue'
-import logger from '../logger.js'
-
-// The registered actions list
-const actions = getFileActions()
-
-export default Vue.extend({
-       name: 'FilesListHeaderActions',
-
-       components: {
-               CustomSvgIconRender,
-               NcActions,
-               NcActionButton,
-               NcLoadingIcon,
-       },
-
-       mixins: [
-               filesListWidthMixin,
-       ],
-
-       props: {
-               currentView: {
-                       type: Object,
-                       required: true,
-               },
-               selectedNodes: {
-                       type: Array,
-                       default: () => ([]),
-               },
-       },
-
-       setup() {
-               const actionsMenuStore = useActionsMenuStore()
-               const filesStore = useFilesStore()
-               const selectionStore = useSelectionStore()
-               return {
-                       actionsMenuStore,
-                       filesStore,
-                       selectionStore,
-               }
-       },
-
-       data() {
-               return {
-                       loading: null,
-               }
-       },
-
-       computed: {
-               dir() {
-                       // Remove any trailing slash but leave root slash
-                       return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
-               },
-               enabledActions() {
-                       return actions
-                               .filter(action => action.execBatch)
-                               .filter(action => !action.enabled || action.enabled(this.nodes, this.currentView))
-                               .sort((a, b) => (a.order || 0) - (b.order || 0))
-               },
-
-               nodes() {
-                       return this.selectedNodes
-                               .map(fileid => this.getNode(fileid))
-                               .filter(node => node)
-               },
-
-               areSomeNodesLoading() {
-                       return this.nodes.some(node => node._loading)
-               },
-
-               openedMenu: {
-                       get() {
-                               return this.actionsMenuStore.opened === 'global'
-                       },
-                       set(opened) {
-                               this.actionsMenuStore.opened = opened ? 'global' : null
-                       },
-               },
-
-               inlineActions() {
-                       if (this.filesListWidth < 512) {
-                               return 0
-                       }
-                       if (this.filesListWidth < 768) {
-                               return 1
-                       }
-                       if (this.filesListWidth < 1024) {
-                               return 2
-                       }
-                       return 3
-               },
-       },
-
-       methods: {
-               /**
-                * Get a cached note from the store
-                *
-                * @param {number} fileId the file id to get
-                * @return {Folder|File}
-                */
-               getNode(fileId) {
-                       return this.filesStore.getNode(fileId)
-               },
-
-               async onActionClick(action) {
-                       const displayName = action.displayName(this.nodes, this.currentView)
-                       const selectionIds = this.selectedNodes
-                       try {
-                               // Set loading markers
-                               this.loading = action.id
-                               this.nodes.forEach(node => {
-                                       Vue.set(node, '_loading', true)
-                               })
-
-                               // Dispatch action execution
-                               const results = await action.execBatch(this.nodes, this.currentView, this.dir)
-
-                               // Check if all actions returned null
-                               if (!results.some(result => result !== null)) {
-                                       // If the actions returned null, we stay silent
-                                       this.selectionStore.reset()
-                                       return
-                               }
-
-                               // Handle potential failures
-                               if (results.some(result => result === false)) {
-                                       // Remove the failed ids from the selection
-                                       const failedIds = selectionIds
-                                               .filter((fileid, index) => results[index] === false)
-                                       this.selectionStore.set(failedIds)
-
-                                       showError(this.t('files', '"{displayName}" failed on some elements ', { displayName }))
-                                       return
-                               }
-
-                               // Show success message and clear selection
-                               showSuccess(this.t('files', '"{displayName}" batch action executed successfully', { displayName }))
-                               this.selectionStore.reset()
-                       } catch (e) {
-                               logger.error('Error while executing action', { action, e })
-                               showError(this.t('files', '"{displayName}" action failed', { displayName }))
-                       } finally {
-                               // Remove loading markers
-                               this.loading = null
-                               this.nodes.forEach(node => {
-                                       Vue.set(node, '_loading', false)
-                               })
-                       }
-               },
-
-               t: translate,
-       },
-})
-</script>
-
-<style scoped lang="scss">
-.files-list__row-actions-batch {
-       flex: 1 1 100% !important;
-
-       // Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged
-       ::v-deep .button-vue__wrapper {
-               width: 100%;
-               span.button-vue__text {
-                       overflow: hidden;
-                       text-overflow: ellipsis;
-                       white-space: nowrap;
-               }
-       }
-}
-</style>
diff --git a/apps/files/src/components/FilesListHeaderButton.vue b/apps/files/src/components/FilesListHeaderButton.vue
deleted file mode 100644 (file)
index 9aac83a..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-<!--
-  - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
-  -
-  - @author John Molakvoæ <skjnldsv@protonmail.com>
-  -
-  - @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.stop.prevent="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 { 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 filesSortingMixin from '../mixins/filesSorting.ts'
-
-export default Vue.extend({
-       name: 'FilesListHeaderButton',
-
-       components: {
-               MenuDown,
-               MenuUp,
-               NcButton,
-       },
-
-       mixins: [
-               filesSortingMixin,
-       ],
-
-       props: {
-               name: {
-                       type: String,
-                       required: true,
-               },
-               mode: {
-                       type: String,
-                       required: 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,
-                       })
-               },
-
-               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
-               // Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged
-               width: 100%;
-       }
-
-       .button-vue__icon {
-               transition-timing-function: linear;
-               transition-duration: .1s;
-               transition-property: opacity;
-               opacity: 0;
-       }
-
-       // Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged
-       .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/FilesListTableFooter.vue b/apps/files/src/components/FilesListTableFooter.vue
new file mode 100644 (file)
index 0000000..4bda140
--- /dev/null
@@ -0,0 +1,175 @@
+<!--
+  - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @author John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @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>
+       <tr class="files-list__row-footer">
+               <th class="files-list__row-checkbox">
+                       <span class="hidden-visually">{{ t('files', 'Total rows summary') }}</span>
+               </th>
+
+               <!-- Link to file -->
+               <td class="files-list__row-name">
+                       <!-- Icon or preview -->
+                       <span class="files-list__row-icon" />
+
+                       <!-- Summary -->
+                       <span>{{ summary }}</span>
+               </td>
+
+               <!-- Actions -->
+               <td class="files-list__row-actions" />
+
+               <!-- Size -->
+               <td v-if="isSizeAvailable"
+                       class="files-list__column files-list__row-size">
+                       <span>{{ totalSize }}</span>
+               </td>
+
+               <!-- Mtime -->
+               <td v-if="isMtimeAvailable"
+                       class="files-list__column files-list__row-mtime" />
+
+               <!-- Custom views columns -->
+               <th v-for="column in columns"
+                       :key="column.id"
+                       :class="classForColumn(column)">
+                       <span>{{ column.summary?.(nodes, currentView) }}</span>
+               </th>
+       </tr>
+</template>
+
+<script lang="ts">
+import { formatFileSize } from '@nextcloud/files'
+import { translate } from '@nextcloud/l10n'
+import Vue from 'vue'
+
+import { useFilesStore } from '../store/files.ts'
+import { usePathsStore } from '../store/paths.ts'
+
+export default Vue.extend({
+       name: 'FilesListTableFooter',
+
+       components: {
+       },
+
+       props: {
+               isMtimeAvailable: {
+                       type: Boolean,
+                       default: false,
+               },
+               isSizeAvailable: {
+                       type: Boolean,
+                       default: false,
+               },
+               nodes: {
+                       type: Array,
+                       required: true,
+               },
+               summary: {
+                       type: String,
+                       default: '',
+               },
+               filesListWidth: {
+                       type: Number,
+                       default: 0,
+               },
+       },
+
+       setup() {
+               const pathsStore = usePathsStore()
+               const filesStore = useFilesStore()
+               return {
+                       filesStore,
+                       pathsStore,
+               }
+       },
+
+       computed: {
+               currentView() {
+                       return this.$navigation.active
+               },
+
+               dir() {
+                       // Remove any trailing slash but leave root slash
+                       return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
+               },
+
+               currentFolder() {
+                       if (!this.currentView?.id) {
+                               return
+                       }
+
+                       if (this.dir === '/') {
+                               return this.filesStore.getRoot(this.currentView.id)
+                       }
+                       const fileId = this.pathsStore.getPath(this.currentView.id, this.dir)
+                       return this.filesStore.getNode(fileId)
+               },
+
+               columns() {
+                       // Hide columns if the list is too small
+                       if (this.filesListWidth < 512) {
+                               return []
+                       }
+                       return this.currentView?.columns || []
+               },
+
+               totalSize() {
+                       // If we have the size already, let's use it
+                       if (this.currentFolder?.size) {
+                               return formatFileSize(this.currentFolder.size, true)
+                       }
+
+                       // Otherwise let's compute it
+                       return formatFileSize(this.nodes.reduce((total, node) => total + node.size || 0, 0), true)
+               },
+       },
+
+       methods: {
+               classForColumn(column) {
+                       return {
+                               'files-list__row-column-custom': true,
+                               [`files-list__row-${this.currentView.id}-${column.id}`]: true,
+                       }
+               },
+
+               t: translate,
+       },
+})
+</script>
+
+<style scoped lang="scss">
+// Scoped row
+tr {
+       padding-bottom: 300px;
+       border-top: 1px solid var(--color-border);
+       // Prevent hover effect on the whole row
+       background-color: transparent !important;
+       border-bottom: none !important;
+}
+
+td {
+       user-select: none;
+       // Make sure the cell colors don't apply to column headers
+       color: var(--color-text-maxcontrast) !important;
+}
+
+</style>
diff --git a/apps/files/src/components/FilesListTableHeader.vue b/apps/files/src/components/FilesListTableHeader.vue
new file mode 100644 (file)
index 0000000..52060d2
--- /dev/null
@@ -0,0 +1,213 @@
+<!--
+  - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @author John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @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>
+       <tr class="files-list__row-head">
+               <th class="files-list__column files-list__row-checkbox">
+                       <NcCheckboxRadioSwitch v-bind="selectAllBind" @update:checked="onToggleAll" />
+               </th>
+
+               <!-- Actions multiple if some are selected -->
+               <FilesListTableHeaderActions v-if="!isNoneSelected"
+                       :current-view="currentView"
+                       :selected-nodes="selectedNodes" />
+
+               <!-- Columns display -->
+               <template v-else>
+                       <!-- Link to file -->
+                       <th class="files-list__column files-list__row-name files-list__column--sortable"
+                               @click.stop.prevent="toggleSortBy('basename')">
+                               <!-- Icon or preview -->
+                               <span class="files-list__row-icon" />
+
+                               <!-- Name -->
+                               <FilesListTableHeaderButton :name="t('files', 'Name')" mode="basename" />
+                       </th>
+
+                       <!-- Actions -->
+                       <th class="files-list__row-actions" />
+
+                       <!-- Size -->
+                       <th v-if="isSizeAvailable"
+                               :class="{'files-list__column--sortable': isSizeAvailable}"
+                               class="files-list__column files-list__row-size">
+                               <FilesListTableHeaderButton :name="t('files', 'Size')" mode="size" />
+                       </th>
+
+                       <!-- Mtime -->
+                       <th v-if="isMtimeAvailable"
+                               :class="{'files-list__column--sortable': isMtimeAvailable}"
+                               class="files-list__column files-list__row-mtime">
+                               <FilesListTableHeaderButton :name="t('files', 'Modified')" mode="mtime" />
+                       </th>
+
+                       <!-- Custom views columns -->
+                       <th v-for="column in columns"
+                               :key="column.id"
+                               :class="classForColumn(column)">
+                               <FilesListTableHeaderButton v-if="!!column.sort" :name="column.title" :mode="column.id" />
+                               <span v-else>
+                                       {{ column.title }}
+                               </span>
+                       </th>
+               </template>
+       </tr>
+</template>
+
+<script lang="ts">
+import { translate } from '@nextcloud/l10n'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
+import Vue from 'vue'
+
+import { useFilesStore } from '../store/files.ts'
+import { useSelectionStore } from '../store/selection.ts'
+import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue'
+import FilesListTableHeaderButton from './FilesListTableHeaderButton.vue'
+import filesSortingMixin from '../mixins/filesSorting.ts'
+import logger from '../logger.js'
+
+export default Vue.extend({
+       name: 'FilesListTableHeader',
+
+       components: {
+               FilesListTableHeaderButton,
+               NcCheckboxRadioSwitch,
+               FilesListTableHeaderActions,
+       },
+
+       mixins: [
+               filesSortingMixin,
+       ],
+
+       props: {
+               isMtimeAvailable: {
+                       type: Boolean,
+                       default: false,
+               },
+               isSizeAvailable: {
+                       type: Boolean,
+                       default: false,
+               },
+               nodes: {
+                       type: Array,
+                       required: true,
+               },
+               filesListWidth: {
+                       type: Number,
+                       default: 0,
+               },
+       },
+
+       setup() {
+               const filesStore = useFilesStore()
+               const selectionStore = useSelectionStore()
+               return {
+                       filesStore,
+                       selectionStore,
+               }
+       },
+
+       computed: {
+               currentView() {
+                       return this.$navigation.active
+               },
+
+               columns() {
+                       // Hide columns if the list is too small
+                       if (this.filesListWidth < 512) {
+                               return []
+                       }
+                       return this.currentView?.columns || []
+               },
+
+               dir() {
+                       // Remove any trailing slash but leave root slash
+                       return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
+               },
+
+               selectAllBind() {
+                       const label = this.isNoneSelected || this.isSomeSelected
+                               ? this.t('files', 'Select all')
+                               : this.t('files', 'Unselect all')
+                       return {
+                               'aria-label': label,
+                               checked: this.isAllSelected,
+                               indeterminate: this.isSomeSelected,
+                               title: label,
+                       }
+               },
+
+               selectedNodes() {
+                       return this.selectionStore.selected
+               },
+
+               isAllSelected() {
+                       return this.selectedNodes.length === this.nodes.length
+               },
+
+               isNoneSelected() {
+                       return this.selectedNodes.length === 0
+               },
+
+               isSomeSelected() {
+                       return !this.isAllSelected && !this.isNoneSelected
+               },
+       },
+
+       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,
+                       }
+               },
+
+               onToggleAll(selected) {
+                       if (selected) {
+                               const selection = this.nodes.map(node => node.fileid.toString())
+                               logger.debug('Added all nodes to selection', { selection })
+                               this.selectionStore.setLastIndex(null)
+                               this.selectionStore.set(selection)
+                       } else {
+                               logger.debug('Cleared selection')
+                               this.selectionStore.reset()
+                       }
+               },
+
+               t: translate,
+       },
+})
+</script>
+
+<style scoped lang="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/FilesListTableHeaderActions.vue b/apps/files/src/components/FilesListTableHeaderActions.vue
new file mode 100644 (file)
index 0000000..f55487d
--- /dev/null
@@ -0,0 +1,226 @@
+<!--
+  - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @author John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @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>
+       <th class="files-list__column files-list__row-actions-batch" colspan="2">
+               <NcActions ref="actionsMenu"
+                       :disabled="!!loading || areSomeNodesLoading"
+                       :force-name="true"
+                       :inline="inlineActions"
+                       :menu-name="inlineActions <= 1 ? t('files', 'Actions') : null"
+                       :open.sync="openedMenu">
+                       <NcActionButton v-for="action in enabledActions"
+                               :key="action.id"
+                               :class="'files-list__row-actions-batch-' + action.id"
+                               @click="onActionClick(action)">
+                               <template #icon>
+                                       <NcLoadingIcon v-if="loading === action.id" :size="18" />
+                                       <CustomSvgIconRender v-else :svg="action.iconSvgInline(nodes, currentView)" />
+                               </template>
+                               {{ action.displayName(nodes, currentView) }}
+                       </NcActionButton>
+               </NcActions>
+       </th>
+</template>
+
+<script lang="ts">
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import { translate } from '@nextcloud/l10n'
+import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
+import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
+import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
+import Vue from 'vue'
+
+import { getFileActions } from '../services/FileAction.ts'
+import { useActionsMenuStore } from '../store/actionsmenu.ts'
+import { useFilesStore } from '../store/files.ts'
+import { useSelectionStore } from '../store/selection.ts'
+import filesListWidthMixin from '../mixins/filesListWidth.ts'
+import CustomSvgIconRender from './CustomSvgIconRender.vue'
+import logger from '../logger.js'
+
+// The registered actions list
+const actions = getFileActions()
+
+export default Vue.extend({
+       name: 'FilesListTableHeaderActions',
+
+       components: {
+               CustomSvgIconRender,
+               NcActions,
+               NcActionButton,
+               NcLoadingIcon,
+       },
+
+       mixins: [
+               filesListWidthMixin,
+       ],
+
+       props: {
+               currentView: {
+                       type: Object,
+                       required: true,
+               },
+               selectedNodes: {
+                       type: Array,
+                       default: () => ([]),
+               },
+       },
+
+       setup() {
+               const actionsMenuStore = useActionsMenuStore()
+               const filesStore = useFilesStore()
+               const selectionStore = useSelectionStore()
+               return {
+                       actionsMenuStore,
+                       filesStore,
+                       selectionStore,
+               }
+       },
+
+       data() {
+               return {
+                       loading: null,
+               }
+       },
+
+       computed: {
+               dir() {
+                       // Remove any trailing slash but leave root slash
+                       return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
+               },
+               enabledActions() {
+                       return actions
+                               .filter(action => action.execBatch)
+                               .filter(action => !action.enabled || action.enabled(this.nodes, this.currentView))
+                               .sort((a, b) => (a.order || 0) - (b.order || 0))
+               },
+
+               nodes() {
+                       return this.selectedNodes
+                               .map(fileid => this.getNode(fileid))
+                               .filter(node => node)
+               },
+
+               areSomeNodesLoading() {
+                       return this.nodes.some(node => node._loading)
+               },
+
+               openedMenu: {
+                       get() {
+                               return this.actionsMenuStore.opened === 'global'
+                       },
+                       set(opened) {
+                               this.actionsMenuStore.opened = opened ? 'global' : null
+                       },
+               },
+
+               inlineActions() {
+                       if (this.filesListWidth < 512) {
+                               return 0
+                       }
+                       if (this.filesListWidth < 768) {
+                               return 1
+                       }
+                       if (this.filesListWidth < 1024) {
+                               return 2
+                       }
+                       return 3
+               },
+       },
+
+       methods: {
+               /**
+                * Get a cached note from the store
+                *
+                * @param {number} fileId the file id to get
+                * @return {Folder|File}
+                */
+               getNode(fileId) {
+                       return this.filesStore.getNode(fileId)
+               },
+
+               async onActionClick(action) {
+                       const displayName = action.displayName(this.nodes, this.currentView)
+                       const selectionIds = this.selectedNodes
+                       try {
+                               // Set loading markers
+                               this.loading = action.id
+                               this.nodes.forEach(node => {
+                                       Vue.set(node, '_loading', true)
+                               })
+
+                               // Dispatch action execution
+                               const results = await action.execBatch(this.nodes, this.currentView, this.dir)
+
+                               // Check if all actions returned null
+                               if (!results.some(result => result !== null)) {
+                                       // If the actions returned null, we stay silent
+                                       this.selectionStore.reset()
+                                       return
+                               }
+
+                               // Handle potential failures
+                               if (results.some(result => result === false)) {
+                                       // Remove the failed ids from the selection
+                                       const failedIds = selectionIds
+                                               .filter((fileid, index) => results[index] === false)
+                                       this.selectionStore.set(failedIds)
+
+                                       showError(this.t('files', '"{displayName}" failed on some elements ', { displayName }))
+                                       return
+                               }
+
+                               // Show success message and clear selection
+                               showSuccess(this.t('files', '"{displayName}" batch action executed successfully', { displayName }))
+                               this.selectionStore.reset()
+                       } catch (e) {
+                               logger.error('Error while executing action', { action, e })
+                               showError(this.t('files', '"{displayName}" action failed', { displayName }))
+                       } finally {
+                               // Remove loading markers
+                               this.loading = null
+                               this.nodes.forEach(node => {
+                                       Vue.set(node, '_loading', false)
+                               })
+                       }
+               },
+
+               t: translate,
+       },
+})
+</script>
+
+<style scoped lang="scss">
+.files-list__row-actions-batch {
+       flex: 1 1 100% !important;
+
+       // Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged
+       ::v-deep .button-vue__wrapper {
+               width: 100%;
+               span.button-vue__text {
+                       overflow: hidden;
+                       text-overflow: ellipsis;
+                       white-space: nowrap;
+               }
+       }
+}
+</style>
diff --git a/apps/files/src/components/FilesListTableHeaderButton.vue b/apps/files/src/components/FilesListTableHeaderButton.vue
new file mode 100644 (file)
index 0000000..ebd1abb
--- /dev/null
@@ -0,0 +1,122 @@
+<!--
+  - @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @author John Molakvoæ <skjnldsv@protonmail.com>
+  -
+  - @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.stop.prevent="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 { 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 filesSortingMixin from '../mixins/filesSorting.ts'
+
+export default Vue.extend({
+       name: 'FilesListTableHeaderButton',
+
+       components: {
+               MenuDown,
+               MenuUp,
+               NcButton,
+       },
+
+       mixins: [
+               filesSortingMixin,
+       ],
+
+       props: {
+               name: {
+                       type: String,
+                       required: true,
+               },
+               mode: {
+                       type: String,
+                       required: 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,
+                       })
+               },
+
+               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
+               // Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged
+               width: 100%;
+       }
+
+       .button-vue__icon {
+               transition-timing-function: linear;
+               transition-duration: .1s;
+               transition-property: opacity;
+               opacity: 0;
+       }
+
+       // Remove when https://github.com/nextcloud/nextcloud-vue/pull/3936 is merged
+       .button-vue__text {
+               overflow: hidden;
+               white-space: nowrap;
+               text-overflow: ellipsis;
+       }
+
+       &--active,
+       &:hover,
+       &:focus,
+       &:active {
+               .button-vue__icon {
+                       opacity: 1 !important;
+               }
+       }
+}
+</style>
index 014f0a89f00a7aa37ab7debe3f844ef446a4d217..69cab260963cee38dcb54beafe66f08528ea733e 100644 (file)
   -
   -->
 <template>
-       <RecycleScroller ref="recycleScroller"
-               class="files-list"
-               key-field="source"
-               :items="nodes"
-               :item-size="55"
-               :table-mode="true"
-               item-class="files-list__row"
-               item-tag="tr"
-               list-class="files-list__body"
-               list-tag="tbody"
-               role="table">
-               <template #default="{ item, active, index }">
-                       <!-- File row -->
-                       <FileEntry :active="active"
-                               :index="index"
-                               :is-mtime-available="isMtimeAvailable"
-                               :is-size-available="isSizeAvailable"
-                               :files-list-width="filesListWidth"
-                               :nodes="nodes"
-                               :source="item" />
-               </template>
-
+       <VirtualList :data-component="FileEntry"
+               :data-key="'source'"
+               :data-sources="nodes"
+               :item-height="56"
+               :extra-props="{
+                       isMtimeAvailable,
+                       isSizeAvailable,
+                       nodes,
+                       filesListWidth,
+               }"
+               :scroll-to-index="scrollToIndex">
+               <!-- Accessibility description and headers -->
                <template #before>
                        <!-- Accessibility description -->
                        <caption class="hidden-visually">
                                {{ t('files', 'This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list.') }}
                        </caption>
 
-                       <!-- Thead-->
-                       <FilesListHeader :files-list-width="filesListWidth"
+                       <!-- Headers -->
+                       <FilesListHeader v-for="header in sortedHeaders"
+                               :key="header.id"
+                               :current-folder="currentFolder"
+                               :current-view="currentView"
+                               :header="header" />
+               </template>
+
+               <!-- Thead-->
+               <template #header>
+                       <FilesListTableHeader :files-list-width="filesListWidth"
                                :is-mtime-available="isMtimeAvailable"
                                :is-size-available="isSizeAvailable"
                                :nodes="nodes" />
                </template>
 
-               <template #after>
-                       <!-- Tfoot-->
-                       <FilesListFooter :files-list-width="filesListWidth"
+               <!-- Tfoot-->
+               <template #footer>
+                       <FilesListTableFooter :files-list-width="filesListWidth"
                                :is-mtime-available="isMtimeAvailable"
                                :is-size-available="isSizeAvailable"
                                :nodes="nodes"
                                :summary="summary" />
                </template>
-       </RecycleScroller>
+       </VirtualList>
 </template>
 
 <script lang="ts">
-import { RecycleScroller } from 'vue-virtual-scroller'
 import { translate, translatePlural } from '@nextcloud/l10n'
+import { getFileListHeaders } from '@nextcloud/files'
 import Vue from 'vue'
+import VirtualList from './VirtualList.vue'
 
 import FileEntry from './FileEntry.vue'
-import FilesListFooter from './FilesListFooter.vue'
 import FilesListHeader from './FilesListHeader.vue'
+import FilesListTableFooter from './FilesListTableFooter.vue'
+import FilesListTableHeader from './FilesListTableHeader.vue'
 import filesListWidthMixin from '../mixins/filesListWidth.ts'
+import { showError } from '@nextcloud/dialogs'
 
 export default Vue.extend({
        name: 'FilesListVirtual',
 
        components: {
-               RecycleScroller,
-               FileEntry,
                FilesListHeader,
-               FilesListFooter,
+               FilesListTableHeader,
+               FilesListTableFooter,
+               VirtualList,
        },
 
        mixins: [
@@ -96,6 +98,10 @@ export default Vue.extend({
                        type: Object,
                        required: true,
                },
+               currentFolder: {
+                       type: Object,
+                       required: true,
+               },
                nodes: {
                        type: Array,
                        required: true,
@@ -105,6 +111,7 @@ export default Vue.extend({
        data() {
                return {
                        FileEntry,
+                       headers: getFileListHeaders(),
                }
        },
 
@@ -113,6 +120,21 @@ export default Vue.extend({
                        return this.nodes.filter(node => node.type === 'file')
                },
 
+               fileId() {
+                       return parseInt(this.$route.params.fileid || this.$route.query.fileid) || null
+               },
+
+               scrollToIndex() {
+                       if (!this.fileId) {
+                               return
+                       }
+                       const index = this.nodes.findIndex(node => node.fileid === this.fileId)
+                       if (index === -1) {
+                               showError(this.t('files', 'File not found'))
+                       }
+                       return Math.max(0, index)
+               },
+
                summaryFile() {
                        const count = this.files.length
                        return translatePlural('files', '{count} file', '{count} files', count, { count })
@@ -138,13 +160,14 @@ export default Vue.extend({
                        }
                        return this.nodes.some(node => node.attributes.size !== undefined)
                },
-       },
 
-       mounted() {
-               // Make the root recycle scroller a table for proper semantics
-               const slots = this.$el.querySelectorAll('.vue-recycle-scroller__slot')
-               slots[0].setAttribute('role', 'thead')
-               slots[1].setAttribute('role', 'tfoot')
+               sortedHeaders() {
+                       if (!this.currentFolder || !this.currentView) {
+                               return []
+                       }
+
+                       return [...this.headers].sort((a, b) => a.order - b.order)
+               },
        },
 
        methods: {
@@ -173,7 +196,7 @@ export default Vue.extend({
 
        &::v-deep {
                // Table head, body and footer
-               tbody, .vue-recycle-scroller__slot {
+               tbody {
                        display: flex;
                        flex-direction: column;
                        width: 100%;
@@ -181,23 +204,35 @@ export default Vue.extend({
                        position: relative;
                }
 
+               // Before table and thead
+               .files-list__before {
+                       display: flex;
+                       flex-direction: column;
+               }
+
                // Table header
-               .vue-recycle-scroller__slot[role='thead'] {
+               .files-list__thead {
                        // Pinned on top when scrolling
                        position: sticky;
                        z-index: 10;
                        top: 0;
-                       height: var(--row-height);
+               }
+
+               .files-list__thead,
+               .files-list__tfoot {
+                       display: flex;
+                       width: 100%;
                        background-color: var(--color-main-background);
+
                }
 
                tr {
-                       position: absolute;
+                       position: relative;
                        display: flex;
                        align-items: center;
                        width: 100%;
-                       border-bottom: 1px solid var(--color-border);
                        user-select: none;
+                       border-bottom: 1px solid var(--color-border);
                }
 
                td, th {
index d38d4d2fd9e4be08b7335196fd20ff8c6ba17be9..02d473e7def8497f7ccae868b89e352a5808a355 100644 (file)
@@ -134,7 +134,7 @@ export default {
 // User storage stats display
 .app-navigation-entry__settings-quota {
        // Align title with progress and icon
-       &--not-unlimited::v-deep .app-navigation-entry__title {
+       &--not-unlimited::v-deep .app-navigation-entry__name {
                margin-top: -4px;
        }
 
diff --git a/apps/files/src/legacy/navigationMapper.js b/apps/files/src/legacy/navigationMapper.js
deleted file mode 100644 (file)
index 764a7cb..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
- */
-
-import { loadState } from '@nextcloud/initial-state'
-import logger from '../logger.js'
-
-/**
- * Fetch and register the legacy files views
- */
-export default function() {
-       const legacyViews = Object.values(loadState('files', 'navigation', {}))
-
-       if (legacyViews.length > 0) {
-               logger.debug('Legacy files views detected. Processing...', legacyViews)
-               legacyViews.forEach(view => {
-                       registerLegacyView(view)
-                       if (view.sublist) {
-                               view.sublist.forEach(subview => registerLegacyView({ ...subview, parent: view.id }))
-                       }
-               })
-       }
-}
-
-const registerLegacyView = function({ id, name, order, icon, parent, classes = '', expanded, params }) {
-       OCP.Files.Navigation.register({
-               id,
-               name,
-               order,
-               params,
-               parent,
-               expanded: expanded === true,
-               iconClass: icon ? `icon-${icon}` : 'nav-icon-' + id,
-               legacy: true,
-               sticky: classes.includes('pinned'),
-       })
-}
index 2fbae13693a06bc38e0cb53586ee14be83d1f59d..d0fb3922229138b0e129838c7e5206b06c7e861f 100644 (file)
@@ -15,14 +15,13 @@ import Vue from 'vue'
 import { createPinia, PiniaVuePlugin } from 'pinia'
 
 import FilesListView from './views/FilesList.vue'
-import NavigationService from './services/Navigation'
+import { NavigationService } from './services/Navigation'
 import NavigationView from './views/Navigation.vue'
-import processLegacyFilesViews from './legacy/navigationMapper.js'
 import registerFavoritesView from './views/favorites'
 import registerRecentView from './views/recent'
 import registerFilesView from './views/files'
 import registerPreviewServiceWorker from './services/ServiceWorker.js'
-import router from './router/router.js'
+import router from './router/router'
 import RouterService from './services/RouterService'
 import SettingsModel from './models/Setting.js'
 import SettingsService from './services/Settings.js'
@@ -79,7 +78,6 @@ const FilesList = new ListView({
 FilesList.$mount('#app-content-vue')
 
 // Init legacy and new files views
-processLegacyFilesViews()
 registerFavoritesView()
 registerFilesView()
 registerRecentView()
index 2f79a3eb171b43a5226e0fad5fd5c44a13623ccd..e766ea631f301053363c0080361b8b77f1dbaf3d 100644 (file)
@@ -23,14 +23,14 @@ import Vue from 'vue'
 
 import { mapState } from 'pinia'
 import { useViewConfigStore } from '../store/viewConfig'
-import type { Navigation } from '../services/Navigation'
+import type { NavigationService, Navigation } from '../services/Navigation'
 
 export default Vue.extend({
        computed: {
                ...mapState(useViewConfigStore, ['getConfig', 'setSortingBy', 'toggleSortingDirection']),
 
                currentView(): Navigation {
-                       return this.$navigation.active
+                       return (this.$navigation as NavigationService).active as Navigation
                },
 
                /**
diff --git a/apps/files/src/router/router.js b/apps/files/src/router/router.js
deleted file mode 100644 (file)
index 0d833cd..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * 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/>.
- *
- */
-import Vue from 'vue'
-import Router from 'vue-router'
-import { generateUrl } from '@nextcloud/router'
-import queryString from 'query-string'
-
-Vue.use(Router)
-
-const router = new Router({
-       mode: 'history',
-
-       // if index.php is in the url AND we got this far, then it's working:
-       // let's keep using index.php in the url
-       base: generateUrl('/apps/files', ''),
-       linkActiveClass: 'active',
-
-       routes: [
-               {
-                       path: '/',
-                       // Pretending we're using the default view
-                       alias: '/files',
-               },
-               {
-                       path: '/:view/:fileid?',
-                       name: 'filelist',
-                       props: true,
-               },
-       ],
-
-       // Custom stringifyQuery to prevent encoding of slashes in the url
-       stringifyQuery(query) {
-               const result = queryString.stringify(query).replace(/%2F/gmi, '/')
-               return result ? ('?' + result) : ''
-       },
-})
-
-export default router
diff --git a/apps/files/src/router/router.ts b/apps/files/src/router/router.ts
new file mode 100644 (file)
index 0000000..6ba8bec
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * 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/>.
+ *
+ */
+import { generateUrl } from '@nextcloud/router'
+import queryString from 'query-string'
+import Router from 'vue-router'
+import Vue from 'vue'
+
+Vue.use(Router)
+
+const router = new Router({
+       mode: 'history',
+
+       // if index.php is in the url AND we got this far, then it's working:
+       // let's keep using index.php in the url
+       base: generateUrl('/apps/files'),
+       linkActiveClass: 'active',
+
+       routes: [
+               {
+                       path: '/',
+                       // Pretending we're using the default view
+                       alias: '/files',
+               },
+               {
+                       path: '/:view/:fileid?',
+                       name: 'filelist',
+                       props: true,
+               },
+       ],
+
+       // Custom stringifyQuery to prevent encoding of slashes in the url
+       stringifyQuery(query) {
+               const result = queryString.stringify(query).replace(/%2F/gmi, '/')
+               return result ? ('?' + result) : ''
+       },
+})
+
+export default router
index 56d3ba0b97d04653df67c0e5ee09454f6268cbef..8f8212783ca48745b99e2690b2384dd8f6b10e4e 100644 (file)
@@ -96,22 +96,9 @@ export interface Navigation {
         * 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
-        */
-       legacy?: boolean
-
-       /**
-        * An icon class.
-        * @deprecated It will be removed in a near future
-        */
-       iconClass?: string
 }
 
-export default class {
+export class NavigationService {
 
        private _views: Navigation[] = []
        private _currentView: Navigation | null = null
@@ -131,14 +118,6 @@ export default class {
                        throw e
                }
 
-               if (view.legacy) {
-                       logger.warn('Legacy view detected, please migrate to Vue')
-               }
-
-               if (view.iconClass) {
-                       view.legacy = true
-               }
-
                this._views.push(view)
        }
 
@@ -192,18 +171,12 @@ const isValidNavigation = function(view: Navigation): boolean {
                throw new Error('Navigation caption is required for top-level views and must be a string')
        }
 
-       /**
-        * Legacy handle their content and icon differently
-        * TODO: remove when support for legacy views is removed
-        */
-       if (!view.legacy) {
-               if (!view.getContents || typeof view.getContents !== 'function') {
-                       throw new Error('Navigation getContents is required and must be a function')
-               }
+       if (!view.getContents || typeof view.getContents !== 'function') {
+               throw new Error('Navigation getContents is required and must be a function')
+       }
 
-               if (!view.icon || typeof view.icon !== 'string' || !isSvg(view.icon)) {
-                       throw new Error('Navigation icon is required and must be a valid svg string')
-               }
+       if (!view.icon || typeof view.icon !== 'string' || !isSvg(view.icon)) {
+               throw new Error('Navigation icon is required and must be a valid svg string')
        }
 
        if (!('order' in view) || typeof view.order !== 'number') {
index 1a7ca5769aabc3bdfb8b150625bb10ccf607a341..ae93642f9854864218a0fc1ac04337b07305e41a 100644 (file)
@@ -20,9 +20,7 @@
   -
   -->
 <template>
-       <NcAppContent v-show="!currentView?.legacy"
-               :class="{'app-content--hidden': currentView?.legacy}"
-               data-cy-files-content>
+       <NcAppContent data-cy-files-content>
                <div class="files-list__header">
                        <!-- Current folder breadcrumbs -->
                        <BreadCrumbs :path="dir" @reload="fetchContent" />
                <!-- File list -->
                <FilesListVirtual v-else
                        ref="filesListVirtual"
+                       :current-folder="currentFolder"
                        :current-view="currentView"
                        :nodes="dirContents" />
        </NcAppContent>
 </template>
 
 <script lang="ts">
-import { Folder, File, Node } from '@nextcloud/files'
+import type { Route } from 'vue-router'
+import type { Navigation, ContentsWithRoot } from '../services/Navigation.ts'
+import type { UserConfig } from '../types.ts'
+
+import { Folder, Node } from '@nextcloud/files'
 import { join } from 'path'
 import { 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'
 import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
+import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
 import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
 import Vue from 'vue'
 
@@ -83,8 +87,6 @@ import BreadCrumbs from '../components/BreadCrumbs.vue'
 import FilesListVirtual from '../components/FilesListVirtual.vue'
 import filesSortingMixin from '../mixins/filesSorting.ts'
 import logger from '../logger.js'
-import Navigation, { ContentsWithRoot } from '../services/Navigation.ts'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
 
 export default Vue.extend({
        name: 'FilesList',
@@ -126,32 +128,27 @@ export default Vue.extend({
        },
 
        computed: {
-               userConfig() {
+               userConfig(): UserConfig {
                        return this.userConfigStore.userConfig
                },
 
-               /** @return {Navigation} */
-               currentView() {
-                       return this.$navigation.active
-                               || this.$navigation.views.find(view => view.id === 'files')
+               currentView(): Navigation {
+                       return (this.$navigation.active
+                               || this.$navigation.views.find(view => view.id === 'files')) as Navigation
                },
 
                /**
                 * The current directory query.
-                *
-                * @return {string}
                 */
-               dir() {
+               dir(): string {
                        // Remove any trailing slash but leave root slash
                        return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
                },
 
                /**
                 * The current folder.
-                *
-                * @return {Folder|undefined}
                 */
-               currentFolder() {
+               currentFolder(): Folder|undefined {
                        if (!this.currentView?.id) {
                                return
                        }
@@ -165,10 +162,8 @@ export default Vue.extend({
 
                /**
                 * The current directory contents.
-                *
-                * @return {Node[]}
                 */
-               dirContents() {
+               dirContents(): Node[] {
                        if (!this.currentView) {
                                return []
                        }
@@ -207,7 +202,7 @@ export default Vue.extend({
                /**
                 * The current directory is empty.
                 */
-               isEmptyDir() {
+               isEmptyDir(): boolean {
                        return this.dirContents.length === 0
                },
 
@@ -216,7 +211,7 @@ export default Vue.extend({
                 * But we already have a cached version of it
                 * that is not empty.
                 */
-               isRefreshing() {
+               isRefreshing(): boolean {
                        return this.currentFolder !== undefined
                                && !this.isEmptyDir
                                && this.loading
@@ -225,7 +220,7 @@ export default Vue.extend({
                /**
                 * Route to the previous directory.
                 */
-               toPreviousDir() {
+               toPreviousDir(): Route {
                        const dir = this.dir.split('/').slice(0, -1).join('/') || '/'
                        return { ...this.$route, query: { dir } }
                },
@@ -257,10 +252,6 @@ export default Vue.extend({
 
        methods: {
                async fetchContent() {
-                       if (this.currentView?.legacy) {
-                               return
-                       }
-
                        this.loading = true
                        const dir = this.dir
                        const currentView = this.currentView
@@ -272,8 +263,7 @@ export default Vue.extend({
                        }
 
                        // Fetch the current dir contents
-                       /** @type {Promise<ContentsWithRoot>} */
-                       this.promise = currentView.getContents(dir)
+                       this.promise = currentView.getContents(dir) as Promise<ContentsWithRoot>
                        try {
                                const { folder, contents } = await this.promise
                                logger.debug('Fetched contents', { dir, folder, contents })
@@ -333,12 +323,6 @@ export default Vue.extend({
        overflow: hidden;
        flex-direction: column;
        max-height: 100%;
-
-       // TODO: remove after all legacy views are migrated
-       // Hides the legacy app-content if shown view is not legacy
-       &:not(&--hidden)::v-deep + #app-content {
-               display: none;
-       }
 }
 
 $margin: 4px;
index 8678465841a853927276e13146134a3a4509d0f7..a65016da2974ed888c18b6ba15fa2e50d4cbcaf5 100644 (file)
@@ -2,9 +2,9 @@ import FolderSvg from '@mdi/svg/svg/folder.svg'
 import ShareSvg from '@mdi/svg/svg/share-variant.svg'
 import { createTestingPinia } from '@pinia/testing'
 
-import NavigationService from '../services/Navigation'
+import { NavigationService } from '../services/Navigation'
 import NavigationView from './Navigation.vue'
-import router from '../router/router.js'
+import router from '../router/router'
 import { useViewConfigStore } from '../store/viewConfig'
 
 describe('Navigation renders', () => {
index 81ceac80a7f0635bfbd3a1ecc879a8f3ca706ee8..068db016ddb51985753d30738e74948bacd175bd 100644 (file)
@@ -72,7 +72,7 @@
        </NcAppNavigation>
 </template>
 
-<script>
+<script lang="ts">
 import { emit, subscribe } from '@nextcloud/event-bus'
 import { translate } from '@nextcloud/l10n'
 import Cog from 'vue-material-design-icons/Cog.vue'
@@ -83,7 +83,7 @@ import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js
 import { setPageHeading } from '../../../../core/src/OCP/accessibility.js'
 import { useViewConfigStore } from '../store/viewConfig.ts'
 import logger from '../logger.js'
-import Navigation from '../services/Navigation.ts'
+import type { NavigationService, Navigation } from '../services/Navigation.ts'
 import NavigationQuota from '../components/NavigationQuota.vue'
 import SettingsModal from './Settings.vue'
 
@@ -102,7 +102,7 @@ export default {
        props: {
                // eslint-disable-next-line vue/prop-name-casing
                Navigation: {
-                       type: Navigation,
+                       type: Object as Navigation,
                        required: true,
                },
        },
@@ -125,18 +125,15 @@ export default {
                        return this.$route?.params?.view || 'files'
                },
 
-               /** @return {Navigation} */
-               currentView() {
+               currentView(): Navigation {
                        return this.views.find(view => view.id === this.currentViewId)
                },
 
-               /** @return {Navigation[]} */
-               views() {
+               views(): Navigation[] {
                        return this.Navigation.views
                },
 
-               /** @return {Navigation[]} */
-               parentViews() {
+               parentViews(): Navigation[] {
                        return this.views
                                // filter child views
                                .filter(view => !view.parent)
@@ -146,8 +143,7 @@ export default {
                                })
                },
 
-               /** @return {Navigation[]} */
-               childViews() {
+               childViews(): Navigation[] {
                        return this.views
                                // filter parent views
                                .filter(view => !!view.parent)
@@ -165,13 +161,6 @@ export default {
 
        watch: {
                currentView(view, oldView) {
-                       // If undefined, it means we're initializing the view
-                       // This is handled by the legacy-view:initialized event
-                       // TODO: remove when legacy views are dropped
-                       if (view?.id === oldView?.id) {
-                               return
-                       }
-
                        this.Navigation.setActive(view)
                        logger.debug('Navigation changed', { id: view.id, view })
 
@@ -184,70 +173,22 @@ export default {
                        logger.debug('Navigation mounted. Showing requested view', { view: this.currentView })
                        this.showView(this.currentView)
                }
-
-               subscribe('files:legacy-navigation:changed', this.onLegacyNavigationChanged)
-
-               // TODO: remove this once the legacy navigation is gone
-               subscribe('files:legacy-view:initialized', () => {
-                       logger.debug('Legacy view initialized', { ...this.currentView })
-                       this.showView(this.currentView)
-               })
        },
 
        methods: {
-               /**
-                * @param {Navigation} view the new active view
-                * @param {Navigation} oldView the old active view
-                */
-               showView(view, oldView) {
+               showView(view: Navigation) {
                        // Closing any opened sidebar
                        window?.OCA?.Files?.Sidebar?.close?.()
-
-                       if (view?.legacy) {
-                               const newAppContent = document.querySelector('#app-content #app-content-' + this.currentView.id + '.viewcontainer')
-                               document.querySelectorAll('#app-content .viewcontainer').forEach(el => {
-                                       el.classList.add('hidden')
-                               })
-                               newAppContent.classList.remove('hidden')
-
-                               // Triggering legacy navigation events
-                               const { dir = '/' } = OC.Util.History.parseUrlQuery()
-                               const params = { itemId: view.id, dir }
-
-                               logger.debug('Triggering legacy navigation event', params)
-                               window.jQuery(newAppContent).trigger(new window.jQuery.Event('show', params))
-                               window.jQuery(newAppContent).trigger(new window.jQuery.Event('urlChanged', params))
-                       }
-
                        this.Navigation.setActive(view)
                        setPageHeading(view.name)
                        emit('files:navigation:changed', view)
                },
 
-               /**
-                * Coming from the legacy files app.
-                * TODO: remove when all views are migrated.
-                *
-                * @param {Navigation} view the new active view
-                */
-               onLegacyNavigationChanged({ id } = { id: 'files' }) {
-                       const view = this.Navigation.views.find(view => view.id === id)
-                       if (view && view.legacy && view.id !== this.currentView.id) {
-                               // Force update the current route as the request comes
-                               // from the legacy files app router
-                               this.$router.replace({ ...this.$route, params: { view: view.id } })
-                               this.Navigation.setActive(view)
-                               this.showView(view)
-                       }
-               },
-
                /**
                 * Expand/collapse a a view with children and permanently
                 * save this setting in the server.
-                *
-                * @param {Navigation} view the view to toggle
                 */
-               onToggleExpand(view) {
+               onToggleExpand(view: Navigation) {
                        // Invert state
                        const isExpanded = this.isExpanded(view)
                        // Update the view expanded state, might not be necessary
@@ -258,10 +199,8 @@ export default {
                /**
                 * Check if a view is expanded by user config
                 * or fallback to the default value.
-                *
-                * @param {Navigation} view the view to check
                 */
-               isExpanded(view) {
+               isExpanded(view: Navigation): boolean {
                        return typeof this.viewConfigStore.getConfig(view.id)?.expanded === 'boolean'
                                ? this.viewConfigStore.getConfig(view.id).expanded === true
                                : view.expanded === true
@@ -269,10 +208,8 @@ export default {
 
                /**
                 * Generate the route to a view
-                *
-                * @param {Navigation} view the view to toggle
                 */
-               generateToNavigation(view) {
+               generateToNavigation(view: Navigation) {
                        if (view.params) {
                                const { dir, fileid } = view.params
                                return { name: 'filelist', params: view.params, query: { dir, fileid } }
index 12581222f8e0c866555492f60320604b942372d4..b17aa93e63ed00c307fdfc39641963633e35251a 100644 (file)
@@ -396,13 +396,6 @@ export default {
                                                ${state ? '</d:set>' : '</d:remove>'}
                                                </d:propertyupdate>`,
                                })
-
-                               // TODO: Obliterate as soon as possible and use events with new files app
-                               // Terrible fallback for legacy files: toggle filelist as well
-                               if (OCA.Files && OCA.Files.App && OCA.Files.App.fileList && OCA.Files.App.fileList.fileActions) {
-                                       OCA.Files.App.fileList.fileActions.triggerAction('Favorite', OCA.Files.App.fileList.getModelForFile(this.fileInfo.name), OCA.Files.App.fileList)
-                               }
-
                        } catch (error) {
                                OC.Notification.showTemporary(t('files', 'Unable to change the favourite state of the file'))
                                console.error('Unable to change favourite state', error)
index d39def283a92827e23632f54a552750e3e41a6aa..b13401872443b6f6ea3edee62d339c575d7363d8 100644 (file)
@@ -27,7 +27,7 @@ import * as eventBus from '@nextcloud/event-bus'
 
 import { action } from '../actions/favoriteAction'
 import * as favoritesService from '../services/Favorites'
-import NavigationService from '../services/Navigation'
+import { NavigationService } from '../services/Navigation'
 import registerFavoritesView from './favorites'
 
 jest.mock('webdav/dist/node/request.js', () => ({
index 20baa2c582defcba6c02863014bdf4489b1b4f51..7485340a2fe727dfd84755f1fd00fe6830f14b67 100644 (file)
@@ -19,8 +19,7 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  *
  */
-import type { Navigation } from '../services/Navigation'
-import type NavigationService from '../services/Navigation'
+import type { Navigation, NavigationService } from '../services/Navigation'
 import { getLanguage, translate as t } from '@nextcloud/l10n'
 import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
 import StarSvg from '@mdi/svg/svg/star.svg?raw'
index 1a174f54e42c5c9e741c44d7d651f1f08da714db..baafc8572c25e8e9d9c3af6d4ff7ebbcf8cc9e19 100644 (file)
@@ -19,8 +19,7 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  *
  */
-import type NavigationService from '../services/Navigation'
-import type { Navigation } from '../services/Navigation'
+import type { NavigationService, Navigation } from '../services/Navigation'
 
 import { translate as t } from '@nextcloud/l10n'
 import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
index 5a0dbd91860d0497b166ba894445fed753f60ce0..3e0c51184e4da164eae8b00a8c09327082b01d8e 100644 (file)
@@ -19,8 +19,7 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  *
  */
-import type NavigationService from '../services/Navigation'
-import type { Navigation } from '../services/Navigation'
+import type { NavigationService, Navigation } from '../services/Navigation'
 
 import { translate as t } from '@nextcloud/l10n'
 import HistorySvg from '@mdi/svg/svg/history.svg?raw'
index c6f145bfe40f447c4f56e89278079574e07064cc..c974a37aa5bcdffbaba8f0afeadaf9d0d1f47ee6 100644 (file)
@@ -1,41 +1,9 @@
-<?php /** @var \OCP\IL10N $l */ ?>
-<?php $_['appNavigation']->printPage(); ?>
+<!-- File navigation -->
+<div id="app-navigation-files" role="navigation"></div>
 
-<!-- New files vue container -->
+<!-- File list vue container -->
 <div id="app-content-vue" class="hidden"></div>
 
-<div id="app-content" tabindex="0">
-
-       <input type="checkbox" class="hidden-visually" id="showgridview"
-               aria-label="<?php p($l->t('Toggle grid view'))?>"
-               <?php if ($_['showgridview']) { ?>checked="checked" <?php } ?>/>
-       <label id="view-toggle" for="showgridview" tabindex="0" class="button <?php p($_['showgridview'] ? 'icon-toggle-filelist' : 'icon-toggle-pictures') ?>"
-               title="<?php p($_['showgridview'] ? $l->t('Show list view') : $l->t('Show grid view'))?>"></label>
-
-
-       <!-- Legacy views -->
-       <?php foreach ($_['appContents'] as $content) { ?>
-       <div id="app-content-<?php p($content['id']) ?>" class="hidden viewcontainer">
-       <?php print_unescaped($content['content']) ?>
-       </div>
-       <?php } ?>
-       <div id="searchresults" class="hidden"></div>
-</div><!-- closing app-content -->
-
 <!-- config hints for javascript -->
 <input type="hidden" name="filesApp" id="filesApp" value="1" />
-<input type="hidden" name="usedSpacePercent" id="usedSpacePercent" value="<?php p($_['usedSpacePercent']); ?>" />
-<input type="hidden" name="owner" id="owner" value="<?php p($_['owner']); ?>" />
-<input type="hidden" name="ownerDisplayName" id="ownerDisplayName" value="<?php p($_['ownerDisplayName']); ?>" />
 <input type="hidden" name="fileNotFound" id="fileNotFound" value="<?php p($_['fileNotFound']); ?>" />
-<?php if (!$_['isPublic']) :?>
-<input type="hidden" name="allowShareWithLink" id="allowShareWithLink" value="<?php p($_['allowShareWithLink']) ?>" />
-<input type="hidden" name="defaultFileSorting" id="defaultFileSorting" value="<?php p($_['defaultFileSorting']) ?>" />
-<input type="hidden" name="defaultFileSortingDirection" id="defaultFileSortingDirection" value="<?php p($_['defaultFileSortingDirection']) ?>" />
-<input type="hidden" name="showHiddenFiles" id="showHiddenFiles" value="<?php p($_['showHiddenFiles']); ?>" />
-<input type="hidden" name="cropImagePreviews" id="cropImagePreviews" value="<?php p($_['cropImagePreviews']); ?>" />
-<?php endif;
-
-foreach ($_['hiddenFields'] as $name => $value) {?>
-<input type="hidden" name="<?php p($name) ?>" id="<?php p($name) ?>" value="<?php p($value) ?>" />
-<?php }
index e72cb8673d082c53954419a8a43602dca9594751..250ad51e38f878954dc6da5c1fd127ae2a9cca27 100644 (file)
@@ -19,8 +19,7 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  *
  */
-import type NavigationService from '../../files/src/services/Navigation'
-import type { Navigation } from '../../files/src/services/Navigation'
+import type { NavigationService, Navigation } from '../../files/src/services/Navigation'
 
 import { translate as t } from '@nextcloud/l10n'
 import { loadState } from '@nextcloud/initial-state'
index e5c7e6853c613436da3b33908653d38f8b5e6317..424d3680411932582d9f0fde3b3dc7acfa6d9776 100644 (file)
@@ -25,7 +25,7 @@ import axios from '@nextcloud/axios'
 
 import { type Navigation } from '../../../files/src/services/Navigation'
 import { type OCSResponse } from '../services/SharingService'
-import NavigationService from '../../../files/src/services/Navigation'
+import { NavigationService } from '../../../files/src/services/Navigation'
 import registerSharingViews from './shares'
 
 import '../main'
index ff37983813ef0c3fd877fdf2d9d76a7329603c94..74be9e7a503f8e555276c9abc98560f844735d60 100644 (file)
@@ -19,8 +19,7 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  *
  */
-import type NavigationService from '../../../files/src/services/Navigation'
-import type { Navigation } from '../../../files/src/services/Navigation'
+import type { NavigationService, Navigation } from '../../../files/src/services/Navigation'
 
 import { translate as t } from '@nextcloud/l10n'
 import AccountClockSvg from '@mdi/svg/svg/account-clock.svg?raw'
index eb235f34c8d751277a862c7d263c2baefcc3adac..cf5a95bb1d89d3ab9333133980c6f20a4b32cc90 100644 (file)
@@ -19,8 +19,7 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  *
  */
-import type NavigationService from '../../files/src/services/Navigation'
-import type { Navigation } from '../../files/src/services/Navigation'
+import type { NavigationService, Navigation } from '../../files/src/services/Navigation'
 
 import { translate as t, translate } from '@nextcloud/l10n'
 import DeleteSvg from '@mdi/svg/svg/delete.svg?raw'
index 72f2d6f09152cfa7704eb7ba90f8113de0bbf5db..261ba02c905e13981744ab9994234c0335f01b6d 100644 (file)
  */
 
 (function(OC) {
+       if (OC?.Files?.Client) {
+               _.extend(OC.Files.Client, {
+                       PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id',
+                       PROPERTY_CAN_ASSIGN: '{' + OC.Files.Client.NS_OWNCLOUD + '}can-assign',
+                       PROPERTY_DISPLAYNAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}display-name',
+                       PROPERTY_USERVISIBLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-visible',
+                       PROPERTY_USERASSIGNABLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-assignable',
+               })
 
-       _.extend(OC.Files.Client, {
-               PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id',
-               PROPERTY_CAN_ASSIGN: '{' + OC.Files.Client.NS_OWNCLOUD + '}can-assign',
-               PROPERTY_DISPLAYNAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}display-name',
-               PROPERTY_USERVISIBLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-visible',
-               PROPERTY_USERASSIGNABLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-assignable',
-       })
-
-       /**
-        * @class OCA.SystemTags.SystemTagsCollection
-        * @classdesc
-        *
-        * System tag
-        *
-        */
-       const SystemTagModel = OC.Backbone.Model.extend(
-               /** @lends OCA.SystemTags.SystemTagModel.prototype */ {
-                       sync: OC.Backbone.davSync,
+               /**
+                * @class OCA.SystemTags.SystemTagsCollection
+                * @classdesc
+                *
+                * System tag
+                *
+                */
+               const SystemTagModel = OC.Backbone.Model.extend(
+                       /** @lends OCA.SystemTags.SystemTagModel.prototype */ {
+                               sync: OC.Backbone.davSync,
 
-                       defaults: {
-                               userVisible: true,
-                               userAssignable: true,
-                               canAssign: true,
-                       },
+                               defaults: {
+                                       userVisible: true,
+                                       userAssignable: true,
+                                       canAssign: true,
+                               },
 
-                       davProperties: {
-                               id: OC.Files.Client.PROPERTY_FILEID,
-                               name: OC.Files.Client.PROPERTY_DISPLAYNAME,
-                               userVisible: OC.Files.Client.PROPERTY_USERVISIBLE,
-                               userAssignable: OC.Files.Client.PROPERTY_USERASSIGNABLE,
-                               // read-only, effective permissions computed by the server,
-                               canAssign: OC.Files.Client.PROPERTY_CAN_ASSIGN,
-                       },
+                               davProperties: {
+                                       id: OC.Files.Client.PROPERTY_FILEID,
+                                       name: OC.Files.Client.PROPERTY_DISPLAYNAME,
+                                       userVisible: OC.Files.Client.PROPERTY_USERVISIBLE,
+                                       userAssignable: OC.Files.Client.PROPERTY_USERASSIGNABLE,
+                                       // read-only, effective permissions computed by the server,
+                                       canAssign: OC.Files.Client.PROPERTY_CAN_ASSIGN,
+                               },
 
-                       parse(data) {
-                               return {
-                                       id: data.id,
-                                       name: data.name,
-                                       userVisible: data.userVisible === true || data.userVisible === 'true',
-                                       userAssignable: data.userAssignable === true || data.userAssignable === 'true',
-                                       canAssign: data.canAssign === true || data.canAssign === 'true',
-                               }
-                       },
-               })
+                               parse(data) {
+                                       return {
+                                               id: data.id,
+                                               name: data.name,
+                                               userVisible: data.userVisible === true || data.userVisible === 'true',
+                                               userAssignable: data.userAssignable === true || data.userAssignable === 'true',
+                                               canAssign: data.canAssign === true || data.canAssign === 'true',
+                                       }
+                               },
+                       })
 
-       OC.SystemTags = OC.SystemTags || {}
-       OC.SystemTags.SystemTagModel = SystemTagModel
+               OC.SystemTags = OC.SystemTags || {}
+               OC.SystemTags.SystemTagModel = SystemTagModel
+       }
 })(OC)
index 7740fbc67edeb3801ed469743a03f52b90db3378..6cc2c9e17c718ab9cb4793b9e3fbf198e182f74f 100644 (file)
@@ -19,7 +19,7 @@
         "@nextcloud/capabilities": "^1.0.4",
         "@nextcloud/dialogs": "^4.1.0",
         "@nextcloud/event-bus": "^3.1.0",
-        "@nextcloud/files": "^3.0.0-beta.13",
+        "@nextcloud/files": "^3.0.0-beta.14",
         "@nextcloud/initial-state": "^2.0.0",
         "@nextcloud/l10n": "^2.1.0",
         "@nextcloud/logger": "^2.5.0",
index 626ea8da367455b045a28491a99abbecc16fc7a9..f0b7d870e1ea5d51ce145bae1a8be89c939bfc21 100644 (file)
@@ -45,7 +45,7 @@
     "@nextcloud/capabilities": "^1.0.4",
     "@nextcloud/dialogs": "^4.1.0",
     "@nextcloud/event-bus": "^3.1.0",
-    "@nextcloud/files": "^3.0.0-beta.13",
+    "@nextcloud/files": "^3.0.0-beta.14",
     "@nextcloud/initial-state": "^2.0.0",
     "@nextcloud/l10n": "^2.1.0",
     "@nextcloud/logger": "^2.5.0",