diff --git a/models/system-messages b/models/system-messages index 0547718ae..0ceec9223 100644 --- a/models/system-messages +++ b/models/system-messages @@ -2,7 +2,7 @@ SystemMessage from UTCTime Maybe to UTCTime Maybe authenticatedOnly Bool - severity MessageClass + severity MessageStatus defaultLanguage Lang content Html summary Html Maybe diff --git a/src/Foundation.hs b/src/Foundation.hs index 047e3f670..70ad9da14 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -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 diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs index 501cc97b9..946310640 100644 --- a/src/Handler/Admin.hs +++ b/src/Handler/Admin.hs @@ -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|
diff --git a/src/Handler/Utils/Form.hs b/src/Handler/Utils/Form.hs index ebabd7b0d..a3454fe32 100644 --- a/src/Handler/Utils/Form.hs +++ b/src/Handler/Utils/Form.hs @@ -654,5 +654,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 diff --git a/src/Model.hs b/src/Model.hs index 54acc1b28..4a0e3f1c9 100644 --- a/src/Model.hs +++ b/src/Model.hs @@ -19,7 +19,7 @@ import Data.Aeson (Value) import Data.CaseInsensitive (CI) import Data.CaseInsensitive.Instances () -import Utils.Message (MessageClass) +import Utils.Message (MessageStatus) import Settings.Cluster (ClusterSettingsKey) import Data.Binary (Binary) diff --git a/src/Settings.hs b/src/Settings.hs index 81f98eb45..f717ee378 100644 --- a/src/Settings.hs +++ b/src/Settings.hs @@ -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) diff --git a/src/Utils/Message.hs b/src/Utils/Message.hs index 7cf7f653f..69ce9e45e 100644 --- a/src/Utils/Message.hs +++ b/src/Utils/Message.hs @@ -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)) diff --git a/static/css/utils/asyncForm.scss b/static/css/utils/asyncForm.scss index 4241d8f31..a0f9956dd 100644 --- a/static/css/utils/asyncForm.scss +++ b/static/css/utils/asyncForm.scss @@ -1,5 +1,74 @@ .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 { diff --git a/static/css/utils/modal.scss b/static/css/utils/modal.scss index 5cac989a3..2f5d0e168 100644 --- a/static/css/utils/modal.scss +++ b/static/css/utils/modal.scss @@ -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%; +} diff --git a/static/js/utils/alerts.js b/static/js/utils/alerts.js index 03f36b7aa..b854495a0 100644 --- a/static/js/utils/alerts.js +++ b/static/js/utils/alerts.js @@ -89,6 +89,7 @@ alertElements.forEach(initAlert); return { + scope: alertsEl, destroy: function() {}, }; }; diff --git a/static/js/utils/asidenav.js b/static/js/utils/asidenav.js index cbe43fb4f..bb95f6455 100644 --- a/static/js/utils/asidenav.js +++ b/static/js/utils/asidenav.js @@ -57,6 +57,7 @@ initAsidenavSubmenus(); return { + scope: asideEl, destroy: function() {}, }; }; diff --git a/static/js/utils/asyncForm.js b/static/js/utils/asyncForm.js index 0ce2ed986..aa57ed2a0 100644 --- a/static/js/utils/asyncForm.js +++ b/static/js/utils/asyncForm.js @@ -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,17 +58,28 @@ 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() {}, }; }; diff --git a/static/js/utils/asyncTable.js b/static/js/utils/asyncTable.js index 0eeb7528d..ea6458633 100644 --- a/static/js/utils/asyncTable.js +++ b/static/js/utils/asyncTable.js @@ -225,6 +225,7 @@ init(); return { + scope: wrapper, destroy: destroyUtils, }; }; diff --git a/static/js/utils/asyncTableFilter.js b/static/js/utils/asyncTableFilter.js index 478eba9d1..98d9cda75 100644 --- a/static/js/utils/asyncTableFilter.js +++ b/static/js/utils/asyncTableFilter.js @@ -159,6 +159,7 @@ setup(); return { + scope: formElement, destroy: function() {}, }; } diff --git a/static/js/utils/checkAll.js b/static/js/utils/checkAll.js index 8cbeb28b6..b37a89454 100644 --- a/static/js/utils/checkAll.js +++ b/static/js/utils/checkAll.js @@ -124,6 +124,7 @@ init(); return { + scope: wrapper, destroy: destroy, }; }; diff --git a/static/js/utils/form.js b/static/js/utils/form.js index 0c919ee8f..8dc8642a2 100644 --- a/static/js/utils/form.js +++ b/static/js/utils/form.js @@ -54,6 +54,7 @@ } return { + scope: form, destroy: destroyUtils, }; }; @@ -97,6 +98,7 @@ } return { + scope: form, destroy: function() {}, }; }; @@ -138,6 +140,7 @@ } return { + scope: form, destroy: function() {}, }; }; @@ -149,6 +152,7 @@ } return { + scope: form, destroy: function() {}, }; }; diff --git a/static/js/utils/inputs.js b/static/js/utils/inputs.js index 4d8f79946..85229e678 100644 --- a/static/js/utils/inputs.js +++ b/static/js/utils/inputs.js @@ -44,6 +44,7 @@ } return { + scope: wrapper, destroy: destroyUtils, }; }; @@ -135,6 +136,7 @@ }); return { + scope: input, destroy: function() {}, }; } @@ -169,6 +171,7 @@ setup(); return { + scope: input, destroy: function() {}, }; } @@ -195,6 +198,7 @@ } return { + scope: input, destroy: function() {}, }; } @@ -218,6 +222,7 @@ } return { + scope: input, destroy: function() {}, }; } diff --git a/static/js/utils/modal.js b/static/js/utils/modal.js index 4273c5d4d..8bf15bd1a 100644 --- a/static/js/utils/modal.js +++ b/static/js/utils/modal.js @@ -146,6 +146,7 @@ } return { + scope: modalElement, destroy: destroyUtils, }; }; diff --git a/static/js/utils/setup.js b/static/js/utils/setup.js index 6ed7c4a35..e9afb216b 100644 --- a/static/js/utils/setup.js +++ b/static/js/utils/setup.js @@ -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,57 +14,98 @@ */ window.utils.setup = function(utilName, scope, options) { - - var utilInstance; - if (!utilName || !scope) { return; } options = options || {}; - var listener = function(event) { + var utilInstance; - 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); - } - }; - - window.utils.teardown(utilName); - if (registeredSetupListeners[utilName] && !options.singleton) { - registeredSetupListeners[utilName].push(listener); - } else { - 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]; + } } })(); diff --git a/static/js/utils/showHide.js b/static/js/utils/showHide.js index 9f7a4a4df..0441cde4b 100644 --- a/static/js/utils/showHide.js +++ b/static/js/utils/showHide.js @@ -70,6 +70,7 @@ }); return { + scope: wrapper, destroy: function() {}, }; }; diff --git a/static/js/utils/tabber.js b/static/js/utils/tabber.js index e0dd952fe..38fb43578 100644 --- a/static/js/utils/tabber.js +++ b/static/js/utils/tabber.js @@ -86,9 +86,5 @@ $(t).tabgroup(); }); } - - return { - destroy: function() {}, - }; }); })($); diff --git a/templates/default-layout.julius b/templates/default-layout.julius index 52f28f0d7..d83daccd0 100644 --- a/templates/default-layout.julius +++ b/templates/default-layout.julius @@ -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.
Falls das erneut passiert schicke uns gerne eine kurze Beschreibung dieses Ereignisses über das Hilfe-Widget rechts oben.

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); });