Skip to content

Pinia Task 7 - product-brokers #6824

@n-lark

Description

@n-lark

Task 7 — product-brokers (PR 8)

Gate: Phase 0 merged
Vuex module: frontend/src/store/modules/product/index.js (the product root module)
New file: frontend/src/stores/product-brokers.js
Persistence: brokers.expandedTopics → localStorage (cleared on logout)
Cross-store dependency: all API actions read account.team.id via _account-bridge.js

7.1 — Create the Pinia store

// frontend/src/stores/product-brokers.js
import { defineStore } from 'pinia'
import brokerApi from '../api/broker.js'
import { useAccountBridge } from './_account-bridge.js'

export const useProductBrokersStore = defineStore('product-brokers', {
    state: () => ({
        flags: null,
        interview: null,
        UNS: { clients: [], brokers: [] },
        brokers: { expandedTopics: {} }
    }),
    getters: {
        hasFfUnsClients: (state) => state.UNS.clients.length > 0,
        hasBrokers: (state) => state.UNS.brokers.length > 0,
        brokerExpandedTopics: (state) => (brokerId) => {
            const { team } = useAccountBridge()
            if (!team || !brokerId) return {}
            return state.brokers.expandedTopics?.[team.slug]?.[brokerId] ?? {}
        }
    },
    actions: {
        checkFlags () {
            try {
                window.posthog?.onFeatureFlags((flags, values) => {
                    const storeFlags = {}
                    for (const flagName of flags) {
                        const payload = window.posthog?.getFeatureFlagPayload(flagName)
                        storeFlags[flagName] = { value: values[flagName], payload }
                        const flagStartsWithKeyword = flagName.startsWith('interview-')
                        const flagEnabled = window.posthog?.isFeatureEnabled(flagName, { send_event: false })
                        const flagNotShownBefore = !localStorage.getItem('ph-$interview-popup-seen')
                        if (flagStartsWithKeyword && flagEnabled && flagNotShownBefore) {
                            this.interview = { flag: flagName, enabled: flagEnabled, payload }
                        }
                    }
                    this.flags = storeFlags
                })
            } catch (err) {
                console.error('posthog error logging feature flags')
            }
        },
        async fetchUnsClients () {
            const { team } = useAccountBridge()
            const response = await brokerApi.getClients(team.id)
            this.UNS.clients = response.clients
        },
        async getBrokers () {
            const { team } = useAccountBridge()
            const response = await brokerApi.getBrokers(team.id)
            this.UNS.brokers = response.brokers
            this.addFfBroker()
        },
        async createBroker (payload) {
            const { team } = useAccountBridge()
            const broker = await brokerApi.createBroker(team.id, payload).catch(e => e)
            this.UNS.brokers.push(broker)
            return broker
        },
        async updateBroker ({ payload, brokerId }) {
            const { team } = useAccountBridge()
            const broker = await brokerApi.updateBroker(team.id, brokerId, payload).catch(e => e)
            const i = this.UNS.brokers.findIndex(b => b.id === broker.id)
            if (i !== -1) this.UNS.brokers[i] = { ...this.UNS.brokers[i], ...broker }
            return broker
        },
        deleteBroker (brokerId) {
            const { team } = useAccountBridge()
            return brokerApi.deleteBroker(team.id, brokerId).then(() => {
                const i = this.UNS.brokers.findIndex(b => b.id === brokerId)
                if (i !== -1) this.UNS.brokers.splice(i, 1)
            })
        },
        addFfBroker () {
            if (!this.UNS.brokers.find(b => b.local) && this.UNS.clients.length > 0) {
                this.UNS.brokers.push({ local: true, id: 'team-broker', name: 'FlowFuse Broker', clientId: 'team-broker', host: '??', port: 0, protocol: '', ssl: false, verifySSL: true })
            }
        },
        removeFfBroker () { this.UNS.brokers = this.UNS.brokers.filter(b => !b.local) },
        clearUns () { this.UNS.brokers = []; this.UNS.clients = [] },
        handleBrokerTopicState ({ topic, brokerId }) {
            const { team } = useAccountBridge()
            if (!this.brokers.expandedTopics[team.slug]) this.brokers.expandedTopics[team.slug] = {}
            if (!this.brokers.expandedTopics[team.slug][brokerId]) {
                this.brokers.expandedTopics[team.slug][brokerId] = {}
                this.brokers.expandedTopics[team.slug][brokerId][topic] = ''
                return
            }
            if (Object.prototype.hasOwnProperty.call(this.brokers.expandedTopics[team.slug][brokerId], topic)) {
                delete this.brokers.expandedTopics[team.slug][brokerId][topic]
            } else {
                this.brokers.expandedTopics[team.slug][brokerId][topic] = ''
            }
        }
    },
    persist: {
        pick: ['brokers'],
        storage: localStorage
    }
})

7.2 — Bridge: clearUns called from Vuex account

Same pattern as tables — add a Pinia call in the Vuex account/clearOtherStores action alongside the existing Vuex dispatch.

7.3 — Find and update all consumers

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

Primary consumers: frontend/src/pages/team/Brokers/ and any component that reads UNS clients.

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.

7.4 — Delete the Vuex module

Remove the product root module from store/index.js after all nested modules (assistant, tables, expert) are also migrated. Until then, remove only the broker-related state/actions and keep the module shell. Full deletion happens in PR 12 after product-expert ships.

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

7.5 — Write store tests

// frontend/src/tests/stores/product-brokers.spec.js
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { useProductBrokersStore } from '@/stores/product-brokers.js'
import * as bridge from '@/stores/_account-bridge.js'

describe('product-brokers store', () => {
    beforeEach(() => {
        setActivePinia(createPinia())
        vi.spyOn(bridge, 'useAccountBridge').mockReturnValue({ team: { id: 'team-1', slug: 'my-team' } })
    })

    it('initializes with empty UNS state', () => {
        const store = useProductBrokersStore()
        expect(store.UNS.clients).toEqual([])
        expect(store.UNS.brokers).toEqual([])
    })

    it('hasFfUnsClients returns true when clients exist', () => {
        const store = useProductBrokersStore()
        store.UNS.clients = [{ id: 'client-1' }]
        expect(store.hasFfUnsClients).toBe(true)
    })

    it('handleBrokerTopicState toggles a topic off when already present', () => {
        const store = useProductBrokersStore()
        store.handleBrokerTopicState({ topic: 'sensors/#', brokerId: 'broker-1' })
        expect(store.brokers.expandedTopics['my-team']['broker-1']).toHaveProperty('sensors/#')
        // Toggle off
        store.handleBrokerTopicState({ topic: 'sensors/#', brokerId: 'broker-1' })
        expect(store.brokers.expandedTopics['my-team']['broker-1']).not.toHaveProperty('sensors/#')
    })

    it('clearUns resets UNS brokers and clients', () => {
        const store = useProductBrokersStore()
        store.UNS.brokers = [{ id: 'b1' }]
        store.UNS.clients = [{ id: 'c1' }]
        store.clearUns()
        expect(store.UNS.brokers).toEqual([])
        expect(store.UNS.clients).toEqual([])
    })
})

7.6 — Export from stores index

export { useProductBrokersStore } from './product-brokers.js'

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Status

In Progress

Status

No status

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions