diff --git a/actions/supabase/queries/templates.ts b/actions/supabase/queries/templates.ts index 30d3ff69..0b9ab10c 100644 --- a/actions/supabase/queries/templates.ts +++ b/actions/supabase/queries/templates.ts @@ -194,7 +194,8 @@ export const fetchTemplatesExercise = async (userGroup: string) => { const { data, error } = await supabase .from("template") .select("*") - .or(`user_group_id.eq.${userGroup},accessible_to_all.eq.true`); + .or(`user_group_id.eq.${userGroup},accessible_to_all.eq.true`) + .eq("archived", false); if (error) { console.error("Error fetching templates:", error); @@ -203,6 +204,50 @@ export const fetchTemplatesExercise = async (userGroup: string) => { return data; }; +// archive or delete a template +// can only do if from the right user_group (or if admin) +// no data is deleted (for PDFs and sessions) +export async function setTemplateArchived( + template_id: UUID, + archived: boolean, + requester_group_id: UUID, + is_admin: boolean, +): Promise { + const supabase = await getSupabaseServerClient(); + + const { data: tmpl, error: fetchErr } = await supabase + .from("template") + .select("user_group_id, accessible_to_all") + .eq("template_id", template_id) + .single(); + + if (fetchErr || !tmpl) { + console.error("Error loading template for archive:", fetchErr); + return false; + } + + const ownsTemplate = tmpl.user_group_id === requester_group_id; + const canArchive = + ownsTemplate || (is_admin && Boolean(tmpl.accessible_to_all)); + + if (!canArchive) { + console.error("Not authorized to archive template:", template_id); + return false; + } + + const { error } = await supabase + .from("template") + .update({ archived }) + .eq("template_id", template_id); + + if (error) { + console.error("Error archiving template:", error); + return false; + } + + return true; +} + export async function fetchFullTemplate(template_id: string) { const supabase = await getSupabaseServerClient(); diff --git a/app/facilitator/exercises/start/page.tsx b/app/facilitator/exercises/start/page.tsx index b22c3cf6..6c740372 100644 --- a/app/facilitator/exercises/start/page.tsx +++ b/app/facilitator/exercises/start/page.tsx @@ -10,7 +10,10 @@ import { createSession, fetchRoles, } from "@/actions/supabase/queries/sessions"; -import { fetchTemplatesExercise } from "@/actions/supabase/queries/templates"; +import { + fetchTemplate, + fetchTemplatesExercise, +} from "@/actions/supabase/queries/templates"; import { fetchUserGroupMembers } from "@/actions/supabase/queries/user-groups"; import Play from "@/assets/images/play.svg"; import AccessError from "@/components/AccessError/AccessError"; @@ -81,13 +84,22 @@ export default function Page() { setTemplatesLoading(true); try { const data = await fetchTemplatesExercise(profile.user_group_id as UUID); - setTemplates(data || []); + let list = data || []; if (preselectedTemplateId) { + // The list is decluttered (archived templates dropped), but if we were + // sent here with a specific template, make sure it's an option so the + // dropdown populates instead of showing blank. + if (!list.some(t => t.template_id === preselectedTemplateId)) { + const preselected = await fetchTemplate(preselectedTemplateId); + if (preselected) list = [preselected, ...list]; + } setSelectedTemplateId(preselectedTemplateId); const rolesData = await fetchRoles(preselectedTemplateId); setRoles((rolesData as Role[]) || []); } + + setTemplates(list); } finally { setTemplatesLoading(false); } diff --git a/app/templates/page.tsx b/app/templates/page.tsx index 622c3218..a70ca43a 100644 --- a/app/templates/page.tsx +++ b/app/templates/page.tsx @@ -83,6 +83,7 @@ const createInitialStore = (): LocalStore => { template_id: templateID, template_name: "New Template", accessible_to_all: null, + archived: false, user_group_id: null, summary: "", setting: "", diff --git a/components/TemplateList/TemplateList.tsx b/components/TemplateList/TemplateList.tsx index 7508648d..ea045788 100644 --- a/components/TemplateList/TemplateList.tsx +++ b/components/TemplateList/TemplateList.tsx @@ -4,7 +4,13 @@ import type { Template, UUID } from "@/types/schema"; import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { CircularProgress } from "@mui/material"; -import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react"; +import { + Archive, + ArchiveRestore, + ArrowDown, + ArrowUp, + ArrowUpDown, +} from "lucide-react"; import { assignTagToTemplate, createTag, @@ -15,6 +21,7 @@ import { import { copyTemplate, fetchTemplatesWithTags, + setTemplateArchived, } from "@/actions/supabase/queries/templates"; import TopNavBar from "@/components/FacilitatorNavBar/FacilitatorNavBar"; import { Tag } from "@/components/Tag/TagCreator"; @@ -35,6 +42,7 @@ import { SortButton, } from "../../app/facilitator/styles"; import { + ArchivedToggle, AssociatedTags, ContentWrapper, DateColumn, @@ -62,10 +70,11 @@ export default function TemplateListPage({ const router = useRouter(); const { profile } = useProfile(); const user_group_id = profile?.user_group_id as UUID; + const adminAccess = profile?.user_type === "Admin"; const loading = !user_group_id; - const [filterMode, setFilterMode] = useState<"All" | "Your" | "Browse">( - "All", - ); + const [filterMode, setFilterMode] = useState< + "All" | "Your" | "Browse" | "Archived" + >("All"); const [searchInput, setSearchInput] = useState(""); const [templates, setTemplates] = useState([]); const [templatesLoading, setTemplatesLoading] = useState(true); @@ -81,6 +90,11 @@ export default function TemplateListPage({ const [copyConfirm, setCopyConfirm] = useState(null); const [copying, setCopying] = useState(false); + const [archiveConfirm, setArchiveConfirm] = useState( + null, + ); + const [archiving, setArchiving] = useState(false); + useEffect(() => { if (!user_group_id) return; @@ -104,14 +118,25 @@ export default function TemplateListPage({ const filteredTemplates = useMemo(() => { let updated = [...templates]; - if (filterMode === "All") { + if (filterMode === "Archived") { updated = updated.filter( - t => t.accessible_to_all || t.user_group_id === user_group_id, + t => + t.archived && + (t.user_group_id === user_group_id || + (adminAccess && Boolean(t.accessible_to_all))), ); - } else if (filterMode === "Your") { - updated = updated.filter(t => t.user_group_id === user_group_id); } else { - updated = updated.filter(t => t.accessible_to_all); + updated = updated.filter(t => !t.archived); + + if (filterMode === "All") { + updated = updated.filter( + t => t.accessible_to_all || t.user_group_id === user_group_id, + ); + } else if (filterMode === "Your") { + updated = updated.filter(t => t.user_group_id === user_group_id); + } else { + updated = updated.filter(t => t.accessible_to_all); + } } if (selectedTagIds) { @@ -147,6 +172,7 @@ export default function TemplateListPage({ sortKey, sortOrder, user_group_id, + adminAccess, ]); const toggleSort = (key: "name" | "date") => { @@ -160,9 +186,25 @@ export default function TemplateListPage({ const isAdminTemplate = (t: TemplateWithTags) => Boolean(t.accessible_to_all); + // You manage a template if your group owns it, or you're an admin + const canManageTemplate = (t: TemplateWithTags) => + t.user_group_id === user_group_id || + (adminAccess && Boolean(t.accessible_to_all)); + + // A shared template you don't manage stays copy-to-edit only + const isLockedTemplate = (t: TemplateWithTags) => + isAdminTemplate(t) && !canManageTemplate(t); + const handleOpenTemplate = (t: TemplateWithTags) => { - if (isAdminTemplate(t)) return; - router.push(`/templates?templateId=${t.template_id}&fromTemplateList=true`); + if (isLockedTemplate(t)) return; + + const params = new URLSearchParams({ + templateId: t.template_id, + fromTemplateList: "true", + }); + if (t.accessible_to_all) params.set("isAdmin", "true"); + + router.push(`/templates?${params.toString()}`); }; const handleCopyConfirm = async (action: WarningAction) => { @@ -185,6 +227,40 @@ export default function TemplateListPage({ } }; + const handleArchiveConfirm = async (action: WarningAction) => { + if (action === "cancel" || !archiveConfirm) { + setArchiveConfirm(null); + return; + } + + const target = archiveConfirm; + const nextArchived = !target.archived; + + setArchiving(true); + try { + const ok = await setTemplateArchived( + target.template_id, + nextArchived, + user_group_id, + adminAccess, + ); + if (ok) { + setTemplates(prev => + prev.map(t => + t.template_id === target.template_id + ? { ...t, archived: nextArchived } + : t, + ), + ); + } + } catch (err) { + console.error("Archive failed", err); + } finally { + setArchiving(false); + setArchiveConfirm(null); + } + }; + async function deleteTagComponent(tag_id: UUID, template_id?: UUID) { if (template_id) { const success = await removeTagFromTemplate(template_id, tag_id); @@ -363,6 +439,21 @@ export default function TemplateListPage({ } showBorder={true} /> + + {!showSidebar && ( + + setFilterMode(prev => + prev === "Archived" ? "All" : "Archived", + ) + } + > + {filterMode === "Archived" + ? "Active templates" + : "Archived"} + + )} {templatesLoading ? ( @@ -406,15 +497,15 @@ export default function TemplateListPage({ )} {filteredTemplates.map(t => { - const isAdmin = isAdminTemplate(t); + const isLocked = isLockedTemplate(t); return ( - + handleOpenTemplate(t)} title={ - isAdmin - ? "Shared template — make a copy to edit" + isLocked + ? "You cannot edit this template. View content through downloading the PDF." : undefined } > @@ -536,6 +627,22 @@ export default function TemplateListPage({ + + {canManageTemplate(t) && ( + { + e.stopPropagation(); + setArchiveConfirm(t); + }} + title={t.archived ? "Unarchive" : "Archive"} + > + {t.archived ? ( + + ) : ( + + )} + + )} ); @@ -554,6 +661,22 @@ export default function TemplateListPage({ confirmLabel="Make a copy" loading={copying} /> + +