Merge and bugfix sorting by participants registration date
This commit is contained in:
commit
889c3ebb35
@ -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
|
||||
|
||||
@ -2,7 +2,7 @@ SystemMessage
|
||||
from UTCTime Maybe
|
||||
to UTCTime Maybe
|
||||
authenticatedOnly Bool
|
||||
severity MessageClass
|
||||
severity MessageStatus
|
||||
defaultLanguage Lang
|
||||
content Html
|
||||
summary Html Maybe
|
||||
|
||||
43
shell.nix
43
shell.nix
@ -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}
|
||||
'';
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -145,7 +145,7 @@ getTermShowR = do
|
||||
, dbtIdent = "terms" :: Text
|
||||
}
|
||||
defaultLayout $ do
|
||||
setTitle "Freigeschaltete Semester"
|
||||
setTitleI MsgTermsHeading
|
||||
$(widgetFile "terms")
|
||||
|
||||
getTermEditR :: Handler Html
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
12
stack.nix
12
stack.nix
@ -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";
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
5
static/css/utils/asyncTable.scss
Normal file
5
static/css/utils/asyncTable.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.async-table--loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
transition: opacity 400ms ease-out;
|
||||
}
|
||||
5
static/css/utils/asyncTableFilter.scss
Normal file
5
static/css/utils/asyncTableFilter.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.async-table-filter--loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
transition: opacity 400ms ease-out;
|
||||
}
|
||||
74
static/css/utils/checkbox.scss
Normal file
74
static/css/utils/checkbox.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
|
||||
76
static/css/utils/radio.scss
Normal file
76
static/css/utils/radio.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -87,5 +87,10 @@
|
||||
|
||||
initToggler();
|
||||
alertElements.forEach(initAlert);
|
||||
|
||||
return {
|
||||
scope: alertsEl,
|
||||
destroy: function() {},
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
@ -55,5 +55,10 @@
|
||||
|
||||
initFavoritesButton();
|
||||
initAsidenavSubmenus();
|
||||
|
||||
return {
|
||||
scope: asideEl,
|
||||
destroy: function() {},
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
@ -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() {},
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
166
static/js/utils/asyncTableFilter.js
Normal file
166
static/js/utils/asyncTableFilter.js
Normal 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() {},
|
||||
};
|
||||
}
|
||||
})();
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
@ -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() {},
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
@ -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() {},
|
||||
};
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
@ -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() {},
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
|
||||
4
templates/table/layout-filter-default.lucius
Normal file
4
templates/table/layout-filter-default.lucius
Normal file
@ -0,0 +1,4 @@
|
||||
.table-filter {
|
||||
border-bottom: 1px solid #d3d3d3;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
14
templates/widgets/fields/bool.hamlet
Normal file
14
templates/widgets/fields/bool.hamlet
Normal 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}
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user