Browse Source

Completed implementation of #81 + #90

webpack5
Lukas Schreiner 4 months ago
parent
commit
efb122f808
  1. 1
      app.config.example.js
  2. 13
      main/components/TemplateEditor.vue
  3. 6
      main/config.js
  4. 4
      main/locales/Imprint.json
  5. 4
      main/locales/Privacy.json
  6. 4
      main/locales/Terms.json
  7. 2
      main/server/router.js
  8. 80
      main/server/templates/controller.js
  9. 21
      main/server/templates/templates.js
  10. 20
      main/views/Imprint.vue
  11. 20
      main/views/Privacy.vue
  12. 22
      main/views/TemplatesList.vue
  13. 20
      main/views/Terms.vue
  14. 42
      main/views/TextPage.vue

1
app.config.example.js

@ -20,6 +20,7 @@ export default {
urlHome:'', //url of external link from home icon
defaultLocale:'de', //default locale
fixNavbar: true, //always display top navbar at fixed position
editStandardTemplates: false, //for developments, it may be possible to edit standard templates online.
allModules: [
modAbout,
modAbcd,

13
main/components/TemplateEditor.vue

@ -34,6 +34,7 @@
import WarningList from 'Main/components/WarningList.vue';
import md from "Main/js/markdown.js";
import consts from '../../consts';
import store from 'Main/js/store.js';
export default {
components: {ModalDialog, WarningList},
@ -43,12 +44,7 @@
return {
warningList: [],
consts: consts,
tplWrk: {
type: '',
language: '',
title: '',
content: ''
},
tplWrk: undefined,
isLoading: true,
templateTypes: [],
@ -60,7 +56,7 @@
if (!this.tplWrk) {
this.tplWrk = this.tpl ? this.tpl : {
type: '',
language: '',
language: store.lang,
title: '',
content: ''
};
@ -78,12 +74,11 @@
return this.warningList.map(c => { c.msg = this.$t(c.msg); return c; });
},
mdPreview() {
return md.renderFull(this.tplWrk.content);
return (this.tplWrk.content ? md.renderFull(this.tplWrk.content) : '');
}
},
methods: {
saveTemplate: function() {
console.log(this.tplWrk);
var baseUrl = 'main/admin/templates';
var req = undefined;
if (this.tplWrk._id) {

6
main/config.js

@ -18,9 +18,9 @@ export default {
{ path: '/main-index', name:'main-index', component: () => import('./views/Home.vue') },
{ path: '/main-admin', name: 'main-admin', component: () => import('./views/Admin.vue')},
{ path: '/main-error', name: 'main-error', component: () => import('./views/Error.vue')},
{ path: '/main-imprint', name: 'main-imprint', component: () => import('./views/Imprint.vue')},
{ path: '/main-privacy', name: 'main-privacy', component: () => import('./views/Privacy.vue')},
{ path: '/main-terms', name: 'main-terms', component: () => import('./views/Terms.vue')},
{ path: '/main-imprint', name: 'main-imprint', meta: {tplId: 'imprint'}, component: () => import('./views/TextPage.vue')},
{ path: '/main-privacy', name: 'main-privacy', meta: {tplId: 'privacy'}, component: () => import('./views/TextPage.vue')},
{ path: '/main-terms', name: 'main-terms', meta: {tplId: 'terms'}, component: () => import('./views/TextPage.vue')},
{ path: '/main-forbidden', name: 'main-forbidden', component: () => import('./views/Forbidden.vue')},
{ path: '/main-version', name: 'main-version', component: () => import('./views/Version.vue')},
{ path: '/main-templates', name: 'main-templates', component: () => import('./views/TemplatesList.vue')},

4
main/locales/Imprint.json

@ -1,4 +0,0 @@
{
"de": {},
"en": {}
}

4
main/locales/Privacy.json

@ -1,4 +0,0 @@
{
"de": {},
"en": {}
}

4
main/locales/Terms.json

@ -1,4 +0,0 @@
{
"de": {},
"en": {}
}

2
main/server/router.js

@ -45,4 +45,6 @@ router.delete('/admin/templates/:id', controller.checkAdmin, controllerTemplates
router.put('/admin/templates/:id', controller.checkAdmin, controllerTemplates.update);
router.post('/admin/templates', controller.checkAdmin, controllerTemplates.create);
router.get('/admin/templatesAvailable', controller.checkAdmin, controllerTemplates.getListAvailable);
// Regular templates
router.get('/templatesByTypeLang/:tplId/:lang', controllerTemplates.getByTypeLang);
module.exports=router;

80
main/server/templates/controller.js

@ -16,9 +16,13 @@ var Templates = require('./templates');
exports.getList = function (req, res, next) {
var filter = {};
var lang = req.query.lang || null;
var defaultTpl = req.query.include_default || false;
if (lang) {
filter.language = lang;
}
if (!defaultTpl) {
filter.defaultTpl = false;
}
Template.find(filter).sort({ type: 'asc', language: 'asc', defaultTpl: 'asc' }).exec(function (err, tplList) {
if (err) return res.status(500).json([{ msg: 'database-error' }]);
return res.status(200).json(tplList);
@ -38,20 +42,38 @@ exports.get = async function (req, res, next) {
}
}
exports.create = async function (req, res, next) {
var reqTpl = new Template(req.body);
var errorList = validateTemplate(reqTpl);
// Check for duplicate
if (errorList.length <= 0) {
var tpl = await Template.findOne({type: reqTpl.type, language: reqTpl.language, defaultTpl: reqTpl.defaultTpl});
if (tpl) {
errorList.push({
field: 'body',
msg: 'tpl-already-exist'
exports.getByTypeLang = async function (req, res, next) {
var tplId = req.params.tplId;
var lang = req.params.lang;
var firstOnly = !req.query.firstOnly || req.query.firstOnly === 'true';
var convert = req.query.convert === 'true';
debug(convert, firstOnly);
var filter = {
type: tplId,
language: lang
};
var replaceOption = {};
// sort defaultTpl 1 => false first, -1 => true first
Template.find(filter).sort({ type: 'asc', language: 'asc', defaultTpl: 1 }).exec(function (err, tplList) {
if (err) return res.status(500).json([{ msg: 'database-error' }]);
if (tplList.length <= 0) return res.status(404).end();
var tplData = firstOnly ? tplList[0] : tplList;
if (convert && firstOnly) {
tplData = Templates.parseReplaceTemplate(tplData, replaceOption);
} else if (convert) {
tplData = tplList.map((tpl) => {
return Templates.parseReplaceTemplate(tpl, replaceOption);
});
}
}
return res.status(200).json(tplData);
});
}
exports.create = async function (req, res, next) {
var reqTpl = new Template(req.body);
var errorList = await validateTemplate(reqTpl);
// Stopp here if there is an error.
if (errorList.length > 0) {
@ -70,7 +92,7 @@ exports.create = async function (req, res, next) {
exports.update = async function (req, res, next) {
var reqTpl = new Template(req.body);
var errorList = validateTemplate(reqTpl);
var errorList = await validateTemplate(reqTpl, req.params.id);
// Check if the tpl exists.
var tpl = await Templates.getById(req.params.id);
@ -78,17 +100,6 @@ exports.update = async function (req, res, next) {
return res.status(404).end();
}
// Ensure, that the changes do not conflict.
if (errorList.length <= 0) {
var dupTpl = await Template.findOne({type: reqTpl.type, language: reqTpl.language}).where('_id').ne(req.params.id);
if (dupTpl) {
errorList.push({
field: 'body',
msg: 'tpl-already-exist'
});
}
}
// Stopp here if there is an error.
if (errorList.length > 0) {
return res.status(400).json(errorList);
@ -127,7 +138,7 @@ exports.delete = async function (req, res, next) {
}
}
function validateTemplate(tpl) {
async function validateTemplate(tpl, id = null) {
var errorList = [];
if (!Templates.isValidType(tpl.type)) {
errorList.push({
@ -154,5 +165,24 @@ function validateTemplate(tpl) {
});
}
// Ensure, that the changes do not conflict.
if (errorList.length <= 0) {
var filter = {
$and: [
{type: tpl.type, language: tpl.language, defaultTpl: tpl.defaultTpl}
]
};
if (id) {
filter.$and.push({_id: {$ne : id}});
}
var dupTpl = await Template.findOne(filter);
if (dupTpl) {
errorList.push({
field: 'body',
msg: 'tpl-already-exist'
});
}
}
return errorList;
}

21
main/server/templates/templates.js

@ -3,6 +3,8 @@
*/
var md = require('markdown-it')();
const MarkdownItAttrs = require('markdown-it-attrs');
md.use(MarkdownItAttrs);
var mustache = require('mustache');
const debug=require('debug')('templates');
@ -24,7 +26,8 @@ const TEMPLATE_TYPES=[
'mails::user::pwreset',
'mails::user::welcome',
'pages::imprint',
'pages::privacy'
'pages::privacy',
'pages::terms'
];
exports.getValidTypes = function() {
@ -125,7 +128,7 @@ exports.getTemplate=async function(tplId, language) {
return tpl;
}
exports.parseTemplate = function(tpl, options) {
function parseTemplate(tpl, options) {
var ret = {
title: "",
content: {
@ -140,6 +143,20 @@ exports.parseTemplate = function(tpl, options) {
return ret;
}
exports.parseTemplate = parseTemplate;
exports.parseReplaceTemplate = function(tpl, options) {
var data = parseTemplate(tpl, options);
tpl = tpl.toObject();
tpl.title = data.title;
tpl.content = {
html: data.content.html,
text: data.content.text,
md: tpl.content
}
return tpl;
}
exports.getById = async function(tplId) {
return await Template.findOne({_id: tplId});
}

20
main/views/Imprint.vue

@ -1,20 +0,0 @@
<template>
</template>
<script>
export default {
name: 'imprint',
mounted: function() {
var url=this.$config.urlImprint
if (url) {
this.$router.replace({name:'main-index'});
location.replace(url);
}
else this.$router.replace({name:'main-text', params: {module:'main', file:'imprint'}});
}
}
</script>
<i18n src="../locales/Imprint.json"/>

20
main/views/Privacy.vue

@ -1,20 +0,0 @@
<template>
</template>
<script>
export default {
name: 'privacy',
mounted: function() {
var url=this.$config.urlPrivacy;
if (url) {
this.$router.replace({name:'main-index'});
location.replace(url);
}
else this.$router.replace({name:'main-text', params: {module:'main', file:'privacy'}});
}
}
</script>
<i18n src="../locales/Privacy.json"/>

22
main/views/TemplatesList.vue

@ -22,15 +22,15 @@
<th style="cursor: pointer">{{$t('col-number')}}</th>
<th style="cursor: pointer" v-on:click="sort('type')">{{$t('type')}}</th>
<th style="cursor: pointer" v-on:click="sort('language')">{{$t('language')}}</th>
<th style="cursor: pointer" v-on:click="sort('defaultTpl')">{{$t('default-tpl')}}</th>
<th style="cursor: pointer" v-on:click="sort('defaultTpl')" v-if="$config.editStandardTemplates">{{$t('default-tpl')}}</th>
<th style="cursor: pointer" v-on:click="sort('title')" class="d-none d-sm-table-cell">{{$t('title')}}</th>
<th style="width:10em">{{$t('action')}}</th>
</tr>
<tr v-for="(tpl,index) in templatesList" v-bind:key="tpl._id" v-show="search(tpl)">
<tr v-for="(tpl,index) in filteredTemplatesList" v-bind:key="tpl._id" v-show="search(tpl)">
<td>{{index+1}}</td>
<td>{{tpl.type}}</td>
<td>{{tpl.language}}</td>
<td>{{getBooleanText(tpl.defaultTpl)}}</td>
<td v-if="$config.editStandardTemplates">{{getBooleanText(tpl.defaultTpl)}}</td>
<td class="d-none d-sm-table-cell hyphenate">{{tpl.title}}</td>
<td style="min-width:6em">
<tooltip-icon :tip="$t('edit-template')" src='~Main/img/edit.svg' v-on:click="edit(tpl)"/>
@ -74,6 +74,13 @@
showEditor: false
}
},
computed: {
filteredTemplatesList: function() {
return this.templatesList.filter((value) => {
return (value.language == store.lang) ? value : undefined;
});
}
},
methods: {
closeModals: function() {
this.showDialogDelete = false;
@ -109,7 +116,12 @@
this.isLoading = true;
this.warningsList=[];
// skip: , { params: { lang: store.lang } }
this.$axios.get('main/admin/templates').then(response=> {
var params = {};
if (this.$config.editStandardTemplates) {
params.include_default = true;
}
this.$axios.get('main/admin/templates', {params:params}).then(response=> {
this.templatesList = response.data;
this.isLoading = false;
});
@ -131,7 +143,7 @@
this.closeModals();
},
createTemplateDialog: function() {
this.modifyTpl = {};
this.modifyTpl = undefined;
this.showEditor = true;
},
edit: function(tpl) {

20
main/views/Terms.vue

@ -1,20 +0,0 @@
<template>
</template>
<script>
export default {
name: 'terms',
mounted: function() {
var url=this.$config.urlTerms;
if (url) {
this.$router.replace({name:'main-index'});
location.replace(url);
}
else this.$router.replace({name:'main-text', params: {module:'main', file:'terms'}});
}
}
</script>
<i18n src="../locales/Terms.json"/>

42
main/views/TextPage.vue

@ -50,6 +50,11 @@ export default {
this.$refs['markdown-block-container'].innerHTML="";
this.warning='';
this.loading=true;
if (this.$route.meta.tplId) {
this.updateBasedTemplate();
return;
}
var md=new MarkdownIt();
md.use(MarkdownItAttrs);
var moduleName=this.module ? this.module : this.$route.params['module'];
@ -83,6 +88,43 @@ export default {
this.loading=false;
}
},
updateBasedTemplate: async function() {
var tplId = 'pages::' + this.$route.meta.tplId;
var lang = this.$route.params['lang'] || store.lang;
var baseUrl = 'main/templatesByTypeLang/' + tplId + '/';
var url = baseUrl + lang;
this.$axios.get(url, {params: {convert: true}}).catch((error) => {
if (error.response && error.response.status == 404 && lang != this.$config.defaultLocale) {
this.$axios.head(baseUrl + this.$config.defaultLocale).catch((error) => {
this.setErrorPage('missing-page-info');
}).then((response) => {
this.setErrorPage('lang-not-available');
});
} else {
this.setErrorPage('missing-page-info');
}
}).then((response) => {
if (response) {
//render
var container = this.$refs['markdown-block-container'];
container.innerHTML = response.data.content.html;
//update html meta infos
var h1 = container.querySelector('h1');
var title = h1 ? h1.innerHTML : response.data.content.title;
document.title=title;
var sts=document.querySelector('p.subtitle');
var desc = sts ? sts.innerHTML : "";
document.querySelector('meta[name="description"]').setAttribute("content", desc);
//update view and add listeners
this.addListeners();
}
this.loading=false;
});
},
setErrorPage: function(code) {
this.warning=this.$t(code)
this.loading=false;
},
addListeners: function() {
var elements=document.getElementsByClassName('preview');
for (var e of elements) {

Loading…
Cancel
Save