generated from coulomb/repo-seed
A2 — Compilation fixes: - Remove inline FK constraints from Schema.sql; IHP schema compiler cannot parse them. Add 1744329600-restore-fk-constraints.sql migration to restore referential integrity at the DB level. - Rename `#label` → `#label_` throughout to avoid clash with Haskell built-in. - Fix `hub.id == hid` UUID comparisons to use `toUUID hub.id`. - Replace non-existent `setStatus`/`respondJson` calls with `renderJsonWithStatusCode` throughout Api controllers. - Fix qualified package import for `cryptohash-sha256` in Auth.hs. - Add `CanSelect (Text, Text)` instance in Helper.View. - Refactor HSX inline lambdas to named helper functions in 100+ views (GHC cannot infer types for anonymous functions inside quasi-quoted HSX). - Fix missing imports (IHP.QueryBuilder, IHP.Fetch, Web.Routes, Only, etc.) across helpers and controllers. - Remove duplicate `diffUTCTime` definition in BottleneckDetector. - Change `createEventForHub` return type from `IO ResponseReceived` to `IO ()`. - Seed type-registry vocabulary via 1744502400-seed-type-registries.sql (moved from Schema.sql where IHP does not execute INSERT statements). A3 — Tailwind build pipeline: - Add `tailwindcss` to flake.nix native packages. - Uncomment `tailwind.exec` process in devenv shell config. - Add tailwind/tailwind.config.js (scans Web/View/**/*.hs). - Add tailwind/app.css with @tailwind directives. A4 — Admin user seed: - Add 1744416000-seed-admin-user.sql: inserts admin@inter-hub.local with bcrypt-hashed password admin1234! (cost 10). - Add .env.example documenting all required environment variables and default admin credentials. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
279 lines
12 KiB
Haskell
279 lines
12 KiB
Haskell
module Web.Controller.TypeRegistries where
|
|
|
|
import Web.Types
|
|
import Web.View.TypeRegistries.WidgetTypes
|
|
import Web.View.TypeRegistries.EventTypes
|
|
import Web.View.TypeRegistries.AnnotationCategories
|
|
import Web.View.TypeRegistries.PolicyScopes
|
|
import Generated.Types
|
|
import IHP.Prelude
|
|
import IHP.ControllerPrelude
|
|
|
|
instance Controller TypeRegistriesController where
|
|
beforeAction = ensureIsUser
|
|
|
|
-- ── Widget Types ─────────────────────────────────────────────────────────
|
|
|
|
action WidgetTypeRegistryAction = do
|
|
entries <- query @WidgetTypeRegistry
|
|
|> orderByAsc #label_
|
|
|> fetch
|
|
hubs <- query @Hub |> fetch
|
|
render WidgetTypesView { entries, hubs }
|
|
|
|
action ShowWidgetTypeAction { widgetTypeRegistryId } = do
|
|
entry <- fetch widgetTypeRegistryId
|
|
mOwner <- case entry.ownerHubId of
|
|
Nothing -> pure Nothing
|
|
Just hid -> fmap Just (fetch hid)
|
|
render ShowWidgetTypeView { entry, mOwner }
|
|
|
|
action NewWidgetTypeAction = do
|
|
let entry = newRecord @WidgetTypeRegistry
|
|
hubs <- query @Hub |> fetch
|
|
render NewWidgetTypeView { entry, hubs }
|
|
|
|
action CreateWidgetTypeAction = do
|
|
let entry = newRecord @WidgetTypeRegistry
|
|
hubs <- query @Hub |> fetch
|
|
entry
|
|
|> fill @'["name", "label", "description", "ownerHubId"]
|
|
|> validateField #name nonEmpty
|
|
|> validateField #label_ nonEmpty
|
|
|> ifValid \case
|
|
Left entry -> render NewWidgetTypeView { entry, hubs }
|
|
Right entry -> do
|
|
createRecord entry
|
|
setSuccessMessage ("Widget type '" <> entry.name <> "' registered")
|
|
redirectTo WidgetTypeRegistryAction
|
|
|
|
action EditWidgetTypeAction { widgetTypeRegistryId } = do
|
|
entry <- fetch widgetTypeRegistryId
|
|
hubs <- query @Hub |> fetch
|
|
render EditWidgetTypeView { entry, hubs }
|
|
|
|
action UpdateWidgetTypeAction { widgetTypeRegistryId } = do
|
|
entry <- fetch widgetTypeRegistryId
|
|
hubs <- query @Hub |> fetch
|
|
-- name is immutable after creation
|
|
entry
|
|
|> fill @'["label", "description", "ownerHubId"]
|
|
|> validateField #label_ nonEmpty
|
|
|> ifValid \case
|
|
Left entry -> render EditWidgetTypeView { entry, hubs }
|
|
Right entry -> do
|
|
updateRecord entry
|
|
setSuccessMessage "Widget type updated"
|
|
redirectTo WidgetTypeRegistryAction
|
|
|
|
action DeprecateWidgetTypeAction { widgetTypeRegistryId } = do
|
|
entry <- fetch widgetTypeRegistryId
|
|
replacedBy <- param @Text "deprecated_in_favour_of"
|
|
when (null replacedBy) do
|
|
setErrorMessage "You must specify the replacement type name"
|
|
redirectTo WidgetTypeRegistryAction
|
|
entry
|
|
|> set #status "deprecated"
|
|
|> set #deprecatedInFavourOf (Just replacedBy)
|
|
|> updateRecord
|
|
setSuccessMessage ("Widget type '" <> entry.name <> "' deprecated")
|
|
redirectTo WidgetTypeRegistryAction
|
|
|
|
-- ── Event Types ──────────────────────────────────────────────────────────
|
|
|
|
action EventTypeRegistryAction = do
|
|
entries <- query @EventTypeRegistry
|
|
|> orderByAsc #label_
|
|
|> fetch
|
|
hubs <- query @Hub |> fetch
|
|
render EventTypesView { entries, hubs }
|
|
|
|
action ShowEventTypeAction { eventTypeRegistryId } = do
|
|
entry <- fetch eventTypeRegistryId
|
|
mOwner <- case entry.ownerHubId of
|
|
Nothing -> pure Nothing
|
|
Just hid -> fmap Just (fetch hid)
|
|
render ShowEventTypeView { entry, mOwner }
|
|
|
|
action NewEventTypeAction = do
|
|
let entry = newRecord @EventTypeRegistry
|
|
hubs <- query @Hub |> fetch
|
|
render NewEventTypeView { entry, hubs }
|
|
|
|
action CreateEventTypeAction = do
|
|
let entry = newRecord @EventTypeRegistry
|
|
hubs <- query @Hub |> fetch
|
|
entry
|
|
|> fill @'["name", "label", "description", "ownerHubId"]
|
|
|> validateField #name nonEmpty
|
|
|> validateField #label_ nonEmpty
|
|
|> ifValid \case
|
|
Left entry -> render NewEventTypeView { entry, hubs }
|
|
Right entry -> do
|
|
createRecord entry
|
|
setSuccessMessage ("Event type '" <> entry.name <> "' registered")
|
|
redirectTo EventTypeRegistryAction
|
|
|
|
action EditEventTypeAction { eventTypeRegistryId } = do
|
|
entry <- fetch eventTypeRegistryId
|
|
hubs <- query @Hub |> fetch
|
|
render EditEventTypeView { entry, hubs }
|
|
|
|
action UpdateEventTypeAction { eventTypeRegistryId } = do
|
|
entry <- fetch eventTypeRegistryId
|
|
hubs <- query @Hub |> fetch
|
|
entry
|
|
|> fill @'["label", "description", "ownerHubId"]
|
|
|> validateField #label_ nonEmpty
|
|
|> ifValid \case
|
|
Left entry -> render EditEventTypeView { entry, hubs }
|
|
Right entry -> do
|
|
updateRecord entry
|
|
setSuccessMessage "Event type updated"
|
|
redirectTo EventTypeRegistryAction
|
|
|
|
action DeprecateEventTypeAction { eventTypeRegistryId } = do
|
|
entry <- fetch eventTypeRegistryId
|
|
replacedBy <- param @Text "deprecated_in_favour_of"
|
|
when (null replacedBy) do
|
|
setErrorMessage "You must specify the replacement type name"
|
|
redirectTo EventTypeRegistryAction
|
|
entry
|
|
|> set #status "deprecated"
|
|
|> set #deprecatedInFavourOf (Just replacedBy)
|
|
|> updateRecord
|
|
setSuccessMessage ("Event type '" <> entry.name <> "' deprecated")
|
|
redirectTo EventTypeRegistryAction
|
|
|
|
-- ── Annotation Categories ────────────────────────────────────────────────
|
|
|
|
action AnnotationCategoryRegistryAction = do
|
|
entries <- query @AnnotationCategoryRegistry
|
|
|> orderByAsc #label_
|
|
|> fetch
|
|
hubs <- query @Hub |> fetch
|
|
render AnnotationCategoriesView { entries, hubs }
|
|
|
|
action ShowAnnotationCategoryAction { annotationCategoryRegistryId } = do
|
|
entry <- fetch annotationCategoryRegistryId
|
|
mOwner <- case entry.ownerHubId of
|
|
Nothing -> pure Nothing
|
|
Just hid -> fmap Just (fetch hid)
|
|
render ShowAnnotationCategoryView { entry, mOwner }
|
|
|
|
action NewAnnotationCategoryAction = do
|
|
let entry = newRecord @AnnotationCategoryRegistry
|
|
hubs <- query @Hub |> fetch
|
|
render NewAnnotationCategoryView { entry, hubs }
|
|
|
|
action CreateAnnotationCategoryAction = do
|
|
let entry = newRecord @AnnotationCategoryRegistry
|
|
hubs <- query @Hub |> fetch
|
|
entry
|
|
|> fill @'["name", "label", "description", "ownerHubId"]
|
|
|> validateField #name nonEmpty
|
|
|> validateField #label_ nonEmpty
|
|
|> ifValid \case
|
|
Left entry -> render NewAnnotationCategoryView { entry, hubs }
|
|
Right entry -> do
|
|
createRecord entry
|
|
setSuccessMessage ("Annotation category '" <> entry.name <> "' registered")
|
|
redirectTo AnnotationCategoryRegistryAction
|
|
|
|
action EditAnnotationCategoryAction { annotationCategoryRegistryId } = do
|
|
entry <- fetch annotationCategoryRegistryId
|
|
hubs <- query @Hub |> fetch
|
|
render EditAnnotationCategoryView { entry, hubs }
|
|
|
|
action UpdateAnnotationCategoryAction { annotationCategoryRegistryId } = do
|
|
entry <- fetch annotationCategoryRegistryId
|
|
hubs <- query @Hub |> fetch
|
|
entry
|
|
|> fill @'["label", "description", "ownerHubId"]
|
|
|> validateField #label_ nonEmpty
|
|
|> ifValid \case
|
|
Left entry -> render EditAnnotationCategoryView { entry, hubs }
|
|
Right entry -> do
|
|
updateRecord entry
|
|
setSuccessMessage "Annotation category updated"
|
|
redirectTo AnnotationCategoryRegistryAction
|
|
|
|
action DeprecateAnnotationCategoryAction { annotationCategoryRegistryId } = do
|
|
entry <- fetch annotationCategoryRegistryId
|
|
replacedBy <- param @Text "deprecated_in_favour_of"
|
|
when (null replacedBy) do
|
|
setErrorMessage "You must specify the replacement category name"
|
|
redirectTo AnnotationCategoryRegistryAction
|
|
entry
|
|
|> set #status "deprecated"
|
|
|> set #deprecatedInFavourOf (Just replacedBy)
|
|
|> updateRecord
|
|
setSuccessMessage ("Annotation category '" <> entry.name <> "' deprecated")
|
|
redirectTo AnnotationCategoryRegistryAction
|
|
|
|
-- ── Policy Scopes ────────────────────────────────────────────────────────
|
|
|
|
action PolicyScopeRegistryAction = do
|
|
entries <- query @PolicyScopeRegistry
|
|
|> orderByAsc #label_
|
|
|> fetch
|
|
hubs <- query @Hub |> fetch
|
|
render PolicyScopesView { entries, hubs }
|
|
|
|
action ShowPolicyScopeAction { policyScopeRegistryId } = do
|
|
entry <- fetch policyScopeRegistryId
|
|
mOwner <- case entry.ownerHubId of
|
|
Nothing -> pure Nothing
|
|
Just hid -> fmap Just (fetch hid)
|
|
render ShowPolicyScopeView { entry, mOwner }
|
|
|
|
action NewPolicyScopeAction = do
|
|
let entry = newRecord @PolicyScopeRegistry
|
|
hubs <- query @Hub |> fetch
|
|
render NewPolicyScopeView { entry, hubs }
|
|
|
|
action CreatePolicyScopeAction = do
|
|
let entry = newRecord @PolicyScopeRegistry
|
|
hubs <- query @Hub |> fetch
|
|
entry
|
|
|> fill @'["name", "label", "description", "ownerHubId"]
|
|
|> validateField #name nonEmpty
|
|
|> validateField #label_ nonEmpty
|
|
|> ifValid \case
|
|
Left entry -> render NewPolicyScopeView { entry, hubs }
|
|
Right entry -> do
|
|
createRecord entry
|
|
setSuccessMessage ("Policy scope '" <> entry.name <> "' registered")
|
|
redirectTo PolicyScopeRegistryAction
|
|
|
|
action EditPolicyScopeAction { policyScopeRegistryId } = do
|
|
entry <- fetch policyScopeRegistryId
|
|
hubs <- query @Hub |> fetch
|
|
render EditPolicyScopeView { entry, hubs }
|
|
|
|
action UpdatePolicyScopeAction { policyScopeRegistryId } = do
|
|
entry <- fetch policyScopeRegistryId
|
|
hubs <- query @Hub |> fetch
|
|
entry
|
|
|> fill @'["label", "description", "ownerHubId"]
|
|
|> validateField #label_ nonEmpty
|
|
|> ifValid \case
|
|
Left entry -> render EditPolicyScopeView { entry, hubs }
|
|
Right entry -> do
|
|
updateRecord entry
|
|
setSuccessMessage "Policy scope updated"
|
|
redirectTo PolicyScopeRegistryAction
|
|
|
|
action DeprecatePolicyScopeAction { policyScopeRegistryId } = do
|
|
entry <- fetch policyScopeRegistryId
|
|
replacedBy <- param @Text "deprecated_in_favour_of"
|
|
when (null replacedBy) do
|
|
setErrorMessage "You must specify the replacement scope name"
|
|
redirectTo PolicyScopeRegistryAction
|
|
entry
|
|
|> set #status "deprecated"
|
|
|> set #deprecatedInFavourOf (Just replacedBy)
|
|
|> updateRecord
|
|
setSuccessMessage ("Policy scope '" <> entry.name <> "' deprecated")
|
|
redirectTo PolicyScopeRegistryAction
|