Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7c34c28
Convert final vuex to pinia for account store& teardown cleanup
n-lark Mar 27, 2026
707bf71
Update imports to fix circular dep and update specs
n-lark Mar 27, 2026
453fc77
Await the router in checkState
n-lark Mar 27, 2026
74374dc
Remove awaiting router change
n-lark Mar 30, 2026
a94cca1
Merge remote-tracking branch 'origin/6943-pinia-task-14-account-setti…
n-lark Mar 30, 2026
2a0d52f
Update account-auth logic to use new ux-loading store and fix pending…
n-lark Mar 30, 2026
8752e81
Update deploy-blueprint spec to be friends with pinias async nature
n-lark Mar 30, 2026
e5f57bc
Remove unneeded variable
n-lark Mar 30, 2026
ec9dec3
Merge remote-tracking branch 'origin/6943-pinia-task-14-account-setti…
n-lark Apr 2, 2026
afd614d
Update from team move to context store
n-lark Apr 2, 2026
ec909fe
Fix remaining uses of improper deconstructing pattern across codebase
n-lark Apr 2, 2026
15c8bcb
Remove unused team mapping
n-lark Apr 2, 2026
1588c18
Merge branch '6943-pinia-task-14-account-settings' into 6944-pinia-ta…
n-lark Apr 2, 2026
e1b2149
Merge branch '6943-pinia-task-14-account-settings' into 6944-pinia-ta…
n-lark Apr 9, 2026
4e9b718
Merge branch '6943-pinia-task-14-account-settings' into 6944-pinia-ta…
n-lark Apr 9, 2026
4ae5bde
Merge branch '6943-pinia-task-14-account-settings' into 6944-pinia-ta…
n-lark Apr 9, 2026
10fd65a
Merge branch 'main' into 6944-pinia-task-15-teardown
n-lark Apr 14, 2026
0224c03
Remove lazy imports
n-lark Apr 15, 2026
5f7b3a7
extract lazy imported modules into top level imports
cstns Apr 16, 2026
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
2 changes: 1 addition & 1 deletion frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export default {
}
},
mounted () {
this.$store.dispatch('account/checkState')
useAccountAuthStore().checkState()
useProductBrokersStore().checkFlags()
},
methods: {
Expand Down
13 changes: 5 additions & 8 deletions frontend/src/api/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,21 @@ const client = axios.create({
// Common error handler
Comment thread
n-lark marked this conversation as resolved.
client.interceptors.response.use(function (response) {
return response
}, function (error) {
}, async function (error) {
if (/^http/.test(error.config.url)) {
// This request is to an external URL. Allow this error to pass back to the caller
return Promise.reject(error)
}

// This is an error response from our own API (or failure to reach it)
// Lazy require to avoid circular dependency:
// api/client.js → stores/account-auth.js → api/user.js → api/client.js
const { useAccountAuthStore } = require('../stores/account-auth.js')
const { useUxLoadingStore } = require('../stores/ux-loading.js')
const store = require('../store/index.js').default
// Dynamic import breaks the circular dep: client.js → account-auth.js → api/* → client.js
const { useAccountAuthStore } = await import('../stores/account-auth.js')
Comment thread
n-lark marked this conversation as resolved.
Outdated
const { useUxLoadingStore } = await import('../stores/ux-loading.js')
if (error.code === 'ERR_NETWORK') {
// Backend failed to respond
useUxLoadingStore().setOffline(true)
} else if (error.response && error.response.status === 401 && !useUxLoadingStore().appLoader && !useAccountAuthStore().loginInflight) {
// 401 when !pending && !loginInflight means the session has expired
store.dispatch('account/logout')
useAccountAuthStore().logout()
Comment thread
cstns marked this conversation as resolved.
} else if (error.response && error.response.status === 500) {
// show toast notification
Alerts.emit(error.response.data.error + ': ' + error.response.data.message, 'warning', 7500)
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/Offline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
</div>
</template>
<script>
import { useAccountAuthStore } from '@/stores/account-auth.js'

export default {
name: 'OfflineMessage',
methods: {
reload () {
this.$store.dispatch('account/checkState')
useAccountAuthStore().checkState()
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/components/auth/UpdateExpiredPassword.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
import { mapState } from 'pinia'

import userApi from '../../api/user.js'
import store from '../../store/index.js'
import FormRow from '../FormRow.vue'

import { useAccountAuthStore } from '@/stores/account-auth.js'
Expand Down Expand Up @@ -89,7 +88,7 @@ export default {
return false
}
userApi.changePassword(this.input.old_password, this.input.password).then(() => {
this.$store.dispatch('account/checkState')
useAccountAuthStore().checkState()
this.$router.go()
}).catch(e => {
this.errors.password_change = 'Password change failed'
Expand All @@ -106,7 +105,7 @@ export default {
this.$refs['row-confirm'].focus()
},
logout () {
store.dispatch('account/logout')
useAccountAuthStore().logout()
}
},
mounted () {
Expand Down
6 changes: 1 addition & 5 deletions frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,19 @@ import router from './routes.js'
import Alerts from './services/alerts.js'
import { setupSentry } from './services/error-tracking.js'
import { getServiceFactory } from './services/service.factory.js'
import store from './store/index.js'
import { skipResetPlugin } from './stores/plugins/skip-reset.plugin.js'

import './index.css'

import ForgeUIComponents from './ui-components/index.js'

store.commit('initializeStore')

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
pinia.use(skipResetPlugin)

const app = createApp(App)
.use(ForgeUIComponents)
.use(pinia)
.use(store)
.use(router)
.use(VueShepherdPlugin)

Expand All @@ -46,7 +42,7 @@ const serviceFactory = getServiceFactory()
setupSentry(app, router)

// Boot all services before mounting
serviceFactory.bootAllServices(app, store, router)
serviceFactory.bootAllServices(app, router)
.then((services) => services.bootstrap.init())
.catch((error) => {
console.error('Bootstrap initialization failed:', error)
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -220,15 +220,15 @@ export default {
}
if (valid) {
this.loggingIn = true
this.$store.dispatch('account/login', {
useAccountAuthStore().loginWithCredentials({
username: this.input.username,
password: this.input.password
})
}
},
submitMFAToken () {
this.loggingIn = true
this.$store.dispatch('account/login', {
useAccountAuthStore().loginWithCredentials({
token: this.input.token
})
},
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/pages/TermsAndConditions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { mapState } from 'pinia'

import userApi from '../api/user.js'
import FFLayoutBox from '../layouts/Box.vue'
import store from '../store/index.js'

import { useAccountAuthStore } from '@/stores/account-auth.js'
import { useAccountSettingsStore } from '@/stores/account-settings.js'
Expand All @@ -43,15 +42,15 @@ export default {
},
methods: {
logout () {
store.dispatch('account/logout')
useAccountAuthStore().logout()
},
async acceptAction () {
const options = {}
try {
options.tcs_accepted = this.accept
this.loading = true
await userApi.updateUser(options)
this.$store.dispatch('account/checkState')
useAccountAuthStore().checkState()
} catch (error) {
console.warn(error)
} finally {
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/pages/UnverifiedEmail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import { mapActions, mapState } from 'pinia'

import userApi from '../api/user.js'
import FFLayoutBox from '../layouts/Box.vue'
import store from '../store/index.js'

import { useAccountAuthStore } from '@/stores/account-auth.js'
import { useUxToursStore } from '@/stores/ux-tours.js'
Expand Down Expand Up @@ -89,7 +88,7 @@ export default {
tick()
},
logout () {
store.dispatch('account/logout')
useAccountAuthStore().logout()
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/account/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export default {
if (this.settings['user:offboarding-required']) {
window.location.href = this.settings['user:offboarding-url']
} else {
this.$store.dispatch('account/checkState')
useAccountAuthStore().checkState()
}
})
.catch(error => {
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/pages/account/VerifyPendingEmailChange.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<form v-if="!pending" class="px-4 sm:px-6 lg:px-8 mt-8 space-y-6">
<form v-if="!appLoader" class="px-4 sm:px-6 lg:px-8 mt-8 space-y-6">
<div>
<ff-button class="m-auto" @click="verify()">Click here to verify your change of email address</ff-button>
</div>
Expand All @@ -8,18 +8,20 @@

<script>

import { mapState } from 'vuex'
import { mapState } from 'pinia'

import userApi from '../../api/user.js'
import alerts from '../../services/alerts.js'

import { useUxLoadingStore } from '@/stores/ux-loading.js'

export default {
name: 'VerifyPendingEmailChange',
props: {
token: { type: String, required: true }
},
computed: {
...mapState(['pending'])
...mapState(useUxLoadingStore, ['appLoader'])
},
methods: {
async verify () {
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/pages/account/routes.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { CogIcon } from '@heroicons/vue/outline'

import store from '../../store/index.js'

import AccessRequest from './AccessRequest.vue'
import AccessRequestEditor from './AccessRequestEditor.vue'
import AccountCreate from './Create.vue'
Expand All @@ -20,6 +18,8 @@ import VerifyPendingEmailChange from './VerifyPendingEmailChange.vue'

import Account from './index.vue'

import { useAccountAuthStore } from '@/stores/account-auth.js'

export default [
{
// This is the editor being authenticated. This component bounces the user
Expand Down Expand Up @@ -114,7 +114,7 @@ export default [
path: '/account/logout',
name: 'Sign out',
redirect: function () {
store.dispatch('account/logout')
useAccountAuthStore().logout()
return { path: '/' }
}
},
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/team/Settings/Danger.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export default {
deleteTeam () {
teamApi.deleteTeam(this.team.id).then(() => {
alerts.emit('Team successfully deleted', 'confirmation')
this.$store.dispatch('account/checkState', '/')
useAccountAuthStore().checkState('/')
}).catch(err => {
alerts.emit('Problem deleting team', 'warning')
console.warn(err)
Expand Down
44 changes: 13 additions & 31 deletions frontend/src/services/bootstrap.service.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { nextTick } from 'vue'

import { useAccountAuthStore } from '@/stores/account-auth.js'
import { useAccountSettingsStore } from '@/stores/account-settings.js'
import { useAccountTeamStore } from '@/stores/account-team.js'

/**
* Bootstrap Service - Handles application lifecycle and readiness detection
* @class
Expand All @@ -10,11 +14,6 @@ class BootstrapService {
*/
$app

/**
* @type {import('vuex').Store} - Vuex store instance
*/
$store

/**
* @type {import('vue-router').Router} - Vue router instance
*/
Expand All @@ -26,16 +25,14 @@ class BootstrapService {
$services

/**
* @param {{app: import('vue').App, store: import('vuex').Store, router: import('vue-router').Router, services?: Object}} options - Constructor options
* @param {{app: import('vue').App, router: import('vue-router').Router, services?: Object}} options - Constructor options
*/
constructor ({
app,
store,
router,
services = {}
}) {
this.$app = app
this.$store = store
this.$router = router
this.$services = services

Expand All @@ -57,7 +54,12 @@ class BootstrapService {
*/
async init () {
return this.waitForAppMount()
.then(() => this.waitForStoreHydration())
.then(() => {
// Eagerly create account stores — restores persisted state from localStorage instantly
useAccountAuthStore()
useAccountTeamStore()
useAccountSettingsStore()
})
.then(() => this.checkUser())
.then(() => this.mountApp())
.then(() => this.waitForRouterReady())
Expand All @@ -76,31 +78,13 @@ class BootstrapService {
})
}

async waitForStoreHydration () {
if (this.$store.state.initialized || this.$store.state._hydrated) {
return Promise.resolve()
}

// Wait for store hydration
return new Promise((resolve) => {
const unsubscribe = this.$store.subscribe((mutation) => {
if (mutation.type === 'initializeStore' ||
mutation.type === 'HYDRATE_COMPLETE' ||
this.$store.state._hydrated) {
unsubscribe()
resolve()
}
})
})
}

async waitForRouterReady () {
await this.$router.isReady()
}

async checkUser () {
if (window.opener) {
return this.$store.dispatch('account/checkIfAuthenticated').catch(e => e)
return useAccountAuthStore().checkIfAuthenticated().catch(e => e)
}

return Promise.resolve()
Expand Down Expand Up @@ -133,19 +117,17 @@ class BootstrapService {
let BootstrapServiceInstance = null

/**
* @param {{app: import('vue').App, store: import('vuex').Store, router: import('vue-router').Router, services?: Object}} options - Constructor options
* @param {{app: import('vue').App, router: import('vue-router').Router, services?: Object}} options - Constructor options
* @returns {BootstrapService}
*/
export function createBootstrapService ({
app,
store,
router,
services = {}
} = {}) {
if (!BootstrapServiceInstance) {
BootstrapServiceInstance = new BootstrapService({
app,
store,
router,
services
})
Expand Down
Loading
Loading