feat(WP-0009): IHF GAAF Compliance Foundation — type registries, extension manifests, architectural contracts
Some checks failed
Test / test (push) Has been cancelled

Implements IHUB-WP-0009: closes four GAAF-2026 gaps before domain hub work begins.
- TypeRegistry helper + controllers/views (hub_kind, hub_capability_manifest)
- HubCapabilityManifest entity with validation and registry linkage
- ARCHITECTURE-LAYERS.md + CI-enforced boundary contracts
- Alembic migration 1743724800, fitness tests (Test/Architecture/)
- GAAF spec, Operational Architecture spec, domain hub extension guide
- Updates to CLAUDE.md, SCOPE.md, Schema.sql, Routes, FrontController, Types

state_hub_sync: pending (tunnel was STALE at completion time; run fix-consistency)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-31 21:17:39 +00:00
parent 1a7732d7da
commit b5d73aa18b
47 changed files with 4855 additions and 104 deletions

View File

@@ -0,0 +1,151 @@
module Web.View.TypeRegistries.AnnotationCategories where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
data AnnotationCategoriesView = AnnotationCategoriesView { entries :: ![AnnotationCategoryRegistry], hubs :: ![Hub] }
data ShowAnnotationCategoryView = ShowAnnotationCategoryView { entry :: !AnnotationCategoryRegistry, mOwner :: !(Maybe Hub) }
data NewAnnotationCategoryView = NewAnnotationCategoryView { entry :: !AnnotationCategoryRegistry, hubs :: ![Hub] }
data EditAnnotationCategoryView = EditAnnotationCategoryView { entry :: !AnnotationCategoryRegistry, hubs :: ![Hub] }
hubName :: [Hub] -> Maybe (Id Hub) -> Text
hubName _ Nothing = "Framework"
hubName hubs (Just i) = maybe "Unknown" (.name) (find (\h -> h.id == i) hubs)
statusBadge :: Text -> Html
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
statusBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">deprecated</span>|]
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
instance View AnnotationCategoriesView where
html AnnotationCategoriesView { .. } = [hsx|
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-semibold">Annotation Category Registry</h1>
<p class="text-sm text-gray-500 mt-1">Framework and domain-owned annotation categories</p>
</div>
<a href={NewAnnotationCategoryAction}
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
Register Category
</a>
</div>
<div class="flex gap-4 mb-4 text-sm">
<a href={WidgetTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Widget Types</a>
<a href={EventTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Event Types</a>
<a href={AnnotationCategoryRegistryAction} class="text-indigo-600 font-medium border-b-2 border-indigo-600 pb-1">Annotation Categories</a>
<a href={PolicyScopeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Policy Scopes</a>
</div>
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="text-left px-4 py-3 font-medium text-gray-700">Name</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Label</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Owner</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody>
{forEach entries (renderRow hubs)}
</tbody>
</table>
</div>
|]
renderRow :: [Hub] -> AnnotationCategoryRegistry -> Html
renderRow hubs e = [hsx|
<tr class="border-b border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 font-mono text-xs">{e.name}</td>
<td class="px-4 py-3">{e.label}</td>
<td class="px-4 py-3 text-gray-500">{hubName hubs e.ownerHubId}</td>
<td class="px-4 py-3">{statusBadge e.status}</td>
<td class="px-4 py-3 text-right text-xs">
<a href={ShowAnnotationCategoryAction { annotationCategoryRegistryId = e.id }} class="text-gray-500 hover:text-gray-700 mr-2">View</a>
<a href={EditAnnotationCategoryAction { annotationCategoryRegistryId = e.id }} class="text-gray-500 hover:text-gray-700">Edit</a>
</td>
</tr>
|]
instance View ShowAnnotationCategoryView where
html ShowAnnotationCategoryView { .. } = [hsx|
<div class="mb-4">
<a href={AnnotationCategoryRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Annotation Categories</a>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
<div class="flex items-center justify-between mb-4">
<h1 class="text-xl font-semibold">{entry.name}</h1>
{statusBadge entry.status}
</div>
<dl class="space-y-3 text-sm">
<div><dt class="text-gray-500">Label</dt><dd class="font-medium">{entry.label}</dd></div>
<div><dt class="text-gray-500">Description</dt><dd>{fromMaybe "" entry.description}</dd></div>
<div><dt class="text-gray-500">Owner</dt><dd>{maybe "Framework (cross-domain)" (.name) mOwner}</dd></div>
<div><dt class="text-gray-500">Replaced by</dt><dd>{fromMaybe "" entry.deprecatedInFavourOf}</dd></div>
</dl>
<div class="mt-6">
<a href={EditAnnotationCategoryAction { annotationCategoryRegistryId = entry.id }}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">Edit</a>
</div>
</div>
|]
typeForm :: AnnotationCategoryRegistry -> [Hub] -> Bool -> Html
typeForm entry hubs isNew = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
<div class="space-y-4">
{if isNew then [hsx|
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(permanent, lowercase-underscored)</span></label>
{(textField #name) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
|] else [hsx|
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(immutable)</span></label>
<p class="font-mono text-sm bg-gray-50 border border-gray-200 rounded px-3 py-2">{entry.name}</p>
</div>
|]}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Label</label>
{(textField #label) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description <span class="text-gray-400 text-xs">(optional)</span></label>
{(textField #description) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Owner Hub <span class="text-gray-400 text-xs">(blank = framework-level)</span></label>
{selectField #ownerHubId hubs}
</div>
</div>
<div class="mt-6">
<button type="submit" class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
{if isNew then ("Register" :: Text) else "Save"}
</button>
</div>
</div>
|]
instance View NewAnnotationCategoryView where
html NewAnnotationCategoryView { .. } = [hsx|
<div class="mb-4">
<a href={AnnotationCategoryRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Annotation Categories</a>
</div>
<h1 class="text-xl font-semibold mb-6">Register Annotation Category</h1>
<form method="POST" action={CreateAnnotationCategoryAction}>
{typeForm entry hubs True}
</form>
|]
instance View EditAnnotationCategoryView where
html EditAnnotationCategoryView { .. } = [hsx|
<div class="mb-4">
<a href={AnnotationCategoryRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Annotation Categories</a>
</div>
<h1 class="text-xl font-semibold mb-6">Edit Annotation Category</h1>
<form method="POST" action={UpdateAnnotationCategoryAction { annotationCategoryRegistryId = entry.id }}>
{typeForm entry hubs False}
</form>
|]

View File

@@ -0,0 +1,151 @@
module Web.View.TypeRegistries.EventTypes where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
data EventTypesView = EventTypesView { entries :: ![EventTypeRegistry], hubs :: ![Hub] }
data ShowEventTypeView = ShowEventTypeView { entry :: !EventTypeRegistry, mOwner :: !(Maybe Hub) }
data NewEventTypeView = NewEventTypeView { entry :: !EventTypeRegistry, hubs :: ![Hub] }
data EditEventTypeView = EditEventTypeView { entry :: !EventTypeRegistry, hubs :: ![Hub] }
hubName :: [Hub] -> Maybe (Id Hub) -> Text
hubName _ Nothing = "Framework"
hubName hubs (Just i) = maybe "Unknown" (.name) (find (\h -> h.id == i) hubs)
statusBadge :: Text -> Html
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
statusBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">deprecated</span>|]
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
instance View EventTypesView where
html EventTypesView { .. } = [hsx|
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-semibold">Event Type Registry</h1>
<p class="text-sm text-gray-500 mt-1">Framework and domain-owned interaction event types</p>
</div>
<a href={NewEventTypeAction}
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
Register Type
</a>
</div>
<div class="flex gap-4 mb-4 text-sm">
<a href={WidgetTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Widget Types</a>
<a href={EventTypeRegistryAction} class="text-indigo-600 font-medium border-b-2 border-indigo-600 pb-1">Event Types</a>
<a href={AnnotationCategoryRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Annotation Categories</a>
<a href={PolicyScopeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Policy Scopes</a>
</div>
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="text-left px-4 py-3 font-medium text-gray-700">Name</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Label</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Owner</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody>
{forEach entries (renderRow hubs)}
</tbody>
</table>
</div>
|]
renderRow :: [Hub] -> EventTypeRegistry -> Html
renderRow hubs e = [hsx|
<tr class="border-b border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 font-mono text-xs">{e.name}</td>
<td class="px-4 py-3">{e.label}</td>
<td class="px-4 py-3 text-gray-500">{hubName hubs e.ownerHubId}</td>
<td class="px-4 py-3">{statusBadge e.status}</td>
<td class="px-4 py-3 text-right text-xs">
<a href={ShowEventTypeAction { eventTypeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700 mr-2">View</a>
<a href={EditEventTypeAction { eventTypeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700">Edit</a>
</td>
</tr>
|]
instance View ShowEventTypeView where
html ShowEventTypeView { .. } = [hsx|
<div class="mb-4">
<a href={EventTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Event Types</a>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
<div class="flex items-center justify-between mb-4">
<h1 class="text-xl font-semibold">{entry.name}</h1>
{statusBadge entry.status}
</div>
<dl class="space-y-3 text-sm">
<div><dt class="text-gray-500">Label</dt><dd class="font-medium">{entry.label}</dd></div>
<div><dt class="text-gray-500">Description</dt><dd>{fromMaybe "" entry.description}</dd></div>
<div><dt class="text-gray-500">Owner</dt><dd>{maybe "Framework (cross-domain)" (.name) mOwner}</dd></div>
<div><dt class="text-gray-500">Replaced by</dt><dd>{fromMaybe "" entry.deprecatedInFavourOf}</dd></div>
</dl>
<div class="mt-6 flex gap-2">
<a href={EditEventTypeAction { eventTypeRegistryId = entry.id }}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">Edit</a>
</div>
</div>
|]
typeForm :: EventTypeRegistry -> [Hub] -> Bool -> Html
typeForm entry hubs isNew = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
<div class="space-y-4">
{if isNew then [hsx|
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(permanent, lowercase-underscored)</span></label>
{(textField #name) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
|] else [hsx|
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(immutable)</span></label>
<p class="font-mono text-sm bg-gray-50 border border-gray-200 rounded px-3 py-2">{entry.name}</p>
</div>
|]}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Label</label>
{(textField #label) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description <span class="text-gray-400 text-xs">(optional)</span></label>
{(textField #description) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Owner Hub <span class="text-gray-400 text-xs">(blank = framework-level)</span></label>
{selectField #ownerHubId hubs}
</div>
</div>
<div class="mt-6">
<button type="submit" class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
{if isNew then ("Register" :: Text) else "Save"}
</button>
</div>
</div>
|]
instance View NewEventTypeView where
html NewEventTypeView { .. } = [hsx|
<div class="mb-4">
<a href={EventTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Event Types</a>
</div>
<h1 class="text-xl font-semibold mb-6">Register Event Type</h1>
<form method="POST" action={CreateEventTypeAction}>
{typeForm entry hubs True}
</form>
|]
instance View EditEventTypeView where
html EditEventTypeView { .. } = [hsx|
<div class="mb-4">
<a href={EventTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Event Types</a>
</div>
<h1 class="text-xl font-semibold mb-6">Edit Event Type</h1>
<form method="POST" action={UpdateEventTypeAction { eventTypeRegistryId = entry.id }}>
{typeForm entry hubs False}
</form>
|]

View File

@@ -0,0 +1,151 @@
module Web.View.TypeRegistries.PolicyScopes where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
data PolicyScopesView = PolicyScopesView { entries :: ![PolicyScopeRegistry], hubs :: ![Hub] }
data ShowPolicyScopeView = ShowPolicyScopeView { entry :: !PolicyScopeRegistry, mOwner :: !(Maybe Hub) }
data NewPolicyScopeView = NewPolicyScopeView { entry :: !PolicyScopeRegistry, hubs :: ![Hub] }
data EditPolicyScopeView = EditPolicyScopeView { entry :: !PolicyScopeRegistry, hubs :: ![Hub] }
hubName :: [Hub] -> Maybe (Id Hub) -> Text
hubName _ Nothing = "Framework"
hubName hubs (Just i) = maybe "Unknown" (.name) (find (\h -> h.id == i) hubs)
statusBadge :: Text -> Html
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
statusBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">deprecated</span>|]
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
instance View PolicyScopesView where
html PolicyScopesView { .. } = [hsx|
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-semibold">Policy Scope Registry</h1>
<p class="text-sm text-gray-500 mt-1">Framework and domain-owned policy scopes</p>
</div>
<a href={NewPolicyScopeAction}
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
Register Scope
</a>
</div>
<div class="flex gap-4 mb-4 text-sm">
<a href={WidgetTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Widget Types</a>
<a href={EventTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Event Types</a>
<a href={AnnotationCategoryRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Annotation Categories</a>
<a href={PolicyScopeRegistryAction} class="text-indigo-600 font-medium border-b-2 border-indigo-600 pb-1">Policy Scopes</a>
</div>
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="text-left px-4 py-3 font-medium text-gray-700">Name</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Label</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Owner</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody>
{forEach entries (renderRow hubs)}
</tbody>
</table>
</div>
|]
renderRow :: [Hub] -> PolicyScopeRegistry -> Html
renderRow hubs e = [hsx|
<tr class="border-b border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 font-mono text-xs">{e.name}</td>
<td class="px-4 py-3">{e.label}</td>
<td class="px-4 py-3 text-gray-500">{hubName hubs e.ownerHubId}</td>
<td class="px-4 py-3">{statusBadge e.status}</td>
<td class="px-4 py-3 text-right text-xs">
<a href={ShowPolicyScopeAction { policyScopeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700 mr-2">View</a>
<a href={EditPolicyScopeAction { policyScopeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700">Edit</a>
</td>
</tr>
|]
instance View ShowPolicyScopeView where
html ShowPolicyScopeView { .. } = [hsx|
<div class="mb-4">
<a href={PolicyScopeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Policy Scopes</a>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
<div class="flex items-center justify-between mb-4">
<h1 class="text-xl font-semibold">{entry.name}</h1>
{statusBadge entry.status}
</div>
<dl class="space-y-3 text-sm">
<div><dt class="text-gray-500">Label</dt><dd class="font-medium">{entry.label}</dd></div>
<div><dt class="text-gray-500">Description</dt><dd>{fromMaybe "" entry.description}</dd></div>
<div><dt class="text-gray-500">Owner</dt><dd>{maybe "Framework (cross-domain)" (.name) mOwner}</dd></div>
<div><dt class="text-gray-500">Replaced by</dt><dd>{fromMaybe "" entry.deprecatedInFavourOf}</dd></div>
</dl>
<div class="mt-6">
<a href={EditPolicyScopeAction { policyScopeRegistryId = entry.id }}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">Edit</a>
</div>
</div>
|]
typeForm :: PolicyScopeRegistry -> [Hub] -> Bool -> Html
typeForm entry hubs isNew = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
<div class="space-y-4">
{if isNew then [hsx|
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(permanent, lowercase-hyphenated)</span></label>
{(textField #name) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
|] else [hsx|
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(immutable)</span></label>
<p class="font-mono text-sm bg-gray-50 border border-gray-200 rounded px-3 py-2">{entry.name}</p>
</div>
|]}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Label</label>
{(textField #label) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description <span class="text-gray-400 text-xs">(optional)</span></label>
{(textField #description) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Owner Hub <span class="text-gray-400 text-xs">(blank = framework-level)</span></label>
{selectField #ownerHubId hubs}
</div>
</div>
<div class="mt-6">
<button type="submit" class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
{if isNew then ("Register" :: Text) else "Save"}
</button>
</div>
</div>
|]
instance View NewPolicyScopeView where
html NewPolicyScopeView { .. } = [hsx|
<div class="mb-4">
<a href={PolicyScopeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Policy Scopes</a>
</div>
<h1 class="text-xl font-semibold mb-6">Register Policy Scope</h1>
<form method="POST" action={CreatePolicyScopeAction}>
{typeForm entry hubs True}
</form>
|]
instance View EditPolicyScopeView where
html EditPolicyScopeView { .. } = [hsx|
<div class="mb-4">
<a href={PolicyScopeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Policy Scopes</a>
</div>
<h1 class="text-xl font-semibold mb-6">Edit Policy Scope</h1>
<form method="POST" action={UpdatePolicyScopeAction { policyScopeRegistryId = entry.id }}>
{typeForm entry hubs False}
</form>
|]

View File

@@ -0,0 +1,159 @@
module Web.View.TypeRegistries.WidgetTypes where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
data WidgetTypesView = WidgetTypesView { entries :: ![WidgetTypeRegistry], hubs :: ![Hub] }
data ShowWidgetTypeView = ShowWidgetTypeView { entry :: !WidgetTypeRegistry, mOwner :: !(Maybe Hub) }
data NewWidgetTypeView = NewWidgetTypeView { entry :: !WidgetTypeRegistry, hubs :: ![Hub] }
data EditWidgetTypeView = EditWidgetTypeView { entry :: !WidgetTypeRegistry, hubs :: ![Hub] }
hubName :: [Hub] -> Maybe (Id Hub) -> Text
hubName _ Nothing = "Framework"
hubName hubs (Just i) = maybe "Unknown" (.name) (find (\h -> h.id == i) hubs)
statusBadge :: Text -> Html
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
statusBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">deprecated</span>|]
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
instance View WidgetTypesView where
html WidgetTypesView { .. } = [hsx|
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-semibold">Widget Type Registry</h1>
<p class="text-sm text-gray-500 mt-1">Framework and domain-owned widget types</p>
</div>
<a href={NewWidgetTypeAction}
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
Register Type
</a>
</div>
<div class="flex gap-4 mb-4 text-sm">
<a href={WidgetTypeRegistryAction} class="text-indigo-600 font-medium border-b-2 border-indigo-600 pb-1">Widget Types</a>
<a href={EventTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Event Types</a>
<a href={AnnotationCategoryRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Annotation Categories</a>
<a href={PolicyScopeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Policy Scopes</a>
</div>
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="text-left px-4 py-3 font-medium text-gray-700">Name</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Label</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Owner</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Status</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody>
{forEach entries (renderRow hubs)}
</tbody>
</table>
</div>
|]
renderRow :: [Hub] -> WidgetTypeRegistry -> Html
renderRow hubs e = [hsx|
<tr class="border-b border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 font-mono text-xs">{e.name}</td>
<td class="px-4 py-3">{e.label}</td>
<td class="px-4 py-3 text-gray-500">{hubName hubs e.ownerHubId}</td>
<td class="px-4 py-3">{statusBadge e.status}</td>
<td class="px-4 py-3 text-right text-xs">
<a href={ShowWidgetTypeAction { widgetTypeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700 mr-2">View</a>
<a href={EditWidgetTypeAction { widgetTypeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700">Edit</a>
</td>
</tr>
|]
instance View ShowWidgetTypeView where
html ShowWidgetTypeView { .. } = [hsx|
<div class="mb-4">
<a href={WidgetTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Widget Types</a>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
<div class="flex items-center justify-between mb-4">
<h1 class="text-xl font-semibold">{entry.name}</h1>
{statusBadge entry.status}
</div>
<dl class="space-y-3 text-sm">
<div><dt class="text-gray-500">Label</dt><dd class="font-medium">{entry.label}</dd></div>
<div><dt class="text-gray-500">Description</dt><dd>{fromMaybe "" entry.description}</dd></div>
<div><dt class="text-gray-500">Owner</dt><dd>{maybe "Framework (cross-domain)" (.name) mOwner}</dd></div>
<div><dt class="text-gray-500">Replaced by</dt><dd>{fromMaybe "" entry.deprecatedInFavourOf}</dd></div>
</dl>
<div class="mt-6 flex gap-2">
<a href={EditWidgetTypeAction { widgetTypeRegistryId = entry.id }}
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">Edit</a>
{if entry.status == "active"
then [hsx|
<form method="POST" action={DeprecateWidgetTypeAction { widgetTypeRegistryId = entry.id }}>
<input type="hidden" name="deprecated_in_favour_of" value="" placeholder="replacement name" />
<button type="submit" class="text-sm border border-amber-300 text-amber-700 px-3 py-1.5 rounded hover:bg-amber-50">Deprecate</button>
</form>
|]
else mempty}
</div>
</div>
|]
typeForm :: WidgetTypeRegistry -> [Hub] -> Bool -> Html
typeForm entry hubs isNew = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
<div class="space-y-4">
{if isNew then [hsx|
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(permanent identifier, lowercase-hyphenated)</span></label>
{(textField #name) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
|] else [hsx|
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(immutable)</span></label>
<p class="font-mono text-sm bg-gray-50 border border-gray-200 rounded px-3 py-2">{entry.name}</p>
</div>
|]}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Label</label>
{(textField #label) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description <span class="text-gray-400 text-xs">(optional)</span></label>
{(textField #description) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Owner Hub <span class="text-gray-400 text-xs">(leave blank for framework-level)</span></label>
{selectField #ownerHubId hubs}
</div>
</div>
<div class="mt-6">
<button type="submit" class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
{if isNew then ("Register" :: Text) else "Save"}
</button>
</div>
</div>
|]
instance View NewWidgetTypeView where
html NewWidgetTypeView { .. } = [hsx|
<div class="mb-4">
<a href={WidgetTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Widget Types</a>
</div>
<h1 class="text-xl font-semibold mb-6">Register Widget Type</h1>
<form method="POST" action={CreateWidgetTypeAction}>
{typeForm entry hubs True}
</form>
|]
instance View EditWidgetTypeView where
html EditWidgetTypeView { .. } = [hsx|
<div class="mb-4">
<a href={WidgetTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700"> Widget Types</a>
</div>
<h1 class="text-xl font-semibold mb-6">Edit Widget Type</h1>
<form method="POST" action={UpdateWidgetTypeAction { widgetTypeRegistryId = entry.id }}>
{typeForm entry hubs False}
</form>
|]