feat: UI changs

This commit is contained in:
Vlad Stan
2026-03-27 12:33:19 +02:00
parent 1bedc428a5
commit 3d919aa1a8
+346 -61
View File
@@ -1389,7 +1389,7 @@
v-model="testEntry.callParamsText"
class="native-textarea test-json-editor"
spellcheck="false"
@blur="persistTestCallParams(testEntry)"
@input="persistTestCallParams(testEntry, { silent: true })"
></textarea>
<div v-if="testEntry.callParamsError" class="settings-error q-mt-sm">
{{ testEntry.callParamsError }}
@@ -1407,7 +1407,7 @@
v-model="testEntry.expectedText"
class="native-textarea test-json-editor"
spellcheck="false"
@blur="persistTestExpected(testEntry)"
@input="persistTestExpected(testEntry, { silent: true })"
></textarea>
<div v-if="testEntry.expectedError" class="settings-error q-mt-sm">
{{ testEntry.expectedError }}
@@ -1472,7 +1472,7 @@
class="native-input"
type="text"
spellcheck="false"
@blur="persistTestMockSet(testEntry, mockSet)"
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
/>
</div>
@@ -1554,7 +1554,7 @@
type="text"
placeholder="Optional label"
spellcheck="false"
@blur="persistTestMockSet(testEntry, mockSet)"
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
/>
</div>
@@ -1572,7 +1572,7 @@
type="text"
placeholder="json"
spellcheck="false"
@blur="persistTestMockSet(testEntry, mockSet)"
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
/>
</div>
@@ -1588,7 +1588,7 @@
v-model="item.requestBodyText"
class="native-textarea test-mock-item-json"
spellcheck="false"
@blur="persistTestMockSet(testEntry, mockSet)"
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
></textarea>
<div v-if="item.requestBodyError" class="settings-error q-mt-sm">
{{ item.requestBodyError }}
@@ -1609,7 +1609,7 @@
type="text"
placeholder="json"
spellcheck="false"
@blur="persistTestMockSet(testEntry, mockSet)"
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
/>
</div>
@@ -1625,7 +1625,7 @@
v-model="item.responseText"
class="native-textarea test-mock-item-json"
spellcheck="false"
@blur="persistTestMockSet(testEntry, mockSet)"
@input="persistTestMockSet(testEntry, mockSet, { silent: true })"
></textarea>
<div v-if="item.responseError" class="settings-error q-mt-sm">
{{ item.responseError }}
@@ -1784,7 +1784,7 @@
type="text"
placeholder="setting_key"
spellcheck="false"
@blur="persistFundingSourceSettings()"
@input="persistFundingSourceSettings({ silent: true })"
/>
</div>
@@ -1823,7 +1823,7 @@
v-model="row.valueText"
class="native-textarea"
spellcheck="false"
@blur="persistFundingSourceSettings()"
@input="persistFundingSourceSettings({ silent: true })"
></textarea>
<input
v-else
@@ -1832,7 +1832,7 @@
class="native-input"
type="text"
spellcheck="false"
@blur="persistFundingSourceSettings()"
@input="persistFundingSourceSettings({ silent: true })"
/>
</div>
@@ -1939,7 +1939,7 @@
type="text"
placeholder="mock_name"
spellcheck="false"
@blur="persistFunctionMocks()"
@input="persistFunctionMocks({ silent: true })"
/>
<div class="settings-hint q-mt-sm">
Mock Name: Stable mock identifier used inside this function's fixture map.
@@ -1963,7 +1963,7 @@
type="text"
placeholder="json"
spellcheck="false"
@blur="persistFunctionMocks()"
@input="persistFunctionMocks({ silent: true })"
/>
<div class="settings-hint">
Request Type: Optional request payload format such as
@@ -1979,7 +1979,7 @@
type="text"
placeholder="GET"
spellcheck="false"
@blur="persistFunctionMocks()"
@input="persistFunctionMocks({ silent: true })"
/>
<div class="settings-hint">
Method: HTTP verb or RPC method name used by this reusable mock.
@@ -1994,7 +1994,7 @@
type="text"
placeholder="/path"
spellcheck="false"
@blur="persistFunctionMocks()"
@input="persistFunctionMocks({ silent: true })"
/>
<div class="settings-hint">
Path: Request path or URI matcher for REST mocks.
@@ -2009,7 +2009,7 @@
class="native-textarea"
placeholder='{"Authorization":"Bearer ..."}'
spellcheck="false"
@blur="persistFunctionMocks()"
@input="persistFunctionMocks({ silent: true })"
></textarea>
<div class="settings-hint q-mt-sm">
Headers JSON: JSON object of request headers that must match.
@@ -2023,7 +2023,7 @@
class="native-textarea"
placeholder='{"foo":"bar"}'
spellcheck="false"
@blur="persistFunctionMocks()"
@input="persistFunctionMocks({ silent: true })"
></textarea>
<div class="settings-hint q-mt-sm">
Query Params JSON: JSON object of expected query-string parameters.
@@ -2039,7 +2039,7 @@
type="text"
placeholder="json"
spellcheck="false"
@blur="persistFunctionMocks()"
@input="persistFunctionMocks({ silent: true })"
/>
<div class="settings-hint">
Response Type: Optional response mode such as
@@ -2057,7 +2057,7 @@
class="native-textarea"
placeholder='{"field":"value"}'
spellcheck="false"
@blur="persistFunctionMocks()"
@input="persistFunctionMocks({ silent: true })"
></textarea>
<div class="settings-hint">
Response Body: JSON payload, data object, or exception body returned by the mock.
@@ -2198,6 +2198,7 @@
autocapitalize="off"
autocomplete="off"
autocorrect="off"
@input="syncEditorFromRawText(selectedDatasetKey, { silent: true })"
></textarea>
</q-card-section>
</q-card>
@@ -2231,6 +2232,7 @@
testFile: 'tests/wallets/test_rpc_wallets.py'
}
]
const STORAGE_PREFIX = 'lnbits-wallet-fixture-studio'
const JsonPanel = {
name: 'JsonPanel',
@@ -3064,19 +3066,39 @@
this.ensureSelections(this.selectedDatasetKey)
this.ensureSettingsDraftForCurrent()
this.ensureFunctionMockDraftForCurrent()
this.persistUiState()
},
currentFundingSourceName() {
this.ensureSettingsDraftForCurrent()
this.persistUiState()
},
currentTestFundingSourceName() {
this.ensureFunctionMockDraftForCurrent()
this.persistUiState()
},
currentFunctionName() {
this.ensureFunctionMockDraftForCurrent()
this.persistUiState()
},
currentSelectedTestTypeKey() {
this.persistUiState()
}
},
mounted() {
this.loadRelative(this.selectedDatasetKey)
this.restoreUiState()
let restoredAny = this.restoreActiveDatasetFromStorage()
if (!restoredAny) {
for (const def of DATASET_DEFS) {
restoredAny = this.restoreDatasetFromStorage(def.key) || restoredAny
}
}
if (!this.datasets[this.selectedDatasetKey].analysis) {
this.loadRelative(this.selectedDatasetKey)
} else if (!restoredAny) {
this.loadRelative(this.selectedDatasetKey)
}
window.addEventListener('keydown', this.handleGlobalKeydown)
},
@@ -3084,6 +3106,224 @@
window.removeEventListener('keydown', this.handleGlobalKeydown)
},
methods: {
storageKey(datasetKey) {
return `${STORAGE_PREFIX}:dataset:${datasetKey}`
},
activeDatasetStorageKey() {
return `${STORAGE_PREFIX}:active-dataset`
},
uiStateStorageKey() {
return `${STORAGE_PREFIX}:ui-state`
},
canUseLocalStorage() {
try {
return typeof window !== 'undefined' && Boolean(window.localStorage)
} catch (error) {
return false
}
},
persistDatasetToStorage(datasetKey) {
if (!this.canUseLocalStorage()) {
return
}
const dataset = this.datasets[datasetKey]
if (!dataset?.rawText) {
window.localStorage.removeItem(this.storageKey(datasetKey))
if (datasetKey === this.selectedDatasetKey) {
window.localStorage.removeItem(this.activeDatasetStorageKey())
}
return
}
const payload = JSON.stringify({
rawText: dataset.rawText,
snapshotText: dataset.snapshotText,
sourceLabel: dataset.sourceLabel || ''
})
window.localStorage.setItem(this.storageKey(datasetKey), payload)
if (datasetKey === this.selectedDatasetKey) {
window.localStorage.setItem(
this.activeDatasetStorageKey(),
JSON.stringify({
datasetKey,
rawText: dataset.rawText,
snapshotText: dataset.snapshotText,
sourceLabel: dataset.sourceLabel || ''
})
)
}
},
restoreActiveDatasetFromStorage() {
if (!this.canUseLocalStorage()) {
return false
}
const storedValue = window.localStorage.getItem(this.activeDatasetStorageKey())
if (!storedValue) {
return false
}
const parsedStoredValue = parseJsonText(storedValue)
if (!parsedStoredValue.ok || !isPlainObject(parsedStoredValue.data)) {
window.localStorage.removeItem(this.activeDatasetStorageKey())
return false
}
const { datasetKey } = parsedStoredValue.data
if (
typeof datasetKey !== 'string' ||
!DATASET_DEFS.some(def => def.key === datasetKey)
) {
window.localStorage.removeItem(this.activeDatasetStorageKey())
return false
}
const rawText = String(parsedStoredValue.data.rawText || '').trim()
const snapshotText =
typeof parsedStoredValue.data.snapshotText === 'string'
? parsedStoredValue.data.snapshotText
: rawText
if (!rawText) {
window.localStorage.removeItem(this.activeDatasetStorageKey())
return false
}
const parsedDataset = parseJsonText(rawText)
if (!parsedDataset.ok) {
window.localStorage.removeItem(this.activeDatasetStorageKey())
return false
}
this.selectedDatasetKey = datasetKey
this.commitDataset(datasetKey, parsedDataset.data, {
rawText,
snapshotText,
sourceLabel:
parsedStoredValue.data.sourceLabel ||
`Local storage: ${this.datasets[datasetKey].fileName}`,
fileHandle: null,
loadError: ''
})
return true
},
restoreDatasetFromStorage(datasetKey) {
if (!this.canUseLocalStorage()) {
return false
}
const storedValue = window.localStorage.getItem(this.storageKey(datasetKey))
if (!storedValue) {
return false
}
const parsedStoredValue = parseJsonText(storedValue)
if (!parsedStoredValue.ok || !isPlainObject(parsedStoredValue.data)) {
window.localStorage.removeItem(this.storageKey(datasetKey))
return false
}
const rawText = String(parsedStoredValue.data.rawText || '').trim()
const snapshotText =
typeof parsedStoredValue.data.snapshotText === 'string'
? parsedStoredValue.data.snapshotText
: rawText
if (!rawText) {
window.localStorage.removeItem(this.storageKey(datasetKey))
return false
}
const parsedDataset = parseJsonText(rawText)
if (!parsedDataset.ok) {
window.localStorage.removeItem(this.storageKey(datasetKey))
return false
}
this.commitDataset(datasetKey, parsedDataset.data, {
rawText,
snapshotText,
sourceLabel:
parsedStoredValue.data.sourceLabel ||
`Local storage: ${this.datasets[datasetKey].fileName}`,
fileHandle: null,
loadError: ''
})
return true
},
persistUiState() {
if (!this.canUseLocalStorage()) {
return
}
window.localStorage.setItem(
this.uiStateStorageKey(),
JSON.stringify({
selectedDatasetKey: this.selectedDatasetKey,
selectedFunctionByDataset: this.selectedFunctionByDataset,
selectedFundingSourceByDataset: this.selectedFundingSourceByDataset,
selectedTestFundingSourceByDataset:
this.selectedTestFundingSourceByDataset,
selectedTestTypeByScope: this.selectedTestTypeByScope
})
)
},
restoreUiState() {
if (!this.canUseLocalStorage()) {
return
}
const storedValue = window.localStorage.getItem(this.uiStateStorageKey())
if (!storedValue) {
return
}
const parsedStoredValue = parseJsonText(storedValue)
if (!parsedStoredValue.ok || !isPlainObject(parsedStoredValue.data)) {
window.localStorage.removeItem(this.uiStateStorageKey())
return
}
const state = parsedStoredValue.data
if (
typeof state.selectedDatasetKey === 'string' &&
DATASET_DEFS.some(def => def.key === state.selectedDatasetKey)
) {
this.selectedDatasetKey = state.selectedDatasetKey
}
if (isPlainObject(state.selectedFunctionByDataset)) {
Object.assign(this.selectedFunctionByDataset, state.selectedFunctionByDataset)
}
if (isPlainObject(state.selectedFundingSourceByDataset)) {
Object.assign(
this.selectedFundingSourceByDataset,
state.selectedFundingSourceByDataset
)
}
if (isPlainObject(state.selectedTestFundingSourceByDataset)) {
Object.assign(
this.selectedTestFundingSourceByDataset,
state.selectedTestFundingSourceByDataset
)
}
if (isPlainObject(state.selectedTestTypeByScope)) {
this.selectedTestTypeByScope = {
...state.selectedTestTypeByScope
}
}
},
notify(message, type = 'info', caption = '') {
Quasar.Notify.create({
message,
@@ -3416,7 +3656,7 @@
return parsed.data
},
persistTestCallParams(testEntry) {
persistTestCallParams(testEntry, options = {}) {
if (!this.currentFunctionName) {
return
}
@@ -3432,7 +3672,13 @@
} catch (error) {
testEntry.callParamsError =
error instanceof Error ? error.message : String(error)
this.notify('Could not save call params.', 'negative', testEntry.callParamsError)
if (!options.silent) {
this.notify(
'Could not save call params.',
'negative',
testEntry.callParamsError
)
}
return
}
@@ -3448,7 +3694,7 @@
}
)
},
persistTestExpected(testEntry) {
persistTestExpected(testEntry, options = {}) {
if (!this.currentFunctionName) {
return
}
@@ -3464,11 +3710,13 @@
} catch (error) {
testEntry.expectedError =
error instanceof Error ? error.message : String(error)
this.notify(
`Could not save ${testEntry.expectedLabel.toLowerCase()}.`,
'negative',
testEntry.expectedError
)
if (!options.silent) {
this.notify(
`Could not save ${testEntry.expectedLabel.toLowerCase()}.`,
'negative',
testEntry.expectedError
)
}
return
}
@@ -3484,7 +3732,7 @@
}
)
},
persistTestMockSet(testEntry, mockSet) {
persistTestMockSet(testEntry, mockSet, options = {}) {
if (!this.currentFunctionName || !this.currentTestFundingSourceName) {
return
}
@@ -3506,11 +3754,13 @@
if (!mockName) {
if ((binding.items || []).length) {
mockSet.error = 'Select a function mock before editing its overrides.'
this.notify(
'Could not save test mock definition.',
'negative',
mockSet.error
)
if (!options.silent) {
this.notify(
'Could not save test mock definition.',
'negative',
mockSet.error
)
}
return
}
@@ -3519,11 +3769,13 @@
if (Object.prototype.hasOwnProperty.call(nextValue, mockName)) {
mockSet.error = `Duplicate function mock selected: ${mockName}`
this.notify(
'Could not save test mock definition.',
'negative',
mockSet.error
)
if (!options.silent) {
this.notify(
'Could not save test mock definition.',
'negative',
mockSet.error
)
}
return
}
@@ -3555,11 +3807,13 @@
item.requestBodyError =
error instanceof Error ? error.message : String(error)
mockSet.error = item.requestBodyError
this.notify(
'Could not save test mock definition.',
'negative',
mockSet.error
)
if (!options.silent) {
this.notify(
'Could not save test mock definition.',
'negative',
mockSet.error
)
}
return
}
}
@@ -3578,11 +3832,13 @@
item.responseError =
error instanceof Error ? error.message : String(error)
mockSet.error = item.responseError
this.notify(
'Could not save test mock definition.',
'negative',
mockSet.error
)
if (!options.silent) {
this.notify(
'Could not save test mock definition.',
'negative',
mockSet.error
)
}
return
}
}
@@ -3675,7 +3931,7 @@
rows.splice(rowIndex, 1)
this.persistFundingSourceSettings()
},
persistFundingSourceSettings() {
persistFundingSourceSettings(options = {}) {
if (!this.currentFundingSourceName) {
return
}
@@ -3699,11 +3955,13 @@
} catch (error) {
row.error =
error instanceof Error ? error.message : String(error)
this.notify(
`Could not save setting "${key || 'new setting'}".`,
'negative',
row.error
)
if (!options.silent) {
this.notify(
`Could not save setting "${key || 'new setting'}".`,
'negative',
row.error
)
}
return
}
}
@@ -3736,7 +3994,7 @@
rows.splice(rowIndex, 1)
this.persistFunctionMocks()
},
persistFunctionMocks() {
persistFunctionMocks(options = {}) {
if (!this.currentFunctionName || !this.currentTestFundingSourceName) {
return
}
@@ -3760,11 +4018,13 @@
} catch (error) {
row.error =
error instanceof Error ? error.message : String(error)
this.notify(
`Could not save mock "${name || 'new mock'}".`,
'negative',
row.error
)
if (!options.silent) {
this.notify(
`Could not save mock "${name || 'new mock'}".`,
'negative',
row.error
)
}
return
}
}
@@ -3890,6 +4150,31 @@
)
}
}
this.persistDatasetToStorage(datasetKey)
this.persistUiState()
},
syncEditorFromRawText(datasetKey, options = {}) {
const dataset = this.datasets[datasetKey]
const parsed = parseJsonText(dataset.rawText)
if (!parsed.ok) {
dataset.parseError = `Invalid JSON in ${dataset.fileName}: ${parsed.error}`
if (!options.silent) {
this.notify('Could not apply the editor contents.', 'negative')
}
return false
}
this.commitDataset(datasetKey, parsed.data, {
rawText: dataset.rawText,
snapshotText: dataset.snapshotText,
fileHandle: dataset.fileHandle,
sourceLabel: dataset.sourceLabel,
loadError: dataset.loadError
})
return true
},
validateEditor(datasetKey) {
const dataset = this.datasets[datasetKey]