Implemented config validation into the JSON editor

This commit is contained in:
Alicia Sykes 2021-06-06 17:09:37 +01:00
parent 7d5a99d9d3
commit 89ac1d1e36
5 changed files with 9828 additions and 9720 deletions

View File

@ -13,6 +13,7 @@
}, },
"dependencies": { "dependencies": {
"ajv": "^8.5.0", "ajv": "^8.5.0",
"ajv7": "npm:ajv@^7.2.2",
"axios": "^0.21.1", "axios": "^0.21.1",
"connect": "^3.7.0", "connect": "^3.7.0",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
@ -76,4 +77,4 @@
"> 1%", "> 1%",
"last 2 versions" "last 2 versions"
] ]
} }

View File

@ -48,7 +48,7 @@
</div> </div>
</TabItem> </TabItem>
<TabItem name="Edit Sections"> <TabItem name="Edit Sections">
<JsonEditor :sections="sections" /> <JsonEditor :config="config" />
</TabItem> </TabItem>
<TabItem name="Edit Site Meta"> <TabItem name="Edit Site Meta">
<EditSiteMeta :config="config" /> <EditSiteMeta :config="config" />

View File

@ -5,7 +5,17 @@
:options="options" :options="options"
height="650px" height="650px"
/> />
<button class="save-button" @click="save()">Save Changes</button> <button :class="`save-button ${!isValid ? 'err' : ''}`" @click="save()">Save Changes</button>
<p class="errors">
<ul>
<li v-for="(error, index) in errorMessages" :key="index" :class="`type-${error.type}`">
{{error.msg}}
</li>
<li v-if="errorMessages.length < 1" class="type-valid">
Config is Valid
</li>
</ul>
</p>
<p class="note"> <p class="note">
It is recommend to backup your existing confiruration before making any changes. It is recommend to backup your existing confiruration before making any changes.
<br> <br>
@ -19,30 +29,84 @@
import VJsoneditor from 'v-jsoneditor'; import VJsoneditor from 'v-jsoneditor';
import { localStorageKeys } from '@/utils/defaults'; import { localStorageKeys } from '@/utils/defaults';
import configSchema from '@/utils/ConfigSchema';
import Ajv from 'ajv7';
export default { export default {
name: 'JsonEditor', name: 'JsonEditor',
props: { props: {
sections: Array, config: Object,
}, },
components: { components: {
VJsoneditor, VJsoneditor,
}, },
data() { data() {
return { return {
jsonData: this.sections, jsonData: this.config,
errorMessages: [],
options: { options: {
schema: configSchema,
mode: 'tree', mode: 'tree',
modes: ['tree', 'code', 'preview'], modes: ['tree', 'code', 'preview'],
name: 'sections', name: 'config',
ajv: new Ajv({
allErrors: true,
verbose: true,
jsPropertySyntax: false,
$data: true,
}),
onValidationError: this.validationErrors,
}, },
}; };
}, },
computed: {
isValid() {
return this.errorMessages.length < 1;
},
},
methods: { methods: {
save() { save() {
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(this.jsonData)); const data = this.jsonData;
if (data.sections) {
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(data.sections));
}
if (data.pageInfo) {
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(data.pageInfo));
}
if (data.appConfig) {
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(data.appConfig));
}
if (data.appConfig.theme) {
localStorage.setItem(localStorageKeys.THEME, data.appConfig.theme);
}
this.$toasted.show('Changes saved succesfully'); this.$toasted.show('Changes saved succesfully');
}, },
validationErrors(errors) {
const errorMessages = [];
errors.forEach((error) => {
switch (error.type) {
case 'validation':
errorMessages.push({
type: 'validation',
msg: `Validatation Warning: ${error.error.keyword} ${error.error.message}`,
});
break;
case 'error':
errorMessages.push({
type: 'parse',
msg: error.message,
});
break;
default:
errorMessages.push({
type: 'editor',
msg: 'Error in JSON',
});
break;
}
});
this.errorMessages = errorMessages;
},
}, },
}; };
</script> </script>
@ -57,6 +121,30 @@ p.note {
color: var(--medium-grey); color: var(--medium-grey);
margin: 0.2rem; margin: 0.2rem;
} }
p.errors {
text-align: left;
margin: 0.5rem auto;
width: 95%;
ul {
list-style: none;
padding: 0;
margin: 0;
li {
&.type-validation {
color: var(--warning);
&::before { content: "⚠️"; }
}
&.type-parse {
color: var(--danger);
&::before { content: "❌"; }
}
&.type-valid {
color: var(--success);
&::before { content: "✅"; }
}
}
}
}
button.save-button { button.save-button {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
margin: 0.25rem auto; margin: 0.25rem auto;
@ -71,6 +159,15 @@ button.save-button {
color: var(--config-settings-color); color: var(--config-settings-color);
border-color: var(--config-settings-color); border-color: var(--config-settings-color);
} }
&.err {
opacity: 0.8;
cursor: default;
&:hover {
background: var(--config-settings-color);
color: var(--config-settings-background);
border-color: var(--danger);
}
}
} }
.jsoneditor-menu { .jsoneditor-menu {

View File

@ -1,217 +1,217 @@
/** /**
* This is the schema for the main app configuration (usually ./public/conf.yml) * This is the schema for the main app configuration (usually ./public/conf.yml)
* It enables the users data to be validated when making changes, * It enables the users data to be validated when making changes,
* and detailed warnings shown, to avoid any unexpected errors or issues * and detailed warnings shown, to avoid any unexpected errors or issues
*/ */
module.exports = { module.exports = {
type: 'object', type: 'object',
required: ['sections'], required: ['sections'],
additionalProperties: false, additionalProperties: false,
properties: { properties: {
/* Page Info */ /* Page Info */
pageInfo: { pageInfo: {
type: 'object', type: 'object',
properties: { properties: {
title: { title: {
type: 'string', type: 'string',
description: 'Title and heading for the app', description: 'Title and heading for the app',
}, },
description: { description: {
type: 'string', type: 'string',
description: 'Sub-title, displayed in header', description: 'Sub-title, displayed in header',
}, },
navLinks: { navLinks: {
type: 'array', type: 'array',
maxItems: 6, maxItems: 6,
description: 'Quick access links, displayed in header', description: 'Quick access links, displayed in header',
items: { items: {
type: 'object', type: 'object',
additionalProperties: false, additionalProperties: false,
required: ['title', 'path'], required: ['title', 'path'],
properties: { properties: {
title: { title: {
type: 'string', type: 'string',
}, },
path: { path: {
type: 'string', type: 'string',
}, },
}, },
}, },
}, },
footerText: { type: 'string' }, footerText: { type: 'string' },
}, },
required: ['title'], required: ['title'],
additionalProperties: false, additionalProperties: false,
}, },
/* App Config */ /* App Config */
appConfig: { appConfig: {
type: 'object', type: 'object',
description: 'Application configuration', description: 'Application configuration',
properties: { properties: {
backgroundImg: { backgroundImg: {
type: 'string', type: 'string',
description: 'A URL to an image asset to be displayed as background', description: 'A URL to an image asset to be displayed as background',
}, },
theme: { theme: {
type: 'string', type: 'string',
default: 'Callisto', default: 'Callisto',
description: 'A theme to be applied by default on first load', description: 'A theme to be applied by default on first load',
}, },
enableFontAwesome: { enableFontAwesome: {
type: 'boolean', type: 'boolean',
default: true, default: true,
description: 'Should load font-awesome assets', description: 'Should load font-awesome assets',
}, },
fontAwesomeKey: { fontAwesomeKey: {
type: 'string', type: 'string',
pattern: '^[a-z0-9]{10}$', pattern: '^[a-z0-9]{10}$',
description: 'API key for font-awesome', description: 'API key for font-awesome',
}, },
cssThemes: { cssThemes: {
type: 'array', type: 'array',
description: 'Theme names to be added to the dropdown', description: 'Theme names to be added to the dropdown',
items: { items: {
type: 'string', type: 'string',
} },
}, },
externalStyleSheet: { externalStyleSheet: {
description: 'URL or URLs of external stylesheets to add to dropdown/ load', description: 'URL or URLs of external stylesheets to add to dropdown/ load',
type: [ type: [
'string', 'array' 'string', 'array',
], ],
items: { items: {
type: 'string', type: 'string',
} },
}, },
customCss: { customCss: {
type: 'string', type: 'string',
description: 'Any custom CSS overides, must be minified', description: 'Any custom CSS overides, must be minified',
}, },
}, },
additionalProperties: false, additionalProperties: false,
}, },
/* Sections */ /* Sections */
sections: { sections: {
type: 'array', type: 'array',
description: 'Array of sections, containing items', description: 'Array of sections, containing items',
items: { items: {
type: 'object', type: 'object',
required: ['name', 'items'], required: ['name', 'items'],
additionalProperties: false, additionalProperties: false,
properties: { properties: {
name: { name: {
type: 'string', type: 'string',
description: 'Title/ heading for a section', description: 'Title/ heading for a section',
}, },
icon: { icon: {
type: 'string', type: 'string',
description: 'Icon will be displayed next to title', description: 'Icon will be displayed next to title',
}, },
/* Section Display Data */ /* Section Display Data */
displayData: { displayData: {
type: 'object', type: 'object',
additionalProperties: false, additionalProperties: false,
description: 'Optional meta data for customizing a section', description: 'Optional meta data for customizing a section',
properties: { properties: {
collapsed: { collapsed: {
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'If true, section needs to be clicked to open', description: 'If true, section needs to be clicked to open',
}, },
color: { color: {
type: 'string', type: 'string',
description: 'Hex code, or HTML color for section fill', description: 'Hex code, or HTML color for section fill',
}, },
customStyles: { customStyles: {
type: 'string', type: 'string',
description: 'CSS overides for section container', description: 'CSS overides for section container',
}, },
itemSize: { itemSize: {
enum: ['small', 'medium', 'large'], enum: ['small', 'medium', 'large'],
default: 'medium', default: 'medium',
description: 'Size of items within the section', description: 'Size of items within the section',
}, },
rows: { rows: {
type: 'number', type: 'number',
minimum: 1, minimum: 1,
maximum: 5, maximum: 5,
default: 1, default: 1,
description: 'The amount of space that the section spans vertically', description: 'The amount of space that the section spans vertically',
}, },
cols: { cols: {
type: 'number', type: 'number',
minimum: 1, minimum: 1,
maximum: 5, maximum: 5,
default: 1, default: 1,
description: 'The amount of space that the section spans horizontally', description: 'The amount of space that the section spans horizontally',
}, },
layout: { layout: {
enum: ['grid', 'auto'], enum: ['grid', 'auto'],
default: 'auto', default: 'auto',
description: 'If set to grid, items have uniform width, and itemCount can be set', description: 'If set to grid, items have uniform width, and itemCount can be set',
}, },
itemCountX: { itemCountX: {
type: 'number', type: 'number',
minimum: 1, minimum: 1,
maximum: 12, maximum: 12,
description: 'Number of items per column', description: 'Number of items per column',
}, },
itemCountY: { itemCountY: {
type: 'number', type: 'number',
minimum: 1, minimum: 1,
maximum: 12, maximum: 12,
description: 'Number of items per row', description: 'Number of items per row',
}, },
}, },
}, },
/* Items within a section */ /* Items within a section */
items: { items: {
type: 'array', type: 'array',
description: 'Array of items to display with a section', description: 'Array of items to display with a section',
items: { items: {
type: 'object', type: 'object',
additionalProperties: false, additionalProperties: false,
required: ['title'], required: ['title'],
properties: { properties: {
title: { title: {
type: 'string', type: 'string',
description: 'Text shown on the item', description: 'Text shown on the item',
}, },
description: { description: {
type: 'string', type: 'string',
nullable: true, nullable: true,
description: 'Short description, shown on hover or in a tooltip', description: 'Short description, shown on hover or in a tooltip',
}, },
icon: { icon: {
type: 'string', type: 'string',
nullable: true, nullable: true,
description: 'An icon, either as a font-awesome identifier, local or remote URL, or auto-fetched favicon', description: 'An icon, either as a font-awesome identifier, local or remote URL, or auto-fetched favicon',
}, },
url: { url: {
type: 'string', type: 'string',
description: 'The destination to navigate to when item is clicked', description: 'The destination to navigate to when item is clicked',
}, },
target: { target: {
enum: ['newtab', 'sametab', 'iframe'], enum: ['newtab', 'sametab', 'iframe'],
default: 'newtab', default: 'newtab',
description: 'Opening method, when item is clicked', description: 'Opening method, when item is clicked',
}, },
color: { color: {
type: 'string', type: 'string',
description: 'A custom fill color of the item', description: 'A custom fill color of the item',
}, },
provider: { provider: {
type: 'string', type: 'string',
description: 'Provider name, e.g. Microsoft', description: 'Provider name, e.g. Microsoft',
}, },
}, },
}, },
}, },
}, },
} },
}, },
}, },
}; };

19002
yarn.lock

File diff suppressed because it is too large Load Diff