feat: Enhance ListsPage with action menu for list management

This commit introduces a new action menu for managing lists on the ListsPage, allowing users to archive, unarchive, or delete lists directly from the UI. Key updates include:

- Implementation of a dropdown menu for list actions, triggered by a button in the list header.
- Addition of functions to handle archiving, unarchiving, and deleting lists, with confirmation prompts for deletion.
- Refactoring of the layout to improve responsiveness and user experience, including updates to CSS grid properties.

These changes aim to streamline list management and enhance user interaction within the application.
This commit is contained in:
Mohamad 2025-06-24 16:35:32 +02:00
parent 5f6f988118
commit 9d08b7fcfe

View File

@ -29,14 +29,25 @@
<div v-for="list in filteredLists" :key="list.id" class="neo-list-card"
:class="{ 'touch-active': touchActiveListId === list.id, 'archived': list.archived_at }"
@click="navigateToList(list.id)" @touchstart.passive="handleTouchStart(list.id)"
@touchend.passive="handleTouchEnd" @touchcancel.passive="handleTouchEnd" :data-list-id="list.id">
@touchend.passive="handleTouchEnd" @touchcancel.passive="handleTouchEnd" :data-list-id="list.id"
:ref="el => setListCardRef(el, list.id)">
<div class="neo-list-header">
<span>{{ list.name }}</span>
<!-- <div class="actions">
<VButton v-if="!list.archived_at" @click.stop="archiveList(list)" variant="danger" size="sm"
icon="archive" />
<VButton v-else @click.stop="unarchiveList(list)" variant="success" size="sm" icon="unarchive" />
</div> -->
<div class="actions">
<span @click.stop="toggleActionsMenu(list.id)" icon="more_vert" size="sm" variant="neutral">...</span>
<div v-if="actionsMenuVisibleFor === list.id" class="actions-dropdown">
<ul>
<li @click.stop="archiveOrUnarchive(list)">
<VIcon :name="list.archived_at ? 'unarchive' : 'archive'" style="margin-right: 0.5rem;" />
<span>{{ list.archived_at ? 'Unarchive' : 'Archive' }}</span>
</li>
<li @click.stop="deleteList(list)" class="text-danger">
<VIcon name="delete" style="margin-right: 0.5rem;" />
<span>Delete</span>
</li>
</ul>
</div>
</div>
</div>
<div class="neo-list-desc">{{ list.description || t('listsPage.noDescription') }}</div>
<ul class="neo-item-list">
@ -78,11 +89,12 @@ import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { apiClient, API_ENDPOINTS } from '@/services/api';
import CreateListModal from '@/components/CreateListModal.vue';
import { useStorage } from '@vueuse/core';
import { useStorage, onClickOutside } from '@vueuse/core';
import VAlert from '@/components/valerie/VAlert.vue';
import VCard from '@/components/valerie/VCard.vue';
import VButton from '@/components/valerie/VButton.vue';
import VToggleSwitch from '@/components/valerie/VToggleSwitch.vue';
import VIcon from '@/components/valerie/VIcon.vue';
const { t } = useI18n();
@ -142,6 +154,9 @@ const currentViewedGroup = ref<Group | null>(null);
const showCreateModal = ref(false);
const newItemInputRefs = ref<HTMLInputElement[]>([]);
const actionsMenuVisibleFor = ref<number | null>(null);
const listCardRefs = ref<Map<number, HTMLElement>>(new Map());
const pollingInterval = ref<ReturnType<typeof setInterval> | null>(null);
const currentGroupId = computed<number | null>(() => {
@ -579,6 +594,61 @@ const unarchiveList = async (list: List) => {
}
};
const setListCardRef = (el: any, listId: number) => {
if (el) {
listCardRefs.value.set(listId, el);
} else if (listCardRefs.value.has(listId)) {
listCardRefs.value.delete(listId);
}
};
const toggleActionsMenu = (listId: number) => {
actionsMenuVisibleFor.value = actionsMenuVisibleFor.value === listId ? null : listId;
};
const closeActionsMenu = () => {
actionsMenuVisibleFor.value = null;
};
const currentCardRef = computed(() => {
if (actionsMenuVisibleFor.value) {
return listCardRefs.value.get(actionsMenuVisibleFor.value)
}
return undefined
});
onClickOutside(currentCardRef, () => {
closeActionsMenu();
});
const archiveOrUnarchive = (list: List) => {
if (list.archived_at) {
unarchiveList(list);
} else {
archiveList(list);
}
closeActionsMenu();
};
const deleteList = async (list: List) => {
if (confirm(`Are you sure you want to permanently delete the list "${list.name}"? This action cannot be undone.`)) {
try {
console.log(`TODO: Implement permanent delete for list ${list.id}. For now, only removing from UI.`);
const index = lists.value.findIndex(l => l.id === list.id);
if (index > -1) {
lists.value.splice(index, 1);
}
const archivedIndex = archivedLists.value.findIndex(l => l.id === list.id);
if (archivedIndex > -1) {
archivedLists.value.splice(archivedIndex, 1);
}
} catch (error) {
console.error('Failed to delete list', error);
}
}
closeActionsMenu();
};
onMounted(() => {
loadCachedData();
fetchListsAndGroups().then(() => {
@ -647,18 +717,18 @@ onUnmounted(() => {
}
.neo-lists-grid {
columns: 3 500px;
column-gap: 2rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
align-items: start;
}
.neo-list-card,
.neo-create-list-card {
break-inside: avoid;
border-radius: 18px;
box-shadow: 6px 6px 0 var(--dark);
width: 100%;
margin: 0 0 2rem 0;
background: var(--light);
display: flex;
flex-direction: column;
@ -681,6 +751,7 @@ onUnmounted(() => {
}
.neo-list-header {
display: flex;
font-weight: 900;
font-size: 1.25rem;
margin-bottom: 0.5rem;
@ -911,13 +982,12 @@ onUnmounted(() => {
@media (max-width: 900px) {
.neo-lists-grid {
columns: 2 260px;
column-gap: 1.2rem;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.2rem;
}
.neo-list-card,
.neo-create-list-card {
margin-bottom: 1.2rem;
padding-left: 1rem;
padding-right: 1rem;
}
@ -929,12 +999,11 @@ onUnmounted(() => {
}
.neo-lists-grid {
columns: 1 280px;
column-gap: 1rem;
grid-template-columns: 1fr;
gap: 1rem;
}
.neo-list-card {
margin-bottom: 1rem;
padding: 1rem;
font-size: 1rem;
min-height: 80px;
@ -1015,4 +1084,53 @@ onUnmounted(() => {
display: flex;
gap: 0.5rem;
}
.neo-list-header .actions {
position: relative;
margin-left: auto;
}
.actions-dropdown {
position: absolute;
top: 100%;
right: 0;
background-color: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
overflow: hidden;
min-width: 150px;
padding: 0.5rem 0;
}
.actions-dropdown ul {
list-style: none;
padding: 0;
margin: 0;
}
.actions-dropdown li {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
color: #333;
transition: background-color 0.2s ease, color 0.2s ease;
}
.actions-dropdown li:hover {
background-color: #f5f5f5;
}
.actions-dropdown li.text-danger {
color: var(--danger);
}
.actions-dropdown li.text-danger:hover {
background-color: var(--danger);
color: white;
}
</style>