Skip to content

Commit

Permalink
Merge pull request #4902 from benthie/feat/read-only-setting
Browse files Browse the repository at this point in the history
Feat/read only setting
  • Loading branch information
max-nextcloud authored Dec 16, 2024
2 parents 09c3802 + d7f4a75 commit 386f793
Show file tree
Hide file tree
Showing 16 changed files with 219 additions and 20 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
101 changes: 101 additions & 0 deletions cypress/e2e/openreadonly.spec.js
Original file line number Diff line number Diff line change
@@ -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')
})
})
})
8 changes: 8 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
4 changes: 4 additions & 0 deletions lib/Service/ConfigService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
5 changes: 5 additions & 0 deletions lib/Service/InitialStateProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
25 changes: 20 additions & 5 deletions src/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@
:has-connection-issue="hasConnectionIssue"
:content-loaded="contentLoaded"
:show-outline-outside="showOutlineOutside"
@read-only-toggled="readOnlyToggled"
@outline-toggled="outlineToggled">
<MainContainer v-if="hasEditor">
<!-- Readonly -->
<div v-if="readOnly" class="text-editor--readonly-bar">
<div v-if="readOnly || (openReadOnlyEnabled && !editMode)" class="text-editor--readonly-bar">
<slot name="readonlyBar">
<ReadonlyBar>
<ReadonlyBar :open-read-only="openReadOnlyEnabled">
<Status :document="document"
:dirty="dirty"
:sessions="filteredSessions"
Expand All @@ -43,6 +44,7 @@
<MenuBar v-if="renderMenus"
ref="menubar"
:is-hidden="hideMenu"
:open-read-only="openReadOnlyEnabled"
:loaded.sync="menubarLoaded">
<Status :document="document"
:dirty="dirty"
Expand Down Expand Up @@ -260,6 +262,8 @@ export default {
hasConnectionIssue: false,
hasEditor: false,
readOnly: true,
openReadOnlyEnabled: OCA.Text.OpenReadOnlyEnabled,
editMode: true,
forceRecreate: false,
menubarLoaded: false,
draggedOver: false,
Expand Down Expand Up @@ -508,8 +512,10 @@ export default {
this.currentSession = session
this.document = document
this.readOnly = document.readOnly
this.editMode = !document.readOnly && !this.openReadOnlyEnabled

if (this.$editor) {
this.$editor.setEditable(!this.readOnly)
this.$editor.setEditable(this.editMode)
}
this.lock = this.$syncService.lock
localStorage.setItem('nick', this.currentSession.guestName)
Expand Down Expand Up @@ -586,7 +592,7 @@ export default {
this.document = document

this.syncError = null
const editable = !this.readOnly && !this.hasConnectionIssue
const editable = this.editMode && !this.hasConnectionIssue
if (this.$editor.isEditable !== editable) {
this.$editor.setEditable(editable)
}
Expand Down Expand Up @@ -682,7 +688,8 @@ export default {
this.$syncService.close()
this.idle = true
this.readOnly = true
this.$editor.setEditable(!this.readOnly)
this.editMode = false
this.$editor.setEditable(this.editMode)

this.$nextTick(() => {
this.emit('sync-service:idle')
Expand Down Expand Up @@ -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()
Expand Down
12 changes: 12 additions & 0 deletions src/components/Editor/Wrapper.provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -28,3 +29,14 @@ export const useOutlineActions = {
},
},
}

export const useReadOnlyActions = {
inject: {
$readOnlyActions: {
from: READ_ONLY_ACTIONS,
default: {
toggle: () => {},
},
},
},
}
10 changes: 9 additions & 1 deletion src/components/Editor/Wrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

<script>
import { useIsRichEditorMixin, useIsRichWorkspaceMixin } from './../Editor.provider.js'
import { OUTLINE_STATE, OUTLINE_ACTIONS } from './Wrapper.provider.js'
import { OUTLINE_STATE, OUTLINE_ACTIONS, READ_ONLY_ACTIONS } from './Wrapper.provider.js'
import useStore from '../../mixins/store.js'
import { mapState } from 'vuex'

Expand All @@ -35,6 +35,11 @@ export default {
toggle: this.outlineToggle,
}),
},
[READ_ONLY_ACTIONS]: {
get: () => ({
toggle: this.readOnlyToggle,
}),
},
})

return val
Expand Down Expand Up @@ -116,6 +121,9 @@ export default {
this.outlineToggle()
}
},
readOnlyToggle() {
this.$emit('read-only-toggled')
},
},

}
Expand Down
11 changes: 9 additions & 2 deletions src/components/Menu/BaseActionEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import debounce from 'debounce'

import { useEditorMixin, useIsMobileMixin } from '../Editor.provider.js'
import { useOutlineActions, useOutlineStateMixin } from '../Editor/Wrapper.provider.js'
import { useOutlineActions, useOutlineStateMixin, useReadOnlyActions } from '../Editor/Wrapper.provider.js'
import { getActionState, getKeys, getKeyshortcuts } from './utils.js'
import useStore from '../../mixins/store.js'

Expand All @@ -18,7 +18,14 @@ import './ActionEntry.scss'
* @type {import("vue").ComponentOptions} BaseActionEntry
*/
const BaseActionEntry = {
mixins: [useEditorMixin, useIsMobileMixin, useStore, useOutlineActions, useOutlineStateMixin],
mixins: [
useEditorMixin,
useIsMobileMixin,
useStore,
useOutlineActions,
useOutlineStateMixin,
useReadOnlyActions,
],
props: {
actionEntry: {
type: Object,
Expand Down
8 changes: 6 additions & 2 deletions src/components/Menu/MenuBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ import ActionSingle from './ActionSingle.vue'
import CharacterCount from './CharacterCount.vue'
import HelpModal from '../HelpModal.vue'
import ToolBarLogic from './ToolBarLogic.js'
import actionsFullEntries from './entries.js'
import { ReadOnlyDoneEntries, MenuEntries } from './entries.js'
import { MENU_ID } from './MenuBar.provider.js'
import { DotsHorizontal, TranslateVariant } from '../icons.js'
import {
Expand Down Expand Up @@ -116,6 +116,10 @@ export default {
type: Boolean,
default: false,
},
openReadOnly: {
type: Boolean,
default: false,
},
},

setup() {
Expand All @@ -126,7 +130,7 @@ export default {

data() {
return {
entries: [...actionsFullEntries],
entries: this.openReadOnly ? [...ReadOnlyDoneEntries, ...MenuEntries] : [...MenuEntries],
randomID: `menu-bar-${(Math.ceil((Math.random() * 10000) + 500)).toString(16)}`,
displayHelp: false,
isReady: false,
Expand Down
10 changes: 8 additions & 2 deletions src/components/Menu/ReadonlyBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

<script>
import { defineComponent } from 'vue'
import { ReadonlyEntries as entries } from './entries.js'
import { ReadOnlyEditEntries, OutlineEntries } from './entries.js'

import ActionList from './ActionList.vue'
import ActionSingle from './ActionSingle.vue'
Expand All @@ -38,9 +38,15 @@ export default defineComponent({
ActionSingle,
},
extends: ToolBarLogic,
props: {
openReadOnly: {
type: Boolean,
default: false,
},
},
data() {
return {
entries,
entries: this.openReadOnly ? [...ReadOnlyEditEntries, ...OutlineEntries] : [...OutlineEntries],
}
},
})
Expand Down
Loading

0 comments on commit 386f793

Please sign in to comment.