Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions src/components/layout/AppNavDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@
{{ $t('app.general.title.system') }}
</app-nav-item>

<template v-if="naviPoints.length">
<v-divider class="my-2" />
<app-nav-item-external
v-for="point in naviPoints"
:key="point.href"
:title="point.title"
:href="point.href"
:target="point.target"
:icon="point.icon"
/>
</template>

<app-nav-item
icon="$cog"
to="settings"
Expand Down Expand Up @@ -147,12 +159,16 @@
</template>

<script lang="ts">
import { Component, Mixins, VModel } from 'vue-property-decorator'
import { Component, Mixins, VModel, Watch } from 'vue-property-decorator'

import StateMixin from '@/mixins/state'
import BrowserMixin from '@/mixins/browser'

@Component({})
@Component({
components: {
AppNavItemExternal: () => import('@/components/ui/AppNavItemExternal.vue')
}
})
export default class AppNavDrawer extends Mixins(StateMixin, BrowserMixin) {
@VModel({ type: Boolean })
open?: boolean
Expand Down Expand Up @@ -188,6 +204,27 @@ export default class AppNavDrawer extends Mixins(StateMixin, BrowserMixin) {
set layoutMode (val: boolean) {
this.$typedCommit('config/setLayoutMode', val)
}

get naviPoints () {
return this.$store.getters['plugins/getNaviPoints'] ?? []
}

get naviPointsLoaded (): boolean {
return this.$store.getters['plugins/getNaviPointsLoaded'] ?? false
}

@Watch('socketConnected')
onSocketConnected (connected: boolean) {
if (connected && !this.naviPointsLoaded) {
this.$store.dispatch('plugins/fetchNaviPoints')
}
}

mounted () {
if (this.socketConnected && !this.naviPointsLoaded) {
this.$store.dispatch('plugins/fetchNaviPoints')
}
}
}
</script>

Expand Down
51 changes: 51 additions & 0 deletions src/components/ui/AppNavItemExternal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<template>
<v-tooltip
right
:disabled="isMobileViewport"
>
<template #activator="{ attrs, on }">
<v-list-item
:href="href"
:target="target"
link
color="secondary"
v-bind="attrs"
v-on="on"
>
<v-list-item-icon>
<v-icon>{{ icon || '$link' }}</v-icon>
</v-list-item-icon>

<v-list-item-content>
<v-list-item-title>{{ title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
<span>{{ title }}</span>
</v-tooltip>
</template>

<script lang="ts">
import { Component as VueComponent, Prop } from 'vue-property-decorator'
import { mixins } from 'vue-class-component'
import BrowserMixin from '@/mixins/browser'

@VueComponent({})
export default class AppNavItemExternal extends mixins(BrowserMixin) {
@Prop({ type: String, required: true })
readonly title!: string

@Prop({ type: String, required: true })
readonly href!: string

@Prop({ type: String, default: '_self' })
readonly target!: string

@Prop({ type: String, default: '' })
readonly icon!: string
}
</script>

<style lang="scss" scoped>
@import 'vuetify/src/styles/styles.sass';
</style>
7 changes: 5 additions & 2 deletions src/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1243,12 +1243,15 @@ app:
remove_model: Remove %{name} model
msg:
hint: >-
If saving as something other than %{name}, you can choose to also remove
the %{name} model
If saving as something other than '%{name}', you can choose to also remove
the '%{name}' model
not_found: No existing beacon models found.
tooltip:
delete: Delete Model
load: Load Model
external_navigation:
tooltip:
external_link: External navigation link
database:
btn:
compact_database: Compact Database
Expand Down
4 changes: 3 additions & 1 deletion src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { sensors } from './sensors'
import { database } from './database'
import { analysis } from './analysis'
import { afc } from './afc'
import { plugins } from './plugins'

Vue.use(Vuex)

Expand Down Expand Up @@ -64,7 +65,8 @@ export const storeOptions = {
sensors,
database,
analysis,
afc
afc,
plugins
} satisfies RootModules,
mutations: {},
actions: {
Expand Down
114 changes: 114 additions & 0 deletions src/store/plugins/__tests__/plugins.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { getters } from '../getters'
import { mutations } from '../mutations'
import { defaultState } from '../state'

const mockServerFilesGet = vi.fn()

vi.mock('@/api/httpClientActions', () => ({
httpClientActions: {
serverFilesGet: (...args: any[]) => mockServerFilesGet(...args)
}
}))

import { actions } from '../actions'

describe('plugins store', () => {
beforeEach(() => {
vi.clearAllMocks()
})

describe('state', () => {
it('returns default state', () => {
const state = defaultState()
expect(state.naviPoints).toEqual([])
expect(state.naviPointsLoaded).toBe(false)
})
})

describe('getters', () => {
it('getNaviPoints returns naviPoints from state', () => {
const state = {
naviPoints: [
{ title: 'Test', href: '/test', target: '_self', icon: 'mdi-test', position: 1 }
],
naviPointsLoaded: true
}
const result = getters.getNaviPoints(state, {} as any, {} as any, {} as any)
expect(result).toEqual(state.naviPoints)
})

it('getNaviPointsLoaded returns loaded status from state', () => {
const state = {
naviPoints: [],
naviPointsLoaded: true
}
const result = getters.getNaviPointsLoaded(state, {} as any, {} as any, {} as any)
expect(result).toBe(true)
})
})

describe('mutations', () => {
it('setReset resets state', () => {
const state = {
naviPoints: [{ title: 'Test', href: '/test', target: '_self', icon: '', position: 1 }],
naviPointsLoaded: true
}
mutations.setReset(state)
expect(state.naviPoints).toEqual([])
expect(state.naviPointsLoaded).toBe(false)
})

it('setNaviPoints sets naviPoints', () => {
const state = defaultState()
const points = [
{ title: 'KlipperFleet', href: '/klipperfleet.html', target: '_self', icon: 'mdi-fleet', position: 86 }
]
mutations.setNaviPoints(state, points)
expect(state.naviPoints).toEqual(points)
})

it('setNaviPointsLoaded sets loaded status', () => {
const state = defaultState()
mutations.setNaviPointsLoaded(state, true)
expect(state.naviPointsLoaded).toBe(true)
})
})

describe('actions', () => {
it('reset dispatches setReset mutation', async () => {
const commit = vi.fn()
await (actions as any).reset({ commit })
expect(commit).toHaveBeenCalledWith('setReset')
})

it('fetchNaviPoints fetches and sets navi points', async () => {
const mockPoints = [
{ title: 'KlipperFleet', href: '/klipperfleet.html', target: '_self', icon: '', position: 86 }
]

mockServerFilesGet.mockResolvedValue({
data: mockPoints
})

const commit = vi.fn()
const rootState = { config: { apiUrl: 'http://localhost' } }

await (actions as any).fetchNaviPoints({ commit, rootState } as any)

expect(commit).toHaveBeenCalledWith('setNaviPoints', expect.any(Array))
expect(commit).toHaveBeenCalledWith('setNaviPointsLoaded', true)
})

it('fetchNaviPoints handles errors gracefully', async () => {
mockServerFilesGet.mockRejectedValue(new Error('Not found'))

const commit = vi.fn()
const rootState = { config: { apiUrl: 'http://localhost' } }

await (actions as any).fetchNaviPoints({ commit, rootState } as any)

expect(commit).toHaveBeenCalledWith('setNaviPointsLoaded', true)
expect(commit).not.toHaveBeenCalledWith('setNaviPoints', expect.any(Array))
})
})
})
33 changes: 33 additions & 0 deletions src/store/plugins/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { ActionTree } from 'vuex'
import type { PluginsState } from './types'
import type { RootState } from '../types'
import { httpClientActions } from '@/api/httpClientActions'

export const actions: ActionTree<PluginsState, RootState> = {
async reset ({ commit }) {
commit('setReset')
},

async fetchNaviPoints ({ commit }) {
try {
const response = await httpClientActions.serverFilesGet<any>('config/.theme/navi.json')

if (Array.isArray(response?.data)) {
const points = response.data.map((item: any) => ({
title: item.title ?? 'Unknown',
href: item.href ?? '#',
target: item.target ?? '_self',
icon: item.icon ?? '',
position: item.position ?? 999,
visible: item.visible ?? true
}))

commit('setNaviPoints', points)
commit('setNaviPointsLoaded', true)
}
} catch (err) {
console.debug('Unable to fetch .theme/navi.json:', err)
commit('setNaviPointsLoaded', true)
}
}
}
13 changes: 13 additions & 0 deletions src/store/plugins/getters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { GetterTree } from 'vuex'
import type { PluginsState } from './types'
import type { RootState } from '../types'

export const getters: GetterTree<PluginsState, RootState> = {
getNaviPoints: (state) => {
return state.naviPoints
},

getNaviPointsLoaded: (state) => {
return state.naviPointsLoaded
}
}
17 changes: 17 additions & 0 deletions src/store/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Module } from 'vuex'
import { state } from './state'
import { getters } from './getters'
import { actions } from './actions'
import { mutations } from './mutations'
import type { PluginsState } from './types'
import type { RootState } from '../types'

const namespaced = true

export const plugins: Module<PluginsState, RootState> = {
namespaced,
state,
getters,
actions,
mutations
}
17 changes: 17 additions & 0 deletions src/store/plugins/mutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { MutationTree } from 'vuex'
import type { PluginsState, NaviPoint } from './types'

export const mutations: MutationTree<PluginsState> = {
setReset (state) {
state.naviPoints = []
state.naviPointsLoaded = false
},

setNaviPoints (state, points: NaviPoint[]) {
state.naviPoints = points
},

setNaviPointsLoaded (state, loaded: boolean) {
state.naviPointsLoaded = loaded
}
}
10 changes: 10 additions & 0 deletions src/store/plugins/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { PluginsState } from './types'

export const defaultState = (): PluginsState => {
return {
naviPoints: [],
naviPointsLoaded: false
}
}

export const state = defaultState()
13 changes: 13 additions & 0 deletions src/store/plugins/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface NaviPoint {
title: string;
href: string;
target?: string;
icon: string;
position: number;
visible?: boolean;
}

export interface PluginsState {
naviPoints: NaviPoint[];
naviPointsLoaded: boolean;
}
4 changes: 3 additions & 1 deletion src/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { sensors } from './sensors'
import type { database } from './database'
import type { analysis } from './analysis'
import type { afc } from './afc'
import type { plugins } from './plugins'
import type { storeOptions } from '.'

type RootModulesType = {
Expand Down Expand Up @@ -55,7 +56,8 @@ type RootModulesType = {
sensors: typeof sensors,
database: typeof database,
analysis: typeof analysis,
afc: typeof afc
afc: typeof afc,
plugins: typeof plugins,
}

type RootStateType = {
Expand Down
Loading
Loading