refactor: move template settings to vue component
Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>
This commit is contained in:
Родитель
938876af1b
Коммит
fc11c12ae1
112
css/admin.scss
112
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;
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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.get('@newFileMenu').click()
|
||||
cy.get('button[role="menuitem"]').contains('New presentation').click()
|
||||
|
||||
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('form.templates-picker__form').as('templatePicker')
|
||||
cy.get('@templatePicker').contains('presentation').click()
|
||||
cy.get('@templatePicker').find('input[type="submit"]').click()
|
||||
|
||||
cy.waitForViewer()
|
||||
cy.waitForCollabora()
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
it('Create a file from a system template as user', () => {
|
||||
cy.uploadSystemTemplate()
|
||||
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('.toast-error').contains('Template "systemtemplate.otp" already exists').should('be.visible')
|
||||
})
|
||||
|
||||
it('Can be deleted', function() {
|
||||
cy.login(new User('admin', 'admin'))
|
||||
cy.visit('/settings/admin/richdocuments')
|
||||
|
||||
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('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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace OCA\Richdocuments\Service;
|
|||
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 @@ class InitialStateService {
|
|||
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 @@ class InitialStateService {
|
|||
$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'),
|
||||
|
|
|
@ -30,6 +30,8 @@ class Admin implements ISettings {
|
|||
|
||||
public function getForm(): TemplateResponse {
|
||||
$this->initialStateService->provideCapabilities();
|
||||
$this->initialStateService->provideAdminSettings();
|
||||
|
||||
return new TemplateResponse(
|
||||
'richdocuments',
|
||||
'admin',
|
||||
|
@ -45,8 +47,6 @@ class Admin implements ISettings {
|
|||
'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 @@ class Admin implements ISettings {
|
|||
);
|
||||
}
|
||||
|
||||
public function getSection() {
|
||||
public function getSection(): string {
|
||||
return 'richdocuments';
|
||||
}
|
||||
|
||||
public function getPriority() {
|
||||
public function getPriority(): int {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
118
src/admin.js
118
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()
|
||||
})
|
||||
|
|
|
@ -390,6 +390,8 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GlobalTemplates v-if="isSetup" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -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')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<NcSettingsSection v-if="templatesAvailable"
|
||||
:name="t('richdocuments', 'Global Templates')">
|
||||
<input ref="newTemplateInput"
|
||||
type="file"
|
||||
class="hidden-visually"
|
||||
@change="selectFile">
|
||||
|
||||
<div class="template-buttons">
|
||||
<NcButton type="tertiary-no-background" @click="newTemplate">
|
||||
<div class="template-btn new-template-btn">
|
||||
<div class="template-icon">
|
||||
<NewTemplateIcon :size="38" />
|
||||
</div>
|
||||
<span>{{ t('richdocuments', 'New') }}</span>
|
||||
</div>
|
||||
</NcButton>
|
||||
|
||||
<div v-for="template in existingTemplates" :key="template.id">
|
||||
<NcButton type="tertiary-no-background"
|
||||
@click="deleteTemplate(template.id)">
|
||||
<div class="template-btn" :data-cy-template-btn-name="basename(template.name)">
|
||||
<div class="template-icon"
|
||||
:style="`background-image: url(${template.preview})`">
|
||||
<div class="template-delete-overlay">
|
||||
<DeleteIcon :size="38" />
|
||||
</div>
|
||||
</div>
|
||||
<span :title="template.name">
|
||||
{{ basename(template.name) }}
|
||||
</span>
|
||||
</div>
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { NcSettingsSection, NcButton } from '@nextcloud/vue'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import '@nextcloud/dialogs/style.css'
|
||||
import axios from '@nextcloud/axios'
|
||||
import NewTemplateIcon from 'vue-material-design-icons/FileDocumentPlusOutline.vue'
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
|
||||
|
||||
export default {
|
||||
name: 'GlobalTemplates',
|
||||
|
||||
components: {
|
||||
NcSettingsSection,
|
||||
NcButton,
|
||||
NewTemplateIcon,
|
||||
DeleteIcon,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
existingTemplates: [],
|
||||
templatesAvailable: false,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Later maybe we can retrieve these settings from AdminSettings.vue`
|
||||
// and pass them in as props (once AdminSettings is cleaned up)
|
||||
const settings = loadState('richdocuments', 'adminSettings', {})
|
||||
|
||||
this.templatesAvailable = settings.templatesAvailable
|
||||
this.existingTemplates = settings.templates?.filter((template) => {
|
||||
return template.name !== 'Empty'
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
newTemplate() {
|
||||
this.$refs.newTemplateInput?.click()
|
||||
},
|
||||
async selectFile() {
|
||||
const selectedFile = this.$refs.newTemplateInput?.files[0]
|
||||
const templateAlreadyExists = this.existingTemplates.some((template) => {
|
||||
return template.name === selectedFile.name
|
||||
})
|
||||
|
||||
if (!templateAlreadyExists) {
|
||||
const template = await this.uploadTemplate(selectedFile)
|
||||
|
||||
this.existingTemplates.push(template)
|
||||
showSuccess(t('richdocuments', 'Uploaded template "{name}"', { name: template.name }))
|
||||
} else {
|
||||
showError(t('richdocuments', 'Template "{name}" already exists', { name: selectedFile.name }))
|
||||
}
|
||||
},
|
||||
async uploadTemplate(file) {
|
||||
const url = generateUrl('/apps/richdocuments/template')
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('files', file)
|
||||
|
||||
let res = null
|
||||
try {
|
||||
res = await axios.post(url, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
showError(error.response.data.data.message)
|
||||
}
|
||||
|
||||
return res.data.data
|
||||
},
|
||||
async deleteTemplate(templateId) {
|
||||
const url = generateUrl('/apps/richdocuments/template/' + templateId)
|
||||
|
||||
try {
|
||||
await axios.delete(url)
|
||||
} catch {
|
||||
showError(t('richdocuments', 'Unable to delete template'))
|
||||
return
|
||||
}
|
||||
|
||||
const templateIndex = this.existingTemplates.findIndex((template) => {
|
||||
return template.id === templateId
|
||||
})
|
||||
|
||||
if (templateIndex !== -1) {
|
||||
this.existingTemplates.splice(templateIndex, 1)
|
||||
}
|
||||
|
||||
showSuccess(t('richdocuments', 'Deleted template'))
|
||||
},
|
||||
basename(filename) {
|
||||
return filename.substr(0, filename.lastIndexOf('.'))
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
$padding: calc(var(--default-grid-baseline) * 3);
|
||||
|
||||
.template-buttons {
|
||||
display: grid;
|
||||
gap: calc(var(--default-grid-baseline) * 4);
|
||||
grid-template-columns: repeat(auto-fit, 175px);
|
||||
}
|
||||
|
||||
.template-btn {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
border:
|
||||
var(--border-width-input)
|
||||
solid
|
||||
var(--color-border)
|
||||
;
|
||||
border-radius: var(--border-radius-element);
|
||||
width: 175px;
|
||||
height: calc(175px * 1.5);
|
||||
padding: $padding;
|
||||
|
||||
span {
|
||||
text-align: start;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: normal;
|
||||
flex-basis: var(--default-line-height);
|
||||
}
|
||||
}
|
||||
|
||||
.template-btn:hover .template-delete-overlay {
|
||||
background-color: var(--color-box-shadow);
|
||||
|
||||
svg { visibility: visible; }
|
||||
}
|
||||
|
||||
.template-icon {
|
||||
flex-basis: 100%;
|
||||
display: flex;
|
||||
border-radius: var(--border-radius-element);
|
||||
background-size: cover;
|
||||
margin-bottom: $padding;
|
||||
|
||||
svg { color: var(--color-text-lighter); }
|
||||
}
|
||||
|
||||
.template-delete-overlay {
|
||||
border-radius: var(--border-radius-element);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-basis: 100%;
|
||||
|
||||
svg {
|
||||
visibility: hidden;
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
.new-template-btn {
|
||||
.template-icon { justify-content: center; }
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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 $_ */
|
||||
?>
|
||||
<div id="admin-vue" data-initial="<?php p(json_encode($_['settings'], true)); ?>"></div>
|
||||
|
||||
<?php if ($_['settings']['templatesAvailable'] === true) { ?>
|
||||
<form class="section hidden" id="richdocuments-templates" method="post" action="/template/">
|
||||
<input name="files" class="hidden-visually" id="add-template" type="file" />
|
||||
<h2>
|
||||
<?php p($l->t('Global templates')) ?>
|
||||
<label for="add-template" class="icon-add" title="<?php p($l->t('Add a new template')); ?>"></label>
|
||||
</h2>
|
||||
<div id="emptycontent" class="<?php p(empty($_['settings']['templates'])?:'hidden') ?>">
|
||||
<div class="icon-file"></div>
|
||||
<h2>
|
||||
<?php p($l->t('No templates defined.')); ?>
|
||||
</h2>
|
||||
<label for="add-template"><?php p($l->t('Add a new one?')); ?></label>
|
||||
</div>
|
||||
<ul class="<?php p(!empty($_['settings']['templates'])?:'hidden') ?>">
|
||||
<li class="hidden template-model">
|
||||
<figure>
|
||||
<img src="" alt="<?php p($l->t('template preview')) ?>" />
|
||||
<figcaption></figcaption>
|
||||
</figure>
|
||||
<a href="" class="delete-template icon-delete"></a>
|
||||
<div class="delete-cover"></div>
|
||||
</li>
|
||||
<?php foreach ($_['settings']['templates'] as $template) {?>
|
||||
<li data-filename="<?php p($template['name']); ?>">
|
||||
<figure>
|
||||
<?php if (isset($template['preview'])) { ?>
|
||||
<img src="<?php p($template['preview']) ?>?y=297&x=210" alt="<?php p($l->t('template preview')) ?>" />
|
||||
<?php } else { ?>
|
||||
<div class="templatePlaceholder"></div>
|
||||
<?php } ?>
|
||||
<figcaption><?php p($template['name']) ?></figcaption>
|
||||
</figure>
|
||||
<?php if (isset($template['delete'])) { ?><a href="<?php p($template['delete']) ?>" class="delete-template icon-delete"></a><?php } ?>
|
||||
<div class="delete-cover"></div>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</form>
|
||||
<?php } ?>
|
||||
|
||||
<div id="admin-vue" data-initial="<?php p(json_encode($_['settings'], true)); ?>"></div>
|
Загрузка…
Ссылка в новой задаче