support renaming queries, duplicating queries, and displaying query expressions separately for each query

This commit is contained in:
MaysWind
2025-12-24 01:30:18 +08:00
parent 76af5d946a
commit 0dc2825e5d
2 changed files with 125 additions and 25 deletions
+16
View File
@@ -212,6 +212,22 @@ export class TransactionExploreQuery {
return finalTokens; return finalTokens;
} }
public clone(): TransactionExploreQuery {
const clonedConditions: TransactionExploreConditionWithRelation[] = [];
for (const condition of this.conditions) {
const clonedCondition = TransactionExploreConditionWithRelation.parse(condition.toJsonObject());
if (!clonedCondition) {
continue;
}
clonedConditions.push(clonedCondition);
}
return new TransactionExploreQuery(this.name, clonedConditions);
}
public toJson(): string { public toJson(): string {
return JSON.stringify({ return JSON.stringify({
name: this.name, name: this.name,
@@ -1,31 +1,66 @@
<template> <template>
<v-card-text class="pt-0"> <v-card-text class="pt-0">
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<v-btn color="primary" variant="outlined" <v-btn color="default" variant="outlined"
:disabled="loading" :disabled="loading || !!editingQuery"
@click="addQuery">{{ tt('Add Query') }}</v-btn> @click="addQuery">{{ tt('Add Query') }}</v-btn>
<v-spacer /> <v-spacer />
<v-btn color="secondary" variant="tonal" <v-btn color="secondary" variant="tonal"
:disabled="loading || queries.length < 1" :disabled="loading || !!editingQuery || queries.length < 1"
@click="clearAllQueries">{{ tt('Clear All') }}</v-btn> @click="clearAllQueries">{{ tt('Clear All') }}</v-btn>
</div> </div>
<div :key="queryIndex" v-for="(query, queryIndex) in queries"> <div :key="queryIndex" v-for="(query, queryIndex) in queries">
<v-card class="mt-4" variant="outlined"> <v-card class="mt-4" variant="outlined">
<v-card-title class="d-flex align-center py-2 px-4"> <v-card-title class="d-flex align-center py-2 px-4">
<span class="text-subtitle-1">{{ tt('Query') }} {{ `#${queryIndex + 1}` }}</span> <span class="text-subtitle-1" v-if="editingQuery !== query">{{ query.name || `${tt('Query')} #${queryIndex + 1}` }}</span>
<div class="query-name-edit" v-if="editingQuery === query">
<v-text-field type="text" density="compact" variant="underlined"
:disabled="loading"
:placeholder="`${tt('Query')} #${queryIndex + 1}`"
v-model="editingQueryName"
@keyup.enter="updateQueryName(query)" />
</div>
<v-btn class="ms-2" density="compact" color="primary" variant="text" size="small"
:icon="true" :disabled="loading"
@click="updateQueryName(query)"
v-if="editingQuery === query">
<v-icon :icon="mdiCheck" size="18" />
<v-tooltip activator="parent">{{ tt('Update') }}</v-tooltip>
</v-btn>
<v-btn class="ms-2" density="compact" color="default" variant="text" size="small"
:icon="true" :disabled="loading"
@click="cancelUpdateQueryName"
v-if="editingQuery === query">
<v-icon :icon="mdiClose" size="18" />
<v-tooltip activator="parent">{{ tt('Cancel') }}</v-tooltip>
</v-btn>
<v-btn class="ms-2" density="compact" color="default" variant="text" size="small"
:icon="true" :disabled="loading || !!editingQuery"
@click="editingQueryName = query.name; editingQuery = query"
v-if="!editingQuery || editingQuery !== query">
<v-icon :icon="mdiPencilOutline" size="18" />
<v-tooltip activator="parent">{{ tt('Modify Query Name') }}</v-tooltip>
</v-btn>
<v-btn class="ms-2" density="compact" color="default" variant="text" size="small"
:icon="true" :disabled="loading || !!editingQuery"
@click="duplicateQuery(query)"
v-if="!editingQuery || editingQuery !== query">
<v-icon :icon="mdiContentCopy" size="18" />
<v-tooltip activator="parent">{{ tt('Duplicate') }}</v-tooltip>
</v-btn>
<v-spacer /> <v-spacer />
<v-switch class="bidirectional-switch ms-2" color="secondary" <v-switch class="bidirectional-switch ms-2" color="secondary"
:disabled="!query.conditions || query.conditions.length < 1" :disabled="loading || !!editingQuery || !query.conditions || query.conditions.length < 1"
:label="tt('Expression')" :label="tt('Expression')"
v-model="showExpression" v-model="showExpression[queryIndex]"
@click="showExpression = !showExpression"> @click="showExpression[queryIndex] = !showExpression[queryIndex]">
<template #prepend> <template #prepend>
<span>{{ tt('Editor') }}</span> <span>{{ tt('Editor') }}</span>
</template> </template>
</v-switch> </v-switch>
<v-btn class="ms-2" density="compact" color="default" variant="text" size="small" <v-btn class="ms-2" density="compact" color="default" variant="text" size="small"
:icon="true" :disabled="loading || queries.length < 1" :icon="true" :disabled="loading || !!editingQuery || queries.length < 1"
@click="removeQuery(queryIndex)"> @click="removeQuery(queryIndex)">
<v-icon :icon="mdiClose" size="18" /> <v-icon :icon="mdiClose" size="18" />
<v-tooltip activator="parent">{{ tt('Remove Query') }}</v-tooltip> <v-tooltip activator="parent">{{ tt('Remove Query') }}</v-tooltip>
@@ -41,7 +76,7 @@
{{ tt('No conditions defined. All transactions will match.') }} {{ tt('No conditions defined. All transactions will match.') }}
</div> </div>
<div v-else-if="query.conditions && query.conditions.length > 0 && !showExpression"> <div v-else-if="query.conditions && query.conditions.length > 0 && !showExpression[queryIndex]">
<div :key="conditionIndex" v-for="(conditionWithRelation, conditionIndex) in query.conditions"> <div :key="conditionIndex" v-for="(conditionWithRelation, conditionIndex) in query.conditions">
<div class="d-flex overflow-x-auto align-center gap-2 mb-4"> <div class="d-flex overflow-x-auto align-center gap-2 mb-4">
<v-select <v-select
@@ -62,7 +97,7 @@
density="compact" density="compact"
item-title="displayName" item-title="displayName"
item-value="value" item-value="value"
:disabled="loading" :disabled="loading || !!editingQuery"
:items="[ :items="[
{ value: TransactionExploreConditionRelation.And, displayName: tt('AND') }, { value: TransactionExploreConditionRelation.And, displayName: tt('AND') },
{ value: TransactionExploreConditionRelation.Or, displayName: tt('OR') } { value: TransactionExploreConditionRelation.Or, displayName: tt('OR') }
@@ -76,7 +111,7 @@
density="compact" density="compact"
item-title="name" item-title="name"
item-value="value" item-value="value"
:disabled="loading" :disabled="loading || !!editingQuery"
:items="allTransactionExploreConditionFields" :items="allTransactionExploreConditionFields"
@update:model-value="updateConditionField(queryIndex, conditionIndex, TransactionExploreConditionField.valueOf($event))" @update:model-value="updateConditionField(queryIndex, conditionIndex, TransactionExploreConditionField.valueOf($event))"
v-model="conditionWithRelation.condition.field" v-model="conditionWithRelation.condition.field"
@@ -87,7 +122,7 @@
density="compact" density="compact"
item-title="name" item-title="name"
item-value="value" item-value="value"
:disabled="loading" :disabled="loading || !!editingQuery"
:items="getAllTransactionExploreConditionOperators(conditionWithRelation.getSupportedOperators())" :items="getAllTransactionExploreConditionOperators(conditionWithRelation.getSupportedOperators())"
v-model="conditionWithRelation.condition.operator" v-model="conditionWithRelation.condition.operator"
/> />
@@ -98,7 +133,7 @@
density="compact" density="compact"
item-title="displayName" item-title="displayName"
item-value="type" item-value="type"
:disabled="loading" :disabled="loading || !!editingQuery"
:placeholder="tt('None')" :placeholder="tt('None')"
:items="[ :items="[
{ type: TransactionType.Expense, displayName: tt('Expense') }, { type: TransactionType.Expense, displayName: tt('Expense') },
@@ -126,7 +161,7 @@
item-value="type" item-value="type"
persistent-placeholder persistent-placeholder
:readonly="true" :readonly="true"
:disabled="loading || !hasAnyTransactionCategory" :disabled="loading || !!editingQuery || !hasAnyTransactionCategory"
:placeholder="tt('None')" :placeholder="tt('None')"
:model-value="getFilteredTransactionCategoriesDisplayContent(arrayItemToObjectField(conditionWithRelation.condition.value as string[], true))" :model-value="getFilteredTransactionCategoriesDisplayContent(arrayItemToObjectField(conditionWithRelation.condition.value as string[], true))"
@click="currentCondition = conditionWithRelation.condition; showFilterTransactionCategoriesDialog = true" @click="currentCondition = conditionWithRelation.condition; showFilterTransactionCategoriesDialog = true"
@@ -140,7 +175,7 @@
item-value="type" item-value="type"
persistent-placeholder persistent-placeholder
:readonly="true" :readonly="true"
:disabled="loading || !hasAnyAccount" :disabled="loading || !!editingQuery || !hasAnyAccount"
:placeholder="tt('None')" :placeholder="tt('None')"
:model-value="getFilteredAccountsDisplayContent(arrayItemToObjectField(conditionWithRelation.condition.value as string[], true))" :model-value="getFilteredAccountsDisplayContent(arrayItemToObjectField(conditionWithRelation.condition.value as string[], true))"
@click="currentCondition = conditionWithRelation.condition; showFilterSourceAccountsDialog = true" @click="currentCondition = conditionWithRelation.condition; showFilterSourceAccountsDialog = true"
@@ -154,7 +189,7 @@
item-value="type" item-value="type"
persistent-placeholder persistent-placeholder
:readonly="true" :readonly="true"
:disabled="loading || !hasAnyAccount" :disabled="loading || !!editingQuery || !hasAnyAccount"
:placeholder="tt('None')" :placeholder="tt('None')"
:model-value="getFilteredAccountsDisplayContent(arrayItemToObjectField(conditionWithRelation.condition.value as string[], true))" :model-value="getFilteredAccountsDisplayContent(arrayItemToObjectField(conditionWithRelation.condition.value as string[], true))"
@click="currentCondition = conditionWithRelation.condition; showFilterDestinationAccountsDialog = true" @click="currentCondition = conditionWithRelation.condition; showFilterDestinationAccountsDialog = true"
@@ -166,7 +201,7 @@
conditionWithRelation.condition.field === TransactionExploreConditionField.DestinationAmount.value"> conditionWithRelation.condition.field === TransactionExploreConditionField.DestinationAmount.value">
<amount-input density="compact" <amount-input density="compact"
:currency="defaultCurrency" :currency="defaultCurrency"
:disabled="loading" :disabled="loading || !!editingQuery"
v-model="conditionWithRelation.condition.value[0]" v-model="conditionWithRelation.condition.value[0]"
/> />
<span class="ms-2 me-2" <span class="ms-2 me-2"
@@ -174,7 +209,7 @@
conditionWithRelation.condition.operator === TransactionExploreConditionOperator.NotBetween.value">~</span> conditionWithRelation.condition.operator === TransactionExploreConditionOperator.NotBetween.value">~</span>
<amount-input density="compact" <amount-input density="compact"
:currency="defaultCurrency" :currency="defaultCurrency"
:disabled="loading" :disabled="loading || !!editingQuery"
v-model="conditionWithRelation.condition.value[1]" v-model="conditionWithRelation.condition.value[1]"
v-if="conditionWithRelation.condition.operator === TransactionExploreConditionOperator.Between.value || v-if="conditionWithRelation.condition.operator === TransactionExploreConditionOperator.Between.value ||
conditionWithRelation.condition.operator === TransactionExploreConditionOperator.NotBetween.value" conditionWithRelation.condition.operator === TransactionExploreConditionOperator.NotBetween.value"
@@ -200,7 +235,7 @@
multiple multiple
chips chips
closable-chips closable-chips
:disabled="loading" :disabled="loading || !!editingQuery"
:placeholder="tt('None')" :placeholder="tt('None')"
:items="allTags" :items="allTags"
v-model="conditionWithRelation.condition.value" v-model="conditionWithRelation.condition.value"
@@ -250,7 +285,7 @@
/> />
<v-text-field density="compact" <v-text-field density="compact"
:disabled="loading" :disabled="loading || !!editingQuery"
:placeholder="tt('None')" :placeholder="tt('None')"
v-model="conditionWithRelation.condition.value" v-model="conditionWithRelation.condition.value"
v-else-if="conditionWithRelation.condition.field === TransactionExploreConditionField.Description.value && v-else-if="conditionWithRelation.condition.field === TransactionExploreConditionField.Description.value &&
@@ -261,7 +296,7 @@
<v-btn color="default" density="compact" <v-btn color="default" density="compact"
variant="text" size="small" variant="text" size="small"
:icon="true" :icon="true"
:disabled="loading" :disabled="loading || !!editingQuery"
@click="removeCondition(queryIndex, conditionIndex)"> @click="removeCondition(queryIndex, conditionIndex)">
<v-icon :icon="mdiClose" size="18" /> <v-icon :icon="mdiClose" size="18" />
<v-tooltip activator="parent">{{ tt('Remove Condition') }}</v-tooltip> <v-tooltip activator="parent">{{ tt('Remove Condition') }}</v-tooltip>
@@ -269,7 +304,7 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else-if="query.conditions && query.conditions.length > 0 && showExpression"> <div v-else-if="query.conditions && query.conditions.length > 0 && showExpression[queryIndex]">
<div class="w-100 code-container"> <div class="w-100 code-container">
<v-textarea class="w-100 always-cursor-text mb-4" :readonly="true" <v-textarea class="w-100 always-cursor-text mb-4" :readonly="true"
:value="getExpression(queryIndex)"></v-textarea> :value="getExpression(queryIndex)"></v-textarea>
@@ -279,7 +314,7 @@
<v-btn color="primary" density="comfortable" <v-btn color="primary" density="comfortable"
variant="text" size="small" variant="text" size="small"
:prepend-icon="mdiPlus" :prepend-icon="mdiPlus"
:disabled="loading || showExpression" :disabled="loading || !!editingQuery || showExpression[queryIndex]"
@click="addCondition(queryIndex)"> @click="addCondition(queryIndex)">
{{ tt('Add Condition') }} {{ tt('Add Condition') }}
</v-btn> </v-btn>
@@ -334,7 +369,7 @@ import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
import { useTransactionTagsStore } from '@/stores/transactionTag.ts'; import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
import { useExploresStore } from '@/stores/explore.ts'; import { useExploresStore } from '@/stores/explore.ts';
import { type NameValue, values } from '@/core/base.ts'; import { type NameValue, entries, values } from '@/core/base.ts';
import { AccountType } from '@/core/account.ts'; import { AccountType } from '@/core/account.ts';
import { TransactionType } from '@/core/transaction.ts'; import { TransactionType } from '@/core/transaction.ts';
import { import {
@@ -362,6 +397,9 @@ import logger from '@/lib/logger.ts';
import { import {
mdiPlus, mdiPlus,
mdiPencilOutline,
mdiContentCopy,
mdiCheck,
mdiClose, mdiClose,
mdiPound mdiPound
} from '@mdi/js'; } from '@mdi/js';
@@ -390,11 +428,13 @@ const exploresStore = useExploresStore();
const snackbar = useTemplateRef<SnackBarType>('snackbar'); const snackbar = useTemplateRef<SnackBarType>('snackbar');
const currentCondition = ref<TransactionExploreCondition | undefined>(undefined); const currentCondition = ref<TransactionExploreCondition | undefined>(undefined);
const showExpression = ref<boolean>(false); const showExpression = ref<Record<number, boolean>>({});
const showFilterSourceAccountsDialog = ref<boolean>(false); const showFilterSourceAccountsDialog = ref<boolean>(false);
const showFilterDestinationAccountsDialog = ref<boolean>(false); const showFilterDestinationAccountsDialog = ref<boolean>(false);
const showFilterTransactionCategoriesDialog = ref<boolean>(false); const showFilterTransactionCategoriesDialog = ref<boolean>(false);
const tagSearchContent = ref<string>(''); const tagSearchContent = ref<string>('');
const editingQuery = ref<TransactionExploreQuery | undefined>(undefined);
const editingQueryName = ref<string>('');
const queries = computed<TransactionExploreQuery[]>(() => exploresStore.transactionExploreFilter.query); const queries = computed<TransactionExploreQuery[]>(() => exploresStore.transactionExploreFilter.query);
@@ -492,11 +532,40 @@ function addQuery(): void {
queries.value.push(TransactionExploreQuery.create()); queries.value.push(TransactionExploreQuery.create());
} }
function updateQueryName(query: TransactionExploreQuery): void {
query.name = editingQueryName.value;
editingQuery.value = undefined;
editingQueryName.value = '';
}
function cancelUpdateQueryName(): void {
editingQuery.value = undefined;
editingQueryName.value = '';
}
function duplicateQuery(query: TransactionExploreQuery): void {
queries.value.push(query.clone());
}
function removeQuery(queryIndex: number): void { function removeQuery(queryIndex: number): void {
if (queries.value.length > 0) { if (queries.value.length > 0) {
queries.value.splice(queryIndex, 1); queries.value.splice(queryIndex, 1);
} }
const newShowExpression: Record<number, boolean> = {};
for (const [key, state] of entries(showExpression.value)) {
const index = parseInt(key);
if (queryIndex > index) {
newShowExpression[index] = state;
} else if (queryIndex < index) {
newShowExpression[index - 1] = state;
}
}
showExpression.value = newShowExpression;
if (queries.value.length < 1) { if (queries.value.length < 1) {
queries.value.push(TransactionExploreQuery.create()); queries.value.push(TransactionExploreQuery.create());
} }
@@ -610,3 +679,18 @@ if (queries.value.length === 0) {
queries.value.push(TransactionExploreQuery.create()); queries.value.push(TransactionExploreQuery.create());
} }
</script> </script>
<style>
.query-name-edit {
width: 200px;
height: 36px;
> .v-text-field {
.v-field__input {
margin-top: -2px;
margin-bottom: -3px;
padding-top: 0;
}
}
}
</style>