Author: Patrick Miguel M. Babala
Developers & Reviewers: Clyde Ador, Patrick Babala, and Christian Denzon
Creation Date: September 29, 2026
Status: Published
References: Issue #777, docs/FEATURE_AUDIT_MAY2026.md:127,285,508, PR #780, ad657492, b65e7991, 13373adb, 1210c258, 07530207, supabase/migrations/20260414_create_audit_logs_table.sql, docs/PR_DESCRIPTION_reusable-confirm-destructive-modal.md, docs/PR_Iterations_reusable-confirm-destructive-modal.md
Introduction & Goals
Problem Summary
Before Issue #777, every destructive action in WyzQuests used a one-off confirmation dialog. Destructive flows were inconsistent: some had no confirmation at all (gamification badge/trigger deletes, notification deletes, activity deletes ran immediately), some used a soft "Are you sure?" button, and the four high-risk flows used bespoke type-to-confirm inputs with divergent tokens. The audit (docs/FEATURE_AUDIT_MAY2026.md:127) flagged this as "Inconsistent" and recommended a single reusable <ConfirmDestructiveModal>.
Goals & Non-Goals
Goals:
One controlled component that enforces a typed-confirmation challenge; the destructive button stays disabled until the exact token is typed (
components/shared/confirm-destructive-modal.tsx:19-38,76-83).Server-side re-validation of the token on the four named high-risk endpoints (Issue
#777AC3).Replace every ad-hoc confirm dialog across creator hub, content library, quest editor, learner, forum, reviewer, admin, and agency with the shared component; delete the orphans.
Preserve per-flow requirements (member-reassignment select, bulk item lists, case-insensitive title tokens) without branching the component.
Non-Goals:
Server-side token enforcement for the 13+ non-named destructive flows (explicitly scoped out; follow-up issue —
docs/PR_DESCRIPTION_reusable-confirm-destructive-modal.md:101).Admin re-authentication (W0.5) — unchanged; the modal chains before the existing
ReAuthModalfor admin user delete (app/admin/manage-users/page.tsx:319-341).Soft delete / archive / restore flows — those keep their own non-destructive dialogs (e.g.
components/creator/background-assets/DeleteAssetModal, "Move to Trash").Making the modal fetch or own request lifecycle — the parent owns the request and
isSubmitting(components/shared/confirm-destructive-modal.tsx:28-31).
Glossary
Term | Definition |
Token | The literal text the user must type ( |
|
|
| Optional slot rendered above the token input (item lists, reassign selects, warnings) — |
AC1/AC2/AC3 | Acceptance criteria from Issue |
W0.5 | Pre-existing admin re-auth token flow (5-minute token) — |
W1.4 | API response-standardization envelope ( |
High-Level Architecture
System Diagram
+----------------------------------------------------------------------------------------------------+| Client components (parent owns state + fetch) || || [Creator hub] [Content library] [Quest editor] || MyContent, ContentCard, FolderCard, canvas linear/exploration, || TrashContent, FolderTreeView, form-editor, enrollment, || ContentListRow, FolderWrapper, SettingsDropdown, || archive FolderPermissionsModal, ArchivedNodesModal || ContentLibraryPage || || [Learner] [Forum / reviewer] [Admin / agency] || QuestPlayer, PostDetail, manage-users, || ActiveQuestsList CommentSection, agencies, agency/team, || CommentThread, gamification || GuestInviteModal |+--------------------------------------------------+-------------------------------------------------+ | v+----------------------------------------------------------------------------------------------------+| ConfirmDestructiveModal || components/shared/confirm-destructive-modal.tsx || • controlled open/onOpenChange || • typed-token gate (AC2) || • extraContent slot, isSubmitting |+--------------------------------------------------+-------------------------------------------------+ | | onConfirm(confirmText) v+----------------------------------------------------------------------------------------------------+| Parent fetch (token attached) |+--------------------------------------------------+-------------------------------------------------+ | | | | v v v v+-----------------------+ +-----------------------+ +--------------------+ +-------------------------+| DELETE /api/creator/ | | DELETE /api/creator/ | | POST /api/learner/ | | DELETE /api/admin/ || permanent-delete | | purge-assets | | reset-quest | | delete-user |+-----------------------+ +-----------------------+ +--------------------+ +-------------------------+ | | | | | confirm_text === 'DELETE' | z.literal('DELETE') | z.literal('RESET') | z.literal('DELETE') | (permanentDeleteSchema) | (purgeAssetsSchema) | | + reauthToken v v v v+-----------------------+ +-----------------------+ +--------------------+ +-------------------------+| Supabase Postgres | | Supabase Postgres | | Supabase Postgres | | Supabase Postgres / || quests / adventures | | asset_metadata | | quest_enrollments | | Supabase Auth || | | | | | app_users |+-----------------------+ +-----------------------+ +--------------------+ +-------------------------+ | | | +---------------+---------------+ | | v v +-----------------+ +-------------------+ | audit_logs | | Storage: | | USER_DELETE | | public-assets | +-----------------+ +-------------------+ ^ | (on failure) +-------------------------+ | audit_logs | | PERMANENT_DELETE_FAILED | +-------------------------+ +----------------------------------------------------------------------------------------------------+| Shared helper (Learner flow): || ActiveQuestsList / QuestPlayer ---> resetQuestProgress(questId): Promise<boolean> ---> POST reset || (components/learner/resetQuest.ts:13) |+----------------------------------------------------------------------------------------------------+
Technologies Used
Concern | Technology |
UI Runtime | Next.js 16 App Router, React 19, TypeScript strict ( |
Component Primitives | Shadcn/UI |
Styling | Tailwind v4 utility classes; brand tokens |
Validation | Zod v4 ( |
Data & Storage | Supabase Postgres + service-role client; Supabase Storage |
API Contract |
|
Auth & Authorization | Supabase Auth → internal |
Testing | Playwright e2e ( |
Detailed Design & Implementation
Data Model / Schema
The feature adds no new tables or columns. It relies on pre-existing tables for audit and deletion. Where migrations conflict, the latest wins:
20260414_create_audit_logs_table.sql: Createsaudit_logs, indexes, RLS. Partially superseded by20260526_create_admin_reauth_tokens.sql(re-declares idempotently) and RLS superseded by20260612_update_rls_policies_multi_role.sql.20260526_create_admin_reauth_tokens.sql: Re-declaresaudit_logsidempotently and addsadmin_reauth_tokens. Supersedes20260414for RLS-policy creation mechanism (usesDO $$ ... EXCEPTION); latestaudit_logsshape wins.20260612_update_rls_policies_multi_role.sql:188-196: Replaces"Admins can view audit logs"with a multi-roleEXISTScheck. Latest RLS policy wins over20260414and20260526.20260824_fix_not_null_set_null_user_fks.sql:18-30: DropsNOT NULLfromaudit_logs.user_id. Latest definition; required forON DELETE SET NULLto work when deleting a user.20260504_create_quest_folder.sql:24:quests.quest_folder_id ... ON DELETE SET NULL. Governs folder-delete copy semantics (items move to root, are not deleted).
+-----------------------+ +-----------------------+| app_users | | audit_logs ||-----------------------| |-----------------------|| id (PK) | 1 * | id (PK) || ... |--------------| user_id (FK, nullable)|+-----------------------+ | action | | 1 | entity_type | | | entity_id | | | details | | | created_at | | +-----------------------+ | | 1 +-------------------------------+-----------------------+ | | | | * | * | * v v v+-----------------------+ +-----------------------+ +-----------------------+| quests | | adventures | | asset_metadata ||-----------------------| |-----------------------| |-----------------------|| id (PK) | | id (PK) | | id (PK) || title | | title | | creator_id (FK) || publishing_status | | publishing_status | | file_url || creator_id (FK) | | creator_id (FK) | | file_path || quest_folder_id (FK) | | adventure_folder_id FK| | asset_type |+-----------------------+ +-----------------------+ | file_name | ^ +-----------------------+ | * | | 1+-----------------------+ +-----------------------+| quest_folders | | quest_enrollments ||-----------------------| |-----------------------|| id (PK) | | id (PK) || creator_id (FK) | | quest_id (FK) || parent_folder_id (FK) | | learner_id (FK) <-----+ (app_users.id)+-----------------------+ | progress | +-----------------------+
Composite keys: None on
audit_logs.quest_milestone_earningsuses a composite unique constraint on(enrollment_id, milestone).Foreign keys:
audit_logs.user_idreferencesapp_users(id)(ON DELETE SET NULL).quests.creator_idreferencesapp_users(id).quests.quest_folder_idreferencesquest_folders(id)(ON DELETE SET NULL).adventures.creator_idreferencesapp_users(id).asset_metadata.creator_idreferencesapp_users(id).quest_enrollments.quest_idreferencesquests(id).quest_enrollments.learner_idreferencesapp_users(id).quest_folders.creator_idreferencesapp_users(id).quest_folders.parent_folder_idreferencesquest_folders(id).
ON DELETE CASCADE: Used by child entities under quests and adventures; folder deletion relies on
ON DELETE SET NULLonquests.quest_folder_id.Triggers: None added by this feature.
RLS helper functions: Latest RLS policy on
audit_logsuses multi-roleEXISTScheck acrossapp_userswhereid = auth.uid()and role isADMIN.
Table: audit_logs
Column | Type | Constraints | Notes |
|
| PK, | Generated record identifier |
|
| FK → | Internal |
|
| NOT NULL |
|
|
| Nullable | e.g. |
|
| Nullable | For bulk delete this is omitted ( |
|
| Nullable | e.g. |
|
| Nullable | Not written by this feature |
|
| Nullable | Not written by this feature |
|
|
| Routers also set |
Indexes on audit_logs:
idx_audit_logs_user_id:audit_logs(user_id)idx_audit_logs_action:audit_logs(action)idx_audit_logs_created_at:audit_logs(created_at DESC)idx_audit_logs_entity:audit_logs(entity_type, entity_id)
RLS policies on audit_logs:
"Admins can view audit logs":SELECTusingEXISTS (SELECT 1 FROM app_users WHERE id = auth.uid() AND role = 'ADMIN')(latest multi-role variant in20260612_update_rls_policies_multi_role.sql:188-196)."System can insert audit logs":INSERTWITH CHECK (true)(20260414_create_audit_logs_table.sql:35-37).
API Specification
All four endpoints use the W1.4 envelope (lib/api/response.ts:32-50): success { success: true, message?, data, error: null }, error { success: false, message, data: null, error: { code, message, details? } }.
Method & Path | Auth | Purpose | Body / Query |
|
| Permanently delete quests or adventures from trash | Body: |
|
| Permanently delete assets from storage and database | Body: |
|
| Reset learner quest progress to initial state | Body: |
|
| Permanently delete a user account and profile | Body: |
Logic & Workflows
Component token gate (AC2):
On every open,
useEffectresetsconfirmText = ""(components/shared/confirm-destructive-modal.tsx:70-72).Match computation:
case-insensitivecompares trimmed lowercase; defaultexactcompares raw strings. A non-empty token is required (confirmToken.trim().length > 0) so an untitled draft cannot enable the button on an empty input (:76-83).showErrorevaluates to true only after the user types something that does not match (:82); the input receivesaria-invalidand a red border (:132-143).canConfirmis derived asisMatch && !isSubmitting(:83). The destructive button is set todisabled={!canConfirm}(:158); pressing Enter submits only whencanConfirmis true (:126-131).Dialog closing is blocked while
isSubmittingis true (:92-95); both action buttons disable, and a spinner plussubmittingLabelrender (:146-166).Event propagation for
onClickis stopped on the dialog content (:99) so the modal can be nested cleanly inside canvas or drag-and-drop trees.
Permanent delete workflow (single + bulk):
Parent modal opens requiring token
"DELETE".User types
"DELETE", settingcanConfirmto true, then triggersonConfirm().Parent client component calls
DELETE /api/creator/permanent-deletewith{ content_type, ids, confirm_text: "DELETE" }.Route handler validates payload via Zod; on schema failure, returns
400 Invalid permanent delete request payload.Handler fetches target rows matching the IDs; if missing or empty, returns
404 Content not found or access denied.Handler executes ownership checks or folder-admin checks per item; on failure, returns
403 You do not have permission to delete this content.Handler validates
confirm_text === "DELETE"; if validation fails, insertsaudit_logsrecord withaction = 'PERMANENT_DELETE_FAILED'and returns400 Please type "DELETE" to confirm permanent deletion.Handler executes an atomic database delete:
DELETE ... WHERE id IN (...) RETURNING id. On empty response or database error, returns500 Failed to permanently delete content. Please try again.(mappingPGRST116to"already deleted").For each deleted ID, handler initiates cleanup of Supabase Storage bucket files under
public-assets/<type>/<id>and deletes relatedasset_metadatarows scoped to the item creator's ID. Cleanup failures are logged withconsole.errorand remain non-fatal.Parent component displays a success toast, closes the modal, and refreshes the content list.
Failure / rollback: The database row deletion is atomic within a single statement, but storage cleanup is not wrapped in a database transaction. If storage cleanup fails, the database record is already deleted, leaving orphaned files in storage.
Bulk capacity:
permanentDeleteSchemaplaces no upper bound oncontent_ids. Bulk deletions are processed in a serialfor ... awaitloop.Ownership handling: Storage cleanup uses each item's individual
creator_idrather than the requester's ID, ensuring folder administrators deleting another creator's content do not orphan relatedasset_metadata.
Secure asset purge workflow:
Database rows are deleted first from
asset_metadata, then storage files are deleted only for records confirmed indeletedRows(app/api/creator/(background-assets)/purge-assets/route.ts:74-91).For video assets, cleanup also attempts to remove derived
.jpgthumbnail files (:16-22).Storage cleanup errors log
Storage cleanup failed:without failing the HTTP request.
Admin user deletion workflow (chained confirmation → re-auth → delete):
The UI at
app/admin/manage-users/page.tsx:319-341rendersConfirmDestructiveModalrequiring token"DELETE", followed byReAuthModal.The user completes the typed confirmation dialog, which invokes
handleDeleteConfirmto open the W0.5 re-authentication challenge.Upon successful re-authentication,
handleDeleteReAuthSuccesscallsDELETE /api/admin/delete-user.Execution sequence inside the endpoint:
Authenticate calling session via
authenticateRole("ADMIN")and confirm Supabase session (app/api/admin/delete-user/route.ts:26-28).Parse
{ userId, reauthToken, confirm_text: "DELETE" }using Zod (:32-36).Consume re-authentication token via
consumeReAuthToken(clerkId, reauthToken); on failure, writeREAUTH_TOKEN_INVALIDtoaudit_logsand return403(:41-56).Fetch target user and roles; refuse deletion if the target holds the
ADMINrole (:59-89).Delete user from Supabase Auth admin API; log and continue on error (
:91-99).Delete user records from
agency_members(:102-111).Delete
app_usersdatabase row, which cascades to dependent tables (:113-122); on success, writeUSER_DELETEtoaudit_logs(:124-136).
Failure / rollback: No unified transaction exists across Supabase Auth and the database. If auth deletion succeeds but database deletion fails, the auth identity is lost while database records remain. Migration
20260824_fix_not_null_set_null_user_fks.sqlis required soON DELETE SET NULLonaudit_logs.user_iddoes not throw error23502.
Learner quest progress reset workflow:
components/learner/resetQuest.ts:13-36acts as the shared helper executingPOST /api/learner/reset-questwithconfirm_text: "RESET".The helper returns a
Promise<boolean>. Consumers (QuestPlayerandActiveQuestsList) check the boolean before resetting local client-side progress, reloading the player, or dismissing the modal dialog.The endpoint resets
quest_enrollments.progressto{ percentage: 0, visited_cards: [], last_visited_at: null }(app/api/learner/reset-quest/route.ts:42-53).Milestone XP records are preserved and not re-armed due to the unique constraint on
(enrollment_id, milestone)inquest_milestone_earnings.
Folder delete copy correction:
Because
quests.quest_folder_idis configured withON DELETE SET NULL, deleting a folder re-parents its contents to the root directory rather than deleting them.FolderCardandFolderTreeViewdisplay updated informational copy reflecting this behavior; empty folders delete immediately, while folders containing items or sub-folders trigger the typed-confirmation modal.
Infrastructure & Operations
Dependencies
Upstream:
Supabase service-role client (
lib/supabase): performs database queries, storage mutations, andaudit_logsinserts.Supabase Storage bucket
public-assets: stores media files deleted during permanent removal and purge routines.Supabase Auth admin API: provides user account deletion via
supabase.auth.admin.deleteUser.Admin re-auth subsystem:
admin_reauth_tokenstable andconsumeReAuthTokenhelper (lib/auth/reauth).Validation: Zod v4 schemas in
shared/schemas/contentManagementSchema.tsand route-level validation definitions.
Downstream:
API Contract:
ApiResponseHelperformatting JSON responses into W1.4 envelopes (lib/api/response.ts).UI Components: Creator content managers, folder views, learner dashboards, quest players, and administration tables consuming
<ConfirmDestructiveModal>.
Monitoring & Alerting
There is no dedicated external alerting or Datadog dashboard wired for this feature. Observability relies on structured log output and rows stored in audit_logs.
Symptom | Likely cause | Fix |
| Storage bucket permission issue or missing folder path | Inspect bucket accessibility and check service-role credentials. |
| Database constraint or transient network error during metadata cleanup | Query |
| Storage object deletion failure | Manually inspect |
| Database constraint violation on | Inspect PostgreSQL logs for foreign key violations. |
| Storage object deletion failure during asset purge | Manually remove unreferenced files from |
| Unhandled exception in asset purge route handler | Review route logs and check payload structure. |
| Database update failure on | Confirm record exists and caller has valid enrollment ownership. |
| GoTrue auth admin API failure | Reconcile the user manually in the Supabase Auth dashboard. |
| Database query failure during agency membership cleanup | Query |
| Foreign key failure deleting record from | Ensure migration |
| Unhandled exception in admin user deletion handler | Check server execution stack trace. |
| Uncaught server-side exception formatted by | Inspect error details and stack trace in operational logs. |
Key audit_logs action codes to monitor:
PERMANENT_DELETE_FAILED: indicates repeated payload validation failures or potential token bypass attempts.USER_DELETE: tracks successful account purges by administrators.REAUTH_TOKEN_INVALID: indicates expired, missing, or reused administrative re-authentication tokens.
Deployment Plan
Migration order:
20260414_create_audit_logs_table.sql20260504_create_quest_folder.sql20260526_create_admin_reauth_tokens.sql20260612_update_rls_policies_multi_role.sql20260824_fix_not_null_set_null_user_fks.sql
Migration prerequisite: Migration
20260824_fix_not_null_set_null_user_fks.sqlmust be applied before executing administrative user deletion, or the database delete will fail with error code23502due to non-null constraints onaudit_logs.user_id.Feature flags: None; the component and endpoint logic ship unconditionally.
Backfills: None; historical deletion actions performed prior to this feature are not backfilled into
audit_logs.Rollout note: PR
#780was merged todevelop(ad657492) and integrated intofix/creator-ui(b65e7991) with a global CSS scroll-lock fix inapp/globals.css. Do not re-introduce deleted ad-hoc modals in future rebases.
Testing & Quality Assurance
Test Strategy
E2E — Creator Permanent Delete (
tests/e2e/creator/permanent-delete.spec.ts:42-83, QA-024): Navigates to Trash, opens confirmation modal, types confirmation token, clicks "Delete Forever", and asserts toast confirmation and item removal.E2E — Archive, Restore, and Delete (
tests/e2e/creator/archive-restore-delete.spec.ts:147-184, QA-024): Tests full lifecycle flow from archival to permanent deletion using placeholder matching.E2E — Secure Asset Purge (
tests/e2e/creator/secure-asset-delete.spec.ts:96-167, QA-045, QA-046): Verifies that supplying the correct token deletes the asset, while typing an invalid token leaves the button disabled and displays a mismatch hint.E2E — Admin User Delete (
tests/e2e/admin/secure-user-delete.spec.ts:7-93, QA-073.5): Asserts user deletion is blocked when an incorrect administrator password is supplied during re-authentication.Static verification: Validated with
tsc --noEmitmaintaining baseline type-check counts; ESLint passed with 0 errors across modified files; codebase confirmed zero residual imports of the 10 deleted ad-hoc confirmation modals.
Known Limitations
Unit test gaps: No isolated unit or component tests exist for
<ConfirmDestructiveModal>or its Zod validation schemas (permanentDeleteSchema,purgeAssetsSchema). Edge cases like empty token validation andmatchModehandling rely entirely on E2E test runs.Stale E2E test selectors: Existing tests in
tests/e2e/creator/permanent-delete.spec.ts:69target#confirm-delete, andtests/e2e/creator/secure-asset-delete.spec.ts:107,145target#confirm-purge-asset. The unified component hardcodesid="confirm-destructive"(components/shared/confirm-destructive-modal.tsx:122), causing older selectors to mismatch.Hardcoded input identifier: The modal input fixed attribute
id="confirm-destructive"causes duplicate DOM IDs and ambiguous label associations if multiple modal instances mount concurrently.Partial server-side enforcement (AC3): Server-side token re-validation is implemented only on the four high-risk endpoints (
permanent-delete,purge-assets,reset-quest,delete-user). Approximately 13 other destructive operations across the platform remain client-gated only.Schema mismatch on
permanentDeleteSchema: Inshared/schemas/contentManagementSchema.ts:61,confirm_textis defined asz.string().min(1)rather thanz.literal("DELETE"). Exact matching is enforced imperatively in the route handler (permanent-delete/route.ts:134).Unused field in purge route:
purge-assetsvalidatesconfirm_textviapurgeAssetsSchema, but the route handler destructures onlyasset_idandasset_ids, relying entirely on schema validation.Unbounded bulk deletion arrays: Schemas do not enforce maximum array limits on
content_idsorasset_ids, allowing large payloads that execute unbounded serial cleanup operations.Non-transactional storage cleanup: Database row deletions and storage object removals are not wrapped in a distributed transaction; failures during storage cleanup can leave orphaned files in
public-assets.Missing rate limits: No dedicated rate-limiting or re-authentication rules protect
permanent-delete,purge-assets, orreset-quest.Inconsistent busy-state UI: Several parent components (
FolderCard,FolderTreeView,SettingsDropdown,ArchivedNodesModal) do not yet passisSubmittingto<ConfirmDestructiveModal>, causing the dialog to dismiss without inline loading indicators.Accessibility: The input element relies on
DialogDescriptionandaria-invalid, but does not link error messages viaaria-describedby.Product acceptance documentation: Acceptance verification from product management for Issue
#777remains undocumented in the repository.
Maintenance & Support
Troubleshooting
Symptom | Likely cause | Fix |
| Entered text does not match | Verify expected token string passed to |
| Request payload sent | Verify client caller sends the literal |
| Zod schema parse error: malformed UUID, missing IDs, empty | Inspect |
| Payload failed | Ensure payload adheres to |
| Payload omitted both singular and array ID properties | Update request payload to provide at least one valid target ID ( |
| Caller lacks creator ownership and is not an authorized folder manager | Verify |
| Target record missing during deletion query ( | Concurrently deleted or stale client state; refresh UI content list ( |
| Initial ID lookup returned zero rows | Item is already deleted or access is restricted by RLS policies ( |
| Unhandled database error occurred during deletion | Review PostgreSQL logs for foreign key constraint errors ( |
| Database error deleting row from | Inspect database error; note that storage deletion is intentionally skipped if DB delete fails ( |
| No record found in | Confirm learner is actively enrolled in target quest ( |
| Database update failed on | Check |
| Schema parse failure on delete-user: invalid UUIDs or | Review |
| Admin re-auth token missing, expired (> 5 min), already consumed, or generated by another user | Re-open re-authentication modal to generate a new token ( |
| Target account holds the | Deletion of admin users is prohibited; select a non-admin account ( |
| Database deletion error on | Verify migration |
| Auth user deletion failed, but DB delete proceeded by design | Manually remove residual auth account from Supabase Auth dashboard ( |
| Database row deleted, but Storage object removal failed | Inspect |
| Unhandled server runtime exception | Inspect attached stack trace in operational error logs ( |
E2E test fails to locate confirmation input | Test script targets old modal element ID ( | Update test selector to locate |
Changelog
August 21, 2026 —
merge: integrate reusable confirm-destructive modal, fix sticky layout break at root— accepted PR#780's overhaul; fixed Radix scroll-lock at root viaapp/globals.cssbody[data-scroll-locked]instead of disabling modal mode onfix/creator-ui(b65e7991).
August 20, 2026 —
feat: standardize destructive actions on shared ConfirmDestructiveModal— addscomponents/shared/confirm-destructive-modal.tsx; server token double-checks on permanent-delete / purge-assets / reset-quest / delete-user;purgeAssetsSchema;ApiResponseHelperenvelope for purge;resetQuestProgresshelper; deletes 9 ad-hoc modals; 44 files changed, +988/−1324 onfeat/reusable-confirm-destructive-modal(13373adb).August 20, 2026 —
feat: adopt ConfirmDestructiveModal in learner groups + content library—DeleteGroupDialogrebuilt as thin wrapper withextraContentreassign select;GroupMembersTable,GroupEnrollmentTable, learner-groups unassign,AgencyBadgeRequirements,ContentListRow,ContentLibraryPage,FolderWrapper; deletesFolderDeleteConfirmationModal(10th orphan); 9 files, +275/−378 onfeat/reusable-confirm-destructive-modal(1210c258).August 20, 2026 —
fix: address round-1 review findings on confirm-destructive-modal—resetQuestProgressreturns boolean and callers gate client state (blocker); non-empty token guard; folder-delete copy correction + instant delete for empty folders; busy-state modals kept open; restored PostDetail copy feedback onfeat/reusable-confirm-destructive-modal(07530207).August 20, 2026 —
Merge pull request #780 from wyzlab/feat/reusable-confirm-destructive-modalondevelop(ad657492).
Document version:
1.0 - Draft, Initial Technical Guide, 07/29/2026
1.1 - Published, Reusable Confirm-Destructive Modal Technical Guide, 07/29/2026