From a71db095dbe9ed3abd02e6d392427a10371e1774 Mon Sep 17 00:00:00 2001 From: patritzenfeld Date: Fri, 27 Mar 2026 16:44:01 +0100 Subject: [PATCH 01/12] add Optional SingleChoice fields and do not allow empty selection in mandatory dropdowns --- .../src/FlexTask/Generic/FormInternal.hs | 33 ++++++++++++++-- flex-tasks/src/FlexTask/Widgets.hs | 39 ++++++++++++++++++- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/flex-tasks/src/FlexTask/Generic/FormInternal.hs b/flex-tasks/src/FlexTask/Generic/FormInternal.hs index a08d4f12..1e332266 100644 --- a/flex-tasks/src/FlexTask/Generic/FormInternal.hs +++ b/flex-tasks/src/FlexTask/Generic/FormInternal.hs @@ -34,7 +34,6 @@ import Yesod ( multiSelectField, optionsPairs, renderMessage, - selectField, textareaField, textField, ) @@ -45,6 +44,7 @@ import FlexTask.Widgets , radioField , joinWidgets , renderForm + , selectField ) import FlexTask.YesodConfig (FlexForm(..), Handler, Rendered, Widget) @@ -476,7 +476,7 @@ instance Formify [String] where formifyImplementation = formifyInstanceList -instance (BaseForm a, Formify a) => Formify (Maybe a) where +instance {-# Overlappable #-} (BaseForm a, Formify a) => Formify (Maybe a) where formifyImplementation = formifyInstanceOptionalField @@ -491,7 +491,8 @@ instance Formify SingleChoiceSelection where instance Formify MultipleChoiceSelection where formifyImplementation = renderNextMultipleChoiceField (`zip` [1..]) . fmap getAnswers - +instance Formify (Maybe SingleChoiceSelection) where + formifyImplementation = renderNextOptionalSingleChoiceField (`zip` [1..]) . (=<<) (fmap getAnswer) {- | This is the main way to build generic forms. @@ -796,7 +797,7 @@ renderNextSingleChoiceField pairsWith = (\case ChoicesDropdown fs opts -> ( fs - , areq $ selectField $ withOptions opts + , areq $ selectField True $ withOptions opts ) ChoicesButtons align fs opts -> ( fs @@ -809,6 +810,30 @@ renderNextSingleChoiceField pairsWith = ) where withOptions = optionsPairs . pairsWith +renderNextOptionalSingleChoiceField + :: Eq a + => ([SomeMessage FlexForm] -> [(SomeMessage FlexForm, a)]) + -> Maybe (Maybe a) + -> [[FieldInfo]] + -> ([[FieldInfo]], Rendered [[Widget]]) +renderNextOptionalSingleChoiceField pairsWith = + renderNextField + (\case + ChoicesDropdown fs opts -> + ( fs + , aopt $ selectField False $ withOptions opts + ) + ChoicesButtons align fs opts -> + ( fs + , aopt $ case align of + Vertical -> radioField True + Horizontal -> radioField False + $ withOptions opts + ) + _ -> error "Incorrect FieldInfo for a single choice field! Use one of the 'buttons' or 'dropdown' functions." + ) + where withOptions = optionsPairs . pairsWith + renderNextMultipleChoiceField :: Eq a => ([SomeMessage FlexForm] -> [(SomeMessage FlexForm, a)]) diff --git a/flex-tasks/src/FlexTask/Widgets.hs b/flex-tasks/src/FlexTask/Widgets.hs index bd10a8bd..fa290be8 100644 --- a/flex-tasks/src/FlexTask/Widgets.hs +++ b/flex-tasks/src/FlexTask/Widgets.hs @@ -60,13 +60,24 @@ joinWidgets = mapM_ (insertDiv . sequence_) radioField :: Eq a => Bool -> Handler (OptionList a) -> Field Handler a -radioField isVertical = selectFieldHelper outside (\_ _ _ -> pure ()) inside Nothing +radioField isVertical = selectFieldHelper outside onOpt inside Nothing where outside theId _name _attrs inside' = toWidget horizontalRBStyle >> [whamlet| $newline never
^{inside'} +|] + onOpt theId name isSel = nothingFun theId [whamlet| +$newline never + +|] + nothingFun theId optionWidget = [whamlet| +$newline never +<.radio> + ^{optionWidget} + -$if req -
-} newtype MultipleChoiceSelection = MultipleChoiceSelection - { getAnswers :: [Int] -- ^ Retrieve the list of selected options. @[]@ if none. + { getAnswers :: [Int] -- ^ Retrieve the list of selected options. @[]@ if none are selected. } deriving (Show,Eq,Generic) +{-# DEPRECATED singleChoiceEmpty + "This function only existed to satisfy a legacy interface in Autotool. It will be removed in a future version." + #-} -- | Value with no option selected. singleChoiceEmpty :: SingleChoiceSelection -singleChoiceEmpty = SingleChoiceSelection Nothing - +singleChoiceEmpty = singleChoiceAnswer 0 -- | Value with given number option selected. singleChoiceAnswer :: Int -> SingleChoiceSelection -singleChoiceAnswer = SingleChoiceSelection . Just +singleChoiceAnswer = SingleChoiceSelection -- | Value with no options selected. @@ -488,14 +490,14 @@ instance Formify (Maybe a) => Formify [Maybe a] where instance Formify SingleChoiceSelection where - formifyImplementation = renderNextSingleChoiceField (`zip` [1..]) . (=<<) getAnswer + formifyImplementation = renderNextSingleChoiceField (`zip` [1..]) . (=<<) (Just . getAnswer) instance Formify MultipleChoiceSelection where formifyImplementation = renderNextMultipleChoiceField (`zip` [1..]) . fmap getAnswers instance Formify (Maybe SingleChoiceSelection) where - formifyImplementation = renderNextOptionalSingleChoiceField (`zip` [1..]) . (=<<) (fmap getAnswer) + formifyImplementation = renderNextOptionalSingleChoiceField (`zip` [1..]) . (=<<) (fmap (Just . getAnswer)) {- | This is the main way to build generic forms. diff --git a/flex-tasks/src/FlexTask/Generic/ParseInternal.hs b/flex-tasks/src/FlexTask/Generic/ParseInternal.hs index ef08704a..d7861c7f 100644 --- a/flex-tasks/src/FlexTask/Generic/ParseInternal.hs +++ b/flex-tasks/src/FlexTask/Generic/ParseInternal.hs @@ -72,7 +72,6 @@ import FlexTask.Generic.FormInternal , SingleInputList(..) , multipleChoiceAnswer , singleChoiceAnswer - , singleChoiceEmpty ) @@ -233,7 +232,7 @@ instance Parse a => Parse (Maybe a) where instance Parse SingleChoiceSelection where - formParser = maybe singleChoiceEmpty singleChoiceAnswer <$> formParser + formParser = singleChoiceAnswer <$> formParser instance Parse MultipleChoiceSelection where @@ -414,7 +413,7 @@ the input form is "infallible" since only constructed from String text fields, s >>> import Control.OutputCapable.Blocks.Debug (run) >>> run German $ parseInfallibly (formParser @SingleChoiceSelection) $ asSubmission [["1"]] -Just (SingleChoiceSelection {getAnswer = Just 1}) +Just (SingleChoiceSelection {getAnswer = 1}) >>> run English $ parseInfallibly (formParser @(SingleInputList Double)) $ asSubmission [["Wrong input"]] *** Exception: The impossible happened: (line 1, column 3): From d6b496d3a512d9cb7d189601352ba4a0c7af9e39 Mon Sep 17 00:00:00 2001 From: patritzenfeld Date: Tue, 31 Mar 2026 18:01:48 +0200 Subject: [PATCH 05/12] allow html tag spelling --- .github/actions/spelling/expect.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 4a735744..393a65bc 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -13,6 +13,7 @@ hasdata img lindex lucius +optgroup RWS syb subdirs From 92f55835851183e9e3cb94e65ada917a7fd106d1 Mon Sep 17 00:00:00 2001 From: patritzenfeld Date: Wed, 1 Apr 2026 10:35:46 +0200 Subject: [PATCH 06/12] adjust comment --- flex-tasks/src/FlexTask/Widgets.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flex-tasks/src/FlexTask/Widgets.hs b/flex-tasks/src/FlexTask/Widgets.hs index 39f37235..a0d9613a 100644 --- a/flex-tasks/src/FlexTask/Widgets.hs +++ b/flex-tasks/src/FlexTask/Widgets.hs @@ -147,7 +147,7 @@ $newline never (\_theId _name isSel -> [whamlet| $newline never