Skip to content

Pinia Task 8 - product-assistant #6825

@n-lark

Description

@n-lark

Task 8 — product-assistant (PR 9)

Gate: Phase 0 merged
Vuex module: frontend/src/store/modules/product/assistant/index.js
New file: frontend/src/stores/product-assistant.js
No persistence.
Cross-store dependency: immersiveInstance and immersiveDevice getters read from context store (already on Pinia after PR 6). allowedInboundOrigins getter depends on these. sendMessage action posts to the instance iframe.

8.1 — Create the Pinia store

The state and all actions are a direct port. The key things to note:

  • immersiveInstance and immersiveDevice getters now import useContextStore() directly instead of reading rootState.context
  • sendMessage uses messagingService() — no change needed there
  • The eventsRegistry constant and ALL_CONTEXT_OPTIONS array can be moved to a separate product-assistant.constants.js file or kept at the top of the store file
// frontend/src/stores/product-assistant.js
import { defineStore } from 'pinia'
import messagingService from '../services/messaging.service.js'
import { useContextStore } from './context.js'

// eventsRegistry and ALL_CONTEXT_OPTIONS copied verbatim from Vuex module...

export const useProductAssistantStore = defineStore('product-assistant', {
    state: () => ({
        version: null,
        supportedActions: {},
        assistantFeatures: {},
        palette: {},
        scope: { target: 'nr-assistant', scope: 'flowfuse-expert', source: 'flowfuse-expert' },
        nodeRedVersion: null,
        selectedNodes: [],
        selectedContext: [...ALL_CONTEXT_OPTIONS],
        debugLog: [],
        editorState: { /* initialised from eventsRegistry */ }
    }),
    getters: {
        immersiveInstance: () => useContextStore().instance,   // ← direct Pinia import
        immersiveDevice: () => useContextStore().device,       // ← direct Pinia import
        hasUserSelection: (state) => state.selectedNodes.length > 0,
        hasContextSelection: (state) => state.selectedContext.length > 0,
        isFeaturePaletteEnabled: (state) => state.assistantFeatures?.commands?.['get-palette']?.enabled ?? false,
        isFeatureDebugLogEnabled: (state) => state.assistantFeatures?.debugLog?.enabled ?? false,
        availableContextOptions (state) { /* same logic */ },
        paletteContribOnly (state) { /* same logic */ },
        debugLog (state) { /* same logic */ },
        allowedInboundOrigins () {
            const allowed = [window.origin]
            const instance = useContextStore().instance
            const device = useContextStore().device
            if (instance?.url) allowed.push(instance.url)
            if (device?.editor?.url) allowed.push(device.editor.url)
            return allowed
        },
        isEditorRunning: (state) => state.editorState?.flowsLoaded || state.editorState?.runtimeState?.state === 'start'
    },
    actions: {
        handleMessage (payload) { /* same switch logic, dispatch → this.actionName() */ },
        sendMessage (payload) {
            const service = messagingService()
            return service.sendMessage({
                message: { ...payload, ...this.scope },
                target: window.frames['immersive-editor-iframe'],
                targetOrigin: this.immersiveInstance?.url
            })
        },
        // all other actions are simple state assignments — port directly
        reset () { Object.assign(this, initialState()) }
    }
})

8.2 — Find and update all consumers

grep -rl "product/assistant\|mapState.*assistant\|mapGetters.*assistant\|mapActions.*assistant" frontend/src/

Primary consumers: expert assistant Vue components in frontend/src/components/expert/.

For each consumer, check if it's inside a mixin or rendered as <component :is="..."> inside <ff-dialog> — use Pattern C from PINIA_COMPONENT_PATTERNS.md (mapState / mapActions from Pinia) — this works correctly in all component types.

Also update messaging.service.js:112: replace this.$store.dispatch('product/assistant/handleMessage', event) with useProductAssistantStore().handleMessage(event).

8.3 — Update context.js expert getter (partial)

Now that product-assistant is on Pinia, update the expert getter in context.js to import useProductAssistantStore() directly instead of reading from store.state.product.assistant. Remove the bridge reads for assistant data.

8.4 — Delete the Vuex module

Remove assistant from store/modules/product/index.js modules registration. Delete frontend/src/store/modules/product/assistant/index.js.

Logout bridge: uncomment useProductAssistantStore().$reset() in the Vuex logout action (Task 0.7).

8.5 — Write store tests

// frontend/src/tests/stores/product-assistant.spec.js
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, beforeEach } from 'vitest'
import { useProductAssistantStore } from '@/stores/product-assistant.js'
import { useContextStore } from '@/stores/context.js'

describe('product-assistant store', () => {
    beforeEach(() => {
        setActivePinia(createPinia())
    })

    it('immersiveInstance reads from contextStore', () => {
        const store = useProductAssistantStore()
        const contextStore = useContextStore()
        contextStore.setInstance({ id: 'inst-1', url: 'http://localhost' })
        expect(store.immersiveInstance).toEqual({ id: 'inst-1', url: 'http://localhost' })
    })

    it('allowedInboundOrigins includes window.origin and instance url', () => {
        const store = useProductAssistantStore()
        const contextStore = useContextStore()
        contextStore.setInstance({ id: 'inst-1', url: 'http://node-red.local' })
        const origins = store.allowedInboundOrigins
        expect(origins).toContain(window.origin)
        expect(origins).toContain('http://node-red.local')
    })

    it('reset clears state back to initial values', () => {
        const store = useProductAssistantStore()
        store.version = '2.0'
        store.reset()
        expect(store.version).toBeNull()
    })
})

8.6 — Export from stores index

export { useProductAssistantStore } from './product-assistant.js'

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Status

Review

Status

Scheduled

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions