mirror of
https://github.com/nextcloud/server.git
synced 2026-04-22 14:50:17 -04:00
chore(files): add Headers, remove legacy methods and cleanup
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
This commit is contained in:
parent
998b3a2581
commit
410f58e43e
30 changed files with 439 additions and 603 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
@ -20,194 +20,51 @@
|
|||
-
|
||||
-->
|
||||
<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,
|
||||
},
|
||||
nodes: {
|
||||
type: Array,
|
||||
header: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
filesListWidth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
currentFolder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
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,
|
||||
watch: {
|
||||
enabled(enabled) {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
this.header.updated(this.currentFolder, this.currentView)
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
mounted() {
|
||||
console.debug('Mounted', this.header.id)
|
||||
this.header.render(this.$refs.mount, this.currentFolder, this.currentView)
|
||||
},
|
||||
}
|
||||
|
||||
</style>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
-
|
||||
-->
|
||||
<template>
|
||||
<tr>
|
||||
<tr class="files-list__row-footer">
|
||||
<th class="files-list__row-checkbox">
|
||||
<span class="hidden-visually">{{ t('files', 'Total rows summary') }}</span>
|
||||
</th>
|
||||
|
|
@ -65,7 +65,7 @@ import { useFilesStore } from '../store/files.ts'
|
|||
import { usePathsStore } from '../store/paths.ts'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilesListFooter',
|
||||
name: 'FilesListTableFooter',
|
||||
|
||||
components: {
|
||||
},
|
||||
213
apps/files/src/components/FilesListTableHeader.vue
Normal file
213
apps/files/src/components/FilesListTableHeader.vue
Normal file
|
|
@ -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>
|
||||
|
|
@ -61,7 +61,7 @@ import logger from '../logger.js'
|
|||
const actions = getFileActions()
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilesListHeaderActions',
|
||||
name: 'FilesListTableHeaderActions',
|
||||
|
||||
components: {
|
||||
CustomSvgIconRender,
|
||||
|
|
@ -42,7 +42,7 @@ import Vue from 'vue'
|
|||
import filesSortingMixin from '../mixins/filesSorting.ts'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilesListHeaderButton',
|
||||
name: 'FilesListTableHeaderButton',
|
||||
|
||||
components: {
|
||||
MenuDown,
|
||||
|
|
@ -20,28 +20,18 @@
|
|||
-
|
||||
-->
|
||||
<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">
|
||||
|
|
@ -49,42 +39,54 @@
|
|||
{{ 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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
})
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@
|
|||
* 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'
|
||||
import Router from 'vue-router'
|
||||
import Vue from 'vue'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ const router = new Router({
|
|||
|
||||
// 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', ''),
|
||||
base: generateUrl('/apps/files'),
|
||||
linkActiveClass: 'active',
|
||||
|
||||
routes: [
|
||||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
@ -58,19 +56,25 @@
|
|||
<!-- 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;
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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', () => ({
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -24,52 +24,53 @@
|
|||
*/
|
||||
|
||||
(function(OC) {
|
||||
|
||||
_.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,
|
||||
|
||||
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,
|
||||
},
|
||||
|
||||
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',
|
||||
}
|
||||
},
|
||||
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',
|
||||
})
|
||||
|
||||
OC.SystemTags = OC.SystemTags || {}
|
||||
OC.SystemTags.SystemTagModel = SystemTagModel
|
||||
/**
|
||||
* @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,
|
||||
},
|
||||
|
||||
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',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
OC.SystemTags = OC.SystemTags || {}
|
||||
OC.SystemTags.SystemTagModel = SystemTagModel
|
||||
}
|
||||
})(OC)
|
||||
|
|
|
|||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue