feat(ui): constrain add-account to a root-type dropdown + sub-path
Free-typing the full hierarchical name let admins fat-finger the parent (wrong/invalid root). Replace the single name field with a required Account Type select (the 5 valid roots, mirroring _VALID_ACCOUNT_PREFIXES) plus a sub-account input, a live 'Will create: ...' preview, and per-segment validation (each part must be a capitalized Beancount account component). The root prefix is now structurally guaranteed valid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
caef3cf5e8
commit
051c9f0c22
2 changed files with 44 additions and 11 deletions
|
|
@ -71,7 +71,8 @@ window.app = Vue.createApp({
|
||||||
},
|
},
|
||||||
addAccountDialog: {
|
addAccountDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
name: '',
|
rootType: 'Expenses',
|
||||||
|
subPath: '',
|
||||||
description: '',
|
description: '',
|
||||||
loading: false
|
loading: false
|
||||||
},
|
},
|
||||||
|
|
@ -292,6 +293,16 @@ window.app = Vue.createApp({
|
||||||
})
|
})
|
||||||
return options
|
return options
|
||||||
},
|
},
|
||||||
|
accountRootTypes() {
|
||||||
|
// The five Beancount root account types — the only valid parents.
|
||||||
|
// Mirrors the server's _VALID_ACCOUNT_PREFIXES.
|
||||||
|
return ['Assets', 'Liabilities', 'Equity', 'Income', 'Expenses']
|
||||||
|
},
|
||||||
|
addAccountFullName() {
|
||||||
|
const sub = (this.addAccountDialog.subPath || '').trim().replace(/^:+|:+$/g, '')
|
||||||
|
if (!this.addAccountDialog.rootType || !sub) return ''
|
||||||
|
return `${this.addAccountDialog.rootType}:${sub}`
|
||||||
|
},
|
||||||
userOptions() {
|
userOptions() {
|
||||||
const options = []
|
const options = []
|
||||||
this.users.forEach(user => {
|
this.users.forEach(user => {
|
||||||
|
|
@ -573,17 +584,26 @@ window.app = Vue.createApp({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showAddAccountDialog() {
|
showAddAccountDialog() {
|
||||||
this.addAccountDialog.name = ''
|
this.addAccountDialog.rootType = 'Expenses'
|
||||||
|
this.addAccountDialog.subPath = ''
|
||||||
this.addAccountDialog.description = ''
|
this.addAccountDialog.description = ''
|
||||||
this.addAccountDialog.show = true
|
this.addAccountDialog.show = true
|
||||||
},
|
},
|
||||||
async submitAddAccount() {
|
async submitAddAccount() {
|
||||||
const name = (this.addAccountDialog.name || '').trim()
|
const name = this.addAccountFullName
|
||||||
const validPrefixes = ['Assets:', 'Liabilities:', 'Equity:', 'Income:', 'Expenses:']
|
if (!name) {
|
||||||
if (!validPrefixes.some(p => name.startsWith(p))) {
|
this.$q.notify({type: 'warning', message: 'Enter a sub-account name'})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Each segment under the root must be a valid Beancount account
|
||||||
|
// component: start with an uppercase letter, then letters/digits/hyphens.
|
||||||
|
const badSegment = name.split(':').slice(1).find(
|
||||||
|
seg => !/^[A-Z][A-Za-z0-9-]*$/.test(seg)
|
||||||
|
)
|
||||||
|
if (badSegment !== undefined) {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: `Account name must start with one of: ${validPrefixes.join(', ')}`
|
message: `Invalid segment "${badSegment}" — each part must start with a capital letter (letters, digits, hyphens only)`
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1251,15 +1251,28 @@
|
||||||
<q-form @submit="submitAddAccount" class="q-gutter-md">
|
<q-form @submit="submitAddAccount" class="q-gutter-md">
|
||||||
<div class="text-h6 q-mb-md">Add Account</div>
|
<div class="text-h6 q-mb-md">Add Account</div>
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="addAccountDialog.rootType"
|
||||||
|
:options="accountRootTypes"
|
||||||
|
label="Account Type *"
|
||||||
|
hint="Top-level category — the only valid parents"
|
||||||
|
></q-select>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.trim="addAccountDialog.name"
|
v-model.trim="addAccountDialog.subPath"
|
||||||
label="Account Name *"
|
label="Sub-account *"
|
||||||
placeholder="e.g., Expenses:Services:Domain"
|
placeholder="e.g., Vehicle:Gas"
|
||||||
hint="Full hierarchical name. Must start with Assets:, Liabilities:, Equity:, Income: or Expenses:"
|
hint="Path under the type. Use ':' to nest; capitalize each part."
|
||||||
></q-input>
|
></q-input>
|
||||||
|
|
||||||
|
<div v-if="addAccountFullName" class="text-caption text-grey">
|
||||||
|
Will create: <span class="text-weight-medium">{% raw %}{{ addAccountFullName }}{% endraw %}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
|
@ -1279,7 +1292,7 @@
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
:loading="addAccountDialog.loading"
|
:loading="addAccountDialog.loading"
|
||||||
:disable="!addAccountDialog.name"
|
:disable="!addAccountFullName"
|
||||||
>
|
>
|
||||||
Create Account
|
Create Account
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue