module Handler.Utils.Workflow.EdgeForm ( WorkflowEdgeForm(..) , workflowEdgeForm, WorkflowEdgeFormException(..) , workflowEdgeFormToAction ) where import Import hiding (StateT) import Utils.Form import Utils.Workflow import Handler.Utils.Form import Handler.Utils.Workflow.CanonicalRoute import Handler.Utils.Widgets import Handler.Utils.Workflow.Restriction import qualified ListT import Data.RFC5051 (compareUnicode) import qualified Data.Text as Text import Text.Unidecode (unidecode) import qualified Data.Map as Map import Data.Map ((!), (!?)) import qualified Data.Set as Set import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap import qualified Crypto.MAC.KMAC as Crypto import qualified Crypto.Saltine.Class as Saltine import qualified Data.Binary as Binary import qualified Data.ByteArray as BA import Crypto.Hash.Algorithms (SHAKE256) import qualified Control.Monad.State.Class as State import Control.Monad.Trans.RWS.Lazy (runRWST, mapRWST) import Control.Monad.Trans.State.Strict (execState, evalStateT) import Control.Monad.Trans.RWS.Strict (RWST, evalRWST) import Data.Bitraversable import Data.List (findIndex) import qualified Data.List as List (delete) import qualified Data.Aeson as Aeson import qualified Data.Scientific as Scientific import Numeric.Lens (subtracting) import qualified Data.Conduit.Combinators as C import qualified Database.Esqueleto as E import qualified Topograph import qualified Text.Blaze as Blaze import qualified Text.Blaze.Renderer.Text as Blaze {-# ANN module ("HLint: ignore Use newtype instead of data"::String) #-} data WorkflowEdgeForm = WorkflowEdgeForm { wefEdge :: (WorkflowGraphNodeLabel, WorkflowGraphEdgeLabel) , wefPayload :: Map WorkflowPayloadLabel (Set (WorkflowFieldPayloadW FileReference UserId)) } data WorkflowEdgeFormException = WorkflowEdgeFormPayloadFieldReferenceCycle [WorkflowPayloadLabel] deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving anyclass (Exception) workflowEdgeForm :: ( MonadHandler m , HandlerSite m ~ UniWorX , MonadHandler m' , HandlerSite m' ~ UniWorX , MonadCatch m' ) => Either WorkflowInstanceId WorkflowWorkflowId -> Maybe WorkflowEdgeForm -> SqlPersistT m' (Maybe (AForm m WorkflowEdgeForm)) workflowEdgeForm mwwId mPrev = runMaybeT $ do MsgRenderer mr <- getMsgRenderer ctx' <- bitraverse (MaybeT . getEntity) (MaybeT . getEntity) mwwId let (scope, graph) = case ctx of Left WorkflowInstance{..} -> ( _DBWorkflowScope # workflowInstanceScope , _DBWorkflowGraph # workflowInstanceGraph ) Right WorkflowWorkflow{..} -> ( _DBWorkflowScope # workflowWorkflowScope , _DBWorkflowGraph # workflowWorkflowGraph ) wState = ctx ^? _Right . _workflowWorkflowState . to last . _wpTo wPayload' = ctx ^? _Right . _workflowWorkflowState . re _DBWorkflowState ctx = bimap entityVal entityVal ctx' mAuthId <- maybeAuthId wPayload <- case mwwId of Right wwId -> workflowStateCurrentPayloads <$> filterM (lift . hoist liftHandler . mayViewWorkflowAction mAuthId wwId) (maybe [] otoList wPayload') Left _ -> return Map.empty rScope <- toRouteWorkflowScope scope -- edges :: [((WorkflowGraphNodeLabel, WorkflowGraphEdgeLabel), (I18nText, Map WorkflowPayloadLabel (NonNull (Set (WorkflowPayloadSpec FileReference UserId)))))] edges <- ListT.toList $ do (nodeLabel, WGN{..}) <- ListT.fromFoldable . Map.toList $ wgNodes graph (edgeLabel, edge) <- ListT.fromFoldable $ Map.toList wgnEdges ((nodeLabel, edgeLabel), ) <$> case edge of WorkflowGraphEdgeManual{..} -> do guard $ Just wgeSource == wState wwId <- hoistMaybe $ mwwId ^? _Right cID <- lift $ encrypt wwId guardM . anyM (Set.toList wgeActors) $ \role -> lift . lift $ is _Authorized <$> hasWorkflowRole (Just wwId) role (_WorkflowScopeRoute # (rScope, WorkflowWorkflowR cID WWWorkflowR)) True return (wgeDisplayLabel, (wgeForm, wgeMessages)) WorkflowGraphEdgeInitial{..} -> do guard $ is _Nothing wState win <- hoistMaybe $ ctx ^? _Left . _workflowInstanceName guardM . anyM (Set.toList wgeActors) $ \role -> lift . lift $ is _Authorized <$> hasWorkflowRole Nothing role (_WorkflowScopeRoute # (rScope, WorkflowInstanceR win WIInitiateR)) True return (wgeDisplayLabel, (wgeForm, wgeMessages)) _other -> mzero guard . not $ null edges -- edgesOptList :: OptionList (WorkflowGraphNodeLabel, WorkflowGraphEdgeLabel) edgesOptList <- do sBoxKey <- secretBoxKey let olReadExternal ciphertext = do edgeIdent <- fromMaybeT . exceptTMaybe $ encodedSecretBoxOpen' sBoxKey ciphertext guard $ any (\(edgeIdent', _) -> edgeIdent == edgeIdent') edges return edgeIdent olOptions' <- ListT.toList $ do (edgeIdent, (edgeLabel, _)) <- ListT.fromFoldable edges optionDisplay <- lift $ selectLanguageI18n edgeLabel let optionInternalValue = edgeIdent optionExternalValue <- encodedSecretBox' sBoxKey SecretBoxShort edgeIdent return Option{..} let olOptions = concat $ do let optSort = (compareUnicode `on` (Text.toLower . optionDisplay)) <> comparing (fallbackSortKey . optionInternalValue) where fallbackSortKey = toDigest . kmaclazy ("workflow-edge-sorting" :: ByteString) (Saltine.encode sBoxKey) . Binary.encode . (mwwId, ) where toDigest :: Crypto.KMAC (SHAKE256 256) -> ByteString toDigest = BA.convert opts <- sortBy optSort olOptions' & map (\opt@Option{..} -> (Text.concatMap (pack . unidecode) optionDisplay, opt)) & foldr (\(k, v) -> InsOrdHashMap.insertWith (<>) k [v]) InsOrdHashMap.empty & InsOrdHashMap.elems if | [_] <- opts -> return opts | otherwise -> do return $ zipWith (\Option{..} i -> Option{ optionDisplay = mr $ MsgWorkflowEdgeNumberedVariant optionDisplay i, ..}) opts [1..] return OptionList{..} let edges' = flip sortOn edges $ \(edgeIdent, _) -> flip findIndex (olOptions edgesOptList) $ (== edgeIdent) . optionInternalValue let edgeForms :: Map (WorkflowGraphNodeLabel, WorkflowGraphEdgeLabel) (AForm Handler WorkflowEdgeForm) edgeForms = Map.fromList . flip map edges' $ \(edgeIdent@(tState, _), (_, (WorkflowGraphEdgeForm{..}, edgeMessages))) -> (edgeIdent, ) . fmap (WorkflowEdgeForm edgeIdent) . wFormToAForm . fmap sequenceA $ do forM_ edgeMessages $ \WorkflowEdgeMessage{..} -> void . runMaybeT $ do let hasWorkflowRole' role = liftHandler . runDB $ case ctx' of Right (Entity wwId _) -> do cID <- encrypt wwId is _Authorized <$> hasWorkflowRole (Just wwId) role (_WorkflowScopeRoute # (rScope, WorkflowWorkflowR cID WWWorkflowR)) True Left (Entity _ WorkflowInstance{..}) -> is _Authorized <$> hasWorkflowRole Nothing role (_WorkflowScopeRoute # (rScope, WorkflowInstanceR workflowInstanceName WIInitiateR)) True guardM $ anyM (otoList wemViewers) hasWorkflowRole' whenIsJust wemRestriction $ guard . checkWorkflowRestriction wPayload' let messageStatus = wemStatus messageIcon = Nothing messageContent <- selectLanguageI18n wemContent lift $ wformMessage Message{..} let fieldSort :: [(WorkflowPayloadLabel, [[(Either WorkflowGraphEdgeFormOrder ByteString, WorkflowPayloadSpec FileReference UserId)]])] -> _ fieldSort = sortOn ((,) <$> foldOf (_2 . folded . folded . _1 . _Left) <*> foldMapOf (_2 . folded . folded . _1 . _Right) (Just . Min)) . over (traverse . _2) (sortOn $ (,) <$> foldOf (folded . _1 . _Left) <*> foldMapOf (folded . _1 . _Right) (Just . Min)) . over (traverse . _2 . traverse) (sortOn $ (,) <$> preview (_1 . _Left) <*> preview (_1 . _Right)) orderedFields <- lift . lift . fmap fieldSort . for (Map.toList wgefFields) $ \(payloadLabel, Set.toList . toNullable -> payloadSpecs) -> fmap (payloadLabel, ) . for payloadSpecs $ \(Map.toList . toNullable -> payloadSpecs') -> for payloadSpecs' $ \(payloadOrder, payloadSpec) -> if | payloadOrder /= mempty -> return (Left payloadOrder, payloadSpec) | otherwise -> do sBoxKey <- secretBoxKey payloadSpec' <- traverseOf (typesCustom @WorkflowChildren @(WorkflowPayloadSpec FileReference UserId) @(WorkflowPayloadSpec FileReference CryptoUUIDUser) @UserId @CryptoUUIDUser) encrypt payloadSpec let toDigest :: Crypto.KMAC (SHAKE256 256) -> ByteString toDigest = BA.convert fallbackSortKey = toDigest . kmaclazy ("workflow-edge-form-payload-field-sorting" :: ByteString) (Saltine.encode sBoxKey) $ Aeson.encode (mwwId, payloadSpec') return (Right fallbackSortKey, payloadSpec) orderedFields' <- flip evalStateT 1 . for orderedFields $ \x@(payloadLabel, _) -> do let generateDisplayLabel = State.state $ \n -> (mr $ MsgWorkflowEdgeFormHiddenPayload n, succ n) (mayView, payloadDisplayLabel) <- hoist (lift . lift . runDB) . maybeT ((False, ) <$> generateDisplayLabel) $ let displayNameFromState s = do WorkflowPayloadView{..} <- hoistMaybe . Map.lookup payloadLabel $ Map.findWithDefault Map.empty s (wgnPayloadView <$> wgNodes graph) wRoute <- case ctx' of Right (Entity wwId _) -> do cID <- encrypt wwId return $ _WorkflowScopeRoute # (rScope, WorkflowWorkflowR cID WWWorkflowR) Left (Entity _ WorkflowInstance{..}) -> return $ _WorkflowScopeRoute # (rScope, WorkflowInstanceR workflowInstanceName WIInitiateR) guardM . anyM (Set.toList $ toNullable wpvViewers) $ \role -> lift . lift $ is _Authorized <$> hasWorkflowRole (mwwId ^? _Right) role wRoute False (True, ) <$> selectLanguageI18n wpvDisplayLabel in displayNameFromState tState <|> maybe mzero displayNameFromState wState return ((mayView, payloadDisplayLabel), x) fields <- for orderedFields' $ \((mayView, payloadDisplayLabel), (payloadLabel, payloadSpecs)) -> (payloadLabel, ) <$> do let payloadSpecs' = payloadSpecs ^.. folded . folded . _2 payloadFields = workflowEdgePayloadFields payloadSpecs' $ fmap otoList . Map.lookup payloadLabel =<< prevSrc where prevSrc = asum [ wefPayload <$> assertM ((== edgeIdent) . wefEdge) mPrev , guardOn mayView wPayload ] ((payloadRes, isOptional), payloadFieldViews) <- wFormFields payloadFields return ((payloadDisplayLabel, getAll isOptional), (payloadRes, payloadFieldViews)) fields' <- let payloadReferenceAdjacency = fieldsMap <&> setOf (_2 . _1 . folded . _Left) fieldsMap :: Map WorkflowPayloadLabel ((Text, Bool), ([Either WorkflowPayloadLabel (Bool, FormResult (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId))))], [FieldView UniWorX])) fieldsMap = Map.fromList fields resolveReferences :: forall i. Topograph.G WorkflowPayloadLabel i -> [(WorkflowPayloadLabel, ((Text, Bool), ([(Bool, FormResult (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId))))], [FieldView UniWorX])))] resolveReferences Topograph.G{gVertices, gFromVertex} = resort . Map.toList . flip execState Map.empty . for topoOrder $ \payloadLabel -> whenIsJust (Map.lookup payloadLabel fieldsMap) $ \(payloadDisplay, (payloadRes, payloadFieldViews)) -> State.modify' $ \oldState -> let payloadRes' = flip concatMap payloadRes $ \case Right res -> pure res Left ref -> Map.lookup ref oldState ^. _Just . _2 . _1 in Map.insert payloadLabel (payloadDisplay, (payloadRes', payloadFieldViews)) oldState where topoOrder = map gFromVertex gVertices resort = sortOn $ \(payloadLabel, _) -> findIndex (views _1 (== payloadLabel)) fields in either (throwM . WorkflowEdgeFormPayloadFieldReferenceCycle) return $ Topograph.runG payloadReferenceAdjacency resolveReferences fmap Map.fromList . for fields' $ \(payloadLabel, ((payloadDisplayLabel, isOptional), (payloadRes, payloadFieldViews))) -> (payloadLabel, ) <$> do $logWarnS "WorkflowEdgeForm" $ toPathPiece payloadLabel <> ": " <> tshow payloadRes let payloadRes' = let res = foldMap (views _2 . fmap $ maybe Set.empty (Set.fromList . otoList)) payloadRes in if | doErrMsg -> FormFailure $ view _FormFailure res ++ pure (mr $ MsgWorkflowEdgeFormPayloadOneFieldRequiredFor payloadDisplayLabel) | otherwise -> res doErrMsg = flip none payloadRes $ \res -> view _1 res || hasn't (_2 . _FormSuccess) res addErrMsg pErrs = Just [shamlet| $newline never $maybe errs <- pErrs #{errs}
#{mr MsgWorkflowEdgeFormPayloadOneFieldRequired} |] case payloadFieldViews of [] -> return () [fv] -> lift . tell . pure $ fv & _fvRequired .~ not isOptional & _fvErrors %~ bool id addErrMsg doErrMsg _other -> do fvId <- newIdent let fvLabel = toHtml payloadDisplayLabel fvTooltip = Nothing fvInput = renderFieldViews FormWorkflowDataset $ payloadFieldViews & traverse . _fvRequired .~ not isOptional fvErrors = bool id addErrMsg doErrMsg Nothing fvRequired = not isOptional in lift . tell $ pure FieldView{..} return payloadRes' return . hoistAForm liftHandler . multiActionAOpts edgeForms (return edgesOptList) actFS $ wefEdge <$> mPrev where actFS = fslI MsgWorkflowEdgeFormEdge workflowEdgePayloadFields :: [WorkflowPayloadSpec FileReference UserId] -> Maybe [WorkflowFieldPayloadW FileReference UserId] -> WForm Handler ([Either WorkflowPayloadLabel (Bool, FormResult (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId))))], All) -- ^ @isFilled@, @foldMap ala All . map isOptional@ workflowEdgePayloadFields specs = evalRWST (forM specs $ runExceptT . renderSpecField) Nothing . fromMaybe [] where renderSpecField :: WorkflowPayloadSpec FileReference UserId -> ExceptT WorkflowPayloadLabel (RWST (Maybe (Text -> Text)) All [WorkflowFieldPayloadW FileReference UserId] (MForm (WriterT [FieldView UniWorX] Handler))) (Bool, FormResult (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId)))) renderSpecField (WorkflowPayloadSpec (specField :: WorkflowPayloadField FileReference UserId payload)) = do let f' :: forall payload' payload''. _ => (payload' -> Maybe (NonEmpty payload'')) -> Bool -- ^ @isOpt@ -> Field Handler payload' -> FieldSettings UniWorX -> Maybe payload' -> _ (Bool, FormResult (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId)))) f' toNonEmpty' isOpt fld fs mx = lift . (<* tell (All isOpt)) . lift $ over (_2 . mapped) (fmap (fmap . review $ _WorkflowFieldPayloadW . _WorkflowFieldPayload) . toNonEmpty' =<<) . bool (is (_FormSuccess . _Just) &&& id) (True, ) isOpt <$> wopt fld fs (Just <$> mx) f :: forall payload'. _ => Bool -- ^ @isOpt@ -> Field Handler payload' -> FieldSettings UniWorX -> Maybe payload' -> _ (Bool, FormResult (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId)))) f = f' (nonEmpty . pure) extractPrevs :: forall payload' m xs. ( IsWorkflowFieldPayload' FileReference UserId payload' , State.MonadState [WorkflowFieldPayloadW FileReference UserId] m ) => (payload' -> Maybe xs -> Maybe xs) -> m (Maybe xs) extractPrevs accum = State.state $ foldl' go (Nothing, []) . map (matching $ _WorkflowFieldPayloadW @payload' @FileReference @UserId . _WorkflowFieldPayload) where go (mPrev', xs) (Left x) = (mPrev', xs ++ [x]) go (acc, xs) (Right p) = case accum p acc of acc'@(Just _) -> (acc', xs) Nothing -> (acc, xs ++ [_WorkflowFieldPayloadW @payload' @FileReference @UserId . _WorkflowFieldPayload # p]) extractPrev :: forall payload' m. ( IsWorkflowFieldPayload' FileReference UserId payload' , State.MonadState [WorkflowFieldPayloadW FileReference UserId] m ) => m (Maybe payload') extractPrev = extractPrevs $ \p -> \case Nothing -> Just p Just _ -> Nothing wSetTooltip' :: _ => Maybe Html -> t' (t (MForm (WriterT [FieldView UniWorX] Handler))) a -> t' (t (MForm (WriterT [FieldView UniWorX] Handler))) a wSetTooltip' tip = hoist (hoist (wSetTooltip tip)) MsgRenderer mr <- getMsgRenderer LanguageSelectI18n{..} <- getLanguageSelectI18n mNudge <- ask case specField of WorkflowPayloadFieldText{..} -> do prev <- extractPrev @Text wSetTooltip' (fmap slI18n wpftTooltip) $ f wpftOptional (bool (textField & cfStrip) (textareaField & isoField _Wrapped & cfStrip) wpftLarge) ( fsl (slI18n wpftLabel) & maybe id (addPlaceholder . slI18n) wpftPlaceholder & maybe id (addName . ($ "text")) mNudge ) (prev <|> wpftDefault) WorkflowPayloadFieldNumber{..} -> do prev <- extractPrev @Scientific wSetTooltip' (fmap slI18n wpfnTooltip) $ f wpfnOptional ( fractionalField & maybe id (\wpfnMin' -> checkBool (>= wpfnMin') $ MsgWorkflowEdgeFormFieldNumberTooSmall wpfnMin') wpfnMin & maybe id (\wpfnMax' -> checkBool (>= wpfnMax') $ MsgWorkflowEdgeFormFieldNumberTooSmall wpfnMax') wpfnMax & maybe id (\wpfnStep' -> flip convertField id . over (maybe id subtracting wpfnMin) $ \n -> fromInteger (round $ n / wpfnStep') * wpfnStep') wpfnStep ) ( fsl (slI18n wpfnLabel) & maybe id (addPlaceholder . slI18n) wpfnPlaceholder & maybe id (addAttr "min" . tshow . formatScientific Scientific.Fixed Nothing) wpfnMin & maybe id (addAttr "max" . tshow . formatScientific Scientific.Fixed Nothing) wpfnMax & maybe (addAttr "step" "any") (addAttr "step" . tshow . formatScientific Scientific.Fixed Nothing) wpfnStep & maybe id (addName . ($ "number")) mNudge ) (prev <|> wpfnDefault) WorkflowPayloadFieldBool{..} -> do prev <- extractPrev @Bool wSetTooltip' (fmap slI18n wpfbTooltip) $ f (is _Just wpfbOptional) (maybe checkBoxField (boolField . Just . SomeMessage . slI18n) wpfbOptional) ( fsl (slI18n wpfbLabel) & maybe id (addName . ($ "bool")) mNudge ) (prev <|> wpfbDefault) WorkflowPayloadFieldDay{..} -> do prev <- extractPrev @Day wSetTooltip' (fmap slI18n wpfdTooltip) $ f wpfdOptional dayField ( fsl (slI18n wpfdLabel) & maybe id (addName . ($ "day")) mNudge ) (prev <|> wpfdDefault) WorkflowPayloadFieldFile{..} -> do fRefs <- extractPrevs @FileReference $ \p -> if | fieldMultiple wpffConfig -> Just . maybe (Set.singleton p) (Set.insert p) | otherwise -> \case Nothing -> Just $ Set.singleton p Just _ -> Nothing let wpffConfig' = wpffConfig & _fieldAdditionalFiles %~ (fRefs' <>) where fRefs' = review _FileReferenceFileReferenceTitleMap . Map.fromList $ do FileReference{..} <- Set.toList =<< hoistMaybe fRefs return (fileReferenceTitle, (fileReferenceContent, fileReferenceModified, FileFieldUserOption False True)) wSetTooltip' (fmap slI18n wpffTooltip) $ f' (nonEmpty . Set.toList) wpffOptional (convertFieldM (\p -> runConduit $ transPipe liftHandler p .| C.foldMap Set.singleton) yieldMany . genericFileField $ return wpffConfig') ( fsl (slI18n wpffLabel) & maybe id (addName . ($ "file")) mNudge ) fRefs WorkflowPayloadFieldUser{..} -> do fRefs <- extractPrev @UserId let suggestions uid = E.from $ \user -> do E.where_ $ user E.^. UserId E.==. E.val uid return user wSetTooltip' (fmap slI18n wpfuTooltip) $ f wpfuOptional (checkMap (first $ const MsgWorkflowEdgeFormFieldUserNotFound) Right . userField False $ suggestions <$> fRefs) ( fslI (slI18n wpfuLabel) & maybe id (addName . ($ "user")) mNudge ) (fRefs <|> wpfuDefault) WorkflowPayloadFieldCaptureUser -> do mAuthId <- liftHandler maybeAuth case mAuthId of Just (Entity uid User{userDisplayName, userSurname}) -> do fvId <- newIdent State.modify . List.delete $ _WorkflowFieldPayloadW . _WorkflowFieldPayload # uid lift . lift . lift . tell $ pure FieldView { fvLabel = [shamlet|#{mr MsgWorkflowEdgeFormFieldCaptureUserLabel}|] , fvTooltip = Nothing , fvId , fvInput = [whamlet| $newline never ^{nameWidget userDisplayName userSurname} |] , fvErrors = Nothing , fvRequired = False } (True, FormSuccess . Just . (:| []) $ _WorkflowFieldPayloadW . _WorkflowFieldPayload # uid) <$ tell (All True) Nothing -> (False, FormMissing) <$ tell (All False) WorkflowPayloadFieldReference{..} -> throwE wpfrTarget WorkflowPayloadFieldMultiple{..} -> do fRefs <- nonEmpty <$> State.state (maybe (, []) (splitAt . fromIntegral) $ (+ wpfmMin) <$> wpfmRange) miIdent <- newIdent wSetTooltip' (fmap slI18n wpfmTooltip) $ let mPrev' :: Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId)) mPrev' = fRefs <|> wpfmDefault mPrev :: Maybe (Map ListPosition (Maybe (WorkflowFieldPayloadW FileReference UserId), Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId)))) mPrev = Just . Map.fromList . zip [0..] . ensureLength . map (\x -> (Just x, Just $ x :| [])) $ mPrev' ^.. _Just . folded where ensureLength :: forall a b. [(Maybe a, Maybe b)] -> [(Maybe a, Maybe b)] ensureLength = (\l -> (l ++) $ replicate (fromIntegral wpfmMin - length l) (Nothing, Nothing)) . maybe id (take . fromIntegral) ((+ wpfmMin) <$> wpfmRange) mangleResult :: forall a. FormResult (Map ListPosition (a, Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId)))) -> (Bool, FormResult (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId)))) -- FieldMultiple are always filled since `massInput` ensures cardinality constraints (iff @mPrev'@ correctly initializes `massInput` with a list of fields of the appropriate length) mangleResult res = case matching _FormSuccess res of Right ress -> (True, FormSuccess . nonEmpty $ ress ^.. folded . _2 . _Just . folded) Left res' -> (False, res') runMI :: forall a. WForm (ExceptT WorkflowPayloadLabel Handler) a -> ExceptT WorkflowPayloadLabel (RWST (Maybe (Text -> Text)) All [WorkflowFieldPayloadW FileReference UserId] (MForm (WriterT [FieldView UniWorX] Handler))) a runMI mx = do r <- lift $ lift ask s <- lift $ lift State.get ((a, s', w), w') <- ExceptT . lift . lift . lift . runExceptT . runWriterT $ runRWST mx r s lift . lift $ do State.put s' tell w lift $ tell w' lift . tell . All $ wpfmMin <= 0 return a miAdd :: ListPosition -> Natural -> (Text -> Text) -> FieldView UniWorX -> Maybe (Html -> MForm (ExceptT WorkflowPayloadLabel Handler) (FormResult (Map ListPosition (Maybe (WorkflowFieldPayloadW FileReference UserId)) -> FormResult (Map ListPosition (Maybe (WorkflowFieldPayloadW FileReference UserId)))), Widget)) miAdd _pos _dim nudge submitView = Just $ over (mapped . _1 . _FormSuccess) tweakRes . miForm nudge (Left submitView) where tweakRes :: Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId)) -> Map ListPosition (Maybe (WorkflowFieldPayloadW FileReference UserId)) -> FormResult (Map ListPosition (Maybe (WorkflowFieldPayloadW FileReference UserId))) tweakRes newDat prevData = pure . Map.fromList . zip [startKey..] . map Just $ newDat ^.. _Just . folded where startKey = maybe 0 succ $ fst <$> Map.lookupMax prevData miCell :: ListPosition -> Maybe (WorkflowFieldPayloadW FileReference UserId) -> Maybe (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId))) -> (Text -> Text) -> (Html -> MForm (ExceptT WorkflowPayloadLabel Handler) (FormResult (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId))), Widget)) miCell _pos dat mPrev'' nudge = miForm nudge . Right $ fromMaybe (fmap (:| []) dat) mPrev'' miForm :: (Text -> Text) -> Either (FieldView UniWorX) (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId))) -> (Html -> MForm (ExceptT WorkflowPayloadLabel Handler) (FormResult (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId))), Widget)) miForm nudge mode csrf = do let runSpecRender :: WriterT [FieldView UniWorX] Handler (Either WorkflowPayloadLabel (Bool, FormResult (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId)))), Ints, Enctype) -> ExceptT WorkflowPayloadLabel Handler (((Bool, FormResult (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId)))), [FieldView UniWorX]), Ints, Enctype) runSpecRender mSR = do ((eRes, s, w), fvs) <- lift $ runWriterT mSR ExceptT . return $ (, s, w) . (, fvs) <$> eRes ((fFilled, fmRes), fvs') <- mapRWST runSpecRender . fmap (view _1) $ evalRWST (runExceptT $ renderSpecField wpfmSub) (Just $ fromMaybe id mNudge . nudge) (mode ^.. _Right . _Just . folded) let fFilled' = fFilled || isn't _FormSuccess fmRes fmRes' | not fFilled' = FormFailure . pure . maybe (mr MsgValueRequired) (mr . valueRequired) $ fvs ^? _head . to fvLabel' | otherwise = fmRes fvLabel' = toStrict . Blaze.renderMarkup . Blaze.contents . fvLabel -- Dirty, but probably good enough; if not: `censor` writer with actual `Text` in `renderSpecField` and discard that information in `workflowEdgePayloadFields` fvs | not fFilled' = fvs' <&> \fv -> fv { fvErrors = Just [shamlet| $newline never $maybe errs <- fvErrors fv #{errs}
#{mr (valueRequired (fvLabel' fv))} |] } | otherwise = fvs' valueRequired :: forall msg. _ => msg -> ValueRequired UniWorX valueRequired = ValueRequired return ( fmRes' , case mode of Left btn -> $(widgetFile "widgets/massinput/workflow-payload-field-multiple/add") Right _ -> $(widgetFile "widgets/massinput/workflow-payload-field-multiple/cell") ) miDelete :: forall m. Monad m => Map ListPosition (Maybe (WorkflowFieldPayloadW FileReference UserId)) -> ListPosition -> MaybeT m (Map ListPosition ListPosition) miDelete dat pos = do ListLength l <- hoistMaybe . preview liveCoords $ Map.keysSet dat guard $ l > wpfmMin miDeleteList dat pos miAllowAdd :: ListPosition -> Natural -> ListLength -> Bool miAllowAdd _ _ (ListLength l) = maybe True (l <) $ (+ wpfmMin) <$> wpfmRange miAddEmpty :: ListPosition -> Natural -> ListLength -> Set ListPosition miAddEmpty _ _ _ = Set.empty miButtonAction :: forall p. p -> Maybe (SomeRoute UniWorX) miButtonAction _ = Nothing miLayout :: MassInputLayout ListLength (Maybe (WorkflowFieldPayloadW FileReference UserId)) (Maybe (NonEmpty (WorkflowFieldPayloadW FileReference UserId))) miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/workflow-payload-field-multiple/layout") in runMI . fmap mangleResult $ massInputW MassInput{..} (fslI $ slI18n wpfmLabel) False mPrev workflowEdgeFormToAction :: ( MonadHandler m , HandlerSite m ~ UniWorX ) => WorkflowEdgeForm -> m (WorkflowAction FileReference UserId) workflowEdgeFormToAction WorkflowEdgeForm{..} = do wpUser <- Just <$> maybeAuthId wpTime <- liftIO getCurrentTime return WorkflowAction{..} where (wpTo, wpVia) = wefEdge wpPayload = wefPayload