🔀 Merge pull request #271 from Lissy93/ARCH/implement-vuex-state

[ARCH] Implement VueX State Management
This commit is contained in:
Alicia Sykes 2021-10-10 18:39:10 +01:00 committed by GitHub
commit 7f555ee142
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 408 additions and 216 deletions

View File

@ -1,5 +1,8 @@
# Changelog
## ⚡️ 1.8.6 - Implementation of VueX [PR: #271](https://github.com/Lissy93/dashy/pull/271)
- New state management pattern, which should lead to a more organized code base long term, and will also make building out the new UI editor significantly easier to do in a clean and reliable way
## 💄 1.8.5 - Lots of Requested UI Improvements [PR #261](https://github.com/Lissy93/dashy/pull/261)
- Adds an option for landing URL in workspace, Re: #255
- Switches to a new API for generative icons, Re: #163

69
.github/pr-badge.yml vendored
View File

@ -1,5 +1,5 @@
# Config file for pull-request-badge. See: https://pullrequestbadge.com/
# Enables badges to be inserted into the PR description, based on certain conditions
# Config file for pull-request-badge. See: https://pullrequestbadge.com/ by @stefanbuck
# Dynamically inserts status badges into PR description, based on certain conditions
# Checks if the required sections are missing
- label: "⚠Missing"
@ -23,21 +23,43 @@
color: "#f25265"
when: "$labels.length == 0"
# Show note when in draft mode
# Show note when task list has unfinished items
- label: "⚠Notice"
message: "Unchecked Tasks"
when: "$payload.pull_request.body.includes('- [ ] ')"
color: "#f25265"
# Show badge indicating PR status
- label: "Status"
message: "Draft"
message: "✏️ Draft"
when: "$isDraft"
color: "#ffa933"
- label: "Status"
message: "🧱 Work in Progress"
when: "$payload.pull_request.title.includes('WIP')"
color: "#29e3f4"
- label: "Status"
message: "✅ Ready"
color: "#3ef963"
when: "$labels.includes('🔀 Ready for Merge')"
# Add size label based on very large or tiny PRs
- label: "PR Size"
message: "Extra Large"
color: "#f9833e"
when: "$additions > 1000"
- label: "PR Size"
message: "Large"
color: "#f79c47"
when: "$additions > 600"
color: "#f4b546"
when: "$additions > 500 && $additions < 1000"
- label: "PR Size"
message: "Medium"
color: "#f3ff59"
when: "$additions > 10 && $additions < 500"
- label: "PR Size"
message: "Quick"
color: "#3eef8b"
when: "$additions < 5"
when: "$additions < 10"
# Show PR number, to destination and from destination
- label: "#$prNumber"
@ -57,7 +79,7 @@
when: "$payload.pull_request.author_association !== 'OWNER'"
url: "https://github.com/$payload.pull_request.user.login"
# Show a badge indicating the PR category
# Show a badge indicating the PR category, based on tag
- label: "Type"
message: "✨ Feature"
color: "#39b0fd"
@ -90,3 +112,34 @@
message: "🌟 Showcase Addition"
color: "#39b0fd"
when: "$labels.includes('💯 Showcase')"
- label: "Type"
message: "🏗️ Architecture"
color: "#39b0fd"
when: "$labels.includes('🏗️ Architectural Changes')"
- label: "Type"
message: "🤖 Auto Submission"
color: "#39b0fd"
when: "$labels.includes('🤖 Auto')"
- label: "Type"
message: "🌐 Language Update"
color: "#39b0fd"
when: "$labels.includes('🌐 Language')"
# Show warning, when certain tags are applied
- label: "Warning"
message: "⛔ Do Not Merge"
color: "#f25265"
when: "$labels.includes('⛔ Don't Merge')"
- label: "Warning"
message: "🚫 Merge Conflicts"
color: "#f25265"
when: "$labels.includes('🚫 Merge Conflicts')"
- label: "Warning"
message: "🕸️ Inactive"
color: "#f25265"
when: "$labels.includes('🕸️ Inactive')"
- label: "Warning"
message: "💀 Spam"
color: "#f25265"
when: "$labels.includes('💀 Spam')"

View File

@ -1,10 +1,11 @@
# PR labels and the branch patterns they should be auto-assigned to
🦋 Bug Fix: ['FIX/*', 'HOT-FIX/*', 'BUG-FIX/*']
✨ New Feature: ['FEATURE/*']
✨ New Feature: ['FEATURE/*', 'FEAT/*']
🚚 Refactor: ['IMPROVMENTS/*', 'REFACTOR/*']
💯 Showcase: ['SHOWCASE/*']
💄 Stylistic Changes: ['STYLES/*', 'THEME/*']
🛠️ Build Changes: ['ARCH/*', 'ARCHITECTURE/*', 'DOCKER/*', 'BUILD/*']
🦋 Bug Fix: ['FIX/*', 'HOT-FIX/*', 'BUG-FIX/*']
💯 Showcase: ['SHOWCASE/*', 'SHOWCASE_SUBMISSION/*']
💄 Stylistic Changes: ['STYLES/*', 'THEME/*', 'UI/*']
🏗️ Architectural Changes: ['ARCH/*', 'ARCHITECTURE/*']
🛠️ Build Changes: ['DOCKER/*', 'BUILD/*', 'CI/*', 'ACTIONS/*']
🌐 Language: ['LANG/*', 'INTERNATIONALIZATION/*', 'I18N/*', 'TEXT-UPDATE/*']
🤖 Auto: ['AUTO/*', 'BOT/*', 'snyk-upgrade-*', 'snyk-fix-*']
⛔ Don't Merge: ['WEBSITE/*', 'EXPERIMENT/*', 'DEPLOY/*', 'deploy_*', 'gh-pages', 'dev-demo']

View File

@ -4,7 +4,7 @@ on:
repository_dispatch:
workflow_dispatch:
schedule:
- cron: '0 1 * * 0' # At 01:00 on Sunday.
- cron: '0 1 1 * *' # Run monthly
jobs:
link-checker:
runs-on: ubuntu-latest
@ -14,7 +14,7 @@ jobs:
- name: Check for Broken Links
uses: lycheeverse/lychee-action@v1.0.8
with:
args: --verbose --no-progress **/*.md **/*.html
args: --verbose -a 200,302,304,429 --no-progress **/*.md **/*.html
env:
GITHUB_TOKEN: ${{secrets.BOT_GITHUB_TOKEN}}
LYCHEE_OUT: .github/broken-link-report.md

View File

@ -19,7 +19,7 @@ jobs:
add-awaiting-author:
runs-on: ubuntu-latest
if: ${{ github.event.comment.author_association != 'COLLABORATOR' && github.event.comment.author_association != 'OWNER' }}
if: ${{ !github.event.issue.pull_request && github.event.comment.author_association != 'COLLABORATOR' && github.event.comment.author_association != 'OWNER' }}
steps:
- name: Add Awaiting Author labels when Updated
uses: actions-cool/issues-helper@v2

View File

@ -1,6 +1,6 @@
{
"name": "Dashy",
"version": "1.8.5",
"version": "1.8.6",
"license": "MIT",
"main": "server",
"scripts": {
@ -39,7 +39,8 @@
"vue-router": "^3.0.3",
"vue-select": "^3.12.1",
"vue-swatches": "^2.1.1",
"vue-toasted": "^1.1.28"
"vue-toasted": "^1.1.28",
"vuex": "^3.6.2"
},
"devDependencies": {
"@architect/sandbox": "^3.7.4",

View File

@ -11,10 +11,9 @@
import Header from '@/components/PageStrcture/Header.vue';
import Footer from '@/components/PageStrcture/Footer.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,10 +21,6 @@ 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: {
@ -33,17 +28,9 @@ export default {
Footer,
LoadingScreen,
},
provide: {
config,
visibleComponents,
},
data() {
return {
isLoading: true, // Set to false after mount complete
showFooter: visibleComponents.footer,
appConfig: Accumulator.appConfig(),
pageInfo: Accumulator.pageInfo(),
visibleComponents,
};
},
computed: {
@ -55,6 +42,24 @@ export default {
shouldShowSplash() {
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;
},
},
created() {
this.$store.dispatch('initializeConfig');
},
methods: {
/* Injects the users custom CSS as a style tag */
@ -103,6 +108,7 @@ 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);
},

View File

@ -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 {

View File

@ -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;
}
},
};

View File

@ -1,6 +1,6 @@
<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')">
@ -33,7 +33,6 @@ import WorkspaceOpenIcon from '@/assets/interface-icons/open-workspace.svg';
export default {
name: 'ContextMenu',
inject: ['config'],
components: {
SameTabOpenIcon,
NewTabOpenIcon,
@ -45,10 +44,10 @@ export default {
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: {
appConfig() {
return this.$store.getters.appConfig;
},
},
methods: {
/* Called on item click, emits an event up to Item */
@ -58,10 +57,7 @@ export default {
},
/* Checks if the user as disabled context menu in config */
isMenuDisabled() {
if (this.config && this.config.appConfig) {
return !!this.config.appConfig.disableContextMenu;
}
return false;
return !!this.appConfig.disableContextMenu;
},
},
};

View File

@ -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);
},
},
};

View File

@ -53,7 +53,6 @@ import { localStorageKeys, serviceEndpoints } 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
@ -77,6 +76,11 @@ export default {
statusCheckInterval: Number,
statusCheckAllowInsecure: Boolean,
},
computed: {
appConfig() {
return this.$store.getters.appConfig;
},
},
data() {
return {
contextMenuOpen: false,
@ -110,7 +114,7 @@ export default {
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);
}

View File

@ -30,7 +30,6 @@ 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
@ -40,6 +39,10 @@ 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);
@ -96,7 +99,7 @@ export default {
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 faviconApi = specificApi || this.appConfig.faviconApi || defaultFaviconApi;
const endpoint = faviconApiEndpoints[faviconApi];
return endpoint.replace('$URL', host);
}
@ -120,7 +123,7 @@ 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) {

View File

@ -44,7 +44,6 @@
:ref="`iframeModal-${groupId}`"
:name="`iframeModal-${groupId}`"
@closed="$emit('itemClicked')"
@modalChanged="modalChanged"
/>
</Collapsable>
</template>
@ -58,7 +57,6 @@ import IframeModal from '@/components/LinkItems/IframeModal.vue';
export default {
name: 'Section',
inject: ['config'],
props: {
groupId: String,
title: String,
@ -66,7 +64,6 @@ export default {
displayData: Object,
items: Array,
itemSize: String,
modalOpen: Boolean,
},
components: {
Collapsable,
@ -74,13 +71,16 @@ export default {
IframeModal,
},
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') {
@ -122,18 +122,14 @@ export default {
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;

View File

@ -12,7 +12,6 @@ import SearchBar from '@/components/Settings/SearchBar';
export default {
name: 'MinimalSearch',
inject: ['config'],
components: {
SearchBar,
},
@ -24,16 +23,29 @@ export default {
input: '', // Users current search term
};
},
computed: {
appConfig() {
return this.$store.getters.appConfig;
},
webSearchEnabled() {
if (this.appConfig && this.appConfig.webSearch) {
return !this.appConfig.webSearch.disableWebSearch;
}
return true;
},
},
methods: {
/* Emmits users's search term up to parent */
userIsTypingSomething(searchValue) {
this.input = searchValue;
this.$emit('user-is-searchin', searchValue);
},
/* Emmits an event to reset state when user is finished searching */
clearMinFilterInput() {
this.$refs.MinimalSearchBar.clearFilterInput();
},
},
mounted() {
window.addEventListener('keydown', this.startFiltering);
},
beforeDestroy() {
window.removeEventListener('keydown', this.startFiltering);
},
};
</script>

View File

@ -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;

View File

@ -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>

View File

@ -11,7 +11,7 @@
<!-- 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>
@ -48,6 +48,7 @@
import ConfigContainer from '@/components/Configuration/ConfigContainer';
import LanguageSwitcher from '@/components/Settings/LanguageSwitcher';
import { topLevelConfKeys, localStorageKeys, modalNames } from '@/utils/defaults';
import Keys from '@/utils/StoreMutations';
import IconSpanner from '@/assets/interface-icons/config-editor.svg';
import IconViewMode from '@/assets/interface-icons/application-change-view.svg';
import IconHome from '@/assets/interface-icons/application-home.svg';
@ -71,15 +72,24 @@ export default {
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;
},
},
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 = {};

View File

@ -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.state.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);
},
},
};

View File

@ -35,9 +35,7 @@ import {
export default {
name: 'FilterTile',
inject: ['config'],
props: {
active: Boolean,
minimalSearch: Boolean, // If true, then keep it simple
},
data() {
@ -48,8 +46,11 @@ export default {
};
},
computed: {
active() {
return !this.$store.state.modalOpen;
},
searchPrefs() {
return this.config.appConfig.webSearch || {};
return this.$store.getters.webSearch || {};
},
},
mounted() {

View File

@ -3,16 +3,14 @@
<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"
<ThemeSelector :externalThemes="externalThemes"
:confTheme="getInitialTheme()" :userThemes="getUserThemes()" />
<LayoutSelector :displayLayout="displayLayout" @layoutUpdated="updateDisplayLayout"/>
<ItemSizeSelector :iconSize="iconSize" @iconSizeUpdated="updateIconSize" />
<ConfigLauncher :sections="sections" :pageInfo="pageInfo" :appConfig="appConfig"
@modalChanged="modalChanged" />
<ConfigLauncher />
<AuthButtons v-if="userState != 'noone'" :userType="userState" />
</div>
<div :class="`show-hide-container ${settingsVisible? 'hide-btn' : 'show-btn'}`">
@ -52,10 +50,6 @@ export default {
displayLayout: String,
iconSize: String,
externalThemes: Object,
appConfig: Object,
pageInfo: Object,
sections: Array,
modalOpen: Boolean,
},
components: {
SearchBar,
@ -69,7 +63,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);
@ -83,9 +113,6 @@ export default {
updateIconSize(iconSize) {
this.$emit('change-icon-size', iconSize);
},
modalChanged(changedTo) {
this.$emit('change-modal-visibility', changedTo);
},
getInitialTheme() {
return this.appConfig.theme || '';
},
@ -104,25 +131,6 @@ export default {
|| (this.visibleComponents || 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>

View File

@ -31,6 +31,7 @@ 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 {
@ -94,13 +95,15 @@ export default {
},
/* 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 */

View File

@ -36,7 +36,6 @@ import IconMinimalView from '@/assets/interface-icons/application-minimal.svg';
export default {
name: 'SideBar',
inject: ['config'],
props: {
sections: Array,
initUrl: String,

View File

@ -12,7 +12,6 @@ import Icon from '@/components/LinkItems/ItemIcon.vue';
export default {
name: 'SideBarItem',
inject: ['config'],
props: {
icon: String,
title: String,

View File

@ -19,7 +19,6 @@ import SideBarItem from '@/components/Workspace/SideBarItem.vue';
export default {
name: 'SideBarSection',
inject: ['config'],
props: {
items: Array,
},

View File

@ -14,6 +14,7 @@ import Toasted from 'vue-toasted'; // Toast component, used to show confirm
// 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
@ -48,9 +49,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 +69,7 @@ if (!isKeycloakEnabled()) {
window.location.reload();
} else {
// Yay - user successfully authenticated with Keycloak, render the app!
new Vue({ router, render, i18n }).$mount('#app');
mount();
}
});
}

View File

@ -14,10 +14,10 @@ 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 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';
@ -32,8 +32,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,
@ -51,7 +61,7 @@ const getStartingComponent = () => {
/* Returns the meta tags for each route */
const makeMetaTags = (defaultTitle) => ({
title: config.pageInfo.title || defaultTitle,
title: pageInfo.title || defaultTitle,
metaTags: metaTagData,
});
@ -62,37 +72,30 @@ const router = new Router({
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'),
},
{ // Workspace view page
path: routePaths.workspace,
name: 'workspace',
component: Workspace,
props: config,
meta: makeMetaTags('Workspace'),
},
{ // Minimal view page
path: routePaths.minimal,
name: 'minimal',
component: Minimal,
props: config,
meta: makeMetaTags('Start Page'),
},
{ // The login page
path: routePaths.login,
name: 'login',
component: Login,
props: {
appConfig: config.appConfig,
},
beforeEnter: (to, from, next) => {
// If the user already logged in + guest mode not enabled, then redirect home
if (isAuthenticated() && !isGuestAccessEnabled()) router.push({ path: '/' });
@ -109,7 +112,6 @@ const router = new Router({
path: routePaths.download,
name: 'download',
component: () => import('./views/DownloadConfig.vue'),
props: config,
meta: makeMetaTags('Download Config'),
},
{ // Page not found, any non-defined routes will land here

61
src/store.js Normal file
View File

@ -0,0 +1,61 @@
/* 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 filterUserSections from '@/utils/CheckSectionVisibility';
Vue.use(Vuex);
const { UPDATE_CONFIG, SET_MODAL_OPEN, SET_LANGUAGE } = Keys;
const store = new Vuex.Store({
state: {
config: {},
lang: '', // The users language, auto-detected or read from local storage / config
modalOpen: false, // KB shortcut functionality will be disabled when modal is open
},
getters: {
config(state) {
return state.config;
},
pageInfo(state) {
return state.config.pageInfo || {};
},
appConfig(state) {
return state.config.appConfig || {};
},
sections(state) {
return filterUserSections(state.config.sections || []);
},
webSearch(state, getters) {
return getters.appConfig.webSearch || {};
},
visibleComponents(state, getters) {
return componentVisibility(getters.appConfig);
},
},
mutations: {
[UPDATE_CONFIG](state, config) {
state.config = config;
},
[SET_LANGUAGE](state, lang) {
state.lang = lang;
},
[SET_MODAL_OPEN](state, modalOpen) {
state.modalOpen = modalOpen;
},
},
actions: {
/* Called when app first loaded. Reads config and sets state */
initializeConfig({ commit }) {
const Accumulator = new ConfigAccumulator();
const config = Accumulator.config();
commit(UPDATE_CONFIG, config);
},
},
modules: {},
});
export default store;

View File

@ -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 */

View File

@ -11,9 +11,8 @@ import {
pageInfo as defaultPageInfo,
iconSize as defaultIconSize,
layout as defaultLayout,
// language as defaultLanguage,
} from '@/utils/defaults';
import ErrorHandler from '@/utils/ErrorHandler';
import conf from '../../public/conf.yml';
export default class ConfigAccumulator {
@ -46,24 +45,14 @@ 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 */
@ -75,13 +64,11 @@ export default class ConfigAccumulator {
const json = JSON.parse(localSections);
if (json.length >= 1) return 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;
return this.conf ? this.conf.sections || [] : [];
}
/* Complete config */

View File

@ -0,0 +1,11 @@
// A list of mutation names
const KEY_NAMES = [
'UPDATE_CONFIG',
'SET_MODAL_OPEN',
'SET_LANGUAGE',
];
// Convert array of key names into an object, and export
const MUTATIONS = {};
KEY_NAMES.forEach((key) => { MUTATIONS[key] = key; });
export default MUTATIONS;

View File

@ -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,
};
},

View File

@ -10,9 +10,6 @@
:displayLayout="layout"
:iconSize="itemSizeBound"
:externalThemes="getExternalCSSLinks()"
:sections="allSections"
:appConfig="appConfig"
:pageInfo="pageInfo"
:modalOpen="modalOpen"
class="settings-outer"
/>
@ -55,11 +52,6 @@ import Defaults, { localStorageKeys, iconCdns } from '@/utils/defaults';
export default {
name: 'home',
props: {
sections: Array, // Main site content
appConfig: Object, // Main site configuation (optional)
pageInfo: Object, // Page metadata (optional)
},
components: {
SettingsContainer,
Section,
@ -68,9 +60,20 @@ export default {
searchValue: '',
layout: '',
itemSizeBound: '',
modalOpen: false, // When true, keybindings are disabled
}),
computed: {
sections() {
return this.$store.getters.sections;
},
appConfig() {
return this.$store.getters.appConfig;
},
pageInfo() {
return this.$store.getters.pageInfo;
},
modalOpen() {
return this.$store.state.modalOpen;
},
/* Get class for num columns, if specified by user */
colCount() {
let { colCount } = this.appConfig;
@ -143,7 +146,7 @@ export default {
},
/* 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);
},
/* Returns an array of links to external CSS from the Config */
getExternalCSSLinks() {

View File

@ -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 [

View File

@ -2,8 +2,7 @@
<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">
@ -62,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,
@ -81,6 +75,17 @@ 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() {

View File

@ -16,10 +16,6 @@ import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHe
export default {
name: 'Workspace',
props: {
sections: Array,
appConfig: Object,
},
data: () => ({
url: '',
GetTheme,
@ -27,6 +23,12 @@ export default {
ApplyCustomVariables,
}),
computed: {
sections() {
return this.$store.getters.sections;
},
appConfig() {
return this.$store.getters.appConfig;
},
isMultiTaskingEnabled() {
return this.appConfig.enableMultiTasking || false;
},

View File

@ -9866,6 +9866,11 @@ vue@^2.6.10:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==
vuex@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==
watchpack-chokidar2@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"