Feature complete MassInput

This commit is contained in:
Gregor Kleen 2019-03-20 20:53:10 +01:00
parent 4b2d6d3aa2
commit aca5d180bc
7 changed files with 145 additions and 43 deletions

29
clean.sh Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
case $1 in
"")
exec -- stack clean
;;
*)
target=".stack-work-${1}"
if [[ ! -d "${target}" ]]; then
printf "%s does not exist or is no directory\n" "${target}" >&2
exit 1
fi
if [[ -e .stack-work-clean ]]; then
printf ".stack-work-clean exists\n" >&2
exit 1
fi
move-back() {
mv -v .stack-work "${target}"
[[ -d .stack-work-clean ]] && mv -v .stack-work-clean .stack-work
}
mv -v .stack-work .stack-work-clean
mv -v "${target}" .stack-work
trap move-back EXIT
stack clean
;;
esac

View File

@ -307,6 +307,7 @@ data instance ButtonClass UniWorX
| BCWarning | BCWarning
| BCDanger | BCDanger
| BCLink | BCLink
| BCMassInputAdd | BCMassInputDelete
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
instance Universe (ButtonClass UniWorX) instance Universe (ButtonClass UniWorX)
instance Finite (ButtonClass UniWorX) instance Finite (ButtonClass UniWorX)

View File

@ -135,23 +135,20 @@ postAdminTestR = do
let let
-- | Make a form for adding a point/line/plane/hyperplane/... (in this case: cell) -- | Make a form for adding a point/line/plane/hyperplane/... (in this case: cell)
-- --
-- This /needs/ to use @nudge@ (for deterministic field naming) and to replace all occurances of @mreq@ with @mpreq@ (no fields should be /actually/ required) -- This /needs/ to replace all occurances of @mreq@ with @mpreq@ (no fields should be /actually/ required)
mkAddForm :: ListPosition -- ^ Approximate position of the add-widget mkAddForm :: ListPosition -- ^ Approximate position of the add-widget
-> Natural -- ^ Dimension Index, outermost dimension ist 0 i.e. if dimension is 3 hyperplane-adders get passed 0, planes get passed 1, lines get 2, and points get 3 -> Natural -- ^ Dimension Index, outermost dimension ist 0 i.e. if dimension is 3 hyperplane-adders get passed 0, planes get passed 1, lines get 2, and points get 3
-> ListLength -- ^ Liveliness
-> (Text -> Text) -- ^ Nudge deterministic field ids so they're unique -> (Text -> Text) -- ^ Nudge deterministic field ids so they're unique
-> FieldView UniWorX -- ^ Submit-Button for this add-widget -> FieldView UniWorX -- ^ Submit-Button for this add-widget
-> Maybe (Form (ListPosition, Int)) -- ^ Nothing if no further cells should be added; returns index of new cell and data needed to initialize cell -> Maybe (Form (ListLength -> (ListPosition, Int))) -- ^ Nothing iff adding further cells in this position/dimension makes no sense; returns callback to determine index of new cell and data needed to initialize cell
mkAddForm 0 0 listLength nudge submitBtn mkAddForm 0 0 nudge submitBtn = Just $ \csrf -> do
| listLength >= 7 = Nothing (addRes, addView) <- mpreq textField ("" & addName (nudge "text")) Nothing
| otherwise = Just $ \csrf -> do let addRes' = fromMaybe 0 . readMay . Text.filter isDigit <$> addRes
(addRes, addView) <- mpreq textField ("" & addName (nudge "text")) Nothing return ((\dat l -> (fromIntegral l, dat)) <$> addRes', toWidget csrf >> fvInput addView >> fvInput submitBtn)
let addRes' = fromMaybe 0 . readMay . Text.filter isDigit <$> addRes mkAddForm _pos _dim _ _ = error "Dimension and Position is always 0 for our 1-dimensional form"
return ((fromIntegral listLength, ) <$> addRes', toWidget csrf >> fvInput addView >> fvInput submitBtn)
mkAddForm _pos _dim _ _ _ = error "Dimension and Position is always 0 for our 1-dimensional form"
-- | Make a single massInput-Cell -- | Make a single massInput-Cell
-- --
-- This /needs/ to use @nudge@ for deterministic field naming -- This /needs/ to use @nudge@ and deterministic field naming (this allows for correct value-shifting when cells are deleted)
mkCellForm :: ListPosition -- ^ Position of this cell mkCellForm :: ListPosition -- ^ Position of this cell
-> Int -- ^ Data needed to initialize the cell (see return of @mkAddForm@) -> Int -- ^ Data needed to initialize the cell (see return of @mkAddForm@)
-> Maybe Int -- ^ Initial cell result from Argument to `massInput` -> Maybe Int -- ^ Initial cell result from Argument to `massInput`
@ -167,8 +164,11 @@ postAdminTestR = do
deleteCell l pos deleteCell l pos
| l >= 2 = return . Map.fromSet (\pos' -> bool pos' (succ pos') $ pos' >= pos) $ Set.fromList [0..fromIntegral (l - 2)] | l >= 2 = return . Map.fromSet (\pos' -> bool pos' (succ pos') $ pos' >= pos) $ Set.fromList [0..fromIntegral (l - 2)]
| otherwise = return Map.empty | otherwise = return Map.empty
-- | Make a decision on whether an add widget should be allowed to further cells, given the current @liveliness@ (i.e. before performing the addition)
allowAdd :: ListPosition -> Natural -> ListLength -> Bool
allowAdd _ _ l = l < 7
((miResult, (fvInput -> miForm)), miEnc) <- runFormPost . identifyForm ("massinput" :: Text) $ massInput (MassInput mkAddForm mkCellForm deleteCell) "" True Nothing ((miResult, (fvInput -> miForm)), miEnc) <- runFormPost . identifyForm ("massinput" :: Text) $ massInput (MassInput mkAddForm mkCellForm deleteCell allowAdd) "" True Nothing
let locallyDefinedPageHeading = [whamlet|Admin TestPage for Uni2work|] let locallyDefinedPageHeading = [whamlet|Admin TestPage for Uni2work|]
@ -193,7 +193,7 @@ postAdminTestR = do
$forall err <- errs $forall err <- errs
<li>#{err} <li>#{err}
$of FormSuccess res $of FormSuccess res
<pre> <p style="white-space:pre-wrap; font-family:monospace;">
#{tshow res} #{tshow res}
|] |]

View File

@ -24,9 +24,11 @@ import qualified Data.Text as Text
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Foldable as Fold
import Data.List (genericLength, genericIndex, iterate) import Data.List (genericLength, genericIndex, iterate)
import Control.Monad.Trans.Maybe import Control.Monad.Trans.Maybe
import Control.Monad.Reader.Class (MonadReader(local))
import Control.Monad.Fix import Control.Monad.Fix
@ -122,8 +124,8 @@ instance RenderMessage UniWorX (ButtonMassInput coord) where
instance PathPiece coord => Button UniWorX (ButtonMassInput coord) where instance PathPiece coord => Button UniWorX (ButtonMassInput coord) where
btnValidate _ _ = False btnValidate _ _ = False
btnClasses (MassInputAddDimension _ _) = [BCIsButton, BCDefault] btnClasses (MassInputAddDimension _ _) = [BCIsButton, BCDefault, BCMassInputAdd]
btnClasses (MassInputDeleteCell _) = [BCIsButton, BCWarning] btnClasses (MassInputDeleteCell _) = [BCIsButton, BCWarning, BCMassInputDelete]
data MassInputFieldName coord data MassInputFieldName coord
@ -181,16 +183,16 @@ instance Exception MassInputException
data MassInput handler liveliness cellData cellResult = MassInput data MassInput handler liveliness cellData cellResult = MassInput
{ miAdd :: BoxCoord liveliness -- Position (dimensions after @dimIx@ are zero) { miAdd :: BoxCoord liveliness -- Position (dimensions after @dimIx@ are zero)
-> Natural -- Zero-based dimension index @dimIx@ -> Natural -- Zero-based dimension index @dimIx@
-> liveliness
-> (Text -> Text) -- Nudge deterministic field ids -> (Text -> Text) -- Nudge deterministic field ids
-> FieldView UniWorX -- Submit button -> FieldView UniWorX -- Submit button
-> Maybe (Markup -> MForm handler (FormResult (BoxCoord liveliness, cellData), Widget)) -> Maybe (Markup -> MForm handler (FormResult (liveliness -> (BoxCoord liveliness, cellData)), Widget))
, miCell :: BoxCoord liveliness -- Position , miCell :: BoxCoord liveliness -- Position
-> cellData -- Initialisation data -> cellData -- Initialisation data
-> Maybe cellResult -- Previous result -> Maybe cellResult -- Previous result
-> (Text -> Text) -- Nudge deterministic field ids -> (Text -> Text) -- Nudge deterministic field ids
-> (Markup -> MForm handler (FormResult cellResult, Widget)) -> (Markup -> MForm handler (FormResult cellResult, Widget))
, miDelete :: liveliness -> BoxCoord liveliness -> MaybeT (MForm handler) (Map (BoxCoord liveliness) (BoxCoord liveliness)) , miDelete :: liveliness -> BoxCoord liveliness -> MaybeT (MForm handler) (Map (BoxCoord liveliness) (BoxCoord liveliness))
, miAllowAdd :: BoxCoord liveliness -> Natural -> liveliness -> Bool
} }
massInput :: forall handler cellData cellResult liveliness. massInput :: forall handler cellData cellResult liveliness.
@ -204,7 +206,7 @@ massInput :: forall handler cellData cellResult liveliness.
-> Bool -- ^ Required? -> Bool -- ^ Required?
-> Maybe (Map (BoxCoord liveliness) (cellData, cellResult)) -> Maybe (Map (BoxCoord liveliness) (cellData, cellResult))
-> (Markup -> MForm handler (FormResult (Map (BoxCoord liveliness) (cellData, cellResult)), FieldView UniWorX)) -> (Markup -> MForm handler (FormResult (Map (BoxCoord liveliness) (cellData, cellResult)), FieldView UniWorX))
massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = mdo
let initialShape = fmap fst <$> initialResult let initialShape = fmap fst <$> initialResult
miName <- maybe newFormIdent return fsName miName <- maybe newFormIdent return fsName
@ -223,18 +225,21 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
| otherwise -> throwM MassInputInvalidShape | otherwise -> throwM MassInputInvalidShape
sentLiveliness <- maybe (throwM MassInputInvalidShape) return $ Map.keysSet sentShape' ^? liveCoords :: MForm handler liveliness sentLiveliness <- maybe (throwM MassInputInvalidShape) return $ Map.keysSet sentShape' ^? liveCoords :: MForm handler liveliness
let addForm :: [BoxDimension (BoxCoord liveliness)] -> MForm handler (Map (Natural, BoxCoord liveliness) (FormResult (BoxCoord liveliness, cellData), Widget)) let addForm :: [BoxDimension (BoxCoord liveliness)] -> MForm handler (Map (Natural, BoxCoord liveliness) (FormResult (Maybe (liveliness -> (BoxCoord liveliness, cellData))), Maybe Widget))
addForm = addForm' boxOrigin . zip [0..] addForm = addForm' boxOrigin . zip [0..]
where where
addForm' _ [] = return Map.empty addForm' _ [] = return Map.empty
addForm' miCoord ((dimIx, _) : remDims) = do addForm' miCoord ((dimIx, _) : remDims) = do
let nudgeAddWidgetName :: Text -> Text let nudgeAddWidgetName :: Text -> Text
nudgeAddWidgetName miAddWidgetField = toPathPiece MassInputAddWidget{..} nudgeAddWidgetName miAddWidgetField = toPathPiece MassInputAddWidget{..}
dimRes <- runMaybeT $ do (btnRes', btnView) <- mopt (buttonField $ MassInputAddDimension dimIx miCoord) ("" & addName MassInputAddButton{..}) Nothing
(btnRes, btnView) <- lift $ mpreq (buttonField $ MassInputAddDimension dimIx miCoord) ("" & addName MassInputAddButton{..}) Nothing let btnRes
(addRes, addView) <- MaybeT . traverse ($ mempty) $ miAdd miCoord dimIx sentLiveliness nudgeAddWidgetName btnView | FormSuccess Nothing <- btnRes' = FormMissing
return (btnRes *> addRes, addView) | FormSuccess (Just x) <- btnRes' = FormSuccess x
let dimRes' = maybe Map.empty (Map.singleton (dimIx, miCoord)) dimRes | otherwise = error "Value of btnRes should only be inspected if FormSuccess" <$ btnRes'
addRes' <- over (mapped . _Just . _1) (btnRes *>) . local (bool id (set _1 Nothing) $ is _FormMissing btnRes) . traverse ($ mempty) $
miAdd miCoord dimIx nudgeAddWidgetName btnView
let dimRes' = Map.singleton (dimIx, miCoord) (maybe (Nothing <$ btnRes) (fmap Just) $ fmap fst addRes', fmap snd addRes')
case remDims of case remDims of
[] -> return dimRes' [] -> return dimRes'
((_, BoxDimension dim) : _) -> do ((_, BoxDimension dim) : _) -> do
@ -245,7 +250,8 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
addResults <- addForm boxDimensions addResults <- addForm boxDimensions
let addShape let addShape
| [FormSuccess (bCoord, cData)] <- Map.elems . Map.filter (is _FormSuccess) $ fmap fst addResults = Just $ Map.insert bCoord cData sentShape' | [((dimIx, miCoord), FormSuccess (Just mkResult))] <- Map.toList . Map.filter (is $ _FormSuccess . _Just) $ fmap fst addResults
= Just $ maybe id (uncurry Map.insert) (mkResult sentLiveliness <$ guard (miAllowAdd miCoord dimIx sentLiveliness)) sentShape'
| otherwise = Nothing | otherwise = Nothing
addedShape <- if addedShape <- if
@ -257,42 +263,58 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
delForm :: BoxCoord liveliness -> MaybeT (MForm handler) (FormResult (Map (BoxCoord liveliness) (BoxCoord liveliness)), FieldView UniWorX) delForm :: BoxCoord liveliness -> MaybeT (MForm handler) (FormResult (Map (BoxCoord liveliness) (BoxCoord liveliness)), FieldView UniWorX)
delForm miCoord = do delForm miCoord = do
(delRes, delView) <- lift $ mpreq (buttonField $ MassInputDeleteCell miCoord) ("" & addName MassInputDeleteButton{..}) Nothing (delRes, delView) <- lift $ mpreq (buttonField $ MassInputDeleteCell miCoord) ("" & addName MassInputDeleteButton{..}) Nothing
$logDebugS "delForm" . tshow $ fmap toPathPiece delRes -- $logDebugS "delForm" . tshow $ fmap toPathPiece delRes
shapeUpdate <- miDelete addedLiveliness miCoord shapeUpdate <- miDelete addedLiveliness miCoord
guard $ isJust (Map.keysSet shapeUpdate ^? liveCoords :: Maybe liveliness) guard $ isJust (Map.keysSet shapeUpdate ^? liveCoords :: Maybe liveliness)
return (shapeUpdate <$ delRes, delView) return (shapeUpdate <$ delRes, delView)
delResults <- fmap (Map.mapMaybe id) . sequence $ Map.fromSet (runMaybeT . delForm) (Map.keysSet addedShape) delResults <- fmap (Map.mapMaybe id) . sequence $ Map.fromSet (runMaybeT . delForm) (Map.keysSet addedShape)
let delShape let
| [FormSuccess shapeUpdate'] <- Map.elems . Map.filter (is _FormSuccess) $ fmap fst delResults = traverse (flip Map.lookup addedShape) shapeUpdate' delShapeUpdate
| [FormSuccess shapeUpdate'] <- Map.elems . Map.filter (is _FormSuccess) $ fmap fst delResults = Just shapeUpdate'
| otherwise = Nothing | otherwise = Nothing
delShape = traverse (flip Map.lookup addedShape) =<< delShapeUpdate
let shapeChanged = Fold.any (isn't _FormMissing . view _1) addResults || Fold.any (is _FormSuccess . view _1) delResults
shape <- if shape <- if
| Just s <- delShape -> return s
| Just s <- addShape -> return s | Just s <- addShape -> return s
| Just s <- delShape -> return s
| otherwise -> return sentShape' | otherwise -> return sentShape'
$logDebugS "massInput" [st|Current shape: #{tshow (map toPathPiece (Map.keys shape))}|]
shapeId <- newIdent shapeId <- newIdent
let shapeInput = fieldView shapeField shapeId (toPathPiece shapeName) [] (Right shape) True
let let
shapeInput = fieldView shapeField shapeId (toPathPiece shapeName) [] (Right shape) True applyDelShapeUpdate :: Maybe (Env, FileEnv) -> Maybe (Env, FileEnv)
applyDelShapeUpdate prevEnv
| Just delShapeUpdate' <- delShapeUpdate
, Just (env, fEnv) <- prevEnv
= let reverseUpdate = Map.fromList . map swap $ Map.toList delShapeUpdate'
in Just . (, fEnv) . flip (Map.mapKeysWith mappend) env $ \k -> fromMaybe k $ do
cell@MassInputCell{miCoord} <- fromPathPiece k
newCoord <- Map.lookup miCoord reverseUpdate
return $ toPathPiece cell{ miCoord = newCoord }
| otherwise = prevEnv
cellResults <- flip Map.traverseWithKey shape $ \miCoord cData -> do cellResults <- flip Map.traverseWithKey shape $ \miCoord cData -> do
let let
nudgeCellName :: Text -> Text nudgeCellName :: Text -> Text
nudgeCellName miCellField = toPathPiece (MassInputCell{..} :: MassInputFieldName (BoxCoord liveliness)) nudgeCellName miCellField = toPathPiece (MassInputCell{..} :: MassInputFieldName (BoxCoord liveliness))
(cData, ) <$> miCell miCoord cData (fmap snd . Map.lookup miCoord =<< initialResult) nudgeCellName mempty local (over _1 applyDelShapeUpdate) $ (cData, ) <$> miCell miCoord cData (fmap snd . Map.lookup miCoord =<< initialResult) nudgeCellName mempty
let result :: FormResult (Map (BoxCoord liveliness) (cellData, cellResult)) let result :: FormResult (Map (BoxCoord liveliness) (cellData, cellResult))
result result
| isJust addShape || isJust delShape = FormMissing | shapeChanged = FormMissing
| otherwise = traverse (\(cData, (cResult, _)) -> (cData, ) <$> cResult) cellResults | otherwise = traverse (\(cData, (cResult, _)) -> (cData, ) <$> cResult) cellResults
liveliness <- maybe (throwM MassInputInvalidShape) return $ Map.keysSet shape ^? liveCoords :: MForm handler liveliness liveliness <- maybe (throwM MassInputInvalidShape) return $ Map.keysSet shape ^? liveCoords :: MForm handler liveliness
let miWidget :: BoxCoord liveliness -> [(Natural, BoxDimension (BoxCoord liveliness))] -> Widget let miWidget' :: BoxCoord liveliness -> [(Natural, BoxDimension (BoxCoord liveliness))] -> Widget
miWidget _ [] = mempty miWidget' _ [] = mempty
miWidget miCoord ((dimIx, BoxDimension dim) : remDims) = miWidget' miCoord ((dimIx, BoxDimension dim) : remDims) =
let coords = takeWhile (\c -> review (liveCoord c) liveliness) $ iterate (over dim succ) miCoord let coords = takeWhile (\c -> review (liveCoord c) liveliness) $ iterate (over dim succ) miCoord
cells cells
| [] <- remDims = do | [] <- remDims = do
@ -301,20 +323,18 @@ massInput MassInput{..} FieldSettings{..} fvRequired initialResult csrf = do
let deleteButton = snd <$> Map.lookup coord delResults let deleteButton = snd <$> Map.lookup coord delResults
return (coord, $(widgetFile "widgets/massinput/cell")) return (coord, $(widgetFile "widgets/massinput/cell"))
| otherwise = | otherwise =
[ (coord, miWidget coord remDims) | coord <- coords ] [ (coord, miWidget' coord remDims) | coord <- coords ]
addWidget = snd <$> Map.lookup (dimIx, miCoord) addResults addWidget = (\(_, mWgt) -> mWgt <* guard (miAllowAdd miCoord dimIx liveliness)) =<< Map.lookup (dimIx, miCoord) addResults
in $(widgetFile "widgets/massinput/row") in $(widgetFile "widgets/massinput/row")
miWidget = miWidget' boxOrigin $ zip [0..] boxDimensions
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
fvId <- maybe newIdent return fsId fvId <- maybe newIdent return fsId
let let
fvLabel = toHtml $ mr fsLabel fvLabel = toHtml $ mr fsLabel
fvTooltip = toHtml . mr <$> fsTooltip fvTooltip = toHtml . mr <$> fsTooltip
fvInput = mconcat fvInput = $(widgetFile "widgets/massinput/massinput")
[ toWidget csrf
, shapeInput
, miWidget boxOrigin $ zip [0..] boxDimensions
]
fvErrors = Nothing fvErrors = Nothing
in return (result, FieldView{..}) in return (result, FieldView{..})

View File

@ -229,4 +229,30 @@
}; };
} }
// Override implicit submit (pressing enter) behaviour to trigger a specified submit button instead of the default
window.utils.implicitSubmit = function(input, options) {
var submit = options.submit;
console.log('implicitSubmit', input, submit);
if (!submit) {
throw new Error('window.utils.implicitSubmit(input, options) needs to be passed a submit element via options');
}
var doSubmit = function(event) {
if (event.keyCode == 13) {
event.preventDefault();
submit.click();
}
};
input.addEventListener('keypress', doSubmit);
return {
scope: input,
destroy: function() {
input.removeEventListener('keypress', doSubmit);
},
};
}
})(); })();

View File

@ -0,0 +1,5 @@
$newline never
<div .massinput ##{fvId}>
#{csrf}
^{shapeInput}
^{miWidget}

View File

@ -0,0 +1,21 @@
document.addEventListener('DOMContentLoaded', function() {
var form = document.getElementById(#{String fvId}).closest('form');
var formSubmit = form.querySelector('input[type=submit], button[type=submit]:not(.btn-mass-input-add):not(.btn-mass-input-delete)');
var cellInputs = Array.from(form.querySelectorAll('.massinput--cell input:not([type=hidden])'));
cellInputs.forEach(function(input) {
window.utils.setup('implicitSubmit', input, { submit: formSubmit });
});
Array.from(form.querySelectorAll('.massinput--add')).forEach(function(wrapper) {
var addSubmit = wrapper.querySelector('.btn-mass-input-add');
var addInputs = Array.from(wrapper.querySelectorAll('input:not([type=hidden]):not(.btn-mass-input-add)'));
addInputs.forEach(function(input) {
window.utils.setup('implicitSubmit', input, { submit: addSubmit });
});
});
});