feat: Listen/Kalender/Adressbuch-Namen im 3-Punkte-Menue umbenennbar

Stift-Icon neben dem Namen oeffnet Inline-Editor (Eingabefeld + Check/X).
Enter speichert, ESC bricht ab. Nur fuer Eigentuemer sichtbar.
Backend-PUT-Endpunkte sind bereits vorhanden.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-14 15:52:12 +02:00
parent 2ef186e262
commit ed944339c4
3 changed files with 135 additions and 3 deletions

View File

@ -256,7 +256,22 @@
<!-- Calendar Menu -->
<Dialog v-model:visible="showCalMenu" header="Kalender-Optionen" modal :style="{ width: '480px' }">
<div v-if="selectedCal" class="cal-menu-content">
<p><strong>{{ selectedCal.name }}</strong></p>
<div class="rename-row">
<template v-if="!isRenamingCal">
<strong>{{ selectedCal.name }}</strong>
<Button v-if="selectedCal.permission === 'owner'"
icon="pi pi-pencil" text size="small" title="Umbenennen"
@click="startRenameCal" />
</template>
<template v-else>
<InputText v-model="renameCalValue" fluid autofocus
@keyup.enter="saveRenameCal" @keyup.escape="isRenamingCal = false" />
<Button icon="pi pi-check" text size="small" severity="success"
title="Speichern" @click="saveRenameCal" />
<Button icon="pi pi-times" text size="small"
title="Abbrechen" @click="isRenamingCal = false" />
</template>
</div>
<div class="field">
<label>
@ -1070,10 +1085,38 @@ function openCalendarMenu(cal) {
icalPassword.value = ''
shareUsername.value = ''
shareSearchResults.value = []
isRenamingCal.value = false
showCalMenu.value = true
loadShares()
}
const isRenamingCal = ref(false)
const renameCalValue = ref('')
function startRenameCal() {
renameCalValue.value = selectedCal.value?.name || ''
isRenamingCal.value = true
}
async function saveRenameCal() {
const newName = renameCalValue.value.trim()
if (!newName || !selectedCal.value || newName === selectedCal.value.name) {
isRenamingCal.value = false
return
}
try {
await apiClient.put(`/calendars/${selectedCal.value.id}`, { name: newName })
selectedCal.value.name = newName
isRenamingCal.value = false
await loadCalendars()
refreshEvents()
toast.add({ severity: 'success', summary: 'Umbenannt', life: 2000 })
} catch (err) {
toast.add({ severity: 'error', summary: 'Fehler',
detail: err.response?.data?.error || err.message, life: 4000 })
}
}
function onShareSearch() {
clearTimeout(shareSearchTimer)
const q = shareUsername.value.trim()
@ -1307,6 +1350,8 @@ onUnmounted(() => {
.user-result { padding: 0.5rem 0.75rem; cursor: pointer; font-size: 0.875rem; display: flex; gap: 0.5rem; align-items: center; }
.user-result:hover { background: var(--p-primary-50); }
.user-fullname { color: var(--p-text-muted-color); font-size: 0.75rem; margin-left: auto; }
.rename-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
.rename-row strong { font-size: 1rem; }
.existing-shares { margin-top: 0.5rem; }
.share-perm-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0; font-size: 0.875rem; flex-wrap: wrap; }
.share-perm-item.editing { background: var(--p-surface-50); padding: 0.5rem; border-radius: 4px; }

View File

@ -92,7 +92,22 @@
<!-- Book Menu (3-dot) -->
<Dialog v-model:visible="showBookMenu" header="Adressbuch-Optionen" modal :style="{ width: '560px' }">
<div v-if="menuBook" class="book-menu-content">
<p><strong>{{ menuBook.name }}</strong></p>
<div class="rename-row">
<template v-if="!isRenamingBook">
<strong>{{ menuBook.name }}</strong>
<Button v-if="menuBook.permission === 'owner'"
icon="pi pi-pencil" text size="small" title="Umbenennen"
@click="startRenameBook" />
</template>
<template v-else>
<InputText v-model="renameBookValue" fluid autofocus
@keyup.enter="saveRenameBook" @keyup.escape="isRenamingBook = false" />
<Button icon="pi pi-check" text size="small" severity="success"
title="Speichern" @click="saveRenameBook" />
<Button icon="pi pi-times" text size="small"
title="Abbrechen" @click="isRenamingBook = false" />
</template>
</div>
<div class="field">
<label>
@ -529,10 +544,37 @@ function openBookMenu(book) {
shareUsername.value = ''
shareSearchResults.value = []
editingShareId.value = null
isRenamingBook.value = false
showBookMenu.value = true
loadShares()
}
const isRenamingBook = ref(false)
const renameBookValue = ref('')
function startRenameBook() {
renameBookValue.value = menuBook.value?.name || ''
isRenamingBook.value = true
}
async function saveRenameBook() {
const newName = renameBookValue.value.trim()
if (!newName || !menuBook.value || newName === menuBook.value.name) {
isRenamingBook.value = false
return
}
try {
await apiClient.put(`/addressbooks/${menuBook.value.id}`, { name: newName })
menuBook.value.name = newName
isRenamingBook.value = false
await loadBooks()
toast.add({ severity: 'success', summary: 'Umbenannt', life: 2000 })
} catch (err) {
toast.add({ severity: 'error', summary: 'Fehler',
detail: err.response?.data?.error || err.message, life: 4000 })
}
}
async function loadShares() {
if (!menuBook.value || menuBook.value.permission !== 'owner') {
bookShares.value = []
@ -896,6 +938,8 @@ watch(selectedBookId, loadContacts)
.multi-row { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.4rem; }
.address-card { border: 1px solid var(--p-surface-200); padding: 0.75rem; border-radius: 6px; margin-bottom: 0.75rem; }
.share-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.rename-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
.rename-row strong { font-size: 1rem; }
.user-search-popup { position: absolute; top: 100%; left: 0; right: 0; z-index: 10;
background: white; border: 1px solid var(--p-surface-200);
border-radius: 4px; max-height: 160px; overflow-y: auto;

View File

@ -108,7 +108,22 @@
<!-- List Menu -->
<Dialog v-model:visible="showListMenu" header="Listen-Optionen" modal :style="{ width: '480px' }">
<div v-if="menuList">
<p><strong>{{ menuList.name }}</strong></p>
<div class="rename-row">
<template v-if="!isRenaming">
<strong>{{ menuList.name }}</strong>
<Button v-if="menuList.permission === 'owner'"
icon="pi pi-pencil" text size="small" title="Umbenennen"
@click="startRename" />
</template>
<template v-else>
<InputText v-model="renameValue" fluid autofocus
@keyup.enter="saveRename" @keyup.escape="isRenaming = false" />
<Button icon="pi pi-check" text size="small" severity="success"
title="Speichern" @click="saveRename" />
<Button icon="pi pi-times" text size="small"
title="Abbrechen" @click="isRenaming = false" />
</template>
</div>
<div class="field">
<label>Farbe</label>
<InputText :modelValue="menuList.color" @change="onListColor($event)" type="color" style="width:60px; height:36px" />
@ -273,6 +288,31 @@ const listShares = ref([])
const shareSearchResults = ref([])
const editingShareId = ref(null)
const editSharePermission = ref('read')
const isRenaming = ref(false)
const renameValue = ref('')
function startRename() {
renameValue.value = menuList.value?.name || ''
isRenaming.value = true
}
async function saveRename() {
const newName = renameValue.value.trim()
if (!newName || !menuList.value || newName === menuList.value.name) {
isRenaming.value = false
return
}
try {
await apiClient.put(`/tasklists/${menuList.value.id}`, { name: newName })
menuList.value.name = newName
isRenaming.value = false
await loadLists()
toast.add({ severity: 'success', summary: 'Umbenannt', life: 2000 })
} catch (err) {
toast.add({ severity: 'error', summary: 'Fehler',
detail: err.response?.data?.error || err.message, life: 4000 })
}
}
let shareSearchTimer = null
function startEditShare(s) {
@ -423,6 +463,7 @@ function openListMenu(tl) {
menuList.value = tl
shareUsername.value = ''
shareSearchResults.value = []
isRenaming.value = false
showListMenu.value = true
loadShares()
}
@ -705,6 +746,8 @@ watch(selectedListId, loadTasks)
.field-row { display: flex; gap: 0.75rem; }
.field-row .field { flex: 1; }
.share-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.rename-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
.rename-row strong { font-size: 1rem; }
.user-search-popup { position: absolute; top: 100%; left: 0; right: 0; z-index: 10;
background: white; border: 1px solid var(--p-surface-200);
border-radius: 4px; max-height: 160px; overflow-y: auto;