blueprint maange
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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,14 +127,26 @@ export default function SiteBuilderBlueprints() {
|
||||
Review and preview structures generated for your active site.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => navigate("/sites/builder")}
|
||||
variant="solid"
|
||||
tone="brand"
|
||||
startIcon={<Plus className="h-4 w-4" />}
|
||||
>
|
||||
Create blueprint
|
||||
</Button>
|
||||
<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"
|
||||
tone="brand"
|
||||
startIcon={<Plus className="h-4 w-4" />}
|
||||
>
|
||||
Create blueprint
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!activeSite ? (
|
||||
@@ -96,16 +171,61 @@ export default function SiteBuilderBlueprints() {
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
<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>
|
||||
<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 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 ${
|
||||
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>
|
||||
<Button
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
onClick={() => navigate(`/sites/${blueprint.site}/editor`)}
|
||||
>
|
||||
Open in editor
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user