diff --git a/css/admin.scss b/css/admin.scss index 19822c4112..32c88717ef 100644 --- a/css/admin.scss +++ b/css/admin.scss @@ -10,37 +10,21 @@ #richdocuments { #use_group_select, #edit_group_select { - width: 200px; display: block; + width: 200px; + display: block; } + p { margin-bottom: 15px; } + #s2id_use_group_select, #s2id_edit_group_select { margin-left: 18px; margin-top: -4px; width: 300px !important; } -} - -input#zoteroAPIKeyField { - width: 300px; -} - -textarea#documentSigningCertField { - width: 600px; -} -textarea#documentSigningKeyField { - width: 600px; -} - -textarea#documentSigningCaField { - width: 600px; -} - -#richdocuments, -#richdocuments-templates { // inline buttons on section headers > h2 { display: inline-flex; @@ -58,6 +42,7 @@ textarea#documentSigningCaField { line-height: 44px; padding-left: 44px; font-size: 16px; + &:hover, &:focus, &:active { @@ -67,77 +52,18 @@ textarea#documentSigningCaField { } } -#richdocuments-templates { - > input { - // feedback for keyboard navigation - &:hover, - &:focus, - &:active { - + h2 .icon-add, - + h2 .icon-loading-small { - opacity: 0.7; - } - + #emptycontent label { - color: var(--color-text-light); - } - } - } - ul:not(.hidden) { - display: flex; - flex-wrap: wrap; - li { - $size: 150px; - $sizeY: math.div($size, 210) * 297; - $space: 10px; - border-radius: var(--border-radius); - border: 1px solid var(--color-border); - margin: $space; - position: relative; - figure { - display: flex; - flex-direction: column; - width: $size; - margin: $space; - img, .templatePlaceholder { - width: $size; - height: $sizeY; - background-color: var(--color-background-dark); - } - figcaption { - margin-top: $space; - } - } - .delete-cover, - .delete-template { - width: $size; - height: $sizeY; - top: 0; - left: 0; - position: absolute; - margin: $space; - opacity: 0; - transition: opacity 250ms ease-in-out; - z-index: 3; - line-height: $sizeY; - text-align: center; - font-size: 20px; - background-size: 24px; - // text is set as bg - color: var(--color-background-darker); - } - .delete-cover { - // bg is set as color - background-color: var(--color-text-lighter); - z-index: 2; - } - &:hover .delete-template, - .delete-template:focus, - .delete-template.icon-loading { - opacity: 1; - + .delete-cover { - opacity: 0.5; - } - } - } - } +textarea#documentSigningCertField { + width: 600px; +} + +textarea#documentSigningKeyField { + width: 600px; +} + +textarea#documentSigningCaField { + width: 600px; +} + +input#zoteroAPIKeyField { + width: 300px; } diff --git a/cypress/e2e/settings.spec.js b/cypress/e2e/settings.spec.js index baccb65239..9ad953276b 100644 --- a/cypress/e2e/settings.spec.js +++ b/cypress/e2e/settings.spec.js @@ -72,11 +72,9 @@ describe('Office admin settings', function() { cy.get('.settings-entry.font-list-settings').contains(font) }) - // FIXME: Template settings only get visible after reload - cy.reload() - cy.get('#richdocuments-templates') + cy.get('.settings-section__name') + .contains('Global Templates') .scrollIntoView() .should('be.visible') - }) }) diff --git a/cypress/e2e/templates.spec.js b/cypress/e2e/templates.spec.js index fa136ed712..f4c54d1ec8 100644 --- a/cypress/e2e/templates.spec.js +++ b/cypress/e2e/templates.spec.js @@ -3,7 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -describe('Create new office files from templates', function() { +import {User} from "@nextcloud/cypress"; + +describe('Global templates', function() { let randUser before(function() { @@ -15,29 +17,54 @@ describe('Create new office files from templates', function() { }) }) - it('Create a new file from a user template', function() { - cy.visit('/apps/files') + it('Can be uploaded', function() { + cy.intercept('POST', '**/richdocuments/template').as('templateUploadRequest') + cy.uploadSystemTemplate({ + fixturePath: 'templates/presentation.otp', + fileName: 'systemtemplate.otp', + mimeType: 'application/vnd.oasis.opendocument.presentation-template', + }) - cy.get('[data-cy-upload-picker=""]') - .should('be.visible') - .as('newFileMenu') + cy.wait('@templateUploadRequest').then(({ response }) => { + expect(response.statusCode).to.equal(201) + expect(response.body.data.name).to.equal('systemtemplate.otp') + expect(response.body.data.type).to.equal('presentation') + }) + }) - cy.get('@newFileMenu').click() - cy.get('button[role="menuitem"]').contains('New presentation').click() + it('Can prevent uploading a duplicate', function() { + cy.uploadSystemTemplate({ + fixturePath: 'templates/presentation.otp', + fileName: 'systemtemplate.otp', + mimeType: 'application/vnd.oasis.opendocument.presentation-template', + }) - cy.get('input[data-cy-files-new-node-dialog-input=""]').type('FileFromTemplate') - cy.get('button[data-cy-files-new-node-dialog-submit=""]').click() + cy.get('.toast-error').contains('Template "systemtemplate.otp" already exists').should('be.visible') + }) - cy.get('form.templates-picker__form').as('templatePicker') - cy.get('@templatePicker').contains('presentation').click() - cy.get('@templatePicker').find('input[type="submit"]').click() + it('Can be deleted', function() { + cy.login(new User('admin', 'admin')) + cy.visit('/settings/admin/richdocuments') - cy.waitForViewer() - cy.waitForCollabora() + cy.get('.settings-section__name') + .contains('Global Templates') + .scrollIntoView() + + cy.intercept('DELETE', '**/richdocuments/template/*').as('templateDeleteRequest') + cy.get('.template-btn[data-cy-template-btn-name="systemtemplate"]').click() + + cy.wait('@templateDeleteRequest').then(({ response }) => { + expect(response.statusCode).to.equal(204) + }) }) - it('Create a file from a system template as user', () => { - cy.uploadSystemTemplate() + it('Can be created by a user', () => { + cy.uploadSystemTemplate({ + fixturePath: 'templates/presentation.otp', + fileName: 'systemtemplate.otp', + mimeType: 'application/vnd.oasis.opendocument.presentation-template', + }) + cy.login(randUser) cy.visit('/apps/files') @@ -53,8 +80,15 @@ describe('Create new office files from templates', function() { cy.get('form.templates-picker__form').as('templatePicker') cy.get('@templatePicker').contains('systemtemplate').click() + + cy.intercept('POST', '**/templates/create').as('templateCreateRequest') cy.get('@templatePicker').find('input[type="submit"]').click() + cy.wait('@templateCreateRequest').then(({ response }) => { + expect(response.statusCode).to.equal(200) + expect(response.body.ocs.data.basename).to.equal('FileFromSystemTemplate.odp') + }) + cy.waitForViewer() cy.waitForCollabora() }) @@ -101,76 +135,99 @@ describe('Create new office files from templates', function() { }) }) -describe('Create templates with fields', () => { - let randUser - - before(() => { - cy.createRandomUser().then(user => { - randUser = user - - cy.login(randUser) - cy.visit('/apps/files') - - // Create a templates folder - cy.get('[data-cy-upload-picker=""]') - .should('be.visible') - .as('newFileMenu') - - cy.get('@newFileMenu').click() - cy.get('button[role="menuitem"]').contains('Create templates folder').click() - - cy.get('button[data-cy-files-new-node-dialog-submit=""]').click() - - // Upload the fixtures into the templates folder - cy.uploadFile(randUser, 'templates/document_template_with_fields.odt', 'application/vnd.oasis.opendocument.text', '/Templates/document.odt') - }) - }) - - it('Create a document from a template with fields', () => { - const fields = [ - { type: 'rich-text', alias: 'Name', content: 'Nextcloud' }, - { type: 'rich-text', alias: 'Favorite app', content: 'richdocuments' }, - { type: 'checkbox', alias: 'Uses Nextcloud at home', checked: true }, - ] - +describe('User templates', function() { + it.skip('Create a new file from a user template', function() { cy.visit('/apps/files') - // Create a new document cy.get('[data-cy-upload-picker=""]') .should('be.visible') .as('newFileMenu') cy.get('@newFileMenu').click() - cy.get('button[role="menuitem"]').contains('New document').click() + cy.get('button[role="menuitem"]').contains('New presentation').click() - cy.get('input[data-cy-files-new-node-dialog-input=""]').type('FileFromTemplateWithFields') + cy.get('input[data-cy-files-new-node-dialog-input=""]').type('FileFromTemplate') cy.get('button[data-cy-files-new-node-dialog-submit=""]').click() - // Choose the document template cy.get('form.templates-picker__form').as('templatePicker') - cy.get('@templatePicker').contains('document').click() + cy.get('@templatePicker').contains('presentation').click() cy.get('@templatePicker').find('input[type="submit"]').click() - // Intercept the POST request to verify the correct fields are submitted - cy.intercept('POST', '**/templates/create', (req) => { - const templateFields = Object.values(req.body.templateFields) + cy.waitForViewer() + cy.waitForCollabora() + }) - expect(templateFields[0].content).to.equal(fields[0].content) - expect(templateFields[1].content).to.equal(fields[1].content) + describe('Create templates with fields', () => { + let randUser - req.continue() - }).as('reqFillFields') + before(() => { + cy.createRandomUser().then(user => { + randUser = user - cy.submitTemplateFields(fields) + cy.login(randUser) + cy.visit('/apps/files') - // Wait for the response and collect the file ID of the created file - cy.wait('@reqFillFields').then(({ response }) => { - cy.wrap(response.body.ocs.data.fileid).as('createdFileId') + // Create a templates folder + cy.get('[data-cy-upload-picker=""]') + .should('be.visible') + .as('newFileMenu') + + cy.get('@newFileMenu').click() + cy.get('button[role="menuitem"]').contains('Create templates folder').click() + + cy.get('button[data-cy-files-new-node-dialog-submit=""]').click() + + // Upload the fixtures into the templates folder + cy.uploadFile(randUser, 'templates/document_template_with_fields.odt', 'application/vnd.oasis.opendocument.text', '/Templates/document.odt') + }) }) - // Test if the fields currently match the values we passed to the template - cy.get('@createdFileId').then(createdFileId => { - cy.verifyTemplateFields(fields, createdFileId) + it('Create a document from a template with fields', () => { + const fields = [ + { type: 'rich-text', alias: 'Name', content: 'Nextcloud' }, + { type: 'rich-text', alias: 'Favorite app', content: 'richdocuments' }, + { type: 'checkbox', alias: 'Uses Nextcloud at home', checked: true }, + ] + + cy.visit('/apps/files') + + // Create a new document + cy.get('[data-cy-upload-picker=""]') + .should('be.visible') + .as('newFileMenu') + + cy.get('@newFileMenu').click() + cy.get('button[role="menuitem"]').contains('New document').click() + + cy.get('input[data-cy-files-new-node-dialog-input=""]').type('FileFromTemplateWithFields') + cy.get('button[data-cy-files-new-node-dialog-submit=""]').click() + + // Choose the document template + cy.get('form.templates-picker__form').as('templatePicker') + cy.get('@templatePicker').contains('document').click() + cy.get('@templatePicker').find('input[type="submit"]').click() + + // Intercept the POST request to verify the correct fields are submitted + cy.intercept('POST', '**/templates/create', (req) => { + const templateFields = Object.values(req.body.templateFields) + + expect(templateFields[0].content).to.equal(fields[0].content) + expect(templateFields[1].content).to.equal(fields[1].content) + + req.continue() + }).as('reqFillFields') + + cy.submitTemplateFields(fields) + + // Wait for the response and collect the file ID of the created file + cy.wait('@reqFillFields').then(({ response }) => { + cy.wrap(response.body.ocs.data.fileid).as('createdFileId') + }) + + // Test if the fields currently match the values we passed to the template + cy.get('@createdFileId').then(createdFileId => { + cy.verifyTemplateFields(fields, createdFileId) + }) }) }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 39e2ba55fa..786550a7bc 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -293,16 +293,19 @@ Cypress.Commands.add('verifyOpen', (filename) => { .should('contain.text', filename) }) -Cypress.Commands.add('uploadSystemTemplate', () => { +Cypress.Commands.add('uploadSystemTemplate', ({ fixturePath, fileName, mimeType }) => { cy.login(new User('admin', 'admin')) cy.visit('/settings/admin/richdocuments') - cy.get('#richdocuments-templates').scrollIntoView() - cy.get('input[type=file]#add-template').selectFile({ - contents: 'cypress/fixtures/templates/presentation.otp', - fileName: 'systemtemplate.otp', - mimeType: 'application/vnd.oasis.opendocument.presentation-template', + + cy.get('.settings-section__name') + .contains('Global Templates') + .scrollIntoView() + + cy.get('.settings-section input[type="file"]').selectFile({ + contents: `cypress/fixtures/${fixturePath}`, + fileName, + mimeType, }, { force: true }) - cy.get('#richdocuments-templates li').contains('systemtemplate.otp') }) Cypress.Commands.add('submitTemplateFields', (fields) => { diff --git a/lib/Service/InitialStateService.php b/lib/Service/InitialStateService.php index 910db97733..72fab2fe97 100644 --- a/lib/Service/InitialStateService.php +++ b/lib/Service/InitialStateService.php @@ -11,6 +11,7 @@ use OCA\Richdocuments\AppConfig; use OCA\Richdocuments\AppInfo\Application; use OCA\Richdocuments\Db\Wopi; +use OCA\Richdocuments\TemplateManager; use OCA\Theming\ImageManager; use OCP\AppFramework\Services\IInitialState; use OCP\Defaults; @@ -24,6 +25,7 @@ public function __construct( private IInitialState $initialState, private AppConfig $appConfig, private ImageManager $imageManager, + private TemplateManager $templateManager, private CapabilitiesService $capabilitiesService, private IURLGenerator $urlGenerator, private Defaults $themingDefaults, @@ -58,6 +60,13 @@ public function provideDocument(Wopi $wopi, array $params): void { $this->provideOptions(); } + public function provideAdminSettings(): void { + $this->initialState->provideInitialState('adminSettings', [ + 'templatesAvailable' => $this->capabilitiesService->hasTemplateSource(), + 'templates' => $this->templateManager->getSystemFormatted(), + ]); + } + public function prepareParams(array $params): array { $defaults = [ 'instanceId' => $this->config->getSystemValue('instanceid'), diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 9536f86293..a5e9778923 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -30,6 +30,8 @@ public function __construct( public function getForm(): TemplateResponse { $this->initialStateService->provideCapabilities(); + $this->initialStateService->provideAdminSettings(); + return new TemplateResponse( 'richdocuments', 'admin', @@ -45,8 +47,6 @@ public function getForm(): TemplateResponse { 'external_apps' => $this->config->getAppValue('richdocuments', 'external_apps'), 'canonical_webroot' => $this->config->getAppValue('richdocuments', 'canonical_webroot'), 'disable_certificate_verification' => $this->config->getAppValue('richdocuments', 'disable_certificate_verification', '') === 'yes', - 'templates' => $this->manager->getSystemFormatted(), - 'templatesAvailable' => $this->capabilitiesService->hasTemplateSource(), 'settings' => $this->appConfig->getAppSettings(), 'demo_servers' => $this->demoService->fetchDemoServers(), 'web_server' => strtolower($_SERVER['SERVER_SOFTWARE']), @@ -59,11 +59,11 @@ public function getForm(): TemplateResponse { ); } - public function getSection() { + public function getSection(): string { return 'richdocuments'; } - public function getPriority() { + public function getPriority(): int { return 0; } } diff --git a/src/admin.js b/src/admin.js index 0b716f4333..1bea381dba 100644 --- a/src/admin.js +++ b/src/admin.js @@ -4,8 +4,6 @@ */ import './init-shared.js' import Vue from 'vue' -import axios from '@nextcloud/axios' -import { generateUrl } from '@nextcloud/router' import AdminSettings from './components/AdminSettings.vue' import '../css/admin.scss' @@ -29,119 +27,3 @@ const element = document.getElementById('admin-vue') new Vue({ render: h => h(AdminSettings, { props: { initial: JSON.parse(element.dataset.initial) } }), }).$mount('#admin-vue') - -/** - * Append a new template to the dom - * - * @param {object} data the template data from the template controller response - */ -function appendTemplateFromData(data) { - const template = document.querySelector('.template-model').cloneNode(true) - template.className = '' - template.dataset.filename = data.name - template.querySelector('img').src = data.preview - template.querySelector('figcaption').textContent = data.name - template.querySelector('.delete-template').href = data.delete - - document.querySelector('#richdocuments-templates > ul').appendChild(template) - template.querySelector('.delete-template').addEventListener('click', deleteTemplate) -} - -/** - * Delete template event handler - * - * @param {Event} event the button click event - */ -function deleteTemplate(event) { - event.preventDefault() - const emptyElmt = document.querySelector('#richdocuments-templates #emptycontent') - const tplListElmt = document.querySelector('#richdocuments-templates > ul') - const elmt = event.target - - // ensure no request is in progress - if (elmt.className.indexOf('loading') === -1 && elmt.textContent === '') { - const remote = event.target.href - elmt.classList.add('icon-loading') - elmt.classList.remove('icon-delete') - - // send request - axios.delete(remote) - .then(function() { - // remove template - elmt.parentElement.remove() - // is list empty? Only the default template is left - if (tplListElmt.querySelectorAll('li').length === 1) { - tplListElmt.classList.add('hidden') - emptyElmt.classList.remove('hidden') - } - }) - .catch(function(e) { - // failure, show warning - elmt.textContent = t('richdocuments', 'Error') - elmt.classList.remove('icon-loading') - setTimeout(function() { - elmt.classList.add('icon-delete') - elmt.textContent = '' - }, 2000) - }) - } -} - -/** - * Init the upload manager and the delete template handler - */ -function initTemplateManager() { - const inputElmt = document.querySelector('#add-template') - const buttonElmt = document.querySelector('.icon-add') - const deleteElmts = document.querySelectorAll('.delete-template') - const emptyElmt = document.querySelector('#richdocuments-templates #emptycontent') - const tplListElmt = document.querySelector('#richdocuments-templates > ul') - - deleteElmts.forEach(function(elmt) { - elmt.addEventListener('click', deleteTemplate) - }) - - // fileupload plugin - $('#richdocuments-templates').fileupload({ - dataType: 'json', - url: generateUrl('apps/richdocuments/template'), - type: 'POST', - - add(e, data) { - // submit on file selection - data.submit() - inputElmt.disabled = true - buttonElmt.className = 'icon-loading-small' - }, - - submit(e, data) { - data.formData = _.extend(data.formData || {}, { - requesttoken: OC.requestToken, - }) - }, - - success(e) { - document.querySelector(`[data-filename="${e.data.name}"]`)?.remove() - inputElmt.disabled = false - buttonElmt.className = 'icon-add' - // add template to dom - appendTemplateFromData(e.data) - tplListElmt.classList.remove('hidden') - emptyElmt.classList.add('hidden') - }, - - fail(e, data) { - // failure, show warning - buttonElmt.className = 'icon-add' - buttonElmt.textContent = t('richdocuments', 'An error occurred') + ': ' + data.jqXHR.responseJSON.data.message - setTimeout(function() { - inputElmt.disabled = false - buttonElmt.textContent = '' - }, 2000) - }, - }) -} - -document.addEventListener('DOMContentLoaded', () => { - initTemplateManager() -}) diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 4ca5e687e1..059e6c4992 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -390,6 +390,8 @@

+ + @@ -406,6 +408,7 @@ import SettingsSelectGroup from './SettingsSelectGroup.vue' import SettingsExternalApps from './SettingsExternalApps.vue' import SettingsInputFile from './SettingsInputFile.vue' import SettingsFontList from './SettingsFontList.vue' +import GlobalTemplates from './AdminSettings/GlobalTemplates.vue' import '@nextcloud/dialogs/style.css' import { getCallbackBaseUrl } from '../helpers/url.js' @@ -435,6 +438,7 @@ export default { SettingsExternalApps, SettingsInputFile, SettingsFontList, + GlobalTemplates, NcModal, NcNoteCard, }, @@ -533,9 +537,6 @@ export default { else this.serverError = Object.values(getCapabilities().collabora).length > 0 ? SERVER_STATE_OK : SERVER_STATE_CONNECTION_ERROR } }, - isSetup() { - this.toggleTemplateSettings() - }, }, beforeMount() { for (const key in this.initial.settings) { @@ -581,7 +582,6 @@ export default { } this.checkIfDemoServerIsActive() this.checkSettings() - this.toggleTemplateSettings() }, methods: { async checkSettings() { @@ -815,13 +815,6 @@ export default { this.settings.fonts.splice(index, 1) } }, - toggleTemplateSettings() { - if (this.isSetup) { - document.getElementById('richdocuments-templates').classList.remove('hidden') - } else { - document.getElementById('richdocuments-templates').classList.add('hidden') - } - }, }, } diff --git a/src/components/AdminSettings/GlobalTemplates.vue b/src/components/AdminSettings/GlobalTemplates.vue new file mode 100644 index 0000000000..8127a98c3d --- /dev/null +++ b/src/components/AdminSettings/GlobalTemplates.vue @@ -0,0 +1,217 @@ + + + + + + + diff --git a/templates/admin.php b/templates/admin.php index a5dca9c522..4c27962b7a 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -4,51 +4,8 @@ * SPDX-FileCopyrightText: 2014-2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-or-later */ -script('richdocuments', 'richdocuments-admin'); -script('files', 'jquery.fileupload'); +\OCP\Util::addScript('richdocuments', 'richdocuments-admin'); /** @var array $_ */ ?> -
- - - - - +
\ No newline at end of file