From d90da85df353da63794eeb95b79b87f485bb6908 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Tue, 12 May 2020 16:44:53 +0200 Subject: [PATCH] feat(communication): send test emails --- .eslintrc.json | 3 +- .../utils/form/communication-recipients.js | 88 +++++++++++++++++++ frontend/src/utils/form/form.js | 2 + frontend/src/utils/inputs/inputs.sass | 5 +- messages/uniworx/de-de-formal.msg | 5 ++ src/Handler/Course/Communication.hs | 17 +--- src/Handler/Tutorial/Communication.hs | 17 +--- src/Handler/Utils/Communication.hs | 74 +++++++++++++--- .../i18n/comm-test-tip/de-de-formal.hamlet | 17 ++++ .../communication/recipientLayout.julius | 87 ------------------ 10 files changed, 185 insertions(+), 130 deletions(-) create mode 100644 frontend/src/utils/form/communication-recipients.js create mode 100644 templates/i18n/comm-test-tip/de-de-formal.hamlet delete mode 100644 templates/widgets/communication/recipientLayout.julius diff --git a/.eslintrc.json b/.eslintrc.json index 6fcef7c27..6fcc6b0fb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,6 +23,7 @@ "no-extra-semi": "off", "semi": ["error", "always"], "comma-dangle": ["error", "always-multiline"], - "quotes": ["error", "single"] + "quotes": ["error", "single"], + "no-var": "error" } } diff --git a/frontend/src/utils/form/communication-recipients.js b/frontend/src/utils/form/communication-recipients.js new file mode 100644 index 000000000..2a9367f47 --- /dev/null +++ b/frontend/src/utils/form/communication-recipients.js @@ -0,0 +1,88 @@ +import { Utility } from '../../core/utility'; + +const MASS_INPUT_SELECTOR = '.massinput'; +const RECIPIENT_CATEGORIES_SELECTOR = '.recipient-categories'; +const RECIPIENT_CATEGORY_SELECTOR = '.recipient-category'; +const RECIPIENT_CATEGORY_CHECKBOX_SELECTOR = '.recipient-category__checkbox'; +const RECIPIENT_CATEGORY_OPTIONS_SELECTOR = '.recipient-category__options'; +const RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR = '.recipient-category__toggle-all [type="checkbox"]'; +const RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR = '.recipient-category__checked-counter'; + + +@Utility({ + selector: RECIPIENT_CATEGORIES_SELECTOR, +}) +export class CommunicationRecipients { + massInputElement; + + constructor(element) { + if (!element) { + throw new Error('Communication Recipient utility cannot be setup without an element!'); + } + + this.massInputElement = element.closest(MASS_INPUT_SELECTOR); + + this.setupRecipientCategories(); + + const recipientObserver = new MutationObserver(this.setupRecipientCategories.bind(this)); + recipientObserver.observe(this.massInputElement, { childList: true }); + } + + setupRecipientCategories() { + Array.from(this.massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR)).forEach(setupRecipientCategory); + } +} + +function setupRecipientCategory(recipientCategoryElement) { + const categoryCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKBOX_SELECTOR); + const categoryOptions = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_OPTIONS_SELECTOR); + + if (categoryOptions) { + const categoryCheckboxes = Array.from(categoryOptions.querySelectorAll('[type="checkbox"]')); + const toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR); + + // setup category checkbox to toggle all child checkboxes if changed + categoryCheckbox.addEventListener('change', () => { + categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => { + checkbox.checked = categoryCheckbox.checked; + }); + updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); + }); + + // update counter and toggle checkbox initially + updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); + + // register change listener for individual checkboxes + categoryCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', () => { + updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); + }); + }); + + // register change listener for toggle all checkbox + if (toggleAllCheckbox) { + toggleAllCheckbox.addEventListener('change', () => { + categoryCheckboxes.filter(checkbox => !checkbox.disabled).forEach(checkbox => { + checkbox.checked = toggleAllCheckbox.checked; + }); + updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); + }); + } + } +} + +// update checked state of toggle all checkbox based on all other checkboxes +function updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes) { + const allChecked = categoryCheckboxes.reduce((acc, checkbox) => acc && checkbox.checked, true); + toggleAllCheckbox.checked = allChecked; +} + +// update value of checked counter +function updateCheckedCounter(recipientCategoryElement, categoryCheckboxes) { + const checkedCounter = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR); + const checkedCheckboxes = categoryCheckboxes.reduce((acc, checkbox) => checkbox.checked ? acc + 1 : acc, 0); + checkedCounter.innerHTML = checkedCheckboxes + '/' + categoryCheckboxes.length; +} diff --git a/frontend/src/utils/form/form.js b/frontend/src/utils/form/form.js index 6d30a501b..c541de367 100644 --- a/frontend/src/utils/form/form.js +++ b/frontend/src/utils/form/form.js @@ -5,6 +5,7 @@ import { Datepicker } from './datepicker'; import { FormErrorRemover } from './form-error-remover'; import { InteractiveFieldset } from './interactive-fieldset'; import { NavigateAwayPrompt } from './navigate-away-prompt'; +import { CommunicationRecipients } from './communication-recipients'; export const FormUtils = [ AutoSubmitButton, @@ -13,5 +14,6 @@ export const FormUtils = [ FormErrorRemover, InteractiveFieldset, NavigateAwayPrompt, + CommunicationRecipients, // ReactiveSubmitButton // not used currently ]; diff --git a/frontend/src/utils/inputs/inputs.sass b/frontend/src/utils/inputs/inputs.sass index 7288f47e7..f1eee6cad 100644 --- a/frontend/src/utils/inputs/inputs.sass +++ b/frontend/src/utils/inputs/inputs.sass @@ -1,3 +1,5 @@ +@use "../../app" as * + // GENERAL STYLES FOR FORMS // FORM GROUPS @@ -24,7 +26,7 @@ margin-top: 40px .form-section-legend - color: var(--color-fontsec) + @extend .explanation margin: 7px 0 .form-group__hint, .form-section-title__hint @@ -51,6 +53,7 @@ content: ' *' color: var(--color-error) font-weight: 600 + font-style: normal .form-group--submit .form-group__input grid-column: 2 diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index 72f677075..3af96a3fd 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -37,6 +37,9 @@ BtnAllocationAccept: Vergabe akzeptieren BtnSystemMessageHide: Verstecken BtnSystemMessageUnhide: Nicht mehr verstecken +BtnCommunicationSend: Senden +BtnCommunicationTest: Test-Nachricht verschicken + Aborted: Abgebrochen Remarks: Hinweise @@ -952,6 +955,7 @@ MailEditNotifications: Benachrichtigungen ein-/ausschalten MailSubjectSupport: Supportanfrage MailSubjectSupportCustom customSubject@Text: [Support] #{customSubject} +CommCourseTestSubject customSubject@Text: [TEST] #{customSubject} CommCourseSubject: Kursmitteilung MailSubjectLecturerInvitation tid@TermId ssh@SchoolId csh@CourseShorthand: [#{tid}-#{ssh}-#{csh}] Einladung zum Kursverwalter InvitationAcceptDecline: Einladung annehmen/ablehnen @@ -1415,6 +1419,7 @@ CommRecipientsTip: Sie selbst erhalten immer eine Kopie der Nachricht CommRecipientsList: Die an Sie selbst verschickte Kopie der Nachricht wird, zu Archivierungszwecken, eine vollständige Liste aller Empfänger enthalten. Die Empfängerliste wird im CSV-Format an die E-Mail angehängt. Andere Empfänger erhalten die Liste nicht. Bitte entfernen Sie dementsprechend den Anhang bevor Sie die E-Mail weiterleiten oder anderweitig mit Dritten teilen. CommDuplicateRecipients n@Int: #{n} #{pluralDE n "doppelter" "doppelte"} Empfänger ignoriert CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt +CommTestSuccess: Nachricht wurde zu Testzwecken nur an Sie selbst versandt CommUndisclosedRecipients: Verborgene Empfänger CommAllRecipients: alle-empfaenger diff --git a/src/Handler/Course/Communication.hs b/src/Handler/Course/Communication.hs index 1b86fb4fd..a984f021e 100644 --- a/src/Handler/Course/Communication.hs +++ b/src/Handler/Course/Communication.hs @@ -7,9 +7,6 @@ import Import import Handler.Utils import Handler.Utils.Communication -import qualified Data.CaseInsensitive as CI - -import qualified Data.Set as Set import qualified Data.Map as Map import qualified Database.Esqueleto as E @@ -18,23 +15,13 @@ import qualified Database.Esqueleto as E getCCommR, postCCommR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCCommR = postCCommR postCCommR tid ssh csh = do - jSender <- requireAuthId cid <- runDB . getKeyBy404 $ TermSchoolCourseShort tid ssh csh commR CommunicationRoute { crHeading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommCourseHeading , crUltDest = SomeRoute $ CourseR tid ssh csh CCommR - , crJobs = \Communication{..} -> do - let jSubject = cSubject - jMailContent = cBody - jCourse = cid - allRecipients = Set.toList $ Set.insert (Right jSender) cRecipients - jMailObjectUUID <- liftIO getRandom - jAllRecipientAddresses <- lift . fmap Set.fromList . forM allRecipients $ \case - Left email -> return . Address Nothing $ CI.original email - Right rid -> userAddress <$> getJust rid - forM_ allRecipients $ \jRecipientEmail -> - yield JobSendCourseCommunication{..} + , crJobs = crJobsCourseCommunication cid + , crTestJobs = crTestJobsCourseCommunication cid , crRecipients = Map.fromList [ ( RGCourseParticipants , E.from $ \(user `E.InnerJoin` participant) -> do diff --git a/src/Handler/Tutorial/Communication.hs b/src/Handler/Tutorial/Communication.hs index 6257caeb1..126eef925 100644 --- a/src/Handler/Tutorial/Communication.hs +++ b/src/Handler/Tutorial/Communication.hs @@ -11,31 +11,18 @@ import qualified Database.Esqueleto as E import qualified Database.Esqueleto.Utils as E import qualified Data.Map as Map -import qualified Data.Set as Set - -import qualified Data.CaseInsensitive as CI getTCommR, postTCommR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html getTCommR = postTCommR postTCommR tid ssh csh tutn = do - jSender <- requireAuthId (cid, tutid) <- runDB $ fetchCourseIdTutorialId tid ssh csh tutn commR CommunicationRoute { crHeading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommTutorialHeading , crUltDest = SomeRoute $ CTutorialR tid ssh csh tutn TCommR - , crJobs = \Communication{..} -> do - let jSubject = cSubject - jMailContent = cBody - jCourse = cid - allRecipients = Set.toList $ Set.insert (Right jSender) cRecipients - jMailObjectUUID <- liftIO getRandom - jAllRecipientAddresses <- lift . fmap Set.fromList . forM allRecipients $ \case - Left email -> return . Address Nothing $ CI.original email - Right rid -> userAddress <$> getJust rid - forM_ allRecipients $ \jRecipientEmail -> - yield JobSendCourseCommunication{..} + , crJobs = crJobsCourseCommunication cid + , crTestJobs = crTestJobsCourseCommunication cid , crRecipients = Map.fromList [ ( RGTutorialParticipants , E.from $ \(user `E.InnerJoin` participant) -> do diff --git a/src/Handler/Utils/Communication.hs b/src/Handler/Utils/Communication.hs index 88560a5c3..da6ae9c72 100644 --- a/src/Handler/Utils/Communication.hs +++ b/src/Handler/Utils/Communication.hs @@ -3,6 +3,7 @@ module Handler.Utils.Communication , CommunicationRoute(..) , Communication(..) , commR + , crJobsCourseCommunication, crTestJobsCourseCommunication -- * Re-Exports , Job(..) ) where @@ -18,6 +19,8 @@ import Data.Map ((!), (!?)) import qualified Data.Map as Map import qualified Data.Set as Set +import qualified Data.Conduit.Combinators as C + data RecipientGroup = RGCourseParticipants | RGCourseLecturers | RGCourseCorrectors | RGCourseTutors | RGTutorialParticipants @@ -67,11 +70,25 @@ instance RenderMessage UniWorX RecipientCategory where renderMessage' :: forall msg. RenderMessage UniWorX msg => msg -> Text renderMessage' = renderMessage foundation ls +data CommunicationButton + = BtnCommunicationSend + | BtnCommunicationTest + deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) + deriving anyclass (Universe, Finite) + +nullaryPathPiece ''CommunicationButton $ camelToPathPiece' 2 +embedRenderMessage ''UniWorX ''CommunicationButton id +makePrisms ''CommunicationButton + +instance Button UniWorX CommunicationButton where + btnClasses BtnCommunicationSend = [BCIsButton, BCPrimary] + btnClasses BtnCommunicationTest = [BCIsButton] + data CommunicationRoute = CommunicationRoute { crRecipients :: Map RecipientGroup (E.SqlQuery (E.SqlExpr (Entity User))) , crRecipientAuth :: Maybe (UserId -> DB AuthResult) -- ^ Only resolve userids given as GET-Parameter if they fulfil this criterion - , crJobs :: Communication -> ConduitT () Job (YesodDB UniWorX) () + , crJobs, crTestJobs :: Communication -> ConduitT () Job (YesodDB UniWorX) () , crHeading :: SomeMessage UniWorX , crUltDest :: SomeRoute UniWorX } @@ -83,6 +100,26 @@ data Communication = Communication } +crJobsCourseCommunication, crTestJobsCourseCommunication :: CourseId -> Communication -> ConduitT () Job (YesodDB UniWorX) () +crJobsCourseCommunication jCourse Communication{..} = do + jSender <- requireAuthId + let jSubject = cSubject + jMailContent = cBody + allRecipients = Set.toList $ Set.insert (Right jSender) cRecipients + jMailObjectUUID <- liftIO getRandom + jAllRecipientAddresses <- lift . fmap Set.fromList . forM allRecipients $ \case + Left email -> return . Address Nothing $ CI.original email + Right rid -> userAddress <$> getJust rid + forM_ allRecipients $ \jRecipientEmail -> + yield JobSendCourseCommunication{..} +crTestJobsCourseCommunication jCourse comm = do + jSender <- requireAuthId + + MsgRenderer mr <- getMsgRenderer + let comm' = comm { cSubject = Just . mr . MsgCommCourseTestSubject . fromMaybe (mr MsgCommCourseSubject) $ cSubject comm } + crJobsCourseCommunication jCourse comm' .| C.filter ((== Right jSender) . jRecipientEmail) + + commR :: CommunicationRoute -> Handler Html commR CommunicationRoute{..} = do cUser <- maybeAuth @@ -129,14 +166,18 @@ commR CommunicationRoute{..} = do miAdd (EnumPosition RecipientCustom, 0) 1 nudge submitView = Just $ \csrf -> do (addRes, addView) <- mpreq (multiUserField True Nothing) (fslpI MsgEMail (mr MsgEMail) & setTooltip MsgMultiEmailFieldTip & addName (nudge "email")) Nothing let - addRes' = addRes <&> \(Set.toList -> nEmails) (maybe 0 (succ . snd . fst) . Map.lookupMax . Map.filterWithKey (\(EnumPosition c, _) _ -> c == RecipientCustom) -> kStart) -> FormSuccess . Map.fromList $ zip (map (EnumPosition RecipientCustom, ) [kStart..]) nEmails + addRes' = addRes <&> \nEmails ((Map.elems &&& maybe 0 (succ . snd . fst) . Map.lookupMax) . Map.filterWithKey (\(EnumPosition c, _) _ -> c == RecipientCustom) -> (oEmails, kStart)) -> FormSuccess . Map.fromList . zip (map (EnumPosition RecipientCustom, ) [kStart..]) . Set.toList $ nEmails `Set.difference` Set.fromList oEmails return (addRes', $(widgetFile "widgets/communication/recipientAdd")) miAdd _ _ _ _ = Nothing miCell _ (Left (CI.original -> email)) initRes nudge csrf = do (tickRes, tickView) <- mpreq checkBoxField ("" & addName (nudge "tick")) $ initRes <|> Just True return (tickRes, $(widgetFile "widgets/communication/recipientEmail")) - miCell _ (Right (lookupUser -> User{..})) initRes nudge csrf = do - (tickRes, tickView) <- mpreq checkBoxField ("" & addName (nudge "tick")) $ initRes <|> Just True + miCell _ (Right uid@(lookupUser -> User{..})) initRes nudge csrf = do + (tickRes, tickView) <- if + | fmap entityKey cUser == Just uid + -> mforced checkBoxField ("" & addName (nudge "tick")) True + | otherwise + -> mpreq checkBoxField ("" & addName (nudge "tick")) $ initRes <|> Just True return (tickRes, $(widgetFile "widgets/communication/recipientName")) miAllowAdd (EnumPosition RecipientCustom, 0) 1 _ = True miAllowAdd _ _ _ = False @@ -167,22 +208,33 @@ commR CommunicationRoute{..} = do recipientsListMsg <- messageI Info MsgCommRecipientsList - ((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . renderAForm FormStandard $ Communication + ((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . withButtonForm' universeF . renderAForm FormStandard $ Communication <$> recipientAForm <* aformMessage recipientsListMsg <*> aopt textField (fslI MsgCommSubject) Nothing <*> areq htmlField (fslI MsgCommBody) Nothing - formResult commRes $ \comm -> do - runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs - addMessageI Success . MsgCommSuccess . Set.size $ cRecipients comm - redirect crUltDest - + formResult commRes $ \case + (comm, BtnCommunicationSend) -> do + runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs + addMessageI Success . MsgCommSuccess . Set.size $ cRecipients comm + redirect crUltDest + (comm, BtnCommunicationTest) -> do + runDBJobs . runConduit $ transPipe (mapReaderT lift) (crTestJobs comm) .| sinkDBJobs + addMessageI Info MsgCommTestSuccess let formWdgt = wrapForm commWdgt def { formMethod = POST , formAction = SomeRoute <$> mbCurrentRoute , formEncoding = commEncoding + , formSubmit = FormNoSubmit } siteLayoutMsg crHeading $ do setTitleI crHeading - formWdgt + let commTestTip = $(i18nWidgetFile "comm-test-tip") + [whamlet| + $newline never +
+ ^{formWdgt} +
+ ^{commTestTip} + |] diff --git a/templates/i18n/comm-test-tip/de-de-formal.hamlet b/templates/i18n/comm-test-tip/de-de-formal.hamlet new file mode 100644 index 000000000..838f0c707 --- /dev/null +++ b/templates/i18n/comm-test-tip/de-de-formal.hamlet @@ -0,0 +1,17 @@ +$newline never + +

+ + Über den Knopf „_{MsgBtnCommunicationTest}“ haben Sie die # + Möglichkeit zunächst eine Kopie der Nachricht nur an sich selbst zu # + schicken. + +
+ + Diese wird den Präfix „[TEST]“ im Betreff enthalten und ansonsten in # + allen Aspekten identisch sein zur eigentlichen Nachricht. + +

+ + Um die Nachricht tatsächlich an alle Empfänger zu verschicken, # + verwenden Sie bitte „_{MsgBtnSubmit}“. diff --git a/templates/widgets/communication/recipientLayout.julius b/templates/widgets/communication/recipientLayout.julius deleted file mode 100644 index d2fc512ab..000000000 --- a/templates/widgets/communication/recipientLayout.julius +++ /dev/null @@ -1,87 +0,0 @@ -(function() { - - var MASS_INPUT_SELECTOR = '.massinput'; - var RECIPIENT_CATEGORIES_SELECTOR = '.recipient-categories'; - var RECIPIENT_CATEGORY_SELECTOR = '.recipient-category'; - var RECIPIENT_CATEGORY_CHECKBOX_SELECTOR = '.recipient-category__checkbox '; - var RECIPIENT_CATEGORY_OPTIONS_SELECTOR = '.recipient-category__options'; - var RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR = '.recipient-category__toggle-all [type="checkbox"]'; - var RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR = '.recipient-category__checked-counter'; - - var massInputElement; - - document.addEventListener('DOMContentLoaded', function() { - var recipientCategoriesElement = document.querySelector(RECIPIENT_CATEGORIES_SELECTOR); - massInputElement = recipientCategoriesElement.closest(MASS_INPUT_SELECTOR); - - setupRecipientCategories(); - - var recipientObserver = new MutationObserver(setupRecipientCategories); - recipientObserver.observe(massInputElement, { childList: true }); - }); - - function setupRecipientCategories() { - var recipientCategoryElements = Array.from(massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR)); - - recipientCategoryElements.forEach(function(element) { - setupRecipientCategory(element); - }); - } - - function setupRecipientCategory(recipientCategoryElement) { - var categoryCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKBOX_SELECTOR); - var categoryOptions = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_OPTIONS_SELECTOR); - if (categoryOptions) { - - var categoryCheckboxes = Array.from(categoryOptions.querySelectorAll('[type="checkbox"]')); - var toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR); - - // setup category checkbox to toggle all child checkboxes if changed - categoryCheckbox.addEventListener('change', function() { - categoryCheckboxes.forEach(function(checkbox) { - checkbox.checked = categoryCheckbox.checked; - }); - updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); - }); - - // update counter and toggle checkbox initially - updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); - - // register change listener for individual checkboxes - categoryCheckboxes.forEach(function(checkbox) { - checkbox.addEventListener('change', function() { - updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes); - }); - }); - - // register change listener for toggle all checkbox - if (toggleAllCheckbox) { - toggleAllCheckbox.addEventListener('change', function() { - categoryCheckboxes.forEach(function(checkbox) { - checkbox.checked = toggleAllCheckbox.checked; - }); - updateCheckedCounter(recipientCategoryElement, categoryCheckboxes); - }); - } - } - } - - // update checked state of toggle all checkbox based on all other checkboxes - function updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes) { - var allChecked = categoryCheckboxes.reduce(function(acc, checkbox) { - return acc && checkbox.checked; - }, true); - toggleAllCheckbox.checked = allChecked; - } - - // update value of checked counter - function updateCheckedCounter(recipientCategoryElement, categoryCheckboxes) { - var checkedCounter = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR); - var checkedCheckboxes = categoryCheckboxes.reduce(function(acc, checkbox) { return checkbox.checked ? acc + 1 : acc; }, 0); - checkedCounter.innerHTML = checkedCheckboxes + '/' + categoryCheckboxes.length; - } - -})();