Browse Source

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

tags/v28.0.0beta1
John Molakvoæ 9 months ago
parent
commit
fe692f2c7f
No account linked to committer's email address

+ 3
- 3
apps/files/src/actions/sidebarAction.ts View File

* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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 { translate as t } from '@nextcloud/l10n'
import InformationSvg from '@mdi/svg/svg/information-variant.svg?raw' import InformationSvg from '@mdi/svg/svg/information-variant.svg?raw'


return (nodes[0].root?.startsWith('/files/') && nodes[0].permissions !== Permission.NONE) ?? false 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 { try {
// TODO: migrate Sidebar to use a Node instead // TODO: migrate Sidebar to use a Node instead
await window.OCA.Files.Sidebar.open(node.path) await window.OCA.Files.Sidebar.open(node.path)
window.OCP.Files.Router.goToRoute( window.OCP.Files.Router.goToRoute(
null, null,
{ view: view.id, fileid: node.fileid }, { view: view.id, fileid: node.fileid },
{ dir: node.dirname },
{ dir },
true, true,
) )



+ 12
- 10
apps/files/src/components/FileEntry.vue View File

</template> </template>


<script lang='ts'> <script lang='ts'>
import type { PropType } from 'vue'
import type { Node } from '@nextcloud/files'

import { CancelablePromise } from 'cancelable-promise' import { CancelablePromise } from 'cancelable-promise'
import { debounce } from 'debounce' import { debounce } from 'debounce'
import { emit } from '@nextcloud/event-bus' import { emit } from '@nextcloud/event-bus'
import { extname } from 'path' import { extname } from 'path'
import { generateUrl } from '@nextcloud/router' 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 { showError, showSuccess } from '@nextcloud/dialogs'
import { translate } from '@nextcloud/l10n' import { translate } from '@nextcloud/l10n'
import { vOnClickOutside } from '@vueuse/components' import { vOnClickOutside } from '@vueuse/components'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import Vue from 'vue' 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 { hashCode } from '../utils/hashUtils.ts'
import { isCachedPreview } from '../services/PreviewService.ts' import { isCachedPreview } from '../services/PreviewService.ts'
import { useActionsMenuStore } from '../store/actionsmenu.ts' import { useActionsMenuStore } from '../store/actionsmenu.ts'
default: false, default: false,
}, },
source: { source: {
type: Object,
type: [Folder, File, Node] as PropType<Node>,
required: true, required: true,
}, },
index: { index: {
required: true, required: true,
}, },
nodes: { nodes: {
type: Array,
type: Array as PropType<Node[]>,
required: true, required: true,
}, },
filesListWidth: { filesListWidth: {


currentDir() { currentDir() {
// Remove any trailing slash but leave root slash // Remove any trailing slash but leave root slash
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
return (this.$route?.query?.dir?.toString() || '/').replace(/^(.+)\/$/, '$1')
}, },
currentFileId() { currentFileId() {
return this.$route.params.fileid || this.$route.query.fileid || null return this.$route.params.fileid || this.$route.query.fileid || null
}, },


openDetailsIfAvailable(event) { 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)
} }
}, },



+ 9
- 6
apps/files/src/components/FilesListVirtual.vue View File

</template> </template>


<script lang="ts"> <script lang="ts">
import type { PropType } from 'vue'
import type { Node } from '@nextcloud/files'

import { translate, translatePlural } from '@nextcloud/l10n' 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 { showError } from '@nextcloud/dialogs'
import Vue from 'vue' import Vue from 'vue'
import VirtualList from './VirtualList.vue'


import { action as sidebarAction } from '../actions/sidebarAction.ts' import { action as sidebarAction } from '../actions/sidebarAction.ts'
import FileEntry from './FileEntry.vue' import FileEntry from './FileEntry.vue'
import FilesListTableHeader from './FilesListTableHeader.vue' import FilesListTableHeader from './FilesListTableHeader.vue'
import filesListWidthMixin from '../mixins/filesListWidth.ts' import filesListWidthMixin from '../mixins/filesListWidth.ts'
import logger from '../logger.js' import logger from '../logger.js'
import VirtualList from './VirtualList.vue'


export default Vue.extend({ export default Vue.extend({
name: 'FilesListVirtual', name: 'FilesListVirtual',


props: { props: {
currentView: { currentView: {
type: Object,
type: View,
required: true, required: true,
}, },
currentFolder: { currentFolder: {
type: Object,
type: Folder,
required: true, required: true,
}, },
nodes: { nodes: {
type: Array,
type: Array as PropType<Node[]>,
required: true, required: true,
}, },
}, },
const node = this.nodes.find(n => n.fileid === this.fileId) as Node const node = this.nodes.find(n => n.fileid === this.fileId) as Node
if (node && sidebarAction?.enabled?.([node], this.currentView)) { if (node && sidebarAction?.enabled?.([node], this.currentView)) {
logger.debug('Opening sidebar on file ' + node.path, { node }) logger.debug('Opening sidebar on file ' + node.path, { node })
sidebarAction.exec(node, this.currentView, this.currentFolder)
sidebarAction.exec(node, this.currentView, this.currentFolder.path)
} }
} }
}, },

+ 1
- 1
apps/files/src/newMenu/newFolder.ts View File

iconSvgInline: FolderPlusSvg, iconSvgInline: FolderPlusSvg,
async handler(context: Folder, content: Node[]) { async handler(context: Folder, content: Node[]) {
const contentNames = content.map((node: Node) => node.basename) 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) const { fileid, source } = await createNewFolder(context.source, name)


// Create the folder in the store // Create the folder in the store

+ 80
- 4
apps/files/src/views/FilesList.vue View File

<!-- Current folder breadcrumbs --> <!-- Current folder breadcrumbs -->
<BreadCrumbs :path="dir" @reload="fetchContent"> <BreadCrumbs :path="dir" @reload="fetchContent">
<template #actions> <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 --> <!-- Uploader -->
<UploadPicker v-if="currentFolder"
<UploadPicker v-if="currentFolder && canUpload"
:content="dirContents" :content="dirContents"
:destination="currentFolder" :destination="currentFolder"
:multiple="true" :multiple="true"
import type { UserConfig } from '../types.ts' import type { UserConfig } from '../types.ts'
import type { View, ContentsWithRoot } from '@nextcloud/files' 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 { join, dirname } from 'path'
import { orderBy } from 'natural-orderby' import { orderBy } from 'natural-orderby'
import { translate } from '@nextcloud/l10n' import { translate } from '@nextcloud/l10n'
import { UploadPicker } from '@nextcloud/upload' import { UploadPicker } from '@nextcloud/upload'
import { Type } from '@nextcloud/sharing'
import Vue from 'vue'

import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js' import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.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 { useFilesStore } from '../store/files.ts'
import { usePathsStore } from '../store/paths.ts' import { usePathsStore } from '../store/paths.ts'
import { useSelectionStore } from '../store/selection.ts' import { useSelectionStore } from '../store/selection.ts'
import filesSortingMixin from '../mixins/filesSorting.ts' import filesSortingMixin from '../mixins/filesSorting.ts'
import logger from '../logger.js' import logger from '../logger.js'


const isSharingEnabled = getCapabilities()?.files_sharing !== undefined

export default Vue.extend({ export default Vue.extend({
name: 'FilesList', name: 'FilesList',


components: { components: {
BreadCrumbs, BreadCrumbs,
FilesListVirtual, FilesListVirtual,
LinkIcon,
NcAppContent, NcAppContent,
NcButton, NcButton,
NcEmptyContent, NcEmptyContent,
NcIconSvgWrapper, NcIconSvgWrapper,
NcLoadingIcon, NcLoadingIcon,
ShareVariantIcon,
UploadPicker, UploadPicker,
}, },


return { return {
loading: true, loading: true,
promise: null, promise: null,
Type,
} }
}, },


*/ */
dir(): string { dir(): string {
// Remove any trailing slash but leave root slash // Remove any trailing slash but leave root slash
return (this.$route?.query?.dir || '/').replace(/^(.+)\/$/, '$1')
return (this.$route?.query?.dir?.toString() || '/').replace(/^(.+)\/$/, '$1')
}, },


/** /**
const dir = this.dir.split('/').slice(0, -1).join('/') || '/' const dir = this.dir.split('/').slice(0, -1).join('/') || '/'
return { ...this.$route, query: { dir } } 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: { watch: {
} }
}, },


openSharingSidebar() {
if (window?.OCA?.Files?.Sidebar?.setActiveTab) {
window.OCA.Files.Sidebar.setActiveTab('sharing')
}
sidebarAction.exec(this.currentFolder, this.currentView, this.currentFolder.path)
},

t: translate, t: translate,
}, },
}) })
// Only the breadcrumbs shrinks // Only the breadcrumbs shrinks
flex: 0 0; flex: 0 0;
} }

&-share-button {
opacity: .3;
&--shared {
opacity: 1;
}
}
} }

&__refresh-icon { &__refresh-icon {
flex: 0 0 44px; flex: 0 0 44px;
width: 44px; width: 44px;
height: 44px; height: 44px;
} }

&__loading-icon { &__loading-icon {
margin: auto; margin: auto;
} }

+ 11
- 2
apps/sharebymail/lib/Capabilities.php View File

namespace OCA\ShareByMail; namespace OCA\ShareByMail;


use OCA\ShareByMail\Settings\SettingsManager; use OCA\ShareByMail\Settings\SettingsManager;
use OCP\App\IAppManager;
use OCP\Capabilities\ICapability; use OCP\Capabilities\ICapability;
use OCP\Share\IManager; use OCP\Share\IManager;


/** @var SettingsManager */ /** @var SettingsManager */
private $settingsManager; private $settingsManager;


/** @var IAppManager */
private $appManager;

public function __construct(IManager $manager, public function __construct(IManager $manager,
SettingsManager $settingsManager) {
SettingsManager $settingsManager,
IAppManager $appManager) {
$this->manager = $manager; $this->manager = $manager;
$this->settingsManager = $settingsManager; $this->settingsManager = $settingsManager;
$this->appManager = $appManager;
} }


/** /**
* }, * },
* } * }
* } * }
* }
* }|array<empty>
*/ */
public function getCapabilities(): array { public function getCapabilities(): array {
if (!$this->appManager->isEnabledForUser('files_sharing')) {
return [];
}
return [ return [
'files_sharing' => 'files_sharing' =>
[ [

+ 8
- 1
apps/sharebymail/tests/CapabilitiesTest.php View File



use OCA\ShareByMail\Capabilities; use OCA\ShareByMail\Capabilities;
use OCA\ShareByMail\Settings\SettingsManager; use OCA\ShareByMail\Settings\SettingsManager;
use OCP\App\IAppManager;
use OCP\Share\IManager; use OCP\Share\IManager;
use Test\TestCase; use Test\TestCase;


/** @var IManager | \PHPUnit\Framework\MockObject\MockObject */ /** @var IManager | \PHPUnit\Framework\MockObject\MockObject */
private $settingsManager; private $settingsManager;


/** @var IAppManager | \PHPUnit\Framework\MockObject\MockObject */
private $appManager;

protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();




$this->manager = $this::createMock(IManager::class); $this->manager = $this::createMock(IManager::class);
$this->settingsManager = $this::createMock(SettingsManager::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() { public function testGetCapabilities() {
->willReturn(false); ->willReturn(false);
$this->settingsManager->method('sendPasswordByMail') $this->settingsManager->method('sendPasswordByMail')
->willReturn(true); ->willReturn(true);
$this->appManager->method('isEnabledForUser')
->willReturn(true);


$capabilities = [ $capabilities = [
'files_sharing' => 'files_sharing' =>

+ 1
- 0
cypress/e2e/files_versions/version_creation.cy.ts View File

}) })


it('Opens the versions panel and sees the versions', () => { it('Opens the versions panel and sees the versions', () => {
cy.visit('/apps/files')
openVersionsPanel(randomFileName) openVersionsPanel(randomFileName)


cy.get('#tab-version_vue').within(() => { cy.get('#tab-version_vue').within(() => {

dist/7761-7761.js
File diff suppressed because it is too large
View File


dist/9872-9872.js.LICENSE.txt → dist/7761-7761.js.LICENSE.txt View File


+ 1
- 0
dist/7761-7761.js.map
File diff suppressed because it is too large
View File


+ 0
- 1
dist/9872-9872.js.map
File diff suppressed because it is too large
View File


+ 2
- 2
dist/core-common.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/core-common.js.map
File diff suppressed because it is too large
View File


+ 2
- 2
dist/federatedfilesharing-vue-settings-admin.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/federatedfilesharing-vue-settings-admin.js.map
File diff suppressed because it is too large
View File


+ 2
- 2
dist/files-main.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/files-main.js.map
File diff suppressed because it is too large
View File


+ 2
- 2
dist/files-personal-settings.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/files-personal-settings.js.map
File diff suppressed because it is too large
View File


+ 2
- 2
dist/files_sharing-files_sharing_tab.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/files_sharing-files_sharing_tab.js.map
File diff suppressed because it is too large
View File


+ 2
- 2
dist/updatenotification-updatenotification.js
File diff suppressed because it is too large
View File


+ 1
- 1
dist/updatenotification-updatenotification.js.map
File diff suppressed because it is too large
View File


Loading…
Cancel
Save