Browse Source

feat(files_trashbin): migrate to vue

Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
tags/v27.0.0beta1
John Molakvoæ 1 year ago
parent
commit
29a7f7f6ef
No account linked to committer's email address
39 changed files with 1475 additions and 703 deletions
  1. 2
    3
      apps/files/js/app.js
  2. 58
    0
      apps/files/src/components/BreadCrumbs.vue
  3. 134
    0
      apps/files/src/components/FileEntry.vue
  4. 122
    0
      apps/files/src/components/FilesListHeader.vue
  5. 124
    0
      apps/files/src/components/FilesListVirtual.vue
  6. 15
    0
      apps/files/src/main.js
  7. 63
    0
      apps/files/src/mixins/fileslist-row.scss
  8. 23
    8
      apps/files/src/services/Navigation.ts
  9. 97
    0
      apps/files/src/store/files.ts
  10. 16
    0
      apps/files/src/store/index.ts
  11. 71
    0
      apps/files/src/store/paths.ts
  12. 51
    0
      apps/files/src/store/selection.ts
  13. 56
    0
      apps/files/src/types.ts
  14. 318
    0
      apps/files/src/views/FilesList.vue
  15. 28
    5
      apps/files/src/views/Navigation.vue
  16. 4
    6
      apps/files/templates/appnavigation.php
  17. 4
    2
      apps/files/templates/index.php
  18. 1
    0
      apps/files_trashbin/composer/composer/autoload_classmap.php
  19. 1
    0
      apps/files_trashbin/composer/composer/autoload_static.php
  20. 7
    12
      apps/files_trashbin/lib/AppInfo/Application.php
  21. 41
    0
      apps/files_trashbin/lib/Listeners/LoadAdditionalScripts.php
  22. 39
    0
      apps/files_trashbin/src/main.ts
  23. 33
    0
      apps/files_trashbin/src/services/client.ts
  24. 95
    0
      apps/files_trashbin/src/services/trashbin.ts
  25. 0
    22
      apps/files_trashbin/src/trash.scss
  26. 0
    70
      apps/files_trashbin/tests/js/appSpec.js
  27. 0
    397
      apps/files_trashbin/tests/js/filelistSpec.js
  28. 1
    0
      babel.config.js
  29. 0
    135
      core/src/OC/apps.js
  30. 14
    16
      core/src/OC/index.js
  31. 2
    0
      core/src/OC/util-history.js
  32. 0
    2
      core/src/main.js
  33. 10
    6
      custom.d.ts
  34. 34
    0
      cypress.d.ts
  35. 2
    14
      cypress/support/component.ts
  36. 5
    2
      package.json
  37. 2
    2
      tsconfig.json
  38. 1
    0
      webpack.common.js
  39. 1
    1
      webpack.modules.js

+ 2
- 3
apps/files/js/app.js View File

@@ -51,7 +51,6 @@
* Initializes the files app
*/
initialize: function() {
this.navigation = OCP.Files.Navigation;
this.$showHiddenFiles = $('input#showhiddenfilesToggle');
var showHidden = $('#showHiddenFiles').val() === "1";
this.$showHiddenFiles.prop('checked', showHidden);
@@ -135,8 +134,6 @@
OC.Plugins.attach('OCA.Files.App', this);

this._setupEvents();
// trigger URL change event handlers
this._onPopState({ ...OC.Util.History.parseUrlQuery(), view: this.navigation?.active?.id });

this._debouncedPersistShowHiddenFilesState = _.debounce(this._persistShowHiddenFilesState, 1200);
this._debouncedPersistCropImagePreviewsState = _.debounce(this._persistCropImagePreviewsState, 1200);
@@ -145,6 +142,8 @@
OCP.WhatsNew.query(); // for Nextcloud server
sessionStorage.setItem('WhatsNewServerCheck', Date.now());
}

window._nc_event_bus.emit('files:legacy-view:initialized', this);
},

/**

+ 58
- 0
apps/files/src/components/BreadCrumbs.vue View File

@@ -0,0 +1,58 @@
<template>
<NcBreadcrumbs data-cy-files-content-breadcrumbs>
<!-- Current path sections -->
<NcBreadcrumb v-for="section in sections"
:key="section.dir"
:aria-label="t('files', `Go to the '{dir}' directory`, section)"
v-bind="section" />
</NcBreadcrumbs>
</template>

<script>
import NcBreadcrumbs from '@nextcloud/vue/dist/Components/NcBreadcrumbs.js'
import NcBreadcrumb from '@nextcloud/vue/dist/Components/NcBreadcrumb.js'
import { basename } from 'path'

export default {
name: 'BreadCrumbs',

components: {
NcBreadcrumbs,
NcBreadcrumb,
},

props: {
path: {
type: String,
default: '/',
},
},

computed: {
dirs() {
const cumulativePath = (acc) => (value) => (acc += `${value}/`)
return ['/', ...this.path.split('/').filter(Boolean).map(cumulativePath('/'))]
},

sections() {
return this.dirs.map(dir => {
const to = { ...this.$route, query: { dir } }
return {
dir,
to,
title: basename(dir),
}
})
},
},
}
</script>

<style lang="scss" scoped>
.breadcrumb {
// Take as much space as possible
flex: 1 1 100% !important;
width: 100%;
}

</style>

+ 134
- 0
apps/files/src/components/FileEntry.vue View File

@@ -0,0 +1,134 @@
<!--
- @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
-
- @author Gary Kim <gary@garykim.dev>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<Fragment>
<td class="files-list__row-checkbox">
<NcCheckboxRadioSwitch :aria-label="t('files', 'Select the row for {displayName}', { displayName })"
:checked.sync="selectedFiles"
:value="fileid.toString()"
name="selectedFiles" />
</td>

<!-- Icon or preview -->
<td class="files-list__row-icon">
<FolderIcon v-if="source.type === 'folder'" />
</td>

<!-- Link to file and -->
<td class="files-list__row-name">
<a v-bind="linkTo">
{{ displayName }}
</a>
</td>
</Fragment>
</template>

<script lang="ts">
import { Folder, File } from '@nextcloud/files'
import { Fragment } from 'vue-fragment'
import { join } from 'path'
import { translate } from '@nextcloud/l10n'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import FolderIcon from 'vue-material-design-icons/Folder.vue'

import logger from '../logger'

export default {
name: 'FileEntry',

components: {
FolderIcon,
Fragment,
NcCheckboxRadioSwitch,
},

props: {
index: {
type: Number,
required: true,
},
source: {
type: [File, Folder],
required: true,
},
},

computed: {
dir() {
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
},

fileid() {
return this.source.attributes.fileid
},
displayName() {
return this.source.attributes.displayName
|| this.source.basename
},

linkTo() {
if (this.source.type === 'folder') {
const to = { ...this.$route, query: { dir: join(this.dir, this.source.basename) } }
return {
is: 'router-link',
title: this.t('files', 'Open folder {name}', { name: this.displayName }),
to,
}
}
return {
href: this.source.source,
// TODO: Use first action title ?
title: this.t('files', 'Download file {name}', { name: this.displayName }),
}
},

selectedFiles: {
get() {
return this.$store.state.selection.selected
},
set(selection) {
logger.debug('Added node to selection', { selection })
this.$store.dispatch('selection/set', selection)
},
},
},

methods: {
/**
* Get a cached note from the store
*
* @param {number} fileId the file id to get
* @return {Folder|File}
*/
getNode(fileId) {
return this.$store.getters['files/getNode'](fileId)
},

t: translate,
},
}
</script>

<style scoped lang="scss">
@import '../mixins/fileslist-row.scss'
</style>

+ 122
- 0
apps/files/src/components/FilesListHeader.vue View File

@@ -0,0 +1,122 @@
<!--
- @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
-
- @author Gary Kim <gary@garykim.dev>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<tr>
<th class="files-list__row-checkbox">
<NcCheckboxRadioSwitch v-bind="selectAllBind" @update:checked="onToggleAll" />
</th>

<!-- Icon or preview -->
<th class="files-list__row-icon" />

<!-- Link to file and -->
<th class="files-list__row-name">
{{ t('files', 'Name') }}
</th>
</tr>
</template>

<script lang="ts">
import { translate } from '@nextcloud/l10n'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'

import logger from '../logger'
import { File, Folder } from '@nextcloud/files'

export default {
name: 'FilesListHeader',

components: {
NcCheckboxRadioSwitch,
},

props: {
nodes: {
type: [File, Folder],
required: true,
},
},

computed: {
dir() {
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
},

selectAllBind() {
return {
ariaLabel: this.isNoneSelected || this.isSomeSelected
? this.t('files', 'Select all')
: this.t('files', 'Unselect all'),
checked: this.isAllSelected,
indeterminate: this.isSomeSelected,
}
},

isAllSelected() {
return this.selectedFiles.length === this.nodes.length
},

isNoneSelected() {
return this.selectedFiles.length === 0
},

isSomeSelected() {
return !this.isAllSelected && !this.isNoneSelected
},

selectedFiles() {
return this.$store.state.selection.selected
},
},

methods: {
/**
* Get a cached note from the store
*
* @param {number} fileId the file id to get
* @return {Folder|File}
*/
getNode(fileId) {
return this.$store.getters['files/getNode'](fileId)
},

onToggleAll(selected) {
if (selected) {
const selection = this.nodes.map(node => node.attributes.fileid.toString())
logger.debug('Added all nodes to selection', { selection })
this.$store.dispatch('selection/set', selection)
} else {
logger.debug('Cleared selection')
this.$store.dispatch('selection/reset')
}
},

t: translate,
},
}
</script>

<style scoped lang="scss">
@import '../mixins/fileslist-row.scss'

</style>

+ 124
- 0
apps/files/src/components/FilesListVirtual.vue View File

@@ -0,0 +1,124 @@
<!--
- @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
-
- @author Gary Kim <gary@garykim.dev>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<VirtualList class="files-list"
:data-component="FileEntry"
:data-key="getFileId"
:data-sources="nodes"
:estimate-size="55"
:table-mode="true"
item-class="files-list__row"
wrap-class="files-list__body">
<template #before>
<caption v-show="false" class="files-list__caption">
{{ summary }}
</caption>
</template>

<template #header>
<FilesListHeader :nodes="nodes" />
</template>
</VirtualList>
</template>

<script lang="ts">
import { Folder, File } from '@nextcloud/files'
import { translate, translatePlural } from '@nextcloud/l10n'
import VirtualList from 'vue-virtual-scroll-list'

import FileEntry from './FileEntry.vue'
import FilesListHeader from './FilesListHeader.vue'

export default {
name: 'FilesListVirtual',

components: {
VirtualList,
FilesListHeader,
},

props: {
nodes: {
type: [File, Folder],
required: true,
},
},

data() {
return {
FileEntry,
}
},

computed: {
files() {
return this.nodes.filter(node => node.type === 'file')
},

summaryFile() {
const count = this.files.length
return translatePlural('files', '{count} file', '{count} files', count, { count })
},
summaryFolder() {
const count = this.nodes.length - this.files.length
return translatePlural('files', '{count} folder', '{count} folders', count, { count })
},
summary() {
return translate('files', '{summaryFile} and {summaryFolder}', this)
},
},

methods: {
getFileId(node) {
return node.attributes.fileid
},

t: translate,
},
}
</script>

<style scoped lang="scss">
.files-list {
--row-height: 55px;
--checkbox-padding: calc((var(--row-height) - var(--checkbox-size)) / 2);
--checkbox-size: 24px;
--clickable-area: 44px;
--icon-preview-size: 32px;

display: block;
overflow: auto;
height: 100%;

&::v-deep {
tbody, thead, tfoot {
display: flex;
flex-direction: column;
width: 100%;
}

thead, .files-list__row {
border-bottom: 1px solid var(--color-border);
}
}
}
</style>

+ 15
- 0
apps/files/src/main.js View File

@@ -4,12 +4,15 @@ import processLegacyFilesViews from './legacy/navigationMapper.js'

import Vue from 'vue'
import NavigationService from './services/Navigation.ts'

import NavigationView from './views/Navigation.vue'
import FilesListView from './views/FilesList.vue'

import SettingsService from './services/Settings.js'
import SettingsModel from './models/Setting.js'

import router from './router/router.js'
import store from './store/index.ts'

// Init private and public Files namespace
window.OCA.Files = window.OCA.Files ?? {}
@@ -35,5 +38,17 @@ const FilesNavigationRoot = new View({
})
FilesNavigationRoot.$mount('#app-navigation-files')

// Init content list view
const ListView = Vue.extend(FilesListView)
const FilesList = new ListView({
name: 'FilesListRoot',
propsData: {
Navigation,
},
router,
store,
})
FilesList.$mount('#app-content-vue')

// Init legacy files views
processLegacyFilesViews()

+ 63
- 0
apps/files/src/mixins/fileslist-row.scss View File

@@ -0,0 +1,63 @@
/**
* @copyright Copyright (c) 2023 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/>.
*
*/
td, th {
height: var(--row-height);
vertical-align: middle;
padding: 0px;
border: none;
}

.files-list__row-checkbox {
width: var(--row-height);
&::v-deep .checkbox-radio-switch {
--icon-size: var(--checkbox-size);

display: flex;
justify-content: center;

label.checkbox-radio-switch__label {
margin: 0;
height: var(--clickable-area);
width: var(--clickable-area);
padding: calc((var(--clickable-area) - var(--checkbox-size)) / 2)
}

.checkbox-radio-switch__icon {
margin: 0 !important;
}
}
}

.files-list__row-icon {
// Remove left padding to look nicer with the checkbox
// => ico preview size + one checkbox td padding
width: calc(var(--icon-preview-size) + var(--checkbox-padding));
padding-right: var(--checkbox-padding);
color: var(--color-primary-element);
& > span {
justify-content: flex-start;
}
&::v-deep svg {
width: var(--icon-preview-size);
height: var(--icon-preview-size);
}
}

+ 23
- 8
apps/files/src/services/Navigation.ts View File

@@ -19,19 +19,27 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type Node from '@nextcloud/files/dist/files/node'
/* eslint-disable */
import type { Folder, Node } from '@nextcloud/files'
import isSvg from 'is-svg'

import logger from '../logger.js'

export type ContentsWithRoot = {
folder: Folder,
contents: Node[]
}

export interface Column {
/** Unique column ID */
id: string
/** Translated column title */
title: string
/** Property key from Node main or additional attributes.
Will be used if no custom sort function is provided.
Sorting will be done by localCompare */
/**
* Property key from Node main or additional attributes.
* Will be used if no custom sort function is provided.
* Sorting will be done by localCompare
*/
property: string
/** Special function used to sort Nodes between them */
sortFunction?: (nodeA: Node, nodeB: Node) => number;
@@ -45,8 +53,15 @@ export interface Navigation {
id: string
/** Translated view name */
name: string
/** Method return the content of the provided path */
getFiles: (path: string) => Node[]
/**
* Method return the content of the provided path
* This ideally should be a cancellable promise.
* promise.cancel(reason) will be called when the directory
* change and the promise is not resolved yet.
* You _must_ also return the current directory
* information alongside with its content.
*/
getContents: (path: string) => Promise<ContentsWithRoot[]>
/** The view icon as an inline svg */
icon: string
/** The view order */
@@ -150,8 +165,8 @@ const isValidNavigation = function(view: Navigation): boolean {
* TODO: remove when support for legacy views is removed
*/
if (!view.legacy) {
if (!view.getFiles || typeof view.getFiles !== 'function') {
throw new Error('Navigation getFiles 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)) {

+ 97
- 0
apps/files/src/store/files.ts View File

@@ -0,0 +1,97 @@
/**
* @copyright Copyright (c) 2023 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/>.
*
*/
/* eslint-disable */
import type { Folder, Node } from '@nextcloud/files'
import Vue from 'vue'
import type { FileStore, RootStore, RootOptions, Service } from '../types'

const state = {
files: {} as FileStore,
roots: {} as RootStore,
}

const getters = {
/**
* Get a file or folder by id
*/
getNode: (state) => (id: number): Node|undefined => state.files[id],

/**
* Get a list of files or folders by their IDs
* Does not return undefined values
*/
getNodes: (state) => (ids: number[]): Node[] => ids
.map(id => state.files[id])
.filter(Boolean),
/**
* Get a file or folder by id
*/
getRoot: (state) => (service: Service): Folder|undefined => state.roots[service],
}

const mutations = {
updateNodes: (state, nodes: Node[]) => {
nodes.forEach(node => {
if (!node.attributes.fileid) {
return
}
Vue.set(state.files, node.attributes.fileid, node)
// state.files = {
// ...state.files,
// [node.attributes.fileid]: node,
// }
})
},

setRoot: (state, { service, root }: RootOptions) => {
state.roots = {
...state.roots,
[service]: root,
}
}
}

const actions = {
/**
* Insert valid nodes into the store.
* Roots (that does _not_ have a fileid) should
* be defined in the roots store
*/
addNodes: (context, nodes: Node[]) => {
context.commit('updateNodes', nodes)
},

/**
* Set the root of a service
*/
setRoot(context, { service, root }: RootOptions) {
context.commit('setRoot', { service, root })
}
}

export default {
namespaced: true,
state,
getters,
mutations,
actions,
}

+ 16
- 0
apps/files/src/store/index.ts View File

@@ -0,0 +1,16 @@
import Vue from 'vue'
import Vuex, { Store } from 'vuex'

import files from './files'
import paths from './paths'
import selection from './selection'

Vue.use(Vuex)

export default new Store({
modules: {
files,
paths,
selection,
},
})

+ 71
- 0
apps/files/src/store/paths.ts View File

@@ -0,0 +1,71 @@
/**
* @copyright Copyright (c) 2023 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/>.
*
*/
/* eslint-disable */
import type { Folder } from '@nextcloud/files'
import Vue from 'vue'
import type { PathOptions, ServicePaths, ServiceStore } from '../types'

const module = {
state: {
services: {
files: {} as ServicePaths,
} as ServiceStore,
},

getters: {
getPath(state: { services: ServiceStore }) {
return (service: string, path: string): number|undefined => {
if (!state.services[service]) {
return undefined
}
return state.services[service][path]
}
},
},

mutations: {
addPath: (state, opts: PathOptions) => {
// If it doesn't exists, init the service state
if (!state.services[opts.service]) {
// TODO: investigate why Vue.set is not working
state.services = {
[opts.service]: {} as ServicePaths,
...state.services
}
}

// Now we can set the path
Vue.set(state.services[opts.service], opts.path, opts.fileid)
}
},

actions: {
addPath: (context, opts: PathOptions) => {
context.commit('addPath', opts)
},
}
}

export default {
namespaced: true,
...module,
}

+ 51
- 0
apps/files/src/store/selection.ts View File

@@ -0,0 +1,51 @@
/**
* @copyright Copyright (c) 2023 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/>.
*
*/
/* eslint-disable */
import type { Folder } from '@nextcloud/files'
import Vue from 'vue'
import type { PathOptions, ServicePaths, ServiceStore } from '../types'

const module = {
state: {
selected: [] as number[]
},

mutations: {
set: (state, selection: number[]) => {
Vue.set(state, 'selected', selection)
}
},

actions: {
set: (context, selection = [] as number[]) => {
context.commit('set', selection)
},
reset(context) {
context.commit('set', [])
}
}
}

export default {
namespaced: true,
...module,
}

+ 56
- 0
apps/files/src/types.ts View File

@@ -0,0 +1,56 @@
/**
* @copyright Copyright (c) 2023 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/>.
*
*/
/* eslint-disable */
import type { Folder } from '@nextcloud/files'
import type { Node } from '@nextcloud/files'

// Global definitions
export type Service = string

// Files store
export type FileStore = {
[id: number]: Node
}

export type RootStore = {
[service: Service]: Folder
}

export interface RootOptions {
root: Folder
service: Service
}

// Paths store
export type ServicePaths = {
[path: string]: number
}

export type ServiceStore = {
[service: Service]: ServicePaths
}

export interface PathOptions {
service: Service
path: string
fileid: number
}

+ 318
- 0
apps/files/src/views/FilesList.vue View File

@@ -0,0 +1,318 @@
<!--
- @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
-
- @author Gary Kim <gary@garykim.dev>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<NcAppContent v-show="!currentView?.legacy"
:class="{'app-content--hidden': currentView?.legacy}"
data-cy-files-content>
<div class="files-list__header">
<!-- Current folder breadcrumbs -->
<BreadCrumbs :path="dir" />

<!-- Secondary loading indicator -->
<NcLoadingIcon v-if="isRefreshing" class="files-list__refresh-icon" />
</div>

<!-- Initial loading -->
<NcLoadingIcon v-if="loading && !isRefreshing"
class="files-list__loading-icon"
:size="38"
:title="t('files', 'Loading current folder')" />

<!-- Empty content placeholder -->
<NcEmptyContent v-else-if="!loading && isEmptyDir"
:title="t('files', 'No files in here')"
:description="t('files', 'No files or folders have been deleted yet')"
data-cy-files-content-empty>
<template #action>
<NcButton v-if="dir !== '/'"
aria-label="t('files', 'Go to the previous folder')"
type="primary"
:to="toPreviousDir">
{{ t('files', 'Go back') }}
</NcButton>
</template>
<template #icon>
<TrashCan />
</template>
</NcEmptyContent>

<!-- File list -->
<FilesListVirtual v-else :nodes="dirContents" />
</NcAppContent>
</template>

<script lang="ts">
import { Folder } from '@nextcloud/files'
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 NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import TrashCan from 'vue-material-design-icons/TrashCan.vue'

import BreadCrumbs from '../components/BreadCrumbs.vue'
import logger from '../logger.js'
import Navigation from '../services/Navigation'
import FilesListVirtual from '../components/FilesListVirtual.vue'
import { ContentsWithRoot } from '../services/Navigation'
import { join } from 'path'

export default {
name: 'FilesList',

components: {
BreadCrumbs,
FilesListVirtual,
NcAppContent,
NcButton,
NcEmptyContent,
NcLoadingIcon,
TrashCan,
},

props: {
// eslint-disable-next-line vue/prop-name-casing
Navigation: {
type: Navigation,
required: true,
},
},

data() {
return {
loading: true,
promise: null,
}
},

computed: {
currentViewId() {
return this.$route.params.view || 'files'
},

/** @return {Navigation} */
currentView() {
return this.views.find(view => view.id === this.currentViewId)
},

/** @return {Navigation[]} */
views() {
return this.Navigation.views
},

/**
* The current directory query.
* @return {string}
*/
dir() {
// Remove any trailing slash but leave root slash
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
},

/**
* The current folder.
* @return {Folder|undefined}
*/
currentFolder() {
if (this.dir === '/') {
return this.$store.getters['files/getRoot'](this.currentViewId)
}
const fileId = this.$store.getters['paths/getPath'](this.currentViewId, this.dir)
return this.$store.getters['files/getNode'](fileId)
},

/**
* The current directory contents.
* @return {Node[]}
*/
dirContents() {
return (this.currentFolder?.children || []).map(this.getNode)
},

/**
* The current directory is empty.
*/
isEmptyDir() {
return this.dirContents.length === 0
},

/**
* We are refreshing the current directory.
* But we already have a cached version of it
* that is not empty.
*/
isRefreshing() {
return this.currentFolder !== undefined
&& !this.isEmptyDir
&& this.loading
},

/**
* Route to the previous directory.
*/
toPreviousDir() {
const dir = this.dir.split('/').slice(0, -1).join('/') || '/'
return { ...this.$route, query: { dir } }
},
},

watch: {
currentView(newView, oldView) {
if (newView?.id === oldView?.id) {
return
}

logger.debug('View changed', { newView, oldView })
this.$store.dispatch('selection/reset')
this.fetchContent()
},

dir(newDir, oldDir) {
logger.debug('Directory changed', { newDir, oldDir })
// TODO: preserve selection on browsing?
this.$store.dispatch('selection/reset')
this.fetchContent()
},

paths(paths) {
logger.debug('Paths changed', { paths })
},

currentFolder(currentFolder) {
logger.debug('currentFolder changed', { currentFolder })
},
},

methods: {
async fetchContent() {
if (this.currentView?.legacy) {
return
}

this.loading = true
const dir = this.dir
const currentView = this.currentView

// If we have a cancellable promise ongoing, cancel it
if (typeof this.promise?.cancel === 'function') {
this.promise.cancel()
logger.debug('Cancelled previous ongoing fetch')
}

// Fetch the current dir contents
/** @type {Promise<ContentsWithRoot>} */
this.promise = currentView.getContents(dir)
try {
const { folder, contents } = await this.promise
logger.debug('Fetched contents', { dir, folder, contents })

// Update store
this.$store.dispatch('files/addNodes', contents)

// Define current directory children
folder.children = contents.map(node => node.attributes.fileid)

// If we're in the root dir, define the root
if (dir === '/') {
console.debug('files', 'Setting root', { service: currentView.id, folder })
this.$store.dispatch('files/setRoot', { service: currentView.id, root: folder })
} else
// Otherwise, add the folder to the store
if (folder.attributes.fileid) {
this.$store.dispatch('files/addNodes', [folder])
this.$store.dispatch('paths/addPath', { service: currentView.id, fileid: folder.attributes.fileid, path: dir })
} else {
// If we're here, the view API messed up
logger.error('Invalid root folder returned', { dir, folder, currentView })
}

// Update paths store
const folders = contents.filter(node => node.type === 'folder')
folders.forEach(node => {
this.$store.dispatch('paths/addPath', { service: currentView.id, fileid: node.attributes.fileid, path: join(dir, node.basename) })
})
} catch (error) {
logger.error('Error while fetching content', { error })
} finally {
this.loading = false
}

},

/**
* Get a cached note from the store
*
* @param {number} fileId the file id to get
* @return {Folder|File}
*/
getNode(fileId) {
return this.$store.getters['files/getNode'](fileId)
},

t: translate,
},
}
</script>

<style scoped lang="scss">
.app-content {
// Virtual list needs to be full height and is scrollable
display: flex;
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;
$navigationToggleSize: 50px;

.files-list {
&__header {
display: flex;
align-content: center;
// Do not grow or shrink (vertically)
flex: 0 0;
// Align with the navigation toggle icon
margin: $margin $margin $margin $navigationToggleSize;
> * {
// Do not grow or shrink (horizontally)
// Only the breadcrumbs shrinks
flex: 0 0;
}
}
&__refresh-icon {
flex: 0 0 44px;
width: 44px;
height: 44px;
}
&__loading-icon {
margin: auto;
}
}

</style>

+ 28
- 5
apps/files/src/views/Navigation.vue View File

@@ -32,13 +32,20 @@
:title="view.name"
:to="generateToNavigation(view)"
@update:open="onToggleExpand(view)">
<!-- Sanitized icon as svg if provided -->
<NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" />

<!-- Child views if any -->
<NcAppNavigationItem v-for="child in childViews[view.id]"
:key="child.id"
:data-cy-files-navigation-item="child.id"
:exact="true"
:icon="child.iconClass"
:title="child.name"
:to="generateToNavigation(child)" />
:to="generateToNavigation(child)">
<!-- Sanitized icon as svg if provided -->
<NcIconSvgWrapper v-if="view.icon" slot="icon" :svg="view.icon" />
</NcAppNavigationItem>
</NcAppNavigationItem>
</template>

@@ -74,6 +81,7 @@ import axios from '@nextcloud/axios'
import Cog from 'vue-material-design-icons/Cog.vue'
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'

import logger from '../logger.js'
import Navigation from '../services/Navigation.ts'
@@ -86,10 +94,11 @@ export default {

components: {
Cog,
NavigationQuota,
NcAppNavigation,
NcAppNavigationItem,
NcIconSvgWrapper,
SettingsModal,
NavigationQuota,
},

props: {
@@ -151,7 +160,16 @@ export default {

watch: {
currentView(view, oldView) {
logger.debug('View changed', { id: view.id, view })
// If undefined, it means we're initializing the view
// This is handled by the legacy-view:initialized event
if (view?.id === oldView?.id) {
return
}

this.Navigation.setActive(view.id)
logger.debug('Navigation changed', { id: view.id, view })

// debugger
this.showView(view, oldView)
},
},
@@ -163,6 +181,12 @@ export default {
}

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: {
@@ -174,7 +198,7 @@ export default {
// Closing any opened sidebar
window?.OCA?.Files?.Sidebar?.close?.()

if (view.legacy) {
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')
@@ -188,7 +212,6 @@ export default {
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)

+ 4
- 6
apps/files/templates/appnavigation.php View File

@@ -1,13 +1,11 @@
<div id="app-navigation-files" role="navigation"></div>
<div class="hidden">
<ul class="with-icon" tabindex="0">

<?php

$pinned = 0;
foreach ($_['navigationItems'] as $item) {
$pinned = NavigationListElements($item, $l, $pinned);
}
$pinned = 0;
foreach ($_['navigationItems'] as $item) {
$pinned = NavigationListElements($item, $l, $pinned);
}
?>
</ul>
</div>

+ 4
- 2
apps/files/templates/index.php View File

@@ -1,5 +1,9 @@
<?php /** @var \OCP\IL10N $l */ ?>
<?php $_['appNavigation']->printPage(); ?>

<!-- New files vue container -->
<div id="app-content-vue" class="hidden"></div>

<div id="app-content" tabindex="0">

<input type="checkbox" class="hidden-visually" id="showgridview"
@@ -8,8 +12,6 @@
<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>

<!-- New files vue container -->
<div id="app-content-vue" class="hidden"></div>

<!-- Legacy views -->
<?php foreach ($_['appContents'] as $content) { ?>

+ 1
- 0
apps/files_trashbin/composer/composer/autoload_classmap.php View File

@@ -21,6 +21,7 @@ return array(
'OCA\\Files_Trashbin\\Expiration' => $baseDir . '/../lib/Expiration.php',
'OCA\\Files_Trashbin\\Helper' => $baseDir . '/../lib/Helper.php',
'OCA\\Files_Trashbin\\Hooks' => $baseDir . '/../lib/Hooks.php',
'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => $baseDir . '/../lib/Listeners/LoadAdditionalScripts.php',
'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => $baseDir . '/../lib/Migration/Version1010Date20200630192639.php',
'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => $baseDir . '/../lib/Sabre/AbstractTrash.php',
'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => $baseDir . '/../lib/Sabre/AbstractTrashFile.php',

+ 1
- 0
apps/files_trashbin/composer/composer/autoload_static.php View File

@@ -36,6 +36,7 @@ class ComposerStaticInitFiles_Trashbin
'OCA\\Files_Trashbin\\Expiration' => __DIR__ . '/..' . '/../lib/Expiration.php',
'OCA\\Files_Trashbin\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php',
'OCA\\Files_Trashbin\\Hooks' => __DIR__ . '/..' . '/../lib/Hooks.php',
'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => __DIR__ . '/..' . '/../lib/Listeners/LoadAdditionalScripts.php',
'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => __DIR__ . '/..' . '/../lib/Migration/Version1010Date20200630192639.php',
'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrash.php',
'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrashFile.php',

+ 7
- 12
apps/files_trashbin/lib/AppInfo/Application.php View File

@@ -26,8 +26,10 @@
namespace OCA\Files_Trashbin\AppInfo;

use OCA\DAV\Connector\Sabre\Principal;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files_Trashbin\Capabilities;
use OCA\Files_Trashbin\Expiration;
use OCA\Files_Trashbin\Listeners\LoadAdditionalScripts;
use OCA\Files_Trashbin\Trash\ITrashManager;
use OCA\Files_Trashbin\Trash\TrashManager;
use OCA\Files_Trashbin\UserMigration\TrashbinMigrator;
@@ -55,6 +57,11 @@ class Application extends App implements IBootstrap {
$context->registerServiceAlias('principalBackend', Principal::class);

$context->registerUserMigrator(TrashbinMigrator::class);

$context->registerEventListener(
LoadAdditionalScriptsEvent::class,
LoadAdditionalScripts::class
);
}

public function boot(IBootContext $context): void {
@@ -68,18 +75,6 @@ class Application extends App implements IBootstrap {
\OCP\Util::connectHook('OC_Filesystem', 'post_write', 'OCA\Files_Trashbin\Hooks', 'post_write_hook');
// pre and post-rename, disable trash logic for the copy+unlink case
\OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Trashbin\Trashbin', 'ensureFileScannedHook');

\OCA\Files\App::getNavigationManager()->add(function () {
$l = \OC::$server->getL10N(self::APP_ID);
return [
'id' => 'trashbin',
'appname' => self::APP_ID,
'script' => 'list.php',
'order' => 50,
'name' => $l->t('Deleted files'),
'classes' => 'pinned',
];
});
}

public function registerTrashBackends(IServerContainer $serverContainer, ILogger $logger, IAppManager $appManager, ITrashManager $trashManager) {

+ 41
- 0
apps/files_trashbin/lib/Listeners/LoadAdditionalScripts.php View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);

/**
* @copyright Copyright (c) 2022, 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/>.
*
*/
namespace OCA\Files_Trashbin\Listeners;

use OCA\Files_Trashbin\AppInfo\Application;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;

class LoadAdditionalScripts implements IEventListener {
public function handle(Event $event): void {
if (!($event instanceof LoadAdditionalScriptsEvent)) {
return;
}

Util::addScript(Application::APP_ID, 'main');
}
}

+ 39
- 0
apps/files_trashbin/src/main.ts View File

@@ -0,0 +1,39 @@
/**
* @copyright Copyright (c) 2023 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 type NavigationService from '../../files/src/services/Navigation'

import { translate as t } from '@nextcloud/l10n'
import DeleteSvg from '@mdi/svg/svg/delete.svg?raw'

import getContents from './services/trashbin'

const Navigation = window.OCP.Files.Navigation as NavigationService
Navigation.register({
id: 'trashbin',
name: t('files_trashbin', 'Deleted files'),

icon: DeleteSvg,
order: 50,
sticky: true,

getContents,
})

+ 33
- 0
apps/files_trashbin/src/services/client.ts View File

@@ -0,0 +1,33 @@
/**
* @copyright Copyright (c) 2023 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 { createClient } from 'webdav'
import { generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser, getRequestToken } from '@nextcloud/auth'

export const rootPath = `/trashbin/${getCurrentUser()?.uid}/trash`
export const rootUrl = generateRemoteUrl('dav' + rootPath)
const client = createClient(rootUrl, {
headers: {
requesttoken: getRequestToken(),
},
})
export default client

+ 95
- 0
apps/files_trashbin/src/services/trashbin.ts View File

@@ -0,0 +1,95 @@
/**
* @copyright Copyright (c) 2023 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/>.
*
*/
/* eslint-disable */
import { getCurrentUser } from '@nextcloud/auth'
import { File, Folder, parseWebdavPermissions } from '@nextcloud/files'
import { generateRemoteUrl } from '@nextcloud/router'

import type { FileStat, ResponseDataDetailed } from 'webdav'
import type { ContentsWithRoot } from '../../../files/src/services/Navigation'

import client, { rootPath } from './client'

const data = `<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:"
xmlns:oc="http://owncloud.org/ns"
xmlns:nc="http://nextcloud.org/ns">
<d:prop>
<nc:trashbin-filename />
<nc:trashbin-deletion-time />
<nc:trashbin-original-location />
<nc:trashbin-title />
<d:getlastmodified />
<d:getetag />
<d:getcontenttype />
<d:resourcetype />
<oc:fileid />
<oc:permissions />
<oc:size />
<d:getcontentlength />
</d:prop>
</d:propfind>`

const resultToNode = function(node: FileStat): File | Folder {
const permissions = parseWebdavPermissions(node.props?.permissions)
const owner = getCurrentUser()?.uid as string

const nodeData = {
id: node.props?.fileid as number || 0,
source: generateRemoteUrl('dav' + rootPath + node.filename),
mtime: new Date(node.lastmod),
mime: node.mime as string,
size: node.props?.size as number || 0,
permissions,
owner,
root: rootPath,
attributes: {
...node,
...node.props,
// Override displayed name on the list
displayName: node.props?.['trashbin-filename'],
},
}

return node.type === 'file'
? new File(nodeData)
: new Folder(nodeData)
}

export default async (path: string = '/'): Promise<ContentsWithRoot> => {
// TODO: use only one request when webdav-client supports it
// @see https://github.com/perry-mitchell/webdav-client/pull/334
const rootResponse = await client.stat(path, {
details: true,
data,
}) as ResponseDataDetailed<FileStat>

const contentsResponse = await client.getDirectoryContents(path, {
details: true,
data,
}) as ResponseDataDetailed<FileStat[]>

return {
folder: resultToNode(rootResponse.data) as Folder,
contents: contentsResponse.data.map(resultToNode),
}
}

+ 0
- 22
apps/files_trashbin/src/trash.scss View File

@@ -1,22 +0,0 @@
/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
#app-content-trashbin tbody tr[data-type="file"] td a.name,
#app-content-trashbin tbody tr[data-type="file"] td a.name span.nametext,
#app-content-trashbin tbody tr[data-type="file"] td a.name span.nametext span {
cursor: default;
}

#app-content-trashbin .summary :last-child {
padding: 0;
}
#app-content-trashbin .files-filestable .summary .filesize {
display: none;
}


+ 0
- 70
apps/files_trashbin/tests/js/appSpec.js View File

@@ -1,70 +0,0 @@
/**
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* @author Vincent Petry <vincent@nextcloud.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/>.
*
*/

describe('OCA.Trashbin.App tests', function() {
var App = OCA.Trashbin.App;

beforeEach(function() {
$('#testArea').append(
'<div id="app-navigation">' +
'<ul><li data-id="files"><a>Files</a></li>' +
'<li data-id="trashbin"><a>Trashbin</a></li>' +
'</div>' +
'<div id="app-content">' +
'<div id="app-content-files" class="hidden">' +
'</div>' +
'<div id="app-content-trashbin" class="hidden">' +
'</div>' +
'</div>' +
'</div>'
);
App.initialize($('#app-content-trashbin'));
});
afterEach(function() {
App._initialized = false;
App.fileList = null;
});

describe('initialization', function() {
it('creates a custom filelist instance', function() {
App.initialize();
expect(App.fileList).toBeDefined();
expect(App.fileList.$el.is('#app-content-trashbin')).toEqual(true);
});

it('registers custom file actions', function() {
var fileActions;
App.initialize();

fileActions = App.fileList.fileActions;

expect(fileActions.actions.all).toBeDefined();
expect(fileActions.actions.all.Restore).toBeDefined();
expect(fileActions.actions.all.Delete).toBeDefined();

expect(fileActions.actions.all.Rename).not.toBeDefined();
expect(fileActions.actions.all.Download).not.toBeDefined();

expect(fileActions.defaults.dir).toEqual('Open');
});
});
});

+ 0
- 397
apps/files_trashbin/tests/js/filelistSpec.js View File

@@ -1,397 +0,0 @@
/**
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* @author Abijeet <abijeetpatro@gmail.com>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Jan C. Borchardt <hey@jancborchardt.net>
* @author Jan-Christoph Borchardt <hey@jancborchardt.net>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Vincent Petry <vincent@nextcloud.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/>.
*
*/

describe('OCA.Trashbin.FileList tests', function () {
var testFiles, alertStub, notificationStub, fileList, client;

beforeEach(function () {
alertStub = sinon.stub(OC.dialogs, 'alert');
notificationStub = sinon.stub(OC.Notification, 'show');

client = new OC.Files.Client({
host: 'localhost',
port: 80,
root: '/remote.php/dav/trashbin/user',
useHTTPS: OC.getProtocol() === 'https'
});

// init parameters and test table elements
$('#testArea').append(
'<div id="app-content">' +
// set this but it shouldn't be used (could be the one from the
// files app)
'<input type="hidden" id="permissions" value="31"></input>' +
// dummy controls
'<div class="files-controls">' +
' <div class="actions creatable"></div>' +
' <div class="notCreatable"></div>' +
'</div>' +
// dummy table
// TODO: at some point this will be rendered by the fileList class itself!
'<table class="files-filestable list-container view-grid">' +
'<thead><tr><th class="hidden column-name">' +
'<input type="checkbox" id="select_all_trash" class="select-all">' +
'<span class="name">Name</span>' +
'<span class="selectedActions hidden">' +
'<a href="" class="actions-selected"><span class="icon icon-more"></span><span>Actions</span>' +
'</span>' +
'</th></tr></thead>' +
'<tbody class="files-fileList"></tbody>' +
'<tfoot></tfoot>' +
'</table>' +
'<div class="emptyfilelist emptycontent">Empty content message</div>' +
'</div>'
);

testFiles = [{
id: 1,
type: 'file',
name: 'One.txt.d11111',
displayName: 'One.txt',
mtime: 11111000,
mimetype: 'text/plain',
etag: 'abc'
}, {
id: 2,
type: 'file',
name: 'Two.jpg.d22222',
displayName: 'Two.jpg',
mtime: 22222000,
mimetype: 'image/jpeg',
etag: 'def',
}, {
id: 3,
type: 'file',
name: 'Three.pdf.d33333',
displayName: 'Three.pdf',
mtime: 33333000,
mimetype: 'application/pdf',
etag: '123',
}, {
id: 4,
type: 'dir',
mtime: 99999000,
name: 'somedir.d99999',
displayName: 'somedir',
mimetype: 'httpd/unix-directory',
etag: '456'
}];

// register file actions like the trashbin App does
var fileActions = OCA.Trashbin.App._createFileActions(fileList);
fileList = new OCA.Trashbin.FileList(
$('#app-content'), {
fileActions: fileActions,
multiSelectMenu: [{
name: 'restore',
displayName: t('files', 'Restore'),
iconClass: 'icon-history',
},
{
name: 'delete',
displayName: t('files', 'Delete'),
iconClass: 'icon-delete',
}
],
client: client
}
);
});
afterEach(function () {
testFiles = undefined;
fileList.destroy();
fileList = undefined;

notificationStub.restore();
alertStub.restore();
});
describe('Initialization', function () {
it('Sorts by mtime by default', function () {
expect(fileList._sort).toEqual('mtime');
expect(fileList._sortDirection).toEqual('desc');
});
it('Always returns read and delete permission', function () {
expect(fileList.getDirectoryPermissions()).toEqual(OC.PERMISSION_READ | OC.PERMISSION_DELETE);
});
});
describe('Breadcrumbs', function () {
beforeEach(function () {
var data = {
status: 'success',
data: {
files: testFiles,
permissions: 1
}
};
fakeServer.respondWith(/\/index\.php\/apps\/files_trashbin\/ajax\/list.php\?dir=%2Fsubdir/, [
200, {
"Content-Type": "application/json"
},
JSON.stringify(data)
]);
});
it('links the breadcrumb to the trashbin view', function () {
fileList.changeDirectory('/subdir', false, true);
fakeServer.respond();
var $crumbs = fileList.$el.find('.files-controls .crumb');
expect($crumbs.length).toEqual(3);
expect($crumbs.eq(1).find('a').text()).toEqual('Home');
expect($crumbs.eq(1).find('a').attr('href'))
.toEqual(OC.getRootPath() + '/index.php/apps/files?view=trashbin&dir=/');
expect($crumbs.eq(2).find('a').text()).toEqual('subdir');
expect($crumbs.eq(2).find('a').attr('href'))
.toEqual(OC.getRootPath() + '/index.php/apps/files?view=trashbin&dir=/subdir');
});
});
describe('Rendering rows', function () {
it('renders rows with the correct data when in root', function () {
// dir listing is false when in root
fileList.setFiles(testFiles);
var $rows = fileList.$el.find('tbody tr');
var $tr = $rows.eq(0);
expect($rows.length).toEqual(4);
expect($tr.attr('data-id')).toEqual('1');
expect($tr.attr('data-type')).toEqual('file');
expect($tr.attr('data-file')).toEqual('One.txt.d11111');
expect($tr.attr('data-size')).not.toBeDefined();
expect($tr.attr('data-etag')).toEqual('abc');
expect($tr.attr('data-permissions')).toEqual('9'); // read and delete
expect($tr.attr('data-mime')).toEqual('text/plain');
expect($tr.attr('data-mtime')).toEqual('11111000');
expect($tr.find('a.name').attr('href')).toEqual('#');

expect($tr.find('.nametext').text().trim()).toEqual('One.txt');

expect(fileList.findFileEl('One.txt.d11111')[0]).toEqual($tr[0]);
});
it('renders rows with the correct data when in root after calling setFiles with the same data set', function () {
// dir listing is false when in root
fileList.setFiles(testFiles);
fileList.setFiles(fileList.files);
var $rows = fileList.$el.find('tbody tr');
var $tr = $rows.eq(0);
expect($rows.length).toEqual(4);
expect($tr.attr('data-id')).toEqual('1');
expect($tr.attr('data-type')).toEqual('file');
expect($tr.attr('data-file')).toEqual('One.txt.d11111');
expect($tr.attr('data-size')).not.toBeDefined();
expect($tr.attr('data-etag')).toEqual('abc');
expect($tr.attr('data-permissions')).toEqual('9'); // read and delete
expect($tr.attr('data-mime')).toEqual('text/plain');
expect($tr.attr('data-mtime')).toEqual('11111000');
expect($tr.find('a.name').attr('href')).toEqual('#');

expect($tr.find('.nametext').text().trim()).toEqual('One.txt');

expect(fileList.findFileEl('One.txt.d11111')[0]).toEqual($tr[0]);
});
it('renders rows with the correct data when in subdirectory', function () {
fileList.setFiles(testFiles.map(function (file) {
file.name = file.displayName;
return file;
}));
var $rows = fileList.$el.find('tbody tr');
var $tr = $rows.eq(0);
expect($rows.length).toEqual(4);
expect($tr.attr('data-id')).toEqual('1');
expect($tr.attr('data-type')).toEqual('file');
expect($tr.attr('data-file')).toEqual('One.txt');
expect($tr.attr('data-size')).not.toBeDefined();
expect($tr.attr('data-etag')).toEqual('abc');
expect($tr.attr('data-permissions')).toEqual('9'); // read and delete
expect($tr.attr('data-mime')).toEqual('text/plain');
expect($tr.attr('data-mtime')).toEqual('11111000');
expect($tr.find('a.name').attr('href')).toEqual('#');

expect($tr.find('.nametext').text().trim()).toEqual('One.txt');

expect(fileList.findFileEl('One.txt')[0]).toEqual($tr[0]);
});
it('does not render a size column', function () {
expect(fileList.$el.find('tbody tr .filesize').length).toEqual(0);
});
});
describe('File actions', function () {
describe('Deleting single files', function () {
// TODO: checks ajax call
// TODO: checks spinner
// TODO: remove item after delete
// TODO: bring back item if delete failed
});
describe('Restoring single files', function () {
// TODO: checks ajax call
// TODO: checks spinner
// TODO: remove item after restore
// TODO: bring back item if restore failed
});
});
describe('file previews', function () {
// TODO: check that preview URL is going through files_trashbin
});
describe('loading file list', function () {
// TODO: check that ajax URL is going through files_trashbin
});
describe('breadcrumbs', function () {
// TODO: test label + URL
});
describe('elementToFile', function () {
var $tr;

beforeEach(function () {
fileList.setFiles(testFiles);
$tr = fileList.findFileEl('One.txt.d11111');
});

it('converts data attributes to file info structure', function () {
var fileInfo = fileList.elementToFile($tr);
expect(fileInfo.id).toEqual(1);
expect(fileInfo.name).toEqual('One.txt.d11111');
expect(fileInfo.displayName).toEqual('One.txt');
expect(fileInfo.mtime).toEqual(11111000);
expect(fileInfo.etag).toEqual('abc');
expect(fileInfo.permissions).toEqual(OC.PERMISSION_READ | OC.PERMISSION_DELETE);
expect(fileInfo.mimetype).toEqual('text/plain');
expect(fileInfo.type).toEqual('file');
});
});
describe('Global Actions', function () {
beforeEach(function () {
fileList.setFiles(testFiles);
fileList.findFileEl('One.txt.d11111').find('input:checkbox').click();
fileList.findFileEl('Three.pdf.d33333').find('input:checkbox').click();
fileList.findFileEl('somedir.d99999').find('input:checkbox').click();
fileList.$el.find('.actions-selected').click();
});

afterEach(function () {
fileList.$el.find('.actions-selected').click();
});

describe('Delete', function () {
it('Shows trashbin actions', function () {
// visible because a few files were selected
expect($('.selectedActions').is(':visible')).toEqual(true);
expect($('.selectedActions .item-delete').is(':visible')).toEqual(true);
expect($('.selectedActions .item-restore').is(':visible')).toEqual(true);

// check
fileList.$el.find('.select-all').click();

// stays visible
expect($('.selectedActions').is(':visible')).toEqual(true);
expect($('.selectedActions .item-delete').is(':visible')).toEqual(true);
expect($('.selectedActions .item-restore').is(':visible')).toEqual(true);

// uncheck
fileList.$el.find('.select-all').click();

// becomes hidden now
expect($('.selectedActions').is(':visible')).toEqual(false);
expect($('.selectedActions .item-delete').is(':visible')).toEqual(false);
expect($('.selectedActions .item-restore').is(':visible')).toEqual(false);
});
it('Deletes selected files when "Delete" clicked', function (done) {
var request;
var promise = fileList._onClickDeleteSelected({
preventDefault: function () {
}
});
var files = ["One.txt.d11111", "Three.pdf.d33333", "somedir.d99999"];
expect(fakeServer.requests.length).toEqual(files.length);
for (var i = 0; i < files.length; i++) {
request = fakeServer.requests[i];
expect(request.url).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/trash/' + files[i]);
request.respond(200);
}
return promise.then(function () {
expect(fileList.findFileEl('One.txt.d11111').length).toEqual(0);
expect(fileList.findFileEl('Three.pdf.d33333').length).toEqual(0);
expect(fileList.findFileEl('somedir.d99999').length).toEqual(0);
expect(fileList.findFileEl('Two.jpg.d22222').length).toEqual(1);
}).then(done, done);
});
it('Deletes all files when all selected when "Delete" clicked', function (done) {
var request;
$('.select-all').click();
var promise = fileList._onClickDeleteSelected({
preventDefault: function () {
}
});
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/trash');
request.respond(200);
return promise.then(function () {
expect(fileList.isEmpty).toEqual(true);
}).then(done, done);
});
});
describe('Restore', function () {
it('Restores selected files when "Restore" clicked', function (done) {
var request;
var promise = fileList._onClickRestoreSelected({
preventDefault: function () {
}
});
var files = ["One.txt.d11111", "Three.pdf.d33333", "somedir.d99999"];
expect(fakeServer.requests.length).toEqual(files.length);
for (var i = 0; i < files.length; i++) {
request = fakeServer.requests[i];
expect(request.url).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/trash/' + files[i]);
expect(request.requestHeaders.Destination).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/restore/' + files[i]);
request.respond(200);
}
return promise.then(function() {
expect(fileList.findFileEl('One.txt.d11111').length).toEqual(0);
expect(fileList.findFileEl('Three.pdf.d33333').length).toEqual(0);
expect(fileList.findFileEl('somedir.d99999').length).toEqual(0);
expect(fileList.findFileEl('Two.jpg.d22222').length).toEqual(1);
}).then(done, done);
});
it('Restores all files when all selected when "Restore" clicked', function (done) {
var request;
$('.select-all').click();
var promise = fileList._onClickRestoreSelected({
preventDefault: function () {
}
});
var files = ["One.txt.d11111", "Two.jpg.d22222", "Three.pdf.d33333", "somedir.d99999"];
expect(fakeServer.requests.length).toEqual(files.length);
for (var i = 0; i < files.length; i++) {
request = fakeServer.requests[i];
expect(request.url).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/trash/' + files[i]);
expect(request.requestHeaders.Destination).toEqual(OC.getRootPath() + '/remote.php/dav/trashbin/user/restore/' + files[i]);
request.respond(200);
}
return promise.then(function() {
expect(fileList.isEmpty).toEqual(true);
}).then(done, done);
});
});
});
});

+ 1
- 0
babel.config.js View File

@@ -10,6 +10,7 @@ module.exports = {
'@babel/preset-env',
{
useBuiltIns: false,
modules: 'auto',
},
],
],

+ 0
- 135
core/src/OC/apps.js View File

@@ -1,135 +0,0 @@
/**
* @copyright Bernhard Posselt 2014
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @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 $ from 'jquery'

let dynamicSlideToggleEnabled = false

const Apps = {
enableDynamicSlideToggle() {
dynamicSlideToggleEnabled = true
},
}

/**
* Shows the #app-sidebar and add .with-app-sidebar to subsequent siblings
*
* @param {object} [$el] sidebar element to show, defaults to $('#app-sidebar')
*/
Apps.showAppSidebar = function($el) {
const $appSidebar = $el || $('#app-sidebar')
$appSidebar.removeClass('disappear').show()
$('#app-content').trigger(new $.Event('appresized'))
}

/**
* Shows the #app-sidebar and removes .with-app-sidebar from subsequent
* siblings
*
* @param {object} [$el] sidebar element to hide, defaults to $('#app-sidebar')
*/
Apps.hideAppSidebar = function($el) {
const $appSidebar = $el || $('#app-sidebar')
$appSidebar.hide().addClass('disappear')
$('#app-content').trigger(new $.Event('appresized'))
}

/**
* Provides a way to slide down a target area through a button and slide it
* up if the user clicks somewhere else. Used for the news app settings and
* add new field.
*
* Usage:
* <button data-apps-slide-toggle=".slide-area">slide</button>
* <div class=".slide-area" class="hidden">I'm sliding up</div>
*/
export const registerAppsSlideToggle = () => {
let buttons = $('[data-apps-slide-toggle]')

if (buttons.length === 0) {
$('#app-navigation').addClass('without-app-settings')
}

$(document).click(function(event) {

if (dynamicSlideToggleEnabled) {
buttons = $('[data-apps-slide-toggle]')
}

buttons.each(function(index, button) {

const areaSelector = $(button).data('apps-slide-toggle')
const area = $(areaSelector)

/**
*
*/
function hideArea() {
area.slideUp(OC.menuSpeed * 4, function() {
area.trigger(new $.Event('hide'))
})
area.removeClass('opened')
$(button).removeClass('opened')
}

/**
*
*/
function showArea() {
area.slideDown(OC.menuSpeed * 4, function() {
area.trigger(new $.Event('show'))
})
area.addClass('opened')
$(button).addClass('opened')
const input = $(areaSelector + ' [autofocus]')
if (input.length === 1) {
input.focus()
}
}

// do nothing if the area is animated
if (!area.is(':animated')) {

// button toggles the area
if ($(button).is($(event.target).closest('[data-apps-slide-toggle]'))) {
if (area.is(':visible')) {
hideArea()
} else {
showArea()
}

// all other areas that have not been clicked but are open
// should be slid up
} else {
const closest = $(event.target).closest(areaSelector)
if (area.is(':visible') && closest[0] !== area[0]) {
hideArea()
}
}
}
})

})
}

export default Apps

+ 14
- 16
core/src/OC/index.js View File

@@ -30,7 +30,6 @@ import {
processAjaxError,
registerXHRForErrorProcessing,
} from './xhr-error.js'
import Apps from './apps.js'
import { AppConfig, appConfig } from './appconfig.js'
import { appSettings } from './appsettings.js'
import appswebroots from './appswebroots.js'
@@ -45,8 +44,8 @@ import {
import {
build as buildQueryString,
parse as parseQueryString,
} from './query-string.js'
import Config from './config.js'
} from './query-string'
import Config from './config'
import {
coreApps,
menuSpeed,
@@ -58,30 +57,30 @@ import {
PERMISSION_SHARE,
PERMISSION_UPDATE,
TAG_FAVORITE,
} from './constants.js'
import ContactsMenu from './contactsmenu.js'
import { currentUser, getCurrentUser } from './currentuser.js'
import Dialogs from './dialogs.js'
import EventSource from './eventsource.js'
import { get, set } from './get_set.js'
import { getCapabilities } from './capabilities.js'
} from './constants'
import ContactsMenu from './contactsmenu'
import { currentUser, getCurrentUser } from './currentuser'
import Dialogs from './dialogs'
import EventSource from './eventsource'
import { get, set } from './get_set'
import { getCapabilities } from './capabilities'
import {
getHost,
getHostName,
getPort,
getProtocol,
} from './host.js'
} from './host'
import {
getToken as getRequestToken,
} from './requesttoken.js'
} from './requesttoken'
import {
hideMenus,
registerMenu,
showMenu,
unregisterMenu,
} from './menu.js'
import { isUserAdmin } from './admin.js'
import L10N from './l10n.js'
} from './menu'
import { isUserAdmin } from './admin'
import L10N from './l10n'
import {
getCanonicalLocale,
getLanguage,
@@ -141,7 +140,6 @@ export default {

addScript,
addStyle,
Apps,
AppConfig,
appConfig,
appSettings,

+ 2
- 0
core/src/OC/util-history.js View File

@@ -165,6 +165,8 @@ export default {
},

_onPopState(e) {
debugger

if (this._cancelPop) {
this._cancelPop = false
return

+ 0
- 2
core/src/main.js View File

@@ -35,11 +35,9 @@ import OC from './OC/index.js'
import './globals.js'
import './jquery/index.js'
import { initCore } from './init.js'
import { registerAppsSlideToggle } from './OC/apps.js'

window.addEventListener('DOMContentLoaded', function() {
initCore()
registerAppsSlideToggle()

// fallback to hashchange when no history support
if (window.history.pushState) {

apps/files_trashbin/src/files_trashbin.js → custom.d.ts View File

@@ -1,7 +1,7 @@
/**
* @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl>
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
@@ -19,9 +19,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
declare module '*.svg' {
const content: any
export default content
}

import './app.js'
import './filelist.js'
import './trash.scss'
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

window.OCA.Trashbin = OCA.Trashbin

+ 34
- 0
cypress.d.ts View File

@@ -0,0 +1,34 @@
/**
* @copyright Copyright (c) 2023 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/>.
*
*/
/* eslint-disable */
import { mount } from 'cypress/vue2'

type MountParams = Parameters<typeof mount>;
type OptionsParam = MountParams[1];

declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount;
}
}
}

+ 2
- 14
cypress/support/component.ts View File

@@ -19,21 +19,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* eslint-disable */
import { mount } from 'cypress/vue2'

// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}

// Example use:
// cy.mount(MyComponent)
Cypress.Commands.add('mount', (component, optionsOrProps) => {

+ 5
- 2
package.json View File

@@ -44,7 +44,7 @@
"@nextcloud/capabilities": "^1.0.4",
"@nextcloud/dialogs": "^4.0.0-beta.2",
"@nextcloud/event-bus": "^3.0.2",
"@nextcloud/files": "^3.0.0-beta.5",
"@nextcloud/files": "^3.0.0-beta.7",
"@nextcloud/initial-state": "^2.0.0",
"@nextcloud/l10n": "^2.1.0",
"@nextcloud/logger": "^2.5.0",
@@ -99,15 +99,17 @@
"vue": "^2.7.14",
"vue-click-outside": "^1.1.0",
"vue-cropperjs": "^4.2.0",
"vue-fragment": "^1.6.0",
"vue-infinite-loading": "^2.4.5",
"vue-localstorage": "^0.6.2",
"vue-material-design-icons": "^5.0.0",
"vue-multiselect": "^2.1.6",
"vue-router": "^3.6.5",
"vue-virtual-scroll-list": "github:skjnldsv/vue-virtual-scroll-list#feat/table",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0",
"webdav": "^4.11.0"
"webdav": "^5.0.0-r1"
},
"devDependencies": {
"@babel/node": "^7.20.7",
@@ -160,6 +162,7 @@
"sass-loader": "^13.2.0",
"sinon": "<= 5.0.7",
"style-loader": "^3.3.1",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tslib": "^2.4.1",
"typescript": "^4.9.3",

+ 2
- 2
tsconfig.json View File

@@ -1,8 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["./apps/**/*.ts", "./core/**/*.ts"],
"include": ["./apps/**/*.ts", "./core/**/*.ts", "./*.d.ts"],
"compilerOptions": {
"types": ["node"],
"types": ["cypress", "node", "vue"],
"outDir": "./dist/",
"target": "ESNext",
"module": "esnext",

+ 1
- 0
webpack.common.js View File

@@ -171,6 +171,7 @@ module.exports = {
alias: {
// make sure to use the handlebar runtime when importing
handlebars: 'handlebars/runtime',
vue$: path.resolve('./node_modules/vue'),
},
extensions: ['*', '.ts', '.js', '.vue'],
symlinks: true,

+ 1
- 1
webpack.modules.js View File

@@ -63,7 +63,7 @@ module.exports = {
'personal-settings': path.join(__dirname, 'apps/files_sharing/src', 'personal-settings.js'),
},
files_trashbin: {
files_trashbin: path.join(__dirname, 'apps/files_trashbin/src', 'files_trashbin.js'),
main: path.join(__dirname, 'apps/files_trashbin/src', 'main.ts'),
},
files_versions: {
files_versions: path.join(__dirname, 'apps/files_versions/src', 'files_versions_tab.js'),

Loading…
Cancel
Save