feat(files_sharing): provide web-components based sidebar API

This fixes apps providing vue components, which is invalid and does not
always work - and never work with Vue 3.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/54788/head
Ferdinand Thiessen 1 month ago committed by Carl Schwan
parent c5a093d842
commit ee962f3a37
  1. 32
      apps/files_sharing/src/components/SharingEntryLink.vue
  2. 71
      apps/files_sharing/src/components/SidebarTabExternal/SidebarTabExternalAction.vue
  3. 8
      apps/files_sharing/src/components/SidebarTabExternal/SidebarTabExternalActionLegacy.vue
  4. 33
      apps/files_sharing/src/components/SidebarTabExternal/SidebarTabExternalSection.vue
  5. 33
      apps/files_sharing/src/components/SidebarTabExternal/SidebarTabExternalSectionLegacy.vue
  6. 8
      apps/files_sharing/src/services/ExternalLinkActions.js
  7. 10
      apps/files_sharing/src/services/ExternalShareActions.js
  8. 44
      apps/files_sharing/src/views/SharingDetailsTab.vue
  9. 49
      apps/files_sharing/src/views/SharingTab.vue

@ -159,7 +159,16 @@
<NcActionSeparator />
<!-- external actions -->
<ExternalShareAction v-for="action in externalLinkActions"
<NcActionButton v-for="action in sortedExternalShareActions"
:key="action.id"
@click="action.exec(share, fileInfo.node)">
<template #icon>
<NcIconSvgWrapper :svg="action.iconSvg" />
</template>
{{ action.label(share, fileInfo.node) }}
</NcActionButton>
<SidebarTabExternalActionLegacy v-for="action in externalLegacyShareActions"
:id="action.id"
:key="action.id"
:action="action"
@ -230,6 +239,8 @@ import { t } from '@nextcloud/l10n'
import moment from '@nextcloud/moment'
import { generateUrl, getBaseUrl } from '@nextcloud/router'
import { ShareType } from '@nextcloud/sharing'
import { getSidebarInlineActions } from '@nextcloud/sharing/ui'
import { toRaw } from 'vue'
import VueQrcode from '@chenfengyuan/vue-qrcode'
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
@ -255,8 +266,8 @@ import PlusIcon from 'vue-material-design-icons/Plus.vue'
import SharingEntryQuickShareSelect from './SharingEntryQuickShareSelect.vue'
import ShareExpiryTime from './ShareExpiryTime.vue'
import SidebarTabExternalActionLegacy from './SidebarTabExternal/SidebarTabExternalActionLegacy.vue'
import ExternalShareAction from './ExternalShareAction.vue'
import GeneratePassword from '../utils/GeneratePassword.ts'
import Share from '../models/Share.ts'
import SharesMixin from '../mixins/SharesMixin.js'
@ -267,7 +278,6 @@ export default {
name: 'SharingEntryLink',
components: {
ExternalShareAction,
NcActions,
NcActionButton,
NcActionCheckbox,
@ -290,6 +300,7 @@ export default {
PlusIcon,
SharingEntryQuickShareSelect,
ShareExpiryTime,
SidebarTabExternalActionLegacy,
},
mixins: [SharesMixin, ShareDetails],
@ -323,6 +334,7 @@ export default {
ExternalLegacyLinkActions: OCA.Sharing.ExternalLinkActions.state,
ExternalShareActions: OCA.Sharing.ExternalShareActions.state,
externalShareActions: getSidebarInlineActions(),
// tracks whether modal should be opened or not
showQRCode: false,
@ -568,13 +580,25 @@ export default {
*
* @return {Array}
*/
externalLinkActions() {
externalLegacyShareActions() {
const filterValidAction = (action) => (action.shareType.includes(ShareType.Link) || action.shareType.includes(ShareType.Email)) && !action.advanced
// filter only the registered actions for said link
console.error('external legacy', this.ExternalShareActions, this.ExternalShareActions.actions.filter(filterValidAction))
return this.ExternalShareActions.actions
.filter(filterValidAction)
},
/**
* Additional actions for the menu
*
* @return {import('@nextcloud/sharing/ui').ISidebarInlineAction[]}
*/
sortedExternalShareActions() {
return this.externalShareActions
.filter((action) => action.enabled(toRaw(this.share), toRaw(this.fileInfo.node)))
.sort((a, b) => a.order - b.order)
},
isPasswordPolicyEnabled() {
return typeof this.config.passwordPolicy === 'object'
},

@ -0,0 +1,71 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<component :is="action.element"
:key="action.id"
ref="actionElement"
:share.prop="share"
:node.prop="node"
:on-save.prop="onSave" />
</template>
<script lang="ts" setup>
import type { INode } from '@nextcloud/files'
import type { IShare } from '@nextcloud/sharing'
import type { ISidebarAction } from '@nextcloud/sharing/ui'
import type { PropType } from 'vue'
import { ref, toRaw, watchEffect } from 'vue'
const props = defineProps({
action: {
type: Object as PropType<ISidebarAction>,
required: true,
},
node: {
type: Object as PropType<INode>,
required: true,
},
share: {
type: Object as PropType<IShare | undefined>,
required: true,
},
})
defineExpose({ save })
const actionElement = ref()
const savingCallback = ref()
watchEffect(() => {
if (!actionElement.value) {
return
}
// This seems to be only needed in Vue 2 as the .prop modifier does not really work on the vue 2 version of web components
// TODO: Remove with Vue 3
actionElement.value.node = toRaw(props.node)
actionElement.value.onSave = onSave
actionElement.value.share = toRaw(props.share)
})
/**
* The share is reset thus save the state of the component.
*/
async function save() {
await savingCallback.value?.()
}
/**
* Vue does not allow to call methods on wrapped web components
* so we need to pass it per callback.
*
* @param callback - The callback to be called on save
*/
function onSave(callback: () => Promise<void>) {
savingCallback.value = callback
}
</script>

@ -4,18 +4,18 @@
-->
<template>
<Component :is="data.is"
<component :is="data.is"
v-bind="data"
v-on="action.handlers">
{{ data.text }}
</Component>
</component>
</template>
<script>
import Share from '../models/Share.ts'
import Share from '../../models/Share.ts'
export default {
name: 'ExternalShareAction',
name: 'SidebarTabExternalActionLegacy',
props: {
id: {

@ -0,0 +1,33 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<component :is="section.element" ref="sectionElement" :node.prop="node" />
</template>
<script lang="ts" setup>
import type { INode } from '@nextcloud/files'
import type { ISidebarSection } from '@nextcloud/sharing/ui'
import type { PropType } from 'vue'
import { ref, watchEffect } from 'vue'
const props = defineProps({
node: {
type: Object as PropType<INode>,
required: true,
},
section: {
type: Object as PropType<ISidebarSection>,
required: true,
},
})
// TOOD: Remove with Vue 3
const sectionElement = ref()
watchEffect(() => {
sectionElement.value.node = props.node
})
</script>

@ -0,0 +1,33 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="sharing-tab-external-section-legacy">
<component :is="component" :file-info="fileInfo" />
</div>
</template>
<script lang="ts" setup>
import { computed, type Component, type PropType } from 'vue'
const props = defineProps({
fileInfo: {
type: Object,
required: true,
},
sectionCallback: {
type: Function as PropType<(el: HTMLElement | undefined, fileInfo: unknown) => Component>,
required: true,
},
})
const component = computed(() => props.sectionCallback(undefined, props.fileInfo))
</script>
<style scoped>
.sharing-tab-external-section-legacy {
width: 100%;
}
</style>

@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import logger from './logger.ts'
export default class ExternalLinkActions {
_state
@ -13,7 +15,7 @@ export default class ExternalLinkActions {
// init default values
this._state.actions = []
console.debug('OCA.Sharing.ExternalLinkActions initialized')
logger.debug('OCA.Sharing.ExternalLinkActions initialized')
}
/**
@ -35,13 +37,13 @@ export default class ExternalLinkActions {
* @return {boolean}
*/
registerAction(action) {
OC.debug && console.warn('OCA.Sharing.ExternalLinkActions is deprecated, use OCA.Sharing.ExternalShareAction instead')
logger.warn('OCA.Sharing.ExternalLinkActions is deprecated, use `registerSidebarAction` from `@nextcloud/sharing` instead')
if (typeof action === 'object' && action.icon && action.name && action.url) {
this._state.actions.push(action)
return true
}
console.error('Invalid action provided', action)
logger.error('Invalid action provided', action)
return false
}

@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import logger from './logger.ts'
export default class ExternalShareActions {
_state
@ -13,7 +15,7 @@ export default class ExternalShareActions {
// init default values
this._state.actions = []
console.debug('OCA.Sharing.ExternalShareActions initialized')
logger.debug('OCA.Sharing.ExternalShareActions initialized')
}
/**
@ -44,6 +46,8 @@ export default class ExternalShareActions {
* @return {boolean}
*/
registerAction(action) {
logger.warn('OCA.Sharing.ExternalShareActions is deprecated, use `registerSidebarAction` from `@nextcloud/sharing` instead')
// Validate action
if (typeof action !== 'object'
|| typeof action.id !== 'string'
@ -51,14 +55,14 @@ export default class ExternalShareActions {
|| !Array.isArray(action.shareType) // [\@nextcloud/sharing.Types.Link, ...]
|| typeof action.handlers !== 'object' // {click: () => {}, ...}
|| !Object.values(action.handlers).every(handler => typeof handler === 'function')) {
console.error('Invalid action provided', action)
logger.error('Invalid action provided', action)
return false
}
// Check duplicates
const hasDuplicate = this._state.actions.findIndex(check => check.id === action.id) > -1
if (hasDuplicate) {
console.error(`An action with the same id ${action.id} already exists`, action)
logger.error(`An action with the same id ${action.id} already exists`, action)
return false
}

@ -187,13 +187,21 @@
:checked.sync="showInGridView">
{{ t('files_sharing', 'Show files in grid view') }}
</NcCheckboxRadioSwitch>
<ExternalShareAction v-for="action in externalLinkActions"
<SidebarTabExternalAction v-for="action in sortedExternalShareActions"
:key="action.id"
ref="externalShareActions"
:action="action"
:node="fileInfo.node /* TODO: Fix once we have proper Node API */"
:share="share" />
<SidebarTabExternalActionLegacy v-for="action in externalLegacyShareActions"
:id="action.id"
ref="externalLinkActions"
:key="action.id"
:action="action"
:file-info="fileInfo"
:share="share" />
<NcCheckboxRadioSwitch :checked.sync="setCustomPermissions">
{{ t('files_sharing', 'Custom permissions') }}
</NcCheckboxRadioSwitch>
@ -264,11 +272,13 @@
</template>
<script>
import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { getLanguage } from '@nextcloud/l10n'
import { ShareType } from '@nextcloud/sharing'
import { showError } from '@nextcloud/dialogs'
import { getSidebarActions } from '@nextcloud/sharing/ui'
import moment from '@nextcloud/moment'
import { toRaw } from 'vue'
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcButton from '@nextcloud/vue/components/NcButton'
@ -293,8 +303,8 @@ import MenuDownIcon from 'vue-material-design-icons/MenuDown.vue'
import MenuUpIcon from 'vue-material-design-icons/MenuUp.vue'
import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue'
import Refresh from 'vue-material-design-icons/Refresh.vue'
import ExternalShareAction from '../components/ExternalShareAction.vue'
import SidebarTabExternalAction from '../components/SidebarTabExternal/SidebarTabExternalAction.vue'
import SidebarTabExternalActionLegacy from '../components/SidebarTabExternal/SidebarTabExternalActionLegacy.vue'
import GeneratePassword from '../utils/GeneratePassword.ts'
import Share from '../models/Share.ts'
@ -323,7 +333,6 @@ export default {
CloseIcon,
CircleIcon,
EditIcon,
ExternalShareAction,
LinkIcon,
GroupIcon,
ShareIcon,
@ -334,7 +343,10 @@ export default {
MenuUpIcon,
DotsHorizontalIcon,
Refresh,
SidebarTabExternalAction,
SidebarTabExternalActionLegacy,
},
mixins: [ShareRequests, SharesMixin],
props: {
shareRequestValue: {
@ -365,6 +377,8 @@ export default {
initialToken: this.share.token,
loadingToken: false,
externalShareActions: getSidebarActions(),
// legacy
ExternalShareActions: OCA.Sharing.ExternalShareActions.state,
}
},
@ -754,8 +768,20 @@ export default {
*
* @return {Array}
*/
externalLinkActions() {
sortedExternalShareActions() {
return this.externalShareActions
.filter((action) => action.enabled(toRaw(this.share), toRaw(this.fileInfo.node)))
.sort((a, b) => a.order - b.order)
},
/**
* Additional actions for the menu
*
* @return {Array}
*/
externalLegacyShareActions() {
const filterValidAction = (action) => (action.shareType.includes(ShareType.Link) || action.shareType.includes(ShareType.Email)) && action.advanced
console.error('legacy details tab', this.ExternalShareActions, this.ExternalShareActions.actions.filter(filterValidAction))
// filter only the advanced registered actions for said link
return this.ExternalShareActions.actions
.filter(filterValidAction)
@ -1038,6 +1064,12 @@ export default {
await this.getNode()
emit('files:node:updated', this.node)
if (this.$refs.externalShareActions?.length > 0) {
/** @type {import('vue').ComponentPublicInstance<SidebarTabExternalAction>[]} */
const actions = this.$refs.externalShareActions
await Promise.allSettled(actions.map((action) => action.save()))
}
if (this.$refs.externalLinkActions?.length > 0) {
await Promise.allSettled(this.$refs.externalLinkActions.map((action) => {
if (typeof action.$children.at(0)?.onSave !== 'function') {

@ -6,7 +6,7 @@
<template>
<div class="sharingTab" :class="{ 'icon-loading': loading }">
<!-- error message -->
<div v-if="error" class="emptycontent" :class="{ emptyContentWithSections: sections.length > 0 }">
<div v-if="error" class="emptycontent" :class="{ emptyContentWithSections: hasExternalSections }">
<div class="icon icon-error" />
<h2>{{ error }}</h2>
</div>
@ -108,7 +108,7 @@
@open-sharing-details="toggleShareDetailsView" />
</section>
<section v-if="sections.length > 0 && !showSharingDetailsView">
<section v-if="hasExternalSections && !showSharingDetailsView">
<div class="section-header">
<h4>{{ t('files_sharing', 'Additional shares') }}</h4>
<NcPopover popup-role="dialog">
@ -127,11 +127,18 @@
</NcPopover>
</div>
<!-- additional entries, use it with cautious -->
<div v-for="(component, index) in sectionComponents"
<SidebarTabExternalSection v-for="section in sortedExternalSections"
:key="section.id"
:section="section"
:node="fileInfo.node /* TODO: Fix once we have proper Node API */"
class="sharingTab__additionalContent" />
<!-- legacy sections: TODO: Remove as soon as possible -->
<SidebarTabExternalSectionLegacy v-for="(section, index) in legacySections"
:key="index"
class="sharingTab__additionalContent">
<component :is="component" :file-info="fileInfo" />
</div>
:file-info="fileInfo"
:section-callback="section"
class="sharingTab__additionalContent" />
<!-- projects (deprecated as of NC25 (replaced by related_resources) - see instance config "projects.enabled" ; ignore this / remove it / move into own section) -->
<div v-if="projectsEnabled"
@ -161,6 +168,7 @@ import { orderBy } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import { generateOcsUrl } from '@nextcloud/router'
import { ShareType } from '@nextcloud/sharing'
import { getSidebarSections } from '@nextcloud/sharing/ui'
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcButton from '@nextcloud/vue/components/NcButton'
@ -183,6 +191,8 @@ import SharingInherited from './SharingInherited.vue'
import SharingLinkList from './SharingLinkList.vue'
import SharingList from './SharingList.vue'
import SharingDetailsTab from './SharingDetailsTab.vue'
import SidebarTabExternalSection from '../components/SidebarTabExternal/SidebarTabExternalSection.vue'
import SidebarTabExternalSectionLegacy from '../components/SidebarTabExternal/SidebarTabExternalSectionLegacy.vue'
import ShareDetails from '../mixins/ShareDetails.js'
import logger from '../services/logger.ts'
@ -205,6 +215,8 @@ export default {
SharingLinkList,
SharingList,
SharingDetailsTab,
SidebarTabExternalSection,
SidebarTabExternalSectionLegacy,
},
mixins: [ShareDetails],
@ -225,7 +237,9 @@ export default {
linkShares: [],
externalShares: [],
sections: OCA.Sharing.ShareTabSections.getSections(),
legacySections: OCA.Sharing.ShareTabSections.getSections(),
sections: getSidebarSections(),
projectsEnabled: loadState('core', 'projects_enabled', false),
showSharingDetailsView: false,
shareDetailsData: {},
@ -238,6 +252,21 @@ export default {
},
computed: {
/**
* Are any sections registered by other apps.
*
* @return {boolean}
*/
hasExternalSections() {
return this.sections.length > 0 || this.legacySections.length > 0
},
sortedExternalSections() {
return this.sections
.filter((section) => section.enabled(this.fileInfo.node))
.sort((a, b) => a.order - b.order)
},
/**
* Is this share shared with me?
*
@ -287,10 +316,6 @@ export default {
// TRANSLATORS: Type as in with a keyboard
: t('files_sharing', 'Type an email or federated cloud ID')
},
sectionComponents() {
return this.sections.map((section) => section(undefined, this.fileInfo))
},
},
methods: {
/**
@ -618,7 +643,7 @@ export default {
}
&__additionalContent {
margin: 44px 0;
margin: var(--default-clickable-area) 0;
}
}

Loading…
Cancel
Save