generated from coulomb/repo-seed
feat(P6/T04): WidgetAdapterSpecsController, registry, widget adapter integration
CRUD for WidgetAdapterSpec (index, show, new/create, edit/update — status+notes only after creation). Widget new/edit forms expose optional adapter_spec_id select. Widget show page renders adapter badge with link to spec. Widgets controller fetches adapter spec for show action. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
77
Web/Controller/WidgetAdapterSpecs.hs
Normal file
77
Web/Controller/WidgetAdapterSpecs.hs
Normal file
@@ -0,0 +1,77 @@
|
||||
module Web.Controller.WidgetAdapterSpecs where
|
||||
|
||||
import Web.Types
|
||||
import Web.View.WidgetAdapterSpecs.Index
|
||||
import Web.View.WidgetAdapterSpecs.Show
|
||||
import Web.View.WidgetAdapterSpecs.New
|
||||
import Web.View.WidgetAdapterSpecs.Edit
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ControllerPrelude
|
||||
|
||||
instance Controller WidgetAdapterSpecsController where
|
||||
beforeAction = ensureIsUser
|
||||
|
||||
action WidgetAdapterSpecsAction = do
|
||||
specs <- query @WidgetAdapterSpec |> orderByAsc #name |> fetch
|
||||
envelopes <- query @EnvelopeEmissionContract |> fetch
|
||||
reportings <- query @InteractionReportingContract |> fetch
|
||||
render IndexView { specs, envelopes, reportings }
|
||||
|
||||
action ShowWidgetAdapterSpecAction { widgetAdapterSpecId } = do
|
||||
spec <- fetch widgetAdapterSpecId
|
||||
mEnvelope <- case spec.envelopeContractId of
|
||||
Nothing -> pure Nothing
|
||||
Just eid -> fetchOneOrNothing eid
|
||||
mReporting <- case spec.reportingContractId of
|
||||
Nothing -> pure Nothing
|
||||
Just rid -> fetchOneOrNothing rid
|
||||
widgets <- query @Widget
|
||||
|> filterWhere (#adapterSpecId, Just widgetAdapterSpecId)
|
||||
|> orderByAsc #name
|
||||
|> fetch
|
||||
render ShowView { spec, mEnvelope, mReporting, widgets }
|
||||
|
||||
action NewWidgetAdapterSpecAction = do
|
||||
let spec = newRecord @WidgetAdapterSpec
|
||||
envelopes <- query @EnvelopeEmissionContract |> filterWhere (#status, "active") |> fetch
|
||||
reportings <- query @InteractionReportingContract |> filterWhere (#status, "active") |> fetch
|
||||
render NewView { spec, envelopes, reportings }
|
||||
|
||||
action CreateWidgetAdapterSpecAction = do
|
||||
let spec = newRecord @WidgetAdapterSpec
|
||||
envelopes <- query @EnvelopeEmissionContract |> filterWhere (#status, "active") |> fetch
|
||||
reportings <- query @InteractionReportingContract |> filterWhere (#status, "active") |> fetch
|
||||
spec
|
||||
|> fill @'["name", "framework", "version", "envelopeContractId", "reportingContractId", "status", "notes"]
|
||||
|> validateField #name nonEmpty
|
||||
|> validateField #framework nonEmpty
|
||||
|> validateField #version nonEmpty
|
||||
|> validateField #status (isInList ["draft", "active", "deprecated"])
|
||||
|> ifValid \case
|
||||
Left spec -> render NewView { spec, envelopes, reportings }
|
||||
Right spec -> do
|
||||
spec <- createRecord spec
|
||||
setSuccessMessage ("Adapter spec '" <> spec.name <> "' registered")
|
||||
redirectTo ShowWidgetAdapterSpecAction { widgetAdapterSpecId = spec.id }
|
||||
|
||||
action EditWidgetAdapterSpecAction { widgetAdapterSpecId } = do
|
||||
spec <- fetch widgetAdapterSpecId
|
||||
envelopes <- query @EnvelopeEmissionContract |> filterWhere (#status, "active") |> fetch
|
||||
reportings <- query @InteractionReportingContract |> filterWhere (#status, "active") |> fetch
|
||||
render EditView { spec, envelopes, reportings }
|
||||
|
||||
action UpdateWidgetAdapterSpecAction { widgetAdapterSpecId } = do
|
||||
spec <- fetch widgetAdapterSpecId
|
||||
envelopes <- query @EnvelopeEmissionContract |> filterWhere (#status, "active") |> fetch
|
||||
reportings <- query @InteractionReportingContract |> filterWhere (#status, "active") |> fetch
|
||||
-- Only status and notes are editable once a spec exists.
|
||||
spec
|
||||
|> fill @'["status", "notes"]
|
||||
|> validateField #status (isInList ["draft", "active", "deprecated"])
|
||||
|> ifValid \case
|
||||
Left spec -> render EditView { spec, envelopes, reportings }
|
||||
Right spec -> do
|
||||
spec |> updateRecord
|
||||
setSuccessMessage "Adapter spec updated"
|
||||
redirectTo ShowWidgetAdapterSpecAction { widgetAdapterSpecId = spec.id }
|
||||
@@ -23,7 +23,11 @@ instance Controller WidgetsController where
|
||||
action NewWidgetAction = do
|
||||
let widget = newRecord @Widget
|
||||
hubs <- query @Hub |> fetch
|
||||
render NewView { widget, hubs }
|
||||
adapterSpecs <- query @WidgetAdapterSpec
|
||||
|> filterWhere (#status, "active")
|
||||
|> orderByAsc #name
|
||||
|> fetch
|
||||
render NewView { widget, hubs, adapterSpecs }
|
||||
|
||||
action ShowWidgetAction { widgetId } = do
|
||||
widget <- fetch widgetId
|
||||
@@ -57,17 +61,21 @@ instance Controller WidgetsController where
|
||||
allDeployments <- query @DeploymentRecord |> fetch
|
||||
let cycleCounts = widgetCycleCounts allCandidates allRequirements allDecisions allDeployments
|
||||
cycleCount = fromMaybe 0 (lookup widgetId cycleCounts)
|
||||
render ShowView { widget, hub, versions, events, annotations, recentSignals, isRegressed, cycleCount }
|
||||
mAdapterSpec <- case widget.adapterSpecId of
|
||||
Nothing -> pure Nothing
|
||||
Just sid -> fetchOneOrNothing sid
|
||||
render ShowView { widget, hub, versions, events, annotations, recentSignals, isRegressed, cycleCount, mAdapterSpec }
|
||||
|
||||
action CreateWidgetAction = do
|
||||
let widget = newRecord @Widget
|
||||
hubs <- query @Hub |> fetch
|
||||
adapterSpecs <- query @WidgetAdapterSpec |> filterWhere (#status, "active") |> orderByAsc #name |> fetch
|
||||
widget
|
||||
|> fill @'["name", "widgetType", "hubId", "capabilityRef", "viewContext", "policyScope", "status"]
|
||||
|> fill @'["name", "widgetType", "hubId", "capabilityRef", "viewContext", "policyScope", "status", "adapterSpecId"]
|
||||
|> validateField #name nonEmpty
|
||||
|> validateField #widgetType nonEmpty
|
||||
|> ifValid \case
|
||||
Left widget -> render NewView { widget, hubs }
|
||||
Left widget -> render NewView { widget, hubs, adapterSpecs }
|
||||
Right widget -> do
|
||||
widget <- createRecord widget
|
||||
let snapshot = object
|
||||
@@ -91,17 +99,19 @@ instance Controller WidgetsController where
|
||||
action EditWidgetAction { widgetId } = do
|
||||
widget <- fetch widgetId
|
||||
hubs <- query @Hub |> fetch
|
||||
render EditView { widget, hubs }
|
||||
adapterSpecs <- query @WidgetAdapterSpec |> filterWhere (#status, "active") |> orderByAsc #name |> fetch
|
||||
render EditView { widget, hubs, adapterSpecs }
|
||||
|
||||
action UpdateWidgetAction { widgetId } = do
|
||||
widget <- fetch widgetId
|
||||
hubs <- query @Hub |> fetch
|
||||
adapterSpecs <- query @WidgetAdapterSpec |> filterWhere (#status, "active") |> orderByAsc #name |> fetch
|
||||
widget
|
||||
|> fill @'["name", "widgetType", "hubId", "capabilityRef", "viewContext", "policyScope", "status"]
|
||||
|> fill @'["name", "widgetType", "hubId", "capabilityRef", "viewContext", "policyScope", "status", "adapterSpecId"]
|
||||
|> validateField #name nonEmpty
|
||||
|> validateField #widgetType nonEmpty
|
||||
|> ifValid \case
|
||||
Left widget -> render EditView { widget, hubs }
|
||||
Left widget -> render EditView { widget, hubs, adapterSpecs }
|
||||
Right widget -> do
|
||||
let newVersion = widget.version + 1
|
||||
widget <- widget |> set #version newVersion |> updateRecord
|
||||
|
||||
72
Web/View/WidgetAdapterSpecs/Edit.hs
Normal file
72
Web/View/WidgetAdapterSpecs/Edit.hs
Normal file
@@ -0,0 +1,72 @@
|
||||
module Web.View.WidgetAdapterSpecs.Edit where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
|
||||
data EditView = EditView
|
||||
{ spec :: !WidgetAdapterSpec
|
||||
, envelopes :: ![EnvelopeEmissionContract]
|
||||
, reportings :: ![InteractionReportingContract]
|
||||
}
|
||||
|
||||
instance View EditView where
|
||||
html EditView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={ShowWidgetAdapterSpecAction { widgetAdapterSpecId = spec.id }}
|
||||
class="text-sm text-gray-500 hover:text-gray-800">
|
||||
← {spec.name}
|
||||
</a>
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold mb-2">Edit Adapter Spec</h1>
|
||||
<p class="text-sm text-gray-500 mb-6">
|
||||
Only <strong>status</strong> and <strong>notes</strong> can be changed once a spec is registered.
|
||||
Name, framework, version, and linked contracts are immutable.
|
||||
</p>
|
||||
{renderForm spec}
|
||||
|]
|
||||
|
||||
renderForm :: WidgetAdapterSpec -> Html
|
||||
renderForm spec = formFor spec [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 space-y-5 max-w-lg">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-1">Name (read-only)</label>
|
||||
<div class="text-sm text-gray-600 font-mono bg-gray-50 rounded px-3 py-2">{spec.name}</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-1">Framework (read-only)</label>
|
||||
<div class="text-sm text-gray-600 bg-gray-50 rounded px-3 py-2">{spec.framework}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-1">Version (read-only)</label>
|
||||
<div class="text-sm text-gray-600 font-mono bg-gray-50 rounded px-3 py-2">{spec.version}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
||||
<select name="status" class="w-full border border-gray-200 rounded px-3 py-2 text-sm">
|
||||
<option value="draft" selected={spec.status == "draft"}>draft</option>
|
||||
<option value="active" selected={spec.status == "active"}>active</option>
|
||||
<option value="deprecated" selected={spec.status == "deprecated"}>deprecated</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Notes</label>
|
||||
{textareaField #notes}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
{submitButton}
|
||||
<a href={ShowWidgetAdapterSpecAction { widgetAdapterSpecId = spec.id }}
|
||||
class="text-sm text-gray-500 border border-gray-200 rounded px-4 py-2 hover:border-gray-400">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|]
|
||||
82
Web/View/WidgetAdapterSpecs/Index.hs
Normal file
82
Web/View/WidgetAdapterSpecs/Index.hs
Normal file
@@ -0,0 +1,82 @@
|
||||
module Web.View.WidgetAdapterSpecs.Index where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
import Application.Helper.View (adapterStatusBadge)
|
||||
|
||||
data IndexView = IndexView
|
||||
{ specs :: ![WidgetAdapterSpec]
|
||||
, envelopes :: ![EnvelopeEmissionContract]
|
||||
, reportings :: ![InteractionReportingContract]
|
||||
}
|
||||
|
||||
instance View IndexView where
|
||||
html IndexView { .. } = [hsx|
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Widget Adapter Specs</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Register adapters that allow non-IHP UI frameworks to participate in IHF.
|
||||
</p>
|
||||
</div>
|
||||
<a href={NewWidgetAdapterSpecAction}
|
||||
class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
|
||||
+ Register Adapter
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mb-5 text-sm">
|
||||
<a href={EnvelopeEmissionContractsAction}
|
||||
class="text-gray-500 hover:text-gray-800">Envelope Contracts ({length envelopes})</a>
|
||||
<a href={InteractionReportingContractsAction}
|
||||
class="text-gray-500 hover:text-gray-800">Reporting Contracts ({length reportings})</a>
|
||||
</div>
|
||||
|
||||
{if null specs
|
||||
then [hsx|<p class="text-sm text-gray-400">No adapter specs registered yet.</p>|]
|
||||
else renderTable specs}
|
||||
|]
|
||||
|
||||
renderTable :: [WidgetAdapterSpec] -> Html
|
||||
renderTable specs = [hsx|
|
||||
<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-600">Name</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-600">Framework</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-600">Version</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-600">Status</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-600">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
{forEach specs renderRow}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderRow :: WidgetAdapterSpec -> Html
|
||||
renderRow s = [hsx|
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3">
|
||||
<a href={ShowWidgetAdapterSpecAction { widgetAdapterSpecId = s.id }}
|
||||
class="font-medium text-indigo-600 hover:underline">{s.name}</a>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="bg-purple-100 text-purple-800 text-xs px-2 py-0.5 rounded font-medium">
|
||||
{s.framework}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-600 font-mono text-xs">{s.version}</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class={adapterStatusBadge s.status <> " text-xs px-2 py-0.5 rounded font-medium"}>
|
||||
{s.status}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-400 text-xs">{show s.createdAt}</td>
|
||||
</tr>
|
||||
|]
|
||||
89
Web/View/WidgetAdapterSpecs/New.hs
Normal file
89
Web/View/WidgetAdapterSpecs/New.hs
Normal file
@@ -0,0 +1,89 @@
|
||||
module Web.View.WidgetAdapterSpecs.New where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
|
||||
data NewView = NewView
|
||||
{ spec :: !WidgetAdapterSpec
|
||||
, envelopes :: ![EnvelopeEmissionContract]
|
||||
, reportings :: ![InteractionReportingContract]
|
||||
}
|
||||
|
||||
instance View NewView where
|
||||
html NewView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={WidgetAdapterSpecsAction} class="text-sm text-gray-500 hover:text-gray-800">
|
||||
← Adapter Specs
|
||||
</a>
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold mb-6">Register Adapter Spec</h1>
|
||||
{renderForm spec envelopes reportings}
|
||||
|]
|
||||
|
||||
renderForm :: WidgetAdapterSpec -> [EnvelopeEmissionContract] -> [InteractionReportingContract] -> Html
|
||||
renderForm spec envelopes reportings = formFor spec [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 space-y-5 max-w-lg">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-red-500">*</span></label>
|
||||
{textField #name}
|
||||
<p class="text-xs text-gray-400 mt-1">Unique identifier, e.g. react-18, vue-3, web-component</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Framework <span class="text-red-500">*</span></label>
|
||||
{textField #framework}
|
||||
<p class="text-xs text-gray-400 mt-1">e.g. react, vue, angular, vanilla</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Version <span class="text-red-500">*</span></label>
|
||||
{textField #version}
|
||||
<p class="text-xs text-gray-400 mt-1">Adapter spec version, e.g. 1.0</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Envelope Contract</label>
|
||||
<select name="envelopeContractId" class="w-full border border-gray-200 rounded px-3 py-2 text-sm">
|
||||
<option value="">— None —</option>
|
||||
{forEach envelopes (\e -> [hsx|
|
||||
<option value={tshow e.id}>v{e.contractVersion}</option>
|
||||
|])}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Reporting Contract</label>
|
||||
<select name="reportingContractId" class="w-full border border-gray-200 rounded px-3 py-2 text-sm">
|
||||
<option value="">— None —</option>
|
||||
{forEach reportings (\r -> [hsx|
|
||||
<option value={tshow r.id}>v{r.contractVersion}</option>
|
||||
|])}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
||||
<select name="status" class="w-full border border-gray-200 rounded px-3 py-2 text-sm">
|
||||
<option value="draft">draft</option>
|
||||
<option value="active">active</option>
|
||||
<option value="deprecated">deprecated</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Notes</label>
|
||||
{textareaField #notes}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
{submitButton}
|
||||
<a href={WidgetAdapterSpecsAction}
|
||||
class="text-sm text-gray-500 border border-gray-200 rounded px-4 py-2 hover:border-gray-400">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|]
|
||||
118
Web/View/WidgetAdapterSpecs/Show.hs
Normal file
118
Web/View/WidgetAdapterSpecs/Show.hs
Normal file
@@ -0,0 +1,118 @@
|
||||
module Web.View.WidgetAdapterSpecs.Show where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
import Application.Helper.View (adapterStatusBadge)
|
||||
|
||||
data ShowView = ShowView
|
||||
{ spec :: !WidgetAdapterSpec
|
||||
, mEnvelope :: !(Maybe EnvelopeEmissionContract)
|
||||
, mReporting :: !(Maybe InteractionReportingContract)
|
||||
, widgets :: ![Widget]
|
||||
}
|
||||
|
||||
instance View ShowView where
|
||||
html ShowView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={WidgetAdapterSpecsAction} class="text-sm text-gray-500 hover:text-gray-800">
|
||||
← Adapter Specs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="bg-purple-100 text-purple-800 text-sm font-medium px-3 py-1 rounded">
|
||||
{spec.framework}
|
||||
</span>
|
||||
<h1 class="text-2xl font-semibold">{spec.name}</h1>
|
||||
<span class={adapterStatusBadge spec.status <> " text-xs px-2 py-0.5 rounded font-medium"}>
|
||||
{spec.status}
|
||||
</span>
|
||||
</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">
|
||||
Edit status / notes
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<div class="text-xs text-gray-500 mb-1">Spec Version</div>
|
||||
<div class="font-mono font-medium">{spec.version}</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<div class="text-xs text-gray-500 mb-1">Registered Widgets</div>
|
||||
<div class="font-medium">{length widgets}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<div class="text-xs text-gray-500 mb-2">Envelope Contract</div>
|
||||
{renderEnvelopeLink mEnvelope}
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<div class="text-xs text-gray-500 mb-2">Reporting Contract</div>
|
||||
{renderReportingLink mReporting}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{forEach (specNotes spec) (\n -> [hsx|
|
||||
<div class="bg-gray-50 rounded-lg border border-gray-200 p-4 mb-4 text-sm text-gray-700">
|
||||
<strong>Notes:</strong> {n}
|
||||
</div>
|
||||
|])}
|
||||
|
||||
{if null widgets
|
||||
then [hsx|<p class="text-sm text-gray-400 mt-6">No widgets assigned to this adapter spec.</p>|]
|
||||
else [hsx|
|
||||
<h2 class="text-lg font-semibold mb-3 mt-6">Assigned Widgets</h2>
|
||||
<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-600">Name</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-600">Type</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-600">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
{forEach widgets renderWidgetRow}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|]}
|
||||
|]
|
||||
|
||||
renderEnvelopeLink :: Maybe EnvelopeEmissionContract -> Html
|
||||
renderEnvelopeLink Nothing = [hsx|<span class="text-gray-400 text-sm">—</span>|]
|
||||
renderEnvelopeLink (Just c) = [hsx|
|
||||
<a href={ShowEnvelopeEmissionContractAction { envelopeEmissionContractId = c.id }}
|
||||
class="font-mono text-indigo-600 hover:underline text-sm">v{c.contractVersion}</a>
|
||||
|]
|
||||
|
||||
renderReportingLink :: Maybe InteractionReportingContract -> Html
|
||||
renderReportingLink Nothing = [hsx|<span class="text-gray-400 text-sm">—</span>|]
|
||||
renderReportingLink (Just c) = [hsx|
|
||||
<a href={ShowInteractionReportingContractAction { interactionReportingContractId = c.id }}
|
||||
class="font-mono text-indigo-600 hover:underline text-sm">v{c.contractVersion}</a>
|
||||
|]
|
||||
|
||||
renderWidgetRow :: Widget -> Html
|
||||
renderWidgetRow w = [hsx|
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3">
|
||||
<a href={ShowWidgetAction { widgetId = w.id }}
|
||||
class="text-indigo-600 hover:underline">{w.name}</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-500 text-xs">{w.widgetType}</td>
|
||||
<td class="px-4 py-3 text-gray-500 text-xs">{w.status}</td>
|
||||
</tr>
|
||||
|]
|
||||
|
||||
specNotes :: WidgetAdapterSpec -> [Text]
|
||||
specNotes s = case s.notes of
|
||||
Just n -> [n]
|
||||
Nothing -> []
|
||||
@@ -9,6 +9,7 @@ import Web.View.Widgets.New (renderForm)
|
||||
data EditView = EditView
|
||||
{ widget :: !Widget
|
||||
, hubs :: ![Hub]
|
||||
, adapterSpecs :: ![WidgetAdapterSpec]
|
||||
}
|
||||
|
||||
instance View EditView where
|
||||
@@ -22,6 +23,6 @@ instance View EditView where
|
||||
<span>Edit</span>
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold mb-6">Edit Widget</h1>
|
||||
{renderForm widget hubs}
|
||||
{renderForm widget hubs adapterSpecs}
|
||||
</div>
|
||||
|]
|
||||
|
||||
@@ -8,18 +8,19 @@ import IHP.ViewPrelude
|
||||
data NewView = NewView
|
||||
{ widget :: !Widget
|
||||
, hubs :: ![Hub]
|
||||
, adapterSpecs :: ![WidgetAdapterSpec]
|
||||
}
|
||||
|
||||
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}
|
||||
{renderForm widget hubs adapterSpecs}
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderForm :: Widget -> [Hub] -> Html
|
||||
renderForm widget hubs = formFor widget [hsx|
|
||||
renderForm :: Widget -> [Hub] -> [WidgetAdapterSpec] -> Html
|
||||
renderForm widget hubs adapterSpecs = formFor widget [hsx|
|
||||
{textField #name}
|
||||
{selectField #widgetType widgetTypeOptions}
|
||||
{selectField #hubId (hubOptions hubs)}
|
||||
@@ -27,6 +28,15 @@ renderForm widget hubs = formFor widget [hsx|
|
||||
{textField #viewContext}
|
||||
{selectField #policyScope policyScopeOptions}
|
||||
{selectField #status statusOptions}
|
||||
<div>
|
||||
<label class="ihp-form-label">Adapter Spec (optional — leave blank for native IHP widget)</label>
|
||||
<select name="adapterSpecId" class="ihp-form-field">
|
||||
<option value="">— Native IHP widget —</option>
|
||||
{forEach adapterSpecs (\s -> [hsx|
|
||||
<option value={tshow s.id}>{s.name} ({s.framework} v{s.version})</option>
|
||||
|])}
|
||||
</select>
|
||||
</div>
|
||||
{submitButton}
|
||||
|]
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ data ShowView = ShowView
|
||||
, recentSignals :: ![OutcomeSignal]
|
||||
, isRegressed :: !Bool
|
||||
, cycleCount :: !Int
|
||||
, mAdapterSpec :: !(Maybe WidgetAdapterSpec)
|
||||
}
|
||||
|
||||
instance View ShowView where
|
||||
@@ -54,6 +55,7 @@ instance View ShowView where
|
||||
<span class="ml-2 text-xs bg-gray-100 px-1.5 py-0.5 rounded">{widget.policyScope}</span>
|
||||
<span class="ml-2 text-xs bg-green-100 text-green-700 px-1.5 py-0.5 rounded">{widget.status}</span>
|
||||
<span class="ml-2 text-xs text-gray-400">v{show widget.version}</span>
|
||||
{renderAdapterBadge mAdapterSpec}
|
||||
</p>
|
||||
</div>
|
||||
<a href={EditWidgetAction { widgetId = widget.id }}
|
||||
@@ -222,3 +224,12 @@ signalTypeClass "regressed" = "bg-red-100 text-red-800"
|
||||
signalTypeClass "neutral" = "bg-gray-100 text-gray-600"
|
||||
signalTypeClass "inconclusive" = "bg-yellow-100 text-yellow-800"
|
||||
signalTypeClass _ = "bg-gray-100 text-gray-600"
|
||||
|
||||
renderAdapterBadge :: Maybe WidgetAdapterSpec -> Html
|
||||
renderAdapterBadge Nothing = mempty
|
||||
renderAdapterBadge (Just s) = [hsx|
|
||||
<a href={ShowWidgetAdapterSpecAction { widgetAdapterSpecId = s.id }}
|
||||
class="ml-2 text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded hover:bg-purple-200">
|
||||
adapter: {s.name}
|
||||
</a>
|
||||
|]
|
||||
|
||||
@@ -135,7 +135,7 @@ CREATE INDEX widgets_adapter_spec_id_idx ON widgets (adapter_spec_id);
|
||||
|
||||
```task
|
||||
id: IHUB-WP-0006-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "298af675-550b-480b-bed6-05efc79cd0c9"
|
||||
```
|
||||
@@ -163,7 +163,7 @@ against it; contract index/show pages render correctly.
|
||||
|
||||
```task
|
||||
id: IHUB-WP-0006-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "f2767465-ff00-48be-b2dc-5bf3b179cca9"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user