From a26b1ccd43f64689369a8e65b6693dbd1aba5ecd Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Mon, 1 Jun 2026 14:19:27 +0545 Subject: [PATCH 1/8] refactor: split oauth2 settings form Signed-off-by: Saw-jan --- src/api/endpoints.js | 1 + src/api/settings.js | 4 + src/components/AdminSettings.vue | 337 ++--------------- src/components/admin/FormOAuthSettings.vue | 416 +++++++++++++++++++++ 4 files changed, 448 insertions(+), 310 deletions(-) create mode 100644 src/components/admin/FormOAuthSettings.vue diff --git a/src/api/endpoints.js b/src/api/endpoints.js index 75b47379b..295020e81 100644 --- a/src/api/endpoints.js +++ b/src/api/endpoints.js @@ -8,4 +8,5 @@ import { generateUrl } from '@nextcloud/router' export default { validateOPInstance: generateUrl('/apps/integration_openproject/is-valid-op-instance'), adminConfig: generateUrl('/apps/integration_openproject/admin-config'), + nextcloudOAuth: generateUrl('/apps/integration_openproject/nc-oauth'), } diff --git a/src/api/settings.js b/src/api/settings.js index 2efd87766..db92b7521 100644 --- a/src/api/settings.js +++ b/src/api/settings.js @@ -13,3 +13,7 @@ export function validateOPInstance(url) { export function saveAdminConfig(configs) { return axios.put(endpoints.adminConfig, { values: configs }) } + +export function createNextcloudOAuthClient(configs) { + return axios.post(endpoints.nextcloudOAuth) +} diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 880c7d2da..dbbd3741f 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -28,125 +28,16 @@ :sso-providers="state.oidc_providers" :apps="state.apps" @formcomplete="markFormComplete" /> -
- -
- - - - -
- - - {{ t('integration_openproject', 'Replace OpenProject OAuth values') }} - - - - {{ t('integration_openproject', 'Save') }} - -
-
-
-
- -
- - - - -
- - - {{ t('integration_openproject', 'Yes, I have copied these values') }} - - - - {{ t('integration_openproject', 'Replace Nextcloud OAuth values') }} - -
-
-
- - - {{ t('integration_openproject', 'Create Nextcloud OAuth values') }} - -
-
+
{{ t('integration_openproject', 'Reset') }} -
+

{{ t('integration_openproject', 'Default user settings') }}

{{ t('integration_openproject', 'A new user will receive these defaults and they will be applied to the integration app till the user changes them.') }} @@ -357,6 +248,7 @@ import { appLinks } from '../constants/links.js' import FormOpenProjectHost from './admin/FormOpenProjectHost.vue' import FormAuthMethod from './admin/FormAuthMethod.vue' import FormSSOSettings from './admin/FormSSOSettings.vue' +import FormOAuthSettings from './admin/FormOAuthSettings.vue' export default { name: 'AdminSettings', @@ -379,6 +271,7 @@ export default { FormOpenProjectHost, FormAuthMethod, FormSSOSettings, + FormOAuthSettings, }, data() { return { @@ -386,13 +279,11 @@ export default { formMode: { // server host form is never disabled. // it's either editable or view only - opOauth: F_MODES.DISABLE, - ncOauth: F_MODES.DISABLE, opUserAppPassword: F_MODES.DISABLE, projectFolderSetUp: F_MODES.DISABLE, }, isFormCompleted: { - opOauth: false, ncOauth: false, opUserAppPassword: false, projectFolderSetUp: false, + opUserAppPassword: false, projectFolderSetUp: false, }, buttonTextLabel: { keepCurrentChange: t('integration_openproject', 'Keep current setup'), @@ -401,10 +292,8 @@ export default { retrySetupWithProjectFolder: t('integration_openproject', 'Retry setup OpenProject user, group and folder'), }, loadingProjectFolderSetup: false, - loadingOPOauthForm: false, state: loadState('integration_openproject', 'admin-settings-config'), isAdminConfigOk: loadState('integration_openproject', 'admin-config-status'), - oPOAuthTokenRevokeStatus: null, oPUserAppPassword: null, isProjectFolderSwitchEnabled: null, projectFolderSetupError: null, @@ -437,13 +326,6 @@ export default { || this.state.openproject_client_secret return formAdded || hasPreSEtup }, - ncClientId() { - return this.state.nc_oauth_client?.nextcloud_client_id - }, - ncClientSecret() { - return '*******' - - }, opUserAppPassword() { return this.state.app_password_set }, @@ -457,10 +339,7 @@ export default { return this.form.authenticationMethod.complete }, isAuthorizationSettingFormComplete() { - return this.form.ssoSettings.complete - }, - isOPOAuthFormComplete() { - return this.isFormCompleted.opOauth + return (this.form.openprojectOauth.complete && this.form.nextcloudOauth.complete) || this.form.ssoSettings.complete }, isManagedGroupFolderSetUpComplete() { return this.isFormCompleted.projectFolderSetUp @@ -468,18 +347,6 @@ export default { isOPUserAppPasswordFormComplete() { return this.isFormCompleted.opUserAppPassword }, - isNcOAuthFormComplete() { - return this.isFormCompleted.ncOauth - }, - isOPOAuthFormInView() { - return this.formMode.opOauth === F_MODES.VIEW - }, - isNcOAuthFormInEdit() { - return this.formMode.ncOauth === F_MODES.EDIT - }, - isOPOAuthFormInDisableMode() { - return this.formMode.opOauth === F_MODES.DISABLE - }, isOPUserAppPasswordFormInEdit() { return this.formMode.opUserAppPassword === F_MODES.EDIT }, @@ -489,9 +356,6 @@ export default { isProjectFolderFormInDisableMode() { return this.formMode.projectFolderSetUp === F_MODES.DISABLE }, - isNcOAuthFormInDisableMode() { - return this.formMode.ncOauth === F_MODES.DISABLE - }, isOPUserAppPasswordInDisableMode() { return this.formMode.opUserAppPassword === F_MODES.DISABLE }, @@ -510,24 +374,11 @@ export default { isOidcMethod() { return this.getCurrentAuthMethod === AUTH_METHOD.OIDC }, - showOAuthSettings() { - return this.isOAuthMethod || !this.form.authenticationMethod.complete - }, adminFileStorageHref() { const path = '%s/admin/settings/storages' const host = this.form.serverHost.value return util.format(path, host) }, - openProjectClientHint() { - const linkText = t('integration_openproject', 'Administration > File storages') - const htmlLink = `${linkText}` - return t('integration_openproject', 'Go to your OpenProject {htmlLink} as an Administrator and start the setup and copy the values here.', { htmlLink }, null, { escape: false, sanitize: false }) - }, - nextcloudClientHint() { - const linkText = t('integration_openproject', 'Administration > File storages') - const htmlLink = `${linkText}` - return t('integration_openproject', 'Copy the following values back into the OpenProject {htmlLink} as an Administrator.', { htmlLink }, null, { escape: false, sanitize: false }) - }, userAppPasswordHint() { const linkText = t('integration_openproject', 'Administration > File storages') const htmlLink = `${linkText}` @@ -552,23 +403,14 @@ export default { const htmlLink = `${linkText}` return t('integration_openproject', 'Server-side encryption is active, but encryption for Team Folders is not yet enabled. To ensure secure storage of files in project folders, please follow the configuration steps in the {htmlLink}.', { htmlLink }, null, { escape: false, sanitize: false }) }, - isIntegrationCompleteWithOauth2() { + isSetupComplete() { return (this.isServerHostFormComplete && this.isAuthorizationMethodFormComplete - && this.isOPOAuthFormComplete - && this.isNcOAuthFormComplete + && this.isAuthorizationSettingFormComplete && this.isManagedGroupFolderSetUpComplete && !this.isOPUserAppPasswordFormInEdit ) }, - isIntegrationCompleteWithOIDC() { - return (this.isServerHostFormComplete - && this.isAuthorizationMethodFormComplete - && this.isAuthorizationSettingFormComplete - && this.isManagedGroupFolderSetUpComplete - && !this.isOPUserAppPasswordFormInEdit - ) - }, isSetupCompleteWithoutProjectFolders() { if (this.isProjectFolderSetupFormInEdit) { return false @@ -603,7 +445,15 @@ export default { }, watch: { 'form.ssoSettings.complete'() { - if (this.form.ssoSettings.complete && (!this.state.authorization_settings.sso_provider_type || this.formMode.projectFolderSetUp === F_MODES.DISABLE)) { + if (this.form.ssoSettings.complete && this.formMode.projectFolderSetUp === F_MODES.DISABLE) { + this.formMode.projectFolderSetUp = F_MODES.EDIT + this.showDefaultManagedProjectFolders = true + this.isProjectFolderSwitchEnabled = true + this.textLabelProjectFolderSetupButton = this.buttonTextLabel.completeWithProjectFolderSetup + } + }, + 'form.nextcloudOauth.complete'() { + if (this.form.nextcloudOauth.complete && this.formMode.projectFolderSetUp === F_MODES.DISABLE) { this.formMode.projectFolderSetUp = F_MODES.EDIT this.showDefaultManagedProjectFolders = true this.isProjectFolderSwitchEnabled = true @@ -636,30 +486,13 @@ export default { this.textLabelProjectFolderSetupButton = this.buttonTextLabel.keepCurrentChange } // for oauth2 authorization - if (this.state.openproject_instance_url - && this.state.openproject_client_id - && this.state.openproject_client_secret - && this.state.nc_oauth_client - ) { + if (this.state.openproject_instance_url && this.isAuthorizationSettingFormComplete) { this.showDefaultManagedProjectFolders = true } if (this.state.fresh_project_folder_setup === false) { this.showDefaultManagedProjectFolders = true } - if (!!this.state.openproject_client_id && !!this.state.openproject_client_secret) { - this.formMode.opOauth = F_MODES.VIEW - this.isFormCompleted.opOauth = true - } - if (this.state.authorization_method) { - if (!this.state.openproject_client_id && !this.state.openproject_client_secret) { - this.formMode.opOauth = F_MODES.EDIT - } - } - if (this.state.nc_oauth_client) { - this.formMode.ncOauth = F_MODES.VIEW - this.isFormCompleted.ncOauth = true - } if (!this.state.nc_oauth_client && this.state.openproject_instance_url && this.state.openproject_client_id @@ -669,9 +502,6 @@ export default { this.formMode.projectFolderSetUp = F_MODES.VIEW this.isFormCompleted.projectFolderSetUp = true } - if (this.formMode.ncOauth === F_MODES.VIEW || this.form.ssoSettings.complete) { - this.showDefaultManagedProjectFolders = true - } if (this.showDefaultManagedProjectFolders) { this.formMode.projectFolderSetUp = F_MODES.VIEW this.isFormCompleted.projectFolderSetUp = true @@ -701,16 +531,6 @@ export default { this.formMode.projectFolderSetUp = F_MODES.VIEW this.isProjectFolderSetupCorrect = true }, - async setNCOAuthFormToViewMode() { - this.formMode.ncOauth = F_MODES.VIEW - this.isFormCompleted.ncOauth = true - if (!this.isIntegrationCompleteWithOauth2 && this.formMode.projectFolderSetUp !== F_MODES.EDIT && this.formMode.opUserAppPassword !== F_MODES.EDIT) { - this.formMode.projectFolderSetUp = F_MODES.EDIT - this.showDefaultManagedProjectFolders = true - this.isProjectFolderSwitchEnabled = true - this.textLabelProjectFolderSetupButton = this.buttonTextLabel.completeWithProjectFolderSetup - } - }, setOPUserAppPasswordToViewMode() { this.formMode.opUserAppPassword = F_MODES.VIEW this.isFormCompleted.opUserAppPassword = true @@ -762,48 +582,6 @@ export default { this.projectFolderSetupError = null } }, - async saveOPOAuthClientValues() { - this.isFormStep = FORM.OP_OAUTH - if (await this.saveOPOptions()) { - this.formMode.opOauth = F_MODES.VIEW - this.isFormCompleted.opOauth = true - - // if we do not have Nextcloud OAuth client yet, a new client is created - if (!this.state.nc_oauth_client) { - this.createNCOAuthClient() - } - } - }, - resetOPOAuthClientValues() { - OC.dialogs.confirmDestructive( - t('integration_openproject', 'If you proceed you will need to update these settings with the new OpenProject OAuth credentials. Also, all users will need to reauthorize access to their OpenProject account.'), - t('integration_openproject', 'Replace OpenProject OAuth values'), - { - type: OC.dialogs.YES_NO_BUTTONS, - confirm: t('integration_openproject', 'Yes, replace'), - confirmClasses: 'error', - cancel: t('integration_openproject', 'Cancel'), - }, - async (result) => { - if (result) { - await this.clearOPOAuthClientValues() - } - }, - true, - ) - }, - async clearOPOAuthClientValues() { - this.isFormStep = FORM.OP_OAUTH - this.formMode.opOauth = F_MODES.EDIT - this.isFormCompleted.opOauth = false - this.state.openproject_client_id = null - this.state.openproject_client_secret = null - const saved = await this.saveOPOptions() - if (!saved) { - this.formMode.opOauth = F_MODES.VIEW - this.isFormCompleted.opOauth = true - } - }, resetAllAppValuesConfirmation() { OC.dialogs.confirmDestructive( t('integration_openproject', 'Are you sure that you want to reset this app and delete all settings and all connections of all Nextcloud users to OpenProject?'), @@ -828,9 +606,6 @@ export default { // also, form completeness should be set to false // reset form states to default - this.isFormCompleted.opOauth = false - this.formMode.opOauth = F_MODES.EDIT - this.state.default_enable_navigation = false this.state.default_enable_unified_search = false this.oPUserAppPassword = null @@ -901,13 +676,11 @@ export default { this.state.app_password_set = true this.oPUserAppPassword = response?.data?.oPUserAppPassword } - this.oPOAuthTokenRevokeStatus = response?.data?.oPOAuthTokenRevokeStatus showSuccess(t('integration_openproject', 'OpenProject admin options saved')) success = true } catch (error) { console.error() this.isAdminConfigOk = null - this.oPOAuthTokenRevokeStatus = null if (error.response.data.error) { this.projectFolderSetupError = error.response.data.error } @@ -915,7 +688,6 @@ export default { t('integration_openproject', 'Failed to save OpenProject admin options'), ) } - this.notifyAboutOPOAuthTokenRevoke() return success }, async checkIfProjectFolderIsAlreadyReadyForSetup() { @@ -929,46 +701,6 @@ export default { } return success }, - notifyAboutOPOAuthTokenRevoke() { - switch (this.oPOAuthTokenRevokeStatus) { - case 'connection_error': - showError( - t('integration_openproject', 'Failed to perform revoke request due to connection error with the OpenProject server'), - ) - break - case 'other_error': - showError( - t('integration_openproject', 'Failed to revoke some users\' OpenProject OAuth access tokens'), - ) - break - case 'success': - showSuccess( - t('integration_openproject', 'Successfully revoked users\' OpenProject OAuth access tokens'), - ) - break - default: - break - } - }, - resetNcOauthValues() { - OC.dialogs.confirmDestructive( - t('integration_openproject', 'If you proceed you will need to update the settings in your OpenProject with the new Nextcloud OAuth credentials. Also, all users in OpenProject will need to reauthorize access to their Nextcloud account.'), - t('integration_openproject', 'Replace Nextcloud OAuth values'), - { - type: OC.dialogs.YES_NO_BUTTONS, - confirm: t('integration_openproject', 'Yes, replace'), - confirmClasses: 'error', - cancel: t('integration_openproject', 'Cancel'), - }, - async (result) => { - if (result) { - this.state.nc_oauth_client = null - this.createNCOAuthClient() - } - }, - true, - ) - }, async completeIntegrationWithoutProjectFolderSetUp() { this.isFormStep = FORM.GROUP_FOLDER this.textLabelProjectFolderSetupButton = this.buttonTextLabel.keepCurrentChange @@ -1013,21 +745,6 @@ export default { this.isFormCompleted.opUserAppPassword = false await this.saveOPOptions() }, - createNCOAuthClient() { - const url = generateUrl('/apps/integration_openproject/nc-oauth') - axios.post(url).then((response) => { - this.state.nc_oauth_client = response.data - // generate part is complete but still the NC OAuth form is set to - // edit mode and not completed state so that copy buttons will be available for the user - this.formMode.ncOauth = F_MODES.EDIT - this.isFormCompleted.ncOauth = false - }).catch((error) => { - showError( - t('integration_openproject', 'Failed to create Nextcloud OAuth client') - + ': ' + error.response.request.responseText, - ) - }) - }, setDefaultConfig() { const url = generateUrl('/apps/integration_openproject/admin-config') const req = { diff --git a/src/components/admin/FormOAuthSettings.vue b/src/components/admin/FormOAuthSettings.vue new file mode 100644 index 000000000..0a2ac733f --- /dev/null +++ b/src/components/admin/FormOAuthSettings.vue @@ -0,0 +1,416 @@ + + + + + + + From 8e220b14c10b4ed8506a42f29c3d67419616d562 Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Mon, 1 Jun 2026 17:14:18 +0545 Subject: [PATCH 2/8] test: fix AdminSettings unit tests Signed-off-by: Saw-jan --- src/components/AdminSettings.vue | 12 +- tests/jest/components/AdminSettings.spec.js | 742 +------------------- 2 files changed, 26 insertions(+), 728 deletions(-) diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index dbbd3741f..481a4829d 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -485,27 +485,19 @@ export default { } else { this.textLabelProjectFolderSetupButton = this.buttonTextLabel.keepCurrentChange } - // for oauth2 authorization if (this.state.openproject_instance_url && this.isAuthorizationSettingFormComplete) { this.showDefaultManagedProjectFolders = true + this.formMode.projectFolderSetUp = F_MODES.EDIT } if (this.state.fresh_project_folder_setup === false) { this.showDefaultManagedProjectFolders = true } - if (!this.state.nc_oauth_client - && this.state.openproject_instance_url - && this.state.openproject_client_id - && this.state.openproject_client_secret - && this.textLabelProjectFolderSetupButton === 'Keep current setup') { + if (this.textLabelProjectFolderSetupButton === 'Keep current setup') { this.showDefaultManagedProjectFolders = true this.formMode.projectFolderSetUp = F_MODES.VIEW this.isFormCompleted.projectFolderSetUp = true } - if (this.showDefaultManagedProjectFolders) { - this.formMode.projectFolderSetUp = F_MODES.VIEW - this.isFormCompleted.projectFolderSetUp = true - } if (this.state.app_password_set) { this.formMode.opUserAppPassword = F_MODES.VIEW this.isFormCompleted.opUserAppPassword = true diff --git a/tests/jest/components/AdminSettings.spec.js b/tests/jest/components/AdminSettings.spec.js index 4e81f908e..cf9c2cbaa 100644 --- a/tests/jest/components/AdminSettings.spec.js +++ b/tests/jest/components/AdminSettings.spec.js @@ -152,657 +152,18 @@ describe('AdminSettings.vue', () => { afterEach(() => { jest.restoreAllMocks() }) - const confirmSpy = jest.spyOn(global.OC.dialogs, 'confirmDestructive') - describe('form mode and completed status without project folder setup for OAUTH2 authorization config', () => { - it.each([ - [ - 'with empty state', - { - openproject_instance_url: null, - authorization_method: null, - openproject_client_id: null, - openproject_client_secret: null, - nc_oauth_client: null, - }, - { - opOauth: F_MODES.DISABLE, - ncOauth: F_MODES.DISABLE, - projectFolderSetUp: F_MODES.DISABLE, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: false, - opOauth: false, - ncOauth: false, - projectFolderSetUp: false, - opUserAppPassword: false, - }, - ], - [ - 'with incomplete OpenProject Authorization Method', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: null, - openproject_client_id: null, - openproject_client_secret: null, - nc_oauth_client: null, - }, - { - authorizationMethod: F_MODES.EDIT, - opOauth: F_MODES.DISABLE, - ncOauth: F_MODES.DISABLE, - projectFolderSetUp: F_MODES.DISABLE, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: false, - opOauth: false, - ncOauth: false, - projectFolderSetUp: false, - opUserAppPassword: false, - }, - ], - [ - 'with incomplete OpenProject OAuth values', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: null, - openproject_client_secret: null, - nc_oauth_client: null, - }, - { - authorizationMethod: F_MODES.VIEW, - opOauth: F_MODES.EDIT, - ncOauth: F_MODES.DISABLE, - projectFolderSetUp: F_MODES.DISABLE, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - opOauth: false, - ncOauth: false, - projectFolderSetUp: false, - opUserAppPassword: false, - }, - ], - [ - 'with complete OpenProject OAuth values', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'abcd', - openproject_client_secret: 'abcdefgh', - nc_oauth_client: null, - fresh_project_folder_setup: true, - }, - { - authorizationMethod: F_MODES.VIEW, - opOauth: F_MODES.VIEW, - ncOauth: F_MODES.DISABLE, - projectFolderSetUp: F_MODES.DISABLE, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - opOauth: true, - ncOauth: false, - projectFolderSetUp: false, - opUserAppPassword: false, - }, - ], - [ - 'with everything but empty OpenProject OAuth values', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: null, - openproject_client_secret: null, - nc_oauth_client: { - nextcloud_client_id: 'some-client-id-here', - nextcloud_client_secret: 'some-client-secret-here', - }, - }, - { - authorizationMethod: F_MODES.VIEW, - opOauth: F_MODES.EDIT, - ncOauth: F_MODES.VIEW, - projectFolderSetUp: F_MODES.VIEW, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - opOauth: false, - ncOauth: true, - projectFolderSetUp: true, - opUserAppPassword: false, - }, - ], - [ - 'with a complete admin settings', - { - openproject_instance_url: 'https://openproject.example.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'client-id-here', - openproject_client_secret: 'client-id-here', - nc_oauth_client: { - nextcloud_client_id: 'nc-client-id-here', - nextcloud_client_secret: 'nc-client-secret-here', - }, - }, - { - authorizationMethod: F_MODES.VIEW, - opOauth: F_MODES.VIEW, - ncOauth: F_MODES.VIEW, - projectFolderSetUp: F_MODES.VIEW, - opUserAppPassword: F_MODES.DISABLE, - }, - { - authorizationMethod: true, - opOauth: true, - ncOauth: true, - projectFolderSetUp: true, - opUserAppPassword: false, - }, - ], - ])('when the form is loaded %s', (name, state, expectedFormMode, expectedFormState) => { - const wrapper = getWrapper({ state }) - expect(wrapper.vm.formMode.opOauth).toBe(expectedFormMode.opOauth) - expect(wrapper.vm.formMode.ncOauth).toBe(expectedFormMode.ncOauth) - expect(wrapper.vm.formMode.projectFolderSetUp).toBe(expectedFormMode.projectFolderSetUp) - expect(wrapper.vm.formMode.opUserAppPassword).toBe(expectedFormMode.opUserAppPassword) - - expect(wrapper.vm.isFormCompleted.opOauth).toBe(expectedFormState.opOauth) - expect(wrapper.vm.isFormCompleted.ncOauth).toBe(expectedFormState.ncOauth) - expect(wrapper.vm.isFormCompleted.projectFolderSetUp).toBe(expectedFormState.projectFolderSetUp) - expect(wrapper.vm.isFormCompleted.opUserAppPassword).toBe(expectedFormState.opUserAppPassword) - }) - }) - - describe('OpenProject OAuth values form', () => { - describe('view mode and completed state', () => { - let wrapper, opOAuthForm, resetButton - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true, oPOAuthTokenRevokeStatus: '' } })) - beforeEach(() => { - wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'openproject-client-id', - openproject_client_secret: 'openproject-client-secret', - nc_oauth_client: null, - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - opOAuthForm = wrapper.find(selectors.opOauthForm) - resetButton = opOAuthForm.find(selectors.resetOPOAuthFormButton) - }) - it('should show field values and hide the form if server host form is complete', () => { - expect(opOAuthForm).toMatchSnapshot() - }) - describe('reset button', () => { - it('should trigger confirm dialog on click', async () => { - await resetButton.trigger('click') - expect(confirmSpy).toBeCalledTimes(1) - - const expectedDialogMessage = 'If you proceed you will need to update these settings with the new' - + ' OpenProject OAuth credentials. Also, all users will need to reauthorize' - + ' access to their OpenProject account.' - const expectedDialogTitle = 'Replace OpenProject OAuth values' - const expectedDialogOpts = { - cancel: 'Cancel', - confirm: 'Yes, replace', - confirmClasses: 'error', - type: 70, - } - expect(confirmSpy).toHaveBeenCalledWith( - expectedDialogMessage, - expectedDialogTitle, - expectedDialogOpts, - expect.any(Function), - true, - ) - jest.clearAllMocks() - wrapper.destroy() - }) - it('should clear values on confirm', async () => { - jest.clearAllMocks() - await wrapper.vm.clearOPOAuthClientValues() - - expect(saveOPOptionsSpy).toBeCalledTimes(1) - expect(wrapper.vm.state.openproject_client_id).toBe(null) - }) - }) - }) - describe('edit mode', () => { - let wrapper - beforeEach(() => { - jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true } })) - jest.spyOn(axios, 'post') - .mockImplementationOnce(() => Promise.resolve({ - data: { - clientId: 'nc-client-id101', - clientSecret: 'nc-client-secret101', - }, - })) - wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: null, - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - }) - afterEach(() => { - axios.post.mockReset() - axios.put.mockReset() - jest.clearAllMocks() - wrapper.destroy() - }) - it('should show the form and hide the field values', () => { - expect(wrapper.find(selectors.opOauthForm)).toMatchSnapshot() - }) - describe('submit button', () => { - it('should be enabled with complete client values', async () => { - let submitButton - submitButton = wrapper.find(selectors.submitOPOAuthFormButton) - expect(submitButton.attributes().disabled).toBe('true') - await wrapper.find(selectors.opOauthClientIdInput).vm.$emit('input', 'qwerty') - await wrapper.find(selectors.opOauthClientSecretInput).vm.$emit('input', 'qwerty') - - submitButton = wrapper.find(selectors.submitOPOAuthFormButton) - expect(submitButton.attributes().disabled).toBe(undefined) - }) - describe('when clicked', () => { - describe('when the save is successful', () => { - beforeEach(async () => { - jest.spyOn(wrapper.vm, 'saveOPOptions').mockReturnValue(true) - wrapper.find(selectors.opOauthClientIdInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.opOauthClientSecretInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.submitOPOAuthFormButton).vm.$emit('click') - }) - it('should set the form to view mode', async () => { - expect(wrapper.vm.formMode.opOauth).toBe(F_MODES.VIEW) - }) - it('should set the isFormCompleted to true', async () => { - expect(wrapper.vm.isFormCompleted.opOauth).toBe(true) - }) - - it('should not create Nextcloud OAuth client if already present', async () => { - jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true } })) - const createNCOAuthClientSpy = jest.spyOn(AdminSettings.methods, 'createNCOAuthClient') - .mockImplementationOnce(() => jest.fn()) - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: { - nextcloud_client_id: 'abcdefg', - nextcloud_client_secret: 'slkjdlkjlkd', - }, - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - await wrapper.find(`${selectors.opOauthClientIdInput} input`).setValue('qwerty') - await wrapper.find(`${selectors.opOauthClientSecretInput} input`).setValue('qwerty') - await wrapper.find(selectors.submitOPOAuthFormButton).trigger('click') - expect(createNCOAuthClientSpy).not.toHaveBeenCalled() - }) - - it('should create Nextcloud OAuth client if not already present', async () => { - jest.spyOn(axios, 'post') - .mockImplementationOnce(() => Promise.resolve({ data: { status: false } })) - const createNCOAuthClientSpy = jest.spyOn(AdminSettings.methods, 'createNCOAuthClient') - .mockImplementationOnce(() => jest.fn()) - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: '', - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - await wrapper.find(`${selectors.opOauthClientIdInput} input`).setValue('qwerty') - await wrapper.find(`${selectors.opOauthClientSecretInput} input`).setValue('qwerty') - await wrapper.find(selectors.submitOPOAuthFormButton).trigger('click') - - expect(createNCOAuthClientSpy).toBeCalledTimes(1) - }) - }) - - describe('when the save fails', () => { - beforeEach(async () => { - jest.spyOn(wrapper.vm, 'saveOPOptions').mockReturnValue(false) - wrapper.find(selectors.opOauthClientIdInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.opOauthClientSecretInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.submitOPOAuthFormButton).vm.$emit('click') - }) - it('should set the form to view mode', async () => { - expect(wrapper.vm.formMode.opOauth).toBe(F_MODES.EDIT) - }) - it('should set the isFormCompleted to true', async () => { - expect(wrapper.vm.isFormCompleted.opOauth).toBe(false) - }) - - it('should not create Nextcloud OAuth client', async () => { - jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true } })) - const createNCOAuthClientSpy = jest.spyOn(AdminSettings.methods, 'createNCOAuthClient') - .mockImplementationOnce(() => jest.fn()) - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: { - nextcloud_client_id: 'abcdefg', - nextcloud_client_secret: 'slkjdlkjlkd', - }, - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - await wrapper.find(`${selectors.opOauthClientIdInput} input`).setValue('qwerty') - await wrapper.find(`${selectors.opOauthClientSecretInput} input`).setValue('qwerty') - await wrapper.find(selectors.submitOPOAuthFormButton).trigger('click') - expect(createNCOAuthClientSpy).not.toHaveBeenCalled() - }) - }) - - describe('when the admin config is ok on save options', () => { - beforeEach(async () => { - await wrapper.find(selectors.opOauthClientIdInput).vm.$emit('input', 'qwerty') - await wrapper.find(selectors.opOauthClientSecretInput).vm.$emit('input', 'qwerty') - await wrapper.find(selectors.submitOPOAuthFormButton).vm.$emit('click') - await flushPromises() - }) - it('should set the form to view mode', () => { - expect(wrapper.vm.formMode.opOauth).toBe(F_MODES.VIEW) - }) - it('should set the adminConfigStatus as "true"', () => { - expect(wrapper.vm.isAdminConfigOk).toBe(true) - }) - it('should create Nextcloud OAuth client if not already present', () => { - expect(wrapper.vm.state.nc_oauth_client).toMatchObject({ - clientId: 'nc-client-id101', - clientSecret: 'nc-client-secret101', - }) - }) - it('should not create Nextcloud OAuth client if already present', async () => { - jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { status: true } })) - const createNCOAuthClientSpy = jest.spyOn(AdminSettings.methods, 'createNCOAuthClient') - .mockImplementationOnce(() => jest.fn()) - const wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: { - nextcloud_client_id: 'abcdefg', - nextcloud_client_secret: 'slkjdlkjlkd', - }, - }, - form: { - serverHost: { - complete: true, - value: 'http://openproject.com', - }, - authenticationMethod: { complete: true, value: AUTH_METHOD.OAUTH2 }, - }, - }) - wrapper.find(selectors.opOauthClientIdInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.opOauthClientSecretInput).vm.$emit('input', 'qwerty') - wrapper.find(selectors.submitOPOAuthFormButton).vm.$emit('click') - await flushPromises() - expect(createNCOAuthClientSpy).not.toHaveBeenCalled() - }) - - it('should not create new user app password if already present', async () => { - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce(() => Promise.resolve({ data: { oPUserAppPassword: null } })) - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: '', - openproject_client_secret: '', - nc_oauth_client: { - nextcloud_client_id: 'abcdefg', - nextcloud_client_secret: 'slkjdlkjlkd', - }, - fresh_project_folder_setup: false, - project_folder_info: { - status: true, - }, - app_password_set: false, - }, - oPUserAppPassword: 'opUserPassword', - }) - expect(saveOPOptionsSpy).toBeCalledWith( - 'http://localhost/apps/integration_openproject/admin-config', - { - values: { - openproject_client_id: 'qwerty', - openproject_client_secret: 'qwerty', - }, - }, - ) - expect(wrapper.vm.oPUserAppPassword).toBe('opUserPassword') - }) - }) - }) - }) - }) - }) - - describe('Nextcloud OAuth values form', () => { - describe('view mode with complete values', () => { - it('should show the field values and hide the form', () => { - const wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'some-client-id-here', - openproject_client_secret: 'some-client-secret-here', - nc_oauth_client: { - nextcloud_client_id: 'some-nc-client-id-here', - nextcloud_client_secret: 'some-nc-client-secret-here', - }, - }, - }) - expect(wrapper.find(selectors.ncOauthForm)).toMatchSnapshot() - }) - describe('reset button', () => { - afterEach(() => { - jest.clearAllMocks() - }) - it('should trigger the confirm dialog', async () => { - const wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'op-client-id', - openproject_client_secret: 'op-client-secret', - nc_oauth_client: { - nextcloud_client_id: 'nc-clientid', - nextcloud_client_secret: 'nc-clientsecret', - }, - }, - }) - - const expectedConfirmText = 'If you proceed you will need to update the settings in your OpenProject ' - + 'with the new Nextcloud OAuth credentials. Also, all users in OpenProject ' - + 'will need to reauthorize access to their Nextcloud account.' - const expectedConfirmOpts = { - cancel: 'Cancel', - confirm: 'Yes, replace', - confirmClasses: 'error', - type: 70, - } - const expectedConfirmTitle = 'Replace Nextcloud OAuth values' - - const resetButton = wrapper.find(selectors.resetNcOAuthFormButton) - resetButton.vm.$emit('click') - await flushPromises() - - expect(confirmSpy).toBeCalledTimes(1) - expect(confirmSpy).toBeCalledWith( - expectedConfirmText, - expectedConfirmTitle, - expectedConfirmOpts, - expect.any(Function), - true, - ) - wrapper.destroy() - }) - it('should create new client on confirm', async () => { - jest.spyOn(axios, 'post') - .mockImplementationOnce(() => Promise.resolve({ - data: { - clientId: 'new-client-id77', - clientSecret: 'new-client-secret77', - }, - })) - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'op-client-id', - openproject_client_secret: 'op-client-secret', - nc_oauth_client: { - nextcloud_client_id: 'nc-client-id', - nextcloud_client_secret: 'nc-client-secret', - }, - }, - }) - await wrapper.vm.createNCOAuthClient() - expect(wrapper.vm.state.nc_oauth_client).toMatchObject({ - clientId: 'new-client-id77', - clientSecret: 'new-client-secret77', - }) - expect(wrapper.vm.formMode.ncOauth).toBe(F_MODES.EDIT) - expect(wrapper.vm.isFormCompleted.ncOauth).toBe(false) - wrapper.destroy() - }) - }) - }) - describe('recreate button', () => { - it('should be displayed if nextcloud oauth credentials is empty and everything is set', async () => { - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'op-client-id', - openproject_client_secret: 'op-client-secret', - nc_oauth_client: null, - }, - formMode: { - projectFolderSetUp: F_MODES.VIEW, - }, - showDefaultManagedProjectFolders: true, - isFormCompleted: { - projectFolderSetUp: true, - }, - - }) - const resetButton = wrapper.find(selectors.resetNcOAuthFormButton) - expect(resetButton.isVisible()).toBe(true) - expect(resetButton.text()).toBe('Create Nextcloud OAuth values') - wrapper.destroy() - }) - }) - describe('edit mode', () => { - it('should show the form and hide the field values', async () => { - const wrapper = getWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'op-client-id', - openproject_client_secret: 'op-client-secret', - nc_oauth_client: { - nextcloud_client_id: 'nc-client-id', - nextcloud_client_secret: 'nc-client-secret', - }, - }, - }) - await wrapper.setData({ - formMode: { - ncOauth: F_MODES.EDIT, - }, - }) - expect(wrapper.find(selectors.ncOauthForm)).toMatchSnapshot() - }) - describe('done button', () => { - it('should set the form to view mode if the oauth values are complete', async () => { - const wrapper = getMountedWrapper({ - state: { - openproject_instance_url: 'http://openproject.com', - authorization_method: AUTH_METHOD.OAUTH2, - openproject_client_id: 'some-client-id-for-op', - openproject_client_secret: 'some-client-secret-for-op', - nc_oauth_client: { - nextcloud_client_id: 'something', - nextcloud_client_secret: 'something-else', - }, - }, - }) - await wrapper.setData({ - formMode: { - ncOauth: F_MODES.EDIT, - }, - }) - await wrapper.find(selectors.ncOauthForm) - .find(selectors.submitNcOAuthFormButton) - .trigger('click') - expect(wrapper.vm.formMode.ncOauth).toBe(F_MODES.VIEW) - expect(wrapper.vm.isFormCompleted.ncOauth).toBe(true) - }) - }) - }) - }) + const commonState = { + form: { + serverHost: { complete: true }, + authenticationMethod: { + value: AUTH_METHOD.OAUTH2, + complete: true, + }, + openprojectOauth: { complete: true }, + nextcloudOauth: { complete: true }, + }, + } describe('Project folders form (Project Folder Setup)', () => { describe('Disable mode', () => { @@ -862,14 +223,15 @@ describe('AdminSettings.vue', () => { nextcloud_client_id: 'some-nc-client-id-here', nextcloud_client_secret: 'some-nc-client-secret-here', }, - fresh_project_folder_setup: true, - // project folder is already not set up + fresh_project_folder_setup: false, + // project folder is already set up project_folder_info: { - status: false, + status: true, }, app_password_set: false, ...appState, }, + ...commonState, }) const projectFolderStatus = wrapper.find(selectors.projectFolderStatus) const actualProjectFolderStatusValue = projectFolderStatus.text() @@ -904,6 +266,7 @@ describe('AdminSettings.vue', () => { formMode: { projectFolderSetUp: F_MODES.VIEW, }, + ...commonState, }) const formHeading = wrapper.find(selectors.projectFolderFormHeading) const errorNote = wrapper.find(selectors.projectFolderErrorNote) @@ -979,15 +342,15 @@ describe('AdminSettings.vue', () => { nextcloud_client_secret: 'some-nc-client-secret-here', }, fresh_project_folder_setup: true, - // project folder is already not set up + // project folder is not set up project_folder_info: { status: false, }, app_password_set: false, ...appState, }, + ...commonState, }) - await wrapper.vm.setNCOAuthFormToViewMode() expect(wrapper.vm.isProjectFolderSwitchEnabled).toBe(true) const completeProjectFolderSetupWithGroupFolderButton = wrapper.find(selectors.completeProjectFolderSetupWithGroupFolderButton) expect(completeProjectFolderSetupWithGroupFolderButton.text()).toBe('Setup OpenProject user, group and folder') @@ -1012,8 +375,8 @@ describe('AdminSettings.vue', () => { app_password_set: false, ...appState, }, + ...commonState, }) - await wrapper.vm.setNCOAuthFormToViewMode() expect(wrapper.vm.isProjectFolderSwitchEnabled).toBe(true) const projectFolderSetupSwitchButton = wrapper.find(selectors.projectFolderSetupButtonStub) projectFolderSetupSwitchButton.vm.$emit('update:checked', false) @@ -1054,6 +417,7 @@ describe('AdminSettings.vue', () => { }, app_password_set: false, }, + ...commonState, }) await wrapper.setData({ formMode: { @@ -1147,6 +511,7 @@ describe('AdminSettings.vue', () => { projectFolderSetupError: null, ...appState, }, + ...commonState, }) await wrapper.setData({ @@ -1213,6 +578,7 @@ describe('AdminSettings.vue', () => { }, }, isGroupFolderAlreadySetup: null, + ...commonState, }) await wrapper.setData({ formMode: { @@ -1963,10 +1329,7 @@ describe('AdminSettings.vue', () => { nextcloud_client_secret: 'something-else', }, }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true }, - }, + ...commonState, }) const $defaultEnableNavigation = wrapper.find(selectors.defaultEnableNavigation) @@ -2008,10 +1371,7 @@ describe('AdminSettings.vue', () => { nextcloud_client_secret: 'something-else', }, }, - form: { - serverHost: { complete: true }, - authenticationMethod: { complete: true }, - }, + ...commonState, }) const $defaultEnableNavigation = wrapper.find(selectors.defaultEnableNavigation) await $defaultEnableNavigation.trigger('click') @@ -2023,60 +1383,6 @@ describe('AdminSettings.vue', () => { }) }) - describe('revoke OpenProject OAuth token', () => { - beforeEach(() => { - axios.put.mockReset() - dialogs.showSuccess.mockReset() - dialogs.showError.mockReset() - }) - it('should show success when revoke status is success', async () => { - dialogs.showSuccess - .mockImplementationOnce() - .mockImplementationOnce() - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce( - () => Promise.resolve({ data: { status: true, oPOAuthTokenRevokeStatus: 'success' } }), - ) - const wrapper = getMountedWrapper({ - state: completeOAUTH2IntegrationState, - }) - await wrapper.vm.saveOPOptions() - - await localVue.nextTick() - - expect(saveOPOptionsSpy).toBeCalledTimes(1) - expect(dialogs.showSuccess).toBeCalledTimes(2) - expect(dialogs.showSuccess).toBeCalledWith('OpenProject admin options saved') - expect(dialogs.showSuccess).toBeCalledWith('Successfully revoked users\' OpenProject OAuth access tokens') - - }) - it.each([ - ['connection_error', 'Failed to perform revoke request due to connection error with the OpenProject server'], - ['other_error', 'Failed to revoke some users\' OpenProject OAuth access tokens'], - ])('should show error message on various failure', async (errorCode, errorMessage) => { - dialogs.showSuccess - .mockImplementationOnce() - .mockImplementationOnce() - const saveOPOptionsSpy = jest.spyOn(axios, 'put') - .mockImplementationOnce( - () => Promise.resolve({ data: { status: true, oPOAuthTokenRevokeStatus: errorCode } }), - ) - const wrapper = getMountedWrapper({ - state: completeOAUTH2IntegrationState, - }) - await wrapper.vm.saveOPOptions() - - await localVue.nextTick() - - expect(saveOPOptionsSpy).toBeCalledTimes(1) - expect(dialogs.showSuccess).toBeCalledTimes(1) - expect(dialogs.showError).toBeCalledTimes(1) - expect(dialogs.showSuccess).toBeCalledWith('OpenProject admin options saved') - expect(dialogs.showError).toBeCalledWith(errorMessage) - - }) - }) - describe('terms of service', () => { const termsOfServiceComponentStub = 'termsofserviceunsigned-stub' const termsOfServiceComponentStubAttribute = 'isalltermsofservicesignedforuseropenproject' From bc584904c37d864b09d6e64ff51e99c1217b559f Mon Sep 17 00:00:00 2001 From: Saw-jan Date: Tue, 2 Jun 2026 12:39:41 +0545 Subject: [PATCH 3/8] chore: add css classes Signed-off-by: Saw-jan --- src/api/settings.js | 2 +- src/components/admin/FormOAuthSettings.vue | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/api/settings.js b/src/api/settings.js index db92b7521..d8e12372c 100644 --- a/src/api/settings.js +++ b/src/api/settings.js @@ -14,6 +14,6 @@ export function saveAdminConfig(configs) { return axios.put(endpoints.adminConfig, { values: configs }) } -export function createNextcloudOAuthClient(configs) { +export function createNextcloudOAuthClient() { return axios.post(endpoints.nextcloudOAuth) } diff --git a/src/components/admin/FormOAuthSettings.vue b/src/components/admin/FormOAuthSettings.vue index 0a2ac733f..26826bbcf 100644 --- a/src/components/admin/FormOAuthSettings.vue +++ b/src/components/admin/FormOAuthSettings.vue @@ -12,7 +12,7 @@ :is-complete="isOpenProjectFormComplete" :is-disabled="!showOpenProjectForm" :is-dark-theme="isDarkTheme" /> -

+
-
+
+ && isOpenProjectFormInViewMode" + data-test-id="create-nc-oauth-btn" + @click="createNextcloudClient"> @@ -129,7 +131,7 @@ v-else type="primary" :disabled="disableNextcloudFormSave" - data-test-id="submit-nc-oauth-values-form-btn" + data-test-id="submit-nc-oauth-btn" @click="setNextcloudFromToViewMode">