mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-18 08:44:25 +08:00
support renaming queries, duplicating queries, and displaying query expressions separately for each query
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user