mirror of
https://github.com/nextcloud/server.git
synced 2026-05-28 04:32:30 -04:00
Merge pull request #57277 from nextcloud/refactor/files-sidebar-nodeapi
refactor!: migrate files sidebar to Node API
This commit is contained in:
commit
c50c5a9e6b
565 changed files with 3944 additions and 6112 deletions
|
|
@ -180,14 +180,12 @@ describe('Inline unread comments action enabled tests', () => {
|
|||
describe('Inline unread comments action execute tests', () => {
|
||||
test('Action opens sidebar', async () => {
|
||||
const openMock = vi.fn()
|
||||
const setActiveTabMock = vi.fn()
|
||||
window.OCA = {
|
||||
Files: {
|
||||
// @ts-expect-error Mocking for testing
|
||||
Sidebar: {
|
||||
_sidebar: () => ({
|
||||
open: openMock,
|
||||
setActiveTab: setActiveTabMock,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -211,22 +209,19 @@ describe('Inline unread comments action execute tests', () => {
|
|||
})
|
||||
|
||||
expect(result).toBe(null)
|
||||
expect(setActiveTabMock).toBeCalledWith('comments')
|
||||
expect(openMock).toBeCalledWith('/foobar.txt')
|
||||
expect(openMock).toBeCalledWith(file, 'comments')
|
||||
})
|
||||
|
||||
test('Action handles sidebar open failure', async () => {
|
||||
const openMock = vi.fn(() => {
|
||||
throw new Error('Mock error')
|
||||
})
|
||||
const setActiveTabMock = vi.fn()
|
||||
window.OCA = {
|
||||
Files: {
|
||||
// @ts-expect-error Mocking for testing
|
||||
Sidebar: {
|
||||
_sidebar: () => ({
|
||||
open: openMock,
|
||||
setActiveTab: setActiveTabMock,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
vi.spyOn(logger, 'error').mockImplementation(() => vi.fn())
|
||||
|
|
@ -251,8 +246,7 @@ describe('Inline unread comments action execute tests', () => {
|
|||
})
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(setActiveTabMock).toBeCalledWith('comments')
|
||||
expect(openMock).toBeCalledWith('/foobar.txt')
|
||||
expect(openMock).toBeCalledWith(file, 'comments')
|
||||
expect(logger.error).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import CommentProcessingSvg from '@mdi/svg/svg/comment-processing.svg?raw'
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { FileAction, getSidebar } from '@nextcloud/files'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import logger from '../logger.js'
|
||||
|
||||
|
|
@ -34,8 +35,8 @@ export const action = new FileAction({
|
|||
}
|
||||
|
||||
try {
|
||||
window.OCA.Files.Sidebar.setActiveTab('comments')
|
||||
await window.OCA.Files.Sidebar.open(nodes[0].path)
|
||||
const sidebar = getSidebar()
|
||||
sidebar.open(nodes[0], 'comments')
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error('Error while opening sidebar', { error })
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import MessageReplyText from '@mdi/svg/svg/message-reply-text.svg?raw'
|
||||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { registerCommentsPlugins } from './comments-activity-tab.ts'
|
||||
|
||||
// @ts-expect-error __webpack_nonce__ is injected by webpack
|
||||
__webpack_nonce__ = getCSPNonce()
|
||||
|
||||
if (loadState('comments', 'activityEnabled', false) && OCA?.Activity?.registerSidebarAction !== undefined) {
|
||||
// Do not mount own tab but mount into activity
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
registerCommentsPlugins()
|
||||
})
|
||||
} else {
|
||||
// Init Comments tab component
|
||||
let TabInstance = null
|
||||
const commentTab = new OCA.Files.Sidebar.Tab({
|
||||
id: 'comments',
|
||||
name: t('comments', 'Comments'),
|
||||
iconSvg: MessageReplyText,
|
||||
|
||||
async mount(el, fileInfo, context) {
|
||||
if (TabInstance) {
|
||||
TabInstance.$destroy()
|
||||
}
|
||||
TabInstance = new OCA.Comments.View('files', {
|
||||
// Better integration with vue parent component
|
||||
parent: context,
|
||||
propsData: {
|
||||
resourceId: fileInfo.id,
|
||||
},
|
||||
})
|
||||
// Only mount after we have all the info we need
|
||||
await TabInstance.update(fileInfo.id)
|
||||
TabInstance.$mount(el)
|
||||
},
|
||||
update(fileInfo) {
|
||||
TabInstance.update(fileInfo.id)
|
||||
},
|
||||
destroy() {
|
||||
TabInstance.$destroy()
|
||||
TabInstance = null
|
||||
},
|
||||
scrollBottomReached() {
|
||||
TabInstance.onScrollBottomReached()
|
||||
},
|
||||
})
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
if (OCA.Files && OCA.Files.Sidebar) {
|
||||
OCA.Files.Sidebar.registerTab(commentTab)
|
||||
}
|
||||
})
|
||||
}
|
||||
57
apps/comments/src/files-sidebar.ts
Normal file
57
apps/comments/src/files-sidebar.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import MessageReplyText from '@mdi/svg/svg/message-reply-text.svg?raw'
|
||||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
import { registerSidebarTab } from '@nextcloud/files'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import wrap from '@vue/web-component-wrapper'
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia'
|
||||
import Vue from 'vue'
|
||||
import FilesSidebarTab from './views/FilesSidebarTab.vue'
|
||||
import { registerCommentsPlugins } from './comments-activity-tab.ts'
|
||||
|
||||
__webpack_nonce__ = getCSPNonce()
|
||||
|
||||
const tagName = 'comments_files-sidebar-tab'
|
||||
|
||||
if (loadState('comments', 'activityEnabled', false) && OCA?.Activity?.registerSidebarAction !== undefined) {
|
||||
// Do not mount own tab but mount into activity
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
registerCommentsPlugins()
|
||||
})
|
||||
} else {
|
||||
registerSidebarTab({
|
||||
id: 'comments',
|
||||
displayName: t('comments', 'Comments'),
|
||||
iconSvgInline: MessageReplyText,
|
||||
order: 50,
|
||||
tagName,
|
||||
enabled() {
|
||||
if (!window.customElements.get(tagName)) {
|
||||
setupSidebarTab()
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the sidebar tab as a web component
|
||||
*/
|
||||
function setupSidebarTab() {
|
||||
Vue.use(PiniaVuePlugin)
|
||||
Vue.mixin({ pinia: createPinia() })
|
||||
const webComponent = wrap(Vue, FilesSidebarTab)
|
||||
// In Vue 2, wrap doesn't support disabling shadow. Disable with a hack
|
||||
Object.defineProperty(webComponent.prototype, 'attachShadow', {
|
||||
value() { return this },
|
||||
})
|
||||
Object.defineProperty(webComponent.prototype, 'shadowRoot', {
|
||||
get() { return this },
|
||||
})
|
||||
window.customElements.define(tagName, webComponent)
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
|
|
@ -32,7 +33,7 @@ export default defineComponent({
|
|||
},
|
||||
methods: {
|
||||
/**
|
||||
* Autocomplete @mentions
|
||||
* Autocomplete `@mentions`
|
||||
*
|
||||
* @param search the query
|
||||
* @param callback the callback to process the results with
|
||||
|
|
|
|||
40
apps/comments/src/views/FilesSidebarTab.vue
Normal file
40
apps/comments/src/views/FilesSidebarTab.vue
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IFolder, INode, IView } from '@nextcloud/files'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import Comments from './Comments.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
node?: INode
|
||||
// eslint-disable-next-line vue/no-unused-properties -- Required on the web component interface
|
||||
folder?: IFolder
|
||||
// eslint-disable-next-line vue/no-unused-properties -- Required on the web component interface
|
||||
view?: IView
|
||||
}>()
|
||||
|
||||
defineExpose({ setActive })
|
||||
|
||||
const resourceId = computed(() => props.node?.fileid)
|
||||
|
||||
/**
|
||||
* Set this tab as active
|
||||
*
|
||||
* @param active - The active state
|
||||
*/
|
||||
function setActive(active: boolean) {
|
||||
return active
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Comments
|
||||
v-if="resourceId !== undefined"
|
||||
:key="resourceId"
|
||||
:resource-id="resourceId"
|
||||
resource-type="files" />
|
||||
</template>
|
||||
|
|
@ -6,35 +6,19 @@
|
|||
<NcContent app-name="files">
|
||||
<FilesNavigation v-if="!isPublic" />
|
||||
<FilesList :is-public="isPublic" />
|
||||
<FilesSidebar v-if="!isPublic" />
|
||||
</NcContent>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
import { defineComponent } from 'vue'
|
||||
import NcContent from '@nextcloud/vue/components/NcContent'
|
||||
import FilesList from './views/FilesList.vue'
|
||||
import FilesNavigation from './views/FilesNavigation.vue'
|
||||
import FilesSidebar from './views/FilesSidebar.vue'
|
||||
import { useHotKeys } from './composables/useHotKeys.ts'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FilesApp',
|
||||
useHotKeys()
|
||||
|
||||
components: {
|
||||
NcContent,
|
||||
FilesList,
|
||||
FilesNavigation,
|
||||
},
|
||||
|
||||
setup() {
|
||||
// Register global hotkeys
|
||||
useHotKeys()
|
||||
|
||||
const isPublic = isPublicShare()
|
||||
|
||||
return {
|
||||
isPublic,
|
||||
}
|
||||
},
|
||||
})
|
||||
const isPublic = isPublicShare()
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ describe('Favorite action execute tests', () => {
|
|||
|
||||
// Check node change propagation
|
||||
expect(file.attributes.favorite).toBe(1)
|
||||
expect(eventBus.emit).toBeCalledTimes(1)
|
||||
expect(eventBus.emit).toHaveBeenCalled()
|
||||
expect(eventBus.emit).toBeCalledWith('files:favorites:added', file)
|
||||
})
|
||||
|
||||
|
|
@ -251,7 +251,7 @@ describe('Favorite action execute tests', () => {
|
|||
|
||||
// Check node change propagation
|
||||
expect(file.attributes.favorite).toBe(0)
|
||||
expect(eventBus.emit).toBeCalledTimes(1)
|
||||
expect(eventBus.emit).toHaveBeenCalled()
|
||||
expect(eventBus.emit).toBeCalledWith('files:favorites:removed', file)
|
||||
})
|
||||
|
||||
|
|
@ -285,9 +285,9 @@ describe('Favorite action execute tests', () => {
|
|||
|
||||
// Check node change propagation
|
||||
expect(file.attributes.favorite).toBe(0)
|
||||
expect(eventBus.emit).toBeCalledTimes(2)
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file)
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:favorites:removed', file)
|
||||
expect(eventBus.emit).toHaveBeenCalled()
|
||||
expect(eventBus.emit).toHaveBeenCalledWith('files:node:deleted', file)
|
||||
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:removed', file)
|
||||
})
|
||||
|
||||
test('Favorite does NOT triggers node removal if favorite view but NOT root dir', async () => {
|
||||
|
|
@ -320,7 +320,7 @@ describe('Favorite action execute tests', () => {
|
|||
|
||||
// Check node change propagation
|
||||
expect(file.attributes.favorite).toBe(0)
|
||||
expect(eventBus.emit).toBeCalledTimes(1)
|
||||
expect(eventBus.emit).toHaveBeenCalled()
|
||||
expect(eventBus.emit).toBeCalledWith('files:favorites:removed', file)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
/**
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import type { Node, View } from '@nextcloud/files'
|
||||
|
||||
import type { INode, IView } from '@nextcloud/files'
|
||||
|
||||
import StarOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
|
||||
import StarSvg from '@mdi/svg/svg/star.svg?raw'
|
||||
|
|
@ -26,17 +27,18 @@ const queue = new PQueue({ concurrency: 5 })
|
|||
*
|
||||
* @param nodes - The nodes to check
|
||||
*/
|
||||
function shouldFavorite(nodes: Node[]): boolean {
|
||||
function shouldFavorite(nodes: INode[]): boolean {
|
||||
return nodes.some((node) => node.attributes.favorite !== 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Favorite or unfavorite a node
|
||||
*
|
||||
* @param node
|
||||
* @param view
|
||||
* @param willFavorite
|
||||
* @param node - The node to favorite/unfavorite
|
||||
* @param view - The current view
|
||||
* @param willFavorite - Whether to favorite or unfavorite the node
|
||||
*/
|
||||
export async function favoriteNode(node: Node, view: View, willFavorite: boolean): Promise<boolean> {
|
||||
export async function favoriteNode(node: INode, view: IView, willFavorite: boolean): Promise<boolean> {
|
||||
try {
|
||||
// TODO: migrate to webdav tags plugin
|
||||
const url = generateUrl('/apps/files/api/v1/files') + encodePath(node.path)
|
||||
|
|
@ -55,6 +57,7 @@ export async function favoriteNode(node: Node, view: View, willFavorite: boolean
|
|||
|
||||
// Update the node webdav attribute
|
||||
Vue.set(node.attributes, 'favorite', willFavorite ? 1 : 0)
|
||||
emit('files:node:updated', node)
|
||||
|
||||
// Dispatch event to whoever is interested
|
||||
if (willFavorite) {
|
||||
|
|
|
|||
|
|
@ -3,17 +3,32 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { IView } from '@nextcloud/files'
|
||||
|
||||
import { File, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import logger from '../logger.ts'
|
||||
import { action } from './sidebarAction.ts'
|
||||
|
||||
const sidebar = vi.hoisted(() => ({
|
||||
available: true,
|
||||
open: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@nextcloud/files', async (original) => ({
|
||||
...(await original()),
|
||||
getSidebar: () => sidebar,
|
||||
}))
|
||||
|
||||
const view = {
|
||||
id: 'files',
|
||||
name: 'Files',
|
||||
} as View
|
||||
} as IView
|
||||
|
||||
beforeEach(() => {
|
||||
sidebar.available = true
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Open sidebar action conditions tests', () => {
|
||||
test('Default values', () => {
|
||||
|
|
@ -38,9 +53,6 @@ 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({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
|
||||
|
|
@ -60,9 +72,6 @@ describe('Open sidebar action enabled tests', () => {
|
|||
})
|
||||
|
||||
test('Disabled without permissions', () => {
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: {} } }
|
||||
|
||||
const file = new File({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
|
||||
|
|
@ -82,9 +91,6 @@ describe('Open sidebar action enabled tests', () => {
|
|||
})
|
||||
|
||||
test('Disabled if more than one node', () => {
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: {} } }
|
||||
|
||||
const file1 = new File({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
|
||||
|
|
@ -110,8 +116,7 @@ describe('Open sidebar action enabled tests', () => {
|
|||
})
|
||||
|
||||
test('Disabled if no Sidebar', () => {
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = {}
|
||||
sidebar.available = false
|
||||
|
||||
const file = new File({
|
||||
id: 1,
|
||||
|
|
@ -131,9 +136,6 @@ 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({
|
||||
id: 1,
|
||||
source: 'https://domain.com/documents/admin/foobar.txt',
|
||||
|
|
@ -154,14 +156,7 @@ describe('Open sidebar action enabled tests', () => {
|
|||
|
||||
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()
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
sidebar.available = true
|
||||
const file = new File({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
|
||||
|
|
@ -177,33 +172,17 @@ describe('Open sidebar action exec tests', () => {
|
|||
root: '/files/admin',
|
||||
})
|
||||
|
||||
const exec = await action.exec({
|
||||
// Silent action
|
||||
expect(await action.exec({
|
||||
nodes: [file],
|
||||
view,
|
||||
folder,
|
||||
contents: [],
|
||||
})
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(openMock).toBeCalledWith('/foobar.txt')
|
||||
expect(defaultTabMock).toBeCalledWith('sharing')
|
||||
expect(goToRouteMock).toBeCalledWith(
|
||||
null,
|
||||
{ view: view.id, fileid: '1' },
|
||||
{ dir: '/', opendetails: 'true' },
|
||||
true,
|
||||
)
|
||||
})).toBeNull()
|
||||
expect(sidebar.open).toBeCalledWith(file, 'sharing')
|
||||
})
|
||||
|
||||
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()
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar',
|
||||
|
|
@ -227,23 +206,13 @@ describe('Open sidebar action exec tests', () => {
|
|||
})
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(openMock).toBeCalledWith('/foobar')
|
||||
expect(defaultTabMock).toBeCalledWith('sharing')
|
||||
expect(goToRouteMock).toBeCalledWith(
|
||||
null,
|
||||
{ view: view.id, fileid: '1' },
|
||||
{ dir: '/', opendetails: 'true' },
|
||||
true,
|
||||
)
|
||||
expect(sidebar.open).toBeCalledWith(file, 'sharing')
|
||||
})
|
||||
|
||||
test('Open sidebar fails', async () => {
|
||||
const openMock = vi.fn(() => {
|
||||
throw new Error('Mock error')
|
||||
sidebar.open.mockImplementationOnce(() => {
|
||||
throw new Error('Sidebar 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())
|
||||
|
||||
const file = new File({
|
||||
|
|
@ -261,7 +230,7 @@ describe('Open sidebar action exec tests', () => {
|
|||
contents: [],
|
||||
})
|
||||
expect(exec).toBe(false)
|
||||
expect(openMock).toBeCalledTimes(1)
|
||||
expect(logger.error).toBeCalledTimes(1)
|
||||
expect(sidebar.open).toHaveBeenCalledOnce()
|
||||
expect(logger.error).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import InformationSvg from '@mdi/svg/svg/information-outline.svg?raw'
|
||||
import { FileAction, Permission } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { FileAction, getSidebar, Permission } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
import logger from '../logger.ts'
|
||||
|
||||
|
|
@ -17,49 +18,34 @@ export const action = new FileAction({
|
|||
|
||||
// Sidebar currently supports user folder only, /files/USER
|
||||
enabled: ({ nodes }) => {
|
||||
const node = nodes[0]
|
||||
if (nodes.length !== 1 || !node) {
|
||||
return false
|
||||
}
|
||||
|
||||
const sidebar = getSidebar()
|
||||
if (!sidebar.available) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isPublicShare()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only works on single node
|
||||
if (nodes.length !== 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!nodes[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only work if the sidebar is available
|
||||
if (!window?.OCA?.Files?.Sidebar) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (nodes[0].root?.startsWith('/files/') && nodes[0].permissions !== Permission.NONE) ?? false
|
||||
return node.root.startsWith('/files/') && node.permissions !== Permission.NONE
|
||||
},
|
||||
|
||||
async exec({ nodes, view, folder }) {
|
||||
const node = nodes[0]
|
||||
async exec({ nodes }) {
|
||||
const sidebar = getSidebar()
|
||||
const [node] = nodes
|
||||
try {
|
||||
// If the sidebar is already open for the current file, do nothing
|
||||
if (window.OCA.Files?.Sidebar?.file === node.path) {
|
||||
if (sidebar.node?.source === node.source) {
|
||||
logger.debug('Sidebar already open for this file', { node })
|
||||
return null
|
||||
}
|
||||
// Open sidebar and set active tab to sharing by default
|
||||
window.OCA.Files?.Sidebar?.setActiveTab('sharing')
|
||||
|
||||
// TODO: migrate Sidebar to use a Node instead
|
||||
await window.OCA.Files?.Sidebar?.open(node.path)
|
||||
|
||||
// Silently update current fileid
|
||||
window.OCP?.Files?.Router?.goToRoute(
|
||||
null,
|
||||
{ view: view.id, fileid: String(node.fileid) },
|
||||
{ ...window.OCP.Files.Router.query, dir: folder.path, opendetails: 'true' },
|
||||
true,
|
||||
)
|
||||
|
||||
sidebar.open(node, 'sharing')
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.error('Error while opening sidebar', { error })
|
||||
|
|
|
|||
42
apps/files/src/actions/sidebarFavoriteAction.ts
Normal file
42
apps/files/src/actions/sidebarFavoriteAction.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import starOutlineSvg from '@mdi/svg/svg/star-outline.svg?raw'
|
||||
import starSvg from '@mdi/svg/svg/star.svg?raw'
|
||||
import { registerSidebarAction } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { favoriteNode } from './favoriteAction.ts'
|
||||
|
||||
/**
|
||||
* Register the favorite/unfavorite action in the sidebar
|
||||
*/
|
||||
export function registerSidebarFavoriteAction() {
|
||||
registerSidebarAction({
|
||||
id: 'files-favorite',
|
||||
order: 0,
|
||||
|
||||
enabled({ node }) {
|
||||
return node.isDavResource && node.root.startsWith('/files/')
|
||||
},
|
||||
|
||||
displayName({ node }) {
|
||||
if (node.attributes.favorite) {
|
||||
return t('files', 'Unfavorite')
|
||||
}
|
||||
return t('files', 'Favorite')
|
||||
},
|
||||
|
||||
iconSvgInline({ node }) {
|
||||
if (node.attributes.favorite) {
|
||||
return starSvg
|
||||
}
|
||||
return starOutlineSvg
|
||||
},
|
||||
|
||||
onClick({ node, view }) {
|
||||
favoriteNode(node, view, !node.attributes.favorite)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<FolderOpenIcon v-if="dragover" v-once />
|
||||
<template v-else>
|
||||
<FolderIcon v-once />
|
||||
<OverlayIcon
|
||||
<component
|
||||
:is="folderOverlay"
|
||||
v-if="folderOverlay"
|
||||
class="files-list__row-icon-overlay" />
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<FavoriteIcon v-once />
|
||||
</span>
|
||||
|
||||
<OverlayIcon
|
||||
<component
|
||||
:is="fileOverlay"
|
||||
v-if="fileOverlay"
|
||||
class="files-list__row-icon-overlay files-list__row-icon-overlay--file" />
|
||||
|
|
@ -56,11 +56,9 @@ import type { UserConfig } from '../../types.ts'
|
|||
|
||||
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 { computed, defineComponent, toRef } 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'
|
||||
|
|
@ -73,6 +71,7 @@ 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 { usePreviewImage } from '../../composables/usePreviewImage.ts'
|
||||
import logger from '../../logger.ts'
|
||||
import { isLivePhoto } from '../../services/LivePhotos.ts'
|
||||
import { useUserConfigStore } from '../../store/userconfig.ts'
|
||||
|
|
@ -111,16 +110,19 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
setup(props) {
|
||||
const userConfigStore = useUserConfigStore()
|
||||
const isPublic = isPublicShare()
|
||||
const publicSharingToken = getSharingToken()
|
||||
const previewUrl = usePreviewImage(
|
||||
toRef(props, 'source'),
|
||||
computed(() => ({
|
||||
crop: userConfigStore.userConfig.crop_image_previews === true,
|
||||
size: props.gridMode ? 128 : 32,
|
||||
})),
|
||||
)
|
||||
|
||||
return {
|
||||
userConfigStore,
|
||||
|
||||
isPublic,
|
||||
publicSharingToken,
|
||||
previewUrl,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -140,60 +142,6 @@ export default defineComponent({
|
|||
return this.userConfigStore.userConfig
|
||||
},
|
||||
|
||||
cropPreviews(): boolean {
|
||||
return this.userConfig.crop_image_previews === true
|
||||
},
|
||||
|
||||
previewUrl() {
|
||||
if (this.source.type === FileType.Folder) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.backgroundFailed === true) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.source.attributes['has-preview'] !== true
|
||||
&& this.source.mime !== undefined
|
||||
&& this.source.mime !== 'application/octet-stream'
|
||||
) {
|
||||
const previewUrl = generateUrl('/core/mimeicon?mime={mime}', {
|
||||
mime: this.source.mime,
|
||||
})
|
||||
const url = new URL(window.location.origin + previewUrl)
|
||||
return url.href
|
||||
}
|
||||
|
||||
try {
|
||||
const previewUrl = this.source.attributes.previewUrl
|
||||
|| (this.isPublic
|
||||
? generateUrl('/apps/files_sharing/publicpreview/{token}?file={file}', {
|
||||
token: this.publicSharingToken,
|
||||
file: this.source.path,
|
||||
})
|
||||
: generateUrl('/core/preview?fileId={fileid}', {
|
||||
fileid: String(this.source.fileid),
|
||||
})
|
||||
)
|
||||
const url = new URL(window.location.origin + previewUrl)
|
||||
|
||||
// Request tiny previews
|
||||
url.searchParams.set('x', this.gridMode ? '128' : '32')
|
||||
url.searchParams.set('y', this.gridMode ? '128' : '32')
|
||||
url.searchParams.set('mimeFallback', 'true')
|
||||
|
||||
// Etag to force refresh preview on change
|
||||
const etag = this.source?.attributes?.etag || ''
|
||||
url.searchParams.set('v', etag.slice(0, 6))
|
||||
|
||||
// Handle cropping
|
||||
url.searchParams.set('a', this.cropPreviews === true ? '0' : '1')
|
||||
return url.href
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
fileOverlay() {
|
||||
if (isLivePhoto(this.source)) {
|
||||
return PlayCircleIcon
|
||||
|
|
|
|||
|
|
@ -77,8 +77,7 @@ import type { ComponentPublicInstance, PropType } from 'vue'
|
|||
import type { UserConfig } from '../types.ts'
|
||||
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { FileType, Folder, getFileActions, Permission, View } from '@nextcloud/files'
|
||||
import { FileType, Folder, getFileActions, getSidebar, Permission, View } from '@nextcloud/files'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import { useHotKey } from '@nextcloud/vue/composables/useHotKey'
|
||||
import { defineComponent } from 'vue'
|
||||
|
|
@ -90,7 +89,6 @@ import FilesListTableFooter from './FilesListTableFooter.vue'
|
|||
import FilesListTableHeader from './FilesListTableHeader.vue'
|
||||
import FilesListTableHeaderActions from './FilesListTableHeaderActions.vue'
|
||||
import VirtualList from './VirtualList.vue'
|
||||
import { action as sidebarAction } from '../actions/sidebarAction.ts'
|
||||
import { useFileListHeaders } from '../composables/useFileListHeaders.ts'
|
||||
import { useFileListWidth } from '../composables/useFileListWidth.ts'
|
||||
import { useRouteParameters } from '../composables/useRouteParameters.ts'
|
||||
|
|
@ -134,6 +132,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
setup() {
|
||||
const sidebar = getSidebar()
|
||||
const activeStore = useActiveStore()
|
||||
const selectionStore = useSelectionStore()
|
||||
const userConfigStore = useUserConfigStore()
|
||||
|
|
@ -148,6 +147,7 @@ export default defineComponent({
|
|||
openDetails,
|
||||
openFile,
|
||||
|
||||
sidebar,
|
||||
activeStore,
|
||||
selectionStore,
|
||||
userConfigStore,
|
||||
|
|
@ -270,20 +270,18 @@ export default defineComponent({
|
|||
// Add events on parent to cover both the table and DragAndDrop notice
|
||||
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
|
||||
mainContent.addEventListener('dragover', this.onDragOver)
|
||||
subscribe('files:sidebar:closed', this.onSidebarClosed)
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
const mainContent = window.document.querySelector('main.app-content') as HTMLElement
|
||||
mainContent.removeEventListener('dragover', this.onDragOver)
|
||||
unsubscribe('files:sidebar:closed', this.onSidebarClosed)
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleOpenQueries() {
|
||||
// If the list is empty, or we don't have a fileId,
|
||||
// there's nothing to be done.
|
||||
if (this.isEmpty || !this.fileId) {
|
||||
if (this.isEmpty || this.fileId === null) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -311,22 +309,12 @@ export default defineComponent({
|
|||
// Open the sidebar for the given URL fileid
|
||||
// iif we just loaded the app.
|
||||
const node = this.nodes.find((n) => n.fileid === fileId) as NcNode
|
||||
if (node && sidebarAction?.enabled?.({
|
||||
nodes: [node],
|
||||
folder: this.currentFolder,
|
||||
view: this.currentView,
|
||||
contents: this.nodes,
|
||||
})) {
|
||||
if (node && this.sidebar.available) {
|
||||
logger.debug('Opening sidebar on file ' + node.path, { node })
|
||||
sidebarAction.exec({
|
||||
nodes: [node],
|
||||
folder: this.currentFolder,
|
||||
view: this.currentView,
|
||||
contents: this.nodes,
|
||||
})
|
||||
return
|
||||
this.sidebar.open(node)
|
||||
} else {
|
||||
logger.warn(`Failed to open sidebar on file ${fileId}, file isn't cached yet !`, { fileId, node })
|
||||
}
|
||||
logger.warn(`Failed to open sidebar on file ${fileId}, file isn't cached yet !`, { fileId, node })
|
||||
},
|
||||
|
||||
scrollToFile(fileId: number | null, warn = true) {
|
||||
|
|
@ -363,19 +351,6 @@ export default defineComponent({
|
|||
)
|
||||
},
|
||||
|
||||
// When sidebar is closed, we remove the openDetails parameter from the URL
|
||||
onSidebarClosed() {
|
||||
if (this.openDetails) {
|
||||
const query = { ...this.$route.query }
|
||||
delete query.opendetails
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null,
|
||||
this.$route.params,
|
||||
query,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle opening a file (e.g. by ?openfile=true)
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { INode } from '@nextcloud/files'
|
||||
|
||||
import { mdiStar } from '@mdi/js'
|
||||
import { formatFileSize } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed } from 'vue'
|
||||
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import NcUserBubble from '@nextcloud/vue/components/NcUserBubble'
|
||||
|
||||
const props = defineProps<{ node: INode }>()
|
||||
|
||||
const isFavourited = computed(() => props.node.attributes.favorite === 1)
|
||||
const size = computed(() => formatFileSize(props.node.size ?? 0))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.filesSidebarSubname">
|
||||
<NcIconSvgWrapper
|
||||
v-if="isFavourited"
|
||||
inline
|
||||
:path="mdiStar"
|
||||
:name="t('files', 'Favorite')" />
|
||||
|
||||
<span>{{ size }}</span>
|
||||
|
||||
<span v-if="node.mtime">
|
||||
<span :class="$style.filesSidebarSubname__separator">•</span>
|
||||
<NcDateTime :timestamp="node.mtime" />
|
||||
</span>
|
||||
|
||||
<template v-if="node.owner">
|
||||
<span :class="$style.filesSidebarSubname__separator">•</span>
|
||||
<NcUserBubble
|
||||
:class="$style.filesSidebarSubname__userBubble"
|
||||
:title="t('files', 'Owner')"
|
||||
:user="node.owner"
|
||||
:display-name="node.attributes['owner-display-name']" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.filesSidebarSubname {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0 8px;
|
||||
}
|
||||
|
||||
.filesSidebarSubname__separator {
|
||||
display: inline-block;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.filesSidebarSubname__userBubble {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
</style>
|
||||
69
apps/files/src/components/FilesSidebar/FilesSidebarTab.vue
Normal file
69
apps/files/src/components/FilesSidebar/FilesSidebarTab.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ISidebarTab, SidebarComponent } from '@nextcloud/files'
|
||||
|
||||
import { NcIconSvgWrapper, NcLoadingIcon } from '@nextcloud/vue'
|
||||
import { ref, toRef, watch, watchEffect } from 'vue'
|
||||
import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import { useActiveStore } from '../../store/active.ts'
|
||||
import { useSidebarStore } from '../../store/sidebar.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
/**
|
||||
* If this is the currently active tab
|
||||
*/
|
||||
active: boolean
|
||||
|
||||
/**
|
||||
* The sidebar tab definition.
|
||||
*/
|
||||
tab: ISidebarTab
|
||||
}>()
|
||||
|
||||
const sidebar = useSidebarStore()
|
||||
const activeStore = useActiveStore()
|
||||
|
||||
const loading = ref(true)
|
||||
watch(toRef(props, 'tab'), async () => {
|
||||
loading.value = true
|
||||
await window.customElements.whenDefined(props.tab.tagName)
|
||||
loading.value = false
|
||||
}, { immediate: true })
|
||||
|
||||
const tabElement = ref<SidebarComponent>()
|
||||
watchEffect(async () => {
|
||||
if (tabElement.value) {
|
||||
// Mark as active
|
||||
await tabElement.value.setActive?.(props.active)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcAppSidebarTab
|
||||
:id="tab.id"
|
||||
:order="tab.order"
|
||||
:name="tab.displayName">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :svg="tab.iconSvgInline" />
|
||||
</template>
|
||||
<NcEmptyContent v-if="loading">
|
||||
<template #icon>
|
||||
<NcLoadingIcon />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<component
|
||||
:is="tab.tagName"
|
||||
v-else
|
||||
ref="tabElement"
|
||||
:node.prop="sidebar.currentNode"
|
||||
:folder.prop="activeStore.activeFolder"
|
||||
:view.prop="activeStore.activeView" />
|
||||
</NcAppSidebarTab>
|
||||
</template>
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LegacyView',
|
||||
props: {
|
||||
component: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
fileInfo(fileInfo) {
|
||||
// update the backbone model FileInfo
|
||||
this.setFileInfo(fileInfo)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// append the backbone element and set the FileInfo
|
||||
this.component.$el.replaceAll(this.$el)
|
||||
this.setFileInfo(this.fileInfo)
|
||||
},
|
||||
|
||||
methods: {
|
||||
setFileInfo(fileInfo) {
|
||||
this.component.setFileInfo(fileInfo)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<NcAppSidebarTab
|
||||
:id="id"
|
||||
ref="tab"
|
||||
:name="name"
|
||||
:icon="icon"
|
||||
@bottomReached="onScrollBottomReached">
|
||||
<template #icon>
|
||||
<slot name="icon" />
|
||||
</template>
|
||||
<!-- Fallback loading -->
|
||||
<NcEmptyContent v-if="loading" icon="icon-loading" />
|
||||
|
||||
<!-- Using a dummy div as Vue mount replace the element directly
|
||||
It does NOT append to the content -->
|
||||
<div ref="mount" />
|
||||
</NcAppSidebarTab>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
|
||||
export default {
|
||||
name: 'SidebarTab',
|
||||
|
||||
components: {
|
||||
NcAppSidebarTab,
|
||||
NcEmptyContent,
|
||||
},
|
||||
|
||||
props: {
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
/**
|
||||
* Lifecycle methods.
|
||||
* They are prefixed with `on` to avoid conflict with Vue
|
||||
* methods like this.destroy
|
||||
*/
|
||||
onMount: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
onUpdate: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
onDestroy: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
onScrollBottomReached: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// TODO: implement a better way to force pass a prop from Sidebar
|
||||
activeTab() {
|
||||
return this.$parent.activeTab
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
async fileInfo(newFile, oldFile) {
|
||||
// Update fileInfo on change
|
||||
if (newFile.id !== oldFile.id) {
|
||||
this.loading = true
|
||||
await this.onUpdate(this.fileInfo)
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.loading = true
|
||||
// Mount the tab: mounting point, fileInfo, vue context
|
||||
await this.onMount(this.$refs.mount, this.fileInfo, this.$refs.tab)
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
async beforeDestroy() {
|
||||
// unmount the tab
|
||||
await this.onDestroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { Mock } from 'vitest'
|
||||
import type { Location } from 'vue-router'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
|
|
@ -104,7 +105,7 @@ describe('HotKeysService testing', () => {
|
|||
activeStore.activeFolder = root
|
||||
|
||||
// @ts-expect-error mocking for tests
|
||||
window.OCA = { Files: { Sidebar: { async open() {}, setActiveTab: () => {} } } }
|
||||
window.OCA = { Files: { _sidebar: () => ({ open() {} }) } }
|
||||
initialState = document.createElement('input')
|
||||
initialState.setAttribute('type', 'hidden')
|
||||
initialState.setAttribute('id', 'initial-state-files_trashbin-config')
|
||||
|
|
@ -143,6 +144,9 @@ describe('HotKeysService testing', () => {
|
|||
})
|
||||
|
||||
it('Pressing s should toggle favorite', () => {
|
||||
(favoriteAction.enabled as Mock).mockReturnValue(true);
|
||||
(favoriteAction.exec as Mock).mockImplementationOnce(() => Promise.resolve(null))
|
||||
|
||||
vi.spyOn(axios, 'post').mockImplementationOnce(() => Promise.resolve())
|
||||
dispatchEvent({ key: 's', code: 'KeyS' })
|
||||
|
||||
|
|
@ -152,7 +156,6 @@ describe('HotKeysService testing', () => {
|
|||
dispatchEvent({ key: 's', code: 'KeyS', shiftKey: true })
|
||||
dispatchEvent({ key: 's', code: 'KeyS', metaKey: true })
|
||||
|
||||
expect(favoriteAction.enabled).toHaveReturnedWith(true)
|
||||
expect(favoriteAction.exec).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
|
|
|
|||
86
apps/files/src/composables/usePreviewImage.ts
Normal file
86
apps/files/src/composables/usePreviewImage.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { INode } from '@nextcloud/files'
|
||||
import type { MaybeRefOrGetter } from '@vueuse/core'
|
||||
|
||||
import { FileType } from '@nextcloud/files'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { getSharingToken, isPublicShare } from '@nextcloud/sharing/public'
|
||||
import { toValue } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
/**
|
||||
* Get the preview URL for a given node.
|
||||
*
|
||||
* @param node - The node to get the preview for
|
||||
* @param options - The preview options
|
||||
* @param options.crop - Whether to crop the preview (default: true)
|
||||
* @param options.fallback - Whether to use a mime type icon as fallback (default: true)
|
||||
* @param options.size - The size of the preview in pixels (default: 128). Can be a number or a tuple [width, height]
|
||||
*/
|
||||
export function usePreviewImage(
|
||||
node: MaybeRefOrGetter<INode | undefined>,
|
||||
options: MaybeRefOrGetter<{ crop?: boolean, fallback?: boolean, size?: number | [number, number] }> = {},
|
||||
) {
|
||||
return computed(() => {
|
||||
const source = toValue(node)
|
||||
if (!source) {
|
||||
return
|
||||
}
|
||||
|
||||
if (source.type === FileType.Folder) {
|
||||
return
|
||||
}
|
||||
|
||||
const fallback = toValue(options).fallback ?? true
|
||||
if (source.attributes['has-preview'] !== true
|
||||
&& source.mime !== undefined
|
||||
&& source.mime !== 'application/octet-stream'
|
||||
) {
|
||||
if (!fallback) {
|
||||
return
|
||||
}
|
||||
|
||||
const previewUrl = generateUrl('/core/mimeicon?mime={mime}', {
|
||||
mime: source.mime,
|
||||
})
|
||||
const url = new URL(window.location.origin + previewUrl)
|
||||
return url.href
|
||||
}
|
||||
|
||||
const crop = toValue(options).crop ?? true
|
||||
const [sizeX, sizeY] = [toValue(options).size ?? 128].flat()
|
||||
|
||||
try {
|
||||
const previewUrl = source.attributes.previewUrl
|
||||
|| (isPublicShare()
|
||||
? generateUrl('/apps/files_sharing/publicpreview/{token}?file={file}', {
|
||||
token: getSharingToken()!,
|
||||
file: source.path,
|
||||
})
|
||||
: generateUrl('/core/preview?fileId={fileid}', {
|
||||
fileid: String(source.fileid),
|
||||
})
|
||||
)
|
||||
const url = new URL(window.location.origin + previewUrl)
|
||||
|
||||
// Request tiny previews
|
||||
url.searchParams.set('x', sizeX.toString())
|
||||
url.searchParams.set('y', (sizeY ?? sizeX).toString())
|
||||
url.searchParams.set('mimeFallback', fallback.toString())
|
||||
|
||||
// Etag to force refresh preview on change
|
||||
const etag = source.attributes.etag || source.mtime?.getTime() || ''
|
||||
url.searchParams.set('v', etag.slice(0, 6))
|
||||
|
||||
// Handle cropping
|
||||
url.searchParams.set('a', crop ? '0' : '1')
|
||||
return url.href
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
25
apps/files/src/eventbus.d.ts
vendored
25
apps/files/src/eventbus.d.ts
vendored
|
|
@ -3,33 +3,36 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { IFileListFilter, Node, View } from '@nextcloud/files'
|
||||
import type { IFileListFilter, INode, IView } from '@nextcloud/files'
|
||||
import type { SearchScope, UserConfig } from './types.ts'
|
||||
|
||||
declare module '@nextcloud/event-bus' {
|
||||
export interface NextcloudEvents {
|
||||
// mapping of 'event name' => 'event type'
|
||||
'files:config:updated': { key: string, value: UserConfig[string] }
|
||||
'files:view-config:updated': { key: string, value: string | number | boolean, view: string }
|
||||
'files:view-config:updated': { key: string, value: string | number | boolean, IView: string }
|
||||
|
||||
'files:favorites:removed': Node
|
||||
'files:favorites:added': Node
|
||||
'files:favorites:added': INode
|
||||
'files:favorites:removed': INode
|
||||
|
||||
'files:filter:added': IFileListFilter
|
||||
'files:filter:removed': string
|
||||
// the state of some filters has changed
|
||||
'files:filters:changed': undefined
|
||||
|
||||
'files:navigation:changed': View
|
||||
'files:navigation:changed': IView
|
||||
|
||||
'files:node:created': Node
|
||||
'files:node:deleted': Node
|
||||
'files:node:updated': Node
|
||||
'files:node:rename': Node
|
||||
'files:node:renamed': Node
|
||||
'files:node:moved': { node: Node, oldSource: string }
|
||||
'files:node:created': INode
|
||||
'files:node:deleted': INode
|
||||
'files:node:updated': INode
|
||||
'files:node:rename': INode
|
||||
'files:node:renamed': INode
|
||||
'files:node:moved': { INode: INode, oldSource: string }
|
||||
|
||||
'files:search:updated': { query: string, scope: SearchScope }
|
||||
|
||||
'files:sidebar:opened': INode
|
||||
'files:sidebar:closed': undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
29
apps/files/src/global.d.ts
vendored
Normal file
29
apps/files/src/global.d.ts
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { ISidebar } from '@nextcloud/files'
|
||||
import type { Pinia } from 'pinia'
|
||||
import type Router from './services/RouterService.ts'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/** private pinia instance to share it between entry points (needed with Webpack) */
|
||||
_nc_files_pinia: Pinia
|
||||
|
||||
OCP: {
|
||||
Files: {
|
||||
/** The files router service to allow apps to interact with the files router instance */
|
||||
Router: Router
|
||||
}
|
||||
}
|
||||
|
||||
OCA: Record<string, unknown> & {
|
||||
Files?: {
|
||||
/** private implementation of the sidebar to be proxied by `@nextcloud/files` */
|
||||
_sidebar?: () => Omit<ISidebar, 'available' | 'registerTab' | 'registerAction' | 'registerAction'>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import { action as openInFilesAction } from './actions/openInFilesAction.ts'
|
|||
import { action as editLocallyAction } from './actions/openLocallyAction.ts'
|
||||
import { action as renameAction } from './actions/renameAction.ts'
|
||||
import { action as sidebarAction } from './actions/sidebarAction.ts'
|
||||
import { registerSidebarFavoriteAction } from './actions/sidebarFavoriteAction.ts'
|
||||
import { action as viewInFolderAction } from './actions/viewInFolderAction.ts'
|
||||
import { registerFilenameFilter } from './filters/FilenameFilter.ts'
|
||||
import { registerHiddenFilesFilter } from './filters/HiddenFilesFilter.ts'
|
||||
|
|
@ -69,6 +70,9 @@ registerModifiedFilter()
|
|||
registerFilenameFilter()
|
||||
registerFilterToSearchToggle()
|
||||
|
||||
// Register sidebar action
|
||||
registerSidebarFavoriteAction()
|
||||
|
||||
// Register preview service worker
|
||||
registerPreviewServiceWorker()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,125 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
export default class Tab {
|
||||
_id
|
||||
_name
|
||||
_icon
|
||||
_iconSvgSanitized
|
||||
_mount
|
||||
_setIsActive
|
||||
_update
|
||||
_destroy
|
||||
_enabled
|
||||
_scrollBottomReached
|
||||
|
||||
/**
|
||||
* Create a new tab instance
|
||||
*
|
||||
* @param {object} options destructuring object
|
||||
* @param {string} options.id the unique id of this tab
|
||||
* @param {string} options.name the translated tab name
|
||||
* @param {string} [options.icon] the icon css class
|
||||
* @param {string} [options.iconSvg] the icon in svg format
|
||||
* @param {Function} options.mount function to mount the tab
|
||||
* @param {Function} [options.setIsActive] function to forward the active state of the tab
|
||||
* @param {Function} options.update function to update the tab
|
||||
* @param {Function} options.destroy function to destroy the tab
|
||||
* @param {Function} [options.enabled] define conditions whether this tab is active. Must returns a boolean
|
||||
* @param {Function} [options.scrollBottomReached] executed when the tab is scrolled to the bottom
|
||||
*/
|
||||
constructor({ id, name, icon, iconSvg, mount, setIsActive, update, destroy, enabled, scrollBottomReached } = {}) {
|
||||
if (enabled === undefined) {
|
||||
enabled = () => true
|
||||
}
|
||||
if (scrollBottomReached === undefined) {
|
||||
scrollBottomReached = () => { }
|
||||
}
|
||||
|
||||
// Sanity checks
|
||||
if (typeof id !== 'string' || id.trim() === '') {
|
||||
throw new Error('The id argument is not a valid string')
|
||||
}
|
||||
if (typeof name !== 'string' || name.trim() === '') {
|
||||
throw new Error('The name argument is not a valid string')
|
||||
}
|
||||
if ((typeof icon !== 'string' || icon.trim() === '') && typeof iconSvg !== 'string') {
|
||||
throw new Error('Missing valid string for icon or iconSvg argument')
|
||||
}
|
||||
if (typeof mount !== 'function') {
|
||||
throw new Error('The mount argument should be a function')
|
||||
}
|
||||
if (setIsActive !== undefined && typeof setIsActive !== 'function') {
|
||||
throw new Error('The setIsActive argument should be a function')
|
||||
}
|
||||
if (typeof update !== 'function') {
|
||||
throw new Error('The update argument should be a function')
|
||||
}
|
||||
if (typeof destroy !== 'function') {
|
||||
throw new Error('The destroy argument should be a function')
|
||||
}
|
||||
if (typeof enabled !== 'function') {
|
||||
throw new Error('The enabled argument should be a function')
|
||||
}
|
||||
if (typeof scrollBottomReached !== 'function') {
|
||||
throw new Error('The scrollBottomReached argument should be a function')
|
||||
}
|
||||
|
||||
this._id = id
|
||||
this._name = name
|
||||
this._icon = icon
|
||||
this._mount = mount
|
||||
this._setIsActive = setIsActive
|
||||
this._update = update
|
||||
this._destroy = destroy
|
||||
this._enabled = enabled
|
||||
this._scrollBottomReached = scrollBottomReached
|
||||
|
||||
if (typeof iconSvg === 'string') {
|
||||
this._iconSvgSanitized = DOMPurify.sanitize(iconSvg)
|
||||
}
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._id
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this._name
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this._icon
|
||||
}
|
||||
|
||||
get iconSvg() {
|
||||
return this._iconSvgSanitized
|
||||
}
|
||||
|
||||
get mount() {
|
||||
return this._mount
|
||||
}
|
||||
|
||||
get setIsActive() {
|
||||
return this._setIsActive || (() => undefined)
|
||||
}
|
||||
|
||||
get update() {
|
||||
return this._update
|
||||
}
|
||||
|
||||
get destroy() {
|
||||
return this._destroy
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return this._enabled
|
||||
}
|
||||
|
||||
get scrollBottomReached() {
|
||||
return this._scrollBottomReached
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
/**
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { INode } from '@nextcloud/files'
|
||||
import type { RawLocation, Route } from 'vue-router'
|
||||
|
||||
import { subscribe } from '@nextcloud/event-bus'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { relative } from 'path'
|
||||
import queryString from 'query-string'
|
||||
|
|
@ -11,6 +14,7 @@ import Vue from 'vue'
|
|||
import Router, { isNavigationFailure, NavigationFailureType } from 'vue-router'
|
||||
import logger from '../logger.ts'
|
||||
import { useFilesStore } from '../store/files.ts'
|
||||
import { getPinia } from '../store/index.ts'
|
||||
import { usePathsStore } from '../store/paths.ts'
|
||||
import { defaultView } from '../utils/filesViews.ts'
|
||||
|
||||
|
|
@ -142,4 +146,30 @@ router.beforeResolve((to, from, next) => {
|
|||
next()
|
||||
})
|
||||
|
||||
subscribe('files:node:deleted', (node: INode) => {
|
||||
if (router.currentRoute.params.fileid === String(node.fileid)) {
|
||||
const params = { ...router.currentRoute.params }
|
||||
const { getPath } = usePathsStore(getPinia())
|
||||
const { getNode } = useFilesStore(getPinia())
|
||||
const source = getPath(router.currentRoute.params.view, node.dirname)
|
||||
const parentFolder = getNode(source!)
|
||||
if (source && parentFolder) {
|
||||
params.fileid = String(parentFolder.fileid)
|
||||
} else {
|
||||
delete params.fileid
|
||||
}
|
||||
|
||||
const query = { ...router.currentRoute.query }
|
||||
delete query.opendetails
|
||||
delete query.openfile
|
||||
|
||||
router.replace({
|
||||
...router.currentRoute,
|
||||
name: router.currentRoute.name as string,
|
||||
params,
|
||||
query,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Location, Route } from 'vue-router'
|
||||
import type VueRouter from 'vue-router'
|
||||
|
||||
|
|
@ -51,23 +52,25 @@ export default class RouterService {
|
|||
/**
|
||||
* Trigger a route change on the files App
|
||||
*
|
||||
* @param name the route name
|
||||
* @param name - The route name or null to keep current route and just update params/query
|
||||
* @param params the route parameters
|
||||
* @param query the url query parameters
|
||||
* @param replace replace the current history
|
||||
* @see https://router.vuejs.org/guide/essentials/navigation.html#navigate-to-a-different-location
|
||||
*/
|
||||
goToRoute(
|
||||
name?: string,
|
||||
params?: Record<string, string>,
|
||||
name: string | null,
|
||||
params: Record<string, string>,
|
||||
query?: Record<string, string | (string | null)[] | null | undefined>,
|
||||
replace?: boolean,
|
||||
): Promise<Route> {
|
||||
return this.router.push({
|
||||
name,
|
||||
query,
|
||||
params,
|
||||
replace,
|
||||
} as Location)
|
||||
if (!name) {
|
||||
name = this.router.currentRoute.name as string
|
||||
}
|
||||
const location: Location = { name, query, params }
|
||||
if (replace) {
|
||||
return this._router.replace(location)
|
||||
}
|
||||
return this._router.push(location)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import logger from '../logger.ts'
|
||||
|
||||
export default class Sidebar {
|
||||
_state
|
||||
|
||||
constructor() {
|
||||
// init empty state
|
||||
this._state = {}
|
||||
|
||||
// init default values
|
||||
this._state.tabs = []
|
||||
this._state.views = []
|
||||
this._state.file = ''
|
||||
this._state.activeTab = ''
|
||||
logger.debug('OCA.Files.Sidebar initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sidebar state
|
||||
*
|
||||
* @readonly
|
||||
* @memberof Sidebar
|
||||
* @return {object} the data state
|
||||
*/
|
||||
get state() {
|
||||
return this._state
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new tab view
|
||||
*
|
||||
* @memberof Sidebar
|
||||
* @param {object} tab a new unregistered tab
|
||||
* @return {boolean}
|
||||
*/
|
||||
registerTab(tab) {
|
||||
const hasDuplicate = this._state.tabs.findIndex((check) => check.id === tab.id) > -1
|
||||
if (!hasDuplicate) {
|
||||
this._state.tabs.push(tab)
|
||||
return true
|
||||
}
|
||||
logger.error(`An tab with the same id ${tab.id} already exists`, { tab })
|
||||
return false
|
||||
}
|
||||
|
||||
registerSecondaryView(view) {
|
||||
const hasDuplicate = this._state.views.findIndex((check) => check.id === view.id) > -1
|
||||
if (!hasDuplicate) {
|
||||
this._state.views.push(view)
|
||||
return true
|
||||
}
|
||||
logger.error('A similar view already exists', { view })
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current opened file
|
||||
*
|
||||
* @memberof Sidebar
|
||||
* @return {string} the current opened file
|
||||
*/
|
||||
get file() {
|
||||
return this._state.file
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current visible sidebar tab
|
||||
*
|
||||
* @memberof Sidebar
|
||||
* @param {string} id the tab unique id
|
||||
*/
|
||||
setActiveTab(id) {
|
||||
this._state.activeTab = id
|
||||
}
|
||||
}
|
||||
|
|
@ -10,8 +10,9 @@ import { getClient, getDefaultPropfind, getRootPath, resultToNode } from '@nextc
|
|||
export const client = getClient()
|
||||
|
||||
/**
|
||||
* Fetches a node from the given path
|
||||
*
|
||||
* @param path
|
||||
* @param path - The path to fetch the node from
|
||||
*/
|
||||
export async function fetchNode(path: string): Promise<Node> {
|
||||
const propfindPayload = getDefaultPropfind()
|
||||
|
|
|
|||
|
|
@ -1,53 +1,13 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
import SidebarView from './views/FilesSidebar.vue'
|
||||
import Tab from './models/Tab.js'
|
||||
import Sidebar from './services/Sidebar.js'
|
||||
import type { ISidebar } from '@nextcloud/files'
|
||||
|
||||
Vue.prototype.t = t
|
||||
import { getPinia } from './store/index.ts'
|
||||
import { useSidebarStore } from './store/sidebar.ts'
|
||||
|
||||
// Init Sidebar Service
|
||||
if (!window.OCA.Files) {
|
||||
window.OCA.Files = {}
|
||||
}
|
||||
Object.assign(window.OCA.Files, { Sidebar: new Sidebar() })
|
||||
Object.assign(window.OCA.Files.Sidebar, { Tab })
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
const contentElement = document.querySelector('body > .content')
|
||||
|| document.querySelector('body > #content')
|
||||
|
||||
let vueParent
|
||||
|
||||
// Make sure we have a proper layout
|
||||
if (contentElement) {
|
||||
// Make sure we have a mountpoint
|
||||
if (!document.getElementById('app-sidebar')) {
|
||||
const sidebarElement = document.createElement('div')
|
||||
sidebarElement.id = 'app-sidebar'
|
||||
contentElement.appendChild(sidebarElement)
|
||||
}
|
||||
|
||||
// Helps with vue debug, as we mount the sidebar to the
|
||||
// content element which is a vue instance itself
|
||||
vueParent = contentElement.__vue__ as Vue
|
||||
}
|
||||
|
||||
// Init vue app
|
||||
const View = Vue.extend(SidebarView)
|
||||
const AppSidebar = new View({
|
||||
name: 'SidebarRoot',
|
||||
parent: vueParent,
|
||||
}).$mount('#app-sidebar')
|
||||
|
||||
// Expose Sidebar methods
|
||||
window.OCA.Files.Sidebar.open = AppSidebar.open
|
||||
window.OCA.Files.Sidebar.close = AppSidebar.close
|
||||
window.OCA.Files.Sidebar.setFullScreenMode = AppSidebar.setFullScreenMode
|
||||
window.OCA.Files.Sidebar.setShowTagsDefault = AppSidebar.setShowTagsDefault
|
||||
})
|
||||
// Provide sidebar implementation which is proxied by the `@nextcloud/files` library for app usage.
|
||||
window.OCA.Files ??= {}
|
||||
window.OCA.Files._sidebar = () => useSidebarStore(getPinia()) satisfies Omit<ISidebar, 'available' | 'registerAction' | 'registerTab' | 'registerAction'>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { FileAction, Folder, Node, View } from '@nextcloud/files'
|
||||
import type { FileAction, IFolder, INode, IView } from '@nextcloud/files'
|
||||
|
||||
import { subscribe } from '@nextcloud/event-bus'
|
||||
import { getNavigation } from '@nextcloud/files'
|
||||
|
|
@ -20,17 +20,17 @@ export const useActiveStore = defineStore('active', () => {
|
|||
/**
|
||||
* The currently active folder
|
||||
*/
|
||||
const activeFolder = ref<Folder>()
|
||||
const activeFolder = ref<IFolder>()
|
||||
|
||||
/**
|
||||
* The current active node within the folder
|
||||
*/
|
||||
const activeNode = ref<Node>()
|
||||
const activeNode = ref<INode>()
|
||||
|
||||
/**
|
||||
* The current active view
|
||||
*/
|
||||
const activeView = ref<View>()
|
||||
const activeView = ref<IView>()
|
||||
|
||||
initialize()
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ export const useActiveStore = defineStore('active', () => {
|
|||
*
|
||||
* @param node - The node thats deleted
|
||||
*/
|
||||
function onDeletedNode(node: Node) {
|
||||
function onDeletedNode(node: INode) {
|
||||
if (activeNode.value && activeNode.value.source === node.source) {
|
||||
activeNode.value = undefined
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ export const useActiveStore = defineStore('active', () => {
|
|||
*
|
||||
* @param view - The new active view
|
||||
*/
|
||||
function onChangedView(view: View | null = null) {
|
||||
function onChangedView(view: IView | null = null) {
|
||||
logger.debug('Setting active view', { view })
|
||||
activeView.value = view ?? undefined
|
||||
activeNode.value = undefined
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { createPinia } from 'pinia'
|
||||
|
||||
/**
|
||||
*
|
||||
* Get the Pinia instance for the Files app.
|
||||
*/
|
||||
export function getPinia() {
|
||||
if (window._nc_files_pinia) {
|
||||
|
|
|
|||
193
apps/files/src/store/sidebar.ts
Normal file
193
apps/files/src/store/sidebar.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { INode, ISidebarContext } from '@nextcloud/files'
|
||||
|
||||
import { subscribe } from '@nextcloud/event-bus'
|
||||
import { getSidebarActions, getSidebarTabs } from '@nextcloud/files'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import logger from '../logger.ts'
|
||||
import { useActiveStore } from './active.ts'
|
||||
|
||||
export const useSidebarStore = defineStore('sidebar', () => {
|
||||
const activeTab = ref<string>()
|
||||
const currentNode = ref<INode>()
|
||||
const isOpen = computed(() => !!currentNode.value)
|
||||
|
||||
const activeStore = useActiveStore()
|
||||
const hasContext = computed(() => !!(currentNode.value && activeStore.activeFolder && activeStore.activeView))
|
||||
const currentContext = computed<ISidebarContext | undefined>(() => {
|
||||
if (!hasContext.value) {
|
||||
return
|
||||
}
|
||||
return {
|
||||
node: currentNode.value!,
|
||||
folder: activeStore.activeFolder!,
|
||||
view: activeStore.activeView!,
|
||||
}
|
||||
})
|
||||
|
||||
const currentActions = computed(() => currentContext.value ? getActions(currentContext.value) : [])
|
||||
const currentTabs = computed(() => currentContext.value ? getTabs(currentContext.value) : [])
|
||||
|
||||
/**
|
||||
* Open the sidebar for a given node and optional tab ID.
|
||||
*
|
||||
* @param node - The node to display in the sidebar.
|
||||
* @param tabId - Optional ID of the tab to activate.
|
||||
*/
|
||||
function open(node: INode, tabId?: string) {
|
||||
const activeStore = useActiveStore()
|
||||
if (!(node && activeStore.activeFolder && activeStore.activeView)) {
|
||||
logger.debug('Cannot open sidebar because the active folder or view is not set.', {
|
||||
node,
|
||||
activeFolder: activeStore.activeFolder,
|
||||
activeView: activeStore.activeView,
|
||||
})
|
||||
|
||||
throw new Error('Cannot open sidebar because the active folder or view is not set.')
|
||||
}
|
||||
|
||||
const newTabs = getTabs({
|
||||
node,
|
||||
folder: activeStore.activeFolder,
|
||||
view: activeStore.activeView,
|
||||
})
|
||||
|
||||
if (tabId && !newTabs.find(({ id }) => id === tabId)) {
|
||||
logger.warn(`Cannot open sidebar tab '${tabId}' because it is not available for the current context.`)
|
||||
activeTab.value = newTabs[0]?.id
|
||||
} else {
|
||||
activeTab.value = tabId ?? newTabs[0]?.id
|
||||
}
|
||||
currentNode.value = node
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the sidebar.
|
||||
*/
|
||||
function close() {
|
||||
currentNode.value = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available tabs for the sidebar.
|
||||
* If a context is provided, only tabs enabled for that context are returned.
|
||||
*
|
||||
* @param context - Optional context to filter the available tabs.
|
||||
*/
|
||||
function getTabs(context?: ISidebarContext) {
|
||||
let tabs = getSidebarTabs()
|
||||
if (context) {
|
||||
tabs = tabs.filter((tab) => tab.enabled(context))
|
||||
}
|
||||
return tabs.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available actions for the sidebar.
|
||||
* If a context is provided, only actions enabled for that context are returned.
|
||||
*
|
||||
* @param context - Optional context to filter the available actions.
|
||||
*/
|
||||
function getActions(context?: ISidebarContext) {
|
||||
let actions = getSidebarActions()
|
||||
if (context) {
|
||||
actions = actions.filter((tab) => tab.enabled(context))
|
||||
}
|
||||
return actions.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active tab in the sidebar.
|
||||
*
|
||||
* @param tabId - The ID of the tab to activate.
|
||||
*/
|
||||
function setActiveTab(tabId: string) {
|
||||
if (!currentTabs.value.find(({ id }) => id === tabId)) {
|
||||
throw new Error(`Cannot set sidebar tab '${tabId}' because it is not available for the current context.`)
|
||||
}
|
||||
activeTab.value = tabId
|
||||
}
|
||||
|
||||
// update the current node if updated
|
||||
subscribe('files:node:updated', (node: INode) => {
|
||||
if (node.source === currentNode.value?.source) {
|
||||
currentNode.value = node
|
||||
}
|
||||
})
|
||||
|
||||
// close the sidebar if the current node is deleted
|
||||
subscribe('files:node:deleted', (node) => {
|
||||
if (node.fileid === currentNode.value?.fileid) {
|
||||
close()
|
||||
}
|
||||
})
|
||||
|
||||
let initialized = false
|
||||
// close sidebar when parameter is removed from url
|
||||
subscribe('files:list:updated', () => {
|
||||
if (!initialized) {
|
||||
initialized = true
|
||||
window.OCP.Files.Router._router.afterEach((to) => {
|
||||
if (to.query && !('opendetails' in to.query)) {
|
||||
close()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// watch open state and update URL query parameters
|
||||
watch(currentNode, (node) => {
|
||||
const query = { ...(window.OCP?.Files?.Router?.query ?? {}) }
|
||||
|
||||
if (!node && 'opendetails' in query) {
|
||||
delete query.opendetails
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null,
|
||||
{ ...window.OCP.Files.Router.params },
|
||||
{
|
||||
...query,
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if (node) {
|
||||
const fileid = String(node.fileid)
|
||||
if (!('opendetails' in query) || window.OCP.Files.Router.params.fileid !== fileid) {
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null,
|
||||
{
|
||||
...window.OCP.Files.Router.params,
|
||||
fileid,
|
||||
},
|
||||
{
|
||||
...query,
|
||||
opendetails: 'true',
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
currentActions,
|
||||
currentContext,
|
||||
currentNode,
|
||||
currentTabs,
|
||||
hasContext,
|
||||
isOpen,
|
||||
|
||||
open,
|
||||
close,
|
||||
getActions,
|
||||
getTabs,
|
||||
setActiveTab,
|
||||
}
|
||||
})
|
||||
|
|
@ -169,7 +169,7 @@ import { getCurrentUser } from '@nextcloud/auth'
|
|||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { showError, showSuccess, showWarning } from '@nextcloud/dialogs'
|
||||
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { Folder, getFileListActions, Permission, sortNodes } from '@nextcloud/files'
|
||||
import { Folder, getFileListActions, getSidebar, Permission, sortNodes } from '@nextcloud/files'
|
||||
import { getRemoteURL, getRootPath } from '@nextcloud/files/dav'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
|
@ -195,7 +195,6 @@ import ViewGridIcon from 'vue-material-design-icons/ViewGridOutline.vue'
|
|||
import BreadCrumbs from '../components/BreadCrumbs.vue'
|
||||
import DragAndDropNotice from '../components/DragAndDropNotice.vue'
|
||||
import FilesListVirtual from '../components/FilesListVirtual.vue'
|
||||
import { action as sidebarAction } from '../actions/sidebarAction.ts'
|
||||
import { useFileListWidth } from '../composables/useFileListWidth.ts'
|
||||
import { useNavigation } from '../composables/useNavigation.ts'
|
||||
import { useRouteParameters } from '../composables/useRouteParameters.ts'
|
||||
|
|
@ -250,6 +249,8 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
setup() {
|
||||
const sidebar = getSidebar()
|
||||
|
||||
const { currentView } = useNavigation()
|
||||
const { directory, fileId } = useRouteParameters()
|
||||
const fileListWidth = useFileListWidth()
|
||||
|
|
@ -283,6 +284,7 @@ export default defineComponent({
|
|||
viewConfigStore,
|
||||
|
||||
// non reactive data
|
||||
sidebar,
|
||||
enableGridView,
|
||||
forbiddenCharacters,
|
||||
ShareType,
|
||||
|
|
@ -557,9 +559,7 @@ export default defineComponent({
|
|||
logger.debug('Directory changed', { newDir, oldDir })
|
||||
// TODO: preserve selection on browsing?
|
||||
this.selectionStore.reset()
|
||||
if (window.OCA.Files.Sidebar?.close) {
|
||||
window.OCA.Files.Sidebar.close()
|
||||
}
|
||||
this.sidebar.close()
|
||||
this.fetchContent()
|
||||
|
||||
// Scroll to top, force virtual scroller to re-render
|
||||
|
|
@ -578,7 +578,6 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
async mounted() {
|
||||
subscribe('files:node:deleted', this.onNodeDeleted)
|
||||
subscribe('files:node:updated', this.onUpdatedNode)
|
||||
|
||||
// reload on settings change
|
||||
|
|
@ -603,7 +602,6 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
unmounted() {
|
||||
unsubscribe('files:node:deleted', this.onNodeDeleted)
|
||||
unsubscribe('files:node:updated', this.onUpdatedNode)
|
||||
unsubscribe('files:config:updated', this.fetchContent)
|
||||
unsubscribe('files:filters:changed', this.filterDirContent)
|
||||
|
|
@ -686,32 +684,6 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle the node deleted event to reset open file
|
||||
*
|
||||
* @param node The deleted node
|
||||
*/
|
||||
onNodeDeleted(node: Node) {
|
||||
if (node.fileid && node.fileid === this.fileId) {
|
||||
if (node.fileid === this.currentFolder?.fileid) {
|
||||
// Handle the edge case that the current directory is deleted
|
||||
// in this case we need to keep the current view but move to the parent directory
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null,
|
||||
{ view: this.currentView!.id },
|
||||
{ dir: this.currentFolder?.dirname ?? '/' },
|
||||
)
|
||||
} else {
|
||||
// If the currently active file is deleted we need to remove the fileid and possible the `openfile` query
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null,
|
||||
{ ...this.$route.params, fileid: undefined },
|
||||
{ ...this.$route.query, openfile: undefined },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The upload manager have finished handling the queue
|
||||
*
|
||||
|
|
@ -792,15 +764,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
if (window?.OCA?.Files?.Sidebar?.setActiveTab) {
|
||||
window.OCA.Files.Sidebar.setActiveTab('sharing')
|
||||
}
|
||||
sidebarAction.exec({
|
||||
nodes: [this.source],
|
||||
view: this.currentView,
|
||||
folder: this.currentFolder,
|
||||
contents: this.dirContents,
|
||||
})
|
||||
this.sidebar.open(this.currentFolder, 'sharing')
|
||||
},
|
||||
|
||||
toggleGridView() {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ import FilesAppSettings from './FilesAppSettings.vue'
|
|||
import { useNavigation } from '../composables/useNavigation.ts'
|
||||
import logger from '../logger.ts'
|
||||
import { useFiltersStore } from '../store/filters.ts'
|
||||
import { useSidebarStore } from '../store/sidebar.ts'
|
||||
import { useViewConfigStore } from '../store/viewConfig.ts'
|
||||
|
||||
const collator = Intl.Collator(
|
||||
|
|
@ -87,6 +88,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
setup() {
|
||||
const sidebar = useSidebarStore()
|
||||
const filtersStore = useFiltersStore()
|
||||
const viewConfigStore = useViewConfigStore()
|
||||
const { currentView, views } = useNavigation()
|
||||
|
|
@ -96,6 +98,7 @@ export default defineComponent({
|
|||
t,
|
||||
views,
|
||||
|
||||
sidebar,
|
||||
filtersStore,
|
||||
viewConfigStore,
|
||||
}
|
||||
|
|
@ -176,8 +179,7 @@ export default defineComponent({
|
|||
* @param view View to set active
|
||||
*/
|
||||
showView(view: View) {
|
||||
// Closing any opened sidebar
|
||||
window.OCA?.Files?.Sidebar?.close?.()
|
||||
this.sidebar.close()
|
||||
getNavigation().setActive(view.id)
|
||||
emit('files:navigation:changed', view)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,620 +3,120 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { ref, toRef, watch } from 'vue'
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import NcAppSidebar from '@nextcloud/vue/components/NcAppSidebar'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import FilesSidebarSubname from '../components/FilesSidebar/FilesSidebarSubname.vue'
|
||||
import FilesSidebarTab from '../components/FilesSidebar/FilesSidebarTab.vue'
|
||||
import { usePreviewImage } from '../composables/usePreviewImage.ts'
|
||||
import { useSidebarStore } from '../store/sidebar.ts'
|
||||
|
||||
const sidebar = useSidebarStore()
|
||||
const previewUrl = usePreviewImage(toRef(sidebar, 'currentNode'), {
|
||||
crop: false,
|
||||
fallback: false,
|
||||
size: [512, 288],
|
||||
})
|
||||
|
||||
const background = ref<string>()
|
||||
watch(previewUrl, () => {
|
||||
background.value = undefined
|
||||
// only try the background if there is more than a mime icon
|
||||
if (previewUrl.value && !previewUrl.value.includes('/core/mimeicon')) {
|
||||
const image = new Image()
|
||||
image.onload = () => {
|
||||
background.value = previewUrl.value
|
||||
}
|
||||
image.src = previewUrl.value
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
/**
|
||||
* Emitted when the sidebar is fully closed.
|
||||
* Trigger the event-bus event.
|
||||
*/
|
||||
function onClosed() {
|
||||
if (sidebar.isOpen) {
|
||||
// was opened again meanwhile
|
||||
return
|
||||
}
|
||||
sidebar.currentNode = undefined
|
||||
emit('files:sidebar:closed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when the sidebar is fully opened.
|
||||
* Trigger the event-bus event.
|
||||
*/
|
||||
function onOpened() {
|
||||
emit('files:sidebar:opened', sidebar.currentNode!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when the sidebar open state is toggled by the sidebar toggle button.
|
||||
* As we hide the open button this is only triggered when the user closes the sidebar.
|
||||
*
|
||||
* @param open - The new open state
|
||||
*/
|
||||
function onToggle(open: boolean) {
|
||||
if (!open) {
|
||||
sidebar.close()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcAppSidebar
|
||||
v-if="file"
|
||||
ref="sidebar"
|
||||
data-cy-sidebar
|
||||
v-bind="appSidebar"
|
||||
:force-menu="true"
|
||||
@close="close"
|
||||
@update:active="setActiveTab"
|
||||
@[defaultActionListener].stop.prevent="onDefaultAction"
|
||||
@opening="handleOpening"
|
||||
@opened="handleOpened"
|
||||
@closing="handleClosing"
|
||||
@closed="handleClosed">
|
||||
<template v-if="fileInfo" #subname>
|
||||
<div class="sidebar__subname">
|
||||
<NcIconSvgWrapper
|
||||
v-if="fileInfo.isFavourited"
|
||||
:path="mdiStar"
|
||||
:name="t('files', 'Favorite')"
|
||||
inline />
|
||||
<span>{{ size }}</span>
|
||||
<span class="sidebar__subname-separator">•</span>
|
||||
<NcDateTime :timestamp="fileInfo.mtime" />
|
||||
<span class="sidebar__subname-separator">•</span>
|
||||
<span>{{ t('files', 'Owner') }}</span>
|
||||
<NcUserBubble
|
||||
:user="ownerId"
|
||||
:display-name="nodeOwnerLabel" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- TODO: create a standard to allow multiple elements here? -->
|
||||
<template v-if="fileInfo" #description>
|
||||
<div class="sidebar__description">
|
||||
<SystemTags
|
||||
v-if="isSystemTagsEnabled && showTagsDefault"
|
||||
v-show="showTags"
|
||||
:disabled="!fileInfo?.canEdit()"
|
||||
:file-id="fileInfo.id" />
|
||||
<LegacyView
|
||||
v-for="view in views"
|
||||
:key="view.cid"
|
||||
:component="view"
|
||||
:file-info="fileInfo" />
|
||||
</div>
|
||||
force-menu
|
||||
:active.sync="sidebar.activeTab"
|
||||
:background="background"
|
||||
:empty="!sidebar.hasContext"
|
||||
:loading="!sidebar.hasContext"
|
||||
:name="sidebar.currentNode?.displayname ?? t('files', 'Loading …')"
|
||||
no-toggle
|
||||
:open="sidebar.isOpen"
|
||||
@closed="onClosed"
|
||||
@opened="onOpened"
|
||||
@update:open="onToggle">
|
||||
<template v-if="sidebar.currentNode" #subname>
|
||||
<FilesSidebarSubname :node="sidebar.currentNode" />
|
||||
</template>
|
||||
|
||||
<!-- Actions menu -->
|
||||
<template v-if="fileInfo" #secondary-actions>
|
||||
<template v-if="sidebar.currentContext" #secondary-actions>
|
||||
<!-- we cannot use a sub component due to limitations of the NcActions component -->
|
||||
<NcActionButton
|
||||
:close-after-click="true"
|
||||
@click="toggleStarred(!fileInfo.isFavourited)">
|
||||
v-for="action of sidebar.currentActions"
|
||||
:key="action.id"
|
||||
close-after-click
|
||||
@click="action.onClick(sidebar.currentContext)">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="fileInfo.isFavourited ? mdiStar : mdiStarOutline" />
|
||||
<NcIconSvgWrapper :svg="action.iconSvgInline(sidebar.currentContext)" />
|
||||
</template>
|
||||
{{ fileInfo.isFavourited ? t('files', 'Remove from favorites') : t('files', 'Add to favorites') }}
|
||||
</NcActionButton>
|
||||
<!-- TODO: create proper api for apps to register actions
|
||||
And inject themselves here. -->
|
||||
<NcActionButton
|
||||
v-if="isSystemTagsEnabled"
|
||||
:close-after-click="true"
|
||||
@click="toggleTags">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiTagMultipleOutline" />
|
||||
</template>
|
||||
{{ t('files', 'Tags') }}
|
||||
{{ action.displayName(sidebar.currentContext) }}
|
||||
</NcActionButton>
|
||||
</template>
|
||||
|
||||
<!-- Error display -->
|
||||
<NcEmptyContent v-if="error" icon="icon-error">
|
||||
{{ error }}
|
||||
</NcEmptyContent>
|
||||
<!-- Description -->
|
||||
<!-- <template v-if="hasContext" #description>
|
||||
<FilesSidebarDescription />
|
||||
</template> -->
|
||||
|
||||
<!-- If fileInfo fetch is complete, render tabs -->
|
||||
<template v-for="tab in tabs" v-else-if="fileInfo">
|
||||
<!-- Hide them if we're loading another file but keep them mounted -->
|
||||
<SidebarTab
|
||||
v-if="tab.enabled(fileInfo)"
|
||||
v-show="!loading"
|
||||
:id="tab.id"
|
||||
<template v-if="sidebar.hasContext">
|
||||
<FilesSidebarTab
|
||||
v-for="tab in sidebar.currentTabs"
|
||||
:key="tab.id"
|
||||
:name="tab.name"
|
||||
:icon="tab.icon"
|
||||
:on-mount="tab.mount"
|
||||
:on-update="tab.update"
|
||||
:on-destroy="tab.destroy"
|
||||
:on-scroll-bottom-reached="tab.scrollBottomReached"
|
||||
:file-info="fileInfo">
|
||||
<template v-if="tab.iconSvg !== undefined" #icon>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="svg-icon" v-html="tab.iconSvg" />
|
||||
</template>
|
||||
</SidebarTab>
|
||||
:active="sidebar.activeTab === tab.id"
|
||||
:tab="tab" />
|
||||
</template>
|
||||
</NcAppSidebar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { INode } from '@nextcloud/files'
|
||||
|
||||
import { mdiStar, mdiStarOutline, mdiTagMultipleOutline } from '@mdi/js'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { File, Folder, formatFileSize } from '@nextcloud/files'
|
||||
import { getRemoteURL, getRootPath } from '@nextcloud/files/dav'
|
||||
import { encodePath } from '@nextcloud/paths'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { ShareType } from '@nextcloud/sharing'
|
||||
import $ from 'jquery'
|
||||
import { defineComponent } from 'vue'
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import NcAppSidebar from '@nextcloud/vue/components/NcAppSidebar'
|
||||
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import NcUserBubble from '@nextcloud/vue/components/NcUserBubble'
|
||||
import SystemTags from '../../../systemtags/src/components/SystemTags.vue'
|
||||
import LegacyView from '../components/LegacyView.vue'
|
||||
import SidebarTab from '../components/SidebarTab.vue'
|
||||
import logger from '../logger.ts'
|
||||
import FileInfo from '../services/FileInfo.js'
|
||||
import { fetchNode } from '../services/WebdavClient.ts'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FilesSidebar',
|
||||
|
||||
components: {
|
||||
LegacyView,
|
||||
NcActionButton,
|
||||
NcAppSidebar,
|
||||
NcDateTime,
|
||||
NcEmptyContent,
|
||||
NcIconSvgWrapper,
|
||||
SidebarTab,
|
||||
SystemTags,
|
||||
NcUserBubble,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const currentUser = getCurrentUser()
|
||||
|
||||
// Non reactive properties
|
||||
return {
|
||||
currentUser,
|
||||
|
||||
mdiStar,
|
||||
mdiStarOutline,
|
||||
mdiTagMultipleOutline,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
// reactive state
|
||||
Sidebar: OCA.Files.Sidebar.state,
|
||||
showTags: false,
|
||||
showTagsDefault: true,
|
||||
error: null,
|
||||
loading: true,
|
||||
fileInfo: null,
|
||||
node: null as INode | null,
|
||||
isFullScreen: false,
|
||||
hasLowHeight: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Current filename
|
||||
* This is bound to the Sidebar service and
|
||||
* is used to load a new file
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
file() {
|
||||
return this.Sidebar.file
|
||||
},
|
||||
|
||||
/**
|
||||
* List of all the registered tabs
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
tabs() {
|
||||
return this.Sidebar.tabs
|
||||
},
|
||||
|
||||
/**
|
||||
* List of all the registered views
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
views() {
|
||||
return this.Sidebar.views
|
||||
},
|
||||
|
||||
/**
|
||||
* Current user dav root path
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
davPath() {
|
||||
return `${getRemoteURL()}${getRootPath()}${encodePath(this.file)}`
|
||||
},
|
||||
|
||||
/**
|
||||
* Current active tab handler
|
||||
*
|
||||
* @return {string} the current active tab
|
||||
*/
|
||||
activeTab() {
|
||||
return this.Sidebar.activeTab
|
||||
},
|
||||
|
||||
/**
|
||||
* File size formatted string
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
size() {
|
||||
return formatFileSize(this.fileInfo?.size)
|
||||
},
|
||||
|
||||
/**
|
||||
* File background/figure to illustrate the sidebar header
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
background() {
|
||||
return this.getPreviewIfAny(this.fileInfo)
|
||||
},
|
||||
|
||||
/**
|
||||
* App sidebar v-binding object
|
||||
*
|
||||
* @return {object}
|
||||
*/
|
||||
appSidebar() {
|
||||
if (this.fileInfo) {
|
||||
return {
|
||||
'data-mimetype': this.fileInfo.mimetype,
|
||||
active: this.activeTab,
|
||||
background: this.background,
|
||||
class: {
|
||||
'app-sidebar--has-preview': this.fileInfo.hasPreview && !this.isFullScreen,
|
||||
'app-sidebar--full': this.isFullScreen,
|
||||
},
|
||||
|
||||
compact: this.hasLowHeight || !this.fileInfo.hasPreview || this.isFullScreen,
|
||||
loading: this.loading,
|
||||
name: this.node?.displayname ?? this.fileInfo.name,
|
||||
title: this.node?.displayname ?? this.fileInfo.name,
|
||||
}
|
||||
} else if (this.error) {
|
||||
return {
|
||||
key: 'error', // force key to re-render
|
||||
subname: '',
|
||||
name: '',
|
||||
class: {
|
||||
'app-sidebar--full': this.isFullScreen,
|
||||
},
|
||||
}
|
||||
}
|
||||
// no fileInfo yet, showing empty data
|
||||
return {
|
||||
loading: this.loading,
|
||||
subname: '',
|
||||
name: '',
|
||||
class: {
|
||||
'app-sidebar--full': this.isFullScreen,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Default action object for the current file
|
||||
*
|
||||
* @return {object}
|
||||
*/
|
||||
defaultAction() {
|
||||
return this.fileInfo
|
||||
&& OCA.Files && OCA.Files.App && OCA.Files.App.fileList
|
||||
&& OCA.Files.App.fileList.fileActions
|
||||
&& OCA.Files.App.fileList.fileActions.getDefaultFileAction
|
||||
&& OCA.Files.App.fileList
|
||||
.fileActions.getDefaultFileAction(this.fileInfo.mimetype, this.fileInfo.type, OC.PERMISSION_READ)
|
||||
},
|
||||
|
||||
/**
|
||||
* Dynamic header click listener to ensure
|
||||
* nothing is listening for a click if there
|
||||
* is no default action
|
||||
*
|
||||
* @return {string|null}
|
||||
*/
|
||||
defaultActionListener() {
|
||||
return this.defaultAction ? 'figure-click' : null
|
||||
},
|
||||
|
||||
isSystemTagsEnabled() {
|
||||
return getCapabilities()?.systemtags?.enabled === true
|
||||
},
|
||||
|
||||
ownerId() {
|
||||
return this.node?.attributes?.['owner-id'] ?? this.currentUser.uid
|
||||
},
|
||||
|
||||
currentUserIsOwner() {
|
||||
return this.ownerId === this.currentUser.uid
|
||||
},
|
||||
|
||||
nodeOwnerLabel() {
|
||||
let ownerDisplayName = this.node?.attributes?.['owner-display-name']
|
||||
if (this.currentUserIsOwner) {
|
||||
ownerDisplayName = `${ownerDisplayName} (${t('files', 'You')})`
|
||||
}
|
||||
return ownerDisplayName
|
||||
},
|
||||
|
||||
sharedMultipleTimes() {
|
||||
if (Array.isArray(node.attributes?.['share-types']) && node.attributes?.['share-types'].length > 1) {
|
||||
return t('files', 'Shared multiple times with different people')
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
subscribe('files:node:deleted', this.onNodeDeleted)
|
||||
|
||||
window.addEventListener('resize', this.handleWindowResize)
|
||||
this.handleWindowResize()
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
unsubscribe('file:node:deleted', this.onNodeDeleted)
|
||||
window.removeEventListener('resize', this.handleWindowResize)
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Can this tab be displayed ?
|
||||
*
|
||||
* @param {object} tab a registered tab
|
||||
* @return {boolean}
|
||||
*/
|
||||
canDisplay(tab) {
|
||||
return tab.enabled(this.fileInfo)
|
||||
},
|
||||
|
||||
resetData() {
|
||||
this.error = null
|
||||
this.fileInfo = null
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.tabs) {
|
||||
this.$refs.tabs.updateTabs()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
getPreviewIfAny(fileInfo) {
|
||||
if (fileInfo?.hasPreview && !this.isFullScreen) {
|
||||
const etag = fileInfo?.etag || ''
|
||||
return generateUrl(`/core/preview?fileId=${fileInfo.id}&x=${screen.width}&y=${screen.height}&a=true&v=${etag.slice(0, 6)}`)
|
||||
}
|
||||
return this.getIconUrl(fileInfo)
|
||||
},
|
||||
|
||||
/**
|
||||
* Copied from https://github.com/nextcloud/server/blob/16e0887ec63591113ee3f476e0c5129e20180cde/apps/files/js/filelist.js#L1377
|
||||
* TODO: We also need this as a standalone library
|
||||
*
|
||||
* @param {object} fileInfo the fileinfo
|
||||
* @return {string} Url to the icon for mimeType
|
||||
*/
|
||||
getIconUrl(fileInfo) {
|
||||
const mimeType = fileInfo?.mimetype || 'application/octet-stream'
|
||||
if (mimeType === 'httpd/unix-directory') {
|
||||
// use default folder icon
|
||||
if (fileInfo.mountType === 'shared' || fileInfo.mountType === 'shared-root') {
|
||||
return OC.MimeType.getIconUrl('dir-shared')
|
||||
} else if (fileInfo.mountType === 'external-root') {
|
||||
return OC.MimeType.getIconUrl('dir-external')
|
||||
} else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') {
|
||||
return OC.MimeType.getIconUrl('dir-' + fileInfo.mountType)
|
||||
} else if (fileInfo.shareTypes && (
|
||||
fileInfo.shareTypes.indexOf(ShareType.Link) > -1
|
||||
|| fileInfo.shareTypes.indexOf(ShareType.Email) > -1)
|
||||
) {
|
||||
return OC.MimeType.getIconUrl('dir-public')
|
||||
} else if (fileInfo.shareTypes && fileInfo.shareTypes.length > 0) {
|
||||
return OC.MimeType.getIconUrl('dir-shared')
|
||||
}
|
||||
return OC.MimeType.getIconUrl('dir')
|
||||
}
|
||||
return OC.MimeType.getIconUrl(mimeType)
|
||||
},
|
||||
|
||||
/**
|
||||
* Set current active tab
|
||||
*
|
||||
* @param {string} id tab unique id
|
||||
*/
|
||||
setActiveTab(id) {
|
||||
OCA.Files.Sidebar.setActiveTab(id)
|
||||
this.tabs.forEach((tab) => {
|
||||
try {
|
||||
tab.setIsActive(id === tab.id)
|
||||
} catch (error) {
|
||||
logger.error('Error while setting tab active state', { error, id: tab.id, tab })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle favorite state
|
||||
* TODO: better implementation
|
||||
*
|
||||
* @param {boolean} state is favorite or not
|
||||
*/
|
||||
async toggleStarred(state) {
|
||||
try {
|
||||
await axios({
|
||||
method: 'PROPPATCH',
|
||||
url: this.davPath,
|
||||
data: `<?xml version="1.0"?>
|
||||
<d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
|
||||
${state ? '<d:set>' : '<d:remove>'}
|
||||
<d:prop>
|
||||
<oc:favorite>1</oc:favorite>
|
||||
</d:prop>
|
||||
${state ? '</d:set>' : '</d:remove>'}
|
||||
</d:propertyupdate>`,
|
||||
})
|
||||
|
||||
/**
|
||||
* TODO: adjust this when the Sidebar is finally using File/Folder classes
|
||||
*
|
||||
* @see https://github.com/nextcloud/server/blob/8a75cb6e72acd42712ab9fea22296aa1af863ef5/apps/files/src/views/favorites.ts#L83-L115
|
||||
*/
|
||||
const isDir = this.fileInfo.type === 'dir'
|
||||
const Node = isDir ? Folder : File
|
||||
const node = new Node({
|
||||
id: this.fileInfo.id,
|
||||
source: `${getRemoteURL()}${getRootPath()}${this.file}`,
|
||||
root: getRootPath(),
|
||||
owner: null,
|
||||
mime: isDir ? undefined : this.fileInfo.mimetype,
|
||||
attributes: {
|
||||
favorite: 1,
|
||||
},
|
||||
})
|
||||
emit(state ? 'files:favorites:added' : 'files:favorites:removed', node)
|
||||
|
||||
this.fileInfo.isFavourited = state
|
||||
} catch (error) {
|
||||
showError(t('files', 'Unable to change the favorite state of the file'))
|
||||
logger.error('Unable to change favorite state', { error })
|
||||
}
|
||||
},
|
||||
|
||||
onDefaultAction() {
|
||||
if (this.defaultAction) {
|
||||
// generate fake context
|
||||
this.defaultAction.action(this.fileInfo.name, {
|
||||
fileInfo: this.fileInfo,
|
||||
dir: this.fileInfo.dir,
|
||||
fileList: OCA.Files.App.fileList,
|
||||
$file: $('body'),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the tags selector
|
||||
*/
|
||||
toggleTags() {
|
||||
// toggle
|
||||
this.showTags = !this.showTags
|
||||
// save the new state
|
||||
this.setShowTagsDefault(this.showTags)
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the sidebar for the given file
|
||||
*
|
||||
* @param {string} path the file path to load
|
||||
* @return {Promise}
|
||||
* @throws {Error} loading failure
|
||||
*/
|
||||
async open(path) {
|
||||
if (!path || path.trim() === '') {
|
||||
throw new Error(`Invalid path '${path}'`)
|
||||
}
|
||||
|
||||
// Only focus the tab when the selected file/tab is changed in already opened sidebar
|
||||
// Focusing the sidebar on first file open is handled by NcAppSidebar
|
||||
const focusTabAfterLoad = !!this.Sidebar.file
|
||||
|
||||
// update current opened file
|
||||
this.Sidebar.file = path
|
||||
|
||||
// reset data, keep old fileInfo to not reload all tabs and just hide them
|
||||
this.error = null
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
this.node = await fetchNode(this.file)
|
||||
this.fileInfo = FileInfo(this.node)
|
||||
// adding this as fallback because other apps expect it
|
||||
this.fileInfo.dir = this.file.split('/').slice(0, -1).join('/')
|
||||
|
||||
// DEPRECATED legacy views
|
||||
// TODO: remove
|
||||
this.views.forEach((view) => {
|
||||
view.setFileInfo(this.fileInfo)
|
||||
})
|
||||
|
||||
await this.$nextTick()
|
||||
|
||||
this.setActiveTab(this.Sidebar.activeTab || this.tabs[0].id)
|
||||
|
||||
this.loading = false
|
||||
|
||||
await this.$nextTick()
|
||||
|
||||
if (focusTabAfterLoad && this.$refs.sidebar) {
|
||||
this.$refs.sidebar.focusActiveTabContent()
|
||||
}
|
||||
} catch (error) {
|
||||
this.loading = false
|
||||
this.error = t('files', 'Error while loading the file data')
|
||||
logger.error('Error while loading the file data', { error })
|
||||
|
||||
throw new Error(error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the sidebar
|
||||
*/
|
||||
close() {
|
||||
this.Sidebar.file = ''
|
||||
this.showTags = false
|
||||
this.resetData()
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle if the current node was deleted
|
||||
*
|
||||
* @param {import('@nextcloud/files').Node} node The deleted node
|
||||
*/
|
||||
onNodeDeleted(node) {
|
||||
if (this.fileInfo && node && this.fileInfo.id === node.fileid) {
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Allow to set the Sidebar as fullscreen from OCA.Files.Sidebar
|
||||
*
|
||||
* @param {boolean} isFullScreen - Whether or not to render the Sidebar in fullscreen.
|
||||
*/
|
||||
setFullScreenMode(isFullScreen) {
|
||||
this.isFullScreen = isFullScreen
|
||||
const content = document.querySelector('#content') || document.querySelector('#content-vue')
|
||||
if (isFullScreen) {
|
||||
content?.classList.add('with-sidebar--full')
|
||||
} else {
|
||||
content?.classList.remove('with-sidebar--full')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Allow to set whether tags should be shown by default from OCA.Files.Sidebar
|
||||
*
|
||||
* @param {boolean} showTagsDefault - Whether or not to show the tags by default.
|
||||
*/
|
||||
setShowTagsDefault(showTagsDefault) {
|
||||
this.showTagsDefault = showTagsDefault
|
||||
},
|
||||
|
||||
/**
|
||||
* Emit SideBar events.
|
||||
*/
|
||||
handleOpening() {
|
||||
emit('files:sidebar:opening')
|
||||
},
|
||||
|
||||
handleOpened() {
|
||||
emit('files:sidebar:opened')
|
||||
},
|
||||
|
||||
handleClosing() {
|
||||
emit('files:sidebar:closing')
|
||||
},
|
||||
|
||||
handleClosed() {
|
||||
emit('files:sidebar:closed')
|
||||
},
|
||||
|
||||
handleWindowResize() {
|
||||
this.hasLowHeight = document.documentElement.clientHeight < 1024
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-sidebar {
|
||||
&--has-preview:deep {
|
||||
|
|
@ -654,21 +154,6 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
.sidebar__subname {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0 8px;
|
||||
|
||||
&-separator {
|
||||
display: inline-block;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.user-bubble__wrapper {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -130,9 +130,9 @@ describe('Favorites view definition', () => {
|
|||
|
||||
describe('Dynamic update of favorite folders', () => {
|
||||
let Navigation
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
|
||||
delete window._nc_navigation
|
||||
Navigation = getNavigation()
|
||||
})
|
||||
|
|
@ -167,8 +167,9 @@ describe('Dynamic update of favorite folders', () => {
|
|||
contents: [],
|
||||
})
|
||||
|
||||
expect(eventBus.emit).toHaveBeenCalledTimes(1)
|
||||
expect(eventBus.emit).toHaveBeenCalledTimes(2)
|
||||
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:added', folder)
|
||||
expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder)
|
||||
})
|
||||
|
||||
test('Remove a favorite folder remove the entry from the navigation column', async () => {
|
||||
|
|
@ -213,8 +214,9 @@ describe('Dynamic update of favorite folders', () => {
|
|||
contents: [],
|
||||
})
|
||||
|
||||
expect(eventBus.emit).toHaveBeenCalledTimes(1)
|
||||
expect(eventBus.emit).toHaveBeenCalledTimes(2)
|
||||
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:removed', folder)
|
||||
expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder)
|
||||
expect(fo).toHaveBeenCalled()
|
||||
|
||||
favoritesView = Navigation.views.find((view) => view.id === 'favorites')
|
||||
|
|
@ -257,7 +259,8 @@ describe('Dynamic update of favorite folders', () => {
|
|||
folder: {} as NcFolder,
|
||||
contents: [],
|
||||
})
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:favorites:added', folder)
|
||||
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:added', folder)
|
||||
expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder)
|
||||
|
||||
// Create a folder with the same id but renamed
|
||||
const renamedFolder = new Folder({
|
||||
|
|
@ -269,6 +272,6 @@ describe('Dynamic update of favorite folders', () => {
|
|||
|
||||
// Exec the rename action
|
||||
eventBus.emit('files:node:renamed', renamedFolder)
|
||||
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:renamed', renamedFolder)
|
||||
expect(eventBus.emit).toHaveBeenCalledWith('files:node:renamed', renamedFolder)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,28 +3,28 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { INode } from '@nextcloud/files'
|
||||
|
||||
import AccountGroupSvg from '@mdi/svg/svg/account-group-outline.svg?raw'
|
||||
import AccountPlusSvg from '@mdi/svg/svg/account-plus-outline.svg?raw'
|
||||
import LinkSvg from '@mdi/svg/svg/link.svg?raw'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { FileAction, Permission, registerFileAction } from '@nextcloud/files'
|
||||
import { FileAction, getSidebar, Permission, registerFileAction } from '@nextcloud/files'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { ShareType } from '@nextcloud/sharing'
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
import CircleSvg from '../../../../core/img/apps/circles.svg?raw'
|
||||
import { action as sidebarAction } from '../../../files/src/actions/sidebarAction.ts'
|
||||
import { generateAvatarSvg } from '../utils/AccountIcon.ts'
|
||||
|
||||
import './sharingStatusAction.scss'
|
||||
|
||||
/**
|
||||
* Check if the node is external (federated)
|
||||
*
|
||||
* @param node
|
||||
* @param node - The node to check
|
||||
*/
|
||||
function isExternal(node: Node) {
|
||||
function isExternal(node: INode) {
|
||||
return node.attributes?.['is-federated'] ?? false
|
||||
}
|
||||
|
||||
|
|
@ -136,12 +136,12 @@ export const action = new FileAction({
|
|||
&& (node.permissions & Permission.READ) !== 0
|
||||
},
|
||||
|
||||
async exec({ nodes, view, folder, contents }) {
|
||||
async exec({ nodes }) {
|
||||
// You need read permissions to see the sidebar
|
||||
const node = nodes[0]
|
||||
if ((node.permissions & Permission.READ) !== 0) {
|
||||
window.OCA?.Files?.Sidebar?.setActiveTab?.('sharing')
|
||||
sidebarAction.exec({ nodes, view, folder, contents })
|
||||
const sidebar = getSidebar()
|
||||
sidebar.open(node, 'sharing')
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@
|
|||
|
||||
import ShareVariant from '@mdi/svg/svg/share-variant.svg?raw'
|
||||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
import { registerSidebarTab } from '@nextcloud/files'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import wrap from '@vue/web-component-wrapper'
|
||||
import Vue from 'vue'
|
||||
import FilesSidebarTab from './views/FilesSidebarTab.vue'
|
||||
import ExternalShareActions from './services/ExternalShareActions.js'
|
||||
import ShareSearch from './services/ShareSearch.js'
|
||||
import TabSections from './services/TabSections.js'
|
||||
|
|
@ -14,9 +17,7 @@ import TabSections from './services/TabSections.js'
|
|||
__webpack_nonce__ = getCSPNonce()
|
||||
|
||||
// Init Sharing Tab Service
|
||||
if (!window.OCA.Sharing) {
|
||||
window.OCA.Sharing = {}
|
||||
}
|
||||
window.OCA.Sharing ??= {}
|
||||
Object.assign(window.OCA.Sharing, { ShareSearch: new ShareSearch() })
|
||||
Object.assign(window.OCA.Sharing, { ExternalShareActions: new ExternalShareActions() })
|
||||
Object.assign(window.OCA.Sharing, { ShareTabSections: new TabSections() })
|
||||
|
|
@ -24,42 +25,34 @@ Object.assign(window.OCA.Sharing, { ShareTabSections: new TabSections() })
|
|||
Vue.prototype.t = t
|
||||
Vue.prototype.n = n
|
||||
|
||||
// Init Sharing tab component
|
||||
let TabInstance = null
|
||||
const tagName = 'files_sharing-sidebar-tab'
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
if (OCA.Files && OCA.Files.Sidebar) {
|
||||
OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab({
|
||||
id: 'sharing',
|
||||
name: t('files_sharing', 'Sharing'),
|
||||
iconSvg: ShareVariant,
|
||||
|
||||
async mount(el, fileInfo, context) {
|
||||
const SharingTab = (await import('./views/SharingTab.vue')).default
|
||||
const View = Vue.extend(SharingTab)
|
||||
|
||||
if (TabInstance) {
|
||||
TabInstance.$destroy()
|
||||
}
|
||||
TabInstance = new View({
|
||||
// Better integration with vue parent component
|
||||
parent: context,
|
||||
})
|
||||
// Only mount after we have all the info we need
|
||||
await TabInstance.update(fileInfo)
|
||||
TabInstance.$mount(el)
|
||||
},
|
||||
|
||||
update(fileInfo) {
|
||||
TabInstance.update(fileInfo)
|
||||
},
|
||||
|
||||
destroy() {
|
||||
if (TabInstance) {
|
||||
TabInstance.$destroy()
|
||||
TabInstance = null
|
||||
}
|
||||
},
|
||||
}))
|
||||
}
|
||||
registerSidebarTab({
|
||||
id: 'sharing',
|
||||
displayName: t('files_sharing', 'Sharing'),
|
||||
iconSvgInline: ShareVariant,
|
||||
order: 10,
|
||||
tagName,
|
||||
enabled() {
|
||||
if (!window.customElements.get(tagName)) {
|
||||
setupSidebarTab()
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Setup the sidebar tab as a web component
|
||||
*/
|
||||
function setupSidebarTab() {
|
||||
const webComponent = wrap(Vue, FilesSidebarTab)
|
||||
// In Vue 2, wrap doesn't support diseabling shadow. Disable with a hack
|
||||
Object.defineProperty(webComponent.prototype, 'attachShadow', {
|
||||
value() { return this },
|
||||
})
|
||||
Object.defineProperty(webComponent.prototype, 'shadowRoot', {
|
||||
get() { return this },
|
||||
})
|
||||
|
||||
window.customElements.define(tagName, webComponent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
|
||||
import type { Attribute, Node } from '@nextcloud/files'
|
||||
import type { Attribute, INode } from '@nextcloud/files'
|
||||
|
||||
interface RawLegacyFileInfo {
|
||||
id: number
|
||||
|
|
@ -30,11 +28,16 @@ export type LegacyFileInfo = RawLegacyFileInfo & {
|
|||
get: (key: keyof RawLegacyFileInfo) => unknown
|
||||
isDirectory: () => boolean
|
||||
canEdit: () => boolean
|
||||
node: Node
|
||||
node: INode
|
||||
canDownload: () => boolean
|
||||
}
|
||||
|
||||
export default function(node: Node): LegacyFileInfo {
|
||||
/**
|
||||
* Convert Node to legacy file info
|
||||
*
|
||||
* @param node - The Node to convert
|
||||
*/
|
||||
export default function(node: INode): LegacyFileInfo {
|
||||
const rawFileInfo: RawLegacyFileInfo = {
|
||||
id: node.fileid!,
|
||||
path: node.dirname,
|
||||
26
apps/files_sharing/src/views/FilesSidebarTab.vue
Normal file
26
apps/files_sharing/src/views/FilesSidebarTab.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IFolder, INode, IView } from '@nextcloud/files'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import SharingTab from './SharingTab.vue'
|
||||
import FileInfo from '../services/FileInfo.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
node?: INode
|
||||
// eslint-disable-next-line vue/no-unused-properties -- Required on the web component interface
|
||||
folder?: IFolder
|
||||
// eslint-disable-next-line vue/no-unused-properties -- Required on the web component interface
|
||||
view?: IView
|
||||
}>()
|
||||
|
||||
const fileInfo = computed(() => props.node && FileInfo(props.node))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SharingTab v-if="fileInfo" :file-info="fileInfo" />
|
||||
</template>
|
||||
|
|
@ -230,6 +230,13 @@ export default {
|
|||
|
||||
mixins: [ShareDetails],
|
||||
|
||||
props: {
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: new Config(),
|
||||
|
|
@ -238,8 +245,6 @@ export default {
|
|||
expirationInterval: null,
|
||||
loading: true,
|
||||
|
||||
fileInfo: null,
|
||||
|
||||
// reshare Share object
|
||||
reshare: null,
|
||||
sharedWithMe: {},
|
||||
|
|
@ -328,18 +333,19 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update current fileInfo and fetch new data
|
||||
*
|
||||
* @param {object} fileInfo the current file FileInfo
|
||||
*/
|
||||
async update(fileInfo) {
|
||||
this.fileInfo = fileInfo
|
||||
this.resetState()
|
||||
this.getShares()
|
||||
watch: {
|
||||
fileInfo: {
|
||||
immediate: true,
|
||||
handler(newValue, oldValue) {
|
||||
if (oldValue?.id === undefined || oldValue?.id !== newValue?.id) {
|
||||
this.resetState()
|
||||
this.getShares()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the existing shares infos
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -24,6 +24,6 @@ class LoadAdditionalListener implements IEventListener {
|
|||
// TODO: make sure to only include the sidebar script when
|
||||
// we properly split it between files list and sidebar
|
||||
Util::addStyle(Application::APP_ID, 'sidebar-tab');
|
||||
Util::addScript(Application::APP_ID, 'sidebar-tab');
|
||||
Util::addInitScript(Application::APP_ID, 'sidebar-tab');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,6 @@ class LoadSidebarListener implements IEventListener {
|
|||
// TODO: make sure to only include the sidebar script when
|
||||
// we properly split it between files list and sidebar
|
||||
Util::addStyle(Application::APP_ID, 'sidebar-tab');
|
||||
Util::addScript(Application::APP_ID, 'sidebar-tab');
|
||||
Util::addInitScript(Application::APP_ID, 'sidebar-tab');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
:force-display-actions="true"
|
||||
:actions-aria-label="t('files_versions', 'Actions for version from {versionHumanExplicitDate}', { versionHumanExplicitDate })"
|
||||
:data-files-versions-version="version.fileVersion"
|
||||
:href="downloadURL"
|
||||
@click="click">
|
||||
<!-- Icon -->
|
||||
<template #icon>
|
||||
|
|
@ -131,8 +132,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { INode } from '@nextcloud/files'
|
||||
import type { PropType } from 'vue'
|
||||
import type { LegacyFileInfo } from '../../../files/src/services/FileInfo.ts'
|
||||
import type { Version } from '../utils/versions.ts'
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
|
@ -140,7 +141,6 @@ import { formatFileSize, Permission } from '@nextcloud/files'
|
|||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import moment from '@nextcloud/moment'
|
||||
import { join } from '@nextcloud/paths'
|
||||
import { getRootUrl } from '@nextcloud/router'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
|
|
@ -161,8 +161,8 @@ const props = defineProps({
|
|||
required: true,
|
||||
},
|
||||
|
||||
fileInfo: {
|
||||
type: Object as PropType<LegacyFileInfo>,
|
||||
node: {
|
||||
type: Object as PropType<INode>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
|
|
@ -194,8 +194,6 @@ const props = defineProps({
|
|||
|
||||
const emit = defineEmits(['click', 'compare', 'restore', 'delete', 'label-update-request'])
|
||||
|
||||
const hasPermission = (permissions: number, permission: number): boolean => (permissions & permission) !== 0
|
||||
|
||||
const previewLoaded = ref(false)
|
||||
const previewErrored = ref(false)
|
||||
const capabilities = ref(loadState('core', 'capabilities', { files: { version_labeling: false, version_deletion: false } }))
|
||||
|
|
@ -240,7 +238,7 @@ const versionHumanExplicitDate = computed(() => {
|
|||
|
||||
const downloadURL = computed(() => {
|
||||
if (props.isCurrent) {
|
||||
return getRootUrl() + join('/remote.php/webdav', props.fileInfo.path, props.fileInfo.name)
|
||||
return props.node.source
|
||||
} else {
|
||||
return getRootUrl() + props.version.url
|
||||
}
|
||||
|
|
@ -255,21 +253,21 @@ const enableDeletion = computed(() => {
|
|||
})
|
||||
|
||||
const hasDeletePermissions = computed(() => {
|
||||
return hasPermission(props.fileInfo.permissions, Permission.DELETE)
|
||||
return hasPermission(props.node, Permission.DELETE)
|
||||
})
|
||||
|
||||
const hasUpdatePermissions = computed(() => {
|
||||
return hasPermission(props.fileInfo.permissions, Permission.UPDATE)
|
||||
return hasPermission(props.node, Permission.UPDATE)
|
||||
})
|
||||
|
||||
const isDownloadable = computed(() => {
|
||||
if ((props.fileInfo.permissions & Permission.READ) === 0) {
|
||||
if ((props.node.permissions & Permission.READ) === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the mount type is a share, ensure it got download permissions.
|
||||
if (props.fileInfo.mountType === 'shared') {
|
||||
const downloadAttribute = props.fileInfo.shareAttributes
|
||||
if (props.node.attributes['mount-type'] === 'shared' && props.node.attributes['share-attributes']) {
|
||||
const downloadAttribute = JSON.parse(props.node.attributes['share-attributes'])
|
||||
.find((attribute) => attribute.scope === 'permissions' && attribute.key === 'download') || {}
|
||||
// If the download attribute is set to false, the file is not downloadable
|
||||
if (downloadAttribute?.value === false) {
|
||||
|
|
@ -281,21 +279,21 @@ const isDownloadable = computed(() => {
|
|||
})
|
||||
|
||||
/**
|
||||
*
|
||||
* Label update request
|
||||
*/
|
||||
function labelUpdate() {
|
||||
emit('label-update-request')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Restore version
|
||||
*/
|
||||
function restoreVersion() {
|
||||
emit('restore', props.version)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Delete version
|
||||
*/
|
||||
async function deleteVersion() {
|
||||
// Let @nc-vue properly remove the popover before we delete the version.
|
||||
|
|
@ -306,18 +304,20 @@ async function deleteVersion() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handle click on the version entry
|
||||
*
|
||||
* @param event - The click event
|
||||
*/
|
||||
function click() {
|
||||
if (!props.canView) {
|
||||
window.location.href = downloadURL.value
|
||||
return
|
||||
function click(event: MouseEvent) {
|
||||
if (props.canView) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
emit('click', { version: props.version })
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* If the user can compare, emit the compare event
|
||||
*/
|
||||
function compareVersion() {
|
||||
if (!props.canView) {
|
||||
|
|
@ -325,6 +325,16 @@ function compareVersion() {
|
|||
}
|
||||
emit('compare', { version: props.version })
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has the given permission on the node
|
||||
*
|
||||
* @param node - The node to check
|
||||
* @param permission - The permission to check
|
||||
*/
|
||||
function hasPermission(node: INode, permission: number): boolean {
|
||||
return (node.permissions & permission) !== 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
|||
|
|
@ -1,50 +1,46 @@
|
|||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { App, ComponentPublicInstance } from 'vue'
|
||||
|
||||
import BackupRestore from '@mdi/svg/svg/backup-restore.svg?raw'
|
||||
import { FileType, registerSidebarTab } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { createApp } from 'vue'
|
||||
import FilesVersionsSidebarTab from './views/FilesVersionsSidebarTab.vue'
|
||||
import { isPublicShare } from '@nextcloud/sharing/public'
|
||||
import { defineAsyncComponent, defineCustomElement } from 'vue'
|
||||
|
||||
// Init FilesVersions tab component
|
||||
let filesVersionsTabApp: App<Element> | null = null
|
||||
let filesVersionsTabInstance: ComponentPublicInstance<typeof FilesVersionsSidebarTab> | null = null
|
||||
const tagName = 'files-versions_sidebar-tab'
|
||||
const FilesVersionsSidebarTab = defineAsyncComponent(() => import('./views/FilesVersionsSidebarTab.vue'))
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.OCA.Files?.Sidebar === undefined) {
|
||||
registerSidebarTab({
|
||||
id: 'files_versions',
|
||||
order: 90,
|
||||
displayName: t('files_versions', 'Versions'),
|
||||
iconSvgInline: BackupRestore,
|
||||
enabled({ node }) {
|
||||
if (isPublicShare()) {
|
||||
return false
|
||||
}
|
||||
if (node.type !== FileType.File) {
|
||||
return false
|
||||
}
|
||||
// setup tab
|
||||
setupTab()
|
||||
return true
|
||||
},
|
||||
tagName,
|
||||
})
|
||||
|
||||
/**
|
||||
* Setup the custom element for the Files Versions sidebar tab.
|
||||
*/
|
||||
function setupTab() {
|
||||
if (window.customElements.get(tagName)) {
|
||||
// already defined
|
||||
return
|
||||
}
|
||||
|
||||
window.OCA.Files.Sidebar.registerTab(new window.OCA.Files.Sidebar.Tab({
|
||||
id: 'files_versions',
|
||||
name: t('files_versions', 'Versions'),
|
||||
iconSvg: BackupRestore,
|
||||
|
||||
async mount(el, fileInfo) {
|
||||
// destroy previous instance if available
|
||||
if (filesVersionsTabApp) {
|
||||
filesVersionsTabApp.unmount()
|
||||
}
|
||||
filesVersionsTabApp = createApp(FilesVersionsSidebarTab)
|
||||
filesVersionsTabInstance = filesVersionsTabApp.mount(el)
|
||||
filesVersionsTabInstance.update(fileInfo)
|
||||
},
|
||||
update(fileInfo) {
|
||||
filesVersionsTabInstance!.update(fileInfo)
|
||||
},
|
||||
setIsActive(isActive) {
|
||||
filesVersionsTabInstance?.setIsActive(isActive)
|
||||
},
|
||||
destroy() {
|
||||
filesVersionsTabApp?.unmount()
|
||||
filesVersionsTabApp = null
|
||||
},
|
||||
enabled(fileInfo) {
|
||||
return !(fileInfo?.isDirectory() ?? true)
|
||||
},
|
||||
window.customElements.define(tagName, defineCustomElement(FilesVersionsSidebarTab, {
|
||||
shadowRoot: false,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import { createClient } from 'webdav'
|
||||
|
||||
// init webdav client
|
||||
const rootPath = 'dav'
|
||||
const remote = generateRemoteUrl(rootPath)
|
||||
const client = createClient(remote)
|
||||
|
||||
/**
|
||||
* set CSRF token header
|
||||
*
|
||||
* @param token - CSRF token
|
||||
*/
|
||||
function setHeaders(token) {
|
||||
client.setHeaders({
|
||||
// Add this so the server knows it is an request from the browser
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
// Inject user auth
|
||||
requesttoken: token ?? '',
|
||||
})
|
||||
}
|
||||
|
||||
// refresh headers when request token changes
|
||||
onRequestTokenUpdate(setHeaders)
|
||||
setHeaders(getRequestToken())
|
||||
|
||||
export default client
|
||||
|
|
@ -1,18 +1,17 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable jsdoc/require-param */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { INode } from '@nextcloud/files'
|
||||
import type { FileStat, ResponseDataDetailed } from 'webdav'
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { getClient } from '@nextcloud/files/dav'
|
||||
import moment from '@nextcloud/moment'
|
||||
import { encodePath, join } from '@nextcloud/paths'
|
||||
import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
|
||||
import client from '../utils/davClient.ts'
|
||||
import davRequest from '../utils/davRequest.ts'
|
||||
import logger from '../utils/logger.ts'
|
||||
|
||||
|
|
@ -25,7 +24,7 @@ export interface Version {
|
|||
basename: string // A base name generated from the mtime
|
||||
mime: string // Empty for the current version, else the actual mime type of the version
|
||||
etag: string // Empty for the current version, else the actual mime type of the version
|
||||
size: string // Human readable size
|
||||
size: number // File size in bytes
|
||||
type: string // 'file'
|
||||
mtime: number // Version creation date as a timestamp
|
||||
permissions: string // Only readable: 'R'
|
||||
|
|
@ -35,8 +34,15 @@ export interface Version {
|
|||
fileVersion: string | null // The version id, null for the current version
|
||||
}
|
||||
|
||||
export async function fetchVersions(fileInfo: any): Promise<Version[]> {
|
||||
const path = `/versions/${getCurrentUser()?.uid}/versions/${fileInfo.id}`
|
||||
const client = getClient()
|
||||
|
||||
/**
|
||||
* Get file versions for a given node
|
||||
*
|
||||
* @param node - The node to fetch versions for
|
||||
*/
|
||||
export async function fetchVersions(node: INode): Promise<Version[]> {
|
||||
const path = `/versions/${getCurrentUser()?.uid}/versions/${node.fileid}`
|
||||
|
||||
try {
|
||||
const response = await client.getDirectoryContents(path, {
|
||||
|
|
@ -47,7 +53,7 @@ export async function fetchVersions(fileInfo: any): Promise<Version[]> {
|
|||
const versions = response.data
|
||||
// Filter out root
|
||||
.filter(({ mime }) => mime !== '')
|
||||
.map((version) => formatVersion(version, fileInfo))
|
||||
.map((version) => formatVersion(version as Required<FileStat>, node))
|
||||
|
||||
const authorIds = new Set(versions.map((version) => String(version.author)))
|
||||
const authors = await axios.post(generateUrl('/displaynames'), { users: [...authorIds] })
|
||||
|
|
@ -68,6 +74,8 @@ export async function fetchVersions(fileInfo: any): Promise<Version[]> {
|
|||
|
||||
/**
|
||||
* Restore the given version
|
||||
*
|
||||
* @param version - The version to restore
|
||||
*/
|
||||
export async function restoreVersion(version: Version) {
|
||||
try {
|
||||
|
|
@ -84,25 +92,28 @@ export async function restoreVersion(version: Version) {
|
|||
|
||||
/**
|
||||
* Format version
|
||||
*
|
||||
* @param version - The version data from WebDAV
|
||||
* @param node - The original node
|
||||
*/
|
||||
function formatVersion(version: any, fileInfo: any): Version {
|
||||
function formatVersion(version: Required<FileStat>, node: INode): Version {
|
||||
const mtime = moment(version.lastmod).unix() * 1000
|
||||
let previewUrl = ''
|
||||
|
||||
if (mtime === fileInfo.mtime) { // Version is the current one
|
||||
if (mtime === node.mtime?.getTime()) { // Version is the current one
|
||||
previewUrl = generateUrl('/core/preview?fileId={fileId}&c={fileEtag}&x=250&y=250&forceIcon=0&a=0&forceIcon=1&mimeFallback=1', {
|
||||
fileId: fileInfo.id,
|
||||
fileEtag: fileInfo.etag,
|
||||
fileId: node.fileid,
|
||||
fileEtag: node.attributes.etag,
|
||||
})
|
||||
} else {
|
||||
previewUrl = generateUrl('/apps/files_versions/preview?file={file}&version={fileVersion}&mimeFallback=1', {
|
||||
file: join(fileInfo.path, fileInfo.name),
|
||||
file: node.path,
|
||||
fileVersion: version.basename,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
fileId: fileInfo.id,
|
||||
fileId: node.fileid!.toString(),
|
||||
// If version-label is defined make sure it is a string (prevent issue if the label is a number an PHP returns a number then)
|
||||
label: version.props['version-label'] ? String(version.props['version-label']) : '',
|
||||
author: version.props['version-author'] ? String(version.props['version-author']) : null,
|
||||
|
|
@ -122,6 +133,12 @@ function formatVersion(version: any, fileInfo: any): Version {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set version label
|
||||
*
|
||||
* @param version - The version to set the label for
|
||||
* @param newLabel - The new label
|
||||
*/
|
||||
export async function setVersionLabel(version: Version, newLabel: string) {
|
||||
return await client.customRequest(
|
||||
version.filename,
|
||||
|
|
@ -142,6 +159,11 @@ export async function setVersionLabel(version: Version, newLabel: string) {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete version
|
||||
*
|
||||
* @param version - The version to delete
|
||||
*/
|
||||
export async function deleteVersion(version: Version) {
|
||||
await client.deleteFile(version.filename)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<div v-if="fileInfo !== null" class="versions-tab__container">
|
||||
<div v-if="node" class="versions-tab__container">
|
||||
<VirtualScrolling
|
||||
:sections="sections"
|
||||
:header-height="0">
|
||||
|
|
@ -17,8 +17,8 @@
|
|||
:can-compare="canCompare"
|
||||
:load-preview="isActive"
|
||||
:version="row.items[0].version"
|
||||
:file-info="fileInfo"
|
||||
:is-current="row.items[0].version.mtime === fileInfo.mtime"
|
||||
:node="node"
|
||||
:is-current="row.items[0].version.mtime === currentVersionMtime"
|
||||
:is-first-version="row.items[0].version.mtime === initialVersionMtime"
|
||||
@click="openVersion"
|
||||
@compare="compareVersion"
|
||||
|
|
@ -41,16 +41,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { LegacyFileInfo } from '../../../files/src/services/FileInfo.ts'
|
||||
import type { IFolder, INode, IView } from '@nextcloud/files'
|
||||
import type { Version } from '../utils/versions.ts'
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile'
|
||||
import path from 'path'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import VersionEntry from '../components/VersionEntry.vue'
|
||||
import VersionLabelDialog from '../components/VersionLabelDialog.vue'
|
||||
|
|
@ -58,28 +56,49 @@ import VirtualScrolling from '../components/VirtualScrolling.vue'
|
|||
import logger from '../utils/logger.ts'
|
||||
import { deleteVersion, fetchVersions, restoreVersion, setVersionLabel } from '../utils/versions.ts'
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
const props = defineProps<{
|
||||
node?: INode
|
||||
folder?: IFolder
|
||||
view?: IView
|
||||
}>()
|
||||
|
||||
const fileInfo = ref<LegacyFileInfo | null>(null)
|
||||
defineExpose({ setActive })
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
const isActive = ref<boolean>(false)
|
||||
const versions = ref<Version[]>([])
|
||||
const loading = ref(false)
|
||||
const showVersionLabelForm = ref(false)
|
||||
const editedVersion = ref<Version | null>(null)
|
||||
|
||||
watch(toRef(() => props.node), async () => {
|
||||
if (!props.node) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
versions.value = await fetchVersions(props.node)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const currentVersionMtime = computed(() => props.node?.mtime?.getTime() ?? 0)
|
||||
|
||||
/**
|
||||
* Order versions by mtime.
|
||||
* Put the current version at the top.
|
||||
*/
|
||||
const orderedVersions = computed(() => {
|
||||
return [...versions.value].sort((a, b) => {
|
||||
if (fileInfo.value === null) {
|
||||
if (!props.node) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (a.mtime === fileInfo.value.mtime) {
|
||||
if (a.mtime === props.node.mtime?.getTime()) {
|
||||
return -1
|
||||
} else if (b.mtime === fileInfo.value.mtime) {
|
||||
} else if (b.mtime === props.node.mtime?.getTime()) {
|
||||
return 1
|
||||
} else {
|
||||
return b.mtime - a.mtime
|
||||
|
|
@ -88,7 +107,12 @@ const orderedVersions = computed(() => {
|
|||
})
|
||||
|
||||
const sections = computed(() => {
|
||||
const rows = orderedVersions.value.map((version) => ({ key: version.mtime.toString(), height: 68, sectionKey: 'versions', items: [{ id: version.mtime.toString(), version }] }))
|
||||
const rows = orderedVersions.value.map((version) => ({
|
||||
key: version.mtime.toString(),
|
||||
height: 68,
|
||||
sectionKey: 'versions',
|
||||
items: [{ id: version.mtime.toString(), version }],
|
||||
}))
|
||||
return [{ key: 'versions', rows, height: 68 * orderedVersions.value.length }]
|
||||
})
|
||||
|
||||
|
|
@ -101,82 +125,26 @@ const initialVersionMtime = computed(() => {
|
|||
.reduce((a, b) => Math.min(a, b))
|
||||
})
|
||||
|
||||
const viewerFileInfo = computed(() => {
|
||||
if (fileInfo.value === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// We need to remap bitmask to dav permissions as the file info we have is converted through client.js
|
||||
let davPermissions = ''
|
||||
if (fileInfo.value.permissions & 1) {
|
||||
davPermissions += 'R'
|
||||
}
|
||||
if (fileInfo.value.permissions & 2) {
|
||||
davPermissions += 'W'
|
||||
}
|
||||
if (fileInfo.value.permissions & 8) {
|
||||
davPermissions += 'D'
|
||||
}
|
||||
return {
|
||||
...fileInfo.value,
|
||||
mime: fileInfo.value.mimetype,
|
||||
basename: fileInfo.value.name,
|
||||
filename: fileInfo.value.path + '/' + fileInfo.value.name,
|
||||
permissions: davPermissions,
|
||||
fileid: fileInfo.value.id,
|
||||
}
|
||||
})
|
||||
|
||||
const canView = computed(() => {
|
||||
if (fileInfo.value === null) {
|
||||
if (!props.node) {
|
||||
return false
|
||||
}
|
||||
|
||||
return window.OCA.Viewer?.mimetypesCompare?.includes(fileInfo.value.mimetype)
|
||||
return window.OCA.Viewer?.mimetypes?.includes(props.node?.mime)
|
||||
})
|
||||
|
||||
const canCompare = computed(() => {
|
||||
return !isMobile.value
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
subscribe('files_versions:restore:restored', fetchVersions)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unsubscribe('files_versions:restore:restored', fetchVersions)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
/**
|
||||
* Update current fileInfo and fetch new data
|
||||
*
|
||||
* @param _fileInfo the current file FileInfo
|
||||
*/
|
||||
async update(_fileInfo: LegacyFileInfo) {
|
||||
fileInfo.value = _fileInfo
|
||||
resetState()
|
||||
internalFetchVersions()
|
||||
},
|
||||
|
||||
/**
|
||||
* @param _isActive whether the tab is active
|
||||
*/
|
||||
async setIsActive(_isActive: boolean) {
|
||||
isActive.value = _isActive
|
||||
},
|
||||
&& window.OCA.Viewer?.mimetypesCompare?.includes(props.node?.mime)
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the existing versions infos
|
||||
* This method is called by the files app if the sidebar tab state changes.
|
||||
*
|
||||
* @param active - The new active state
|
||||
*/
|
||||
async function internalFetchVersions() {
|
||||
try {
|
||||
loading.value = true
|
||||
versions.value = await fetchVersions(fileInfo.value)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
function setActive(active: boolean) {
|
||||
isActive.value = active
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -185,17 +153,19 @@ async function internalFetchVersions() {
|
|||
* @param version The version to restore
|
||||
*/
|
||||
async function handleRestore(version: Version) {
|
||||
// Update local copy of fileInfo as rendering depends on it.
|
||||
const oldFileInfo = fileInfo.value
|
||||
fileInfo.value = {
|
||||
...fileInfo.value,
|
||||
size: version.size,
|
||||
mtime: version.mtime,
|
||||
if (!props.node) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update local copy of fileInfo as rendering depends on it.
|
||||
const restoredNode = props.node.clone()
|
||||
restoredNode.attributes.etag = version.etag
|
||||
restoredNode.size = version.size
|
||||
restoredNode.mtime = new Date(version.mtime)
|
||||
|
||||
const restoreStartedEventState = {
|
||||
preventDefault: false,
|
||||
fileInfo: fileInfo.value,
|
||||
node: restoredNode,
|
||||
version,
|
||||
}
|
||||
emit('files_versions:restore:requested', restoreStartedEventState)
|
||||
|
|
@ -212,9 +182,9 @@ async function handleRestore(version: Version) {
|
|||
} else {
|
||||
showSuccess(t('files_versions', 'Version restored'))
|
||||
}
|
||||
emit('files_versions:restore:restored', version)
|
||||
emit('files:node:updated', restoredNode)
|
||||
emit('files_versions:restore:restored', { node: restoredNode, version })
|
||||
} catch {
|
||||
fileInfo.value = oldFileInfo
|
||||
showError(t('files_versions', 'Could not restore version'))
|
||||
emit('files_versions:restore:failed', version)
|
||||
}
|
||||
|
|
@ -271,25 +241,18 @@ async function handleDelete(version: Version) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the current view to its default state
|
||||
*/
|
||||
function resetState() {
|
||||
versions.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* @param payload - The event payload
|
||||
* @param payload.version - The version to open
|
||||
*/
|
||||
function openVersion({ version }: { version: Version }) {
|
||||
if (fileInfo.value === null) {
|
||||
if (props.node === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Open current file view instead of read only
|
||||
if (version.mtime === fileInfo.value.mtime) {
|
||||
window.OCA.Viewer.open({ fileInfo: viewerFileInfo.value })
|
||||
if (version.mtime === props.node?.mtime?.getTime()) {
|
||||
window.OCA.Viewer.open({ path: props.node.path })
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -298,7 +261,7 @@ function openVersion({ version }: { version: Version }) {
|
|||
...version,
|
||||
// Versions previews are too small for our use case, so we override previewUrl
|
||||
// to either point to the original file or original version.
|
||||
filename: version.mtime === fileInfo.value.mtime ? path.join('files', getCurrentUser()?.uid ?? '', fileInfo.value.path, fileInfo.value.name) : version.filename,
|
||||
filename: version.filename,
|
||||
previewUrl: undefined,
|
||||
},
|
||||
enableSidebar: false,
|
||||
|
|
@ -312,7 +275,10 @@ function openVersion({ version }: { version: Version }) {
|
|||
function compareVersion({ version }: { version: Version }) {
|
||||
const _versions = versions.value.map((version) => ({ ...version, previewUrl: undefined }))
|
||||
|
||||
window.OCA.Viewer.compare(viewerFileInfo.value, _versions.find((v) => v.source === version.source))
|
||||
window.OCA.Viewer.compare(
|
||||
{ path: props.node!.path },
|
||||
_versions.find((v) => v.source === version.source),
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { INode } from '@nextcloud/files'
|
||||
import type { PropType } from 'vue'
|
||||
import type { Tag, TagWithId } from '../types.ts'
|
||||
|
||||
|
|
@ -216,11 +216,13 @@ export default defineComponent({
|
|||
|
||||
props: {
|
||||
nodes: {
|
||||
type: Array as PropType<Node[]>,
|
||||
type: Array as PropType<INode[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
setup() {
|
||||
return {
|
||||
emit,
|
||||
|
|
@ -381,7 +383,7 @@ export default defineComponent({
|
|||
})
|
||||
|
||||
// Efficient way of counting tags and their occurrences
|
||||
this.tagList = this.nodes.reduce((acc: TagListCount, node: Node) => {
|
||||
this.tagList = this.nodes.reduce((acc: TagListCount, node: INode) => {
|
||||
const tags = getNodeSystemTags(node) || []
|
||||
tags.forEach((tag) => {
|
||||
acc[tag] = (acc[tag] || 0) + 1
|
||||
|
|
@ -531,7 +533,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
const nodes = [] as Node[]
|
||||
const nodes = [] as INode[]
|
||||
|
||||
// Update nodes
|
||||
this.toAdd.forEach((tag) => {
|
||||
|
|
|
|||
|
|
@ -35,11 +35,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { INode } from '@nextcloud/files'
|
||||
import type { Tag, TagWithId } from '../types.js'
|
||||
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { emit, subscribe } from '@nextcloud/event-bus'
|
||||
import { getSidebar } from '@nextcloud/files'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
|
|
@ -226,7 +227,7 @@ export default Vue.extend({
|
|||
this.updateAndDispatchNodeTagsEvent(this.fileId)
|
||||
},
|
||||
|
||||
async onTagUpdated(node: Node) {
|
||||
async onTagUpdated(node: INode) {
|
||||
if (node.fileid !== this.fileId) {
|
||||
return
|
||||
}
|
||||
|
|
@ -243,7 +244,8 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
async updateAndDispatchNodeTagsEvent(fileId: number) {
|
||||
const path = window.OCA?.Files?.Sidebar?.file || ''
|
||||
const sidebar = getSidebar()
|
||||
const path = sidebar.node?.path ?? ''
|
||||
try {
|
||||
const node = await fetchNode(path)
|
||||
if (node) {
|
||||
|
|
|
|||
4
apps/systemtags/src/event-bus.d.ts
vendored
4
apps/systemtags/src/event-bus.d.ts
vendored
|
|
@ -3,12 +3,12 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { INode } from '@nextcloud/files'
|
||||
import type { TagWithId } from './types.ts'
|
||||
|
||||
declare module '@nextcloud/event-bus' {
|
||||
interface NextcloudEvents {
|
||||
'systemtags:node:updated': Node
|
||||
'systemtags:node:updated': INode
|
||||
'systemtags:tag:deleted': TagWithId
|
||||
'systemtags:tag:updated': TagWithId
|
||||
'systemtags:tag:created': TagWithId
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { INode } from '@nextcloud/files'
|
||||
|
||||
import TagMultipleSvg from '@mdi/svg/svg/tag-multiple-outline.svg?raw'
|
||||
import { FileAction, Permission } from '@nextcloud/files'
|
||||
|
|
@ -18,7 +18,7 @@ import { defineAsyncComponent } from 'vue'
|
|||
* @param nodes Nodes to modify tags for
|
||||
* @param nodes.nodes
|
||||
*/
|
||||
async function execBatch({ nodes }: { nodes: Node[] }): Promise<(null | boolean)[]> {
|
||||
async function execBatch({ nodes }: { nodes: INode[] }): Promise<(null | boolean)[]> {
|
||||
const response = await new Promise<null | boolean>((resolve) => {
|
||||
spawnDialog(defineAsyncComponent(() => import('../components/SystemTagPicker.vue')), {
|
||||
nodes,
|
||||
|
|
|
|||
37
apps/systemtags/src/files_actions/filesSidebarAction.ts
Normal file
37
apps/systemtags/src/files_actions/filesSidebarAction.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import tagSvg from '@mdi/svg/svg/tag-outline.svg?raw'
|
||||
import { registerSidebarAction } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { spawnDialog } from '@nextcloud/vue/functions/dialog'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
/**
|
||||
* Register the "Add tags" action in the file sidebar
|
||||
*/
|
||||
export function registerFileSidebarAction() {
|
||||
registerSidebarAction({
|
||||
id: 'systemtags',
|
||||
order: 20,
|
||||
displayName() {
|
||||
return t('systemtags', 'Add tags')
|
||||
},
|
||||
enabled() {
|
||||
return true
|
||||
},
|
||||
iconSvgInline() {
|
||||
return tagSvg
|
||||
},
|
||||
onClick({ node }) {
|
||||
return spawnDialog(
|
||||
defineAsyncComponent(() => import('../components/SystemTagPicker.vue')),
|
||||
{
|
||||
nodes: [node],
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { registerFileAction } from '@nextcloud/files'
|
||||
import { registerDavProperty } from '@nextcloud/files/dav'
|
||||
import { action as bulkSystemTagsAction } from './files_actions/bulkSystemTagsAction.ts'
|
||||
import { registerFileSidebarAction } from './files_actions/filesSidebarAction.ts'
|
||||
import { action as inlineSystemTagsAction } from './files_actions/inlineSystemTagsAction.ts'
|
||||
import { action as openInFilesAction } from './files_actions/openInFilesAction.ts'
|
||||
import { registerSystemTagsView } from './files_views/systemtagsView.ts'
|
||||
|
|
@ -16,6 +18,7 @@ registerFileAction(inlineSystemTagsAction)
|
|||
registerFileAction(openInFilesAction)
|
||||
|
||||
registerSystemTagsView()
|
||||
registerFileSidebarAction()
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
registerHotkeys()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { INode } from '@nextcloud/files'
|
||||
import type { DAVResultResponseProps } from 'webdav'
|
||||
import type { BaseTag, ServerTag, Tag, TagWithId } from './types.js'
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ export function formatTag(initialTag: Tag | ServerTag): ServerTag {
|
|||
*
|
||||
* @param node
|
||||
*/
|
||||
export function getNodeSystemTags(node: Node): string[] {
|
||||
export function getNodeSystemTags(node: INode): string[] {
|
||||
const attribute = node.attributes?.['system-tags']?.['system-tag']
|
||||
if (attribute === undefined) {
|
||||
return []
|
||||
|
|
@ -92,7 +92,7 @@ export function getNodeSystemTags(node: Node): string[] {
|
|||
* @param node
|
||||
* @param tags
|
||||
*/
|
||||
export function setNodeSystemTags(node: Node, tags: string[]): void {
|
||||
export function setNodeSystemTags(node: INode, tags: string[]): void {
|
||||
Vue.set(node.attributes, 'system-tags', {
|
||||
'system-tag': tags,
|
||||
})
|
||||
|
|
|
|||
501
build/frontend-legacy/package-lock.json
generated
501
build/frontend-legacy/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@
|
|||
"scripts": {
|
||||
"build": "webpack --node-env production --progress",
|
||||
"dev": "webpack --node-env development --progress",
|
||||
"postinstall": "patch-package",
|
||||
"lint": "eslint --suppressions-location ../eslint-baseline-legacy.json --no-error-on-unmatched-pattern ./apps/*/ ./core/",
|
||||
"lint:fix": "eslint --suppressions-location ../eslint-baseline-legacy.json --fix --no-error-on-unmatched-pattern ./apps/*/ ./core/",
|
||||
"test": "vitest run",
|
||||
|
|
@ -124,6 +125,7 @@
|
|||
"handlebars-loader": "^1.7.3",
|
||||
"mime": "^4.1.0",
|
||||
"msw": "^2.12.6",
|
||||
"patch-package": "^8.0.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"regextras": "^0.8.0",
|
||||
"sass": "^1.97.1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# SPDX-License-Identifier: MIT
|
||||
# SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
|
||||
#
|
||||
# https://github.com/vuejs/vue-web-component-wrapper/pull/133
|
||||
# https://github.com/vuejs/vue-web-component-wrapper/pull/122
|
||||
#
|
||||
diff --git a/node_modules/@vue/web-component-wrapper/dist/vue-wc-wrapper.js b/node_modules/@vue/web-component-wrapper/dist/vue-wc-wrapper.js
|
||||
index 721bf1f..adba8e8 100644
|
||||
--- a/node_modules/@vue/web-component-wrapper/dist/vue-wc-wrapper.js
|
||||
+++ b/node_modules/@vue/web-component-wrapper/dist/vue-wc-wrapper.js
|
||||
@@ -222,10 +222,17 @@ function wrap (Vue, Component) {
|
||||
if (!wrapper._isMounted) {
|
||||
// initialize attributes
|
||||
const syncInitialAttributes = () => {
|
||||
- wrapper.props = getInitialProps(camelizedPropsList);
|
||||
+ wrapper.props = Object.assign(
|
||||
+ getInitialProps(camelizedPropsList),
|
||||
+ wrapper.props
|
||||
+ )
|
||||
hyphenatedPropsList.forEach(key => {
|
||||
- syncAttribute(this, key);
|
||||
- });
|
||||
+ const camelized = camelize(key)
|
||||
+ // Maybe setted by Element properties earler
|
||||
+ if (typeof wrapper.props[camelized] === 'undefined' || this.hasAttribute(key)) {
|
||||
+ syncAttribute(this, key)
|
||||
+ }
|
||||
+ })
|
||||
};
|
||||
|
||||
if (isInitialized) {
|
||||
diff --git a/node_modules/@vue/web-component-wrapper/types/index.d.ts b/node_modules/@vue/web-component-wrapper/types/index.d.ts
|
||||
index 8b67b7b..612102c 100644
|
||||
--- a/node_modules/@vue/web-component-wrapper/types/index.d.ts
|
||||
+++ b/node_modules/@vue/web-component-wrapper/types/index.d.ts
|
||||
@@ -3,6 +3,6 @@ import _Vue, { Component, AsyncComponent } from 'vue'
|
||||
declare function wrap(
|
||||
Vue: typeof _Vue,
|
||||
Component: Component | AsyncComponent
|
||||
-): HTMLElement
|
||||
+): CustomElementConstructor
|
||||
|
||||
export default wrap
|
||||
|
|
@ -15,7 +15,6 @@ const WebpackSPDXPlugin = require('./WebpackSPDXPlugin.cjs')
|
|||
|
||||
const appVersion = readFileSync(path.join(__dirname, '../../version.php')).toString().match(/OC_Version.+\[([0-9]{2})/)?.[1] ?? 'unknown'
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const isTesting = process.env.TESTING === 'true'
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -87,7 +86,30 @@ const config = {
|
|||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
oneOf: [
|
||||
{
|
||||
resourceQuery: /module/,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
namedExport: false,
|
||||
localIdentName: '_[local]_[hash:base64:5]',
|
||||
exportLocalsConvention: 'asIs',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const path = require('path')
|
|||
module.exports = {
|
||||
comments: {
|
||||
'comments-app': path.join(__dirname, 'apps/comments/src', 'comments-app.js'),
|
||||
'comments-tab': path.join(__dirname, 'apps/comments/src', 'comments-tab.js'),
|
||||
'comments-tab': path.join(__dirname, 'apps/comments/src', 'files-sidebar.ts'),
|
||||
init: path.join(__dirname, 'apps/comments/src', 'init.ts'),
|
||||
},
|
||||
core: {
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ export default defineConfig({
|
|||
viewportWidth: 1280,
|
||||
viewportHeight: 720,
|
||||
|
||||
// Tries again 2 more times on failure
|
||||
// Tries again when in run mode (cypress run) e.g. on CI
|
||||
retries: {
|
||||
runMode: 2,
|
||||
runMode: 3,
|
||||
// do not retry in `cypress open`
|
||||
openMode: 0,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ export const getActionButtonForFile = (filename: string) => getActionsForFile(fi
|
|||
export function getActionEntryForFileId(fileid: number, actionId: string) {
|
||||
return getActionButtonForFileId(fileid)
|
||||
.should('have.attr', 'aria-controls')
|
||||
.then((menuId) => cy.get(`#${menuId}`).find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`))
|
||||
.then((menuId) => cy.get(`#${menuId}`)
|
||||
.should('exist')
|
||||
.find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -35,7 +37,9 @@ export function getActionEntryForFileId(fileid: number, actionId: string) {
|
|||
export function getActionEntryForFile(file: string, actionId: string) {
|
||||
return getActionButtonForFile(file)
|
||||
.should('have.attr', 'aria-controls')
|
||||
.then((menuId) => cy.get(`#${menuId}`).find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`))
|
||||
.then((menuId) => cy.get(`#${menuId}`)
|
||||
.should('exist')
|
||||
.find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,9 +69,8 @@ export function getInlineActionEntryForFile(file: string, actionId: string) {
|
|||
*/
|
||||
export function triggerActionForFileId(fileid: number, actionId: string) {
|
||||
getActionButtonForFileId(fileid)
|
||||
.as('actionButton')
|
||||
.scrollIntoView()
|
||||
cy.get('@actionButton')
|
||||
getActionButtonForFileId(fileid)
|
||||
.click({ force: true }) // force to avoid issues with overlaying file list header
|
||||
getActionEntryForFileId(fileid, actionId)
|
||||
.find('button')
|
||||
|
|
@ -82,9 +85,8 @@ export function triggerActionForFileId(fileid: number, actionId: string) {
|
|||
*/
|
||||
export function triggerActionForFile(filename: string, actionId: string) {
|
||||
getActionButtonForFile(filename)
|
||||
.as('actionButton')
|
||||
.scrollIntoView()
|
||||
cy.get('@actionButton')
|
||||
getActionButtonForFile(filename)
|
||||
.click({ force: true }) // force to avoid issues with overlaying file list header
|
||||
getActionEntryForFile(filename, actionId)
|
||||
.find('button')
|
||||
|
|
@ -281,11 +283,23 @@ export function navigateToFolder(dirPath: string) {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Close the sidebar
|
||||
*/
|
||||
export function closeSidebar() {
|
||||
// {force: true} as it might be hidden behind toasts
|
||||
cy.get('[data-cy-sidebar] .app-sidebar__close').click({ force: true })
|
||||
cy.get('[data-cy-sidebar] .app-sidebar__close')
|
||||
.click({ force: true })
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.should('not.be.visible')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting -- wait for the animation to finish
|
||||
cy.wait(500)
|
||||
cy.url()
|
||||
.should('not.contain', 'opendetails')
|
||||
// close all toasts
|
||||
cy.get('.toast-success')
|
||||
.if()
|
||||
.findAllByRole('button')
|
||||
.click({ force: true, multiple: true })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { getActionButtonForFile, getRowForFile, triggerActionForFile } from './FilesUtils.ts'
|
||||
import { closeSidebar, getActionButtonForFile, getRowForFile, triggerActionForFile } from './FilesUtils.ts'
|
||||
|
||||
describe('files: Favorites', { testIsolation: true }, () => {
|
||||
let user: User
|
||||
|
|
@ -110,29 +110,54 @@ describe('files: Favorites', { testIsolation: true }, () => {
|
|||
.contains('new folder')
|
||||
.should('not.exist')
|
||||
|
||||
cy.intercept('PROPPATCH', '**/remote.php/dav/files/*/new%20folder').as('addToFavorites')
|
||||
cy.intercept('POST', '**/apps/files/api/v1/files/new%20folder').as('addToFavorites')
|
||||
// open sidebar
|
||||
triggerActionForFile('new folder', 'details')
|
||||
// open actions
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.should('be.visible')
|
||||
|
||||
// open sidebar actions
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.findByRole('button', { name: 'Actions' })
|
||||
.click()
|
||||
// trigger menu button
|
||||
cy.findAllByRole('menu')
|
||||
.findByRole('menuitem', { name: 'Add to favorites' })
|
||||
.findByRole('menuitem', { name: 'Favorite' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
cy.wait('@addToFavorites')
|
||||
|
||||
// close sidebar
|
||||
closeSidebar()
|
||||
|
||||
// See favorites star
|
||||
getRowForFile('new folder')
|
||||
.findByRole('img', { name: 'Favorite' })
|
||||
.should('be.visible')
|
||||
|
||||
// See folder in navigation
|
||||
cy.get('[data-cy-files-navigation-item="favorites"]')
|
||||
cy.reload()
|
||||
getRowForFile('new folder')
|
||||
.should('be.visible')
|
||||
.contains('new folder')
|
||||
.should('exist')
|
||||
|
||||
// can unfavorite
|
||||
triggerActionForFile('new folder', 'details')
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.should('be.visible')
|
||||
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.findByRole('button', { name: 'Actions' })
|
||||
.click()
|
||||
// trigger menu button
|
||||
cy.findAllByRole('menu')
|
||||
.findByRole('menuitem', { name: 'Unfavorite' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.wait('@addToFavorites')
|
||||
closeSidebar()
|
||||
|
||||
getRowForFile('new folder')
|
||||
.findByRole('img', { name: 'Favorite' })
|
||||
.should('not.exist')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ describe('Files: Sidebar', { testIsolation: true }, () => {
|
|||
triggerActionForFile('other', 'delete')
|
||||
cy.wait('@deleteFile')
|
||||
|
||||
cy.get('[data-cy-sidebar]').should('not.exist')
|
||||
cy.get('[data-cy-sidebar]').should('not.be.visible')
|
||||
// Ensure the URL is changed
|
||||
cy.url().should('not.contain', `apps/files/files/${otherFileId}`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { triggerActionForFile } from '../files/FilesUtils.ts'
|
||||
import { closeSidebar, triggerActionForFile } from '../files/FilesUtils.ts'
|
||||
|
||||
export interface ShareSetting {
|
||||
read: boolean
|
||||
|
|
@ -18,6 +18,7 @@ export interface ShareSetting {
|
|||
|
||||
export function createShare(fileName: string, username: string, shareSettings: Partial<ShareSetting> = {}) {
|
||||
openSharingPanel(fileName)
|
||||
cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
|
||||
|
||||
cy.get('#app-sidebar-vue').within(() => {
|
||||
cy.intercept({ times: 1, method: 'GET', url: '**/apps/files_sharing/api/v1/sharees?*' }).as('userSearch')
|
||||
|
|
@ -30,13 +31,20 @@ export function createShare(fileName: string, username: string, shareSettings: P
|
|||
|
||||
// HACK: Save the share and then update it, as permissions changes are currently not saved for new share.
|
||||
cy.get('[data-cy-files-sharing-share-editor-action="save"]').click({ scrollBehavior: 'nearest' })
|
||||
cy.wait('@createShare')
|
||||
closeSidebar()
|
||||
|
||||
updateShare(fileName, 0, shareSettings)
|
||||
}
|
||||
|
||||
export function openSharingDetails(index: number) {
|
||||
cy.get('#app-sidebar-vue').within(() => {
|
||||
cy.get('[data-cy-files-sharing-share-actions]').eq(index).click({ force: true })
|
||||
cy.get('[data-cy-files-sharing-share-permissions-bundle="custom"]').click()
|
||||
cy.findAllByRole('button', { name: /open sharing details/i })
|
||||
.should('have.length.at.least', index + 1)
|
||||
.eq(index)
|
||||
.click({ force: true })
|
||||
cy.get('[data-cy-files-sharing-share-permissions-bundle="custom"]')
|
||||
.click()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -51,10 +59,16 @@ export function updateShare(fileName: string, index: number, shareSettings: Part
|
|||
cy.get('[data-cy-files-sharing-share-permissions-checkbox="download"]').find('input').as('downloadCheckbox')
|
||||
if (shareSettings.download) {
|
||||
// Force:true because the checkbox is hidden by the pretty UI.
|
||||
cy.get('@downloadCheckbox').check({ force: true, scrollBehavior: 'nearest' })
|
||||
cy.get('@downloadCheckbox')
|
||||
.check({ force: true, scrollBehavior: 'nearest' })
|
||||
cy.get('@downloadCheckbox')
|
||||
.should('be.checked')
|
||||
} else {
|
||||
// Force:true because the checkbox is hidden by the pretty UI.
|
||||
cy.get('@downloadCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' })
|
||||
cy.get('@downloadCheckbox')
|
||||
.uncheck({ force: true, scrollBehavior: 'nearest' })
|
||||
cy.get('@downloadCheckbox')
|
||||
.should('not.be.checked')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,14 +132,16 @@ export function updateShare(fileName: string, index: number, shareSettings: Part
|
|||
|
||||
cy.wait('@updateShare')
|
||||
})
|
||||
// close all toasts
|
||||
cy.get('.toast-success').findAllByRole('button').click({ force: true, multiple: true })
|
||||
closeSidebar()
|
||||
}
|
||||
|
||||
export function openSharingPanel(fileName: string) {
|
||||
triggerActionForFile(fileName, 'details')
|
||||
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.as('sidebar')
|
||||
.should('be.visible')
|
||||
cy.get('@sidebar')
|
||||
.find('[aria-controls="tab-sharing"]')
|
||||
.click()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,11 +90,13 @@ describe('files_sharing: Expiry date', () => {
|
|||
prepareDirectory(dir)
|
||||
updateShare(dir, 0, { expiryDate: fortnight })
|
||||
validateExpiryDate(dir, fortnightString)
|
||||
|
||||
closeSidebar()
|
||||
|
||||
cy.log('Upadate share and validate expiry date is kept')
|
||||
updateShare(dir, 0, { note: 'Only note changed' })
|
||||
validateExpiryDate(dir, fortnightString)
|
||||
|
||||
cy.log('Reload page and validate expiry date is kept')
|
||||
cy.visit('/apps/files')
|
||||
validateExpiryDate(dir, fortnightString)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ describe('Limit to sharing to people in the same group', () => {
|
|||
cy.createRandomUser()
|
||||
.then((user) => {
|
||||
alice = user
|
||||
cy.createRandomUser()
|
||||
})
|
||||
cy.createRandomUser()
|
||||
.then((user) => {
|
||||
bob = user
|
||||
|
||||
|
|
@ -49,9 +49,12 @@ describe('Limit to sharing to people in the same group', () => {
|
|||
cy.login(alice)
|
||||
cy.visit('/apps/files')
|
||||
createShare(randomFileName1, bob.userId)
|
||||
cy.logout()
|
||||
|
||||
cy.login(bob)
|
||||
cy.visit('/apps/files')
|
||||
createShare(randomFileName2, alice.userId)
|
||||
cy.logout()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import type { ShareSetting } from '../files_sharing/FilesSharingUtils.ts'
|
||||
|
||||
import { basename } from '@nextcloud/paths'
|
||||
import { triggerActionForFile } from '../files/FilesUtils.ts'
|
||||
import { createShare } from '../files_sharing/FilesSharingUtils.ts'
|
||||
|
||||
export function uploadThreeVersions(user: User, fileName: string) {
|
||||
|
|
@ -24,11 +26,13 @@ export function openVersionsPanel(fileName: string) {
|
|||
// Detect the versions list fetch
|
||||
cy.intercept('PROPFIND', '**/dav/versions/*/versions/**').as('getVersions')
|
||||
|
||||
// Open the versions tab
|
||||
cy.window().then((win) => {
|
||||
win.OCA.Files.Sidebar.setActiveTab('files_versions')
|
||||
win.OCA.Files.Sidebar.open(`/${fileName}`)
|
||||
})
|
||||
triggerActionForFile(basename(fileName), 'details')
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.as('sidebar')
|
||||
.should('be.visible')
|
||||
cy.get('@sidebar')
|
||||
.find('[aria-controls="tab-files_versions"]')
|
||||
.click()
|
||||
|
||||
// Wait for the versions list to be fetched
|
||||
cy.wait('@getVersions')
|
||||
|
|
@ -85,6 +89,7 @@ export function setupTestSharedFileFromUser(owner: User, randomFileName: string,
|
|||
cy.login(owner)
|
||||
cy.visit('/apps/files')
|
||||
createShare(randomFileName, recipient.userId, shareOptions)
|
||||
|
||||
cy.login(recipient)
|
||||
cy.visit('/apps/files')
|
||||
return cy.wrap(recipient)
|
||||
|
|
|
|||
|
|
@ -6,95 +6,56 @@
|
|||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { randomString } from '../../support/utils/randomString.ts'
|
||||
import { getRowForFile, navigateToFolder } from '../files/FilesUtils.ts'
|
||||
import { navigateToFolder } from '../files/FilesUtils.ts'
|
||||
import { deleteVersion, doesNotHaveAction, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts'
|
||||
|
||||
describe('Versions restoration', () => {
|
||||
describe('Versions deletion', () => {
|
||||
const folderName = 'shared_folder'
|
||||
const randomFileName = randomString(10) + '.txt'
|
||||
const randomFilePath = `/${folderName}/${randomFileName}`
|
||||
let user: User
|
||||
let versionCount = 0
|
||||
|
||||
before(() => {
|
||||
beforeEach(() => {
|
||||
cy.createRandomUser()
|
||||
.then((_user) => {
|
||||
user = _user
|
||||
cy.mkdir(user, `/${folderName}`)
|
||||
uploadThreeVersions(user, randomFilePath)
|
||||
uploadThreeVersions(user, randomFilePath)
|
||||
versionCount = 6
|
||||
versionCount = 3
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
navigateToFolder(folderName)
|
||||
openVersionsPanel(randomFilePath)
|
||||
})
|
||||
})
|
||||
|
||||
it('Delete initial version', () => {
|
||||
navigateToFolder(folderName)
|
||||
openVersionsPanel(randomFilePath)
|
||||
|
||||
cy.get('[data-files-versions-version]')
|
||||
.should('have.length', versionCount)
|
||||
deleteVersion(--versionCount)
|
||||
cy.get('[data-files-versions-version]')
|
||||
.should('have.length', versionCount)
|
||||
})
|
||||
|
||||
it('Delete versions of shared file with delete permission', () => {
|
||||
setupTestSharedFileFromUser(user, folderName, { delete: true })
|
||||
navigateToFolder(folderName)
|
||||
openVersionsPanel(randomFilePath)
|
||||
|
||||
cy.get('[data-files-versions-version]').should('have.length', versionCount)
|
||||
deleteVersion(2)
|
||||
versionCount--
|
||||
deleteVersion(--versionCount)
|
||||
cy.get('[data-files-versions-version]').should('have.length', versionCount)
|
||||
})
|
||||
|
||||
context('Delete versions of shared file', () => {
|
||||
it('Works with delete permission', () => {
|
||||
setupTestSharedFileFromUser(user, folderName, { delete: true })
|
||||
navigateToFolder(folderName)
|
||||
openVersionsPanel(randomFilePath)
|
||||
it('Delete versions of shared file without delete permission', () => {
|
||||
setupTestSharedFileFromUser(user, folderName, { delete: false })
|
||||
navigateToFolder(folderName)
|
||||
openVersionsPanel(randomFilePath)
|
||||
|
||||
cy.get('[data-files-versions-version]').should('have.length', versionCount)
|
||||
deleteVersion(2)
|
||||
versionCount--
|
||||
cy.get('[data-files-versions-version]').should('have.length', versionCount)
|
||||
})
|
||||
|
||||
it('Does not work without delete permission', () => {
|
||||
setupTestSharedFileFromUser(user, folderName, { delete: false })
|
||||
navigateToFolder(folderName)
|
||||
openVersionsPanel(randomFilePath)
|
||||
|
||||
doesNotHaveAction(0, 'delete')
|
||||
doesNotHaveAction(1, 'delete')
|
||||
doesNotHaveAction(2, 'delete')
|
||||
})
|
||||
|
||||
it('Does not work without delete permission through direct API access', () => {
|
||||
let fileId: string | undefined
|
||||
let versionId: string | undefined
|
||||
|
||||
setupTestSharedFileFromUser(user, folderName, { delete: false })
|
||||
.then((recipient) => {
|
||||
navigateToFolder(folderName)
|
||||
openVersionsPanel(randomFilePath)
|
||||
|
||||
getRowForFile(randomFileName)
|
||||
.should('be.visible')
|
||||
.invoke('attr', 'data-cy-files-list-row-fileid')
|
||||
.then(($fileId) => { fileId = $fileId })
|
||||
|
||||
cy.get('[data-files-versions-version]')
|
||||
.eq(1)
|
||||
.invoke('attr', 'data-files-versions-version')
|
||||
.then(($versionId) => { versionId = $versionId })
|
||||
|
||||
cy.logout()
|
||||
cy.then(() => {
|
||||
const base = Cypress.config('baseUrl')!.replace(/\/index\.php\/?$/, '')
|
||||
return cy.request({
|
||||
method: 'DELETE',
|
||||
url: `${base}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
|
||||
auth: { user: recipient.userId, pass: recipient.password },
|
||||
headers: {
|
||||
cookie: '',
|
||||
},
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
}).then(({ status }) => {
|
||||
expect(status).to.equal(403)
|
||||
})
|
||||
})
|
||||
})
|
||||
doesNotHaveAction(0, 'delete')
|
||||
doesNotHaveAction(1, 'delete')
|
||||
doesNotHaveAction(2, 'delete')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,91 +6,54 @@
|
|||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { randomString } from '../../support/utils/randomString.ts'
|
||||
import { getRowForFile } from '../files/FilesUtils.ts'
|
||||
import { assertVersionContent, doesNotHaveAction, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts'
|
||||
|
||||
describe('Versions download', () => {
|
||||
let randomFileName = ''
|
||||
let user: User
|
||||
|
||||
before(() => {
|
||||
randomFileName = randomString(10) + '.txt'
|
||||
|
||||
cy.runOccCommand('config:app:set --value no core shareapi_allow_view_without_download')
|
||||
cy.createRandomUser()
|
||||
.then((_user) => {
|
||||
user = _user
|
||||
uploadThreeVersions(user, randomFileName)
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
openVersionsPanel(randomFileName)
|
||||
})
|
||||
})
|
||||
|
||||
before(() => cy.runOccCommand('config:app:set --value no core shareapi_allow_view_without_download'))
|
||||
after(() => {
|
||||
cy.runOccCommand('config:app:delete core shareapi_allow_view_without_download')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
randomFileName = randomString(10) + '.txt'
|
||||
|
||||
cy.createRandomUser()
|
||||
.then((_user) => {
|
||||
user = _user
|
||||
uploadThreeVersions(user, randomFileName)
|
||||
})
|
||||
})
|
||||
|
||||
it('Download versions and assert their content', () => {
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
openVersionsPanel(randomFileName)
|
||||
|
||||
assertVersionContent(0, 'v3')
|
||||
assertVersionContent(1, 'v2')
|
||||
assertVersionContent(2, 'v1')
|
||||
})
|
||||
|
||||
context('Download versions of shared file', () => {
|
||||
it('Works with download permission', () => {
|
||||
setupTestSharedFileFromUser(user, randomFileName, { download: true })
|
||||
openVersionsPanel(randomFileName)
|
||||
it('Download versions of shared file with download permission', () => {
|
||||
setupTestSharedFileFromUser(user, randomFileName, { download: true })
|
||||
openVersionsPanel(randomFileName)
|
||||
|
||||
assertVersionContent(0, 'v3')
|
||||
assertVersionContent(1, 'v2')
|
||||
assertVersionContent(2, 'v1')
|
||||
})
|
||||
assertVersionContent(0, 'v3')
|
||||
assertVersionContent(1, 'v2')
|
||||
assertVersionContent(2, 'v1')
|
||||
})
|
||||
|
||||
it('Does not show action without download permission', () => {
|
||||
setupTestSharedFileFromUser(user, randomFileName, { download: false })
|
||||
openVersionsPanel(randomFileName)
|
||||
it('Does not show action without download permission', () => {
|
||||
setupTestSharedFileFromUser(user, randomFileName, { download: false })
|
||||
openVersionsPanel(randomFileName)
|
||||
|
||||
cy.get('[data-files-versions-version]').eq(0).find('.action-item__menutoggle').should('not.exist')
|
||||
cy.get('[data-files-versions-version]').eq(0).get('[data-cy-version-action="download"]').should('not.exist')
|
||||
cy.get('[data-files-versions-version]').eq(0).find('.action-item__menutoggle').should('not.exist')
|
||||
cy.get('[data-files-versions-version]').eq(0).get('[data-cy-version-action="download"]').should('not.exist')
|
||||
|
||||
doesNotHaveAction(1, 'download')
|
||||
doesNotHaveAction(2, 'download')
|
||||
})
|
||||
|
||||
it('Does not work without download permission through direct API access', () => {
|
||||
let fileId: string | undefined
|
||||
let versionId: string | undefined
|
||||
|
||||
setupTestSharedFileFromUser(user, randomFileName, { download: false })
|
||||
.then((recipient) => {
|
||||
openVersionsPanel(randomFileName)
|
||||
|
||||
getRowForFile(randomFileName)
|
||||
.should('be.visible')
|
||||
.invoke('attr', 'data-cy-files-list-row-fileid')
|
||||
.then(($fileId) => { fileId = $fileId })
|
||||
|
||||
cy.get('[data-files-versions-version]')
|
||||
.eq(1)
|
||||
.invoke('attr', 'data-files-versions-version')
|
||||
.then(($versionId) => { versionId = $versionId })
|
||||
|
||||
cy.logout()
|
||||
cy.then(() => {
|
||||
const base = Cypress.config('baseUrl')!.replace(/\/index\.php\/?$/, '')
|
||||
return cy.request({
|
||||
url: `${base}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
|
||||
auth: { user: recipient.userId, pass: recipient.password },
|
||||
headers: {
|
||||
cookie: '',
|
||||
},
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
}).then(({ status }) => {
|
||||
expect(status).to.equal(403)
|
||||
})
|
||||
})
|
||||
})
|
||||
doesNotHaveAction(1, 'download')
|
||||
doesNotHaveAction(2, 'download')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,27 +6,30 @@
|
|||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { randomString } from '../../support/utils/randomString.ts'
|
||||
import { getRowForFile } from '../files/FilesUtils.ts'
|
||||
import { navigateToFolder } from '../files/FilesUtils.ts'
|
||||
import { doesNotHaveAction, nameVersion, openVersionsPanel, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts'
|
||||
|
||||
describe('Versions naming', () => {
|
||||
let randomFileName = ''
|
||||
let user: User
|
||||
|
||||
before(() => {
|
||||
beforeEach(() => {
|
||||
randomFileName = randomString(10) + '.txt'
|
||||
|
||||
cy.createRandomUser()
|
||||
.then((_user) => {
|
||||
user = _user
|
||||
uploadThreeVersions(user, randomFileName)
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
openVersionsPanel(randomFileName)
|
||||
cy.mkdir(_user, '/share')
|
||||
uploadThreeVersions(user, `share/${randomFileName}`)
|
||||
})
|
||||
})
|
||||
|
||||
it('Names the versions', () => {
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
navigateToFolder('share')
|
||||
openVersionsPanel(randomFileName)
|
||||
|
||||
nameVersion(2, 'v1')
|
||||
cy.get('#tab-files_versions').within(() => {
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('v1')
|
||||
|
|
@ -44,92 +47,45 @@ describe('Versions naming', () => {
|
|||
})
|
||||
})
|
||||
|
||||
context('Name versions of shared file', () => {
|
||||
context('with edit permission', () => {
|
||||
before(() => {
|
||||
setupTestSharedFileFromUser(user, randomFileName, { update: true })
|
||||
openVersionsPanel(randomFileName)
|
||||
})
|
||||
it('Name versions of shared file with edit permission', () => {
|
||||
setupTestSharedFileFromUser(user, 'share', { update: true })
|
||||
|
||||
it('Names the versions', () => {
|
||||
nameVersion(2, 'v1 - shared')
|
||||
cy.get('#tab-files_versions').within(() => {
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('v1 - shared')
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist')
|
||||
})
|
||||
navigateToFolder('share')
|
||||
openVersionsPanel(randomFileName)
|
||||
|
||||
nameVersion(1, 'v2 - shared')
|
||||
cy.get('#tab-files_versions').within(() => {
|
||||
cy.get('[data-files-versions-version]').eq(1).contains('v2 - shared')
|
||||
})
|
||||
|
||||
nameVersion(0, 'v3 - shared')
|
||||
cy.get('#tab-files_versions').within(() => {
|
||||
cy.get('[data-files-versions-version]').eq(0).contains('v3 - shared (Current version)')
|
||||
})
|
||||
})
|
||||
nameVersion(2, 'v1 - shared')
|
||||
cy.get('#tab-files_versions').within(() => {
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('v1 - shared')
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist')
|
||||
})
|
||||
|
||||
context('without edit permission', () => {
|
||||
let recipient: User
|
||||
nameVersion(1, 'v2 - shared')
|
||||
cy.get('#tab-files_versions').within(() => {
|
||||
cy.get('[data-files-versions-version]').eq(1).contains('v2 - shared')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
setupTestSharedFileFromUser(user, randomFileName, { update: false })
|
||||
.then(($recipient) => {
|
||||
recipient = $recipient
|
||||
openVersionsPanel(randomFileName)
|
||||
})
|
||||
})
|
||||
|
||||
it('Does not show action', () => {
|
||||
cy.get('[data-files-versions-version]').eq(0).find('.action-item__menutoggle').should('not.exist')
|
||||
cy.get('[data-files-versions-version]').eq(0).get('[data-cy-version-action="label"]').should('not.exist')
|
||||
|
||||
doesNotHaveAction(1, 'label')
|
||||
doesNotHaveAction(2, 'label')
|
||||
})
|
||||
|
||||
it('Does not work without update permission through direct API access', () => {
|
||||
let fileId: string | undefined
|
||||
let versionId: string | undefined
|
||||
|
||||
getRowForFile(randomFileName)
|
||||
.should('be.visible')
|
||||
.invoke('attr', 'data-cy-files-list-row-fileid')
|
||||
.then(($fileId) => { fileId = $fileId })
|
||||
|
||||
cy.get('[data-files-versions-version]')
|
||||
.eq(1)
|
||||
.invoke('attr', 'data-files-versions-version')
|
||||
.then(($versionId) => { versionId = $versionId })
|
||||
|
||||
cy.logout()
|
||||
cy.then(() => {
|
||||
const base = Cypress.config('baseUrl')!.replace(/index\.php\/?/, '')
|
||||
return cy.request({
|
||||
method: 'PROPPATCH',
|
||||
url: `${base}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
|
||||
auth: { user: recipient.userId, pass: recipient.password },
|
||||
headers: {
|
||||
cookie: '',
|
||||
},
|
||||
body: `<?xml version="1.0"?>
|
||||
<d:propertyupdate xmlns:d="DAV:"
|
||||
xmlns:oc="http://owncloud.org/ns"
|
||||
xmlns:nc="http://nextcloud.org/ns"
|
||||
xmlns:ocs="http://open-collaboration-services.org/ns">
|
||||
<d:set>
|
||||
<d:prop>
|
||||
<nc:version-label>not authorized labeling</nc:version-label>
|
||||
</d:prop>
|
||||
</d:set>
|
||||
</d:propertyupdate>`,
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
}).then(({ status }) => {
|
||||
expect(status).to.equal(403)
|
||||
})
|
||||
})
|
||||
nameVersion(0, 'v3 - shared')
|
||||
cy.get('#tab-files_versions').within(() => {
|
||||
cy.get('[data-files-versions-version]').eq(0).contains('v3 - shared (Current version)')
|
||||
})
|
||||
})
|
||||
|
||||
it('Name versions without edit permission fails', () => {
|
||||
setupTestSharedFileFromUser(user, 'share', { update: false })
|
||||
|
||||
navigateToFolder('share')
|
||||
openVersionsPanel(randomFileName)
|
||||
|
||||
cy.get('[data-files-versions-version]')
|
||||
.eq(0)
|
||||
.as('firstVersion')
|
||||
.find('.action-item__menutoggle')
|
||||
.should('not.exist')
|
||||
cy.get('@firstVersion')
|
||||
.find('[data-cy-version-action="label"]')
|
||||
.should('not.exist')
|
||||
|
||||
doesNotHaveAction(1, 'label')
|
||||
doesNotHaveAction(2, 'label')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,31 +6,31 @@
|
|||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { randomString } from '../../support/utils/randomString.ts'
|
||||
import { getRowForFile } from '../files/FilesUtils.ts'
|
||||
import { navigateToFolder } from '../files/FilesUtils.ts'
|
||||
import { assertVersionContent, doesNotHaveAction, openVersionsPanel, restoreVersion, setupTestSharedFileFromUser, uploadThreeVersions } from './filesVersionsUtils.ts'
|
||||
|
||||
describe('Versions restoration', () => {
|
||||
let randomFileName = ''
|
||||
let user: User
|
||||
|
||||
before(() => {
|
||||
beforeEach(() => {
|
||||
randomFileName = randomString(10) + '.txt'
|
||||
|
||||
cy.createRandomUser()
|
||||
.then((_user) => {
|
||||
user = _user
|
||||
uploadThreeVersions(user, randomFileName)
|
||||
cy.mkdir(_user, '/share')
|
||||
uploadThreeVersions(user, `share/${randomFileName}`)
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
openVersionsPanel(randomFileName)
|
||||
})
|
||||
})
|
||||
|
||||
it('Current version does not have restore action', () => {
|
||||
doesNotHaveAction(0, 'restore')
|
||||
})
|
||||
|
||||
it('Restores initial version', () => {
|
||||
navigateToFolder('share')
|
||||
openVersionsPanel(randomFileName)
|
||||
// Current version does not have restore action
|
||||
doesNotHaveAction(0, 'restore')
|
||||
restoreVersion(2)
|
||||
|
||||
cy.get('#tab-files_versions').within(() => {
|
||||
|
|
@ -38,81 +38,38 @@ describe('Versions restoration', () => {
|
|||
cy.get('[data-files-versions-version]').eq(0).contains('Current version')
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('Downloads versions and assert there content', () => {
|
||||
// Downloads versions and assert there content
|
||||
assertVersionContent(0, 'v1')
|
||||
assertVersionContent(1, 'v3')
|
||||
assertVersionContent(2, 'v2')
|
||||
})
|
||||
|
||||
context('Restore versions of shared file', () => {
|
||||
it('Works with update permission', () => {
|
||||
setupTestSharedFileFromUser(user, randomFileName, { update: true })
|
||||
openVersionsPanel(randomFileName)
|
||||
it('Restore versions of shared file with update permission', () => {
|
||||
setupTestSharedFileFromUser(user, 'share', { update: true })
|
||||
navigateToFolder('share')
|
||||
openVersionsPanel(randomFileName)
|
||||
|
||||
it('Restores initial version', () => {
|
||||
restoreVersion(2)
|
||||
cy.get('#tab-files_versions').within(() => {
|
||||
cy.get('[data-files-versions-version]').should('have.length', 3)
|
||||
cy.get('[data-files-versions-version]').eq(0).contains('Current version')
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('Downloads versions and assert there content', () => {
|
||||
assertVersionContent(0, 'v1')
|
||||
assertVersionContent(1, 'v3')
|
||||
assertVersionContent(2, 'v2')
|
||||
})
|
||||
restoreVersion(2)
|
||||
cy.get('#tab-files_versions').within(() => {
|
||||
cy.get('[data-files-versions-version]').should('have.length', 3)
|
||||
cy.get('[data-files-versions-version]').eq(0).contains('Current version')
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist')
|
||||
})
|
||||
assertVersionContent(0, 'v1')
|
||||
assertVersionContent(1, 'v3')
|
||||
assertVersionContent(2, 'v2')
|
||||
})
|
||||
|
||||
it('Does not show action without delete permission', () => {
|
||||
setupTestSharedFileFromUser(user, randomFileName, { update: false })
|
||||
openVersionsPanel(randomFileName)
|
||||
it('Does not show action without delete permission', () => {
|
||||
setupTestSharedFileFromUser(user, 'share', { update: false })
|
||||
navigateToFolder('share')
|
||||
openVersionsPanel(randomFileName)
|
||||
|
||||
cy.get('[data-files-versions-version]').eq(0).find('.action-item__menutoggle').should('not.exist')
|
||||
cy.get('[data-files-versions-version]').eq(0).get('[data-cy-version-action="restore"]').should('not.exist')
|
||||
cy.get('[data-files-versions-version]').eq(0).find('.action-item__menutoggle').should('not.exist')
|
||||
cy.get('[data-files-versions-version]').eq(0).get('[data-cy-version-action="restore"]').should('not.exist')
|
||||
|
||||
doesNotHaveAction(1, 'restore')
|
||||
doesNotHaveAction(2, 'restore')
|
||||
})
|
||||
|
||||
it('Does not work without update permission through direct API access', () => {
|
||||
let fileId: string | undefined
|
||||
let versionId: string | undefined
|
||||
|
||||
setupTestSharedFileFromUser(user, randomFileName, { update: false })
|
||||
.then((recipient) => {
|
||||
openVersionsPanel(randomFileName)
|
||||
|
||||
getRowForFile(randomFileName)
|
||||
.should('be.visible')
|
||||
.invoke('attr', 'data-cy-files-list-row-fileid')
|
||||
.then(($fileId) => { fileId = $fileId })
|
||||
|
||||
cy.get('[data-files-versions-version]')
|
||||
.eq(1)
|
||||
.invoke('attr', 'data-files-versions-version')
|
||||
.then(($versionId) => { versionId = $versionId })
|
||||
|
||||
cy.logout()
|
||||
cy.then(() => {
|
||||
const base = Cypress.config('baseUrl')!.replace(/\/index\.php\/?$/, '')
|
||||
return cy.request({
|
||||
method: 'MOVE',
|
||||
url: `${base}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
|
||||
auth: { user: recipient.userId, pass: recipient.password },
|
||||
headers: {
|
||||
cookie: '',
|
||||
Destination: `${base}}/remote.php/dav/versions/${recipient.userId}/restore/target`,
|
||||
},
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
}).then(({ status }) => {
|
||||
expect(status).to.equal(403)
|
||||
})
|
||||
})
|
||||
})
|
||||
doesNotHaveAction(1, 'restore')
|
||||
doesNotHaveAction(2, 'restore')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
/**
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { randomBytes } from 'crypto'
|
||||
import { closeSidebar, getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'
|
||||
import { getRowForFile } from '../files/FilesUtils.ts'
|
||||
import { addTagToFile } from './utils.ts'
|
||||
|
||||
describe('Systemtags: Files integration', { testIsolation: true }, () => {
|
||||
let user: User
|
||||
|
|
@ -21,31 +23,7 @@ describe('Systemtags: Files integration', { testIsolation: true }, () => {
|
|||
|
||||
it('See first assigned tag in the file list', () => {
|
||||
const tag = randomBytes(8).toString('base64')
|
||||
|
||||
cy.intercept('PROPFIND', `**/remote.php/dav/files/${user.userId}/file.txt`).as('getNode')
|
||||
getRowForFile('file.txt').should('be.visible')
|
||||
triggerActionForFile('file.txt', 'details')
|
||||
cy.wait('@getNode')
|
||||
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.should('be.visible')
|
||||
.findByRole('button', { name: 'Actions' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.findByRole('menuitem', { name: 'Tags' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
|
||||
|
||||
getCollaborativeTagsInput()
|
||||
.type(`{selectAll}${tag}{enter}`)
|
||||
cy.wait('@assignTag')
|
||||
cy.wait('@getNode')
|
||||
|
||||
// Close the sidebar and reload to check the file list
|
||||
closeSidebar()
|
||||
addTagToFile('file.txt', tag)
|
||||
cy.reload()
|
||||
|
||||
getRowForFile('file.txt')
|
||||
|
|
@ -58,38 +36,8 @@ describe('Systemtags: Files integration', { testIsolation: true }, () => {
|
|||
it('See two assigned tags are also shown in the file list', () => {
|
||||
const tag1 = randomBytes(5).toString('base64')
|
||||
const tag2 = randomBytes(5).toString('base64')
|
||||
|
||||
cy.intercept('PROPFIND', `**/remote.php/dav/files/${user.userId}/file.txt`).as('getNode')
|
||||
getRowForFile('file.txt').should('be.visible')
|
||||
triggerActionForFile('file.txt', 'details')
|
||||
cy.wait('@getNode')
|
||||
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.should('be.visible')
|
||||
.findByRole('button', { name: 'Actions' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.findByRole('menuitem', { name: 'Tags' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
|
||||
|
||||
// Assign first tag
|
||||
getCollaborativeTagsInput()
|
||||
.type(`{selectAll}${tag1}{enter}`)
|
||||
cy.wait('@assignTag')
|
||||
cy.wait('@getNode')
|
||||
|
||||
// Assign second tag
|
||||
getCollaborativeTagsInput()
|
||||
.type(`{selectAll}${tag2}{enter}`)
|
||||
cy.wait('@assignTag')
|
||||
cy.wait('@getNode')
|
||||
|
||||
// Close the sidebar and reload to check the file list
|
||||
closeSidebar()
|
||||
addTagToFile('file.txt', tag1)
|
||||
addTagToFile('file.txt', tag2)
|
||||
cy.reload()
|
||||
|
||||
getRowForFile('file.txt')
|
||||
|
|
@ -104,44 +52,9 @@ describe('Systemtags: Files integration', { testIsolation: true }, () => {
|
|||
const tag1 = randomBytes(4).toString('base64')
|
||||
const tag2 = randomBytes(4).toString('base64')
|
||||
const tag3 = randomBytes(4).toString('base64')
|
||||
|
||||
cy.intercept('PROPFIND', `**/remote.php/dav/files/${user.userId}/file.txt`).as('getNode')
|
||||
getRowForFile('file.txt').should('be.visible')
|
||||
triggerActionForFile('file.txt', 'details')
|
||||
cy.wait('@getNode')
|
||||
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.should('be.visible')
|
||||
.findByRole('button', { name: 'Actions' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.findByRole('menuitem', { name: 'Tags' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
|
||||
|
||||
// Assign first tag
|
||||
getCollaborativeTagsInput()
|
||||
.type(`{selectAll}${tag1}{enter}`)
|
||||
cy.wait('@assignTag')
|
||||
cy.wait('@getNode')
|
||||
|
||||
// Assign second tag
|
||||
getCollaborativeTagsInput()
|
||||
.type(`{selectAll}${tag2}{enter}`)
|
||||
cy.wait('@assignTag')
|
||||
cy.wait('@getNode')
|
||||
|
||||
// Assign third tag
|
||||
getCollaborativeTagsInput()
|
||||
.type(`{selectAll}${tag3}{enter}`)
|
||||
cy.wait('@assignTag')
|
||||
cy.wait('@getNode')
|
||||
|
||||
// Close the sidebar and reload to check the file list
|
||||
closeSidebar()
|
||||
addTagToFile('file.txt', tag1)
|
||||
addTagToFile('file.txt', tag2)
|
||||
addTagToFile('file.txt', tag3)
|
||||
cy.reload()
|
||||
|
||||
getRowForFile('file.txt')
|
||||
|
|
@ -163,10 +76,3 @@ describe('Systemtags: Files integration', { testIsolation: true }, () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
function getCollaborativeTagsInput(): Cypress.Chainable<JQuery<HTMLElement>> {
|
||||
return cy.get('[data-cy-sidebar]')
|
||||
.findByRole('combobox', { name: /collaborative tags/i })
|
||||
.should('be.visible')
|
||||
.should('not.have.attr', 'disabled', { timeout: 5000 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { User } from '@nextcloud/e2e-test-server/cypress'
|
|||
|
||||
import { randomBytes } from 'crypto'
|
||||
import { getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'
|
||||
import { createNewTagInDialog } from './utils.ts'
|
||||
|
||||
describe('Systemtags: Files sidebar integration', { testIsolation: true }, () => {
|
||||
let user: User
|
||||
|
|
@ -32,14 +33,9 @@ describe('Systemtags: Files sidebar integration', { testIsolation: true }, () =>
|
|||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.findByRole('menuitem', { name: 'Tags' })
|
||||
cy.findByRole('menuitem', { name: 'Add tags' })
|
||||
.click()
|
||||
|
||||
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.findByRole('combobox', { name: /collaborative tags/i })
|
||||
.should('be.visible')
|
||||
.type(`${tag}{enter}`)
|
||||
cy.wait('@assignTag')
|
||||
createNewTagInDialog(tag)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { randomBytes } from 'crypto'
|
||||
import { closeSidebar, getRowForFile, getRowForFileId, triggerActionForFile } from '../files/FilesUtils.ts'
|
||||
import { getRowForFile } from '../files/FilesUtils.ts'
|
||||
import { addTagToFile } from './utils.ts'
|
||||
|
||||
describe('Systemtags: Files view', { testIsolation: true }, () => {
|
||||
let user: User
|
||||
|
|
@ -22,51 +23,20 @@ describe('Systemtags: Files view', { testIsolation: true }, () => {
|
|||
|
||||
it('See first assigned tag in the file list', () => {
|
||||
const tag = randomBytes(8).toString('base64')
|
||||
let tagId
|
||||
|
||||
// Tag the file
|
||||
tagNode(tag, 'folder')
|
||||
.then((id) => { tagId = id })
|
||||
addTagToFile('folder', tag)
|
||||
|
||||
// open the tags view
|
||||
cy.visit('/apps/files/tags').then(() => {
|
||||
// see the tag
|
||||
getRowForFileId(tagId).should('be.visible')
|
||||
getRowForFile('folder').should('not.exist')
|
||||
getRowForFile('file.txt').should('not.exist')
|
||||
cy.findByRole('cell', { name: tag })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
// see that the tag has its content
|
||||
getRowForFileId(tagId).find('[data-cy-files-list-row-name-link]').click()
|
||||
getRowForFile('folder').should('be.visible')
|
||||
getRowForFile('file.txt').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function getCollaborativeTagsInput(): Cypress.Chainable<JQuery<HTMLElement>> {
|
||||
return cy.get('[data-cy-sidebar]')
|
||||
.findByRole('combobox', { name: /collaborative tags/i })
|
||||
.should('be.visible')
|
||||
.should('not.have.attr', 'disabled', { timeout: 5000 })
|
||||
}
|
||||
|
||||
function tagNode(tag: string, node: string): Cypress.Chainable<number> {
|
||||
getRowForFile(node).should('be.visible')
|
||||
|
||||
triggerActionForFile(node, 'details')
|
||||
cy.get('[data-cy-sidebar]')
|
||||
.should('be.visible')
|
||||
.findByRole('button', { name: 'Actions' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
cy.findByRole('menuitem', { name: 'Tags' })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
cy.intercept('PUT', '**/remote.php/dav/systemtags-relations/files/**').as('assignTag')
|
||||
getCollaborativeTagsInput()
|
||||
.type(`{selectAll}${tag}{enter}`)
|
||||
cy.wait('@assignTag')
|
||||
closeSidebar()
|
||||
return cy.get('@assignTag')
|
||||
.then(({ request }) => request.body.id)
|
||||
}
|
||||
|
|
|
|||
37
cypress/e2e/systemtags/utils.ts
Normal file
37
cypress/e2e/systemtags/utils.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { getRowForFile, triggerActionForFile } from '../files/FilesUtils.ts'
|
||||
|
||||
export function addTagToFile(fileName: string, newTag: string): void {
|
||||
getRowForFile(fileName).should('be.visible')
|
||||
triggerActionForFile(fileName, 'systemtags:bulk')
|
||||
|
||||
createNewTagInDialog(newTag)
|
||||
}
|
||||
|
||||
export function createNewTagInDialog(newTag: string): void {
|
||||
cy.intercept('POST', '/remote.php/dav/systemtags').as('createTag')
|
||||
cy.intercept('PROPFIND', '/remote.php/dav/systemtags/*/files').as('getTagData')
|
||||
cy.intercept('PROPPATCH', '/remote.php/dav/systemtags/*/files').as('assignTagData')
|
||||
|
||||
cy.get('[data-cy-systemtags-picker-input]').type(newTag)
|
||||
|
||||
cy.get('[data-cy-systemtags-picker-tag]').should('have.length', 0)
|
||||
cy.get('[data-cy-systemtags-picker-button-create]').should('be.visible')
|
||||
cy.get('[data-cy-systemtags-picker-button-create]').click()
|
||||
|
||||
cy.wait('@createTag')
|
||||
// Verify the new tag is selected by default
|
||||
cy.get('[data-cy-systemtags-picker-tag]').contains(newTag)
|
||||
.parents('[data-cy-systemtags-picker-tag]')
|
||||
.findByRole('checkbox', { hidden: true }).should('be.checked')
|
||||
|
||||
// Apply changes
|
||||
cy.get('[data-cy-systemtags-picker-button-submit]').click()
|
||||
|
||||
cy.wait('@assignTagData')
|
||||
cy.get('[data-cy-systemtags-picker]').should('not.exist')
|
||||
}
|
||||
2
dist/1035-1035.js
vendored
Normal file
2
dist/1035-1035.js
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
"use strict";(globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[]).push([[1035],{67246(e,t,n){n.d(t,{A:()=>i});var a=n(71354),s=n.n(a),r=n(76314),p=n.n(r)()(s());p.push([e.id,".search-empty-view__input[data-v-4e4bf1e2]{flex:0 1;min-width:min(400px,50vw)}.search-empty-view__wrapper[data-v-4e4bf1e2]{display:flex;flex-wrap:wrap;gap:10px;align-items:baseline}","",{version:3,sources:["webpack://./apps/files/src/views/SearchEmptyView.vue"],names:[],mappings:"AAEC,2CACC,QAAA,CACA,yBAAA,CAGD,6CACC,YAAA,CACA,cAAA,CACA,QAAA,CACA,oBAAA",sourcesContent:["\n.search-empty-view {\n\t&__input {\n\t\tflex: 0 1;\n\t\tmin-width: min(400px, 50vw);\n\t}\n\n\t&__wrapper {\n\t\tdisplay: flex;\n\t\tflex-wrap: wrap;\n\t\tgap: 10px;\n\t\talign-items: baseline;\n\t}\n}\n"],sourceRoot:""}]);const i=p},91035(e,t,n){n.r(t),n.d(t,{default:()=>N});var a=n(85471),s=n(9165),r=n(53334),p=n(46855),i=n(42507),l=n(6695),c=n(16879),o=n(88140),u=n(78246);const A=(0,a.pM)({__name:"SearchEmptyView",setup(e){const t=(0,u.j)((0,o.u)()),n=(0,p.A)(e=>{t.query=e},500);return{__sfc:!0,searchStore:t,debouncedUpdate:n,mdiMagnifyClose:s.WBH,t:r.t,NcEmptyContent:i.A,NcIconSvgWrapper:l.A,NcInputField:c.A}}});var d=n(85072),y=n.n(d),f=n(97825),m=n.n(f),C=n(77659),_=n.n(C),h=n(55056),w=n.n(h),v=n(10540),x=n.n(v),b=n(41113),g=n.n(b),S=n(67246),k={};k.styleTagTransform=g(),k.setAttributes=w(),k.insert=_().bind(null,"head"),k.domAPI=m(),k.insertStyleElement=x(),y()(S.A,k),S.A&&S.A.locals&&S.A.locals;const N=(0,n(14486).A)(A,function(){var e=this,t=e._self._c,n=e._self._setupProxy;return t(n.NcEmptyContent,{attrs:{name:n.t("files","No search results for “{query}”",{query:n.searchStore.query})},scopedSlots:e._u([{key:"icon",fn:function(){return[t(n.NcIconSvgWrapper,{attrs:{path:n.mdiMagnifyClose}})]},proxy:!0},{key:"action",fn:function(){return[t("div",{staticClass:"search-empty-view__wrapper"},[t(n.NcInputField,{staticClass:"search-empty-view__input",attrs:{label:n.t("files","Search for files"),"model-value":n.searchStore.query,type:"search"},on:{"update:model-value":n.debouncedUpdate}})],1)]},proxy:!0}])})},[],!1,null,"4e4bf1e2",null).exports}}]);
|
||||
//# sourceMappingURL=1035-1035.js.map?v=da08d310d18692ca4e27
|
||||
1
dist/1035-1035.js.map
vendored
Normal file
1
dist/1035-1035.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/1035-1035.js.map.license
vendored
Symbolic link
1
dist/1035-1035.js.map.license
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
1035-1035.js.license
|
||||
2
dist/1082-1082.js
vendored
2
dist/1082-1082.js
vendored
|
|
@ -1,2 +0,0 @@
|
|||
"use strict";(globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[]).push([[1082],{1082(e,t,n){n.r(t),n.d(t,{default:()=>N});var a=n(85471),s=n(9165),r=n(53334),p=n(46855),i=n(42507),l=n(6695),c=n(16879),o=n(4114),u=n(82736);const A=(0,a.pM)({__name:"SearchEmptyView",setup(e){const t=(0,u.j)((0,o.u)()),n=(0,p.A)(e=>{t.query=e},500);return{__sfc:!0,searchStore:t,debouncedUpdate:n,mdiMagnifyClose:s.WBH,t:r.t,NcEmptyContent:i.A,NcIconSvgWrapper:l.A,NcInputField:c.A}}});var d=n(85072),y=n.n(d),f=n(97825),m=n.n(f),C=n(77659),_=n.n(C),h=n(55056),w=n.n(h),v=n(10540),x=n.n(v),b=n(41113),g=n.n(b),S=n(67246),k={};k.styleTagTransform=g(),k.setAttributes=w(),k.insert=_().bind(null,"head"),k.domAPI=m(),k.insertStyleElement=x(),y()(S.A,k),S.A&&S.A.locals&&S.A.locals;const N=(0,n(14486).A)(A,function(){var e=this,t=e._self._c,n=e._self._setupProxy;return t(n.NcEmptyContent,{attrs:{name:n.t("files","No search results for “{query}”",{query:n.searchStore.query})},scopedSlots:e._u([{key:"icon",fn:function(){return[t(n.NcIconSvgWrapper,{attrs:{path:n.mdiMagnifyClose}})]},proxy:!0},{key:"action",fn:function(){return[t("div",{staticClass:"search-empty-view__wrapper"},[t(n.NcInputField,{staticClass:"search-empty-view__input",attrs:{label:n.t("files","Search for files"),"model-value":n.searchStore.query,type:"search"},on:{"update:model-value":n.debouncedUpdate}})],1)]},proxy:!0}])})},[],!1,null,"4e4bf1e2",null).exports},67246(e,t,n){n.d(t,{A:()=>i});var a=n(71354),s=n.n(a),r=n(76314),p=n.n(r)()(s());p.push([e.id,".search-empty-view__input[data-v-4e4bf1e2]{flex:0 1;min-width:min(400px,50vw)}.search-empty-view__wrapper[data-v-4e4bf1e2]{display:flex;flex-wrap:wrap;gap:10px;align-items:baseline}","",{version:3,sources:["webpack://./apps/files/src/views/SearchEmptyView.vue"],names:[],mappings:"AAEC,2CACC,QAAA,CACA,yBAAA,CAGD,6CACC,YAAA,CACA,cAAA,CACA,QAAA,CACA,oBAAA",sourcesContent:["\n.search-empty-view {\n\t&__input {\n\t\tflex: 0 1;\n\t\tmin-width: min(400px, 50vw);\n\t}\n\n\t&__wrapper {\n\t\tdisplay: flex;\n\t\tflex-wrap: wrap;\n\t\tgap: 10px;\n\t\talign-items: baseline;\n\t}\n}\n"],sourceRoot:""}]);const i=p}}]);
|
||||
//# sourceMappingURL=1082-1082.js.map?v=d6863ba280d66116c0a8
|
||||
1
dist/1082-1082.js.map
vendored
1
dist/1082-1082.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/1082-1082.js.map.license
vendored
1
dist/1082-1082.js.map.license
vendored
|
|
@ -1 +0,0 @@
|
|||
1082-1082.js.license
|
||||
2
dist/1216-1216.js
vendored
Normal file
2
dist/1216-1216.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/1216-1216.js.map
vendored
Normal file
1
dist/1216-1216.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/1216-1216.js.map.license
vendored
Symbolic link
1
dist/1216-1216.js.map.license
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
1216-1216.js.license
|
||||
4
dist/6358-6358.js → dist/1261-1261.js
vendored
4
dist/6358-6358.js → dist/1261-1261.js
vendored
|
|
@ -1,2 +1,2 @@
|
|||
"use strict";(globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[]).push([[6358],{16358(e,t,a){a.r(t),a.d(t,{default:()=>d});var s=a(53334),l=a(85471),o=a(94219),r=a(371),n=a(93663),i=a(82182);const c=(0,l.pM)({name:"CredentialsDialog",components:{NcDialog:o.A,NcNoteCard:r.A,NcTextField:i.A,NcPasswordField:n.A},setup:()=>({t:s.t}),data:()=>({login:"",password:""}),computed:{dialogButtons:()=>[{label:(0,s.t)("files_external","Confirm"),type:"submit",variant:"primary"}]}}),d=(0,a(14486).A)(c,function(){var e=this,t=e._self._c;return e._self._setupProxy,t("NcDialog",{staticClass:"external-storage-auth",attrs:{buttons:e.dialogButtons,"close-on-click-outside":"","data-cy-external-storage-auth":"","is-form":"",name:e.t("files_external","Storage credentials"),"out-transition":""},on:{submit:function(t){return e.$emit("close",{login:e.login,password:e.password})},"update:open":function(t){return e.$emit("close")}}},[t("NcNoteCard",{staticClass:"external-storage-auth__header",attrs:{text:e.t("files_external","To access the storage, you need to provide the authentication credentials."),type:"info"}}),e._v(" "),t("NcTextField",{ref:"login",staticClass:"external-storage-auth__login",attrs:{"data-cy-external-storage-auth-dialog-login":"",label:e.t("files_external","Login"),placeholder:e.t("files_external","Enter the storage login"),minlength:"2",name:"login",required:""},model:{value:e.login,callback:function(t){e.login=t},expression:"login"}}),e._v(" "),t("NcPasswordField",{ref:"password",staticClass:"external-storage-auth__password",attrs:{"data-cy-external-storage-auth-dialog-password":"",label:e.t("files_external","Password"),placeholder:e.t("files_external","Enter the storage password"),name:"password",required:""},model:{value:e.password,callback:function(t){e.password=t},expression:"password"}})],1)},[],!1,null,null,null).exports}}]);
|
||||
//# sourceMappingURL=6358-6358.js.map?v=32b8826edce0bc146e33
|
||||
"use strict";(globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[]).push([[1261],{11261(e,t,a){a.r(t),a.d(t,{default:()=>d});var s=a(53334),l=a(85471),o=a(94219),r=a(371),n=a(93663),i=a(82182);const c=(0,l.pM)({name:"CredentialsDialog",components:{NcDialog:o.A,NcNoteCard:r.A,NcTextField:i.A,NcPasswordField:n.A},setup:()=>({t:s.t}),data:()=>({login:"",password:""}),computed:{dialogButtons:()=>[{label:(0,s.t)("files_external","Confirm"),type:"submit",variant:"primary"}]}}),d=(0,a(14486).A)(c,function(){var e=this,t=e._self._c;return e._self._setupProxy,t("NcDialog",{staticClass:"external-storage-auth",attrs:{buttons:e.dialogButtons,"close-on-click-outside":"","data-cy-external-storage-auth":"","is-form":"",name:e.t("files_external","Storage credentials"),"out-transition":""},on:{submit:function(t){return e.$emit("close",{login:e.login,password:e.password})},"update:open":function(t){return e.$emit("close")}}},[t("NcNoteCard",{staticClass:"external-storage-auth__header",attrs:{text:e.t("files_external","To access the storage, you need to provide the authentication credentials."),type:"info"}}),e._v(" "),t("NcTextField",{ref:"login",staticClass:"external-storage-auth__login",attrs:{"data-cy-external-storage-auth-dialog-login":"",label:e.t("files_external","Login"),placeholder:e.t("files_external","Enter the storage login"),minlength:"2",name:"login",required:""},model:{value:e.login,callback:function(t){e.login=t},expression:"login"}}),e._v(" "),t("NcPasswordField",{ref:"password",staticClass:"external-storage-auth__password",attrs:{"data-cy-external-storage-auth-dialog-password":"",label:e.t("files_external","Password"),placeholder:e.t("files_external","Enter the storage password"),name:"password",required:""},model:{value:e.password,callback:function(t){e.password=t},expression:"password"}})],1)},[],!1,null,null,null).exports}}]);
|
||||
//# sourceMappingURL=1261-1261.js.map?v=e5911953e2704a8c51c4
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"6358-6358.js?v=32b8826edce0bc146e33","mappings":"kKAAA,I,gEAMA,MCNiQ,GDMlPA,EAAAA,EAAAA,IAAgB,CAC3BC,KAAM,oBACNC,WAAY,CACRC,SAAQ,IACRC,WAAU,IACVC,YAAW,IACXC,gBAAeA,EAAAA,GAEnBC,MAAKA,KACM,CACHC,EAACA,EAAAA,IAGTC,KAAIA,KACO,CACHC,MAAO,GACPC,SAAU,KAGlBC,SAAU,CACNC,cAAaA,IACF,CAAC,CACAC,OAAON,EAAAA,EAAAA,GAAE,iBAAkB,WAC3BO,KAAM,SACNC,QAAS,eEZ7B,GAXgB,E,SAAA,GACd,EFRW,WAAkB,IAAIC,EAAIC,KAAKC,EAAGF,EAAIG,MAAMD,GAAgC,OAAtBF,EAAIG,MAAMC,YAAmBF,EAAG,WAAW,CAACG,YAAY,wBAAwBC,MAAM,CAAC,QAAUN,EAAIJ,cAAc,yBAAyB,GAAG,gCAAgC,GAAG,UAAU,GAAG,KAAOI,EAAIT,EAAE,iBAAkB,uBAAuB,iBAAiB,IAAIgB,GAAG,CAAC,OAAS,SAASC,GAAQ,OAAOR,EAAIS,MAAM,QAAS,CAAEhB,MAAOO,EAAIP,MAAOC,SAAUM,EAAIN,UAAW,EAAE,cAAc,SAASc,GAAQ,OAAOR,EAAIS,MAAM,QAAQ,IAAI,CAACP,EAAG,aAAa,CAACG,YAAY,gCAAgCC,MAAM,CAAC,KAAON,EAAIT,EAAE,iBAAkB,8EAA8E,KAAO,UAAUS,EAAIU,GAAG,KAAKR,EAAG,cAAc,CAACS,IAAI,QAAQN,YAAY,+BAA+BC,MAAM,CAAC,6CAA6C,GAAG,MAAQN,EAAIT,EAAE,iBAAkB,SAAS,YAAcS,EAAIT,EAAE,iBAAkB,2BAA2B,UAAY,IAAI,KAAO,QAAQ,SAAW,IAAIqB,MAAM,CAACC,MAAOb,EAAIP,MAAOqB,SAAS,SAAUC,GAAMf,EAAIP,MAAMsB,CAAG,EAAEC,WAAW,WAAWhB,EAAIU,GAAG,KAAKR,EAAG,kBAAkB,CAACS,IAAI,WAAWN,YAAY,kCAAkCC,MAAM,CAAC,gDAAgD,GAAG,MAAQN,EAAIT,EAAE,iBAAkB,YAAY,YAAcS,EAAIT,EAAE,iBAAkB,8BAA8B,KAAO,WAAW,SAAW,IAAIqB,MAAM,CAACC,MAAOb,EAAIN,SAAUoB,SAAS,SAAUC,GAAMf,EAAIN,SAASqB,CAAG,EAAEC,WAAW,eAAe,EACz7C,EACsB,IESpB,EACA,KACA,KACA,M","sources":["webpack:///nextcloud/apps/files_external/src/views/CredentialsDialog.vue","webpack:///nextcloud/apps/files_external/src/views/CredentialsDialog.vue?vue&type=script&lang=ts","webpack://nextcloud/./apps/files_external/src/views/CredentialsDialog.vue?7767"],"sourcesContent":["var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('NcDialog',{staticClass:\"external-storage-auth\",attrs:{\"buttons\":_vm.dialogButtons,\"close-on-click-outside\":\"\",\"data-cy-external-storage-auth\":\"\",\"is-form\":\"\",\"name\":_vm.t('files_external', 'Storage credentials'),\"out-transition\":\"\"},on:{\"submit\":function($event){return _vm.$emit('close', { login: _vm.login, password: _vm.password })},\"update:open\":function($event){return _vm.$emit('close')}}},[_c('NcNoteCard',{staticClass:\"external-storage-auth__header\",attrs:{\"text\":_vm.t('files_external', 'To access the storage, you need to provide the authentication credentials.'),\"type\":\"info\"}}),_vm._v(\" \"),_c('NcTextField',{ref:\"login\",staticClass:\"external-storage-auth__login\",attrs:{\"data-cy-external-storage-auth-dialog-login\":\"\",\"label\":_vm.t('files_external', 'Login'),\"placeholder\":_vm.t('files_external', 'Enter the storage login'),\"minlength\":\"2\",\"name\":\"login\",\"required\":\"\"},model:{value:(_vm.login),callback:function ($$v) {_vm.login=$$v},expression:\"login\"}}),_vm._v(\" \"),_c('NcPasswordField',{ref:\"password\",staticClass:\"external-storage-auth__password\",attrs:{\"data-cy-external-storage-auth-dialog-password\":\"\",\"label\":_vm.t('files_external', 'Password'),\"placeholder\":_vm.t('files_external', 'Enter the storage password'),\"name\":\"password\",\"required\":\"\"},model:{value:(_vm.password),callback:function ($$v) {_vm.password=$$v},expression:\"password\"}})],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/ts-loader/index.js??clonedRuleSet-4.use[1]!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./CredentialsDialog.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/ts-loader/index.js??clonedRuleSet-4.use[1]!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./CredentialsDialog.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CredentialsDialog.vue?vue&type=template&id=42ed5195\"\nimport script from \"./CredentialsDialog.vue?vue&type=script&lang=ts\"\nexport * from \"./CredentialsDialog.vue?vue&type=script&lang=ts\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports"],"names":["defineComponent","name","components","NcDialog","NcNoteCard","NcTextField","NcPasswordField","setup","t","data","login","password","computed","dialogButtons","label","type","variant","_vm","this","_c","_self","_setupProxy","staticClass","attrs","on","$event","$emit","_v","ref","model","value","callback","$$v","expression"],"ignoreList":[],"sourceRoot":""}
|
||||
{"version":3,"file":"1261-1261.js?v=e5911953e2704a8c51c4","mappings":"kKAAA,I,gEAMA,MCNiQ,GDMlPA,EAAAA,EAAAA,IAAgB,CAC3BC,KAAM,oBACNC,WAAY,CACRC,SAAQ,IACRC,WAAU,IACVC,YAAW,IACXC,gBAAeA,EAAAA,GAEnBC,MAAKA,KACM,CACHC,EAACA,EAAAA,IAGTC,KAAIA,KACO,CACHC,MAAO,GACPC,SAAU,KAGlBC,SAAU,CACNC,cAAaA,IACF,CAAC,CACAC,OAAON,EAAAA,EAAAA,GAAE,iBAAkB,WAC3BO,KAAM,SACNC,QAAS,eEZ7B,GAXgB,E,SAAA,GACd,EFRW,WAAkB,IAAIC,EAAIC,KAAKC,EAAGF,EAAIG,MAAMD,GAAgC,OAAtBF,EAAIG,MAAMC,YAAmBF,EAAG,WAAW,CAACG,YAAY,wBAAwBC,MAAM,CAAC,QAAUN,EAAIJ,cAAc,yBAAyB,GAAG,gCAAgC,GAAG,UAAU,GAAG,KAAOI,EAAIT,EAAE,iBAAkB,uBAAuB,iBAAiB,IAAIgB,GAAG,CAAC,OAAS,SAASC,GAAQ,OAAOR,EAAIS,MAAM,QAAS,CAAEhB,MAAOO,EAAIP,MAAOC,SAAUM,EAAIN,UAAW,EAAE,cAAc,SAASc,GAAQ,OAAOR,EAAIS,MAAM,QAAQ,IAAI,CAACP,EAAG,aAAa,CAACG,YAAY,gCAAgCC,MAAM,CAAC,KAAON,EAAIT,EAAE,iBAAkB,8EAA8E,KAAO,UAAUS,EAAIU,GAAG,KAAKR,EAAG,cAAc,CAACS,IAAI,QAAQN,YAAY,+BAA+BC,MAAM,CAAC,6CAA6C,GAAG,MAAQN,EAAIT,EAAE,iBAAkB,SAAS,YAAcS,EAAIT,EAAE,iBAAkB,2BAA2B,UAAY,IAAI,KAAO,QAAQ,SAAW,IAAIqB,MAAM,CAACC,MAAOb,EAAIP,MAAOqB,SAAS,SAAUC,GAAMf,EAAIP,MAAMsB,CAAG,EAAEC,WAAW,WAAWhB,EAAIU,GAAG,KAAKR,EAAG,kBAAkB,CAACS,IAAI,WAAWN,YAAY,kCAAkCC,MAAM,CAAC,gDAAgD,GAAG,MAAQN,EAAIT,EAAE,iBAAkB,YAAY,YAAcS,EAAIT,EAAE,iBAAkB,8BAA8B,KAAO,WAAW,SAAW,IAAIqB,MAAM,CAACC,MAAOb,EAAIN,SAAUoB,SAAS,SAAUC,GAAMf,EAAIN,SAASqB,CAAG,EAAEC,WAAW,eAAe,EACz7C,EACsB,IESpB,EACA,KACA,KACA,M","sources":["webpack:///nextcloud/apps/files_external/src/views/CredentialsDialog.vue","webpack:///nextcloud/apps/files_external/src/views/CredentialsDialog.vue?vue&type=script&lang=ts","webpack://nextcloud/./apps/files_external/src/views/CredentialsDialog.vue?7767"],"sourcesContent":["var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('NcDialog',{staticClass:\"external-storage-auth\",attrs:{\"buttons\":_vm.dialogButtons,\"close-on-click-outside\":\"\",\"data-cy-external-storage-auth\":\"\",\"is-form\":\"\",\"name\":_vm.t('files_external', 'Storage credentials'),\"out-transition\":\"\"},on:{\"submit\":function($event){return _vm.$emit('close', { login: _vm.login, password: _vm.password })},\"update:open\":function($event){return _vm.$emit('close')}}},[_c('NcNoteCard',{staticClass:\"external-storage-auth__header\",attrs:{\"text\":_vm.t('files_external', 'To access the storage, you need to provide the authentication credentials.'),\"type\":\"info\"}}),_vm._v(\" \"),_c('NcTextField',{ref:\"login\",staticClass:\"external-storage-auth__login\",attrs:{\"data-cy-external-storage-auth-dialog-login\":\"\",\"label\":_vm.t('files_external', 'Login'),\"placeholder\":_vm.t('files_external', 'Enter the storage login'),\"minlength\":\"2\",\"name\":\"login\",\"required\":\"\"},model:{value:(_vm.login),callback:function ($$v) {_vm.login=$$v},expression:\"login\"}}),_vm._v(\" \"),_c('NcPasswordField',{ref:\"password\",staticClass:\"external-storage-auth__password\",attrs:{\"data-cy-external-storage-auth-dialog-password\":\"\",\"label\":_vm.t('files_external', 'Password'),\"placeholder\":_vm.t('files_external', 'Enter the storage password'),\"name\":\"password\",\"required\":\"\"},model:{value:(_vm.password),callback:function ($$v) {_vm.password=$$v},expression:\"password\"}})],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/ts-loader/index.js??clonedRuleSet-6.use[1]!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./CredentialsDialog.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../../node_modules/babel-loader/lib/index.js!../../../../node_modules/ts-loader/index.js??clonedRuleSet-6.use[1]!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./CredentialsDialog.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CredentialsDialog.vue?vue&type=template&id=42ed5195\"\nimport script from \"./CredentialsDialog.vue?vue&type=script&lang=ts\"\nexport * from \"./CredentialsDialog.vue?vue&type=script&lang=ts\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports"],"names":["defineComponent","name","components","NcDialog","NcNoteCard","NcTextField","NcPasswordField","setup","t","data","login","password","computed","dialogButtons","label","type","variant","_vm","this","_c","_self","_setupProxy","staticClass","attrs","on","$event","$emit","_v","ref","model","value","callback","$$v","expression"],"ignoreList":[],"sourceRoot":""}
|
||||
1
dist/1261-1261.js.map.license
vendored
Symbolic link
1
dist/1261-1261.js.map.license
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
1261-1261.js.license
|
||||
4
dist/8741-8741.js → dist/1404-1404.js
vendored
4
dist/8741-8741.js → dist/1404-1404.js
vendored
|
|
@ -1,2 +1,2 @@
|
|||
"use strict";(globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[]).push([[8741],{35693(e,t,n){n.d(t,{A:()=>a});var o=n(71354),i=n.n(o),r=n(76314),s=n.n(r)()(i());s.push([e.id,"\n.note-to-recipient[data-v-086ca7fc] {\n\tmargin-inline: var(--row-height)\n}\n.note-to-recipient__text[data-v-086ca7fc] {\n\t/* respect new lines */\n\twhite-space: pre-line;\n}\n.note-to-recipient__heading[data-v-086ca7fc] {\n\tfont-weight: bold;\n}\n@media screen and (max-width: 512px) {\n.note-to-recipient[data-v-086ca7fc] {\n\t\tmargin-inline: var(--default-grid-baseline);\n}\n}\n","",{version:3,sources:["webpack://./apps/files_sharing/src/views/FilesHeaderNoteToRecipient.vue"],names:[],mappings:";AAwDA;CACA;AACA;AAEA;CACA,sBAAA;CACA,qBAAA;AACA;AAEA;CACA,iBAAA;AACA;AAEA;AACA;EACA,2CAAA;AACA;AACA",sourcesContent:["\x3c!--\n - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors\n - SPDX-License-Identifier: AGPL-3.0-or-later\n--\x3e\n<template>\n\t<NcNoteCard\n\t\tv-if=\"note.length > 0\"\n\t\tclass=\"note-to-recipient\"\n\t\ttype=\"info\">\n\t\t<p v-if=\"displayName\" class=\"note-to-recipient__heading\">\n\t\t\t{{ t('files_sharing', 'Note from') }}\n\t\t\t<NcUserBubble :user=\"user.id\" :display-name=\"user.displayName\" />\n\t\t</p>\n\t\t<p v-else class=\"note-to-recipient__heading\">\n\t\t\t{{ t('files_sharing', 'Note:') }}\n\t\t</p>\n\t\t<p class=\"note-to-recipient__text\" v-text=\"note\" />\n\t</NcNoteCard>\n</template>\n\n<script setup lang=\"ts\">\nimport type { Folder } from '@nextcloud/files'\n\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { t } from '@nextcloud/l10n'\nimport { computed, ref } from 'vue'\nimport NcNoteCard from '@nextcloud/vue/components/NcNoteCard'\nimport NcUserBubble from '@nextcloud/vue/components/NcUserBubble'\n\nconst folder = ref<Folder>()\nconst note = computed<string>(() => folder.value?.attributes.note ?? '')\nconst displayName = computed<string>(() => folder.value?.attributes['owner-display-name'] ?? '')\nconst user = computed(() => {\n\tconst id = folder.value?.owner\n\tif (id !== getCurrentUser()?.uid) {\n\t\treturn {\n\t\t\tid,\n\t\t\tdisplayName: displayName.value,\n\t\t}\n\t}\n\treturn null\n})\n\n/**\n * Update the current folder\n *\n * @param newFolder the new folder to show note for\n */\nfunction updateFolder(newFolder: Folder) {\n\tfolder.value = newFolder\n}\n\ndefineExpose({ updateFolder })\n<\/script>\n\n<style scoped>\n.note-to-recipient {\n\tmargin-inline: var(--row-height)\n}\n\n.note-to-recipient__text {\n\t/* respect new lines */\n\twhite-space: pre-line;\n}\n\n.note-to-recipient__heading {\n\tfont-weight: bold;\n}\n\n@media screen and (max-width: 512px) {\n\t.note-to-recipient {\n\t\tmargin-inline: var(--default-grid-baseline);\n\t}\n}\n</style>\n"],sourceRoot:""}]);const a=s},38741(e,t,n){n.d(t,{default:()=>x});var o=n(85471),i=n(21777),r=n(53334),s=n(371),a=n(77764);const l=(0,o.pM)({__name:"FilesHeaderNoteToRecipient",setup(e,{expose:t}){const n=(0,o.KR)(),l=(0,o.EW)(()=>n.value?.attributes.note??""),c=(0,o.EW)(()=>n.value?.attributes["owner-display-name"]??""),d=(0,o.EW)(()=>{const e=n.value?.owner;return e!==(0,i.HW)()?.uid?{id:e,displayName:c.value}:null});function p(e){n.value=e}return t({updateFolder:p}),{__sfc:!0,folder:n,note:l,displayName:c,user:d,updateFolder:p,t:r.t,NcNoteCard:s.A,NcUserBubble:a.A}}});var c=n(85072),d=n.n(c),p=n(97825),u=n.n(p),A=n(77659),m=n.n(A),f=n(55056),_=n.n(f),h=n(10540),g=n.n(h),v=n(41113),N=n.n(v),C=n(35693),b={};b.styleTagTransform=N(),b.setAttributes=_(),b.insert=m().bind(null,"head"),b.domAPI=u(),b.insertStyleElement=g(),d()(C.A,b),C.A&&C.A.locals&&C.A.locals;const x=(0,n(14486).A)(l,function(){var e=this,t=e._self._c,n=e._self._setupProxy;return n.note.length>0?t(n.NcNoteCard,{staticClass:"note-to-recipient",attrs:{type:"info"}},[n.displayName?t("p",{staticClass:"note-to-recipient__heading"},[e._v("\n\t\t"+e._s(n.t("files_sharing","Note from"))+"\n\t\t"),t(n.NcUserBubble,{attrs:{user:n.user.id,"display-name":n.user.displayName}})],1):t("p",{staticClass:"note-to-recipient__heading"},[e._v("\n\t\t"+e._s(n.t("files_sharing","Note:"))+"\n\t")]),e._v(" "),t("p",{staticClass:"note-to-recipient__text",domProps:{textContent:e._s(n.note)}})]):e._e()},[],!1,null,"086ca7fc",null).exports}}]);
|
||||
//# sourceMappingURL=8741-8741.js.map?v=77f54eaf74efd547e7d3
|
||||
"use strict";(globalThis.webpackChunknextcloud_ui_legacy=globalThis.webpackChunknextcloud_ui_legacy||[]).push([[1404],{35693(e,t,n){n.d(t,{A:()=>a});var o=n(71354),i=n.n(o),r=n(76314),s=n.n(r)()(i());s.push([e.id,"\n.note-to-recipient[data-v-086ca7fc] {\n\tmargin-inline: var(--row-height)\n}\n.note-to-recipient__text[data-v-086ca7fc] {\n\t/* respect new lines */\n\twhite-space: pre-line;\n}\n.note-to-recipient__heading[data-v-086ca7fc] {\n\tfont-weight: bold;\n}\n@media screen and (max-width: 512px) {\n.note-to-recipient[data-v-086ca7fc] {\n\t\tmargin-inline: var(--default-grid-baseline);\n}\n}\n","",{version:3,sources:["webpack://./apps/files_sharing/src/views/FilesHeaderNoteToRecipient.vue"],names:[],mappings:";AAwDA;CACA;AACA;AAEA;CACA,sBAAA;CACA,qBAAA;AACA;AAEA;CACA,iBAAA;AACA;AAEA;AACA;EACA,2CAAA;AACA;AACA",sourcesContent:["\x3c!--\n - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors\n - SPDX-License-Identifier: AGPL-3.0-or-later\n--\x3e\n<template>\n\t<NcNoteCard\n\t\tv-if=\"note.length > 0\"\n\t\tclass=\"note-to-recipient\"\n\t\ttype=\"info\">\n\t\t<p v-if=\"displayName\" class=\"note-to-recipient__heading\">\n\t\t\t{{ t('files_sharing', 'Note from') }}\n\t\t\t<NcUserBubble :user=\"user.id\" :display-name=\"user.displayName\" />\n\t\t</p>\n\t\t<p v-else class=\"note-to-recipient__heading\">\n\t\t\t{{ t('files_sharing', 'Note:') }}\n\t\t</p>\n\t\t<p class=\"note-to-recipient__text\" v-text=\"note\" />\n\t</NcNoteCard>\n</template>\n\n<script setup lang=\"ts\">\nimport type { Folder } from '@nextcloud/files'\n\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { t } from '@nextcloud/l10n'\nimport { computed, ref } from 'vue'\nimport NcNoteCard from '@nextcloud/vue/components/NcNoteCard'\nimport NcUserBubble from '@nextcloud/vue/components/NcUserBubble'\n\nconst folder = ref<Folder>()\nconst note = computed<string>(() => folder.value?.attributes.note ?? '')\nconst displayName = computed<string>(() => folder.value?.attributes['owner-display-name'] ?? '')\nconst user = computed(() => {\n\tconst id = folder.value?.owner\n\tif (id !== getCurrentUser()?.uid) {\n\t\treturn {\n\t\t\tid,\n\t\t\tdisplayName: displayName.value,\n\t\t}\n\t}\n\treturn null\n})\n\n/**\n * Update the current folder\n *\n * @param newFolder the new folder to show note for\n */\nfunction updateFolder(newFolder: Folder) {\n\tfolder.value = newFolder\n}\n\ndefineExpose({ updateFolder })\n<\/script>\n\n<style scoped>\n.note-to-recipient {\n\tmargin-inline: var(--row-height)\n}\n\n.note-to-recipient__text {\n\t/* respect new lines */\n\twhite-space: pre-line;\n}\n\n.note-to-recipient__heading {\n\tfont-weight: bold;\n}\n\n@media screen and (max-width: 512px) {\n\t.note-to-recipient {\n\t\tmargin-inline: var(--default-grid-baseline);\n\t}\n}\n</style>\n"],sourceRoot:""}]);const a=s},41404(e,t,n){n.d(t,{default:()=>x});var o=n(85471),i=n(21777),r=n(53334),s=n(371),a=n(77764);const l=(0,o.pM)({__name:"FilesHeaderNoteToRecipient",setup(e,{expose:t}){const n=(0,o.KR)(),l=(0,o.EW)(()=>n.value?.attributes.note??""),c=(0,o.EW)(()=>n.value?.attributes["owner-display-name"]??""),d=(0,o.EW)(()=>{const e=n.value?.owner;return e!==(0,i.HW)()?.uid?{id:e,displayName:c.value}:null});function p(e){n.value=e}return t({updateFolder:p}),{__sfc:!0,folder:n,note:l,displayName:c,user:d,updateFolder:p,t:r.t,NcNoteCard:s.A,NcUserBubble:a.A}}});var c=n(85072),d=n.n(c),p=n(97825),u=n.n(p),A=n(77659),m=n.n(A),f=n(55056),_=n.n(f),h=n(10540),g=n.n(h),v=n(41113),N=n.n(v),C=n(35693),b={};b.styleTagTransform=N(),b.setAttributes=_(),b.insert=m().bind(null,"head"),b.domAPI=u(),b.insertStyleElement=g(),d()(C.A,b),C.A&&C.A.locals&&C.A.locals;const x=(0,n(14486).A)(l,function(){var e=this,t=e._self._c,n=e._self._setupProxy;return n.note.length>0?t(n.NcNoteCard,{staticClass:"note-to-recipient",attrs:{type:"info"}},[n.displayName?t("p",{staticClass:"note-to-recipient__heading"},[e._v("\n\t\t"+e._s(n.t("files_sharing","Note from"))+"\n\t\t"),t(n.NcUserBubble,{attrs:{user:n.user.id,"display-name":n.user.displayName}})],1):t("p",{staticClass:"note-to-recipient__heading"},[e._v("\n\t\t"+e._s(n.t("files_sharing","Note:"))+"\n\t")]),e._v(" "),t("p",{staticClass:"note-to-recipient__text",domProps:{textContent:e._s(n.note)}})]):e._e()},[],!1,null,"086ca7fc",null).exports}}]);
|
||||
//# sourceMappingURL=1404-1404.js.map?v=e021afe5d02634220086
|
||||
File diff suppressed because one or more lines are too long
1
dist/1404-1404.js.map.license
vendored
Symbolic link
1
dist/1404-1404.js.map.license
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
1404-1404.js.license
|
||||
1
dist/1543-1543.js.map.license
vendored
1
dist/1543-1543.js.map.license
vendored
|
|
@ -1 +0,0 @@
|
|||
1543-1543.js.license
|
||||
2
dist/1879-1879.js
vendored
2
dist/1879-1879.js
vendored
File diff suppressed because one or more lines are too long
1
dist/1879-1879.js.map
vendored
1
dist/1879-1879.js.map
vendored
File diff suppressed because one or more lines are too long
1
dist/1879-1879.js.map.license
vendored
1
dist/1879-1879.js.map.license
vendored
|
|
@ -1 +0,0 @@
|
|||
1879-1879.js.license
|
||||
2
dist/23-23.js
vendored
Normal file
2
dist/23-23.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue