Merge pull request #40192 from nextcloud/feat/sharing-icon-bread

pull/40209/head
John Molakvoæ 9 months ago committed by GitHub
commit fe692f2c7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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(() => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save