refactor(files_external): migrate files integration to script-setup and Vue 3

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2025-12-23 20:08:43 +01:00 committed by nextcloud-command
parent a5225bdf99
commit 5c652484e3
11 changed files with 84 additions and 107 deletions

View file

@ -10,12 +10,13 @@ import type { StorageConfig } from '../services/externalStorage.ts'
import LoginSvg from '@mdi/svg/svg/login.svg?raw'
import axios from '@nextcloud/axios'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { DefaultType, FileAction } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation'
import { generateUrl } from '@nextcloud/router'
import { spawnDialog } from '@nextcloud/vue/functions/dialog'
import Vue, { defineAsyncComponent } from 'vue'
import { defineAsyncComponent } from 'vue'
import { isMissingAuthConfig, STORAGE_STATUS } from '../utils/credentialsUtils.ts'
import { isNodeExternalStorage } from '../utils/externalStorageUtils.ts'
@ -23,11 +24,6 @@ import { isNodeExternalStorage } from '../utils/externalStorageUtils.ts'
// the backend requires the user to confirm their password
addPasswordConfirmationInterceptors(axios)
type CredentialResponse = {
login?: string
password?: string
}
/**
* Set credentials for external storage
*
@ -55,7 +51,9 @@ async function setCredentials(node: Node, login: string, password: string): Prom
// Success update config attribute
showSuccess(t('files_external', 'New configuration successfully saved'))
Vue.set(node.attributes, 'config', config)
node.attributes.config = config
emit('files:node:updated', node)
return true
}
@ -86,14 +84,7 @@ export const action = new FileAction({
},
async exec({ nodes }) {
const { login, password } = await new Promise<CredentialResponse>((resolve) => spawnDialog(
defineAsyncComponent(() => import('../views/CredentialsDialog.vue')),
{},
(args) => {
resolve(args as CredentialResponse)
},
))
const { login, password } = await spawnDialog(defineAsyncComponent(() => import('../views/CredentialsDialog.vue'))) ?? {}
if (login && password) {
try {
await setCredentials(nodes[0], login, password)

View file

@ -1,4 +1,4 @@
/**
/*!
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@ -8,9 +8,9 @@ import type { StorageConfig } from '../services/externalStorage.ts'
import AlertSvg from '@mdi/svg/svg/alert-circle.svg?raw'
import { showWarning } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { FileAction } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import Vue from 'vue'
import { t } from '@nextcloud/l10n'
import { getStatus } from '../services/externalStorage.ts'
import { isMissingAuthConfig, STORAGE_STATUS } from '../utils/credentialsUtils.ts'
import { isNodeExternalStorage } from '../utils/externalStorageUtils.ts'
@ -31,7 +31,8 @@ export const action = new FileAction({
* Use this function to check the storage availability
* We then update the node attributes directly.
*
* @param node The node to render inline
* @param context - The action context
* @param context.nodes - The node to render inline
*/
async renderInline({ nodes }) {
if (nodes.length !== 1 || !nodes[0]) {
@ -44,43 +45,43 @@ export const action = new FileAction({
span.innerHTML = t('files_external', 'Checking storage …')
let config = null as unknown as StorageConfig
getStatus(node.attributes.id, node.attributes.scope === 'system')
.then((response) => {
config = response.data
Vue.set(node.attributes, 'config', config)
try {
const { data } = await getStatus(node.attributes.id, node.attributes.scope === 'system')
config = data
node.attributes.config = config
emit('files:node:updated', node)
if (config.status !== STORAGE_STATUS.SUCCESS) {
throw new Error(config?.statusMessage || t('files_external', 'There was an error with this external storage.'))
}
if (config.status !== STORAGE_STATUS.SUCCESS) {
throw new Error(config?.statusMessage || t('files_external', 'There was an error with this external storage.'))
}
span.remove()
})
.catch((error) => {
// If axios failed or if something else prevented
// us from getting the config
if ((error as AxiosError).response && !config) {
showWarning(t('files_external', 'We were unable to check the external storage {basename}', {
basename: node.basename,
}))
}
span.remove()
} catch (error) {
// If axios failed or if something else prevented
// us from getting the config
if ((error as AxiosError).response && !config) {
showWarning(t('files_external', 'We were unable to check the external storage {basename}', {
basename: node.basename,
}))
}
// Reset inline status
span.innerHTML = ''
// Reset inline status
span.innerHTML = ''
// Checking if we really have an error
const isWarning = !config ? false : isMissingAuthConfig(config)
const overlay = document.createElement('span')
overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`)
// Checking if we really have an error
const isWarning = !config ? false : isMissingAuthConfig(config)
const overlay = document.createElement('span')
overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`)
// Only show an icon for errors, warning like missing credentials
// have a dedicated inline action button
if (!isWarning) {
span.innerHTML = AlertSvg
span.title = (error as Error).message
}
// Only show an icon for errors, warning like missing credentials
// have a dedicated inline action button
if (!isWarning) {
span.innerHTML = AlertSvg
span.title = (error as Error).message
}
span.prepend(overlay)
})
span.prepend(overlay)
}
return span
},

View file

@ -6,7 +6,7 @@ import type { StorageConfig } from '../services/externalStorage.ts'
import { getCurrentUser } from '@nextcloud/auth'
import { DefaultType, FileAction } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { STORAGE_STATUS } from '../utils/credentialsUtils.ts'

View file

@ -1,9 +1,10 @@
import FolderNetworkSvg from '@mdi/svg/svg/folder-network-outline.svg?raw'
import { Column, getNavigation, registerFileAction, View } from '@nextcloud/files'
/**
/*
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import FolderNetworkSvg from '@mdi/svg/svg/folder-network-outline.svg?raw'
import { Column, getNavigation, registerFileAction, View } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import { action as enterCredentialsAction } from './actions/enterCredentialsAction.ts'

View file

@ -48,8 +48,9 @@ export type MountEntry = {
}
/**
* Convert an OCS api result (mount entry) to a Folder instance
*
* @param ocsEntry
* @param ocsEntry - The OCS mount entry
*/
function entryToFolder(ocsEntry: MountEntry): Folder {
const path = (ocsEntry.path + '/' + ocsEntry.name).replace(/^\//gm, '')
@ -69,7 +70,7 @@ function entryToFolder(ocsEntry: MountEntry): Folder {
}
/**
*
* Fetch the contents of external storage mounts
*/
export async function getContents(): Promise<ContentsWithRoot> {
const response = await axios.get(generateOcsUrl('apps/files_external/api/v1/mounts')) as AxiosResponse<OCSResponse<MountEntry[]>>

View file

@ -17,7 +17,9 @@ export enum STORAGE_STATUS {
}
/**
* @param config
* Check if the given storage configuration is missing authentication configuration
*
* @param config - The storage configuration to check
*/
export function isMissingAuthConfig(config: StorageConfig) {
// If we don't know the status, assume it is ok

View file

@ -3,15 +3,17 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
import type { INode } from '@nextcloud/files'
import type { MountEntry } from '../services/externalStorage.ts'
import { FileType } from '@nextcloud/files'
/**
* @param node
* Check if the given node represents an external storage mount
*
* @param node - The node to check
*/
export function isNodeExternalStorage(node: Node) {
export function isNodeExternalStorage(node: INode) {
// Not a folder, not a storage
if (node.type === FileType.File) {
return false

View file

@ -3,6 +3,28 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import { t } from '@nextcloud/l10n'
import { ref } from 'vue'
import NcDialog from '@nextcloud/vue/components/NcDialog'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
import NcTextField from '@nextcloud/vue/components/NcTextField'
defineEmits<{
close: [payload?: { login: string, password: string }]
}>()
const login = ref('')
const password = ref('')
const dialogButtons: InstanceType<typeof NcDialog>['buttons'] = [{
label: t('files_external', 'Confirm'),
type: 'submit',
variant: 'primary',
}]
</script>
<template>
<NcDialog
:buttons="dialogButtons"
@ -44,46 +66,3 @@
required />
</NcDialog>
</template>
<script lang="ts">
import { t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import NcDialog from '@nextcloud/vue/components/NcDialog'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
import NcTextField from '@nextcloud/vue/components/NcTextField'
export default defineComponent({
name: 'CredentialsDialog',
components: {
NcDialog,
NcNoteCard,
NcTextField,
NcPasswordField,
},
setup() {
return {
t,
}
},
data() {
return {
login: '',
password: '',
}
},
computed: {
dialogButtons() {
return [{
label: t('files_external', 'Confirm'),
type: 'submit',
variant: 'primary',
}]
},
},
})
</script>

View file

@ -40,10 +40,6 @@ module.exports = {
'settings-personal': path.join(__dirname, 'apps/files/src', 'main-settings-personal.ts'),
'reference-files': path.join(__dirname, 'apps/files/src', 'reference-files.ts'),
},
files_external: {
init: path.join(__dirname, 'apps/files_external/src', 'init.ts'),
settings: path.join(__dirname, 'apps/files_external/src', 'settings.js'),
},
files_sharing: {
additionalScripts: path.join(__dirname, 'apps/files_sharing/src', 'additionalScripts.js'),
collaboration: path.join(__dirname, 'apps/files_sharing/src', 'collaborationresourceshandler.js'),

View file

@ -20,6 +20,10 @@ const modules = {
'settings-admin': resolve(import.meta.dirname, 'apps/federatedfilesharing/src', 'settings-admin.ts'),
'settings-personal': resolve(import.meta.dirname, 'apps/federatedfilesharing/src', 'settings-personal.ts'),
},
files_external: {
init: resolve(import.meta.dirname, 'apps/files_external/src', 'init.ts'),
settings: resolve(import.meta.dirname, 'apps/files_external/src', 'settings.js'),
},
files_reminders: {
init: resolve(import.meta.dirname, 'apps/files_reminders/src', 'files-init.ts'),
},