Merge and bugfix sorting by participants registration date

This commit is contained in:
SJost 2019-03-12 09:20:07 +01:00
commit 889c3ebb35
42 changed files with 969 additions and 360 deletions

View File

@ -89,6 +89,7 @@ CourseRegisterToTip: Anmeldung darf auch unbegrenzt offen bleiben
CourseDeregisterUntilTip: Abmeldung darf auch unbegrenzt erlaubt bleiben
CourseFilterSearch: Volltext-Suche
CourseFilterRegistered: Registriert
CourseFilterNone: Egal
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
CourseDeleted: Kurs gelöscht
CourseUserNote: Notiz

View File

@ -2,7 +2,7 @@ SystemMessage
from UTCTime Maybe
to UTCTime Maybe
authenticatedOnly Bool
severity MessageClass
severity MessageStatus
defaultLanguage Lang
content Html
summary Html Maybe

View File

@ -1,11 +1,8 @@
{ nixpkgs ? import <nixpkgs> {}, compiler ? null }:
{ nixpkgs ? import <nixpkgs>, compiler ? null }:
let
inherit (nixpkgs) pkgs;
haskellPackages = if isNull compiler
then pkgs.haskellPackages
else pkgs.haskell.packages."${compiler}";
inherit (nixpkgs {}) pkgs;
haskellPackages = if isNull compiler then pkgs.haskellPackages else pkgs.haskell.packages."${compiler}";
drv = haskellPackages.callPackage ./uniworx.nix {};
@ -26,21 +23,29 @@ let
shellHook = ''
export PROMPT_INFO="${oldAttrs.name}"
pgDir=$(mktemp -d)
pgSockDir=$(mktemp -d)
pgLogFile=$(mktemp)
initdb --no-locale -D ''${pgDir}
pg_ctl start -D ''${pgDir} -l ''${pgLogFile} -w -o "-k ''${pgSockDir} -c listen_addresses=''' -c hba_file='${postgresHba}' -c unix_socket_permissions=0700"
export PGHOST=''${pgSockDir} PGLOG=''${pgLogFile}
psql -f ${postgresSchema} postgres
printf "Postgres logfile is %s\nPostgres socket directory is %s\n" ''${pgLogFile} ''${pgSockDir}
if [[ -z "$PGHOST" ]]; then
set -xe
cleanup() {
pg_ctl stop -D ''${pgDir}
rm -rvf ''${pgDir} ''${pgSockDir} ''${pgLogFile}
}
pgDir=$(mktemp -d)
pgSockDir=$(mktemp -d)
pgLogFile=$(mktemp)
initdb --no-locale -D ''${pgDir}
pg_ctl start -D ''${pgDir} -l ''${pgLogFile} -w -o "-k ''${pgSockDir} -c listen_addresses=''' -c hba_file='${postgresHba}' -c unix_socket_permissions=0700"
export PGHOST=''${pgSockDir} PGLOG=''${pgLogFile}
psql -f ${postgresSchema} postgres
printf "Postgres logfile is %s\nPostgres socket directory is %s\n" ''${pgLogFile} ''${pgSockDir}
trap cleanup EXIT
cleanup() {
set +e -x
pg_ctl stop -D ''${pgDir}
rm -rvf ''${pgDir} ''${pgSockDir} ''${pgLogFile}
set +x
}
trap cleanup EXIT
set +xe
fi
${oldAttrs.shellHook}
'';

View File

@ -220,7 +220,7 @@ instance RenderMessage UniWorX MsgLanguage where
instance RenderMessage UniWorX (UnsupportedAuthPredicate (Route UniWorX)) where
renderMessage f ls (UnsupportedAuthPredicate tag route) = renderMessage f ls $ MsgUnsupportedAuthPredicate tag (show route)
embedRenderMessage ''UniWorX ''MessageClass ("Message" <>)
embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>)
embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel
embedRenderMessage ''UniWorX ''StudyFieldType id
embedRenderMessage ''UniWorX ''SheetFileType id
@ -1021,6 +1021,7 @@ siteLayout' headingOverride widget = do
addScript $ StaticR js_utils_asidenav_js
addScript $ StaticR js_utils_asyncForm_js
addScript $ StaticR js_utils_asyncTable_js
addScript $ StaticR js_utils_asyncTableFilter_js
addScript $ StaticR js_utils_checkAll_js
addScript $ StaticR js_utils_httpClient_js
addScript $ StaticR js_utils_form_js
@ -1032,9 +1033,13 @@ siteLayout' headingOverride widget = do
addStylesheet $ StaticR css_utils_alerts_scss
addStylesheet $ StaticR css_utils_asidenav_scss
addStylesheet $ StaticR css_utils_asyncForm_scss
addStylesheet $ StaticR css_utils_asyncTable_scss
addStylesheet $ StaticR css_utils_asyncTableFilter_scss
addStylesheet $ StaticR css_utils_checkbox_scss
addStylesheet $ StaticR css_utils_form_scss
addStylesheet $ StaticR css_utils_inputs_scss
addStylesheet $ StaticR css_utils_modal_scss
addStylesheet $ StaticR css_utils_radio_scss
addStylesheet $ StaticR css_utils_showHide_scss
addStylesheet $ StaticR css_utils_tabber_scss
addStylesheet $ StaticR css_utils_tooltip_scss

View File

@ -20,6 +20,8 @@ import Database.Persist.Sql (fromSqlKey)
-- import qualified Data.UUID.Cryptographic as UUID
import Control.Monad.Trans.Writer (mapWriterT)
-- BEGIN - Buttons needed only here
data ButtonCreate = CreateMath | CreateInf -- Dummy for Example
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
@ -84,15 +86,12 @@ postAdminTestR = do
_other -> addMessage Warning "KEIN Knopf erkannt"
((emailResult, emailWidget), emailEnctype) <- runFormPost . identifyForm "email" $ renderAForm FormStandard emailTestForm
case emailResult of
(FormSuccess (email, ls)) -> do
jId <- runDB $ do
jId <- queueJob $ JobSendTestEmail email ls
addMessage Success [shamlet|Email-test gestartet (Job ##{tshow (fromSqlKey jId)})|]
return jId
writeJobCtl $ JobCtlPerform jId
FormMissing -> return ()
(FormFailure errs) -> forM_ errs $ addMessage Error . toHtml
formResultModal emailResult AdminTestR $ \(email, ls) -> do
jId <- mapWriterT runDB $ do
jId <- queueJob $ JobSendTestEmail email ls
tell . pure $ Message Success [shamlet|Email-test gestartet (Job ##{tshow (fromSqlKey jId)})|]
return jId
writeJobCtl $ JobCtlPerform jId
let emailWidget' = [whamlet|
<form method=post action=@{AdminTestR} enctype=#{emailEnctype} data-ajax-submit>

View File

@ -85,22 +85,22 @@ submissionModeIs sMode ((_course `E.InnerJoin` sheet `E.InnerJoin` _submission)
-- Columns
colTerm :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colTerm :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colTerm = sortable (Just "term") (i18nCell MsgTerm)
$ \DBRow{ dbrOutput=(_, _, course, _, _) } ->
-- cell [whamlet| _{untermKey $ course ^. _3}|] -- lange, internationale Semester
textCell $ termToText $ unTermKey $ course ^. _3 -- kurze Semsterkürzel
colSchool :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colSchool :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colSchool = sortable (Just "school") (i18nCell MsgCourseSchool)
$ \DBRow{ dbrOutput=(_, _, course, _, _) } ->
anchorCell (TermSchoolCourseListR (course ^. _3) (course ^. _4)) [whamlet|#{unSchoolKey (course ^. _4)}|]
colCourse :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colCourse :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colCourse = sortable (Just "course") (i18nCell MsgCourse)
$ \DBRow{ dbrOutput=(_, _, (_,csh,tid,sid), _, _) } -> courseCellCL (tid,sid,csh)
colSheet :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colSheet :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colSheet = sortable (Just "sheet") (i18nCell MsgSheet)
$ \DBRow{ dbrOutput=(_, sheet, course, _, _) } ->
let tid = course ^. _3
@ -109,16 +109,16 @@ colSheet = sortable (Just "sheet") (i18nCell MsgSheet)
shn = sheetName $ entityVal sheet
in anchorCell (CSheetR tid ssh csh shn SShowR) [whamlet|#{display shn}|]
colSheetType :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colSheetType :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colSheetType = sortable (toNothing "sheetType") (i18nCell MsgSheetType)
$ \DBRow{ dbrOutput=(_, sheet, _, _, _) } -> i18nCell . sheetType $ entityVal sheet
colCorrector :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colCorrector :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colCorrector = sortable (Just "corrector") (i18nCell MsgCorrector) $ \case
DBRow{ dbrOutput = (_, _, _, Nothing , _) } -> cell mempty
DBRow{ dbrOutput = (_, _, _, Just (Entity _ User{..}), _) } -> userCell userDisplayName userSurname
colSubmissionLink :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colSubmissionLink :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colSubmissionLink = sortable Nothing (i18nCell MsgSubmission)
$ \DBRow{ dbrOutput=(submission, sheet, course, _, _) } ->
let csh = course ^. _2
@ -134,7 +134,7 @@ colSubmissionLink = sortable Nothing (i18nCell MsgSubmission)
colSelect :: forall act h. (Monoid act, Headedness h) => Colonnade h CorrectionTableData (DBCell _ (FormResult (act, DBFormResult CryptoFileNameSubmission Bool CorrectionTableData), SheetTypeSummary))
colSelect = dbSelect (_1 . applying _2) id $ \DBRow{ dbrOutput=(Entity subId _, _, _, _, _) } -> encrypt subId
colSubmittors :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colSubmittors :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colSubmittors = sortable (Just "submittors") (i18nCell MsgSubmissionUsers) $ \DBRow{ dbrOutput=(_, _, course, _, users) } -> let
csh = course ^. _2
tid = course ^. _3
@ -146,7 +146,7 @@ colSubmittors = sortable (Just "submittors") (i18nCell MsgSubmissionUsers) $ \DB
Just p -> [whamlet|^{nameWidget userDisplayName userSurname} (#{review _PseudonymText p})|]
in protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")]
colSMatrikel :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colSMatrikel :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colSMatrikel = sortable Nothing (i18nCell MsgMatrikelNr) $ \DBRow{ dbrOutput=(_, _, _, _, users) } -> let
protoCell = listCell (Map.toList users) $ \(userId, (User{..}, _)) -> anchorCellM (AdminUserR <$> encrypt userId) (maybe mempty toWidget userMatrikelnummer)
in protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")]
@ -170,26 +170,26 @@ colRating = sortable (Just "rating") (i18nCell MsgRating) $ \DBRow{ dbrOutput=(E
scribe (_2 :: Lens' (a, SheetTypeSummary) SheetTypeSummary) summary
]
colAssigned :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colAssigned :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colAssigned = sortable (Just "assignedtime") (i18nCell MsgAssignedTime) $ \DBRow{ dbrOutput=(Entity _subId Submission{..}, _sheet, _course, _, _) } ->
maybe mempty dateTimeCell submissionRatingAssigned
colRated :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colRated :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colRated = sortable (Just "ratingtime") (i18nCell MsgRatingTime) $ \DBRow{ dbrOutput=(Entity _subId Submission{..}, _sheet, _course, _, _) } ->
maybe mempty dateTimeCell submissionRatingTime
colPseudonyms :: IsDBTable m a => Colonnade _ CorrectionTableData (DBCell m a)
colPseudonyms :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colPseudonyms = sortable Nothing (i18nCell MsgPseudonyms) $ \DBRow{ dbrOutput=(_, _, _, _, users) } -> let
lCell = listCell (catMaybes $ snd . snd <$> Map.toList users) $ \pseudo ->
cell [whamlet|#{review _PseudonymText pseudo}|]
in lCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")]
colRatedField :: Colonnade _ CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (Bool, a, b) CorrectionTableData)))
colRatedField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (Bool, a, b) CorrectionTableData)))
colRatedField = sortable Nothing (i18nCell MsgRatingDone) $ formCell id
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _) } -> return subId)
(\DBRow{ dbrOutput=(Entity _ (submissionRatingDone -> done), _, _, _, _) } mkUnique -> over (_1.mapped) (_1 .~) . over _2 fvInput <$> mreq checkBoxField (fsUniq mkUnique "rated") (Just done))
colPointsField :: Colonnade _ CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, Maybe Points, b) CorrectionTableData)))
colPointsField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, Maybe Points, b) CorrectionTableData)))
colPointsField = sortable (Just "rating") (i18nCell MsgColumnRatingPoints) $ formCell id
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _) } -> return subId)
(\DBRow{ dbrOutput=(Entity _ Submission{..}, Entity _ Sheet{..}, _, _, _) } mkUnique -> case sheetType of
@ -197,7 +197,7 @@ colPointsField = sortable (Just "rating") (i18nCell MsgColumnRatingPoints) $ for
_other -> over (_1.mapped) (_2 .~) . over _2 fvInput <$> mopt pointsField (fsUniq mkUnique "points") (Just submissionRatingPoints)
)
colCommentField :: Colonnade _ CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData)))
colCommentField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData)))
colCommentField = sortable Nothing (i18nCell MsgRatingComment) $ formCell id
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _) } -> return subId)
(\DBRow{ dbrOutput=(Entity _ Submission{..}, _, _, _, _) } mkUnique -> over (_1.mapped) ((_3 .~) . assertM (not . null) . fmap (Text.strip . unTextarea)) . over _2 fvInput <$> mopt textareaField (fsUniq mkUnique "comment") (Just $ Textarea <$> submissionRatingComment))
@ -237,6 +237,9 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d
[ ( "term"
, SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseTerm
)
, ( "school"
, SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseSchool
)
, ( "course"
, SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseShorthand
)

View File

@ -33,7 +33,7 @@ import qualified Database.Esqueleto as E
-- NOTE: Outdated way to use dbTable; see ProfileDataR Handler for a more recent method.
type CourseTableData = DBRow (Entity Course, Int64, Bool, Entity School)
colCourse :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colCourse :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colCourse = sortable (Just "course") (i18nCell MsgCourse)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR)
@ -51,12 +51,12 @@ colDescription = sortable Nothing mempty
Nothing -> mempty
(Just descr) -> cell $ modal (toWidget $ hasComment True) (Right $ toWidget descr)
colCShort :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colCShort :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colCShort = sortable (Just "cshort") (i18nCell MsgCourseShort)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|#{display courseShorthand}|]
-- colCShortDescr :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
-- colCShortDescr :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
-- colCShortDescr = sortable (Just "cshort") (i18nCell MsgCourseShort)
-- $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> mappend
-- ( anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|#{display courseShorthand}|] )
@ -70,39 +70,39 @@ colCShort = sortable (Just "cshort") (i18nCell MsgCourseShort)
-- |]
-- )
colTerm :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colTerm :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colTerm = sortable (Just "term") (i18nCell MsgTerm)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
anchorCell (TermCourseListR courseTerm) [whamlet|#{display courseTerm}|]
colSchool :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colSchool :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colSchool = sortable (Just "school") (i18nCell MsgCourseSchool)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}) } ->
anchorCell (TermSchoolCourseListR courseTerm courseSchool) [whamlet|#{display schoolName}|]
colSchoolShort :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colSchoolShort :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colSchoolShort = sortable (Just "schoolshort") (i18nCell MsgCourseSchoolShort)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}) } ->
anchorCell (TermSchoolCourseListR courseTerm courseSchool) [whamlet|#{display schoolShorthand}|]
colRegFrom :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colRegFrom :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colRegFrom = sortable (Just "register-from") (i18nCell MsgRegisterFrom)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
maybe mempty dateTimeCell courseRegisterFrom
-- cell $ traverse (formatTime SelFormatDateTime) courseRegisterFrom >>= maybe mempty toWidget
colRegTo :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colRegTo :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colRegTo = sortable (Just "register-to") (i18nCell MsgRegisterTo)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
maybe mempty dateTimeCell courseRegisterTo
colMembers :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colMembers :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colMembers = sortable (Just "members") (i18nCell MsgCourseMembers)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, currentParticipants, _, _) } -> i18nCell $ case courseCapacity of
Nothing -> MsgCourseMembersCount currentParticipants
Just limit -> MsgCourseMembersCountLimited currentParticipants limit
colRegistered :: IsDBTable m a => Colonnade _ CourseTableData (DBCell m a)
colRegistered :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colRegistered = sortable (Just "registered") (i18nCell MsgRegistered)
$ \DBRow{ dbrOutput=(_, _, registered, _) } -> tickmarkCell registered

View File

@ -145,7 +145,7 @@ getTermShowR = do
, dbtIdent = "terms" :: Text
}
defaultLayout $ do
setTitle "Freigeschaltete Semester"
setTitleI MsgTermsHeading
$(widgetFile "terms")
getTermEditR :: Handler Html

View File

@ -522,6 +522,30 @@ secretJsonField = Field{..}
|]
fieldEnctype = UrlEncoded
boolField :: ( MonadHandler m
, HandlerSite m ~ UniWorX
)
=> Field m Bool
boolField = Field
{ fieldParse = \e _ -> return $ boolParser e
, fieldView = \theId name attrs val isReq -> $(widgetFile "widgets/fields/bool")
, fieldEnctype = UrlEncoded
}
where
boolParser [] = Right Nothing
boolParser (x:_) = case x of
"" -> Right Nothing
"none" -> Right Nothing
"yes" -> Right $ Just True
"on" -> Right $ Just True
"no" -> Right $ Just False
"true" -> Right $ Just True
"false" -> Right $ Just False
t -> Left $ SomeMessage $ MsgInvalidBool t
showVal = either $ const False
funcForm :: forall k v m.
( Finite k, Ord k
@ -671,5 +695,5 @@ formResultModal res finalDest handler = maybeT_ $ do
if
| isModal -> sendResponse $ toJSON messages
| otherwise -> do
forM_ messages $ \Message{..} -> addMessage messageClass messageContent
forM_ messages $ \Message{..} -> addMessage messageStatus messageContent
redirect finalDest

View File

@ -3,7 +3,7 @@ module Import.NoFoundation
, MForm
) where
import ClassyPrelude.Yesod as Import hiding (formatTime, derivePersistFieldJSON, addMessage, addMessageI, (.=), MForm, Proxy, foldlM, static)
import ClassyPrelude.Yesod as Import hiding (formatTime, derivePersistFieldJSON, addMessage, addMessageI, (.=), MForm, Proxy, foldlM, static, boolField)
import Model as Import
import Model.Types.JSON as Import
import Model.Migration as Import

View File

@ -20,8 +20,8 @@ import Data.CaseInsensitive (CI)
import Data.CaseInsensitive.Instances ()
import Text.Blaze (ToMarkup, toMarkup, Markup)
import Utils.Message (MessageStatus)
import Utils.Message (MessageClass)
import Settings.Cluster (ClusterSettingsKey)
import Data.Binary (Binary)

View File

@ -39,7 +39,7 @@ import qualified Data.Text.Encoding as Text
import qualified Ldap.Client as Ldap
import Utils hiding (MessageClass(..))
import Utils hiding (MessageStatus(..))
import Control.Lens
import Data.Maybe (fromJust)

View File

@ -1,6 +1,6 @@
module Utils.Message
( MessageClass(..)
, UnknownMessageClass(..)
( MessageStatus(..)
, UnknownMessageStatus(..)
, addMessage, addMessageI, addMessageIHamlet, addMessageFile, addMessageWidget
, Message(..)
, messageI, messageIHamlet, messageFile, messageWidget
@ -25,64 +25,64 @@ import Text.Blaze.Html.Renderer.Text (renderHtml)
import Text.HTML.SanitizeXSS (sanitizeBalance)
data MessageClass = Error | Warning | Info | Success
data MessageStatus = Error | Warning | Info | Success
deriving (Eq, Ord, Enum, Bounded, Show, Read, Lift)
instance Universe MessageClass
instance Finite MessageClass
instance Universe MessageStatus
instance Finite MessageStatus
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece
} ''MessageClass
} ''MessageStatus
nullaryPathPiece ''MessageClass camelToPathPiece
derivePersistField "MessageClass"
nullaryPathPiece ''MessageStatus camelToPathPiece
derivePersistField "MessageStatus"
newtype UnknownMessageClass = UnknownMessageClass Text
newtype UnknownMessageStatus = UnknownMessageStatus Text
deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance Exception UnknownMessageClass
instance Exception UnknownMessageStatus
data Message = Message
{ messageClass :: MessageClass
{ messageStatus :: MessageStatus
, messageContent :: Html
}
instance Eq Message where
a == b = ((==) `on` messageClass) a b && ((==) `on` renderHtml . messageContent) a b
a == b = ((==) `on` messageStatus) a b && ((==) `on` renderHtml . messageContent) a b
instance Ord Message where
a `compare` b = (compare `on` messageClass) a b `mappend` (compare `on` renderHtml . messageContent) a b
a `compare` b = (compare `on` messageStatus) a b `mappend` (compare `on` renderHtml . messageContent) a b
instance ToJSON Message where
toJSON Message{..} = object
[ "class" .= messageClass
[ "status" .= messageStatus
, "content" .= renderHtml messageContent
]
instance FromJSON Message where
parseJSON = withObject "Message" $ \o -> do
messageClass <- o .: "class"
messageStatus <- o .: "status"
messageContent <- preEscapedText . sanitizeBalance <$> o .: "content"
return Message{..}
addMessage :: MonadHandler m => MessageClass -> Html -> m ()
addMessage :: MonadHandler m => MessageStatus -> Html -> m ()
addMessage mc = ClassyPrelude.Yesod.addMessage (toPathPiece mc)
addMessageI :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => MessageClass -> msg -> m ()
addMessageI :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => MessageStatus -> msg -> m ()
addMessageI mc = ClassyPrelude.Yesod.addMessageI (toPathPiece mc)
messageI :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => MessageClass -> msg -> m Message
messageI messageClass msg = do
messageI :: (MonadHandler m, RenderMessage (HandlerSite m) msg) => MessageStatus -> msg -> m Message
messageI messageStatus msg = do
messageContent <- toHtml . ($ msg) <$> getMessageRender
return Message{..}
addMessageIHamlet :: ( MonadHandler m
, RenderMessage (HandlerSite m) msg
, HandlerSite m ~ site
) => MessageClass -> HtmlUrlI18n msg (Route site) -> m ()
) => MessageStatus -> HtmlUrlI18n msg (Route site) -> m ()
addMessageIHamlet mc iHamlet = do
mr <- getMessageRender
ClassyPrelude.Yesod.addMessage (toPathPiece mc) =<< withUrlRenderer (iHamlet $ toHtml . mr)
@ -90,22 +90,22 @@ addMessageIHamlet mc iHamlet = do
messageIHamlet :: ( MonadHandler m
, RenderMessage (HandlerSite m) msg
, HandlerSite m ~ site
) => MessageClass -> HtmlUrlI18n msg (Route site) -> m Message
) => MessageStatus -> HtmlUrlI18n msg (Route site) -> m Message
messageIHamlet mc iHamlet = do
mr <- getMessageRender
Message mc <$> withUrlRenderer (iHamlet $ toHtml . mr)
addMessageFile :: MessageClass -> FilePath -> ExpQ
addMessageFile :: MessageStatus -> FilePath -> ExpQ
addMessageFile mc tPath = [e|addMessageIHamlet mc $(ihamletFile tPath)|]
messageFile :: MessageClass -> FilePath -> ExpQ
messageFile :: MessageStatus -> FilePath -> ExpQ
messageFile mc tPath = [e|messageIHamlet mc $(ihamletFile tPath)|]
addMessageWidget :: forall m site.
( MonadHandler m
, HandlerSite m ~ site
, Yesod site
) => MessageClass -> WidgetT site IO () -> m ()
) => MessageStatus -> WidgetT site IO () -> m ()
-- ^ _Note_: `addMessageWidget` ignores `pageTitle` and `pageHead`
addMessageWidget mc wgt = do
PageContent{pageBody} <- liftHandlerT $ widgetToPageContent wgt
@ -115,7 +115,7 @@ messageWidget :: forall m site.
( MonadHandler m
, HandlerSite m ~ site
, Yesod site
) => MessageClass -> WidgetT site IO () -> m Message
) => MessageStatus -> WidgetT site IO () -> m Message
messageWidget mc wgt = do
PageContent{pageBody} <- liftHandlerT $ widgetToPageContent wgt
messageIHamlet mc (const pageBody :: HtmlUrlI18n (SomeMessage site) (Route site))

View File

@ -1,8 +1,14 @@
{ ghc, nixpkgs ? (import <nixpkgs> {}) }:
{ ghc, nixpkgs ? import <nixpkgs> }:
let
inherit (nixpkgs) haskell pkgs;
haskellPackages = if ghc.version == pkgs.haskellPackages.ghc.version then pkgs.haskellPackages else pkgs.haskell.packages."ghc${builtins.replaceStrings ["."] [""] ghc.version}";
snapshot = "lts-10.5";
stackage = import (fetchTarball {
url = "https://stackage.serokell.io/drczwlyf6mi0ilh3kgv01wxwjfgvq14b-stackage/default.nix.tar.gz";
sha256 = "1bwlbxx6np0jfl6z9gkmmcq22crm0pa07a8zrwhz5gkal64y6jpz";
});
inherit (nixpkgs { overlays = [ stackage."${snapshot}" ]; }) haskell pkgs;
haskellPackages = pkgs.haskell.packages."${snapshot}";
in haskell.lib.buildStackProject {
inherit ghc;
name = "stackenv";

View File

@ -17,6 +17,10 @@ packages:
git: https://github.com/pngwjpgh/encoding.git
commit: 67bb87ceff53f0178c988dd4e15eeb2daee92b84
extra-dep: true
- location:
git: https://github.com/pngwjpgh/memcached-binary.git
commit: b5461747e7be226d3b67daebc3c9aefe8a4490ad
extra-dep: true
extra-deps:
- colonnade-1.2.0
@ -45,8 +49,4 @@ extra-deps:
- quickcheck-classes-0.4.14
- semirings-0.2.1.1
- memcached-binary-0.2.0
allow-newer: true
resolver: lts-10.5

View File

@ -57,7 +57,7 @@
padding-left: 10px;
&.js-show-hide__toggle::before {
top: 25px;
z-index: 1;
}
}
}
@ -103,7 +103,6 @@
&::before {
left: auto;
right: 20px;
top: 30px;
color: var(--color-font);
}
}
@ -314,6 +313,10 @@
word-break: break-all;
background-color: var(--color-dark);
color: var(--color-lightwhite);
&:hover {
background-color: var(--color-dark);
}
}
.asidenav__link-shorthand {

View File

@ -1,9 +1,78 @@
.async-form-response {
margin: 20px 0;
position: relative;
width: 100%;
font-size: 18px;
text-align: center;
padding-top: 60px;
}
.async-form-response::before,
.async-form-response::after {
position: absolute;
top: 0px;
left: 50%;
display: block;
}
.async-form-response--success::before {
content: '';
width: 17px;
height: 28px;
border: solid #069e04;
border-width: 0 5px 5px 0;
transform: translateX(-50%) rotate(45deg);
}
.async-form-response--info::before {
content: '';
width: 5px;
height: 30px;
top: 10px;
background-color: #777;
transform: translateX(-50%);
}
.async-form-response--info::after {
content: '';
width: 5px;
height: 5px;
background-color: #777;
transform: translateX(-50%);
}
.async-form-response--warning::before {
content: '';
width: 5px;
height: 30px;
background-color: rgb(255, 187, 0);
transform: translateX(-50%);
}
.async-form-response--warning::after {
content: '';
width: 5px;
height: 5px;
top: 35px;
background-color: rgb(255, 187, 0);
transform: translateX(-50%);
}
.async-form-response--error::before {
content: '';
width: 5px;
height: 40px;
background-color: #940d0d;
transform: translateX(-50%) rotate(-45deg);
}
.async-form-response--error::after {
content: '';
width: 5px;
height: 40px;
background-color: #940d0d;
transform: translateX(-50%) rotate(45deg);
}
.async-form-loading {
opacity: 0.1;
transition: opacity 800ms ease-in-out;
transition: opacity 800ms ease-out;
pointer-events: none;
}

View File

@ -0,0 +1,5 @@
.async-table--loading {
opacity: 0.7;
pointer-events: none;
transition: opacity 400ms ease-out;
}

View File

@ -0,0 +1,5 @@
.async-table-filter--loading {
opacity: 0.7;
pointer-events: none;
transition: opacity 400ms ease-out;
}

View File

@ -0,0 +1,74 @@
/* CUSTOM CHECKBOXES */
/* Completely replaces legacy checkbox */
.checkbox {
position: relative;
display: inline-block;
[type="checkbox"] {
position: fixed;
top: -1px;
left: -1px;
width: 1px;
height: 1px;
overflow: hidden;
}
label {
display: block;
height: 24px;
width: 24px;
background-color: #f3f3f3;
box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05);
border: 2px solid var(--color-primary);
border-radius: 4px;
color: white;
cursor: pointer;
}
label::before,
label::after {
position: absolute;
display: block;
top: 12px;
left: 8px;
height: 2px;
width: 8px;
background-color: var(--color-font);
}
:checked + label {
background-color: var(--color-primary);
}
[type="checkbox"]:focus + label {
border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
outline: 0;
}
:checked + label::before,
:checked + label::after {
content: '';
}
:checked + label::before {
background-color: white;
transform: rotate(45deg);
left: 4px;
}
:checked + label::after {
background-color: white;
transform: rotate(-45deg);
top: 11px;
width: 13px;
}
[disabled] + label {
pointer-events: none;
border: none;
opacity: 0.6;
filter: grayscale(1);
}
}

View File

@ -95,7 +95,6 @@ select {
width: 100%;
max-width: 600px;
-webkit-appearance: none;
align-items: center;
border: 1px solid transparent;
border-radius: 4px;
@ -162,6 +161,11 @@ textarea {
}
/* OPTIONS */
select {
-webkit-appearance: menulist;
}
select,
option {
font-size: 1rem;
@ -171,7 +175,8 @@ option {
border-radius: 2px;
outline: 0;
color: #363636;
min-width: 200px;
min-width: 250px;
width: auto;
background-color: #f3f3f3;
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
}
@ -183,130 +188,6 @@ option {
}
}
/* CUSTOM LEGACY CHECKBOX AND RADIO BOXES */
input[type="checkbox"] {
position: relative;
height: 20px;
width: 20px;
-webkit-appearance: none;
appearance: none;
cursor: pointer;
}
input[type="checkbox"]::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
background-color: var(--color-lighter);
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
}
input[type="checkbox"]:checked::before {
background-color: var(--color-light);
}
input[type="checkbox"]:checked::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
}
/* CUSTOM CHECKBOXES AND RADIO BOXES */
/* Completely replaces legacy checkbox and radiobox */
.checkbox,
.radio {
position: relative;
display: inline-block;
[type="checkbox"],
[type="radio"] {
position: fixed;
top: -1px;
left: -1px;
width: 1px;
height: 1px;
overflow: hidden;
}
label {
display: block;
height: 24px;
width: 24px;
background-color: #f3f3f3;
box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05);
border: 2px solid var(--color-primary);
border-radius: 4px;
color: white;
cursor: pointer;
}
label::before,
label::after {
position: absolute;
display: block;
top: 12px;
left: 8px;
height: 2px;
width: 8px;
background-color: var(--color-font);
}
:checked + label {
background-color: var(--color-primary);
}
[type="checkbox"]:focus + label,
[type="radio"]:focus + label {
border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
outline: 0;
}
:checked + label::before,
:checked + label::after {
content: '';
}
:checked + label::before {
background-color: white;
transform: rotate(45deg);
left: 4px;
}
:checked + label::after {
background-color: white;
transform: rotate(-45deg);
top: 11px;
width: 13px;
}
[disabled] + label {
pointer-events: none;
border: none;
opacity: 0.6;
filter: grayscale(1);
}
}
.radio::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
border-radius: 4px;
border: 2px solid white;
z-index: -1;
}
/* CUSTOM FILE INPUT */
.file-input__label {
cursor: pointer;

View File

@ -3,7 +3,7 @@
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0.8, 0.8);
display: block;
display: flex;
background-color: rgba(255, 255, 255, 1);
min-width: 60vw;
min-height: 100px;
@ -26,10 +26,6 @@
z-index: 200;
transform: translate(-50%, -50%) scale(1, 1);
}
.modal__content {
margin: 20px 0;
}
}
@media (max-width: 1024px) {
@ -96,3 +92,8 @@
color: white;
}
}
.modal__content {
margin: 20px 0;
width: 100%;
}

View File

@ -0,0 +1,76 @@
/* CUSTOM RADIO BOXES */
/* Completely replaces native radiobox */
.radio-group {
display: flex;
}
.radio-group__option {
min-width: 30px;
}
.radio {
position: relative;
display: inline-block;
[type="radio"] {
position: fixed;
top: -1px;
left: -1px;
width: 1px;
height: 1px;
overflow: hidden;
}
label {
display: block;
height: 34px;
min-width: 42px;
line-height: 34px;
text-align: center;
padding: 0 13px;
background-color: #f3f3f3;
box-shadow: inset 2px 1px 2px 1px rgba(50, 50, 50, 0.05);
color: var(--color-font);
cursor: pointer;
}
:checked + label {
background-color: var(--color-primary);
color: var(--color-lightwhite);
box-shadow: inset -2px -1px 2px 1px rgba(255, 255, 255, 0.15);
}
:focus + label {
border-color: #3273dc;
box-shadow: 0 0 0.125em 0 rgba(50,115,220,0.8);
outline: 0;
}
[disabled] + label {
pointer-events: none;
border: none;
opacity: 0.6;
filter: grayscale(1);
}
}
.radio:first-child {
label {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
}
.radio:last-child {
label {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
@media (max-width: 768px) {
.radio + .radio {
margin-left: 10px;
}
}

View File

@ -1,22 +1,30 @@
$show-hide-toggle-size: 6px;
.js-show-hide__toggle {
position: relative;
cursor: pointer;
padding: 3px 7px;
&:hover {
background-color: var(--color-grey-lighter);
cursor: pointer;
}
}
.js-show-hide__toggle::before {
content: '';
position: absolute;
width: 0;
height: 0;
left: -28px;
top: 6px;
width: $show-hide-toggle-size;
height: $show-hide-toggle-size;
left: -15px;
top: 12px - $show-hide-toggle-size / 2;
color: var(--color-primary);
border-right: 8px solid transparent;
border-top: 8px solid transparent;
border-left: 8px solid transparent;
border-bottom: 8px solid currentColor;
border-right: 2px solid currentColor;
border-top: 2px solid currentColor;
transition: transform .2s ease;
transform-origin: 8px 12px;
transform-origin: ($show-hide-toggle-size / 2);
transform: translateY($show-hide-toggle-size) rotate(-45deg);
}
.js-show-hide__target {
@ -26,7 +34,7 @@
.js-show-hide--collapsed {
.js-show-hide__toggle::before {
transform: rotate(180deg);
transform: translateY($show-hide-toggle-size / 3) rotate(135deg);
}
.js-show-hide__target {

View File

@ -87,5 +87,10 @@
initToggler();
alertElements.forEach(initAlert);
return {
scope: alertsEl,
destroy: function() {},
};
};
})();

View File

@ -55,5 +55,10 @@
initFavoritesButton();
initAsidenavSubmenus();
return {
scope: asideEl,
destroy: function() {},
};
};
})();

View File

@ -6,9 +6,12 @@
var ASYNC_FORM_RESPONSE_CLASS = 'async-form-response';
var ASYNC_FORM_LOADING_CLASS = 'async-form-loading';
var ASYNC_FORM_MIN_DELAY = 600;
var DEFAULT_FAILURE_MESSAGE = 'The response we received from the server did not match what we expected. Please let us know this happened via the help widget in the top navigation.';
window.utils.asyncForm = function(formElement, options) {
options = options || {};
var lastRequestTimestamp = 0;
function setup() {
@ -16,19 +19,27 @@
}
function processResponse(response) {
var responseElement = document.createElement('div');
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS);
responseElement.innerHTML = response.content;
var responseElement = makeResponseElement(response.content, response.status);
var parentElement = formElement.parentElement;
// make sure there is a delay between click and response
var delay = Math.max(0, ASYNC_FORM_MIN_DELAY + lastRequestTimestamp - Date.now());
setTimeout(function() {
parentElement.insertBefore(responseElement, formElement);
formElement.remove();
}, delay);
}
function makeResponseElement(content, status) {
var responseElement = document.createElement('div');
status = status || 'info';
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS);
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS + '--' + status);
responseElement.innerHTML = content;
return responseElement;
}
function submitHandler(event) {
event.preventDefault();
@ -47,14 +58,29 @@
window.utils.httpClient.post(url, headers, body)
.then(function(response) {
return response.json();
if (response.headers.get("content-type").indexOf("application/json") !== -1) {// checking response header
return response.json();
} else {
throw new TypeError('Unexpected Content-Type. Expected Content-Type: "application/json". Requested URL:' + url + '"');
}
}).then(function(response) {
processResponse(response[0])
processResponse(response[0]);
}).catch(function(error) {
console.error('could not fetch or process response from ' + url, { error });
var failureMessage = DEFAULT_FAILURE_MESSAGE;
if (options.i18n && options.i18n.asyncFormFailure) {
failureMessage = options.i18n.asyncFormFailure;
}
processResponse({ content: failureMessage });
formElement.classList.remove(ASYNC_FORM_LOADING_CLASS);
});
}
setup();
return {
scope: formElement,
destroy: function() {},
};
};
})();

View File

@ -5,6 +5,10 @@
var HEADER_HEIGHT = 80;
var RESET_OPTIONS = [ 'scrollTo' ];
var TABLE_FILTER_FORM_CLASS = 'table-filter-form';
var ASYNC_TABLE_CONTENT_CHANGED_CLASS = 'async-table--changed';
var ASYNC_TABLE_LOADING_CLASS = 'async-table--loading';
var JS_INITIALIZED_CLASS = 'js-async-table-initialized';
window.utils.asyncTable = function(wrapper, options) {
@ -17,6 +21,8 @@
var pagesizeForm;
var scrollTable;
var utilInstances = [];
function init() {
var table = wrapper.querySelector('#' + tableIdent);
@ -42,17 +48,30 @@
// pagesize form
pagesizeForm = wrapper.querySelector('#' + tableIdent + '-pagesize-form');
// check all
utilInstances.push(window.utils.setup('checkAll', wrapper));
// showhide
utilInstances.push(window.utils.setup('showHide', wrapper));
// filter
var filterForm = wrapper.querySelector('.' + TABLE_FILTER_FORM_CLASS);
if (filterForm) {
options.updateTableFrom = updateTableFrom;
utilInstances.push(window.utils.setup('asyncTableFilter', filterForm, options));
}
// take options into account
if (options && options.scrollTo) {
if (options.scrollTo) {
window.scrollTo(options.scrollTo);
}
if (options && options.horizPos && scrollTable) {
if (options.horizPos && scrollTable) {
scrollTable.scrollLeft = options.horizPos;
}
setupListeners();
wrapper.classList.add('js-initialized');
wrapper.classList.add(JS_INITIALIZED_CLASS);
}
function setupListeners() {
@ -117,32 +136,22 @@
}
function changePagesizeHandler(event) {
var currentTableUrl = options.currentUrl || window.location.href;
var url = getUrlWithUpdatedPagesize(currentTableUrl, event.target.value);
url = new URL(getUrlWithResetPagenumber(url));
var pagesizeParamKey = tableIdent + '-pagesize';
var pageParamKey = tableIdent + '-page';
var url = new URL(options.currentUrl || window.location.href);
url.searchParams.set(pagesizeParamKey, event.target.value);
url.searchParams.set(pageParamKey, 0);
updateTableFrom(url);
}
function getUrlWithUpdatedPagesize(url, pagesize) {
if (url.indexOf('pagesize') >= 0) {
return url.replace(/pagesize=(\d+|all)/, 'pagesize=' + pagesize);
} else if (url.indexOf('?') >= 0) {
return url += '&' + tableIdent + '-pagesize=' + pagesize;
}
return url += '?' + tableIdent + '-pagesize=' + pagesize;
}
function getUrlWithResetPagenumber(url) {
return url.replace(/-page=\d+/, '-page=0');
}
// fetches new sorted table from url with params and replaces contents of current table
function updateTableFrom(url, tableOptions) {
function updateTableFrom(url, tableOptions, callback) {
if (!window.utils.httpClient) {
throw new Error('httpClient not found!');
}
wrapper.classList.add(ASYNC_TABLE_LOADING_CLASS);
tableOptions = tableOptions || {};
var headers = {
'Accept': 'text/html',
@ -157,6 +166,11 @@
tableOptions.currentUrl = url.href;
removeListeners();
updateWrapperContents(data, tableOptions);
if (callback && typeof callback === 'function') {
callback(wrapper);
}
wrapper.classList.remove(ASYNC_TABLE_LOADING_CLASS);
}).catch(function(err) {
console.error(err);
});
@ -165,11 +179,11 @@
function updateWrapperContents(newHtml, tableOptions) {
tableOptions = tableOptions || {};
wrapper.innerHTML = newHtml;
wrapper.classList.remove("js-initialized");
wrapper.classList.remove(JS_INITIALIZED_CLASS);
wrapper.classList.add(ASYNC_TABLE_CONTENT_CHANGED_CLASS);
destroyUtils();
// setup the wrapper and its components to behave async again
window.utils.teardown('asyncTable');
window.utils.teardown('form');
// merge global options and table specific options
var resetOptions = {};
Object.keys(options)
@ -195,13 +209,24 @@
window.utils.setup('asyncTable', wrapper, combinedOptions);
Array.from(wrapper.querySelectorAll('form')).forEach(function(form) {
window.utils.setup('form', form);
utilInstances.push(window.utils.setup('form', form));
});
Array.from(wrapper.querySelectorAll('.modal')).forEach(function(modal) {
window.utils.setup('modal', modal);
utilInstances.push(window.utils.setup('modal', modal));
});
}
function destroyUtils() {
utilInstances.forEach(function(utilInstance) {
utilInstance.destroy();
});
}
init();
return {
scope: wrapper,
destroy: destroyUtils,
};
};
})();

View File

@ -0,0 +1,166 @@
(function () {
'use strict';
window.utils = window.utils || {};
var ASYNC_TABLE_FILTER_LOADING_CLASS = 'async-table-filter--loading';
var JS_INITIALIZED_CLASS = 'js-async-table-filter-initialized';
var INPUT_DEBOUNCE = 600;
// debounce function, taken from Underscore.js
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
window.utils.asyncTableFilter = function(formElement, options) {
if (!options || !options.updateTableFrom) {
return false;
}
if (formElement.matches('.' + JS_INITIALIZED_CLASS)) {
return false;
}
options = options || {};
var tableIdent = options.dbtIdent;
var formId = formElement.querySelector('[name="_formid"]').value;
var inputs = {
search: [],
input: [],
change: [],
select: [],
}
function setup() {
gatherInputs();
addEventListeners();
}
function gatherInputs() {
Array.from(formElement.querySelectorAll('input[type="search"]')).forEach(function(input) {
inputs.search.push(input);
});
Array.from(formElement.querySelectorAll('input[type="text"]')).forEach(function(input) {
inputs.input.push(input);
});
Array.from(formElement.querySelectorAll('input:not([type="text"]):not([type="search"])')).forEach(function(input) {
inputs.change.push(input);
});
Array.from(formElement.querySelectorAll('select')).forEach(function(input) {
inputs.select.push(input);
});
}
function addEventListeners() {
inputs.search.forEach(function(input) {
var debouncedInput = debounce(function() {
if (input.value.length === 0 || input.value.length > 2) {
updateTable();
}
}, INPUT_DEBOUNCE);
input.addEventListener('input', debouncedInput);
});
inputs.input.forEach(function(input) {
var debouncedInput = debounce(function() {
if (input.value.length === 0 || input.value.length > 2) {
updateTable();
}
}, INPUT_DEBOUNCE);
input.addEventListener('input', debouncedInput);
});
inputs.change.forEach(function(input) {
input.addEventListener('change', function() {
updateTable();
});
});
inputs.select.forEach(function(input) {
input.addEventListener('change', function() {
updateTable();
});
});
formElement.addEventListener('submit', function(event) {
event.preventDefault();
updateTable();
});
}
function updateTable() {
var url = serializeFormToURL();
var callback = null;
formElement.classList.add(ASYNC_TABLE_FILTER_LOADING_CLASS);
var focusedSearch = inputs.search.reduce(function(acc, input) {
return acc || (input.matches(':focus') && input);
}, null);
// focus search input
if (focusedSearch) {
var selectionStart = focusedSearch.selectionStart;
callback = function(wrapper) {
var search = wrapper.querySelector('input[type="search"]');
if (search) {
search.focus();
search.selectionStart = selectionStart;
}
};
}
options.updateTableFrom(url, options, callback);
}
function serializeFormToURL() {
var url = new URL(options.currentUrl || window.location.href);
url.searchParams.set('_formid', formId);
url.searchParams.set('_hasdata', 'true');
url.searchParams.set(tableIdent + '-page', '0');
inputs.search.forEach(function(input) {
url.searchParams.set(input.name, input.value);
});
inputs.input.forEach(function(input) {
url.searchParams.set(input.name, input.value);
});
inputs.change.forEach(function(input) {
if (input.checked) {
url.searchParams.set(input.name, input.value);
}
});
inputs.select.forEach(function(select) {
var options = Array.from(select.querySelectorAll('option'));
var selected = options.find(function(option) { return option.selected });
if (selected) {
url.searchParams.set(select.name, selected.value);
}
});
return url;
}
setup();
return {
scope: formElement,
destroy: function() {},
};
}
})();

View File

@ -3,6 +3,7 @@
window.utils = window.utils || {};
var ASYNC_TABLE_CONTENT_CHANGED_CLASS = 'async-table--changed';
var JS_INITIALIZED_CLASS = 'js-check-all-initialized';
var CHECKBOX_SELECTOR = '[type="checkbox"]';
@ -12,7 +13,7 @@
window.utils.checkAll = function(wrapper, options) {
if (!wrapper || wrapper.classList.contains(JS_INITIALIZED_CLASS)) {
if ((!wrapper || wrapper.classList.contains(JS_INITIALIZED_CLASS)) && !wrapper.classList.contains(ASYNC_TABLE_CONTENT_CHANGED_CLASS)) {
return false;
}
options = options || {};
@ -21,6 +22,8 @@
var checkboxColumn = [];
var checkAllCheckbox = null;
var utilInstances = [];
function init() {
columns = gatherColumns(wrapper);
@ -79,7 +82,7 @@
checkAllCheckbox.setAttribute('id', getCheckboxId());
th.innerHTML = '';
th.insertBefore(checkAllCheckbox, null);
window.utils.setup('checkboxRadio', checkAllCheckbox);
utilInstances.push(window.utils.setup('checkbox', checkAllCheckbox));
checkAllCheckbox.addEventListener('input', onCheckAllCheckboxInput);
setupCheckboxListeners();
@ -112,6 +115,17 @@
});
}
function destroy() {
utilInstances.forEach(function(util) {
util.destroy();
});
}
init();
return {
scope: wrapper,
destroy: destroy,
};
};
})();

View File

@ -25,36 +25,48 @@
return false;
}
var utilInstances = [];
// reactive buttons
var submitBtn = form.querySelector(SUBMIT_BUTTON_SELECTOR);
if (submitBtn) {
window.utils.setup('reactiveButton', form, { button: submitBtn });
}
utilInstances.push(window.utils.setup('reactiveButton', form));
// conditonal fieldsets
var fieldSets = Array.from(form.querySelectorAll('fieldset[data-conditional-id][data-conditional-value]'));
window.utils.setup('interactiveFieldset', form, { fieldSets });
utilInstances.push(window.utils.setup('interactiveFieldset', form, { fieldSets }));
// hide autoSubmit submit button
window.utils.setup('autoSubmit', form, options);
utilInstances.push(window.utils.setup('autoSubmit', form, options));
// async form
if (AJAX_SUBMIT_FLAG in form.dataset) {
window.utils.setup('asyncForm', form, options);
utilInstances.push(window.utils.setup('asyncForm', form, options));
}
// inputs
utilInstances.push(window.utils.setup('inputs', form, options));
form.classList.add(JS_INITIALIZED);
function destroyUtils() {
utilInstances.forEach(function(utilInstance) {
utilInstance.destroy();
});
}
return {
scope: form,
destroy: destroyUtils,
};
};
// registers input-listener for each element in <inputs> (array) and
// enables <button> if <formValidator> for these inputs returns true
window.utils.reactiveButton = function(form, options) {
options = options || {};
var button = options.button;
var button = form.querySelector(SUBMIT_BUTTON_SELECTOR);
var requireds = Array.from(form.querySelectorAll('[required]'));
if (!button) {
throw new Error('Please provide both a button to reactiveButton');
if (!button || button.matches(AUTOSUBMIT_BUTTON_SELECTOR)) {
return false;
}
if (requireds.length == 0) {
@ -84,6 +96,11 @@
button.setAttribute('disabled', 'true');
}
}
return {
scope: form,
destroy: function() {},
};
};
window.utils.interactiveFieldset = function(form, options) {
@ -121,6 +138,11 @@
addEventListeners();
updateFields();
}
return {
scope: form,
destroy: function() {},
};
};
window.utils.autoSubmit = function(form, options) {
@ -128,5 +150,10 @@
if (button) {
button.classList.add('hidden');
}
return {
scope: form,
destroy: function() {},
};
};
})();

View File

@ -10,22 +10,43 @@
}
window.utils.inputs = function(wrapper, options) {
// checkboxes / radios
var checkboxes = Array.from(wrapper.querySelectorAll('input[type="checkbox"], input[type="radio"]'));
checkboxes.filter(isNotInitialized).forEach(window.utils.checkboxRadio);
var utilInstances = [];
// checkboxes
var checkboxes = Array.from(wrapper.querySelectorAll('input[type="checkbox"]'));
checkboxes.filter(isNotInitialized).forEach(function(checkbox) {
utilInstances.push(window.utils.setup('checkbox', checkbox));
});
// radios
var radios = Array.from(wrapper.querySelectorAll('input[type="radio"]'));
radios.filter(isNotInitialized).forEach(function(radio) {
utilInstances.push(window.utils.setup('radio', radio));
});
// file-uploads
var fileUploads = Array.from(wrapper.querySelectorAll('input[type="file"]'));
fileUploads.filter(isNotInitialized).forEach(function(input) {
window.utils.fileUpload(input, options);
utilInstances.push(window.utils.setup('fileUpload', input, options));
});
// file-checkboxes
var fileCheckboxes = Array.from(wrapper.querySelectorAll('.file-checkbox'));
fileCheckboxes.filter(isNotInitialized).forEach(function(inp) {
window.utils.fileCheckbox(inp);
inp.classList.add(JS_INITIALIZED_CLASS);
fileCheckboxes.filter(isNotInitialized).forEach(function(input) {
utilInstances.push(window.utils.setup('fileCheckbox', input, options));
});
function destroyUtils() {
utilInstances.forEach(function(utilInstance) {
utilInstance.destroy();
});
}
return {
scope: wrapper,
destroy: destroyUtils,
};
};
// (multiple) dynamic file uploads
@ -113,6 +134,11 @@
updateLabel(input.files);
});
return {
scope: input,
destroy: function() {},
};
}
// to remove previously uploaded files
@ -141,20 +167,24 @@
input.classList.add(JS_INITIALIZED_CLASS);
cont.classList.add(JS_INITIALIZED_CLASS);
}
setup();
return {
scope: input,
destroy: function() {},
};
}
// turns native checkboxes and radio buttons into custom ones
window.utils.checkboxRadio = function(input) {
// turns native checkboxes into custom ones
window.utils.checkbox = function(input) {
var type = input.getAttribute('type');
if (!input.parentElement.classList.contains(type)) {
if (!input.parentElement.classList.contains('checkbox')) {
var parentEl = input.parentElement;
var siblingEl = input.nextElementSibling;
var wrapperEl = document.createElement('div');
var labelEl = document.createElement('label');
wrapperEl.classList.add(type);
wrapperEl.classList.add('checkbox');
labelEl.setAttribute('for', input.id);
wrapperEl.appendChild(input);
wrapperEl.appendChild(labelEl);
@ -166,6 +196,35 @@
parentEl.appendChild(wrapperEl);
}
}
return {
scope: input,
destroy: function() {},
};
}
// turns native radio buttons into custom ones
window.utils.radio = function(input) {
if (!input.parentElement.classList.contains('radio')) {
var parentEl = input.parentElement;
var siblingEl = input.nextElementSibling;
var wrapperEl = document.createElement('div');
wrapperEl.classList.add('radio');
wrapperEl.appendChild(input);
if (siblingEl && siblingEl.matches('label')) {
wrapperEl.appendChild(siblingEl);
}
input.classList.add(JS_INITIALIZED_CLASS);
parentEl.appendChild(wrapperEl);
}
return {
scope: input,
destroy: function() {},
};
}
})();

View File

@ -23,6 +23,8 @@
return;
}
var utilInstances = [];
var overlayElement = document.createElement('div');
var closerElement = document.createElement('div');
var triggerElement = document.querySelector('#' + modalElement.dataset.trigger);
@ -73,7 +75,7 @@
function setupForm() {
var form = modalElement.querySelector('form');
if (form) {
window.utils.setup('form', form, { headers: MODAL_HEADERS });
utilInstances.push(window.utils.setup('form', form, { headers: MODAL_HEADERS }));
}
}
@ -112,10 +114,23 @@
if (previousModalContent) {
previousModalContent.remove();
}
modalContent = withPrefixedInputIDs(modalContent);
modalElement.insertBefore(modalContent, null);
setupForm();
}
function withPrefixedInputIDs(modalContent) {
var idAttrs = ['id', 'for', 'data-conditional-id'];
idAttrs.forEach(function(attr) {
modalContent.querySelectorAll('[' + attr + ']').forEach(function(input) {
var value = modalElement.id + '__' + input.getAttribute(attr);
input.setAttribute(attr, value);
});
});
return modalContent;
}
function keyupHandler(event) {
if (event.key === 'Escape') {
close();
@ -123,5 +138,16 @@
}
setup();
function destroyUtils() {
utilInstances.forEach(function(utilInstance) {
utilInstance.destroy();
});
}
return {
scope: modalElement,
destroy: destroyUtils,
};
};
})();

View File

@ -4,6 +4,7 @@
window.utils = window.utils || {};
var registeredSetupListeners = {};
var activeInstances = {};
/**
* setup function to initiate a util (utilName) on a scope (sope) with options (options).
@ -13,53 +14,98 @@
*/
window.utils.setup = function(utilName, scope, options) {
if (!utilName || !scope) {
return;
}
options = options || {};
var listener = function(event) {
var utilInstance;
if (event.detail.targetUtil !== utilName) {
return false;
}
if (options.setupFunction) {
options.setupFunction(scope, options);
} else {
var util = window.utils[utilName];
if (!util) {
throw new Error('"' + utilName + '" is not a known js util');
}
util(scope, options);
}
};
if (registeredSetupListeners[utilName] && !options.singleton) {
registeredSetupListeners[utilName].push(listener);
} else {
window.utils.teardown(utilName);
registeredSetupListeners[utilName] = [ listener ];
// i18n
if (window.I18N) {
options.i18n = window.I18N;
}
document.addEventListener('setup', listener);
if (activeInstances[utilName]) {
var instanceWithSameScope = activeInstances[utilName]
.filter(function(instance) { return !!instance; })
.find(function(instance) {
return instance.scope === scope;
});
var isAlreadySetup = !!instanceWithSameScope;
document.dispatchEvent(new CustomEvent('setup', {
detail: { targetUtil: utilName, module: 'none' },
bubbles: true,
cancelable: true,
}));
if (isAlreadySetup) {
console.warn('Trying to setup a JS utility that\'s already been set up', { utility: utilName, scope, options });
}
}
function setup() {
var listener = function(event) {
if (event.detail.targetUtil !== utilName) {
return false;
}
if (options.setupFunction) {
utilInstance = options.setupFunction(scope, options);
} else {
var util = window.utils[utilName];
if (!util) {
throw new Error('"' + utilName + '" is not a known js util');
}
utilInstance = util(scope, options);
}
if (utilInstance) {
if (activeInstances[utilName] && Array.isArray(activeInstances[utilName])) {
activeInstances[utilName].push(utilInstance);
} else {
activeInstances[utilName] = [ utilInstance ];
}
}
};
if (registeredSetupListeners[utilName] && Array.isArray(registeredSetupListeners[utilName])) {
window.utils.teardown(utilName);
}
if (!registeredSetupListeners[utilName] || Array.isArray(registeredSetupListeners[utilName])) {
registeredSetupListeners[utilName] = [];
}
registeredSetupListeners[utilName].push(listener);
document.addEventListener('setup', listener);
document.dispatchEvent(new CustomEvent('setup', {
detail: { targetUtil: utilName, module: 'none' },
bubbles: true,
cancelable: true,
}));
}
setup();
return utilInstance;
};
window.utils.teardown = function(utilName) {
window.utils.teardown = function(utilName, destroy) {
if (registeredSetupListeners[utilName]) {
registeredSetupListeners[utilName].forEach(function(listener) {
document.removeEventListener('setup', listener);
});
registeredSetupListeners[utilName]
.filter(function(listener) { return !!listener })
.forEach(function(listener) {
document.removeEventListener('setup', listener);
});
delete registeredSetupListeners[utilName];
}
if (destroy === true && activeInstances[utilName]) {
activeInstances[utilName]
.filter(function(instance) { return !!instance })
.forEach(function(instance) {
instance.destroy();
});
delete activeInstances[utilName];
}
}
})();

View File

@ -3,7 +3,12 @@
window.utils = window.utils || {};
var JS_INITIALIZED_CLASS = 'js-show-hide-initialized';
var LOCAL_STORAGE_SHOW_HIDE = 'SHOW_HIDE';
var SHOW_HIDE_TOGGLE_CLASS = 'js-show-hide__toggle';
var SHOW_HIDE_COLLAPSED_CLASS = 'js-show-hide--collapsed';
var SHOW_HIDE_TARGET_CLASS = 'js-show-hide__target';
/**
* div
* div.js-show-hide__toggle
@ -12,9 +17,12 @@
* content here
*/
window.utils.showHide = function(wrapper, options) {
options = options || {};
function addEventHandler(el) {
el.addEventListener('click', function elClickListener() {
var newState = el.parentElement.classList.toggle('js-show-hide--collapsed');
var newState = el.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS);
updateLSState(el.dataset.shIndex || null, newState);
});
}
@ -23,33 +31,47 @@
if (!index) {
return false;
}
var lsData = fromLocalStorage();
var lsData = getLocalStorageData();
lsData[index] = state;
window.localStorage.setItem(LOCAL_STORAGE_SHOW_HIDE, JSON.stringify(lsData));
}
function collapsedStateInLocalStorage(index) {
return fromLocalStorage()[index] || null;
var lsState = getLocalStorageData();
return lsState[index];
}
function fromLocalStorage() {
function getLocalStorageData() {
return JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_SHOW_HIDE)) || {};
}
Array
.from(wrapper.querySelectorAll('.js-show-hide__toggle'))
.from(wrapper.querySelectorAll('.' + SHOW_HIDE_TOGGLE_CLASS))
.forEach(function(el) {
if (el.classList.contains(JS_INITIALIZED_CLASS)) {
return false;
}
var index = el.dataset.shIndex || null;
el.parentElement.classList.toggle(
'js-show-hide--collapsed',
collapsedStateInLocalStorage(index) || el.dataset.collapsed === 'true'
);
var isCollapsed = el.dataset.collapsed === 'true';
var lsCollapsedState = collapsedStateInLocalStorage(index);
if (typeof lsCollapsedState !== 'undefined') {
isCollapsed = lsCollapsedState;
}
el.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, isCollapsed);
Array.from(el.parentElement.children).forEach(function(el) {
if (!el.classList.contains('js-show-hide__toggle')) {
el.classList.add('js-show-hide__target');
if (!el.classList.contains('' + SHOW_HIDE_TOGGLE_CLASS)) {
el.classList.add(SHOW_HIDE_TARGET_CLASS);
}
});
el.classList.add(JS_INITIALIZED_CLASS);
addEventHandler(el);
});
return {
scope: wrapper,
destroy: function() {},
};
};
})();

View File

@ -35,14 +35,16 @@ function setupDatepicker(wrapper) {
});
}
document.addEventListener('DOMContentLoaded', function() {
var I18N = {
filesSelected: 'Dateien ausgewählt', // TODO: interpolate these to be translated
selectFile: 'Datei auswählen',
selectFiles: 'Datei(en) auswählen',
};
// this global I18N object will be picked up automatically by the setup util
window.I18N = {
filesSelected: 'Dateien ausgewählt', // TODO: interpolate these to be translated
selectFile: 'Datei auswählen',
selectFiles: 'Datei(en) auswählen',
asyncFormFailure: 'Da ist etwas schief gelaufen, das tut uns Leid.<br>Falls das erneut passiert schicke uns gerne eine kurze Beschreibung dieses Ereignisses über das Hilfe-Widget rechts oben.<br><br>Vielen Dank für deine Hilfe',
};
document.addEventListener('DOMContentLoaded', function() {
window.utils.setup('flatpickr', document.body, { setupFunction: setupDatepicker });
window.utils.setup('showHide', document.body);
window.utils.setup('inputs', document.body, { i18n: I18N });
window.utils.setup('inputs', document.body);
});

View File

@ -8,6 +8,7 @@
--color-lightwhite: #fcfffa;
--color-grey: #B1B5C0;
--color-grey-light: #efefef;
--color-grey-lighter: #f5f5f5;
--color-grey-medium: #9A989E;
--color-font: #34303a;
--color-fontsec: #5b5861;

View File

@ -1,8 +1,9 @@
$newline never
<section>
<form method=GET action=#{filterAction} enctype=#{filterEnctype}>
^{filterWgdt}
<button>
^{btnLabel BtnSubmit}
<section>
^{scrolltable}
<div .table-filter>
<h3 .js-show-hide__toggle data-sh-index=table-filter data-collapsed=true>Filter
<div>
<form .table-filter-form method=GET action=#{filterAction} enctype=#{filterEnctype}>
^{filterWgdt}
<button type=submit data-autosubmit>
^{btnLabel BtnSubmit}
^{scrolltable}

View File

@ -0,0 +1,4 @@
.table-filter {
border-bottom: 1px solid #d3d3d3;
margin-bottom: 13px;
}

View File

@ -1,11 +1,10 @@
document.addEventListener('DOMContentLoaded', function() {
var dbtIdent = #{String $ dbtIdent};
var dbtIdent = #{String dbtIdent};
var headerDBTableShortcircuit = #{String (toPathPiece HeaderDBTableShortcircuit)};
var selector = '#' + dbtIdent + '-table-wrapper';
var wrapper = document.querySelector(selector);
if (wrapper) {
window.utils.setup('asyncTable', wrapper, { headerDBTableShortcircuit, dbtIdent });
window.utils.setup('checkAll', wrapper);
}
});

View File

@ -0,0 +1,14 @@
$newline never
<div .radio-group>
$if not isReq
<div .radio>
<input id=#{theId}-none *{attrs} type=radio name=#{name} value=none checked>
<label for=#{theId}-none>_{MsgCourseFilterNone}
<div .radio>
<input id=#{theId}-yes *{attrs} type=radio name=#{name} value=yes :showVal id val:checked>
<label for=#{theId}-yes>_{MsgBoolYes}
<div .radio>
<input id=#{theId}-no *{attrs} type=radio name=#{name} value=no :showVal not val:checked>
<label for=#{theId}-no>_{MsgBoolNo}

View File

@ -1,7 +1,9 @@
document.addEventListener('DOMContentLoaded', function() {
// TODO: replace for loop with one precise query for this specific modal instance
var modalElements = Array.from(document.querySelectorAll('.modal'));
modalElements.forEach(function(modal) {
var modalIdent = #{String modalId};
var selector = '#modal-' + modalIdent;
var modal = document.querySelector(selector);
if (modal) {
window.utils.setup('modal', modal);
});
}
});