Skip to content

Commit e5f8140

Browse files
authored
Merge pull request #50930 from nextcloud/backport/50910/stable29
2 parents e68c88b + c3fcc4a commit e5f8140

19 files changed

+324
-172
lines changed

__mocks__/@nextcloud/axios.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
*
2121
*/
2222
export default {
23+
interceptors: {
24+
response: {
25+
use: () => {},
26+
},
27+
request: {
28+
use: () => {},
29+
},
30+
},
2331
get: async () => ({ status: 200, data: {} }),
2432
delete: async () => ({ status: 200, data: {} }),
2533
post: async () => ({ status: 200, data: {} }),

apps/files_external/src/actions/enterCredentialsAction.ts

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,13 @@
11
/**
2-
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
3-
*
4-
* @author John Molakvoæ <skjnldsv@protonmail.com>
5-
*
6-
* @license AGPL-3.0-or-later
7-
*
8-
* This program is free software: you can redistribute it and/or modify
9-
* it under the terms of the GNU Affero General Public License as
10-
* published by the Free Software Foundation, either version 3 of the
11-
* License, or (at your option) any later version.
12-
*
13-
* This program is distributed in the hope that it will be useful,
14-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16-
* GNU Affero General Public License for more details.
17-
*
18-
* You should have received a copy of the GNU Affero General Public License
19-
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20-
*
2+
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
214
*/
225
// eslint-disable-next-line n/no-extraneous-import
23-
import type { AxiosResponse } from 'axios'
6+
import type { AxiosResponse } from '@nextcloud/axios'
247
import type { Node } from '@nextcloud/files'
258
import type { StorageConfig } from '../services/externalStorage'
269

10+
import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation'
2711
import { generateUrl } from '@nextcloud/router'
2812
import { showError, showSuccess, spawnDialog } from '@nextcloud/dialogs'
2913
import { translate as t } from '@nextcloud/l10n'
@@ -35,14 +19,30 @@ import { FileAction, DefaultType } from '@nextcloud/files'
3519
import { STORAGE_STATUS, isMissingAuthConfig } from '../utils/credentialsUtils'
3620
import { isNodeExternalStorage } from '../utils/externalStorageUtils'
3721

22+
// Add password confirmation interceptors as
23+
// the backend requires the user to confirm their password
24+
addPasswordConfirmationInterceptors(axios)
25+
3826
type CredentialResponse = {
3927
login?: string,
4028
password?: string,
4129
}
4230

31+
/**
32+
* Set credentials for external storage
33+
*
34+
* @param node The node for which to set the credentials
35+
* @param login The username
36+
* @param password The password
37+
*/
4338
async function setCredentials(node: Node, login: string, password: string): Promise<null|true> {
44-
const configResponse = await axios.put(generateUrl('apps/files_external/userglobalstorages/{id}', node.attributes), {
45-
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+
},
4646
}) as AxiosResponse<StorageConfig>
4747

4848
const config = configResponse.data
@@ -59,8 +59,10 @@ async function setCredentials(node: Node, login: string, password: string): Prom
5959
return true
6060
}
6161

62+
export const ACTION_CREDENTIALS_EXTERNAL_STORAGE = 'credentials-external-storage'
63+
6264
export const action = new FileAction({
63-
id: 'credentials-external-storage',
65+
id: ACTION_CREDENTIALS_EXTERNAL_STORAGE,
6466
displayName: () => t('files', 'Enter missing credentials'),
6567
iconSvgInline: () => LoginSvg,
6668

@@ -93,7 +95,14 @@ export const action = new FileAction({
9395
))
9496

9597
if (login && password) {
96-
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+
}
97106
}
98107

99108
return null

apps/files_external/src/actions/inlineStorageCheckAction.ts

Lines changed: 45 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,12 @@
11
/**
2-
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
3-
*
4-
* @author John Molakvoæ <skjnldsv@protonmail.com>
5-
*
6-
* @license AGPL-3.0-or-later
7-
*
8-
* This program is free software: you can redistribute it and/or modify
9-
* it under the terms of the GNU Affero General Public License as
10-
* published by the Free Software Foundation, either version 3 of the
11-
* License, or (at your option) any later version.
12-
*
13-
* This program is distributed in the hope that it will be useful,
14-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16-
* GNU Affero General Public License for more details.
17-
*
18-
* You should have received a copy of the GNU Affero General Public License
19-
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20-
*
2+
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
214
*/
225
// eslint-disable-next-line n/no-extraneous-import
23-
import type { AxiosError } from 'axios'
6+
import type { AxiosError } from '@nextcloud/axios'
247
import type { Node } from '@nextcloud/files'
258

9+
import { FileAction } from '@nextcloud/files'
2610
import { showWarning } from '@nextcloud/dialogs'
2711
import { translate as t } from '@nextcloud/l10n'
2812
import AlertSvg from '@mdi/svg/svg/alert-circle.svg?raw'
@@ -32,7 +16,6 @@ import '../css/fileEntryStatus.scss'
3216
import { getStatus, type StorageConfig } from '../services/externalStorage'
3317
import { isMissingAuthConfig, STORAGE_STATUS } from '../utils/credentialsUtils'
3418
import { isNodeExternalStorage } from '../utils/externalStorageUtils'
35-
import { FileAction } from '@nextcloud/files'
3619

3720
export const action = new FileAction({
3821
id: 'check-external-storage',
@@ -47,47 +30,55 @@ export const action = new FileAction({
4730
/**
4831
* Use this function to check the storage availability
4932
* We then update the node attributes directly.
33+
*
34+
* @param node The node to render inline
5035
*/
5136
async renderInline(node: Node) {
52-
let config = null as any as StorageConfig
53-
try {
54-
const response = await getStatus(node.attributes.id, node.attributes.scope === 'system')
55-
config = response.data
56-
Vue.set(node.attributes, 'config', config)
37+
const span = document.createElement('span')
38+
span.className = 'files-list__row-status'
39+
span.innerHTML = t('files_external', 'Checking storage …')
40+
41+
let config = null as unknown as StorageConfig
42+
getStatus(node.attributes.id, node.attributes.scope === 'system')
43+
.then(response => {
44+
45+
config = response.data
46+
Vue.set(node.attributes, 'config', config)
47+
48+
if (config.status !== STORAGE_STATUS.SUCCESS) {
49+
throw new Error(config?.statusMessage || t('files_external', 'There was an error with this external storage.'))
50+
}
5751

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

62-
return null
63-
} catch (error) {
64-
// If axios failed or if something else prevented
65-
// us from getting the config
66-
if ((error as AxiosError).response && !config) {
67-
showWarning(t('files_external', 'We were unable to check the external storage {basename}', {
68-
basename: node.basename,
69-
}))
70-
return null
71-
}
63+
// Reset inline status
64+
span.innerHTML = ''
7265

73-
// Checking if we really have an error
74-
const isWarning = isMissingAuthConfig(config)
75-
const overlay = document.createElement('span')
76-
overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`)
66+
// Checking if we really have an error
67+
const isWarning = !config ? false : isMissingAuthConfig(config)
68+
const overlay = document.createElement('span')
69+
overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`)
7770

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

81-
// Only show an icon for errors, warning like missing credentials
82-
// have a dedicated inline action button
83-
if (!isWarning) {
84-
span.innerHTML = AlertSvg
85-
span.title = (error as Error).message
86-
}
78+
span.prepend(overlay)
79+
})
8780

88-
span.prepend(overlay)
89-
return span
90-
}
81+
return span
9182
},
9283

9384
order: 10,

apps/files_external/src/actions/openInFilesAction.ts

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

2828
import { FileAction, DefaultType } from '@nextcloud/files'
2929
import { STORAGE_STATUS } from '../utils/credentialsUtils'
30+
import { getCurrentUser } from '@nextcloud/auth'
3031

3132
export const action = new FileAction({
3233
id: 'open-in-files-external-storage',
@@ -49,7 +50,7 @@ export const action = new FileAction({
4950
t('files_external', 'External mount error'),
5051
(redirect) => {
5152
if (redirect === true) {
52-
const scope = node.attributes.scope === 'personal' ? 'user' : 'admin'
53+
const scope = getCurrentUser()?.isAdmin ? 'admin' : 'user'
5354
window.location.href = generateUrl(`/settings/${scope}/externalstorages`)
5455
}
5556
},

apps/files_external/src/css/fileEntryStatus.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
.files-list__row-status {
22
display: flex;
3-
width: 44px;
3+
min-width: 44px;
44
justify-content: center;
55
align-items: center;
66
height: 100%;
7+
text-overflow: ellipsis;
8+
white-space: nowrap;
9+
overflow: hidden;
710

811
svg {
912
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
@@ -68,6 +68,10 @@ export const startNextcloud = async function(branch: string = getCurrentGitBranc
6868
reject(err)
6969
}
7070
}))
71+
72+
const digest = await (await docker.getImage(SERVER_IMAGE).inspect()).RepoDigests.at(0)
73+
const sha = digest?.split('@').at(1)
74+
console.log('├─ Using image ' + sha)
7175
console.log('└─ Done')
7276
} catch (e) {
7377
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
@@ -26,23 +26,52 @@ export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-r
2626
export const getActionsForFileId = (fileid: number) => getRowForFileId(fileid).find('[data-cy-files-list-row-actions]')
2727
export const getActionsForFile = (filename: string) => getRowForFile(filename).find('[data-cy-files-list-row-actions]')
2828

29-
export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).find('button[aria-label="Actions"]')
30-
export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).find('button[aria-label="Actions"]')
29+
export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).findByRole('button', { name: 'Actions' })
30+
export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).findByRole('button', { name: 'Actions' })
31+
32+
const searchForActionInRow = (row: JQuery<HTMLElement>, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
33+
const action = row.find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
34+
if (action.length > 0) {
35+
cy.log('Found action in row')
36+
return cy.wrap(action)
37+
}
38+
39+
// Else look in the action menu
40+
const menuButtonId = row.find('button[aria-controls]').attr('aria-controls')
41+
return cy.get(`#${menuButtonId} [data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
42+
}
43+
44+
export const getActionEntryForFileId = (fileid: number, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
45+
// If we cannot find the action in the row, it might be in the action menu
46+
return getRowForFileId(fileid).should('be.visible')
47+
.then(row => searchForActionInRow(row, actionId))
48+
}
49+
export const getActionEntryForFile = (filename: string, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
50+
// If we cannot find the action in the row, it might be in the action menu
51+
return getRowForFile(filename).should('be.visible')
52+
.then(row => searchForActionInRow(row, actionId))
53+
}
3154

3255
export const triggerActionForFileId = (fileid: number, actionId: string) => {
33-
getActionButtonForFileId(fileid).click()
34-
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
56+
// Even if it's inline, we open the action menu to get all actions visible
57+
getActionButtonForFileId(fileid).click({ force: true })
58+
getActionEntryForFileId(fileid, actionId)
59+
.find('button').last()
60+
.should('exist').click({ force: true })
3561
}
3662
export const triggerActionForFile = (filename: string, actionId: string) => {
37-
getActionButtonForFile(filename).click()
38-
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
63+
// Even if it's inline, we open the action menu to get all actions visible
64+
getActionButtonForFile(filename).click({ force: true })
65+
getActionEntryForFile(filename, actionId)
66+
.find('button').last()
67+
.should('exist').click({ force: true })
3968
}
4069

4170
export const triggerInlineActionForFileId = (fileid: number, actionId: string) => {
4271
getActionsForFileId(fileid).find(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
4372
}
4473
export const triggerInlineActionForFile = (filename: string, actionId: string) => {
45-
getActionsForFile(filename).get(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
74+
getActionsForFile(filename).find(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
4675
}
4776

4877
export const createFolder = (folderName: string) => {

0 commit comments

Comments
 (0)