diff --git a/README.md b/README.md index 93fe6920151..39bb7bddd6d 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,11 @@ The rich workspaces in the file list can be disabled either by the users in the occ config:app:set text workspace_available --value=0 ``` +The app can be configured to open files read-only by default. This setting is globally valid and can be set by the admin with the following command: + +```bash +occ config:app:set text open_read_only_enabled --value=1 +``` ## 🏗 Development setup diff --git a/cypress/e2e/openreadonly.spec.js b/cypress/e2e/openreadonly.spec.js new file mode 100644 index 00000000000..e40638ba00b --- /dev/null +++ b/cypress/e2e/openreadonly.spec.js @@ -0,0 +1,101 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { User } from '@nextcloud/cypress' +import { randUser } from '../utils/index.js' + +const admin = new User('admin', 'admin') +const user = randUser() + +describe('Open read-only mode', function() { + + before(function() { + cy.createUser(user) + cy.login(user) + cy.uploadFile('test.md', 'text/markdown') + cy.uploadFile('test.md', 'text/plain', 'test.txt') + }) + + const setReadOnlyMode = function(mode) { + cy.login(admin) + cy.setAppConfig('open_read_only_enabled', mode) + } + + describe('Disabled', function() { + const checkMenubar = function() { + cy.get('.text-editor--readonly-bar').should('not.exist') + cy.get('.text-menubar', { timeout: 10000 }) + .getActionEntry('done').should('not.exist') + } + + before(function() { + setReadOnlyMode(0) + }) + + beforeEach(function() { + cy.login(user) + cy.visit('/apps/files') + }) + + it('Test writable markdown file', function() { + cy.openFile('test.md') + checkMenubar() + }) + + it('Test writable text file', function() { + cy.openFile('test.txt') + checkMenubar() + }) + }) + + describe('Enabled', function() { + const requireReadOnlyBar = function() { + cy.get('.text-editor--readonly-bar').should('exist') + cy.get('.text-editor--readonly-bar').getActionEntry('edit').should('exist') + } + + const requireMenubar = function() { + cy.get('.text-editor--readonly-bar').should('not.exist') + cy.get('.text-menubar').getActionEntry('done').should('exist') + } + + before(function() { + setReadOnlyMode(1) + }) + + beforeEach(function() { + cy.login(user) + cy.visit('/apps/files') + }) + + it('Test read-only markdown file', function() { + cy.openFile('test.md') + + requireReadOnlyBar() + + // Switch to edit-mode + cy.get('.text-editor--readonly-bar').getActionEntry('edit').click() + + requireMenubar() + + // Switch to read-only mode + cy.get('.text-menubar').getActionEntry('done').click() + + requireReadOnlyBar() + }) + + it('Test read-only text file', function() { + cy.openFile('test.txt') + + requireReadOnlyBar() + + // Switch to edit-mode + cy.get('.text-editor--readonly-bar').getActionEntry('edit').click() + + // Check that read-only bar does not exist + cy.get('.text-editor--readonly-bar').should('not.exist') + }) + }) +}) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 8cf877f7f8f..213522ece4a 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -401,6 +401,14 @@ Cypress.Commands.add('showHiddenFiles', (value = true) => { ) }) +Cypress.Commands.add('setAppConfig', (key, value) => { + Cypress.log() + return axios.post( + `${url}/ocs/v2.php/apps/testing/api/v1/app/text/${key}`, + { value }, + ) +}) + Cypress.Commands.add('createDescription', (buttonLabel = 'Add folder description') => { const url = '**/remote.php/dav/files/**' cy.intercept({ method: 'PUT', url }) diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index da3af31a768..3a8051d927b 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -22,6 +22,10 @@ public function getDefaultFileExtension(): string { return $this->appConfig->getValueString(Application::APP_NAME, 'default_file_extension', 'md'); } + public function isOpenReadOnlyEnabled(): bool { + return $this->appConfig->getValueString(Application::APP_NAME, 'open_read_only_enabled', '0') === '1'; + } + public function isRichEditingEnabled(): bool { return ($this->appConfig->getValueString(Application::APP_NAME, 'rich_editing_enabled', '1') === '1'); } diff --git a/lib/Service/InitialStateProvider.php b/lib/Service/InitialStateProvider.php index f8138f03574..2908b701cf6 100644 --- a/lib/Service/InitialStateProvider.php +++ b/lib/Service/InitialStateProvider.php @@ -42,6 +42,11 @@ public function provideState(): void { $this->configService->isRichWorkspaceEnabledForUser($this->userId) ); + $this->initialState->provideInitialState( + 'open_read_only_enabled', + $this->configService->isOpenReadOnlyEnabled() + ); + $this->initialState->provideInitialState( 'default_file_extension', $this->configService->getDefaultFileExtension() diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 01d02ba583a..76893fca873 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -24,12 +24,13 @@ :has-connection-issue="hasConnectionIssue" :content-loaded="contentLoaded" :show-outline-outside="showOutlineOutside" + @read-only-toggled="readOnlyToggled" @outline-toggled="outlineToggled"> -
+
- + { this.emit('sync-service:idle') @@ -815,6 +822,14 @@ export default { this.emit('outline-toggled', visible) }, + readOnlyToggled() { + if (this.editMode) { + this.$syncService.save() + } + this.editMode = !this.editMode + this.$editor.setEditable(this.editMode) + }, + onKeyDown(event) { if (event.key === 'Escape') { event.preventDefault() diff --git a/src/components/Editor/Wrapper.provider.js b/src/components/Editor/Wrapper.provider.js index 0626e7c4867..56a231bf490 100644 --- a/src/components/Editor/Wrapper.provider.js +++ b/src/components/Editor/Wrapper.provider.js @@ -5,6 +5,7 @@ export const OUTLINE_STATE = Symbol('wrapper:outline-state') export const OUTLINE_ACTIONS = Symbol('wrapper:outline-actions') +export const READ_ONLY_ACTIONS = Symbol('wrapper:read-only-actions') export const useOutlineStateMixin = { inject: { @@ -28,3 +29,14 @@ export const useOutlineActions = { }, }, } + +export const useReadOnlyActions = { + inject: { + $readOnlyActions: { + from: READ_ONLY_ACTIONS, + default: { + toggle: () => {}, + }, + }, + }, +} diff --git a/src/components/Editor/Wrapper.vue b/src/components/Editor/Wrapper.vue index d81e8900429..01a03c53e5f 100644 --- a/src/components/Editor/Wrapper.vue +++ b/src/components/Editor/Wrapper.vue @@ -16,7 +16,7 @@