diff --git a/backend/igny8_core/modules/site_builder/views.py b/backend/igny8_core/modules/site_builder/views.py index e12ae8c1..29fafda2 100644 --- a/backend/igny8_core/modules/site_builder/views.py +++ b/backend/igny8_core/modules/site_builder/views.py @@ -136,6 +136,34 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet): return success_response({'tasks': serializer.data, 'count': len(tasks)}, request=request) except Exception as e: return error_response(str(e), status.HTTP_400_BAD_REQUEST, request) + + @action(detail=False, methods=['POST'], url_path='bulk_delete', url_name='bulk_delete') + def bulk_delete(self, request): + """ + Bulk delete blueprints. + + Request body: + { + "ids": [1, 2, 3] # List of blueprint IDs to delete + } + + Returns: + { + "deleted_count": 3 + } + """ + ids = request.data.get('ids', []) + if not ids: + return error_response( + error='No IDs provided', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) + + queryset = self.get_queryset() + deleted_count, _ = queryset.filter(id__in=ids).delete() + + return success_response(data={'deleted_count': deleted_count}, request=request) class PageBlueprintViewSet(SiteSectorModelViewSet): diff --git a/frontend/src/pages/Sites/Builder/Blueprints.tsx b/frontend/src/pages/Sites/Builder/Blueprints.tsx index 4eaf6d3d..ecb40de7 100644 --- a/frontend/src/pages/Sites/Builder/Blueprints.tsx +++ b/frontend/src/pages/Sites/Builder/Blueprints.tsx @@ -4,11 +4,12 @@ import PageMeta from "../../../components/common/PageMeta"; import { Card } from "../../../components/ui/card"; import Button from "../../../components/ui/button/Button"; import { useToast } from "../../../components/ui/toast/ToastContainer"; -import { FileText, Loader2, Plus } from "lucide-react"; +import { FileText, Loader2, Plus, Trash2, CheckSquare, Square } from "lucide-react"; import { useSiteStore } from "../../../store/siteStore"; import { useBuilderStore } from "../../../store/builderStore"; import { siteBuilderApi } from "../../../services/siteBuilder.api"; import type { SiteBlueprint } from "../../../types/siteBuilder"; +import { Modal } from "../../../components/ui/modal"; export default function SiteBuilderBlueprints() { const navigate = useNavigate(); @@ -18,6 +19,12 @@ export default function SiteBuilderBlueprints() { useBuilderStore(); const [blueprints, setBlueprints] = useState([]); const [loading, setLoading] = useState(false); + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [deleteConfirm, setDeleteConfirm] = useState<{ + isOpen: boolean; + blueprint: SiteBlueprint | null; + isBulk: boolean; + }>({ isOpen: false, blueprint: null, isBulk: false }); useEffect(() => { if (activeSite?.id) { @@ -49,6 +56,62 @@ export default function SiteBuilderBlueprints() { } }; + const handleDeleteClick = (blueprint: SiteBlueprint) => { + setDeleteConfirm({ isOpen: true, blueprint, isBulk: false }); + }; + + const handleBulkDeleteClick = () => { + if (selectedIds.size === 0) { + toast.error("No blueprints selected"); + return; + } + setDeleteConfirm({ isOpen: true, blueprint: null, isBulk: true }); + }; + + const handleDeleteConfirm = async () => { + try { + if (deleteConfirm.isBulk) { + // Bulk delete + const ids = Array.from(selectedIds); + const result = await siteBuilderApi.bulkDeleteBlueprints(ids); + const count = result?.deleted_count || ids.length; + toast.success(`${count} blueprint${count !== 1 ? 's' : ''} deleted successfully`); + setSelectedIds(new Set()); + } else if (deleteConfirm.blueprint) { + // Single delete + await siteBuilderApi.deleteBlueprint(deleteConfirm.blueprint.id); + toast.success("Blueprint deleted successfully"); + } + + setDeleteConfirm({ isOpen: false, blueprint: null, isBulk: false }); + if (activeSite?.id) { + await loadBlueprints(activeSite.id); + } + } catch (error: any) { + toast.error(error?.message || "Failed to delete blueprint(s)"); + } + }; + + const toggleSelection = (id: number) => { + setSelectedIds(prev => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + const toggleSelectAll = () => { + if (selectedIds.size === blueprints.length) { + setSelectedIds(new Set()); + } else { + setSelectedIds(new Set(blueprints.map(b => b.id))); + } + }; + return (
@@ -64,14 +127,26 @@ export default function SiteBuilderBlueprints() { Review and preview structures generated for your active site.

- +
+ {selectedIds.size > 0 && ( + + )} + +
{!activeSite ? ( @@ -96,16 +171,61 @@ export default function SiteBuilderBlueprints() { ) : ( -
- {blueprints.map((blueprint) => ( - -
-

- Blueprint #{blueprint.id} -

-

- {blueprint.name} -

+
+ {blueprints.length > 0 && ( +
+ + {selectedIds.size > 0 && ( + + {selectedIds.size} of {blueprints.length} selected + + )} +
+ )} +
+ {blueprints.map((blueprint) => ( + +
+
+

+ Blueprint #{blueprint.id} +

+

+ {blueprint.name} +

+
+ +
{blueprint.description && (

{blueprint.description} @@ -132,18 +252,58 @@ export default function SiteBuilderBlueprints() { "Open preview" )} - +

+ + +
))}
)} + + setDeleteConfirm({ isOpen: false, blueprint: null, isBulk: false })} + title={deleteConfirm.isBulk ? "Delete Blueprints" : "Delete Blueprint"} + description={ + deleteConfirm.isBulk + ? `Are you sure you want to delete ${selectedIds.size} blueprint${selectedIds.size !== 1 ? 's' : ''}? This will also delete all associated page blueprints. This action cannot be undone.` + : `Are you sure you want to delete "${deleteConfirm.blueprint?.name}"? This will also delete all associated page blueprints. This action cannot be undone.` + } + footer={ + <> + + + + } + />
); } diff --git a/frontend/src/services/siteBuilder.api.ts b/frontend/src/services/siteBuilder.api.ts index 84361708..456892d8 100644 --- a/frontend/src/services/siteBuilder.api.ts +++ b/frontend/src/services/siteBuilder.api.ts @@ -129,5 +129,33 @@ export const siteBuilderApi = { async getMetadata(): Promise { return fetchAPI('/v1/site-builder/metadata/'); }, + + /** + * Delete a blueprint + */ + async deleteBlueprint(id: number): Promise { + return fetchAPI(`/v1/site-builder/blueprints/${id}/`, { + method: 'DELETE', + }); + }, + + /** + * Delete a page blueprint + */ + async deletePage(id: number): Promise { + return fetchAPI(`/v1/site-builder/pages/${id}/`, { + method: 'DELETE', + }); + }, + + /** + * Bulk delete blueprints + */ + async bulkDeleteBlueprints(ids: number[]): Promise<{ deleted_count: number }> { + return fetchAPI('/v1/site-builder/blueprints/bulk_delete/', { + method: 'POST', + body: JSON.stringify({ ids }), + }); + }, };