Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,29 @@ SPDX-License-Identifier: MIT
<lfx-icon name="share-nodes" />
Share
</lfx-button>

<lfx-dropdown
v-if="props.type === 'my-collections'"
placement="bottom-end"
:class="isDeleting ? 'opacity-50 cursor-not-allowed' : ''"
:disabled="isDeleting"
>
<template #trigger>
<lfx-icon-button
icon="ellipsis"
type="transparent"
class="!text-neutral-900"
/>
</template>
<lfx-dropdown-item @click.stop.prevent="handleDelete">
<lfx-icon
name="trash"
:size="16"
class="!text-negative-500"
/>
<span class="text-negative-500">Delete</span>
</lfx-dropdown-item>
</lfx-dropdown>
</div>
</div>
</div>
Expand Down Expand Up @@ -216,18 +239,34 @@ SPDX-License-Identifier: MIT
</div>
</section>
</div>

<!-- Full-page loading overlay for delete operation -->
<teleport to="body">
<div
v-if="isDeleting"
class="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black/50"
>
<lfx-spinner
:size="48"
class="text-white"
/>
<p class="mt-4 text-white text-sm font-medium">Deleting collection...</p>
</div>
</teleport>
</template>

<script lang="ts" setup>
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useRouter } from 'nuxt/app';
import { useQueryClient } from '@tanstack/vue-query';
import { collectionTabs, headerBackground } from '../../config/collection-type-config';
import type { Collection } from '~~/types/collection';
import LfxIconButton from '~/components/uikit/icon-button/icon-button.vue';
import LfxIcon from '~/components/uikit/icon/icon.vue';
import useScroll from '~/components/shared/utils/scroll';
import LfxSkeleton from '~/components/uikit/skeleton/skeleton.vue';
import LfxSpinner from '~/components/uikit/spinner/spinner.vue';
import LfxButton from '~/components/uikit/button/button.vue';
import CollectionOwner from '~/components/shared/components/collection-owner.vue';
import { formatDate } from '~/components/shared/utils/formatter';
Expand All @@ -240,9 +279,16 @@ import LfxLikeButton from '~/components/shared/components/like-button.vue';
import LfxTooltip from '~/components/uikit/tooltip/tooltip.vue';
import { useEditCollectionStore } from '~/components/modules/collection/store/edit-collection.store';
import { useDuplicateCollectionStore } from '~/components/modules/collection/store/duplicate-collection.store';
import LfxDropdown from '~/components/uikit/dropdown/dropdown.vue';
import LfxDropdownItem from '~/components/uikit/dropdown/dropdown-item.vue';
import { useConfirmStore } from '~/components/shared/modules/confirm/store/confirm.store';
import { TanstackKey } from '~/components/shared/types/tanstack';
import { COLLECTIONS_API_SERVICE } from '~/components/modules/collection/services/collections.api.service';

const { openEditModal } = useEditCollectionStore();
const { openDuplicateModal } = useDuplicateCollectionStore();
const { openConfirmModal } = useConfirmStore();
const queryClient = useQueryClient();

const authStore = useAuthStore();
const { user } = storeToRefs(authStore);
Expand Down Expand Up @@ -287,6 +333,8 @@ const currentSort = computed(() => {
return { field: props.sort, direction: 'asc' as const };
});

const isDeleting = ref(false);

const handleSort = (field: string) => {
const defaultDirections: Record<string, 'asc' | 'desc'> = {
name: 'asc',
Expand Down Expand Up @@ -347,6 +395,32 @@ const handleClone = () => {
});
}
};

const handleDelete = () => {
if (props.collection && !isDeleting.value) {
openConfirmModal({
title: 'Delete collection',
message: `Are you sure you want to delete this collection?`,
confirmLabel: 'Delete',
cancelLabel: 'Cancel',
}).then(async (result) => {
if (result) {
isDeleting.value = true;
await COLLECTIONS_API_SERVICE.deleteCollection(props.collection!.id);

invalidateMyCollections();
isDeleting.value = false;
router.push({ name: LfxRoutes.COLLECTIONS_MY_COLLECTIONS });
}
});
}
};

const invalidateMyCollections = () => {
queryClient.invalidateQueries({
queryKey: [TanstackKey.MY_COLLECTIONS, 'starred_desc', undefined, 99, undefined, user.value?.sub],
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invalidateMyCollections() uses a fully-specified/hardcoded queryKey ('starred_desc', 99, etc.), which is unlikely to match the actual fetchMyCollections query keys in all cases. Prefer invalidating by prefix (e.g., [TanstackKey.MY_COLLECTIONS]) or derive the key from the same params source used to build the query key.

Suggested change
queryKey: [TanstackKey.MY_COLLECTIONS, 'starred_desc', undefined, 99, undefined, user.value?.sub],
queryKey: [TanstackKey.MY_COLLECTIONS],

Copilot uses AI. Check for mistakes.
});
};
</script>

<script lang="ts">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ SPDX-License-Identifier: MIT
<lfx-icon
name="trash"
:size="16"
class="text-negative-500"
class="!text-negative-500"
/>
<span class="text-negative-500">Delete</span>
</lfx-dropdown-item>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!--
Copyright (c) 2025 The Linux Foundation and each contributor.
SPDX-License-Identifier: MIT
-->
<template>
<lfx-confirm-modal
v-if="isConfirmModalOpen"
v-model="isConfirmModalOpen"
:options="confirmOptions"
@confirm="confirm"
@cancel="cancel"
/>
</template>

<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import LfxConfirmModal from '~/components/shared/modules/confirm/components/confirm-modal.vue';
import { useConfirmStore } from '~/components/shared/modules/confirm/store/confirm.store';

const confirmStore = useConfirmStore();
const { isConfirmModalOpen, confirmOptions } = storeToRefs(confirmStore);
const { confirm, cancel } = confirmStore;
</script>

<script lang="ts">
export default {
name: 'LfxConfirmGlobal',
};
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!--
Copyright (c) 2025 The Linux Foundation and each contributor.
SPDX-License-Identifier: MIT
-->
<template>
<lfx-modal
v-model="isModalOpen"
width="25rem"
>
<section class="p-6">
<div class="flex justify-between items-start">
<h3 class="text-heading-3 font-bold text-neutral-900">
{{ options.title }}
</h3>
<lfx-icon-button
icon="close"
size="small"
@click="handleCancel"
/>
</div>

<p class="mt-4 text-body-2 text-neutral-700">
{{ options.message }}
</p>

<div class="mt-6 flex justify-end gap-3">
<lfx-button
type="primary"
size="small"
:label="options.confirmLabel"
@click="handleConfirm"
/>
<lfx-button
type="transparent"
size="small"
:label="options.cancelLabel"
@click="handleCancel"
/>
</div>
</section>
</lfx-modal>
</template>

<script lang="ts" setup>
import { computed } from 'vue';
import LfxModal from '~/components/uikit/modal/modal.vue';
import LfxIconButton from '~/components/uikit/icon-button/icon-button.vue';
import LfxButton from '~/components/uikit/button/button.vue';
import type { ConfirmOptions } from '~/components/shared/modules/confirm/store/confirm.store';

const props = defineProps<{
modelValue: boolean;
options: ConfirmOptions;
}>();

const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'confirm'): void;
(e: 'cancel'): void;
}>();

const isModalOpen = computed({
get: () => props.modelValue,
set: (value: boolean) => {
emit('update:modelValue', value);
if (!value) {
emit('cancel');
}
},
});

const handleConfirm = () => {
emit('confirm');
};

const handleCancel = () => {
emit('cancel');
};
</script>

<script lang="ts">
export default {
name: 'LfxConfirmModal',
};
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2025 The Linux Foundation and each contributor.
// SPDX-License-Identifier: MIT
import { defineStore } from 'pinia';
import { ref } from 'vue';

export interface ConfirmOptions {
title?: string;
message: string;
confirmLabel?: string;
cancelLabel?: string;
}

const defaultOptions: ConfirmOptions = {
title: 'Confirm',
message: '',
confirmLabel: 'Ok',
cancelLabel: 'Cancel',
};

export const useConfirmStore = defineStore('confirm', () => {
const isConfirmModalOpen = ref(false);
const confirmOptions = ref<ConfirmOptions>(defaultOptions);
const resolvePromise = ref<((value: boolean) => void) | null>(null);

const openConfirmModal = (options: ConfirmOptions): Promise<boolean> => {
confirmOptions.value = { ...defaultOptions, ...options };
isConfirmModalOpen.value = true;

return new Promise((resolve) => {
resolvePromise.value = resolve;
});
};

const confirm = () => {
isConfirmModalOpen.value = false;
resolvePromise.value?.(true);
resolvePromise.value = null;
};

const cancel = () => {
isConfirmModalOpen.value = false;
resolvePromise.value?.(false);
resolvePromise.value = null;
};

return {
isConfirmModalOpen,
confirmOptions,
openConfirmModal,
confirm,
cancel,
};
});
2 changes: 2 additions & 0 deletions frontend/app/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ SPDX-License-Identifier: MIT
<lf-duplicate-collection-global />
<lfx-copilot-global />
<lfx-community-filter-global />
<lfx-confirm-global />
<lfx-insights-footer />
</main>
</template>
Expand All @@ -35,6 +36,7 @@ import LfEditCollectionGlobal from '~/components/modules/collection/components/e
import LfDuplicateCollectionGlobal from '~/components/modules/collection/components/create-modal/duplicate-collection-global.vue';
import LfxCopilotGlobal from '~/components/shared/modules/copilot/components/copilot-global.vue';
import LfxCommunityFilterGlobal from '~/components/modules/project/components/community/sections/community-filter-global.vue';
import LfxConfirmGlobal from '~/components/shared/modules/confirm/components/confirm-global.vue';
import { useRichSchema } from '~~/composables/useRichSchema';
import { useBannerStore } from '~/components/shared/store/banner.store';

Expand Down
Loading