-
Notifications
You must be signed in to change notification settings - Fork 83
Description
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
Projects
Status
Status