support transaction tag group
This commit is contained in:
@@ -55,60 +55,69 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text :class="{ 'flex-grow-1 overflow-y-auto': dialogMode }" v-else-if="!loading && hasAnyVisibleTag">
|
||||
<div class="mb-4" v-if="includeTagsCount > 1">
|
||||
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
|
||||
mandatory="force" divided
|
||||
:model-value="includeTagFilterType"
|
||||
@update:model-value="updateTransactionTagIncludeType($event)">
|
||||
<v-btn :value="TransactionTagFilterType.HasAny.type">{{ tt(TransactionTagFilterType.HasAny.name) }}</v-btn>
|
||||
<v-btn :value="TransactionTagFilterType.HasAll.type">{{ tt(TransactionTagFilterType.HasAll.name) }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
<div class="mb-4" v-if="excludeTagsCount > 1">
|
||||
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
|
||||
mandatory="force" divided
|
||||
:model-value="excludeTagFilterType"
|
||||
@update:model-value="updateTransactionTagExcludeType($event)">
|
||||
<v-btn :value="TransactionTagFilterType.NotHasAny.type">{{ tt(TransactionTagFilterType.NotHasAny.name) }}</v-btn>
|
||||
<v-btn :value="TransactionTagFilterType.NotHasAll.type">{{ tt(TransactionTagFilterType.NotHasAll.name) }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
<v-expansion-panels class="tag-categories" multiple v-model="expandTagCategories">
|
||||
<v-expansion-panel class="border" key="default" value="default">
|
||||
<v-expansion-panel-title class="expand-panel-title-with-bg py-0">
|
||||
<span class="ms-3">{{ tt('Tags') }}</span>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-list rounded density="comfortable" class="pa-0">
|
||||
<template :key="transactionTag.id"
|
||||
v-for="transactionTag in allVisibleTags">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="2" offset-y="2" :icon="mdiEyeOffOutline"
|
||||
v-if="transactionTag.hidden">
|
||||
<v-icon size="24" :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="24" :icon="mdiPound" v-else-if="!transactionTag.hidden"/>
|
||||
<span class="ms-3">{{ transactionTag.name }}</span>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
|
||||
mandatory="force" divided
|
||||
:model-value="filterTagIds[transactionTag.id]"
|
||||
@update:model-value="updateTransactionTagState(transactionTag, $event)">
|
||||
<v-btn :value="TransactionTagFilterState.Include">{{ tt('Included') }}</v-btn>
|
||||
<v-btn :value="TransactionTagFilterState.Default">{{ tt('Default') }}</v-btn>
|
||||
<v-btn :value="TransactionTagFilterState.Exclude">{{ tt('Excluded') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panels class="tag-categories" multiple v-model="expandTagGroups">
|
||||
<template :key="tagGroup.id" v-for="tagGroup in allTagGroupsWithDefault">
|
||||
<v-expansion-panel class="border" :value="tagGroup.id" v-if="allVisibleTags[tagGroup.id] && allVisibleTags[tagGroup.id]!.length > 0">
|
||||
<v-expansion-panel-title class="expand-panel-title-with-bg py-0">
|
||||
<span class="ms-3 text-truncate">{{ tagGroup.name }}</span>
|
||||
<v-spacer/>
|
||||
<div class="d-flex me-3" v-if="groupTagFilterTypesMap[tagGroup.id] && groupTagFilterStateCountMap[tagGroup.id]">
|
||||
<v-btn color="secondary" density="compact" variant="outlined"
|
||||
v-if="groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Include] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Include] > 1">
|
||||
{{ groupTagFilterTypesMap[tagGroup.id]!.includeType === TransactionTagFilterType.HasAll.type ? tt(TransactionTagFilterType.HasAll.name) : tt(TransactionTagFilterType.HasAny.name) }}
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :key="filterType.type" :title="tt(filterType.name)"
|
||||
:append-icon="groupTagFilterTypesMap[tagGroup.id]!.includeType === filterType.type ? mdiCheck : undefined"
|
||||
v-for="filterType in [TransactionTagFilterType.HasAny, TransactionTagFilterType.HasAll]"
|
||||
@click="updateTransactionTagGroupIncludeType(tagGroup, filterType)"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<v-btn class="ms-2" color="secondary" density="compact" variant="outlined"
|
||||
v-if="groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Exclude] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Exclude] > 1">
|
||||
{{ groupTagFilterTypesMap[tagGroup.id]!.excludeType === TransactionTagFilterType.NotHasAll.type ? tt(TransactionTagFilterType.NotHasAll.name) : tt(TransactionTagFilterType.NotHasAny.name) }}
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :key="filterType.type" :title="tt(filterType.name)"
|
||||
:append-icon="groupTagFilterTypesMap[tagGroup.id]!.excludeType === filterType.type ? mdiCheck : undefined"
|
||||
v-for="filterType in [TransactionTagFilterType.NotHasAny, TransactionTagFilterType.NotHasAll]"
|
||||
@click="updateTransactionTagGroupExcludeType(tagGroup, filterType)"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-list rounded density="comfortable" class="pa-0">
|
||||
<template :key="transactionTag.id"
|
||||
v-for="transactionTag in allVisibleTags[tagGroup.id]">
|
||||
<v-list-item class="ps-2">
|
||||
<template #prepend>
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="2" offset-y="2" :icon="mdiEyeOffOutline"
|
||||
v-if="transactionTag.hidden">
|
||||
<v-icon size="24" :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="24" :icon="mdiPound" v-else-if="!transactionTag.hidden"/>
|
||||
<span class="ms-3">{{ transactionTag.name }}</span>
|
||||
</template>
|
||||
<template #append>
|
||||
<v-btn-toggle class="toggle-buttons" density="compact" variant="outlined"
|
||||
mandatory="force" divided
|
||||
:model-value="tagFilterStateMap[transactionTag.id]"
|
||||
@update:model-value="updateTransactionTagState(transactionTag, $event)">
|
||||
<v-btn :value="TransactionTagFilterState.Include">{{ tt('Included') }}</v-btn>
|
||||
<v-btn :value="TransactionTagFilterState.Default">{{ tt('Default') }}</v-btn>
|
||||
<v-btn :value="TransactionTagFilterState.Exclude">{{ tt('Excluded') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</template>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
|
||||
@@ -136,11 +145,15 @@ import {
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { values } from '@/core/base.ts';
|
||||
import { TransactionTagFilterType } from '@/core/transaction.ts';
|
||||
|
||||
import type { TransactionTagGroup } from '@/models/transaction_tag_group.ts';
|
||||
import type { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
|
||||
import {
|
||||
mdiMagnify,
|
||||
mdiCheck,
|
||||
mdiSelectAll,
|
||||
mdiEyeOutline,
|
||||
mdiEyeOffOutline,
|
||||
@@ -166,14 +179,14 @@ const {
|
||||
loading,
|
||||
showHidden,
|
||||
filterContent,
|
||||
filterTagIds,
|
||||
includeTagFilterType,
|
||||
excludeTagFilterType,
|
||||
includeTagsCount,
|
||||
excludeTagsCount,
|
||||
tagFilterStateMap,
|
||||
groupTagFilterTypesMap,
|
||||
title,
|
||||
applyText,
|
||||
groupTagFilterStateCountMap,
|
||||
allTagGroupsWithDefault,
|
||||
allVisibleTags,
|
||||
allVisibleTagGroupIds,
|
||||
hasAnyAvailableTag,
|
||||
hasAnyVisibleTag,
|
||||
loadFilterTagIds,
|
||||
@@ -184,13 +197,14 @@ const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
const expandTagCategories = ref<string[]>([ 'default' ]);
|
||||
const expandTagGroups = ref<string[]>(allVisibleTagGroupIds.value);
|
||||
|
||||
function init(): void {
|
||||
transactionTagsStore.loadAllTags({
|
||||
force: false
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
expandTagGroups.value = allVisibleTagGroupIds.value;
|
||||
|
||||
if (!loadFilterTagIds()) {
|
||||
snackbar.value?.showError('Parameter Invalid');
|
||||
@@ -205,23 +219,35 @@ function init(): void {
|
||||
}
|
||||
|
||||
function updateTransactionTagState(transactionTag: TransactionTag, value: TransactionTagFilterState): void {
|
||||
filterTagIds.value[transactionTag.id] = value;
|
||||
tagFilterStateMap.value[transactionTag.id] = value;
|
||||
|
||||
if (props.autoSave) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
function updateTransactionTagIncludeType(value: number): void {
|
||||
includeTagFilterType.value = value;
|
||||
function updateTransactionTagGroupIncludeType(tagGroup: TransactionTagGroup, filterType: TransactionTagFilterType): void {
|
||||
const tagFilterTypes = groupTagFilterTypesMap.value[tagGroup.id];
|
||||
|
||||
if (!tagFilterTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
tagFilterTypes.includeType = filterType.type;
|
||||
|
||||
if (props.autoSave) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
function updateTransactionTagExcludeType(value: number): void {
|
||||
excludeTagFilterType.value = value;
|
||||
function updateTransactionTagGroupExcludeType(tagGroup: TransactionTagGroup, filterType: TransactionTagFilterType): void {
|
||||
const tagFilterTypes = groupTagFilterTypesMap.value[tagGroup.id];
|
||||
|
||||
if (!tagFilterTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
tagFilterTypes.excludeType = filterType.type;
|
||||
|
||||
if (props.autoSave) {
|
||||
save();
|
||||
@@ -229,8 +255,10 @@ function updateTransactionTagExcludeType(value: number): void {
|
||||
}
|
||||
|
||||
function setAllTagsState(value: TransactionTagFilterState): void {
|
||||
for (const tag of allVisibleTags.value) {
|
||||
filterTagIds.value[tag.id] = value;
|
||||
for (const tags of values(allVisibleTags.value)) {
|
||||
for (const tag of tags) {
|
||||
tagFilterStateMap.value[tag.id] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.autoSave) {
|
||||
|
||||
@@ -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 '';
|
||||
|
||||
+481
-239
@@ -2,288 +2,406 @@
|
||||
<v-row class="match-height">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<template #title>
|
||||
<div class="title-and-toolbar d-flex align-center">
|
||||
<span>{{ tt('Transaction Tags') }}</span>
|
||||
<v-btn class="ms-3" color="default" variant="outlined"
|
||||
:disabled="loading || updating || hasEditingTag" @click="add">{{ tt('Add') }}</v-btn>
|
||||
<v-btn class="ms-3" color="primary" variant="tonal"
|
||||
:disabled="loading || updating || hasEditingTag" @click="saveSortResult"
|
||||
v-if="displayOrderModified">{{ tt('Save Display Order') }}</v-btn>
|
||||
<v-btn density="compact" color="default" variant="text" size="24"
|
||||
class="ms-2" :icon="true" :disabled="loading || updating || hasEditingTag"
|
||||
:loading="loading" @click="reload">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20"/>
|
||||
</template>
|
||||
<v-icon :icon="mdiRefresh" size="24" />
|
||||
<v-tooltip activator="parent">{{ tt('Refresh') }}</v-tooltip>
|
||||
</v-btn>
|
||||
<v-spacer/>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
|
||||
:disabled="loading || updating || hasEditingTag" :icon="true">
|
||||
<v-icon :icon="mdiDotsVertical" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="mdiEyeOutline"
|
||||
:title="tt('Show Hidden Transaction Tags')"
|
||||
v-if="!showHidden" @click="showHidden = true"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiEyeOffOutline"
|
||||
:title="tt('Hide Hidden Transaction Tags')"
|
||||
v-if="showHidden" @click="showHidden = false"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-table class="transaction-tags-table table-striped" :hover="!loading">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ tt('Tag Title') }}</span>
|
||||
<v-spacer/>
|
||||
<span>{{ tt('Operation') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody v-if="loading && noAvailableTag && !newTag">
|
||||
<tr :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
|
||||
<td class="px-0">
|
||||
<v-skeleton-loader type="text" :loading="true"></v-skeleton-loader>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody v-if="!loading && noAvailableTag && !newTag">
|
||||
<tr>
|
||||
<td>{{ tt('No available tag') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<draggable-list tag="tbody"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
ghost-class="dragging-item"
|
||||
:class="{ 'has-bottom-border': newTag }"
|
||||
:disabled="noAvailableTag"
|
||||
v-model="tags"
|
||||
@change="onMove">
|
||||
<template #item="{ element }">
|
||||
<tr class="transaction-tags-table-row-tag text-sm" v-if="showHidden || !element.hidden">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<div class="d-flex align-center" v-if="editingTag.id !== element.id">
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="8" :icon="mdiEyeOffOutline"
|
||||
v-if="element.hidden">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="20" start :icon="mdiPound" v-else-if="!element.hidden"/>
|
||||
<span class="transaction-tag-name">{{ element.name }}</span>
|
||||
</div>
|
||||
|
||||
<v-text-field class="w-100 me-2" type="text"
|
||||
density="compact" variant="underlined"
|
||||
:disabled="loading || updating"
|
||||
:placeholder="tt('Tag Title')"
|
||||
v-model="editingTag.name"
|
||||
v-else-if="editingTag.id === element.id"
|
||||
@keyup.enter="save(editingTag)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="8" :icon="mdiEyeOffOutline"
|
||||
v-if="element.hidden">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="20" start :icon="mdiPound" v-else-if="!element.hidden"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn class="px-2 ms-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="element.hidden ? mdiEyeOutline : mdiEyeOffOutline"
|
||||
:loading="tagHiding[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="hide(element, !element.hidden)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ element.hidden ? tt('Show') : tt('Hide') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="mdiPencilOutline"
|
||||
:loading="tagUpdating[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="edit(element)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Edit') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="mdiDeleteOutline"
|
||||
:loading="tagRemoving[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="remove(element)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Delete') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiCheck"
|
||||
:loading="tagUpdating[element.id]"
|
||||
:disabled="loading || updating || !isTagModified(element)"
|
||||
v-if="editingTag.id === element.id" @click="save(editingTag)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Save') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiClose"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id === element.id" @click="cancelSave(editingTag)">
|
||||
{{ tt('Cancel') }}
|
||||
</v-btn>
|
||||
<span class="ms-2">
|
||||
<v-icon :class="!loading && !updating && !hasEditingTag && availableTagCount > 1 ? 'drag-handle' : 'disabled'"
|
||||
:icon="mdiDrag"/>
|
||||
<v-tooltip activator="parent" v-if="!loading && !updating && !hasEditingTag && availableTagCount > 1">{{ tt('Drag to Reorder') }}</v-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</draggable-list>
|
||||
|
||||
<tbody v-if="newTag">
|
||||
<tr class="text-sm" :class="{ 'even-row': (availableTagCount & 1) === 1}">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<v-text-field class="w-100 me-2" type="text" color="primary"
|
||||
density="compact" variant="underlined"
|
||||
:disabled="loading || updating" :placeholder="tt('Tag Title')"
|
||||
v-model="newTag.name" @keyup.enter="save(newTag)">
|
||||
<template #prepend>
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn class="px-2" density="comfortable" variant="text"
|
||||
:prepend-icon="mdiCheck"
|
||||
:loading="tagUpdating['']"
|
||||
:disabled="loading || updating || !isTagModified(newTag)"
|
||||
@click="save(newTag)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Save') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiClose"
|
||||
:disabled="loading || updating"
|
||||
@click="cancelSave(newTag)">
|
||||
{{ tt('Cancel') }}
|
||||
</v-btn>
|
||||
<span class="ms-2">
|
||||
<v-icon class="disabled" :icon="mdiDrag"/>
|
||||
<v-layout>
|
||||
<v-navigation-drawer :permanent="alwaysShowNav" v-model="showNav">
|
||||
<div class="mx-6 my-4">
|
||||
<span class="text-subtitle-2">{{ tt('Total tags') }}</span>
|
||||
<p class="transaction-tags-statistic-item-value mt-1">
|
||||
<span v-if="!loading || totalAvailableTagsCount > 0">{{ displayTotalAvailableTagsCount }}</span>
|
||||
<span v-else-if="loading && totalAvailableTagsCount <= 0">
|
||||
<v-skeleton-loader class="skeleton-no-margin pt-2 pb-1" type="text" :loading="true"></v-skeleton-loader>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</p>
|
||||
</div>
|
||||
<v-divider />
|
||||
<v-tabs show-arrows
|
||||
class="scrollable-vertical-tabs"
|
||||
style="max-height: calc(100% - 88px)"
|
||||
direction="vertical"
|
||||
:prev-icon="mdiMenuUp" :next-icon="mdiMenuDown"
|
||||
:disabled="loading || updating" v-model="activeTagGroupId">
|
||||
<v-tab class="tab-text-truncate" :disabled="loading || updating || hasEditingTag"
|
||||
:key="tagGroup.id" :value="tagGroup.id"
|
||||
v-for="tagGroup in allTagGroupsWithDefault"
|
||||
@click="switchTagGroup(tagGroup.id)">
|
||||
<span class="text-truncate">{{ tagGroup.name }}</span>
|
||||
</v-tab>
|
||||
<template v-if="loading && (!allTagGroupsWithDefault || allTagGroupsWithDefault.length < 1)">
|
||||
<v-skeleton-loader class="skeleton-no-margin mx-5 mt-4 mb-3" type="text"
|
||||
:key="itemIdx" :loading="true" v-for="itemIdx in [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]"></v-skeleton-loader>
|
||||
</template>
|
||||
</v-tabs>
|
||||
</v-navigation-drawer>
|
||||
<v-main>
|
||||
<v-window class="d-flex flex-grow-1 disable-tab-transition w-100-window-container" v-model="activeTab">
|
||||
<v-window-item value="tagListPage">
|
||||
<v-card variant="flat" min-height="780">
|
||||
<template #title>
|
||||
<div class="title-and-toolbar d-flex align-center">
|
||||
<v-btn class="me-3 d-md-none" density="compact" color="default" variant="plain"
|
||||
:ripple="false" :icon="true" @click="showNav = !showNav">
|
||||
<v-icon :icon="mdiMenu" size="24" />
|
||||
</v-btn>
|
||||
<span>{{ tt('Transaction Tags') }}</span>
|
||||
<v-btn class="ms-3" color="default" variant="outlined"
|
||||
:disabled="loading || updating || hasEditingTag" @click="add">{{ tt('Add') }}</v-btn>
|
||||
<v-btn class="ms-3" color="primary" variant="tonal"
|
||||
:disabled="loading || updating || hasEditingTag" @click="saveSortResult"
|
||||
v-if="displayOrderModified">{{ tt('Save Display Order') }}</v-btn>
|
||||
<v-btn density="compact" color="default" variant="text" size="24"
|
||||
class="ms-2" :icon="true" :disabled="loading || updating || hasEditingTag"
|
||||
:loading="loading" @click="reload">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20"/>
|
||||
</template>
|
||||
<v-icon :icon="mdiRefresh" size="24" />
|
||||
<v-tooltip activator="parent">{{ tt('Refresh') }}</v-tooltip>
|
||||
</v-btn>
|
||||
<v-spacer/>
|
||||
<v-btn density="comfortable" color="default" variant="text" class="ms-2"
|
||||
:disabled="loading || updating || hasEditingTag" :icon="true">
|
||||
<v-icon :icon="mdiDotsVertical" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item :prepend-icon="mdiPlus" @click="addTagGroup">
|
||||
<v-list-item-title>{{ tt('Add Tag Group') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item :prepend-icon="mdiPencilOutline"
|
||||
@click="renameTagGroup"
|
||||
v-if="activeTagGroupId && activeTagGroupId !== DEFAULT_TAG_GROUP_ID">
|
||||
<v-list-item-title>{{ tt('Rename Tag Group') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item :prepend-icon="mdiDeleteOutline"
|
||||
@click="removeTagGroup"
|
||||
v-if="activeTagGroupId && activeTagGroupId !== DEFAULT_TAG_GROUP_ID">
|
||||
<v-list-item-title>{{ tt('Delete Tag Group') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiSort"
|
||||
:disabled="!allTagGroupsWithDefault || allTagGroupsWithDefault.length < 2"
|
||||
:title="tt('Change Group Display Order')"
|
||||
@click="showChangeGroupDisplayOrderDialog"></v-list-item>
|
||||
<v-divider class="my-2"/>
|
||||
<v-list-item :prepend-icon="mdiEyeOutline"
|
||||
:title="tt('Show Hidden Transaction Tags')"
|
||||
v-if="!showHidden" @click="showHidden = true"></v-list-item>
|
||||
<v-list-item :prepend-icon="mdiEyeOffOutline"
|
||||
:title="tt('Hide Hidden Transaction Tags')"
|
||||
v-if="showHidden" @click="showHidden = false"></v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-table class="transaction-tags-table table-striped" :hover="!loading">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ tt('Tag Title') }}</span>
|
||||
<v-spacer/>
|
||||
<span>{{ tt('Operation') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody v-if="loading && noAvailableTag && !newTag">
|
||||
<tr :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
|
||||
<td class="px-0">
|
||||
<v-skeleton-loader type="text" :loading="true"></v-skeleton-loader>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody v-if="!loading && noAvailableTag && !newTag">
|
||||
<tr>
|
||||
<td>{{ tt('No available tag') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<draggable-list tag="tbody"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
ghost-class="dragging-item"
|
||||
:class="{ 'has-bottom-border': newTag }"
|
||||
:disabled="noAvailableTag"
|
||||
v-model="tags"
|
||||
@change="onMove">
|
||||
<template #item="{ element }">
|
||||
<tr class="transaction-tags-table-row-tag text-sm" v-if="showHidden || !element.hidden">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<div class="d-flex align-center" v-if="editingTag.id !== element.id">
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="8" :icon="mdiEyeOffOutline"
|
||||
v-if="element.hidden">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="20" start :icon="mdiPound" v-else-if="!element.hidden"/>
|
||||
<span class="transaction-tag-name">{{ element.name }}</span>
|
||||
</div>
|
||||
|
||||
<v-text-field class="w-100 me-2" type="text"
|
||||
density="compact" variant="underlined"
|
||||
:disabled="loading || updating"
|
||||
:placeholder="tt('Tag Title')"
|
||||
v-model="editingTag.name"
|
||||
v-else-if="editingTag.id === element.id"
|
||||
@keyup.enter="save(editingTag)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-badge class="right-bottom-icon" color="secondary"
|
||||
location="bottom right" offset-x="8" :icon="mdiEyeOffOutline"
|
||||
v-if="element.hidden">
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</v-badge>
|
||||
<v-icon size="20" start :icon="mdiPound" v-else-if="!element.hidden"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn class="px-2 ms-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="element.hidden ? mdiEyeOutline : mdiEyeOffOutline"
|
||||
:loading="tagHiding[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="hide(element, !element.hidden)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ element.hidden ? tt('Show') : tt('Hide') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="mdiFolderMoveOutline"
|
||||
:loading="tagMoving[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Move') }}
|
||||
<v-menu activator="parent" max-height="500">
|
||||
<v-list>
|
||||
<v-list-subheader :title="tt('Move to...')"/>
|
||||
<template :key="tagGroup.id" v-for="tagGroup in allTagGroupsWithDefault">
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:value="tagGroup.id" v-if="activeTagGroupId !== tagGroup.id">
|
||||
<v-list-item-title class="cursor-pointer"
|
||||
@click="moveTagToGroup(element, tagGroup.id)">
|
||||
<div class="d-flex align-center">
|
||||
<span class="text-sm ms-3">{{ tagGroup.name }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="mdiPencilOutline"
|
||||
:loading="tagUpdating[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="edit(element)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Edit') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:class="{ 'd-none': loading, 'hover-display': !loading }"
|
||||
:prepend-icon="mdiDeleteOutline"
|
||||
:loading="tagRemoving[element.id]"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id !== element.id"
|
||||
@click="remove(element)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Delete') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiCheck"
|
||||
:loading="tagUpdating[element.id]"
|
||||
:disabled="loading || updating || !isTagModified(element)"
|
||||
v-if="editingTag.id === element.id" @click="save(editingTag)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Save') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiClose"
|
||||
:disabled="loading || updating"
|
||||
v-if="editingTag.id === element.id" @click="cancelSave(editingTag)">
|
||||
{{ tt('Cancel') }}
|
||||
</v-btn>
|
||||
<span class="ms-2">
|
||||
<v-icon :class="!loading && !updating && !hasEditingTag && availableTagCount > 1 ? 'drag-handle' : 'disabled'"
|
||||
:icon="mdiDrag"/>
|
||||
<v-tooltip activator="parent" v-if="!loading && !updating && !hasEditingTag && availableTagCount > 1">{{ tt('Drag to Reorder') }}</v-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</draggable-list>
|
||||
|
||||
<tbody v-if="newTag">
|
||||
<tr class="text-sm" :class="{ 'even-row': (availableTagCount & 1) === 1}">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<v-text-field class="w-100 me-2" type="text" color="primary"
|
||||
density="compact" variant="underlined"
|
||||
:disabled="loading || updating" :placeholder="tt('Tag Title')"
|
||||
v-model="newTag.name" @keyup.enter="save(newTag)">
|
||||
<template #prepend>
|
||||
<v-icon size="20" start :icon="mdiPound"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn class="px-2" density="comfortable" variant="text"
|
||||
:prepend-icon="mdiCheck"
|
||||
:loading="tagUpdating['']"
|
||||
:disabled="loading || updating || !isTagModified(newTag)"
|
||||
@click="save(newTag)">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20" width="2"/>
|
||||
</template>
|
||||
{{ tt('Save') }}
|
||||
</v-btn>
|
||||
<v-btn class="px-2" color="default"
|
||||
density="comfortable" variant="text"
|
||||
:prepend-icon="mdiClose"
|
||||
:disabled="loading || updating"
|
||||
@click="cancelSave(newTag)">
|
||||
{{ tt('Cancel') }}
|
||||
</v-btn>
|
||||
<span class="ms-2">
|
||||
<v-icon class="disabled" :icon="mdiDrag"/>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-card>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-main>
|
||||
</v-layout>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<tag-group-change-display-order-dialog ref="tagGroupChangeDisplayOrderDialog" />
|
||||
|
||||
<rename-dialog ref="renameDialog"
|
||||
:default-title="tt('Rename Tag Group')"
|
||||
:label="tt('Tag Group Name')" :placeholder="tt('Tag Group Name')" />
|
||||
<confirm-dialog ref="confirmDialog"/>
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TagGroupChangeDisplayOrderDialog from './dialog/TagGroupChangeDisplayOrderDialog.vue';
|
||||
import RenameDialog from '@/components/desktop/RenameDialog.vue';
|
||||
import ConfirmDialog from '@/components/desktop/ConfirmDialog.vue';
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
|
||||
import { ref, computed, useTemplateRef } from 'vue';
|
||||
import { ref, computed, useTemplateRef, watch } from 'vue';
|
||||
import { useDisplay } from 'vuetify';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
import { useTagListPageBase } from '@/views/base/tags/TagListPageBase.ts';
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { DEFAULT_TAG_GROUP_ID } from '@/consts/tag.ts';
|
||||
|
||||
import { TransactionTagGroup } from '@/models/transaction_tag_group.ts';
|
||||
import { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
|
||||
import {
|
||||
isNoAvailableTag,
|
||||
getAvailableTagCount
|
||||
} from '@/lib/tag.ts';
|
||||
import { getAvailableTagCount } from '@/lib/tag.ts';
|
||||
|
||||
import {
|
||||
mdiRefresh,
|
||||
mdiMenuUp,
|
||||
mdiMenuDown,
|
||||
mdiPencilOutline,
|
||||
mdiCheck,
|
||||
mdiClose,
|
||||
mdiEyeOffOutline,
|
||||
mdiEyeOutline,
|
||||
mdiSort,
|
||||
mdiMenu,
|
||||
mdiPlus,
|
||||
mdiFolderMoveOutline,
|
||||
mdiDeleteOutline,
|
||||
mdiDrag,
|
||||
mdiDotsVertical,
|
||||
mdiPound
|
||||
} from '@mdi/js';
|
||||
|
||||
type TagGroupChangeDisplayOrderDialogType = InstanceType<typeof TagGroupChangeDisplayOrderDialog>;
|
||||
type RenameDialogType = InstanceType<typeof RenameDialog>;
|
||||
type ConfirmDialogType = InstanceType<typeof ConfirmDialog>;
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
const { tt } = useI18n();
|
||||
const display = useDisplay();
|
||||
|
||||
const { tt, formatNumberToLocalizedNumerals } = useI18n();
|
||||
|
||||
const {
|
||||
activeTagGroupId,
|
||||
newTag,
|
||||
editingTag,
|
||||
loading,
|
||||
showHidden,
|
||||
displayOrderModified,
|
||||
allTagGroupsWithDefault,
|
||||
tags,
|
||||
noAvailableTag,
|
||||
hasEditingTag,
|
||||
isTagModified,
|
||||
switchTagGroup,
|
||||
add,
|
||||
edit
|
||||
} = useTagListPageBase();
|
||||
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
const tagGroupChangeDisplayOrderDialog = useTemplateRef<TagGroupChangeDisplayOrderDialogType>('tagGroupChangeDisplayOrderDialog');
|
||||
const renameDialog = useTemplateRef<RenameDialogType>('renameDialog');
|
||||
const confirmDialog = useTemplateRef<ConfirmDialogType>('confirmDialog');
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
const newTag = ref<TransactionTag | null>(null);
|
||||
const editingTag = ref<TransactionTag>(TransactionTag.createNewTag());
|
||||
const loading = ref<boolean>(true);
|
||||
const updating = ref<boolean>(false);
|
||||
const activeTab = ref<string>('tagListPage');
|
||||
const alwaysShowNav = ref<boolean>(display.mdAndUp.value);
|
||||
const showNav = ref<boolean>(display.mdAndUp.value);
|
||||
const tagUpdating = ref<Record<string, boolean>>({});
|
||||
const tagHiding = ref<Record<string, boolean>>({});
|
||||
const tagMoving = ref<Record<string, boolean>>({});
|
||||
const tagRemoving = ref<Record<string, boolean>>({});
|
||||
const displayOrderModified = ref<boolean>(false);
|
||||
const showHidden = ref<boolean>(false);
|
||||
|
||||
const tags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
|
||||
const noAvailableTag = computed<boolean>(() => isNoAvailableTag(tags.value, showHidden.value));
|
||||
const totalAvailableTagsCount = computed<number>(() => transactionTagsStore.allAvailableTagsCount);
|
||||
const displayTotalAvailableTagsCount = computed<string>(() => formatNumberToLocalizedNumerals(transactionTagsStore.allAvailableTagsCount));
|
||||
const availableTagCount = computed<number>(() => getAvailableTagCount(tags.value, showHidden.value));
|
||||
const hasEditingTag = computed<boolean>(() => !!(newTag.value || (editingTag.value.id && editingTag.value.id !== '')));
|
||||
|
||||
function isTagModified(tag: TransactionTag): boolean {
|
||||
if (tag.id) {
|
||||
return editingTag.value.name !== '' && editingTag.value.name !== tag.name;
|
||||
} else {
|
||||
return tag.name !== '';
|
||||
}
|
||||
}
|
||||
|
||||
function reload(): void {
|
||||
if (hasEditingTag.value) {
|
||||
@@ -312,13 +430,125 @@ function reload(): void {
|
||||
});
|
||||
}
|
||||
|
||||
function add(): void {
|
||||
newTag.value = TransactionTag.createNewTag();
|
||||
function addTagGroup(): void {
|
||||
renameDialog.value?.open('', tt('New Tag Group Name')).then((newName: string) => {
|
||||
updating.value = true;
|
||||
|
||||
transactionTagsStore.saveTagGroup({
|
||||
tagGroup: TransactionTagGroup.createNewTagGroup(newName)
|
||||
}).then(() => {
|
||||
updating.value = false;
|
||||
}).catch(error => {
|
||||
updating.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function edit(tag: TransactionTag): void {
|
||||
editingTag.value.id = tag.id;
|
||||
editingTag.value.name = tag.name;
|
||||
function renameTagGroup(): void {
|
||||
const tagGroup = transactionTagsStore.allTransactionTagGroupsMap[activeTagGroupId.value];
|
||||
|
||||
if (!tagGroup) {
|
||||
snackbar.value?.showMessage('Unable to rename this tag group');
|
||||
return;
|
||||
}
|
||||
|
||||
renameDialog.value?.open(tagGroup.name || '').then((newName: string) => {
|
||||
updating.value = true;
|
||||
|
||||
const newTagGroup = tagGroup.clone();
|
||||
newTagGroup.name = newName;
|
||||
|
||||
transactionTagsStore.saveTagGroup({
|
||||
tagGroup: newTagGroup
|
||||
}).then(() => {
|
||||
updating.value = false;
|
||||
}).catch(error => {
|
||||
updating.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showChangeGroupDisplayOrderDialog(): void {
|
||||
tagGroupChangeDisplayOrderDialog.value?.open().then(() => {
|
||||
if (transactionTagsStore.transactionTagGroupListStateInvalid) {
|
||||
loading.value = true;
|
||||
|
||||
transactionTagsStore.loadAllTagGroups({
|
||||
force: false
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
}).catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeTagGroup(): void {
|
||||
const tagGroup = transactionTagsStore.allTransactionTagGroupsMap[activeTagGroupId.value];
|
||||
|
||||
if (!tagGroup) {
|
||||
snackbar.value?.showMessage('Unable to delete this tag group');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTagGroupIndex = allTagGroupsWithDefault.value.findIndex(group => group.id === tagGroup.id);
|
||||
|
||||
confirmDialog.value?.open('Are you sure you want to delete this tag group?').then(() => {
|
||||
updating.value = true;
|
||||
|
||||
transactionTagsStore.deleteTagGroup({
|
||||
tagGroup: tagGroup
|
||||
}).then(() => {
|
||||
updating.value = false;
|
||||
|
||||
if (transactionTagsStore.allTransactionTagGroups[currentTagGroupIndex]) {
|
||||
const newActiveTagGroup = transactionTagsStore.allTransactionTagGroups[currentTagGroupIndex];
|
||||
activeTagGroupId.value = newActiveTagGroup ? newActiveTagGroup.id : '';
|
||||
} else if (transactionTagsStore.allTransactionTagGroups[currentTagGroupIndex - 1]) {
|
||||
const newActiveTagGroup = transactionTagsStore.allTransactionTagGroups[currentTagGroupIndex - 1];
|
||||
activeTagGroupId.value = newActiveTagGroup ? newActiveTagGroup.id : '';
|
||||
} else {
|
||||
activeTagGroupId.value = '';
|
||||
}
|
||||
}).catch(error => {
|
||||
updating.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function moveTagToGroup(tag: TransactionTag, targetTagGroupId: string): void {
|
||||
updating.value = true;
|
||||
tagMoving.value[tag.id] = true;
|
||||
|
||||
const newTag = tag.clone();
|
||||
newTag.groupId = targetTagGroupId;
|
||||
|
||||
transactionTagsStore.saveTag({
|
||||
tag: newTag
|
||||
}).then(() => {
|
||||
updating.value = false;
|
||||
tagMoving.value[tag.id] = false;
|
||||
}).catch(error => {
|
||||
updating.value = false;
|
||||
tagMoving.value[tag.id] = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function save(tag: TransactionTag): void {
|
||||
@@ -363,7 +593,7 @@ function saveSortResult(): void {
|
||||
|
||||
loading.value = true;
|
||||
|
||||
transactionTagsStore.updateTagDisplayOrders().then(() => {
|
||||
transactionTagsStore.updateTagDisplayOrders(activeTagGroupId.value).then(() => {
|
||||
loading.value = false;
|
||||
displayOrderModified.value = false;
|
||||
}).catch(error => {
|
||||
@@ -450,9 +680,21 @@ transactionTagsStore.loadAllTags({
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => display.mdAndUp.value, (newValue) => {
|
||||
alwaysShowNav.value = newValue;
|
||||
|
||||
if (!showNav.value) {
|
||||
showNav.value = newValue;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.transaction-tags-statistic-item-value {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.transaction-tags-table tr.transaction-tags-table-row-tag .hover-display {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<v-dialog width="800" :persistent="displayOrderModified" v-model="showState">
|
||||
<v-card class="pa-sm-1 pa-md-2">
|
||||
<template #title>
|
||||
<div class="d-flex align-center justify-center">
|
||||
<div class="d-flex align-center">
|
||||
<h4 class="text-h4">{{ tt('Transaction Tag Groups') }}</h4>
|
||||
<v-btn class="ms-3" color="primary" variant="tonal"
|
||||
:disabled="loading || updating" @click="saveDisplayOrder"
|
||||
v-if="displayOrderModified">{{ tt('Save Display Order') }}</v-btn>
|
||||
<v-btn density="compact" color="default" variant="text" size="24"
|
||||
class="ms-2" :icon="true" :disabled="loading || updating"
|
||||
:loading="loading" @click="reload">
|
||||
<template #loader>
|
||||
<v-progress-circular indeterminate size="20"/>
|
||||
</template>
|
||||
<v-icon :icon="mdiRefresh" size="24" />
|
||||
<v-tooltip activator="parent">{{ tt('Refresh') }}</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-spacer/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-card-text class="d-flex flex-column flex-md-row flex-grow-1 overflow-y-auto">
|
||||
<v-table hover density="comfortable" class="w-100 table-striped">
|
||||
<tbody v-if="loading && (!allTagGroups || allTagGroups.length < 1)">
|
||||
<tr :key="itemIdx" v-for="itemIdx in [ 1, 2, 3, 4, 5, 6 ]">
|
||||
<td class="px-0">
|
||||
<v-skeleton-loader type="text" :loading="true"></v-skeleton-loader>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody v-if="!loading && (!allTagGroups || allTagGroups.length < 1)">
|
||||
<tr>
|
||||
<td>{{ tt('No available tag group') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<draggable-list tag="tbody"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
ghost-class="dragging-item"
|
||||
v-model="allTagGroups"
|
||||
@change="onMove">
|
||||
<template #item="{ element }">
|
||||
<tr class="text-sm">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ element.name }}</span>
|
||||
</div>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<span class="ms-2">
|
||||
<v-icon :class="!loading && !updating && allTagGroups && allTagGroups.length > 0 ? 'drag-handle' : 'disabled'"
|
||||
:icon="mdiDrag"/>
|
||||
<v-tooltip activator="parent" v-if="!loading && !updating && allTagGroups && allTagGroups.length > 0">{{ tt('Drag to Reorder') }}</v-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</draggable-list>
|
||||
</v-table>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text class="overflow-y-visible">
|
||||
<div class="w-100 d-flex justify-center flex-wrap mt-sm-1 mt-md-2 gap-4">
|
||||
<v-btn color="secondary" variant="tonal"
|
||||
:disabled="loading || updating" @click="close">{{ tt('Close') }}</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<snack-bar ref="snackbar" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||
|
||||
import { ref, computed, useTemplateRef } from 'vue';
|
||||
|
||||
import { useI18n } from '@/locales/helpers.ts';
|
||||
|
||||
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
|
||||
|
||||
import { type TransactionTagGroup } from '@/models/transaction_tag_group.ts';
|
||||
|
||||
import {
|
||||
mdiRefresh,
|
||||
mdiDrag
|
||||
} from '@mdi/js';
|
||||
|
||||
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||
|
||||
const { tt } = useI18n();
|
||||
|
||||
const transactionTagsStore = useTransactionTagsStore();
|
||||
|
||||
let resolveFunc: (() => void) | null = null;
|
||||
|
||||
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||
|
||||
const showState = ref<boolean>(false);
|
||||
const loading = ref<boolean>(true);
|
||||
const updating = ref<boolean>(false);
|
||||
const displayOrderModified = ref<boolean>(false);
|
||||
|
||||
const allTagGroups = computed<TransactionTagGroup[]>(() => transactionTagsStore.allTransactionTagGroups);
|
||||
|
||||
function open(): Promise<void> {
|
||||
showState.value = true;
|
||||
loading.value = true;
|
||||
|
||||
transactionTagsStore.loadAllTagGroups({
|
||||
force: false
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
displayOrderModified.value = false;
|
||||
}).catch(error => {
|
||||
loading.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
resolveFunc = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function reload(): void {
|
||||
loading.value = true;
|
||||
|
||||
transactionTagsStore.loadAllTagGroups({
|
||||
force: true
|
||||
}).then(() => {
|
||||
loading.value = false;
|
||||
displayOrderModified.value = false;
|
||||
|
||||
snackbar.value?.showMessage('Tag group list has been updated');
|
||||
}).catch(error => {
|
||||
loading.value = false;
|
||||
|
||||
if (error && error.isUpToDate) {
|
||||
displayOrderModified.value = false;
|
||||
}
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveDisplayOrder(): void {
|
||||
if (!displayOrderModified.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
transactionTagsStore.updateTagGroupDisplayOrders().then(() => {
|
||||
loading.value = false;
|
||||
displayOrderModified.value = false;
|
||||
}).catch(error => {
|
||||
loading.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
if (loading.value || updating.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
resolveFunc?.();
|
||||
showState.value = false;
|
||||
}
|
||||
|
||||
function onMove(event: { moved: { element: { id: string }; oldIndex: number; newIndex: number } }): void {
|
||||
if (!event || !event.moved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moveEvent = event.moved;
|
||||
|
||||
if (!moveEvent.element || !moveEvent.element.id) {
|
||||
snackbar.value?.showMessage('Unable to move tag group');
|
||||
return;
|
||||
}
|
||||
|
||||
transactionTagsStore.changeTagGroupDisplayOrder({
|
||||
tagGroupId: moveEvent.element.id,
|
||||
from: moveEvent.oldIndex,
|
||||
to: moveEvent.newIndex
|
||||
}).then(() => {
|
||||
displayOrderModified.value = true;
|
||||
}).catch(error => {
|
||||
snackbar.value?.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
});
|
||||
</script>
|
||||
@@ -259,15 +259,17 @@
|
||||
|
||||
<template :key="categoryType"
|
||||
v-for="(categories, categoryType) in allPrimaryCategories">
|
||||
<v-divider />
|
||||
|
||||
<v-list-item density="compact" v-show="categories && categories.length">
|
||||
<v-list-item-title>
|
||||
<span class="text-sm">{{ getTransactionTypeName(categoryTypeToTransactionType(parseInt(categoryType)), 'Type') }}</span>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-group :key="category.id" v-for="category in categories">
|
||||
<v-list-group :key="category.id" v-for="(category, index) in categories">
|
||||
<template #activator="{ props }" v-if="!category.hidden || queryAllFilterCategoryIds[category.id] || allCategories[query.categoryIds]?.parentId === category.id || hasSubCategoryInQuery(category)">
|
||||
<v-divider />
|
||||
<v-divider v-if="index > 0" />
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:class="getCategoryListItemCheckedClass(category, queryAllFilterCategoryIds)"
|
||||
v-bind="props">
|
||||
@@ -474,24 +476,33 @@
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="query.tagFilter && query.tagFilter !== TransactionTagFilter.TransactionNoTagFilterValue" />
|
||||
<template :key="transactionTagGroup.id"
|
||||
v-for="transactionTagGroup in allTransactionTagGroupsWithDefault">
|
||||
<v-divider v-if="allTransactionTagsByGroup[transactionTagGroup.id] && allTransactionTagsByGroup[transactionTagGroup.id]?.length && hasVisibleTagsInTagGroup(transactionTagGroup)" />
|
||||
|
||||
<template :key="transactionTag.id"
|
||||
v-for="transactionTag in allTransactionTags">
|
||||
<v-divider v-if="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])" />
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:value="transactionTag.id"
|
||||
:class="{ 'list-item-selected': queryAllFilterTagIdsCount === 1 && isDefined(queryAllFilterTagIds[transactionTag.id]), 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && isDefined(queryAllFilterTagIds[transactionTag.id]) }"
|
||||
:append-icon="(queryAllFilterTagIds[transactionTag.id] === true ? mdiCheck : (queryAllFilterTagIds[transactionTag.id] === false ? mdiClose : undefined))"
|
||||
v-if="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])">
|
||||
<v-list-item-title class="cursor-pointer"
|
||||
@click="changeTagFilter(TransactionTagFilter.of(transactionTag.id).toTextualTagFilter())">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="24" :icon="mdiPound"/>
|
||||
<span class="text-sm ms-3">{{ transactionTag.name }}</span>
|
||||
</div>
|
||||
<v-list-item density="compact" v-if="allTransactionTagsByGroup[transactionTagGroup.id] && allTransactionTagsByGroup[transactionTagGroup.id]?.length && hasVisibleTagsInTagGroup(transactionTagGroup)">
|
||||
<v-list-item-title>
|
||||
<span class="text-sm">{{ transactionTagGroup.name }}</span>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<template :key="transactionTag.id"
|
||||
v-for="(transactionTag, index) in (allTransactionTagsByGroup[transactionTagGroup.id] ?? [])">
|
||||
<v-divider v-if="index > 0 && (!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id]))" />
|
||||
<v-list-item class="text-sm" density="compact"
|
||||
:value="transactionTag.id"
|
||||
:class="{ 'list-item-selected': queryAllFilterTagIdsCount === 1 && isDefined(queryAllFilterTagIds[transactionTag.id]), 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && isDefined(queryAllFilterTagIds[transactionTag.id]) }"
|
||||
:append-icon="(queryAllFilterTagIds[transactionTag.id] === true ? mdiCheck : (queryAllFilterTagIds[transactionTag.id] === false ? mdiClose : undefined))"
|
||||
v-if="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])">
|
||||
<v-list-item-title class="cursor-pointer"
|
||||
@click="changeTagFilter(TransactionTagFilter.of(transactionTag.id).toTextualTagFilter())">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="24" :icon="mdiPound"/>
|
||||
<span class="text-sm ms-3">{{ transactionTag.name }}</span>
|
||||
</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
@@ -785,6 +796,8 @@ const {
|
||||
allCategories,
|
||||
allPrimaryCategories,
|
||||
allAvailableCategoriesCount,
|
||||
allTransactionTagGroupsWithDefault,
|
||||
allTransactionTagsByGroup,
|
||||
allTransactionTags,
|
||||
allAvailableTagsCount,
|
||||
query,
|
||||
@@ -806,6 +819,7 @@ const {
|
||||
transactionCalendarMaxDate,
|
||||
currentMonthTransactionData,
|
||||
hasSubCategoryInQuery,
|
||||
hasVisibleTagsInTagGroup,
|
||||
isSameAsDefaultTimezoneOffsetMinutes,
|
||||
canAddTransaction,
|
||||
getDisplayTime,
|
||||
|
||||
@@ -78,6 +78,7 @@ import { type NameValue, values } from '@/core/base.ts';
|
||||
import { CategoryType } from '@/core/category.ts';
|
||||
import { AUTOMATICALLY_CREATED_CATEGORY_ICON_ID } from '@/consts/icon.ts';
|
||||
import { DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts';
|
||||
import { DEFAULT_TAG_GROUP_ID } from '@/consts/tag.ts';
|
||||
|
||||
import { type TransactionCategoryCreateRequest, type TransactionCategoryCreateWithSubCategories, TransactionCategory } from '@/models/transaction_category.ts';
|
||||
import { type TransactionTagCreateRequest, TransactionTag } from '@/models/transaction_tag.ts';
|
||||
@@ -284,12 +285,13 @@ function confirm(): void {
|
||||
const submitTags: TransactionTagCreateRequest[] = [];
|
||||
|
||||
for (const item of selectedNames.value) {
|
||||
const tag: TransactionTag = TransactionTag.createNewTag(item);
|
||||
const tag: TransactionTag = TransactionTag.createNewTag(item, DEFAULT_TAG_GROUP_ID);
|
||||
submitTags.push(tag.toCreateRequest());
|
||||
}
|
||||
|
||||
transactionTagsStore.addTags({
|
||||
tags: submitTags,
|
||||
groupId: DEFAULT_TAG_GROUP_ID,
|
||||
skipExists: true
|
||||
}).then(response => {
|
||||
transactionTagsStore.loadAllTags({ force: false }).then(() => {
|
||||
|
||||
@@ -327,57 +327,14 @@
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="12">
|
||||
<v-autocomplete
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
auto-select-first
|
||||
persistent-placeholder
|
||||
multiple
|
||||
chips
|
||||
:closable-chips="mode !== TransactionEditPageMode.View"
|
||||
<transaction-tag-auto-complete
|
||||
:readonly="mode === TransactionEditPageMode.View"
|
||||
:disabled="loading || submitting"
|
||||
:label="tt('Tags')"
|
||||
:placeholder="tt('None')"
|
||||
:items="allTags"
|
||||
:show-label="true"
|
||||
:allow-add-new-tag="true"
|
||||
v-model="transaction.tagIds"
|
||||
v-model:search="tagSearchContent"
|
||||
>
|
||||
<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 v-if="tagSearchContent" @click="saveNewTag(tagSearchContent)">{{ tt('format.misc.addNewTag', { tag: tagSearchContent }) }}</v-list-item>
|
||||
<v-list-item v-else-if="!tagSearchContent">{{ tt('No available tag') }}</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
@tag:saving="onSavingTag"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="12">
|
||||
<v-textarea
|
||||
@@ -535,7 +492,6 @@ import { TemplateType, ScheduledTemplateFrequencyType } from '@/core/template.ts
|
||||
import { KnownErrorCode } from '@/consts/api.ts';
|
||||
import { SUPPORTED_IMAGE_EXTENSIONS } from '@/consts/file.ts';
|
||||
|
||||
import { TransactionTag } from '@/models/transaction_tag.ts';
|
||||
import { TransactionTemplate } from '@/models/transaction_template.ts';
|
||||
import type { TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts';
|
||||
import { Transaction } from '@/models/transaction.ts';
|
||||
@@ -567,7 +523,6 @@ import {
|
||||
mdiSwapHorizontal,
|
||||
mdiMapMarkerOutline,
|
||||
mdiCheck,
|
||||
mdiPound,
|
||||
mdiMenuDown,
|
||||
mdiImagePlusOutline,
|
||||
mdiTrashCanOutline,
|
||||
@@ -621,7 +576,6 @@ const {
|
||||
allVisibleCategorizedAccounts,
|
||||
allCategories,
|
||||
allCategoriesMap,
|
||||
allTags,
|
||||
allTagsMap,
|
||||
firstVisibleAccountId,
|
||||
hasVisibleExpenseCategories,
|
||||
@@ -669,7 +623,6 @@ const activeTab = ref<string>('basicInfo');
|
||||
const originalTransactionEditable = ref<boolean>(false);
|
||||
const noTransactionDraft = ref<boolean>(false);
|
||||
const geoMenuState = ref<boolean>(false);
|
||||
const tagSearchContent = ref<string>('');
|
||||
const removingPictureId = ref<string>('');
|
||||
|
||||
const initAmount = ref<number | undefined>(undefined);
|
||||
@@ -692,22 +645,7 @@ const sourceAmountColor = computed<string | undefined>(() => {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
const isTransactionModified = computed<boolean>(() => {
|
||||
if (mode.value === TransactionEditPageMode.Add) {
|
||||
@@ -1141,26 +1079,6 @@ function clearGeoLocation(): void {
|
||||
transaction.value.removeGeoLocation();
|
||||
}
|
||||
|
||||
function saveNewTag(tagName: string): void {
|
||||
submitting.value = true;
|
||||
|
||||
transactionTagsStore.saveTag({
|
||||
tag: TransactionTag.createNewTag(tagName)
|
||||
}).then(tag => {
|
||||
submitting.value = false;
|
||||
|
||||
if (tag && tag.id) {
|
||||
transaction.value.tagIds.push(tag.id);
|
||||
}
|
||||
}).catch(error => {
|
||||
submitting.value = false;
|
||||
|
||||
if (!error.processed) {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showOpenPictureDialog(): void {
|
||||
if (!canAddTransactionPicture.value || submitting.value) {
|
||||
return;
|
||||
@@ -1231,6 +1149,10 @@ function viewOrRemovePicture(pictureInfo: TransactionPictureInfoBasicResponse):
|
||||
});
|
||||
}
|
||||
|
||||
function onSavingTag(state: boolean): void {
|
||||
submitting.value = state;
|
||||
}
|
||||
|
||||
function onShowDateTimeError(error: string): void {
|
||||
snackbar.value?.showError(error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user