support transaction tag group

This commit is contained in:
MaysWind
2026-01-17 00:47:51 +08:00
parent b556efa510
commit 7d9cfc4ced
59 changed files with 3289 additions and 795 deletions
+8 -6
View File
@@ -171,22 +171,24 @@
@error="onShowDateRangeError" />
<explorer-list-dialog ref="explorerListDialog" />
<explorer-rename-dialog ref="explorerRenameDialog" />
<edit-dialog ref="editDialog" :type="TransactionEditPageType.Transaction" />
<export-dialog ref="exportDialog" />
<rename-dialog ref="renameDialog"
:default-title="tt('Rename Explorer')"
:label="tt('Explorer Name')" :placeholder="tt('Explorer Name')" />
<confirm-dialog ref="confirmDialog"/>
<snack-bar ref="snackbar" />
</template>
<script setup lang="ts">
import RenameDialog from '@/components/desktop/RenameDialog.vue';
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
import SnackBar from '@/components/desktop/SnackBar.vue';
import ExplorerQueryTab from '@/views/desktop/insights/tabs/ExplorerQueryTab.vue';
import ExplorerDataTableTab from '@/views/desktop/insights/tabs/ExplorerDataTableTab.vue';
import ExplorerChartTab from '@/views/desktop/insights/tabs/ExplorerChartTab.vue';
import ExplorerListDialog from '@/views/desktop/insights/dialogs/ExplorerListDialog.vue';
import ExplorerRenameDialog from '@/views/desktop/insights/dialogs/ExplorerRenameDialog.vue';
import EditDialog from '@/views/desktop/transactions/list/dialogs/EditDialog.vue';
import ExportDialog from '@/views/desktop/statistics/transaction/dialogs/ExportDialog.vue';
@@ -251,12 +253,12 @@ const props = defineProps<InsightsExplorerProps>();
type ExplorerPageTabType = 'query' | 'table' | 'chart';
type RenameDialogType = InstanceType<typeof RenameDialog>;
type ConfirmDialogType = InstanceType<typeof ConfirmDialog>;
type SnackBarType = InstanceType<typeof SnackBar>;
type ExplorerDataTableTabType = InstanceType<typeof ExplorerDataTableTab>;
type ExplorerChartTabType = InstanceType<typeof ExplorerChartTab>;
type ExplorerListDialogType = InstanceType<typeof ExplorerListDialog>;
type ExplorerRenameDialogType = InstanceType<typeof ExplorerRenameDialog>;
type EditDialogType = InstanceType<typeof EditDialog>;
type ExportDialogType = InstanceType<typeof ExportDialog>;
@@ -282,12 +284,12 @@ const timezoneTypeIconMap = {
[TimezoneTypeForStatistics.TransactionTimezone.type]: mdiInvoiceTextClockOutline
};
const renameDialog = useTemplateRef<RenameDialogType>('renameDialog');
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
const snackbar = useTemplateRef<SnackBarType>('snackbar');
const explorerDataTableTab = useTemplateRef<ExplorerDataTableTabType>('explorerDataTableTab');
const explorerChartTab = useTemplateRef<ExplorerChartTabType>('explorerChartTab');
const explorerListDialog = useTemplateRef<ExplorerListDialogType>('explorerListDialog');
const explorerRenameDialog = useTemplateRef<ExplorerRenameDialogType>('explorerRenameDialog');
const exportDialog = useTemplateRef<ExportDialogType>('exportDialog');
const editDialog = useTemplateRef<EditDialogType>('editDialog');
@@ -515,7 +517,7 @@ function showChangeExplorerDisplayOrderDialog(): void {
function saveExplorer(saveAs?: boolean): void {
if (saveAs || !currentExplorer.value.name) {
explorerRenameDialog.value?.open(currentExplorer.value.name || '', tt('Set Explorer Name')).then((newName: string) => {
renameDialog.value?.open(currentExplorer.value.name || '', tt('Set Explorer Name')).then((newName: string) => {
currentExplorer.value.name = newName;
doSaveExplorer(saveAs);
})
@@ -571,7 +573,7 @@ function restoreExplorer(): void {
}
function setExplorerName(): void {
explorerRenameDialog.value?.open(currentExplorer.value.name || '').then((newName: string) => {
renameDialog.value?.open(currentExplorer.value.name || '').then((newName: string) => {
currentExplorer.value.name = newName;
});
}
@@ -1,74 +0,0 @@
<template>
<v-dialog max-width="500" :persistent="oldExplorerName !== newExplorerName" v-model="showState">
<v-card class="pa-sm-1 pa-md-2">
<template #title>
<h4 class="text-h4 text-wrap">{{ dialogTitle || tt('Rename Explorer') }}</h4>
</template>
<v-card-text class="w-100 d-flex justify-center">
<v-text-field persistent-placeholder
:autofocus="true"
:label="tt('Explorer Name')"
:placeholder="tt('Explorer Name')"
v-model="newExplorerName"
@keyup.enter="save" />
</v-card-text>
<v-card-text>
<div class="w-100 d-flex justify-center flex-wrap mt-sm-1 mt-md-2 gap-4">
<v-btn color="primary" :disabled="!newExplorerName || oldExplorerName === newExplorerName" @click="save">
{{ tt('Save') }}
</v-btn>
<v-btn color="secondary" variant="tonal" @click="cancel">
{{ tt('Cancel') }}
</v-btn>
</div>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from '@/locales/helpers.ts';
const { tt } = useI18n();
let resolveFunc: ((name: string) => void) | null = null;
let rejectFunc: ((reason?: unknown) => void) | null = null;
const showState = ref<boolean>(false);
const dialogTitle = ref<string | undefined>(undefined);
const oldExplorerName = ref<string>('');
const newExplorerName = ref<string>('');
function open(currentExplorerName: string, title?: string): Promise<string> {
showState.value = true;
dialogTitle.value = title;
oldExplorerName.value = currentExplorerName;
newExplorerName.value = currentExplorerName;
return new Promise((resolve, reject) => {
resolveFunc = resolve;
rejectFunc = reject;
});
}
function save(): void {
if (!newExplorerName.value || oldExplorerName.value === newExplorerName.value) {
return;
}
resolveFunc?.(newExplorerName.value);
showState.value = false;
}
function cancel(): void {
rejectFunc?.();
showState.value = false;
}
defineExpose({
open
});
</script>
@@ -251,56 +251,12 @@
(conditionWithRelation.condition.operator === TransactionExplorerConditionOperator.IsEmpty.value || conditionWithRelation.condition.operator === TransactionExplorerConditionOperator.IsNotEmpty.value)"
/>
<v-autocomplete
<transaction-tag-auto-complete
density="compact"
item-title="name"
item-value="id"
auto-select-first
persistent-placeholder
multiple
chips
closable-chips
:disabled="loading || disabled || !!editingQuery"
:placeholder="tt('None')"
:items="allTags"
v-model="conditionWithRelation.condition.value"
v-model:search="tagSearchContent"
v-else-if="conditionWithRelation.condition.operator !== TransactionExplorerConditionOperator.IsEmpty.value && conditionWithRelation.condition.operator !== TransactionExplorerConditionOperator.IsNotEmpty.value"
>
<template #chip="{ props, item }">
<v-chip :prepend-icon="mdiPound" :text="item.title" v-bind="props"/>
</template>
<template #item="{ props, item }">
<v-list-item :value="item.value" v-bind="props" v-if="!item.raw.hidden">
<template #title>
<v-list-item-title>
<div class="d-flex align-center">
<v-icon size="20" start :icon="mdiPound"/>
<span>{{ item.title }}</span>
</div>
</v-list-item-title>
</template>
</v-list-item>
<v-list-item :disabled="true" v-bind="props"
v-if="item.raw.hidden && item.raw.name.toLowerCase().indexOf(tagSearchContent.toLowerCase()) >= 0 && isAllFilteredTagHidden">
<template #title>
<v-list-item-title>
<div class="d-flex align-center">
<v-icon size="20" start :icon="mdiPound"/>
<span>{{ item.title }}</span>
</div>
</v-list-item-title>
</template>
</v-list-item>
</template>
<template #no-data>
<v-list class="py-0">
<v-list-item>{{ tt('No available tag') }}</v-list-item>
</v-list>
</template>
</v-autocomplete>
/>
</div>
<v-text-field disabled density="compact"
@@ -401,10 +357,6 @@ import {
TransactionExplorerConditionOperator
} from '@/core/explorer.ts';
import {
type TransactionTag
} from '@/models/transaction_tag.ts';
import {
type TransactionExplorerCondition,
TransactionExplorerQuery
@@ -427,8 +379,7 @@ import {
mdiContentCopy,
mdiCheck,
mdiClose,
mdiDrag,
mdiPound
mdiDrag
} from '@mdi/js';
interface ExplorerQueryTabProps {
@@ -460,7 +411,6 @@ const showExpression = ref<Record<string, boolean>>({});
const showFilterSourceAccountsDialog = ref<boolean>(false);
const showFilterDestinationAccountsDialog = ref<boolean>(false);
const showFilterTransactionCategoriesDialog = ref<boolean>(false);
const tagSearchContent = ref<string>('');
const editingQuery = ref<TransactionExplorerQuery | undefined>(undefined);
const editingQueryName = ref<string>('');
@@ -474,27 +424,9 @@ const queries = computed<TransactionExplorerQuery[]>({
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const hasAnyAccount = computed<boolean>(() => accountsStore.allPlainAccounts.length > 0);
const hasAnyTransactionCategory = computed<boolean>(() => !isObjectEmpty(transactionCategoriesStore.allTransactionCategoriesMap));
const allTags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
const allTransactionExplorerConditionFields = computed<NameValue[]>(() => getAllTransactionExplorerConditionFields());
const isAllFilteredTagHidden = computed<boolean>(() => {
const lowerCaseTagSearchContent = tagSearchContent.value.toLowerCase();
let hiddenCount = 0;
for (const tag of allTags.value) {
if (!lowerCaseTagSearchContent || tag.name.toLowerCase().indexOf(lowerCaseTagSearchContent) >= 0) {
if (!tag.hidden) {
return false;
}
hiddenCount++;
}
}
return hiddenCount > 0;
});
function getFilteredAccountsDisplayContent(filterAccountIds?: Record<string, boolean>): string {
if ((props.loading && !hasAnyAccount.value) || !accountsStore.allVisiblePlainAccounts || !accountsStore.allVisiblePlainAccounts.length) {
return '';