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
@@ -49,53 +49,46 @@
<f7-list-item :title="tt('No available tag')"></f7-list-item>
</f7-list>
<f7-block class="combination-list-wrapper margin-vertical" key="default" v-show="!loading && hasAnyVisibleTag">
<f7-list class="margin-top-half margin-bottom" strong inset dividers v-if="includeTagsCount > 1">
<f7-list-item radio
:title="tt(filterType.name)"
:key="filterType.type"
:value="filterType.type"
:checked="includeTagFilterType === filterType.type"
v-for="filterType in [TransactionTagFilterType.HasAny, TransactionTagFilterType.HasAll]"
@change="includeTagFilterType = filterType.type">
</f7-list-item>
</f7-list>
<f7-list class="margin-top-half margin-bottom" strong inset dividers v-if="excludeTagsCount > 1">
<f7-list-item radio
:title="tt(filterType.name)"
:key="filterType.type"
:value="filterType.type"
:checked="excludeTagFilterType === filterType.type"
v-for="filterType in [TransactionTagFilterType.NotHasAny, TransactionTagFilterType.NotHasAll]"
@change="excludeTagFilterType = filterType.type">
</f7-list-item>
</f7-list>
<f7-accordion-item :opened="collapseStates['default']!.opened"
@accordion:open="collapseStates['default']!.opened = true"
@accordion:close="collapseStates['default']!.opened = false">
<f7-block class="combination-list-wrapper margin-vertical"
:key="tagGroup.id" v-for="tagGroup in allTagGroupsWithDefault"
v-show="!loading && hasAnyVisibleTag">
<f7-accordion-item :opened="collapseStates[tagGroup.id]?.opened ?? true"
@accordion:open="collapseStates[tagGroup.id]!.opened = true"
@accordion:close="collapseStates[tagGroup.id]!.opened = false"
v-if="allVisibleTags[tagGroup.id] && allVisibleTags[tagGroup.id]!.length > 0">
<f7-block-title>
<f7-accordion-toggle>
<f7-list strong inset dividers
class="combination-list-header"
:class="collapseStates['default']!.opened ? 'combination-list-opened' : 'combination-list-closed'">
:class="collapseStates[tagGroup.id]?.opened ? 'combination-list-opened' : 'combination-list-closed'">
<f7-list-item group-title>
<small>{{ tt('Tags') }}</small>
<f7-icon class="combination-list-chevron-icon" :f7="collapseStates['default']!.opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
<small class="tag-group-title">{{ tagGroup.name }}</small>
<f7-icon class="combination-list-chevron-icon" :f7="collapseStates[tagGroup.id]?.opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
</f7-list-item>
</f7-list>
</f7-accordion-toggle>
</f7-block-title>
<f7-accordion-content :style="{ height: collapseStates['default']!.opened ? 'auto' : '' }">
<f7-accordion-content :style="{ height: collapseStates[tagGroup.id]?.opened ? 'auto' : '' }">
<f7-list strong inset dividers accordion-list class="combination-list-content">
<f7-list-item link="#"
popover-open=".tag-filter-include-type-popover-menu"
:title="tt(TransactionTagFilterType.parse(groupTagFilterTypesMap[tagGroup.id]?.includeType as number)?.name as string)"
@click="currentTransactionTagGroupId = tagGroup.id"
v-if="groupTagFilterTypesMap[tagGroup.id] && groupTagFilterStateCountMap[tagGroup.id] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Include] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Include] > 1 && TransactionTagFilterType.parse(groupTagFilterTypesMap[tagGroup.id]?.includeType as number)">
</f7-list-item>
<f7-list-item link="#"
popover-open=".tag-filter-exclude-type-popover-menu"
:title="tt(TransactionTagFilterType.parse(groupTagFilterTypesMap[tagGroup.id]?.excludeType as number)?.name as string)"
@click="currentTransactionTagGroupId = tagGroup.id"
v-if="groupTagFilterTypesMap[tagGroup.id] && groupTagFilterStateCountMap[tagGroup.id] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Exclude] && groupTagFilterStateCountMap[tagGroup.id]![TransactionTagFilterState.Exclude] > 1 && TransactionTagFilterType.parse(groupTagFilterTypesMap[tagGroup.id]?.excludeType as number)">
</f7-list-item>
<f7-list-item link="#"
popover-open=".tag-filter-state-popover-menu"
:title="transactionTag.name"
:value="transactionTag.id"
:key="transactionTag.id"
:after="tt(filterTagIds[transactionTag.id] === TransactionTagFilterState.Include ? 'Included' : filterTagIds[transactionTag.id] === TransactionTagFilterState.Exclude ? 'Excluded' : 'Default')"
v-for="transactionTag in allVisibleTags"
:after="tt(tagFilterStateMap[transactionTag.id] === TransactionTagFilterState.Include ? 'Included' : tagFilterStateMap[transactionTag.id] === TransactionTagFilterState.Exclude ? 'Excluded' : 'Default')"
v-for="transactionTag in allVisibleTags[tagGroup.id]"
v-show="showHidden || !transactionTag.hidden"
@click="currentTransactionTagId = transactionTag.id">
<template #media>
@@ -111,11 +104,41 @@
</f7-accordion-item>
</f7-block>
<f7-popover class="tag-filter-include-type-popover-menu">
<f7-list dividers>
<f7-list-item link="#" no-chevron popover-close
:title="tt(filterType.name)"
:class="{ 'list-item-selected': groupTagFilterTypesMap[currentTransactionTagGroupId]?.includeType === filterType.type }"
:key="filterType.type"
v-for="filterType in [TransactionTagFilterType.HasAny, TransactionTagFilterType.HasAll]"
@click="updateTransactionTagGroupIncludeType(filterType)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="groupTagFilterTypesMap[currentTransactionTagGroupId]?.includeType === filterType.type"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
<f7-popover class="tag-filter-exclude-type-popover-menu">
<f7-list dividers>
<f7-list-item link="#" no-chevron popover-close
:title="tt(filterType.name)"
:class="{ 'list-item-selected': groupTagFilterTypesMap[currentTransactionTagGroupId]?.excludeType === filterType.type }"
:key="filterType.type"
v-for="filterType in [TransactionTagFilterType.NotHasAny, TransactionTagFilterType.NotHasAll]"
@click="updateTransactionTagGroupExcludeType(filterType)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="groupTagFilterTypesMap[currentTransactionTagGroupId]?.excludeType === filterType.type"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
<f7-popover class="tag-filter-state-popover-menu">
<f7-list dividers>
<f7-list-item link="#" no-chevron popover-close
:title="state.displayName"
:class="{ 'list-item-selected': filterTagIds[currentTransactionTagId] === state.type }"
:class="{ 'list-item-selected': tagFilterStateMap[currentTransactionTagId] === state.type }"
:key="state.type"
v-for="state in [
{ type: TransactionTagFilterState.Include, displayName: tt('Included') },
@@ -124,7 +147,7 @@
]"
@click="updateCurrentTransactionTagState(state.type)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="filterTagIds[currentTransactionTagId] === state.type"></f7-icon>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="tagFilterStateMap[currentTransactionTagId] === state.type"></f7-icon>
</template>
</f7-list-item>
</f7-list>
@@ -160,6 +183,7 @@ import {
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
import { values } from '@/core/base.ts';
import { TransactionTagFilterType } from '@/core/transaction.ts';
interface CollapseState {
@@ -180,13 +204,13 @@ const {
loading,
showHidden,
filterContent,
filterTagIds,
includeTagFilterType,
excludeTagFilterType,
includeTagsCount,
excludeTagsCount,
tagFilterStateMap,
groupTagFilterTypesMap,
title,
groupTagFilterStateCountMap,
allTagGroupsWithDefault,
allVisibleTags,
allVisibleTagGroupIds,
hasAnyAvailableTag,
hasAnyVisibleTag,
loadFilterTagIds,
@@ -196,20 +220,30 @@ const {
const transactionTagsStore = useTransactionTagsStore();
const loadingError = ref<unknown | null>(null);
const currentTransactionTagGroupId = ref<string>('');
const currentTransactionTagId = ref<string>('');
const showMoreActionSheet = ref<boolean>(false);
const collapseStates = ref<Record<string, CollapseState>>({
default: {
opened: true
const collapseStates = ref<Record<string, CollapseState>>(getInitCollapseState(allVisibleTagGroupIds.value));
function getInitCollapseState(tagGroupIds: string[]): Record<string, CollapseState> {
const states: Record<string, CollapseState> = {};
for (const tagGroupId of tagGroupIds) {
states[tagGroupId] = {
opened: true
};
}
});
return states;
}
function init(): void {
transactionTagsStore.loadAllTags({
force: false
}).then(() => {
loading.value = false;
collapseStates.value = getInitCollapseState(allVisibleTagGroupIds.value);
if (!loadFilterTagIds()) {
showToast('Parameter Invalid');
@@ -225,14 +259,36 @@ function init(): void {
});
}
function updateTransactionTagGroupIncludeType(filterType: TransactionTagFilterType): void {
const tagFilterTypes = groupTagFilterTypesMap.value[currentTransactionTagGroupId.value];
if (!tagFilterTypes) {
return;
}
tagFilterTypes.includeType = filterType.type;
}
function updateTransactionTagGroupExcludeType(filterType: TransactionTagFilterType): void {
const tagFilterTypes = groupTagFilterTypesMap.value[currentTransactionTagGroupId.value];
if (!tagFilterTypes) {
return;
}
tagFilterTypes.excludeType = filterType.type;
}
function updateCurrentTransactionTagState(state: number): void {
filterTagIds.value[currentTransactionTagId.value] = state;
tagFilterStateMap.value[currentTransactionTagId.value] = state;
currentTransactionTagId.value = '';
}
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;
}
}
}
@@ -251,3 +307,10 @@ function onPageAfterIn(): void {
init();
</script>
<style>
.tag-group-title {
overflow: hidden;
text-overflow: ellipsis;
}
</style>
+58 -32
View File
@@ -5,7 +5,12 @@
<f7-nav-left v-else-if="sortable">
<f7-link icon-f7="xmark" :class="{ 'disabled': displayOrderSaving }" @click="cancelSort"></f7-link>
</f7-nav-left>
<f7-nav-title :title="tt('Transaction Tags')"></f7-nav-title>
<f7-nav-title>
<f7-link popover-open=".tag-group-popover-menu" :class="{ 'disabled': sortable || hasEditingTag }">
<span style="color: var(--f7-text-color)">{{ displayTagGroupName }}</span>
<f7-icon class="page-title-bar-icon" color="gray" style="opacity: 0.5" f7="chevron_down_circle_fill"></f7-icon>
</f7-link>
</f7-nav-title>
<f7-nav-right class="navbar-compact-icons">
<f7-link icon-f7="ellipsis" :class="{ 'disabled': hasEditingTag || !tags.length || sortable }" @click="showMoreActionSheet = true"></f7-link>
<f7-link icon-f7="plus" :class="{ 'disabled': hasEditingTag }" v-if="!sortable" @click="add"></f7-link>
@@ -13,6 +18,22 @@
</f7-nav-right>
</f7-navbar>
<f7-popover class="tag-group-popover-menu"
@popover:open="scrollPopoverToSelectedItem">
<f7-list dividers>
<f7-list-item link="#" no-chevron popover-close
:title="tagGroup.name"
:class="{ 'list-item-selected': activeTagGroupId === tagGroup.id }"
:key="tagGroup.id"
v-for="tagGroup in allTagGroupsWithDefault"
@click="switchTagGroup(tagGroup.id)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="activeTagGroupId === tagGroup.id"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
<f7-list strong inset dividers class="tag-item-list margin-top skeleton-text" v-if="loading">
<f7-list-item :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
<template #media>
@@ -155,18 +176,17 @@ import { ref, computed } from 'vue';
import type { Router } from 'framework7/types';
import { useI18n } from '@/locales/helpers.ts';
import { useI18nUIComponents, showLoading, hideLoading, onSwipeoutDeleted } from '@/lib/ui/mobile.ts';
import { type Framework7Dom, useI18nUIComponents, showLoading, hideLoading, onSwipeoutDeleted } from '@/lib/ui/mobile.ts';
import { useTagListPageBase } from '@/views/base/tags/TagListPageBase.ts';
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
import { TextDirection } from '@/core/text.ts';
import { TransactionTag } from '@/models/transaction_tag.ts';
import {
isNoAvailableTag,
getFirstShowingId,
getLastShowingId
} from '@/lib/tag.ts';
import { scrollToSelectedItem } from '@/lib/ui/common.ts';
import { getFirstShowingId, getLastShowingId } from '@/lib/tag.ts';
const props = defineProps<{
f7router: Router.Router;
@@ -175,34 +195,40 @@ const props = defineProps<{
const { tt, getCurrentLanguageTextDirection } = useI18n();
const { showAlert, showToast, routeBackOnError } = useI18nUIComponents();
const {
activeTagGroupId,
newTag,
editingTag,
loading,
showHidden,
displayOrderModified,
allTagGroupsWithDefault,
tags,
noAvailableTag,
hasEditingTag,
isTagModified,
switchTagGroup,
add,
edit
} = useTagListPageBase();
const transactionTagsStore = useTransactionTagsStore();
const newTag = ref<TransactionTag | null>(null);
const editingTag = ref<TransactionTag>(TransactionTag.createNewTag());
const loading = ref<boolean>(true);
const loadingError = ref<unknown | null>(null);
const showHidden = ref<boolean>(false);
const sortable = ref<boolean>(false);
const tagToDelete = ref<TransactionTag | null>(null);
const showMoreActionSheet = ref<boolean>(false);
const showDeleteActionSheet = ref<boolean>(false);
const displayOrderModified = ref<boolean>(false);
const displayOrderSaving = ref<boolean>(false);
const textDirection = computed<TextDirection>(() => getCurrentLanguageTextDirection());
const tags = computed<TransactionTag[]>(() => transactionTagsStore.allTransactionTags);
const firstShowingId = computed<string | null>(() => getFirstShowingId(tags.value, showHidden.value));
const lastShowingId = computed<string | null>(() => getLastShowingId(tags.value, showHidden.value));
const noAvailableTag = computed<boolean>(() => isNoAvailableTag(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 !== '';
}
}
const displayTagGroupName = computed<string>(() => {
const tagGroup = transactionTagsStore.allTransactionTagGroupsMap[activeTagGroupId.value];
return tagGroup ? tagGroup.name : tt('Default Group');
});
function getTagDomId(tag: TransactionTag): string {
return 'tag_' + tag.id;
@@ -258,15 +284,6 @@ function reload(done?: () => void): void {
});
}
function add(): void {
newTag.value = TransactionTag.createNewTag();
}
function edit(tag: TransactionTag): void {
editingTag.value.id = tag.id;
editingTag.value.name = tag.name;
}
function save(tag: TransactionTag): void {
showLoading();
@@ -368,7 +385,7 @@ function saveSortResult(): void {
displayOrderSaving.value = true;
showLoading();
transactionTagsStore.updateTagDisplayOrders().then(() => {
transactionTagsStore.updateTagDisplayOrders(activeTagGroupId.value).then(() => {
displayOrderSaving.value = false;
hideLoading();
@@ -438,6 +455,10 @@ function onSort(event: { el: { id: string }, from: number, to: number }): void {
});
}
function scrollPopoverToSelectedItem(event: { $el: Framework7Dom }): void {
scrollToSelectedItem(event.$el[0], '.popover-inner', '.popover-inner', 'li.list-item-selected');
}
function onPageAfterIn(): void {
if (transactionTagsStore.transactionTagListStateInvalid && !loading.value) {
reload();
@@ -458,4 +479,9 @@ init();
overflow: hidden;
text-overflow: ellipsis;
}
.tag-group-popover-menu .popover-inner {
max-height: 440px;
overflow-y: auto;
}
</style>
+1 -12
View File
@@ -367,7 +367,7 @@
<template #footer>
<f7-block class="margin-top-half no-padding no-margin" v-if="transaction.tagIds && transaction.tagIds.length">
<f7-chip media-text-color="var(--f7-chip-text-color)" class="transaction-edit-tag"
:text="getTagName(tagId)"
:text="allTagsMap[tagId]?.name ?? ''"
:key="tagId"
v-for="tagId in transaction.tagIds">
<template #media>
@@ -570,7 +570,6 @@ const {
allVisibleCategorizedAccounts,
allCategories,
allCategoriesMap,
allTags,
allTagsMap,
firstVisibleAccountId,
hasVisibleExpenseCategories,
@@ -828,16 +827,6 @@ function getFontClassByAmount(amount: number): string {
}
}
function getTagName(tagId: string): string {
for (const tag of allTags.value) {
if (tag.id === tagId) {
return tag.name;
}
}
return '';
}
function init(): void {
if (!pageTypeAndMode) {
showToast('Parameter Invalid');
+37 -18
View File
@@ -558,24 +558,31 @@
</template>
</f7-list-item>
<f7-list-item link="#" no-chevron popover-close
:title="transactionTag.name"
:class="{ 'list-item-selected': queryAllFilterTagIdsCount === 1 && isDefined(queryAllFilterTagIds[transactionTag.id]), 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && isDefined(queryAllFilterTagIds[transactionTag.id]) }"
:key="transactionTag.id"
v-for="transactionTag in allTransactionTags"
v-show="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])"
@click="changeTagFilter(TransactionTagFilter.of(transactionTag.id).toTextualTagFilter())"
>
<template #before-title>
<f7-icon class="transaction-tag-name transaction-tag-icon" f7="number"></f7-icon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon"
:f7="queryAllFilterTagIds[transactionTag.id] === true ? 'checkmark_alt' : (queryAllFilterTagIds[transactionTag.id] === false ? 'multiply' : undefined)"
v-if="isDefined(queryAllFilterTagIds[transactionTag.id])">
</f7-icon>
</template>
</f7-list-item>
<template :key="transactionTagGroup.id"
v-for="transactionTagGroup in allTransactionTagGroupsWithDefault">
<f7-list-item group-title class="transaction-tag-group" v-if="allTransactionTagsByGroup[transactionTagGroup.id] && allTransactionTagsByGroup[transactionTagGroup.id]?.length && hasVisibleTagsInTagGroup(transactionTagGroup)">
<small>{{ transactionTagGroup.name }}</small>
</f7-list-item>
<f7-list-item link="#" no-chevron popover-close
:title="transactionTag.name"
:class="{ 'list-item-selected': queryAllFilterTagIdsCount === 1 && isDefined(queryAllFilterTagIds[transactionTag.id]), 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && isDefined(queryAllFilterTagIds[transactionTag.id]) }"
:key="transactionTag.id"
v-for="transactionTag in (allTransactionTagsByGroup[transactionTagGroup.id] ?? [])"
v-show="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])"
@click="changeTagFilter(TransactionTagFilter.of(transactionTag.id).toTextualTagFilter())"
>
<template #before-title>
<f7-icon class="transaction-tag-name transaction-tag-icon" f7="number"></f7-icon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon"
:f7="queryAllFilterTagIds[transactionTag.id] === true ? 'checkmark_alt' : (queryAllFilterTagIds[transactionTag.id] === false ? 'multiply' : undefined)"
v-if="isDefined(queryAllFilterTagIds[transactionTag.id])">
</f7-icon>
</template>
</f7-list-item>
</template>
</f7-list>
</f7-popover>
@@ -686,6 +693,8 @@ const {
allCategories,
allPrimaryCategories,
allAvailableCategoriesCount,
allTransactionTagGroupsWithDefault,
allTransactionTagsByGroup,
allTransactionTags,
allAvailableTagsCount,
displayPageTypeName,
@@ -708,6 +717,7 @@ const {
transactionCalendarMaxDate,
currentMonthTransactionData,
hasSubCategoryInQuery,
hasVisibleTagsInTagGroup,
isSameAsDefaultTimezoneOffsetMinutes,
canAddTransaction,
getDisplayTime,
@@ -1608,6 +1618,15 @@ html[dir="rtl"] .list.transaction-info-list li.transaction-info .transaction-foo
overflow-y: auto;
}
.more-popover-menu .transaction-tag-group {
background-color: inherit;
> small {
overflow: hidden;
text-overflow: ellipsis;
}
}
.transaction-calendar-container .dp__theme_light,
.transaction-calendar-container .dp__theme_dark {
--dp-background-color: var(--f7-list-strong-bg-color);