mirror of https://github.com/nextcloud/server.git
feat(files_trashbin): migrate to vue
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>pull/36534/head
parent
8eb9505294
commit
29a7f7f6ef
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
@ -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,
|
||||
},
|
||||
})
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
@ -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
|
||||
}
|
@ -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>
|
@ -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');
|
||||
}
|
||||
}
|
@ -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,
|
||||
})
|
@ -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
|
@ -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),
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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,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
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue