diff options
Diffstat (limited to 'core/src/components/PublicPageMenu')
5 files changed, 264 insertions, 0 deletions
diff --git a/core/src/components/PublicPageMenu/PublicPageMenuCustomEntry.vue b/core/src/components/PublicPageMenu/PublicPageMenuCustomEntry.vue new file mode 100644 index 00000000000..f3c57a12042 --- /dev/null +++ b/core/src/components/PublicPageMenu/PublicPageMenuCustomEntry.vue @@ -0,0 +1,36 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later + --> +<template> + <!-- eslint-disable-next-line vue/no-v-html --> + <li ref="listItem" :role="itemRole" v-html="html" /> +</template> + +<script setup lang="ts"> +import { onMounted, ref } from 'vue' + +defineProps<{ + id: string + html: string +}>() + +const listItem = ref<HTMLLIElement>() +const itemRole = ref('presentation') + +onMounted(() => { + // check for proper roles + const menuitem = listItem.value?.querySelector('[role="menuitem"]') + if (menuitem) { + return + } + // check if a button is available + const button = listItem.value?.querySelector('button') ?? listItem.value?.querySelector('a') + if (button) { + button.role = 'menuitem' + } else { + // if nothing is available set role on `<li>` + itemRole.value = 'menuitem' + } +}) +</script> diff --git a/core/src/components/PublicPageMenu/PublicPageMenuEntry.vue b/core/src/components/PublicPageMenu/PublicPageMenuEntry.vue new file mode 100644 index 00000000000..413806c7089 --- /dev/null +++ b/core/src/components/PublicPageMenu/PublicPageMenuEntry.vue @@ -0,0 +1,51 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later + --> +<template> + <NcListItem :anchor-id="`${id}--link`" + compact + :details="details" + :href="href" + :name="label" + role="presentation" + @click="$emit('click')"> + <template #icon> + <slot v-if="$scopedSlots.icon" name="icon" /> + <div v-else role="presentation" :class="['icon', icon, 'public-page-menu-entry__icon']" /> + </template> + </NcListItem> +</template> + +<script setup lang="ts"> +import { onMounted } from 'vue' + +import NcListItem from '@nextcloud/vue/components/NcListItem' + +const props = defineProps<{ + /** Only emit click event but do not open href */ + clickOnly?: boolean + // menu entry props + id: string + label: string + icon?: string + href: string + details?: string +}>() + +onMounted(() => { + const anchor = document.getElementById(`${props.id}--link`) as HTMLAnchorElement + // Make the `<a>` a menuitem + anchor.role = 'menuitem' + // Prevent native click handling if required + if (props.clickOnly) { + anchor.onclick = (event) => event.preventDefault() + } +}) +</script> + +<style scoped> +.public-page-menu-entry__icon { + padding-inline-start: var(--default-grid-baseline); +} +</style> diff --git a/core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue b/core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue new file mode 100644 index 00000000000..0f02bdf7524 --- /dev/null +++ b/core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue @@ -0,0 +1,90 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later + --> +<template> + <NcDialog is-form + :name="label" + :open.sync="open" + @submit="createFederatedShare"> + <NcTextField ref="input" + :label="t('core', 'Federated user')" + :placeholder="t('core', 'user@your-nextcloud.org')" + required + :value.sync="remoteUrl" /> + <template #actions> + <NcButton :disabled="loading" type="primary" native-type="submit"> + <template v-if="loading" #icon> + <NcLoadingIcon /> + </template> + {{ t('core', 'Create share') }} + </NcButton> + </template> + </NcDialog> +</template> + +<script setup lang="ts"> +import type Vue from 'vue' + +import { t } from '@nextcloud/l10n' +import { showError } from '@nextcloud/dialogs' +import { generateUrl } from '@nextcloud/router' +import { getSharingToken } from '@nextcloud/sharing/public' +import { nextTick, onMounted, ref, watch } from 'vue' +import axios from '@nextcloud/axios' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcDialog from '@nextcloud/vue/components/NcDialog' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import NcTextField from '@nextcloud/vue/components/NcTextField' +import logger from '../../logger' + +defineProps<{ + label: string +}>() + +const loading = ref(false) +const remoteUrl = ref('') +// Todo: @nextcloud/vue should expose the types correctly +const input = ref<Vue & { focus: () => void }>() +const open = ref(true) + +// Focus when mounted +onMounted(() => nextTick(() => input.value!.focus())) + +// Check validity +watch(remoteUrl, () => { + let validity = '' + if (!remoteUrl.value.includes('@')) { + validity = t('core', 'The remote URL must include the user.') + } else if (!remoteUrl.value.match(/@(.+\..{2,}|localhost)(:\d\d+)?$/)) { + validity = t('core', 'Invalid remote URL.') + } + input.value!.$el.querySelector('input')!.setCustomValidity(validity) + input.value!.$el.querySelector('input')!.reportValidity() +}) + +/** + * Create a federated share for the current share + */ +async function createFederatedShare() { + loading.value = true + + try { + const url = generateUrl('/apps/federatedfilesharing/createFederatedShare') + const { data } = await axios.post<{ remoteUrl: string }>(url, { + shareWith: remoteUrl.value, + token: getSharingToken(), + }) + if (data.remoteUrl.includes('://')) { + window.location.href = data.remoteUrl + } else { + window.location.href = `${window.location.protocol}//${data.remoteUrl}` + } + } catch (error) { + logger.error('Failed to create federated share', { error }) + showError(t('files_sharing', 'Failed to add the public link to your Nextcloud')) + } finally { + loading.value = false + } +} +</script> diff --git a/core/src/components/PublicPageMenu/PublicPageMenuExternalEntry.vue b/core/src/components/PublicPageMenu/PublicPageMenuExternalEntry.vue new file mode 100644 index 00000000000..a4451a38bbe --- /dev/null +++ b/core/src/components/PublicPageMenu/PublicPageMenuExternalEntry.vue @@ -0,0 +1,36 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later + --> +<template> + <PublicPageMenuEntry :id="id" + :icon="icon" + href="#" + :label="label" + @click="openDialog" /> +</template> + +<script setup lang="ts"> +import { spawnDialog } from '@nextcloud/dialogs' +import PublicPageMenuEntry from './PublicPageMenuEntry.vue' +import PublicPageMenuExternalDialog from './PublicPageMenuExternalDialog.vue' + +const props = defineProps<{ + id: string + label: string + icon: string + href: string +}>() + +const emit = defineEmits<{ + (e: 'click'): void +}>() + +/** + * Open the "create federated share" dialog + */ +function openDialog() { + spawnDialog(PublicPageMenuExternalDialog, { label: props.label }) + emit('click') +} +</script> diff --git a/core/src/components/PublicPageMenu/PublicPageMenuLinkEntry.vue b/core/src/components/PublicPageMenu/PublicPageMenuLinkEntry.vue new file mode 100644 index 00000000000..5f3a4883d6d --- /dev/null +++ b/core/src/components/PublicPageMenu/PublicPageMenuLinkEntry.vue @@ -0,0 +1,51 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later + --> +<template> + <PublicPageMenuEntry :id="id" + click-only + :icon="icon" + :href="href" + :label="label" + @click="onClick" /> +</template> + +<script setup lang="ts"> +import { showSuccess } from '@nextcloud/dialogs' +import { t } from '@nextcloud/l10n' +import PublicPageMenuEntry from './PublicPageMenuEntry.vue' + +const props = defineProps<{ + id: string + label: string + icon: string + href: string +}>() + +const emit = defineEmits<{ + (e: 'click'): void +}>() + +/** + * Copy the href to the clipboard + */ +async function copyLink() { + try { + await window.navigator.clipboard.writeText(props.href) + showSuccess(t('core', 'Direct link copied')) + } catch { + // No secure context -> fallback to dialog + window.prompt(t('core', 'Please copy the link manually:'), props.href) + } +} + +/** + * onclick handler to trigger the "copy link" action + * and emit the event so the menu can be closed + */ +function onClick() { + copyLink() + emit('click') +} +</script> |