@@ -19,7 +19,7 @@ | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
import { Permission, type Node, View, registerFileAction, FileAction } from '@nextcloud/files' | |||
import { Permission, type Node, View, registerFileAction, FileAction, FileType } from '@nextcloud/files' | |||
import { translate as t } from '@nextcloud/l10n' | |||
import InformationSvg from '@mdi/svg/svg/information-variant.svg?raw' | |||
@@ -51,7 +51,7 @@ export const action = new FileAction({ | |||
return (nodes[0].root?.startsWith('/files/') && nodes[0].permissions !== Permission.NONE) ?? false | |||
}, | |||
async exec(node: Node, view: View) { | |||
async exec(node: Node, view: View, dir: string) { | |||
try { | |||
// TODO: migrate Sidebar to use a Node instead | |||
await window.OCA.Files.Sidebar.open(node.path) | |||
@@ -60,7 +60,7 @@ export const action = new FileAction({ | |||
window.OCP.Files.Router.goToRoute( | |||
null, | |||
{ view: view.id, fileid: node.fileid }, | |||
{ dir: node.dirname }, | |||
{ dir }, | |||
true, | |||
) | |||
@@ -166,12 +166,15 @@ | |||
</template> | |||
<script lang='ts'> | |||
import type { PropType } from 'vue' | |||
import type { Node } from '@nextcloud/files' | |||
import { CancelablePromise } from 'cancelable-promise' | |||
import { debounce } from 'debounce' | |||
import { emit } from '@nextcloud/event-bus' | |||
import { extname } from 'path' | |||
import { generateUrl } from '@nextcloud/router' | |||
import { getFileActions, DefaultType, FileType, formatFileSize, Permission, NodeStatus } from '@nextcloud/files' | |||
import { getFileActions, DefaultType, FileType, formatFileSize, Permission, Folder, File } from '@nextcloud/files' | |||
import { showError, showSuccess } from '@nextcloud/dialogs' | |||
import { translate } from '@nextcloud/l10n' | |||
import { vOnClickOutside } from '@vueuse/components' | |||
@@ -186,7 +189,7 @@ import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' | |||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' | |||
import Vue from 'vue' | |||
import { ACTION_DETAILS } from '../actions/sidebarAction.ts' | |||
import { action as sidebarAction } from '../actions/sidebarAction.ts' | |||
import { hashCode } from '../utils/hashUtils.ts' | |||
import { isCachedPreview } from '../services/PreviewService.ts' | |||
import { useActionsMenuStore } from '../store/actionsmenu.ts' | |||
@@ -235,7 +238,7 @@ export default Vue.extend({ | |||
default: false, | |||
}, | |||
source: { | |||
type: Object, | |||
type: [Folder, File, Node] as PropType<Node>, | |||
required: true, | |||
}, | |||
index: { | |||
@@ -243,7 +246,7 @@ export default Vue.extend({ | |||
required: true, | |||
}, | |||
nodes: { | |||
type: Array, | |||
type: Array as PropType<Node[]>, | |||
required: true, | |||
}, | |||
filesListWidth: { | |||
@@ -295,7 +298,7 @@ export default Vue.extend({ | |||
currentDir() { | |||
// Remove any trailing slash but leave root slash | |||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1') | |||
return (this.$route?.query?.dir?.toString() || '/').replace(/^(.+)\/$/, '$1') | |||
}, | |||
currentFileId() { | |||
return this.$route.params.fileid || this.$route.query.fileid || null | |||
@@ -660,11 +663,10 @@ export default Vue.extend({ | |||
}, | |||
openDetailsIfAvailable(event) { | |||
const detailsAction = this.enabledActions.find(action => action.id === ACTION_DETAILS) | |||
if (detailsAction) { | |||
event.preventDefault() | |||
event.stopPropagation() | |||
detailsAction.exec(this.source, this.currentView) | |||
event.preventDefault() | |||
event.stopPropagation() | |||
if (sidebarAction?.enabled?.([this.source], this.currentView)) { | |||
sidebarAction.exec(this.source, this.currentView, this.currentDir) | |||
} | |||
}, | |||
@@ -67,11 +67,13 @@ | |||
</template> | |||
<script lang="ts"> | |||
import type { PropType } from 'vue' | |||
import type { Node } from '@nextcloud/files' | |||
import { translate, translatePlural } from '@nextcloud/l10n' | |||
import { getFileListHeaders, type Node } from '@nextcloud/files' | |||
import { getFileListHeaders, Folder, View } from '@nextcloud/files' | |||
import { showError } from '@nextcloud/dialogs' | |||
import Vue from 'vue' | |||
import VirtualList from './VirtualList.vue' | |||
import { action as sidebarAction } from '../actions/sidebarAction.ts' | |||
import FileEntry from './FileEntry.vue' | |||
@@ -80,6 +82,7 @@ import FilesListTableFooter from './FilesListTableFooter.vue' | |||
import FilesListTableHeader from './FilesListTableHeader.vue' | |||
import filesListWidthMixin from '../mixins/filesListWidth.ts' | |||
import logger from '../logger.js' | |||
import VirtualList from './VirtualList.vue' | |||
export default Vue.extend({ | |||
name: 'FilesListVirtual', | |||
@@ -97,15 +100,15 @@ export default Vue.extend({ | |||
props: { | |||
currentView: { | |||
type: Object, | |||
type: View, | |||
required: true, | |||
}, | |||
currentFolder: { | |||
type: Object, | |||
type: Folder, | |||
required: true, | |||
}, | |||
nodes: { | |||
type: Array, | |||
type: Array as PropType<Node[]>, | |||
required: true, | |||
}, | |||
}, | |||
@@ -179,7 +182,7 @@ export default Vue.extend({ | |||
const node = this.nodes.find(n => n.fileid === this.fileId) as Node | |||
if (node && sidebarAction?.enabled?.([node], this.currentView)) { | |||
logger.debug('Opening sidebar on file ' + node.path, { node }) | |||
sidebarAction.exec(node, this.currentView, this.currentFolder) | |||
sidebarAction.exec(node, this.currentView, this.currentFolder.path) | |||
} | |||
} | |||
}, |
@@ -69,7 +69,7 @@ const entry = { | |||
iconSvgInline: FolderPlusSvg, | |||
async handler(context: Folder, content: Node[]) { | |||
const contentNames = content.map((node: Node) => node.basename) | |||
const name = getUniqueName(t('files', 'New Folder'), contentNames) | |||
const name = getUniqueName(t('files', 'New folder'), contentNames) | |||
const { fileid, source } = await createNewFolder(context.source, name) | |||
// Create the folder in the store |
@@ -25,8 +25,20 @@ | |||
<!-- Current folder breadcrumbs --> | |||
<BreadCrumbs :path="dir" @reload="fetchContent"> | |||
<template #actions> | |||
<NcButton v-if="canShare" | |||
:aria-label="shareButtonLabel" | |||
:class="{ 'files-list__header-share-button--shared': shareButtonType }" | |||
:title="shareButtonLabel" | |||
class="files-list__header-share-button" | |||
type="tertiary" | |||
@click="openSharingSidebar"> | |||
<template #icon> | |||
<LinkIcon v-if="shareButtonType === Type.SHARE_TYPE_LINK" /> | |||
<ShareVariantIcon v-else :size="20" /> | |||
</template> | |||
</NcButton> | |||
<!-- Uploader --> | |||
<UploadPicker v-if="currentFolder" | |||
<UploadPicker v-if="currentFolder && canUpload" | |||
:content="dirContents" | |||
:destination="currentFolder" | |||
:multiple="true" | |||
@@ -77,18 +89,24 @@ import type { Upload } from '@nextcloud/upload' | |||
import type { UserConfig } from '../types.ts' | |||
import type { View, ContentsWithRoot } from '@nextcloud/files' | |||
import { Folder, Node } from '@nextcloud/files' | |||
import { Folder, Node, Permission } from '@nextcloud/files' | |||
import { getCapabilities } from '@nextcloud/capabilities' | |||
import { join, dirname } from 'path' | |||
import { orderBy } from 'natural-orderby' | |||
import { translate } from '@nextcloud/l10n' | |||
import { UploadPicker } from '@nextcloud/upload' | |||
import { Type } from '@nextcloud/sharing' | |||
import Vue from 'vue' | |||
import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js' | |||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' | |||
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' | |||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' | |||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' | |||
import Vue from 'vue' | |||
import LinkIcon from 'vue-material-design-icons/Link.vue' | |||
import ShareVariantIcon from 'vue-material-design-icons/ShareVariant.vue' | |||
import { action as sidebarAction } from '../actions/sidebarAction.ts' | |||
import { useFilesStore } from '../store/files.ts' | |||
import { usePathsStore } from '../store/paths.ts' | |||
import { useSelectionStore } from '../store/selection.ts' | |||
@@ -100,17 +118,21 @@ import FilesListVirtual from '../components/FilesListVirtual.vue' | |||
import filesSortingMixin from '../mixins/filesSorting.ts' | |||
import logger from '../logger.js' | |||
const isSharingEnabled = getCapabilities()?.files_sharing !== undefined | |||
export default Vue.extend({ | |||
name: 'FilesList', | |||
components: { | |||
BreadCrumbs, | |||
FilesListVirtual, | |||
LinkIcon, | |||
NcAppContent, | |||
NcButton, | |||
NcEmptyContent, | |||
NcIconSvgWrapper, | |||
NcLoadingIcon, | |||
ShareVariantIcon, | |||
UploadPicker, | |||
}, | |||
@@ -139,6 +161,7 @@ export default Vue.extend({ | |||
return { | |||
loading: true, | |||
promise: null, | |||
Type, | |||
} | |||
}, | |||
@@ -157,7 +180,7 @@ export default Vue.extend({ | |||
*/ | |||
dir(): string { | |||
// Remove any trailing slash but leave root slash | |||
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1') | |||
return (this.$route?.query?.dir?.toString() || '/').replace(/^(.+)\/$/, '$1') | |||
}, | |||
/** | |||
@@ -242,6 +265,43 @@ export default Vue.extend({ | |||
const dir = this.dir.split('/').slice(0, -1).join('/') || '/' | |||
return { ...this.$route, query: { dir } } | |||
}, | |||
shareAttributes(): number[]|undefined { | |||
if (!this.currentFolder?.attributes?.['share-types']) { | |||
return undefined | |||
} | |||
return Object.values(this.currentFolder?.attributes?.['share-types'] || {}).flat() as number[] | |||
}, | |||
shareButtonLabel() { | |||
if (!this.shareAttributes) { | |||
return this.t('files', 'Share') | |||
} | |||
if (this.shareButtonType === Type.SHARE_TYPE_LINK) { | |||
return this.t('files', 'Shared by link') | |||
} | |||
return this.t('files', 'Shared') | |||
}, | |||
shareButtonType(): Type|null { | |||
if (!this.shareAttributes) { | |||
return null | |||
} | |||
// If all types are links, show the link icon | |||
if (this.shareAttributes.some(type => type === Type.SHARE_TYPE_LINK)) { | |||
return Type.SHARE_TYPE_LINK | |||
} | |||
return Type.SHARE_TYPE_USER | |||
}, | |||
canUpload() { | |||
return this.currentFolder && (this.currentFolder.permissions & Permission.CREATE) !== 0 | |||
}, | |||
canShare() { | |||
return isSharingEnabled | |||
&& this.currentFolder && (this.currentFolder.permissions & Permission.SHARE) !== 0 | |||
}, | |||
}, | |||
watch: { | |||
@@ -348,6 +408,13 @@ export default Vue.extend({ | |||
} | |||
}, | |||
openSharingSidebar() { | |||
if (window?.OCA?.Files?.Sidebar?.setActiveTab) { | |||
window.OCA.Files.Sidebar.setActiveTab('sharing') | |||
} | |||
sidebarAction.exec(this.currentFolder, this.currentView, this.currentFolder.path) | |||
}, | |||
t: translate, | |||
}, | |||
}) | |||
@@ -378,12 +445,21 @@ $navigationToggleSize: 50px; | |||
// Only the breadcrumbs shrinks | |||
flex: 0 0; | |||
} | |||
&-share-button { | |||
opacity: .3; | |||
&--shared { | |||
opacity: 1; | |||
} | |||
} | |||
} | |||
&__refresh-icon { | |||
flex: 0 0 44px; | |||
width: 44px; | |||
height: 44px; | |||
} | |||
&__loading-icon { | |||
margin: auto; | |||
} |
@@ -28,6 +28,7 @@ declare(strict_types=1); | |||
namespace OCA\ShareByMail; | |||
use OCA\ShareByMail\Settings\SettingsManager; | |||
use OCP\App\IAppManager; | |||
use OCP\Capabilities\ICapability; | |||
use OCP\Share\IManager; | |||
@@ -39,10 +40,15 @@ class Capabilities implements ICapability { | |||
/** @var SettingsManager */ | |||
private $settingsManager; | |||
/** @var IAppManager */ | |||
private $appManager; | |||
public function __construct(IManager $manager, | |||
SettingsManager $settingsManager) { | |||
SettingsManager $settingsManager, | |||
IAppManager $appManager) { | |||
$this->manager = $manager; | |||
$this->settingsManager = $settingsManager; | |||
$this->appManager = $appManager; | |||
} | |||
/** | |||
@@ -64,9 +70,12 @@ class Capabilities implements ICapability { | |||
* }, | |||
* } | |||
* } | |||
* } | |||
* }|array<empty> | |||
*/ | |||
public function getCapabilities(): array { | |||
if (!$this->appManager->isEnabledForUser('files_sharing')) { | |||
return []; | |||
} | |||
return [ | |||
'files_sharing' => | |||
[ |
@@ -27,6 +27,7 @@ namespace OCA\ShareByMail\Tests; | |||
use OCA\ShareByMail\Capabilities; | |||
use OCA\ShareByMail\Settings\SettingsManager; | |||
use OCP\App\IAppManager; | |||
use OCP\Share\IManager; | |||
use Test\TestCase; | |||
@@ -40,13 +41,17 @@ class CapabilitiesTest extends TestCase { | |||
/** @var IManager | \PHPUnit\Framework\MockObject\MockObject */ | |||
private $settingsManager; | |||
/** @var IAppManager | \PHPUnit\Framework\MockObject\MockObject */ | |||
private $appManager; | |||
protected function setUp(): void { | |||
parent::setUp(); | |||
$this->manager = $this::createMock(IManager::class); | |||
$this->settingsManager = $this::createMock(SettingsManager::class); | |||
$this->capabilities = new Capabilities($this->manager, $this->settingsManager); | |||
$this->appManager = $this::createMock(IAppManager::class); | |||
$this->capabilities = new Capabilities($this->manager, $this->settingsManager, $this->appManager); | |||
} | |||
public function testGetCapabilities() { | |||
@@ -58,6 +63,8 @@ class CapabilitiesTest extends TestCase { | |||
->willReturn(false); | |||
$this->settingsManager->method('sendPasswordByMail') | |||
->willReturn(true); | |||
$this->appManager->method('isEnabledForUser') | |||
->willReturn(true); | |||
$capabilities = [ | |||
'files_sharing' => |
@@ -38,6 +38,7 @@ describe('Versions creation', () => { | |||
}) | |||
it('Opens the versions panel and sees the versions', () => { | |||
cy.visit('/apps/files') | |||
openVersionsPanel(randomFileName) | |||
cy.get('#tab-version_vue').within(() => { |