mirror of
https://github.com/nextcloud/server.git
synced 2025-12-18 15:56:14 -05:00
chore: adjust code to new codestyle
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
44962c76e7
commit
91f3b6b4ee
807 changed files with 13340 additions and 10372 deletions
|
|
@ -2,7 +2,8 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
export const getCurrentUser = function() {
|
||||
|
||||
export function getCurrentUser() {
|
||||
return {
|
||||
uid: 'test',
|
||||
displayName: 'Test',
|
||||
|
|
@ -10,8 +11,8 @@ export const getCurrentUser = function() {
|
|||
}
|
||||
}
|
||||
|
||||
export const getRequestToken = function() {
|
||||
export function getRequestToken() {
|
||||
return 'test-token-1234'
|
||||
}
|
||||
|
||||
export const onRequestTokenUpdate = function() {}
|
||||
export function onRequestTokenUpdate() {}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Capabilities } from '../../apps/files/src/types'
|
||||
|
||||
export const getCapabilities = (): Capabilities => {
|
||||
import type { Capabilities } from '../../apps/files/src/types.ts'
|
||||
|
||||
export function getCapabilities(): Capabilities {
|
||||
return {
|
||||
files: {
|
||||
bigfilechunking: true,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export const loadState = function(app: string, key: string, fallback?: any) {
|
||||
export function loadState(app: string, key: string, fallback?: any) {
|
||||
return fallback
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
export const createClient = () => {}
|
||||
export const getPatcher = () => {
|
||||
|
||||
export function createClient() {}
|
||||
export function getPatcher() {
|
||||
return {
|
||||
patch: () => {}
|
||||
patch: () => {},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { basename } from 'node:path'
|
||||
|
||||
import mime from 'mime'
|
||||
import { basename } from 'node:path'
|
||||
|
||||
class FileSystemEntry {
|
||||
|
||||
private _isFile: boolean
|
||||
private _fullPath: string
|
||||
|
||||
|
|
@ -26,11 +26,9 @@ class FileSystemEntry {
|
|||
get name() {
|
||||
return basename(this._fullPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class FileSystemFileEntry extends FileSystemEntry {
|
||||
|
||||
private _contents: string
|
||||
private _lastModified: number
|
||||
|
||||
|
|
@ -46,11 +44,9 @@ export class FileSystemFileEntry extends FileSystemEntry {
|
|||
const type = mime.getType(this.name) || ''
|
||||
success(new File([this._contents], this.name, { lastModified, type }))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class FileSystemDirectoryEntry extends FileSystemEntry {
|
||||
|
||||
private _entries: FileSystemEntry[]
|
||||
|
||||
constructor(fullPath: string, entries: FileSystemEntry[]) {
|
||||
|
|
@ -70,7 +66,6 @@ export class FileSystemDirectoryEntry extends FileSystemEntry {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -79,7 +74,6 @@ export class FileSystemDirectoryEntry extends FileSystemEntry {
|
|||
* File API in the same test suite.
|
||||
*/
|
||||
export class DataTransferItem {
|
||||
|
||||
private _type: string
|
||||
private _entry: FileSystemEntry
|
||||
|
||||
|
|
@ -104,7 +98,7 @@ export class DataTransferItem {
|
|||
return this._type
|
||||
}
|
||||
|
||||
getAsFile(): File|null {
|
||||
getAsFile(): File | null {
|
||||
if (this._entry.isFile && this._entry instanceof FileSystemFileEntry) {
|
||||
let file: File | null = null
|
||||
this._entry.file((f) => {
|
||||
|
|
@ -116,10 +110,9 @@ export class DataTransferItem {
|
|||
// The browser will return an empty File object if the entry is a directory
|
||||
return new File([], this._entry.name, { type: '' })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const fileSystemEntryToDataTransferItem = (entry: FileSystemEntry, isFileSystemAPIAvailable = true): DataTransferItem => {
|
||||
export function fileSystemEntryToDataTransferItem(entry: FileSystemEntry, isFileSystemAPIAvailable = true): DataTransferItem {
|
||||
return new DataTransferItem(
|
||||
entry.isFile ? 'text/plain' : 'httpd/unix-directory',
|
||||
entry,
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { File, Permission, View, FileAction } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { action } from './inlineUnreadCommentsAction'
|
||||
import logger from '../logger'
|
||||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import { File, FileAction, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
import logger from '../logger.js'
|
||||
import { action } from './inlineUnreadCommentsAction.ts'
|
||||
|
||||
const view = {
|
||||
id: 'files',
|
||||
|
|
@ -120,6 +122,7 @@ describe('Inline unread comments action execute tests', () => {
|
|||
const setActiveTabMock = vi.fn()
|
||||
window.OCA = {
|
||||
Files: {
|
||||
// @ts-expect-error Mocking for testing
|
||||
Sidebar: {
|
||||
open: openMock,
|
||||
setActiveTab: setActiveTabMock,
|
||||
|
|
@ -146,10 +149,13 @@ describe('Inline unread comments action execute tests', () => {
|
|||
})
|
||||
|
||||
test('Action handles sidebar open failure', async () => {
|
||||
const openMock = vi.fn(() => { throw new Error('Mock error') })
|
||||
const openMock = vi.fn(() => {
|
||||
throw new Error('Mock error')
|
||||
})
|
||||
const setActiveTabMock = vi.fn()
|
||||
window.OCA = {
|
||||
Files: {
|
||||
// @ts-expect-error Mocking for testing
|
||||
Sidebar: {
|
||||
open: openMock,
|
||||
setActiveTab: setActiveTabMock,
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { FileAction, Node } from '@nextcloud/files'
|
||||
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
|
||||
import CommentProcessingSvg from '@mdi/svg/svg/comment-processing.svg?raw'
|
||||
|
||||
import logger from '../logger'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import CommentProcessingSvg from '@mdi/svg/svg/comment-processing.svg?raw'
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import logger from '../logger.js'
|
||||
|
||||
export const action = new FileAction({
|
||||
id: 'comments-unread',
|
||||
|
|
@ -25,7 +27,7 @@ export const action = new FileAction({
|
|||
iconSvgInline: () => CommentProcessingSvg,
|
||||
|
||||
enabled(nodes: Node[]) {
|
||||
const unread = nodes[0].attributes['comments-unread'] as number|undefined
|
||||
const unread = nodes[0].attributes['comments-unread'] as number | undefined
|
||||
return typeof unread === 'number' && unread > 0
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import moment from '@nextcloud/moment'
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia'
|
||||
import Vue, { type ComponentPublicInstance } from 'vue'
|
||||
import logger from './logger.js'
|
||||
import { getComments } from './services/GetComments.js'
|
||||
|
||||
import { PiniaVuePlugin, createPinia } from 'pinia'
|
||||
|
||||
Vue.use(PiniaVuePlugin)
|
||||
|
||||
let ActivityTabPluginView
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import logger from './logger.js'
|
||||
import CommentsInstance from './services/CommentsInstance.js'
|
||||
|
||||
// Init Comments
|
||||
|
|
@ -12,4 +13,4 @@ if (window.OCA && !window.OCA.Comments) {
|
|||
|
||||
// Init Comments App view
|
||||
Object.assign(window.OCA.Comments, { View: CommentsInstance })
|
||||
console.debug('OCA.Comments.View initialized')
|
||||
logger.debug('OCA.Comments.View initialized')
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line n/no-missing-import, import/no-unresolved
|
||||
import MessageReplyText from '@mdi/svg/svg/message-reply-text.svg?raw'
|
||||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<component :is="tag"
|
||||
<component
|
||||
:is="tag"
|
||||
v-show="!deleted && !isLimbo"
|
||||
:class="{'comment--loading': loading}"
|
||||
:class="{ 'comment--loading': loading }"
|
||||
class="comment">
|
||||
<!-- Comment header toolbar -->
|
||||
<div class="comment__side">
|
||||
<!-- Author -->
|
||||
<NcAvatar class="comment__avatar"
|
||||
<NcAvatar
|
||||
class="comment__avatar"
|
||||
:display-name="actorDisplayName"
|
||||
:user="actorId"
|
||||
:size="32" />
|
||||
|
|
@ -23,7 +25,8 @@
|
|||
show if we have a message id and current user is author -->
|
||||
<NcActions v-if="isOwnComment && id && !loading" class="comment__actions">
|
||||
<template v-if="!editing">
|
||||
<NcActionButton close-after-click
|
||||
<NcActionButton
|
||||
close-after-click
|
||||
@click="onEdit">
|
||||
<template #icon>
|
||||
<IconPencilOutline :size="20" />
|
||||
|
|
@ -31,7 +34,8 @@
|
|||
{{ t('comments', 'Edit comment') }}
|
||||
</NcActionButton>
|
||||
<NcActionSeparator />
|
||||
<NcActionButton close-after-click
|
||||
<NcActionButton
|
||||
close-after-click
|
||||
@click="onDeleteWithUndo">
|
||||
<template #icon>
|
||||
<IconTrashCanOutline :size="20" />
|
||||
|
|
@ -52,7 +56,8 @@
|
|||
<div v-if="id && loading" class="comment_loading icon-loading-small" />
|
||||
|
||||
<!-- Relative time to the comment creation -->
|
||||
<NcDateTime v-else-if="creationDateTime"
|
||||
<NcDateTime
|
||||
v-else-if="creationDateTime"
|
||||
class="comment__timestamp"
|
||||
:timestamp="timestamp"
|
||||
:ignore-seconds="true" />
|
||||
|
|
@ -61,19 +66,21 @@
|
|||
<!-- Message editor -->
|
||||
<form v-if="editor || editing" class="comment__editor" @submit.prevent>
|
||||
<div class="comment__editor-group">
|
||||
<NcRichContenteditable ref="editor"
|
||||
<NcRichContenteditable
|
||||
ref="editor"
|
||||
:auto-complete="autoComplete"
|
||||
:contenteditable="!loading"
|
||||
:label="editor ? t('comments', 'New comment') : t('comments', 'Edit comment')"
|
||||
:placeholder="t('comments', 'Write a comment …')"
|
||||
:placeholder="t('comments', 'Write a comment …')"
|
||||
:value="localMessage"
|
||||
:user-data="userData"
|
||||
aria-describedby="tab-comments__editor-description"
|
||||
@update:value="updateLocalMessage"
|
||||
@submit="onSubmit" />
|
||||
<div class="comment__submit">
|
||||
<NcButton type="tertiary-no-background"
|
||||
native-type="submit"
|
||||
<NcButton
|
||||
variant="tertiary-no-background"
|
||||
type="submit"
|
||||
:aria-label="t('comments', 'Post comment')"
|
||||
:disabled="isEmptyMessage"
|
||||
@click="onSubmit">
|
||||
|
|
@ -90,9 +97,10 @@
|
|||
</form>
|
||||
|
||||
<!-- Message content -->
|
||||
<NcRichText v-else
|
||||
<NcRichText
|
||||
v-else
|
||||
class="comment__message"
|
||||
:class="{'comment__message--expanded': expanded}"
|
||||
:class="{ 'comment__message--expanded': expanded }"
|
||||
:text="richContent.message"
|
||||
:arguments="richContent.mentions"
|
||||
@click.native="onExpand" />
|
||||
|
|
@ -103,7 +111,7 @@
|
|||
<script>
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
import { mapStores } from 'pinia'
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import NcActions from '@nextcloud/vue/components/NcActions'
|
||||
import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
|
||||
|
|
@ -112,14 +120,11 @@ import NcButton from '@nextcloud/vue/components/NcButton'
|
|||
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import NcUserBubble from '@nextcloud/vue/components/NcUserBubble'
|
||||
|
||||
import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
|
||||
import IconClose from 'vue-material-design-icons/Close.vue'
|
||||
import IconTrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
|
||||
import IconPencilOutline from 'vue-material-design-icons/PencilOutline.vue'
|
||||
|
||||
import IconTrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
|
||||
import CommentMixin from '../mixins/CommentMixin.js'
|
||||
import { mapStores } from 'pinia'
|
||||
import { useDeletedCommentLimbo } from '../store/deletedCommentLimbo.js'
|
||||
|
||||
// Dynamic loading
|
||||
|
|
@ -127,6 +132,7 @@ const NcRichContenteditable = () => import('@nextcloud/vue/components/NcRichCont
|
|||
const NcRichText = () => import('@nextcloud/vue/components/NcRichText')
|
||||
|
||||
export default {
|
||||
/* eslint vue/multi-word-component-names: "warn" */
|
||||
name: 'Comment',
|
||||
|
||||
components: {
|
||||
|
|
@ -144,6 +150,7 @@ export default {
|
|||
NcRichContenteditable,
|
||||
NcRichText,
|
||||
},
|
||||
|
||||
mixins: [CommentMixin],
|
||||
|
||||
inheritAttrs: false,
|
||||
|
|
@ -153,10 +160,12 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
actorId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
creationDateTime: {
|
||||
type: String,
|
||||
default: null,
|
||||
|
|
@ -177,6 +186,7 @@ export default {
|
|||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
userData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { registerFileAction } from '@nextcloud/files'
|
||||
import { action } from './actions/inlineUnreadCommentsAction'
|
||||
import { action } from './actions/inlineUnreadCommentsAction.ts'
|
||||
|
||||
registerFileAction(action)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
*/
|
||||
|
||||
import { showError, showUndo, TOAST_UNDO_TIMEOUT } from '@nextcloud/dialogs'
|
||||
import NewComment from '../services/NewComment.js'
|
||||
import { mapStores } from 'pinia'
|
||||
import logger from '../logger.js'
|
||||
import DeleteComment from '../services/DeleteComment.js'
|
||||
import EditComment from '../services/EditComment.js'
|
||||
import { mapStores } from 'pinia'
|
||||
import NewComment from '../services/NewComment.js'
|
||||
import { useDeletedCommentLimbo } from '../store/deletedCommentLimbo.js'
|
||||
import logger from '../logger.js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
|
@ -62,7 +62,7 @@ export default {
|
|||
this.editing = false
|
||||
} catch (error) {
|
||||
showError(t('comments', 'An error occurred while trying to edit the comment'))
|
||||
console.error(error)
|
||||
logger.error('An error occurred while trying to edit the comment', { error })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ export default {
|
|||
this.$emit('delete', this.id)
|
||||
} catch (error) {
|
||||
showError(t('comments', 'An error occurred while trying to delete the comment'))
|
||||
console.error(error)
|
||||
logger.error('An error occurred while trying to delete the comment', { error })
|
||||
this.deleted = false
|
||||
this.deletedCommentLimboStore.removeId(this.id)
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@ export default {
|
|||
this.localMessage = ''
|
||||
} catch (error) {
|
||||
showError(t('comments', 'An error occurred while trying to create the comment'))
|
||||
console.error(error)
|
||||
logger.error('An error occurred while trying to create the comment', { error })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import axios from '@nextcloud/axios'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { defineComponent } from 'vue'
|
||||
|
|
@ -33,8 +33,8 @@ export default defineComponent({
|
|||
/**
|
||||
* Autocomplete @mentions
|
||||
*
|
||||
* @param {string} search the query
|
||||
* @param {Function} callback the callback to process the results with
|
||||
* @param search the query
|
||||
* @param callback the callback to process the results with
|
||||
*/
|
||||
async autoComplete(search, callback) {
|
||||
const { data } = await axios.get(generateOcsUrl('core/autocomplete/get'), {
|
||||
|
|
@ -47,7 +47,9 @@ export default defineComponent({
|
|||
},
|
||||
})
|
||||
// Save user data so it can be used by the editor to replace mentions
|
||||
data.ocs.data.forEach(user => { this.userData[user.id] = user })
|
||||
data.ocs.data.forEach((user) => {
|
||||
this.userData[user.id] = user
|
||||
})
|
||||
return callback(Object.values(this.userData))
|
||||
},
|
||||
|
||||
|
|
@ -60,7 +62,7 @@ export default defineComponent({
|
|||
genMentionsData(mentions: any[]): Record<string, object> {
|
||||
Object.values(mentions)
|
||||
.flat()
|
||||
.forEach(mention => {
|
||||
.forEach((mention) => {
|
||||
this.userData[mention.mentionId] = {
|
||||
// TODO: support groups
|
||||
icon: 'icon-user',
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
*/
|
||||
|
||||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
import { t, n } from '@nextcloud/l10n'
|
||||
import { PiniaVuePlugin, createPinia } from 'pinia'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia'
|
||||
import Vue from 'vue'
|
||||
import CommentsApp from '../views/Comments.vue'
|
||||
import logger from '../logger.js'
|
||||
|
||||
Vue.use(PiniaVuePlugin)
|
||||
// eslint-disable-next-line camelcase
|
||||
|
||||
__webpack_nonce__ = getCSPNonce()
|
||||
|
||||
// Add translates functions
|
||||
|
|
@ -28,7 +28,6 @@ Vue.mixin({
|
|||
})
|
||||
|
||||
export default class CommentInstance {
|
||||
|
||||
/**
|
||||
* Initialize a new Comments instance for the desired type
|
||||
*
|
||||
|
|
@ -51,5 +50,4 @@ export default class CommentInstance {
|
|||
const View = Vue.extend(CommentsApp)
|
||||
return new View(options)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,18 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'
|
||||
import { createClient } from 'webdav'
|
||||
import { getRootPath } from '../utils/davUtils.js'
|
||||
import { getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'
|
||||
|
||||
// init webdav client
|
||||
const client = createClient(getRootPath())
|
||||
|
||||
// set CSRF token header
|
||||
const setHeaders = (token) => {
|
||||
/**
|
||||
* @param token
|
||||
*/
|
||||
function setHeaders(token) {
|
||||
client.setHeaders({
|
||||
// Add this so the server knows it is an request from the browser
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import client from './DavClient.js'
|
|||
export default async function(resourceType, resourceId, commentId, message) {
|
||||
const commentPath = ['', resourceType, resourceId, commentId].join('/')
|
||||
|
||||
return await client.customRequest(commentPath, Object.assign({
|
||||
return await client.customRequest(commentPath, {
|
||||
method: 'PROPPATCH',
|
||||
data: `<?xml version="1.0"?>
|
||||
<d:propertyupdate
|
||||
|
|
@ -28,5 +28,5 @@ export default async function(resourceType, resourceId, commentId, message) {
|
|||
</d:prop>
|
||||
</d:set>
|
||||
</d:propertyupdate>`,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { parseXML, type DAVResult, type FileStat, type ResponseDataDetailed } from 'webdav'
|
||||
import type { DAVResult, FileStat, ResponseDataDetailed } from 'webdav'
|
||||
|
||||
// https://github.com/perry-mitchell/webdav-client/issues/339
|
||||
import { parseXML } from 'webdav'
|
||||
import { processResponsePayload } from 'webdav/dist/node/response.js'
|
||||
import { prepareFileFromProps } from 'webdav/dist/node/tools/dav.js'
|
||||
import client from './DavClient.js'
|
||||
|
|
@ -15,19 +15,19 @@ export const DEFAULT_LIMIT = 20
|
|||
/**
|
||||
* Retrieve the comments list
|
||||
*
|
||||
* @param {object} data destructuring object
|
||||
* @param {string} data.resourceType the resource type
|
||||
* @param {number} data.resourceId the resource ID
|
||||
* @param {object} [options] optional options for axios
|
||||
* @param {number} [options.offset] the pagination offset
|
||||
* @param {number} [options.limit] the pagination limit, defaults to 20
|
||||
* @param {Date} [options.datetime] optional date to query
|
||||
* @return {{data: object[]}} the comments list
|
||||
* @param data destructuring object
|
||||
* @param data.resourceType the resource type
|
||||
* @param data.resourceId the resource ID
|
||||
* @param [options] optional options for axios
|
||||
* @param [options.offset] the pagination offset
|
||||
* @param [options.limit] the pagination limit, defaults to 20
|
||||
* @param [options.datetime] optional date to query
|
||||
* @return the comments list
|
||||
*/
|
||||
export const getComments = async function({ resourceType, resourceId }, options: { offset: number, limit?: number, datetime?: Date }) {
|
||||
export async function getComments({ resourceType, resourceId }, options: { offset: number, limit?: number, datetime?: Date }) {
|
||||
const resourcePath = ['', resourceType, resourceId].join('/')
|
||||
const datetime = options.datetime ? `<oc:datetime>${options.datetime.toISOString()}</oc:datetime>` : ''
|
||||
const response = await client.customRequest(resourcePath, Object.assign({
|
||||
const response = await client.customRequest(resourcePath, {
|
||||
method: 'REPORT',
|
||||
data: `<?xml version="1.0"?>
|
||||
<oc:filter-comments
|
||||
|
|
@ -39,16 +39,23 @@ export const getComments = async function({ resourceType, resourceId }, options:
|
|||
<oc:offset>${options.offset || 0}</oc:offset>
|
||||
${datetime}
|
||||
</oc:filter-comments>`,
|
||||
}, options))
|
||||
...options,
|
||||
})
|
||||
|
||||
const responseData = await response.text()
|
||||
const result = await parseXML(responseData)
|
||||
const stat = getDirectoryFiles(result, true)
|
||||
// https://github.com/perry-mitchell/webdav-client/issues/339
|
||||
return processResponsePayload(response, stat, true) as ResponseDataDetailed<FileStat[]>
|
||||
}
|
||||
|
||||
// https://github.com/perry-mitchell/webdav-client/blob/8d9694613c978ce7404e26a401c39a41f125f87f/source/operations/directoryContents.ts
|
||||
const getDirectoryFiles = function(
|
||||
/**
|
||||
* https://github.com/perry-mitchell/webdav-client/blob/8d9694613c978ce7404e26a401c39a41f125f87f/source/operations/directoryContents.ts
|
||||
*
|
||||
* @param result
|
||||
* @param isDetailed
|
||||
*/
|
||||
function getDirectoryFiles(
|
||||
result: DAVResult,
|
||||
isDetailed = false,
|
||||
): Array<FileStat> {
|
||||
|
|
@ -58,7 +65,7 @@ const getDirectoryFiles = function(
|
|||
} = result
|
||||
|
||||
// Map all items to a consistent output structure (results)
|
||||
return responseItems.map(item => {
|
||||
return responseItems.map((item) => {
|
||||
// Each item should contain a stat object
|
||||
const props = item.propstat!.prop!
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
*/
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { getRootPath } from '../utils/davUtils.js'
|
||||
import { decodeHtmlEntities } from '../utils/decodeHtmlEntities.js'
|
||||
import axios from '@nextcloud/axios'
|
||||
import client from './DavClient.js'
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import client from './DavClient.js'
|
||||
|
||||
import type { Response } from 'webdav'
|
||||
|
||||
import client from './DavClient.js'
|
||||
|
||||
/**
|
||||
* Mark comments older than the date timestamp as read
|
||||
*
|
||||
|
|
@ -14,11 +14,11 @@ import type { Response } from 'webdav'
|
|||
* @param resourceId the resource ID
|
||||
* @param date the date object
|
||||
*/
|
||||
export const markCommentsAsRead = (
|
||||
export function markCommentsAsRead(
|
||||
resourceType: string,
|
||||
resourceId: number,
|
||||
date: Date,
|
||||
): Promise<Response> => {
|
||||
): Promise<Response> {
|
||||
const resourcePath = ['', resourceType, resourceId].join('/')
|
||||
const readMarker = date.toUTCString()
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
* @param {Function} request the axios promise request
|
||||
* @return {object}
|
||||
*/
|
||||
const cancelableRequest = function(request) {
|
||||
function cancelableRequest(request) {
|
||||
const controller = new AbortController()
|
||||
const signal = controller.signal
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ const cancelableRequest = function(request) {
|
|||
const fetch = async function(url, options) {
|
||||
const response = await request(
|
||||
url,
|
||||
Object.assign({ signal }, options),
|
||||
{ signal, ...options },
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
|
||||
const getRootPath = function() {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function getRootPath() {
|
||||
return generateRemoteUrl('dav/comments')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<Comment v-bind="editorData"
|
||||
<Comment
|
||||
v-bind="editorData"
|
||||
:auto-complete="autoComplete"
|
||||
:resource-type="resourceType"
|
||||
:editor="true"
|
||||
|
|
@ -15,17 +16,18 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { defineComponent } from 'vue'
|
||||
import Comment from '../components/Comment.vue'
|
||||
import logger from '../logger.js'
|
||||
import CommentView from '../mixins/CommentView.js'
|
||||
import logger from '../logger'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Comment,
|
||||
},
|
||||
|
||||
mixins: [CommentView],
|
||||
props: {
|
||||
reloadCallback: {
|
||||
|
|
@ -33,14 +35,15 @@ export default defineComponent({
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onNewComment() {
|
||||
try {
|
||||
// just force reload
|
||||
this.reloadCallback()
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
showError(t('comments', 'Could not reload comments'))
|
||||
logger.debug(e)
|
||||
logger.error('Could not reload comments', { error })
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<Comment ref="comment"
|
||||
<Comment
|
||||
ref="comment"
|
||||
tag="li"
|
||||
v-bind="comment.props"
|
||||
:auto-complete="autoComplete"
|
||||
|
|
@ -18,10 +19,10 @@
|
|||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import Comment from '../components/Comment.vue'
|
||||
import CommentView from '../mixins/CommentView'
|
||||
import CommentView from '../mixins/CommentView.ts'
|
||||
|
||||
export default {
|
||||
name: 'ActivityCommentEntry',
|
||||
|
|
@ -36,6 +37,7 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
reloadCallback: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: true,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-element-visibility="onVisibilityChange"
|
||||
<div
|
||||
v-element-visibility="onVisibilityChange"
|
||||
class="comments"
|
||||
:class="{ 'icon-loading': isFirstLoading }">
|
||||
<!-- Editor -->
|
||||
<Comment v-bind="editorData"
|
||||
<Comment
|
||||
v-bind="editorData"
|
||||
:auto-complete="autoComplete"
|
||||
:resource-type="resourceType"
|
||||
:editor="true"
|
||||
|
|
@ -18,7 +20,8 @@
|
|||
@new="onNewComment" />
|
||||
|
||||
<template v-if="!isFirstLoading">
|
||||
<NcEmptyContent v-if="!hasComments && done"
|
||||
<NcEmptyContent
|
||||
v-if="!hasComments && done"
|
||||
class="comments__empty"
|
||||
:name="t('comments', 'No comments yet, start the conversation!')">
|
||||
<template #icon>
|
||||
|
|
@ -27,7 +30,8 @@
|
|||
</NcEmptyContent>
|
||||
<ul v-else>
|
||||
<!-- Comments -->
|
||||
<Comment v-for="comment in comments"
|
||||
<Comment
|
||||
v-for="comment in comments"
|
||||
:key="comment.props.id"
|
||||
tag="li"
|
||||
v-bind="comment.props"
|
||||
|
|
@ -69,20 +73,20 @@
|
|||
import { showError } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { vElementVisibility as elementVisibility } from '@vueuse/components'
|
||||
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import IconRefresh from 'vue-material-design-icons/Refresh.vue'
|
||||
import IconMessageReplyTextOutline from 'vue-material-design-icons/MessageReplyTextOutline.vue'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import IconAlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
|
||||
|
||||
import IconMessageReplyTextOutline from 'vue-material-design-icons/MessageReplyTextOutline.vue'
|
||||
import IconRefresh from 'vue-material-design-icons/Refresh.vue'
|
||||
import Comment from '../components/Comment.vue'
|
||||
import CommentView from '../mixins/CommentView'
|
||||
import cancelableRequest from '../utils/cancelableRequest.js'
|
||||
import { getComments, DEFAULT_LIMIT } from '../services/GetComments.ts'
|
||||
import logger from '../logger.js'
|
||||
import CommentView from '../mixins/CommentView.ts'
|
||||
import { DEFAULT_LIMIT, getComments } from '../services/GetComments.ts'
|
||||
import { markCommentsAsRead } from '../services/ReadComments.ts'
|
||||
import cancelableRequest from '../utils/cancelableRequest.js'
|
||||
|
||||
export default {
|
||||
/* eslint vue/multi-word-component-names: "warn" */
|
||||
name: 'Comments',
|
||||
|
||||
components: {
|
||||
|
|
@ -121,6 +125,7 @@ export default {
|
|||
hasComments() {
|
||||
return this.comments.length > 0
|
||||
},
|
||||
|
||||
isFirstLoading() {
|
||||
return this.loading && this.offset === 0
|
||||
},
|
||||
|
|
@ -211,7 +216,7 @@ export default {
|
|||
return
|
||||
}
|
||||
this.error = t('comments', 'Unable to load the comments list')
|
||||
console.error('Error loading the comments list', error)
|
||||
logger.error('Error loading the comments list', { error })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
|
|
@ -232,11 +237,11 @@ export default {
|
|||
* @param {number} id the deleted comment
|
||||
*/
|
||||
onDelete(id) {
|
||||
const index = this.comments.findIndex(comment => comment.props.id === id)
|
||||
const index = this.comments.findIndex((comment) => comment.props.id === id)
|
||||
if (index > -1) {
|
||||
this.comments.splice(index, 1)
|
||||
} else {
|
||||
console.error('Could not find the deleted comment in the list', id)
|
||||
logger.error('Could not find the deleted comment in the list', { id })
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -6,20 +6,23 @@
|
|||
<main id="app-dashboard">
|
||||
<h2>{{ greeting.text }}</h2>
|
||||
<ul class="statuses">
|
||||
<li v-for="status in sortedRegisteredStatus"
|
||||
<li
|
||||
v-for="status in sortedRegisteredStatus"
|
||||
:id="'status-' + status"
|
||||
:key="status">
|
||||
<div :ref="'status-' + status" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Draggable v-model="layout"
|
||||
<Draggable
|
||||
v-model="layout"
|
||||
class="panels"
|
||||
v-bind="{swapThreshold: 0.30, delay: 500, delayOnTouchOnly: true, touchStartThreshold: 3}"
|
||||
v-bind="{ swapThreshold: 0.30, delay: 500, delayOnTouchOnly: true, touchStartThreshold: 3 }"
|
||||
handle=".panel--header"
|
||||
@end="saveLayout">
|
||||
<template v-for="panelId in layout">
|
||||
<div v-if="isApiWidgetV2(panels[panelId].id)"
|
||||
<div
|
||||
v-if="isApiWidgetV2(panels[panelId].id)"
|
||||
:key="`${panels[panelId].id}-v2`"
|
||||
class="panel">
|
||||
<div class="panel--header">
|
||||
|
|
@ -30,7 +33,8 @@
|
|||
</h2>
|
||||
</div>
|
||||
<div class="panel--content">
|
||||
<ApiDashboardWidget :widget="apiWidgets[panels[panelId].id]"
|
||||
<ApiDashboardWidget
|
||||
:widget="apiWidgets[panels[panelId].id]"
|
||||
:data="apiWidgetItems[panels[panelId].id]"
|
||||
:loading="loadingItems" />
|
||||
</div>
|
||||
|
|
@ -63,7 +67,8 @@
|
|||
<h2>{{ t('dashboard', 'Edit widgets') }}</h2>
|
||||
<ol class="panels">
|
||||
<li v-for="status in sortedAllStatuses" :key="status" :class="'panel-' + status">
|
||||
<input :id="'status-checkbox-' + status"
|
||||
<input
|
||||
:id="'status-checkbox-' + status"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="isStatusActive(status)"
|
||||
|
|
@ -75,14 +80,16 @@
|
|||
</label>
|
||||
</li>
|
||||
</ol>
|
||||
<Draggable v-model="layout"
|
||||
<Draggable
|
||||
v-model="layout"
|
||||
class="panels"
|
||||
tag="ol"
|
||||
v-bind="{swapThreshold: 0.30, delay: 500, delayOnTouchOnly: true, touchStartThreshold: 3}"
|
||||
v-bind="{ swapThreshold: 0.30, delay: 500, delayOnTouchOnly: true, touchStartThreshold: 3 }"
|
||||
handle=".draggable"
|
||||
@end="saveLayout">
|
||||
<li v-for="panel in sortedPanels" :key="panel.id" :class="'panel-' + panel.id">
|
||||
<input :id="'panel-checkbox-' + panel.id"
|
||||
<input
|
||||
:id="'panel-checkbox-' + panel.id"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:checked="isActive(panel)"
|
||||
|
|
@ -114,19 +121,19 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { generateUrl, generateOcsUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import axios from '@nextcloud/axios'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
|
||||
import Vue from 'vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcModal from '@nextcloud/vue/components/NcModal'
|
||||
import NcUserStatusIcon from '@nextcloud/vue/components/NcUserStatusIcon'
|
||||
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
||||
import Vue from 'vue'
|
||||
|
||||
import isMobile from './mixins/isMobile.js'
|
||||
import ApiDashboardWidget from './components/ApiDashboardWidget.vue'
|
||||
import { logger } from './logger.ts'
|
||||
import isMobile from './mixins/isMobile.js'
|
||||
|
||||
const panels = loadState('dashboard', 'panels')
|
||||
const firstRun = loadState('dashboard', 'firstRun')
|
||||
|
|
@ -152,6 +159,7 @@ export default {
|
|||
Pencil,
|
||||
NcUserStatusIcon,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
isMobile,
|
||||
],
|
||||
|
|
@ -187,6 +195,7 @@ export default {
|
|||
birthdate,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
greeting() {
|
||||
const time = this.timer.getHours()
|
||||
|
|
@ -214,19 +223,23 @@ export default {
|
|||
generic: t('dashboard', 'Good morning'),
|
||||
withName: t('dashboard', 'Good morning, {name}', { name: this.displayName }, undefined, { escape: false }),
|
||||
},
|
||||
|
||||
afternoon: {
|
||||
generic: t('dashboard', 'Good afternoon'),
|
||||
withName: t('dashboard', 'Good afternoon, {name}', { name: this.displayName }, undefined, { escape: false }),
|
||||
},
|
||||
|
||||
evening: {
|
||||
generic: t('dashboard', 'Good evening'),
|
||||
withName: t('dashboard', 'Good evening, {name}', { name: this.displayName }, undefined, { escape: false }),
|
||||
},
|
||||
|
||||
night: {
|
||||
// Don't use "Good night" as it's not a greeting
|
||||
generic: t('dashboard', 'Hello'),
|
||||
withName: t('dashboard', 'Hello, {name}', { name: this.displayName }, undefined, { escape: false }),
|
||||
},
|
||||
|
||||
birthday: {
|
||||
generic: t('dashboard', 'Happy birthday 🥳🤩🎂🎉'),
|
||||
withName: t('dashboard', 'Happy birthday, {name} 🥳🤩🎂🎉', { name: this.displayName }, undefined, { escape: false }),
|
||||
|
|
@ -241,6 +254,7 @@ export default {
|
|||
isActive() {
|
||||
return (panel) => this.layout.indexOf(panel.id) > -1
|
||||
},
|
||||
|
||||
isStatusActive() {
|
||||
return (status) => this.enabledStatuses.findIndex((s) => s === status) !== -1
|
||||
},
|
||||
|
|
@ -248,6 +262,7 @@ export default {
|
|||
sortedAllStatuses() {
|
||||
return Object.keys(this.allCallbacksStatus).slice().sort(this.sortStatuses)
|
||||
},
|
||||
|
||||
sortedPanels() {
|
||||
return Object.values(this.panels).sort((a, b) => {
|
||||
const indexA = this.layout.indexOf(a.id)
|
||||
|
|
@ -258,6 +273,7 @@ export default {
|
|||
return indexA - indexB || a.id - b.id
|
||||
})
|
||||
},
|
||||
|
||||
sortedRegisteredStatus() {
|
||||
return this.registeredStatus.slice().sort(this.sortStatuses)
|
||||
},
|
||||
|
|
@ -267,6 +283,7 @@ export default {
|
|||
callbacks() {
|
||||
this.rerenderPanels()
|
||||
},
|
||||
|
||||
callbacksStatus() {
|
||||
for (const app in this.callbacksStatus) {
|
||||
const element = this.$refs['status-' + app]
|
||||
|
|
@ -277,7 +294,7 @@ export default {
|
|||
this.callbacksStatus[app](element[0])
|
||||
Vue.set(this.statuses, app, { mounted: true })
|
||||
} else {
|
||||
console.error('Failed to register panel in the frontend as no backend data was provided for ' + app)
|
||||
logger.error('Failed to register panel in the frontend as no backend data was provided for ' + app)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -288,9 +305,9 @@ export default {
|
|||
|
||||
const apiWidgetIdsToFetch = Object
|
||||
.values(this.apiWidgets)
|
||||
.filter(widget => this.isApiWidgetV2(widget.id) && this.layout.includes(widget.id))
|
||||
.map(widget => widget.id)
|
||||
await Promise.all(apiWidgetIdsToFetch.map(id => this.fetchApiWidgetItems([id], true)))
|
||||
.filter((widget) => this.isApiWidgetV2(widget.id) && this.layout.includes(widget.id))
|
||||
.map((widget) => widget.id)
|
||||
await Promise.all(apiWidgetIdsToFetch.map((id) => this.fetchApiWidgetItems([id], true)))
|
||||
|
||||
for (const widget of Object.values(this.apiWidgets)) {
|
||||
if (widget.reload_interval > 0) {
|
||||
|
|
@ -304,6 +321,7 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateSkipLink()
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
|
|
@ -316,6 +334,7 @@ export default {
|
|||
window.addEventListener('scroll', this.disableFirstrunHint)
|
||||
}
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
},
|
||||
|
|
@ -330,6 +349,7 @@ export default {
|
|||
register(app, callback) {
|
||||
Vue.set(this.callbacks, app, callback)
|
||||
},
|
||||
|
||||
registerStatus(app, callback) {
|
||||
// always save callbacks in case user enables the status later
|
||||
Vue.set(this.allCallbacksStatus, app, callback)
|
||||
|
|
@ -341,6 +361,7 @@ export default {
|
|||
})
|
||||
}
|
||||
},
|
||||
|
||||
rerenderPanels() {
|
||||
for (const app in this.callbacks) {
|
||||
// TODO: Properly rerender v2 widgets
|
||||
|
|
@ -361,27 +382,32 @@ export default {
|
|||
})
|
||||
Vue.set(this.panels[app], 'mounted', true)
|
||||
} else {
|
||||
console.error('Failed to register panel in the frontend as no backend data was provided for ' + app)
|
||||
logger.error('Failed to register panel in the frontend as no backend data was provided for ' + app)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
saveLayout() {
|
||||
axios.post(generateOcsUrl('/apps/dashboard/api/v3/layout'), {
|
||||
layout: this.layout,
|
||||
})
|
||||
},
|
||||
|
||||
saveStatuses() {
|
||||
axios.post(generateOcsUrl('/apps/dashboard/api/v3/statuses'), {
|
||||
statuses: this.enabledStatuses,
|
||||
})
|
||||
},
|
||||
|
||||
showModal() {
|
||||
this.modal = true
|
||||
this.firstRun = false
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.modal = false
|
||||
},
|
||||
|
||||
updateCheckbox(panel, currentValue) {
|
||||
const index = this.layout.indexOf(panel.id)
|
||||
if (!currentValue && index > -1) {
|
||||
|
|
@ -396,16 +422,19 @@ export default {
|
|||
this.saveLayout()
|
||||
this.$nextTick(() => this.rerenderPanels())
|
||||
},
|
||||
|
||||
disableFirstrunHint() {
|
||||
window.removeEventListener('scroll', this.disableFirstrunHint)
|
||||
setTimeout(() => {
|
||||
this.firstRun = false
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
updateSkipLink() {
|
||||
// Make sure "Skip to main content" link points to the app content
|
||||
document.getElementsByClassName('skip-navigation')[0].setAttribute('href', '#app-dashboard')
|
||||
},
|
||||
|
||||
updateStatusCheckbox(app, checked) {
|
||||
if (checked) {
|
||||
this.enableStatus(app)
|
||||
|
|
@ -413,11 +442,13 @@ export default {
|
|||
this.disableStatus(app)
|
||||
}
|
||||
},
|
||||
|
||||
enableStatus(app) {
|
||||
this.enabledStatuses.push(app)
|
||||
this.registerStatus(app, this.allCallbacksStatus[app])
|
||||
this.saveStatuses()
|
||||
},
|
||||
|
||||
disableStatus(app) {
|
||||
const i = this.enabledStatuses.findIndex((s) => s === app)
|
||||
if (i !== -1) {
|
||||
|
|
@ -433,6 +464,7 @@ export default {
|
|||
}
|
||||
this.saveStatuses()
|
||||
},
|
||||
|
||||
sortStatuses(a, b) {
|
||||
const al = a.toLowerCase()
|
||||
const bl = b.toLowerCase()
|
||||
|
|
@ -442,6 +474,7 @@ export default {
|
|||
? -1
|
||||
: 0
|
||||
},
|
||||
|
||||
handleScroll() {
|
||||
if (window.scrollY > 70) {
|
||||
document.body.classList.add('dashboard--scrolled')
|
||||
|
|
@ -449,18 +482,20 @@ export default {
|
|||
document.body.classList.remove('dashboard--scrolled')
|
||||
}
|
||||
},
|
||||
|
||||
async fetchApiWidgets() {
|
||||
const { data } = await axios.get(generateOcsUrl('/apps/dashboard/api/v1/widgets'))
|
||||
this.apiWidgets = data.ocs.data
|
||||
},
|
||||
|
||||
async fetchApiWidgetItems(widgetIds, merge = false) {
|
||||
try {
|
||||
const url = generateOcsUrl('/apps/dashboard/api/v2/widget-items')
|
||||
const params = new URLSearchParams(widgetIds.map(id => ['widgets[]', id]))
|
||||
const params = new URLSearchParams(widgetIds.map((id) => ['widgets[]', id]))
|
||||
const response = await axios.get(`${url}?${params.toString()}`)
|
||||
const widgetItems = response.data.ocs.data
|
||||
if (merge) {
|
||||
this.apiWidgetItems = Object.assign({}, this.apiWidgetItems, widgetItems)
|
||||
this.apiWidgetItems = { ...this.apiWidgetItems, ...widgetItems }
|
||||
} else {
|
||||
this.apiWidgetItems = widgetItems
|
||||
}
|
||||
|
|
@ -468,6 +503,7 @@ export default {
|
|||
this.loadingItems = false
|
||||
}
|
||||
},
|
||||
|
||||
isApiWidgetV2(id) {
|
||||
for (const widget of Object.values(this.apiWidgets)) {
|
||||
if (widget.id === id && widget.item_api_versions.includes(2)) {
|
||||
|
|
@ -752,6 +788,7 @@ export default {
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
background-attachment: fixed;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<NcDashboardWidget :items="items"
|
||||
<NcDashboardWidget
|
||||
:items="items"
|
||||
:show-more-label="showMoreLabel"
|
||||
:show-more-url="showMoreUrl"
|
||||
:loading="loading"
|
||||
|
|
@ -13,7 +14,8 @@
|
|||
<ApiDashboardWidgetItem :item="item" :icon-size="iconSize" :rounded-icons="widget.item_icons_round" />
|
||||
</template>
|
||||
<template #empty-content>
|
||||
<NcEmptyContent v-if="items.length === 0"
|
||||
<NcEmptyContent
|
||||
v-if="items.length === 0"
|
||||
:description="emptyContentMessage">
|
||||
<template #icon>
|
||||
<CheckIcon v-if="emptyContentMessage" :size="65" />
|
||||
|
|
@ -44,25 +46,30 @@ export default {
|
|||
NcEmptyContent,
|
||||
NcButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
widget: {
|
||||
type: [Object, undefined],
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
data: {
|
||||
type: [Object, undefined],
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
iconSize: 44,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/** @return {object[]} */
|
||||
items() {
|
||||
|
|
@ -84,17 +91,17 @@ export default {
|
|||
// TODO: Render new button in the template
|
||||
// I couldn't find a widget that makes use of the button. Furthermore, there is no convenient
|
||||
// way to render such a button using the official widget component.
|
||||
return this.widget?.buttons?.find(button => button.type === 'new')
|
||||
return this.widget?.buttons?.find((button) => button.type === 'new')
|
||||
},
|
||||
|
||||
/** @return {object|undefined} */
|
||||
moreButton() {
|
||||
return this.widget?.buttons?.find(button => button.type === 'more')
|
||||
return this.widget?.buttons?.find((button) => button.type === 'more')
|
||||
},
|
||||
|
||||
/** @return {object|undefined} */
|
||||
setupButton() {
|
||||
return this.widget?.buttons?.find(button => button.type === 'setup')
|
||||
return this.widget?.buttons?.find((button) => button.type === 'setup')
|
||||
},
|
||||
|
||||
/** @return {string|undefined} */
|
||||
|
|
@ -107,6 +114,7 @@ export default {
|
|||
return this.moreButton?.link
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const size = window.getComputedStyle(document.body).getPropertyValue('--default-clickable-area')
|
||||
const numeric = Number.parseFloat(size)
|
||||
|
|
|
|||
|
|
@ -34,25 +34,29 @@ const loadingImageFailed = ref(false)
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<NcDashboardWidgetItem :target-url="item.link"
|
||||
<NcDashboardWidgetItem
|
||||
:target-url="item.link"
|
||||
:overlay-icon-url="item.overlayIconUrl ? item.overlayIconUrl : ''"
|
||||
:main-text="item.title"
|
||||
:sub-text="item.subtitle">
|
||||
<template #avatar>
|
||||
<template v-if="item.iconUrl">
|
||||
<NcAvatar v-if="roundedIcons"
|
||||
<NcAvatar
|
||||
v-if="roundedIcons"
|
||||
:size="iconSize"
|
||||
:url="item.iconUrl" />
|
||||
<template v-else>
|
||||
<img v-show="!loadingImageFailed"
|
||||
<img
|
||||
v-show="!loadingImageFailed"
|
||||
alt=""
|
||||
class="api-dashboard-widget-item__icon"
|
||||
:class="{'hidden-visually': !imageLoaded }"
|
||||
:class="{ 'hidden-visually': !imageLoaded }"
|
||||
:src="item.iconUrl"
|
||||
@error="loadingImageFailed = true"
|
||||
@load="imageLoaded = true">
|
||||
<!-- Placeholder while the image is loaded and also the fallback if the URL is broken -->
|
||||
<IconFile v-if="!imageLoaded"
|
||||
<IconFile
|
||||
v-if="!imageLoaded"
|
||||
:size="iconSize" />
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
11
apps/dashboard/src/logger.ts
Normal file
11
apps/dashboard/src/logger.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { getLoggerBuilder } from '@nextcloud/logger'
|
||||
|
||||
export const logger = getLoggerBuilder()
|
||||
.detectLogLevel()
|
||||
.setApp('dashboard')
|
||||
.build()
|
||||
|
|
@ -7,10 +7,8 @@ import { getCSPNonce } from '@nextcloud/auth'
|
|||
import { t } from '@nextcloud/l10n'
|
||||
import VTooltip from '@nextcloud/vue/directives/Tooltip'
|
||||
import Vue from 'vue'
|
||||
|
||||
import DashboardApp from './DashboardApp.vue'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = getCSPNonce()
|
||||
|
||||
Vue.directive('Tooltip', VTooltip)
|
||||
|
|
|
|||
|
|
@ -6,42 +6,47 @@
|
|||
<template>
|
||||
<form class="absence" @submit.prevent="saveForm">
|
||||
<div class="absence__dates">
|
||||
<NcDateTimePickerNative id="absence-first-day"
|
||||
<NcDateTimePickerNative
|
||||
id="absence-first-day"
|
||||
v-model="firstDay"
|
||||
:label="$t('dav', 'First day')"
|
||||
class="absence__dates__picker"
|
||||
:required="true" />
|
||||
<NcDateTimePickerNative id="absence-last-day"
|
||||
<NcDateTimePickerNative
|
||||
id="absence-last-day"
|
||||
v-model="lastDay"
|
||||
:label="$t('dav', 'Last day (inclusive)')"
|
||||
class="absence__dates__picker"
|
||||
:required="true" />
|
||||
</div>
|
||||
<label for="replacement-search-input">{{ $t('dav', 'Out of office replacement (optional)') }}</label>
|
||||
<NcSelect ref="select"
|
||||
<NcSelect
|
||||
ref="select"
|
||||
v-model="replacementUser"
|
||||
input-id="replacement-search-input"
|
||||
:loading="searchLoading"
|
||||
:placeholder="$t('dav', 'Name of the replacement')"
|
||||
:clear-search-on-blur="() => false"
|
||||
:user-select="true"
|
||||
user-select
|
||||
:options="options"
|
||||
@search="asyncFind">
|
||||
<template #no-options="{ search }">
|
||||
{{ search ?$t('dav', 'No results.') : $t('dav', 'Start typing.') }}
|
||||
{{ search ? $t('dav', 'No results.') : $t('dav', 'Start typing.') }}
|
||||
</template>
|
||||
</NcSelect>
|
||||
<NcTextField :value.sync="status" :label="$t('dav', 'Short absence status')" :required="true" />
|
||||
<NcTextArea :value.sync="message" :label="$t('dav', 'Long absence Message')" :required="true" />
|
||||
|
||||
<div class="absence__buttons">
|
||||
<NcButton :disabled="loading || !valid"
|
||||
type="primary"
|
||||
native-type="submit">
|
||||
<NcButton
|
||||
:disabled="loading || !valid"
|
||||
variant="primary"
|
||||
type="submit">
|
||||
{{ $t('dav', 'Save') }}
|
||||
</NcButton>
|
||||
<NcButton :disabled="loading || !valid"
|
||||
type="error"
|
||||
<NcButton
|
||||
:disabled="loading || !valid"
|
||||
variant="error"
|
||||
@click="clearAbsence">
|
||||
{{ $t('dav', 'Disable absence') }}
|
||||
</NcButton>
|
||||
|
|
@ -51,21 +56,21 @@
|
|||
|
||||
<script>
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { ShareType } from '@nextcloud/sharing'
|
||||
import { formatDateAsYMD } from '../utils/date.js'
|
||||
import axios from '@nextcloud/axios'
|
||||
import debounce from 'debounce'
|
||||
import logger from '../service/logger.js'
|
||||
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import NcTextArea from '@nextcloud/vue/components/NcTextArea'
|
||||
import NcSelect from '@nextcloud/vue/components/NcSelect'
|
||||
import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
|
||||
import NcSelect from '@nextcloud/vue/components/NcSelect'
|
||||
import NcTextArea from '@nextcloud/vue/components/NcTextArea'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import logger from '../service/logger.js'
|
||||
import { formatDateAsYMD } from '../utils/date.js'
|
||||
|
||||
/* eslint @nextcloud/vue/no-deprecated-props: "warn" */
|
||||
export default {
|
||||
name: 'AbsenceForm',
|
||||
components: {
|
||||
|
|
@ -75,6 +80,7 @@ export default {
|
|||
NcDateTimePickerNative,
|
||||
NcSelect,
|
||||
},
|
||||
|
||||
data() {
|
||||
const { firstDay, lastDay, status, message, replacementUserId, replacementUserDisplayName } = loadState('dav', 'absence', {})
|
||||
return {
|
||||
|
|
@ -89,6 +95,7 @@ export default {
|
|||
options: [],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* @return {boolean}
|
||||
|
|
@ -107,6 +114,7 @@ export default {
|
|||
&& lastDay >= firstDay
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetForm() {
|
||||
this.status = ''
|
||||
|
|
@ -121,7 +129,7 @@ export default {
|
|||
* @param {object} result select entry item
|
||||
* @return {object}
|
||||
*/
|
||||
formatForMultiselect(result) {
|
||||
formatForMultiselect(result) {
|
||||
return {
|
||||
user: result.uuid || result.value.shareWith,
|
||||
displayName: result.name || result.label,
|
||||
|
|
@ -133,13 +141,13 @@ export default {
|
|||
this.searchLoading = true
|
||||
await this.debounceGetSuggestions(query.trim())
|
||||
},
|
||||
|
||||
/**
|
||||
* Get suggestions
|
||||
*
|
||||
* @param {string} search the search query
|
||||
*/
|
||||
async getSuggestions(search) {
|
||||
|
||||
async getSuggestions(search) {
|
||||
const shareType = [
|
||||
ShareType.User,
|
||||
]
|
||||
|
|
@ -155,7 +163,7 @@ export default {
|
|||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching suggestions', error)
|
||||
logger.error('Error fetching suggestions', { error })
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -164,13 +172,12 @@ export default {
|
|||
data.exact = [] // removing exact from general results
|
||||
const rawExactSuggestions = exact.users
|
||||
const rawSuggestions = data.users
|
||||
console.info('rawExactSuggestions', rawExactSuggestions)
|
||||
console.info('rawSuggestions', rawSuggestions)
|
||||
logger.info('AbsenceForm raw suggestions', { rawExactSuggestions, rawSuggestions })
|
||||
// remove invalid data and format to user-select layout
|
||||
const exactSuggestions = rawExactSuggestions
|
||||
.map(share => this.formatForMultiselect(share))
|
||||
.map((share) => this.formatForMultiselect(share))
|
||||
const suggestions = rawSuggestions
|
||||
.map(share => this.formatForMultiselect(share))
|
||||
.map((share) => this.formatForMultiselect(share))
|
||||
|
||||
const allSuggestions = exactSuggestions.concat(suggestions)
|
||||
|
||||
|
|
@ -186,7 +193,7 @@ export default {
|
|||
return nameCounts
|
||||
}, {})
|
||||
|
||||
this.options = allSuggestions.map(item => {
|
||||
this.options = allSuggestions.map((item) => {
|
||||
// Make sure that items with duplicate displayName get the shareWith applied as a description
|
||||
if (nameCounts[item.displayName] > 1 && !item.desc) {
|
||||
return { ...item, desc: item.shareWithDisplayNameUnique }
|
||||
|
|
@ -195,7 +202,7 @@ export default {
|
|||
})
|
||||
|
||||
this.searchLoading = false
|
||||
console.info('suggestions', this.options)
|
||||
logger.info('AbsenseForm suggestions', { options: this.options })
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -203,7 +210,7 @@ export default {
|
|||
*
|
||||
* @param {...*} args the arguments
|
||||
*/
|
||||
debounceGetSuggestions: debounce(function(...args) {
|
||||
debounceGetSuggestions: debounce(function(...args) {
|
||||
this.getSuggestions(...args)
|
||||
}, 300),
|
||||
|
||||
|
|
@ -229,6 +236,7 @@ export default {
|
|||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async clearAbsence() {
|
||||
this.loading = true
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
-->
|
||||
<template>
|
||||
<div>
|
||||
<CalendarAvailability :slots.sync="slots"
|
||||
<CalendarAvailability
|
||||
:slots.sync="slots"
|
||||
:loading="loading"
|
||||
:l10n-to="t('dav', 'to')"
|
||||
:l10n-delete-slot="t('dav', 'Delete slot')"
|
||||
|
|
@ -25,7 +26,8 @@
|
|||
{{ t('dav', 'Automatically set user status to "Do not disturb" outside of availability to mute all notifications.') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<NcButton :disabled="loading || saving"
|
||||
<NcButton
|
||||
:disabled="loading || saving"
|
||||
variant="primary"
|
||||
@click="save">
|
||||
{{ t('dav', 'Save') }}
|
||||
|
|
@ -35,26 +37,26 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { CalendarAvailability } from '@nextcloud/calendar-availability-vue'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import {
|
||||
showError,
|
||||
showSuccess,
|
||||
} from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import {
|
||||
findScheduleInboxAvailability,
|
||||
getEmptySlots,
|
||||
saveScheduleInboxAvailability,
|
||||
} from '../service/CalendarService.js'
|
||||
import {
|
||||
enableUserStatusAutomation,
|
||||
disableUserStatusAutomation,
|
||||
} from '../service/PreferenceService.js'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import logger from '../service/logger.js'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import {
|
||||
disableUserStatusAutomation,
|
||||
enableUserStatusAutomation,
|
||||
} from '../service/PreferenceService.js'
|
||||
|
||||
// @ts-expect-error capabilities is missing the capability to type it...
|
||||
const timezone = getCapabilities().core.user?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
|
|
@ -95,9 +97,8 @@ async function save() {
|
|||
}
|
||||
|
||||
showSuccess(t('dav', 'Saved availability'))
|
||||
} catch (e) {
|
||||
console.error('could not save availability', e)
|
||||
|
||||
} catch (error) {
|
||||
logger.error('could not save availability', { error })
|
||||
showError(t('dav', 'Failed to save availability'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
|
||||
<template>
|
||||
<div class="example-contact-settings">
|
||||
<NcCheckboxRadioSwitch :checked="enableDefaultContact"
|
||||
<NcCheckboxRadioSwitch
|
||||
:checked="enableDefaultContact"
|
||||
type="switch"
|
||||
@update:model-value="updateEnableDefaultContact">
|
||||
{{ $t('dav', "Add example contact to user's address book when they first log in") }}
|
||||
|
|
@ -17,15 +18,17 @@
|
|||
</template>
|
||||
example_contact.vcf
|
||||
</ExampleContentDownloadButton>
|
||||
<NcButton type="secondary"
|
||||
<NcButton
|
||||
variant="secondary"
|
||||
@click="toggleModal">
|
||||
<template #icon>
|
||||
<IconUpload :size="20" />
|
||||
</template>
|
||||
{{ $t('dav', 'Import contact') }}
|
||||
</NcButton>
|
||||
<NcButton v-if="hasCustomDefaultContact"
|
||||
type="tertiary"
|
||||
<NcButton
|
||||
v-if="hasCustomDefaultContact"
|
||||
variant="tertiary"
|
||||
@click="resetContact">
|
||||
<template #icon>
|
||||
<IconRestore :size="20" />
|
||||
|
|
@ -33,14 +36,16 @@
|
|||
{{ $t('dav', 'Reset to default') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
<NcDialog :open.sync="isModalOpen"
|
||||
<NcDialog
|
||||
:open.sync="isModalOpen"
|
||||
:name="$t('dav', 'Import contacts')"
|
||||
:buttons="buttons">
|
||||
<div>
|
||||
<p>{{ $t('dav', 'Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?') }}</p>
|
||||
</div>
|
||||
</NcDialog>
|
||||
<input id="example-contact-import"
|
||||
<input
|
||||
id="example-contact-import"
|
||||
ref="exampleContactImportInput"
|
||||
:disabled="loading"
|
||||
type="file"
|
||||
|
|
@ -49,19 +54,20 @@
|
|||
@change="processFile">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { NcDialog, NcButton, NcCheckboxRadioSwitch } from '@nextcloud/vue'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import IconUpload from 'vue-material-design-icons/TrayArrowUp.vue'
|
||||
import IconRestore from 'vue-material-design-icons/Restore.vue'
|
||||
import IconAccount from 'vue-material-design-icons/Account.vue'
|
||||
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
|
||||
import IconCheck from '@mdi/svg/svg/check.svg?raw'
|
||||
import logger from '../service/logger.js'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { NcButton, NcCheckboxRadioSwitch, NcDialog } from '@nextcloud/vue'
|
||||
import IconAccount from 'vue-material-design-icons/Account.vue'
|
||||
import IconRestore from 'vue-material-design-icons/Restore.vue'
|
||||
import IconUpload from 'vue-material-design-icons/TrayArrowUp.vue'
|
||||
import ExampleContentDownloadButton from './ExampleContentDownloadButton.vue'
|
||||
import logger from '../service/logger.js'
|
||||
|
||||
const enableDefaultContact = loadState('dav', 'enableDefaultContact')
|
||||
const hasCustomDefaultContact = loadState('dav', 'hasCustomDefaultContact')
|
||||
|
|
@ -77,6 +83,7 @@ export default {
|
|||
IconAccount,
|
||||
ExampleContentDownloadButton,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
enableDefaultContact,
|
||||
|
|
@ -98,11 +105,13 @@ export default {
|
|||
],
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
downloadUrl() {
|
||||
return generateUrl('/apps/dav/api/defaultcontact/contact')
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateEnableDefaultContact() {
|
||||
axios.put(generateUrl('apps/dav/api/defaultcontact/config'), {
|
||||
|
|
@ -113,12 +122,15 @@ export default {
|
|||
showError(this.$t('dav', 'Error while saving settings'))
|
||||
})
|
||||
},
|
||||
|
||||
toggleModal() {
|
||||
this.isModalOpen = !this.isModalOpen
|
||||
},
|
||||
|
||||
clickImportInput() {
|
||||
this.$refs.exampleContactImportInput.click()
|
||||
},
|
||||
|
||||
resetContact() {
|
||||
this.loading = true
|
||||
axios.put(generateUrl('/apps/dav/api/defaultcontact/contact'))
|
||||
|
|
@ -134,6 +146,7 @@ export default {
|
|||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
processFile(event) {
|
||||
this.loading = true
|
||||
|
||||
|
|
@ -159,6 +172,7 @@ export default {
|
|||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.example-contact-settings {
|
||||
margin-block-start: 2rem;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<NcButton type="tertiary" :href="href">
|
||||
<NcButton variant="tertiary" :href="href">
|
||||
<template #icon>
|
||||
<slot name="icon" />
|
||||
</template>
|
||||
|
|
@ -12,7 +12,8 @@
|
|||
<span class="download-button__label">
|
||||
<slot name="default" />
|
||||
</span>
|
||||
<IconDownload class="download-button__icon"
|
||||
<IconDownload
|
||||
class="download-button__icon"
|
||||
:size="20" />
|
||||
</div>
|
||||
</NcButton>
|
||||
|
|
@ -28,6 +29,7 @@ export default {
|
|||
NcButton,
|
||||
IconDownload,
|
||||
},
|
||||
|
||||
props: {
|
||||
href: {
|
||||
type: String,
|
||||
|
|
|
|||
|
|
@ -5,13 +5,15 @@
|
|||
|
||||
<template>
|
||||
<div class="example-event-settings">
|
||||
<NcCheckboxRadioSwitch :checked="createExampleEvent"
|
||||
<NcCheckboxRadioSwitch
|
||||
:checked="createExampleEvent"
|
||||
:disabled="savingConfig"
|
||||
type="switch"
|
||||
@update:model-value="updateCreateExampleEvent">
|
||||
{{ t('dav', "Add example event to user's calendar when they first log in") }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<div v-if="createExampleEvent"
|
||||
<div
|
||||
v-if="createExampleEvent"
|
||||
class="example-event-settings__buttons">
|
||||
<ExampleContentDownloadButton :href="downloadUrl">
|
||||
<template #icon>
|
||||
|
|
@ -19,15 +21,17 @@
|
|||
</template>
|
||||
example_event.ics
|
||||
</ExampleContentDownloadButton>
|
||||
<NcButton type="secondary"
|
||||
<NcButton
|
||||
variant="secondary"
|
||||
@click="showImportModal = true">
|
||||
<template #icon>
|
||||
<IconUpload :size="20" />
|
||||
</template>
|
||||
{{ t('dav', 'Import calendar event') }}
|
||||
</NcButton>
|
||||
<NcButton v-if="hasCustomEvent"
|
||||
type="tertiary"
|
||||
<NcButton
|
||||
v-if="hasCustomEvent"
|
||||
variant="tertiary"
|
||||
:disabled="deleting"
|
||||
@click="deleteCustomEvent">
|
||||
<template #icon>
|
||||
|
|
@ -36,21 +40,24 @@
|
|||
{{ t('dav', 'Reset to default') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
<NcDialog :open.sync="showImportModal"
|
||||
<NcDialog
|
||||
:open.sync="showImportModal"
|
||||
:name="t('dav', 'Import calendar event')">
|
||||
<div class="import-event-modal">
|
||||
<p>
|
||||
{{ t('dav', 'Uploading a new event will overwrite the existing one.') }}
|
||||
</p>
|
||||
<input ref="event-file"
|
||||
<input
|
||||
ref="event-file"
|
||||
:disabled="uploading"
|
||||
type="file"
|
||||
accept=".ics,text/calendar"
|
||||
class="import-event-modal__file-picker"
|
||||
@change="selectFile">
|
||||
<div class="import-event-modal__buttons">
|
||||
<NcButton :disabled="uploading || !selectedFile"
|
||||
type="primary"
|
||||
<NcButton
|
||||
:disabled="uploading || !selectedFile"
|
||||
variant="primary"
|
||||
@click="uploadCustomEvent()">
|
||||
<template #icon>
|
||||
<IconUpload :size="20" />
|
||||
|
|
@ -64,16 +71,16 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { NcButton, NcCheckboxRadioSwitch, NcDialog } from '@nextcloud/vue'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue'
|
||||
import IconUpload from 'vue-material-design-icons/TrayArrowUp.vue'
|
||||
import IconRestore from 'vue-material-design-icons/Restore.vue'
|
||||
import * as ExampleEventService from '../service/ExampleEventService.js'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import logger from '../service/logger.js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { NcButton, NcCheckboxRadioSwitch, NcDialog } from '@nextcloud/vue'
|
||||
import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue'
|
||||
import IconRestore from 'vue-material-design-icons/Restore.vue'
|
||||
import IconUpload from 'vue-material-design-icons/TrayArrowUp.vue'
|
||||
import ExampleContentDownloadButton from './ExampleContentDownloadButton.vue'
|
||||
import * as ExampleEventService from '../service/ExampleEventService.js'
|
||||
import logger from '../service/logger.js'
|
||||
|
||||
export default {
|
||||
name: 'ExampleEventSettings',
|
||||
|
|
@ -86,6 +93,7 @@ export default {
|
|||
IconRestore,
|
||||
ExampleContentDownloadButton,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
createExampleEvent: loadState('dav', 'create_example_event', false),
|
||||
|
|
@ -97,15 +105,18 @@ export default {
|
|||
selectedFile: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
downloadUrl() {
|
||||
return generateUrl('/apps/dav/api/exampleEvent/event')
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectFile() {
|
||||
this.selectedFile = this.$refs['event-file']?.files[0]
|
||||
},
|
||||
|
||||
async updateCreateExampleEvent() {
|
||||
this.savingConfig = true
|
||||
|
||||
|
|
@ -124,6 +135,7 @@ export default {
|
|||
|
||||
this.createExampleEvent = enable
|
||||
},
|
||||
|
||||
uploadCustomEvent() {
|
||||
if (!this.selectedFile) {
|
||||
return
|
||||
|
|
@ -154,6 +166,7 @@ export default {
|
|||
})
|
||||
reader.readAsText(this.selectedFile)
|
||||
},
|
||||
|
||||
async deleteCustomEvent() {
|
||||
this.deleting = true
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { createClient } from 'webdav'
|
||||
import memoize from 'lodash/fp/memoize.js'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser, getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import memoize from 'lodash/fp/memoize.js'
|
||||
import { createClient } from 'webdav'
|
||||
|
||||
export const getClient = memoize((service) => {
|
||||
// init webdav client
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import {
|
||||
slotsToVavailability,
|
||||
vavailabilityToSlots,
|
||||
} from '@nextcloud/calendar-availability-vue'
|
||||
import { parseXML } from 'webdav'
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { getClient } from '../dav/client.js'
|
||||
import logger from './logger.js'
|
||||
import { parseXML } from 'webdav'
|
||||
|
||||
import {
|
||||
slotsToVavailability,
|
||||
vavailabilityToSlots,
|
||||
} from '@nextcloud/calendar-availability-vue'
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -61,7 +60,7 @@ export async function findScheduleInboxAvailability() {
|
|||
* @param {any} timezoneId -
|
||||
*/
|
||||
export async function saveScheduleInboxAvailability(slots, timezoneId) {
|
||||
const all = [...Object.keys(slots).flatMap(dayId => slots[dayId].map(slot => ({
|
||||
const all = [...Object.keys(slots).flatMap((dayId) => slots[dayId].map((slot) => ({
|
||||
...slot,
|
||||
day: dayId,
|
||||
})))]
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
/**
|
||||
* Configure the creation of example events on a user's first login.
|
||||
|
|
|
|||
|
|
@ -25,10 +25,8 @@ export async function enableUserStatusAutomation() {
|
|||
* Disable user status automation based on availability
|
||||
*/
|
||||
export async function disableUserStatusAutomation() {
|
||||
return await axios.delete(
|
||||
generateOcsUrl('/apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
|
||||
appId: 'dav',
|
||||
configKey: 'user_status_automation',
|
||||
}),
|
||||
)
|
||||
return await axios.delete(generateOcsUrl('/apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
|
||||
appId: 'dav',
|
||||
configKey: 'user_status_automation',
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@
|
|||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import ExampleContentSettingsSection from './views/ExampleContentSettingsSection.vue'
|
||||
|
||||
Vue.mixin({
|
||||
methods: {
|
||||
t: translate,
|
||||
$t: translate,
|
||||
t,
|
||||
$t: t,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import Availability from './views/Availability.vue'
|
||||
|
||||
Vue.prototype.$t = translate
|
||||
Vue.prototype.$t = t
|
||||
|
||||
const View = Vue.extend(Availability);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import Vue from 'vue'
|
||||
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
import CalDavSettings from './views/CalDavSettings.vue'
|
||||
|
||||
Vue.prototype.$t = translate
|
||||
Vue.prototype.$t = t
|
||||
|
||||
const View = Vue.extend(CalDavSettings)
|
||||
const CalDavSettingsView = new View({
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@
|
|||
-->
|
||||
<template>
|
||||
<div>
|
||||
<NcSettingsSection id="availability"
|
||||
<NcSettingsSection
|
||||
id="availability"
|
||||
:name="$t('dav', 'Availability')"
|
||||
:description="$t('dav', 'If you configure your working hours, other people will see when you are out of office when they book a meeting.')">
|
||||
<AvailabilityForm />
|
||||
</NcSettingsSection>
|
||||
<NcSettingsSection v-if="!hideAbsenceSettings"
|
||||
<NcSettingsSection
|
||||
v-if="!hideAbsenceSettings"
|
||||
id="absence"
|
||||
:name="$t('dav', 'Absence')"
|
||||
:description="$t('dav', 'Configure your next absence period.')">
|
||||
|
|
@ -19,11 +21,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||
import AbsenceForm from '../components/AbsenceForm.vue'
|
||||
import AvailabilityForm from '../components/AvailabilityForm.vue'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
/* eslint vue/multi-word-component-names: "warn" */
|
||||
export default {
|
||||
name: 'Availability',
|
||||
components: {
|
||||
|
|
@ -31,6 +34,7 @@ export default {
|
|||
AbsenceForm,
|
||||
AvailabilityForm,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
hideAbsenceSettings: loadState('dav', 'hide_absence_settings', true),
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/vue'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import CalDavSettings from './CalDavSettings.vue'
|
||||
|
||||
vi.mock('@nextcloud/axios')
|
||||
|
|
@ -45,29 +45,19 @@ describe('CalDavSettings', () => {
|
|||
}
|
||||
},
|
||||
},
|
||||
Vue => {
|
||||
(Vue) => {
|
||||
Vue.prototype.$t = vi.fn((app, text) => text)
|
||||
},
|
||||
)
|
||||
const sendInvitations = TLUtils.getByLabelText(
|
||||
'Send invitations to attendees',
|
||||
)
|
||||
const sendInvitations = TLUtils.getByLabelText('Send invitations to attendees')
|
||||
expect(sendInvitations).toBeChecked()
|
||||
const generateBirthdayCalendar = TLUtils.getByLabelText(
|
||||
'Automatically generate a birthday calendar',
|
||||
)
|
||||
const generateBirthdayCalendar = TLUtils.getByLabelText('Automatically generate a birthday calendar')
|
||||
expect(generateBirthdayCalendar).toBeChecked()
|
||||
const sendEventReminders = TLUtils.getByLabelText(
|
||||
'Send notifications for events',
|
||||
)
|
||||
const sendEventReminders = TLUtils.getByLabelText('Send notifications for events')
|
||||
expect(sendEventReminders).toBeChecked()
|
||||
const sendEventRemindersToSharedUsers = TLUtils.getByLabelText(
|
||||
'Send reminder notifications to calendar sharees as well',
|
||||
)
|
||||
const sendEventRemindersToSharedUsers = TLUtils.getByLabelText('Send reminder notifications to calendar sharees as well')
|
||||
expect(sendEventRemindersToSharedUsers).toBeChecked()
|
||||
const sendEventRemindersPush = TLUtils.getByLabelText(
|
||||
'Enable notifications for events via push',
|
||||
)
|
||||
const sendEventRemindersPush = TLUtils.getByLabelText('Enable notifications for events via push')
|
||||
expect(sendEventRemindersPush).toBeChecked()
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<NcSettingsSection :name="$t('dav', 'Calendar server')"
|
||||
<NcSettingsSection
|
||||
:name="$t('dav', 'Calendar server')"
|
||||
:doc-url="userSyncCalendarsDocUrl">
|
||||
<!-- Can use v-html as:
|
||||
- $t passes the translated string through DOMPurify.sanitize,
|
||||
|
|
@ -11,7 +12,8 @@
|
|||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p class="settings-hint" v-html="hint" />
|
||||
<p>
|
||||
<NcCheckboxRadioSwitch id="caldavSendInvitations"
|
||||
<NcCheckboxRadioSwitch
|
||||
id="caldavSendInvitations"
|
||||
:checked.sync="sendInvitations"
|
||||
type="switch">
|
||||
{{ $t('dav', 'Send invitations to attendees') }}
|
||||
|
|
@ -23,7 +25,8 @@
|
|||
<em v-html="sendInvitationsHelpText" />
|
||||
</p>
|
||||
<p>
|
||||
<NcCheckboxRadioSwitch id="caldavGenerateBirthdayCalendar"
|
||||
<NcCheckboxRadioSwitch
|
||||
id="caldavGenerateBirthdayCalendar"
|
||||
:checked.sync="generateBirthdayCalendar"
|
||||
type="switch"
|
||||
class="checkbox">
|
||||
|
|
@ -38,7 +41,8 @@
|
|||
</em>
|
||||
</p>
|
||||
<p>
|
||||
<NcCheckboxRadioSwitch id="caldavSendEventReminders"
|
||||
<NcCheckboxRadioSwitch
|
||||
id="caldavSendEventReminders"
|
||||
:checked.sync="sendEventReminders"
|
||||
type="switch">
|
||||
{{ $t('dav', 'Send notifications for events') }}
|
||||
|
|
@ -54,18 +58,20 @@
|
|||
</em>
|
||||
</p>
|
||||
<p class="indented">
|
||||
<NcCheckboxRadioSwitch id="caldavSendEventRemindersToSharedGroupMembers"
|
||||
<NcCheckboxRadioSwitch
|
||||
id="caldavSendEventRemindersToSharedGroupMembers"
|
||||
:checked.sync="sendEventRemindersToSharedUsers"
|
||||
type="switch"
|
||||
:disabled="!sendEventReminders">
|
||||
{{ $t('dav', 'Send reminder notifications to calendar sharees as well' ) }}
|
||||
{{ $t('dav', 'Send reminder notifications to calendar sharees as well') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<em>
|
||||
{{ $t('dav', 'Reminders are always sent to organizers and attendees.' ) }}
|
||||
{{ $t('dav', 'Reminders are always sent to organizers and attendees.') }}
|
||||
</em>
|
||||
</p>
|
||||
<p class="indented">
|
||||
<NcCheckboxRadioSwitch id="caldavSendEventRemindersPush"
|
||||
<NcCheckboxRadioSwitch
|
||||
id="caldavSendEventRemindersPush"
|
||||
:checked.sync="sendEventRemindersPush"
|
||||
type="switch"
|
||||
:disabled="!sendEventReminders">
|
||||
|
|
@ -77,10 +83,10 @@
|
|||
|
||||
<script>
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||
|
||||
const userSyncCalendarsDocUrl = loadState('dav', 'userSyncCalendarsDocUrl', '#')
|
||||
|
||||
|
|
@ -90,11 +96,13 @@ export default {
|
|||
NcCheckboxRadioSwitch,
|
||||
NcSettingsSection,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
userSyncCalendarsDocUrl,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hint() {
|
||||
const translated = this.$t(
|
||||
|
|
@ -106,12 +114,14 @@ export default {
|
|||
.replace('{calendardocopen}', `<a target="_blank" href="${userSyncCalendarsDocUrl}" rel="noreferrer noopener">`)
|
||||
.replace(/\{linkclose\}/g, '</a>')
|
||||
},
|
||||
|
||||
sendInvitationsHelpText() {
|
||||
const translated = this.$t('dav', 'Please make sure to properly set up {emailopen}the email server{linkclose}.')
|
||||
return translated
|
||||
.replace('{emailopen}', '<a href="../admin#mail_general_settings">')
|
||||
.replace('{linkclose}', '</a>')
|
||||
},
|
||||
|
||||
sendEventRemindersHelpText() {
|
||||
const translated = this.$t('dav', 'Please make sure to properly set up {emailopen}the email server{linkclose}.')
|
||||
return translated
|
||||
|
|
@ -119,11 +129,13 @@ export default {
|
|||
.replace('{linkclose}', '</a>')
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
generateBirthdayCalendar(value) {
|
||||
const baseUrl = value ? '/apps/dav/enableBirthdayCalendar' : '/apps/dav/disableBirthdayCalendar'
|
||||
axios.post(generateUrl(baseUrl))
|
||||
},
|
||||
|
||||
sendInvitations(value) {
|
||||
OCP.AppConfig.setValue(
|
||||
'dav',
|
||||
|
|
@ -131,9 +143,11 @@ export default {
|
|||
value ? 'yes' : 'no',
|
||||
)
|
||||
},
|
||||
|
||||
sendEventReminders(value) {
|
||||
OCP.AppConfig.setValue('dav', 'sendEventReminders', value ? 'yes' : 'no')
|
||||
},
|
||||
|
||||
sendEventRemindersToSharedUsers(value) {
|
||||
OCP.AppConfig.setValue(
|
||||
'dav',
|
||||
|
|
@ -141,6 +155,7 @@ export default {
|
|||
value ? 'yes' : 'no',
|
||||
)
|
||||
},
|
||||
|
||||
sendEventRemindersPush(value) {
|
||||
OCP.AppConfig.setValue('dav', 'sendEventRemindersPush', value ? 'yes' : 'no')
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<NcSettingsSection id="example-content"
|
||||
<NcSettingsSection
|
||||
id="example-content"
|
||||
:name="$t('dav', 'Example content')"
|
||||
class="example-content-setting"
|
||||
:description="$t('dav', 'Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content.')">
|
||||
|
|
@ -16,8 +17,8 @@
|
|||
<script>
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { NcSettingsSection } from '@nextcloud/vue'
|
||||
import ExampleEventSettings from '../components/ExampleEventSettings.vue'
|
||||
import ExampleContactSettings from '../components/ExampleContactSettings.vue'
|
||||
import ExampleEventSettings from '../components/ExampleEventSettings.vue'
|
||||
|
||||
export default {
|
||||
name: 'ExampleContentSettingsSection',
|
||||
|
|
@ -26,10 +27,12 @@ export default {
|
|||
ExampleContactSettings,
|
||||
ExampleEventSettings,
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasContactsApp() {
|
||||
return loadState('dav', 'contactsEnabled')
|
||||
},
|
||||
|
||||
hasCalendarApp() {
|
||||
return loadState('dav', 'calendarEnabled')
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,29 +5,28 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
* @memberOf OC
|
||||
* @namespace OC
|
||||
*/
|
||||
OC.Encryption = _.extend(OC.Encryption || {}, {
|
||||
displayEncryptionWarning: function () {
|
||||
displayEncryptionWarning: function() {
|
||||
if (!OC.currentUser || !OC.Notification.isHidden()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
$.get(
|
||||
OC.generateUrl('/apps/encryption/ajax/getStatus'),
|
||||
function (result) {
|
||||
if (result.status === "interactionNeeded") {
|
||||
OC.Notification.show(result.data.message);
|
||||
function(result) {
|
||||
if (result.status === 'interactionNeeded') {
|
||||
OC.Notification.show(result.data.message)
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
// wait for other apps/extensions to register their event handlers and file actions
|
||||
// in the "ready" clause
|
||||
_.defer(function() {
|
||||
OC.Encryption.displayEncryptionWarning();
|
||||
});
|
||||
});
|
||||
OC.Encryption.displayEncryptionWarning()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,82 +4,77 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
$('input:button[name="enableRecoveryKey"]').click(function () {
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
$('input:button[name="enableRecoveryKey"]').click(function() {
|
||||
const recoveryStatus = $(this).attr('status')
|
||||
const newRecoveryStatus = (1 + parseInt(recoveryStatus)) % 2
|
||||
const buttonValue = $(this).attr('value')
|
||||
|
||||
var recoveryStatus = $(this).attr('status');
|
||||
var newRecoveryStatus = (1 + parseInt(recoveryStatus)) % 2;
|
||||
var buttonValue = $(this).attr('value');
|
||||
|
||||
var recoveryPassword = $('#encryptionRecoveryPassword').val();
|
||||
var confirmPassword = $('#repeatEncryptionRecoveryPassword').val();
|
||||
OC.msg.startSaving('#encryptionSetRecoveryKey .msg');
|
||||
const recoveryPassword = $('#encryptionRecoveryPassword').val()
|
||||
const confirmPassword = $('#repeatEncryptionRecoveryPassword').val()
|
||||
OC.msg.startSaving('#encryptionSetRecoveryKey .msg')
|
||||
$.post(
|
||||
OC.generateUrl('/apps/encryption/ajax/adminRecovery'),
|
||||
{
|
||||
adminEnableRecovery: newRecoveryStatus,
|
||||
recoveryPassword: recoveryPassword,
|
||||
confirmPassword: confirmPassword
|
||||
recoveryPassword,
|
||||
confirmPassword,
|
||||
},
|
||||
).done(function(data) {
|
||||
OC.msg.finishedSuccess('#encryptionSetRecoveryKey .msg', data.data.message)
|
||||
|
||||
if (newRecoveryStatus === 0) {
|
||||
$('p[name="changeRecoveryPasswordBlock"]').addClass('hidden')
|
||||
$('input:button[name="enableRecoveryKey"]').attr('value', 'Enable recovery key')
|
||||
$('input:button[name="enableRecoveryKey"]').attr('status', '0')
|
||||
} else {
|
||||
$('input:password[name="changeRecoveryPassword"]').val('')
|
||||
$('p[name="changeRecoveryPasswordBlock"]').removeClass('hidden')
|
||||
$('input:button[name="enableRecoveryKey"]').attr('value', 'Disable recovery key')
|
||||
$('input:button[name="enableRecoveryKey"]').attr('status', '1')
|
||||
}
|
||||
).done(function (data) {
|
||||
OC.msg.finishedSuccess('#encryptionSetRecoveryKey .msg', data.data.message);
|
||||
|
||||
if (newRecoveryStatus === 0) {
|
||||
$('p[name="changeRecoveryPasswordBlock"]').addClass("hidden");
|
||||
$('input:button[name="enableRecoveryKey"]').attr('value', 'Enable recovery key');
|
||||
$('input:button[name="enableRecoveryKey"]').attr('status', '0');
|
||||
} else {
|
||||
$('input:password[name="changeRecoveryPassword"]').val("");
|
||||
$('p[name="changeRecoveryPasswordBlock"]').removeClass("hidden");
|
||||
$('input:button[name="enableRecoveryKey"]').attr('value', 'Disable recovery key');
|
||||
$('input:button[name="enableRecoveryKey"]').attr('status', '1');
|
||||
}
|
||||
})
|
||||
.fail(function(jqXHR) {
|
||||
$('input:button[name="enableRecoveryKey"]').attr('value', buttonValue)
|
||||
$('input:button[name="enableRecoveryKey"]').attr('status', recoveryStatus)
|
||||
OC.msg.finishedError('#encryptionSetRecoveryKey .msg', JSON.parse(jqXHR.responseText).data.message)
|
||||
})
|
||||
.fail(function (jqXHR) {
|
||||
$('input:button[name="enableRecoveryKey"]').attr('value', buttonValue);
|
||||
$('input:button[name="enableRecoveryKey"]').attr('status', recoveryStatus);
|
||||
OC.msg.finishedError('#encryptionSetRecoveryKey .msg', JSON.parse(jqXHR.responseText).data.message);
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
});
|
||||
|
||||
$("#repeatEncryptionRecoveryPassword").keyup(function (event) {
|
||||
$('#repeatEncryptionRecoveryPassword').keyup(function(event) {
|
||||
if (event.keyCode == 13) {
|
||||
$("#enableRecoveryKey").click();
|
||||
$('#enableRecoveryKey').click()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// change recovery password
|
||||
|
||||
$('button:button[name="submitChangeRecoveryKey"]').click(function () {
|
||||
var oldRecoveryPassword = $('#oldEncryptionRecoveryPassword').val();
|
||||
var newRecoveryPassword = $('#newEncryptionRecoveryPassword').val();
|
||||
var confirmNewPassword = $('#repeatedNewEncryptionRecoveryPassword').val();
|
||||
OC.msg.startSaving('#encryptionChangeRecoveryKey .msg');
|
||||
$('button:button[name="submitChangeRecoveryKey"]').click(function() {
|
||||
const oldRecoveryPassword = $('#oldEncryptionRecoveryPassword').val()
|
||||
const newRecoveryPassword = $('#newEncryptionRecoveryPassword').val()
|
||||
const confirmNewPassword = $('#repeatedNewEncryptionRecoveryPassword').val()
|
||||
OC.msg.startSaving('#encryptionChangeRecoveryKey .msg')
|
||||
$.post(
|
||||
OC.generateUrl('/apps/encryption/ajax/changeRecoveryPassword'),
|
||||
{
|
||||
oldPassword: oldRecoveryPassword,
|
||||
newPassword: newRecoveryPassword,
|
||||
confirmPassword: confirmNewPassword
|
||||
}
|
||||
).done(function (data) {
|
||||
OC.msg.finishedSuccess('#encryptionChangeRecoveryKey .msg', data.data.message);
|
||||
confirmPassword: confirmNewPassword,
|
||||
},
|
||||
).done(function(data) {
|
||||
OC.msg.finishedSuccess('#encryptionChangeRecoveryKey .msg', data.data.message)
|
||||
})
|
||||
.fail(function(jqXHR) {
|
||||
OC.msg.finishedError('#encryptionChangeRecoveryKey .msg', JSON.parse(jqXHR.responseText).data.message)
|
||||
})
|
||||
.fail(function (jqXHR) {
|
||||
OC.msg.finishedError('#encryptionChangeRecoveryKey .msg', JSON.parse(jqXHR.responseText).data.message);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
$('#encryptHomeStorage').change(function() {
|
||||
$.post(
|
||||
OC.generateUrl('/apps/encryption/ajax/setEncryptHomeStorage'),
|
||||
{
|
||||
encryptHomeStorage: this.checked
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
encryptHomeStorage: this.checked,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,65 +5,60 @@
|
|||
*/
|
||||
|
||||
OC.Encryption = _.extend(OC.Encryption || {}, {
|
||||
updatePrivateKeyPassword: function () {
|
||||
var oldPrivateKeyPassword = $('input:password[id="oldPrivateKeyPassword"]').val();
|
||||
var newPrivateKeyPassword = $('input:password[id="newPrivateKeyPassword"]').val();
|
||||
OC.msg.startSaving('#ocDefaultEncryptionModule .msg');
|
||||
updatePrivateKeyPassword: function() {
|
||||
const oldPrivateKeyPassword = $('input:password[id="oldPrivateKeyPassword"]').val()
|
||||
const newPrivateKeyPassword = $('input:password[id="newPrivateKeyPassword"]').val()
|
||||
OC.msg.startSaving('#ocDefaultEncryptionModule .msg')
|
||||
$.post(
|
||||
OC.generateUrl('/apps/encryption/ajax/updatePrivateKeyPassword'),
|
||||
{
|
||||
oldPassword: oldPrivateKeyPassword,
|
||||
newPassword: newPrivateKeyPassword
|
||||
}
|
||||
).done(function (data) {
|
||||
OC.msg.finishedSuccess('#ocDefaultEncryptionModule .msg', data.message);
|
||||
})
|
||||
.fail(function (jqXHR) {
|
||||
OC.msg.finishedError('#ocDefaultEncryptionModule .msg', JSON.parse(jqXHR.responseText).message);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
newPassword: newPrivateKeyPassword,
|
||||
},
|
||||
).done(function(data) {
|
||||
OC.msg.finishedSuccess('#ocDefaultEncryptionModule .msg', data.message)
|
||||
}).fail(function(jqXHR) {
|
||||
OC.msg.finishedError('#ocDefaultEncryptionModule .msg', JSON.parse(jqXHR.responseText).message)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
// Trigger ajax on recoveryAdmin status change
|
||||
$('input:radio[name="userEnableRecovery"]').change(
|
||||
function () {
|
||||
var recoveryStatus = $(this).val();
|
||||
OC.msg.startAction('#userEnableRecovery .msg', 'Updating recovery keys. This can take some time...');
|
||||
$.post(
|
||||
OC.generateUrl('/apps/encryption/ajax/userSetRecovery'),
|
||||
{
|
||||
userEnableRecovery: recoveryStatus
|
||||
}
|
||||
).done(function (data) {
|
||||
OC.msg.finishedSuccess('#userEnableRecovery .msg', data.data.message);
|
||||
})
|
||||
.fail(function (jqXHR) {
|
||||
OC.msg.finishedError('#userEnableRecovery .msg', JSON.parse(jqXHR.responseText).data.message);
|
||||
});
|
||||
$('input:radio[name="userEnableRecovery"]').change(function() {
|
||||
const recoveryStatus = $(this).val()
|
||||
OC.msg.startAction('#userEnableRecovery .msg', 'Updating recovery keys. This can take some time...')
|
||||
$.post(
|
||||
OC.generateUrl('/apps/encryption/ajax/userSetRecovery'),
|
||||
{
|
||||
userEnableRecovery: recoveryStatus,
|
||||
},
|
||||
).done(function(data) {
|
||||
OC.msg.finishedSuccess('#userEnableRecovery .msg', data.data.message)
|
||||
})
|
||||
.fail(function(jqXHR) {
|
||||
OC.msg.finishedError('#userEnableRecovery .msg', JSON.parse(jqXHR.responseText).data.message)
|
||||
})
|
||||
// Ensure page is not reloaded on form submit
|
||||
return false;
|
||||
}
|
||||
);
|
||||
return false
|
||||
})
|
||||
|
||||
// update private key password
|
||||
|
||||
$('input:password[name="changePrivateKeyPassword"]').keyup(function (event) {
|
||||
var oldPrivateKeyPassword = $('input:password[id="oldPrivateKeyPassword"]').val();
|
||||
var newPrivateKeyPassword = $('input:password[id="newPrivateKeyPassword"]').val();
|
||||
$('input:password[name="changePrivateKeyPassword"]').keyup(function(event) {
|
||||
const oldPrivateKeyPassword = $('input:password[id="oldPrivateKeyPassword"]').val()
|
||||
const newPrivateKeyPassword = $('input:password[id="newPrivateKeyPassword"]').val()
|
||||
if (newPrivateKeyPassword !== '' && oldPrivateKeyPassword !== '') {
|
||||
$('button:button[name="submitChangePrivateKeyPassword"]').removeAttr("disabled");
|
||||
$('button:button[name="submitChangePrivateKeyPassword"]').removeAttr('disabled')
|
||||
if (event.which === 13) {
|
||||
OC.Encryption.updatePrivateKeyPassword();
|
||||
OC.Encryption.updatePrivateKeyPassword()
|
||||
}
|
||||
} else {
|
||||
$('button:button[name="submitChangePrivateKeyPassword"]').attr("disabled", "true");
|
||||
$('button:button[name="submitChangePrivateKeyPassword"]').attr('disabled', 'true')
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
$('button:button[name="submitChangePrivateKeyPassword"]').click(function () {
|
||||
OC.Encryption.updatePrivateKeyPassword();
|
||||
});
|
||||
|
||||
});
|
||||
$('button:button[name="submitChangePrivateKeyPassword"]').click(function() {
|
||||
OC.Encryption.updatePrivateKeyPassword()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,29 +3,34 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<NcSettingsSection :name="t('federatedfilesharing', 'Federated Cloud Sharing')"
|
||||
<NcSettingsSection
|
||||
:name="t('federatedfilesharing', 'Federated Cloud Sharing')"
|
||||
:description="t('federatedfilesharing', 'Adjust how people can share between servers. This includes shares between people on this server as well if they are using federated sharing.')"
|
||||
:doc-url="sharingFederatedDocUrl">
|
||||
<NcCheckboxRadioSwitch type="switch"
|
||||
<NcCheckboxRadioSwitch
|
||||
type="switch"
|
||||
:checked.sync="outgoingServer2serverShareEnabled"
|
||||
@update:checked="update('outgoing_server2server_share_enabled', outgoingServer2serverShareEnabled)">
|
||||
{{ t('federatedfilesharing', 'Allow people on this server to send shares to other servers (this option also allows WebDAV access to public shares)') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<NcCheckboxRadioSwitch type="switch"
|
||||
<NcCheckboxRadioSwitch
|
||||
type="switch"
|
||||
:checked.sync="incomingServer2serverShareEnabled"
|
||||
@update:checked="update('incoming_server2server_share_enabled', incomingServer2serverShareEnabled)">
|
||||
{{ t('federatedfilesharing', 'Allow people on this server to receive shares from other servers') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<NcCheckboxRadioSwitch v-if="federatedGroupSharingSupported"
|
||||
<NcCheckboxRadioSwitch
|
||||
v-if="federatedGroupSharingSupported"
|
||||
type="switch"
|
||||
:checked.sync="outgoingServer2serverGroupShareEnabled"
|
||||
@update:checked="update('outgoing_server2server_group_share_enabled', outgoingServer2serverGroupShareEnabled)">
|
||||
{{ t('federatedfilesharing', 'Allow people on this server to send shares to groups on other servers') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<NcCheckboxRadioSwitch v-if="federatedGroupSharingSupported"
|
||||
<NcCheckboxRadioSwitch
|
||||
v-if="federatedGroupSharingSupported"
|
||||
type="switch"
|
||||
:checked.sync="incomingServer2serverGroupShareEnabled"
|
||||
@update:checked="update('incoming_server2server_group_share_enabled', incomingServer2serverGroupShareEnabled)">
|
||||
|
|
@ -35,14 +40,16 @@
|
|||
<fieldset>
|
||||
<legend>{{ t('federatedfilesharing', 'The lookup server is only available for global scale.') }}</legend>
|
||||
|
||||
<NcCheckboxRadioSwitch type="switch"
|
||||
<NcCheckboxRadioSwitch
|
||||
type="switch"
|
||||
:checked="lookupServerEnabled"
|
||||
disabled
|
||||
@update:checked="showLookupServerConfirmation">
|
||||
{{ t('federatedfilesharing', 'Search global and public address book for people') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<NcCheckboxRadioSwitch type="switch"
|
||||
<NcCheckboxRadioSwitch
|
||||
type="switch"
|
||||
:checked="lookupServerUploadEnabled"
|
||||
disabled
|
||||
@update:checked="showLookupServerUploadConfirmation">
|
||||
|
|
@ -55,7 +62,8 @@
|
|||
<h3 class="settings-subsection__name">
|
||||
{{ t('federatedfilesharing', 'Trusted federation') }}
|
||||
</h3>
|
||||
<NcCheckboxRadioSwitch type="switch"
|
||||
<NcCheckboxRadioSwitch
|
||||
type="switch"
|
||||
:checked.sync="federatedTrustedShareAutoAccept"
|
||||
@update:checked="update('federatedTrustedShareAutoAccept', federatedTrustedShareAutoAccept)">
|
||||
{{ t('federatedfilesharing', 'Automatically accept shares from trusted federated accounts and groups by default') }}
|
||||
|
|
@ -65,13 +73,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { DialogBuilder, DialogSeverity, showError } from '@nextcloud/dialogs'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { confirmPassword } from '@nextcloud/password-confirmation'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { DialogBuilder, DialogSeverity, showError } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { confirmPassword } from '@nextcloud/password-confirmation'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||
import logger from '../services/logger.ts'
|
||||
|
||||
import '@nextcloud/password-confirmation/dist/style.css'
|
||||
|
||||
|
|
@ -97,6 +106,7 @@ export default {
|
|||
sharingFederatedDocUrl: loadState('federatedfilesharing', 'sharingFederatedDocUrl'),
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setLookupServerUploadEnabled(state) {
|
||||
if (state === this.lookupServerUploadEnabled) {
|
||||
|
|
@ -115,9 +125,7 @@ export default {
|
|||
const dialog = new DialogBuilder(t('federatedfilesharing', 'Confirm data upload to lookup server'))
|
||||
await dialog
|
||||
.setSeverity(DialogSeverity.Warning)
|
||||
.setText(
|
||||
t('federatedfilesharing', 'When enabled, all account properties (e.g. email address) with scope visibility set to "published", will be automatically synced and transmitted to an external system and made available in a public, global address book.'),
|
||||
)
|
||||
.setText(t('federatedfilesharing', 'When enabled, all account properties (e.g. email address) with scope visibility set to "published", will be automatically synced and transmitted to an external system and made available in a public, global address book.'))
|
||||
.addButton({
|
||||
callback: () => this.setLookupServerUploadEnabled(false),
|
||||
label: t('federatedfilesharing', 'Disable upload'),
|
||||
|
|
@ -148,11 +156,9 @@ export default {
|
|||
const dialog = new DialogBuilder(t('federatedfilesharing', 'Confirm querying lookup server'))
|
||||
await dialog
|
||||
.setSeverity(DialogSeverity.Warning)
|
||||
.setText(
|
||||
t('federatedfilesharing', 'When enabled, the search input when creating shares will be sent to an external system that provides a public and global address book.')
|
||||
.setText(t('federatedfilesharing', 'When enabled, the search input when creating shares will be sent to an external system that provides a public and global address book.')
|
||||
+ t('federatedfilesharing', 'This is used to retrieve the federated cloud ID to make federated sharing easier.')
|
||||
+ t('federatedfilesharing', 'Moreover, email addresses of users might be sent to that system in order to verify them.'),
|
||||
)
|
||||
+ t('federatedfilesharing', 'Moreover, email addresses of users might be sent to that system in order to verify them.'))
|
||||
.addButton({
|
||||
callback: () => this.setLookupServerEnabled(false),
|
||||
label: t('federatedfilesharing', 'Disable querying'),
|
||||
|
|
@ -189,15 +195,17 @@ export default {
|
|||
})
|
||||
}
|
||||
},
|
||||
|
||||
async handleResponse({ status, errorMessage, error }) {
|
||||
if (status !== 'ok') {
|
||||
showError(errorMessage)
|
||||
console.error(errorMessage, error)
|
||||
logger.error(errorMessage, { error })
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-subsection {
|
||||
margin-top: 20px;
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<NcSettingsSection :name="t('federatedfilesharing', 'Federated Cloud')"
|
||||
<NcSettingsSection
|
||||
:name="t('federatedfilesharing', 'Federated Cloud')"
|
||||
:description="t('federatedfilesharing', 'You can share with anyone who uses a {productName} server or other Open Cloud Mesh (OCM) compatible servers and services! Just put their Federated Cloud ID in the share dialog. It looks like person@cloud.example.com', { productName })"
|
||||
:doc-url="docUrlFederated">
|
||||
<NcInputField class="federated-cloud__cloud-id"
|
||||
<NcInputField
|
||||
class="federated-cloud__cloud-id"
|
||||
readonly
|
||||
:label="t('federatedfilesharing', 'Your Federated Cloud ID')"
|
||||
:value="cloudId"
|
||||
|
|
@ -29,7 +31,8 @@
|
|||
<img class="social-button__icon social-button__icon--bright" :src="urlFacebookIcon">
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton :aria-label="t('federatedfilesharing', 'X (formerly Twitter)')"
|
||||
<NcButton
|
||||
:aria-label="t('federatedfilesharing', 'X (formerly Twitter)')"
|
||||
:href="shareXUrl">
|
||||
{{ t('federatedfilesharing', 'formerly Twitter') }}
|
||||
<template #icon>
|
||||
|
|
@ -48,7 +51,8 @@
|
|||
<img class="social-button__icon" :src="urlBlueSkyIcon">
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton class="social-button__website-button"
|
||||
<NcButton
|
||||
class="social-button__website-button"
|
||||
@click="showHtml = !showHtml">
|
||||
<template #icon>
|
||||
<IconWeb :size="20" />
|
||||
|
|
@ -59,7 +63,8 @@
|
|||
|
||||
<template v-if="showHtml">
|
||||
<p style="margin: 10px 0">
|
||||
<a target="_blank"
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
:href="reference"
|
||||
:style="backgroundStyle">
|
||||
|
|
@ -82,12 +87,12 @@ import { showSuccess } from '@nextcloud/dialogs'
|
|||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { imagePath } from '@nextcloud/router'
|
||||
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcInputField from '@nextcloud/vue/components/NcInputField'
|
||||
import IconWeb from 'vue-material-design-icons/Web.vue'
|
||||
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||
import IconCheck from 'vue-material-design-icons/Check.vue'
|
||||
import IconClipboard from 'vue-material-design-icons/ContentCopy.vue'
|
||||
import IconWeb from 'vue-material-design-icons/Web.vue'
|
||||
|
||||
export default {
|
||||
name: 'PersonalSettings',
|
||||
|
|
@ -99,6 +104,7 @@ export default {
|
|||
IconClipboard,
|
||||
IconWeb,
|
||||
},
|
||||
|
||||
setup() {
|
||||
return {
|
||||
t,
|
||||
|
|
@ -112,6 +118,7 @@ export default {
|
|||
urlXIcon: imagePath('core', 'x'),
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
color: loadState('federatedfilesharing', 'color'),
|
||||
|
|
@ -122,50 +129,62 @@ export default {
|
|||
isCopied: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
messageWithURL() {
|
||||
return t('federatedfilesharing', 'Share with me through my #Nextcloud Federated Cloud ID, see {url}', { url: this.reference })
|
||||
},
|
||||
|
||||
messageWithoutURL() {
|
||||
return t('federatedfilesharing', 'Share with me through my #Nextcloud Federated Cloud ID')
|
||||
},
|
||||
|
||||
shareMastodonUrl() {
|
||||
return `https://mastodon.social/?text=${encodeURIComponent(this.messageWithoutURL)}&url=${encodeURIComponent(this.reference)}`
|
||||
},
|
||||
|
||||
shareXUrl() {
|
||||
return `https://x.com/intent/tweet?text=${encodeURIComponent(this.messageWithURL)}`
|
||||
},
|
||||
|
||||
shareFacebookUrl() {
|
||||
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(this.reference)}`
|
||||
},
|
||||
|
||||
shareBlueSkyUrl() {
|
||||
return `https://bsky.app/intent/compose?text=${encodeURIComponent(this.messageWithURL)}`
|
||||
},
|
||||
|
||||
logoPathAbsolute() {
|
||||
return window.location.protocol + '//' + window.location.host + this.logoPath
|
||||
},
|
||||
|
||||
backgroundStyle() {
|
||||
return `padding:10px;background-color:${this.color};color:${this.textColor};border-radius:3px;padding-inline-start:4px;`
|
||||
},
|
||||
|
||||
linkStyle() {
|
||||
return `background-image:url(${this.logoPathAbsolute});width:50px;height:30px;position:relative;top:8px;background-size:contain;display:inline-block;background-repeat:no-repeat; background-position: center center;`
|
||||
},
|
||||
|
||||
htmlCode() {
|
||||
return `<a target="_blank" rel="noreferrer noopener" href="${this.reference}" style="${this.backgroundStyle}">
|
||||
<span style="${this.linkStyle}"></span>
|
||||
${t('federatedfilesharing', 'Share with me via Nextcloud')}
|
||||
</a>`
|
||||
},
|
||||
|
||||
copyLinkTooltip() {
|
||||
return this.isCopied ? t('federatedfilesharing', 'Cloud ID copied') : t('federatedfilesharing', 'Copy')
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async copyCloudId(): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.cloudId)
|
||||
showSuccess(t('federatedfilesharing', 'Cloud ID copied'))
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// no secure context or really old browser - need a fallback
|
||||
window.prompt(t('federatedfilesharing', 'Clipboard not available. Please copy the cloud ID manually.'), this.reference)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,14 +43,16 @@ const buttons = computed(() => [
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<NcDialog :buttons="buttons"
|
||||
<NcDialog
|
||||
:buttons="buttons"
|
||||
:is-form="passwordRequired"
|
||||
:name="t('federatedfilesharing', 'Remote share')"
|
||||
@submit="emit('close', true, password)">
|
||||
<p>
|
||||
{{ t('federatedfilesharing', 'Do you want to add the remote share {name} from {owner}@{remote}?', { name, owner, remote }) }}
|
||||
</p>
|
||||
<NcPasswordField v-if="passwordRequired"
|
||||
<NcPasswordField
|
||||
v-if="passwordRequired"
|
||||
class="remote-share-dialog__password"
|
||||
:label="t('federatedfilesharing', 'Remote share password')"
|
||||
:value.sync="password" />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
* SPDX-FileCopyrightText: 2014-2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import axios, { isAxiosError } from '@nextcloud/axios'
|
||||
import { showError, showInfo } from '@nextcloud/dialogs'
|
||||
import { subscribe } from '@nextcloud/event-bus'
|
||||
|
|
@ -35,9 +36,7 @@ window.OCA.Sharing.showAddExternalDialog = function(share, passwordProtected, ca
|
|||
.replace(/\/$/, '') // remove trailing slash
|
||||
|
||||
showRemoteShareDialog(name, owner, remote, passwordProtected)
|
||||
// eslint-disable-next-line n/no-callback-literal
|
||||
.then((password) => callback(true, { ...share, password }))
|
||||
// eslint-disable-next-line n/no-callback-literal
|
||||
.catch(() => callback(false, share))
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +83,6 @@ function processIncomingShareFromUrl() {
|
|||
|
||||
// manually add server-to-server share
|
||||
if (params.remote && params.token && params.name) {
|
||||
|
||||
const callbackAddShare = (result, share) => {
|
||||
if (result === false) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import Vue from 'vue'
|
||||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
import AdminSettings from './components/AdminSettings.vue'
|
||||
|
||||
__webpack_nonce__ = getCSPNonce()
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import Vue from 'vue'
|
||||
|
||||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
import Vue from 'vue'
|
||||
import PersonalSettings from './components/PersonalSettings.vue'
|
||||
|
||||
__webpack_nonce__ = getCSPNonce()
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { showRemoteShareDialog } from './dialogService'
|
||||
import { nextTick } from 'vue'
|
||||
import { showRemoteShareDialog } from './dialogService.ts'
|
||||
|
||||
describe('federatedfilesharing: dialog service', () => {
|
||||
it('mounts dialog', async () => {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ export function showRemoteShareDialog(
|
|||
owner: string,
|
||||
remote: string,
|
||||
passwordRequired = false,
|
||||
): Promise<string|void> {
|
||||
const { promise, reject, resolve } = Promise.withResolvers<string|void>()
|
||||
): Promise<string | void> {
|
||||
const { promise, reject, resolve } = Promise.withResolvers<string | void>()
|
||||
|
||||
spawnDialog(RemoteShareDialog, { name, owner, remote, passwordRequired }, (status, password) => {
|
||||
if (passwordRequired && status) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { getLoggerBuilder } from '@nextcloud/logger'
|
||||
|
||||
const logger = getLoggerBuilder()
|
||||
|
|
|
|||
|
|
@ -1,119 +1,116 @@
|
|||
|
||||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
(function( $ ) {
|
||||
|
||||
// ocFederationAddServer
|
||||
$.fn.ocFederationAddServer = function() {
|
||||
|
||||
/* Go easy on jquery and define some vars
|
||||
/**
|
||||
* @param $ - The jQuery instance
|
||||
*/
|
||||
(function($) {
|
||||
// ocFederationAddServer
|
||||
$.fn.ocFederationAddServer = function() {
|
||||
/* Go easy on jquery and define some vars
|
||||
========================================================================== */
|
||||
|
||||
var $wrapper = $(this),
|
||||
const $wrapper = $(this),
|
||||
|
||||
// Buttons
|
||||
$btnAddServer = $wrapper.find("#ocFederationAddServerButton"),
|
||||
$btnSubmit = $wrapper.find("#ocFederationSubmit"),
|
||||
// Buttons
|
||||
$btnAddServer = $wrapper.find('#ocFederationAddServerButton'),
|
||||
$btnSubmit = $wrapper.find('#ocFederationSubmit'),
|
||||
|
||||
// Inputs
|
||||
$inpServerUrl = $wrapper.find("#serverUrl"),
|
||||
// Inputs
|
||||
$inpServerUrl = $wrapper.find('#serverUrl'),
|
||||
|
||||
// misc
|
||||
$msgBox = $wrapper.find("#ocFederationAddServer .msg"),
|
||||
$srvList = $wrapper.find("#listOfTrustedServers");
|
||||
// misc
|
||||
$msgBox = $wrapper.find('#ocFederationAddServer .msg'),
|
||||
$srvList = $wrapper.find('#listOfTrustedServers')
|
||||
|
||||
|
||||
/* Interaction
|
||||
/* Interaction
|
||||
========================================================================== */
|
||||
|
||||
$btnAddServer.on('click', function() {
|
||||
$btnAddServer.addClass('hidden');
|
||||
$wrapper.find(".serverUrl").removeClass('hidden');
|
||||
$inpServerUrl
|
||||
.focus();
|
||||
});
|
||||
$btnAddServer.on('click', function() {
|
||||
$btnAddServer.addClass('hidden')
|
||||
$wrapper.find('.serverUrl').removeClass('hidden')
|
||||
$inpServerUrl
|
||||
.focus()
|
||||
})
|
||||
|
||||
// trigger server removal
|
||||
$srvList.on('click', 'li > .icon-delete', function() {
|
||||
var $this = $(this).parent();
|
||||
var id = $this.attr('id');
|
||||
// trigger server removal
|
||||
$srvList.on('click', 'li > .icon-delete', function() {
|
||||
const $this = $(this).parent()
|
||||
const id = $this.attr('id')
|
||||
|
||||
removeServer( id );
|
||||
});
|
||||
removeServer(id)
|
||||
})
|
||||
|
||||
$btnSubmit.on("click", function()
|
||||
{
|
||||
addServer($inpServerUrl.val());
|
||||
});
|
||||
$btnSubmit.on('click', function() {
|
||||
addServer($inpServerUrl.val())
|
||||
})
|
||||
|
||||
$inpServerUrl.on("change keyup", function (e) {
|
||||
var url = $(this).val();
|
||||
$inpServerUrl.on('change keyup', function(e) {
|
||||
const url = $(this).val()
|
||||
|
||||
// toggle add-button visibility based on input length
|
||||
if ( url.length > 0 )
|
||||
$btnSubmit.removeClass("hidden")
|
||||
else
|
||||
$btnSubmit.addClass("hidden")
|
||||
// toggle add-button visibility based on input length
|
||||
if (url.length > 0) { $btnSubmit.removeClass('hidden') } else { $btnSubmit.addClass('hidden') }
|
||||
|
||||
if (e.keyCode === 13) { // add server on "enter"
|
||||
addServer(url);
|
||||
} else if (e.keyCode === 27) { // hide input filed again in ESC
|
||||
$btnAddServer.removeClass('hidden');
|
||||
$inpServerUrl.val("").addClass('hidden');
|
||||
$btnSubmit.addClass('hidden');
|
||||
}
|
||||
});
|
||||
};
|
||||
if (e.keyCode === 13) { // add server on "enter"
|
||||
addServer(url)
|
||||
} else if (e.keyCode === 27) { // hide input filed again in ESC
|
||||
$btnAddServer.removeClass('hidden')
|
||||
$inpServerUrl.val('').addClass('hidden')
|
||||
$btnSubmit.addClass('hidden')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* private Functions
|
||||
/* private Functions
|
||||
========================================================================== */
|
||||
|
||||
function addServer( url ) {
|
||||
OC.msg.startSaving('#ocFederationAddServer .msg');
|
||||
/**
|
||||
*
|
||||
* @param url
|
||||
*/
|
||||
function addServer(url) {
|
||||
OC.msg.startSaving('#ocFederationAddServer .msg')
|
||||
|
||||
$.post(
|
||||
OC.getRootPath() + '/ocs/v2.php/apps/federation/trusted-servers',
|
||||
{
|
||||
url: url
|
||||
},
|
||||
null,
|
||||
'json'
|
||||
).done(function({ ocs }) {
|
||||
var data = ocs.data;
|
||||
$("#serverUrl").attr('value', '');
|
||||
$("#listOfTrustedServers").prepend(
|
||||
$('<li>')
|
||||
.attr('id', data.id)
|
||||
.html('<span class="status indeterminate"></span>' +
|
||||
data.url +
|
||||
'<span class="icon icon-delete"></span>')
|
||||
);
|
||||
OC.msg.finishedSuccess('#ocFederationAddServer .msg', data.message);
|
||||
})
|
||||
.fail(function (jqXHR) {
|
||||
OC.msg.finishedError('#ocFederationAddServer .msg', JSON.parse(jqXHR.responseText).ocs.meta.message);
|
||||
});
|
||||
};
|
||||
$.post(
|
||||
OC.getRootPath() + '/ocs/v2.php/apps/federation/trusted-servers',
|
||||
{
|
||||
url,
|
||||
},
|
||||
null,
|
||||
'json',
|
||||
).done(function({ ocs }) {
|
||||
const data = ocs.data
|
||||
$('#serverUrl').attr('value', '')
|
||||
$('#listOfTrustedServers').prepend($('<li>')
|
||||
.attr('id', data.id)
|
||||
.html('<span class="status indeterminate"></span>'
|
||||
+ data.url
|
||||
+ '<span class="icon icon-delete"></span>'))
|
||||
OC.msg.finishedSuccess('#ocFederationAddServer .msg', data.message)
|
||||
})
|
||||
.fail(function(jqXHR) {
|
||||
OC.msg.finishedError('#ocFederationAddServer .msg', JSON.parse(jqXHR.responseText).ocs.meta.message)
|
||||
})
|
||||
}
|
||||
|
||||
function removeServer( id ) {
|
||||
$.ajax({
|
||||
url: OC.getRootPath() + '/ocs/v2.php/apps/federation/trusted-servers/' + id,
|
||||
type: 'DELETE',
|
||||
success: function(response) {
|
||||
$("#ocFederationSettings").find("#" + id).remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
function removeServer(id) {
|
||||
$.ajax({
|
||||
url: OC.getRootPath() + '/ocs/v2.php/apps/federation/trusted-servers/' + id,
|
||||
type: 'DELETE',
|
||||
success: function(response) {
|
||||
$('#ocFederationSettings').find('#' + id).remove()
|
||||
},
|
||||
})
|
||||
}
|
||||
})(jQuery)
|
||||
|
||||
|
||||
})( jQuery );
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
$('#ocFederationSettings').ocFederationAddServer();
|
||||
|
||||
});
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
$('#ocFederationSettings').ocFederationAddServer()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
-->
|
||||
<template>
|
||||
<NcContent app-name="files">
|
||||
<Navigation v-if="!isPublic" />
|
||||
<FilesNavigation v-if="!isPublic" />
|
||||
<FilesList :is-public="isPublic" />
|
||||
</NcContent>
|
||||
</template>
|
||||
|
|
@ -13,9 +13,9 @@
|
|||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
import { defineComponent } from 'vue'
|
||||
import NcContent from '@nextcloud/vue/components/NcContent'
|
||||
import Navigation from './views/Navigation.vue'
|
||||
import FilesList from './views/FilesList.vue'
|
||||
import { useHotKeys } from './composables/useHotKeys'
|
||||
import FilesNavigation from './views/FilesNavigation.vue'
|
||||
import { useHotKeys } from './composables/useHotKeys.ts'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FilesApp',
|
||||
|
|
@ -23,7 +23,7 @@ export default defineComponent({
|
|||
components: {
|
||||
NcContent,
|
||||
FilesList,
|
||||
Navigation,
|
||||
FilesNavigation,
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
|
|
|||
|
|
@ -2,25 +2,28 @@
|
|||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import { FileAction, registerFileAction } from '@nextcloud/files'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
import AutoRenewSvg from '@mdi/svg/svg/autorenew.svg?raw'
|
||||
|
||||
import { convertFile, convertFiles } from './convertUtils'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { FileAction, registerFileAction } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { convertFile, convertFiles } from './convertUtils.ts'
|
||||
|
||||
type ConversionsProvider = {
|
||||
from: string,
|
||||
to: string,
|
||||
displayName: string,
|
||||
from: string
|
||||
to: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
export const ACTION_CONVERT = 'convert'
|
||||
export const registerConvertActions = () => {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function registerConvertActions() {
|
||||
// Generate sub actions
|
||||
const convertProviders = getCapabilities()?.files?.file_conversions as ConversionsProvider[] ?? []
|
||||
const actions = convertProviders.map(({ to, from, displayName }) => {
|
||||
|
|
@ -30,7 +33,7 @@ export const registerConvertActions = () => {
|
|||
iconSvgInline: () => generateIconSvg(to),
|
||||
enabled: (nodes: Node[]) => {
|
||||
// Check that all nodes have the same mime type
|
||||
return nodes.every(node => from === node.mime)
|
||||
return nodes.every((node) => from === node.mime)
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
|
|
@ -42,7 +45,7 @@ export const registerConvertActions = () => {
|
|||
},
|
||||
|
||||
async execBatch(nodes: Node[]) {
|
||||
const fileIds = nodes.map(node => node.fileid).filter(Boolean) as number[]
|
||||
const fileIds = nodes.map((node) => node.fileid).filter(Boolean) as number[]
|
||||
convertFiles(fileIds, to)
|
||||
|
||||
// Silently terminate, we'll handle the UI in the background
|
||||
|
|
@ -56,10 +59,10 @@ export const registerConvertActions = () => {
|
|||
// Register main action
|
||||
registerFileAction(new FileAction({
|
||||
id: ACTION_CONVERT,
|
||||
displayName: () => t('files', 'Save as …'),
|
||||
displayName: () => t('files', 'Save as …'),
|
||||
iconSvgInline: () => AutoRenewSvg,
|
||||
enabled: (nodes: Node[], view: View) => {
|
||||
return actions.some(action => action.enabled!(nodes, view))
|
||||
return actions.some((action) => action.enabled!(nodes, view))
|
||||
},
|
||||
async exec() {
|
||||
return null
|
||||
|
|
@ -71,7 +74,11 @@ export const registerConvertActions = () => {
|
|||
actions.forEach(registerFileAction)
|
||||
}
|
||||
|
||||
export const generateIconSvg = (mime: string) => {
|
||||
/**
|
||||
*
|
||||
* @param mime
|
||||
*/
|
||||
export function generateIconSvg(mime: string) {
|
||||
// Generate icon based on mime type
|
||||
const url = generateUrl('/core/mimeicon?mime=' + encodeURIComponent(mime))
|
||||
return `<svg width="32" height="32" viewBox="0 0 32 32"
|
||||
|
|
|
|||
|
|
@ -2,18 +2,18 @@
|
|||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { AxiosResponse, AxiosError } from '@nextcloud/axios'
|
||||
|
||||
import type { AxiosError, AxiosResponse } from '@nextcloud/axios'
|
||||
import type { OCSResponse } from '@nextcloud/typings/ocs'
|
||||
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { showError, showLoading, showSuccess } from '@nextcloud/dialogs'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import axios, { isAxiosError } from '@nextcloud/axios'
|
||||
import { showError, showLoading, showSuccess } from '@nextcloud/dialogs'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import PQueue from 'p-queue'
|
||||
|
||||
import logger from '../logger.ts'
|
||||
import { fetchNode } from '../services/WebdavClient.ts'
|
||||
import logger from '../logger'
|
||||
|
||||
type ConversionResponse = {
|
||||
path: string
|
||||
|
|
@ -25,30 +25,40 @@ interface PromiseRejectedResult<T> {
|
|||
reason: T
|
||||
}
|
||||
|
||||
type PromiseSettledResult<T, E> = PromiseFulfilledResult<T> | PromiseRejectedResult<E>;
|
||||
type PromiseSettledResult<T, E> = PromiseFulfilledResult<T> | PromiseRejectedResult<E>
|
||||
type ConversionSuccess = AxiosResponse<OCSResponse<ConversionResponse>>
|
||||
type ConversionError = AxiosError<OCSResponse<ConversionResponse>>
|
||||
|
||||
const queue = new PQueue({ concurrency: 5 })
|
||||
const requestConversion = function(fileId: number, targetMimeType: string): Promise<AxiosResponse> {
|
||||
/**
|
||||
*
|
||||
* @param fileId
|
||||
* @param targetMimeType
|
||||
*/
|
||||
function requestConversion(fileId: number, targetMimeType: string): Promise<AxiosResponse> {
|
||||
return axios.post(generateOcsUrl('/apps/files/api/v1/convert'), {
|
||||
fileId,
|
||||
targetMimeType,
|
||||
})
|
||||
}
|
||||
|
||||
export const convertFiles = async function(fileIds: number[], targetMimeType: string) {
|
||||
const conversions = fileIds.map(fileId => queue.add(() => requestConversion(fileId, targetMimeType)))
|
||||
/**
|
||||
*
|
||||
* @param fileIds
|
||||
* @param targetMimeType
|
||||
*/
|
||||
export async function convertFiles(fileIds: number[], targetMimeType: string) {
|
||||
const conversions = fileIds.map((fileId) => queue.add(() => requestConversion(fileId, targetMimeType)))
|
||||
|
||||
// Start conversion
|
||||
const toast = showLoading(t('files', 'Converting files …'))
|
||||
const toast = showLoading(t('files', 'Converting files …'))
|
||||
|
||||
// Handle results
|
||||
try {
|
||||
const results = await Promise.allSettled(conversions) as PromiseSettledResult<ConversionSuccess, ConversionError>[]
|
||||
const failed = results.filter(result => result.status === 'rejected') as PromiseRejectedResult<ConversionError>[]
|
||||
const failed = results.filter((result) => result.status === 'rejected') as PromiseRejectedResult<ConversionError>[]
|
||||
if (failed.length > 0) {
|
||||
const messages = failed.map(result => result.reason?.response?.data?.ocs?.meta?.message)
|
||||
const messages = failed.map((result) => result.reason?.response?.data?.ocs?.meta?.message)
|
||||
logger.error('Failed to convert files', { fileIds, targetMimeType, messages })
|
||||
|
||||
// If all failed files have the same error message, show it
|
||||
|
|
@ -84,16 +94,16 @@ export const convertFiles = async function(fileIds: number[], targetMimeType: st
|
|||
// might have changed as the user navigated away
|
||||
const currentDir = window.OCP.Files.Router.query.dir as string
|
||||
const newPaths = results
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.map(result => result.value.data.ocs.data.path)
|
||||
.filter(path => path.startsWith(currentDir))
|
||||
.filter((result) => result.status === 'fulfilled')
|
||||
.map((result) => result.value.data.ocs.data.path)
|
||||
.filter((path) => path.startsWith(currentDir))
|
||||
|
||||
// Fetch the new files
|
||||
logger.debug('Files to fetch', { newPaths })
|
||||
const newFiles = await Promise.all(newPaths.map(path => fetchNode(path)))
|
||||
const newFiles = await Promise.all(newPaths.map((path) => fetchNode(path)))
|
||||
|
||||
// Inform the file list about the new files
|
||||
newFiles.forEach(file => emit('files:node:created', file))
|
||||
newFiles.forEach((file) => emit('files:node:created', file))
|
||||
|
||||
// Switch to the new files
|
||||
const firstSuccess = results[0] as PromiseFulfilledResult<ConversionSuccess>
|
||||
|
|
@ -109,8 +119,13 @@ export const convertFiles = async function(fileIds: number[], targetMimeType: st
|
|||
}
|
||||
}
|
||||
|
||||
export const convertFile = async function(fileId: number, targetMimeType: string) {
|
||||
const toast = showLoading(t('files', 'Converting file …'))
|
||||
/**
|
||||
*
|
||||
* @param fileId
|
||||
* @param targetMimeType
|
||||
*/
|
||||
export async function convertFile(fileId: number, targetMimeType: string) {
|
||||
const toast = showLoading(t('files', 'Converting file …'))
|
||||
|
||||
try {
|
||||
const result = await queue.add(() => requestConversion(fileId, targetMimeType)) as AxiosResponse<OCSResponse<ConversionResponse>>
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { File, Folder, Permission, View, FileAction } from '@nextcloud/files'
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import * as capabilities from '@nextcloud/capabilities'
|
||||
import * as eventBus from '@nextcloud/event-bus'
|
||||
|
||||
import { action } from './deleteAction'
|
||||
import logger from '../logger'
|
||||
import { shouldAskForConfirmation } from './deleteUtils'
|
||||
import { File, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import logger from '../logger.ts'
|
||||
import { action } from './deleteAction.ts'
|
||||
import { shouldAskForConfirmation } from './deleteUtils.ts'
|
||||
|
||||
vi.mock('@nextcloud/auth')
|
||||
vi.mock('@nextcloud/axios')
|
||||
|
|
@ -389,7 +389,9 @@ describe('Delete action execute tests', () => {
|
|||
})
|
||||
|
||||
test('Delete fails', async () => {
|
||||
vi.spyOn(axios, 'delete').mockImplementation(() => { throw new Error('Mock error') })
|
||||
vi.spyOn(axios, 'delete').mockImplementation(() => {
|
||||
throw new Error('Mock error')
|
||||
})
|
||||
vi.spyOn(logger, 'error').mockImplementation(() => vi.fn())
|
||||
vi.spyOn(eventBus, 'emit')
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { Permission, Node, View, FileAction } from '@nextcloud/files'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import PQueue from 'p-queue'
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import CloseSvg from '@mdi/svg/svg/close.svg?raw'
|
||||
import NetworkOffSvg from '@mdi/svg/svg/network-off.svg?raw'
|
||||
import TrashCanSvg from '@mdi/svg/svg/trash-can-outline.svg?raw'
|
||||
|
||||
import { FileAction, Permission } from '@nextcloud/files'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import PQueue from 'p-queue'
|
||||
import { TRASHBIN_VIEW_ID } from '../../../files_trashbin/src/files_views/trashbinView.ts'
|
||||
import { askConfirmation, canDisconnectOnly, canUnshareOnly, deleteNode, displayName, shouldAskForConfirmation } from './deleteUtils.ts'
|
||||
import logger from '../logger.ts'
|
||||
import { askConfirmation, canDisconnectOnly, canUnshareOnly, deleteNode, displayName, shouldAskForConfirmation } from './deleteUtils.ts'
|
||||
|
||||
const queue = new PQueue({ concurrency: 5 })
|
||||
|
||||
|
|
@ -42,8 +42,8 @@ export const action = new FileAction({
|
|||
}
|
||||
|
||||
return nodes.length > 0 && nodes
|
||||
.map(node => node.permissions)
|
||||
.every(permission => (permission & Permission.DELETE) !== 0)
|
||||
.map((node) => node.permissions)
|
||||
.every((permission) => (permission & Permission.DELETE) !== 0)
|
||||
},
|
||||
|
||||
async exec(node: Node, view: View) {
|
||||
|
|
@ -89,9 +89,9 @@ export const action = new FileAction({
|
|||
}
|
||||
|
||||
// Map each node to a promise that resolves with the result of exec(node)
|
||||
const promises = nodes.map(node => {
|
||||
const promises = nodes.map((node) => {
|
||||
// Create a promise that resolves with the result of exec(node)
|
||||
const promise = new Promise<boolean>(resolve => {
|
||||
const promise = new Promise<boolean>((resolve) => {
|
||||
queue.add(async () => {
|
||||
try {
|
||||
await deleteNode(node)
|
||||
|
|
|
|||
|
|
@ -2,48 +2,74 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Capabilities } from '../types'
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
import type { Capabilities } from '../types.ts'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { FileType } from '@nextcloud/files'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { useUserConfigStore } from '../store/userconfig'
|
||||
import { getPinia } from '../store'
|
||||
import { getPinia } from '../store/index.ts'
|
||||
import { useUserConfigStore } from '../store/userconfig.ts'
|
||||
|
||||
export const isTrashbinEnabled = () => (getCapabilities() as Capabilities)?.files?.undelete === true
|
||||
|
||||
export const canUnshareOnly = (nodes: Node[]) => {
|
||||
return nodes.every(node => node.attributes['is-mount-root'] === true
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
*/
|
||||
export function canUnshareOnly(nodes: Node[]) {
|
||||
return nodes.every((node) => node.attributes['is-mount-root'] === true
|
||||
&& node.attributes['mount-type'] === 'shared')
|
||||
}
|
||||
|
||||
export const canDisconnectOnly = (nodes: Node[]) => {
|
||||
return nodes.every(node => node.attributes['is-mount-root'] === true
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
*/
|
||||
export function canDisconnectOnly(nodes: Node[]) {
|
||||
return nodes.every((node) => node.attributes['is-mount-root'] === true
|
||||
&& node.attributes['mount-type'] === 'external')
|
||||
}
|
||||
|
||||
export const isMixedUnshareAndDelete = (nodes: Node[]) => {
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
*/
|
||||
export function isMixedUnshareAndDelete(nodes: Node[]) {
|
||||
if (nodes.length === 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hasSharedItems = nodes.some(node => canUnshareOnly([node]))
|
||||
const hasDeleteItems = nodes.some(node => !canUnshareOnly([node]))
|
||||
const hasSharedItems = nodes.some((node) => canUnshareOnly([node]))
|
||||
const hasDeleteItems = nodes.some((node) => !canUnshareOnly([node]))
|
||||
return hasSharedItems && hasDeleteItems
|
||||
}
|
||||
|
||||
export const isAllFiles = (nodes: Node[]) => {
|
||||
return !nodes.some(node => node.type !== FileType.File)
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
*/
|
||||
export function isAllFiles(nodes: Node[]) {
|
||||
return !nodes.some((node) => node.type !== FileType.File)
|
||||
}
|
||||
|
||||
export const isAllFolders = (nodes: Node[]) => {
|
||||
return !nodes.some(node => node.type !== FileType.Folder)
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
*/
|
||||
export function isAllFolders(nodes: Node[]) {
|
||||
return !nodes.some((node) => node.type !== FileType.Folder)
|
||||
}
|
||||
|
||||
export const displayName = (nodes: Node[], view: View) => {
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
* @param view
|
||||
*/
|
||||
export function displayName(nodes: Node[], view: View) {
|
||||
/**
|
||||
* If those nodes are all the root node of a
|
||||
* share, we can only unshare them.
|
||||
|
|
@ -103,17 +129,25 @@ export const displayName = (nodes: Node[], view: View) => {
|
|||
return t('files', 'Delete')
|
||||
}
|
||||
|
||||
export const shouldAskForConfirmation = () => {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function shouldAskForConfirmation() {
|
||||
const userConfig = useUserConfigStore(getPinia())
|
||||
return userConfig.userConfig.show_dialog_deletion !== false
|
||||
}
|
||||
|
||||
export const askConfirmation = async (nodes: Node[], view: View) => {
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
* @param view
|
||||
*/
|
||||
export async function askConfirmation(nodes: Node[], view: View) {
|
||||
const message = view.id === 'trashbin' || !isTrashbinEnabled()
|
||||
? n('files', 'You are about to permanently delete {count} item', 'You are about to permanently delete {count} items', nodes.length, { count: nodes.length })
|
||||
: n('files', 'You are about to delete {count} item', 'You are about to delete {count} items', nodes.length, { count: nodes.length })
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
// TODO: Use the new dialog API
|
||||
window.OC.dialogs.confirmDestructive(
|
||||
message,
|
||||
|
|
@ -131,7 +165,11 @@ export const askConfirmation = async (nodes: Node[], view: View) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const deleteNode = async (node: Node) => {
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
*/
|
||||
export async function deleteNode(node: Node) {
|
||||
await axios.delete(node.encodedSource)
|
||||
|
||||
// Let's delete even if it's moved to the trashbin
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { File, Folder, Permission, View, FileAction, DefaultType } from '@nextcloud/files'
|
||||
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { action } from './downloadAction'
|
||||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import * as dialogs from '@nextcloud/dialogs'
|
||||
import * as eventBus from '@nextcloud/event-bus'
|
||||
import { DefaultType, File, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import { action } from './downloadAction.ts'
|
||||
|
||||
vi.mock('@nextcloud/axios')
|
||||
vi.mock('@nextcloud/dialogs')
|
||||
|
|
@ -22,7 +23,6 @@ const view = {
|
|||
|
||||
// Mock webroot variable
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any)._oc_webroot = ''
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -2,19 +2,20 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
import { FileAction, FileType, DefaultType } from '@nextcloud/files'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
import ArrowDownSvg from '@mdi/svg/svg/arrow-down.svg?raw'
|
||||
|
||||
import { isDownloadable } from '../utils/permissions'
|
||||
import { usePathsStore } from '../store/paths'
|
||||
import { getPinia } from '../store'
|
||||
import { useFilesStore } from '../store/files'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { DefaultType, FileAction, FileType } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import logger from '../logger.ts'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { getPinia } from '../store/index.ts'
|
||||
import { usePathsStore } from '../store/paths.ts'
|
||||
import { isDownloadable } from '../utils/permissions.ts'
|
||||
|
||||
/**
|
||||
* Trigger downloading a file.
|
||||
|
|
@ -34,6 +35,7 @@ async function triggerDownload(url: string, name?: string) {
|
|||
|
||||
/**
|
||||
* Find the longest common path prefix of both input paths
|
||||
*
|
||||
* @param first The first path
|
||||
* @param second The second path
|
||||
*/
|
||||
|
|
@ -129,7 +131,7 @@ export const action = new FileAction({
|
|||
}
|
||||
|
||||
// We can only download dav files and folders.
|
||||
if (nodes.some(node => !node.isDavResource)) {
|
||||
if (nodes.some((node) => !node.isDavResource)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -144,8 +146,9 @@ export const action = new FileAction({
|
|||
async exec(node: Node) {
|
||||
try {
|
||||
await downloadNodes([node])
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
showError(t('files', 'The requested file is not available.'))
|
||||
logger.error('The requested file is not available.', { error })
|
||||
emit('files:node:deleted', node)
|
||||
}
|
||||
return null
|
||||
|
|
@ -154,8 +157,9 @@ export const action = new FileAction({
|
|||
async execBatch(nodes: Node[], view: View, dir: string) {
|
||||
try {
|
||||
await downloadNodes(nodes)
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
showError(t('files', 'The requested files are not available.'))
|
||||
logger.error('The requested files are not available.', { error })
|
||||
// Try to reload the current directory to update the view
|
||||
const directory = getCurrentDirectory(view, dir)!
|
||||
emit('files:node:updated', directory)
|
||||
|
|
|
|||
|
|
@ -2,14 +2,16 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { File, Permission, View, FileAction } from '@nextcloud/files'
|
||||
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { action } from './favoriteAction'
|
||||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import * as eventBus from '@nextcloud/event-bus'
|
||||
import * as favoriteAction from './favoriteAction'
|
||||
import logger from '../logger'
|
||||
import { File, FileAction, Permission } from '@nextcloud/files'
|
||||
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import logger from '../logger.ts'
|
||||
import { action } from './favoriteAction.ts'
|
||||
import * as favoriteAction from './favoriteAction.ts'
|
||||
|
||||
vi.mock('@nextcloud/auth')
|
||||
vi.mock('@nextcloud/axios')
|
||||
|
|
@ -30,7 +32,7 @@ beforeAll(() => {
|
|||
...window.OC,
|
||||
TAG_FAVORITE: '_$!<Favorite>!$_',
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
(window as any)._oc_webroot = ''
|
||||
})
|
||||
|
||||
|
|
@ -132,7 +134,9 @@ describe('Favorite action enabled tests', () => {
|
|||
})
|
||||
|
||||
describe('Favorite action execute tests', () => {
|
||||
beforeEach(() => { vi.resetAllMocks() })
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
test('Favorite triggers tag addition', async () => {
|
||||
vi.spyOn(axios, 'post')
|
||||
|
|
@ -247,7 +251,9 @@ describe('Favorite action execute tests', () => {
|
|||
|
||||
test('Favorite fails and show error', async () => {
|
||||
const error = new Error('Mock error')
|
||||
vi.spyOn(axios, 'post').mockImplementation(() => { throw new Error('Mock error') })
|
||||
vi.spyOn(axios, 'post').mockImplementation(() => {
|
||||
throw new Error('Mock error')
|
||||
})
|
||||
vi.spyOn(logger, 'error').mockImplementation(() => vi.fn())
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -277,7 +283,9 @@ describe('Favorite action execute tests', () => {
|
|||
|
||||
test('Removing from favorites fails and show error', async () => {
|
||||
const error = new Error('Mock error')
|
||||
vi.spyOn(axios, 'post').mockImplementation(() => { throw error })
|
||||
vi.spyOn(axios, 'post').mockImplementation(() => {
|
||||
throw error
|
||||
})
|
||||
vi.spyOn(logger, 'error').mockImplementation(() => vi.fn())
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -307,7 +315,9 @@ describe('Favorite action execute tests', () => {
|
|||
})
|
||||
|
||||
describe('Favorite action batch execute tests', () => {
|
||||
beforeEach(() => { vi.restoreAllMocks() })
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
test('Favorite action batch execute with mixed files', async () => {
|
||||
vi.spyOn(favoriteAction, 'favoriteNode')
|
||||
|
|
@ -337,7 +347,7 @@ describe('Favorite action batch execute tests', () => {
|
|||
// Mixed states triggers favorite action
|
||||
const exec = await action.execBatch!([file1, file2], view, '/')
|
||||
expect(exec).toStrictEqual([true, true])
|
||||
expect([file1, file2].every(file => file.attributes.favorite === 1)).toBe(true)
|
||||
expect([file1, file2].every((file) => file.attributes.favorite === 1)).toBe(true)
|
||||
|
||||
expect(axios.post).toBeCalledTimes(2)
|
||||
expect(axios.post).toHaveBeenNthCalledWith(1, '/index.php/apps/files/api/v1/files/foo.txt', { tags: ['_$!<Favorite>!$_'] })
|
||||
|
|
@ -372,7 +382,7 @@ describe('Favorite action batch execute tests', () => {
|
|||
// Mixed states triggers favorite action
|
||||
const exec = await action.execBatch!([file1, file2], view, '/')
|
||||
expect(exec).toStrictEqual([true, true])
|
||||
expect([file1, file2].every(file => file.attributes.favorite === 0)).toBe(true)
|
||||
expect([file1, file2].every((file) => file.attributes.favorite === 0)).toBe(true)
|
||||
|
||||
expect(axios.post).toBeCalledTimes(2)
|
||||
expect(axios.post).toHaveBeenNthCalledWith(1, '/index.php/apps/files/api/v1/files/foo.txt', { tags: [] })
|
||||
|
|
|
|||
|
|
@ -4,19 +4,17 @@
|
|||
*/
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import StarOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
|
||||
import StarSvg from '@mdi/svg/svg/star.svg?raw'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { Permission, FileAction } from '@nextcloud/files'
|
||||
import { FileAction, Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { encodePath } from '@nextcloud/paths'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
import axios from '@nextcloud/axios'
|
||||
import PQueue from 'p-queue'
|
||||
import Vue from 'vue'
|
||||
|
||||
import StarOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
|
||||
import StarSvg from '@mdi/svg/svg/star.svg?raw'
|
||||
|
||||
import logger from '../logger.ts'
|
||||
|
||||
export const ACTION_FAVORITE = 'favorite'
|
||||
|
|
@ -24,11 +22,21 @@ export const ACTION_FAVORITE = 'favorite'
|
|||
const queue = new PQueue({ concurrency: 5 })
|
||||
|
||||
// If any of the nodes is not favorited, we display the favorite action.
|
||||
const shouldFavorite = (nodes: Node[]): boolean => {
|
||||
return nodes.some(node => node.attributes.favorite !== 1)
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
*/
|
||||
function shouldFavorite(nodes: Node[]): boolean {
|
||||
return nodes.some((node) => node.attributes.favorite !== 1)
|
||||
}
|
||||
|
||||
export const favoriteNode = async (node: Node, view: View, willFavorite: boolean): Promise<boolean> => {
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @param view
|
||||
* @param willFavorite
|
||||
*/
|
||||
export async function favoriteNode(node: Node, view: View, willFavorite: boolean): Promise<boolean> {
|
||||
try {
|
||||
// TODO: migrate to webdav tags plugin
|
||||
const url = generateUrl('/apps/files/api/v1/files') + encodePath(node.path)
|
||||
|
|
@ -83,9 +91,9 @@ export const action = new FileAction({
|
|||
}
|
||||
|
||||
// We can only favorite nodes if they are located in files
|
||||
return nodes.every(node => node.root?.startsWith?.('/files'))
|
||||
return nodes.every((node) => node.root?.startsWith?.('/files'))
|
||||
// and we have permissions
|
||||
&& nodes.every(node => node.permissions !== Permission.NONE)
|
||||
&& nodes.every((node) => node.permissions !== Permission.NONE)
|
||||
},
|
||||
|
||||
async exec(node: Node, view: View) {
|
||||
|
|
@ -96,9 +104,9 @@ export const action = new FileAction({
|
|||
const willFavorite = shouldFavorite(nodes)
|
||||
|
||||
// Map each node to a promise that resolves with the result of exec(node)
|
||||
const promises = nodes.map(node => {
|
||||
const promises = nodes.map((node) => {
|
||||
// Create a promise that resolves with the result of exec(node)
|
||||
const promise = new Promise<boolean>(resolve => {
|
||||
const promise = new Promise<boolean>((resolve) => {
|
||||
queue.add(async () => {
|
||||
try {
|
||||
await favoriteNode(node, view, willFavorite)
|
||||
|
|
|
|||
|
|
@ -2,33 +2,33 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Folder, Node, View } from '@nextcloud/files'
|
||||
import type { IFilePickerButton } from '@nextcloud/dialogs'
|
||||
import type { FileStat, ResponseDataDetailed, WebDAVClientError } from 'webdav'
|
||||
import type { MoveCopyResult } from './moveOrCopyActionUtils'
|
||||
|
||||
import type { IFilePickerButton } from '@nextcloud/dialogs'
|
||||
import type { Folder, Node, View } from '@nextcloud/files'
|
||||
import type { FileStat, ResponseDataDetailed, WebDAVClientError } from 'webdav'
|
||||
import type { MoveCopyResult } from './moveOrCopyActionUtils.ts'
|
||||
|
||||
import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw'
|
||||
import CopyIconSvg from '@mdi/svg/svg/folder-multiple-outline.svg?raw'
|
||||
import { isAxiosError } from '@nextcloud/axios'
|
||||
import { FilePickerClosed, getFilePickerBuilder, showError, showInfo, TOAST_PERMANENT_TIMEOUT } from '@nextcloud/dialogs'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { FileAction, FileType, NodeStatus, davGetClient, davRootPath, davResultToNode, davGetDefaultPropfind, getUniqueName, Permission } from '@nextcloud/files'
|
||||
import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath, FileAction, FileType, getUniqueName, NodeStatus, Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { openConflictPicker, hasConflict } from '@nextcloud/upload'
|
||||
import { hasConflict, openConflictPicker } from '@nextcloud/upload'
|
||||
import { basename, join } from 'path'
|
||||
import Vue from 'vue'
|
||||
|
||||
import CopyIconSvg from '@mdi/svg/svg/folder-multiple-outline.svg?raw'
|
||||
import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw'
|
||||
|
||||
import { MoveCopyAction, canCopy, canMove, getQueue } from './moveOrCopyActionUtils'
|
||||
import { getContents } from '../services/Files'
|
||||
import logger from '../logger'
|
||||
import logger from '../logger.ts'
|
||||
import { getContents } from '../services/Files.ts'
|
||||
import { canCopy, canMove, getQueue, MoveCopyAction } from './moveOrCopyActionUtils.ts'
|
||||
|
||||
/**
|
||||
* Return the action that is possible for the given nodes
|
||||
* @param {Node[]} nodes The nodes to check against
|
||||
* @return {MoveCopyAction} The action that is possible for the given nodes
|
||||
*
|
||||
* @param nodes The nodes to check against
|
||||
* @return The action that is possible for the given nodes
|
||||
*/
|
||||
const getActionForNodes = (nodes: Node[]): MoveCopyAction => {
|
||||
function getActionForNodes(nodes: Node[]): MoveCopyAction {
|
||||
if (canMove(nodes)) {
|
||||
if (canCopy(nodes)) {
|
||||
return MoveCopyAction.MOVE_OR_COPY
|
||||
|
|
@ -42,21 +42,25 @@ const getActionForNodes = (nodes: Node[]): MoveCopyAction => {
|
|||
|
||||
/**
|
||||
* Create a loading notification toast
|
||||
*
|
||||
* @param mode The move or copy mode
|
||||
* @param source Name of the node that is copied / moved
|
||||
* @param destination Destination path
|
||||
* @return {() => void} Function to hide the notification
|
||||
* @return Function to hide the notification
|
||||
*/
|
||||
function createLoadingNotification(mode: MoveCopyAction, source: string, destination: string): () => void {
|
||||
const text = mode === MoveCopyAction.MOVE ? t('files', 'Moving "{source}" to "{destination}" …', { source, destination }) : t('files', 'Copying "{source}" to "{destination}" …', { source, destination })
|
||||
const text = mode === MoveCopyAction.MOVE ? t('files', 'Moving "{source}" to "{destination}" …', { source, destination }) : t('files', 'Copying "{source}" to "{destination}" …', { source, destination })
|
||||
|
||||
let toast: ReturnType<typeof showInfo>|undefined
|
||||
let toast: ReturnType<typeof showInfo> | undefined
|
||||
toast = showInfo(
|
||||
`<span class="icon icon-loading-small toast-loading-icon"></span> ${text}`,
|
||||
{
|
||||
isHTML: true,
|
||||
timeout: TOAST_PERMANENT_TIMEOUT,
|
||||
onRemove: () => { toast?.hideToast(); toast = undefined },
|
||||
onRemove() {
|
||||
toast?.hideToast()
|
||||
toast = undefined
|
||||
},
|
||||
},
|
||||
)
|
||||
return () => toast && toast.hideToast()
|
||||
|
|
@ -65,13 +69,14 @@ function createLoadingNotification(mode: MoveCopyAction, source: string, destina
|
|||
/**
|
||||
* Handle the copy/move of a node to a destination
|
||||
* This can be imported and used by other scripts/components on server
|
||||
* @param {Node} node The node to copy/move
|
||||
* @param {Folder} destination The destination to copy/move the node to
|
||||
* @param {MoveCopyAction} method The method to use for the copy/move
|
||||
* @param {boolean} overwrite Whether to overwrite the destination if it exists
|
||||
* @return {Promise<void>} A promise that resolves when the copy/move is done
|
||||
*
|
||||
* @param node The node to copy/move
|
||||
* @param destination The destination to copy/move the node to
|
||||
* @param method The method to use for the copy/move
|
||||
* @param overwrite Whether to overwrite the destination if it exists
|
||||
* @return A promise that resolves when the copy/move is done
|
||||
*/
|
||||
export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, method: MoveCopyAction.COPY | MoveCopyAction.MOVE, overwrite = false) => {
|
||||
export async function handleCopyMoveNodeTo(node: Node, destination: Folder, method: MoveCopyAction.COPY | MoveCopyAction.MOVE, overwrite = false) {
|
||||
if (!destination) {
|
||||
return
|
||||
}
|
||||
|
|
@ -156,7 +161,7 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
|
|||
if (!selected.length && !renamed.length) {
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// User cancelled
|
||||
return
|
||||
}
|
||||
|
|
@ -203,6 +208,7 @@ export const handleCopyMoveNodeTo = async (node: Node, destination: Folder, meth
|
|||
|
||||
/**
|
||||
* Open a file picker for the given action
|
||||
*
|
||||
* @param action The action to open the file picker for
|
||||
* @param dir The directory to start the file picker in
|
||||
* @param nodes The nodes to move/copy
|
||||
|
|
@ -214,7 +220,7 @@ async function openFilePickerForAction(
|
|||
nodes: Node[],
|
||||
): Promise<MoveCopyResult | false> {
|
||||
const { resolve, reject, promise } = Promise.withResolvers<MoveCopyResult | false>()
|
||||
const fileIDs = nodes.map(node => node.fileid).filter(Boolean)
|
||||
const fileIDs = nodes.map((node) => node.fileid).filter(Boolean)
|
||||
const filePicker = getFilePickerBuilder(t('files', 'Choose destination'))
|
||||
.allowDirectories(true)
|
||||
.setFilter((n: Node) => {
|
||||
|
|
@ -228,8 +234,8 @@ async function openFilePickerForAction(
|
|||
const buttons: IFilePickerButton[] = []
|
||||
const target = basename(path)
|
||||
|
||||
const dirnames = nodes.map(node => node.dirname)
|
||||
const paths = nodes.map(node => node.path)
|
||||
const dirnames = nodes.map((node) => node.dirname)
|
||||
const paths = nodes.map((node) => node.path)
|
||||
|
||||
if (action === MoveCopyAction.COPY || action === MoveCopyAction.MOVE_OR_COPY) {
|
||||
buttons.push({
|
||||
|
|
@ -298,12 +304,12 @@ export const action = new FileAction({
|
|||
id: ACTION_COPY_MOVE,
|
||||
displayName(nodes: Node[]) {
|
||||
switch (getActionForNodes(nodes)) {
|
||||
case MoveCopyAction.MOVE:
|
||||
return t('files', 'Move')
|
||||
case MoveCopyAction.COPY:
|
||||
return t('files', 'Copy')
|
||||
case MoveCopyAction.MOVE_OR_COPY:
|
||||
return t('files', 'Move or copy')
|
||||
case MoveCopyAction.MOVE:
|
||||
return t('files', 'Move')
|
||||
case MoveCopyAction.COPY:
|
||||
return t('files', 'Copy')
|
||||
case MoveCopyAction.MOVE_OR_COPY:
|
||||
return t('files', 'Move or copy')
|
||||
}
|
||||
},
|
||||
iconSvgInline: () => FolderMoveSvg,
|
||||
|
|
@ -313,7 +319,7 @@ export const action = new FileAction({
|
|||
return false
|
||||
}
|
||||
// We only support moving/copying files within the user folder
|
||||
if (!nodes.every(node => node.root?.startsWith('/files/'))) {
|
||||
if (!nodes.every((node) => node.root?.startsWith('/files/'))) {
|
||||
return false
|
||||
}
|
||||
return nodes.length > 0 && (canMove(nodes) || canCopy(nodes))
|
||||
|
|
@ -353,7 +359,7 @@ export const action = new FileAction({
|
|||
return nodes.map(() => null)
|
||||
}
|
||||
|
||||
const promises = nodes.map(async node => {
|
||||
const promises = nodes.map(async (node) => {
|
||||
try {
|
||||
await handleCopyMoveNodeTo(node, result.destination, result.action)
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
*/
|
||||
|
||||
import type { Folder, Node } from '@nextcloud/files'
|
||||
import type { ShareAttribute } from '../../../files_sharing/src/sharing'
|
||||
import type { ShareAttribute } from '../../../files_sharing/src/sharing.ts'
|
||||
|
||||
import { Permission } from '@nextcloud/files'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
import PQueue from 'p-queue'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
const sharePermissions = loadState<number>('files_sharing', 'sharePermissions', Permission.NONE)
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ const MAX_CONCURRENCY = 5
|
|||
/**
|
||||
* Get the processing queue
|
||||
*/
|
||||
export const getQueue = () => {
|
||||
export function getQueue() {
|
||||
if (!queue) {
|
||||
queue = new PQueue({ concurrency: MAX_CONCURRENCY })
|
||||
}
|
||||
|
|
@ -40,20 +40,31 @@ export type MoveCopyResult = {
|
|||
action: MoveCopyAction.COPY | MoveCopyAction.MOVE
|
||||
}
|
||||
|
||||
export const canMove = (nodes: Node[]) => {
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
*/
|
||||
export function canMove(nodes: Node[]) {
|
||||
const minPermission = nodes.reduce((min, node) => Math.min(min, node.permissions), Permission.ALL)
|
||||
return Boolean(minPermission & Permission.DELETE)
|
||||
}
|
||||
|
||||
export const canDownload = (nodes: Node[]) => {
|
||||
return nodes.every(node => {
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
*/
|
||||
export function canDownload(nodes: Node[]) {
|
||||
return nodes.every((node) => {
|
||||
const shareAttributes = JSON.parse(node.attributes?.['share-attributes'] ?? '[]') as Array<ShareAttribute>
|
||||
return !shareAttributes.some(attribute => attribute.scope === 'permissions' && attribute.value === false && attribute.key === 'download')
|
||||
|
||||
return !shareAttributes.some((attribute) => attribute.scope === 'permissions' && attribute.value === false && attribute.key === 'download')
|
||||
})
|
||||
}
|
||||
|
||||
export const canCopy = (nodes: Node[]) => {
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
*/
|
||||
export function canCopy(nodes: Node[]) {
|
||||
// a shared file cannot be copied if the download is disabled
|
||||
if (!canDownload(nodes)) {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { File, Folder, Node, Permission, View, DefaultType, FileAction } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { action } from './openFolderAction'
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import { DefaultType, File, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
import { action } from './openFolderAction.ts'
|
||||
|
||||
const view = {
|
||||
id: 'files',
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { Permission, Node, FileType, View, FileAction, DefaultType } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
|
||||
import { DefaultType, FileAction, FileType, Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
export const action = new FileAction({
|
||||
id: 'open-folder',
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { action } from './openInFilesAction'
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import { DefaultType, File, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
import { File, Folder, Permission, View, DefaultType, FileAction } from '@nextcloud/files'
|
||||
import { action } from './openInFilesAction.ts'
|
||||
|
||||
const view = {
|
||||
id: 'files',
|
||||
|
|
@ -43,7 +46,6 @@ describe('Open in files action enabled tests', () => {
|
|||
describe('Open in files action execute tests', () => {
|
||||
test('Open in files', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -65,7 +67,6 @@ describe('Open in files action execute tests', () => {
|
|||
|
||||
test('Open in files with folder', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new Folder({
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import { DefaultType, FileAction, FileType } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { FileType, FileAction, DefaultType } from '@nextcloud/files'
|
||||
import { VIEW_ID as SEARCH_VIEW_ID } from '../views/search'
|
||||
import { VIEW_ID as SEARCH_VIEW_ID } from '../views/search.ts'
|
||||
|
||||
export const action = new FileAction({
|
||||
id: 'open-in-files',
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { File, Permission, View, FileAction } from '@nextcloud/files'
|
||||
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import * as nextcloudDialogs from '@nextcloud/dialogs'
|
||||
import { action } from './openLocallyAction'
|
||||
import { File, FileAction, Permission } from '@nextcloud/files'
|
||||
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import { action } from './openLocallyAction.ts'
|
||||
|
||||
vi.mock('@nextcloud/auth')
|
||||
vi.mock('@nextcloud/axios')
|
||||
|
|
@ -19,9 +21,8 @@ const view = {
|
|||
|
||||
// Mock web root variable
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any)._oc_webroot = '';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
(window as any).OCA = { Viewer: { open: vi.fn() } }
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,20 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { encodePath } from '@nextcloud/paths'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { FileAction, Permission, type Node } from '@nextcloud/files'
|
||||
import { showError, DialogBuilder } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import LaptopSvg from '@mdi/svg/svg/laptop.svg?raw'
|
||||
import IconWeb from '@mdi/svg/svg/web.svg?raw'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { DialogBuilder, showError } from '@nextcloud/dialogs'
|
||||
import { FileAction, Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { encodePath } from '@nextcloud/paths'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
import logger from '../logger.ts'
|
||||
|
||||
export const action = new FileAction({
|
||||
id: 'edit-locally',
|
||||
|
|
@ -79,14 +83,15 @@ async function openLocalClient(path: string): Promise<void> {
|
|||
window.open(url, '_self')
|
||||
} catch (error) {
|
||||
showError(t('files', 'Failed to redirect to client'))
|
||||
logger.error('Failed to redirect to client', { error })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the confirmation dialog.
|
||||
*/
|
||||
async function confirmLocalEditDialog(): Promise<'online'|'local'|false> {
|
||||
let result: 'online'|'local'|false = false
|
||||
async function confirmLocalEditDialog(): Promise<'online' | 'local' | false> {
|
||||
let result: 'online' | 'local' | false = false
|
||||
const dialog = (new DialogBuilder())
|
||||
.setName(t('files', 'Open file locally'))
|
||||
.setText(t('files', 'The file should now open on your device. If it doesn\'t, please check that you have the desktop app installed.'))
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { action } from './renameAction'
|
||||
import { File, Folder, Permission, View, FileAction } from '@nextcloud/files'
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import * as eventBus from '@nextcloud/event-bus'
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest'
|
||||
import { useFilesStore } from '../store/files'
|
||||
import { File, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { getPinia } from '../store/index.ts'
|
||||
import { action } from './renameAction.ts'
|
||||
|
||||
const view = {
|
||||
id: 'files',
|
||||
|
|
@ -59,6 +62,7 @@ describe('Rename action enabled tests', () => {
|
|||
})
|
||||
|
||||
test('Disabled if more than one node', () => {
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: {} } }
|
||||
|
||||
const file1 = new File({
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { Permission, type Node, FileAction, View } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import PencilSvg from '@mdi/svg/svg/pencil-outline.svg?raw'
|
||||
import { getPinia } from '../store'
|
||||
import { useFilesStore } from '../store/files'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { FileAction, Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { dirname } from 'path'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { getPinia } from '../store/index.ts'
|
||||
|
||||
export const ACTION_RENAME = 'rename'
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { File, Permission, View, FileAction, Folder } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { action } from './sidebarAction'
|
||||
import logger from '../logger'
|
||||
import type { View } from '@nextcloud/files'
|
||||
|
||||
import { File, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
import logger from '../logger.ts'
|
||||
import { action } from './sidebarAction.ts'
|
||||
|
||||
const view = {
|
||||
id: 'files',
|
||||
|
|
@ -26,6 +28,7 @@ describe('Open sidebar action conditions tests', () => {
|
|||
|
||||
describe('Open sidebar action enabled tests', () => {
|
||||
test('Enabled for ressources within user root folder', () => {
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: {} } }
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -41,6 +44,7 @@ describe('Open sidebar action enabled tests', () => {
|
|||
})
|
||||
|
||||
test('Disabled without permissions', () => {
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: {} } }
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -53,10 +57,10 @@ describe('Open sidebar action enabled tests', () => {
|
|||
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([file], view)).toBe(false)
|
||||
|
||||
})
|
||||
|
||||
test('Disabled if more than one node', () => {
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: {} } }
|
||||
|
||||
const file1 = new File({
|
||||
|
|
@ -77,6 +81,7 @@ describe('Open sidebar action enabled tests', () => {
|
|||
})
|
||||
|
||||
test('Disabled if no Sidebar', () => {
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = {}
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -91,6 +96,7 @@ describe('Open sidebar action enabled tests', () => {
|
|||
})
|
||||
|
||||
test('Disabled for non-dav ressources', () => {
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: {} } }
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -109,10 +115,10 @@ describe('Open sidebar action exec tests', () => {
|
|||
test('Open sidebar', async () => {
|
||||
const openMock = vi.fn()
|
||||
const defaultTabMock = vi.fn()
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: { open: openMock, setActiveTab: defaultTabMock } } }
|
||||
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -138,10 +144,10 @@ describe('Open sidebar action exec tests', () => {
|
|||
test('Open sidebar for folder', async () => {
|
||||
const openMock = vi.fn()
|
||||
const defaultTabMock = vi.fn()
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: { open: openMock, setActiveTab: defaultTabMock } } }
|
||||
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new Folder({
|
||||
|
|
@ -165,8 +171,11 @@ describe('Open sidebar action exec tests', () => {
|
|||
})
|
||||
|
||||
test('Open sidebar fails', async () => {
|
||||
const openMock = vi.fn(() => { throw new Error('Mock error') })
|
||||
const openMock = vi.fn(() => {
|
||||
throw new Error('Mock error')
|
||||
})
|
||||
const defaultTabMock = vi.fn()
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: { open: openMock, setActiveTab: defaultTabMock } } }
|
||||
vi.spyOn(logger, 'error').mockImplementation(() => vi.fn())
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@
|
|||
*/
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import { Permission, FileAction } from '@nextcloud/files'
|
||||
import InformationSvg from '@mdi/svg/svg/information-outline.svg?raw'
|
||||
import { FileAction, Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
|
||||
import InformationSvg from '@mdi/svg/svg/information-outline.svg?raw'
|
||||
|
||||
import logger from '../logger.ts'
|
||||
|
||||
export const ACTION_DETAILS = 'details'
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { File, Folder, Node, Permission, View, FileAction } from '@nextcloud/files'
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import { File, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
import { action } from './viewInFolderAction'
|
||||
import { action } from './viewInFolderAction.ts'
|
||||
|
||||
const view = {
|
||||
id: 'trashbin',
|
||||
|
|
@ -126,7 +128,6 @@ describe('View in folder action enabled tests', () => {
|
|||
describe('View in folder action execute tests', () => {
|
||||
test('View in folder', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -146,7 +147,6 @@ describe('View in folder action execute tests', () => {
|
|||
|
||||
test('View in (sub) folder', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -167,7 +167,6 @@ describe('View in folder action execute tests', () => {
|
|||
|
||||
test('View in folder fails without node', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const exec = await action.exec(null as unknown as Node, view, '/')
|
||||
|
|
@ -177,7 +176,6 @@ describe('View in folder action execute tests', () => {
|
|||
|
||||
test('View in folder fails without File', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error We only mock what needed, we do not need Files.Router.goTo or Files.Navigation
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const folder = new Folder({
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@
|
|||
*/
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw'
|
||||
import { FileAction, FileType, Permission } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
import FolderMoveSvg from '@mdi/svg/svg/folder-move-outline.svg?raw'
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
|
||||
export const action = new FileAction({
|
||||
id: 'view-in-folder',
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<NcBreadcrumbs data-cy-files-content-breadcrumbs
|
||||
<NcBreadcrumbs
|
||||
data-cy-files-content-breadcrumbs
|
||||
:aria-label="t('files', 'Current directory path')"
|
||||
class="files-list__breadcrumbs"
|
||||
:class="{ 'files-list__breadcrumbs--with-progress': wrapUploadProgressBar }">
|
||||
<!-- Current path sections -->
|
||||
<NcBreadcrumb v-for="(section, index) in sections"
|
||||
<NcBreadcrumb
|
||||
v-for="(section, index) in sections"
|
||||
:key="section.dir"
|
||||
v-bind="section"
|
||||
dir="auto"
|
||||
|
|
@ -21,7 +23,8 @@
|
|||
@dragover.native="onDragOver($event, section.dir)"
|
||||
@drop="onDrop($event, section.dir)">
|
||||
<template v-if="index === 0" #icon>
|
||||
<NcIconSvgWrapper :size="20"
|
||||
<NcIconSvgWrapper
|
||||
:size="20"
|
||||
:svg="viewIcon" />
|
||||
</template>
|
||||
</NcBreadcrumb>
|
||||
|
|
@ -37,25 +40,24 @@
|
|||
import type { Node } from '@nextcloud/files'
|
||||
import type { FileSource } from '../types.ts'
|
||||
|
||||
import { basename } from 'path'
|
||||
import { defineComponent } from 'vue'
|
||||
import HomeSvg from '@mdi/svg/svg/home.svg?raw'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import HomeSvg from '@mdi/svg/svg/home.svg?raw'
|
||||
import { basename } from 'path'
|
||||
import { defineComponent } from 'vue'
|
||||
import NcBreadcrumb from '@nextcloud/vue/components/NcBreadcrumb'
|
||||
import NcBreadcrumbs from '@nextcloud/vue/components/NcBreadcrumbs'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
|
||||
import { useNavigation } from '../composables/useNavigation.ts'
|
||||
import { onDropInternalFiles, dataTransferToFileTree, onDropExternalFiles } from '../services/DropService.ts'
|
||||
import { useFileListWidth } from '../composables/useFileListWidth.ts'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { useNavigation } from '../composables/useNavigation.ts'
|
||||
import logger from '../logger.ts'
|
||||
import { dataTransferToFileTree, onDropExternalFiles, onDropInternalFiles } from '../services/DropService.ts'
|
||||
import { useDragAndDropStore } from '../store/dragging.ts'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { usePathsStore } from '../store/paths.ts'
|
||||
import { useSelectionStore } from '../store/selection.ts'
|
||||
import { useUploaderStore } from '../store/uploader.ts'
|
||||
import logger from '../logger'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BreadCrumbs',
|
||||
|
|
@ -148,9 +150,11 @@ export default defineComponent({
|
|||
getNodeFromSource(source: FileSource): Node | undefined {
|
||||
return this.filesStore.getNode(source)
|
||||
},
|
||||
|
||||
getFileSourceFromPath(path: string): FileSource | null {
|
||||
return (this.currentView && this.pathsStore.getPath(this.currentView.id, path)) ?? null
|
||||
},
|
||||
|
||||
getDirDisplayName(path: string): string {
|
||||
if (path === '/') {
|
||||
return this.currentView?.name || t('files', 'Home')
|
||||
|
|
@ -170,7 +174,7 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
if (node === undefined) {
|
||||
const view = this.views.find(view => view.params?.dir === dir)
|
||||
const view = this.views.find((view) => view.params?.dir === dir)
|
||||
return {
|
||||
...this.$route,
|
||||
params: { fileid: view?.params?.fileid ?? '' },
|
||||
|
|
@ -254,12 +258,12 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// Else we're moving/copying files
|
||||
const nodes = selection.map(source => this.filesStore.getNode(source)) as Node[]
|
||||
const nodes = selection.map((source) => this.filesStore.getNode(source)) as Node[]
|
||||
await onDropInternalFiles(nodes, folder, contents.contents, isCopy)
|
||||
|
||||
// Reset selection after we dropped the files
|
||||
// if the dropped files are within the selection
|
||||
if (selection.some(source => this.selectedFiles.includes(source))) {
|
||||
if (selection.some((source) => this.selectedFiles.includes(source))) {
|
||||
logger.debug('Dropped selection, resetting select store...')
|
||||
this.selectionStore.reset()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,26 +20,32 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
currentView: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
render: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
source() {
|
||||
this.updateRootElement()
|
||||
},
|
||||
|
||||
currentView() {
|
||||
this.updateRootElement()
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateRootElement()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async updateRootElement() {
|
||||
const element = await this.render(this.source, this.currentView)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<div v-show="dragover"
|
||||
<div
|
||||
v-show="dragover"
|
||||
data-cy-files-drag-drop-area
|
||||
class="files-list__drag-drop-notice"
|
||||
@drop="onDrop">
|
||||
|
|
@ -27,20 +28,19 @@
|
|||
|
||||
<script lang="ts">
|
||||
import type { Folder } from '@nextcloud/files'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RawLocation } from 'vue-router'
|
||||
|
||||
import { Permission } from '@nextcloud/files'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { UploadStatus } from '@nextcloud/upload'
|
||||
import { defineComponent, type PropType } from 'vue'
|
||||
import debounce from 'debounce'
|
||||
|
||||
import { defineComponent } from 'vue'
|
||||
import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'
|
||||
|
||||
import { useNavigation } from '../composables/useNavigation'
|
||||
import { dataTransferToFileTree, onDropExternalFiles } from '../services/DropService'
|
||||
import { useNavigation } from '../composables/useNavigation.ts'
|
||||
import logger from '../logger.ts'
|
||||
import type { RawLocation } from 'vue-router'
|
||||
import { dataTransferToFileTree, onDropExternalFiles } from '../services/DropService.ts'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DragAndDropNotice',
|
||||
|
|
@ -77,6 +77,7 @@ export default defineComponent({
|
|||
canUpload() {
|
||||
return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) !== 0
|
||||
},
|
||||
|
||||
isQuotaExceeded() {
|
||||
return this.currentFolder?.attributes?.['quota-available-bytes'] === 0
|
||||
},
|
||||
|
|
@ -169,7 +170,7 @@ export default defineComponent({
|
|||
event.stopPropagation()
|
||||
|
||||
// Caching the selection
|
||||
const items: DataTransferItem[] = [...event.dataTransfer?.items || []]
|
||||
const items: DataTransferItem[] = Array.from(event.dataTransfer?.items || [])
|
||||
|
||||
// We need to process the dataTransfer ASAP before the
|
||||
// browser clears it. This is why we cache the items too.
|
||||
|
|
@ -210,12 +211,13 @@ export default defineComponent({
|
|||
...this.$route.params,
|
||||
fileid: String(lastUpload.response!.headers['oc-fileid']),
|
||||
},
|
||||
|
||||
query: {
|
||||
...this.$route.query,
|
||||
},
|
||||
}
|
||||
// Remove open file from query
|
||||
delete location.query.openfile
|
||||
delete location.query?.openfile
|
||||
this.$router.push(location)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { FileType, Node, formatFileSize } from '@nextcloud/files'
|
||||
import Vue from 'vue'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import { FileType, formatFileSize } from '@nextcloud/files'
|
||||
import Vue from 'vue'
|
||||
import FileMultipleIcon from 'vue-material-design-icons/FileMultiple.vue'
|
||||
import FolderIcon from 'vue-material-design-icons/Folder.vue'
|
||||
|
||||
import { getSummaryFor } from '../utils/fileUtils.ts'
|
||||
|
||||
export default Vue.extend({
|
||||
|
|
@ -40,6 +40,7 @@ export default Vue.extend({
|
|||
isSingleNode() {
|
||||
return this.nodes.length === 1
|
||||
},
|
||||
|
||||
isSingleFolder() {
|
||||
return this.isSingleNode
|
||||
&& this.nodes[0].type === FileType.Folder
|
||||
|
|
@ -51,6 +52,7 @@ export default Vue.extend({
|
|||
}
|
||||
return `${this.summary} – ${this.size}`
|
||||
},
|
||||
|
||||
size() {
|
||||
const totalSize = this.nodes.reduce((total, node) => total + node.size || 0, 0)
|
||||
const size = parseInt(totalSize, 10) || 0
|
||||
|
|
@ -59,6 +61,7 @@ export default Vue.extend({
|
|||
}
|
||||
return formatFileSize(size, true)
|
||||
},
|
||||
|
||||
summary(): string {
|
||||
if (this.isSingleNode) {
|
||||
const node = this.nodes[0]
|
||||
|
|
@ -75,7 +78,7 @@ export default Vue.extend({
|
|||
this.$refs.previewImg.replaceChildren()
|
||||
|
||||
// Clone icon node from the list
|
||||
nodes.slice(0, 3).forEach(node => {
|
||||
nodes.slice(0, 3).forEach((node) => {
|
||||
const preview = document.querySelector(`[data-cy-files-list-row-fileid="${node.fileid}"] .files-list__row-icon img`)
|
||||
if (preview) {
|
||||
const previewElmt = this.$refs.previewImg as HTMLElement
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<tr :class="{
|
||||
<tr
|
||||
:class="{
|
||||
'files-list__row--dragover': dragover,
|
||||
'files-list__row--loading': isLoading,
|
||||
'files-list__row--active': isActive,
|
||||
|
|
@ -19,7 +20,8 @@
|
|||
<span v-if="isFailedSource" class="files-list__row--failed" />
|
||||
|
||||
<!-- Checkbox -->
|
||||
<FileEntryCheckbox :fileid="fileid"
|
||||
<FileEntryCheckbox
|
||||
:fileid="fileid"
|
||||
:is-loading="isLoading"
|
||||
:nodes="nodes"
|
||||
:source="source" />
|
||||
|
|
@ -27,13 +29,15 @@
|
|||
<!-- Link to file -->
|
||||
<td class="files-list__row-name" data-cy-files-list-row-name>
|
||||
<!-- Icon or preview -->
|
||||
<FileEntryPreview ref="preview"
|
||||
<FileEntryPreview
|
||||
ref="preview"
|
||||
:source="source"
|
||||
:dragover="dragover"
|
||||
@auxclick.native="execDefaultAction"
|
||||
@click.native="execDefaultAction" />
|
||||
|
||||
<FileEntryName ref="name"
|
||||
<FileEntryName
|
||||
ref="name"
|
||||
:basename="basename"
|
||||
:extension="extension"
|
||||
:nodes="nodes"
|
||||
|
|
@ -43,14 +47,16 @@
|
|||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<FileEntryActions v-show="!isRenamingSmallScreen"
|
||||
<FileEntryActions
|
||||
v-show="!isRenamingSmallScreen"
|
||||
ref="actions"
|
||||
:class="`files-list__row-actions-${uniqueId}`"
|
||||
:opened.sync="openedMenu"
|
||||
:source="source" />
|
||||
|
||||
<!-- Mime -->
|
||||
<td v-if="isMimeAvailable"
|
||||
<td
|
||||
v-if="isMimeAvailable"
|
||||
:title="mime"
|
||||
class="files-list__row-mime"
|
||||
data-cy-files-list-row-mime
|
||||
|
|
@ -59,7 +65,8 @@
|
|||
</td>
|
||||
|
||||
<!-- Size -->
|
||||
<td v-if="!compact && isSizeAvailable"
|
||||
<td
|
||||
v-if="!compact && isSizeAvailable"
|
||||
:style="sizeOpacity"
|
||||
class="files-list__row-size"
|
||||
data-cy-files-list-row-size
|
||||
|
|
@ -68,25 +75,29 @@
|
|||
</td>
|
||||
|
||||
<!-- Mtime -->
|
||||
<td v-if="!compact && isMtimeAvailable"
|
||||
<td
|
||||
v-if="!compact && isMtimeAvailable"
|
||||
:style="mtimeOpacity"
|
||||
class="files-list__row-mtime"
|
||||
data-cy-files-list-row-mtime
|
||||
@click="openDetailsIfAvailable">
|
||||
<NcDateTime v-if="mtime"
|
||||
<NcDateTime
|
||||
v-if="mtime"
|
||||
ignore-seconds
|
||||
:timestamp="mtime" />
|
||||
<span v-else>{{ t('files', 'Unknown date') }}</span>
|
||||
</td>
|
||||
|
||||
<!-- View columns -->
|
||||
<td v-for="column in columns"
|
||||
<td
|
||||
v-for="column in columns"
|
||||
:key="column.id"
|
||||
:class="`files-list__row-${currentView.id}-${column.id}`"
|
||||
class="files-list__row-column-custom"
|
||||
:data-cy-files-list-row-column-custom="column.id"
|
||||
@click="openDetailsIfAvailable">
|
||||
<CustomElementRender :current-view="currentView"
|
||||
<CustomElementRender
|
||||
:current-view="currentView"
|
||||
:render="column.render"
|
||||
:source="source" />
|
||||
</td>
|
||||
|
|
@ -95,26 +106,24 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { FileType, formatFileSize } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
|
||||
import { defineComponent } from 'vue'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
|
||||
|
||||
import { useNavigation } from '../composables/useNavigation.ts'
|
||||
import CustomElementRender from './CustomElementRender.vue'
|
||||
import FileEntryActions from './FileEntry/FileEntryActions.vue'
|
||||
import FileEntryCheckbox from './FileEntry/FileEntryCheckbox.vue'
|
||||
import FileEntryName from './FileEntry/FileEntryName.vue'
|
||||
import FileEntryPreview from './FileEntry/FileEntryPreview.vue'
|
||||
import { useFileListWidth } from '../composables/useFileListWidth.ts'
|
||||
import { useNavigation } from '../composables/useNavigation.ts'
|
||||
import { useRouteParameters } from '../composables/useRouteParameters.ts'
|
||||
import { useActionsMenuStore } from '../store/actionsmenu.ts'
|
||||
import { useDragAndDropStore } from '../store/dragging.ts'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { useRenamingStore } from '../store/renaming.ts'
|
||||
import { useSelectionStore } from '../store/selection.ts'
|
||||
|
||||
import CustomElementRender from './CustomElementRender.vue'
|
||||
import FileEntryActions from './FileEntry/FileEntryActions.vue'
|
||||
import FileEntryCheckbox from './FileEntry/FileEntryCheckbox.vue'
|
||||
import FileEntryMixin from './FileEntryMixin.ts'
|
||||
import FileEntryName from './FileEntry/FileEntryName.vue'
|
||||
import FileEntryPreview from './FileEntry/FileEntryPreview.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEntry',
|
||||
|
|
@ -137,6 +146,7 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
isSizeAvailable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
|
@ -180,9 +190,9 @@ export default defineComponent({
|
|||
const conditionals = this.isRenaming
|
||||
? {}
|
||||
: {
|
||||
dragstart: this.onDragStart,
|
||||
dragover: this.onDragOver,
|
||||
}
|
||||
dragstart: this.onDragStart,
|
||||
dragover: this.onDragOver,
|
||||
}
|
||||
|
||||
return {
|
||||
...conditionals,
|
||||
|
|
@ -192,6 +202,7 @@ export default defineComponent({
|
|||
drop: this.onDrop,
|
||||
}
|
||||
},
|
||||
|
||||
columns() {
|
||||
// Hide columns if the list is too small
|
||||
if (this.filesListWidth < 512 || this.compact) {
|
||||
|
|
@ -230,6 +241,7 @@ export default defineComponent({
|
|||
|
||||
return this.source.mime
|
||||
},
|
||||
|
||||
size() {
|
||||
const size = this.source.size
|
||||
if (size === undefined || isNaN(size) || size < 0) {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<span :aria-hidden="!title"
|
||||
<span
|
||||
:aria-hidden="!title"
|
||||
:aria-label="title"
|
||||
class="material-design-icon collectives-icon"
|
||||
role="img"
|
||||
v-bind="$attrs"
|
||||
@click="$emit('click', $event)">
|
||||
<svg :fill="fillColor"
|
||||
<svg
|
||||
:fill="fillColor"
|
||||
class="material-design-icon__svg"
|
||||
:width="size"
|
||||
:height="size"
|
||||
|
|
@ -32,10 +34,12 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
fillColor: {
|
||||
type: String,
|
||||
default: 'currentColor',
|
||||
},
|
||||
|
||||
size: {
|
||||
type: Number,
|
||||
default: 24,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import StarSvg from '@mdi/svg/svg/star.svg?raw'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import StarSvg from '@mdi/svg/svg/star.svg?raw'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
|
||||
/**
|
||||
|
|
@ -29,17 +28,20 @@ export default defineComponent({
|
|||
components: {
|
||||
NcIconSvgWrapper,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
StarSvg,
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
await this.$nextTick()
|
||||
// MDI default viewBox is "0 0 24 24" but we add a stroke of 10px so we must adjust it
|
||||
const el = this.$el.querySelector('svg')
|
||||
el?.setAttribute?.('viewBox', '-4 -4 30 30')
|
||||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<td class="files-list__row-actions"
|
||||
<td
|
||||
class="files-list__row-actions"
|
||||
data-cy-files-list-row-actions>
|
||||
<!-- Render actions -->
|
||||
<CustomElementRender v-for="action in enabledRenderActions"
|
||||
<CustomElementRender
|
||||
v-for="action in enabledRenderActions"
|
||||
:key="action.id"
|
||||
:class="'files-list__row-action-' + action.id"
|
||||
:current-view="currentView"
|
||||
|
|
@ -15,11 +17,12 @@
|
|||
class="files-list__row-action--inline" />
|
||||
|
||||
<!-- Menu actions -->
|
||||
<NcActions ref="actionsMenu"
|
||||
<NcActions
|
||||
ref="actionsMenu"
|
||||
:boundaries-element="getBoundariesElement"
|
||||
:container="getBoundariesElement"
|
||||
:force-name="true"
|
||||
type="tertiary"
|
||||
variant="tertiary"
|
||||
:force-menu="enabledInlineActions.length === 0 /* forceMenu only if no inline actions */"
|
||||
:inline="enabledInlineActions.length"
|
||||
:open="openedMenu"
|
||||
|
|
@ -27,7 +30,8 @@
|
|||
@closed="onMenuClosed">
|
||||
<!-- Non-destructive actions list -->
|
||||
<!-- Please keep this block in sync with the destructive actions block below -->
|
||||
<NcActionButton v-for="action, index in renderedNonDestructiveActions"
|
||||
<NcActionButton
|
||||
v-for="action, index in renderedNonDestructiveActions"
|
||||
:key="action.id"
|
||||
:ref="`action-${action.id}`"
|
||||
class="files-list__row-action"
|
||||
|
|
@ -44,7 +48,8 @@
|
|||
@click="onActionClick(action)">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="isLoadingAction(action)" />
|
||||
<NcIconSvgWrapper v-else
|
||||
<NcIconSvgWrapper
|
||||
v-else
|
||||
class="files-list__row-action-icon"
|
||||
:svg="action.iconSvgInline([source], currentView)" />
|
||||
</template>
|
||||
|
|
@ -54,15 +59,15 @@
|
|||
<!-- Destructive actions list -->
|
||||
<template v-if="renderedDestructiveActions.length > 0">
|
||||
<NcActionSeparator />
|
||||
<NcActionButton v-for="action, index in renderedDestructiveActions"
|
||||
<NcActionButton
|
||||
v-for="action, index in renderedDestructiveActions"
|
||||
:key="action.id"
|
||||
:ref="`action-${action.id}`"
|
||||
class="files-list__row-action"
|
||||
class="files-list__row-action files-list__row-action--destructive"
|
||||
:class="{
|
||||
[`files-list__row-action-${action.id}`]: true,
|
||||
'files-list__row-action--inline': index < enabledInlineActions.length,
|
||||
'files-list__row-action--menu': isValidMenu(action),
|
||||
'files-list__row-action--destructive': true,
|
||||
}"
|
||||
:close-after-click="!isValidMenu(action)"
|
||||
:data-cy-files-list-row-action="action.id"
|
||||
|
|
@ -72,7 +77,8 @@
|
|||
@click="onActionClick(action)">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="isLoadingAction(action)" />
|
||||
<NcIconSvgWrapper v-else
|
||||
<NcIconSvgWrapper
|
||||
v-else
|
||||
class="files-list__row-action-icon"
|
||||
:svg="action.iconSvgInline([source], currentView)" />
|
||||
</template>
|
||||
|
|
@ -92,7 +98,8 @@
|
|||
<NcActionSeparator />
|
||||
|
||||
<!-- Submenu actions -->
|
||||
<NcActionButton v-for="action in enabledSubmenuActions[openedSubmenu?.id]"
|
||||
<NcActionButton
|
||||
v-for="action in enabledSubmenuActions[openedSubmenu?.id]"
|
||||
:key="action.id"
|
||||
:class="`files-list__row-action-${action.id}`"
|
||||
class="files-list__row-action--submenu"
|
||||
|
|
@ -113,29 +120,27 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { FileAction, Node } from '@nextcloud/files'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
import { DefaultType, NodeStatus } from '@nextcloud/files'
|
||||
import { defineComponent, inject } from 'vue'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
|
||||
|
||||
import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
|
||||
import CustomElementRender from '../CustomElementRender.vue'
|
||||
import { defineComponent, inject } from 'vue'
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import NcActions from '@nextcloud/vue/components/NcActions'
|
||||
import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
|
||||
import { executeAction } from '../../utils/actionUtils.ts'
|
||||
import { useActiveStore } from '../../store/active.ts'
|
||||
import ArrowLeftIcon from 'vue-material-design-icons/ArrowLeft.vue'
|
||||
import CustomElementRender from '../CustomElementRender.vue'
|
||||
import { useFileListWidth } from '../../composables/useFileListWidth.ts'
|
||||
import { useNavigation } from '../../composables/useNavigation'
|
||||
import { useNavigation } from '../../composables/useNavigation.ts'
|
||||
import { useRouteParameters } from '../../composables/useRouteParameters.ts'
|
||||
import actionsMixins from '../../mixins/actionsMixin.ts'
|
||||
import logger from '../../logger.ts'
|
||||
import actionsMixins from '../../mixins/actionsMixin.ts'
|
||||
import { useActiveStore } from '../../store/active.ts'
|
||||
import { executeAction } from '../../utils/actionUtils.ts'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEntryActions',
|
||||
|
|
@ -157,10 +162,12 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
source: {
|
||||
type: Object as PropType<Node>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
gridMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
|
@ -199,7 +206,7 @@ export default defineComponent({
|
|||
if (this.filesListWidth < 768 || this.gridMode) {
|
||||
return []
|
||||
}
|
||||
return this.enabledFileActions.filter(action => {
|
||||
return this.enabledFileActions.filter((action) => {
|
||||
try {
|
||||
return action?.inline?.(this.source, this.currentView)
|
||||
} catch (error) {
|
||||
|
|
@ -214,7 +221,7 @@ export default defineComponent({
|
|||
if (this.gridMode) {
|
||||
return []
|
||||
}
|
||||
return this.enabledFileActions.filter(action => typeof action.renderInline === 'function')
|
||||
return this.enabledFileActions.filter((action) => typeof action.renderInline === 'function')
|
||||
},
|
||||
|
||||
// Actions shown in the menu
|
||||
|
|
@ -229,31 +236,32 @@ export default defineComponent({
|
|||
// Showing inline first for the NcActions inline prop
|
||||
...this.enabledInlineActions,
|
||||
// Then the rest
|
||||
...this.enabledFileActions.filter(action => action.default !== DefaultType.HIDDEN && typeof action.renderInline !== 'function'),
|
||||
...this.enabledFileActions.filter((action) => action.default !== DefaultType.HIDDEN && typeof action.renderInline !== 'function'),
|
||||
].filter((value, index, self) => {
|
||||
// Then we filter duplicates to prevent inline actions to be shown twice
|
||||
return index === self.findIndex(action => action.id === value.id)
|
||||
return index === self.findIndex((action) => action.id === value.id)
|
||||
})
|
||||
|
||||
// Generate list of all top-level actions ids
|
||||
const topActionsIds = actions.filter(action => !action.parent).map(action => action.id) as string[]
|
||||
const topActionsIds = actions.filter((action) => !action.parent).map((action) => action.id) as string[]
|
||||
|
||||
// Filter actions that are not top-level AND have a valid parent
|
||||
return actions.filter(action => !(action.parent && topActionsIds.includes(action.parent)))
|
||||
return actions.filter((action) => !(action.parent && topActionsIds.includes(action.parent)))
|
||||
},
|
||||
|
||||
renderedNonDestructiveActions() {
|
||||
return this.enabledMenuActions.filter(action => !action.destructive)
|
||||
return this.enabledMenuActions.filter((action) => !action.destructive)
|
||||
},
|
||||
|
||||
renderedDestructiveActions() {
|
||||
return this.enabledMenuActions.filter(action => action.destructive)
|
||||
return this.enabledMenuActions.filter((action) => action.destructive)
|
||||
},
|
||||
|
||||
openedMenu: {
|
||||
get() {
|
||||
return this.opened
|
||||
},
|
||||
|
||||
set(value) {
|
||||
this.$emit('update:opened', value)
|
||||
},
|
||||
|
|
@ -295,7 +303,9 @@ export default defineComponent({
|
|||
// if an inline action is rendered in the menu for
|
||||
// lack of space we use the title first if defined
|
||||
const title = action.title([this.source], this.currentView)
|
||||
if (title) return title
|
||||
if (title) {
|
||||
return title
|
||||
}
|
||||
}
|
||||
return action.displayName([this.source], this.currentView)
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<td class="files-list__row-checkbox"
|
||||
<td
|
||||
class="files-list__row-checkbox"
|
||||
@keyup.esc.exact="resetSelection">
|
||||
<NcLoadingIcon v-if="isLoading" :name="loadingLabel" />
|
||||
<NcCheckboxRadioSwitch v-else
|
||||
<NcCheckboxRadioSwitch
|
||||
v-else
|
||||
:aria-label="ariaLabel"
|
||||
:checked="isSelected"
|
||||
data-cy-files-list-row-checkbox
|
||||
|
|
@ -23,14 +25,12 @@ import { FileType } from '@nextcloud/files'
|
|||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
|
||||
import logger from '../../logger.ts'
|
||||
import { useActiveStore } from '../../store/active.ts'
|
||||
import { useKeyboardStore } from '../../store/keyboard.ts'
|
||||
import { useSelectionStore } from '../../store/selection.ts'
|
||||
import logger from '../../logger.ts'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEntryCheckbox',
|
||||
|
|
@ -45,14 +45,17 @@ export default defineComponent({
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
nodes: {
|
||||
type: Array as PropType<Node[]>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
source: {
|
||||
type: Object as PropType<Node>,
|
||||
required: true,
|
||||
|
|
@ -80,20 +83,25 @@ export default defineComponent({
|
|||
selectedFiles() {
|
||||
return this.selectionStore.selected
|
||||
},
|
||||
|
||||
isSelected() {
|
||||
return this.selectedFiles.includes(this.source.source)
|
||||
},
|
||||
|
||||
index() {
|
||||
return this.nodes.findIndex((node: Node) => node.source === this.source.source)
|
||||
},
|
||||
|
||||
isFile() {
|
||||
return this.source.type === FileType.File
|
||||
},
|
||||
|
||||
ariaLabel() {
|
||||
return this.isFile
|
||||
? t('files', 'Toggle selection for file "{displayName}"', { displayName: this.source.basename })
|
||||
: t('files', 'Toggle selection for folder "{displayName}"', { displayName: this.source.basename })
|
||||
},
|
||||
|
||||
loadingLabel() {
|
||||
return this.isFile
|
||||
? t('files', 'File is loading')
|
||||
|
|
@ -132,13 +140,13 @@ export default defineComponent({
|
|||
|
||||
const lastSelection = this.selectionStore.lastSelection
|
||||
const filesToSelect = this.nodes
|
||||
.map(file => file.source)
|
||||
.map((file) => file.source)
|
||||
.slice(start, end + 1)
|
||||
.filter(Boolean) as FileSource[]
|
||||
|
||||
// If already selected, update the new selection _without_ the current file
|
||||
const selection = [...lastSelection, ...filesToSelect]
|
||||
.filter(source => !isAlreadySelected || source !== this.source.source)
|
||||
.filter((source) => !isAlreadySelected || source !== this.source.source)
|
||||
|
||||
logger.debug('Shift key pressed, selecting all files in between', { start, end, filesToSelect, isAlreadySelected })
|
||||
// Keep previous lastSelectedIndex to be use for further shift selections
|
||||
|
|
@ -148,7 +156,7 @@ export default defineComponent({
|
|||
|
||||
const selection = selected
|
||||
? [...this.selectedFiles, this.source.source]
|
||||
: this.selectedFiles.filter(source => source !== this.source.source)
|
||||
: this.selectedFiles.filter((source) => source !== this.source.source)
|
||||
|
||||
logger.debug('Updating selection', { selection })
|
||||
this.selectionStore.set(selection)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@
|
|||
-->
|
||||
<template>
|
||||
<!-- Rename input -->
|
||||
<form v-if="isRenaming"
|
||||
<form
|
||||
v-if="isRenaming"
|
||||
ref="renameForm"
|
||||
v-on-click-outside="onRename"
|
||||
:aria-label="t('files', 'Rename file')"
|
||||
class="files-list__row-rename"
|
||||
@submit.prevent.stop="onRename">
|
||||
<NcTextField ref="renameInput"
|
||||
<NcTextField
|
||||
ref="renameInput"
|
||||
:label="renameLabel"
|
||||
:autofocus="true"
|
||||
:minlength="1"
|
||||
|
|
@ -20,7 +22,8 @@
|
|||
@keyup.esc="stopRenaming" />
|
||||
</form>
|
||||
|
||||
<component :is="linkTo.is"
|
||||
<component
|
||||
:is="linkTo.is"
|
||||
v-else
|
||||
ref="basename"
|
||||
class="files-list__row-name-link"
|
||||
|
|
@ -43,16 +46,14 @@ import { showError, showSuccess } from '@nextcloud/dialogs'
|
|||
import { FileType, NodeStatus } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { defineComponent, inject } from 'vue'
|
||||
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
|
||||
import { getFilenameValidity } from '../../utils/filenameValidity.ts'
|
||||
import { useFileListWidth } from '../../composables/useFileListWidth.ts'
|
||||
import { useNavigation } from '../../composables/useNavigation.ts'
|
||||
import { useRenamingStore } from '../../store/renaming.ts'
|
||||
import { useRouteParameters } from '../../composables/useRouteParameters.ts'
|
||||
import { useUserConfigStore } from '../../store/userconfig.ts'
|
||||
import logger from '../../logger.ts'
|
||||
import { useRenamingStore } from '../../store/renaming.ts'
|
||||
import { useUserConfigStore } from '../../store/userconfig.ts'
|
||||
import { getFilenameValidity } from '../../utils/filenameValidity.ts'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEntryName',
|
||||
|
|
@ -69,6 +70,7 @@ export default defineComponent({
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* The extension of the filename
|
||||
*/
|
||||
|
|
@ -76,14 +78,17 @@ export default defineComponent({
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
nodes: {
|
||||
type: Array as PropType<Node[]>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
source: {
|
||||
type: Object as PropType<Node>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
gridMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
|
@ -115,13 +120,16 @@ export default defineComponent({
|
|||
isRenaming() {
|
||||
return this.renamingStore.renamingNode === this.source
|
||||
},
|
||||
|
||||
isRenamingSmallScreen() {
|
||||
return this.isRenaming && this.filesListWidth < 512
|
||||
},
|
||||
|
||||
newName: {
|
||||
get(): string {
|
||||
return this.renamingStore.newNodeName
|
||||
},
|
||||
|
||||
set(newName: string) {
|
||||
this.renamingStore.newNodeName = newName
|
||||
},
|
||||
|
|
@ -169,6 +177,7 @@ export default defineComponent({
|
|||
/**
|
||||
* If renaming starts, select the filename
|
||||
* in the input, without the extension.
|
||||
*
|
||||
* @param renaming
|
||||
*/
|
||||
isRenaming: {
|
||||
|
|
@ -183,7 +192,7 @@ export default defineComponent({
|
|||
newName() {
|
||||
// Check validity of the new name
|
||||
const newName = this.newName.trim?.() || ''
|
||||
const input = (this.$refs.renameInput as Vue|undefined)?.$el.querySelector('input')
|
||||
const input = (this.$refs.renameInput as Vue | undefined)?.$el.querySelector('input')
|
||||
if (!input) {
|
||||
return
|
||||
}
|
||||
|
|
@ -204,13 +213,13 @@ export default defineComponent({
|
|||
|
||||
methods: {
|
||||
checkIfNodeExists(name: string) {
|
||||
return this.nodes.find(node => node.basename === name && node !== this.source)
|
||||
return this.nodes.find((node) => node.basename === name && node !== this.source)
|
||||
},
|
||||
|
||||
startRenaming() {
|
||||
this.$nextTick(() => {
|
||||
// Using split to get the true string length
|
||||
const input = (this.$refs.renameInput as Vue|undefined)?.$el.querySelector('input')
|
||||
const input = (this.$refs.renameInput as Vue | undefined)?.$el.querySelector('input')
|
||||
if (!input) {
|
||||
logger.error('Could not find the rename input')
|
||||
return
|
||||
|
|
@ -251,9 +260,7 @@ export default defineComponent({
|
|||
try {
|
||||
const status = await this.renamingStore.rename()
|
||||
if (status) {
|
||||
showSuccess(
|
||||
t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName: this.source.basename }),
|
||||
)
|
||||
showSuccess(t('files', 'Renamed "{oldName}" to "{newName}"', { oldName, newName: this.source.basename }))
|
||||
this.$nextTick(() => {
|
||||
const nameContainer = this.$refs.basename as HTMLElement | undefined
|
||||
nameContainer?.focus()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
<FolderOpenIcon v-if="dragover" v-once />
|
||||
<template v-else>
|
||||
<FolderIcon v-once />
|
||||
<OverlayIcon :is="folderOverlay"
|
||||
<OverlayIcon
|
||||
:is="folderOverlay"
|
||||
v-if="folderOverlay"
|
||||
class="files-list__row-icon-overlay" />
|
||||
</template>
|
||||
|
|
@ -16,16 +17,18 @@
|
|||
|
||||
<!-- Decorative images, should not be aria documented -->
|
||||
<span v-else-if="previewUrl" class="files-list__row-icon-preview-container">
|
||||
<canvas v-if="hasBlurhash && (backgroundFailed === true || !backgroundLoaded)"
|
||||
<canvas
|
||||
v-if="hasBlurhash && (backgroundFailed === true || !backgroundLoaded)"
|
||||
ref="canvas"
|
||||
class="files-list__row-icon-blurhash"
|
||||
aria-hidden="true" />
|
||||
<img v-if="backgroundFailed !== true"
|
||||
<img
|
||||
v-if="backgroundFailed !== true"
|
||||
:key="source.fileid"
|
||||
ref="previewImg"
|
||||
alt=""
|
||||
class="files-list__row-icon-preview"
|
||||
:class="{'files-list__row-icon-preview--loaded': backgroundFailed === false}"
|
||||
:class="{ 'files-list__row-icon-preview--loaded': backgroundFailed === false }"
|
||||
loading="lazy"
|
||||
:src="previewUrl"
|
||||
@error="onBackgroundError"
|
||||
|
|
@ -39,24 +42,25 @@
|
|||
<FavoriteIcon v-once />
|
||||
</span>
|
||||
|
||||
<OverlayIcon :is="fileOverlay"
|
||||
<OverlayIcon
|
||||
:is="fileOverlay"
|
||||
v-if="fileOverlay"
|
||||
class="files-list__row-icon-overlay files-list__row-icon-overlay--file" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { PropType } from 'vue'
|
||||
import type { UserConfig } from '../../types.ts'
|
||||
|
||||
import { Node, FileType } from '@nextcloud/files'
|
||||
import { FileType } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { ShareType } from '@nextcloud/sharing'
|
||||
import { getSharingToken, isPublicShare } from '@nextcloud/sharing/public'
|
||||
import { decode } from 'blurhash'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import AccountGroupIcon from 'vue-material-design-icons/AccountGroup.vue'
|
||||
import AccountPlusIcon from 'vue-material-design-icons/AccountPlus.vue'
|
||||
import FileIcon from 'vue-material-design-icons/File.vue'
|
||||
|
|
@ -65,15 +69,13 @@ import FolderOpenIcon from 'vue-material-design-icons/FolderOpen.vue'
|
|||
import KeyIcon from 'vue-material-design-icons/Key.vue'
|
||||
import LinkIcon from 'vue-material-design-icons/Link.vue'
|
||||
import NetworkIcon from 'vue-material-design-icons/NetworkOutline.vue'
|
||||
import TagIcon from 'vue-material-design-icons/Tag.vue'
|
||||
import PlayCircleIcon from 'vue-material-design-icons/PlayCircle.vue'
|
||||
|
||||
import TagIcon from 'vue-material-design-icons/Tag.vue'
|
||||
import CollectivesIcon from './CollectivesIcon.vue'
|
||||
import FavoriteIcon from './FavoriteIcon.vue'
|
||||
|
||||
import { isLivePhoto } from '../../services/LivePhotos'
|
||||
import { useUserConfigStore } from '../../store/userconfig.ts'
|
||||
import logger from '../../logger.ts'
|
||||
import { isLivePhoto } from '../../services/LivePhotos.ts'
|
||||
import { useUserConfigStore } from '../../store/userconfig.ts'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEntryPreview',
|
||||
|
|
@ -97,10 +99,12 @@ export default defineComponent({
|
|||
type: Object as PropType<Node>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
dragover: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
gridMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
|
@ -135,6 +139,7 @@ export default defineComponent({
|
|||
userConfig(): UserConfig {
|
||||
return this.userConfigStore.userConfig
|
||||
},
|
||||
|
||||
cropPreviews(): boolean {
|
||||
return this.userConfig.crop_image_previews === true
|
||||
},
|
||||
|
|
@ -163,12 +168,12 @@ export default defineComponent({
|
|||
const previewUrl = this.source.attributes.previewUrl
|
||||
|| (this.isPublic
|
||||
? generateUrl('/apps/files_sharing/publicpreview/{token}?file={file}', {
|
||||
token: this.publicSharingToken,
|
||||
file: this.source.path,
|
||||
})
|
||||
token: this.publicSharingToken,
|
||||
file: this.source.path,
|
||||
})
|
||||
: generateUrl('/core/preview?fileId={fileid}', {
|
||||
fileid: String(this.source.fileid),
|
||||
})
|
||||
fileid: String(this.source.fileid),
|
||||
})
|
||||
)
|
||||
const url = new URL(window.location.origin + previewUrl)
|
||||
|
||||
|
|
@ -184,7 +189,7 @@ export default defineComponent({
|
|||
// Handle cropping
|
||||
url.searchParams.set('a', this.cropPreviews === true ? '0' : '1')
|
||||
return url.href
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
|
@ -214,7 +219,7 @@ export default defineComponent({
|
|||
|
||||
// Link and mail shared folders
|
||||
const shareTypes = Object.values(this.source?.attributes?.['share-types'] || {}).flat() as number[]
|
||||
if (shareTypes.some(type => type === ShareType.Link || type === ShareType.Email)) {
|
||||
if (shareTypes.some((type) => type === ShareType.Link || type === ShareType.Email)) {
|
||||
return LinkIcon
|
||||
}
|
||||
|
||||
|
|
@ -224,15 +229,15 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
switch (this.source?.attributes?.['mount-type']) {
|
||||
case 'external':
|
||||
case 'external-session':
|
||||
return NetworkIcon
|
||||
case 'group':
|
||||
return AccountGroupIcon
|
||||
case 'collective':
|
||||
return CollectivesIcon
|
||||
case 'shared':
|
||||
return AccountPlusIcon
|
||||
case 'external':
|
||||
case 'external-session':
|
||||
return NetworkIcon
|
||||
case 'group':
|
||||
return AccountGroupIcon
|
||||
case 'collective':
|
||||
return CollectivesIcon
|
||||
case 'shared':
|
||||
return AccountPlusIcon
|
||||
}
|
||||
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<tr :class="{'files-list__row--active': isActive, 'files-list__row--dragover': dragover, 'files-list__row--loading': isLoading}"
|
||||
<tr
|
||||
:class="{ 'files-list__row--active': isActive, 'files-list__row--dragover': dragover, 'files-list__row--loading': isLoading }"
|
||||
data-cy-files-list-row
|
||||
:data-cy-files-list-row-fileid="fileid"
|
||||
:data-cy-files-list-row-name="source.basename"
|
||||
|
|
@ -20,7 +21,8 @@
|
|||
<span v-if="isFailedSource" class="files-list__row--failed" />
|
||||
|
||||
<!-- Checkbox -->
|
||||
<FileEntryCheckbox :fileid="fileid"
|
||||
<FileEntryCheckbox
|
||||
:fileid="fileid"
|
||||
:is-loading="isLoading"
|
||||
:nodes="nodes"
|
||||
:source="source" />
|
||||
|
|
@ -28,14 +30,16 @@
|
|||
<!-- Link to file -->
|
||||
<td class="files-list__row-name" data-cy-files-list-row-name>
|
||||
<!-- Icon or preview -->
|
||||
<FileEntryPreview ref="preview"
|
||||
<FileEntryPreview
|
||||
ref="preview"
|
||||
:dragover="dragover"
|
||||
:grid-mode="true"
|
||||
:source="source"
|
||||
@auxclick.native="execDefaultAction"
|
||||
@click.native="execDefaultAction" />
|
||||
|
||||
<FileEntryName ref="name"
|
||||
<FileEntryName
|
||||
ref="name"
|
||||
:basename="basename"
|
||||
:extension="extension"
|
||||
:grid-mode="true"
|
||||
|
|
@ -46,18 +50,21 @@
|
|||
</td>
|
||||
|
||||
<!-- Mtime -->
|
||||
<td v-if="!compact && isMtimeAvailable"
|
||||
<td
|
||||
v-if="!compact && isMtimeAvailable"
|
||||
:style="mtimeOpacity"
|
||||
class="files-list__row-mtime"
|
||||
data-cy-files-list-row-mtime
|
||||
@click="openDetailsIfAvailable">
|
||||
<NcDateTime v-if="mtime"
|
||||
<NcDateTime
|
||||
v-if="mtime"
|
||||
ignore-seconds
|
||||
:timestamp="mtime" />
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<FileEntryActions ref="actions"
|
||||
<FileEntryActions
|
||||
ref="actions"
|
||||
:class="`files-list__row-actions-${uniqueId}`"
|
||||
:grid-mode="true"
|
||||
:opened.sync="openedMenu"
|
||||
|
|
@ -67,9 +74,11 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
|
||||
|
||||
import FileEntryActions from './FileEntry/FileEntryActions.vue'
|
||||
import FileEntryCheckbox from './FileEntry/FileEntryCheckbox.vue'
|
||||
import FileEntryName from './FileEntry/FileEntryName.vue'
|
||||
import FileEntryPreview from './FileEntry/FileEntryPreview.vue'
|
||||
import { useNavigation } from '../composables/useNavigation.ts'
|
||||
import { useRouteParameters } from '../composables/useRouteParameters.ts'
|
||||
import { useActionsMenuStore } from '../store/actionsmenu.ts'
|
||||
|
|
@ -78,10 +87,6 @@ import { useFilesStore } from '../store/files.ts'
|
|||
import { useRenamingStore } from '../store/renaming.ts'
|
||||
import { useSelectionStore } from '../store/selection.ts'
|
||||
import FileEntryMixin from './FileEntryMixin.ts'
|
||||
import FileEntryActions from './FileEntry/FileEntryActions.vue'
|
||||
import FileEntryCheckbox from './FileEntry/FileEntryCheckbox.vue'
|
||||
import FileEntryName from './FileEntry/FileEntryName.vue'
|
||||
import FileEntryPreview from './FileEntry/FileEntryPreview.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileEntryGrid',
|
||||
|
|
|
|||
|
|
@ -6,21 +6,20 @@
|
|||
import type { PropType } from 'vue'
|
||||
import type { FileSource } from '../types.ts'
|
||||
|
||||
import { extname } from 'path'
|
||||
import { FileType, Permission, Folder, File as NcFile, NodeStatus, Node, getFileActions } from '@nextcloud/files'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { FileType, Folder, getFileActions, File as NcFile, Node, NodeStatus, Permission } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { vOnClickOutside } from '@vueuse/components'
|
||||
import { extname } from 'path'
|
||||
import Vue, { computed, defineComponent } from 'vue'
|
||||
|
||||
import { action as sidebarAction } from '../actions/sidebarAction.ts'
|
||||
import logger from '../logger.ts'
|
||||
import { dataTransferToFileTree, onDropExternalFiles, onDropInternalFiles } from '../services/DropService.ts'
|
||||
import { getDragAndDropPreview } from '../utils/dragUtils.ts'
|
||||
import { hashCode } from '../utils/hashUtils.ts'
|
||||
import { isDownloadable } from '../utils/permissions.ts'
|
||||
import logger from '../logger.ts'
|
||||
|
||||
Vue.directive('onClickOutside', vOnClickOutside)
|
||||
|
||||
|
|
@ -149,7 +148,7 @@ export default defineComponent({
|
|||
|
||||
// If we're dragging a selection, we need to check all files
|
||||
if (this.selectedFiles.length > 0) {
|
||||
const nodes = this.selectedFiles.map(source => this.filesStore.getNode(source)) as Node[]
|
||||
const nodes = this.selectedFiles.map((source) => this.filesStore.getNode(source)) as Node[]
|
||||
return nodes.every(canDrag)
|
||||
}
|
||||
return canDrag(this.source)
|
||||
|
|
@ -236,7 +235,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
return actions
|
||||
.filter(action => {
|
||||
.filter((action) => {
|
||||
if (!action.enabled) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -262,6 +261,7 @@ export default defineComponent({
|
|||
/**
|
||||
* When the source changes, reset the preview
|
||||
* and fetch the new one.
|
||||
*
|
||||
* @param newSource The new value of the source prop
|
||||
* @param oldSource The previous value
|
||||
*/
|
||||
|
|
@ -439,7 +439,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
const nodes = this.draggingStore.dragging
|
||||
.map(source => this.filesStore.getNode(source)) as Node[]
|
||||
.map((source) => this.filesStore.getNode(source)) as Node[]
|
||||
|
||||
const image = await getDragAndDropPreview(nodes)
|
||||
event.dataTransfer?.setDragImage(image, -10, -10)
|
||||
|
|
@ -493,12 +493,12 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// Else we're moving/copying files
|
||||
const nodes = selection.map(source => this.filesStore.getNode(source)) as Node[]
|
||||
const nodes = selection.map((source) => this.filesStore.getNode(source)) as Node[]
|
||||
await onDropInternalFiles(nodes, folder, contents.contents, isCopy)
|
||||
|
||||
// Reset selection after we dropped the files
|
||||
// if the dropped files are within the selection
|
||||
if (selection.some(source => this.selectedFiles.includes(source))) {
|
||||
if (selection.some((source) => this.selectedFiles.includes(source))) {
|
||||
logger.debug('Dropped selection, resetting select store...')
|
||||
this.selectionStore.reset()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<NcActions force-menu
|
||||
:type="isActive ? 'secondary' : 'tertiary'"
|
||||
<NcActions
|
||||
force-menu
|
||||
:variant="isActive ? 'secondary' : 'tertiary'"
|
||||
:menu-name="filterName">
|
||||
<template #icon>
|
||||
<slot name="icon" />
|
||||
|
|
@ -13,7 +14,8 @@
|
|||
|
||||
<template v-if="isActive">
|
||||
<NcActionSeparator />
|
||||
<NcActionButton class="files-list-filter__clear-button"
|
||||
<NcActionButton
|
||||
class="files-list-filter__clear-button"
|
||||
close-after-click
|
||||
@click="$emit('reset-filter')">
|
||||
{{ t('files', 'Clear filter') }}
|
||||
|
|
@ -24,8 +26,8 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import NcActions from '@nextcloud/vue/components/NcActions'
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import NcActions from '@nextcloud/vue/components/NcActions'
|
||||
import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
|
||||
|
||||
defineProps<{
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<FileListFilter :is-active="isActive"
|
||||
<FileListFilter
|
||||
:is-active="isActive"
|
||||
:filter-name="t('files', 'Modified')"
|
||||
@reset-filter="resetFilter">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiCalendarRangeOutline" />
|
||||
</template>
|
||||
<NcActionButton v-for="preset of timePresets"
|
||||
<NcActionButton
|
||||
v-for="preset of timePresets"
|
||||
:key="preset.id"
|
||||
type="radio"
|
||||
close-after-click
|
||||
|
|
@ -28,7 +30,6 @@ import type { ITimePreset } from '../../filters/ModifiedFilter.ts'
|
|||
import { mdiCalendarRangeOutline } from '@mdi/js'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import FileListFilter from './FileListFilter.vue'
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<FileListFilter class="file-list-filter-type"
|
||||
<FileListFilter
|
||||
class="file-list-filter-type"
|
||||
:is-active="isActive"
|
||||
:filter-name="t('files', 'Type')"
|
||||
@reset-filter="resetFilter">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiFileOutline" />
|
||||
</template>
|
||||
<NcActionButton v-for="fileType of typePresets"
|
||||
<NcActionButton
|
||||
v-for="fileType of typePresets"
|
||||
:key="fileType.id"
|
||||
type="checkbox"
|
||||
:model-value="selectedOptions.includes(fileType)"
|
||||
|
|
@ -30,7 +32,6 @@ import type { ITypePreset } from '../../filters/TypeFilter.ts'
|
|||
import { mdiFileOutline } from '@mdi/js'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import FileListFilter from './FileListFilter.vue'
|
||||
|
|
@ -49,6 +50,7 @@ export default defineComponent({
|
|||
type: Array as PropType<ITypePreset[]>,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
typePresets: {
|
||||
type: Array as PropType<ITypePreset[]>,
|
||||
required: true,
|
||||
|
|
@ -79,6 +81,7 @@ export default defineComponent({
|
|||
presets() {
|
||||
this.selectedOptions = this.presets ?? []
|
||||
},
|
||||
|
||||
selectedOptions(newValue, oldValue) {
|
||||
if (this.selectedOptions.length === 0) {
|
||||
if (oldValue.length !== 0) {
|
||||
|
|
@ -101,6 +104,7 @@ export default defineComponent({
|
|||
|
||||
/**
|
||||
* Toggle option from selected option
|
||||
*
|
||||
* @param option The option to toggle
|
||||
*/
|
||||
toggleOption(option: ITypePreset) {
|
||||
|
|
|
|||
|
|
@ -5,19 +5,22 @@
|
|||
<template>
|
||||
<div class="file-list-filters">
|
||||
<div class="file-list-filters__filter" data-cy-files-filters>
|
||||
<span v-for="filter of visualFilters"
|
||||
<span
|
||||
v-for="filter of visualFilters"
|
||||
:key="filter.id"
|
||||
ref="filterElements" />
|
||||
</div>
|
||||
<ul v-if="activeChips.length > 0" class="file-list-filters__active" :aria-label="t('files', 'Active filters')">
|
||||
<li v-for="(chip, index) of activeChips" :key="index">
|
||||
<NcChip :aria-label-close="t('files', 'Remove filter')"
|
||||
<NcChip
|
||||
:aria-label-close="t('files', 'Remove filter')"
|
||||
:icon-svg="chip.icon"
|
||||
:text="chip.text"
|
||||
@close="chip.onclick">
|
||||
<template v-if="chip.user" #icon>
|
||||
<NcAvatar disable-menu
|
||||
:show-user-status="false"
|
||||
<NcAvatar
|
||||
disable-menu
|
||||
hide-status
|
||||
:size="24"
|
||||
:user="chip.user" />
|
||||
</template>
|
||||
|
|
@ -30,10 +33,9 @@
|
|||
<script setup lang="ts">
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
import { useFiltersStore } from '../store/filters.ts'
|
||||
|
||||
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
|
||||
import NcChip from '@nextcloud/vue/components/NcChip'
|
||||
import { useFiltersStore } from '../store/filters.ts'
|
||||
|
||||
const filterStore = useFiltersStore()
|
||||
const visualFilters = computed(() => filterStore.filtersWithUI)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue