Task 6 — product-tables (PR 7)
Gate: Phase 0 merged
Vuex module: frontend/src/store/modules/product/tables/index.js
New file: frontend/src/stores/product-tables.js
Persistence: databases, newTable → localStorage (cleared on logout)
Cross-store dependency: reads account.team.id for all API calls — use _account-bridge.js
6.1 — Create the Pinia store
// frontend/src/stores/product-tables.js
import { defineStore } from 'pinia'
import tablesApi from '../api/tables.js'
import { hashString } from '../composables/String.js'
import { useAccountBridge } from './_account-bridge.js'
const emptyColumn = { name: '', type: '', nullable: false, default: '', hasDefault: false, unsigned: false }
export const useProductTablesStore = defineStore('product-tables', {
state: () => ({
databases: {},
databaseSelection: null,
tables: {},
tableSelection: null,
newTable: { name: '', columns: [{ ...emptyColumn }] },
isLoading: false
}),
getters: {
database: (state) => state.databases[state.databaseSelection] ?? null,
tables: (state) => (databaseId) => state.tables[databaseId] ?? [],
selectedTable: (state) => {
if (Object.keys(state.tables).includes(state.databaseSelection)) {
return state.tables[state.databaseSelection]?.find(t => t.name === state.tableSelection)
}
return null
}
},
actions: {
async createDatabase ({ teamId, databaseName = '' }) {
const database = await tablesApi.createDatabase(teamId, databaseName)
this.databases[database.id] = database
return database
},
async getDatabases () {
const { team } = useAccountBridge()
const databases = await tablesApi.getDataBases(team.id)
databases.forEach(db => { this.databases[db.id] = db })
return databases
},
async getTables (databaseId) {
const { team } = useAccountBridge()
let tables = await tablesApi.getTables(team.id, databaseId)
tables = [...tables].sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }))
this.tables[databaseId] = tables
if (tables.length > 0) this.tableSelection = tables[0].name
return tables
},
clearState () { Object.assign(this, { databases: {}, databaseSelection: null, tables: {}, tableSelection: null, newTable: { name: '', columns: [{ ...emptyColumn }] }, isLoading: false }) },
updateTableSelection (tableName) { this.tableSelection = tableName },
updateDatabaseSelection (databaseId) { this.databaseSelection = databaseId },
async getTableSchema ({ databaseId, tableName, teamId }) {
const schema = await tablesApi.getTableSchema(teamId, databaseId, tableName)
schema.forEach(col => { col.safeName = hashString(col.name) })
Object.keys(this.tables[databaseId]).forEach(key => {
if (this.tables[databaseId][key].name === tableName) {
this.tables[databaseId][key].schema = schema
}
})
},
async getTableData ({ databaseId, tableName, teamId }) {
const data = await tablesApi.getTableData(teamId, databaseId, tableName)
const payload = {
data,
safe: data.map(row => Object.fromEntries(Object.entries(row).map(([k, v]) => [hashString(k), v])))
}
Object.keys(this.tables[databaseId]).forEach(key => {
if (this.tables[databaseId][key].name === tableName) {
this.tables[databaseId][key].payload = payload
}
})
},
async createTable ({ databaseId }) {
const { team } = useAccountBridge()
const sanitizedColumns = this.newTable.columns.map(col => {
const c = { ...col }
if (!c.hasDefault) delete c.default
if (!c.unsigned) delete c.unsigned
return c
})
return tablesApi.createTable(team.id, databaseId, { name: this.newTable.name, columns: sanitizedColumns })
},
deleteTable ({ teamId, databaseId, tableName }) {
return tablesApi.deleteTable(teamId, databaseId, tableName)
},
addNewTableColumn () { this.newTable.columns.push({ ...emptyColumn }) },
removeNewTableColumn (columnKey) { this.newTable.columns.splice(columnKey, 1) },
setTableLoadingState (isLoading) { this.isLoading = isLoading }
},
persist: {
pick: ['databases', 'newTable'],
storage: localStorage
}
})
6.2 — Bridge: clearState called from Vuex account
The Vuex account module's clearOtherStores action dispatches product/tables/clearState. Until account is migrated, add a Pinia call alongside the existing Vuex dispatch in store/modules/account/index.js:
// In Vuex account/clearOtherStores action (temporary)
async clearOtherStores ({ dispatch }) {
// existing:
dispatch('product/tables/clearState', null, { root: true })
// add:
const { useProductTablesStore } = await import('@/stores/product-tables.js')
useProductTablesStore().clearState()
}
Remove the bridge once account is migrated.
6.3 — Find and update all consumers
grep -rl "product/tables\|mapState.*tables\|mapActions.*tables\|mapGetters.*tables" frontend/src/
Primary consumers: frontend/src/pages/team/Tables/ (20+ files).
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.
6.4 — Delete the Vuex module
Remove tables from store/modules/product/index.js modules registration. Delete frontend/src/store/modules/product/tables/index.js.
Logout bridge: uncomment useProductTablesStore().$reset() in the Vuex logout action (Task 0.7).
6.5 — Write store tests
frfr
6.6 — Export from stores index
export { useProductTablesStore } from './product-tables.js'
Task 6 —
product-tables(PR 7)Gate: Phase 0 merged
Vuex module:
frontend/src/store/modules/product/tables/index.jsNew file:
frontend/src/stores/product-tables.jsPersistence:
databases,newTable→ localStorage (cleared on logout)Cross-store dependency: reads
account.team.idfor all API calls — use_account-bridge.js6.1 — Create the Pinia store
6.2 — Bridge:
clearStatecalled from Vuex accountThe Vuex
accountmodule'sclearOtherStoresaction dispatchesproduct/tables/clearState. Until account is migrated, add a Pinia call alongside the existing Vuex dispatch instore/modules/account/index.js:Remove the bridge once account is migrated.
6.3 — Find and update all consumers
grep -rl "product/tables\|mapState.*tables\|mapActions.*tables\|mapGetters.*tables" frontend/src/Primary consumers:
frontend/src/pages/team/Tables/(20+ files).For each consumer, check if it's inside a mixin or rendered as
<component :is="...">inside<ff-dialog>— use Pattern C fromPINIA_COMPONENT_PATTERNS.md(mapState/mapActionsfrom Pinia) — this works correctly in all component types.6.4 — Delete the Vuex module
Remove
tablesfromstore/modules/product/index.jsmodules registration. Deletefrontend/src/store/modules/product/tables/index.js.Logout bridge: uncomment
useProductTablesStore().$reset()in the Vuex logout action (Task 0.7).6.5 — Write store tests
frfr
6.6 — Export from stores index