}
}
- $nav = new \OCP\Template('files', 'appnavigation', '');
-
// Load the files we need
\OCP\Util::addStyle('files', 'merged');
\OCP\Util::addScript('files', 'merged-index', 'files');
$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 {
}
$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'] ?? []);
$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)) {
$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,
/**
* 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
+++ /dev/null
-<!--
- - @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>
-
-->
<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>
+++ /dev/null
-<!--
- - @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>
+++ /dev/null
-<!--
- - @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>
--- /dev/null
+<!--
+ - @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>
--- /dev/null
+<!--
+ - @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>
--- /dev/null
+<!--
+ - @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>
--- /dev/null
+<!--
+ - @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>
-
-->
<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: [
type: Object,
required: true,
},
+ currentFolder: {
+ type: Object,
+ required: true,
+ },
nodes: {
type: Array,
required: true,
data() {
return {
FileEntry,
+ headers: getFileListHeaders(),
}
},
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 })
}
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: {
&::v-deep {
// Table head, body and footer
- tbody, .vue-recycle-scroller__slot {
+ tbody {
display: flex;
flex-direction: column;
width: 100%;
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 {
// 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;
}
+++ /dev/null
-/**
- * @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'),
- })
-}
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'
FilesList.$mount('#app-content-vue')
// Init legacy and new files views
-processLegacyFilesViews()
registerFavoritesView()
registerFilesView()
registerRecentView()
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
},
/**
+++ /dev/null
-/**
- * @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
--- /dev/null
+/**
+ * @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
* 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
throw e
}
- if (view.legacy) {
- logger.warn('Legacy view detected, please migrate to Vue')
- }
-
- if (view.iconClass) {
- view.legacy = true
- }
-
this._views.push(view)
}
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') {
-
-->
<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'
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',
},
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
}
/**
* The current directory contents.
- *
- * @return {Node[]}
*/
- dirContents() {
+ dirContents(): Node[] {
if (!this.currentView) {
return []
}
/**
* The current directory is empty.
*/
- isEmptyDir() {
+ isEmptyDir(): boolean {
return this.dirContents.length === 0
},
* But we already have a cached version of it
* that is not empty.
*/
- isRefreshing() {
+ isRefreshing(): boolean {
return this.currentFolder !== undefined
&& !this.isEmptyDir
&& this.loading
/**
* Route to the previous directory.
*/
- toPreviousDir() {
+ toPreviousDir(): Route {
const dir = this.dir.split('/').slice(0, -1).join('/') || '/'
return { ...this.$route, query: { dir } }
},
methods: {
async fetchContent() {
- if (this.currentView?.legacy) {
- return
- }
-
this.loading = true
const dir = this.dir
const currentView = this.currentView
}
// 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 })
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;
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', () => {
</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'
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'
props: {
// eslint-disable-next-line vue/prop-name-casing
Navigation: {
- type: Navigation,
+ type: Object as Navigation,
required: true,
},
},
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)
})
},
- /** @return {Navigation[]} */
- childViews() {
+ childViews(): Navigation[] {
return this.views
// filter parent views
.filter(view => !!view.parent)
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 })
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
/**
* 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
/**
* 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 } }
${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)
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', () => ({
* 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'
* 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'
* 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'
-<?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 }
* 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'
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'
* 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'
* 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'
*/
(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)
"@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",
"@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",