Merge branch 'master' into FEATURE/login-remember-me-always
48
src/App.vue
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div id="dashy">
|
||||
<EditModeTopBanner v-if="isEditMode" />
|
||||
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" />
|
||||
<Header :pageInfo="pageInfo" />
|
||||
<router-view />
|
||||
@@ -10,11 +11,11 @@
|
||||
|
||||
import Header from '@/components/PageStrcture/Header.vue';
|
||||
import Footer from '@/components/PageStrcture/Footer.vue';
|
||||
import EditModeTopBanner from '@/components/InteractiveEditor/EditModeTopBanner.vue';
|
||||
import LoadingScreen from '@/components/PageStrcture/LoadingScreen.vue';
|
||||
import { componentVisibility } from '@/utils/ConfigHelpers';
|
||||
import ConfigAccumulator from '@/utils/ConfigAccumalator';
|
||||
import { welcomeMsg } from '@/utils/CoolConsole';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import Keys from '@/utils/StoreMutations';
|
||||
import {
|
||||
localStorageKeys,
|
||||
splashScreenTime,
|
||||
@@ -22,28 +23,17 @@ import {
|
||||
language as defaultLanguage,
|
||||
} from '@/utils/defaults';
|
||||
|
||||
const Accumulator = new ConfigAccumulator();
|
||||
const config = Accumulator.config();
|
||||
const visibleComponents = componentVisibility(config.appConfig) || defaultVisibleComponents;
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
Header,
|
||||
Footer,
|
||||
LoadingScreen,
|
||||
},
|
||||
provide: {
|
||||
config,
|
||||
visibleComponents,
|
||||
EditModeTopBanner,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: true, // Set to false after mount complete
|
||||
showFooter: visibleComponents.footer,
|
||||
appConfig: Accumulator.appConfig(),
|
||||
pageInfo: Accumulator.pageInfo(),
|
||||
visibleComponents,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -53,9 +43,29 @@ export default {
|
||||
},
|
||||
/* Determine if splash screen should be shown */
|
||||
shouldShowSplash() {
|
||||
return (this.visibleComponents || defaultVisibleComponents).splashScreen
|
||||
|| !localStorage[localStorageKeys.HIDE_WELCOME_BANNER];
|
||||
return (this.visibleComponents || defaultVisibleComponents).splashScreen;
|
||||
},
|
||||
config() {
|
||||
return this.$store.state.config;
|
||||
},
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
pageInfo() {
|
||||
return this.$store.getters.pageInfo;
|
||||
},
|
||||
sections() {
|
||||
return this.$store.getters.pageInfo;
|
||||
},
|
||||
visibleComponents() {
|
||||
return this.$store.getters.visibleComponents;
|
||||
},
|
||||
isEditMode() {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch(Keys.INITIALIZE_CONFIG);
|
||||
},
|
||||
methods: {
|
||||
/* Injects the users custom CSS as a style tag */
|
||||
@@ -104,9 +114,14 @@ export default {
|
||||
/* Fetch or detect users language, then apply it */
|
||||
applyLanguage() {
|
||||
const language = this.getLanguage();
|
||||
this.$store.commit(Keys.SET_LANGUAGE, language);
|
||||
this.$i18n.locale = language;
|
||||
document.getElementsByTagName('html')[0].setAttribute('lang', language);
|
||||
},
|
||||
hideLoader() {
|
||||
const loader = document.getElementById('loader');
|
||||
if (loader) loader.style.display = 'none';
|
||||
},
|
||||
},
|
||||
/* When component mounted, hide splash and initiate the injection of custom styles */
|
||||
mounted() {
|
||||
@@ -115,6 +130,7 @@ export default {
|
||||
if (this.appConfig.customCss) {
|
||||
const cleanedCss = this.appConfig.customCss.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
this.injectCustomStyles(cleanedCss);
|
||||
this.hideLoader();
|
||||
}
|
||||
welcomeMsg();
|
||||
},
|
||||
|
||||
1
src/assets/interface-icons/back-arrow.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="arrow-alt-left" class="svg-inline--fa fa-arrow-alt-left fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 208v96c0 13.3-10.7 24-24 24H224v103.8c0 21.4-25.8 32.1-41 17L7 273c-9.4-9.4-9.4-24.6 0-34L183 63.3c15.1-15.1 41-4.4 41 17V184h200c13.3 0 24 10.7 24 24z"></path></svg>
|
||||
|
After Width: | Height: | Size: 404 B |
1
src/assets/interface-icons/burger-menu.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="bars" class="svg-inline--fa fa-bars fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"></path></svg>
|
||||
|
After Width: | Height: | Size: 569 B |
1
src/assets/interface-icons/interactive-editor-add.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="plus" class="svg-inline--fa fa-plus fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"></path></svg>
|
||||
|
After Width: | Height: | Size: 467 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="cogs" class="svg-inline--fa fa-cogs fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M217.1 478.1c-23.8 0-41.6-3.5-57.5-7.5-10.6-2.7-18.1-12.3-18.1-23.3v-31.7c-9.4-4.4-18.4-9.6-26.9-15.6l-26.7 15.4c-9.6 5.6-21.9 3.8-29.5-4.3-35.4-37.6-44.2-58.6-57.2-98.5-3.6-10.9 1.1-22.7 11-28.4l26.8-15c-.9-10.3-.9-20.7 0-31.1L12.2 223c-10-5.6-14.6-17.5-11-28.4 13.1-40 21.9-60.9 57.2-98.5 7.6-8.1 19.8-9.9 29.5-4.3l26.7 15.4c8.5-6 17.5-11.2 26.9-15.6V61.4c0-11.1 7.6-20.8 18.4-23.3 44.2-10.5 70-10.5 114.3 0 10.8 2.6 18.4 12.2 18.4 23.3v30.4c9.4 4.4 18.4 9.6 26.9 15.6L346.2 92c9.7-5.6 21.9-3.7 29.6 4.4 26.1 27.9 48.4 58.5 56.8 100.3 2 9.8-2.4 19.8-10.9 25.1l-26.6 16.5c.9 10.3.9 20.7 0 31.1l26.6 16.5c8.4 5.2 12.9 15.2 10.9 24.9-8.1 40.5-29.6 71.3-56.9 100.6-7.6 8.1-19.8 9.9-29.5 4.3l-26.7-15.4c-8.5 6-17.5 11.2-26.9 15.6v31.7c0 11-7.4 20.6-18.1 23.3-15.8 3.8-33.6 7.2-57.4 7.2zm-27.6-50.7c18.3 2.9 36.9 2.9 55.1 0v-44.8l16-5.7c15.2-5.4 29.1-13.4 41.3-23.9l12.9-11 38.8 22.4c11.7-14.4 21-30.5 27.6-47.7l-38.8-22.4 3.1-16.7c2.9-15.9 2.9-32 0-47.9l-3.1-16.7 38.8-22.4c-6.6-17.2-15.9-33.3-27.6-47.7l-38.8 22.4-12.9-11c-12.3-10.5-26.2-18.6-41.3-23.9l-16-5.7V80c-18.3-2.9-36.9-2.9-55.1 0v44.8l-16 5.7c-15.2 5.4-29.1 13.4-41.3 23.9l-12.9 11L80.5 143c-11.7 14.4-21 30.5-27.6 47.7l38.8 22.4-3.1 16.7c-2.9 15.9-2.9 32 0 47.9l3.1 16.7-38.8 22.4c6.6 17.2 15.9 33.4 27.6 47.7l38.8-22.4 12.9 11c12.3 10.5 26.2 18.6 41.3 23.9l16 5.7v44.7zm27.1-85.1c-22.6 0-45.2-8.6-62.4-25.8-34.4-34.4-34.4-90.4 0-124.8 34.4-34.4 90.4-34.4 124.8 0 34.4 34.4 34.4 90.4 0 124.8-17.3 17.2-39.9 25.8-62.4 25.8zm0-128.4c-10.3 0-20.6 3.9-28.5 11.8-15.7 15.7-15.7 41.2 0 56.9 15.7 15.7 41.2 15.7 56.9 0 15.7-15.7 15.7-41.2 0-56.9-7.8-7.9-18.1-11.8-28.4-11.8zM638.5 85c-1-5.8-6-10-11.9-10h-16.1c-3.5-9.9-8.8-19-15.5-26.8l8-13.9c2.9-5.1 1.8-11.6-2.7-15.3C591 11.3 580.5 5.1 569 .8c-5.5-2.1-11.8.1-14.7 5.3l-8 13.9c-10.2-1.9-20.7-1.9-30.9 0l-8-13.9c-3-5.1-9.2-7.3-14.7-5.3-11.5 4.3-22.1 10.5-31.4 18.2-4.5 3.7-5.7 10.2-2.7 15.3l8 13.9c-6.7 7.8-12 16.9-15.5 26.8H435c-5.9 0-11 4.3-11.9 10.2-2 12.2-1.9 24.5 0 36.2 1 5.8 6 10 11.9 10h16.1c3.5 9.9 8.8 19 15.5 26.8l-8 13.9c-2.9 5.1-1.8 11.6 2.7 15.3 9.3 7.7 19.9 13.9 31.4 18.2 5.5 2.1 11.8-.1 14.7-5.3l8-13.9c10.2 1.9 20.7 1.9 30.9 0l8 13.9c3 5.1 9.2 7.3 14.7 5.3 11.5-4.3 22.1-10.5 31.4-18.2 4.5-3.7 5.7-10.2 2.7-15.3l-8-13.9c6.7-7.8 12-16.9 15.5-26.8h16.1c5.9 0 11-4.3 11.9-10.2 1.9-12.2 1.9-24.4-.1-36.2zm-107.8 50.2c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm107.8 255.4c-1-5.8-6-10-11.9-10h-16.1c-3.5-9.9-8.8-19-15.5-26.8l8-13.9c2.9-5.1 1.8-11.6-2.7-15.3-9.3-7.7-19.9-13.9-31.4-18.2-5.5-2.1-11.8.1-14.7 5.3l-8 13.9c-10.2-1.9-20.7-1.9-30.9 0l-8-13.9c-3-5.1-9.2-7.3-14.7-5.3-11.5 4.3-22.1 10.5-31.4 18.2-4.5 3.7-5.7 10.2-2.7 15.3l8 13.9c-6.7 7.8-12 16.9-15.5 26.8h-16.1c-5.9 0-11 4.3-11.9 10.2-2 12.2-1.9 24.5 0 36.2 1 5.8 6 10 11.9 10H451c3.5 9.9 8.8 19 15.5 26.8l-8 13.9c-2.9 5.1-1.8 11.6 2.7 15.3 9.3 7.7 19.9 13.9 31.4 18.2 5.5 2.1 11.8-.1 14.7-5.3l8-13.9c10.2 1.9 20.7 1.9 30.9 0l8 13.9c3 5.1 9.2 7.3 14.7 5.3 11.5-4.3 22.1-10.5 31.4-18.2 4.5-3.7 5.7-10.2 2.7-15.3l-8-13.9c6.7-7.8 12-16.9 15.5-26.8h16.1c5.9 0 11-4.3 11.9-10.2 2-12.1 2-24.4 0-36.2zm-107.8 50.2c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="ban" class="svg-inline--fa fa-ban fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm141.421 106.579c73.176 73.175 77.05 187.301 15.964 264.865L132.556 98.615c77.588-61.105 191.709-57.193 264.865 15.964zM114.579 397.421c-73.176-73.175-77.05-187.301-15.964-264.865l280.829 280.829c-77.588 61.105-191.709 57.193-264.865-15.964z"></path></svg>
|
||||
|
After Width: | Height: | Size: 556 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="copy" class="svg-inline--fa fa-copy fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM266 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 0 1-6 6zm128-96H182a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h106v88c0 13.255 10.745 24 24 24h88v202a6 6 0 0 1-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 0 1 1.757 4.243V112z"></path></svg>
|
||||
|
After Width: | Height: | Size: 736 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fal" data-icon="pencil-alt" class="svg-inline--fa fa-pencil-alt fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M493.255 56.236l-37.49-37.49c-24.993-24.993-65.515-24.994-90.51 0L12.838 371.162.151 485.346c-1.698 15.286 11.22 28.203 26.504 26.504l114.184-12.687 352.417-352.417c24.992-24.994 24.992-65.517-.001-90.51zM164.686 347.313c6.249 6.249 16.379 6.248 22.627 0L368 166.627l30.059 30.059L174 420.745V386h-48v-48H91.255l224.059-224.059L345.373 144 164.686 324.687c-6.249 6.248-6.249 16.378 0 22.626zm-38.539 121.285l-58.995 6.555-30.305-30.305 6.555-58.995L63.255 366H98v48h48v34.745l-19.853 19.853zm344.48-344.48l-49.941 49.941-82.745-82.745 49.941-49.941c12.505-12.505 32.748-12.507 45.255 0l37.49 37.49c12.506 12.506 12.507 32.747 0 45.255z"></path></svg>
|
||||
|
After Width: | Height: | Size: 875 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="file-download" class="svg-inline--fa fa-file-download fa-w-12" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M224 136V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V160H248c-13.2 0-24-10.8-24-24zm76.45 211.36l-96.42 95.7c-6.65 6.61-17.39 6.61-24.04 0l-96.42-95.7C73.42 337.29 80.54 320 94.82 320H160v-80c0-8.84 7.16-16 16-16h32c8.84 0 16 7.16 16 16v80h65.18c14.28 0 21.4 17.29 11.27 27.36zM377 105L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128v-6.1c0-6.3-2.5-12.4-7-16.9z"></path></svg>
|
||||
|
After Width: | Height: | Size: 630 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="exchange" class="svg-inline--fa fa-exchange fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M0 168v-16c0-13.255 10.745-24 24-24h381.97l-30.467-27.728c-9.815-9.289-10.03-24.846-.474-34.402l10.84-10.84c9.373-9.373 24.568-9.373 33.941 0l82.817 82.343c12.497 12.497 12.497 32.758 0 45.255l-82.817 82.343c-9.373 9.373-24.569 9.373-33.941 0l-10.84-10.84c-9.556-9.556-9.341-25.114.474-34.402L405.97 192H24c-13.255 0-24-10.745-24-24zm488 152H106.03l30.467-27.728c9.815-9.289 10.03-24.846.474-34.402l-10.84-10.84c-9.373-9.373-24.568-9.373-33.941 0L9.373 329.373c-12.497 12.497-12.497 32.758 0 45.255l82.817 82.343c9.373 9.373 24.569 9.373 33.941 0l10.84-10.84c9.556-9.556 9.341-25.113-.474-34.402L106.03 384H488c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24z"></path></svg>
|
||||
|
After Width: | Height: | Size: 901 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="quote-right" class="svg-inline--fa fa-quote-right fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M200 32H72C32.3 32 0 64.3 0 104v112c0 39.7 32.3 72 72 72h56v8c0 22.1-17.9 40-40 40h-8c-26.5 0-48 21.5-48 48v48c0 26.5 21.5 48 48 48h8c101.5 0 184-82.5 184-184V104c0-39.7-32.3-72-72-72zm24 264c0 75-61 136-136 136h-8v-48h8c48.5 0 88-39.5 88-88v-56H72c-13.2 0-24-10.8-24-24V104c0-13.2 10.8-24 24-24h128c13.2 0 24 10.8 24 24v192zM504 32H376c-39.7 0-72 32.3-72 72v112c0 39.7 32.3 72 72 72h56v8c0 22.1-17.9 40-40 40h-8c-26.5 0-48 21.5-48 48v48c0 26.5 21.5 48 48 48h8c101.5 0 184-82.5 184-184V104c0-39.7-32.3-72-72-72zm24 264c0 75-61 136-136 136h-8v-48h8c48.5 0 88-39.5 88-88v-56H376c-13.2 0-24-10.8-24-24V104c0-13.2 10.8-24 24-24h128c13.2 0 24 10.8 24 24v192z"></path></svg>
|
||||
|
After Width: | Height: | Size: 895 B |
1
src/assets/interface-icons/interactive-editor-remove.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="trash-alt" class="svg-inline--fa fa-trash-alt fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M268 416h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12zM432 80h-82.41l-34-56.7A48 48 0 0 0 274.41 0H173.59a48 48 0 0 0-41.16 23.3L98.41 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16v336a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM171.84 50.91A6 6 0 0 1 177 48h94a6 6 0 0 1 5.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12z"></path></svg>
|
||||
|
After Width: | Height: | Size: 739 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="save" class="svg-inline--fa fa-save fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM272 80v80H144V80h128zm122 352H54a6 6 0 0 1-6-6V86a6 6 0 0 1 6-6h42v104c0 13.255 10.745 24 24 24h176c13.255 0 24-10.745 24-24V83.882l78.243 78.243a6 6 0 0 1 1.757 4.243V426a6 6 0 0 1-6 6zM224 232c-48.523 0-88 39.477-88 88s39.477 88 88 88 88-39.477 88-88-39.477-88-88-88zm0 128c-22.056 0-40-17.944-40-40s17.944-40 40-40 40 17.944 40 40-17.944 40-40 40z"></path></svg>
|
||||
|
After Width: | Height: | Size: 747 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="memory" class="svg-inline--fa fa-memory fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M480 160h-64v128h64V160zm-128 0h-64v128h64V160zm-128 0h-64v128h64V160zm408 0h8V96c0-17.67-14.33-32-32-32H32C14.33 64 0 78.33 0 96v64h8c13.26 0 24 10.74 24 24 0 13.25-10.74 24-24 24H0v240h640V208h-8c-13.25 0-24-10.75-24-24 0-13.26 10.75-24 24-24zm-40 240h-64c0-8.84-7.16-16-16-16s-16 7.16-16 16h-96c0-8.84-7.16-16-16-16s-16 7.16-16 16h-96c0-8.84-7.16-16-16-16s-16 7.16-16 16h-96c0-8.84-7.16-16-16-16s-16 7.16-16 16H48v-48h544v48zm0-275.84c-19.29 12.93-32 34.93-32 59.84s12.71 46.91 32 59.84V320H48v-76.16c19.29-12.93 32-34.93 32-59.84s-12.71-46.91-32-59.84V112h544v12.16z"></path></svg>
|
||||
|
After Width: | Height: | Size: 802 B |
1
src/assets/interface-icons/open-parent.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="level-up" class="svg-inline--fa fa-level-up fa-w-11" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M345.04 144l-136-136.901c-9.388-9.465-24.691-9.465-34.079 0L38.96 144c-9.307 9.384-9.277 24.526.069 33.872l22.056 22.056c9.619 9.619 25.301 9.329 34.557-.639L152 138.84V432H68.024a11.996 11.996 0 0 0-8.485 3.515l-56 56C-4.021 499.074 1.333 512 12.024 512H208c13.255 0 24-10.745 24-24V138.84l56.357 60.448c9.256 9.968 24.938 10.258 34.557.639l22.056-22.056c9.346-9.345 9.377-24.487.07-33.871z"></path></svg>
|
||||
|
After Width: | Height: | Size: 627 B |
1
src/assets/interface-icons/open-top.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="box-open" class="svg-inline--fa fa-box-open fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M638.3 143.8L586.8 41c-4-8-12.1-9.5-16.7-8.9L320 64 69.8 32.1c-4.6-.6-12.6.9-16.6 8.9L1.7 143.8c-4.6 9.2.3 20.2 10.1 23L64 181.7V393c0 14.7 10 27.5 24.2 31l216.2 54.1c6 1.5 17.4 3.4 31 0L551.8 424c14.2-3.6 24.2-16.4 24.2-31V181.7l52.1-14.9c9.9-2.8 14.7-13.8 10.2-23zM86 82.6l154.8 19.7-41.2 68.3-138-39.4L86 82.6zm26 112.8l97.8 27.9c8 2.3 15.2-1.8 18.5-7.3L296 103.8v322.7l-184-46V195.4zm416 185.1l-184 46V103.8l67.7 112.3c3.3 5.5 10.6 9.6 18.5 7.3l97.8-27.9v185zm-87.7-209.9l-41.2-68.3L554 82.6l24.3 48.6-138 39.4z"></path></svg>
|
||||
|
After Width: | Height: | Size: 751 B |
@@ -1,20 +1 @@
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
data-prefix="far"
|
||||
data-icon="browser"
|
||||
class="svg-inline--fa fa-browser fa-w-16"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
transform = "rotate(-90 250 250)"
|
||||
fill="currentColor"
|
||||
d="M464 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5
|
||||
48-48V80c0-26.5-21.5-48-48-48zM48 92c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12
|
||||
12v24c0 6.6-5.4 12-12 12H60c-6.6 0-12-5.4-12-12V92zm416 334c0 3.3-2.7 6-6
|
||||
6H54c-3.3 0-6-2.7-6-6V168h416v258zm0-310c0 6.6-5.4 12-12 12H172c-6.6
|
||||
0-12-5.4-12-12V92c0-6.6 5.4-12 12-12h280c6.6 0 12 5.4 12 12v24z">
|
||||
</path>
|
||||
</svg>
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="briefcase" class="svg-inline--fa fa-briefcase fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M464 128h-80V80c0-26.51-21.49-48-48-48H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v256c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V176c0-26.51-21.49-48-48-48zM176 80h160v48H176V80zM54 176h404c3.31 0 6 2.69 6 6v74H48v-74c0-3.31 2.69-6 6-6zm404 256H54c-3.31 0-6-2.69-6-6V304h144v24c0 13.25 10.75 24 24 24h80c13.25 0 24-10.75 24-24v-24h144v122c0 3.31-2.69 6-6 6z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 697 B After Width: | Height: | Size: 617 B |
1
src/assets/interface-icons/unknown-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="question" class="svg-inline--fa fa-question fa-w-12" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M202.021 0C122.202 0 70.503 32.703 29.914 91.026c-7.363 10.58-5.093 25.086 5.178 32.874l43.138 32.709c10.373 7.865 25.132 6.026 33.253-4.148 25.049-31.381 43.63-49.449 82.757-49.449 30.764 0 68.816 19.799 68.816 49.631 0 22.552-18.617 34.134-48.993 51.164-35.423 19.86-82.299 44.576-82.299 106.405V320c0 13.255 10.745 24 24 24h72.471c13.255 0 24-10.745 24-24v-5.773c0-42.86 125.268-44.645 125.268-160.627C377.504 66.256 286.902 0 202.021 0zM192 373.459c-38.196 0-69.271 31.075-69.271 69.271 0 38.195 31.075 69.27 69.271 69.27s69.271-31.075 69.271-69.271-31.075-69.27-69.271-69.27z"></path></svg>
|
||||
|
After Width: | Height: | Size: 816 B |
@@ -38,7 +38,7 @@
|
||||
"edit-config-tab": "Edit Config",
|
||||
"custom-css-tab": "Custom Styles",
|
||||
"heading": "Configuration Options",
|
||||
"download-config-button": "Download Config",
|
||||
"download-config-button": "View / Export Config",
|
||||
"edit-config-button": "Edit Config",
|
||||
"edit-css-button": "Edit Custom CSS",
|
||||
"cloud-sync-button": "Enable Cloud Sync",
|
||||
@@ -102,6 +102,7 @@
|
||||
"export-button": "Export Custom Variables",
|
||||
"reset-button": "Reset Styles for",
|
||||
"show-all-button": "Show All Variables",
|
||||
"change-fonts-button": "Change Fonts",
|
||||
"save-button": "Save",
|
||||
"cancel-button": "Cancel",
|
||||
"saved-toast": "{theme} Updated Successfully",
|
||||
@@ -113,6 +114,7 @@
|
||||
"location-local-label": "Apply Locally",
|
||||
"location-disk-label": "Write Changes to Config File",
|
||||
"save-button": "Save Changes",
|
||||
"preview-button": "Preview Changes",
|
||||
"valid-label": "Config is Valid",
|
||||
"status-success-msg": "Task Complete",
|
||||
"status-fail-msg": "Task Failed",
|
||||
@@ -165,9 +167,82 @@
|
||||
"restore-success-msg": "Config Restored Successfully"
|
||||
},
|
||||
"menu": {
|
||||
"sametab": "Open in Current Tab",
|
||||
"newtab": "Open in New Tab",
|
||||
"modal": "Open in Pop-Up Modal",
|
||||
"workspace": "Open in Workspace View"
|
||||
"open-section-title": "Open In",
|
||||
"sametab": "Current Tab",
|
||||
"newtab": "New Tab",
|
||||
"modal": "Pop-Up Modal",
|
||||
"workspace": "Workspace View",
|
||||
"options-section-title": "Options",
|
||||
"edit-item": "Edit",
|
||||
"move-item": "Copy or Move",
|
||||
"remove-item": "Remove"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Open In",
|
||||
"sametab": "Current Tab",
|
||||
"newtab": "New Tab",
|
||||
"modal": "Pop-Up Modal",
|
||||
"workspace": "Workspace View",
|
||||
"options-section-title": "Options",
|
||||
"edit-item": "Edit",
|
||||
"move-item": "Copy or Move",
|
||||
"remove-item": "Remove"
|
||||
},
|
||||
"section": {
|
||||
"open-section": "Open Section",
|
||||
"edit-section": "Edit",
|
||||
"move-section": "Move To",
|
||||
"remove-section": "Remove"
|
||||
}
|
||||
},
|
||||
"interactive-editor": {
|
||||
"menu": {
|
||||
"start-editing-tooltip": "Enter the Interactive Editor",
|
||||
"edit-site-data-subheading": "Edit Site Data",
|
||||
"edit-page-info-btn": "Edit Page Info",
|
||||
"edit-page-info-tooltip": "App title, description, nav links, footer text, etc",
|
||||
"edit-app-config-btn": "Edit App Config",
|
||||
"edit-app-config-tooltip": "All other app configuration options",
|
||||
"config-save-methods-subheading": "Config Saving Options",
|
||||
"save-locally-btn": "Save Locally",
|
||||
"save-locally-tooltip": "Save config locally, to browser storage. This will not affect your config file, but changes will only be saved on this device",
|
||||
"save-disk-btn": "Save to Disk",
|
||||
"save-disk-tooltip": "Save config to the conf.yml file on disk. This will backup, and then over-write your existing config",
|
||||
"export-config-btn": "Export Config",
|
||||
"export-config-tooltip": "View and export new config, either to a file, or to clipboard",
|
||||
"cloud-backup-btn": "Backup to Cloud",
|
||||
"cloud-backup-tooltip": "Save encrypted backup of configuration to cloud",
|
||||
"edit-raw-config-btn": "Edit Raw Config",
|
||||
"edit-raw-config-tooltip": "View and modify raw config via JSON editor",
|
||||
"cancel-changes-btn": "Cancel Edit",
|
||||
"cancel-changes-tooltip": "Reset current modifications, and exit Edit Mode. This will not affect your saved config",
|
||||
"edit-mode-name": "Edit Mode",
|
||||
"edit-mode-subtitle": "You are in Edit Mode",
|
||||
"edit-mode-description": "This means you can make modifications to your config, and preview the results, but until you save, none of your changes will be preserved.",
|
||||
"save-stage-btn": "Save",
|
||||
"cancel-stage-btn": "Cancel"
|
||||
},
|
||||
"edit-section": {
|
||||
"edit-section-title": "Edit Section",
|
||||
"add-section-title": "Add New Section",
|
||||
"edit-tooltip": "Click to Edit, or right-click for more options",
|
||||
"remove-confirm": "Are you sure you want to remove this section? This action can be undone later."
|
||||
},
|
||||
"edit-app-config": {
|
||||
"warning-msg-title": "Proceed with Caution",
|
||||
"warning-msg-l1": "The following options are for advanced app configuration.",
|
||||
"warning-msg-l2": "If you are unsure about any of the fields, please reference the",
|
||||
"warning-msg-docs": "documentation",
|
||||
"warning-msg-l3": "to avoid unintended consequences."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Export Config",
|
||||
"copy-clipboard-btn": "Copy to Clipboard",
|
||||
"copy-clipboard-tooltip": "Copy all app config to system clipboard, in YAML format",
|
||||
"download-file-btn": "Download as File",
|
||||
"download-file-tooltip": "Download all app config to your device, in a YAML file",
|
||||
"view-title": "View Config"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@
|
||||
"item-size-small": "Petite",
|
||||
"item-size-medium": "Moyenne",
|
||||
"item-size-large": "Grande",
|
||||
"config-launcher-label": "Config.",
|
||||
"config-launcher-label": "Configuration",
|
||||
"config-launcher-tooltip": "Modifier la configuration",
|
||||
"sign-out-tooltip": "Déconnexion",
|
||||
"sign-in-tooltip": "Connexion",
|
||||
@@ -102,6 +102,7 @@
|
||||
"export-button": "Exporter des variables personnalisées",
|
||||
"reset-button": "Réinitialiser les styles pour",
|
||||
"show-all-button": "Afficher toutes les variables",
|
||||
"change-fonts-button": "Changer les Polices d'écritures",
|
||||
"save-button": "Enregistrer",
|
||||
"cancel-button": "Annuler",
|
||||
"saved-toast": "{theme} mis à jour avec succès",
|
||||
@@ -113,6 +114,7 @@
|
||||
"location-local-label": "Appliquer localement",
|
||||
"location-disk-label": "Appliquer dans le fichier de configuration",
|
||||
"save-button": "Enregistrer",
|
||||
"preview-button": "Prévisuliser les modifications",
|
||||
"valid-label": "La configuration est valide",
|
||||
"status-success-msg": "Tâche terminée",
|
||||
"status-fail-msg": "Échec de la tâche",
|
||||
@@ -165,9 +167,78 @@
|
||||
"restore-success-msg": "Configuration restaurée avec succès"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Ouvrir ...",
|
||||
"sametab": "Ouvrir dans l'onglet actuel",
|
||||
"newtab": "Ouvrir dans un nouvel onglet",
|
||||
"modal": "Ouvrir en mode fenêtré",
|
||||
"workspace": "Ouvrir en plein écran"
|
||||
"workspace": "Ouvrir en plein écran",
|
||||
"options-section-title": "Options",
|
||||
"edit-item": "Modifier",
|
||||
"move-item": "Copier et Déplacer",
|
||||
"remove-item": "Supprimer"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Ouvrir ...",
|
||||
"sametab": "Ouvrir dans l'onglet actuel",
|
||||
"newtab": "Ouvrir dans un nouvel onglet",
|
||||
"modal": "Ouvrir en mode fenêtré",
|
||||
"workspace": "Ouvrir en plein écran",
|
||||
"options-section-title": "Options",
|
||||
"edit-item": "Modifier",
|
||||
"move-item": "Copier et Déplacer",
|
||||
"remove-item": "Supprimer"
|
||||
},
|
||||
"section": {
|
||||
"open-section": "Ouvrir",
|
||||
"edit-section": "Modifier",
|
||||
"move-section": "Déplacer vers",
|
||||
"remove-section": "Supprimer"
|
||||
}
|
||||
},
|
||||
"interactive-editor": {
|
||||
"menu": {
|
||||
"start-editing-tooltip": "Entrer dans l'éditeur interactif",
|
||||
"edit-site-data-subheading": "Modifier l'application",
|
||||
"edit-page-info-btn": "Modifier les informations",
|
||||
"edit-page-info-tooltip": "Titre de l'application, description, liens de navigation, texte de pied de page, etc.",
|
||||
"edit-app-config-btn": "Modifier la configuration",
|
||||
"edit-app-config-tooltip": "Toutes les autres options de configuration",
|
||||
"config-save-methods-subheading": "Options de sauvegarde",
|
||||
"save-locally-btn": "Enregistrer localement",
|
||||
"save-locally-tooltip": "Enregistrez la configuration localement, dans le stockage du navigateur. Cela n'affectera pas votre fichier de configuration, mais les modifications ne seront présentes que sur cet appareil",
|
||||
"save-disk-btn": "Enregistrer sur le disque",
|
||||
"save-disk-tooltip": "Enregistrez la configuration dans le fichier conf.yml sur le disque. Cela sauvegardera, puis écrasera votre configuration existante",
|
||||
"export-config-btn": "Exporter la configuration",
|
||||
"export-config-tooltip": "Afficher et exporter la nouvelle configuration, soit dans un fichier, soit dans le presse-papier",
|
||||
"cancel-changes-btn": "Annuler",
|
||||
"cancel-changes-tooltip": "Réinitialisez les modifications en cours et quittez le mode d'édition. Cela n'affectera pas votre configuration enregistrée",
|
||||
"edit-mode-name": "Éditeur interactif",
|
||||
"edit-mode-subtitle": "Vous êtes en mode d'édition",
|
||||
"edit-mode-description": "Vous pouvez apporter des modifications à votre configuration et prévisualiser les résultats, mais jusqu'à ce que vous sauvegardiez, aucune de vos modifications ne sera conservée.",
|
||||
"save-stage-btn": "Enregistrer",
|
||||
"cancel-stage-btn": "Annuler"
|
||||
},
|
||||
"edit-section": {
|
||||
"edit-section-title": "Éditeur",
|
||||
"add-section-title": "Ajouter une section",
|
||||
"edit-tooltip": "Cliquer pour modifier ou cliquer droit pour plus d'options",
|
||||
"remove-confirm": "Voulez-vous vraiment supprimer cette section ? Cette action peut être annulée ultérieurement."
|
||||
},
|
||||
"edit-app-config": {
|
||||
"warning-msg-title": "Procéder avec prudence",
|
||||
"warning-msg-l1": "Les options suivantes concernent la configuration avancée de l'application.",
|
||||
"warning-msg-l2": "Si vous n'êtes pas sûr de l'un des champs, veuillez consulter la",
|
||||
"warning-msg-docs": "documentation",
|
||||
"warning-msg-l3": "pour éviter des conséquences inattendues."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Exporter la configuration",
|
||||
"copy-clipboard-btn": "Copier dans le presse-papier",
|
||||
"copy-clipboard-tooltip": "Copier la configuration complète de l'application sur votre appareil dans un fichier YAML",
|
||||
"download-file-btn": "Télécharger",
|
||||
"download-file-tooltip": "Téléchargez la configuration complète de l'application sur votre appareil dans un fichier YAML",
|
||||
"view-title": "Afficher la configuration"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
172
src/assets/locales/nb.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"home": {
|
||||
"no-results": "Ingen søkeresultater",
|
||||
"no-data": "Ingen data konfigurert"
|
||||
},
|
||||
"search": {
|
||||
"search-label": "Søk",
|
||||
"search-placeholder": "Begynn å skrive for å filtrere",
|
||||
"clear-search-tooltip": "Fjern søk",
|
||||
"enter-to-search-web": "Trykk enter for å søke på nettet"
|
||||
},
|
||||
"login": {
|
||||
"title": "Dashy",
|
||||
"username-label": "Brukernavn",
|
||||
"password-label": "Passord",
|
||||
"login-button": "Logg inn",
|
||||
"remember-me-label": "Husk meg",
|
||||
"remember-me-never": "Aldri",
|
||||
"remember-me-hour": "4 timer",
|
||||
"remember-me-day": "1 dag",
|
||||
"remember-me-week": "1 uke",
|
||||
"error-missing-username": "Mangler brukernavn",
|
||||
"error-missing-password": "Manglende passord",
|
||||
"error-incorrect-username": "Bruker ikke funnet",
|
||||
"error-incorrect-password": "Feil passord",
|
||||
"success-message": "Logger på...",
|
||||
"logout-message": "Logget ut",
|
||||
"already-logged-in-title": "Allerede logget inn",
|
||||
"already-logged-in-text": "Du er logget inn som",
|
||||
"continue-to-dashboard": "Fortsett til dashbordet",
|
||||
"log-out-button": "Logg ut",
|
||||
"continue-guest-button": "Fortsett som gjest"
|
||||
},
|
||||
"config": {
|
||||
"main-tab": "Hovedmeny",
|
||||
"view-config-tab": "Vis konfigurering",
|
||||
"edit-config-tab": "Rediger konfigurering",
|
||||
"custom-css-tab": "Egendefinerte stiler",
|
||||
"heading": "Konfigurasjonsalternativer",
|
||||
"download-config-button": "Last ned konfigurasjon",
|
||||
"edit-config-button": "Rediger konfigurering",
|
||||
"edit-css-button": "Rediger tilpasset CSS",
|
||||
"cloud-sync-button": "Aktiver skysynkronisering",
|
||||
"edit-cloud-sync-button": "Rediger skysynkronisering",
|
||||
"rebuild-app-button": "Bygg program",
|
||||
"change-language-button": "Endre appspråk",
|
||||
"reset-settings-button": "Tilbakestill lokale innstillinger",
|
||||
"app-info-button": "Appinfo",
|
||||
"backup-note": "Det anbefales å ta en sikkerhetskopi av konfigurasjonen din før du gjør endringer.",
|
||||
"reset-config-msg-l1": "Dette fjerner alle brukerinnstillinger fra lokal lagring, men påvirker ikke din 'conf.yml' -fil.",
|
||||
"reset-config-msg-l2": "Du bør først ta sikkerhetskopi av eventuelle endringer du har gjort lokalt, hvis du vil bruke dem i fremtiden.",
|
||||
"reset-config-msg-l3": "Er du sikker på at du vil fortsette?",
|
||||
"data-cleared-msg": "Data slettet vellykket",
|
||||
"actions-label": "Handlinger",
|
||||
"copy-config-label": "Kopier konfigurasjon",
|
||||
"data-copied-msg": "Konfig er kopiert til utklippstavlen",
|
||||
"reset-config-label": "Tilbakestill konfigurasjon",
|
||||
"css-save-btn": "Lagre endringer",
|
||||
"css-note-label": "Merk",
|
||||
"css-note-l1": "Du må oppdatere siden for at endringene dine skal tre i kraft.",
|
||||
"css-note-l2": "Overstyring av stiler lagres bare lokalt, så det anbefales å lage en kopi av CSS.",
|
||||
"css-note-l3": "For å fjerne alle egendefinerte stiler, slett innholdet og trykk Lagre endringer"
|
||||
},
|
||||
"alternate-views": {
|
||||
"alternate-view-heading": "Bytt visning",
|
||||
"default": "Standard",
|
||||
"workspace": "Workspace",
|
||||
"minimal": "Minimal"
|
||||
},
|
||||
"settings": {
|
||||
"theme-label": "Tema",
|
||||
"layout-label": "Layout",
|
||||
"layout-auto": "Auto",
|
||||
"layout-horizontal": "Horisontal",
|
||||
"layout-vertical": "Vertikal",
|
||||
"item-size-label": "Enhetsstørrelse",
|
||||
"item-size-small": "Small",
|
||||
"item-size-medium": "Medium",
|
||||
"item-size-large": "Large",
|
||||
"config-launcher-label": "Konfig",
|
||||
"config-launcher-tooltip": "Oppdater konfigurasjon",
|
||||
"sign-out-tooltip": "Logg av",
|
||||
"sign-in-tooltip": "Logg inn",
|
||||
"sign-in-welcome": "Hei {brukernavn}!"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashy-versjon",
|
||||
"up-to-date": "Oppdatert",
|
||||
"out-of-date": "Oppdatering tilgjengelig",
|
||||
"unsupported-version-l1": "Du bruker en ikke-støttet versjon av Dashy",
|
||||
"unsupported-version-l2": "For den beste opplevelsen og de siste sikkerhetsoppdateringene, vennligst oppdater til"
|
||||
},
|
||||
"language-switcher": {
|
||||
"title": "Endre applikasjonsspråk",
|
||||
"dropdown-label": "Velg et språk",
|
||||
"save-button": "Lagre",
|
||||
"success-msg": "Språk oppdatert til"
|
||||
},
|
||||
"theme-maker": {
|
||||
"title": "Temakonfigurator",
|
||||
"export-button": "Eksporter tilpassede variabler",
|
||||
"reset-button": "Tilbakestill stiler for",
|
||||
"show-all-button": "Vis alle variabler",
|
||||
"save-button": "Lagre",
|
||||
"cancel-button": "Avbryt",
|
||||
"saved-toast": "{theme} Oppdatert vellykket",
|
||||
"copied-toast": "Temadata for {theme} kopiert til utklippstavlen",
|
||||
"reset-toast": "Egendefinerte farger for {theme} fjernet"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Lagre beliggenhet",
|
||||
"location-local-label": "Søk lokalt",
|
||||
"location-disk-label": "Skriv endringer i konfigurasjonsfil",
|
||||
"save-button": "Lagre endringer",
|
||||
"valid-label": "Konfigurasjon er gyldig",
|
||||
"status-success-msg": "Oppgaven fullført",
|
||||
"status-fail-msg": "Oppgaven mislyktes",
|
||||
"success-msg-disk": "Konfigurasjonsfil skrevet til disk med hell",
|
||||
"success-msg-local": "Lokale endringer er lagret",
|
||||
"success-note-l1": "Appen bør bygge om automatisk.",
|
||||
"success-note-l2": "Dette kan ta opptil et minutt.",
|
||||
"success-note-l3": "Du må oppdatere siden for at endringene skal tre i kraft.",
|
||||
"error-msg-save-mode": "Velg en lagringsmodus: lokal eller fil",
|
||||
"error-msg-cannot-save": "Det oppsto en feil under konfigurering",
|
||||
"error-msg-bad-json": "Feil i JSON, muligens feilformet",
|
||||
"warning-msg-validation": "Valideringsadvarsel",
|
||||
"not-admin-note": "Du kan ikke skrive endret til disk, fordi du ikke er logget inn som admin"
|
||||
},
|
||||
"app-rebuild": {
|
||||
"title": "Ombygg applikasjon",
|
||||
"rebuild-note-l1": "En ombygging er nødvendig for at endringer skrevet i conf.yml-filen skal tre i kraft.",
|
||||
"rebuild-note-l2": "Dette bør skje automatisk, men hvis det ikke har blitt gjort, kan du manuelt utløse det her.",
|
||||
"rebuild-note-l3": "Dette er ikke nødvendig for endringer som er lagret lokalt.",
|
||||
"rebuild-button": "Start Build",
|
||||
"rebuilding-status-1": "Building ...",
|
||||
"rebuilding-status-2": "Dette kan ta noen minutter",
|
||||
"error-permission": "Du har ikke tillatelse til å utløse denne handlingen",
|
||||
"success-msg": "Byggingen er fullført",
|
||||
"fail-msg": "Byggoperasjonen mislyktes",
|
||||
"reload-note": "En sideinnlasting er nå nødvendig for at endringer skal tre i kraft",
|
||||
"reload-button": "Last siden på nytt"
|
||||
},
|
||||
"cloud-sync": {
|
||||
"title": "Sikkerhetskopiering & gjenoppretting",
|
||||
"intro-l1": "Sikkerhetskopiering og gjenoppretting er en valgfri funksjon, som lar deg laste opp konfigurasjonen din til internett og deretter gjenopprette den på en hvilken som helst annen enhet eller forekomst av Dashy.",
|
||||
"intro-l2": "Alle data er helt ende-til-ende-kryptert med AES, og bruker passordet ditt som nøkkelen.",
|
||||
"intro-l3": "For mer informasjon, se",
|
||||
"backup-title-setup": "Lag en sikkerhetskopi",
|
||||
"backup-title-update": "Oppdater sikkerhetskopi",
|
||||
"password-label-setup": "Velg et passord",
|
||||
"password-label-update": "Skriv inn passordet ditt",
|
||||
"backup-button-setup": "Sikkerhetskopiering",
|
||||
"backup-button-update": "Oppdater sikkerhetskopi",
|
||||
"backup-id-label": "Din sikkerhetskopi-ID",
|
||||
"backup-id-note": "Dette brukes til å gjenopprette fra sikkerhetskopier senere. Så behold det, sammen med passordet ditt et trygt sted.",
|
||||
"restore-title": "Gjenopprett en sikkerhetskopi",
|
||||
"restore-id-label": "Gjenopprett ID",
|
||||
"restore-password-label": "Passord",
|
||||
"restore-button": "Gjenopprett",
|
||||
"backup-missing-password": "Manglende passord",
|
||||
"backup-error-unknown": "Kan ikke behandle forespørselen",
|
||||
"backup-error-password": "Feil passord. Skriv inn ditt nåværende passord.",
|
||||
"backup-success-msg": "Fullført vellykket",
|
||||
"restore-success-msg": "Konfigurasjon gjenopprettet vellykket"
|
||||
},
|
||||
"menu": {
|
||||
"sametab": "Åpne i nåværende fane",
|
||||
"newtab": "Åpne i ny fane",
|
||||
"modal": "Åpne i popup-modus",
|
||||
"workspace": "Åpne i Workspace-visning"
|
||||
}
|
||||
}
|
||||
172
src/assets/locales/pl.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"home": {
|
||||
"no-results": "Brak wyników",
|
||||
"no-data": "Brak danych"
|
||||
},
|
||||
"search": {
|
||||
"search-label": "Wyszukaj",
|
||||
"search-placeholder": "Zacznij pisać aby przefiltrować",
|
||||
"clear-search-tooltip": "Wyczyść",
|
||||
"enter-to-search-web": "Naciśnij ENTER aby przeszukać internet"
|
||||
},
|
||||
"login": {
|
||||
"title": "Dashy",
|
||||
"username-label": "Użytkownik",
|
||||
"password-label": "Hasło",
|
||||
"login-button": "Zaloguj",
|
||||
"remember-me-label": "Zapamiętaj mnie",
|
||||
"remember-me-never": "Nigdy",
|
||||
"remember-me-hour": "4 godziny",
|
||||
"remember-me-day": "Dzień",
|
||||
"remember-me-week": "Tydzień",
|
||||
"error-missing-username": "Nie podano nazwy użytkownika",
|
||||
"error-missing-password": "Nie podano hasła",
|
||||
"error-incorrect-username": "Nie znaleziono użytkownika",
|
||||
"error-incorrect-password": "Niepoprawne hasło",
|
||||
"success-message": "Zalogowano...",
|
||||
"logout-message": "Wylogowano",
|
||||
"already-logged-in-title": "Jesteś już zalogowany",
|
||||
"already-logged-in-text": "Zalogowano jako",
|
||||
"proceed-to-dashboard": "Przejdź do panelu",
|
||||
"log-out-button": "Wyloguj",
|
||||
"proceed-guest-button": "Kontynuuj jako gość"
|
||||
},
|
||||
"config": {
|
||||
"main-tab": "Menu główne",
|
||||
"view-config-tab": "Wyświetl konfigurację",
|
||||
"edit-config-tab": "Edytuj konfigurację",
|
||||
"custom-css-tab": "Niestandardowy styl",
|
||||
"heading": "Opcje konfiguracji",
|
||||
"download-config-button": "Pobierz plik konfiguracji",
|
||||
"edit-config-button": "Edytuj konfigurację",
|
||||
"edit-css-button": "Edytuj styl CSS",
|
||||
"cloud-sync-button": "Ustawienia chmury",
|
||||
"edit-cloud-sync-button": "Ustawienia chmury",
|
||||
"rebuild-app-button": "Przebuduj aplikację",
|
||||
"change-language-button": "Zmień język",
|
||||
"reset-settings-button": "Zresetuj pamięć podręczną",
|
||||
"app-info-button": "Informacje o aplikacji",
|
||||
"backup-note": "Przed dokonaniem zmian zaleca się zapisanie kopii zapasowej konfiguracji.",
|
||||
"reset-config-msg-l1": "Zostaną usunięte wszystkie ustawienia zapisane w pamięci podręcznej (Nie dotyczy pliku 'conf.yml'). ",
|
||||
"reset-config-msg-l2": "Zrób kopię zapasową jeśli obecne ustawienia są ważne.",
|
||||
"reset-config-msg-l3": "Czy na pewno chcesz kontynuować?",
|
||||
"data-cleared-msg": "Dane wyczyszczone pomyślnie",
|
||||
"actions-label": "Akcje",
|
||||
"copy-config-label": "Kopia konfiguracji",
|
||||
"data-copied-msg": "Konfiguracja skopiowana do schowka",
|
||||
"reset-config-label": "Zresetuj konfigurację",
|
||||
"css-save-btn": "Zapisz zmiany",
|
||||
"css-note-label": "Informacja",
|
||||
"css-note-l1": "Po dokonaniu zmian konieczne będzie odświeżenie strony.",
|
||||
"css-note-l2": "Nadpisane style przechowywane są w pamięci podręcznej, zaleca się więc wykonanie kopii stylu CSS.",
|
||||
"css-note-l3": "Aby usunąć niestandardowe style, wyczyść zawartość pola tekstowego i naciśnij Zapisz zmiany"
|
||||
},
|
||||
"alternate-views": {
|
||||
"alternate-view-heading": "Zmień widok",
|
||||
"default": "Domyślny",
|
||||
"workspace": "Obszar roboczy",
|
||||
"minimal": "Minimalny"
|
||||
},
|
||||
"settings": {
|
||||
"theme-label": "Motyw",
|
||||
"layout-label": "Układ",
|
||||
"layout-auto": "Automatyczny",
|
||||
"layout-horizontal": "Poziomy",
|
||||
"layout-vertical": "Pionowy",
|
||||
"item-size-label": "Rozmiar elementu",
|
||||
"item-size-small": "Mały",
|
||||
"item-size-medium": "Średni",
|
||||
"item-size-large": "Duży",
|
||||
"config-launcher-label": "Konfiguracja",
|
||||
"config-launcher-tooltip": "Przejdź do ustawień",
|
||||
"sign-out-tooltip": "Wyloguj",
|
||||
"sign-in-tooltip": "Zaloguj",
|
||||
"sign-in-welcome": "Cześć {username}!"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "wersja Dashy",
|
||||
"up-to-date": "Aktualna",
|
||||
"out-of-date": "Dostępna aktualizacja",
|
||||
"unsupported-version-l1": "Używasz niewspieranej wersji Dashy",
|
||||
"unsupported-version-l2": "Zaleca się zaktualizowanie do"
|
||||
},
|
||||
"language-switcher": {
|
||||
"title": "Zmień język",
|
||||
"dropdown-label": "Wybierz język",
|
||||
"save-button": "Zapisz",
|
||||
"success-msg": "Język zmieniony na"
|
||||
},
|
||||
"theme-maker": {
|
||||
"title": "Konfigurator motywu",
|
||||
"export-button": "Eksportuj zmienne",
|
||||
"reset-button": " Zresetuj styl",
|
||||
"show-all-button": "Pokaż wszystkie zmienne",
|
||||
"save-button": "Zapisz",
|
||||
"cancel-button": "Anuluj",
|
||||
"saved-toast": "Pomyślnie zaktualizowano {theme}",
|
||||
"copied-toast": "Dane motywu {theme} zostały skopiowane do schowka",
|
||||
"reset-toast": "Niestandardowe kolory dla {theme} usunięte"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Lokalizacja zapisu",
|
||||
"location-local-label": "Pamięć podręczna",
|
||||
"location-disk-label": "Plik na dysku",
|
||||
"save-button": "Zapisz",
|
||||
"valid-label": "Konfiguracja poprawna",
|
||||
"status-success-msg": "Zadanie ukończone",
|
||||
"status-fail-msg": "Zadanie nie powiodło się",
|
||||
"success-msg-disk": "Pomyślnie zapisano na dysku",
|
||||
"success-msg-local": "Pomyślnie zapisano w pamięci podręcznej",
|
||||
"success-note-l1": "Aplikacja powinna automatycznie się przebudować.",
|
||||
"success-note-l2": "Może to zająć około minuty.",
|
||||
"success-note-l3": "Będzie konieczne odświeżenie strony",
|
||||
"error-msg-save-mode": "Proszę wybrać pomiędzy pamięcią podręczną lub plikiem na dysku",
|
||||
"error-msg-cannot-save": "Wystąpił błąd podczas zapisywania",
|
||||
"error-msg-bad-json": "Błąd w JSON",
|
||||
"warning-msg-validation": "Ostrzeżenie",
|
||||
"not-admin-note": "Nie możesz zapisywać na dysku, wymagane uprawnienia administratora"
|
||||
},
|
||||
"app-rebuild": {
|
||||
"title": "Przebuduj aplikację",
|
||||
"rebuild-note-l1": "Przebudowanie jest koniecznne po dokonaniu zmian w pliku: conf.yml.",
|
||||
"rebuild-note-l2": "Powinno to nastąpić automatycznie, jeśli jednak tak się nie stanie możesz je wymusić tutaj.",
|
||||
"rebuild-note-l3": "Zmiany w pamięci podręcznej nie wymagają przebudowania aplikacji.",
|
||||
"rebuild-button": "Rozpocznij",
|
||||
"rebuilding-status-1": "Budowanie...",
|
||||
"rebuilding-status-2": "Może to zająć kilka minut",
|
||||
"error-permission": "Nie masz odpowiednich uprawnień do wykonania tej akcji",
|
||||
"success-msg": "Budowanie zakończone pomyślnie",
|
||||
"fail-msg": "Budowanie nie powiodło się",
|
||||
"reload-note": "Zmiany będą widoczne po odświeżeniu strony",
|
||||
"reload-button": "Odśwież stronę"
|
||||
},
|
||||
"cloud-sync": {
|
||||
"title": "Kopia zapasowa w chmurze",
|
||||
"intro-l1": "Tworzenie i przywracanie z chmury to opcjonalna funkcja, która umożliwia zapisanie konfiguracji w sieci, by później wgrać je na innym urządzeniu z Dashy.",
|
||||
"intro-l2": "Wszystkie dane są w pełni zaszyfrowane z wykorzystaniem AES, kluczem będzie podane hasło.",
|
||||
"intro-l3": "Aby uzyskać więcej informacji przejdź do",
|
||||
"backup-title-setup": "Tworzenie",
|
||||
"backup-title-update": "Zaktualizuj",
|
||||
"password-label-setup": "Wybierz hasło",
|
||||
"password-label-update": "Wprowadź hasło",
|
||||
"backup-button-setup": "Zapisz",
|
||||
"backup-button-update": "Zaktualizuj",
|
||||
"backup-id-label": "Identyfikator kopii zapasowej",
|
||||
"backup-id-note": "Wymagany do przywrócenia. Zapisz wraz z hasłem w bezpiecznym miejscu",
|
||||
"restore-title": "Przywracanie",
|
||||
"restore-id-label": "Identyfikator",
|
||||
"restore-password-label": "Hasło",
|
||||
"restore-button": "Przywróć",
|
||||
"backup-missing-password": "Nie podano hasła",
|
||||
"backup-error-unknown": "Nie udało się wykonać operacji",
|
||||
"backup-error-password": "Hasło niepoprawne. Proszę wprowadzić aktualne hasło.",
|
||||
"backup-success-msg": "zakończono pomyślnie",
|
||||
"restore-success-msg": "Przywrócono pomyślnie"
|
||||
},
|
||||
"menu": {
|
||||
"sametab": "Otwórz w tej karcie",
|
||||
"newtab": "Otwórz w nowej karcie",
|
||||
"modal": "Otwórz w oknie modalnym",
|
||||
"workspace": "Otwórz w obszarze roboczym"
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"already-logged-in-text": "Prijavljeni ste kot",
|
||||
"proceed-to-dashboard": "Nadaljujte na nadzorno ploščo",
|
||||
"log-out-button": "Odjava",
|
||||
"proceed-guest-button": "Nadaljujte kot gost"
|
||||
"proceed-guest-button": "Nadaljujte kot gost"
|
||||
},
|
||||
"config": {
|
||||
"main-tab": "Glavni Meni",
|
||||
@@ -62,10 +62,11 @@
|
||||
"css-note-l3": "Če želite odstraniti vse sloge po meri, izbrišite vsebino in pritisnite Shrani spremembe"
|
||||
},
|
||||
"alternate-views": {
|
||||
"alternate-view-heading": "Spremeni Pogled",
|
||||
"default": "Privzeto",
|
||||
"workspace": "Delovni prostor",
|
||||
"minimal": "Minimalno"
|
||||
},
|
||||
},
|
||||
"settings": {
|
||||
"theme-label": "Tema",
|
||||
"layout-label": "Postavitev",
|
||||
@@ -100,6 +101,7 @@
|
||||
"export-button": "Izvozi Spremenljivke po Meri",
|
||||
"reset-button": "Ponastavi Sloge za",
|
||||
"show-all-button": "Pokaži Vse Spremenljivke",
|
||||
"change-fonts-button": "Spremeni Pisavo",
|
||||
"save-button": "Shrani",
|
||||
"cancel-button": "Prekliči",
|
||||
"saved-toast": "{theme} Posodbljena Uspešno",
|
||||
@@ -111,6 +113,7 @@
|
||||
"location-local-label": "Shrani Lokalno",
|
||||
"location-disk-label": "Zapišite spremembe v datoteko za konfiguracijo",
|
||||
"save-button": "Shrani Spremembe",
|
||||
"preview-button": "Predogled Sprememb",
|
||||
"valid-label": "Konfiguracija je veljavna",
|
||||
"status-success-msg": "Operacija dokončana",
|
||||
"status-fail-msg": "Operacija ni uspela",
|
||||
@@ -163,10 +166,82 @@
|
||||
"restore-success-msg": "Konfiguracija Uspešno Obnovljena"
|
||||
},
|
||||
"menu": {
|
||||
"open-section-title": "Odpri V",
|
||||
"sametab": "Odpri v Trenutnem Zavihku",
|
||||
"newtab": "Odpri v Novem Zavihku",
|
||||
"modal": "Odpri v Pojavnem Oknu",
|
||||
"workspace": "Odpri v Delovnem Pogledu"
|
||||
"workspace": "Odpri v Delovnem Pogledu",
|
||||
"options-section-title": "Nastavitve",
|
||||
"edit-item": "Uredi",
|
||||
"move-item": "Kopiral ali Premakni",
|
||||
"remove-item": "Odstrani"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Odpri V",
|
||||
"sametab": "Trenutni Zavihek",
|
||||
"newtab": "Nov Zavihek",
|
||||
"modal": "Pojavno Okno",
|
||||
"workspace": "Delovni Pogled",
|
||||
"options-section-title": "Nastavitve",
|
||||
"edit-item": "Uredi",
|
||||
"move-item": "Kopiral ali Premakni",
|
||||
"remove-item": "Odstrani"
|
||||
},
|
||||
"section": {
|
||||
"open-section": "Odpri Razdelek",
|
||||
"edit-section": "Uredi",
|
||||
"move-section": "Premakni v",
|
||||
"remove-section": "Odstrani"
|
||||
}
|
||||
},
|
||||
"interactive-editor": {
|
||||
"menu": {
|
||||
"start-editing-tooltip": "Vstopite v Interaktivni Urejevalnik",
|
||||
"edit-site-data-subheading": "Uredi Podatke Strani",
|
||||
"edit-page-info-btn": "Uredi Informacije Strani",
|
||||
"edit-page-info-tooltip": "Naslov, opis, nav linki, teks noge. itd",
|
||||
"edit-app-config-btn": "Urejanje Konfiguracije",
|
||||
"edit-app-config-tooltip": "Vse druge možnosti konfiguracije aplikacije",
|
||||
"config-save-methods-subheading": "Možnosti Shranjevanja Konfiguracije",
|
||||
"save-locally-btn": "Shrani Lokalno",
|
||||
"save-locally-tooltip": "Shranite konfiguracijo lokalno v pomnilnik brskalnika. To ne bo vplivalo na vašo konfiguracijsko datoteko, vendar bodo spremembe shranjene samo v tej napravi",
|
||||
"save-disk-btn": "Shrani na Disk",
|
||||
"save-disk-tooltip": "Shranite konfiguracijo v datoteko conf.yml na disku. To bo varnostno kopiralo in nato prepisalo vašo obstoječo konfiguracijo",
|
||||
"export-config-btn": "Izvozi Nastavitve",
|
||||
"export-config-tooltip": "Oglejte si in izvozite novo konfiguracijo v datoteko ali v odložišče",
|
||||
"cloud-backup-btn": "Varnostno kopiranje v Oblak",
|
||||
"cloud-backup-tooltip": "Shranite šifrirano varnostno kopijo konfiguracije v oblak",
|
||||
"edit-raw-config-btn": "Urejanje Raw Konfiguracije",
|
||||
"edit-raw-config-tooltip": "Oglejte si in spremenite raw konfiguracijo prek urejevalnika JSON",
|
||||
"cancel-changes-btn": "Prekliči Urejanje",
|
||||
"cancel-changes-tooltip": "Ponastavite trenutne spremembe in zapustite način urejanja. To ne bo vplivalo na vašo shranjeno konfiguracijo",
|
||||
"edit-mode-name": "Način Urejanja",
|
||||
"edit-mode-subtitle": "Ste v načinu za Urejanje",
|
||||
"edit-mode-description": "To pomeni, da lahko spremenite svojo konfiguracijo in si ogledate predogled rezultatov, vendar dokler ne shranite, nobena od vaših sprememb ne bo ohranjena.",
|
||||
"save-stage-btn": "Shrani",
|
||||
"cancel-stage-btn": "Prekliči"
|
||||
},
|
||||
"edit-section": {
|
||||
"edit-section-title": "Uredi Razdelek",
|
||||
"add-section-title": "Dodaj Razdelek",
|
||||
"edit-tooltip": "Kliknite za Urejanje ali z desno tipko miške kliknite za Več Možnosti",
|
||||
"remove-confirm": "Ali ste prepričani, da želite odstraniti ta razdelek? To dejanje lahko pozneje razveljavite."
|
||||
},
|
||||
"edit-app-config": {
|
||||
"warning-msg-title": "Nadaljuj Previdno",
|
||||
"warning-msg-l1": "Naslednje možnosti so za napredno konfiguracijo aplikacije.",
|
||||
"warning-msg-l2": "Če niste prepričani glede katerega od polj, se obrnite na",
|
||||
"warning-msg-docs": "dokumentacija",
|
||||
"warning-msg-l3": "da bi se izognili neželenim posledicam."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Izvozi Nastavitve",
|
||||
"copy-clipboard-btn": "Kopirati v Odložišče",
|
||||
"copy-clipboard-tooltip": "Kopirajte vso konfiguracijo aplikacije v sistemsko odložišče v formatu YAML",
|
||||
"download-file-btn": "Prenesi kot Datoteko",
|
||||
"download-file-tooltip": "Prenesite vso konfiguracijo aplikacije v svojo napravo v formatu YAML",
|
||||
"view-title": "Ogled Konfiguracije"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
104
src/assets/locales/zz-pirate.json
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"home": {
|
||||
"no-results": "Nay Search Results",
|
||||
"no-data": "Nay Data Configured"
|
||||
},
|
||||
"search": {
|
||||
"search-placeholder": "Start typin' t' filter",
|
||||
"enter-to-search-web": "Press enter t' search th' web"
|
||||
},
|
||||
"login": {
|
||||
"remember-me-label": "Remember me fer",
|
||||
"error-missing-username": "Missin' Username",
|
||||
"error-missing-password": "Missin' Password",
|
||||
"success-message": "Loggin' in...",
|
||||
"already-logged-in-text": "ye're logged in as",
|
||||
"proceed-to-dashboard": "Proceed t' Dashboard",
|
||||
"log-out-button": "Logout Ye All",
|
||||
"proceed-guest-button": "Proceed as Ye Guest"
|
||||
},
|
||||
"config": {
|
||||
"main-tab": "Ya Main Menu",
|
||||
"heading": "Ye Configuration Options",
|
||||
"download-config-button": "Download Config",
|
||||
"reset-settings-button": "Reset Ship Settin's",
|
||||
"change-language-button": "Change Ye Language",
|
||||
"cloud-sync-button": "Enable Ship Sync",
|
||||
"app-info-button": "Th' Ship Info",
|
||||
"backup-note": "It be recommend t' make a backup o' yer configuration before makin' changes.",
|
||||
"reset-config-msg-l1": "This will remove all user settin's from local storage, but won't effect yer 'conf.yml' file.",
|
||||
"reset-config-msg-l2": "ye should first backup any changes ye've made locally, if ye want t' use them in th' future.",
|
||||
"reset-config-msg-l3": "be ye sure ye want t' proceed?",
|
||||
"data-copied-msg": "Config has been copied t' clipboardd",
|
||||
"css-note-l1": "ye will need t' refresh th' page fer yer changes t' take effect.",
|
||||
"css-note-l2": "Styles overrides be only stored locally, so it be recommended t' make a copy o' yer CSS.",
|
||||
"css-note-l3": "To remove all custom styles, delete th' contents and hit Save Changes"
|
||||
},
|
||||
"settings": {
|
||||
"sign-in-welcome": "Ahoy {username}!"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Dashy version",
|
||||
"up-to-date": "Up-to-Date",
|
||||
"out-of-date": "Update Available",
|
||||
"unsupported-version-l1": "You are using a ye' old version of Dashy",
|
||||
"unsupported-version-l2": "For th' best experience, and recent security patches, please update to"
|
||||
},
|
||||
"language-switcher": {
|
||||
"success-msg": "Language Updated t'"
|
||||
},
|
||||
"theme-maker": {
|
||||
"copied-toast": "Theme data for {theme} copied t' ye clipboard"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Save Location",
|
||||
"location-local-label": "Apply Locally",
|
||||
"location-disk-label": "Write Changes to Config File",
|
||||
"save-button": "Save Changes",
|
||||
"valid-label": "Config is Valid",
|
||||
"status-success-msg": "Task Complete",
|
||||
"status-fail-msg": "Task Failed",
|
||||
"success-msg-disk": "Th' config file written to disk successfully",
|
||||
"success-msg-local": "Ye local changes were successfully saved",
|
||||
"success-note-l1": "th' app should rebuild automatically.",
|
||||
"success-note-l2": "This may take up t' a minute.",
|
||||
"success-note-l3": "ye will need t' refresh th' page fer changes t' take effect.",
|
||||
"error-msg-cannot-save": "An error occurred savin' config",
|
||||
"error-msg-bad-json": "Error in ye JSON, possibly malformed",
|
||||
"warning-msg-validation": "Validation Warnin' Ahead",
|
||||
"not-admin-note": "ye cannot write changed t' disk, because ye be not logged in as an admin"
|
||||
},
|
||||
"app-rebuild": {
|
||||
"title": "Rebuild Application",
|
||||
"rebuild-note-l1": "A rebuild be required fer changes written t' th' conf.yml file t' take effect.",
|
||||
"rebuild-note-l2": "This should happen automatically, but if it hasn't, ye can manually trigger it here.",
|
||||
"rebuild-note-l3": "This be not required fer modifications stored locally.",
|
||||
"rebuild-button": "Start Build",
|
||||
"rebuilding-status-1": "Buildin...",
|
||||
"error-permission": "ye dern't have permission t' trigger this action",
|
||||
"success-msg": "Ayhyo, build did complete successfully!",
|
||||
"fail-msg": "Build operation did fail",
|
||||
"reload-note": "A page reload be now required fer changes t' take effect",
|
||||
"reload-button": "Reload Ye Page"
|
||||
},
|
||||
"cloud-sync": {
|
||||
"intro-l1": "Cloud backup and restore be an optional feature, that enables ye t' upload yer config t' th' internet, and then restore it on any other device or instance o' Dashy.",
|
||||
"intro-l2": "All data be fully end-t'-end encrypted with AES, usin' yer password as th' key.",
|
||||
"intro-l3": "For more info, please see th'",
|
||||
"backup-title-setup": "Make ye Backup",
|
||||
"backup-title-update": "Update ye Backup",
|
||||
"password-label-setup": "Choose ye Password",
|
||||
"password-label-update": "Enter yer Password",
|
||||
"backup-id-label": "Yer Backup ID",
|
||||
"backup-id-note": "This be used t' restore from backups later. So keep it, along with yer password somewhere safe.",
|
||||
"backup-missing-password": "Missin'g' Password",
|
||||
"backup-error-unknown": "Unable t' process request",
|
||||
"backup-error-password": "Incorrect password. Walk the plank! Please enter yer current password."
|
||||
},
|
||||
"menu": {
|
||||
"sametab": "Stay Aboard",
|
||||
"newtab": "Walk the Plank",
|
||||
"modal": "Open in ye Pop-Up Ship",
|
||||
"workspace": "Open on Workspace Deck"
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,30 @@
|
||||
<template>
|
||||
<modal :name="modalName" :resizable="true" width="60%" height="60%" classes="dashy-modal">
|
||||
<modal :name="modalName" :resizable="true" width="55%" height="80%" classes="dashy-modal">
|
||||
<div class="about-modal">
|
||||
<router-link to="/about" class="title"><h2>App Info</h2></router-link>
|
||||
<!-- App Version -->
|
||||
<h3>Version</h3>
|
||||
<AppVersion class="app-version" />
|
||||
<!-- Error Log -->
|
||||
<h3>Error Log</h3>
|
||||
<pre v-if="errorLog" class="logs"><code>{{ errorLog }}</code></pre>
|
||||
<p v-else>No recent errors detected :)</p>
|
||||
<!-- Service Worker Status -->
|
||||
<h3>Service Worker Status</h3>
|
||||
<pre class="logs"><code>{{ serviceWorkerInfo }}</code></pre>
|
||||
<!-- Config Validation Status -->
|
||||
<h3>Config Validation Status</h3>
|
||||
<pre class="logs"><code>{{getIsConfigValidStatus()}}</code></pre>
|
||||
<hr />
|
||||
<!-- Help Links -->
|
||||
<h3>Help & Support</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/Lissy93/dashy/discussions">Get Support</a></li>
|
||||
<li><a href="https://github.com/Lissy93/dashy/issues/new/choose">Report a Bug</a></li>
|
||||
</ul>
|
||||
<span class="small-note">Please include the following info in your bug report: </span>
|
||||
<a class="info" @click="showInfo = !showInfo">{{ showInfo ? 'Hide' : 'Show'}} system info</a>
|
||||
<div class="system-info" v-if="showInfo">
|
||||
<h4>System Info</h4>
|
||||
<code><b>Dashy Version:</b> V {{appVersion}}</code><br>
|
||||
<code><b>Browser:</b> {{systemInfo.browser}}</code><br>
|
||||
<code><b>Is Mobile?</b> {{systemInfo.isMobile ? 'Yes' : 'No'}}</code><br>
|
||||
<code><b>OS:</b> {{systemInfo.os}}</code><br>
|
||||
</div>
|
||||
<!-- About App -->
|
||||
<h3>About</h3>
|
||||
<p class="about-text">
|
||||
Source: <a href="https://github.com/lissy93/dashy">github.com/lissy93/dashy</a><br>
|
||||
Documentation: <a href="https://dashy.to/docs">dashy.to/docs</a>
|
||||
</p>
|
||||
For getting support with running or configuring Dashy, see the <a href="https://github.com/Lissy93/dashy/discussions">Discussions</a>
|
||||
<h3>Supporting Dashy</h3>
|
||||
For ways that you can get involved, check out the <a href="https://github.com/Lissy93/dashy/blob/master/docs/contributing.md">Contributing</a> page.
|
||||
<h3>Report a Bug</h3>
|
||||
If you think you've found a bug, then please <a href="https://github.com/Lissy93/dashy/issues/new/choose">raise an Issue</a>.
|
||||
<h3>More Info</h3>
|
||||
Source: <a href="https://github.com/lissy93/dashy">github.com/lissy93/dashy</a><br>
|
||||
Documentation: <a href="https://dashy.to/docs">dashy.to/docs</a>
|
||||
<!-- License -->
|
||||
<h3>License</h3>
|
||||
<p>Licensed under MIT X11. Copyright © 2021</p>
|
||||
<br><br>
|
||||
Licensed under MIT X11. Copyright <a href="https://aliciasykes.com">Alicia Sykes</a> © 2021.<br>
|
||||
For licenses for third-party modules, please see <a href="https://github.com/Lissy93/dashy/blob/master/.github/LEGAL.md">Legal</a>.<br>
|
||||
For the full list of contributors and thanks, see <a href="https://github.com/Lissy93/dashy/blob/master/docs/credits.md">Credits</a>.
|
||||
<!-- App Version -->
|
||||
<h3>Version</h3>
|
||||
<AppVersion class="app-version" />
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
@@ -58,71 +42,13 @@ export default {
|
||||
return {
|
||||
modalName: modalNames.ABOUT_APP,
|
||||
appVersion: process.env.VUE_APP_VERSION,
|
||||
systemInfo: this.getSystemInfo(),
|
||||
errorLog: this.getErrorLog(),
|
||||
serviceWorkerInfo: 'Checking...',
|
||||
showInfo: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.serviceWorkerInfo = this.getSwStatus();
|
||||
}, 100);
|
||||
},
|
||||
methods: {
|
||||
getErrorLog() {
|
||||
return sessionStorage.getItem(sessionStorageKeys.ERROR_LOG) || '';
|
||||
},
|
||||
getIsConfigValidStatus() {
|
||||
const isValidVar = process.env.VUE_APP_CONFIG_VALID;
|
||||
if (isValidVar === undefined) return 'Config validation status is missing';
|
||||
return `Config is ${isValidVar ? 'Valid' : 'Invalid'}`;
|
||||
},
|
||||
getSwStatus() {
|
||||
const sessionData = sessionStorage[sessionStorageKeys.SW_STATUS];
|
||||
const swInfo = sessionData ? JSON.parse(sessionData) : {};
|
||||
let swStatus = '';
|
||||
if (swInfo.registered) swStatus += 'Service worker registered\n';
|
||||
if (swInfo.ready) swStatus += 'Dashy is being served from service worker\n';
|
||||
if (swInfo.cached) swStatus += 'Content has been cached for offline use\n';
|
||||
if (swInfo.updateFound) swStatus += 'New content is downloading\n';
|
||||
if (swInfo.updated) swStatus += 'New content is available; please refresh\n';
|
||||
if (swInfo.offline) swStatus += 'No internet connection found. App is running in offline mode\n';
|
||||
if (swInfo.error) swStatus += 'Error during service worker registration\n';
|
||||
if (swInfo.devMode) swStatus += 'App running in dev mode, no need for service worker\n';
|
||||
if (swStatus.length === 0) swStatus += 'No service worker info available';
|
||||
return swStatus;
|
||||
},
|
||||
getSystemInfo() {
|
||||
const { userAgent } = navigator;
|
||||
|
||||
// Find Operating System
|
||||
let os = 'Unknown';
|
||||
if (userAgent.indexOf('Win') !== -1) os = 'Windows';
|
||||
else if (userAgent.indexOf('Mac') !== -1) os = 'MacOS';
|
||||
else if (userAgent.indexOf('Android') !== -1) os = 'Android';
|
||||
else if (userAgent.indexOf('iPhone') !== -1) os = 'iOS';
|
||||
else if (userAgent.indexOf('Linux') !== -1) os = 'Linux';
|
||||
else if (userAgent.indexOf('X11') !== -1) os = 'UNIX';
|
||||
|
||||
// Find Browser
|
||||
let browser = 'Unknown';
|
||||
if (userAgent.indexOf('Opera') !== -1) browser = 'Opera';
|
||||
else if (userAgent.indexOf('Chrome') !== -1) browser = 'Chrome';
|
||||
else if (userAgent.indexOf('Safari') !== -1) browser = 'Safari';
|
||||
else if (userAgent.indexOf('Firefox') !== -1) browser = 'Firefox';
|
||||
else if (userAgent.indexOf('MSIE') !== -1) browser = 'IE';
|
||||
else browser = 'Unknown';
|
||||
|
||||
const isMobile = !!navigator.userAgent.match(/iphone|android|blackberry/ig) || false;
|
||||
|
||||
return {
|
||||
os,
|
||||
browser,
|
||||
userAgent,
|
||||
isMobile,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -152,40 +78,17 @@ div.about-modal {
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.3rem;
|
||||
margin: 1rem 0 0.2rem 0;
|
||||
font-size: 1.2rem;
|
||||
margin: 0.75rem 0 0.2rem 0;
|
||||
color: var(--about-page-accent);
|
||||
}
|
||||
p.small-note {
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
p.about-text {
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
a {
|
||||
color: var(--about-page-accent);
|
||||
}
|
||||
ul {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
a.info {
|
||||
text-decoration: underline;
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
.system-info {
|
||||
font-size: 0.8rem;
|
||||
background: var(--black);
|
||||
color: var(--white);
|
||||
border-radius: var(--curve-factor-small);
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--white);
|
||||
width: fit-content;
|
||||
h4 {
|
||||
font-size: 0.8rem;
|
||||
margin: 0 0 0.2rem 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.app-version {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -203,3 +106,14 @@ div.about-modal {
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
div.about-modal {
|
||||
.app-version {
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: self-end;
|
||||
p { margin: 0; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
❗ {{ $t('updates.out-of-date') }}: <b>{{ latestVersion }}</b>
|
||||
<span class="please-update">
|
||||
{{ $t('updates.unsupported-version-l1') }}.<br>
|
||||
{{ $t('updates.unsupported-version-2') }} {{ latestVersion }}
|
||||
{{ $t('updates.unsupported-version-l2') }} {{ latestVersion }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -36,7 +36,11 @@ import ErrorHandler from '@/utils/ErrorHandler';
|
||||
|
||||
export default {
|
||||
name: 'AppInfoModal',
|
||||
inject: ['config'],
|
||||
computed: {
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
appVersion: process.env.VUE_APP_VERSION, // Current version, from package.json
|
||||
@@ -50,8 +54,7 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const appConfig = this.config.appConfig || {};
|
||||
if (!this.appVersion || (appConfig && appConfig.disableUpdateChecks)) {
|
||||
if (!this.appVersion || (this.appConfig && this.appConfig.disableUpdateChecks)) {
|
||||
// Either current version isn't found, or user disabled checks
|
||||
this.checksEnabled = false;
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="cloud-backup-restore-wrapper">
|
||||
<!-- Intro text -->
|
||||
<div class="section intro">
|
||||
<h2>{{ $t('cloud-sync.title') }}</h2>
|
||||
<p class="intro">
|
||||
@@ -11,6 +12,7 @@
|
||||
<a href="https://github.com/Lissy93/dashy/blob/master/docs/backup-restore.md">docs</a>
|
||||
</p>
|
||||
</div>
|
||||
<!-- Create or update a backup form -->
|
||||
<div class="section backup-section">
|
||||
<h3 v-if="backupId">{{ $t('cloud-sync.backup-title-setup') }}</h3>
|
||||
<h3 v-else>{{ $t('cloud-sync.backup-title-setup') }}</h3>
|
||||
@@ -23,11 +25,9 @@
|
||||
type="password"
|
||||
/>
|
||||
<Button :click="checkPass">
|
||||
<template v-slot:text>
|
||||
{{backupId
|
||||
? $t('cloud-sync.backup-button-update') : $t('cloud-sync.backup-button-setup')}}
|
||||
</template>
|
||||
<template v-slot:icon><IconBackup /></template>
|
||||
{{backupId
|
||||
? $t('cloud-sync.backup-button-update') : $t('cloud-sync.backup-button-setup')}}
|
||||
<IconBackup />
|
||||
</Button>
|
||||
<div class="results-view" v-if="backupId">
|
||||
<span class="backup-id-label">{{ $t('cloud-sync.backup-id-label') }}: </span>
|
||||
@@ -35,6 +35,7 @@
|
||||
<span class="backup-id-note">{{ $t('cloud-sync.backup-id-note') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Restore from backup form -->
|
||||
<div class="section restore-section">
|
||||
<h3>{{ $t('cloud-sync.restore-title') }}</h3>
|
||||
<Input
|
||||
@@ -49,32 +50,38 @@
|
||||
type="password"
|
||||
/>
|
||||
<Button :click="restoreBackup">
|
||||
<template v-slot:text>{{ $t('cloud-sync.restore-button') }}</template>
|
||||
<template v-slot:icon><IconRestore /></template>
|
||||
{{ $t('cloud-sync.restore-button') }}
|
||||
<IconRestore />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
// Import libraries
|
||||
import sha256 from 'crypto-js/sha256';
|
||||
import ProgressBar from 'rsup-progress';
|
||||
// Import form elements
|
||||
import Button from '@/components/FormElements/Button';
|
||||
import Input from '@/components/FormElements/Input';
|
||||
import IconBackup from '@/assets/interface-icons/config-backup.svg';
|
||||
import IconRestore from '@/assets/interface-icons/config-restore.svg';
|
||||
// Import utils and constants
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import { backup, update, restore } from '@/utils/CloudBackup';
|
||||
import { localStorageKeys } from '@/utils/defaults';
|
||||
import ErrorHandler, { InfoHandler } from '@/utils/ErrorHandler';
|
||||
import { InfoHandler, WarningInfoHandler, InfoKeys } from '@/utils/ErrorHandler';
|
||||
// Import Icons
|
||||
import IconBackup from '@/assets/interface-icons/config-backup.svg';
|
||||
import IconRestore from '@/assets/interface-icons/config-restore.svg';
|
||||
|
||||
export default {
|
||||
name: 'CloudBackupRestore',
|
||||
props: {
|
||||
config: Object,
|
||||
computed: {
|
||||
config() { // Users config from store
|
||||
return this.$store.state.config;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
return { // Store current form data (temp)
|
||||
backupPassword: '',
|
||||
restorePassword: '',
|
||||
restoreCode: '',
|
||||
@@ -82,36 +89,26 @@ export default {
|
||||
progress: new ProgressBar({ color: 'var(--progress-bar)' }),
|
||||
};
|
||||
},
|
||||
components: {
|
||||
components: { // UI components / icons
|
||||
Button,
|
||||
Input,
|
||||
IconBackup,
|
||||
IconRestore,
|
||||
},
|
||||
methods: {
|
||||
/* Make request to server-side, then either show error, or proceed to restore */
|
||||
restoreBackup() {
|
||||
this.progress.start();
|
||||
restore(this.restoreCode, this.restorePassword)
|
||||
.then((response) => {
|
||||
this.restoreFromBackup(response, this.restoreCode);
|
||||
this.applyRestoredData(response, this.restoreCode);
|
||||
this.progress.end();
|
||||
}).catch((msg) => {
|
||||
this.showErrorMsg(msg);
|
||||
this.progress.end();
|
||||
});
|
||||
},
|
||||
checkPass() {
|
||||
const savedHash = localStorage[localStorageKeys.BACKUP_HASH] || undefined;
|
||||
if (!this.backupPassword) {
|
||||
this.showErrorMsg(this.$t('cloud-sync.backup-missing-password'));
|
||||
} else if (!savedHash) {
|
||||
this.makeBackup();
|
||||
} else if (savedHash === this.makeHash(this.backupPassword)) {
|
||||
this.makeUpdate();
|
||||
} else {
|
||||
this.showErrorMsg(this.$t('cloud-sync.backup-error-password'));
|
||||
}
|
||||
},
|
||||
/* Send request to backup server, to upload a new backup */
|
||||
makeBackup() {
|
||||
this.progress.start();
|
||||
backup(this.config, this.backupPassword)
|
||||
@@ -127,6 +124,7 @@ export default {
|
||||
this.progress.end();
|
||||
});
|
||||
},
|
||||
/* Send request to backup server, to update an existing backup */
|
||||
makeUpdate() {
|
||||
this.progress.start();
|
||||
update(this.config, this.backupPassword, this.backupId)
|
||||
@@ -142,17 +140,36 @@ export default {
|
||||
this.progress.end();
|
||||
});
|
||||
},
|
||||
restoreFromBackup(config, backupId) {
|
||||
/* For create / update a backup- checks pass is valid, then calls makeBackup */
|
||||
checkPass() {
|
||||
const savedHash = localStorage[localStorageKeys.BACKUP_HASH] || undefined;
|
||||
if (!this.backupPassword) {
|
||||
this.showErrorMsg(this.$t('cloud-sync.backup-missing-password'));
|
||||
} else if (!savedHash) {
|
||||
this.makeBackup();
|
||||
} else if (savedHash === this.makeHash(this.backupPassword)) {
|
||||
this.makeUpdate();
|
||||
} else {
|
||||
this.showErrorMsg(this.$t('cloud-sync.backup-error-password'));
|
||||
}
|
||||
},
|
||||
/* When restored data is revieved, then save to local storage, and apply it in state */
|
||||
applyRestoredData(config, backupId) {
|
||||
// Store restored data in local storage
|
||||
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
|
||||
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
|
||||
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
|
||||
if (config.appConfig.theme) {
|
||||
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
|
||||
}
|
||||
// Save hashed token in local storage
|
||||
this.setBackupIdLocally(backupId, this.restorePassword);
|
||||
// Update the current state
|
||||
this.$store.commit(StoreKeys.SET_CONFIG, config);
|
||||
// Show success message
|
||||
this.showSuccessMsg(this.$t('cloud-sync.restore-success-msg'));
|
||||
setTimeout(() => { location.reload(); }, 1500); // eslint-disable-line no-restricted-globals
|
||||
},
|
||||
/* After backup/ update is made, then replace 'Make Backup' with 'Update Backup' */
|
||||
updateUiAfterBackup(backupId, isUpdate = false) {
|
||||
this.setBackupIdLocally(backupId, this.backupPassword);
|
||||
this.showSuccessMsg(
|
||||
@@ -160,17 +177,21 @@ export default {
|
||||
);
|
||||
this.backupPassword = '';
|
||||
},
|
||||
/* If the server returns a warning, then show to user and log it */
|
||||
showErrorMsg(errorMsg) {
|
||||
ErrorHandler(errorMsg);
|
||||
WarningInfoHandler(errorMsg, InfoKeys.CLOUD_BACKUP);
|
||||
this.$toasted.show(errorMsg, { className: 'toast-error' });
|
||||
},
|
||||
/* When server returns success message, then show to user and log it */
|
||||
showSuccessMsg(msg) {
|
||||
InfoHandler(msg, 'Cloud Backup');
|
||||
InfoHandler(msg, InfoKeys.CLOUD_BACKUP);
|
||||
this.$toasted.show(msg, { className: 'toast-success' });
|
||||
},
|
||||
/* Call to hash function, to hash the users chosen/ entered password */
|
||||
makeHash(pass) {
|
||||
return sha256(pass).toString();
|
||||
},
|
||||
/* After backup is applied, hash the backup ID, and save in browser storage */
|
||||
setBackupIdLocally(backupId, pass) {
|
||||
this.backupId = backupId;
|
||||
const hash = this.makeHash(pass);
|
||||
@@ -185,49 +206,48 @@ export default {
|
||||
@import '@/styles/style-helpers.scss';
|
||||
div.cloud-backup-restore-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
text-align: center;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
background: var(--config-settings-background);
|
||||
color: var(--config-settings-color);
|
||||
color: var(--cloud-backup-color);
|
||||
background: var(--cloud-backup-background);
|
||||
@extend .scroll-bar;
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
margin: 0 auto 1rem auto;
|
||||
padding: 0 0.5rem 1rem 0.5rem;
|
||||
&:first-child {
|
||||
border-bottom: 1px dashed var(--config-settings-color);
|
||||
}
|
||||
&.intro {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
a {
|
||||
color: var(--config-settings-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2 { font-size: 2rem; }
|
||||
h3 { font-size: 1.6rem; }
|
||||
/* Text styling */
|
||||
h2, h3 { font-size: 1.6rem; }
|
||||
p.intro {
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
margin: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
/* Main sections */
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
margin: 0 auto 1rem auto;
|
||||
padding: 0 0.5rem 1rem 0.5rem;
|
||||
}
|
||||
/* Intro section */
|
||||
.section.intro {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
border-bottom: 1px dashed var(--cloud-backup-color);
|
||||
a { color: var(--cloud-backup-color); }
|
||||
}
|
||||
}
|
||||
|
||||
/* Container to show backup ID result from server */
|
||||
div.results-view {
|
||||
width: 16rem;
|
||||
margin: 0.5rem auto;
|
||||
padding: 0.5rem 0.75rem;
|
||||
box-sizing: border-box;
|
||||
border: 1px dashed var(--config-settings-color);
|
||||
border: 1px dashed var(--cloud-backup-color);
|
||||
border-radius: var(--curve-factor);
|
||||
text-align: left;
|
||||
.backup-id-label, .backup-id-value {
|
||||
@@ -244,22 +264,19 @@ export default {
|
||||
}
|
||||
|
||||
/* Overide form element colors, so that config menu can be themed by user */
|
||||
input, button, {
|
||||
color: var(--config-settings-color);
|
||||
border: 1px solid var(--config-settings-color);
|
||||
input, button {
|
||||
color: var(--cloud-backup-color);
|
||||
border: 1px solid var(--cloud-backup-color);
|
||||
background: none;
|
||||
width: 16rem;
|
||||
}
|
||||
input:focus {
|
||||
box-shadow: 1px 1px 6px var(--config-settings-color);
|
||||
box-shadow: 1px 1px 6px var(--cloud-backup-color);
|
||||
}
|
||||
button:hover {
|
||||
color: var(--config-settings-background);
|
||||
border: 1px solid var(--config-settings-background);
|
||||
background: var(--config-settings-color);
|
||||
}
|
||||
h2, h3 {
|
||||
margin: 1rem;
|
||||
color: var(--cloud-backup-background);
|
||||
border: 1px solid var(--cloud-backup-background);
|
||||
background: var(--cloud-backup-color);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<TabItem :name="$t('config.main-tab')" class="main-tab">
|
||||
<div class="main-options-container">
|
||||
<div class="config-buttons">
|
||||
<h2>Configuration Options</h2>
|
||||
<a class="hyperlink-wrapper" @click="downloadConfigFile('conf.yml', yaml)">
|
||||
<h2>{{ $t('config.heading') }}</h2>
|
||||
<a class="hyperlink-wrapper" @click="openExportConfigModal()">
|
||||
<button class="config-button center">
|
||||
<DownloadIcon class="button-icon"/>
|
||||
{{ $t('config.download-config-button') }}
|
||||
@@ -14,6 +14,10 @@
|
||||
<EditIcon class="button-icon"/>
|
||||
{{ $t('config.edit-config-button') }}
|
||||
</button>
|
||||
<button class="config-button center" @click="openLanguageSwitchModal()">
|
||||
<LanguageIcon class="button-icon"/>
|
||||
{{ $t('config.change-language-button') }}
|
||||
</button>
|
||||
<button class="config-button center" @click="() => navigateToTab(3)">
|
||||
<CustomCssIcon class="button-icon"/>
|
||||
{{ $t('config.edit-css-button') }}
|
||||
@@ -22,10 +26,6 @@
|
||||
<CloudIcon class="button-icon"/>
|
||||
{{backupId ? $t('config.edit-cloud-sync-button') : $t('config.cloud-sync-button') }}
|
||||
</button>
|
||||
<button class="config-button center" @click="openLanguageSwitchModal()">
|
||||
<LanguageIcon class="button-icon"/>
|
||||
{{ $t('config.change-language-button') }}
|
||||
</button>
|
||||
<button class="config-button center" @click="openRebuildAppModal()">
|
||||
<RebuildIcon class="button-icon"/>
|
||||
{{ $t('config.rebuild-app-button') }}
|
||||
@@ -52,13 +52,13 @@
|
||||
<RebuildApp />
|
||||
</TabItem>
|
||||
<TabItem :name="$t('config.edit-config-tab')">
|
||||
<JsonEditor :config="config" />
|
||||
<JsonEditor />
|
||||
</TabItem>
|
||||
<TabItem :name="$t('cloud-sync.title')">
|
||||
<CloudBackupRestore :config="config" />
|
||||
<CloudBackupRestore />
|
||||
</TabItem>
|
||||
<TabItem :name="$t('config.custom-css-tab')">
|
||||
<CustomCssEditor :config="config" />
|
||||
<CustomCssEditor />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</template>
|
||||
@@ -68,6 +68,7 @@
|
||||
import JsonToYaml from '@/utils/JsonToYaml';
|
||||
import { localStorageKeys, modalNames } from '@/utils/defaults';
|
||||
import { getUsersLanguage } from '@/utils/ConfigHelpers';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import JsonEditor from '@/components/Configuration/JsonEditor';
|
||||
import CustomCssEditor from '@/components/Configuration/CustomCss';
|
||||
import CloudBackupRestore from '@/components/Configuration/CloudBackupRestore';
|
||||
@@ -134,37 +135,34 @@ export default {
|
||||
openLanguageSwitchModal() {
|
||||
this.$modal.show(modalNames.LANG_SWITCHER);
|
||||
},
|
||||
copyConfigToClipboard() {
|
||||
navigator.clipboard.writeText(this.jsonParser(this.config));
|
||||
this.$toasted.show(this.$t('config.data-copied-msg'));
|
||||
openExportConfigModal() {
|
||||
this.$modal.show(modalNames.EXPORT_CONFIG_MENU);
|
||||
},
|
||||
/* Checks that the user is sure, then resets site-wide local storage, and reloads page */
|
||||
resetLocalSettings() {
|
||||
const msg = `${this.$t('config.reset-config-msg-l1')
|
||||
}${this.$t('config.reset-config-msg-l2')}\n\n${this.$t('config.reset-config-msg-l3')}`;
|
||||
const msg = `${this.$t('config.reset-config-msg-l1')} `
|
||||
+ `${this.$t('config.reset-config-msg-l2')}\n\n${this.$t('config.reset-config-msg-l3')}`;
|
||||
const isTheUserSure = confirm(msg); // eslint-disable-line no-alert, no-restricted-globals
|
||||
if (isTheUserSure) {
|
||||
localStorage.clear();
|
||||
this.$toasted.show(this.$t('config.data-cleared-msg'));
|
||||
setTimeout(() => {
|
||||
location.reload(true); // eslint-disable-line no-restricted-globals
|
||||
}, 1900);
|
||||
this.$store.dispatch(StoreKeys.INITIALIZE_CONFIG);
|
||||
}
|
||||
},
|
||||
/* Generates a new file, with the YAML contents, and triggers a download */
|
||||
downloadConfigFile(filename, filecontents) {
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('href', `data:text/plain;charset=utf-8, ${encodeURIComponent(filecontents)}`);
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
},
|
||||
getLanguage() {
|
||||
const lang = getUsersLanguage();
|
||||
return lang ? `${lang.flag} ${lang.name}` : '';
|
||||
},
|
||||
/* If launching menu from editor, navigate to correct starting tab */
|
||||
navigateToStartingTab() {
|
||||
const navToTab = this.$store.state.navigateConfToTab;
|
||||
const isValidTabIndex = (indx) => typeof indx === 'number' && indx >= 0 && indx <= 5;
|
||||
if (navToTab && isValidTabIndex(navToTab)) this.navigateToTab(navToTab);
|
||||
this.$store.commit(StoreKeys.CONF_MENU_INDEX, undefined);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.navigateToStartingTab();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,108 +1,130 @@
|
||||
<template>
|
||||
<div class="css-editor-outer">
|
||||
<!-- Add raw custom CSS -->
|
||||
<div class="css-wrapper">
|
||||
<h2 class="css-input-title">Custom CSS</h2>
|
||||
<div class="style-section css-wrapper">
|
||||
<h3>Custom CSS</h3>
|
||||
<textarea class="css-editor" v-model="customCss" />
|
||||
<button class="save-button" @click="save()">{{ $t('config.css-save-btn') }}</button>
|
||||
<Button class="save-button" :click="save">{{ $t('config.css-save-btn') }}</Button>
|
||||
<p class="quick-note">
|
||||
<b>{{ $t('config.css-note-label') }}:</b>
|
||||
{{ $t('config.css-note-l1') }} {{ $t('config.css-note-l2') }} {{ $t('config.css-note-l3') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Theme Selector -->
|
||||
<div class="style-section base-theme-wrapper">
|
||||
<h3>Base Theme</h3>
|
||||
<ThemeSelector :hidePallete="true" />
|
||||
</div>
|
||||
<!-- UI color configurator -->
|
||||
<CustomThemeMaker :themeToEdit="currentTheme" class="color-config" />
|
||||
<div class="style-section">
|
||||
<CustomThemeMaker :themeToEdit="currentTheme" class="color-config" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import CustomThemeMaker from '@/components/Settings/CustomThemeMaker';
|
||||
import { getTheme } from '@/utils/ConfigHelpers';
|
||||
import { localStorageKeys } from '@/utils/defaults';
|
||||
import { InfoHandler } from '@/utils/ErrorHandler';
|
||||
import ThemeSelector from '@/components/Settings/ThemeSelector';
|
||||
import Button from '@/components/FormElements/Button';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import { localStorageKeys, theme as defaultTheme } from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
name: 'StyleEditor',
|
||||
props: {
|
||||
config: Object,
|
||||
},
|
||||
components: {
|
||||
Button,
|
||||
ThemeSelector,
|
||||
CustomThemeMaker,
|
||||
},
|
||||
computed: {
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
currentTheme() {
|
||||
return this.appConfig.theme || defaultTheme;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
customCss: this.config.appConfig.customCss || '\n\n',
|
||||
currentTheme: getTheme(),
|
||||
customCss: '',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// Get existing custom styles (if present) from appConfig
|
||||
this.customCss = this.appConfig.customCss || '\n\n';
|
||||
},
|
||||
methods: {
|
||||
/* A regex to validate the users CSS */
|
||||
validate(css) {
|
||||
return css === '' || css.match(/([#.@]?[\w.:> ]+)[\s]{[\r\n]?([A-Za-z\- \r\n\t]+[:][\s]*[\w ./()\-!]+;[\r\n]*(?:[A-Za-z\- \r\n\t]+[:][\s]*[\w ./()\-!]+;[\r\n]*(2)*)*)}/gmi);
|
||||
},
|
||||
/* Save custom CSS in browser, call inject, and show success message */
|
||||
/* Sanitizes input, saves to browser and store, applies to page and shows message */
|
||||
save() {
|
||||
let msg = '';
|
||||
if (this.validate(this.customCss)) {
|
||||
const appConfig = { ...this.config.appConfig };
|
||||
appConfig.customCss = this.customCss;
|
||||
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(appConfig));
|
||||
msg = 'Changes saved successfully';
|
||||
InfoHandler('User syles has been saved', 'Custom CSS Update');
|
||||
this.inject(this.customCss);
|
||||
if (this.customCss === '') setTimeout(() => { location.reload(); }, 1500); // eslint-disable-line no-restricted-globals
|
||||
} else {
|
||||
msg = 'Error - Invalid CSS';
|
||||
InfoHandler(msg, 'Custom CSS Update');
|
||||
}
|
||||
this.$toasted.show(msg);
|
||||
const css = this.customCss.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
this.$store.commit(StoreKeys.UPDATE_CUSTOM_CSS, css);
|
||||
this.saveToBrowser(css);
|
||||
this.injectToPage(css);
|
||||
this.showSuccessMsg();
|
||||
if (css === '') this.reloadPage();
|
||||
},
|
||||
/* Formats CSS, and applies it to page */
|
||||
inject(userStyles) {
|
||||
injectToPage(userStyles) {
|
||||
const cleanedCss = userStyles.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
const style = document.createElement('style');
|
||||
style.textContent = cleanedCss;
|
||||
document.head.append(style);
|
||||
},
|
||||
/* Saves custom CSS local storage */
|
||||
saveToBrowser(css) {
|
||||
const localAppConfig = JSON.parse(localStorage.getItem(localStorageKeys.APP_CONFIG) || '{}');
|
||||
localAppConfig.customCss = css;
|
||||
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(localAppConfig));
|
||||
},
|
||||
/* Reload the page (only called if removing styles) */
|
||||
reloadPage() {
|
||||
setTimeout(() => { location.reload(); }, 1500); // eslint-disable-line no-restricted-globals
|
||||
},
|
||||
/* Show success toast and lot update */
|
||||
showSuccessMsg() {
|
||||
this.$toasted.show('Changes saved successfully');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
// Main layout
|
||||
div.css-editor-outer {
|
||||
text-align: center;
|
||||
padding-bottom: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.style-section {
|
||||
padding: 1rem;
|
||||
&:not(:last-child) { border-bottom: 1px dashed var(--config-settings-color); }
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
margin: 0.5rem 0 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
div.css-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
h2.css-input-title {
|
||||
margin: 0.5rem 0 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Save button
|
||||
button.save-button {
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0.25rem auto;
|
||||
font-size: 1.2rem;
|
||||
background: var(--config-settings-color);
|
||||
color: var(--config-settings-background);
|
||||
border: 1px solid var(--config-settings-background);
|
||||
border-radius: var(--curve-factor);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: var(--config-settings-background);
|
||||
color: var(--config-settings-color);
|
||||
border-color: var(--config-settings-color);
|
||||
background: var(--config-settings-background);
|
||||
color: var(--config-settings-color);
|
||||
border: 1px solid var(--config-settings-color);
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--config-settings-color);
|
||||
color: var(--config-settings-background);
|
||||
}
|
||||
}
|
||||
|
||||
// CSS textarea input
|
||||
.css-editor {
|
||||
margin: 1rem auto;
|
||||
padding: 0.5rem;
|
||||
@@ -121,6 +143,7 @@ button.save-button {
|
||||
}
|
||||
}
|
||||
|
||||
// Info note
|
||||
p.quick-note {
|
||||
text-align: left;
|
||||
width: 80%;
|
||||
@@ -131,13 +154,38 @@ p.quick-note {
|
||||
border-radius: var(--curve-factor);
|
||||
}
|
||||
|
||||
// Base Theme Selector
|
||||
.base-theme-wrapper {
|
||||
span.theme-label {
|
||||
display: none;
|
||||
}
|
||||
div.vs__dropdown-toggle {
|
||||
border-color: var(--config-settings-color);
|
||||
min-width: 16rem;
|
||||
max-width: 32rem;
|
||||
height: 2.4rem;
|
||||
font-size: 1rem;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
ul.vs__dropdown-menu {
|
||||
min-width: 16rem;
|
||||
max-width: 32rem;
|
||||
background: var(--config-settings-background);
|
||||
border-top: 1px solid var(--config-settings-color);
|
||||
}
|
||||
li.vs__dropdown-option--highlight {
|
||||
background: var(--config-settings-color);
|
||||
color: var(--config-settings-background);
|
||||
}
|
||||
}
|
||||
|
||||
// Theme editor
|
||||
.color-config.theme-configurator-wrapper {
|
||||
border: 1px solid var(--config-settings-color);
|
||||
background: var(--config-settings-background);
|
||||
color: var(--config-settings-color);
|
||||
position: relative;
|
||||
width: 80%;
|
||||
max-width: 24rem;
|
||||
max-width: 32rem;
|
||||
margin: 1rem auto;
|
||||
box-shadow: none;
|
||||
right: 0;
|
||||
@@ -147,6 +195,12 @@ p.quick-note {
|
||||
text-align: left;
|
||||
max-height: unset;
|
||||
}
|
||||
.misc-input {
|
||||
width: 6rem;
|
||||
}
|
||||
.misc-input.long-input {
|
||||
width: 18rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
<template>
|
||||
<div class="json-editor-outer">
|
||||
<!-- Main JSON editor -->
|
||||
<v-jsoneditor
|
||||
v-model="jsonData"
|
||||
:options="options"
|
||||
/>
|
||||
<v-jsoneditor v-model="jsonData" :options="options" />
|
||||
<!-- Options raido, and save button -->
|
||||
<div class="save-options">
|
||||
<span class="save-option-title">{{ $t('config-editor.save-location-label') }}:</span>
|
||||
<div class="option">
|
||||
<input type="radio" id="local" value="local"
|
||||
v-model="saveMode" class="radio-option" :disabled="!allowWriteToDisk" />
|
||||
<label for="local" class="save-option-label">
|
||||
{{ $t('config-editor.location-local-label') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="option">
|
||||
<input type="radio" id="file" value="file" v-model="saveMode" class="radio-option"
|
||||
:disabled="!allowWriteToDisk" />
|
||||
<label for="file" class="save-option-label">
|
||||
{{ $t('config-editor.location-disk-label') }}
|
||||
</label>
|
||||
</div>
|
||||
<Radio class="save-options"
|
||||
v-model="saveMode"
|
||||
:label="$t('config-editor.save-location-label')"
|
||||
:options="saveOptions"
|
||||
:initialOption="initialSaveMode"
|
||||
:disabled="!allowWriteToDisk"
|
||||
/>
|
||||
<!-- Save Buttons -->
|
||||
<div :class="`btn-container ${!isValid ? 'err' : ''}`">
|
||||
<Button :click="save">
|
||||
{{ $t('config-editor.save-button') }}
|
||||
</Button>
|
||||
<Button :click="startPreview">
|
||||
{{ $t('config-editor.preview-button') }}
|
||||
</Button>
|
||||
</div>
|
||||
<button :class="`save-button ${!isValid ? 'err' : ''}`" @click="save()">
|
||||
{{ $t('config-editor.save-button') }}
|
||||
</button>
|
||||
<!-- List validation warnings -->
|
||||
<p class="errors">
|
||||
<ul>
|
||||
@@ -50,7 +43,6 @@
|
||||
<p v-if="saveSuccess" class="response-output">
|
||||
{{ $t('config-editor.success-note-l1') }}
|
||||
{{ $t('config-editor.success-note-l2') }}
|
||||
{{ $t('config-editor.success-note-l3') }}
|
||||
</p>
|
||||
<p class="note">{{ $t('config.backup-note') }}</p>
|
||||
</div>
|
||||
@@ -61,25 +53,27 @@
|
||||
import axios from 'axios';
|
||||
import ProgressBar from 'rsup-progress';
|
||||
import VJsoneditor from 'v-jsoneditor';
|
||||
import ErrorHandler, { InfoHandler } from '@/utils/ErrorHandler';
|
||||
import jsYaml from 'js-yaml';
|
||||
import ErrorHandler, { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
|
||||
import configSchema from '@/utils/ConfigSchema.json';
|
||||
import JsonToYaml from '@/utils/JsonToYaml';
|
||||
import { localStorageKeys, serviceEndpoints } from '@/utils/defaults';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import { localStorageKeys, serviceEndpoints, modalNames } from '@/utils/defaults';
|
||||
import { isUserAdmin } from '@/utils/Auth';
|
||||
import Button from '@/components/FormElements/Button';
|
||||
import Radio from '@/components/FormElements/Radio';
|
||||
|
||||
export default {
|
||||
name: 'JsonEditor',
|
||||
props: {
|
||||
config: Object,
|
||||
},
|
||||
components: {
|
||||
VJsoneditor,
|
||||
Button,
|
||||
Radio,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
jsonData: this.config,
|
||||
jsonData: {},
|
||||
errorMessages: [],
|
||||
saveMode: 'file',
|
||||
saveMode: '',
|
||||
options: {
|
||||
schema: configSchema,
|
||||
mode: 'tree',
|
||||
@@ -87,26 +81,36 @@ export default {
|
||||
name: 'config',
|
||||
onValidationError: this.validationErrors,
|
||||
},
|
||||
jsonParser: JsonToYaml,
|
||||
responseText: '',
|
||||
saveSuccess: undefined,
|
||||
allowWriteToDisk: this.shouldAllowWriteToDisk(),
|
||||
progress: new ProgressBar({ color: 'var(--progress-bar)' }),
|
||||
saveOptions: [
|
||||
{ label: this.$t('config-editor.location-disk-label'), value: 'file' },
|
||||
{ label: this.$t('config-editor.location-local-label'), value: 'local' },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
config() {
|
||||
return this.$store.state.config;
|
||||
},
|
||||
isValid() {
|
||||
return this.errorMessages.length < 1;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!this.allowWriteToDisk) this.saveMode = 'local';
|
||||
},
|
||||
methods: {
|
||||
shouldAllowWriteToDisk() {
|
||||
allowWriteToDisk() {
|
||||
const { appConfig } = this.config;
|
||||
return appConfig.allowConfigEdit !== false && isUserAdmin();
|
||||
},
|
||||
initialSaveMode() {
|
||||
return this.allowWriteToDisk ? 'file' : 'local';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.jsonData = this.config;
|
||||
if (!this.allowWriteToDisk) this.saveMode = 'local';
|
||||
},
|
||||
methods: {
|
||||
/* Calls appropriate save method, based on save-type radio selected */
|
||||
save() {
|
||||
if (this.saveMode === 'local' || !this.allowWriteToDisk) {
|
||||
this.saveConfigLocally();
|
||||
@@ -116,9 +120,21 @@ export default {
|
||||
this.$toasted.show(this.$t('config-editor.error-msg-save-mode'));
|
||||
}
|
||||
},
|
||||
/* Applies changes to the local state, begins edit mode and closes modal */
|
||||
startPreview() {
|
||||
InfoHandler('Applying changes to local state...', InfoKeys.RAW_EDITOR);
|
||||
const data = this.jsonData;
|
||||
this.$store.commit(StoreKeys.SET_APP_CONFIG, data.appConfig);
|
||||
this.$store.commit(StoreKeys.SET_PAGE_INFO, data.pageInfo);
|
||||
this.$store.commit(StoreKeys.SET_SECTIONS, data.sections);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
this.$store.commit(StoreKeys.SET_EDIT_MODE, true);
|
||||
this.$modal.hide(modalNames.CONF_EDITOR);
|
||||
},
|
||||
/* Converts config to YAML, and writes it to disk */
|
||||
writeConfigToDisk() {
|
||||
// 1. Convert JSON into YAML
|
||||
const yaml = this.jsonParser(this.jsonData);
|
||||
const yaml = jsYaml.dump(this.config);
|
||||
// 2. Prepare the request
|
||||
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
||||
const endpoint = `${baseUrl}${serviceEndpoints.save}`;
|
||||
@@ -136,7 +152,8 @@ export default {
|
||||
} else {
|
||||
this.showToast(this.$t('config-editor.error-msg-cannot-save'), false);
|
||||
}
|
||||
InfoHandler('Config has been written to disk succesfully', 'Config Update');
|
||||
InfoHandler('Config has been written to disk succesfully', InfoKeys.RAW_EDITOR);
|
||||
this.$store.commit(StoreKeys.SET_CONFIG, this.jsonData);
|
||||
this.progress.end();
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -147,6 +164,7 @@ export default {
|
||||
this.progress.end();
|
||||
});
|
||||
},
|
||||
/* Saves config to local browser storage */
|
||||
saveConfigLocally() {
|
||||
const data = this.jsonData;
|
||||
if (data.sections) {
|
||||
@@ -156,20 +174,22 @@ export default {
|
||||
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(data.pageInfo));
|
||||
}
|
||||
if (data.appConfig) {
|
||||
data.appConfig.auth = this.config.appConfig.auth || [];
|
||||
data.appConfig.auth = this.config.appConfig.auth || {};
|
||||
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(data.appConfig));
|
||||
}
|
||||
if (data.appConfig.theme) {
|
||||
localStorage.setItem(localStorageKeys.THEME, data.appConfig.theme);
|
||||
}
|
||||
InfoHandler('Config has succesfully been saved in browser storage', 'Config Update');
|
||||
InfoHandler('Config has succesfully been saved in browser storage', InfoKeys.RAW_EDITOR);
|
||||
this.showToast(this.$t('config-editor.success-msg-local'), true);
|
||||
},
|
||||
/* Clears config from browser storage, only removing relevant items */
|
||||
carefullyClearLocalStorage() {
|
||||
localStorage.removeItem(localStorageKeys.PAGE_INFO);
|
||||
localStorage.removeItem(localStorageKeys.APP_CONFIG);
|
||||
localStorage.removeItem(localStorageKeys.CONF_SECTIONS);
|
||||
},
|
||||
/* Convert error messages into readable format for UI */
|
||||
validationErrors(errors) {
|
||||
const errorMessages = [];
|
||||
errors.forEach((error) => {
|
||||
@@ -197,6 +217,7 @@ export default {
|
||||
});
|
||||
this.errorMessages = errorMessages;
|
||||
},
|
||||
/* Shows toast message */
|
||||
showToast(message, success) {
|
||||
this.$toasted.show(message, { className: `toast-${success ? 'success' : 'error'}` });
|
||||
},
|
||||
@@ -259,52 +280,59 @@ p.no-permission-note {
|
||||
color: var(--config-settings-color);
|
||||
}
|
||||
|
||||
button.save-button {
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0.25rem auto;
|
||||
font-size: 1.2rem;
|
||||
background: var(--config-settings-color);
|
||||
color: var(--config-settings-background);
|
||||
border: 1px solid var(--config-settings-background);
|
||||
border-radius: var(--curve-factor);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
.btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0.25rem;
|
||||
font-size: 1.2rem;
|
||||
background: var(--config-settings-background);
|
||||
color: var(--config-settings-color);
|
||||
border-color: var(--config-settings-color);
|
||||
}
|
||||
&.err {
|
||||
opacity: 0.8;
|
||||
cursor: default;
|
||||
border: 1px solid var(--config-settings-color);
|
||||
border-radius: var(--curve-factor);
|
||||
&:hover {
|
||||
background: var(--config-settings-color);
|
||||
color: var(--config-settings-background);
|
||||
border-color: var(--config-settings-background);
|
||||
}
|
||||
}
|
||||
&.err button {
|
||||
opacity: 0.8;
|
||||
cursor: default;
|
||||
&:hover {
|
||||
background: var(--config-settings-background);
|
||||
color: var(--config-settings-color);
|
||||
border-color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.save-options {
|
||||
div.save-options.radio-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: var(--code-editor-background);
|
||||
color: var(--code-editor-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-top: 2px solid var(--config-settings-background);
|
||||
@include tablet-down { flex-direction: column; }
|
||||
.option {
|
||||
@include tablet-up { margin-left: 2rem; }
|
||||
background: var(--code-editor-background);
|
||||
label.radio-label {
|
||||
font-size: 1rem;
|
||||
flex-grow: revert;
|
||||
flex-basis: revert;
|
||||
color: var(--code-editor-color);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
span.save-option-title {
|
||||
cursor: default;
|
||||
}
|
||||
input.radio-option {
|
||||
cursor: pointer;
|
||||
}
|
||||
label.save-option-label {
|
||||
cursor: pointer;
|
||||
.radio-wrapper {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
justify-content: space-around;
|
||||
background: var(--code-editor-background);
|
||||
color: var(--code-editor-color);
|
||||
.radio-option:hover:not(.wrap-disabled) {
|
||||
border: 1px solid var(--code-editor-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,11 @@ import { modalNames, serviceEndpoints } from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
name: 'RebuildApp',
|
||||
inject: ['config'],
|
||||
computed: {
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Button,
|
||||
RebuildIcon,
|
||||
@@ -112,12 +116,8 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.config) {
|
||||
if (this.config.appConfig) {
|
||||
if (this.config.appConfig.allowConfigEdit === false) {
|
||||
this.allowRebuild = false;
|
||||
}
|
||||
}
|
||||
if (this.appConfig.allowConfigEdit === false) {
|
||||
this.allowRebuild = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<button @click="click()" :disabled="disabled" :class="disallow ? 'disallowed': ''">
|
||||
<button
|
||||
@click="click ? click() : () => null"
|
||||
:class="disallow ? 'disallowed': ''"
|
||||
:type="type || 'button'"
|
||||
:disabled="disabled"
|
||||
v-tooltip="hoverText"
|
||||
:title="tooltip"
|
||||
>
|
||||
<slot></slot>
|
||||
<slot name="text"></slot>
|
||||
<slot name="icon"></slot>
|
||||
@@ -11,10 +18,21 @@
|
||||
export default {
|
||||
name: 'Button',
|
||||
props: {
|
||||
text: String,
|
||||
click: Function,
|
||||
disabled: Boolean,
|
||||
disallow: Boolean,
|
||||
text: String, // The text to be displayed in the button
|
||||
click: Function, // Function to call when clicked
|
||||
disabled: Boolean, // If true, button cannot be clicked
|
||||
disallow: Boolean, // Show not-allowed cursor when true
|
||||
type: String, // The html button type attribute
|
||||
tooltip: String, // Text to be displayed on hover
|
||||
},
|
||||
computed: {
|
||||
/* If tooltip prop specified, then return config for v-tooltip */
|
||||
hoverText() {
|
||||
const content = this.tooltip;
|
||||
const trigger = 'hover focus';
|
||||
const delay = { show: 350, hide: 100 };
|
||||
return (content) ? { content, trigger, delay } : undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<template>
|
||||
<div :class="`input-container ${layout}`">
|
||||
<label v-if="label" for="name">{{label}}</label>
|
||||
<label
|
||||
v-if="label"
|
||||
for="name"
|
||||
class="input-label"
|
||||
>
|
||||
{{label}}
|
||||
</label>
|
||||
<input
|
||||
:type="type"
|
||||
:value="value"
|
||||
@@ -8,7 +14,14 @@
|
||||
:name="name"
|
||||
:id="name"
|
||||
:placeholder="placeholder"
|
||||
class="input-field"
|
||||
/>
|
||||
<p
|
||||
v-if="description"
|
||||
class="input-description"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,10 +30,11 @@
|
||||
export default {
|
||||
name: 'Input',
|
||||
props: {
|
||||
value: String, // The value bound to v-model
|
||||
value: [String, Number], // The value bound to v-model
|
||||
label: String, // An optional label to display above
|
||||
name: String, // Required unique ID value, for accessibility
|
||||
placeholder: String, // Optional placeholder value
|
||||
description: String, // Optional info paragraph
|
||||
type: {
|
||||
default: 'text', // Input type, e.g. text, password, number
|
||||
type: String,
|
||||
@@ -40,6 +54,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/media-queries.scss';
|
||||
|
||||
div.input-container {
|
||||
margin: 0.25rem auto;
|
||||
display: flex;
|
||||
@@ -48,12 +64,23 @@ div.input-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
label { margin-right: 0.25rem; }
|
||||
@include tablet-up {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
label.input-label,
|
||||
input.input-field,
|
||||
p.input-description {
|
||||
margin: 0.25rem;
|
||||
flex-basis: 8rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
input.input-field { flex-grow: 2; }
|
||||
p.input-description { flex-grow: 3; }
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
input.input-field {
|
||||
min-width: 10rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin: 0.5rem auto;
|
||||
@@ -68,6 +95,22 @@ div.input-container {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
label.input-label {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p.input-description {
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
|
||||
@include tablet-down {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
input.input-field {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
109
src/components/FormElements/Radio.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="radio-container">
|
||||
<label v-if="label" class="radio-label">{{ label }}</label>
|
||||
<div class="radio-wrapper">
|
||||
<div v-for="radio in options" :key="radio.value"
|
||||
:class="`radio-option ${disabled ? 'wrap-disabled' : ''}`">
|
||||
<label :for="`id-${radio.value}`" class="option-label">{{ radio.label }}</label>
|
||||
<input type="radio" class="radio-input"
|
||||
:id=" `id-${radio.value}`"
|
||||
:name="makeGroupName"
|
||||
:value="radio.value"
|
||||
:disabled="disabled || radio.disabled"
|
||||
v-model="selectedRadio"
|
||||
v-on:input="updateValue($event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="description" class="radio-description">{{ description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Radio',
|
||||
components: {},
|
||||
props: {
|
||||
options: Array, // Array of objects for available options
|
||||
initialOption: String, // Optional default option
|
||||
label: String, // Form label for element
|
||||
description: String, // Optional description text
|
||||
disabled: Boolean, // Disable all radio buttons
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedRadio: '', // The currently radio val
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.initialOption) {
|
||||
this.updateValue(this.initialOption);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
makeGroupName() {
|
||||
return this.label.toLowerCase().replace(/[^a-z]+/, '');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateValue(value) {
|
||||
this.$emit('input', value);
|
||||
this.selectedRadio = value;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
div.radio-container {
|
||||
margin: 0.25rem auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
label.radio-label,
|
||||
.radio-wrapper,
|
||||
p.radio-description {
|
||||
margin: 0.25rem;
|
||||
flex-basis: 8rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
label.radio-label {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
p.radio-description {
|
||||
flex-grow: 3;
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
.radio-wrapper {
|
||||
display: flex;
|
||||
flex-grow: 2;
|
||||
margin: 0.5rem auto;
|
||||
font-size: 1.2rem;
|
||||
color: var(--primary);
|
||||
background: var(--background);;
|
||||
border-radius: var(--curve-factor);
|
||||
min-width: 8rem;
|
||||
.radio-option {
|
||||
margin: 0.2rem;
|
||||
padding: 0.2rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--curve-factor);
|
||||
&:hover:not(.wrap-disabled) {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
&:disabled {
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
label.option-label, input.radio-input {
|
||||
cursor: pointer;
|
||||
text-transform: capitalize;
|
||||
margin: 0.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
128
src/components/FormElements/Select.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="select-container">
|
||||
<label v-if="label" class="select-label">{{ label }}</label>
|
||||
<v-select
|
||||
@input="updateValue"
|
||||
:value="selectedOption"
|
||||
:selectOnTab="true"
|
||||
:options="options"
|
||||
class="form-dropdown"
|
||||
/>
|
||||
<p v-if="description" class="select-description">{{ description }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Select',
|
||||
components: {},
|
||||
props: {
|
||||
options: Array, // Array of available options
|
||||
initialOption: String, // Optional default option
|
||||
label: String, // Form label for element
|
||||
description: String, // Optional description text
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedOption: '', // The currently selected val
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.initialOption) {
|
||||
this.selectedOption = this.initialOption;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValue(value) {
|
||||
this.$emit('input', value);
|
||||
this.selectedOption = value;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/media-queries.scss';
|
||||
|
||||
div.select-container {
|
||||
margin: 0.25rem auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
label.select-label,
|
||||
.form-dropdown,
|
||||
p.select-description {
|
||||
margin: 0.25rem;
|
||||
flex-basis: 8rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
label.select-label {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
p.select-description {
|
||||
flex-grow: 3;
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
.form-dropdown {
|
||||
flex-grow: 2;
|
||||
min-width: 12rem;
|
||||
margin: 0.5rem auto;
|
||||
font-size: 1.2rem;
|
||||
box-sizing: border-box;
|
||||
color: var(--primary);
|
||||
background: var(--background);;
|
||||
border-radius: var(--curve-factor);
|
||||
&:focus {
|
||||
box-shadow: 1px 1px 6px var(--config-settings-color);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
@include tablet-down {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
label.select-label,
|
||||
.form-dropdown,
|
||||
p.select-description {
|
||||
margin: 0.5rem;
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/style-helpers.scss';
|
||||
|
||||
.form-dropdown {
|
||||
margin: 1rem auto;
|
||||
ul.vs__dropdown-menu {
|
||||
max-height: 14rem;
|
||||
@extend .scroll-bar;
|
||||
}
|
||||
input.vs__search {
|
||||
color: var(--primary);
|
||||
}
|
||||
div.vs__dropdown-toggle {
|
||||
padding: 0.2rem 0;
|
||||
border-color: var(--primary);
|
||||
background: var(--background);
|
||||
.vs__actions svg {
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0.2rem 0 0 0.2rem;
|
||||
&:hover {
|
||||
background: var(--primary);
|
||||
path { fill: var(--background); }
|
||||
}
|
||||
}
|
||||
}
|
||||
div, input {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
66
src/components/InteractiveEditor/AddNewSectionLauncher.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<!-- Main homepage for default view -->
|
||||
<template>
|
||||
<div class="add-section">
|
||||
<!-- When in edit mode, show Add New Section button -->
|
||||
<div v-if="isEditMode" @click="openAddNewSectionMenu()" class="add-new-section">
|
||||
<p>➕ {{ $t('interactive-editor.edit-section.add-section-title') }}</p>
|
||||
</div>
|
||||
<!-- Add new section form -->
|
||||
<EditSectionMenu
|
||||
v-if="isEditMode && addNewSectionOpen"
|
||||
:isAddNew="true"
|
||||
@closeEditSection="closeEditSection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import EditSectionMenu from '@/components/InteractiveEditor/EditSection.vue';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import { modalNames } from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
name: 'add-section-container',
|
||||
components: {
|
||||
EditSectionMenu,
|
||||
},
|
||||
data: () => ({
|
||||
addNewSectionOpen: false,
|
||||
}),
|
||||
computed: {
|
||||
isEditMode() {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openAddNewSectionMenu() {
|
||||
this.addNewSectionOpen = true;
|
||||
this.$modal.show(modalNames.EDIT_SECTION);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, true);
|
||||
},
|
||||
closeEditSection() {
|
||||
this.addNewSectionOpen = false;
|
||||
this.$modal.hide(modalNames.EDIT_SECTION);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.add-new-section {
|
||||
border: 2px dashed var(--primary);
|
||||
border-radius: var(--curve-factor);
|
||||
padding: var(--item-group-padding);
|
||||
background: var(--item-group-background);
|
||||
color: var(--primary);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
height: fit-content;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
134
src/components/InteractiveEditor/EditAppConfig.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<modal
|
||||
:name="modalName"
|
||||
:resizable="true"
|
||||
width="50%"
|
||||
height="80%"
|
||||
classes="dashy-modal edit-app-config"
|
||||
@closed="modalClosed"
|
||||
>
|
||||
<div class="edit-app-config-inner">
|
||||
<h3>{{ $t('interactive-editor.menu.edit-app-config-btn') }}</h3>
|
||||
<!-- Show caution message -->
|
||||
<div class="app-config-intro">
|
||||
<p class="use-caution">
|
||||
{{ $t('interactive-editor.edit-app-config.warning-msg-title') }}
|
||||
</p>
|
||||
{{ $t('interactive-editor.edit-app-config.warning-msg-l1') }}
|
||||
{{ $t('interactive-editor.edit-app-config.warning-msg-l2') }}
|
||||
<a href="https://dashy.to/docs/configuring#appconfig-optional">
|
||||
{{ $t('interactive-editor.edit-app-config.warning-msg-docs') }}
|
||||
</a>
|
||||
{{ $t('interactive-editor.edit-app-config.warning-msg-l3') }}
|
||||
</div>
|
||||
<!-- Save Button, upper -->
|
||||
<SaveCancelButtons :saveClick="saveToState" :cancelClick="cancelEditing" />
|
||||
<!-- The main form -->
|
||||
<FormSchema
|
||||
:schema="schema"
|
||||
v-model="formData"
|
||||
@submit.prevent="saveToState"
|
||||
:search="true"
|
||||
class="app-config-form"
|
||||
name="appConfigForm"
|
||||
></FormSchema>
|
||||
<!-- Save Button, lower -->
|
||||
<SaveCancelButtons :saveClick="saveToState" :cancelClick="cancelEditing" />
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSchema from '@formschema/native';
|
||||
import DashySchema from '@/utils/ConfigSchema';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import { modalNames } from '@/utils/defaults';
|
||||
import SaveCancelButtons from '@/components/InteractiveEditor/SaveCancelButtons';
|
||||
|
||||
export default {
|
||||
name: 'EditAppConfig',
|
||||
data() {
|
||||
return {
|
||||
formData: {},
|
||||
schema: DashySchema.properties.appConfig,
|
||||
modalName: modalNames.EDIT_APP_CONFIG,
|
||||
};
|
||||
},
|
||||
props: {},
|
||||
components: {
|
||||
FormSchema,
|
||||
SaveCancelButtons,
|
||||
},
|
||||
mounted() {
|
||||
this.formData = this.appConfig;
|
||||
},
|
||||
computed: {
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* When form submitteed, update VueX store with new appConfig, and close modal */
|
||||
saveToState() {
|
||||
const processedFormData = this.removeUndefinedValues(this.formData);
|
||||
this.$store.commit(StoreKeys.SET_APP_CONFIG, processedFormData);
|
||||
this.$modal.hide(this.modalName);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
this.$store.commit(StoreKeys.SET_EDIT_MODE, true);
|
||||
},
|
||||
cancelEditing() {
|
||||
this.$modal.hide(this.modalName);
|
||||
},
|
||||
/* Called when modal manually closed, updates state to allow searching again */
|
||||
modalClosed() {
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
},
|
||||
/* Remove any attribute which has an undefined value before saving */
|
||||
removeUndefinedValues(rawAppConfig) {
|
||||
const raw = rawAppConfig;
|
||||
const isEmpty = (value) => (value === undefined);
|
||||
Object.keys(raw).forEach(key => isEmpty(raw[key]) && delete raw[key]);
|
||||
return raw;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/style-helpers.scss';
|
||||
@import '@/styles/media-queries.scss';
|
||||
@import '@/styles/schema-editor.scss';
|
||||
|
||||
.edit-app-config-inner {
|
||||
padding: 1rem;
|
||||
background: var(--interactive-editor-background);
|
||||
color: var(--interactive-editor-color);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
@extend .scroll-bar;
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
.app-config-form {
|
||||
@extend .schema-form;
|
||||
border-top: 1px dashed var(--interactive-editor-color);
|
||||
}
|
||||
.app-config-intro {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-background-darker);
|
||||
border-radius: var(--interactive-editor-color);
|
||||
p.use-caution {
|
||||
color: var(--warning);
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
a {
|
||||
color: var(--interactive-editor-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
344
src/components/InteractiveEditor/EditItem.vue
Normal file
@@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<modal
|
||||
:name="modalName"
|
||||
:resizable="true"
|
||||
width="50%"
|
||||
height="80%"
|
||||
classes="dashy-modal edit-item"
|
||||
@closed="modalClosed"
|
||||
>
|
||||
<div class="edit-item-inner">
|
||||
<!-- Title and Item ID -->
|
||||
<h3 class="title">Edit Item</h3>
|
||||
<p class="sub-title">Editing {{item.title}} (ID: {{itemId}})</p>
|
||||
<!-- If no elements added to form, show info message -->
|
||||
<p class="warning-note" v-if="formData.length === 0">
|
||||
No data configured yet. Click an attribute in the list below to add the field to the form.
|
||||
</p>
|
||||
<!-- For each data attribute, render the correct type of input field -->
|
||||
<div class="row" v-for="(row, index) in formData" :key="row.name">
|
||||
<!-- Text box, for text/ number/ raw input elements -->
|
||||
<Input
|
||||
v-if="row.type === 'text' || row.type === 'number'"
|
||||
v-model="formData[index].value"
|
||||
:description="row.description"
|
||||
:label="row.title || row.name"
|
||||
:type="row.type"
|
||||
layout="horizontal"
|
||||
/>
|
||||
<!-- Radio button, used for True or False input -->
|
||||
<Radio
|
||||
v-else-if="row.type === 'boolean'"
|
||||
v-model="formData[index].value"
|
||||
:description="row.description"
|
||||
:label="row.title || row.name"
|
||||
:options="[ ...boolRadioOptions ]"
|
||||
:initialOption="boolToStr(formData[index].value)"
|
||||
/>
|
||||
<!-- Select/ dropdown for enum multiple-choice input -->
|
||||
<Select
|
||||
v-else-if="row.type === 'select'"
|
||||
v-model="formData[index].value"
|
||||
:options="formData[index].enum"
|
||||
:description="row.description"
|
||||
:initialOption="formData[index].value"
|
||||
:label="row.title || row.name"
|
||||
class="edit-item-select"
|
||||
/>
|
||||
<!-- Warning note, for any other data types, that aren't yet supported -->
|
||||
<div v-else>
|
||||
{{ row.name }} cannot currently be edited through the UI.
|
||||
</div>
|
||||
<BinIcon @click="() => removeField(row.name)" />
|
||||
</div>
|
||||
<!-- Show Add chips, for adding more data elements to the form -->
|
||||
<div class="add-more-inputs" v-if="additionalFormData.length > 0">
|
||||
<h4>More Fields</h4>
|
||||
<div class="more-fields">
|
||||
<span
|
||||
v-for="row in additionalFormData"
|
||||
:key="row.name"
|
||||
@click="() => appendNewField(row.name)"
|
||||
class="add-field-tag">
|
||||
<AddIcon /> {{ row.title || row.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Save to state button -->
|
||||
<SaveCancelButtons :saveClick="saveItem" :cancelClick="modalClosed" />
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AddIcon from '@/assets/interface-icons/interactive-editor-add.svg';
|
||||
import BinIcon from '@/assets/interface-icons/interactive-editor-remove.svg';
|
||||
import SaveCancelButtons from '@/components/InteractiveEditor/SaveCancelButtons';
|
||||
import Input from '@/components/FormElements/Input';
|
||||
import Radio from '@/components/FormElements/Radio';
|
||||
import Select from '@/components/FormElements/Select';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import DashySchema from '@/utils/ConfigSchema';
|
||||
import { modalNames } from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
name: 'EditItem',
|
||||
data() {
|
||||
return {
|
||||
modalName: modalNames.EDIT_ITEM,
|
||||
schema: DashySchema.properties.sections.items.properties.items.items.properties,
|
||||
formData: [], // Array of form fields
|
||||
additionalFormData: [], // Array of not-yet-used form fields
|
||||
item: {},
|
||||
boolRadioOptions: [
|
||||
{ label: 'true', value: 'true' },
|
||||
{ label: 'false', value: 'false' },
|
||||
],
|
||||
};
|
||||
},
|
||||
props: {
|
||||
itemId: String,
|
||||
isNew: Boolean,
|
||||
parentSectionTitle: String, // If adding new item, which section to add it under
|
||||
},
|
||||
computed: {},
|
||||
components: {
|
||||
Input,
|
||||
Radio,
|
||||
Select,
|
||||
AddIcon,
|
||||
BinIcon,
|
||||
SaveCancelButtons,
|
||||
},
|
||||
mounted() {
|
||||
if (!this.isNew) { // Get existing item data
|
||||
this.item = this.getItemFromState(this.itemId);
|
||||
}
|
||||
this.formData = this.makeInitialFormData();
|
||||
this.$modal.show(modalNames.EDIT_ITEM);
|
||||
},
|
||||
methods: {
|
||||
/* For a given item ID, return the item obj from store */
|
||||
getItemFromState(id) {
|
||||
return this.$store.getters.getItemById(id);
|
||||
},
|
||||
/* Using the schema, make data structure for the UI form fields to use */
|
||||
makeRowData(property) {
|
||||
return {
|
||||
name: property,
|
||||
description: this.schema[property].description,
|
||||
value: this.item[property],
|
||||
type: this.getInputType(this.schema[property]),
|
||||
enum: this.schema[property].enum,
|
||||
title: this.schema[property].title,
|
||||
};
|
||||
},
|
||||
/* Make formatted data structure to be rendered as form elements */
|
||||
makeInitialFormData() {
|
||||
const formData = [];
|
||||
const requiredFields = ['title', 'description', 'url', 'icon', 'target'];
|
||||
const unneededFields = ['id'];
|
||||
const isPrimaryField = (property) => (
|
||||
this.item[property] || requiredFields.includes(property)
|
||||
) && !unneededFields.includes(property);
|
||||
Object.keys(this.schema).forEach((property) => {
|
||||
const singleRow = this.makeRowData(property);
|
||||
if (isPrimaryField(property)) {
|
||||
formData.push(singleRow);
|
||||
} else {
|
||||
this.additionalFormData.push(singleRow);
|
||||
}
|
||||
});
|
||||
return formData;
|
||||
},
|
||||
/* Convert boolean to string */
|
||||
boolToStr(bool) {
|
||||
if (bool) return 'true';
|
||||
if (bool === false) return 'false';
|
||||
return undefined;
|
||||
},
|
||||
/* Adds field from extras list to main form, then removes from extras list */
|
||||
appendNewField(fieldId) {
|
||||
Object.keys(this.schema).forEach((property) => {
|
||||
if (property === fieldId) {
|
||||
this.formData.push(this.makeRowData(property));
|
||||
}
|
||||
});
|
||||
this.additionalFormData.forEach((elem, index) => {
|
||||
if (elem.name === fieldId) {
|
||||
this.additionalFormData.splice(index, 1);
|
||||
}
|
||||
});
|
||||
},
|
||||
/* On Remove Field click, removes field from main form, and adds to chip list */
|
||||
removeField(fieldId) {
|
||||
this.formData.forEach((elem, index) => {
|
||||
if (elem.name === fieldId) {
|
||||
this.formData.splice(index, 1);
|
||||
this.additionalFormData.push(elem);
|
||||
}
|
||||
});
|
||||
},
|
||||
/* Use schema to determine type of form element to render, for a given attribute */
|
||||
getInputType(schemaItem) {
|
||||
const definedType = schemaItem.type;
|
||||
if (definedType === 'text') {
|
||||
return 'text';
|
||||
} else if (definedType === 'number') {
|
||||
return 'number';
|
||||
} else if (definedType === 'boolean') {
|
||||
return 'boolean';
|
||||
} else if (schemaItem.enum) {
|
||||
return 'select';
|
||||
}
|
||||
return 'text';
|
||||
},
|
||||
/* Saves the updated item to VueX Store */
|
||||
saveItem() {
|
||||
// Convert form data back into section.item data structure
|
||||
const structured = {};
|
||||
this.formData.forEach((row) => { structured[row.name] = row.value; });
|
||||
// Some attributes need a little extra formatting
|
||||
const newItem = this.formatBeforeSave(structured);
|
||||
if (this.isNew) { // Insert new item into data store
|
||||
newItem.id = `temp_${newItem.title}`;
|
||||
const payload = { newItem, targetSection: this.parentSectionTitle };
|
||||
this.$store.commit(StoreKeys.INSERT_ITEM, payload);
|
||||
} else { // Update existing item from form data, in the store
|
||||
this.$store.commit(StoreKeys.UPDATE_ITEM, { newItem, itemId: this.itemId });
|
||||
}
|
||||
// If we're not already in edit mode, enable it now
|
||||
this.$store.commit(StoreKeys.SET_EDIT_MODE, true);
|
||||
// Close edit menu
|
||||
this.$emit('closeEditMenu');
|
||||
},
|
||||
/* Some fields require a bit of extra processing before they're saved */
|
||||
formatBeforeSave(item) {
|
||||
const newItem = item;
|
||||
newItem.id = this.itemId;
|
||||
if (newItem.hotkey) newItem.hotkey = parseInt(newItem.hotkey, 10);
|
||||
const strToTags = (str) => {
|
||||
const tagArr = str.split(',');
|
||||
return tagArr.map((tag) => tag.trim().toLowerCase().replace(/[^a-z]+/, ''));
|
||||
};
|
||||
const strToBool = (str) => {
|
||||
if (str === undefined) return undefined;
|
||||
return str === 'true';
|
||||
};
|
||||
if (newItem.tags) newItem.tags = strToTags(newItem.tags);
|
||||
if (newItem.statusCheck) newItem.statusCheck = strToBool(newItem.statusCheck);
|
||||
if (newItem.statusCheckAllowInsecure) {
|
||||
newItem.statusCheckAllowInsecure = strToBool(newItem.statusCheckAllowInsecure);
|
||||
}
|
||||
// if (newItem.hotkey) newItem.hotkey = parseInt(newItem.hotkey, 10);
|
||||
return newItem;
|
||||
},
|
||||
/* Clean up work, triggered when modal closed */
|
||||
modalClosed() {
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
this.$emit('closeEditMenu');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/style-helpers.scss';
|
||||
@import '@/styles/media-queries.scss';
|
||||
|
||||
.edit-item-inner {
|
||||
padding: 1rem;
|
||||
background: var(--interactive-editor-background);
|
||||
color: var(--interactive-editor-color);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
@extend .svg-button;
|
||||
h3.title {
|
||||
font-size: 1.5rem;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
p.sub-title {
|
||||
margin: 0.25rem 0;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
p.warning-note {
|
||||
color: var(--warning);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
padding: 0.5rem 0.25rem;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dotted var(--interactive-editor-color);
|
||||
}
|
||||
.input-container, .select-container {
|
||||
width: 100%;
|
||||
input.input-field {
|
||||
font-size: 1rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.more-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
span.add-field-tag {
|
||||
margin: 0.2rem;
|
||||
padding: 0.2rem 0.5rem;;
|
||||
min-width: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
border: 1px solid var(--interactive-editor-color);
|
||||
border-radius: var(--curve-factor);
|
||||
&:hover {
|
||||
background: var(--interactive-editor-color);
|
||||
color: var(--interactive-editor-background);
|
||||
svg {
|
||||
background: var(--interactive-editor-color);
|
||||
path { fill: var(--interactive-editor-background); }
|
||||
}
|
||||
}
|
||||
svg {
|
||||
margin-right: 0.25rem;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Override form element colors, with local CSS variables */
|
||||
div.input-container input.input-field,
|
||||
.radio-container div.radio-wrapper,
|
||||
.form-dropdown div.vs__dropdown-toggle {
|
||||
color: var(--interactive-editor-color);
|
||||
border-color: var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-background);
|
||||
}
|
||||
svg {
|
||||
path { fill: var(--interactive-editor-color); }
|
||||
background: var(--interactive-editor-background);
|
||||
&:hover, &.selected {
|
||||
path { fill: var(--interactive-editor-background); }
|
||||
background: var(--interactive-editor-color);
|
||||
}
|
||||
}
|
||||
.edit-item-select .v-select {
|
||||
input.vs__search { color: var(--interactive-editor-color); }
|
||||
div.vs__dropdown-toggle {
|
||||
border-color: var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-background);
|
||||
span.vs__selected { color: var(--interactive-editor-color); }
|
||||
.vs__actions svg {
|
||||
background: var(--interactive-editor-background);
|
||||
path { fill: var(--interactive-editor-color); }
|
||||
&:hover {
|
||||
background: var(--interactive-editor-color);
|
||||
path { fill: var(--interactive-editor-background); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
279
src/components/InteractiveEditor/EditModeSaveMenu.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<!-- Intro Info -->
|
||||
<div class="edit-mode-bottom-banner">
|
||||
<div class="edit-banner-section intro-container">
|
||||
<p class="section-sub-title edit-mode-intro l-1">
|
||||
{{ $t('interactive-editor.menu.edit-mode-subtitle') }}
|
||||
</p>
|
||||
<p class="edit-mode-intro l-2">
|
||||
{{ $t('interactive-editor.menu.edit-mode-description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="edit-banner-section empty-space"></div>
|
||||
<!-- Save Buttons -->
|
||||
<div class="edit-banner-section save-buttons-container">
|
||||
<p class="section-sub-title">
|
||||
{{ $t('interactive-editor.menu.config-save-methods-subheading') }}
|
||||
</p>
|
||||
<Button
|
||||
:click="saveLocally"
|
||||
v-tooltip="tooltip($t('interactive-editor.menu.save-locally-tooltip'))"
|
||||
>
|
||||
{{ $t('interactive-editor.menu.save-locally-btn') }}
|
||||
<SaveLocallyIcon />
|
||||
</Button>
|
||||
<Button
|
||||
:click="writeToDisk"
|
||||
:disabled="!allowWriteToDisk"
|
||||
v-tooltip="tooltip($t('interactive-editor.menu.save-disk-tooltip'))"
|
||||
>
|
||||
{{ $t('interactive-editor.menu.save-disk-btn') }}
|
||||
<SaveToDiskIcon />
|
||||
</Button>
|
||||
<Button
|
||||
:click="openExportConfigMenu"
|
||||
v-tooltip="tooltip($t('interactive-editor.menu.export-config-tooltip'))"
|
||||
>
|
||||
{{ $t('interactive-editor.menu.export-config-btn') }}
|
||||
<ExportIcon />
|
||||
</Button>
|
||||
<Button
|
||||
:click="reset"
|
||||
v-tooltip="tooltip($t('interactive-editor.menu.cancel-changes-tooltip'))"
|
||||
>
|
||||
{{ $t('interactive-editor.menu.cancel-changes-btn') }}
|
||||
<CancelIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Open Modal Buttons -->
|
||||
<div class="edit-banner-section edit-site-config-buttons">
|
||||
<p class="section-sub-title">
|
||||
{{ $t('interactive-editor.menu.edit-site-data-subheading') }}
|
||||
</p>
|
||||
<Button
|
||||
:click="openEditPageInfo"
|
||||
v-tooltip="tooltip($t('interactive-editor.menu.edit-page-info-tooltip'))"
|
||||
>
|
||||
{{ $t('interactive-editor.menu.edit-page-info-btn') }}
|
||||
<PageInfoIcon />
|
||||
</Button>
|
||||
<Button
|
||||
:click="openEditAppConfig"
|
||||
v-tooltip="tooltip($t('interactive-editor.menu.edit-app-config-tooltip'))"
|
||||
>
|
||||
{{ $t('interactive-editor.menu.edit-app-config-btn') }}
|
||||
<AppConfigIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Modals for editing appConfig + pageInfo -->
|
||||
<EditPageInfo />
|
||||
<EditAppConfig />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import jsYaml from 'js-yaml';
|
||||
import ProgressBar from 'rsup-progress';
|
||||
|
||||
import Button from '@/components/FormElements/Button';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import EditPageInfo from '@/components/InteractiveEditor/EditPageInfo';
|
||||
import EditAppConfig from '@/components/InteractiveEditor/EditAppConfig';
|
||||
import { modalNames, localStorageKeys, serviceEndpoints } from '@/utils/defaults';
|
||||
import ErrorHandler, { InfoHandler } from '@/utils/ErrorHandler';
|
||||
import { isUserAdmin } from '@/utils/Auth';
|
||||
|
||||
import SaveLocallyIcon from '@/assets/interface-icons/interactive-editor-save-locally.svg';
|
||||
import SaveToDiskIcon from '@/assets/interface-icons/interactive-editor-save-disk.svg';
|
||||
import ExportIcon from '@/assets/interface-icons/interactive-editor-export-changes.svg';
|
||||
import CancelIcon from '@/assets/interface-icons/interactive-editor-cancel-changes.svg';
|
||||
import AppConfigIcon from '@/assets/interface-icons/interactive-editor-app-config.svg';
|
||||
import PageInfoIcon from '@/assets/interface-icons/interactive-editor-page-info.svg';
|
||||
|
||||
export default {
|
||||
name: 'EditModeSaveMenu',
|
||||
components: {
|
||||
Button,
|
||||
EditPageInfo,
|
||||
SaveLocallyIcon,
|
||||
SaveToDiskIcon,
|
||||
ExportIcon,
|
||||
CancelIcon,
|
||||
AppConfigIcon,
|
||||
PageInfoIcon,
|
||||
EditAppConfig,
|
||||
},
|
||||
computed: {
|
||||
config() {
|
||||
return this.$store.state.config;
|
||||
},
|
||||
allowWriteToDisk() {
|
||||
const { appConfig } = this.config;
|
||||
return appConfig.allowConfigEdit !== false && isUserAdmin();
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
saveSuccess: undefined,
|
||||
responseText: '',
|
||||
progress: new ProgressBar({ color: 'var(--progress-bar)' }),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.$store.dispatch(StoreKeys.INITIALIZE_CONFIG);
|
||||
this.$store.commit(StoreKeys.SET_EDIT_MODE, false);
|
||||
},
|
||||
openExportConfigMenu() {
|
||||
this.$modal.show(modalNames.EXPORT_CONFIG_MENU);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, true);
|
||||
},
|
||||
openEditPageInfo() {
|
||||
this.$modal.show(modalNames.EDIT_PAGE_INFO);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, true);
|
||||
},
|
||||
openEditAppConfig() {
|
||||
this.$modal.show(modalNames.EDIT_APP_CONFIG);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, true);
|
||||
},
|
||||
tooltip(content) {
|
||||
return { content, trigger: 'hover focus', delay: 250 };
|
||||
},
|
||||
showToast(message, success) {
|
||||
this.$toasted.show(message, { className: `toast-${success ? 'success' : 'error'}` });
|
||||
},
|
||||
carefullyClearLocalStorage() {
|
||||
localStorage.removeItem(localStorageKeys.PAGE_INFO);
|
||||
localStorage.removeItem(localStorageKeys.APP_CONFIG);
|
||||
localStorage.removeItem(localStorageKeys.CONF_SECTIONS);
|
||||
},
|
||||
saveLocally() {
|
||||
const data = this.config;
|
||||
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(data.sections));
|
||||
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(data.pageInfo));
|
||||
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(data.appConfig));
|
||||
if (data.appConfig.theme) {
|
||||
localStorage.setItem(localStorageKeys.THEME, data.appConfig.theme);
|
||||
}
|
||||
InfoHandler('Config has succesfully been saved in browser storage', 'Config Update');
|
||||
this.showToast(this.$t('config-editor.success-msg-local'), true);
|
||||
this.$store.commit(StoreKeys.SET_EDIT_MODE, false);
|
||||
},
|
||||
writeToDisk() {
|
||||
// 1. Convert JSON into YAML
|
||||
const yamlOptions = {};
|
||||
const yaml = jsYaml.dump(this.config, yamlOptions);
|
||||
// 2. Prepare the request
|
||||
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
||||
const endpoint = `${baseUrl}${serviceEndpoints.save}`;
|
||||
const headers = { 'Content-Type': 'text/plain' };
|
||||
const body = { config: yaml, timestamp: new Date() };
|
||||
const request = axios.post(endpoint, body, headers);
|
||||
// 3. Make the request, and handle response
|
||||
this.progress.start();
|
||||
request.then((response) => {
|
||||
this.saveSuccess = response.data.success || false;
|
||||
this.responseText = response.data.message;
|
||||
if (this.saveSuccess) {
|
||||
this.carefullyClearLocalStorage();
|
||||
this.showToast(this.$t('config-editor.success-msg-disk'), true);
|
||||
} else {
|
||||
this.showToast(this.$t('config-editor.error-msg-cannot-save'), false);
|
||||
}
|
||||
InfoHandler('Config has been written to disk succesfully', 'Config Update');
|
||||
this.progress.end();
|
||||
this.$store.commit(StoreKeys.SET_EDIT_MODE, false);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.saveSuccess = false;
|
||||
this.responseText = error;
|
||||
this.showToast(error, false);
|
||||
ErrorHandler(`Failed to save config. ${error}`);
|
||||
this.progress.end();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/media-queries.scss';
|
||||
|
||||
div.edit-mode-bottom-banner {
|
||||
position: fixed;
|
||||
display: grid;
|
||||
z-index: 5;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 0.25rem 0;
|
||||
border-top: 2px solid var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-background-darker);
|
||||
box-shadow: 0 -5px 7px var(--transparent-50);
|
||||
grid-template-columns: 45% 10% 45%;
|
||||
@include laptop-up { grid-template-columns: 40% 20% 40%; }
|
||||
@include monitor-up { grid-template-columns: 30% 40% 30%; }
|
||||
@include big-screen-up { grid-template-columns: 25% 50% 25%; }
|
||||
|
||||
/* Main sections */
|
||||
.edit-banner-section {
|
||||
padding: 0.5rem;
|
||||
height: 90%;
|
||||
/* Section sub-titles */
|
||||
p.section-sub-title {
|
||||
margin: 0;
|
||||
color: var(--interactive-editor-color);
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
/* Intro-text container */
|
||||
&.intro-container {
|
||||
p.edit-mode-intro {
|
||||
margin: 0;
|
||||
color: var(--interactive-editor-color);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
/* Button containers */
|
||||
&.edit-site-config-buttons,
|
||||
&.save-buttons-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
button {
|
||||
margin: 0.25rem;
|
||||
height: stretch;
|
||||
}
|
||||
p.section-sub-title {
|
||||
grid-column-start: span 2;
|
||||
}
|
||||
}
|
||||
&.save-buttons-container {
|
||||
grid-row-start: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile layout */
|
||||
@include tablet-down {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.edit-banner-section,
|
||||
.edit-banner-section.intro-container {
|
||||
max-width: 90%;
|
||||
width: 100%;
|
||||
margin: 0.2rem auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
/* Set colors for buttons */
|
||||
.edit-banner-section button {
|
||||
color: var(--interactive-editor-color);
|
||||
border-color: var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-background);
|
||||
&:hover {
|
||||
color: var(--interactive-editor-background);
|
||||
border-color: var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
src/components/InteractiveEditor/EditModeTopBanner.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="edit-mode-top-banner">
|
||||
<span>Edit Mode Enabled</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
div.edit-mode-top-banner {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 0.2rem 0;
|
||||
background: var(--interactive-editor-color);
|
||||
opacity: var(--dimming-factor);
|
||||
span {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
color: var(--interactive-editor-background);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
93
src/components/InteractiveEditor/EditPageInfo.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<modal
|
||||
:name="modalName" @closed="modalClosed"
|
||||
:resizable="true" width="50%" height="80%"
|
||||
classes="dashy-modal edit-page-info"
|
||||
>
|
||||
<div class="edit-page-info-inner">
|
||||
<h3>{{ $t('interactive-editor.menu.edit-page-info-btn') }}</h3>
|
||||
<FormSchema
|
||||
:schema="schema"
|
||||
v-model="formData"
|
||||
@submit.prevent="saveToState"
|
||||
class="page-info-form"
|
||||
name="pageInfoForm"
|
||||
>
|
||||
<Button type="submit">
|
||||
{{ $t('interactive-editor.menu.save-stage-btn') }}
|
||||
<SaveIcon />
|
||||
</Button>
|
||||
</FormSchema>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSchema from '@formschema/native';
|
||||
import DashySchema from '@/utils/ConfigSchema';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import { modalNames } from '@/utils/defaults';
|
||||
import Button from '@/components/FormElements/Button';
|
||||
import SaveIcon from '@/assets/interface-icons/save-config.svg';
|
||||
|
||||
export default {
|
||||
name: 'EditPageInfo',
|
||||
data() {
|
||||
return {
|
||||
formData: {},
|
||||
schema: DashySchema.properties.pageInfo,
|
||||
modalName: modalNames.EDIT_PAGE_INFO,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
FormSchema,
|
||||
Button,
|
||||
SaveIcon,
|
||||
},
|
||||
mounted() {
|
||||
this.formData = this.pageInfo;
|
||||
},
|
||||
computed: {
|
||||
pageInfo() {
|
||||
return this.$store.getters.pageInfo;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* When form submitteed, update VueX store with new pageInfo, and close modal */
|
||||
saveToState() {
|
||||
this.$store.commit(StoreKeys.SET_PAGE_INFO, this.formData);
|
||||
this.$modal.hide(this.modalName);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
this.$store.commit(StoreKeys.SET_EDIT_MODE, true);
|
||||
},
|
||||
/* Called when modal manually closed, updates state to allow searching again */
|
||||
modalClosed() {
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/style-helpers.scss';
|
||||
@import '@/styles/media-queries.scss';
|
||||
@import '@/styles/schema-editor.scss';
|
||||
|
||||
.edit-page-info-inner {
|
||||
padding: 1rem;
|
||||
background: var(--interactive-editor-background);
|
||||
color: var(--interactive-editor-color);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
@extend .scroll-bar;
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
.page-info-form {
|
||||
@extend .schema-form;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
129
src/components/InteractiveEditor/EditSection.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<modal
|
||||
:name="modalName" @closed="modalClosed"
|
||||
:resizable="true" width="50%" height="80%"
|
||||
classes="dashy-modal edit-section"
|
||||
>
|
||||
<div class="edit-section-inner">
|
||||
<h3>
|
||||
{{ $t(`interactive-editor.edit-section.${isAddNew ? 'add' : 'edit'}-section-title`) }}
|
||||
</h3>
|
||||
<FormSchema
|
||||
:schema="customSchema"
|
||||
v-model="sectionData"
|
||||
name="editSectionForm"
|
||||
class="edit-section-form"
|
||||
/>
|
||||
<SaveCancelButtons
|
||||
:saveClick="saveSection"
|
||||
:cancelClick="modalClosed"
|
||||
/>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSchema from '@formschema/native';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import DashySchema from '@/utils/ConfigSchema';
|
||||
import { modalNames } from '@/utils/defaults';
|
||||
import SaveCancelButtons from '@/components/InteractiveEditor/SaveCancelButtons';
|
||||
|
||||
export default {
|
||||
name: 'EditSection',
|
||||
props: {
|
||||
sectionIndex: Number,
|
||||
isAddNew: Boolean,
|
||||
},
|
||||
components: {
|
||||
SaveCancelButtons,
|
||||
FormSchema,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalName: modalNames.EDIT_SECTION,
|
||||
schema: DashySchema.properties.sections.items.properties,
|
||||
sectionData: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/* Make a custom schema object, using fields from ConfigSchema */
|
||||
customSchema() {
|
||||
const sectionSchema = this.schema;
|
||||
const displayDataSchema = this.schema.displayData.properties;
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: sectionSchema.name,
|
||||
icon: sectionSchema.icon,
|
||||
displayData: {
|
||||
title: '',
|
||||
description: '',
|
||||
type: 'object',
|
||||
properties: {
|
||||
sortBy: displayDataSchema.sortBy,
|
||||
rows: displayDataSchema.rows,
|
||||
cols: displayDataSchema.cols,
|
||||
collapsed: displayDataSchema.collapsed,
|
||||
hideForGuests: displayDataSchema.hideForGuests,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.sectionData = this.$store.getters.getSectionByIndex(this.sectionIndex);
|
||||
this.$modal.show(modalNames.EDIT_SECTION);
|
||||
},
|
||||
methods: {
|
||||
/* From the current index, return section data */
|
||||
getSectionFromState(index) {
|
||||
if (this.isAddNew) return {};
|
||||
return this.$store.getters.getSectionByIndex(index);
|
||||
},
|
||||
/* Clean up work, triggered when modal closed */
|
||||
modalClosed() {
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
this.$emit('closeEditSection');
|
||||
},
|
||||
/* Either update existing section, or insert new one, then close modal */
|
||||
saveSection() {
|
||||
const { sectionIndex, sectionData } = this;
|
||||
if (this.isAddNew) {
|
||||
this.$store.commit(StoreKeys.INSERT_SECTION, sectionData);
|
||||
} else {
|
||||
this.$store.commit(StoreKeys.UPDATE_SECTION, { sectionIndex, sectionData });
|
||||
}
|
||||
this.$store.commit(StoreKeys.SET_EDIT_MODE, true);
|
||||
this.$emit('closeEditSection');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/style-helpers.scss';
|
||||
@import '@/styles/media-queries.scss';
|
||||
@import '@/styles/schema-editor.scss';
|
||||
|
||||
.edit-section-inner {
|
||||
padding: 1rem;
|
||||
background: var(--interactive-editor-background);
|
||||
color: var(--interactive-editor-color);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
@extend .scroll-bar;
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
.edit-section-form {
|
||||
@extend .schema-form;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
.edit-section-save-btn {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
127
src/components/InteractiveEditor/ExportConfigMenu.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<modal
|
||||
:name="modalName"
|
||||
:resizable="true"
|
||||
width="50%"
|
||||
height="80%"
|
||||
classes="dashy-modal edit-item"
|
||||
@closed="modalClosed"
|
||||
>
|
||||
<div class="export-config-inner">
|
||||
<!-- Download and Copy to CLipboard Buttons -->
|
||||
<h3>{{ $t('interactive-editor.export.export-title') }}</h3>
|
||||
<div class="download-button-container">
|
||||
<Button :click="copyConfigToClipboard"
|
||||
v-tooltip="tooltip($t('interactive-editor.export.copy-clipboard-tooltip'))">
|
||||
{{ $t('interactive-editor.export.copy-clipboard-btn') }}
|
||||
<CopyConfigIcon />
|
||||
</Button>
|
||||
<Button :click="downloadConfig"
|
||||
v-tooltip="tooltip($t('interactive-editor.export.download-file-tooltip'))">
|
||||
{{ $t('interactive-editor.export.download-file-btn') }}
|
||||
<DownloadConfigIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- View Config in Tree Mode Section -->
|
||||
<h3>{{ $t('interactive-editor.export.view-title') }}</h3>
|
||||
<tree-view :data="config" class="config-tree-view" />
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JsYaml from 'js-yaml';
|
||||
import Button from '@/components/FormElements/Button';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import { modalNames } from '@/utils/defaults';
|
||||
import DownloadConfigIcon from '@/assets/interface-icons/config-download-file.svg';
|
||||
import CopyConfigIcon from '@/assets/interface-icons/interactive-editor-copy-clipboard.svg';
|
||||
import { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
|
||||
|
||||
export default {
|
||||
name: 'ExportConfigMenu',
|
||||
components: {
|
||||
Button,
|
||||
CopyConfigIcon,
|
||||
DownloadConfigIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalName: modalNames.EXPORT_CONFIG_MENU,
|
||||
};
|
||||
},
|
||||
props: {},
|
||||
computed: {
|
||||
config() {
|
||||
return this.$store.state.config;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
convertJsonToYaml() {
|
||||
return JsYaml.dump(this.config);
|
||||
},
|
||||
downloadConfig() {
|
||||
const filename = 'dashy_conf.yml';
|
||||
const config = this.convertJsonToYaml();
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('href', `data:text/plain;charset=utf-8, ${encodeURIComponent(config)}`);
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
InfoHandler('Config downloaded as YAML file', InfoKeys.EDITOR);
|
||||
},
|
||||
copyConfigToClipboard() {
|
||||
const config = this.convertJsonToYaml();
|
||||
navigator.clipboard.writeText(config);
|
||||
this.$toasted.show(this.$t('config.data-copied-msg'));
|
||||
InfoHandler('Config copied to clipboard', InfoKeys.EDITOR);
|
||||
},
|
||||
modalClosed() {
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
},
|
||||
tooltip(content) {
|
||||
return {
|
||||
content, trigger: 'hover focus', delay: 250, classes: 'in-modal-tt',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/style-helpers.scss';
|
||||
@import '@/styles/media-queries.scss';
|
||||
.tooltip { z-index: 99; }
|
||||
.export-config-inner {
|
||||
padding: 1rem;
|
||||
background: var(--interactive-editor-background);
|
||||
color: var(--interactive-editor-color);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
h3 {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.download-button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 0.5rem 1rem;
|
||||
border-bottom: 1px dashed var(--interactive-editor-color);
|
||||
button { margin: 0 1rem; }
|
||||
}
|
||||
.config-tree-view {
|
||||
padding: 0.5rem;
|
||||
font-family: var(--font-monospace);
|
||||
color: var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-background-darker);
|
||||
border-radius: var(--curve-factor);
|
||||
box-shadow: 0px 0px 3px var(--interactive-editor-color);
|
||||
margin-bottom: 1.5rem;
|
||||
span {
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
136
src/components/InteractiveEditor/MoveItemTo.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<modal
|
||||
:name="modalName" @closed="close"
|
||||
:resizable="true" width="40%" height="40%" classes="dashy-modal">
|
||||
<div class="move-menu-inner">
|
||||
<!-- Title and item ID -->
|
||||
<h3 class="move-title">Move or Copy Item</h3>
|
||||
<p class="item-id">Editing {{ itemId }}</p>
|
||||
<!-- Radio, for move or copy -->
|
||||
<Radio
|
||||
v-model="operation"
|
||||
:options="operationRadioOptions"
|
||||
label="Operation Type"
|
||||
:initialOption="operation"
|
||||
/>
|
||||
<!-- Select destionation section -->
|
||||
<Select
|
||||
v-model="selectedSection"
|
||||
:options="sectionList"
|
||||
:initialOption="selectedSection"
|
||||
label="Destination"
|
||||
/>
|
||||
<!-- Radio, for choosing append to beginning or end -->
|
||||
<Radio
|
||||
v-model="appendTo"
|
||||
:options="appendToRadioOptions"
|
||||
label="Append To"
|
||||
:initialOption="appendTo"
|
||||
/>
|
||||
<!-- Save and cancel buttons -->
|
||||
<SaveCancelButtons :saveClick="save" :cancelClick="close" />
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select from '@/components/FormElements/Select';
|
||||
import Radio from '@/components/FormElements/Radio';
|
||||
import SaveCancelButtons from '@/components/InteractiveEditor/SaveCancelButtons';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import { modalNames } from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
name: 'MoveItemTo',
|
||||
components: {
|
||||
Select,
|
||||
Radio,
|
||||
SaveCancelButtons,
|
||||
},
|
||||
props: {
|
||||
itemId: String, // Unique ID for item
|
||||
initialSection: String, // The current section
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedSection: '',
|
||||
operation: 'move',
|
||||
appendTo: 'end',
|
||||
modalName: `${modalNames.MOVE_ITEM_TO}-${this.itemId}`,
|
||||
operationRadioOptions: [
|
||||
{ label: 'Move', value: 'move' },
|
||||
{ label: 'Copy', value: 'copy' },
|
||||
],
|
||||
appendToRadioOptions: [
|
||||
{ label: 'Beginning', value: 'beginning' },
|
||||
{ label: 'End', value: 'end' },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sections() {
|
||||
return this.$store.getters.sections;
|
||||
},
|
||||
sectionList() {
|
||||
return this.sections.map((section) => section.name);
|
||||
},
|
||||
currentSection() {
|
||||
let sectionName = '';
|
||||
this.sections.forEach((section) => {
|
||||
section.items.forEach((item) => {
|
||||
if (item.id === this.itemId) sectionName = section.name;
|
||||
});
|
||||
});
|
||||
return sectionName;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.selectedSection = this.currentSection;
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
const item = this.$store.getters.getItemById(this.itemId);
|
||||
// Copy item to new section
|
||||
const copyPayload = { item, toSection: this.selectedSection, appendTo: this.appendTo };
|
||||
this.$store.commit(StoreKeys.COPY_ITEM, copyPayload);
|
||||
// Remove item from previous section
|
||||
if (this.operation === 'move') {
|
||||
const payload = { itemId: this.itemId, sectionName: this.currentSection };
|
||||
this.$store.commit(StoreKeys.REMOVE_ITEM, payload);
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
close() {
|
||||
this.$modal.hide(this.modalName);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.move-menu-inner {
|
||||
padding: 1rem;
|
||||
background: var(--interactive-editor-background);
|
||||
color: var(--interactive-editor-color);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
h3.move-title {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
p.item-id {
|
||||
font-size: 1rem;
|
||||
font-style: italic;
|
||||
margin: 0.25rem 0;
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin: 1.5rem auto;
|
||||
button {
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
61
src/components/InteractiveEditor/SaveCancelButtons.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="save-cancel-btn-container">
|
||||
<Button class="save-app-config-btn" :click="saveClick">
|
||||
{{ $t('interactive-editor.menu.save-stage-btn') }}
|
||||
<SaveIcon />
|
||||
</Button>
|
||||
<Button class="save-app-config-btn" :click="cancelClick">
|
||||
{{ $t('interactive-editor.menu.cancel-stage-btn') }}
|
||||
<CancelIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Button from '@/components/FormElements/Button';
|
||||
import SaveIcon from '@/assets/interface-icons/save-config.svg';
|
||||
import CancelIcon from '@/assets/interface-icons/config-close.svg';
|
||||
|
||||
export default {
|
||||
name: 'SaveCancelButton',
|
||||
props: {
|
||||
saveClick: Function,
|
||||
cancelClick: Function,
|
||||
},
|
||||
components: {
|
||||
Button,
|
||||
SaveIcon,
|
||||
CancelIcon,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.save-cancel-btn-container {
|
||||
display: flex;
|
||||
margin: 0.5rem 0;
|
||||
justify-content: center;
|
||||
border-top: 1px dashed var(--interactive-editor-color);
|
||||
button {
|
||||
margin: 1rem 0.5rem;
|
||||
color: var(--interactive-editor-color);
|
||||
border-color: var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-background);
|
||||
svg {
|
||||
border: none;
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
&:hover {
|
||||
color: var(--interactive-editor-background);
|
||||
border-color: var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-color);
|
||||
svg {
|
||||
background: var(--interactive-editor-color);
|
||||
path { fill: var(--interactive-editor-background); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,22 @@
|
||||
<template>
|
||||
<div :class="`collapsable ${checkSpanNum(cols, 'col')} ${checkSpanNum(rows, 'row')}`"
|
||||
<div
|
||||
:class="`collapsable ${rowColSpanClass} ${collapseClass}`"
|
||||
:style="`${color ? 'background: '+color : ''}; ${sanitizeCustomStyles(customStyles)};`"
|
||||
>
|
||||
<input
|
||||
:id="`collapsible-${uniqueKey}`"
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
:checked="getCollapseState()"
|
||||
@change="collapseChanged"
|
||||
tabIndex="-1"
|
||||
:id="sectionKey"
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
:checked="isExpanded"
|
||||
@change="collapseChanged"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<label :for="`collapsible-${uniqueKey}`" class="lbl-toggle" tabindex="-1">
|
||||
<label :for="sectionKey" class="lbl-toggle" tabindex="-1"
|
||||
@mouseup.right="openContextMenu" @contextmenu.prevent>
|
||||
<Icon v-if="icon" :icon="icon" size="small" :url="title" class="section-icon" />
|
||||
<h3>{{ title }}</h3>
|
||||
<EditModeIcon v-if="isEditMode" @click="openEditModal"
|
||||
v-tooltip="editTooltip()" class="edit-mode-item" />
|
||||
</label>
|
||||
<div class="collapsible-content">
|
||||
<div class="content-inner">
|
||||
@@ -26,26 +30,50 @@
|
||||
|
||||
import { localStorageKeys } from '@/utils/defaults';
|
||||
import Icon from '@/components/LinkItems/ItemIcon.vue';
|
||||
import EditModeIcon from '@/assets/interface-icons/interactive-editor-edit-mode.svg';
|
||||
|
||||
export default {
|
||||
name: 'CollapsableContainer',
|
||||
props: {
|
||||
uniqueKey: String,
|
||||
title: String,
|
||||
icon: String,
|
||||
collapsed: Boolean,
|
||||
cols: Number,
|
||||
rows: Number,
|
||||
color: String,
|
||||
customStyles: String,
|
||||
uniqueKey: String, // Generated unique ID
|
||||
title: String, // The section title
|
||||
icon: String, // An optional section icon
|
||||
collapsed: Boolean, // Optional override collapse state
|
||||
cols: Number, // Set section horizontal col span / width
|
||||
rows: Number, // Set section vertical row span / height
|
||||
color: String, // Optional color override
|
||||
customStyles: String, // Optional custom stylings
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
EditModeIcon,
|
||||
},
|
||||
computed: {
|
||||
isEditMode() {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
sectionKey() {
|
||||
if (this.isEditMode) return undefined;
|
||||
return `collapsible-${this.uniqueKey}`;
|
||||
},
|
||||
collapseClass() {
|
||||
return !this.isExpanded ? ' is-collapsed' : 'is-open';
|
||||
},
|
||||
rowColSpanClass() {
|
||||
const { rows, cols, checkSpanNum } = this;
|
||||
return `${checkSpanNum(cols, 'col')} ${checkSpanNum(rows, 'row')}`;
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
isExpanded: false,
|
||||
}),
|
||||
mounted() {
|
||||
this.isExpanded = this.getCollapseState();
|
||||
},
|
||||
methods: {
|
||||
/* Check that row & column span is valid, and not over the max */
|
||||
checkSpanNum(span, classPrefix) {
|
||||
const maxSpan = 4;
|
||||
const maxSpan = 5;
|
||||
let numSpan = /^\d*$/.test(span) ? parseInt(span, 10) : 1;
|
||||
numSpan = (numSpan > maxSpan) ? maxSpan : numSpan;
|
||||
return `${classPrefix}-${numSpan}`;
|
||||
@@ -54,28 +82,30 @@ export default {
|
||||
sanitizeCustomStyles(userCss) {
|
||||
return userCss ? userCss.replace(/[^a-zA-Z0-9- :;.]/g, '') : '';
|
||||
},
|
||||
/* If not already done, then add object structure to local storage */
|
||||
/* Returns local storage collapse state data, and if not yet set then initialized is */
|
||||
initialiseStorage() {
|
||||
const storageKey = localStorageKeys.COLLAPSE_STATE;
|
||||
/* Initialize function will create and set a blank object to storage */
|
||||
const initStorage = () => localStorage.setItem(
|
||||
localStorageKeys.COLLAPSE_STATE, JSON.stringify({}),
|
||||
);
|
||||
if (!localStorage[localStorageKeys.COLLAPSE_STATE]) initStorage(); // Initialise if not set
|
||||
try { // Check storage is valid JSON, and has not been corrupted
|
||||
JSON.parse(localStorage[localStorageKeys.COLLAPSE_STATE]);
|
||||
} catch {
|
||||
const initStorage = () => localStorage.setItem(storageKey, JSON.stringify({}));
|
||||
// If not yet set, then call initialize
|
||||
if (!localStorage[storageKey]) {
|
||||
initStorage();
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(localStorage[localStorageKeys.COLLAPSE_STATE]);
|
||||
// Otherwise, return value of local storage
|
||||
return JSON.parse(localStorage[storageKey]);
|
||||
},
|
||||
/* If specified by user, return conf collapse state, otherwise check local storage */
|
||||
getCollapseState() {
|
||||
const collapseStateObject = this.initialiseStorage();
|
||||
let collapseState = !this.collapsed;
|
||||
if (this.collapsed !== undefined) return !this.collapsed; // Check users config
|
||||
const collapseStateObject = this.initialiseStorage(); // Check local storage
|
||||
if (collapseStateObject[this.uniqueKey] !== undefined) {
|
||||
collapseState = collapseStateObject[this.uniqueKey];
|
||||
return collapseStateObject[this.uniqueKey];
|
||||
}
|
||||
return collapseState;
|
||||
// Nothing specified, return Open
|
||||
return true;
|
||||
},
|
||||
/* When section collapsed, update local storage, to remember for next time */
|
||||
setCollapseState(id, newState) {
|
||||
// Get the current localstorage collapse state object
|
||||
const collapseState = JSON.parse(localStorage[localStorageKeys.COLLAPSE_STATE]);
|
||||
@@ -84,9 +114,23 @@ export default {
|
||||
// Stringify, and set the new object into local storage
|
||||
localStorage.setItem(localStorageKeys.COLLAPSE_STATE, JSON.stringify(collapseState));
|
||||
},
|
||||
/* Called when collapse state changes, trigger local storage update if needed */
|
||||
collapseChanged(whatChanged) {
|
||||
this.initialiseStorage();
|
||||
this.setCollapseState(this.uniqueKey.toString(), whatChanged.srcElement.checked);
|
||||
this.isExpanded = whatChanged.srcElement.checked;
|
||||
if (this.collapseState === undefined) { // Only run, if user hasn't manually set prop
|
||||
this.initialiseStorage();
|
||||
this.setCollapseState(this.uniqueKey.toString(), this.isExpanded);
|
||||
}
|
||||
},
|
||||
openEditModal() {
|
||||
this.$emit('openEditSection');
|
||||
},
|
||||
openContextMenu(e) {
|
||||
this.$emit('openContextMenu', e);
|
||||
},
|
||||
editTooltip() {
|
||||
const content = this.$t('interactive-editor.edit-section.edit-tooltip');
|
||||
return { content, trigger: 'hover focus', delay: { show: 100, hide: 0 } };
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -110,22 +154,26 @@ export default {
|
||||
&.row-2 { grid-row-start: span 2; }
|
||||
&.row-3 { grid-row-start: span 3; }
|
||||
&.row-4 { grid-row-start: span 4; }
|
||||
&.row-5 { grid-row-start: span 5; }
|
||||
|
||||
grid-column-start: span 1;
|
||||
@include tablet-up {
|
||||
&.col-2 { grid-column-start: span 2; }
|
||||
&.col-3 { grid-column-start: span 2; }
|
||||
&.col-4 { grid-column-start: span 2; }
|
||||
&.col-5 { grid-column-start: span 2; }
|
||||
}
|
||||
@include laptop-up {
|
||||
&.col-2 { grid-column-start: span 2; }
|
||||
&.col-3 { grid-column-start: span 3; }
|
||||
&.col-4 { grid-column-start: span 3; }
|
||||
&.col-5 { grid-column-start: span 3; }
|
||||
}
|
||||
@include monitor-up {
|
||||
&.col-2 { grid-column-start: span 2; }
|
||||
&.col-3 { grid-column-start: span 3; }
|
||||
&.col-4 { grid-column-start: span 4; }
|
||||
&.col-5 { grid-column-start: span 5; }
|
||||
}
|
||||
|
||||
.wrap-collabsible {
|
||||
@@ -144,7 +192,7 @@ export default {
|
||||
border-radius: var(--curve-factor);
|
||||
transition: all 0.25s ease-out;
|
||||
text-align: left;
|
||||
color: var(--item-group-heading-text-color); //var(--item-group-background);
|
||||
color: var(--item-group-heading-text-color);
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -196,5 +244,13 @@ export default {
|
||||
.collapsible-content .content-inner {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.edit-mode-item {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
float: right;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Keys from '@/utils/StoreMutations';
|
||||
|
||||
export default {
|
||||
name: 'IframeModal',
|
||||
props: {
|
||||
@@ -21,13 +23,13 @@ export default {
|
||||
show(url) {
|
||||
this.url = url;
|
||||
this.$modal.show(this.name);
|
||||
this.$emit('modalChanged', true);
|
||||
this.$store.commit(Keys.SET_MODAL_OPEN, true);
|
||||
},
|
||||
hide() {
|
||||
this.$modal.hide(this.name);
|
||||
},
|
||||
modalClosed() {
|
||||
this.$emit('modalChanged', false);
|
||||
this.$store.commit(Keys.SET_MODAL_OPEN, false);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template ref="container">
|
||||
<div class="item-wrapper">
|
||||
<div :class="`item-wrapper wrap-size-${itemSize}`">
|
||||
<a @click="itemOpened"
|
||||
@mouseup.right="openContextMenu"
|
||||
@contextmenu.prevent
|
||||
:href="(target !== 'modal' && target !== 'workspace') ? url : '#'"
|
||||
:target="target === 'newtab' ? '_blank' : ''"
|
||||
:class="`item ${!icon? 'short': ''} size-${itemSize}`"
|
||||
:href="hyperLinkHref"
|
||||
:target="anchorTarget"
|
||||
:class="`item ${makeClassList}`"
|
||||
v-tooltip="getTooltipOptions()"
|
||||
rel="noopener noreferrer" tabindex="0"
|
||||
:id="`link-${id}`"
|
||||
@@ -21,7 +21,7 @@
|
||||
v-bind:style="customStyles" class="bounce" />
|
||||
<!-- Small icon, showing opening method on hover -->
|
||||
<ItemOpenMethodIcon class="opening-method-icon" :isSmall="!icon || itemSize === 'small'"
|
||||
:openingMethod="target" position="bottom right"
|
||||
:openingMethod="accumulatedTarget" position="bottom right"
|
||||
:hotkey="hotkey" />
|
||||
<!-- Status indicator dot (if enabled) showing weather srevice is availible -->
|
||||
<StatusIndicator
|
||||
@@ -30,15 +30,26 @@
|
||||
:statusSuccess="statusResponse ? statusResponse.successStatus : undefined"
|
||||
:statusText="statusResponse ? statusResponse.message : undefined"
|
||||
/>
|
||||
<!-- Edit icon (displayed only when in edit mode) -->
|
||||
<EditModeIcon v-if="isEditMode" class="edit-mode-item" @click="openItemSettings()" />
|
||||
</a>
|
||||
<!-- Right-click context menu -->
|
||||
<ContextMenu
|
||||
:show="contextMenuOpen"
|
||||
:show="contextMenuOpen && !isAddNew"
|
||||
v-click-outside="closeContextMenu"
|
||||
:posX="contextPos.posX"
|
||||
:posY="contextPos.posY"
|
||||
:id="`context-menu-${id}`"
|
||||
@contextItemClick="contextItemClick"
|
||||
@launchItem="launchItem"
|
||||
@openItemSettings="openItemSettings"
|
||||
@openMoveItemMenu="openMoveItemMenu"
|
||||
@openDeleteItem="openDeleteItem"
|
||||
/>
|
||||
<!-- Edit and move item menu modals -->
|
||||
<MoveItemTo v-if="isEditMode" :itemId="id" />
|
||||
<EditItem v-if="editMenuOpen" :itemId="id"
|
||||
@closeEditMenu="closeEditMenu"
|
||||
:isNew="isAddNew" :parentSectionTitle="parentSectionTitle" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -48,12 +59,21 @@ import router from '@/router';
|
||||
import Icon from '@/components/LinkItems/ItemIcon.vue';
|
||||
import ItemOpenMethodIcon from '@/components/LinkItems/ItemOpenMethodIcon';
|
||||
import StatusIndicator from '@/components/LinkItems/StatusIndicator';
|
||||
import ContextMenu from '@/components/LinkItems/ContextMenu';
|
||||
import { localStorageKeys, serviceEndpoints } from '@/utils/defaults';
|
||||
import EditItem from '@/components/InteractiveEditor/EditItem';
|
||||
import MoveItemTo from '@/components/InteractiveEditor/MoveItemTo';
|
||||
import ContextMenu from '@/components/LinkItems/ItemContextMenu';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import { targetValidator } from '@/utils/ConfigHelpers';
|
||||
import EditModeIcon from '@/assets/interface-icons/interactive-editor-edit-mode.svg';
|
||||
import {
|
||||
localStorageKeys,
|
||||
serviceEndpoints,
|
||||
modalNames,
|
||||
openingMethod as defaultOpeningMethod,
|
||||
} from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
name: 'Item',
|
||||
inject: ['config'],
|
||||
props: {
|
||||
id: String, // The unique ID of a tile (e.g. 001)
|
||||
title: String, // The main text of tile, required
|
||||
@@ -67,15 +87,63 @@ export default {
|
||||
hotkey: Number, // Shortcut for quickly launching app
|
||||
target: { // Where resource will open, either 'newtab', 'sametab' or 'modal'
|
||||
type: String,
|
||||
default: 'newtab',
|
||||
validator: (value) => ['newtab', 'sametab', 'modal', 'workspace'].indexOf(value) !== -1,
|
||||
validator: targetValidator,
|
||||
},
|
||||
itemSize: String, // Item size: small | medium | large
|
||||
enableStatusCheck: Boolean, // Should run status checks
|
||||
statusCheckHeaders: Object, // Custom status check headers
|
||||
statusCheckUrl: String, // Custom URL for status check endpoint
|
||||
statusCheckInterval: Number, // Num seconds beteween repeating checks
|
||||
statusCheckAllowInsecure: Boolean, // Status check ignore SSL certs
|
||||
parentSectionTitle: String, // Title of parent section (for add new)
|
||||
isAddNew: Boolean, // Only set if 'fake' item used as Add New button
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
ItemOpenMethodIcon,
|
||||
StatusIndicator,
|
||||
ContextMenu,
|
||||
MoveItemTo,
|
||||
EditItem,
|
||||
EditModeIcon,
|
||||
},
|
||||
computed: {
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
isEditMode() {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
accumulatedTarget() {
|
||||
return this.target || this.appConfig.defaultOpeningMethod || defaultOpeningMethod;
|
||||
},
|
||||
/* Based on item props, adjust class names */
|
||||
makeClassList() {
|
||||
const {
|
||||
icon, itemSize, isAddNew, isEditMode,
|
||||
} = this;
|
||||
return `size-${itemSize} ${!icon ? 'short' : ''} `
|
||||
+ `${isAddNew ? 'add-new' : ''} ${isEditMode ? 'is-edit-mode' : ''}`;
|
||||
},
|
||||
/* Convert config target value, into HTML anchor target attribute */
|
||||
anchorTarget() {
|
||||
if (this.isEditMode) return '_self';
|
||||
const target = this.accumulatedTarget;
|
||||
switch (target) {
|
||||
case 'sametab': return '_self';
|
||||
case 'newtab': return '_blank';
|
||||
case 'parent': return '_parent';
|
||||
case 'top': return '_top';
|
||||
default: return undefined;
|
||||
}
|
||||
},
|
||||
/* Get href for anchor, if not in edit mode, or opening in modal/ workspace */
|
||||
hyperLinkHref() {
|
||||
const nothing = '#';
|
||||
if (this.isEditMode) return nothing;
|
||||
const noAnchorNeeded = ['modal', 'workspace'];
|
||||
return noAnchorNeeded.includes(this.accumulatedTarget) ? nothing : this.url;
|
||||
},
|
||||
itemSize: String,
|
||||
enableStatusCheck: Boolean,
|
||||
statusCheckHeaders: Object,
|
||||
statusCheckUrl: String,
|
||||
statusCheckInterval: Number,
|
||||
statusCheckAllowInsecure: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -90,27 +158,27 @@ export default {
|
||||
posX: undefined,
|
||||
posY: undefined,
|
||||
},
|
||||
editMenuOpen: false,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
ItemOpenMethodIcon,
|
||||
StatusIndicator,
|
||||
ContextMenu,
|
||||
},
|
||||
methods: {
|
||||
/* Called when an item is clicked, manages the opening of modal & resets the search field */
|
||||
itemOpened(e) {
|
||||
if (e.altKey || this.target === 'modal') {
|
||||
if (this.isEditMode) {
|
||||
// If in edit mode, open settings, and don't launch app
|
||||
this.openItemSettings();
|
||||
return;
|
||||
}
|
||||
if (e.altKey || this.accumulatedTarget === 'modal') {
|
||||
e.preventDefault();
|
||||
this.$emit('triggerModal', this.url);
|
||||
} else if (this.target === 'workspace') {
|
||||
} else if (this.accumulatedTarget === 'workspace') {
|
||||
router.push({ name: 'workspace', query: { url: this.url } });
|
||||
} else {
|
||||
this.$emit('itemClicked');
|
||||
}
|
||||
// Update the most/ last used ledger, for smart-sorting
|
||||
if (!this.config.appConfig.disableSmartSort) {
|
||||
if (!this.appConfig.disableSmartSort) {
|
||||
this.incrementMostUsedCount(this.id);
|
||||
this.incrementLastUsedCount(this.id);
|
||||
}
|
||||
@@ -137,22 +205,27 @@ export default {
|
||||
const providerText = this.provider ? `<b>Provider</b>: ${this.provider}` : '';
|
||||
const lb1 = description && providerText ? '<br>' : '';
|
||||
const hotkeyText = this.hotkey ? `<br>Press '${this.hotkey}' to launch` : '';
|
||||
const tooltipText = providerText + lb1 + description + hotkeyText;
|
||||
const editText = this.$t('interactive-editor.edit-section.edit-tooltip');
|
||||
return {
|
||||
content: providerText + lb1 + description + hotkeyText,
|
||||
content: (this.isEditMode ? editText : tooltipText),
|
||||
trigger: 'hover focus',
|
||||
hideOnTargetClick: true,
|
||||
html: true,
|
||||
placement: this.statusResponse ? 'left' : 'auto',
|
||||
delay: { show: 600, hide: 200 },
|
||||
classes: 'item-description-tooltip',
|
||||
classes: `item-description-tooltip tooltip-is-${this.itemSize}`,
|
||||
};
|
||||
},
|
||||
/* Used by certain themes, which display an icon with animated CSS */
|
||||
/* Used by certain themes (material), to show animated CSS icon */
|
||||
getUnicodeOpeningIcon() {
|
||||
switch (this.target) {
|
||||
switch (this.accumulatedTarget) {
|
||||
case 'newtab': return '"\\f360"';
|
||||
case 'sametab': return '"\\f24d"';
|
||||
case 'parent': return '"\\f3bf"';
|
||||
case 'top': return '"\\f102"';
|
||||
case 'modal': return '"\\f2d0"';
|
||||
case 'workspace': return '"\\f0b1"';
|
||||
default: return '"\\f054"';
|
||||
}
|
||||
},
|
||||
@@ -190,7 +263,7 @@ export default {
|
||||
});
|
||||
},
|
||||
/* Handle navigation options from the context menu */
|
||||
contextItemClick(method) {
|
||||
launchItem(method) {
|
||||
const { url } = this;
|
||||
this.contextMenuOpen = false;
|
||||
switch (method) {
|
||||
@@ -209,6 +282,19 @@ export default {
|
||||
default: window.open(url, '_blank');
|
||||
}
|
||||
},
|
||||
/* Open the Edit Item moal form */
|
||||
openItemSettings() {
|
||||
this.editMenuOpen = true;
|
||||
this.contextMenuOpen = false;
|
||||
this.$modal.show(modalNames.EDIT_ITEM);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, true);
|
||||
},
|
||||
/* Ensure conditional is updated, once menu closed */
|
||||
closeEditMenu() {
|
||||
this.editMenuOpen = false;
|
||||
this.$modal.hide(modalNames.EDIT_ITEM);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
},
|
||||
/* Used for smart-sort when sorting items by most used apps */
|
||||
incrementMostUsedCount(itemId) {
|
||||
const mostUsed = JSON.parse(localStorage.getItem(localStorageKeys.MOST_USED) || '{}');
|
||||
@@ -223,6 +309,19 @@ export default {
|
||||
lastUsed[itemId] = new Date().getTime();
|
||||
localStorage.setItem(localStorageKeys.LAST_USED, JSON.stringify(lastUsed));
|
||||
},
|
||||
/* Open the modal for moving/ copying item to other section */
|
||||
openMoveItemMenu() {
|
||||
this.$modal.show(`${modalNames.MOVE_ITEM_TO}-${this.id}`);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, true);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
/* Deletes the current item from the state */
|
||||
openDeleteItem() {
|
||||
const parentSection = this.$store.getters.getParentSectionOfItem(this.id);
|
||||
const payload = { itemId: this.id, sectionName: parentSection.name };
|
||||
this.$store.commit(StoreKeys.REMOVE_ITEM, payload);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// If ststus checking is enabled, then check service status
|
||||
@@ -239,6 +338,10 @@ export default {
|
||||
|
||||
.item-wrapper {
|
||||
flex-grow: 1;
|
||||
flex-basis: 6rem;
|
||||
&.wrap-size-large {
|
||||
flex-basis: 12rem;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
@@ -261,29 +364,35 @@ export default {
|
||||
box-shadow: var(--item-hover-shadow);
|
||||
background: var(--item-background-hover);
|
||||
color: var(--item-text-color-hover);
|
||||
position: relative;
|
||||
.tile-title span.text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
// position: relative;
|
||||
// .tile-title span.text {
|
||||
// white-space: pre-wrap;
|
||||
// }
|
||||
}
|
||||
&:focus {
|
||||
outline: 2px solid var(--primary);
|
||||
}
|
||||
&.short {
|
||||
height: 18px;
|
||||
&.short:not(.size-large) {
|
||||
height: 2rem;
|
||||
}
|
||||
&.add-new {
|
||||
border: 2px dashed var(--primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Text in tile */
|
||||
.tile-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 120px;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
z-index: 2;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: keep-all;
|
||||
span.text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -323,6 +432,15 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit Mode Icon */
|
||||
.item .edit-mode-item {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
position: absolute;
|
||||
top: 0.2rem;
|
||||
right: 0.2rem;
|
||||
}
|
||||
|
||||
/* Specify layout for alternate sized icons */
|
||||
.item {
|
||||
/* Small Tile Specific Themes */
|
||||
@@ -333,6 +451,7 @@ export default {
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
padding-top: 4px;
|
||||
max-width: 14rem;
|
||||
div img, div svg.missing-image {
|
||||
width: 2rem;
|
||||
}
|
||||
@@ -340,7 +459,8 @@ export default {
|
||||
height: fit-content;
|
||||
min-height: 1.2rem;
|
||||
text-align: left;
|
||||
max-width:140px;
|
||||
max-width: 12rem;
|
||||
overflow: hidden;
|
||||
span.text {
|
||||
text-align: left;
|
||||
padding-left: 10%;
|
||||
@@ -392,11 +512,13 @@ export default {
|
||||
width: 100%;
|
||||
}
|
||||
p.description {
|
||||
display: block;
|
||||
margin: 0;
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
font-size: .9em;
|
||||
text-overflow: ellipsis;
|
||||
font-size: .9em;
|
||||
line-height: 1rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,13 +532,18 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
/* Adjust positioning of status indicator, when in edit mode */
|
||||
a.item.is-edit-mode {
|
||||
&.size-medium .status-indicator { top: 1rem; }
|
||||
&.size-small .status-indicator { right: 1rem; }
|
||||
&.size-large .status-indicator { top: 1.5rem; }
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<!-- An un-scoped style tag, since tooltip is outside this DOM tree -->
|
||||
<style lang="scss">
|
||||
|
||||
.disabled-link {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
156
src/components/LinkItems/ItemContextMenu.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<transition name="slide">
|
||||
<div class="context-menu" v-if="show && !isMenuDisabled"
|
||||
:style="posX && posY ? `top:${posY}px;left:${posX}px;` : ''">
|
||||
<!-- Open Options -->
|
||||
<ul class="menu-section">
|
||||
<li class="section-title">
|
||||
{{ $t('context-menus.item.open-section-title') }}
|
||||
</li>
|
||||
<li @click="launch('sametab')">
|
||||
<SameTabOpenIcon />
|
||||
<span>{{ $t('context-menus.item.sametab') }}</span>
|
||||
</li>
|
||||
<li @click="launch('newtab')">
|
||||
<NewTabOpenIcon />
|
||||
<span>{{ $t('context-menus.item.newtab') }}</span>
|
||||
</li>
|
||||
<li @click="launch('modal')">
|
||||
<IframeOpenIcon />
|
||||
<span>{{ $t('context-menus.item.modal') }}</span>
|
||||
</li>
|
||||
<li @click="launch('workspace')">
|
||||
<WorkspaceOpenIcon />
|
||||
<span>{{ $t('context-menus.item.workspace') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Edit Options -->
|
||||
<ul class="menu-section">
|
||||
<li class="section-title">
|
||||
{{ $t('context-menus.item.options-section-title') }}
|
||||
</li>
|
||||
<li @click="openSettings()">
|
||||
<EditIcon />
|
||||
<span>{{ $t('context-menus.item.edit-item') }}</span>
|
||||
</li>
|
||||
<li v-if="isEditMode" @click="openMoveMenu()">
|
||||
<MoveIcon />
|
||||
<span>{{ $t('context-menus.item.move-item') }}</span>
|
||||
</li>
|
||||
<li v-if="isEditMode" @click="openDeleteItem()">
|
||||
<BinIcon />
|
||||
<span>{{ $t('context-menus.item.remove-item') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Import icons for each element
|
||||
import EditIcon from '@/assets/interface-icons/config-edit-json.svg';
|
||||
import BinIcon from '@/assets/interface-icons/interactive-editor-remove.svg';
|
||||
import MoveIcon from '@/assets/interface-icons/interactive-editor-move-to.svg';
|
||||
import SameTabOpenIcon from '@/assets/interface-icons/open-current-tab.svg';
|
||||
import NewTabOpenIcon from '@/assets/interface-icons/open-new-tab.svg';
|
||||
import IframeOpenIcon from '@/assets/interface-icons/open-iframe.svg';
|
||||
import WorkspaceOpenIcon from '@/assets/interface-icons/open-workspace.svg';
|
||||
|
||||
export default {
|
||||
name: 'ContextMenu',
|
||||
components: {
|
||||
EditIcon,
|
||||
MoveIcon,
|
||||
BinIcon,
|
||||
SameTabOpenIcon,
|
||||
NewTabOpenIcon,
|
||||
IframeOpenIcon,
|
||||
WorkspaceOpenIcon,
|
||||
},
|
||||
props: {
|
||||
posX: Number, // The X coordinate for positioning
|
||||
posY: Number, // The Y coordinate for positioning
|
||||
show: Boolean, // Should show or hide the menu
|
||||
},
|
||||
computed: {
|
||||
isMenuDisabled() {
|
||||
return !!this.$store.getters.appConfig.disableContextMenu;
|
||||
},
|
||||
isEditMode() {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Called on item click, emits an event up to Item */
|
||||
/* in order to launch the current app to a given target */
|
||||
launch(target) {
|
||||
this.$emit('launchItem', target);
|
||||
},
|
||||
openSettings() {
|
||||
this.$emit('openItemSettings');
|
||||
},
|
||||
openMoveMenu() {
|
||||
this.$emit('openMoveItemMenu');
|
||||
},
|
||||
openDeleteItem() {
|
||||
this.$emit('openDeleteItem');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
div.context-menu {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 8;
|
||||
background: var(--context-menu-background);
|
||||
color: var(--context-menu-color);
|
||||
border: 1px solid var(--context-menu-secondary-color);
|
||||
border-radius: var(--curve-factor);
|
||||
box-shadow: var(--context-menu-shadow);
|
||||
opacity: 0.98;
|
||||
|
||||
ul.menu-section {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--context-menu-color);
|
||||
}
|
||||
li {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 1rem;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--context-menu-secondary-color);
|
||||
}
|
||||
&:hover:not(.section-title) {
|
||||
background: var(--context-menu-secondary-color);
|
||||
}
|
||||
&.section-title {
|
||||
cursor: default;
|
||||
font-weight: bold;
|
||||
justify-content: center;
|
||||
}
|
||||
svg {
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
path { fill: currentColor; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define enter and leave transitions
|
||||
.slide-enter-active { animation: slide-in .1s; }
|
||||
.slide-leave-active { animation: slide-in .1s reverse; }
|
||||
@keyframes slide-in {
|
||||
0% { transform: scaleY(0.5) scaleX(0.8) translateY(-50px); }
|
||||
100% { transform: scaleY(1) translateY(0) translateY(0); }
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="item-icon">
|
||||
<div :class="`item-icon wrapper-${size}`">
|
||||
<!-- Font-Awesome Icon -->
|
||||
<i v-if="iconType === 'font-awesome'" :class="`${icon} ${size}`" ></i>
|
||||
<!-- Emoji Icon -->
|
||||
@@ -23,13 +23,13 @@
|
||||
import simpleIcons from 'simple-icons';
|
||||
import BrokenImage from '@/assets/interface-icons/broken-icon.svg';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import { faviconApi as defaultFaviconApi, faviconApiEndpoints, iconCdns } from '@/utils/defaults';
|
||||
import EmojiUnicodeRegex from '@/utils/EmojiUnicodeRegex';
|
||||
import emojiLookup from '@/utils/emojis.json';
|
||||
import { faviconApi as defaultFaviconApi, faviconApiEndpoints, iconCdns } from '@/utils/defaults';
|
||||
import { asciiHash } from '@/utils/MiscHelpers';
|
||||
|
||||
export default {
|
||||
name: 'Icon',
|
||||
inject: ['config'],
|
||||
props: {
|
||||
icon: String, // Path to icon asset
|
||||
url: String, // Used for fetching the favicon
|
||||
@@ -39,18 +39,24 @@ export default {
|
||||
BrokenImage,
|
||||
},
|
||||
computed: {
|
||||
/* Get appConfig from store */
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
/* Determines the type of icon */
|
||||
iconType: function iconType() {
|
||||
return this.determineImageType(this.icon);
|
||||
},
|
||||
/* Gets the icon path, dependent on icon type */
|
||||
iconPath: function iconPath() {
|
||||
if (this.broken) return this.getFallbackIcon();
|
||||
return this.getIconPath(this.icon, this.url);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
broken: false, // If true, was unable to resolve icon
|
||||
attemptedFallback: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -90,12 +96,12 @@ export default {
|
||||
},
|
||||
/* Get favicon URL, for items which use the favicon as their icon */
|
||||
getFavicon(fullUrl, specificApi) {
|
||||
if (this.shouldUseDefaultFavicon(fullUrl)) { // Check if we should use local icon
|
||||
const faviconApi = specificApi || this.appConfig.faviconApi || defaultFaviconApi;
|
||||
if (this.shouldUseDefaultFavicon(fullUrl) || faviconApi === 'local') { // Check if we should use local icon
|
||||
const urlParts = fullUrl.split('/');
|
||||
if (urlParts.length >= 2) return `${urlParts[0]}/${urlParts[1]}/${urlParts[2]}/${iconCdns.faviconName}`;
|
||||
} else if (fullUrl.includes('http')) { // Service is running publicly
|
||||
const host = this.getHostName(fullUrl);
|
||||
const faviconApi = specificApi || this.config.appConfig.faviconApi || defaultFaviconApi;
|
||||
const endpoint = faviconApiEndpoints[faviconApi];
|
||||
return endpoint.replace('$URL', host);
|
||||
}
|
||||
@@ -119,15 +125,16 @@ export default {
|
||||
/* or if user prefers local favicon, then return true */
|
||||
shouldUseDefaultFavicon(fullUrl) {
|
||||
const isLocalIP = /(127\.)|(192\.168\.)|(10\.)|(172\.1[6-9]\.)|(172\.2[0-9]\.)|(172\.3[0-1]\.)|(::1$)|([fF][cCdD])|(localhost)/;
|
||||
return (isLocalIP.test(fullUrl) || this.config.appConfig.faviconApi === 'local');
|
||||
return (isLocalIP.test(fullUrl) || this.appConfig.faviconApi === 'local');
|
||||
},
|
||||
/* Fetches the path of local images, from Docker container */
|
||||
getLocalImagePath(img) {
|
||||
return `${iconCdns.localPath}/${img}`;
|
||||
},
|
||||
/* Formats the URL for fetching the generative icons */
|
||||
getGenerativeIcon(url) {
|
||||
return `${iconCdns.generative}/${this.getHostName(url)}.svg`;
|
||||
getGenerativeIcon(url, cdn) {
|
||||
const host = encodeURI(url) || Math.random().toString();
|
||||
return (cdn || iconCdns.generative).replace('{icon}', asciiHash(host));
|
||||
},
|
||||
/* Returns the SVG path content */
|
||||
getSimpleIcon(img) {
|
||||
@@ -135,6 +142,11 @@ export default {
|
||||
const icon = simpleIcons.Get(imageName);
|
||||
return icon.path;
|
||||
},
|
||||
/* Gets home-lab icon from GitHub */
|
||||
getHomeLabIcon(img) {
|
||||
const imageName = img.replace('hl-', '').toLocaleLowerCase();
|
||||
return iconCdns.homeLabIcons.replace('{icon}', imageName);
|
||||
},
|
||||
/* Checks if the icon is from a local image, remote URL, SVG or font-awesome */
|
||||
getIconPath(img, url) {
|
||||
switch (this.determineImageType(img)) {
|
||||
@@ -145,6 +157,7 @@ export default {
|
||||
case 'generative': return this.getGenerativeIcon(url);
|
||||
case 'mdi': return img; // Material design icons
|
||||
case 'simple-icons': return this.getSimpleIcon(img);
|
||||
case 'home-lab-icons': return this.getHomeLabIcon(img);
|
||||
case 'svg': return img; // Local SVG icon
|
||||
case 'emoji': return img; // Emoji/ unicode
|
||||
default: return '';
|
||||
@@ -159,6 +172,7 @@ export default {
|
||||
else if (img.includes('fa-')) imgType = 'font-awesome';
|
||||
else if (img.includes('mdi-')) imgType = 'mdi';
|
||||
else if (img.includes('si-')) imgType = 'si';
|
||||
else if (img.includes('hl-')) imgType = 'home-lab-icons';
|
||||
else if (img.includes('favicon-')) imgType = 'custom-favicon';
|
||||
else if (img === 'favicon') imgType = 'favicon';
|
||||
else if (img === 'generative') imgType = 'generative';
|
||||
@@ -175,11 +189,40 @@ export default {
|
||||
this.broken = true;
|
||||
ErrorHandler(`The path to '${this.icon}' could not be resolved`);
|
||||
},
|
||||
/* Called when initial icon has resulted in 404. Attempts to find new icon */
|
||||
getFallbackIcon() {
|
||||
if (this.attemptedFallback) return undefined; // If this is second attempt, then give up
|
||||
const { iconType } = this;
|
||||
const markAsSttempted = () => {
|
||||
this.broken = false;
|
||||
this.attemptedFallback = true;
|
||||
};
|
||||
if (iconType.includes('favicon')) { // Specify fallback for favicon-based icons
|
||||
markAsSttempted();
|
||||
return this.getFavicon(this.url, 'local');
|
||||
} else if (iconType === 'generative') {
|
||||
markAsSttempted();
|
||||
return this.getGenerativeIcon(this.url, iconCdns.generativeFallback);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
/* Icon wraper */
|
||||
.item-icon {
|
||||
&.wrapper-medium {
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
&.wrapper-large {
|
||||
min-width: 3.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Default Image Icon */
|
||||
.tile-icon {
|
||||
min-width: 1rem;
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<SameTabOpenIcon v-else-if="openingMethod === 'sametab'" />
|
||||
<IframeOpenIcon v-else-if="openingMethod === 'modal'" />
|
||||
<WorkspaceOpenIcon v-else-if="openingMethod === 'workspace'" />
|
||||
<ParentOpenIcon v-else-if="openingMethod === 'parent'" />
|
||||
<TopOpenIcon v-else-if="openingMethod === 'top'" />
|
||||
<UnknownIcon v-else />
|
||||
</div>
|
||||
<div v-if="hotkey" :class="`hotkey-denominator ${makeClass(position, isSmall, isTransparent)}`">
|
||||
{{ hotkey }}
|
||||
@@ -20,11 +23,14 @@ import NewTabOpenIcon from '@/assets/interface-icons/open-new-tab.svg';
|
||||
import SameTabOpenIcon from '@/assets/interface-icons/open-current-tab.svg';
|
||||
import IframeOpenIcon from '@/assets/interface-icons/open-iframe.svg';
|
||||
import WorkspaceOpenIcon from '@/assets/interface-icons/open-workspace.svg';
|
||||
import ParentOpenIcon from '@/assets/interface-icons/open-parent.svg';
|
||||
import TopOpenIcon from '@/assets/interface-icons/open-top.svg';
|
||||
import UnknownIcon from '@/assets/interface-icons/unknown-icon.svg';
|
||||
|
||||
export default {
|
||||
name: 'ItemOpenMethodIcon',
|
||||
props: {
|
||||
openingMethod: String, // newtab | sametab | modal | workspace
|
||||
openingMethod: String, // newtab | sametab | parent | top | modal | workspace
|
||||
isSmall: Boolean, // If true, will apply small class
|
||||
position: String, // Position classes: top, bottom, left, right
|
||||
isTransparent: Boolean, // If true, will apply opacity
|
||||
@@ -44,6 +50,9 @@ export default {
|
||||
SameTabOpenIcon,
|
||||
IframeOpenIcon,
|
||||
WorkspaceOpenIcon,
|
||||
ParentOpenIcon,
|
||||
TopOpenIcon,
|
||||
UnknownIcon,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -8,18 +8,22 @@
|
||||
:rows="displayData.rows"
|
||||
:color="displayData.color"
|
||||
:customStyles="displayData.customStyles"
|
||||
@openEditSection="openEditSection"
|
||||
@openContextMenu="openContextMenu"
|
||||
>
|
||||
<div v-if="!items || items.length < 1" class="no-items">
|
||||
<!-- If no items, show message -->
|
||||
<div v-if="(!items || items.length < 1) && !isEditMode" class="no-items">
|
||||
No Items to Show Yet
|
||||
</div>
|
||||
<!-- Item Container -->
|
||||
<div v-else
|
||||
:class="`there-are-items ${isGridLayout? 'item-group-grid': ''}`"
|
||||
:style="gridStyle"
|
||||
>
|
||||
:class="`there-are-items ${isGridLayout? 'item-group-grid': ''} inner-size-${itemSize}`"
|
||||
:style="gridStyle" :id="`section-${groupId}`"
|
||||
> <!-- Show for each item -->
|
||||
<Item
|
||||
v-for="(item) in sortedItems"
|
||||
:id="makeId(title, item.title)"
|
||||
:key="makeId(title, item.title)"
|
||||
:id="item.id"
|
||||
:key="item.id"
|
||||
:url="item.url"
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
@@ -32,33 +36,72 @@
|
||||
:itemSize="newItemSize"
|
||||
:hotkey="item.hotkey"
|
||||
:provider="item.provider"
|
||||
:parentSectionTitle="title"
|
||||
:enableStatusCheck="shouldEnableStatusCheck(item.statusCheck)"
|
||||
:statusCheckInterval="getStatusCheckInterval()"
|
||||
:statusCheckAllowInsecure="item.statusCheckAllowInsecure"
|
||||
@itemClicked="$emit('itemClicked')"
|
||||
@triggerModal="triggerModal"
|
||||
:isAddNew="false"
|
||||
/>
|
||||
<!-- When in edit mode, show additional item, for Add New item -->
|
||||
<Item v-if="isEditMode"
|
||||
:isAddNew="true"
|
||||
:parentSectionTitle="title"
|
||||
icon=":heavy_plus_sign:"
|
||||
id="add-new"
|
||||
title="Add New Item"
|
||||
description="Click to add new item"
|
||||
key="add-new"
|
||||
class="add-new-item"
|
||||
:itemSize="newItemSize"
|
||||
/>
|
||||
<div ref="modalContainer"></div>
|
||||
</div>
|
||||
<!-- Modal for opening in modal view -->
|
||||
<IframeModal
|
||||
:ref="`iframeModal-${groupId}`"
|
||||
:name="`iframeModal-${groupId}`"
|
||||
@closed="$emit('itemClicked')"
|
||||
@modalChanged="modalChanged"
|
||||
/>
|
||||
<!-- Edit item menu -->
|
||||
<EditSection
|
||||
v-if="editMenuOpen"
|
||||
@closeEditSection="closeEditSection"
|
||||
:sectionIndex="index"
|
||||
:isAddNew="false"
|
||||
/>
|
||||
<!-- Right-click item options context menu -->
|
||||
<ContextMenu
|
||||
:show="contextMenuOpen"
|
||||
:posX="contextPos.posX"
|
||||
:posY="contextPos.posY"
|
||||
:id="`context-menu-${groupId}`"
|
||||
v-click-outside="closeContextMenu"
|
||||
@openEditSection="openEditSection"
|
||||
@navigateToSection="navigateToSection"
|
||||
@removeSection="removeSection"
|
||||
/>
|
||||
</Collapsable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { sortOrder as defaultSortOrder, localStorageKeys } from '@/utils/defaults';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import router from '@/router';
|
||||
import Item from '@/components/LinkItems/Item.vue';
|
||||
import Collapsable from '@/components/LinkItems/Collapsable.vue';
|
||||
import IframeModal from '@/components/LinkItems/IframeModal.vue';
|
||||
import EditSection from '@/components/InteractiveEditor/EditSection.vue';
|
||||
import ContextMenu from '@/components/LinkItems/SectionContextMenu.vue';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import {
|
||||
sortOrder as defaultSortOrder,
|
||||
localStorageKeys,
|
||||
modalNames,
|
||||
} from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
name: 'Section',
|
||||
inject: ['config'],
|
||||
props: {
|
||||
groupId: String,
|
||||
title: String,
|
||||
@@ -66,21 +109,36 @@ export default {
|
||||
displayData: Object,
|
||||
items: Array,
|
||||
itemSize: String,
|
||||
modalOpen: Boolean,
|
||||
index: Number,
|
||||
},
|
||||
components: {
|
||||
Collapsable,
|
||||
ContextMenu,
|
||||
Item,
|
||||
IframeModal,
|
||||
EditSection,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editMenuOpen: false,
|
||||
contextMenuOpen: false,
|
||||
contextPos: {
|
||||
posX: undefined,
|
||||
posY: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
sortOrder() {
|
||||
return this.displayData.sortBy || defaultSortOrder;
|
||||
},
|
||||
/* If the sortBy attribute is specified, then return sorted data */
|
||||
sortedItems() {
|
||||
let { items } = this;
|
||||
if (this.config.appConfig.disableSmartSort) return items;
|
||||
if (this.appConfig.disableSmartSort) return items;
|
||||
if (this.sortOrder === 'alphabetical') {
|
||||
this.sortAlphabetically(items);
|
||||
} else if (this.sortOrder === 'reverse-alphabetical') {
|
||||
@@ -105,35 +163,31 @@ export default {
|
||||
},
|
||||
gridStyle() {
|
||||
let styles = '';
|
||||
styles += this.displayData.itemCountX
|
||||
? `grid-template-columns: repeat(${this.displayData.itemCountX}, 1fr);` : '';
|
||||
styles += this.displayData.itemCountY
|
||||
? `grid-template-rows: repeat(${this.displayData.itemCountY}, 1fr);` : '';
|
||||
if (document.body.clientWidth > 600) { // Only proceed if not on tiny screen
|
||||
styles += this.displayData.itemCountX
|
||||
? `grid-template-columns: repeat(${this.displayData.itemCountX}, minmax(0, 1fr));` : '';
|
||||
styles += this.displayData.itemCountY
|
||||
? `grid-template-rows: repeat(${this.displayData.itemCountY}, minmax(0, 1fr));` : '';
|
||||
}
|
||||
return styles;
|
||||
},
|
||||
isEditMode() {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Returns a unique lowercase string, based on name, for section ID */
|
||||
makeId(sectionStr, itemStr) {
|
||||
const charSum = sectionStr.split('').map((a) => a.charCodeAt(0)).reduce((x, y) => x + y);
|
||||
return `${charSum}_${itemStr.replace(/\s+/g, '-').replace(/[^a-zA-Z ]/g, '').toLowerCase()}`;
|
||||
},
|
||||
/* Opens the iframe modal */
|
||||
triggerModal(url) {
|
||||
this.$refs[`iframeModal-${this.groupId}`].show(url);
|
||||
},
|
||||
/* Emmit value upwards when iframe modal opened/ closed */
|
||||
modalChanged(changedTo) {
|
||||
this.$emit('change-modal-visibility', changedTo);
|
||||
},
|
||||
/* Determines if user has enabled online status checks */
|
||||
shouldEnableStatusCheck(itemPreference) {
|
||||
const globalPreference = this.config.appConfig.statusCheck || false;
|
||||
const globalPreference = this.appConfig.statusCheck || false;
|
||||
return itemPreference !== undefined ? itemPreference : globalPreference;
|
||||
},
|
||||
/* Determine how often to re-fire status checks */
|
||||
getStatusCheckInterval() {
|
||||
let interval = this.config.appConfig.statusCheckInterval;
|
||||
let interval = this.appConfig.statusCheckInterval;
|
||||
if (!interval) return 0;
|
||||
if (interval > 60) interval = 60;
|
||||
if (interval < 1) interval = 0;
|
||||
@@ -146,14 +200,14 @@ export default {
|
||||
/* Sorts items by most used to least used, based on click-count */
|
||||
sortByMostUsed(items) {
|
||||
const usageCount = JSON.parse(localStorage.getItem(localStorageKeys.MOST_USED) || '{}');
|
||||
const gmu = (item) => usageCount[this.makeId(this.title, item.title)] || 0;
|
||||
const gmu = (item) => usageCount[item.id] || 0;
|
||||
items.reverse().sort((a, b) => (gmu(a) < gmu(b) ? 1 : -1));
|
||||
return items;
|
||||
},
|
||||
/* Sorts items by most recently used */
|
||||
sortBLastUsed(items) {
|
||||
const usageCount = JSON.parse(localStorage.getItem(localStorageKeys.LAST_USED) || '{}');
|
||||
const glu = (item) => usageCount[this.makeId(this.title, item.title)] || 0;
|
||||
const glu = (item) => usageCount[item.id] || 0;
|
||||
items.reverse().sort((a, b) => (glu(a) < glu(b) ? 1 : -1));
|
||||
return items;
|
||||
},
|
||||
@@ -164,6 +218,50 @@ export default {
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map(({ value }) => value);
|
||||
},
|
||||
/* Navigate to the section's single-section view page */
|
||||
navigateToSection() {
|
||||
const parse = (section) => section.replace(' ', '-').toLowerCase().trim();
|
||||
const sectionIdentifier = parse(this.title);
|
||||
router.push({ path: `/home/${sectionIdentifier}` });
|
||||
this.closeContextMenu();
|
||||
},
|
||||
/* Open the Section Edit Menu */
|
||||
openEditSection() {
|
||||
this.editMenuOpen = true;
|
||||
this.$modal.show(modalNames.EDIT_SECTION);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, true);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
/* Close the section edit menu */
|
||||
closeEditSection() {
|
||||
this.editMenuOpen = false;
|
||||
this.$modal.hide(modalNames.EDIT_SECTION);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
},
|
||||
/* Deletes current section, in local state */
|
||||
removeSection() {
|
||||
const confirmMsg = this.$t('interactive-editor.edit-section.remove-confirm');
|
||||
const youSure = confirm(confirmMsg); // eslint-disable-line no-alert, no-restricted-globals
|
||||
if (youSure) {
|
||||
const payload = { sectionIndex: this.index, sectionName: this.title };
|
||||
this.$store.commit(StoreKeys.REMOVE_SECTION, payload);
|
||||
}
|
||||
this.closeContextMenu();
|
||||
},
|
||||
/* Open custom context menu, and set position */
|
||||
openContextMenu(e) {
|
||||
this.contextMenuOpen = true;
|
||||
if (e && window) {
|
||||
this.contextPos = {
|
||||
posX: e.clientX + window.pageXOffset,
|
||||
posY: e.clientY + window.pageYOffset,
|
||||
};
|
||||
}
|
||||
},
|
||||
/* Hide the right-click context menu */
|
||||
closeContextMenu() {
|
||||
this.contextMenuOpen = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -178,9 +276,9 @@ export default {
|
||||
padding: 0.8rem;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
border-radius: var(--curve-factor);
|
||||
background: #607d8b33;
|
||||
color: var(--primary);
|
||||
background: var(--item-background);
|
||||
border-radius: var(--curve-factor);
|
||||
box-shadow: var(--item-shadow);
|
||||
}
|
||||
|
||||
@@ -192,27 +290,44 @@ export default {
|
||||
display: grid;
|
||||
overflow: auto;
|
||||
@extend .scroll-bar;
|
||||
@include phone { grid-template-columns: repeat(1, 1fr); }
|
||||
@include tablet { grid-template-columns: repeat(2, 1fr); }
|
||||
@include laptop { grid-template-columns: repeat(2, 1fr); }
|
||||
@include monitor { grid-template-columns: repeat(3, 1fr); }
|
||||
@include big-screen { grid-template-columns: repeat(4, 1fr); }
|
||||
@include big-screen-up { grid-template-columns: repeat(5, 1fr); }
|
||||
@include phone { --item-col-count: 1; }
|
||||
@include tablet { --item-col-count: 2; }
|
||||
@include laptop { --item-col-count: 2; }
|
||||
@include monitor { --item-col-count: 3; }
|
||||
@include big-screen { --item-col-count: 4; }
|
||||
@include big-screen-up { --item-col-count: 5; }
|
||||
grid-template-columns: repeat(var(--item-col-count, 2), minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.orientation-horizontal {
|
||||
.orientation-horizontal:not(.single-section-view) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.there-are-items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
@include phone { grid-template-columns: repeat(2, 1fr); }
|
||||
@include tablet { grid-template-columns: repeat(4, 1fr); }
|
||||
@include laptop { grid-template-columns: repeat(6, 1fr); }
|
||||
@include monitor { grid-template-columns: repeat(8, 1fr); }
|
||||
@include big-screen { grid-template-columns: repeat(10, 1fr); }
|
||||
@include big-screen-up { grid-template-columns: repeat(12, 1fr); }
|
||||
@include phone { --item-col-count: 2; }
|
||||
@include tablet { --item-col-count: 4; }
|
||||
@include laptop { --item-col-count: 6; }
|
||||
@include monitor { --item-col-count: 8; }
|
||||
@include big-screen { --item-col-count: 10; }
|
||||
@include big-screen-up { --item-col-count: 12; }
|
||||
grid-template-columns: repeat(var(--item-col-count, 2), minmax(0, 1fr));
|
||||
}
|
||||
.there-are-items.inner-size-large {
|
||||
display: grid;
|
||||
@include phone { --item-col-count: 1; }
|
||||
@include tablet { --item-col-count: 2; }
|
||||
@include laptop { --item-col-count: 3; }
|
||||
@include monitor { --item-col-count: 5; }
|
||||
@include big-screen { --item-col-count: 6; }
|
||||
@include big-screen-up { --item-col-count: 8; }
|
||||
grid-template-columns: repeat(var(--item-col-count, 2), minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.add-new-item {
|
||||
display: flex;
|
||||
a {
|
||||
border-style: dashed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
<template>
|
||||
<transition name="slide">
|
||||
<div class="context-menu" v-if="show && menuEnabled"
|
||||
<div class="context-menu" v-if="show && !isMenuDisabled"
|
||||
:style="posX && posY ? `top:${posY}px;left:${posX}px;` : ''">
|
||||
<ul>
|
||||
<li @click="launch('sametab')">
|
||||
<!-- Open Options -->
|
||||
<ul class="menu-section">
|
||||
<li @click="openSection()">
|
||||
<SameTabOpenIcon />
|
||||
<span>{{ $t('menu.sametab') }}</span>
|
||||
<span>{{ $t('context-menus.section.open-section') }}</span>
|
||||
</li>
|
||||
<li @click="launch('newtab')">
|
||||
<NewTabOpenIcon />
|
||||
<span>{{ $t('menu.newtab') }}</span>
|
||||
<li @click="openEditSectionMenu">
|
||||
<EditIcon />
|
||||
<span>{{ $t('context-menus.section.edit-section') }}</span>
|
||||
</li>
|
||||
<li @click="launch('modal')">
|
||||
<IframeOpenIcon />
|
||||
<span>{{ $t('menu.modal') }}</span>
|
||||
</li>
|
||||
<li @click="launch('workspace')">
|
||||
<WorkspaceOpenIcon />
|
||||
<span>{{ $t('menu.workspace') }}</span>
|
||||
<li v-if="isEditMode" @click="removeSection">
|
||||
<BinIcon />
|
||||
<span>{{ $t('context-menus.section.remove-section') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -26,48 +23,47 @@
|
||||
|
||||
<script>
|
||||
// Import icons for each element
|
||||
import EditIcon from '@/assets/interface-icons/config-edit-json.svg';
|
||||
import BinIcon from '@/assets/interface-icons/interactive-editor-remove.svg';
|
||||
import SameTabOpenIcon from '@/assets/interface-icons/open-current-tab.svg';
|
||||
import NewTabOpenIcon from '@/assets/interface-icons/open-new-tab.svg';
|
||||
import IframeOpenIcon from '@/assets/interface-icons/open-iframe.svg';
|
||||
import WorkspaceOpenIcon from '@/assets/interface-icons/open-workspace.svg';
|
||||
|
||||
export default {
|
||||
name: 'ContextMenu',
|
||||
inject: ['config'],
|
||||
components: {
|
||||
EditIcon,
|
||||
BinIcon,
|
||||
SameTabOpenIcon,
|
||||
NewTabOpenIcon,
|
||||
IframeOpenIcon,
|
||||
WorkspaceOpenIcon,
|
||||
},
|
||||
props: {
|
||||
posX: Number, // The X coordinate for positioning
|
||||
posY: Number, // The Y coordinate for positioning
|
||||
show: Boolean, // Should show or hide the menu
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menuEnabled: !this.isMenuDisabled(), // Specifies if the context menu should be used
|
||||
};
|
||||
computed: {
|
||||
isMenuDisabled() {
|
||||
return !!this.$store.getters.appConfig.disableContextMenu;
|
||||
},
|
||||
isEditMode() {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Called on item click, emits an event up to Item */
|
||||
/* in order to launch the current app to a given target */
|
||||
launch(target) {
|
||||
this.$emit('contextItemClick', target);
|
||||
openSection() {
|
||||
this.$emit('navigateToSection');
|
||||
},
|
||||
/* Checks if the user as disabled context menu in config */
|
||||
isMenuDisabled() {
|
||||
if (this.config && this.config.appConfig) {
|
||||
return !!this.config.appConfig.disableContextMenu;
|
||||
}
|
||||
return false;
|
||||
openEditSectionMenu() {
|
||||
this.$emit('openEditSection');
|
||||
},
|
||||
removeSection() {
|
||||
this.$emit('removeSection');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style scoped lang="scss">
|
||||
|
||||
div.context-menu {
|
||||
position: absolute;
|
||||
@@ -81,10 +77,13 @@ div.context-menu {
|
||||
box-shadow: var(--context-menu-shadow);
|
||||
opacity: 0.98;
|
||||
|
||||
ul {
|
||||
ul.menu-section {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--context-menu-color);
|
||||
}
|
||||
li {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
@@ -94,9 +93,6 @@ div.context-menu {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--context-menu-secondary-color);
|
||||
}
|
||||
&:hover {
|
||||
background: var(--context-menu-secondary-color);
|
||||
}
|
||||
svg {
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
@@ -1,137 +1,44 @@
|
||||
<template>
|
||||
<form @submit.prevent="searchSubmitted">
|
||||
<div class="minimal-search-wrap">
|
||||
<input
|
||||
id="filter-tiles"
|
||||
v-model="input"
|
||||
ref="filter"
|
||||
class="minimal-search"
|
||||
:placeholder="$t('search.search-placeholder')"
|
||||
v-on:input="userIsTypingSomething"
|
||||
@keydown.esc="clearFilterInput"
|
||||
/>
|
||||
<p v-if="webSearchEnabled && input.length > 0" class="web-search-note">
|
||||
{{ $t('search.enter-to-search-web') }}
|
||||
</p>
|
||||
</div>
|
||||
<i v-if="input.length > 0"
|
||||
class="clear-search"
|
||||
:title="$t('search.clear-search-tooltip')"
|
||||
@click="clearFilterInput">x</i>
|
||||
</form>
|
||||
<SearchBar
|
||||
ref="MinimalSearchBar"
|
||||
@user-is-searchin="userIsTypingSomething"
|
||||
:active="true"
|
||||
:minimalSearch="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import router from '@/router';
|
||||
import ArrowKeyNavigation from '@/utils/ArrowKeyNavigation';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import { getCustomKeyShortcuts } from '@/utils/ConfigHelpers';
|
||||
import {
|
||||
searchEngineUrls,
|
||||
defaultSearchEngine,
|
||||
defaultSearchOpeningMethod,
|
||||
} from '@/utils/defaults';
|
||||
import SearchBar from '@/components/Settings/SearchBar';
|
||||
|
||||
export default {
|
||||
name: 'MinimalSearch',
|
||||
inject: ['config'],
|
||||
components: {
|
||||
SearchBar,
|
||||
},
|
||||
props: {
|
||||
active: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
input: '', // Users current search term
|
||||
akn: new ArrowKeyNavigation(), // Class that manages arrow key naviagtion
|
||||
getCustomKeyShortcuts,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
webSearchEnabled() {
|
||||
const { appConfig } = this.config;
|
||||
if (appConfig && appConfig.webSearch) {
|
||||
return !appConfig.webSearch.disableWebSearch;
|
||||
if (this.appConfig && this.appConfig.webSearch) {
|
||||
return !this.appConfig.webSearch.disableWebSearch;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Emmits users's search term up to parent */
|
||||
userIsTypingSomething() {
|
||||
this.$emit('user-is-searchin', this.input);
|
||||
},
|
||||
/* Resets everything to initial state, when user is finished */
|
||||
clearFilterInput() {
|
||||
this.input = ''; // Clear input model
|
||||
this.userIsTypingSomething(); // Emmit new empty value
|
||||
document.activeElement.blur(); // Remove focus
|
||||
this.akn.resetIndex(); // Reset current element index
|
||||
},
|
||||
/* Launches a given app when hotkey pressed */
|
||||
handleHotKey(key) {
|
||||
const usersHotKeys = this.getCustomKeyShortcuts();
|
||||
usersHotKeys.forEach((hotkey) => {
|
||||
if (hotkey.hotkey === parseInt(key, 10)) {
|
||||
if (hotkey.url) window.open(hotkey.url, '_blank');
|
||||
}
|
||||
});
|
||||
},
|
||||
/* Filter results as user types */
|
||||
startFiltering(event) {
|
||||
const currentElem = document.activeElement.id;
|
||||
const { key, keyCode } = event;
|
||||
/* If a modal is open, then do nothing */
|
||||
if (!this.active) return;
|
||||
if (/^[a-zA-Z]$/.test(key) && currentElem !== 'filter-tiles') {
|
||||
/* Letter key pressed - start searching */
|
||||
if (this.$refs.filter) this.$refs.filter.focus();
|
||||
this.userIsTypingSomething();
|
||||
} else if (/^[0-9]$/.test(key)) {
|
||||
/* Number key pressed, check if user has a custom binding */
|
||||
this.handleHotKey(key);
|
||||
} else if (keyCode >= 37 && keyCode <= 40) {
|
||||
/* Arrow key pressed - start navigation */
|
||||
this.akn.arrowNavigation(keyCode);
|
||||
} else if (keyCode === 27) {
|
||||
/* Esc key pressed - reset form */
|
||||
this.clearFilterInput();
|
||||
}
|
||||
},
|
||||
/* Open web search results in users desired method */
|
||||
launchWebSearch(url, method) {
|
||||
switch (method) {
|
||||
case 'newtab':
|
||||
window.open(url, '_blank');
|
||||
break;
|
||||
case 'sametab':
|
||||
window.open(url, '_self');
|
||||
break;
|
||||
case 'workspace':
|
||||
router.push({ name: 'workspace', query: { url } });
|
||||
break;
|
||||
default:
|
||||
ErrorHandler(`Unknown opening method: ${method}`);
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
},
|
||||
/* If web search enabled, then launch search results when enter is pressed */
|
||||
searchSubmitted() {
|
||||
// Get search preferences from appConfig
|
||||
const { appConfig } = this.config;
|
||||
const searchPrefs = appConfig.webSearch || {};
|
||||
if (this.webSearchEnabled) { // Only proceed if user hasn't disabled web search
|
||||
const openingMethod = searchPrefs.openingMethod || defaultSearchOpeningMethod;
|
||||
// Get search engine, and make URL
|
||||
const searchEngine = searchPrefs.searchEngine || defaultSearchEngine;
|
||||
let searchUrl = searchEngineUrls[searchEngine];
|
||||
if (!searchUrl) ErrorHandler(`Search engine not found - ${searchEngine}`);
|
||||
if (searchEngine === 'custom' && searchPrefs.customSearchEngine) {
|
||||
searchUrl = searchPrefs.customSearchEngine;
|
||||
}
|
||||
// Append users encoded query onto search URL, and launch
|
||||
searchUrl += encodeURIComponent(this.input);
|
||||
this.launchWebSearch(searchUrl, openingMethod);
|
||||
}
|
||||
userIsTypingSomething(searchValue) {
|
||||
this.input = searchValue;
|
||||
this.$emit('user-is-searchin', searchValue);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -142,61 +49,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@import '@/styles/media-queries.scss';
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.minimal-search-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
p.web-search-note {
|
||||
margin: 0;
|
||||
color: var(--minimal-view-search-color);
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
}
|
||||
input {
|
||||
display: inline-block;
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 1rem auto;
|
||||
outline: none;
|
||||
border: 1px solid var(--outline-color);
|
||||
border-radius: var(--curve-factor);
|
||||
background: var(--minimal-view-search-background);
|
||||
color: var(--minimal-view-search-color);
|
||||
&:focus {
|
||||
border-color: var(--minimal-view-search-color);
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
}
|
||||
.clear-search {
|
||||
color: var(--minimal-view-search-color);
|
||||
padding: 0.15rem 0.5rem 0.2rem 0.5rem;
|
||||
font-style: normal;
|
||||
font-size: 1rem;
|
||||
opacity: var(--dimming-factor);
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
right: 0.5rem;
|
||||
top: 1rem;
|
||||
border: 1px solid var(--minimal-view-search-color);
|
||||
font-size: 1rem;
|
||||
margin: 0.5rem;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--minimal-view-search-background);
|
||||
background: var(--minimal-view-search-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
:ref="`iframeModal-${groupId}`"
|
||||
:name="`iframeModal-${groupId}`"
|
||||
@closed="$emit('itemClicked')"
|
||||
@modalChanged="modalChanged"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -37,7 +36,6 @@ import IframeModal from '@/components/LinkItems/IframeModal.vue';
|
||||
|
||||
export default {
|
||||
name: 'ItemGroup',
|
||||
inject: ['config'],
|
||||
props: {
|
||||
groupId: String,
|
||||
title: String,
|
||||
@@ -50,6 +48,11 @@ export default {
|
||||
selected: Boolean,
|
||||
showAll: Boolean,
|
||||
},
|
||||
computed: {
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Item,
|
||||
IframeModal,
|
||||
@@ -66,15 +69,12 @@ export default {
|
||||
triggerModal(url) {
|
||||
this.$refs[`iframeModal-${this.groupId}`].show(url);
|
||||
},
|
||||
modalChanged(changedTo) {
|
||||
this.$emit('change-modal-visibility', changedTo);
|
||||
},
|
||||
shouldEnableStatusCheck(itemPreference) {
|
||||
const globalPreference = this.config.appConfig.statusCheck || false;
|
||||
const globalPreference = this.appConfig.statusCheck || false;
|
||||
return itemPreference !== undefined ? itemPreference : globalPreference;
|
||||
},
|
||||
getStatusCheckInterval() {
|
||||
let interval = this.config.appConfig.statusCheckInterval;
|
||||
let interval = this.appConfig.statusCheckInterval;
|
||||
if (!interval) return 0;
|
||||
if (interval > 60) interval = 60;
|
||||
if (interval < 1) interval = 0;
|
||||
@@ -97,12 +97,13 @@ export default {
|
||||
border-radius: 0 0 var(--curve-factor) var(--curve-factor);
|
||||
.section-items {
|
||||
display: grid;
|
||||
@include phone { grid-template-columns: repeat(1, 1fr); }
|
||||
@include tablet { grid-template-columns: repeat(2, 1fr); }
|
||||
@include laptop { grid-template-columns: repeat(3, 1fr); }
|
||||
@include monitor { grid-template-columns: repeat(4, 1fr); }
|
||||
@include big-screen { grid-template-columns: repeat(5, 1fr); }
|
||||
@include big-screen-up { grid-template-columns: repeat(6, 1fr); }
|
||||
@include phone { --minimal-col-count: 1; }
|
||||
@include tablet { --minimal-col-count: 2; }
|
||||
@include laptop { --minimal-col-count: 3; }
|
||||
@include monitor { --minimal-col-count: 4; }
|
||||
@include big-screen { --minimal-col-count: 5; }
|
||||
@include big-screen-up { --minimal-col-count: 6; }
|
||||
grid-template-columns: repeat(var(--minimal-col-count, 1), minmax(0, 1fr));
|
||||
}
|
||||
&.selected {
|
||||
border: 1px solid var(--minimal-view-group-color);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<header v-if="visible">
|
||||
<header v-if="componentVisible">
|
||||
<PageTitle
|
||||
v-if="titleVisible"
|
||||
:title="pageInfo.title"
|
||||
@@ -13,12 +13,10 @@
|
||||
<script>
|
||||
import PageTitle from '@/components/PageStrcture/PageTitle.vue';
|
||||
import Nav from '@/components/PageStrcture/Nav.vue';
|
||||
import { visibleComponents as defaultVisibleComponents } from '@/utils/defaults';
|
||||
import { shouldBeVisible } from '@/utils/MiscHelpers';
|
||||
|
||||
export default {
|
||||
name: 'Header',
|
||||
inject: ['visibleComponents'],
|
||||
components: {
|
||||
PageTitle,
|
||||
Nav,
|
||||
@@ -26,16 +24,19 @@ export default {
|
||||
props: {
|
||||
pageInfo: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
titleVisible: (this.visibleComponents || defaultVisibleComponents).pageTitle,
|
||||
navVisible: (this.visibleComponents || defaultVisibleComponents).navigation,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visible() {
|
||||
componentVisible() {
|
||||
return shouldBeVisible(this.$route.name);
|
||||
},
|
||||
visibleComponents() {
|
||||
return this.$store.getters.visibleComponents;
|
||||
},
|
||||
titleVisible() {
|
||||
return this.visibleComponents.pageTitle;
|
||||
},
|
||||
navVisible() {
|
||||
return this.visibleComponents.navigation;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,52 +1,93 @@
|
||||
<template>
|
||||
<nav id="nav">
|
||||
<div class="nav-outer">
|
||||
<IconBurger
|
||||
:class="`burger ${!navVisible ? 'visible' : ''}`"
|
||||
@click="navVisible = !navVisible"
|
||||
/>
|
||||
<nav id="nav" v-if="navVisible">
|
||||
<router-link
|
||||
v-for="(link, index) in links"
|
||||
:key="index"
|
||||
:to="link.path"
|
||||
:href="link.path"
|
||||
:target="isUrl(link.path) ? '_blank' : ''"
|
||||
rel="noopener noreferrer"
|
||||
class="nav-item"
|
||||
v-for="(link, index) in links"
|
||||
:key="index"
|
||||
:to="link.path"
|
||||
:href="link.path"
|
||||
:target="isUrl(link.path) ? '_blank' : ''"
|
||||
rel="noopener noreferrer"
|
||||
class="nav-item"
|
||||
>{{link.title}}</router-link>
|
||||
</nav>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconBurger from '@/assets/interface-icons/burger-menu.svg';
|
||||
|
||||
export default {
|
||||
name: 'Nav',
|
||||
components: {
|
||||
IconBurger,
|
||||
},
|
||||
props: {
|
||||
links: Array,
|
||||
},
|
||||
data: () => ({
|
||||
navVisible: true,
|
||||
isMobile: false,
|
||||
}),
|
||||
created() {
|
||||
this.navVisible = !this.detectMobile();
|
||||
this.isMobile = this.detectMobile();
|
||||
},
|
||||
methods: {
|
||||
detectMobile() {
|
||||
const screenWidth = document.body.clientWidth;
|
||||
return screenWidth && screenWidth < 600;
|
||||
},
|
||||
isUrl: (str) => new RegExp(/(http|https):\/\/(\S+)(:[0-9]+)?/).test(str),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/style-helpers.scss';
|
||||
@import '@/styles/media-queries.scss';
|
||||
|
||||
nav {
|
||||
.nav-outer {
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 0.5rem;
|
||||
margin: 0.5rem;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: var(--curve-factor);
|
||||
-webkit-box-shadow: 1px 1px 2px #232323;
|
||||
box-shadow: 1px 1px 2px #232323;
|
||||
color: var(--nav-link-text-color);
|
||||
background: var(--nav-link-background-color);
|
||||
border: 1px solid var(--nav-link-border-color);
|
||||
text-decoration: none;
|
||||
&.router-link-active, &:hover {
|
||||
color: var(--nav-link-text-color-hover);
|
||||
background: var(--nav-link-background-color-hover);
|
||||
border: 1px solid var(--nav-link-border-color-hover);
|
||||
}
|
||||
display: inline-block;
|
||||
padding: 0.75rem 0.5rem;
|
||||
margin: 0.5rem;
|
||||
min-width: 5rem;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: var(--curve-factor);
|
||||
box-shadow: var(--nav-link-shadow);
|
||||
color: var(--nav-link-text-color);
|
||||
background: var(--nav-link-background-color);
|
||||
border: 1px solid var(--nav-link-border-color);
|
||||
text-decoration: none;
|
||||
&.router-link-active, &:hover {
|
||||
color: var(--nav-link-text-color-hover);
|
||||
background: var(--nav-link-background-color-hover);
|
||||
border: 1px solid var(--nav-link-border-color-hover);
|
||||
box-shadow: var(--nav-link-shadow-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Mobile and Burger-Menu Styles */
|
||||
@extend .svg-button;
|
||||
@include phone {
|
||||
width: 100%;
|
||||
nav { flex-wrap: wrap; }
|
||||
}
|
||||
.burger {
|
||||
display: none;
|
||||
&.visible { display: block; }
|
||||
@include phone { display: block; }
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
<template>
|
||||
<router-link to="/" class="page-titles">
|
||||
<router-link to="/" class="page-titles" :disabled="isEditMode">
|
||||
<!-- Optional page logo image -->
|
||||
<img v-if="logo" :src="logo" class="site-logo" />
|
||||
<!-- Page heading and sub-heading -->
|
||||
<div class="text">
|
||||
<h1>{{ title }}</h1>
|
||||
<span class="subtitle">{{ description }}</span>
|
||||
<h1>{{ title }}</h1>
|
||||
<span class="subtitle">{{ description }}</span>
|
||||
</div>
|
||||
<!-- When in edit mode, show Edit Title button -->
|
||||
<EditModeIcon v-if="isEditMode" @click="editTitle()"
|
||||
class="edit-icon" v-tooltip="tooltip()" />
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditModeIcon from '@/assets/interface-icons/interactive-editor-edit-mode.svg';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import { modalNames } from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
name: 'PageTitle',
|
||||
props: {
|
||||
@@ -16,6 +25,26 @@ export default {
|
||||
description: String,
|
||||
logo: String,
|
||||
},
|
||||
components: {
|
||||
EditModeIcon,
|
||||
},
|
||||
computed: {
|
||||
isEditMode() {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* On edit button click, open the edit pageInfo modal */
|
||||
editTitle() {
|
||||
this.$modal.show(modalNames.EDIT_PAGE_INFO);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, true);
|
||||
},
|
||||
/* Edit button tooltip */
|
||||
tooltip() {
|
||||
const content = this.$t('interactive-editor.menu.edit-page-info-btn');
|
||||
return { content, trigger: 'hover focus', delay: 250 };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -28,6 +57,7 @@ export default {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
h1 {
|
||||
color: var(--heading-text-color);
|
||||
font-size: 2.5rem;
|
||||
@@ -49,5 +79,21 @@ export default {
|
||||
text-align: center;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
&[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
svg.edit-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
right: 1rem;
|
||||
top: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
margin: 0.25rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--background-darker);
|
||||
border-radius: var(--curve-factor);
|
||||
path { fill: var(--primary); }
|
||||
&:hover { border: 1px solid var(--primary); }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
<div class="config-buttons">
|
||||
<IconSpanner @click="showEditor()" tabindex="-2"
|
||||
v-tooltip="tooltip($t('settings.config-launcher-tooltip'))" />
|
||||
<IconViewMode @click="openChangeViewMenu()" tabindex="-2"
|
||||
<IconInteractiveEditor @click="startInteractiveEditor()" tabindex="-2"
|
||||
v-tooltip="tooltip(enterEditModeTooltip)"
|
||||
:class="isEditMode ? 'disabled' : ''" />
|
||||
<IconViewMode @click="openChangeViewMenu()" tabindex="-2"
|
||||
v-tooltip="tooltip($t('alternate-views.alternate-view-heading'))" />
|
||||
</div>
|
||||
|
||||
<!-- Modal containing all the configuration options -->
|
||||
<modal :name="modalNames.CONF_EDITOR" :resizable="true" width="60%" height="85%"
|
||||
@closed="$emit('modalChanged', false)" classes="dashy-modal">
|
||||
@closed="editorClosed" classes="dashy-modal">
|
||||
<ConfigContainer :config="combineConfig()" />
|
||||
</modal>
|
||||
|
||||
@@ -44,11 +47,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
// Import components, and store-key identifiers
|
||||
import ConfigContainer from '@/components/Configuration/ConfigContainer';
|
||||
import LanguageSwitcher from '@/components/Settings/LanguageSwitcher';
|
||||
import Keys from '@/utils/StoreMutations';
|
||||
import { topLevelConfKeys, localStorageKeys, modalNames } from '@/utils/defaults';
|
||||
// Import icons for config launcher buttons
|
||||
import IconSpanner from '@/assets/interface-icons/config-editor.svg';
|
||||
import IconInteractiveEditor from '@/assets/interface-icons/interactive-editor-edit-mode.svg';
|
||||
import IconViewMode from '@/assets/interface-icons/application-change-view.svg';
|
||||
import IconHome from '@/assets/interface-icons/application-home.svg';
|
||||
import IconWorkspaceView from '@/assets/interface-icons/open-workspace.svg';
|
||||
@@ -66,20 +72,40 @@ export default {
|
||||
ConfigContainer,
|
||||
LanguageSwitcher,
|
||||
IconSpanner,
|
||||
IconInteractiveEditor,
|
||||
IconViewMode,
|
||||
IconHome,
|
||||
IconWorkspaceView,
|
||||
IconMinimalView,
|
||||
},
|
||||
props: {
|
||||
sections: Array,
|
||||
pageInfo: Object,
|
||||
appConfig: Object,
|
||||
computed: {
|
||||
sections() {
|
||||
return this.$store.getters.sections;
|
||||
},
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
pageInfo() {
|
||||
return this.$store.getters.pageInfo;
|
||||
},
|
||||
isEditMode() {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
/* Tooltip text for Edit Mode button, to change depending on it in edit mode */
|
||||
enterEditModeTooltip() {
|
||||
return this.$t(
|
||||
`interactive-editor.menu.${this.isEditMode
|
||||
? 'edit-mode-subtitle' : 'start-editing-tooltip'}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showEditor: function show() {
|
||||
this.$modal.show(modalNames.CONF_EDITOR);
|
||||
this.$emit('modalChanged', true);
|
||||
this.$store.commit(Keys.SET_MODAL_OPEN, true);
|
||||
},
|
||||
editorClosed: function show() {
|
||||
this.$store.commit(Keys.SET_MODAL_OPEN, false);
|
||||
},
|
||||
combineConfig() {
|
||||
const conf = {};
|
||||
@@ -99,34 +125,24 @@ export default {
|
||||
closeViewSwitcher() {
|
||||
this.viewSwitcherOpen = false;
|
||||
},
|
||||
startInteractiveEditor() {
|
||||
if (!this.isEditMode) {
|
||||
this.$store.commit(Keys.SET_EDIT_MODE, true);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/style-helpers.scss';
|
||||
|
||||
.config-options {
|
||||
@extend .svg-button;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--settings-text-color);
|
||||
min-width: 3.2rem;
|
||||
svg {
|
||||
path {
|
||||
fill: var(--settings-text-color);
|
||||
}
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0.2rem;
|
||||
padding: 0.2rem;
|
||||
text-align: center;
|
||||
background: var(--background);
|
||||
border: 1px solid currentColor;
|
||||
border-radius: var(--curve-factor);
|
||||
cursor: pointer;
|
||||
&:hover, &.selected {
|
||||
background: var(--settings-text-color);
|
||||
path { fill: var(--background); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view-switcher {
|
||||
|
||||
@@ -2,45 +2,51 @@
|
||||
<div :class="`theme-configurator-wrapper ${showingAllVars ? 'showing-all' : ''}`">
|
||||
<h3 class="configurator-title">{{ $t('theme-maker.title') }}</h3>
|
||||
<div class="color-row-container">
|
||||
<div class="color-row" v-for="colorName in Object.keys(customColors)" :key="colorName">
|
||||
<label :for="`color-input-${colorName}`" class="color-name">
|
||||
{{colorName.replaceAll('-', ' ')}}
|
||||
</label>
|
||||
<v-swatches
|
||||
v-if="isColor(colorName, customColors[colorName])"
|
||||
v-model="customColors[colorName]"
|
||||
show-fallback
|
||||
fallback-input-type="color"
|
||||
popover-x="left"
|
||||
:swatches="swatches"
|
||||
@input="setVariable(colorName, customColors[colorName])"
|
||||
>
|
||||
<input
|
||||
:id="`color-input-${colorName}`"
|
||||
slot="trigger"
|
||||
:value="customColors[colorName]"
|
||||
class="swatch-input form__input__element"
|
||||
readonly
|
||||
:style="makeSwatchStyles(colorName)"
|
||||
/>
|
||||
</v-swatches>
|
||||
<input v-else
|
||||
:id="`color-input-${colorName}`"
|
||||
:value="customColors[colorName]"
|
||||
class="misc-input"
|
||||
@input="setVariable(colorName, customColors[colorName])"
|
||||
/>
|
||||
</div> <!-- End of color list -->
|
||||
<!-- Show color swatch input for each color -->
|
||||
<div class="color-row" v-for="colorName in Object.keys(customColors)" :key="colorName">
|
||||
<label :for="`color-input-${colorName}`" class="color-name">
|
||||
{{colorName.replaceAll('-', ' ')}}
|
||||
</label>
|
||||
<v-swatches
|
||||
v-if="isColor(colorName, customColors[colorName])"
|
||||
v-model="customColors[colorName]"
|
||||
show-fallback
|
||||
fallback-input-type="color"
|
||||
popover-x="left"
|
||||
:swatches="swatches"
|
||||
@input="setVariable(colorName, customColors[colorName])"
|
||||
>
|
||||
<input
|
||||
:id="`color-input-${colorName}`"
|
||||
slot="trigger"
|
||||
:value="customColors[colorName]"
|
||||
class="swatch-input form__input__element"
|
||||
readonly
|
||||
:style="makeSwatchStyles(colorName)"
|
||||
/>
|
||||
</v-swatches>
|
||||
<input v-else
|
||||
:id="`color-input-${colorName}`"
|
||||
v-model="customColors[colorName]"
|
||||
:class="`misc-input ${isTextual(colorName, customColors[colorName]) ? 'long-input' : ''}`"
|
||||
@input="setVariable(colorName, customColors[colorName])"
|
||||
/>
|
||||
</div> <!-- End of color list -->
|
||||
</div>
|
||||
<!-- More options: Export, Reset, Show all -->
|
||||
<p @click="showFontVariables" class="action-text-btn show-all-vars-btn">
|
||||
{{ $t('theme-maker.change-fonts-button') }}
|
||||
</p>
|
||||
<p @click="findAllVariableNames" class="action-text-btn show-all-vars-btn">
|
||||
{{ $t('theme-maker.show-all-button') }}
|
||||
</p>
|
||||
<p @click="exportToClipboard" class="action-text-btn">
|
||||
{{ $t('theme-maker.export-button') }}
|
||||
</p>
|
||||
<p @click="resetAndSave" class="action-text-btn show-all-vars-btn">
|
||||
<p @click="resetAndSave" class="action-text-btn">
|
||||
{{ $t('theme-maker.reset-button') }} '{{ themeToEdit }}'
|
||||
</p>
|
||||
<p @click="findAllVariableNames" class="action-text-btn">
|
||||
{{ $t('theme-maker.show-all-button') }}
|
||||
</p>
|
||||
<!-- Save and cancel buttons -->
|
||||
<div class="action-buttons">
|
||||
<Button :click="saveChanges">
|
||||
<SaveIcon /> {{ $t('theme-maker.save-button') }}
|
||||
@@ -55,8 +61,8 @@
|
||||
<script>
|
||||
import VSwatches from 'vue-swatches';
|
||||
import 'vue-swatches/dist/vue-swatches.css';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import { localStorageKeys, mainCssVars, swatches } from '@/utils/defaults';
|
||||
|
||||
import Button from '@/components/FormElements/Button';
|
||||
import SaveIcon from '@/assets/interface-icons/save-config.svg';
|
||||
import CancelIcon from '@/assets/interface-icons/config-cancel.svg';
|
||||
@@ -88,11 +94,12 @@ export default {
|
||||
setVariable(variable, value) {
|
||||
document.documentElement.style.setProperty(`--${variable}`, value);
|
||||
},
|
||||
/* Saves the users omdified variables in local storage */
|
||||
/* Updates browser storage, and srore with new color settings, and shows success msg */
|
||||
saveChanges() {
|
||||
const priorSettings = JSON.parse(localStorage[localStorageKeys.CUSTOM_COLORS] || '{}');
|
||||
priorSettings[this.themeToEdit] = this.customColors;
|
||||
localStorage.setItem(localStorageKeys.CUSTOM_COLORS, JSON.stringify(priorSettings));
|
||||
this.$store.commit(StoreKeys.SET_CUSTOM_COLORS, priorSettings);
|
||||
this.$toasted.show(this.$t('theme-maker.saved-toast', { theme: this.themeToEdit }));
|
||||
this.$emit('closeThemeConfigurator');
|
||||
},
|
||||
@@ -134,6 +141,13 @@ export default {
|
||||
});
|
||||
return data;
|
||||
},
|
||||
/* Adds font variables to list */
|
||||
showFontVariables() {
|
||||
const currentVariables = this.customColors;
|
||||
const fonts = ['font-headings', 'font-body', 'font-monospace'];
|
||||
const fontVariables = this.makeInitialData(fonts);
|
||||
this.customColors = { ...currentVariables, ...fontVariables };
|
||||
},
|
||||
/* Find all available CSS variables for the current applied theme */
|
||||
findAllVariableNames() {
|
||||
const availableVariables = Array.from(document.styleSheets)
|
||||
@@ -142,7 +156,7 @@ export default {
|
||||
((acc, sheet) => ([
|
||||
...acc,
|
||||
...Array.from(sheet.cssRules).reduce(
|
||||
(def, rule) => (rule.selectorText === ':root'
|
||||
(def, rule) => (rule.selectorText === ':root' || rule.selectorText === 'html'
|
||||
? [...def, ...Array.from(rule.style).filter(name => name.startsWith('--'))] : def),
|
||||
[],
|
||||
),
|
||||
@@ -155,13 +169,25 @@ export default {
|
||||
/* Returns a complmenting text color for the palete input foreground */
|
||||
/* White if the color is dark, otherwise black */
|
||||
getForegroundColor(colorHex) {
|
||||
/* Converts a 3-digit hex code to a 6-digit hex code (e.g. #f01 --> #ff0011) */
|
||||
const threeToSix = (hex) => {
|
||||
let digit = hex;
|
||||
digit = digit.split('').map((item) => (item === '#' ? item : item + item)).join('');
|
||||
return digit;
|
||||
};
|
||||
/* Converts hex code to RGB (e.g. #ff0011 --> rgb(255,0,0) ) */
|
||||
const hexToRgb = (hex) => {
|
||||
const colorParts = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
let hexCode = hex.slice(0, 7);
|
||||
if (hex.startsWith('#') && hex.length === 4) hexCode = threeToSix(hexCode);
|
||||
const colorParts = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexCode);
|
||||
if (!colorParts || colorParts.length < 3) return 'black';
|
||||
const parse = (index) => parseInt(colorParts[index], 16);
|
||||
return colorParts ? { r: parse(1), g: parse(2), b: parse(3) } : null;
|
||||
};
|
||||
/* Given an RGB value, return the lightness ratio */
|
||||
const getLightness = (rgb) => (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
|
||||
if (!colorHex.startsWith('#')) return 'white'; // Not a hex, do nothing
|
||||
// Convert hex to RGB obj, get lightness, and return opposing color
|
||||
return getLightness(hexToRgb(colorHex.trim())) < 100 ? 'white' : 'black';
|
||||
},
|
||||
/* The contents of the style attribute, to set background and text color of swatch */
|
||||
@@ -175,6 +201,7 @@ export default {
|
||||
// If value is a dimension, then it aint a color
|
||||
if ((/rem|px|%/.exec(variableValue))) return false;
|
||||
const nonColorVariables = [ // Known non-color variables
|
||||
'--font-headings', '--font-body', '--font-monospace',
|
||||
'--curve-factor', '--curve-factor-navbar', '--curve-factor-small',
|
||||
'--dimming-factor', '--scroll-bar-width', '--header-height', '--footer-height',
|
||||
'--item-group-padding', '--item-shadow', '--item-hover-shadow:', '--item-icon-transform',
|
||||
@@ -185,6 +212,10 @@ export default {
|
||||
if (nonColorVariables.includes(`--${variableName}`)) return false;
|
||||
return true; // It must be a color, we'll use the color picker
|
||||
},
|
||||
/* Determine if a given key is that of a known font variable, or has a long value */
|
||||
isTextual(varName, varValue) {
|
||||
return varName.startsWith('font-') || (varValue && varValue.length > 12);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -198,7 +229,7 @@ div.theme-configurator-wrapper {
|
||||
right: 1rem;
|
||||
width: 16rem;
|
||||
min-height: 12rem;
|
||||
max-height: 28rem;
|
||||
max-height: 32rem;
|
||||
padding: 0.5rem;
|
||||
z-index: 5;
|
||||
overflow-y: visible;
|
||||
@@ -214,7 +245,7 @@ div.theme-configurator-wrapper {
|
||||
}
|
||||
|
||||
div.color-row-container {
|
||||
max-height: 16rem;
|
||||
max-height: 20rem;
|
||||
overflow-y: visible;
|
||||
@extend .scroll-bar;
|
||||
div.color-row {
|
||||
@@ -246,6 +277,15 @@ div.theme-configurator-wrapper {
|
||||
box-shadow: inset 0 0 4px 4px #00000080;
|
||||
outline: none;
|
||||
}
|
||||
&.long-input {
|
||||
cursor: text;
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
padding: 0.5rem 0.2rem;
|
||||
font-size: 0.75rem;
|
||||
width: 9rem;
|
||||
&:hover { box-shadow: none; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import IconSmall from '@/assets/interface-icons/icon-size-small.svg';
|
||||
import IconMedium from '@/assets/interface-icons/icon-size-medium.svg';
|
||||
import IconLarge from '@/assets/interface-icons/icon-size-large.svg';
|
||||
@@ -46,7 +47,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateIconSize(iconSize) {
|
||||
this.$emit('iconSizeUpdated', iconSize);
|
||||
this.$store.commit(StoreKeys.SET_ITEM_SIZE, iconSize);
|
||||
},
|
||||
tooltip(content) {
|
||||
return { content, trigger: 'hover focus', delay: 250 };
|
||||
|
||||
@@ -28,23 +28,40 @@
|
||||
import Button from '@/components/FormElements/Button';
|
||||
import SaveConfigIcon from '@/assets/interface-icons/save-config.svg';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import Keys from '@/utils/StoreMutations';
|
||||
import { languages } from '@/utils/languages';
|
||||
import { localStorageKeys, modalNames } from '@/utils/defaults';
|
||||
|
||||
export default {
|
||||
name: 'LanguageSwitcher',
|
||||
inject: ['config'],
|
||||
components: {
|
||||
Button,
|
||||
SaveConfigIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
language: this.getCurrentLanguage(), // The currently selected language
|
||||
language: '', // The currently selected language
|
||||
modalName: modalNames.LANG_SWITCHER, // Key for modal
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// Initiate the current language, with VueX state
|
||||
this.language = this.savedLanguage;
|
||||
},
|
||||
computed: {
|
||||
/* Get appConfig from store */
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
/* The ISO code for the users language, synced with VueX store */
|
||||
savedLanguage: {
|
||||
get() {
|
||||
return this.getIsoFromLangObj(this.$store.getters.appConfig.lang);
|
||||
},
|
||||
set(newLang) {
|
||||
this.$store.commit(Keys.SET_LANGUAGE, newLang.code);
|
||||
},
|
||||
},
|
||||
/* Return the array of language objects, plus a friends name */
|
||||
languageList: () => languages.map((lang) => {
|
||||
const newLang = lang;
|
||||
@@ -73,6 +90,7 @@ export default {
|
||||
if (this.checkLocale(selectedLanguage)) {
|
||||
localStorage.setItem(localStorageKeys.LANGUAGE, selectedLanguage.code);
|
||||
this.applyLanguageLocally();
|
||||
this.savedLanguage = selectedLanguage;
|
||||
const successMsg = `${selectedLanguage.flag} `
|
||||
+ `${this.$t('language-switcher.success-msg')} ${selectedLanguage.name}`;
|
||||
this.$toasted.show(successMsg, { className: 'toast-success' });
|
||||
@@ -82,11 +100,10 @@ export default {
|
||||
ErrorHandler('Unable to apply language');
|
||||
}
|
||||
},
|
||||
/* Gets the users current language from local storage */
|
||||
getCurrentLanguage() {
|
||||
/* Gets the ISO code for a given language object */
|
||||
getIsoFromLangObj(langObj) {
|
||||
const getLanguageFromIso = (iso) => languages.find((lang) => lang.code === iso);
|
||||
const current = localStorage[localStorageKeys.LANGUAGE] || this.config.appConfig.language;
|
||||
return getLanguageFromIso(current);
|
||||
return getLanguageFromIso(langObj);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,17 +25,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import IconDeafault from '@/assets/interface-icons/layout-default.svg';
|
||||
import IconHorizontal from '@/assets/interface-icons/layout-horizontal.svg';
|
||||
import IconVertical from '@/assets/interface-icons/layout-vertical.svg';
|
||||
|
||||
export default {
|
||||
name: 'LayoutSelector',
|
||||
data() {
|
||||
return {
|
||||
input: '',
|
||||
};
|
||||
},
|
||||
props: {
|
||||
displayLayout: String,
|
||||
},
|
||||
@@ -46,7 +42,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateDisplayLayout(layout) {
|
||||
this.$emit('layoutUpdated', layout);
|
||||
this.$store.commit(StoreKeys.SET_ITEM_LAYOUT, layout);
|
||||
},
|
||||
tooltip(content) {
|
||||
return { content, trigger: 'hover focus', delay: 250 };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<form @submit.prevent="searchSubmitted">
|
||||
<form @submit.prevent="searchSubmitted" :class="minimalSearch ? 'minimal' : 'normal'">
|
||||
<label for="filter-tiles">{{ $t('search.search-label') }}</label>
|
||||
<div class="search-wrap">
|
||||
<input
|
||||
@@ -35,9 +35,8 @@ import {
|
||||
|
||||
export default {
|
||||
name: 'FilterTile',
|
||||
inject: ['config'],
|
||||
props: {
|
||||
active: Boolean,
|
||||
minimalSearch: Boolean, // If true, then keep it simple
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -47,8 +46,11 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
active() {
|
||||
return !this.$store.state.modalOpen;
|
||||
},
|
||||
searchPrefs() {
|
||||
return this.config.appConfig.webSearch || {};
|
||||
return this.$store.getters.webSearch || {};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -129,7 +131,10 @@ export default {
|
||||
const searchEngine = searchPrefs.searchEngine || defaultSearchEngine;
|
||||
// Use either search bang, or preffered search engine
|
||||
const desiredSearchEngine = searchBang || searchEngine;
|
||||
let searchUrl = findUrlForSearchEngine(desiredSearchEngine, searchEngineUrls);
|
||||
const isCustomSearch = (searchPrefs.searchEngine === 'custom' && searchPrefs.customSearchEngine);
|
||||
let searchUrl = isCustomSearch
|
||||
? searchPrefs.customSearchEngine
|
||||
: findUrlForSearchEngine(desiredSearchEngine, searchEngineUrls);
|
||||
if (searchUrl) { // Append search query to URL, and launch
|
||||
searchUrl += encodeURIComponent(stripBangs(this.input, bangList));
|
||||
this.launchWebSearch(searchUrl, openingMethod);
|
||||
@@ -145,13 +150,7 @@ export default {
|
||||
|
||||
@import '@/styles/media-queries.scss';
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
background: linear-gradient(0deg, var(--background) 0%, var(--background-darker) 100%);
|
||||
}
|
||||
form {
|
||||
form.normal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 0 0 var(--curve-factor-navbar) 0;
|
||||
@@ -192,7 +191,6 @@ export default {
|
||||
}
|
||||
}
|
||||
.clear-search {
|
||||
//position: absolute;
|
||||
color: var(--settings-text-color);
|
||||
padding: 0 0.3rem 0.1rem 0.3rem;
|
||||
font-style: normal;
|
||||
@@ -203,7 +201,6 @@ export default {
|
||||
right: 0.5rem;
|
||||
top: 1rem;
|
||||
border: 1px solid var(--settings-text-color);
|
||||
font-size: 1rem;
|
||||
margin: 0.25rem;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
@@ -213,13 +210,13 @@ export default {
|
||||
}
|
||||
|
||||
@include tablet {
|
||||
form {
|
||||
form.normal {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@include phone {
|
||||
form {
|
||||
form.nomral {
|
||||
flex: 1;
|
||||
border-radius: 0;
|
||||
text-align: center;
|
||||
@@ -227,4 +224,56 @@ export default {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
form.minimal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
label { display: none; }
|
||||
.search-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
p.web-search-note {
|
||||
margin: 0;
|
||||
color: var(--minimal-view-search-color);
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
}
|
||||
input {
|
||||
display: inline-block;
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 1rem auto;
|
||||
outline: none;
|
||||
border: 1px solid var(--outline-color);
|
||||
border-radius: var(--curve-factor);
|
||||
background: var(--minimal-view-search-background);
|
||||
color: var(--minimal-view-search-color);
|
||||
&:focus {
|
||||
border-color: var(--minimal-view-search-color);
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
}
|
||||
.clear-search {
|
||||
color: var(--minimal-view-search-color);
|
||||
padding: 0.15rem 0.5rem 0.2rem 0.5rem;
|
||||
font-style: normal;
|
||||
font-size: 1rem;
|
||||
opacity: var(--dimming-factor);
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
right: 0.5rem;
|
||||
top: 1rem;
|
||||
border: 1px solid var(--minimal-view-search-color);
|
||||
margin: 0.5rem;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--minimal-view-search-background);
|
||||
background: var(--minimal-view-search-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,16 +3,13 @@
|
||||
<SearchBar ref="SearchBar"
|
||||
@user-is-searchin="userIsTypingSomething"
|
||||
v-if="searchVisible"
|
||||
:active="!modalOpen"
|
||||
/>
|
||||
<div class="options-outer">
|
||||
<div :class="`options-container ${!settingsVisible ? 'hide' : ''}`">
|
||||
<ThemeSelector :externalThemes="externalThemes" @modalChanged="modalChanged"
|
||||
:confTheme="getInitialTheme()" :userThemes="getUserThemes()" />
|
||||
<LayoutSelector :displayLayout="displayLayout" @layoutUpdated="updateDisplayLayout"/>
|
||||
<ItemSizeSelector :iconSize="iconSize" @iconSizeUpdated="updateIconSize" />
|
||||
<ConfigLauncher :sections="sections" :pageInfo="pageInfo" :appConfig="appConfig"
|
||||
@modalChanged="modalChanged" />
|
||||
<ThemeSelector />
|
||||
<LayoutSelector :displayLayout="displayLayout" />
|
||||
<ItemSizeSelector :iconSize="iconSize" />
|
||||
<ConfigLauncher />
|
||||
<AuthButtons v-if="userState != 'noone'" :userType="userState" />
|
||||
</div>
|
||||
<div :class="`show-hide-container ${settingsVisible? 'hide-btn' : 'show-btn'}`">
|
||||
@@ -52,10 +49,6 @@ export default {
|
||||
displayLayout: String,
|
||||
iconSize: String,
|
||||
externalThemes: Object,
|
||||
appConfig: Object,
|
||||
pageInfo: Object,
|
||||
sections: Array,
|
||||
modalOpen: Boolean,
|
||||
},
|
||||
components: {
|
||||
SearchBar,
|
||||
@@ -69,7 +62,43 @@ export default {
|
||||
IconOpen,
|
||||
IconClose,
|
||||
},
|
||||
inject: ['visibleComponents'],
|
||||
data() {
|
||||
return {
|
||||
settingsVisible: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sections() {
|
||||
return this.$store.getters.sections;
|
||||
},
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
pageInfo() {
|
||||
return this.$store.getters.pageInfo;
|
||||
},
|
||||
/**
|
||||
* Determines which button should display, based on the user type
|
||||
* 0 = Auth not configured, don't show anything
|
||||
* 1 = Auth condifured, and user logged in, show logout button
|
||||
* 2 = Auth configured, guest access enabled, and not logged in, show login
|
||||
* Note that if auth is enabled, but not guest access, and user not logged in,
|
||||
* then they will never be able to view the homepage, so no button needed
|
||||
*/
|
||||
userState() {
|
||||
return getUserState();
|
||||
},
|
||||
/* Object indicating which components should be hidden, based on user preferences */
|
||||
visibleComponents() {
|
||||
return this.$store.getters.visibleComponents;
|
||||
},
|
||||
searchVisible() {
|
||||
return this.$store.getters.visibleComponents.searchBar;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.settingsVisible = this.getSettingsVisibility();
|
||||
},
|
||||
methods: {
|
||||
userIsTypingSomething(something) {
|
||||
this.$emit('user-is-searchin', something);
|
||||
@@ -77,15 +106,6 @@ export default {
|
||||
clearFilterInput() {
|
||||
this.$refs.SearchBar.clearFilterInput();
|
||||
},
|
||||
updateDisplayLayout(layout) {
|
||||
this.$emit('change-display-layout', layout);
|
||||
},
|
||||
updateIconSize(iconSize) {
|
||||
this.$emit('change-icon-size', iconSize);
|
||||
},
|
||||
modalChanged(changedTo) {
|
||||
this.$emit('change-modal-visibility', changedTo);
|
||||
},
|
||||
getInitialTheme() {
|
||||
return this.appConfig.theme || '';
|
||||
},
|
||||
@@ -100,29 +120,13 @@ export default {
|
||||
localStorage.setItem(localStorageKeys.HIDE_SETTINGS, this.settingsVisible);
|
||||
},
|
||||
getSettingsVisibility() {
|
||||
return JSON.parse(localStorage[localStorageKeys.HIDE_SETTINGS]
|
||||
|| (this.visibleComponents || defaultVisibleComponents).settings);
|
||||
const screenWidth = document.body.clientWidth;
|
||||
if (screenWidth && screenWidth < 600) return false;
|
||||
if ((this.visibleComponents || {}).settings === false) return false;
|
||||
if (localStorage[localStorageKeys.HIDE_SETTINGS] === 'false') return false;
|
||||
return defaultVisibleComponents.settings;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* Determines which button should display, based on the user type
|
||||
* 0 = Auth not configured, don't show anything
|
||||
* 1 = Auth condifured, and user logged in, show logout button
|
||||
* 2 = Auth configured, guest access enabled, and not logged in, show login
|
||||
* Note that if auth is enabled, but not guest access, and user not logged in,
|
||||
* then they will never be able to view the homepage, so no button needed
|
||||
*/
|
||||
userState() {
|
||||
return getUserState();
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
settingsVisible: this.getSettingsVisibility(),
|
||||
searchVisible: (this.visibleComponents || defaultVisibleComponents).searchBar,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -165,6 +169,11 @@ export default {
|
||||
@include very-tiny-phone {
|
||||
flex-direction: column;
|
||||
align-items: baseline;
|
||||
div {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
.theme-selector-section { justify-content: center; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
<v-select
|
||||
:options="themeNames"
|
||||
v-model="selectedTheme"
|
||||
:value="$store.getters.theme"
|
||||
class="theme-dropdown"
|
||||
:tabindex="-2"
|
||||
@input="themeChanged"
|
||||
/>
|
||||
</div>
|
||||
<IconPalette
|
||||
v-if="!hidePallete"
|
||||
class="color-button"
|
||||
@click="openThemeConfigurator"
|
||||
v-tooltip="$t('theme-maker.title')"
|
||||
@@ -31,76 +34,119 @@ import {
|
||||
ApplyCustomVariables,
|
||||
} from '@/utils/ThemeHelper';
|
||||
import Defaults, { localStorageKeys } from '@/utils/defaults';
|
||||
import Keys from '@/utils/StoreMutations';
|
||||
import IconPalette from '@/assets/interface-icons/config-color-palette.svg';
|
||||
|
||||
export default {
|
||||
name: 'ThemeSelector',
|
||||
props: {
|
||||
externalThemes: Object,
|
||||
confTheme: String,
|
||||
userThemes: Array,
|
||||
hidePallete: Boolean,
|
||||
},
|
||||
components: {
|
||||
CustomThemeMaker,
|
||||
IconPalette,
|
||||
},
|
||||
watch: {
|
||||
/* When the theme changes, then call the update method */
|
||||
selectedTheme(newTheme) {
|
||||
/* When theme in VueX store changes, then update theme */
|
||||
themeFromStore(newTheme) {
|
||||
this.selectedTheme = newTheme;
|
||||
this.updateTheme(newTheme);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTheme: this.getInitialTheme(),
|
||||
builtInThemes: [...Defaults.builtInThemes, ...this.userThemes],
|
||||
themeHelper: new LoadExternalTheme(),
|
||||
selectedTheme: '',
|
||||
themeConfiguratorOpen: false, // Control the opening of theme config popup
|
||||
themeHelper: new LoadExternalTheme(),
|
||||
ApplyLocalTheme,
|
||||
ApplyCustomVariables,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/* Get appConfig from store */
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
/* Get users theme from store */
|
||||
themeFromStore() {
|
||||
return this.$store.getters.theme;
|
||||
},
|
||||
/* Combines all theme names (builtin and user defined) together */
|
||||
themeNames: function themeNames() {
|
||||
const externalThemeNames = Object.keys(this.externalThemes);
|
||||
const specialThemes = ['custom'];
|
||||
return [...externalThemeNames, ...this.builtInThemes, ...specialThemes];
|
||||
return [...externalThemeNames, ...Defaults.builtInThemes, ...specialThemes];
|
||||
},
|
||||
extraThemeNames() {
|
||||
const userThemes = this.appConfig.cssThemes || [];
|
||||
if (typeof userThemes === 'string') return [userThemes];
|
||||
return userThemes;
|
||||
},
|
||||
/* Returns an array of links to external CSS from the Config */
|
||||
externalThemes() {
|
||||
const availibleThemes = {};
|
||||
if (this.appConfig) {
|
||||
if (this.appConfig.externalStyleSheet) {
|
||||
const externals = this.appConfig.externalStyleSheet;
|
||||
if (Array.isArray(externals)) {
|
||||
externals.forEach((ext, i) => {
|
||||
availibleThemes[`External Stylesheet ${i + 1}`] = ext;
|
||||
});
|
||||
} else {
|
||||
availibleThemes['External Stylesheet'] = this.appConfig.externalStyleSheet;
|
||||
}
|
||||
}
|
||||
}
|
||||
availibleThemes.Default = '#';
|
||||
return availibleThemes;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
mounted() {
|
||||
const initialTheme = this.getInitialTheme();
|
||||
this.selectedTheme = initialTheme;
|
||||
// Pass all user custom stylesheets to the themehelper
|
||||
const added = Object.keys(this.externalThemes).map(
|
||||
name => this.themeHelper.add(name, this.externalThemes[name]),
|
||||
);
|
||||
// Quicker loading, if the theme is local we can apply it immidiatley
|
||||
if (this.isThemeLocal(this.selectedTheme)) {
|
||||
this.updateTheme(this.selectedTheme);
|
||||
if (this.isThemeLocal(initialTheme)) {
|
||||
this.updateTheme(initialTheme);
|
||||
// If it's an external stylesheet, then wait for promise to resolve
|
||||
} else if (this.selectedTheme !== Defaults.theme) {
|
||||
} else if (initialTheme !== Defaults.theme) {
|
||||
Promise.all(added).then(() => {
|
||||
this.updateTheme(this.selectedTheme);
|
||||
this.updateTheme(initialTheme);
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/* Get default theme */
|
||||
/* Called when dropdown changed
|
||||
* Updates store, which will in turn update theme through watcher
|
||||
*/
|
||||
themeChanged() {
|
||||
this.$store.commit(Keys.SET_THEME, this.selectedTheme);
|
||||
},
|
||||
/* Returns the initial theme */
|
||||
getInitialTheme() {
|
||||
return localStorage[localStorageKeys.THEME] || this.confTheme || Defaults.theme;
|
||||
const localTheme = localStorage[localStorageKeys.THEME];
|
||||
if (localTheme && localTheme !== 'undefined') return localTheme;
|
||||
return this.appConfig.theme || Defaults.theme;
|
||||
},
|
||||
/* Determines if a given theme is local / not a custom user stylesheet */
|
||||
isThemeLocal(themeToCheck) {
|
||||
return this.builtInThemes.includes(themeToCheck);
|
||||
const localThemes = [...Defaults.builtInThemes, ...this.extraThemeNames];
|
||||
return localThemes.includes(themeToCheck);
|
||||
},
|
||||
/* Opens the theme color configurator popup */
|
||||
openThemeConfigurator() {
|
||||
this.$emit('modalChanged', true);
|
||||
this.$store.commit(Keys.SET_MODAL_OPEN, true);
|
||||
this.themeConfiguratorOpen = true;
|
||||
},
|
||||
/* Closes the theme color configurator popup */
|
||||
closeThemeConfigurator() {
|
||||
// this.$emit('modalChanged', false);
|
||||
this.themeConfiguratorOpen = false;
|
||||
if (this.themeConfiguratorOpen) {
|
||||
this.$store.commit(Keys.SET_MODAL_OPEN, false);
|
||||
this.themeConfiguratorOpen = false;
|
||||
}
|
||||
},
|
||||
/* Updates theme. Checks if the new theme is local or external,
|
||||
and calls appropirate updating function. Updates local storage */
|
||||
@@ -169,6 +215,7 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
span.theme-label {
|
||||
font-size: 1rem;
|
||||
|
||||
@@ -36,9 +36,9 @@ import IconMinimalView from '@/assets/interface-icons/application-minimal.svg';
|
||||
|
||||
export default {
|
||||
name: 'SideBar',
|
||||
inject: ['config'],
|
||||
props: {
|
||||
sections: Array,
|
||||
initUrl: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -56,9 +56,26 @@ export default {
|
||||
openSection(index) {
|
||||
this.isOpen = this.isOpen.map((val, ind) => (ind !== index ? false : !val));
|
||||
},
|
||||
launchApp(url) {
|
||||
this.$emit('launch-app', url);
|
||||
/* When item clicked, emit a launch event */
|
||||
launchApp(options) {
|
||||
this.$emit('launch-app', options);
|
||||
},
|
||||
/* If an initial URL is specified, then open relevant section */
|
||||
openDefaultSection() {
|
||||
if (!this.initUrl) return;
|
||||
const process = (url) => url.replace(/[^\w\s]/gi, '').toLowerCase();
|
||||
const compare = (item) => (process(item.url) === process(this.initUrl));
|
||||
this.sections.forEach((section, sectionIndex) => {
|
||||
if (section.items.findIndex(compare) !== -1) this.openSection(sectionIndex);
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.sections.length === 1) { // If only 1 section, go ahead and open it
|
||||
this.openSection(0);
|
||||
} else { // Otherwise, see if user set a default section, and open that
|
||||
this.openDefaultSection();
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -12,11 +12,11 @@ import Icon from '@/components/LinkItems/ItemIcon.vue';
|
||||
|
||||
export default {
|
||||
name: 'SideBarItem',
|
||||
inject: ['config'],
|
||||
props: {
|
||||
icon: String,
|
||||
title: String,
|
||||
url: String,
|
||||
target: String,
|
||||
click: Function,
|
||||
},
|
||||
components: {
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
itemClicked() {
|
||||
if (this.url) this.$emit('launch-app', this.url);
|
||||
if (this.url) this.$emit('launch-app', { url: this.url, target: this.target });
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
:icon="item.icon"
|
||||
:title="item.title"
|
||||
:url="item.url"
|
||||
:target="item.target"
|
||||
@launch-app="launchApp"
|
||||
/>
|
||||
</div>
|
||||
@@ -18,7 +19,6 @@ import SideBarItem from '@/components/Workspace/SideBarItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'SideBarSection',
|
||||
inject: ['config'],
|
||||
props: {
|
||||
items: Array,
|
||||
},
|
||||
@@ -26,8 +26,8 @@ export default {
|
||||
SideBarItem,
|
||||
},
|
||||
methods: {
|
||||
launchApp(url) {
|
||||
this.$emit('launch-app', url);
|
||||
launchApp(options) {
|
||||
this.$emit('launch-app', options);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
12
src/main.js
@@ -10,10 +10,12 @@ import VModal from 'vue-js-modal'; // Modal component
|
||||
import VSelect from 'vue-select'; // Select dropdown component
|
||||
import VTabs from 'vue-material-tabs'; // Tab view component, used on the config page
|
||||
import Toasted from 'vue-toasted'; // Toast component, used to show confirmation notifications
|
||||
import TreeView from 'vue-json-tree-view';
|
||||
|
||||
// Import base Dashy components and utils
|
||||
import Dashy from '@/App.vue'; // Main Dashy Vue app
|
||||
import router from '@/router'; // Router, for navigation
|
||||
import store from '@/store'; // Store, for local state management
|
||||
import serviceWorker from '@/utils/InitServiceWorker'; // Service worker initialization
|
||||
import clickOutside from '@/utils/ClickOutside'; // Directive for closing popups, modals, etc
|
||||
import { messages } from '@/utils/languages'; // Language texts
|
||||
@@ -26,6 +28,7 @@ Vue.use(VueI18n);
|
||||
Vue.use(VTooltip, tooltipOptions);
|
||||
Vue.use(VModal);
|
||||
Vue.use(VTabs);
|
||||
Vue.use(TreeView);
|
||||
Vue.use(Toasted, toastedOptions);
|
||||
Vue.component('v-select', VSelect);
|
||||
Vue.directive('clickOutside', clickOutside);
|
||||
@@ -48,9 +51,14 @@ ErrorReporting(Vue, router);
|
||||
// Render function
|
||||
const render = (awesome) => awesome(Dashy);
|
||||
|
||||
// Mount the app, with router, store i18n and render func
|
||||
const mount = () => new Vue({
|
||||
store, router, render, i18n,
|
||||
}).$mount('#app');
|
||||
|
||||
// If Keycloak not enabled, then proceed straight to the app
|
||||
if (!isKeycloakEnabled()) {
|
||||
new Vue({ router, render, i18n }).$mount('#app');
|
||||
mount();
|
||||
} else { // Keycloak is enabled, redirect to KC login page
|
||||
const { serverUrl, realm, clientId } = getKeycloakConfig();
|
||||
const initOptions = {
|
||||
@@ -63,7 +71,7 @@ if (!isKeycloakEnabled()) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
// Yay - user successfully authenticated with Keycloak, render the app!
|
||||
new Vue({ router, render, i18n }).$mount('#app');
|
||||
mount();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,20 +7,19 @@
|
||||
// Import Vue.js and vue router
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import ProgressBar from 'rsup-progress';
|
||||
|
||||
// Import views
|
||||
// Import views, that are not lazy-loaded
|
||||
import Home from '@/views/Home.vue';
|
||||
import Login from '@/views/Login.vue';
|
||||
import Workspace from '@/views/Workspace.vue';
|
||||
import Minimal from '@/views/Minimal.vue';
|
||||
import DownloadConfig from '@/views/DownloadConfig.vue';
|
||||
import ConfigAccumulator from '@/utils/ConfigAccumalator';
|
||||
|
||||
// Import helper functions, config data and defaults
|
||||
import { isAuthEnabled, isLoggedIn, isGuestAccessEnabled } from '@/utils/Auth';
|
||||
import { config } from '@/utils/ConfigHelpers';
|
||||
import { metaTagData, startingView, routePaths } from '@/utils/defaults';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
|
||||
Vue.use(Router);
|
||||
const progress = new ProgressBar({ color: 'var(--progress-bar)' });
|
||||
|
||||
/* Returns true if user is already authenticated, or if auth is not enabled */
|
||||
const isAuthenticated = () => {
|
||||
@@ -30,8 +29,18 @@ const isAuthenticated = () => {
|
||||
return (!authEnabled || userLoggedIn || guestEnabled);
|
||||
};
|
||||
|
||||
const getConfig = () => {
|
||||
const Accumulator = new ConfigAccumulator();
|
||||
return {
|
||||
appConfig: Accumulator.appConfig(),
|
||||
pageInfo: Accumulator.pageInfo(),
|
||||
};
|
||||
};
|
||||
|
||||
const { appConfig, pageInfo } = getConfig();
|
||||
|
||||
/* Get the users chosen starting view from app config, or return default */
|
||||
const getStartingView = () => config.appConfig.startingView || startingView;
|
||||
const getStartingView = () => appConfig.startingView || startingView;
|
||||
|
||||
/**
|
||||
* Returns the component that should be rendered at the base path,
|
||||
@@ -40,57 +49,59 @@ const getStartingView = () => config.appConfig.startingView || startingView;
|
||||
const getStartingComponent = () => {
|
||||
const usersPreference = getStartingView();
|
||||
switch (usersPreference) {
|
||||
case 'default': return Home;
|
||||
case 'minimal': return Minimal;
|
||||
case 'workspace': return Workspace;
|
||||
case 'minimal': return () => import('./views/Minimal.vue');
|
||||
case 'workspace': return () => import('./views/Workspace.vue');
|
||||
default: return Home;
|
||||
}
|
||||
};
|
||||
|
||||
/* Returns the meta tags for each route */
|
||||
const makeMetaTags = (defaultTitle) => ({
|
||||
title: config.pageInfo.title || defaultTitle,
|
||||
title: pageInfo.title || defaultTitle,
|
||||
metaTags: metaTagData,
|
||||
});
|
||||
|
||||
/* Routing mode, can be either 'hash', 'history' or 'abstract' */
|
||||
const mode = appConfig.routingMode || 'history';
|
||||
|
||||
/* List of all routes, props, components and metadata */
|
||||
const router = new Router({
|
||||
mode,
|
||||
routes: [
|
||||
{ // The default view can be customized by the user
|
||||
path: '/',
|
||||
name: `landing-page-${getStartingView()}`,
|
||||
component: getStartingComponent(),
|
||||
props: config,
|
||||
meta: makeMetaTags('Home Page'),
|
||||
},
|
||||
{ // Default home page
|
||||
path: routePaths.home,
|
||||
name: 'home',
|
||||
component: Home,
|
||||
props: config,
|
||||
meta: makeMetaTags('Home Page'),
|
||||
},
|
||||
{ // View only single section
|
||||
path: `${routePaths.home}/:section`,
|
||||
name: 'home-section',
|
||||
component: Home,
|
||||
meta: makeMetaTags('Home Page'),
|
||||
},
|
||||
{ // Workspace view page
|
||||
path: routePaths.workspace,
|
||||
name: 'workspace',
|
||||
component: Workspace,
|
||||
props: config,
|
||||
component: () => import('./views/Workspace.vue'),
|
||||
meta: makeMetaTags('Workspace'),
|
||||
},
|
||||
{ // Minimal view page
|
||||
path: routePaths.minimal,
|
||||
name: 'minimal',
|
||||
component: Minimal,
|
||||
props: config,
|
||||
component: () => import('./views/Minimal.vue'),
|
||||
meta: makeMetaTags('Start Page'),
|
||||
},
|
||||
{ // The login page
|
||||
path: routePaths.login,
|
||||
name: 'login',
|
||||
component: Login,
|
||||
props: {
|
||||
appConfig: config.appConfig,
|
||||
},
|
||||
component: () => import('./views/Login.vue'),
|
||||
beforeEnter: (to, from, next) => {
|
||||
// If the user already logged in + guest mode not enabled, then redirect home
|
||||
if (isAuthenticated() && !isGuestAccessEnabled()) router.push({ path: '/' });
|
||||
@@ -100,16 +111,31 @@ const router = new Router({
|
||||
{ // The about app page
|
||||
path: routePaths.about,
|
||||
name: 'about', // We lazy load the About page so as to not slow down the app
|
||||
component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
|
||||
component: () => import('./views/About.vue'),
|
||||
meta: makeMetaTags('About Dashy'),
|
||||
},
|
||||
{ // The export config page
|
||||
path: routePaths.download,
|
||||
name: 'download',
|
||||
component: DownloadConfig,
|
||||
props: config,
|
||||
component: () => import('./views/DownloadConfig.vue'),
|
||||
meta: makeMetaTags('Download Config'),
|
||||
},
|
||||
{ // Page not found, any non-defined routes will land here
|
||||
path: routePaths.notFound,
|
||||
name: '404',
|
||||
component: () => import('./views/404.vue'),
|
||||
meta: makeMetaTags('404 Not Found'),
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (to.redirectedFrom) { // Log error, if redirected here from another route
|
||||
ErrorHandler(`Route not found: '${to.redirectedFrom}'`);
|
||||
}
|
||||
next();
|
||||
},
|
||||
},
|
||||
{ // Redirect any not-found routed to the 404 view
|
||||
path: '*',
|
||||
redirect: '/404',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -119,12 +145,14 @@ const router = new Router({
|
||||
* If not logged in, prevent all access and redirect them to login page
|
||||
* */
|
||||
router.beforeEach((to, from, next) => {
|
||||
progress.start();
|
||||
if (to.name !== 'login' && !isAuthenticated()) next({ name: 'login' });
|
||||
else next();
|
||||
});
|
||||
|
||||
/* If title is missing, then apply default page title */
|
||||
router.afterEach((to) => {
|
||||
progress.end();
|
||||
Vue.nextTick(() => {
|
||||
document.title = to.meta.title || 'Dashy';
|
||||
});
|
||||
|
||||
249
src/store.js
Normal file
@@ -0,0 +1,249 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import Keys from '@/utils/StoreMutations';
|
||||
import ConfigAccumulator from '@/utils/ConfigAccumalator';
|
||||
import { componentVisibility } from '@/utils/ConfigHelpers';
|
||||
import { applyItemId } from '@/utils/MiscHelpers';
|
||||
import filterUserSections from '@/utils/CheckSectionVisibility';
|
||||
import { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const {
|
||||
INITIALIZE_CONFIG,
|
||||
SET_CONFIG,
|
||||
SET_MODAL_OPEN,
|
||||
SET_LANGUAGE,
|
||||
SET_ITEM_LAYOUT,
|
||||
SET_ITEM_SIZE,
|
||||
SET_THEME,
|
||||
SET_CUSTOM_COLORS,
|
||||
UPDATE_ITEM,
|
||||
SET_EDIT_MODE,
|
||||
SET_PAGE_INFO,
|
||||
SET_APP_CONFIG,
|
||||
SET_SECTIONS,
|
||||
UPDATE_SECTION,
|
||||
INSERT_SECTION,
|
||||
REMOVE_SECTION,
|
||||
COPY_ITEM,
|
||||
REMOVE_ITEM,
|
||||
INSERT_ITEM,
|
||||
UPDATE_CUSTOM_CSS,
|
||||
CONF_MENU_INDEX,
|
||||
} = Keys;
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
config: {},
|
||||
editMode: false, // While true, the user can drag and edit items + sections
|
||||
modalOpen: false, // KB shortcut functionality will be disabled when modal is open
|
||||
navigateConfToTab: undefined, // Used to switch active tab in config modal
|
||||
},
|
||||
getters: {
|
||||
config(state) {
|
||||
return state.config;
|
||||
},
|
||||
pageInfo(state) {
|
||||
return state.config.pageInfo || {};
|
||||
},
|
||||
appConfig(state) {
|
||||
return state.config.appConfig || {};
|
||||
},
|
||||
theme(state) {
|
||||
return state.config.appConfig.theme;
|
||||
},
|
||||
sections(state) {
|
||||
return filterUserSections(state.config.sections || []);
|
||||
},
|
||||
webSearch(state, getters) {
|
||||
return getters.appConfig.webSearch || {};
|
||||
},
|
||||
visibleComponents(state, getters) {
|
||||
return componentVisibility(getters.appConfig);
|
||||
},
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
getSectionByIndex: (state, getters) => (index) => {
|
||||
return getters.sections[index];
|
||||
},
|
||||
getItemById: (state, getters) => (id) => {
|
||||
let item;
|
||||
getters.sections.forEach(sec => {
|
||||
const foundItem = sec.items.find((itm) => itm.id === id);
|
||||
if (foundItem) item = foundItem;
|
||||
});
|
||||
return item;
|
||||
},
|
||||
getParentSectionOfItem: (state, getters) => (itemId) => {
|
||||
let foundSection;
|
||||
getters.sections.forEach((section) => {
|
||||
section.items.forEach((item) => {
|
||||
if (item.id === itemId) foundSection = section;
|
||||
});
|
||||
});
|
||||
return foundSection;
|
||||
},
|
||||
layout(state) {
|
||||
return state.config.appConfig.layout || 'auto';
|
||||
},
|
||||
iconSize(state) {
|
||||
return state.config.appConfig.iconSize || 'medium';
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
[SET_CONFIG](state, config) {
|
||||
state.config = config;
|
||||
},
|
||||
[SET_LANGUAGE](state, lang) {
|
||||
const newConfig = state.config;
|
||||
newConfig.appConfig.language = lang;
|
||||
state.config = newConfig;
|
||||
},
|
||||
[SET_MODAL_OPEN](state, modalOpen) {
|
||||
state.modalOpen = modalOpen;
|
||||
},
|
||||
[SET_EDIT_MODE](state, editMode) {
|
||||
if (editMode !== state.editMode) {
|
||||
InfoHandler(editMode ? 'Edit session started' : 'Edit session ended', InfoKeys.EDITOR);
|
||||
state.editMode = editMode;
|
||||
}
|
||||
},
|
||||
[UPDATE_ITEM](state, payload) {
|
||||
const { itemId, newItem } = payload;
|
||||
const newConfig = { ...state.config };
|
||||
newConfig.sections.forEach((section, secIndex) => {
|
||||
section.items.forEach((item, itemIndex) => {
|
||||
if (item.id === itemId) {
|
||||
newConfig.sections[secIndex].items[itemIndex] = newItem;
|
||||
InfoHandler('Item updated', InfoKeys.EDITOR);
|
||||
}
|
||||
});
|
||||
});
|
||||
state.config = newConfig;
|
||||
},
|
||||
[SET_PAGE_INFO](state, newPageInfo) {
|
||||
const newConfig = state.config;
|
||||
newConfig.pageInfo = newPageInfo;
|
||||
state.config = newConfig;
|
||||
InfoHandler('Page info updated', InfoKeys.EDITOR);
|
||||
},
|
||||
[SET_APP_CONFIG](state, newAppConfig) {
|
||||
const newConfig = state.config;
|
||||
newConfig.appConfig = newAppConfig;
|
||||
state.config = newConfig;
|
||||
InfoHandler('App config updated', InfoKeys.EDITOR);
|
||||
},
|
||||
[SET_SECTIONS](state, newSections) {
|
||||
const newConfig = state.config;
|
||||
newConfig.sections = newSections;
|
||||
state.config = newConfig;
|
||||
InfoHandler('Sections updated', InfoKeys.EDITOR);
|
||||
},
|
||||
[UPDATE_SECTION](state, payload) {
|
||||
const { sectionIndex, sectionData } = payload;
|
||||
const newConfig = { ...state.config };
|
||||
newConfig.sections[sectionIndex] = sectionData;
|
||||
state.config = newConfig;
|
||||
InfoHandler('Section updated', InfoKeys.EDITOR);
|
||||
},
|
||||
[INSERT_SECTION](state, newSection) {
|
||||
const newConfig = { ...state.config };
|
||||
newSection.items = [];
|
||||
newConfig.sections.push(newSection);
|
||||
state.config = newConfig;
|
||||
InfoHandler('New section added', InfoKeys.EDITOR);
|
||||
},
|
||||
[REMOVE_SECTION](state, payload) {
|
||||
const { sectionIndex, sectionName } = payload;
|
||||
const newConfig = { ...state.config };
|
||||
if (newConfig.sections[sectionIndex].name === sectionName) {
|
||||
newConfig.sections.splice(sectionIndex, 1);
|
||||
InfoHandler('Section removed', InfoKeys.EDITOR);
|
||||
}
|
||||
state.config = newConfig;
|
||||
},
|
||||
[INSERT_ITEM](state, payload) {
|
||||
const { newItem, targetSection } = payload;
|
||||
const config = { ...state.config };
|
||||
config.sections.forEach((section) => {
|
||||
if (section.name === targetSection) {
|
||||
section.items.push(newItem);
|
||||
InfoHandler('New item added', InfoKeys.EDITOR);
|
||||
}
|
||||
});
|
||||
config.sections = applyItemId(config.sections);
|
||||
state.config = config;
|
||||
},
|
||||
[COPY_ITEM](state, payload) {
|
||||
const { item, toSection, appendTo } = payload;
|
||||
const config = { ...state.config };
|
||||
const newItem = { ...item };
|
||||
config.sections.forEach((section) => {
|
||||
if (section.name === toSection) {
|
||||
if (appendTo === 'beginning') {
|
||||
section.items.unshift(newItem);
|
||||
} else {
|
||||
section.items.push(newItem);
|
||||
}
|
||||
InfoHandler('Item copied', InfoKeys.EDITOR);
|
||||
}
|
||||
});
|
||||
config.sections = applyItemId(config.sections);
|
||||
state.config = config;
|
||||
},
|
||||
[REMOVE_ITEM](state, payload) {
|
||||
const { itemId, sectionName } = payload;
|
||||
const config = { ...state.config };
|
||||
config.sections.forEach((section) => {
|
||||
if (section.name === sectionName) {
|
||||
section.items.forEach((item, index) => {
|
||||
if (item.id === itemId) {
|
||||
section.items.splice(index, 1);
|
||||
InfoHandler('Item removed', InfoKeys.EDITOR);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
state.config = config;
|
||||
},
|
||||
[SET_THEME](state, theme) {
|
||||
const newConfig = { ...state.config };
|
||||
newConfig.appConfig.theme = theme;
|
||||
state.config = newConfig;
|
||||
InfoHandler('Theme updated', InfoKeys.VISUAL);
|
||||
},
|
||||
[SET_CUSTOM_COLORS](state, customColors) {
|
||||
const newConfig = { ...state.config };
|
||||
newConfig.appConfig.customColors = customColors;
|
||||
state.config = newConfig;
|
||||
InfoHandler('Color palette updated', InfoKeys.VISUAL);
|
||||
},
|
||||
[SET_ITEM_LAYOUT](state, layout) {
|
||||
state.config.appConfig.layout = layout;
|
||||
InfoHandler('Layout updated', InfoKeys.VISUAL);
|
||||
},
|
||||
[SET_ITEM_SIZE](state, iconSize) {
|
||||
state.config.appConfig.iconSize = iconSize;
|
||||
InfoHandler('Item size updated', InfoKeys.VISUAL);
|
||||
},
|
||||
[UPDATE_CUSTOM_CSS](state, customCss) {
|
||||
state.config.appConfig.customCss = customCss;
|
||||
InfoHandler('Custom colors updated', InfoKeys.VISUAL);
|
||||
},
|
||||
[CONF_MENU_INDEX](state, index) {
|
||||
state.navigateConfToTab = index;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/* Called when app first loaded. Reads config and sets state */
|
||||
[INITIALIZE_CONFIG]({ commit }) {
|
||||
const deepCopy = (json) => JSON.parse(JSON.stringify(json));
|
||||
const config = deepCopy(new ConfigAccumulator().config());
|
||||
commit(SET_CONFIG, config);
|
||||
},
|
||||
},
|
||||
modules: {},
|
||||
});
|
||||
|
||||
export default store;
|
||||
@@ -42,21 +42,31 @@
|
||||
--nav-link-background-color-hover: #607d8b33;
|
||||
--nav-link-border-color: transparent;
|
||||
--nav-link-border-color-hover: var(--primary);
|
||||
--nav-link-shadow: 1px 1px 2px #232323;
|
||||
--nav-link-shadow-hover: 1px 1px 2px #232323;
|
||||
// Link items and sections
|
||||
--item-text-color: var(--primary);
|
||||
--item-text-color-hover: var(--item-text-color);
|
||||
--item-group-outer-background: var(--primary);
|
||||
--item-group-heading-text-color: var(--item-group-background);
|
||||
--item-group-heading-text-color-hover: var(--background);
|
||||
// Settings and config
|
||||
--settings-background: var(--background);
|
||||
// Homepage settings
|
||||
--settings-text-color: var(--primary);
|
||||
--config-code-background: #fff;
|
||||
--config-code-color: var(--background);
|
||||
--settings-background: var(--background);
|
||||
// Config menu
|
||||
--config-settings-color: var(--primary);
|
||||
--config-settings-background: var(--background-darker);
|
||||
--config-code-color: var(--background);
|
||||
--config-code-background: #fff;
|
||||
--code-editor-color: var(--black);
|
||||
--code-editor-background: var(--white);
|
||||
// Interactive editor
|
||||
--interactive-editor-color: var(--primary);
|
||||
--interactive-editor-background: var(--background);
|
||||
--interactive-editor-background-darker: var(--background-darker);
|
||||
// Cloud backup/ restore menu
|
||||
--cloud-backup-color: var(--config-settings-color);
|
||||
--cloud-backup-background: var(--config-settings-background);
|
||||
// Search bar (on homepage)
|
||||
--search-container-background: var(--background-darker);
|
||||
--search-field-background: var(--background);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/* Basic Page Components */
|
||||
--scroll-bar-width: 8px;
|
||||
--header-height: 6.3rem;
|
||||
--footer-height: 125px;
|
||||
--footer-height: 128px;
|
||||
|
||||
/* Section & Item dimensions */
|
||||
--item-group-padding: 5px; // Determines width of item-group outline
|
||||
|
||||
@@ -154,7 +154,6 @@ html {
|
||||
margin: var(--tooltip-arrow-size) 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-hidden='true'] {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
@@ -165,4 +164,5 @@ html {
|
||||
opacity: 1;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
&.in-modal-tt { z-index: 999; }
|
||||
}
|
||||
|
||||
101
src/styles/schema-editor.scss
Normal file
@@ -0,0 +1,101 @@
|
||||
/* Form elements in the auto-schema form */
|
||||
.schema-form {
|
||||
fieldset {
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> div {
|
||||
border-bottom: 1px dashed var(--interactive-editor-color);
|
||||
margin: 0.5rem 0;
|
||||
label {
|
||||
font-size: 1rem;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
div[data-fs-wrapper] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0;
|
||||
@include tablet-down {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
span {
|
||||
font-style: italic;
|
||||
margin-right: 0.5rem;
|
||||
max-width: 20rem;
|
||||
opacity: var(--dimming-factor);
|
||||
}
|
||||
input {
|
||||
min-width: 15rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin: 0.5rem auto;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
color: var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-background);
|
||||
border: 1px solid var(--interactive-editor-color);
|
||||
border-radius: var(--curve-factor);
|
||||
&[type=text]:focus, &[type=number]:focus {
|
||||
box-shadow: 1px 1px 6px var(--interactive-editor-color);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
input[type=checkbox] {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type=radio] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
div[data-fs-input=object], div[data-fs-input=array] {
|
||||
width: 100%;
|
||||
padding-left: 0.5rem;
|
||||
border-left: 1px dashed var(--interactive-editor-color);
|
||||
}
|
||||
div[data-fs-kind="radio"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
label {
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
select {
|
||||
width: 15rem;
|
||||
height: 2rem;
|
||||
padding: 0.2rem;
|
||||
font-size: 1rem;
|
||||
color: var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-background);
|
||||
border: 1px solid var(--interactive-editor-color);
|
||||
border-radius: var(--curve-factor);
|
||||
&:focus {
|
||||
box-shadow: 1px 1px 6px var(--interactive-editor-color);
|
||||
}
|
||||
}
|
||||
div[data-fs-input=array] button {
|
||||
font-size: 1rem;
|
||||
margin: 0.25rem;
|
||||
border-radius: var(--curve-factor);
|
||||
color: var(--interactive-editor-color);
|
||||
background: var(--interactive-editor-background);
|
||||
border: 1px solid var(--interactive-editor-color);
|
||||
&:hover {
|
||||
color: var(--interactive-editor-background);
|
||||
background: var(--interactive-editor-color);
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 1px 1px 6px var(--interactive-editor-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
@import '@/styles/media-queries.scss';
|
||||
|
||||
/* Fancy scrollbar */
|
||||
.scroll-bar {
|
||||
@@ -34,6 +35,15 @@
|
||||
background: var(--settings-text-color);
|
||||
path { fill: var(--background); }
|
||||
}
|
||||
&.disabled {
|
||||
opacity: var(--dimming-factor);
|
||||
cursor: not-allowed;
|
||||
&:hover {
|
||||
border: 1px solid currentColor;
|
||||
background: var(--background);
|
||||
path { fill: var(--settings-text-color); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +59,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Single-style helpers */
|
||||
.bold { font-weight: bold; }
|
||||
.light { font-weight: lighter; }
|
||||
|
||||
@@ -38,7 +38,6 @@ html {
|
||||
|
||||
/* Monospace, for code and raw data output */
|
||||
code, pre, pre *, .jsoneditor *, .mono * {
|
||||
font-family: var(--font-monospace);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ const getAppConfig = () => {
|
||||
* Support for old user structure will be removed in V 1.7.0
|
||||
*/
|
||||
const printWarning = () => {
|
||||
const msg = 'From V 1.6.5 onwards, the structure of the users object has changed.';
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(msg);
|
||||
ErrorHandler('From V 1.6.5 onwards, the structure of the users object has changed.');
|
||||
};
|
||||
|
||||
/* Returns true if keycloak is enabled */
|
||||
@@ -48,7 +46,7 @@ const getUsers = () => {
|
||||
// Check if the user is still using previous schema type
|
||||
if (Array.isArray(auth)) {
|
||||
printWarning(); // Print warning message
|
||||
return auth; // Let the user proceed anyway, will remove in V 1.7.0
|
||||
return []; // Support for old data structure now removed
|
||||
}
|
||||
// Otherwise, return the users array, if available
|
||||
return auth.users || [];
|
||||
@@ -97,12 +95,7 @@ export const isAuthEnabled = () => {
|
||||
/* Returns true if guest access is enabled */
|
||||
export const isGuestAccessEnabled = () => {
|
||||
const appConfig = getAppConfig();
|
||||
if (appConfig.enableGuestAccess) {
|
||||
// User is still using the old auth method
|
||||
printWarning();
|
||||
return true;
|
||||
}
|
||||
if (appConfig.auth && !Array.isArray(appConfig.auth)) {
|
||||
if (appConfig.auth && typeof appConfig.auth === 'object') {
|
||||
return appConfig.auth.enableGuestAccess || false;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
pageInfo as defaultPageInfo,
|
||||
iconSize as defaultIconSize,
|
||||
layout as defaultLayout,
|
||||
// language as defaultLanguage,
|
||||
} from '@/utils/defaults';
|
||||
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import { applyItemId } from '@/utils/MiscHelpers';
|
||||
import conf from '../../public/conf.yml';
|
||||
|
||||
export default class ConfigAccumulator {
|
||||
@@ -46,42 +46,36 @@ export default class ConfigAccumulator {
|
||||
|
||||
/* Page Info */
|
||||
pageInfo() {
|
||||
const defaults = defaultPageInfo;
|
||||
let localPageInfo;
|
||||
try {
|
||||
localPageInfo = JSON.parse(localStorage[localStorageKeys.PAGE_INFO]);
|
||||
} catch (e) {
|
||||
localPageInfo = {};
|
||||
let localPageInfo = {};
|
||||
if (localStorage[localStorageKeys.PAGE_INFO]) {
|
||||
// eslint-disable-next-line brace-style
|
||||
try { localPageInfo = JSON.parse(localStorage[localStorageKeys.PAGE_INFO]); }
|
||||
catch (e) { ErrorHandler('Malformed pageInfo data in local storage'); }
|
||||
}
|
||||
let filePageInfo = {};
|
||||
if (this.conf) {
|
||||
filePageInfo = this.conf.pageInfo || {};
|
||||
}
|
||||
const pi = filePageInfo || defaults; // The page info object to return
|
||||
pi.title = localPageInfo.title || filePageInfo.title || defaults.title;
|
||||
pi.logo = localPageInfo.logo || filePageInfo.logo || defaults.logo;
|
||||
pi.description = localPageInfo.description || filePageInfo.description || defaults.description;
|
||||
pi.navLinks = localPageInfo.navLinks || filePageInfo.navLinks || defaults.navLinks;
|
||||
pi.footerText = localPageInfo.footerText || filePageInfo.footerText || defaults.footerText;
|
||||
return pi;
|
||||
const filePageInfo = this.conf ? this.conf.pageInfo || {} : {};
|
||||
return { ...defaultPageInfo, ...filePageInfo, ...localPageInfo };
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
sections() {
|
||||
let sections = [];
|
||||
// If the user has stored sections in local storage, return those
|
||||
const localSections = localStorage[localStorageKeys.CONF_SECTIONS];
|
||||
if (localSections) {
|
||||
try {
|
||||
const json = JSON.parse(localSections);
|
||||
if (json.length >= 1) return json;
|
||||
if (json.length >= 1) sections = json;
|
||||
} catch (e) {
|
||||
// The data in local storage has been malformed, will return conf.sections instead
|
||||
ErrorHandler('Malformed section data in local storage');
|
||||
}
|
||||
}
|
||||
// If the function hasn't yet returned, then return the config file sections
|
||||
let sectionsFile = [];
|
||||
if (this.conf) sectionsFile = this.conf.sections || [];
|
||||
return sectionsFile;
|
||||
// If sections were not set from local data, then use config file instead
|
||||
if (sections.length === 0) {
|
||||
sections = this.conf ? this.conf.sections || [] : [];
|
||||
}
|
||||
// Apply a unique ID to each item
|
||||
sections = applyItemId(sections);
|
||||
return sections;
|
||||
}
|
||||
|
||||
/* Complete config */
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
theme as defaultTheme,
|
||||
language as defaultLanguage,
|
||||
} from '@/utils/defaults';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import ConfigSchema from '@/utils/ConfigSchema.json';
|
||||
|
||||
/**
|
||||
* Initiates the Accumulator class and generates a complete config object
|
||||
@@ -97,3 +99,17 @@ export const getUsersLanguage = () => {
|
||||
const langObj = languages.find(lang => lang.code === langCode);
|
||||
return langObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* validator for item target attribute
|
||||
* Uses enum values from config schema, and shows warning if invalid
|
||||
* @param {String} target
|
||||
* @returns {Boolean} isValid
|
||||
*/
|
||||
export const targetValidator = (target) => {
|
||||
const acceptedTargets = ConfigSchema.properties.sections.items
|
||||
.properties.items.items.properties.target.enum;
|
||||
const isTargetValid = acceptedTargets.indexOf(target) !== -1;
|
||||
if (!isTargetValid) ErrorHandler(`Unknown target value: ${target}`);
|
||||
return isTargetValid;
|
||||
};
|
||||
|
||||
@@ -9,16 +9,19 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"title": "Title",
|
||||
"type": "string",
|
||||
"description": "Title and heading for the app"
|
||||
},
|
||||
"description": {
|
||||
"title": "Description",
|
||||
"type": "string",
|
||||
"description": "Sub-title, displayed in header"
|
||||
},
|
||||
"navLinks": {
|
||||
"type": "array",
|
||||
"maxItems": 6,
|
||||
"title": "Navigation Links",
|
||||
"description": "Quick access links, displayed in header",
|
||||
"items": {
|
||||
"type": "object",
|
||||
@@ -38,12 +41,15 @@
|
||||
}
|
||||
},
|
||||
"footerText": {
|
||||
"title": "Footer Text",
|
||||
"description": "Content to display within the global page footer",
|
||||
"type": "string"
|
||||
},
|
||||
"logo": {
|
||||
"title": "App Logo",
|
||||
"type": "string",
|
||||
"description": "Path to an optional image asset, to be displayed in the header",
|
||||
"pattern": "^(http|/)",
|
||||
"pattern": "^(http|/)(.*?)",
|
||||
"examples": [
|
||||
"/web-icons/dashy-logo.png",
|
||||
"https://i.ibb.co/yhbt6CY/dashy.png"
|
||||
@@ -57,17 +63,10 @@
|
||||
},
|
||||
"appConfig": {
|
||||
"type": "object",
|
||||
"description": "Application configuration",
|
||||
"properties": {
|
||||
"backgroundImg": {
|
||||
"type": "string",
|
||||
"description": "A URL to an image asset to be displayed as background"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "The ISO code of your desired language, must have translations present, check docs for more info"
|
||||
},
|
||||
"startingView": {
|
||||
"title": "Starting View",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"minimal",
|
||||
@@ -76,7 +75,39 @@
|
||||
"default": "default",
|
||||
"description": "Which page to load by default, and on the base page or domain root. You can still switch to different views from within the UI"
|
||||
},
|
||||
"defaultOpeningMethod": {
|
||||
"title": "Default Opening Method",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"newtab",
|
||||
"sametab",
|
||||
"parent",
|
||||
"top",
|
||||
"modal",
|
||||
"workspace"
|
||||
],
|
||||
"default": "newtab",
|
||||
"description": "The default opening method for items. Only used if no item.target is specified"
|
||||
},
|
||||
"statusCheck": {
|
||||
"title": "Enable Status Checks",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Displays an online/ offline status for each of your services"
|
||||
},
|
||||
"statusCheckInterval": {
|
||||
"title": "Status Check Interval",
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "How often to recheck statuses. If set to 0, status will only be checked on page load"
|
||||
},
|
||||
"language": {
|
||||
"title": "Language",
|
||||
"type": "string",
|
||||
"description": "The ISO code of your desired language, must have translations present, check docs for more info"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme",
|
||||
"type": "string",
|
||||
"default": "callisto",
|
||||
"description": "A theme to be applied by default on first load",
|
||||
@@ -104,17 +135,14 @@
|
||||
"high-contrast-light"
|
||||
]
|
||||
},
|
||||
"enableFontAwesome": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Should load font-awesome assets"
|
||||
},
|
||||
"fontAwesomeKey": {
|
||||
"backgroundImg": {
|
||||
"title": "Background Image",
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]{10}$",
|
||||
"description": "API key for font-awesome"
|
||||
"description": "A URL to an image asset to be displayed as background"
|
||||
},
|
||||
"faviconApi": {
|
||||
"title": "Favicon API",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"local",
|
||||
"faviconkit",
|
||||
@@ -127,6 +155,8 @@
|
||||
"description": "Which service to use to resolve favicons. Set to local to do this locally instead"
|
||||
},
|
||||
"layout": {
|
||||
"title": "Default Layout",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"horizontal",
|
||||
"vertical",
|
||||
@@ -137,6 +167,8 @@
|
||||
"description": "Specifies sections layout orientation on the home screen"
|
||||
},
|
||||
"iconSize": {
|
||||
"title": "Default Icon Size",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"small",
|
||||
"medium",
|
||||
@@ -145,88 +177,48 @@
|
||||
"default": "medium",
|
||||
"description": "The size of each link item / icon"
|
||||
},
|
||||
"hideComponents": {
|
||||
"type": "object",
|
||||
"description": "Hide individual parts of the page. If not set, all components are visible by default",
|
||||
"properties": {
|
||||
"hideHeading": {
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "If set to true, the page heading & subtitle will be hidden"
|
||||
},
|
||||
"hideNav": {
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "If set to true, the navigation menu will be hidden"
|
||||
},
|
||||
"hideSearch": {
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "If set to true, the search bar will be hidden"
|
||||
},
|
||||
"hideSettings": {
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "If set to true, the settings buttons will be hidden"
|
||||
},
|
||||
"hideFooter": {
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "If set to true, the page footer will be hidden"
|
||||
},
|
||||
"hideSplashScreen": {
|
||||
"type": "boolean",
|
||||
"default": "true",
|
||||
"description": "If set to true, the loading / splash screen will not be shown"
|
||||
}
|
||||
}
|
||||
"colCount": {
|
||||
"title": "Column Count",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"maximum": 8,
|
||||
"description": "Number of section columns for homepage. Leave blank for column count to be responsively calculated based on screen size"
|
||||
},
|
||||
"cssThemes": {
|
||||
"type": "array",
|
||||
"description": "Theme names to be added to the dropdown",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"customColors": {
|
||||
"type": "object",
|
||||
"description": "Set a custom color palette for any theme"
|
||||
},
|
||||
"externalStyleSheet": {
|
||||
"description": "URL or URLs of external stylesheets to add to dropdown/ load",
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"customCss": {
|
||||
"routingMode": {
|
||||
"title": "Routing Mode",
|
||||
"type": "string",
|
||||
"description": "Any custom CSS overides, must be minified"
|
||||
"enum": [
|
||||
"hash",
|
||||
"history"
|
||||
],
|
||||
"default": "history",
|
||||
"description": "The Vue routing mode to use, history mode will remove the annoying hash from the URL, but requires some extra config on some systems"
|
||||
},
|
||||
"statusCheck": {
|
||||
"workspaceLandingUrl": {
|
||||
"title": "Workspace Landing URL",
|
||||
"type": "string",
|
||||
"description": "The URL of an app, service or website to render when the Workspace view is opened"
|
||||
},
|
||||
"enableMultiTasking": {
|
||||
"title": "Enable Multi-Tasking",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Displays an online/ offline status for each of your services"
|
||||
},
|
||||
"statusCheckInterval": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "How often to recheck statuses. If set to 0, status will only be checked on page load"
|
||||
"description": "If set to true, will keep apps opened in the workspace open in the background. Useful for switching between sites, but comes at the cost of performance"
|
||||
},
|
||||
"webSearch": {
|
||||
"title": "Web Search",
|
||||
"type": "object",
|
||||
"description": "Configure options for web search",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"disableWebSearch": {
|
||||
"title": "Disable Web Search?",
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "If set to true, web search will be disabled all together"
|
||||
},
|
||||
"searchEngine": {
|
||||
"title": "Search Engine",
|
||||
"type": "string",
|
||||
"default": "duckduckgo",
|
||||
"description": "Set your default search engine. Reference provider by key, see docs for all supported search engines, or set to custom to use your own",
|
||||
@@ -248,10 +240,13 @@
|
||||
]
|
||||
},
|
||||
"customSearchEngine": {
|
||||
"title": "Custom Search Engine",
|
||||
"type": "string",
|
||||
"description": "Set the URL of a self-hosted or custom search engine, including GET query params. You must also set searchEngine: custom"
|
||||
},
|
||||
"openingMethod": {
|
||||
"title": "Search Opening Method",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"newtab",
|
||||
"sametab",
|
||||
@@ -262,6 +257,7 @@
|
||||
"description": "Set where you would like search results to open to"
|
||||
},
|
||||
"searchBangs": {
|
||||
"title": "Search Bangs",
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"examples": [
|
||||
@@ -274,17 +270,101 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"enableFontAwesome": {
|
||||
"title": "Enable Font-Awesome?",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Should load font-awesome assets"
|
||||
},
|
||||
"fontAwesomeKey": {
|
||||
"title": "Font-Awesome API Key",
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]{10}$",
|
||||
"description": "API key for font-awesome"
|
||||
},
|
||||
"cssThemes": {
|
||||
"title": "Additional CSS Themes",
|
||||
"type": "array",
|
||||
"description": "Theme names to be added to the dropdown, once added you can then add custom CSS to style your theme",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"customColors": {
|
||||
"title": "Custom Colors",
|
||||
"type": "object",
|
||||
"description": "Set a custom color palette for any theme, see docs for more info"
|
||||
},
|
||||
"externalStyleSheet": {
|
||||
"title": "External Stylesheets",
|
||||
"description": "List of URLs of external stylesheets to add to dropdown/ load",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"customCss": {
|
||||
"title": "Custom CSS",
|
||||
"type": "string",
|
||||
"description": "Any custom CSS overides to be applied globally, should be minified"
|
||||
},
|
||||
"hideComponents": {
|
||||
"title": "Hidden Components",
|
||||
"type": "object",
|
||||
"description": "Hide individual parts of the page. If not set, all components are visible by default",
|
||||
"properties": {
|
||||
"hideHeading": {
|
||||
"title": "Hide Heading?",
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "If set to true, the page heading & subtitle will be hidden"
|
||||
},
|
||||
"hideNav": {
|
||||
"title": "Hide Nav Bar?",
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "If set to true, the navigation menu will be hidden"
|
||||
},
|
||||
"hideSearch": {
|
||||
"title": "Hide Search Bar?",
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "If set to true, the search bar will be hidden"
|
||||
},
|
||||
"hideSettings": {
|
||||
"title": "Hide Settings?",
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "If set to true, the settings buttons will be hidden"
|
||||
},
|
||||
"hideFooter": {
|
||||
"title": "Hide Footer?",
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "If set to true, the page footer will be hidden"
|
||||
},
|
||||
"hideSplashScreen": {
|
||||
"title": "Hide Splash Screen?",
|
||||
"type": "boolean",
|
||||
"default": "true",
|
||||
"description": "If set to true, the loading / splash screen will not be shown"
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"title": "Authentication",
|
||||
"type": "object",
|
||||
"description": "Settings for enabling authentication",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enableGuestAccess": {
|
||||
"title": "Enable Guest Mode?",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If set to true, an unauthenticated user will be able to have read-only access to dashboard, without needing to login. Requires auth to be configured."
|
||||
},
|
||||
"users": {
|
||||
"title": "Users",
|
||||
"type": "array",
|
||||
"description": "Usernames and hashed credentials for frontend authentication",
|
||||
"items": {
|
||||
@@ -296,16 +376,20 @@
|
||||
],
|
||||
"properties": {
|
||||
"user": {
|
||||
"title": "Username",
|
||||
"type": "string",
|
||||
"description": "The username for a user"
|
||||
},
|
||||
"hash": {
|
||||
"title": "Hashed Pass",
|
||||
"type": "string",
|
||||
"description": "A SHA-256 hashed password for that user",
|
||||
"minLength": 64,
|
||||
"maxLength": 64
|
||||
},
|
||||
"type": {
|
||||
"title": "Privileges",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"admin",
|
||||
"normal"
|
||||
@@ -317,6 +401,7 @@
|
||||
}
|
||||
},
|
||||
"enableKeycloak": {
|
||||
"title": "Enable Keycloak?",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If set to true, and auth.keycloak is also configured, then Keycloak will be used for app auth"
|
||||
@@ -332,14 +417,17 @@
|
||||
],
|
||||
"properties": {
|
||||
"serverUrl": {
|
||||
"title": "Server URL",
|
||||
"type": "string",
|
||||
"description": "The URL (or URL/ IP + Port) where your keycloak server is running"
|
||||
},
|
||||
"realm": {
|
||||
"title": "Realm",
|
||||
"type": "string",
|
||||
"description": "The name of the realm (must already be created) that you want to use"
|
||||
},
|
||||
"clientId": {
|
||||
"title": "Client ID",
|
||||
"type": "string",
|
||||
"description": "The Client ID of the client you created for use with Dashy"
|
||||
}
|
||||
@@ -347,42 +435,44 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"enableMultiTasking": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If set to true, will keep apps opened in the workspace open in the background. Useful for switching between sites, but comes at the cost of performance"
|
||||
},
|
||||
"allowConfigEdit": {
|
||||
"title": "Allow Config Editing",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Can user write changes to conf.yml file from the UI. If set to false, preferences are only stored locally"
|
||||
},
|
||||
"enableServiceWorker": {
|
||||
"title": "Enable Service Worker",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If set to true, then service workers will be used to cache page contents"
|
||||
},
|
||||
"disableContextMenu": {
|
||||
"title": "Disable Context Menus",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If set to true, custom right-click context menu will be disabled"
|
||||
},
|
||||
"disableUpdateChecks": {
|
||||
"title": "Disable Update Checks",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Prevents Dashy from checking for updates"
|
||||
},
|
||||
"disableSmartSort": {
|
||||
"title": "Disable Smart-Sort",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Prevents the app storing local click count, required for the last-used and most-used sort orders"
|
||||
},
|
||||
"enableErrorReporting": {
|
||||
"title": "Enable Error Reporting",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable anonymous crash reports. This helps bugs be found and fixed, in order to make Dashy more stable. Reporting is off by default, and no data will EVER be collected without your explicit and active concent."
|
||||
},
|
||||
"sentryDsn": {
|
||||
"title": "Custom Sentry DSN",
|
||||
"type": "string",
|
||||
"description": "The DSN to your self-hosted Sentry server, if you need to collect bug reports. Only used if enableErrorReporting is enabled"
|
||||
}
|
||||
@@ -393,6 +483,7 @@
|
||||
"type": "array",
|
||||
"description": "Array of sections, containing items",
|
||||
"items": {
|
||||
"title": "Items",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
@@ -401,19 +492,24 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"title": "Section Name",
|
||||
"type": "string",
|
||||
"description": "Title/ heading for a section"
|
||||
},
|
||||
"icon": {
|
||||
"title": "Section Icon",
|
||||
"type": "string",
|
||||
"description": "Icon will be displayed next to title"
|
||||
},
|
||||
"displayData": {
|
||||
"title": "Display Data",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Optional meta data for customizing a section",
|
||||
"properties": {
|
||||
"sortBy": {
|
||||
"title": "Sort By",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"most-used",
|
||||
@@ -426,19 +522,24 @@
|
||||
"description": "How to sort items within the section. By default items are displayed in the order in which they are listed in within the config"
|
||||
},
|
||||
"collapsed": {
|
||||
"title": "Is Collapsed?",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If true, section needs to be clicked to open"
|
||||
},
|
||||
"color": {
|
||||
"title": "Color",
|
||||
"type": "string",
|
||||
"description": "Hex code, or HTML color for section fill"
|
||||
},
|
||||
"customStyles": {
|
||||
"title": "Custom Styles",
|
||||
"type": "string",
|
||||
"description": "CSS overides for section container"
|
||||
},
|
||||
"itemSize": {
|
||||
"title": "Item Size",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"small",
|
||||
"medium",
|
||||
@@ -448,6 +549,7 @@
|
||||
"description": "Size of items within the section"
|
||||
},
|
||||
"rows": {
|
||||
"title": "Num Rows",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
@@ -455,6 +557,7 @@
|
||||
"description": "The amount of space that the section spans vertically"
|
||||
},
|
||||
"cols": {
|
||||
"title": "Num Cols",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
@@ -462,6 +565,8 @@
|
||||
"description": "The amount of space that the section spans horizontally"
|
||||
},
|
||||
"sectionLayout": {
|
||||
"title": "Layout Type",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"grid",
|
||||
"auto"
|
||||
@@ -470,18 +575,21 @@
|
||||
"description": "If set to grid, items have uniform width, and itemCount can be set"
|
||||
},
|
||||
"itemCountX": {
|
||||
"title": "Item Count X",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"maximum": 12,
|
||||
"description": "Number of items per column"
|
||||
},
|
||||
"itemCountY": {
|
||||
"title": "Item Count Y",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
"maximum": 12,
|
||||
"description": "Number of items per row"
|
||||
},
|
||||
"hideForUsers": {
|
||||
"title": "Hide for Users",
|
||||
"type": "array",
|
||||
"description": "Section will be visible to all users, except for those specified in this list",
|
||||
"items": {
|
||||
@@ -490,6 +598,7 @@
|
||||
}
|
||||
},
|
||||
"showForUsers": {
|
||||
"title": "Show for Users",
|
||||
"type": "array",
|
||||
"description": "Section will be hidden from all users, except for those specified in this list",
|
||||
"items": {
|
||||
@@ -498,6 +607,7 @@
|
||||
}
|
||||
},
|
||||
"hideForGuests": {
|
||||
"title": "Hide for Guests?",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If set to true, section will be visible for logged in users, but not for guests"
|
||||
@@ -505,6 +615,7 @@
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"title": "Items",
|
||||
"type": "array",
|
||||
"description": "Array of items to display with a section",
|
||||
"items": {
|
||||
@@ -515,70 +626,91 @@
|
||||
],
|
||||
"properties": {
|
||||
"title": {
|
||||
"title": "Item Text",
|
||||
"type": "string",
|
||||
"description": "Text shown on the item"
|
||||
"description": "Title of the item"
|
||||
},
|
||||
"description": {
|
||||
"title": "Description",
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Short description, shown on hover or in a tooltip"
|
||||
},
|
||||
"icon": {
|
||||
"title": "Icon",
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "An icon, either as a font-awesome identifier, local or remote URL, or the word favicon or generative"
|
||||
"description": "An icon, either as a font-awesome, simple-icon or mdi identifier, emoji, favicon, generative or the URL/ path to a local or remote icon asset"
|
||||
},
|
||||
"url": {
|
||||
"title": "Service URL",
|
||||
"type": "string",
|
||||
"description": "The destination to navigate to when item is clicked"
|
||||
"description": "The destination to navigate to when item is clicked, expressed as a valid URL, IP or hostname"
|
||||
},
|
||||
"target": {
|
||||
"title": "Opening Method",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"newtab",
|
||||
"sametab",
|
||||
"parent",
|
||||
"top",
|
||||
"modal",
|
||||
"workspace"
|
||||
],
|
||||
"default": "newtab",
|
||||
"description": "Opening method, when item is clicked"
|
||||
"description": "Where / how the item is opened when it's clicked"
|
||||
},
|
||||
"hotkey": {
|
||||
"title": "Hot Key",
|
||||
"type": "number",
|
||||
"description": "A numeric shortcut key, between 0 and 9. Useful for quickly launching frequently used applications"
|
||||
},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
"type": "array",
|
||||
"description": "Tags, which can be used for improved search",
|
||||
"description": "A list of tags for improved search. Separate using a comma",
|
||||
"maxItems": 12,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"description": "A custom fill color of the item"
|
||||
},
|
||||
"provider": {
|
||||
"title": "Provider",
|
||||
"type": "string",
|
||||
"description": "Provider name, e.g. Microsoft"
|
||||
"description": "Provider name, e.g. Microsoft, Nebucasa, DigitalOcean, etc"
|
||||
},
|
||||
"statusCheck": {
|
||||
"title": "Enable Status Check",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether or not to display online/ offline status for this service. Will override appConfig.statusCheck"
|
||||
},
|
||||
"statusCheckUrl": {
|
||||
"title": "Status Check URL",
|
||||
"type": "string",
|
||||
"description": "If you've enabled statusCheck, and want to use a different URL to what is defined under the item, then specify it here"
|
||||
"description": "Custom status check endpoint for this item. Useful if the default URL doesn't return 200, or if your service has a dedicated status check endpoint"
|
||||
},
|
||||
"statusCheckHeaders": {
|
||||
"title": "Status Check Headers",
|
||||
"type": "object",
|
||||
"description": " If you're endpoint requires any specific headers for the status checking, then define them here"
|
||||
"description": " Custom headers for status checking, useful if your service requires authorization headers to return a 200"
|
||||
},
|
||||
"statusCheckAllowInsecure": {
|
||||
"title": "Status Check Disable SSL",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Allows for running status checks on insecure content/ non-HTTPS apps"
|
||||
"description": "Allows for running status checks on insecure content/ non-HTTPS apps. Prevents checks failing for non-SSL sites"
|
||||
},
|
||||
"color": {
|
||||
"title": "Custom Color",
|
||||
"type": "string",
|
||||
"description": "A custom fill color of the item, expressed either as hex code or color name"
|
||||
},
|
||||
"id": {
|
||||
"title": "Item ID",
|
||||
"type": "string",
|
||||
"description": "Unique ID for each item. Generated automatically, shouldn't need to be set manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export const statusMsg = (title, msg) => {
|
||||
/* Prints status message, with a stack trace */
|
||||
export const statusErrorMsg = (title, msg, errorLog) => {
|
||||
console.log(
|
||||
`%c${title || ''}\n%c${msg} \n%c${errorLog}`,
|
||||
`%c${title || ''}\n%c${msg} \n%c${errorLog || ''}`,
|
||||
'font-weight: bold; color: #0dd8d8; text-decoration: underline;',
|
||||
'color: #ff025a',
|
||||
'color: #ff025a80;',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Sentry from '@sentry/vue';
|
||||
import { warningMsg, statusMsg } from '@/utils/CoolConsole';
|
||||
import { warningMsg, statusMsg, statusErrorMsg } from '@/utils/CoolConsole';
|
||||
import { sessionStorageKeys } from '@/utils/defaults';
|
||||
|
||||
/* Makes the current time, like hh:mm:ss */
|
||||
@@ -33,4 +33,18 @@ export const InfoHandler = (msg, title) => {
|
||||
statusMsg(title || 'Info', msg);
|
||||
};
|
||||
|
||||
/* Outputs warnings caused by the user, such as missing field */
|
||||
export const WarningInfoHandler = (msg, title, log) => {
|
||||
statusErrorMsg(title || 'Warning', msg, log);
|
||||
};
|
||||
|
||||
/* Titles for info logging */
|
||||
export const InfoKeys = {
|
||||
AUTH: 'Authentication',
|
||||
CLOUD_BACKUP: 'Cloud Backup & Restore',
|
||||
EDITOR: 'Interactive Editor',
|
||||
RAW_EDITOR: 'Raw Config Editor',
|
||||
VISUAL: 'Layout & Styles',
|
||||
};
|
||||
|
||||
export default ErrorHandler;
|
||||
|
||||
@@ -3,4 +3,46 @@ import { hideFurnitureOn } from '@/utils/defaults';
|
||||
/* Returns false if page furniture should be hidden on said route */
|
||||
export const shouldBeVisible = (routeName) => !hideFurnitureOn.includes(routeName);
|
||||
|
||||
export const x = () => null;
|
||||
/* Very rudimentary hash function for generative icons */
|
||||
export const asciiHash = (input) => {
|
||||
const str = (!input || input.length === 0) ? Math.random().toString() : input;
|
||||
const reducer = (previousHash, char) => (previousHash || 0) + char.charCodeAt(0);
|
||||
const asciiSum = str.split('').reduce(reducer).toString();
|
||||
const shortened = asciiSum.slice(0, 30) + asciiSum.slice(asciiSum.length - 30);
|
||||
return window.btoa(shortened);
|
||||
};
|
||||
|
||||
/* Encode potentially malicious characters from string */
|
||||
export const sanitize = (string) => {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
};
|
||||
const reg = /[&<>"'/]/ig;
|
||||
return string.replace(reg, (match) => (map[match]));
|
||||
};
|
||||
|
||||
/* Based on section title, item name and index, return a string value for ID */
|
||||
const makeItemId = (sectionStr, itemStr, index) => {
|
||||
const charSum = sectionStr.split('').map((a) => a.charCodeAt(0)).reduce((x, y) => x + y);
|
||||
const itemTitleStr = itemStr.replace(/\s+/g, '-').replace(/[^a-zA-Z ]/g, '').toLowerCase();
|
||||
return `${index}_${charSum}_${itemTitleStr}`;
|
||||
};
|
||||
|
||||
/* Given an array of sections, apply a unique ID to each item, and return modified array */
|
||||
export const applyItemId = (inputSections) => {
|
||||
const sections = inputSections || [];
|
||||
sections.forEach((sec, secIdx) => {
|
||||
if (sec.items) {
|
||||
sec.items.forEach((item, itemIdx) => {
|
||||
sections[secIdx].items[itemIdx].id = makeItemId(sec.name, item.title, itemIdx);
|
||||
// TODO: Check if ID already exists, and if so, modify it
|
||||
});
|
||||
}
|
||||
});
|
||||
return sections;
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ const getDomainFromUrl = (url) => {
|
||||
*/
|
||||
const filterHelper = (compareStr, searchStr) => {
|
||||
if (!compareStr) return false;
|
||||
const process = (input) => input.toString().toLowerCase().replace(/[^\w\s]/gi, '');
|
||||
const process = (input) => input && input.toString().toLowerCase().replace(/[^\w\s]/gi, '');
|
||||
return process(compareStr).includes(process(searchStr));
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ const filterHelper = (compareStr, searchStr) => {
|
||||
* @returns A filtered array of tiles
|
||||
*/
|
||||
export const searchTiles = (allTiles, searchTerm) => {
|
||||
if (!searchTerm) return allTiles; // If no search term, then return all
|
||||
if (!allTiles) return []; // If no data, then skip
|
||||
return allTiles.filter((tile) => {
|
||||
const {
|
||||
|
||||
29
src/utils/StoreMutations.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// A list of mutation names
|
||||
const KEY_NAMES = [
|
||||
'INITIALIZE_CONFIG',
|
||||
'SET_CONFIG',
|
||||
'SET_MODAL_OPEN',
|
||||
'SET_LANGUAGE',
|
||||
'SET_EDIT_MODE',
|
||||
'SET_ITEM_LAYOUT',
|
||||
'SET_ITEM_SIZE',
|
||||
'SET_THEME',
|
||||
'SET_CUSTOM_COLORS',
|
||||
'UPDATE_ITEM',
|
||||
'SET_PAGE_INFO',
|
||||
'SET_APP_CONFIG',
|
||||
'SET_SECTIONS',
|
||||
'UPDATE_SECTION',
|
||||
'INSERT_SECTION',
|
||||
'REMOVE_SECTION',
|
||||
'COPY_ITEM',
|
||||
'REMOVE_ITEM',
|
||||
'INSERT_ITEM',
|
||||
'UPDATE_CUSTOM_CSS',
|
||||
'CONF_MENU_INDEX',
|
||||
];
|
||||
|
||||
// Convert array of key names into an object, and export
|
||||
const MUTATIONS = {};
|
||||
KEY_NAMES.forEach((key) => { MUTATIONS[key] = key; });
|
||||
export default MUTATIONS;
|
||||
@@ -24,7 +24,7 @@ module.exports = {
|
||||
/* Default Font-Awesome API key, for FA icons (if used) */
|
||||
fontAwesomeKey: '0821c65656',
|
||||
/* Default API to use for fetching of user service favicon icons (if enabled) */
|
||||
faviconApi: 'faviconkit',
|
||||
faviconApi: 'allesedv',
|
||||
/* The default sort order for sections */
|
||||
sortOrder: 'default',
|
||||
/* The page paths for each route within the app for the router */
|
||||
@@ -35,6 +35,7 @@ module.exports = {
|
||||
about: '/about',
|
||||
login: '/login',
|
||||
download: '/download',
|
||||
notFound: '/404',
|
||||
},
|
||||
/* Server Endpoints */
|
||||
serviceEndpoints: {
|
||||
@@ -49,17 +50,21 @@ module.exports = {
|
||||
'oblivion',
|
||||
'material',
|
||||
'material-dark',
|
||||
'dracula',
|
||||
'colorful',
|
||||
'dashy-docs',
|
||||
'colorful',
|
||||
'one-dark',
|
||||
'dracula',
|
||||
'adventure',
|
||||
'nord-frost',
|
||||
'nord',
|
||||
'minimal-dark',
|
||||
'minimal-light',
|
||||
'nord',
|
||||
'nord-frost',
|
||||
'thebe',
|
||||
'cyberpunk',
|
||||
'matrix',
|
||||
'matrix-red',
|
||||
'color-block',
|
||||
'glow',
|
||||
'raspberry-jam',
|
||||
'bee',
|
||||
'tiger',
|
||||
@@ -71,8 +76,9 @@ module.exports = {
|
||||
],
|
||||
/* Which structural components should be visible by default */
|
||||
visibleComponents: {
|
||||
pageTitle: true,
|
||||
splashScreen: false,
|
||||
navigation: true,
|
||||
pageTitle: true,
|
||||
searchBar: true,
|
||||
settings: true,
|
||||
footer: true,
|
||||
@@ -83,6 +89,7 @@ module.exports = {
|
||||
'login',
|
||||
'download',
|
||||
'landing-page-minimal',
|
||||
// '404',
|
||||
],
|
||||
/* Key names for local storage identifiers */
|
||||
localStorageKeys: {
|
||||
@@ -115,11 +122,15 @@ module.exports = {
|
||||
/* Unique IDs of modals within the app */
|
||||
modalNames: {
|
||||
CONF_EDITOR: 'CONF_EDITOR',
|
||||
CLOUD_BACKUP: 'CLOUD_BACKUP',
|
||||
REBUILD_APP: 'REBUILD_APP',
|
||||
THEME_MAKER: 'THEME_MAKER',
|
||||
ABOUT_APP: 'ABOUT_APP',
|
||||
LANG_SWITCHER: 'LANG_SWITCHER',
|
||||
EDIT_ITEM: 'EDIT_ITEM',
|
||||
EDIT_SECTION: 'EDIT_SECTION',
|
||||
EDIT_PAGE_INFO: 'EDIT_PAGE_INFO',
|
||||
EDIT_APP_CONFIG: 'EDIT_APP_CONFIG',
|
||||
EXPORT_CONFIG_MENU: 'EXPORT_CONFIG_MENU',
|
||||
MOVE_ITEM_TO: 'MOVE_ITEM_TO',
|
||||
},
|
||||
/* Key names for the top-level objects in conf.yml */
|
||||
topLevelConfKeys: {
|
||||
@@ -135,6 +146,8 @@ module.exports = {
|
||||
metaTagData: [
|
||||
{ name: 'description', content: 'A simple static homepage for you\'re server' },
|
||||
],
|
||||
/* If no 'target' specified, this is the default opening method */
|
||||
openingMethod: 'newtab',
|
||||
/* Default option for Toast messages */
|
||||
toastedOptions: {
|
||||
position: 'bottom-center',
|
||||
@@ -158,21 +171,26 @@ module.exports = {
|
||||
backupEndpoint: 'https://dashy-sync-service.as93.net',
|
||||
/* Available services for fetching favicon icon for user apps */
|
||||
faviconApiEndpoints: {
|
||||
mcapi: 'https://eu.mc-api.net/v3/server/favicon/$URL',
|
||||
allesedv: 'https://f1.allesedv.com/128/$URL',
|
||||
clearbit: 'https://logo.clearbit.com/$URL',
|
||||
faviconkit: 'https://api.faviconkit.com/$URL/64',
|
||||
duckduckgo: 'https://icons.duckduckgo.com/ip2/$URL.ico',
|
||||
yandex: 'https://favicon.yandex.net/favicon/$URL',
|
||||
google: 'https://www.google.com/s2/favicons?sz=128&domain_url=$URL',
|
||||
allesedv: 'https://f1.allesedv.com/128/$URL',
|
||||
besticon: 'https://besticon-demo.herokuapp.com/icon?url=$URL&size=80..120..200',
|
||||
webmasterapi: 'https://api.webmasterapi.com/v1/favicon/yEwx0ZFs0CSPshHq/$URL',
|
||||
mcapi: 'https://eu.mc-api.net/v3/server/favicon/$URL',
|
||||
},
|
||||
/* The URL to CDNs used for external icons. These are only loaded when required */
|
||||
iconCdns: {
|
||||
fa: 'https://kit.fontawesome.com',
|
||||
mdi: 'https://cdn.jsdelivr.net/npm/@mdi/font@5.9.55/css/materialdesignicons.min.css',
|
||||
si: 'https://unpkg.com/simple-icons@v5/icons',
|
||||
generative: 'https://ipsicon.io',
|
||||
localPath: '/item-icons',
|
||||
generative: 'https://avatars.dicebear.com/api/identicon/{icon}.svg',
|
||||
generativeFallback: 'https://evatar.io/{icon}',
|
||||
localPath: './item-icons',
|
||||
faviconName: 'favicon.ico',
|
||||
homeLabIcons: 'https://raw.githubusercontent.com/WalkxCode/dashboard-icons/master/png/{icon}.png',
|
||||
},
|
||||
/* URLs for web search engines */
|
||||
searchEngineUrls: {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import en from '@/assets/locales/en.json';
|
||||
import de from '@/assets/locales/de.json';
|
||||
import nl from '@/assets/locales/nl.json';
|
||||
import pl from '@/assets/locales/pl.json';
|
||||
import fr from '@/assets/locales/fr.json';
|
||||
import sl from '@/assets/locales/sl.json';
|
||||
import es from '@/assets/locales/es.json';
|
||||
@@ -12,6 +13,8 @@ import hi from '@/assets/locales/hi.json';
|
||||
import ja from '@/assets/locales/ja.json';
|
||||
import pt from '@/assets/locales/pt.json';
|
||||
import ru from '@/assets/locales/ru.json';
|
||||
import nb from '@/assets/locales/nb.json';
|
||||
import pirate from '@/assets/locales/zz-pirate.json';
|
||||
|
||||
// Language data - Next register your language by adding it to this list
|
||||
export const languages = [
|
||||
@@ -33,6 +36,12 @@ export const languages = [
|
||||
locale: nl,
|
||||
flag: '🇳🇱',
|
||||
},
|
||||
{
|
||||
name: 'polski',
|
||||
code: 'pl',
|
||||
locale: pl,
|
||||
flag: '🇵🇱',
|
||||
},
|
||||
{
|
||||
name: 'Français',
|
||||
code: 'fr',
|
||||
@@ -93,6 +102,18 @@ export const languages = [
|
||||
locale: ru,
|
||||
flag: '🇷🇺',
|
||||
},
|
||||
{ // Norwegian
|
||||
name: 'Norsk',
|
||||
code: 'nb',
|
||||
locale: nb,
|
||||
flag: '🇳🇴',
|
||||
},
|
||||
{ // Joke Language - Pirate
|
||||
name: 'Pirate',
|
||||
code: 'pirate',
|
||||
locale: pirate,
|
||||
flag: '🏴☠️',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
90
src/views/404.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<main class="not-found-page">
|
||||
<h1 class="not-found-title">404</h1>
|
||||
<h2 class="not-found-sad-face">:(</h2>
|
||||
<p class="not-found-subtitle">Page Not Found</p>
|
||||
<p class="not-found-message">
|
||||
Facing Issues?
|
||||
<a href="https://git.io/JzpL5">Get Support</a>.
|
||||
</p>
|
||||
<router-link to="/" class="go-home">Back Home</router-link>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'not-found',
|
||||
methods: {
|
||||
setTheme() {
|
||||
document.getElementsByTagName('html')[0].setAttribute('data-theme', 'dashy-docs');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setTheme();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/media-queries.scss';
|
||||
@import '@/styles/style-helpers.scss';
|
||||
main.not-found-page {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
background: #202020;
|
||||
min-height: calc(99vh - var(--footer-height));
|
||||
background-color: #202020;
|
||||
h1.not-found-title, h2.not-found-sad-face {
|
||||
font-size: 20vh;
|
||||
font-family: Tahoma, monospace;
|
||||
cursor: default;
|
||||
color: #0c0c0c;
|
||||
text-shadow: 0px 4px 4px #090909, 0 0 0 #000, 0px 2px 2px #000000;
|
||||
margin: 1rem 0 0;
|
||||
}
|
||||
h2.not-found-sad-face {
|
||||
font-size: 4rem;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
p {
|
||||
font-family: monospace;
|
||||
cursor: default;
|
||||
color: #0c0c0c;
|
||||
margin: 0.2rem 0;
|
||||
text-shadow: 0 1px 1px #090909, 0 0 0 #000, 0 1px 1px #000000;
|
||||
}
|
||||
p.not-found-subtitle {
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
p.not-found-message {
|
||||
font-size: 1.4rem;
|
||||
font-weight: normal;
|
||||
a {
|
||||
color: #0c0c0c;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
a.go-home {
|
||||
padding: 0.3rem 1rem;
|
||||
border-radius: 3px;
|
||||
font-size: 1.7rem;
|
||||
cursor: pointer;
|
||||
font-family: Tahoma, monospace;
|
||||
color: #0c0c0c;
|
||||
margin: 2rem 0 0;
|
||||
text-decoration: none;
|
||||
background: #db78fc;
|
||||
box-shadow: 0 4px #b83ddd;
|
||||
&:hover { box-shadow: 0 2px #b83ddd; }
|
||||
}
|
||||
::selection { background-color: #db78fc; color: #121212; }
|
||||
}
|
||||
</style>
|
||||
@@ -7,18 +7,13 @@ import JsonToYaml from '@/utils/JsonToYaml';
|
||||
|
||||
export default {
|
||||
name: 'DownloadConfig',
|
||||
props: {
|
||||
sections: Array,
|
||||
appConfig: Object,
|
||||
pageInfo: Object,
|
||||
computed: {
|
||||
config() {
|
||||
return this.$store.state.config;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
config: {
|
||||
appConfig: this.appConfig,
|
||||
pageInfo: this.pageInfo,
|
||||
sections: this.sections,
|
||||
},
|
||||
jsonParser: JsonToYaml,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -4,24 +4,33 @@
|
||||
<!-- Search bar, layout options and settings -->
|
||||
<SettingsContainer ref="filterComp"
|
||||
@user-is-searchin="searching"
|
||||
@change-display-layout="setLayoutOrientation"
|
||||
@change-icon-size="setItemSize"
|
||||
@change-modal-visibility="updateModalVisibility"
|
||||
:displayLayout="layout"
|
||||
:iconSize="itemSizeBound"
|
||||
:externalThemes="getExternalCSSLinks()"
|
||||
:sections="allSections"
|
||||
:appConfig="appConfig"
|
||||
:pageInfo="pageInfo"
|
||||
:modalOpen="modalOpen"
|
||||
class="settings-outer"
|
||||
/>
|
||||
<!-- Show back button, when on single-section view -->
|
||||
<div v-if="singleSectionView">
|
||||
<router-link to="/home" class="back-to-all-link">
|
||||
<BackIcon />
|
||||
<span>Back to All</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- Main content, section for each group of items -->
|
||||
<div v-if="checkTheresData(sections)"
|
||||
:class="`item-group-container orientation-${layout} item-size-${itemSizeBound}`">
|
||||
:class="`item-group-container `
|
||||
+ `orientation-${layout} `
|
||||
+ `item-size-${itemSizeBound} `
|
||||
+ (isEditMode ? 'edit-mode ' : '')
|
||||
+ (singleSectionView ? 'single-section-view ' : '')
|
||||
+ (this.colCount ? `col-count-${this.colCount} ` : '')"
|
||||
>
|
||||
<Section
|
||||
v-for="(section, index) in filteredTiles"
|
||||
:key="index"
|
||||
:index="index"
|
||||
:title="section.name"
|
||||
:icon="section.icon || undefined"
|
||||
:displayData="getDisplayData(section)"
|
||||
@@ -34,11 +43,17 @@
|
||||
:class="
|
||||
(searchValue && filterTiles(section.items, searchValue).length === 0) ? 'no-results' : ''"
|
||||
/>
|
||||
<!-- Show add new section button, in edit mode -->
|
||||
<AddNewSection v-if="isEditMode" />
|
||||
</div>
|
||||
<!-- Show message when there's no data to show -->
|
||||
<div v-if="checkIfResults()" class="no-data">
|
||||
{{searchValue ? $t('home.no-results') : $t('home.no-data')}}
|
||||
</div>
|
||||
<!-- Show banner at bottom of screen, for Saving config changes -->
|
||||
<EditModeSaveMenu v-if="isEditMode" />
|
||||
<!-- Modal for viewing and exporting configuration file -->
|
||||
<ExportConfigMenu />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -46,57 +61,80 @@
|
||||
|
||||
import SettingsContainer from '@/components/Settings/SettingsContainer.vue';
|
||||
import Section from '@/components/LinkItems/Section.vue';
|
||||
import EditModeSaveMenu from '@/components/InteractiveEditor/EditModeSaveMenu.vue';
|
||||
import ExportConfigMenu from '@/components/InteractiveEditor/ExportConfigMenu.vue';
|
||||
import AddNewSection from '@/components/InteractiveEditor/AddNewSectionLauncher.vue';
|
||||
import { searchTiles } from '@/utils/Search';
|
||||
import Defaults, { localStorageKeys, iconCdns } from '@/utils/defaults';
|
||||
import StoreKeys from '@/utils/StoreMutations';
|
||||
import Defaults, { localStorageKeys, iconCdns, modalNames } from '@/utils/defaults';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import BackIcon from '@/assets/interface-icons/back-arrow.svg';
|
||||
|
||||
export default {
|
||||
name: 'home',
|
||||
props: {
|
||||
sections: Array, // Main site content
|
||||
appConfig: Object, // Main site configuation (optional)
|
||||
pageInfo: Object, // Page metadata (optional)
|
||||
},
|
||||
components: {
|
||||
SettingsContainer,
|
||||
EditModeSaveMenu,
|
||||
ExportConfigMenu,
|
||||
AddNewSection,
|
||||
Section,
|
||||
BackIcon,
|
||||
},
|
||||
data: () => ({
|
||||
searchValue: '',
|
||||
layout: '',
|
||||
itemSizeBound: '',
|
||||
modalOpen: false, // When true, keybindings are disabled
|
||||
addNewSectionOpen: false,
|
||||
}),
|
||||
computed: {
|
||||
/* Combines sections from config file, with those in local storage */
|
||||
allSections() {
|
||||
// If the user has stored sections in local storage, return those
|
||||
const localSections = localStorage[localStorageKeys.CONF_SECTIONS];
|
||||
if (localSections) {
|
||||
const json = JSON.parse(localSections);
|
||||
if (json.length >= 1) return json;
|
||||
}
|
||||
// Otherwise, return the usuall data from conf.yml
|
||||
return this.sections;
|
||||
sections() {
|
||||
return this.$store.getters.sections;
|
||||
},
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
pageInfo() {
|
||||
return this.$store.getters.pageInfo;
|
||||
},
|
||||
modalOpen() {
|
||||
return this.$store.state.modalOpen;
|
||||
},
|
||||
singleSectionView() {
|
||||
return this.findSingleSection(this.$store.getters.sections, this.$route.params.section);
|
||||
},
|
||||
isEditMode() {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
/* Get class for num columns, if specified by user */
|
||||
colCount() {
|
||||
let { colCount } = this.appConfig;
|
||||
if (!colCount) return null;
|
||||
if (colCount < 1) colCount = 1;
|
||||
if (colCount > 8) colCount = 8;
|
||||
return colCount;
|
||||
},
|
||||
/* Return all sections, that match users search term */
|
||||
filteredTiles() {
|
||||
const sections = this.allSections;
|
||||
const sections = this.singleSectionView || this.sections;
|
||||
return sections.filter((section) => this.filterTiles(section.items, this.searchValue));
|
||||
},
|
||||
/* Updates layout (when button clicked), and saves in local storage */
|
||||
layoutOrientation: {
|
||||
get() { return this.appConfig.layout || Defaults.layout; },
|
||||
set: function setLayout(layout) {
|
||||
localStorage.setItem(localStorageKeys.LAYOUT_ORIENTATION, layout);
|
||||
this.layout = layout;
|
||||
},
|
||||
layoutOrientation() {
|
||||
return this.$store.getters.layout;
|
||||
},
|
||||
/* Updates icon size (when button clicked), and saves in local storage */
|
||||
iconSize: {
|
||||
get() { return this.appConfig.iconSize || Defaults.iconSize; },
|
||||
set: function setIconSize(iconSize) {
|
||||
localStorage.setItem(localStorageKeys.ICON_SIZE, iconSize);
|
||||
this.itemSizeBound = iconSize;
|
||||
},
|
||||
iconSize() {
|
||||
return this.$store.getters.iconSize;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
layoutOrientation(layout) {
|
||||
localStorage.setItem(localStorageKeys.LAYOUT_ORIENTATION, layout);
|
||||
this.layout = layout;
|
||||
},
|
||||
iconSize(size) {
|
||||
localStorage.setItem(localStorageKeys.ICON_SIZE, size);
|
||||
this.itemSizeBound = size;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -121,17 +159,32 @@ export default {
|
||||
getDisplayData(section) {
|
||||
return !section.displayData ? {} : section.displayData;
|
||||
},
|
||||
/* Sets layout attribute, which is used by Section */
|
||||
setLayoutOrientation(layout) {
|
||||
this.layoutOrientation = layout;
|
||||
},
|
||||
/* Sets item size attribute, which is used by Section */
|
||||
setItemSize(itemSize) {
|
||||
this.iconSize = itemSize;
|
||||
},
|
||||
/* Update data when modal is open (so that key bindings can be disabled) */
|
||||
updateModalVisibility(modalState) {
|
||||
this.modalOpen = modalState;
|
||||
this.$store.commit('SET_MODAL_OPEN', modalState);
|
||||
},
|
||||
openAddNewSectionMenu() {
|
||||
this.addNewSectionOpen = true;
|
||||
this.$modal.show(modalNames.EDIT_SECTION);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, true);
|
||||
},
|
||||
closeEditSection() {
|
||||
this.addNewSectionOpen = false;
|
||||
this.$modal.hide(modalNames.EDIT_SECTION);
|
||||
this.$store.commit(StoreKeys.SET_MODAL_OPEN, false);
|
||||
},
|
||||
/* If on sub-route, and section exists, then return only that section */
|
||||
findSingleSection: (allSections, sectionTitle) => {
|
||||
if (!sectionTitle) return undefined;
|
||||
let sectionToReturn;
|
||||
const parse = (section) => section.replaceAll(' ', '-').toLowerCase().trim();
|
||||
allSections.forEach((section) => {
|
||||
if (parse(sectionTitle) === parse(section.name)) {
|
||||
sectionToReturn = [section];
|
||||
}
|
||||
});
|
||||
if (!sectionToReturn) ErrorHandler(`No section named '${sectionTitle}' was found`);
|
||||
return sectionToReturn;
|
||||
},
|
||||
/* Returns an array of links to external CSS from the Config */
|
||||
getExternalCSSLinks() {
|
||||
@@ -154,8 +207,8 @@ export default {
|
||||
/* Checks if any sections or items use icons from a given CDN */
|
||||
checkIfIconLibraryNeeded(prefix) {
|
||||
let isNeeded = false;
|
||||
if (!this.allSections) return false;
|
||||
this.allSections.forEach((section) => {
|
||||
if (!this.sections) return false;
|
||||
this.sections.forEach((section) => {
|
||||
if (section.icon && section.icon.includes(prefix)) isNeeded = true;
|
||||
section.items.forEach((item) => {
|
||||
if (item.icon && item.icon.includes(prefix)) isNeeded = true;
|
||||
@@ -194,10 +247,10 @@ export default {
|
||||
},
|
||||
/* Returns true if there is more than 1 sub-result visible during searching */
|
||||
checkIfResults() {
|
||||
if (!this.allSections) return false;
|
||||
if (!this.sections) return false;
|
||||
else {
|
||||
let itemsFound = true;
|
||||
this.allSections.forEach((section) => {
|
||||
this.sections.forEach((section) => {
|
||||
if (this.filterTiles(section.items, this.searchValue).length > 0) itemsFound = false;
|
||||
});
|
||||
return itemsFound;
|
||||
@@ -227,10 +280,19 @@ export default {
|
||||
.home {
|
||||
padding-bottom: 1px;
|
||||
background: var(--background);
|
||||
// min-height: calc(100vh - 126px);
|
||||
min-height: calc(99.9vh - var(--footer-height));
|
||||
}
|
||||
|
||||
.back-to-all-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem;
|
||||
margin: 0.25rem;
|
||||
@extend .svg-button;
|
||||
svg { margin-right: 0.5rem; }
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Outside container wrapping the item groups*/
|
||||
.item-group-container {
|
||||
display: grid;
|
||||
@@ -255,29 +317,61 @@ export default {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
&.orientation-horizontal, &.orientation-vertical, &.single-section-view {
|
||||
@include phone { --content-max-width: 100%; }
|
||||
@include tablet { --content-max-width: 98%; }
|
||||
@include laptop { --content-max-width: 90%; }
|
||||
@include monitor { --content-max-width: 85%; }
|
||||
@include big-screen { --content-max-width: 80%; }
|
||||
@include big-screen-up { --content-max-width: 60%; }
|
||||
max-width: var(--content-max-width, 90%);
|
||||
}
|
||||
|
||||
/* Specify number of columns, based on screen size */
|
||||
@include phone {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
@include tablet {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
@include laptop {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
@include monitor {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
@include big-screen {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
@include big-screen-up {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
/* Specify number of columns, based on screen size or user preference */
|
||||
@include phone { --col-count: 1; }
|
||||
@include tablet { --col-count: 2; }
|
||||
@include laptop { --col-count: 2; }
|
||||
@include monitor { --col-count: 3; }
|
||||
@include big-screen { --col-count: 4; }
|
||||
@include big-screen-up { --col-count: 5; }
|
||||
|
||||
@include tablet-up {
|
||||
&.col-count-1 { --col-count: 1; }
|
||||
&.col-count-2 { --col-count: 2; }
|
||||
&.col-count-3 { --col-count: 3; }
|
||||
&.col-count-4 { --col-count: 4; }
|
||||
&.col-count-5 { --col-count: 5; }
|
||||
&.col-count-6 { --col-count: 6; }
|
||||
&.col-count-7 { --col-count: 7; }
|
||||
&.col-count-8 { --col-count: 8; }
|
||||
}
|
||||
|
||||
grid-template-columns: repeat(var(--col-count, 2), minmax(0, 1fr));
|
||||
|
||||
/* Hide when search term returns nothing */
|
||||
.no-results { display: none; }
|
||||
|
||||
/* Additional spacing when in edit mode */
|
||||
&.edit-mode {
|
||||
margin-bottom: 12rem;
|
||||
}
|
||||
|
||||
/* When in single-section view mode */
|
||||
&.single-section-view {
|
||||
display: block;
|
||||
}
|
||||
.add-new-section {
|
||||
border: 2px dashed var(--primary);
|
||||
border-radius: var(--curve-factor);
|
||||
padding: var(--item-group-padding);
|
||||
background: var(--item-group-background);
|
||||
color: var(--primary);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
height: fit-content;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom styles only applied when there is no sections in config */
|
||||
|
||||
@@ -76,7 +76,7 @@ import router from '@/router';
|
||||
import Button from '@/components/FormElements/Button';
|
||||
import Input from '@/components/FormElements/Input';
|
||||
import Defaults, { localStorageKeys } from '@/utils/defaults';
|
||||
import { InfoHandler } from '@/utils/ErrorHandler';
|
||||
import { InfoHandler, WarningInfoHandler, InfoKeys } from '@/utils/ErrorHandler';
|
||||
import {
|
||||
checkCredentials,
|
||||
login,
|
||||
@@ -91,9 +91,6 @@ export default {
|
||||
Button,
|
||||
Input,
|
||||
},
|
||||
props: {
|
||||
appConfig: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
@@ -104,6 +101,9 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
/* Data for timeout dropdown menu, translated label + value in ms */
|
||||
dropDownMenu() {
|
||||
return [
|
||||
@@ -159,9 +159,9 @@ export default {
|
||||
if (response.correct) { // Yay, credentials were correct :)
|
||||
login(this.username, this.password, timeout); // Login, to set the cookie
|
||||
this.goHome();
|
||||
InfoHandler(`Succesfully signed in as ${this.username}`, 'Authentication');
|
||||
InfoHandler(`Succesfully signed in as ${this.username}`, InfoKeys.AUTH);
|
||||
} else {
|
||||
InfoHandler(`Unable to Sign In - ${this.message}`, 'Authentication');
|
||||
WarningInfoHandler('Unable to Sign In', InfoKeys.AUTH, this.message);
|
||||
}
|
||||
},
|
||||
/* Calls function to double-check guest access enabled, then log in as guest */
|
||||
@@ -169,9 +169,11 @@ export default {
|
||||
const isAllowed = this.isGuestAccessEnabled;
|
||||
if (isAllowed) {
|
||||
this.$toasted.show('Logged in as Guest, Redirecting...', { className: 'toast-success' });
|
||||
InfoHandler('Logged in as Guest', InfoKeys.AUTH);
|
||||
this.goHome();
|
||||
} else {
|
||||
this.$toasted.show('Guest access not allowed', { className: 'toast-error' });
|
||||
this.$toasted.show('Guest Access Not Allowed', { className: 'toast-error' });
|
||||
WarningInfoHandler('Guest Access Not Allowed', InfoKeys.AUTH);
|
||||
}
|
||||
},
|
||||
/* Calls logout, shows status message, and refreshed page */
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
<div class="minimal-home" :style="getBackgroundImage() + setColumnCount()">
|
||||
<!-- Buttons for config and home page -->
|
||||
<div class="minimal-buttons">
|
||||
<ConfigLauncher :sections="sections" :pageInfo="pageInfo" :appConfig="appConfig"
|
||||
@modalChanged="modalChanged" class="config-launcher" />
|
||||
<ConfigLauncher @modalChanged="modalChanged" class="config-launcher" />
|
||||
</div>
|
||||
<!-- Page title and search bar -->
|
||||
<div class="title-and-search">
|
||||
<router-link to="/">
|
||||
<h1>{{ pageInfo.title }}</h1>
|
||||
</router-link>
|
||||
<MinimalSearch @user-is-searchin="(s) => { this.searchValue = s; }" :active="!modalOpen" />
|
||||
<MinimalSearch
|
||||
@user-is-searchin="(s) => { this.searchValue = s; }"
|
||||
:active="!modalOpen" ref="filterComp" />
|
||||
</div>
|
||||
<div v-if="checkTheresData(sections)"
|
||||
:class="`item-group-container ${!tabbedView ? 'showing-all' : ''}`">
|
||||
@@ -60,11 +61,6 @@ import ConfigLauncher from '@/components/Settings/ConfigLauncher';
|
||||
|
||||
export default {
|
||||
name: 'home',
|
||||
props: {
|
||||
sections: Array, // Main site content
|
||||
appConfig: Object, // Main site configuation (optional)
|
||||
pageInfo: Object,
|
||||
},
|
||||
components: {
|
||||
MinimalSection,
|
||||
MinimalHeading,
|
||||
@@ -79,10 +75,21 @@ export default {
|
||||
tabbedView: true, // By default use tabs, when searching then show all instead
|
||||
theme: GetTheme(),
|
||||
}),
|
||||
computed: {
|
||||
sections() {
|
||||
return this.$store.getters.sections;
|
||||
},
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
pageInfo() {
|
||||
return this.$store.getters.pageInfo;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
/* When the theme changes, then call the update method */
|
||||
searchValue() {
|
||||
this.tabbedView = !(this.searchValue.length > 0);
|
||||
this.tabbedView = !this.searchValue || this.searchValue.length === 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -111,7 +118,7 @@ export default {
|
||||
},
|
||||
/* Clears input field, once a searched item is opened */
|
||||
finishedSearching() {
|
||||
this.$refs.filterComp.clearFilterInput();
|
||||
this.$refs.filterComp.clearMinFilterInput();
|
||||
},
|
||||
/* Extracts the site name from domain, used for the searching functionality */
|
||||
getDomainFromUrl(url) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="work-space">
|
||||
<SideBar :sections="sections" @launch-app="launchApp" />
|
||||
<SideBar :sections="sections" @launch-app="launchApp" :initUrl="getInitialUrl()" />
|
||||
<WebContent :url="url" v-if="!isMultiTaskingEnabled" />
|
||||
<MultiTaskingWebComtent :url="url" v-else />
|
||||
</div>
|
||||
@@ -16,17 +16,19 @@ import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHe
|
||||
|
||||
export default {
|
||||
name: 'Workspace',
|
||||
props: {
|
||||
sections: Array,
|
||||
appConfig: Object,
|
||||
},
|
||||
data: () => ({
|
||||
url: '', // this.$route.query.url || '',
|
||||
url: '',
|
||||
GetTheme,
|
||||
ApplyLocalTheme,
|
||||
ApplyCustomVariables,
|
||||
}),
|
||||
computed: {
|
||||
sections() {
|
||||
return this.$store.getters.sections;
|
||||
},
|
||||
appConfig() {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
isMultiTaskingEnabled() {
|
||||
return this.appConfig.enableMultiTasking || false;
|
||||
},
|
||||
@@ -37,8 +39,12 @@ export default {
|
||||
MultiTaskingWebComtent,
|
||||
},
|
||||
methods: {
|
||||
launchApp(url) {
|
||||
this.url = url;
|
||||
launchApp(options) {
|
||||
if (options.target === 'newtab') {
|
||||
window.open(options.url, '_blank');
|
||||
} else {
|
||||
this.url = options.url;
|
||||
}
|
||||
},
|
||||
setTheme() {
|
||||
const theme = this.GetTheme();
|
||||
@@ -51,16 +57,21 @@ export default {
|
||||
fontAwesomeScript.setAttribute('src', `https://kit.fontawesome.com/${faKey}.js`);
|
||||
document.head.appendChild(fontAwesomeScript);
|
||||
},
|
||||
repositionFooter() {
|
||||
document.getElementsByTagName('footer')[0].style.position = 'fixed';
|
||||
/* Returns a service URL, if set as a URL param, or if user has specified landing URL */
|
||||
getInitialUrl() {
|
||||
const route = this.$route;
|
||||
if (route.query && route.query.url) {
|
||||
return decodeURI(route.query.url);
|
||||
} else if (this.appConfig.workspaceLandingUrl) {
|
||||
return this.appConfig.workspaceLandingUrl;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const route = this.$route;
|
||||
if (route.query && route.query.url) this.url = decodeURI(route.query.url);
|
||||
this.setTheme();
|
||||
this.initiateFontAwesome();
|
||||
// this.repositionFooter();
|
||||
this.url = this.getInitialUrl();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||