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

@@ -8,6 +8,7 @@ import IHP.ViewPrelude
data NewView = NewView
{ widget :: !Widget
, annotation :: !Annotation
, categories :: ![AnnotationCategoryRegistry]
}
instance View NewView where
@@ -21,28 +22,20 @@ instance View NewView where
<span>New</span>
</div>
<h1 class="text-2xl font-semibold mb-6">Add Annotation</h1>
{renderForm annotation widget.id}
{renderForm annotation widget.id categories}
</div>
|]
renderForm :: Annotation -> Id Widget -> Html
renderForm annotation widgetId = formFor annotation [hsx|
renderForm :: Annotation -> Id Widget -> [AnnotationCategoryRegistry] -> Html
renderForm annotation widgetId categories = formFor annotation [hsx|
{(textareaField #body) { fieldLabel = "Comment" }}
{selectField #category categoryOptions}
{selectField #category (categoryOptions categories)}
{selectField #severity severityOptions}
{submitButton}
|]
categoryOptions :: [(Text, Text)]
categoryOptions =
[ ("Friction", "friction")
, ("Defect", "defect")
, ("Wish", "wish")
, ("Policy Concern", "policy_concern")
, ("Documentation Gap", "doc_gap")
, ("Trust", "trust")
, ("Other", "other")
]
categoryOptions :: [AnnotationCategoryRegistry] -> [(Text, Text)]
categoryOptions = map (\r -> (r.label, r.name))
severityOptions :: [(Text, Text)]
severityOptions =

View File

@@ -25,6 +25,7 @@ instance View ShowView where
<span class={adapterStatusBadge contract.status <> " text-xs px-2 py-0.5 rounded font-medium"}>
{contract.status}
</span>
{maturityBadge contract.maturity}
</div>
{forEach (contractDescription contract) (\d -> [hsx|
@@ -57,3 +58,10 @@ contractDescription :: EnvelopeEmissionContract -> [Text]
contractDescription c = case c.description of
Just d -> [d]
Nothing -> []
maturityBadge :: Text -> Html
maturityBadge "stable" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800 font-medium">Stable</span>|]
maturityBadge "beta" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800 font-medium">Beta</span>|]
maturityBadge "experimental" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800 font-medium">Experimental</span>|]
maturityBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500 font-medium">Deprecated</span>|]
maturityBadge m = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600 font-medium">{m}</span>|]

View File

@@ -0,0 +1,130 @@
module Web.View.HubCapabilityManifests.Edit where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Data.Aeson (Value(..), encode, decode)
import qualified Data.Vector as V
import qualified Data.ByteString.Lazy.Char8 as BL
data EditView = EditView
{ manifest :: !HubCapabilityManifest
, hub :: !Hub
, widgetTypeEntries :: ![WidgetTypeRegistry]
, eventTypeEntries :: ![EventTypeRegistry]
, categoryEntries :: ![AnnotationCategoryRegistry]
, policyScopeEntries :: ![PolicyScopeRegistry]
}
instance View EditView where
html EditView { .. } = [hsx|
<div class="mb-4">
<a href={ShowHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }}
class="text-sm text-gray-500 hover:text-gray-700">
{hub.name} Manifest
</a>
</div>
<h1 class="text-xl font-semibold mb-2">Edit Capability Manifest {hub.name}</h1>
<p class="text-sm text-gray-500 mb-6">
Declare the type names this hub owns. After saving, activate the manifest to register them.
</p>
{if manifest.status /= "draft"
then [hsx|
<div class="mb-6 bg-amber-50 border border-amber-200 rounded p-4 text-sm text-amber-800">
This manifest is <strong>{manifest.status}</strong> and is read-only.
Retire it first to create a new draft amendment.
</div>
|]
else [hsx||]}
<form method="POST" action={UpdateHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }}>
<div class="space-y-6 max-w-2xl">
<div class="bg-white rounded-lg border border-gray-200 p-5 space-y-4">
<h2 class="text-sm font-semibold text-gray-700">Manifest Details</h2>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Capability Description</label>
{(textareaField #capabilityDescription) { 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">Contact</label>
{(textField #contact) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
</div>
{typeArraySection "Declared Widget Types" "declaredWidgetTypes" manifest.declaredWidgetTypes widgetTypeEntries}
{typeArraySection "Declared Event Types" "declaredEventTypes" manifest.declaredEventTypes eventTypeEntries}
{typeArraySection2 "Declared Annotation Categories" "declaredAnnotationCategories" manifest.declaredAnnotationCategories categoryEntries}
{typeArraySection3 "Declared Policy Scopes" "declaredPolicyScopes" manifest.declaredPolicyScopes policyScopeEntries}
<div class="flex gap-3">
<button type="submit"
class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700"
{if manifest.status /= "draft" then ("disabled" :: Text) else ""}>
Save
</button>
{if manifest.status == "draft" then [hsx|
<a href={ActivateManifestAction { hubCapabilityManifestId = manifest.id }}
class="text-sm bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
Save &amp; Activate
</a>
|] else [hsx||]}
</div>
</div>
</form>
|]
-- | Render a JSON array text area with available registry options shown below.
typeArraySection :: Text -> Text -> Value -> [WidgetTypeRegistry] -> Html
typeArraySection title fieldName val entries = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-5">
<h2 class="text-sm font-semibold text-gray-700 mb-1">{title}</h2>
<p class="text-xs text-gray-500 mb-2">
JSON array of type names to declare ownership of.
Names that don't yet exist in the registry will be created on activation.
</p>
<textarea name={fieldName}
class="w-full border border-gray-300 rounded px-3 py-2 text-sm font-mono"
rows="3">{valueText val}</textarea>
<p class="text-xs text-gray-400 mt-1">
Registered: {intercalate ", " (map (.name) entries)}
</p>
</div>
|]
typeArraySection2 :: Text -> Text -> Value -> [AnnotationCategoryRegistry] -> Html
typeArraySection2 title fieldName val entries = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-5">
<h2 class="text-sm font-semibold text-gray-700 mb-1">{title}</h2>
<p class="text-xs text-gray-500 mb-2">JSON array of annotation category names.</p>
<textarea name={fieldName}
class="w-full border border-gray-300 rounded px-3 py-2 text-sm font-mono"
rows="3">{valueText val}</textarea>
<p class="text-xs text-gray-400 mt-1">
Registered: {intercalate ", " (map (.name) entries)}
</p>
</div>
|]
typeArraySection3 :: Text -> Text -> Value -> [PolicyScopeRegistry] -> Html
typeArraySection3 title fieldName val entries = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-5">
<h2 class="text-sm font-semibold text-gray-700 mb-1">{title}</h2>
<p class="text-xs text-gray-500 mb-2">JSON array of policy scope names.</p>
<textarea name={fieldName}
class="w-full border border-gray-300 rounded px-3 py-2 text-sm font-mono"
rows="3">{valueText val}</textarea>
<p class="text-xs text-gray-400 mt-1">
Registered: {intercalate ", " (map (.name) entries)}
</p>
</div>
|]
valueText :: Value -> Text
valueText v = cs (BL.unpack (encode v))
intercalate :: Text -> [Text] -> Text
intercalate _ [] = ""
intercalate _ [x] = x
intercalate sep (x:xs) = x <> sep <> intercalate sep xs

View File

@@ -0,0 +1,77 @@
module Web.View.HubCapabilityManifests.Index where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Data.Aeson (Value(..))
import qualified Data.Vector as V
data IndexView = IndexView
{ manifests :: ![HubCapabilityManifest]
, hubs :: ![Hub]
}
instance View IndexView where
html IndexView { .. } = [hsx|
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-semibold">Hub Capability Manifests</h1>
<p class="text-sm text-gray-500 mt-1">Extension registrations for domain and shared hubs</p>
</div>
<a href={NewHubCapabilityManifestAction}
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
New Manifest
</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">Hub</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Status</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Widget Types</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Event Types</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Categories</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Scopes</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Activated</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody>
{forEach manifests (renderRow hubs)}
</tbody>
</table>
</div>
|]
renderRow :: [Hub] -> HubCapabilityManifest -> Html
renderRow hubs m = [hsx|
<tr class="border-b border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 font-medium">{hubName hubs m.hubId}</td>
<td class="px-4 py-3">{statusBadge m.status}</td>
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredWidgetTypes}</td>
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredEventTypes}</td>
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredAnnotationCategories}</td>
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredPolicyScopes}</td>
<td class="px-4 py-3 text-gray-400 text-xs">{maybe "" show m.activatedAt}</td>
<td class="px-4 py-3 text-right text-xs">
<a href={ShowHubCapabilityManifestAction { hubCapabilityManifestId = m.id }}
class="text-indigo-600 hover:text-indigo-800">View</a>
</td>
</tr>
|]
hubName :: [Hub] -> Id Hub -> Text
hubName hubs 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 "draft" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">draft</span>|]
statusBadge "retired" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500">retired</span>|]
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
jsonCount :: Value -> Text
jsonCount (Array v) | V.null v = "0"
| otherwise = tshow (V.length v)
jsonCount _ = "0"

View File

@@ -0,0 +1,55 @@
module Web.View.HubCapabilityManifests.New where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
data NewView = NewView
{ manifest :: !HubCapabilityManifest
, hubs :: ![Hub]
}
instance View NewView where
html NewView { .. } = [hsx|
<div class="mb-4">
<a href={HubCapabilityManifestsAction} class="text-sm text-gray-500 hover:text-gray-700">
Capability Manifests
</a>
</div>
<h1 class="text-xl font-semibold mb-6">New Capability Manifest</h1>
<div class="bg-amber-50 border border-amber-200 rounded p-4 mb-6 text-sm text-amber-800">
A capability manifest lets a domain or shared hub declare the widget types, event types,
annotation categories, and policy scopes it owns. Create a draft, declare your types,
then activate to register them with the framework.
</div>
<form method="POST" action={CreateHubCapabilityManifestAction}>
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Hub</label>
{selectField #hubId (hubOptions hubs)}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
Capability Description <span class="text-gray-400 text-xs">(optional)</span>
</label>
{(textareaField #capabilityDescription) { 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">
Contact <span class="text-gray-400 text-xs">(team or person)</span>
</label>
{(textField #contact) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
</div>
<div class="pt-2">
<button type="submit"
class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
Create Draft
</button>
</div>
</div>
</form>
|]
hubOptions :: [Hub] -> [(Text, Id Hub)]
hubOptions hubs = map (\h -> (h.name <> " (" <> h.hubKind <> ")", h.id)) hubs

View File

@@ -0,0 +1,116 @@
module Web.View.HubCapabilityManifests.Show where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Data.Aeson (Value(..), encode)
import qualified Data.Vector as V
import qualified Data.ByteString.Lazy.Char8 as BL
data ShowView = ShowView
{ manifest :: !HubCapabilityManifest
, hub :: !Hub
}
instance View ShowView where
html ShowView { .. } = [hsx|
<div class="mb-4">
<a href={HubCapabilityManifestsAction} class="text-sm text-gray-500 hover:text-gray-700">
Capability Manifests
</a>
</div>
<div class="flex items-center gap-3 mb-6">
<h1 class="text-2xl font-semibold">{hub.name} Capability Manifest</h1>
{statusBadge manifest.status}
</div>
{if manifest.status == "draft"
then [hsx|
<div class="mb-4 flex gap-2">
<a href={EditHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }}
class="text-sm border border-indigo-300 text-indigo-700 px-3 py-1.5 rounded hover:bg-indigo-50">
Edit Draft
</a>
<a href={ActivateManifestAction { hubCapabilityManifestId = manifest.id }}
class="text-sm bg-green-600 text-white px-3 py-1.5 rounded hover:bg-green-700">
Activate
</a>
</div>
|]
else if manifest.status == "active"
then [hsx|
<div class="mb-4">
<a href={RetireManifestAction { hubCapabilityManifestId = manifest.id }}
data-confirm="Retire this manifest? The hub's types will remain registered."
class="text-sm border border-gray-300 text-gray-600 px-3 py-1.5 rounded hover:bg-gray-50">
Retire
</a>
</div>
|]
else [hsx||]}
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="bg-white rounded-lg border border-gray-200 p-4">
<p class="text-xs text-gray-500 uppercase tracking-wide">Manifest Version</p>
<p class="font-mono font-medium mt-1">{manifest.manifestVersion}</p>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-4">
<p class="text-xs text-gray-500 uppercase tracking-wide">Activated</p>
<p class="font-medium mt-1">{maybe "" show manifest.activatedAt}</p>
</div>
</div>
{forEach (maybeText manifest.capabilityDescription) (\d -> [hsx|
<p class="text-sm text-gray-600 mb-4">{d}</p>
|])}
{forEach (maybeText manifest.contact) (\c -> [hsx|
<p class="text-xs text-gray-400 mb-6">Contact: {c}</p>
|])}
<div class="grid grid-cols-2 gap-4">
{jsonArraySection "Declared Widget Types" manifest.declaredWidgetTypes}
{jsonArraySection "Declared Event Types" manifest.declaredEventTypes}
{jsonArraySection "Declared Annotation Categories" manifest.declaredAnnotationCategories}
{jsonArraySection "Declared Policy Scopes" manifest.declaredPolicyScopes}
</div>
|]
jsonArraySection :: Text -> Value -> Html
jsonArraySection title val = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-4">
<h3 class="text-sm font-semibold text-gray-700 mb-2">{title}
<span class="text-gray-400 font-normal ml-1">({arrayLen val})</span>
</h3>
{renderArrayItems val}
</div>
|]
renderArrayItems :: Value -> Html
renderArrayItems (Array v) | V.null v =
[hsx|<p class="text-xs text-gray-400">None declared</p>|]
renderArrayItems (Array v) = [hsx|
<ul class="space-y-1">
{forEach (V.toList v) renderItem}
</ul>
|]
renderArrayItems _ = [hsx|<p class="text-xs text-gray-400"></p>|]
renderItem :: Value -> Html
renderItem (String t) = [hsx|<li class="font-mono text-xs text-gray-700">{t}</li>|]
renderItem v = [hsx|<li class="font-mono text-xs text-gray-500">{cs (BL.unpack (encode v)) :: Text}</li>|]
arrayLen :: Value -> Text
arrayLen (Array v) = tshow (V.length v)
arrayLen _ = "0"
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 "draft" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">draft</span>|]
statusBadge "retired" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500">retired</span>|]
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
maybeText :: Maybe Text -> [Text]
maybeText Nothing = []
maybeText (Just t) = [t]

View File

@@ -24,6 +24,7 @@ instance View IndexView where
<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">Slug</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Domain</th>
<th class="text-left px-4 py-3 font-medium text-gray-700">Kind</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
@@ -34,6 +35,11 @@ instance View IndexView where
</div>
|]
kindBadge :: Text -> Html
kindBadge "framework" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-purple-100 text-purple-800">framework</span>|]
kindBadge "shared" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-teal-100 text-teal-800">shared</span>|]
kindBadge _ = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800">domain</span>|]
renderHub :: Hub -> Html
renderHub hub = [hsx|
<tr class="border-b border-gray-100 hover:bg-gray-50">
@@ -45,6 +51,7 @@ renderHub hub = [hsx|
</td>
<td class="px-4 py-3 text-gray-500 font-mono text-xs">{hub.slug}</td>
<td class="px-4 py-3 text-gray-500">{hub.domain}</td>
<td class="px-4 py-3">{kindBadge hub.hubKind}</td>
<td class="px-4 py-3 text-right">
<a href={EditHubAction { hubId = hub.id }}
class="text-gray-500 hover:text-gray-700 text-xs mr-3">Edit</a>

View File

@@ -10,6 +10,7 @@ data ShowView = ShowView
, widgets :: ![Widget]
, recentEvents :: ![InteractionEvent]
, recentAnnotations :: ![Annotation]
, mManifest :: !(Maybe HubCapabilityManifest)
}
instance View ShowView where
@@ -22,7 +23,10 @@ instance View ShowView where
</div>
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-semibold">{hub.name}</h1>
<div class="flex items-center gap-2">
<h1 class="text-2xl font-semibold">{hub.name}</h1>
{kindBadge hub.hubKind}
</div>
<p class="text-sm text-gray-500 mt-1">
<span class="font-mono bg-gray-100 px-1 rounded">{hub.slug}</span>
<span class="ml-2">{hub.domain}</span>
@@ -131,6 +135,11 @@ instance View ShowView where
{forEach recentAnnotations renderAnnotationCard}
</div>
</section>
<section class="mt-8">
<h2 class="text-lg font-medium mb-3">Capability Manifest</h2>
{renderManifestSection mManifest hub.id}
</section>
|]
renderWidgetRow :: Widget -> Html
@@ -171,3 +180,48 @@ renderAnnotationCard a = [hsx|
<p class="text-sm text-gray-700">{a.body}</p>
</div>
|]
renderManifestSection :: Maybe HubCapabilityManifest -> Id Hub -> Html
renderManifestSection Nothing hubId = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-5 flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">No capability manifest registered for this hub.</p>
<p class="text-xs text-gray-400 mt-1">
Domain hubs should declare their vocabulary before creating hub-owned type registry entries.
</p>
</div>
<a href={NewHubCapabilityManifestAction}
class="text-sm border border-indigo-300 text-indigo-700 px-3 py-1.5 rounded hover:bg-indigo-50">
Register Capabilities
</a>
</div>
|]
renderManifestSection (Just m) _ = [hsx|
<div class="bg-white rounded-lg border border-gray-200 p-5">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
{manifestStatusBadge m.status}
<span class="text-sm text-gray-600">v{m.manifestVersion}</span>
{forEach (maybeText m.capabilityDescription) (\d -> [hsx|<span class="text-sm text-gray-500"> {d}</span>|])}
</div>
<a href={ShowHubCapabilityManifestAction { hubCapabilityManifestId = m.id }}
class="text-sm text-indigo-600 hover:text-indigo-800">View manifest </a>
</div>
{forEach (maybeText m.contact) (\c -> [hsx|<p class="text-xs text-gray-400">Contact: {c}</p>|])}
</div>
|]
manifestStatusBadge :: Text -> Html
manifestStatusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
manifestStatusBadge "draft" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">draft</span>|]
manifestStatusBadge "retired" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500">retired</span>|]
manifestStatusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
kindBadge :: Text -> Html
kindBadge "framework" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-purple-100 text-purple-800">framework</span>|]
kindBadge "shared" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-teal-100 text-teal-800">shared</span>|]
kindBadge _ = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800">domain</span>|]
maybeText :: Maybe Text -> [Text]
maybeText Nothing = []
maybeText (Just t) = [t]

View File

@@ -25,6 +25,7 @@ instance View ShowView where
<span class={adapterStatusBadge contract.status <> " text-xs px-2 py-0.5 rounded font-medium"}>
{contract.status}
</span>
{maturityBadge contract.maturity}
</div>
{forEach (contractDescription contract) (\d -> [hsx|
@@ -74,3 +75,10 @@ contractDescription :: InteractionReportingContract -> [Text]
contractDescription c = case c.description of
Just d -> [d]
Nothing -> []
maturityBadge :: Text -> Html
maturityBadge "stable" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800 font-medium">Stable</span>|]
maturityBadge "beta" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800 font-medium">Beta</span>|]
maturityBadge "experimental" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800 font-medium">Experimental</span>|]
maturityBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500 font-medium">Deprecated</span>|]
maturityBadge m = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600 font-medium">{m}</span>|]

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>
|]

View File

@@ -30,6 +30,7 @@ instance View ShowView where
<span class={adapterStatusBadge spec.status <> " text-xs px-2 py-0.5 rounded font-medium"}>
{spec.status}
</span>
{maturityBadge spec.maturity}
</div>
<a href={EditWidgetAdapterSpecAction { widgetAdapterSpecId = spec.id }}
class="text-sm text-gray-500 border border-gray-200 rounded px-3 py-1 hover:border-gray-400">
@@ -116,3 +117,10 @@ specNotes :: WidgetAdapterSpec -> [Text]
specNotes s = case s.notes of
Just n -> [n]
Nothing -> []
maturityBadge :: Text -> Html
maturityBadge "stable" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800 font-medium">Stable</span>|]
maturityBadge "beta" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800 font-medium">Beta</span>|]
maturityBadge "experimental" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800 font-medium">Experimental</span>|]
maturityBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500 font-medium">Deprecated</span>|]
maturityBadge m = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600 font-medium">{m}</span>|]

View File

@@ -10,6 +10,8 @@ data EditView = EditView
{ widget :: !Widget
, hubs :: ![Hub]
, adapterSpecs :: ![WidgetAdapterSpec]
, widgetTypes :: ![WidgetTypeRegistry]
, policyScopes :: ![PolicyScopeRegistry]
}
instance View EditView where
@@ -23,6 +25,6 @@ instance View EditView where
<span>Edit</span>
</div>
<h1 class="text-2xl font-semibold mb-6">Edit Widget</h1>
{renderForm widget hubs adapterSpecs}
{renderForm widget hubs adapterSpecs widgetTypes policyScopes}
</div>
|]

View File

@@ -9,24 +9,26 @@ data NewView = NewView
{ widget :: !Widget
, hubs :: ![Hub]
, adapterSpecs :: ![WidgetAdapterSpec]
, widgetTypes :: ![WidgetTypeRegistry]
, policyScopes :: ![PolicyScopeRegistry]
}
instance View NewView where
html NewView { .. } = [hsx|
<div class="max-w-lg">
<h1 class="text-2xl font-semibold mb-6">Register Widget</h1>
{renderForm widget hubs adapterSpecs}
{renderForm widget hubs adapterSpecs widgetTypes policyScopes}
</div>
|]
renderForm :: Widget -> [Hub] -> [WidgetAdapterSpec] -> Html
renderForm widget hubs adapterSpecs = formFor widget [hsx|
renderForm :: Widget -> [Hub] -> [WidgetAdapterSpec] -> [WidgetTypeRegistry] -> [PolicyScopeRegistry] -> Html
renderForm widget hubs adapterSpecs widgetTypes policyScopes = formFor widget [hsx|
{textField #name}
{selectField #widgetType widgetTypeOptions}
{selectField #widgetType (widgetTypeOptions widgetTypes)}
{selectField #hubId (hubOptions hubs)}
{textField #capabilityRef}
{textField #viewContext}
{selectField #policyScope policyScopeOptions}
{selectField #policyScope (policyScopeOptions policyScopes)}
{selectField #status statusOptions}
<div>
<label class="ihp-form-label">Adapter Spec (optional leave blank for native IHP widget)</label>
@@ -43,23 +45,11 @@ renderForm widget hubs adapterSpecs = formFor widget [hsx|
hubOptions :: [Hub] -> [(Text, Id Hub)]
hubOptions hubs = map (\h -> (h.name, h.id)) hubs
widgetTypeOptions :: [(Text, Text)]
widgetTypeOptions =
[ ("Chart", "chart")
, ("Form", "form")
, ("Table", "table")
, ("Action", "action")
, ("Panel", "panel")
, ("Navigation", "nav")
, ("Other", "other")
]
widgetTypeOptions :: [WidgetTypeRegistry] -> [(Text, Text)]
widgetTypeOptions = map (\r -> (r.label, r.name))
policyScopeOptions :: [(Text, Text)]
policyScopeOptions =
[ ("Internal", "internal")
, ("Hub", "hub")
, ("Public", "public")
]
policyScopeOptions :: [PolicyScopeRegistry] -> [(Text, Text)]
policyScopeOptions = map (\r -> (r.label, r.name))
statusOptions :: [(Text, Text)]
statusOptions =