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 CourseDeregisterUntilTip: Abmeldung darf auch unbegrenzt erlaubt bleiben
CourseFilterSearch: Volltext-Suche CourseFilterSearch: Volltext-Suche
CourseFilterRegistered: Registriert CourseFilterRegistered: Registriert
CourseFilterNone: Egal
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen? CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
CourseDeleted: Kurs gelöscht CourseDeleted: Kurs gelöscht
CourseUserNote: Notiz CourseUserNote: Notiz

View File

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

View File

@ -1,11 +1,8 @@
{ nixpkgs ? import <nixpkgs> {}, compiler ? null }: { nixpkgs ? import <nixpkgs>, compiler ? null }:
let let
inherit (nixpkgs) pkgs; inherit (nixpkgs {}) pkgs;
haskellPackages = if isNull compiler then pkgs.haskellPackages else pkgs.haskell.packages."${compiler}";
haskellPackages = if isNull compiler
then pkgs.haskellPackages
else pkgs.haskell.packages."${compiler}";
drv = haskellPackages.callPackage ./uniworx.nix {}; drv = haskellPackages.callPackage ./uniworx.nix {};
@ -26,21 +23,29 @@ let
shellHook = '' shellHook = ''
export PROMPT_INFO="${oldAttrs.name}" export PROMPT_INFO="${oldAttrs.name}"
pgDir=$(mktemp -d) if [[ -z "$PGHOST" ]]; then
pgSockDir=$(mktemp -d) set -xe
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}
cleanup() { pgDir=$(mktemp -d)
pg_ctl stop -D ''${pgDir} pgSockDir=$(mktemp -d)
rm -rvf ''${pgDir} ''${pgSockDir} ''${pgLogFile} 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} ${oldAttrs.shellHook}
''; '';

View File

@ -220,7 +220,7 @@ instance RenderMessage UniWorX MsgLanguage where
instance RenderMessage UniWorX (UnsupportedAuthPredicate (Route UniWorX)) where instance RenderMessage UniWorX (UnsupportedAuthPredicate (Route UniWorX)) where
renderMessage f ls (UnsupportedAuthPredicate tag route) = renderMessage f ls $ MsgUnsupportedAuthPredicate tag (show route) 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 ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel
embedRenderMessage ''UniWorX ''StudyFieldType id embedRenderMessage ''UniWorX ''StudyFieldType id
embedRenderMessage ''UniWorX ''SheetFileType id embedRenderMessage ''UniWorX ''SheetFileType id
@ -1021,6 +1021,7 @@ siteLayout' headingOverride widget = do
addScript $ StaticR js_utils_asidenav_js addScript $ StaticR js_utils_asidenav_js
addScript $ StaticR js_utils_asyncForm_js addScript $ StaticR js_utils_asyncForm_js
addScript $ StaticR js_utils_asyncTable_js addScript $ StaticR js_utils_asyncTable_js
addScript $ StaticR js_utils_asyncTableFilter_js
addScript $ StaticR js_utils_checkAll_js addScript $ StaticR js_utils_checkAll_js
addScript $ StaticR js_utils_httpClient_js addScript $ StaticR js_utils_httpClient_js
addScript $ StaticR js_utils_form_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_alerts_scss
addStylesheet $ StaticR css_utils_asidenav_scss addStylesheet $ StaticR css_utils_asidenav_scss
addStylesheet $ StaticR css_utils_asyncForm_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_form_scss
addStylesheet $ StaticR css_utils_inputs_scss addStylesheet $ StaticR css_utils_inputs_scss
addStylesheet $ StaticR css_utils_modal_scss addStylesheet $ StaticR css_utils_modal_scss
addStylesheet $ StaticR css_utils_radio_scss
addStylesheet $ StaticR css_utils_showHide_scss addStylesheet $ StaticR css_utils_showHide_scss
addStylesheet $ StaticR css_utils_tabber_scss addStylesheet $ StaticR css_utils_tabber_scss
addStylesheet $ StaticR css_utils_tooltip_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 qualified Data.UUID.Cryptographic as UUID
import Control.Monad.Trans.Writer (mapWriterT)
-- BEGIN - Buttons needed only here -- BEGIN - Buttons needed only here
data ButtonCreate = CreateMath | CreateInf -- Dummy for Example data ButtonCreate = CreateMath | CreateInf -- Dummy for Example
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
@ -84,15 +86,12 @@ postAdminTestR = do
_other -> addMessage Warning "KEIN Knopf erkannt" _other -> addMessage Warning "KEIN Knopf erkannt"
((emailResult, emailWidget), emailEnctype) <- runFormPost . identifyForm "email" $ renderAForm FormStandard emailTestForm ((emailResult, emailWidget), emailEnctype) <- runFormPost . identifyForm "email" $ renderAForm FormStandard emailTestForm
case emailResult of formResultModal emailResult AdminTestR $ \(email, ls) -> do
(FormSuccess (email, ls)) -> do jId <- mapWriterT runDB $ do
jId <- runDB $ do jId <- queueJob $ JobSendTestEmail email ls
jId <- queueJob $ JobSendTestEmail email ls tell . pure $ Message Success [shamlet|Email-test gestartet (Job ##{tshow (fromSqlKey jId)})|]
addMessage Success [shamlet|Email-test gestartet (Job ##{tshow (fromSqlKey jId)})|] return jId
return jId writeJobCtl $ JobCtlPerform jId
writeJobCtl $ JobCtlPerform jId
FormMissing -> return ()
(FormFailure errs) -> forM_ errs $ addMessage Error . toHtml
let emailWidget' = [whamlet| let emailWidget' = [whamlet|
<form method=post action=@{AdminTestR} enctype=#{emailEnctype} data-ajax-submit> <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 -- 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) colTerm = sortable (Just "term") (i18nCell MsgTerm)
$ \DBRow{ dbrOutput=(_, _, course, _, _) } -> $ \DBRow{ dbrOutput=(_, _, course, _, _) } ->
-- cell [whamlet| _{untermKey $ course ^. _3}|] -- lange, internationale Semester -- cell [whamlet| _{untermKey $ course ^. _3}|] -- lange, internationale Semester
textCell $ termToText $ unTermKey $ course ^. _3 -- kurze Semsterkürzel 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) colSchool = sortable (Just "school") (i18nCell MsgCourseSchool)
$ \DBRow{ dbrOutput=(_, _, course, _, _) } -> $ \DBRow{ dbrOutput=(_, _, course, _, _) } ->
anchorCell (TermSchoolCourseListR (course ^. _3) (course ^. _4)) [whamlet|#{unSchoolKey (course ^. _4)}|] 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) colCourse = sortable (Just "course") (i18nCell MsgCourse)
$ \DBRow{ dbrOutput=(_, _, (_,csh,tid,sid), _, _) } -> courseCellCL (tid,sid,csh) $ \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) colSheet = sortable (Just "sheet") (i18nCell MsgSheet)
$ \DBRow{ dbrOutput=(_, sheet, course, _, _) } -> $ \DBRow{ dbrOutput=(_, sheet, course, _, _) } ->
let tid = course ^. _3 let tid = course ^. _3
@ -109,16 +109,16 @@ colSheet = sortable (Just "sheet") (i18nCell MsgSheet)
shn = sheetName $ entityVal sheet shn = sheetName $ entityVal sheet
in anchorCell (CSheetR tid ssh csh shn SShowR) [whamlet|#{display shn}|] 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) colSheetType = sortable (toNothing "sheetType") (i18nCell MsgSheetType)
$ \DBRow{ dbrOutput=(_, sheet, _, _, _) } -> i18nCell . sheetType $ entityVal sheet $ \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 colCorrector = sortable (Just "corrector") (i18nCell MsgCorrector) $ \case
DBRow{ dbrOutput = (_, _, _, Nothing , _) } -> cell mempty DBRow{ dbrOutput = (_, _, _, Nothing , _) } -> cell mempty
DBRow{ dbrOutput = (_, _, _, Just (Entity _ User{..}), _) } -> userCell userDisplayName userSurname 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) colSubmissionLink = sortable Nothing (i18nCell MsgSubmission)
$ \DBRow{ dbrOutput=(submission, sheet, course, _, _) } -> $ \DBRow{ dbrOutput=(submission, sheet, course, _, _) } ->
let csh = course ^. _2 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 :: 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 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 colSubmittors = sortable (Just "submittors") (i18nCell MsgSubmissionUsers) $ \DBRow{ dbrOutput=(_, _, course, _, users) } -> let
csh = course ^. _2 csh = course ^. _2
tid = course ^. _3 tid = course ^. _3
@ -146,7 +146,7 @@ colSubmittors = sortable (Just "submittors") (i18nCell MsgSubmissionUsers) $ \DB
Just p -> [whamlet|^{nameWidget userDisplayName userSurname} (#{review _PseudonymText p})|] Just p -> [whamlet|^{nameWidget userDisplayName userSurname} (#{review _PseudonymText p})|]
in protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] 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 colSMatrikel = sortable Nothing (i18nCell MsgMatrikelNr) $ \DBRow{ dbrOutput=(_, _, _, _, users) } -> let
protoCell = listCell (Map.toList users) $ \(userId, (User{..}, _)) -> anchorCellM (AdminUserR <$> encrypt userId) (maybe mempty toWidget userMatrikelnummer) protoCell = listCell (Map.toList users) $ \(userId, (User{..}, _)) -> anchorCellM (AdminUserR <$> encrypt userId) (maybe mempty toWidget userMatrikelnummer)
in protoCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] 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 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, _, _) } -> colAssigned = sortable (Just "assignedtime") (i18nCell MsgAssignedTime) $ \DBRow{ dbrOutput=(Entity _subId Submission{..}, _sheet, _course, _, _) } ->
maybe mempty dateTimeCell submissionRatingAssigned 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, _, _) } -> colRated = sortable (Just "ratingtime") (i18nCell MsgRatingTime) $ \DBRow{ dbrOutput=(Entity _subId Submission{..}, _sheet, _course, _, _) } ->
maybe mempty dateTimeCell submissionRatingTime 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 colPseudonyms = sortable Nothing (i18nCell MsgPseudonyms) $ \DBRow{ dbrOutput=(_, _, _, _, users) } -> let
lCell = listCell (catMaybes $ snd . snd <$> Map.toList users) $ \pseudo -> lCell = listCell (catMaybes $ snd . snd <$> Map.toList users) $ \pseudo ->
cell [whamlet|#{review _PseudonymText pseudo}|] cell [whamlet|#{review _PseudonymText pseudo}|]
in lCell & cellAttrs <>~ [("class", "list--inline list--comma-separated")] 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 colRatedField = sortable Nothing (i18nCell MsgRatingDone) $ formCell id
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _) } -> return subId) (\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)) (\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 colPointsField = sortable (Just "rating") (i18nCell MsgColumnRatingPoints) $ formCell id
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _) } -> return subId) (\DBRow{ dbrOutput=(Entity subId _, _, _, _, _) } -> return subId)
(\DBRow{ dbrOutput=(Entity _ Submission{..}, Entity _ Sheet{..}, _, _, _) } mkUnique -> case sheetType of (\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) _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 colCommentField = sortable Nothing (i18nCell MsgRatingComment) $ formCell id
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _) } -> return subId) (\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)) (\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" [ ( "term"
, SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseTerm , SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseTerm
) )
, ( "school"
, SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseSchool
)
, ( "course" , ( "course"
, SortColumn $ \((course `E.InnerJoin` _ `E.InnerJoin` _) `E.LeftOuterJoin` _) -> course E.^. CourseShorthand , 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. -- NOTE: Outdated way to use dbTable; see ProfileDataR Handler for a more recent method.
type CourseTableData = DBRow (Entity Course, Int64, Bool, Entity School) 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) colCourse = sortable (Just "course") (i18nCell MsgCourse)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR)
@ -51,12 +51,12 @@ colDescription = sortable Nothing mempty
Nothing -> mempty Nothing -> mempty
(Just descr) -> cell $ modal (toWidget $ hasComment True) (Right $ toWidget descr) (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) colCShort = sortable (Just "cshort") (i18nCell MsgCourseShort)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|#{display courseShorthand}|] 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) -- colCShortDescr = sortable (Just "cshort") (i18nCell MsgCourseShort)
-- $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> mappend -- $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> mappend
-- ( anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|#{display courseShorthand}|] ) -- ( 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) colTerm = sortable (Just "term") (i18nCell MsgTerm)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
anchorCell (TermCourseListR courseTerm) [whamlet|#{display courseTerm}|] 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) colSchool = sortable (Just "school") (i18nCell MsgCourseSchool)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}) } ->
anchorCell (TermSchoolCourseListR courseTerm courseSchool) [whamlet|#{display schoolName}|] 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) colSchoolShort = sortable (Just "schoolshort") (i18nCell MsgCourseSchoolShort)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}) } ->
anchorCell (TermSchoolCourseListR courseTerm courseSchool) [whamlet|#{display schoolShorthand}|] 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) colRegFrom = sortable (Just "register-from") (i18nCell MsgRegisterFrom)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
maybe mempty dateTimeCell courseRegisterFrom maybe mempty dateTimeCell courseRegisterFrom
-- cell $ traverse (formatTime SelFormatDateTime) courseRegisterFrom >>= maybe mempty toWidget -- 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) colRegTo = sortable (Just "register-to") (i18nCell MsgRegisterTo)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
maybe mempty dateTimeCell courseRegisterTo 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) colMembers = sortable (Just "members") (i18nCell MsgCourseMembers)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, currentParticipants, _, _) } -> i18nCell $ case courseCapacity of $ \DBRow{ dbrOutput=(Entity _ Course{..}, currentParticipants, _, _) } -> i18nCell $ case courseCapacity of
Nothing -> MsgCourseMembersCount currentParticipants Nothing -> MsgCourseMembersCount currentParticipants
Just limit -> MsgCourseMembersCountLimited currentParticipants limit 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) colRegistered = sortable (Just "registered") (i18nCell MsgRegistered)
$ \DBRow{ dbrOutput=(_, _, registered, _) } -> tickmarkCell registered $ \DBRow{ dbrOutput=(_, _, registered, _) } -> tickmarkCell registered

View File

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

View File

@ -522,6 +522,30 @@ secretJsonField = Field{..}
|] |]
fieldEnctype = UrlEncoded 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. funcForm :: forall k v m.
( Finite k, Ord k ( Finite k, Ord k
@ -671,5 +695,5 @@ formResultModal res finalDest handler = maybeT_ $ do
if if
| isModal -> sendResponse $ toJSON messages | isModal -> sendResponse $ toJSON messages
| otherwise -> do | otherwise -> do
forM_ messages $ \Message{..} -> addMessage messageClass messageContent forM_ messages $ \Message{..} -> addMessage messageStatus messageContent
redirect finalDest redirect finalDest

View File

@ -3,7 +3,7 @@ module Import.NoFoundation
, MForm , MForm
) where ) 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 as Import
import Model.Types.JSON as Import import Model.Types.JSON as Import
import Model.Migration as Import import Model.Migration as Import

View File

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

View File

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

View File

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

View File

@ -1,8 +1,14 @@
{ ghc, nixpkgs ? (import <nixpkgs> {}) }: { ghc, nixpkgs ? import <nixpkgs> }:
let let
inherit (nixpkgs) haskell pkgs; snapshot = "lts-10.5";
haskellPackages = if ghc.version == pkgs.haskellPackages.ghc.version then pkgs.haskellPackages else pkgs.haskell.packages."ghc${builtins.replaceStrings ["."] [""] ghc.version}"; 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 { in haskell.lib.buildStackProject {
inherit ghc; inherit ghc;
name = "stackenv"; name = "stackenv";

View File

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

View File

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

View File

@ -1,9 +1,78 @@
.async-form-response { .async-form-response {
margin: 20px 0; 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 { .async-form-loading {
opacity: 0.1; opacity: 0.1;
transition: opacity 800ms ease-in-out; transition: opacity 800ms ease-out;
pointer-events: none; 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%; width: 100%;
max-width: 600px; max-width: 600px;
-webkit-appearance: none;
align-items: center; align-items: center;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 4px; border-radius: 4px;
@ -162,6 +161,11 @@ textarea {
} }
/* OPTIONS */ /* OPTIONS */
select {
-webkit-appearance: menulist;
}
select, select,
option { option {
font-size: 1rem; font-size: 1rem;
@ -171,7 +175,8 @@ option {
border-radius: 2px; border-radius: 2px;
outline: 0; outline: 0;
color: #363636; color: #363636;
min-width: 200px; min-width: 250px;
width: auto;
background-color: #f3f3f3; background-color: #f3f3f3;
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05); 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 */ /* CUSTOM FILE INPUT */
.file-input__label { .file-input__label {
cursor: pointer; cursor: pointer;

View File

@ -3,7 +3,7 @@
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%) scale(0.8, 0.8); transform: translate(-50%, -50%) scale(0.8, 0.8);
display: block; display: flex;
background-color: rgba(255, 255, 255, 1); background-color: rgba(255, 255, 255, 1);
min-width: 60vw; min-width: 60vw;
min-height: 100px; min-height: 100px;
@ -26,10 +26,6 @@
z-index: 200; z-index: 200;
transform: translate(-50%, -50%) scale(1, 1); transform: translate(-50%, -50%) scale(1, 1);
} }
.modal__content {
margin: 20px 0;
}
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
@ -96,3 +92,8 @@
color: white; 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 { .js-show-hide__toggle {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
padding: 3px 7px;
&:hover {
background-color: var(--color-grey-lighter);
cursor: pointer;
}
} }
.js-show-hide__toggle::before { .js-show-hide__toggle::before {
content: ''; content: '';
position: absolute; position: absolute;
width: 0; width: $show-hide-toggle-size;
height: 0; height: $show-hide-toggle-size;
left: -28px; left: -15px;
top: 6px; top: 12px - $show-hide-toggle-size / 2;
color: var(--color-primary); color: var(--color-primary);
border-right: 8px solid transparent; border-right: 2px solid currentColor;
border-top: 8px solid transparent; border-top: 2px solid currentColor;
border-left: 8px solid transparent;
border-bottom: 8px solid currentColor;
transition: transform .2s ease; 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 { .js-show-hide__target {
@ -26,7 +34,7 @@
.js-show-hide--collapsed { .js-show-hide--collapsed {
.js-show-hide__toggle::before { .js-show-hide__toggle::before {
transform: rotate(180deg); transform: translateY($show-hide-toggle-size / 3) rotate(135deg);
} }
.js-show-hide__target { .js-show-hide__target {

View File

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

View File

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

View File

@ -6,9 +6,12 @@
var ASYNC_FORM_RESPONSE_CLASS = 'async-form-response'; var ASYNC_FORM_RESPONSE_CLASS = 'async-form-response';
var ASYNC_FORM_LOADING_CLASS = 'async-form-loading'; var ASYNC_FORM_LOADING_CLASS = 'async-form-loading';
var ASYNC_FORM_MIN_DELAY = 600; 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) { window.utils.asyncForm = function(formElement, options) {
options = options || {};
var lastRequestTimestamp = 0; var lastRequestTimestamp = 0;
function setup() { function setup() {
@ -16,19 +19,27 @@
} }
function processResponse(response) { function processResponse(response) {
var responseElement = document.createElement('div'); var responseElement = makeResponseElement(response.content, response.status);
responseElement.classList.add(ASYNC_FORM_RESPONSE_CLASS);
responseElement.innerHTML = response.content;
var parentElement = formElement.parentElement; var parentElement = formElement.parentElement;
// make sure there is a delay between click and response // make sure there is a delay between click and response
var delay = Math.max(0, ASYNC_FORM_MIN_DELAY + lastRequestTimestamp - Date.now()); var delay = Math.max(0, ASYNC_FORM_MIN_DELAY + lastRequestTimestamp - Date.now());
setTimeout(function() { setTimeout(function() {
parentElement.insertBefore(responseElement, formElement); parentElement.insertBefore(responseElement, formElement);
formElement.remove(); formElement.remove();
}, delay); }, 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) { function submitHandler(event) {
event.preventDefault(); event.preventDefault();
@ -47,14 +58,29 @@
window.utils.httpClient.post(url, headers, body) window.utils.httpClient.post(url, headers, body)
.then(function(response) { .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) { }).then(function(response) {
processResponse(response[0]) processResponse(response[0]);
}).catch(function(error) { }).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(); setup();
return {
scope: formElement,
destroy: function() {},
};
}; };
})(); })();

View File

@ -5,6 +5,10 @@
var HEADER_HEIGHT = 80; var HEADER_HEIGHT = 80;
var RESET_OPTIONS = [ 'scrollTo' ]; 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) { window.utils.asyncTable = function(wrapper, options) {
@ -17,6 +21,8 @@
var pagesizeForm; var pagesizeForm;
var scrollTable; var scrollTable;
var utilInstances = [];
function init() { function init() {
var table = wrapper.querySelector('#' + tableIdent); var table = wrapper.querySelector('#' + tableIdent);
@ -42,17 +48,30 @@
// pagesize form // pagesize form
pagesizeForm = wrapper.querySelector('#' + tableIdent + '-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 // take options into account
if (options && options.scrollTo) { if (options.scrollTo) {
window.scrollTo(options.scrollTo); window.scrollTo(options.scrollTo);
} }
if (options && options.horizPos && scrollTable) { if (options.horizPos && scrollTable) {
scrollTable.scrollLeft = options.horizPos; scrollTable.scrollLeft = options.horizPos;
} }
setupListeners(); setupListeners();
wrapper.classList.add('js-initialized'); wrapper.classList.add(JS_INITIALIZED_CLASS);
} }
function setupListeners() { function setupListeners() {
@ -117,32 +136,22 @@
} }
function changePagesizeHandler(event) { function changePagesizeHandler(event) {
var currentTableUrl = options.currentUrl || window.location.href; var pagesizeParamKey = tableIdent + '-pagesize';
var url = getUrlWithUpdatedPagesize(currentTableUrl, event.target.value); var pageParamKey = tableIdent + '-page';
url = new URL(getUrlWithResetPagenumber(url)); var url = new URL(options.currentUrl || window.location.href);
url.searchParams.set(pagesizeParamKey, event.target.value);
url.searchParams.set(pageParamKey, 0);
updateTableFrom(url); 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 // 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) { if (!window.utils.httpClient) {
throw new Error('httpClient not found!'); throw new Error('httpClient not found!');
} }
wrapper.classList.add(ASYNC_TABLE_LOADING_CLASS);
tableOptions = tableOptions || {}; tableOptions = tableOptions || {};
var headers = { var headers = {
'Accept': 'text/html', 'Accept': 'text/html',
@ -157,6 +166,11 @@
tableOptions.currentUrl = url.href; tableOptions.currentUrl = url.href;
removeListeners(); removeListeners();
updateWrapperContents(data, tableOptions); updateWrapperContents(data, tableOptions);
if (callback && typeof callback === 'function') {
callback(wrapper);
}
wrapper.classList.remove(ASYNC_TABLE_LOADING_CLASS);
}).catch(function(err) { }).catch(function(err) {
console.error(err); console.error(err);
}); });
@ -165,11 +179,11 @@
function updateWrapperContents(newHtml, tableOptions) { function updateWrapperContents(newHtml, tableOptions) {
tableOptions = tableOptions || {}; tableOptions = tableOptions || {};
wrapper.innerHTML = newHtml; 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 // merge global options and table specific options
var resetOptions = {}; var resetOptions = {};
Object.keys(options) Object.keys(options)
@ -195,13 +209,24 @@
window.utils.setup('asyncTable', wrapper, combinedOptions); window.utils.setup('asyncTable', wrapper, combinedOptions);
Array.from(wrapper.querySelectorAll('form')).forEach(function(form) { 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) { 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(); 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 || {}; window.utils = window.utils || {};
var ASYNC_TABLE_CONTENT_CHANGED_CLASS = 'async-table--changed';
var JS_INITIALIZED_CLASS = 'js-check-all-initialized'; var JS_INITIALIZED_CLASS = 'js-check-all-initialized';
var CHECKBOX_SELECTOR = '[type="checkbox"]'; var CHECKBOX_SELECTOR = '[type="checkbox"]';
@ -12,7 +13,7 @@
window.utils.checkAll = function(wrapper, options) { 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; return false;
} }
options = options || {}; options = options || {};
@ -21,6 +22,8 @@
var checkboxColumn = []; var checkboxColumn = [];
var checkAllCheckbox = null; var checkAllCheckbox = null;
var utilInstances = [];
function init() { function init() {
columns = gatherColumns(wrapper); columns = gatherColumns(wrapper);
@ -79,7 +82,7 @@
checkAllCheckbox.setAttribute('id', getCheckboxId()); checkAllCheckbox.setAttribute('id', getCheckboxId());
th.innerHTML = ''; th.innerHTML = '';
th.insertBefore(checkAllCheckbox, null); th.insertBefore(checkAllCheckbox, null);
window.utils.setup('checkboxRadio', checkAllCheckbox); utilInstances.push(window.utils.setup('checkbox', checkAllCheckbox));
checkAllCheckbox.addEventListener('input', onCheckAllCheckboxInput); checkAllCheckbox.addEventListener('input', onCheckAllCheckboxInput);
setupCheckboxListeners(); setupCheckboxListeners();
@ -112,6 +115,17 @@
}); });
} }
function destroy() {
utilInstances.forEach(function(util) {
util.destroy();
});
}
init(); init();
return {
scope: wrapper,
destroy: destroy,
};
}; };
})(); })();

View File

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

View File

@ -10,22 +10,43 @@
} }
window.utils.inputs = function(wrapper, options) { window.utils.inputs = function(wrapper, options) {
// checkboxes / radios
var checkboxes = Array.from(wrapper.querySelectorAll('input[type="checkbox"], input[type="radio"]')); var utilInstances = [];
checkboxes.filter(isNotInitialized).forEach(window.utils.checkboxRadio);
// 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 // file-uploads
var fileUploads = Array.from(wrapper.querySelectorAll('input[type="file"]')); var fileUploads = Array.from(wrapper.querySelectorAll('input[type="file"]'));
fileUploads.filter(isNotInitialized).forEach(function(input) { fileUploads.filter(isNotInitialized).forEach(function(input) {
window.utils.fileUpload(input, options); utilInstances.push(window.utils.setup('fileUpload', input, options));
}); });
// file-checkboxes // file-checkboxes
var fileCheckboxes = Array.from(wrapper.querySelectorAll('.file-checkbox')); var fileCheckboxes = Array.from(wrapper.querySelectorAll('.file-checkbox'));
fileCheckboxes.filter(isNotInitialized).forEach(function(inp) { fileCheckboxes.filter(isNotInitialized).forEach(function(input) {
window.utils.fileCheckbox(inp); utilInstances.push(window.utils.setup('fileCheckbox', input, options));
inp.classList.add(JS_INITIALIZED_CLASS);
}); });
function destroyUtils() {
utilInstances.forEach(function(utilInstance) {
utilInstance.destroy();
});
}
return {
scope: wrapper,
destroy: destroyUtils,
};
}; };
// (multiple) dynamic file uploads // (multiple) dynamic file uploads
@ -113,6 +134,11 @@
updateLabel(input.files); updateLabel(input.files);
}); });
return {
scope: input,
destroy: function() {},
};
} }
// to remove previously uploaded files // to remove previously uploaded files
@ -141,20 +167,24 @@
input.classList.add(JS_INITIALIZED_CLASS); input.classList.add(JS_INITIALIZED_CLASS);
cont.classList.add(JS_INITIALIZED_CLASS); cont.classList.add(JS_INITIALIZED_CLASS);
} }
setup(); setup();
return {
scope: input,
destroy: function() {},
};
} }
// turns native checkboxes and radio buttons into custom ones // turns native checkboxes into custom ones
window.utils.checkboxRadio = function(input) { window.utils.checkbox = function(input) {
var type = input.getAttribute('type'); if (!input.parentElement.classList.contains('checkbox')) {
if (!input.parentElement.classList.contains(type)) {
var parentEl = input.parentElement; var parentEl = input.parentElement;
var siblingEl = input.nextElementSibling; var siblingEl = input.nextElementSibling;
var wrapperEl = document.createElement('div'); var wrapperEl = document.createElement('div');
var labelEl = document.createElement('label'); var labelEl = document.createElement('label');
wrapperEl.classList.add(type); wrapperEl.classList.add('checkbox');
labelEl.setAttribute('for', input.id); labelEl.setAttribute('for', input.id);
wrapperEl.appendChild(input); wrapperEl.appendChild(input);
wrapperEl.appendChild(labelEl); wrapperEl.appendChild(labelEl);
@ -166,6 +196,35 @@
parentEl.appendChild(wrapperEl); 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; return;
} }
var utilInstances = [];
var overlayElement = document.createElement('div'); var overlayElement = document.createElement('div');
var closerElement = document.createElement('div'); var closerElement = document.createElement('div');
var triggerElement = document.querySelector('#' + modalElement.dataset.trigger); var triggerElement = document.querySelector('#' + modalElement.dataset.trigger);
@ -73,7 +75,7 @@
function setupForm() { function setupForm() {
var form = modalElement.querySelector('form'); var form = modalElement.querySelector('form');
if (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) { if (previousModalContent) {
previousModalContent.remove(); previousModalContent.remove();
} }
modalContent = withPrefixedInputIDs(modalContent);
modalElement.insertBefore(modalContent, null); modalElement.insertBefore(modalContent, null);
setupForm(); 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) { function keyupHandler(event) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
close(); close();
@ -123,5 +138,16 @@
} }
setup(); setup();
function destroyUtils() {
utilInstances.forEach(function(utilInstance) {
utilInstance.destroy();
});
}
return {
scope: modalElement,
destroy: destroyUtils,
};
}; };
})(); })();

View File

@ -4,6 +4,7 @@
window.utils = window.utils || {}; window.utils = window.utils || {};
var registeredSetupListeners = {}; var registeredSetupListeners = {};
var activeInstances = {};
/** /**
* setup function to initiate a util (utilName) on a scope (sope) with options (options). * 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) { window.utils.setup = function(utilName, scope, options) {
if (!utilName || !scope) { if (!utilName || !scope) {
return; return;
} }
options = options || {}; options = options || {};
var listener = function(event) { var utilInstance;
if (event.detail.targetUtil !== utilName) { // i18n
return false; if (window.I18N) {
} options.i18n = window.I18N;
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 ];
} }
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', { if (isAlreadySetup) {
detail: { targetUtil: utilName, module: 'none' }, console.warn('Trying to setup a JS utility that\'s already been set up', { utility: utilName, scope, options });
bubbles: true, }
cancelable: true, }
}));
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]) { if (registeredSetupListeners[utilName]) {
registeredSetupListeners[utilName].forEach(function(listener) { registeredSetupListeners[utilName]
document.removeEventListener('setup', listener); .filter(function(listener) { return !!listener })
}); .forEach(function(listener) {
document.removeEventListener('setup', listener);
});
delete registeredSetupListeners[utilName]; 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 || {}; window.utils = window.utils || {};
var JS_INITIALIZED_CLASS = 'js-show-hide-initialized';
var LOCAL_STORAGE_SHOW_HIDE = 'SHOW_HIDE'; 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
* div.js-show-hide__toggle * div.js-show-hide__toggle
@ -12,9 +17,12 @@
* content here * content here
*/ */
window.utils.showHide = function(wrapper, options) { window.utils.showHide = function(wrapper, options) {
options = options || {};
function addEventHandler(el) { function addEventHandler(el) {
el.addEventListener('click', function elClickListener() { 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); updateLSState(el.dataset.shIndex || null, newState);
}); });
} }
@ -23,33 +31,47 @@
if (!index) { if (!index) {
return false; return false;
} }
var lsData = fromLocalStorage(); var lsData = getLocalStorageData();
lsData[index] = state; lsData[index] = state;
window.localStorage.setItem(LOCAL_STORAGE_SHOW_HIDE, JSON.stringify(lsData)); window.localStorage.setItem(LOCAL_STORAGE_SHOW_HIDE, JSON.stringify(lsData));
} }
function collapsedStateInLocalStorage(index) { 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)) || {}; return JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_SHOW_HIDE)) || {};
} }
Array Array
.from(wrapper.querySelectorAll('.js-show-hide__toggle')) .from(wrapper.querySelectorAll('.' + SHOW_HIDE_TOGGLE_CLASS))
.forEach(function(el) { .forEach(function(el) {
if (el.classList.contains(JS_INITIALIZED_CLASS)) {
return false;
}
var index = el.dataset.shIndex || null; var index = el.dataset.shIndex || null;
el.parentElement.classList.toggle( var isCollapsed = el.dataset.collapsed === 'true';
'js-show-hide--collapsed', var lsCollapsedState = collapsedStateInLocalStorage(index);
collapsedStateInLocalStorage(index) || el.dataset.collapsed === 'true' if (typeof lsCollapsedState !== 'undefined') {
); isCollapsed = lsCollapsedState;
}
el.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, isCollapsed);
Array.from(el.parentElement.children).forEach(function(el) { Array.from(el.parentElement.children).forEach(function(el) {
if (!el.classList.contains('js-show-hide__toggle')) { if (!el.classList.contains('' + SHOW_HIDE_TOGGLE_CLASS)) {
el.classList.add('js-show-hide__target'); el.classList.add(SHOW_HIDE_TARGET_CLASS);
} }
}); });
el.classList.add(JS_INITIALIZED_CLASS);
addEventHandler(el); addEventHandler(el);
}); });
return {
scope: wrapper,
destroy: function() {},
};
}; };
})(); })();

View File

@ -35,14 +35,16 @@ function setupDatepicker(wrapper) {
}); });
} }
document.addEventListener('DOMContentLoaded', function() { // this global I18N object will be picked up automatically by the setup util
var I18N = { window.I18N = {
filesSelected: 'Dateien ausgewählt', // TODO: interpolate these to be translated filesSelected: 'Dateien ausgewählt', // TODO: interpolate these to be translated
selectFile: 'Datei auswählen', selectFile: 'Datei auswählen',
selectFiles: 'Datei(en) 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('flatpickr', document.body, { setupFunction: setupDatepicker });
window.utils.setup('showHide', document.body); 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-lightwhite: #fcfffa;
--color-grey: #B1B5C0; --color-grey: #B1B5C0;
--color-grey-light: #efefef; --color-grey-light: #efefef;
--color-grey-lighter: #f5f5f5;
--color-grey-medium: #9A989E; --color-grey-medium: #9A989E;
--color-font: #34303a; --color-font: #34303a;
--color-fontsec: #5b5861; --color-fontsec: #5b5861;

View File

@ -1,8 +1,9 @@
$newline never $newline never
<section> <div .table-filter>
<form method=GET action=#{filterAction} enctype=#{filterEnctype}> <h3 .js-show-hide__toggle data-sh-index=table-filter data-collapsed=true>Filter
^{filterWgdt} <div>
<button> <form .table-filter-form method=GET action=#{filterAction} enctype=#{filterEnctype}>
^{btnLabel BtnSubmit} ^{filterWgdt}
<section> <button type=submit data-autosubmit>
^{scrolltable} ^{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() { document.addEventListener('DOMContentLoaded', function() {
var dbtIdent = #{String $ dbtIdent}; var dbtIdent = #{String dbtIdent};
var headerDBTableShortcircuit = #{String (toPathPiece HeaderDBTableShortcircuit)}; var headerDBTableShortcircuit = #{String (toPathPiece HeaderDBTableShortcircuit)};
var selector = '#' + dbtIdent + '-table-wrapper'; var selector = '#' + dbtIdent + '-table-wrapper';
var wrapper = document.querySelector(selector); var wrapper = document.querySelector(selector);
if (wrapper) { if (wrapper) {
window.utils.setup('asyncTable', wrapper, { headerDBTableShortcircuit, dbtIdent }); 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() { document.addEventListener('DOMContentLoaded', function() {
// TODO: replace for loop with one precise query for this specific modal instance var modalIdent = #{String modalId};
var modalElements = Array.from(document.querySelectorAll('.modal')); var selector = '#modal-' + modalIdent;
modalElements.forEach(function(modal) { var modal = document.querySelector(selector);
if (modal) {
window.utils.setup('modal', modal); window.utils.setup('modal', modal);
}); }
}); });