generated from coulomb/repo-seed
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ? \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|]
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (1–5) <span class="text-red-500">*</span>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 <hub-api-key>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"{"}
|
||||
-d '{
|
||||
"widget_id": "<uuid>",
|
||||
"hub_id": "<uuid>",
|
||||
"event_type": "clicked",
|
||||
"occurred_at": "2026-03-29T12:00:00Z"
|
||||
{"}"}'</pre>
|
||||
}'</pre>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-xs text-gray-400">
|
||||
|
||||
@@ -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>
|
||||
|]
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">{ "name": "PR Review Widget",<br/>
|
||||
"widgetType": "dev.code-review",<br/>
|
||||
"hubId": "...",<br/>
|
||||
"viewContext": "pull-request-sidebar" {"}"}</span>
|
||||
"viewContext": "pull-request-sidebar" }</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -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>|]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user