blueprint maange

This commit is contained in:
alorig
2025-11-18 22:05:44 +05:00
parent f1a3504b72
commit 11766454e9
3 changed files with 242 additions and 26 deletions

View File

@@ -137,6 +137,34 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet):
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):
"""

View File

@@ -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<SiteBlueprint[]>([]);
const [loading, setLoading] = useState(false);
const [selectedIds, setSelectedIds] = useState<Set<number>>(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 (
<div className="space-y-6 p-6">
<PageMeta title="Blueprints - IGNY8" />
@@ -64,6 +127,17 @@ export default function SiteBuilderBlueprints() {
Review and preview structures generated for your active site.
</p>
</div>
<div className="flex gap-2">
{selectedIds.size > 0 && (
<Button
onClick={handleBulkDeleteClick}
variant="solid"
tone="danger"
startIcon={<Trash2 className="h-4 w-4" />}
>
Delete {selectedIds.size} selected
</Button>
)}
<Button
onClick={() => navigate("/sites/builder")}
variant="solid"
@@ -73,6 +147,7 @@ export default function SiteBuilderBlueprints() {
Create blueprint
</Button>
</div>
</div>
{!activeSite ? (
<Card className="p-8 text-center">
@@ -96,16 +171,61 @@ export default function SiteBuilderBlueprints() {
</Button>
</Card>
) : (
<div className="space-y-4">
{blueprints.length > 0 && (
<div className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 px-4 py-2 dark:border-white/10 dark:bg-white/[0.02]">
<button
onClick={toggleSelectAll}
className="flex items-center gap-2 text-sm text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
>
{selectedIds.size === blueprints.length ? (
<CheckSquare className="h-4 w-4" />
) : (
<Square className="h-4 w-4" />
)}
<span>
{selectedIds.size === blueprints.length
? "Deselect all"
: "Select all"}
</span>
</button>
{selectedIds.size > 0 && (
<span className="text-sm text-gray-600 dark:text-gray-400">
{selectedIds.size} of {blueprints.length} selected
</span>
)}
</div>
)}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
{blueprints.map((blueprint) => (
<Card key={blueprint.id} className="space-y-4 p-5">
<div>
<Card
key={blueprint.id}
className={`space-y-4 p-5 ${
selectedIds.has(blueprint.id)
? "ring-2 ring-brand-500 dark:ring-brand-400"
: ""
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
Blueprint #{blueprint.id}
</p>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{blueprint.name}
</h3>
</div>
<button
onClick={() => toggleSelection(blueprint.id)}
className="ml-2 flex-shrink-0"
>
{selectedIds.has(blueprint.id) ? (
<CheckSquare className="h-5 w-5 text-brand-600 dark:text-brand-400" />
) : (
<Square className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
{blueprint.description && (
<p className="text-sm text-gray-500 dark:text-gray-400">
{blueprint.description}
@@ -132,18 +252,58 @@ export default function SiteBuilderBlueprints() {
"Open preview"
)}
</Button>
<div className="flex gap-2">
<Button
variant="ghost"
tone="neutral"
fullWidth
onClick={() => navigate(`/sites/${blueprint.site}/editor`)}
>
Open in editor
</Button>
<Button
variant="ghost"
tone="danger"
onClick={() => handleDeleteClick(blueprint)}
startIcon={<Trash2 className="h-4 w-4" />}
>
Delete
</Button>
</div>
</div>
</Card>
))}
</div>
)}
<Modal
isOpen={deleteConfirm.isOpen}
onClose={() => 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={
<>
<Button
variant="outline"
tone="neutral"
onClick={() => setDeleteConfirm({ isOpen: false, blueprint: null, isBulk: false })}
>
Cancel
</Button>
<Button
variant="solid"
tone="danger"
onClick={handleDeleteConfirm}
>
Delete
</Button>
</>
}
/>
</div>
);
}

View File

@@ -129,5 +129,33 @@ export const siteBuilderApi = {
async getMetadata(): Promise<SiteBuilderMetadata> {
return fetchAPI('/v1/site-builder/metadata/');
},
/**
* Delete a blueprint
*/
async deleteBlueprint(id: number): Promise<void> {
return fetchAPI(`/v1/site-builder/blueprints/${id}/`, {
method: 'DELETE',
});
},
/**
* Delete a page blueprint
*/
async deletePage(id: number): Promise<void> {
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 }),
});
},
};