fix(WP-0017/E3): Layer 3 error fixes — controllers and views

Fix compilation errors across 6 controllers and 29 views: import cleanup,
ResponseException pattern for API auth, type fixes, unused import removal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 23:40:31 +00:00
parent 58cad31042
commit c40f11d657
35 changed files with 96 additions and 116 deletions

View File

@@ -10,6 +10,7 @@ import qualified Data.Text.Encoding as TE
import qualified "cryptohash-sha256" Crypto.Hash.SHA256 as SHA256
import qualified Data.ByteString.Base16 as Base16
import Network.Wai (requestHeaders, responseLBS)
import IHP.Controller.Response (ResponseException (..))
-- | Extract Bearer token from Authorization header and validate it
-- against the api_keys table. Returns the ApiConsumer on success,
@@ -52,12 +53,10 @@ unauthorized401 = respondWithStatus 401 $ object
]
respondWithStatus :: (?respond :: Respond) => Int -> Value -> IO a
respondWithStatus status body = do
respondAndExit $ responseLBS
(toEnum status)
[("Content-Type", "application/json")]
(encode body)
error "respondAndExit: unreachable"
respondWithStatus status body = throwIO $ ResponseException $ responseLBS
(toEnum status)
[("Content-Type", "application/json")]
(encode body)
-- | SHA-256 hex hash of the key (same as stored in key_hash column)
hashApiKey :: Text -> Text

View File

@@ -5,6 +5,7 @@ import Web.View.ApiDashboard.Show
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Web.Routes ()
import Database.PostgreSQL.Simple (Only(..))
instance Controller ApiDashboardController where

View File

@@ -6,6 +6,7 @@ import Web.View.EnvelopeEmissionContracts.Show
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Web.Routes ()
instance Controller EnvelopeEmissionContractsController where
beforeAction = ensureIsUser

View File

@@ -30,8 +30,7 @@ instance Controller HubRegistryController where
|> limit 10
|> fetch
adoptedPatterns <- sqlQuery
"SELECT wp.id, wp.name, wp.widget_type, wp.hub_id, \
\ pa.id AS adoption_id, pa.is_version_pinned, pa.adopted_at \
"SELECT wp.id, wp.name, wp.widget_type, pa.is_version_pinned, pa.adopted_at \
\ FROM pattern_adoptions pa \
\ JOIN widget_patterns wp ON wp.id = pa.widget_pattern_id \
\ WHERE pa.adopting_hub_id = ? \

View File

@@ -5,7 +5,6 @@ import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Data.Aeson (object, (.=), decode, Value)
import Data.Coerce (coerce)
import qualified Data.Aeson as A
import qualified Data.Text as T
import qualified Data.ByteString.Lazy.Char8 as LBSC
@@ -27,8 +26,8 @@ instance Controller InteractionEventsController where
unless (eventType `elem` validEventTypes) do
renderJsonWithStatusCode status422 (object ["error" .= ("unknown event_type" :: Text), "valid" .= validEventTypes])
let mUser = currentUserOrNothing
let actorId = fmap (.id) mUser
let mUser = currentUserOrNothing @User
let actorId = fmap (\(Id uuid) -> uuid) (fmap (.id) mUser)
actorType = maybe "anonymous" (const "user") mUser
actorTypeParam = paramOrDefault @Text actorType "actor_type"
viewContextRef = paramOrNothing @Text "view_context_ref"
@@ -41,7 +40,7 @@ instance Controller InteractionEventsController where
event <- newRecord @InteractionEvent
|> set #widgetId widgetId
|> set #eventType eventType
|> set #actorId (coerce actorId)
|> set #actorId actorId
|> set #actorType actorTypeParam
|> set #viewContextRef viewContextRef
|> set #metadata metadata

View File

@@ -6,6 +6,7 @@ import Web.View.Requirements.Show
import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Web.Routes ()
instance Controller RequirementsController where
beforeAction = ensureIsUser

View File

@@ -37,7 +37,6 @@ instance View IndexView where
{renderCfgStatus mCfg}
</div>
<form method="POST" action={CalibrateThresholdsAction (h.id)}>
{csrfTokenTag}
<button type="submit"
class="px-3 py-1.5 text-sm bg-indigo-600 text-white rounded hover:bg-indigo-700">
Calibrate

View File

@@ -24,10 +24,7 @@ instance View ShowView where
{statusBadge agent.isActive}
<a href={EditAgentRegistrationAction agent.id}
class="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 rounded">Edit</a>
{when agent.isActive [hsx|
<a href={DeactivateAgentAction agent.id}
class="px-3 py-1 text-sm bg-red-50 text-red-700 hover:bg-red-100 rounded">Deactivate</a>
|]}
{when agent.isActive deactivateButton}
<a href={ComputeAgentPerformanceAction agent.id}
class="px-3 py-1 text-sm bg-blue-50 text-blue-700 hover:bg-blue-100 rounded">Compute Performance</a>
</div>
@@ -62,6 +59,11 @@ instance View ShowView where
</div>
|]
where
deactivateButton = [hsx|
<a href={DeactivateAgentAction agent.id}
class="px-3 py-1 text-sm bg-red-50 text-red-700 hover:bg-red-100 rounded">Deactivate</a>
|]
policiesTable = [hsx|
<div class="bg-white shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">

View File

@@ -1,9 +1,6 @@
module Web.View.Annotations.New where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.View.Prelude
import Web.Routes ()
data NewView = NewView

View File

@@ -5,7 +5,6 @@ import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
import Data.Coerce (coerce)
data ShowView = ShowView
{ record :: !ArchiveRecord
@@ -62,7 +61,7 @@ renderLineageLink :: ArchiveRecord -> Html
renderLineageLink record
| record.subjectType == "Widget" = [hsx|
<div class="mt-4">
<a href={LineageInspectorAction (coerce record.subjectId)}
<a href={LineageInspectorAction (Id record.subjectId)}
class="text-sm text-indigo-600 hover:underline">View Lineage </a>
</div>
|]

View File

@@ -1,9 +1,6 @@
module Web.View.DecisionRecords.New where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.View.Prelude
import Web.Routes ()
data NewView = NewView
@@ -29,7 +26,7 @@ instance View NewView where
renderForm :: HasPath action => DecisionRecord -> [Requirement] -> [RequirementCandidate] -> [User] -> action -> Html
renderForm record requirements candidates users submitAction = [hsx|
<form method="POST" action={submitAction} class="bg-white rounded-lg border border-gray-200 px-6 py-5 space-y-4">
<form method="POST" action={pathTo submitAction} class="bg-white rounded-lg border border-gray-200 px-6 py-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Title</label>
<input type="text" name="title" value={record.title}

View File

@@ -66,7 +66,7 @@ instance View ShowView where
{forEach policyRefs renderPolicyRef}
<form method="POST" action={AddPolicyReferenceAction (record.id)}
class="mt-3 flex items-end gap-2">
{hiddenField "authenticity_token"}
<div>
<label class="text-xs text-gray-500 block mb-1">Scope</label>
<select name="policyScope"
@@ -113,7 +113,7 @@ instance View ShowView where
{forEach implRefs renderImplRef}
<form method="POST" action={AddImplementationRefAction (record.id)}
class="mt-3 flex items-end gap-2">
{hiddenField "authenticity_token"}
<div>
<label class="text-xs text-gray-500 block mb-1">System</label>
<select name="system"
@@ -168,7 +168,7 @@ renderPolicyRef ref = [hsx|
</div>
<form method="POST"
action={DeletePolicyReferenceAction (ref.id)}>
{hiddenField "authenticity_token"}
<button type="submit"
class="text-xs text-red-500 hover:text-red-700 ml-2">Remove</button>
</form>
@@ -187,7 +187,7 @@ renderImplRef ref = [hsx|
</div>
<form method="POST"
action={DeleteImplementationRefAction (ref.id)}>
{hiddenField "authenticity_token"}
<button type="submit"
class="text-xs text-red-500 hover:text-red-700 ml-2">Remove</button>
</form>

View File

@@ -183,7 +183,6 @@ renderNoEvaluationForm :: Id DeploymentRecord -> Html
renderNoEvaluationForm deploymentRecordId = [hsx|
<form method="POST" action={EvaluateChangeAction deploymentRecordId}
class="space-y-3">
{hiddenField "authenticity_token"}
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">
Score (15) <span class="text-red-500">*</span>

View File

@@ -22,7 +22,6 @@ instance View NewView where
<h1 class="text-2xl font-semibold mb-6">New Governance Template</h1>
<form method="POST" action={CreateGovernanceTemplateAction}>
{csrfTokenFormField}
<div class="space-y-4 max-w-lg">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name</label>

View File

@@ -59,7 +59,7 @@ instance View EditView where
<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 ""}>
disabled={manifest.status /= "draft"}>
Save
</button>
{if manifest.status == "draft" then renderActivateLink manifest.id else mempty}

View File

@@ -1,9 +1,6 @@
module Web.View.HubCapabilityManifests.New where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.View.Prelude
import Web.Routes ()
data NewView = NewView

View File

@@ -11,7 +11,7 @@ import qualified Data.ByteString.Lazy.Char8 as BL
-- | Row from the adopted patterns query.
-- (patternId, patternName, widgetType, patternHubId, adoptionId, isVersionPinned, adoptedAt)
type AdoptedPatternRow = (Id WidgetPattern, Text, Text, Id Hub, Id PatternAdoption, Bool, UTCTime)
type AdoptedPatternRow = (Id WidgetPattern, Text, Text, Bool, UTCTime)
data ShowView = ShowView
{ hub :: !Hub
@@ -170,7 +170,7 @@ renderSnapshotRow s = [hsx|
|]
renderAdoptedPattern :: AdoptedPatternRow -> Html
renderAdoptedPattern (patternId, patternName, widgetType, _, _, isPinned, adoptedAt) = [hsx|
renderAdoptedPattern (patternId, patternName, widgetType, isPinned, adoptedAt) = [hsx|
<div class="bg-white rounded border border-gray-200 p-3 flex items-center justify-between">
<div>
<a href={ShowWidgetPatternAction (patternId)}

View File

@@ -1,9 +1,6 @@
module Web.View.HubRoutingRules.New where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.View.Prelude
import Web.Routes ()
data NewView = NewView
@@ -21,11 +18,14 @@ instance View NewView where
renderForm :: HubRoutingRule -> [Hub] -> Html
renderForm rule hubs = formFor rule [hsx|
{(selectField #sourceHubId hubs){ fieldLabel = "Source Hub" }}
{(selectField #targetHubId hubs){ fieldLabel = "Target Hub" }}
{(selectField #sourceHubId (hubOptions hubs)){ fieldLabel = "Source Hub" }}
{(selectField #targetHubId (hubOptions hubs)){ fieldLabel = "Target Hub" }}
{(textField #matchCategory){ helpText = "Leave blank to match any category" }}
{(textField #matchWidgetType){ helpText = "Leave blank to match any widget type" }}
{(numberField #priority){ helpText = "Higher priority rules are evaluated first" }}
{textareaField #notes}
{submitButton}
|]
hubOptions :: [Hub] -> [(Text, Id Hub)]
hubOptions = map (\h -> (h.name, h.id))

View File

@@ -1,9 +1,6 @@
module Web.View.HubRoutingRules.RoutedCandidates where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.View.Prelude
import Web.Routes ()
data RoutedCandidatesView = RoutedCandidatesView
@@ -49,7 +46,7 @@ renderRoutedCandidates candidates = [hsx|
renderCandidateRow :: RequirementCandidate -> Html
renderCandidateRow c = [hsx|
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 text-gray-800">{c.summary}</td>
<td class="px-4 py-3 text-gray-800">{c.title}</td>
<td class="px-4 py-3 text-gray-500">{c.category}</td>
<td class="px-4 py-3">
<span class="text-xs bg-yellow-100 text-yellow-800 px-2 py-0.5 rounded font-medium">

View File

@@ -1,9 +1,6 @@
module Web.View.HubRoutingRules.Show where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.View.Prelude
import Web.Routes ()
import Web.View.HubRoutingRules.Index (statusBadge)
@@ -35,11 +32,11 @@ instance View ShowView where
<dl class="grid grid-cols-2 gap-4 text-sm">
<div>
<dt class="text-gray-500">Match Category</dt>
<dd class="font-medium">{maybe "any" id rule.matchCategory}</dd>
<dd class="font-medium">{fromMaybe "any" rule.matchCategory}</dd>
</div>
<div>
<dt class="text-gray-500">Match Widget Type</dt>
<dd class="font-medium">{maybe "any" id rule.matchWidgetType}</dd>
<dd class="font-medium">{fromMaybe "any" rule.matchWidgetType}</dd>
</div>
<div>
<dt class="text-gray-500">Priority</dt>

View File

@@ -54,16 +54,7 @@ instance View AdapterCompatibilityDashboardView where
</div>
</div>
{renderCoverageBar adapterBacked nativeCount}
{forEach coverageBySpec (\(sid, cnt) ->
let mSpec = find (\s -> s.id == sid) specs
label = maybe "(unknown)" (.name) mSpec
in [hsx|
<div class="flex items-center gap-3 mt-2 text-xs text-gray-600">
<span class="bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">{label}</span>
<span>{show cnt} widgets</span>
</div>
|]
)}
{forEach coverageBySpec (renderCoverageSpecRow specs)}
</div>
<!-- Panel 3: Contract versions in use -->
@@ -116,6 +107,17 @@ instance View AdapterCompatibilityDashboardView where
in sortBy (comparing (Down . snd))
[ (sid, length (filter (== sid) assigned)) | sid <- specIds ]
renderCoverageSpecRow :: [WidgetAdapterSpec] -> (Id WidgetAdapterSpec, Int) -> Html
renderCoverageSpecRow ss (sid, cnt) =
let mSpec = find (\s -> s.id == sid) ss
label = maybe "(unknown)" (.name) mSpec
in [hsx|
<div class="flex items-center gap-3 mt-2 text-xs text-gray-600">
<span class="bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">{label}</span>
<span>{show cnt} widgets</span>
</div>
|]
renderActiveSpecsTable :: [WidgetAdapterSpec] -> Html
renderActiveSpecsTable [] = [hsx|<p class="text-sm text-gray-400">No active adapter specs.</p>|]
renderActiveSpecsTable ss = [hsx|

View File

@@ -93,7 +93,7 @@ instance View AntifragilityDashboardView where
{renderRecurrenceSection recurrenceLeaderboard widgets}
</div>
|]
where
where
deployedIds = map (.id) allDeployments
openGaps = filter (\d -> any (\r -> r.decisionId == d.id) allImplRefs
&& not (any (\dp -> dp.decisionId == d.id) allDeployments))

View File

@@ -92,7 +92,7 @@ instance View GovernanceDashboardView where
</table>
</div>
|]
where
where
awaitingDecision = filter (isAwaitingDecision allDecisions) allRequirements
regressedWidgets = filter (\w -> w.id `elem` regressionWidgetIds) widgets

View File

@@ -57,12 +57,12 @@ instance View ShowView where
<pre class="text-xs text-blue-900 overflow-auto">curl -X POST {contract.endpointPath} \
-H "Authorization: Bearer &lt;hub-api-key&gt;" \
-H "Content-Type: application/json" \
-d '{"{"}
-d '&#123;
"widget_id": "&lt;uuid&gt;",
"hub_id": "&lt;uuid&gt;",
"event_type": "clicked",
"occurred_at": "2026-03-29T12:00:00Z"
{"}"}'</pre>
&#125;'</pre>
</div>
<div class="mt-4 text-xs text-gray-400">

View File

@@ -2,6 +2,7 @@ module Web.View.LearningDashboard.Show where
import Web.View.Prelude
import Data.Time (diffUTCTime, getCurrentTime, nominalDay)
import qualified Data.Text as T
data ShowView = ShowView
{ topCorrelations :: ![OutcomeCorrelation]
@@ -159,7 +160,7 @@ renderKnowledge e = [hsx|
<div>
<a href={ShowInstitutionalKnowledgeAction (e.id)}
class="text-sm text-blue-600 hover:underline">
{take 80 e.summary <> if length e.summary > 80 then "" else ""}
{T.take 80 e.summary <> if T.length e.summary > 80 then "" else ""}
</a>
</div>
|]

View File

@@ -36,7 +36,6 @@ instance View IndexView where
</p>
</div>
<form method="POST" action={EnrichLineageAction (h.id)}>
{csrfTokenTag}
<button type="submit"
class="px-3 py-1.5 text-sm bg-green-600 text-white rounded hover:bg-green-700"
disabled={unenriched == 0}>

View File

@@ -58,7 +58,6 @@ instance View IndexView where
renderRecomputeButton :: Hub -> Html
renderRecomputeButton h = [hsx|
<form method="POST" action={ComputePatternPerformanceAction (h.id)} class="inline">
{csrfTokenTag}
<button type="submit"
class="px-3 py-1.5 text-sm bg-indigo-600 text-white rounded hover:bg-indigo-700">
Recompute for {h.name}

View File

@@ -1,9 +1,6 @@
module Web.View.RequirementCandidates.Edit where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.View.Prelude
import Web.Routes ()
data EditView = EditView
@@ -33,16 +30,22 @@ renderForm candidate widgets threads = formFor candidate [hsx|
{(textField #title) { fieldLabel = "Title" }}
{(textareaField #description) { fieldLabel = "Description" }}
{selectField #sourceWidgetId (widgetOptions widgets)}
{selectField #sourceThreadId (threadOptions threads)}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Source Thread</label>
<select name="sourceThreadId" class="w-full border border-gray-300 rounded px-3 py-2 text-sm">
<option value="">None</option>
{forEach threads renderThreadOption}
</select>
</div>
{selectField #category categoryOptions}
{submitButton}
|]
widgetOptions :: [Widget] -> [(Text, Text)]
widgetOptions = map (\w -> (w.name, show w.id))
widgetOptions :: [Widget] -> [(Text, Id Widget)]
widgetOptions = map (\w -> (w.name, w.id))
threadOptions :: [AnnotationThread] -> [(Text, Text)]
threadOptions threads = ("None", "") : map (\t -> (t.title, show t.id)) threads
renderThreadOption :: AnnotationThread -> Html
renderThreadOption t = [hsx|<option value={show t.id}>{t.title}</option>|]
categoryOptions :: [(Text, Text)]
categoryOptions =

View File

@@ -1,9 +1,6 @@
module Web.View.RequirementCandidates.Index where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.View.Prelude
import Web.Routes ()
data IndexView = IndexView
@@ -50,7 +47,7 @@ renderFilterPills current = [hsx|
renderPill :: Maybe Text -> Maybe Text -> Text -> Html
renderPill target current label =
let isActive = target == current
baseClass = "text-xs px-3 py-1.5 rounded-full border "
baseClass = "text-xs px-3 py-1.5 rounded-full border " :: Text
cls = if isActive
then baseClass <> "bg-indigo-600 text-white border-indigo-600"
else baseClass <> "border-gray-300 text-gray-600 hover:bg-gray-50"

View File

@@ -1,9 +1,6 @@
module Web.View.RequirementCandidates.New where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.View.Prelude
import Web.Routes ()
data NewView = NewView
@@ -30,16 +27,22 @@ renderForm candidate widgets threads = formFor candidate [hsx|
{(textField #title) { fieldLabel = "Title" }}
{(textareaField #description) { fieldLabel = "Description" }}
{selectField #sourceWidgetId (widgetOptions widgets)}
{selectField #sourceThreadId (threadOptions threads)}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Source Thread</label>
<select name="sourceThreadId" class="w-full border border-gray-300 rounded px-3 py-2 text-sm">
<option value="">None</option>
{forEach threads renderThreadOption}
</select>
</div>
{selectField #category categoryOptions}
{submitButton}
|]
widgetOptions :: [Widget] -> [(Text, Text)]
widgetOptions = map (\w -> (w.name, show w.id))
widgetOptions :: [Widget] -> [(Text, Id Widget)]
widgetOptions = map (\w -> (w.name, w.id))
threadOptions :: [AnnotationThread] -> [(Text, Text)]
threadOptions threads = ("None", "") : map (\t -> (t.title, show t.id)) threads
renderThreadOption :: AnnotationThread -> Html
renderThreadOption t = [hsx|<option value={show t.id}>{t.title}</option>|]
categoryOptions :: [(Text, Text)]
categoryOptions =

View File

@@ -1,9 +1,6 @@
module Web.View.RequirementCandidates.Show where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.View.Prelude
import Web.Routes ()
data ShowView = ShowView
@@ -122,7 +119,6 @@ renderTriageButton :: Id RequirementCandidate -> Text -> Html
renderTriageButton candidateId newStatus = [hsx|
<form method="POST" action={UpdateTriageStatusAction (candidateId)}
class="inline">
{hiddenField "authenticity_token"}
<input type="hidden" name="status" value={newStatus} />
<button type="submit" class={triageButtonClass newStatus}>
{newStatus}
@@ -144,7 +140,6 @@ renderReviewerSection candidate mAssignment users = [hsx|
</div>
<form method="POST" action={AssignReviewerAction (candidate.id)}
class="flex items-center gap-2">
{hiddenField "authenticity_token"}
<select name="userId" class="text-sm border border-gray-300 rounded px-2 py-1">
{forEach users renderUserOption}
</select>
@@ -216,7 +211,6 @@ renderPromoteButton candidate =
Nothing -> [hsx|
<form method="POST"
action={PromoteToRequirementAction (candidate.id)}>
{hiddenField "authenticity_token"}
<button type="submit"
class="text-sm bg-indigo-600 text-white px-3 py-1.5 rounded hover:bg-indigo-700">
Promote to Requirement
@@ -228,7 +222,6 @@ renderLinkDecisionButton :: RequirementCandidate -> Html
renderLinkDecisionButton candidate = [hsx|
<form method="POST"
action={LinkToDecisionAction (candidate.id)}>
{hiddenField "authenticity_token"}
<button type="submit"
class="text-sm bg-gray-700 text-white px-3 py-1.5 rounded hover:bg-gray-800">
Create Decision Record

View File

@@ -85,10 +85,10 @@ instance View ExtensionGuideView where
</p>
<div class="bg-gray-900 rounded-lg p-4 text-sm font-mono text-green-400">
POST /api/v2/widgets<br/>
<span class="text-gray-400">{"{"} "name": "PR Review Widget",<br/>
<span class="text-gray-400">&#123; "name": "PR Review Widget",<br/>
&nbsp;&nbsp;"widgetType": "dev.code-review",<br/>
&nbsp;&nbsp;"hubId": "...",<br/>
&nbsp;&nbsp;"viewContext": "pull-request-sidebar" {"}"}</span>
&nbsp;&nbsp;"viewContext": "pull-request-sidebar" &#125;</span>
</div>
</section>

View File

@@ -115,9 +115,9 @@ instance View LandingView where
</div>
|]
where
chainLink label color = [hsx|
chainLink (label :: Text) (color :: Text) = [hsx|
<span class={"inline-block px-2 py-1 rounded text-xs bg-" <> color <> "-100 text-" <> color <> "-800 font-mono"}>
{label :: Text}
{label}
</span>
|]
arrow = [hsx|<span class="text-gray-400"></span>|]

View File

@@ -25,7 +25,7 @@ instance View TutorialView where
</p>
<div class="bg-gray-900 rounded-lg p-4 text-sm font-mono text-green-400">
<div class="text-gray-400 mb-1">-- Every rendered widget wraps its HSX in widgetEnvelope</div>
widgetEnvelope widgetId viewContext [hsx|...|]
{"widgetEnvelope widgetId viewContext [hsx|...|]" :: Text}
</div>
<p class="text-sm text-gray-500 mt-2">
The envelope injects <code>data-widget-id</code> and <code>data-view-context</code> attributes,

View File

@@ -1,9 +1,6 @@
module Web.View.StewardshipRoles.New where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.View.Prelude
import Web.Routes ()
data NewView = NewView
@@ -21,9 +18,12 @@ instance View NewView where
renderForm :: StewardshipRole -> [Hub] -> Html
renderForm role hubs = formFor role [hsx|
{(selectField #hubId hubs){ fieldLabel = "Hub" }}
{(selectField #hubId (hubOptions hubs)){ fieldLabel = "Hub" }}
{(textField #roleName){ helpText = "e.g. Hub Lead, Policy Steward, Triage Owner" }}
{(textField #assignedTo){ helpText = "Person name or identifier" }}
{(textareaField #notes){ fieldLabel = "Notes (optional)" }}
{submitButton}
|]
hubOptions :: [Hub] -> [(Text, Id Hub)]
hubOptions = map (\h -> (h.name, h.id))