feat: collection rename

Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
2026-02-15 21:36:00 -05:00
parent 25d5c620bf
commit b1fe063df3
5 changed files with 434 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref } from 'vue'
import type { CollectionObject } from '@MailManager/models/collection'
import type { ServiceInterface } from '@MailManager/types/service'
@@ -18,14 +18,28 @@ const props = defineProps<Props>()
const emit = defineEmits<{
select: [folder: CollectionObject]
createFolder: [service: ServiceInterface, parentFolder: CollectionObject | null]
editFolder: [service: ServiceInterface, folder: CollectionObject]
}>()
// Page-based navigation state
const currentPageLevel = ref<(string | number | null)[]>([null]) // Stack of parent IDs (null = root)
// Page-based navigation state per service account
const pageLevels = ref<Record<string, (string | number | null)[]>>({})
const getServiceKey = (service: ServiceInterface): string => {
return `${service.provider}-${service.identifier}`
}
const getCurrentPageLevel = (service: ServiceInterface): (string | number | null)[] => {
const key = getServiceKey(service)
if (!pageLevels.value[key]) {
pageLevels.value[key] = [null]
}
return pageLevels.value[key]
}
// Get folders for current page level
const getCurrentPageFolders = (folders: CollectionObject[]): CollectionObject[] => {
const currentParent = currentPageLevel.value[currentPageLevel.value.length - 1]
const getCurrentPageFolders = (service: ServiceInterface, folders: CollectionObject[]): CollectionObject[] => {
const level = getCurrentPageLevel(service)
const currentParent = level[level.length - 1]
return folders.filter(f => {
if (currentParent === null) {
return f.collection === null || f.collection === undefined
@@ -95,29 +109,39 @@ const handleFolderClick = (folder: CollectionObject) => {
}
// Navigate into a folder to show its children
const handleNavigateInto = (folderId: string | number) => {
currentPageLevel.value.push(folderId)
const handleNavigateInto = (service: ServiceInterface, folderId: string | number) => {
const level = getCurrentPageLevel(service)
level.push(folderId)
}
// Navigate back in page-based view
const navigateBack = () => {
if (currentPageLevel.value.length > 1) {
currentPageLevel.value.pop()
const navigateBack = (service: ServiceInterface) => {
const level = getCurrentPageLevel(service)
if (level.length > 1) {
level.pop()
}
}
// Get breadcrumb label for current page
const getCurrentBreadcrumb = (folders: CollectionObject[]): string => {
const currentParent = currentPageLevel.value[currentPageLevel.value.length - 1]
const getCurrentBreadcrumb = (service: ServiceInterface, folders: CollectionObject[]): string => {
const level = getCurrentPageLevel(service)
const currentParent = level[level.length - 1]
if (currentParent === null) return 'All Folders'
const parentFolder = folders.find(f => String(f.identifier) === String(currentParent))
return parentFolder?.properties.label || 'Folders'
const labels = level
.filter((id): id is string | number => id !== null)
.map(id => folders.find(f => String(f.identifier) === String(id))?.properties.label)
.filter((label): label is string => !!label)
if (labels.length === 0) return 'Folders'
if (labels.length <= 2) return labels.join('/')
return `../${labels.slice(-2).join('/')}`
}
// Get current parent folder for dialog context
const getCurrentParentFolder = (folders: CollectionObject[]): CollectionObject | null => {
const currentParent = currentPageLevel.value[currentPageLevel.value.length - 1]
const getCurrentParentFolder = (service: ServiceInterface, folders: CollectionObject[]): CollectionObject | null => {
const level = getCurrentPageLevel(service)
const currentParent = level[level.length - 1]
if (currentParent === null) return null
// Search through all folders in the array
@@ -125,18 +149,20 @@ const getCurrentParentFolder = (folders: CollectionObject[]): CollectionObject |
return found || null
}
// Computed helper to get all folders from all service groups
const allFolders = computed(() => props.serviceGroups.flatMap(g => g.folders))
</script>
<template>
<div>
<template v-for="group in serviceGroups" :key="`${group.service.provider}-${group.service.identifier}`">
<template v-for="(group, index) in serviceGroups" :key="`${group.service.provider}-${group.service.identifier}`">
<!-- Service account group -->
<v-list-group v-if="serviceGroups.length > 1">
<v-list-group
v-if="serviceGroups.length > 1"
:class="['no-indent', { 'account-group-spaced': index < serviceGroups.length - 1 }]"
>
<template v-slot:activator="{ props: activatorProps }">
<v-list-item
v-bind="activatorProps"
class="account-header-item"
:title="group.service.label || 'Mail Account'"
:subtitle="group.service.primaryAddress || undefined"
>
@@ -150,7 +176,7 @@ const allFolders = computed(() => props.serviceGroups.flatMap(g => g.folders))
variant="text"
size="small"
density="compact"
@click.stop="emit('createFolder', group.service, getCurrentParentFolder(allFolders))"
@click.stop="emit('createFolder', group.service, getCurrentParentFolder(group.service, group.folders))"
>
<v-icon>mdi-folder-plus</v-icon>
<v-tooltip activator="parent" location="bottom">New Folder</v-tooltip>
@@ -159,32 +185,34 @@ const allFolders = computed(() => props.serviceGroups.flatMap(g => g.folders))
</v-list-item>
</template>
<!-- Back button if not at root -->
<v-list-item
v-if="currentPageLevel.length > 1"
@click="navigateBack"
prepend-icon="mdi-arrow-left"
title="Back"
/>
<!-- Breadcrumb with New Folder button -->
<v-list-subheader v-if="currentPageLevel.length > 1" class="d-flex align-center">
<span class="flex-grow-1">{{ getCurrentBreadcrumb(group.folders) }}</span>
<v-list-subheader v-if="getCurrentPageLevel(group.service).length > 1" class="d-flex align-center">
<span class="flex-grow-1">{{ getCurrentBreadcrumb(group.service, group.folders) }}</span>
<v-btn
icon="mdi-folder-plus"
variant="text"
size="x-small"
@click="emit('createFolder', group.service, getCurrentParentFolder(allFolders))"
@click="emit('createFolder', group.service, getCurrentParentFolder(group.service, group.folders))"
>
<v-icon size="small">mdi-folder-plus</v-icon>
<v-tooltip activator="parent" location="bottom">New Subfolder</v-tooltip>
</v-btn>
</v-list-subheader>
<!-- Back button if not at root -->
<v-list-item
v-if="getCurrentPageLevel(group.service).length > 1"
class="back-row-item"
@click="navigateBack(group.service)"
prepend-icon="mdi-arrow-left"
title="Back"
/>
<!-- Current level folders -->
<v-list-item
v-for="folder in getCurrentPageFolders(group.folders)"
v-for="folder in getCurrentPageFolders(group.service, group.folders)"
:key="`${folder.provider}-${folder.service}-${folder.identifier}`"
class="folder-page-item folder-row-item"
:title="folder.properties.label"
:active="isSelected(folder)"
@click="handleFolderClick(folder)"
@@ -213,11 +241,11 @@ const allFolders = computed(() => props.serviceGroups.flatMap(g => g.folders))
variant="text"
size="small"
density="compact"
@click.stop="handleNavigateInto(folder.identifier)"
@click.stop="handleNavigateInto(group.service, folder.identifier)"
/>
<!-- Menu for folder actions -->
<v-menu v-else>
<v-menu>
<template v-slot:activator="{ props: menuProps }">
<v-btn
v-bind="menuProps"
@@ -230,6 +258,12 @@ const allFolders = computed(() => props.serviceGroups.flatMap(g => g.folders))
</template>
<v-list density="compact">
<v-list-item
prepend-icon="mdi-pencil"
@click="emit('editFolder', group.service, folder)"
>
<v-list-item-title>Edit Folder Name</v-list-item-title>
</v-list-item>
<v-list-item
prepend-icon="mdi-folder-plus"
@click="emit('createFolder', group.service, folder)"
@@ -247,33 +281,35 @@ const allFolders = computed(() => props.serviceGroups.flatMap(g => g.folders))
<!-- Header with New Folder button -->
<v-list-subheader class="d-flex align-center">
<span class="flex-grow-1">
{{ currentPageLevel.length > 1 ? getCurrentBreadcrumb(group.folders) : 'FOLDERS' }}
{{ getCurrentPageLevel(group.service).length > 1 ? getCurrentBreadcrumb(group.service, group.folders) : 'FOLDERS' }}
</span>
<v-btn
icon="mdi-folder-plus"
variant="text"
size="x-small"
@click="emit('createFolder', group.service, getCurrentParentFolder(allFolders))"
@click="emit('createFolder', group.service, getCurrentParentFolder(group.service, group.folders))"
>
<v-icon size="small">mdi-folder-plus</v-icon>
<v-tooltip activator="parent" location="bottom">
{{ currentPageLevel.length > 1 ? 'New Subfolder' : 'New Folder' }}
{{ getCurrentPageLevel(group.service).length > 1 ? 'New Subfolder' : 'New Folder' }}
</v-tooltip>
</v-btn>
</v-list-subheader>
<!-- Back button if not at root -->
<v-list-item
v-if="currentPageLevel.length > 1"
@click="navigateBack"
v-if="getCurrentPageLevel(group.service).length > 1"
class="back-row-item"
@click="navigateBack(group.service)"
prepend-icon="mdi-arrow-left"
title="Back"
/>
<!-- Current level folders -->
<v-list-item
v-for="folder in getCurrentPageFolders(group.folders)"
v-for="folder in getCurrentPageFolders(group.service, group.folders)"
:key="`${folder.provider}-${folder.service}-${folder.identifier}`"
class="folder-row-item"
:title="folder.properties.label"
:active="isSelected(folder)"
@click="handleFolderClick(folder)"
@@ -302,10 +338,10 @@ const allFolders = computed(() => props.serviceGroups.flatMap(g => g.folders))
variant="text"
size="small"
density="compact"
@click.stop="handleNavigateInto(folder.identifier)"
@click.stop="handleNavigateInto(group.service, folder.identifier)"
/>
<v-menu v-else>
<v-menu>
<template v-slot:activator="{ props: menuProps }">
<v-btn
v-bind="menuProps"
@@ -318,6 +354,12 @@ const allFolders = computed(() => props.serviceGroups.flatMap(g => g.folders))
</template>
<v-list density="compact">
<v-list-item
prepend-icon="mdi-pencil"
@click="emit('editFolder', group.service, folder)"
>
<v-list-item-title>Edit Folder Name</v-list-item-title>
</v-list-item>
<v-list-item
prepend-icon="mdi-folder-plus"
@click="emit('createFolder', group.service, folder)"
@@ -337,4 +379,35 @@ const allFolders = computed(() => props.serviceGroups.flatMap(g => g.folders))
.v-list-item--active {
background-color: rgba(var(--v-theme-primary), 0.12);
}
/* Keep folder rows left-aligned inside multi-account groups */
.folder-page-item {
--indent-padding: 0 !important;
}
.no-indent :deep(.v-list-group__items) {
--indent-padding: 0;
}
.account-group-spaced {
margin-bottom: 16px;
}
.account-header-item {
background-color: rgba(var(--v-theme-primary), 0.1);
border-radius: 6px;
}
.folder-row-item,
.back-row-item,
.account-header-item {
--v-list-item-prepend-size: 22px;
}
.folder-row-item :deep(.v-list-item__prepend),
.back-row-item :deep(.v-list-item__prepend),
.account-header-item :deep(.v-list-item__prepend) {
padding-inline-start: 4px;
margin-inline-end: 2px;
}
</style>