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)
|
return success_response({'tasks': serializer.data, 'count': len(tasks)}, request=request)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return error_response(str(e), status.HTTP_400_BAD_REQUEST, request)
|
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):
|
class PageBlueprintViewSet(SiteSectorModelViewSet):
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import PageMeta from "../../../components/common/PageMeta";
|
|||||||
import { Card } from "../../../components/ui/card";
|
import { Card } from "../../../components/ui/card";
|
||||||
import Button from "../../../components/ui/button/Button";
|
import Button from "../../../components/ui/button/Button";
|
||||||
import { useToast } from "../../../components/ui/toast/ToastContainer";
|
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 { useSiteStore } from "../../../store/siteStore";
|
||||||
import { useBuilderStore } from "../../../store/builderStore";
|
import { useBuilderStore } from "../../../store/builderStore";
|
||||||
import { siteBuilderApi } from "../../../services/siteBuilder.api";
|
import { siteBuilderApi } from "../../../services/siteBuilder.api";
|
||||||
import type { SiteBlueprint } from "../../../types/siteBuilder";
|
import type { SiteBlueprint } from "../../../types/siteBuilder";
|
||||||
|
import { Modal } from "../../../components/ui/modal";
|
||||||
|
|
||||||
export default function SiteBuilderBlueprints() {
|
export default function SiteBuilderBlueprints() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -18,6 +19,12 @@ export default function SiteBuilderBlueprints() {
|
|||||||
useBuilderStore();
|
useBuilderStore();
|
||||||
const [blueprints, setBlueprints] = useState<SiteBlueprint[]>([]);
|
const [blueprints, setBlueprints] = useState<SiteBlueprint[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (activeSite?.id) {
|
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 (
|
return (
|
||||||
<div className="space-y-6 p-6">
|
<div className="space-y-6 p-6">
|
||||||
<PageMeta title="Blueprints - IGNY8" />
|
<PageMeta title="Blueprints - IGNY8" />
|
||||||
@@ -64,14 +127,26 @@ export default function SiteBuilderBlueprints() {
|
|||||||
Review and preview structures generated for your active site.
|
Review and preview structures generated for your active site.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
onClick={() => navigate("/sites/builder")}
|
{selectedIds.size > 0 && (
|
||||||
variant="solid"
|
<Button
|
||||||
tone="brand"
|
onClick={handleBulkDeleteClick}
|
||||||
startIcon={<Plus className="h-4 w-4" />}
|
variant="solid"
|
||||||
>
|
tone="danger"
|
||||||
Create blueprint
|
startIcon={<Trash2 className="h-4 w-4" />}
|
||||||
</Button>
|
>
|
||||||
|
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>
|
</div>
|
||||||
|
|
||||||
{!activeSite ? (
|
{!activeSite ? (
|
||||||
@@ -96,16 +171,61 @@ export default function SiteBuilderBlueprints() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
<div className="space-y-4">
|
||||||
{blueprints.map((blueprint) => (
|
{blueprints.length > 0 && (
|
||||||
<Card key={blueprint.id} className="space-y-4 p-5">
|
<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]">
|
||||||
<div>
|
<button
|
||||||
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
|
onClick={toggleSelectAll}
|
||||||
Blueprint #{blueprint.id}
|
className="flex items-center gap-2 text-sm text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
|
||||||
</p>
|
>
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
{selectedIds.size === blueprints.length ? (
|
||||||
{blueprint.name}
|
<CheckSquare className="h-4 w-4" />
|
||||||
</h3>
|
) : (
|
||||||
|
<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 && (
|
{blueprint.description && (
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{blueprint.description}
|
{blueprint.description}
|
||||||
@@ -132,18 +252,58 @@ export default function SiteBuilderBlueprints() {
|
|||||||
"Open preview"
|
"Open preview"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
variant="ghost"
|
<Button
|
||||||
tone="neutral"
|
variant="ghost"
|
||||||
onClick={() => navigate(`/sites/${blueprint.site}/editor`)}
|
tone="neutral"
|
||||||
>
|
fullWidth
|
||||||
Open in editor
|
onClick={() => navigate(`/sites/${blueprint.site}/editor`)}
|
||||||
</Button>
|
>
|
||||||
|
Open in editor
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
tone="danger"
|
||||||
|
onClick={() => handleDeleteClick(blueprint)}
|
||||||
|
startIcon={<Trash2 className="h-4 w-4" />}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,5 +129,33 @@ export const siteBuilderApi = {
|
|||||||
async getMetadata(): Promise<SiteBuilderMetadata> {
|
async getMetadata(): Promise<SiteBuilderMetadata> {
|
||||||
return fetchAPI('/v1/site-builder/metadata/');
|
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