Skip to content

Commit 306fa8d

Browse files
authored
Merge pull request #50931 from nextcloud/backport/50910/stable30
2 parents fb5f907 + d84cd4f commit 306fa8d

19 files changed

+306
-60
lines changed

apps/files_external/src/actions/enterCredentialsAction.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { AxiosResponse } from '@nextcloud/axios'
77
import type { Node } from '@nextcloud/files'
88
import type { StorageConfig } from '../services/externalStorage'
99

10+
import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation'
1011
import { generateUrl } from '@nextcloud/router'
1112
import { showError, showSuccess, spawnDialog } from '@nextcloud/dialogs'
1213
import { translate as t } from '@nextcloud/l10n'
@@ -18,20 +19,30 @@ import { FileAction, DefaultType } from '@nextcloud/files'
1819
import { STORAGE_STATUS, isMissingAuthConfig } from '../utils/credentialsUtils'
1920
import { isNodeExternalStorage } from '../utils/externalStorageUtils'
2021

22+
// Add password confirmation interceptors as
23+
// the backend requires the user to confirm their password
24+
addPasswordConfirmationInterceptors(axios)
25+
2126
type CredentialResponse = {
2227
login?: string,
2328
password?: string,
2429
}
2530

2631
/**
32+
* Set credentials for external storage
2733
*
28-
* @param node
29-
* @param login
30-
* @param password
34+
* @param node The node for which to set the credentials
35+
* @param login The username
36+
* @param password The password
3137
*/
3238
async function setCredentials(node: Node, login: string, password: string): Promise<null|true> {
33-
const configResponse = await axios.put(generateUrl('apps/files_external/userglobalstorages/{id}', node.attributes), {
34-
backendOptions: { user: login, password },
39+
const configResponse = await axios.request({
40+
method: 'PUT',
41+
url: generateUrl('apps/files_external/userglobalstorages/{id}', { id: node.attributes.id }),
42+
confirmPassword: PwdConfirmationMode.Strict,
43+
data: {
44+
backendOptions: { user: login, password },
45+
},
3546
}) as AxiosResponse<StorageConfig>
3647

3748
const config = configResponse.data
@@ -48,8 +59,10 @@ async function setCredentials(node: Node, login: string, password: string): Prom
4859
return true
4960
}
5061

62+
export const ACTION_CREDENTIALS_EXTERNAL_STORAGE = 'credentials-external-storage'
63+
5164
export const action = new FileAction({
52-
id: 'credentials-external-storage',
65+
id: ACTION_CREDENTIALS_EXTERNAL_STORAGE,
5366
displayName: () => t('files', 'Enter missing credentials'),
5467
iconSvgInline: () => LoginSvg,
5568

@@ -82,7 +95,14 @@ export const action = new FileAction({
8295
))
8396

8497
if (login && password) {
85-
return await setCredentials(node, login, password)
98+
try {
99+
await setCredentials(node, login, password)
100+
showSuccess(t('files_external', 'Credentials successfully set'))
101+
} catch (error) {
102+
showError(t('files_external', 'Error while setting credentials: {error}', {
103+
error: (error as Error).message,
104+
}))
105+
}
86106
}
87107

88108
return null

apps/files_external/src/actions/inlineStorageCheckAction.ts

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import type { AxiosError } from '@nextcloud/axios'
77
import type { Node } from '@nextcloud/files'
88

9+
import { FileAction } from '@nextcloud/files'
910
import { showWarning } from '@nextcloud/dialogs'
1011
import { translate as t } from '@nextcloud/l10n'
1112
import AlertSvg from '@mdi/svg/svg/alert-circle.svg?raw'
@@ -15,7 +16,6 @@ import '../css/fileEntryStatus.scss'
1516
import { getStatus, type StorageConfig } from '../services/externalStorage'
1617
import { isMissingAuthConfig, STORAGE_STATUS } from '../utils/credentialsUtils'
1718
import { isNodeExternalStorage } from '../utils/externalStorageUtils'
18-
import { FileAction } from '@nextcloud/files'
1919

2020
export const action = new FileAction({
2121
id: 'check-external-storage',
@@ -33,45 +33,51 @@ export const action = new FileAction({
3333
* @param node
3434
*/
3535
async renderInline(node: Node) {
36+
const span = document.createElement('span')
37+
span.className = 'files-list__row-status'
38+
span.innerHTML = t('files_external', 'Checking storage...')
39+
3640
let config = null as unknown as StorageConfig
37-
try {
38-
const response = await getStatus(node.attributes.id, node.attributes.scope === 'system')
39-
config = response.data
40-
Vue.set(node.attributes, 'config', config)
41+
getStatus(node.attributes.id, node.attributes.scope === 'system')
42+
.then(response => {
43+
44+
config = response.data
45+
Vue.set(node.attributes, 'config', config)
46+
47+
if (config.status !== STORAGE_STATUS.SUCCESS) {
48+
throw new Error(config?.statusMessage || t('files_external', 'There was an error with this external storage.'))
49+
}
4150

42-
if (config.status !== STORAGE_STATUS.SUCCESS) {
43-
throw new Error(config?.statusMessage || t('files_external', 'There was an error with this external storage.'))
44-
}
51+
span.remove()
52+
})
53+
.catch(error => {
54+
// If axios failed or if something else prevented
55+
// us from getting the config
56+
if ((error as AxiosError).response && !config) {
57+
showWarning(t('files_external', 'We were unable to check the external storage {basename}', {
58+
basename: node.basename,
59+
}))
60+
}
4561

46-
return null
47-
} catch (error) {
48-
// If axios failed or if something else prevented
49-
// us from getting the config
50-
if ((error as AxiosError).response && !config) {
51-
showWarning(t('files_external', 'We were unable to check the external storage {basename}', {
52-
basename: node.basename,
53-
}))
54-
return null
55-
}
62+
// Reset inline status
63+
span.innerHTML = ''
5664

57-
// Checking if we really have an error
58-
const isWarning = isMissingAuthConfig(config)
59-
const overlay = document.createElement('span')
60-
overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`)
65+
// Checking if we really have an error
66+
const isWarning = !config ? false : isMissingAuthConfig(config)
67+
const overlay = document.createElement('span')
68+
overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`)
6169

62-
const span = document.createElement('span')
63-
span.className = 'files-list__row-status'
70+
// Only show an icon for errors, warning like missing credentials
71+
// have a dedicated inline action button
72+
if (!isWarning) {
73+
span.innerHTML = AlertSvg
74+
span.title = (error as Error).message
75+
}
6476

65-
// Only show an icon for errors, warning like missing credentials
66-
// have a dedicated inline action button
67-
if (!isWarning) {
68-
span.innerHTML = AlertSvg
69-
span.title = (error as Error).message
70-
}
77+
span.prepend(overlay)
78+
})
7179

72-
span.prepend(overlay)
73-
return span
74-
}
80+
return span
7581
},
7682

7783
order: 10,

apps/files_external/src/actions/openInFilesAction.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { translate as t } from '@nextcloud/l10n'
1010

1111
import { FileAction, DefaultType } from '@nextcloud/files'
1212
import { STORAGE_STATUS } from '../utils/credentialsUtils'
13+
import { getCurrentUser } from '@nextcloud/auth'
1314

1415
export const action = new FileAction({
1516
id: 'open-in-files-external-storage',
@@ -32,7 +33,7 @@ export const action = new FileAction({
3233
t('files_external', 'External mount error'),
3334
(redirect) => {
3435
if (redirect === true) {
35-
const scope = node.attributes.scope === 'personal' ? 'user' : 'admin'
36+
const scope = getCurrentUser()?.isAdmin ? 'admin' : 'user'
3637
window.location.href = generateUrl(`/settings/${scope}/externalstorages`)
3738
}
3839
},

apps/files_external/src/css/fileEntryStatus.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
*/
55
.files-list__row-status {
66
display: flex;
7-
width: 44px;
7+
min-width: 44px;
88
justify-content: center;
99
align-items: center;
1010
height: 100%;
11+
text-overflow: ellipsis;
12+
white-space: nowrap;
13+
overflow: hidden;
1114

1215
svg {
1316
width: 24px;

apps/files_external/src/views/CredentialsDialog.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export default defineComponent({
7676
computed: {
7777
dialogButtons() {
7878
return [{
79-
label: t('files_external', 'Submit'),
79+
label: t('files_external', 'Confirm'),
8080
type: 'primary',
8181
nativeType: 'submit',
8282
}]

cypress/dockerNode.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export const startNextcloud = async function(branch: string = getCurrentGitBranc
5353
reject(err)
5454
}
5555
}))
56+
57+
const digest = await (await docker.getImage(SERVER_IMAGE).inspect()).RepoDigests.at(0)
58+
const sha = digest?.split('@').at(1)
59+
console.log('├─ Using image ' + sha)
5660
console.log('└─ Done')
5761
} catch (e) {
5862
console.log('└─ Failed to pull images')

cypress/e2e/files/FilesUtils.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,52 @@ export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-r
1111
export const getActionsForFileId = (fileid: number) => getRowForFileId(fileid).find('[data-cy-files-list-row-actions]')
1212
export const getActionsForFile = (filename: string) => getRowForFile(filename).find('[data-cy-files-list-row-actions]')
1313

14-
export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).find('button[aria-label="Actions"]')
15-
export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).find('button[aria-label="Actions"]')
14+
export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).findByRole('button', { name: 'Actions' })
15+
export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).findByRole('button', { name: 'Actions' })
16+
17+
const searchForActionInRow = (row: JQuery<HTMLElement>, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
18+
const action = row.find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
19+
if (action.length > 0) {
20+
cy.log('Found action in row')
21+
return cy.wrap(action)
22+
}
23+
24+
// Else look in the action menu
25+
const menuButtonId = row.find('button[aria-controls]').attr('aria-controls')
26+
return cy.get(`#${menuButtonId} [data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
27+
}
28+
29+
export const getActionEntryForFileId = (fileid: number, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
30+
// If we cannot find the action in the row, it might be in the action menu
31+
return getRowForFileId(fileid).should('be.visible')
32+
.then(row => searchForActionInRow(row, actionId))
33+
}
34+
export const getActionEntryForFile = (filename: string, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
35+
// If we cannot find the action in the row, it might be in the action menu
36+
return getRowForFile(filename).should('be.visible')
37+
.then(row => searchForActionInRow(row, actionId))
38+
}
1639

1740
export const triggerActionForFileId = (fileid: number, actionId: string) => {
18-
getActionButtonForFileId(fileid).click()
19-
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
41+
// Even if it's inline, we open the action menu to get all actions visible
42+
getActionButtonForFileId(fileid).click({ force: true })
43+
getActionEntryForFileId(fileid, actionId)
44+
.find('button').last()
45+
.should('exist').click({ force: true })
2046
}
2147
export const triggerActionForFile = (filename: string, actionId: string) => {
22-
getActionButtonForFile(filename).click()
23-
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
48+
// Even if it's inline, we open the action menu to get all actions visible
49+
getActionButtonForFile(filename).click({ force: true })
50+
getActionEntryForFile(filename, actionId)
51+
.find('button').last()
52+
.should('exist').click({ force: true })
2453
}
2554

2655
export const triggerInlineActionForFileId = (fileid: number, actionId: string) => {
2756
getActionsForFileId(fileid).find(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
2857
}
2958
export const triggerInlineActionForFile = (filename: string, actionId: string) => {
30-
getActionsForFile(filename).get(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
59+
getActionsForFile(filename).find(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
3160
}
3261

3362
export const selectAllFiles = () => {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { User } from "@nextcloud/cypress"
7+
8+
export type StorageConfig = {
9+
[key: string]: string
10+
}
11+
12+
export enum StorageBackend {
13+
DAV = 'dav',
14+
SMB = 'smb',
15+
SFTP = 'sftp',
16+
}
17+
18+
export enum AuthBackend {
19+
GlobalAuth = 'password::global',
20+
LoginCredentials = 'password::logincredentials',
21+
Password = 'password::password',
22+
SessionCredentials = 'password::sessioncredentials',
23+
UserGlobalAuth = 'password::global::user',
24+
UserProvided = 'password::userprovided',
25+
}
26+
27+
/**
28+
* Create a storage via occ
29+
*/
30+
export function createStorageWithConfig(mountPoint: string, storageBackend: StorageBackend, authBackend: AuthBackend, configs: StorageConfig, user?: User): Cypress.Chainable {
31+
const configsFlag = Object.keys(configs).map(key => `--config "${key}=${configs[key]}"`).join(' ')
32+
const userFlag = user ? `--user ${user.userId}` : ''
33+
34+
const command = `files_external:create "${mountPoint}" "${storageBackend}" "${authBackend}" ${configsFlag} ${userFlag}`
35+
36+
cy.log(`Creating storage with command: ${command}`)
37+
return cy.runOccCommand(command)
38+
}

0 commit comments

Comments
 (0)