FlowFuse / FlowFuse/flowfuse

Typescript: Phase 4 — Pinia Stores

Open
#7,217 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
400
Forks
89
Avg merge
1d 20h
Merged PRs (30d)
149

Description

### Phase 4 - Pinia Stores

> All stores are already Pinia. This is a pure `.js` → `.ts` conversion pass. The codebase uses the **Options API** style (`defineStore('id', { state, getters, actions })`) — instructions below assume that shape.

**Per store:**
- [ ] Rename `.js` → `.ts`
- [ ] Define a `*State` interface and annotate the `state` factory's return: `state: (): AccountState => ({ ... })`
- [ ] Replace `null` placeholder fields with typed equivalents (`Team | null`, `string | null`) so getters/actions don't see `any`
- [ ] Annotate action parameters and return types; getter `this` and `state` types flow from the state interface
- [ ] Import domain types from `@/types`; extension/UI-only shapes go next to the store
- [ ] Replace the file extension on every cross-store / cross-API import (`'@/stores/foo.js'` → `'@/stores/foo'`, `'@/api/foo.js'` → `'@/api/foo'`) — TS resolution drops the explicit `.js`
- [ ] Run `vue-tsc --noEmit` from `frontend/` — no new errors
- [ ] Rename `.spec.js` → `.spec.ts` alongside each converted source file (Vitest is already TS-native)

**Per phase**:

- [ ] Enable `strict: true` per-directory as each Phase 3 directory is fully converted

#### How to type each piece (Options API)

```ts
// 1. State interface — drives `state`, `getters`, and `this` inside actions
import type { Team, Notification, Invitation, FlowBlueprint } from '@/types'

interface AccountState {
teams: Team[]
teamBlueprints: Record
pendingTeamChange: boolean
notifications: Notification[]
invitations: Invitation[]
}

export const useAccountStore = defineStore('account', {
state: (): AccountState => ({
teams: [],
teamBlueprints: {},
pendingTeamChange: false,
notifications: [],
invitations: []
}),
getters: {
// `state` is typed as AccountState automatically
notificationsCount: (state) => state.notifications.length,
// `this`-based getters: use function form (not arrow) so Pinia can bind `this`
unreadNotifications (): Notification[] {
return this.notifications.filter(n => !n.read)
}
},
actions: {
// Annotate parameters + return type; `this` is inferred
async fetchTeam (slug: string): Promise {
const team = await teamApi.getTeamBySlug(slug)
this.teams.push(team)
return team
}
}
})
```

#### Plugin + module augmentation

`plugins/skip-reset.plugin.js` adds a custom `skipReset` option to store definitions. After conversion, the option needs to be declared via Pinia module augmentation so `defineStore({ skipReset: [...] })` type-checks:

```ts
import type { PiniaPluginContext, StateTree } from 'pinia'

declare module 'pinia' {
export interface DefineStoreOptionsBase {
skipReset?: Array
}
}

export function skipResetPlugin ({ store, options }: PiniaPluginContext) {
// ...
}
```

Convert the plugin first (or alongside the first store that uses `skipReset`) so the augmentation lands before any consuming store.

#### Common gotchas

- **Empty arrays:** `teams: []` infers `never[]` without the state interface. The `state` factory return-type annotation fixes the whole object in one place.
- **`null` placeholders:** the existing `ux-dialog` `dialog: { header: null, html: null, ... }` shape needs each field widened (`header: string | null`) — otherwise actions assigning strings will fail.
- **`anyOf` returns:** stores that surface `Team` from `team.js` or `MQTTBroker` from `broker.js` now hold a union. Either narrow at the action boundary (`if ('state' in broker) ...`) or store the union in state and let consumers narrow.
- **`storeToRefs` callers:** consumers of these stores often destructure via `storeToRefs(useFooStore())`. Types flow through, but converting the *consumer* (component or composable) is a separate Phase 4 / Composables task — don't chase them from the store PR.
- **`this` in arrow getters:** arrow getters can't access `this`; use the function shorthand for any getter that calls another getter or action.
- **`index.js`:** pure re-exports — convert last; rename to `index.ts` and replace `.js` extensions in the export paths.

| # | File | Notes |
|---|---|---|
| 1 | `account.js` | Core — touched by most other stores |
| 2 | `account-auth.js` | |
| 3 | `account-settings.js` | |
| 4 | `context.js` | |
| 5 | `index.js` | Wiring file — convert last |
| 6 | `plugins/skip-reset.plugin.js` | Pinia plugin |
| 7 | `product-assistant.js` | |
| 8 | `product-brokers.js` | Watch `MQTTBroker` union return |
| 9 | `product-expert.js` | |
| 10 | `product-expert-agents.js` | |
| 11 | `product-expert-insights-agent.js` | |
| 12 | `product-expert-support-agent.js` | |
| 13 | `product-tables.js` | |
| 14 | `ux.js` | |
| 15 | `ux-dialog.js` | |
| 16 | `ux-drawers.js` | |
| 17 | `ux-loading.js` | |
| 18 | `ux-navigation.js` | |
| 19 | `ux-tours.js` | |

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.