diff --git a/_posts/2019-09-05-project-atlantic.markdown b/_posts/2019-09-05-project-atlantic.markdown index e529764..6995838 100644 --- a/_posts/2019-09-05-project-atlantic.markdown +++ b/_posts/2019-09-05-project-atlantic.markdown @@ -60,7 +60,9 @@ date: '2018-08-29 10:26' 8. UI/UX Improvments -- [ ] Merge File Select and Tagging Section +- [x] Merge File Select and Tagging Section +- [ ] Instant Search for Batch Tagging +- [ ] Move 'Manage Tags' and 'Tagged Records' Sections into Tagging Section accessible via NavBar Icon 9. Integrate `elm-program-test` diff --git a/src/elm/Input.elm b/src/elm/Input.elm index 20ffd15..b9dff58 100755 --- a/src/elm/Input.elm +++ b/src/elm/Input.elm @@ -1,23 +1,24 @@ module Input exposing (viewAutocomplete, viewDefault, viewRadio, viewRadioGroup, viewWithButton) import Button -import Html exposing (Attribute, Html, datalist, div, input, label, option, text) -import Html.Attributes exposing (class, id, list, name, style, type_, value) +import Html exposing (Attribute, Html, datalist, div, input, label, option, span, text) +import Html.Attributes exposing (attribute, class, id, list, name, style, type_, value) import Html.Events exposing (onClick) import Set exposing (Set) view : List (Html msg) -> Html msg view childs = - div [ class "uk-form-controls" ] + div [ class "uk-inline uk-width-expand" ] childs -viewDefault : String -> List (Attribute msg) -> Html msg -viewDefault val inputAttr = +viewDefault : String -> String -> List (Attribute msg) -> Html msg +viewDefault icon val inputAttr = view - [ input - ([ class "uk-input", type_ "text", value val ] + [ span [ class "uk-form-icon", attribute "uk-icon" "icon: search" ] [] + , input + ([ class "uk-input ", type_ "text", value val ] ++ inputAttr ) [] @@ -66,17 +67,19 @@ viewRadioGroup groupName msg radioLabels = List.map (viewRadio msg groupName) radioLabels -viewAutocomplete : String -> String -> String -> List (Html.Attribute msg) -> Set String -> Html msg -viewAutocomplete labelText val idVal inputAttr options = - div [ class "uk-form-horizontal" ] - [ div [ class "uk-margin" ] - [ label [ class "uk-form-label" ] [ text labelText ] - , viewDefault val +viewAutocomplete : String -> String -> String -> String -> List (Html.Attribute msg) -> Set String -> Html msg +viewAutocomplete labelText icon val idVal inputAttr options = + div [ class "uk-form-horizontal uk-margin uk-grid" ] + [ div [ class "uk-width-2-6 uk-width-1-6@m" ] [ label [ class "uk-form-label" ] [ text labelText ] ] + , div [ class "uk-width-4-6 uk-width-5-6@m" ] + [ viewDefault + icon + val (inputAttr ++ [ list idVal ] ) - , viewDataList idVal options ] + , viewDataList idVal options ] diff --git a/src/elm/Main.elm b/src/elm/Main.elm index e34b138..da32e56 100755 --- a/src/elm/Main.elm +++ b/src/elm/Main.elm @@ -127,9 +127,10 @@ type alias Model = , tableData : List TableData , tableDataTagged : List (List TableDataTagged) , batchTaggingOptions : Dict ColumnHeadingName SearchPattern - , optionTagging : TaggingOption + , taggingMode : TaggingOption , showModal : Maybe (Modal.State ModalContent) , settingStackImportedData : ImportStacking + , selectedWorkingData : TableData } @@ -182,9 +183,10 @@ init flags = , tableData = tableData , tableDataTagged = tableDataTagged , batchTaggingOptions = batchTaggingOptions - , optionTagging = BatchTagging + , taggingMode = BatchTagging , showModal = showModal , settingStackImportedData = settingStackImportedData + , selectedWorkingData = TableData [] [] } , Cmd.none ) @@ -384,20 +386,20 @@ type Msg | CreateTagFromBuffer | MapRecordToTag (Bucket Row) Tag | SearchPatternInput ColumnHeadingName SearchPattern - | SetTaggingOption TaggingOption + | SetTaggingMode TaggingOption | NoOp | CloseModal | UndoMapRecordToTag UndoStrategy | TableDownload TableDataTagged - | ShowMatchingRecords (List ColumnHeadingName) Tag (List Row) | SortTaggedTable Tag ColumnHeadingName - | UserClickedFileSelectButton + | UserClickedInitialFileSelectButton | UserSelectedFileFromSysDialog File | RuntimeCompletedFileLoadingTask String | UserClickedStackingCheckboxInImportDialog (List ColumnHeadingName) (List (List String)) ImportStacking - | UserClickedImportFileDataButtonInImportDialog ImportStacking (List ColumnHeadingName) (List (List String)) + | UserClickedConfirmDataImportButton ImportStacking (List ColumnHeadingName) (List (List String)) | UserClickedDropButtonInDropDialog (List ColumnHeadingName) (List (List String)) (List (List String)) | UserClickedViewWorkingDataNavButtonInTaggingSection + | UserClickedImportFileButton {-| We want to `setStorage` on every update. This function adds the setStorage @@ -415,6 +417,9 @@ updateWithStorage msg model = update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of + UserClickedImportFileButton -> + ( model, File.Select.file [ "text/csv" ] UserSelectedFileFromSysDialog ) + UserClickedViewWorkingDataNavButtonInTaggingSection -> let { headers, rows } = @@ -448,7 +453,7 @@ update msg model = Replace -> ( updateShowImportConfirmation headers records Stack model, Cmd.none ) - UserClickedImportFileDataButtonInImportDialog stacking headers records -> + UserClickedConfirmDataImportButton stacking headers records -> ( updateCloseModal <| createTableDataFromCsv stacking (Csv.Csv headers records) model, Cmd.none ) RuntimeCompletedFileLoadingTask content -> @@ -510,11 +515,11 @@ update msg model = EmptyWorkspace -> checkForIrregularityOrProceed in - ( newModel |> updateResetBatchTaggingOptions + ( newModel , Cmd.none ) - UserClickedFileSelectButton -> + UserClickedInitialFileSelectButton -> ( model, File.Select.file [ "text/csv" ] UserSelectedFileFromSysDialog ) UserSelectedFileFromSysDialog file -> @@ -652,14 +657,30 @@ update msg model = ( updateCloseModal updatedModel, Cmd.none ) SearchPatternInput columnKey searchPatternInput -> - if String.isEmpty searchPatternInput then - ( { model | batchTaggingOptions = Dict.remove columnKey model.batchTaggingOptions }, Cmd.none ) + let + updatedBatchTaggingOptions = + if String.isEmpty searchPatternInput then + Dict.remove columnKey model.batchTaggingOptions - else - ( { model | batchTaggingOptions = Dict.insert columnKey searchPatternInput model.batchTaggingOptions }, Cmd.none ) + else + Dict.insert columnKey searchPatternInput model.batchTaggingOptions + + headers = + Maybe.withDefault (Table.TableData [] []) (getWorkingData model) |> .headers + + rows = + Maybe.withDefault (Table.TableData [] []) (getWorkingData model) |> .rows + + matchedRows = + matchRows updatedBatchTaggingOptions headers rows + + modelWithUpdatedSelectedWorkingData = + TableData headers matchedRows + in + ( { model | batchTaggingOptions = updatedBatchTaggingOptions, selectedWorkingData = modelWithUpdatedSelectedWorkingData }, Cmd.none ) - SetTaggingOption opt -> - ( { model | optionTagging = opt }, Cmd.none ) + SetTaggingMode opt -> + ( { model | taggingMode = opt }, Cmd.none ) CloseModal -> ( updateCloseModal model, Cmd.none ) @@ -698,68 +719,6 @@ update msg model = in ( model, Download.string (tag ++ "-" ++ Locale.translateTableFileName model.locale ++ ".csv") "text/csv" theCsvString ) - ShowMatchingRecords headers tag rows -> - let - columnSearchPatternCount = - Dict.size model.batchTaggingOptions - - matchedRows = - rows - |> List.map - (mapRowCellsToHaveColumns headers) - |> List.filter - (\row_ -> - let - matchingCellCount = - List.foldr - (\( column, cell ) count -> - case Dict.get column model.batchTaggingOptions of - Just searchPattern -> - let - regexPattern = - Regex.fromStringWith { caseInsensitive = True, multiline = False } searchPattern - |> Maybe.withDefault Regex.never - in - if Regex.contains regexPattern cell then - count + 1 - - else - count - - Nothing -> - count - ) - 0 - row_.cells - in - columnSearchPatternCount > 0 && columnSearchPatternCount == matchingCellCount - ) - - matchedRowsAsRowType = - matchedRows - |> List.map mapRowCellsToRemoveColumns - - plainMatchedRecords = - Table.rowPlain matchedRowsAsRowType - - modalTitleText = - Locale.translateRecordsThatWillBeTagged model.locale (List.length plainMatchedRecords) - - modalDisplay = - if List.isEmpty plainMatchedRecords then - Modal.RegularView - - else - Modal.Fullscreen - in - ( updateShowModal - modalDisplay - modalTitleText - (ViewMapRecordsToTag headers plainMatchedRecords tag) - model - , Cmd.none - ) - SortTaggedTable theTagToLookup column -> let currentTableDataTaggedList = @@ -820,11 +779,6 @@ updateCloseModal model = { model | showModal = Nothing } -updateResetBatchTaggingOptions : Model -> Model -updateResetBatchTaggingOptions model = - { model | batchTaggingOptions = Dict.empty } - - -- SUBSCRIPTIONS @@ -848,6 +802,11 @@ viewRecords responsive headers records = (List.map (List.map text) records) +viewRows : Table.Responsive -> List ColumnHeadingName -> List Row -> Html.Html Msg +viewRows responsive headers rows = + viewRecords responsive headers <| unwrapRows rows + + viewModalContent : Locale -> ModalContent -> Html.Html Msg viewModalContent locale modalContent = case modalContent of @@ -934,7 +893,7 @@ getModalButtons locale modalContent = ] else - [ Modal.IconButton Button.Primary (UserClickedImportFileDataButtonInImportDialog stacking headers records) (Locale.translateImport locale) Button.Import + [ Modal.IconButton Button.Primary (UserClickedConfirmDataImportButton stacking headers records) (Locale.translateImport locale) Button.Import , Modal.DefaultButton Button.Secondary CloseModal (Locale.translateCancel locale) ] @@ -1021,7 +980,15 @@ view model = , a [ href "https://github.com/eimfach/elm-csv-batch-tagger-app", target "_blank" ] [ text <| " " ++ Locale.translateViewSourceCode model.locale ] ] , hr [] [] - , div [ class "uk-margin-top" ] + , div [] + [ viewTaggingSection + model + tableData.headers + currentRow + tableData.rows + taggingSectionNav + ] + , div [ class "uk-margin" ] [ button [ class "uk-button uk-button-small uk-align-right", onClick ToggleLocale ] [ text <| Locale.translateLocale model.locale ++ ": " ++ localeTranslation ] , button [ class "uk-button uk-button-small uk-align-right uk-button-danger", onClick UserClickedRequestDeleteDataButton ] [ text (Locale.translateDeleteYourLocalData model.locale) ] @@ -1030,19 +997,6 @@ view model = [ text "NOTE" ] , span [ class "uk-text-small uk-text-light" ] [ text <| " " ++ Locale.translateInfoOnHowDataIsStored model.locale ] ] - , div [] - [ viewFileUploadSection (Locale.translateSelectAcsvFile model.locale) ] - , div [] - [ viewTaggingSection - model.locale - model.optionTagging - model.batchTaggingOptions - model.tags - tableData.headers - currentRow - tableData.rows - taggingSectionNav - ] , div [] [ viewManageTagsSection (Locale.translateManageYourTags model.locale) (Locale.translateEnterATag model.locale) model.addTagInputError model.addTagInputBuffer model.tags TagInput CreateTagFromBuffer RemoveTag ] @@ -1102,37 +1056,28 @@ viewTagButton msg tag = viewTagCloud : (Table.Tag -> msg) -> Set Table.Tag -> Html msg viewTagCloud action tags = p - [ class "tag-cloud" ] - (viewTagButtons - action - tags - ) + [ class "uk-flex uk-flex-right" ] + [ button [ class "uk-button-default uk-button" ] [ span [ attribute "uk-icon" "icon: database" ] [] ] ] -- VIEW SECTIONS -viewFileUploadSection : String -> Html Msg -viewFileUploadSection headerText = - div [ class "uk-section uk-section-small" ] - [ h3 - [ class "uk-heading-line uk-text-center" ] - [ span [ class "uk-text-background uk-text-large" ] [ text headerText ] +viewInitialFileSelect : Html Msg +viewInitialFileSelect = + div + [ class "uk-padding uk-center uk-flex uk-flex-center" ] + [ button + [ onClick UserClickedInitialFileSelectButton + , class "uk-button uk-button-primary uk-button-large uk-width-1-3" ] - , div - [ class "uk-padding" ] - [ button - [ onClick UserClickedFileSelectButton - , class "file-upload-button uk-button uk-button-default uk-width-1-1 uk-margin" - ] - [ label - [ attribute "uk-icon" "icon: upload" - , for "csv-upload" - , class "file-label" - ] - [ text "" ] + [ label + [ attribute "uk-icon" "icon: plus" + , for "csv-upload" + , class "file-label" ] + [ text "" ] ] ] @@ -1158,28 +1103,28 @@ viewManageTagsSection headerText inputPlaceholder _ buffer tags tagInputMsg crea ] -viewTaggingSection : Locale -> TaggingOption -> Dict ColumnHeadingName SearchPattern -> Set Tag -> List ColumnHeadingName -> Row -> List Row -> HtmlNode -> HtmlNode -viewTaggingSection locale taggingOption batchTaggingOptions tags headers row rows nav = +viewTaggingSection : Model -> List ColumnHeadingName -> Row -> List Row -> HtmlNode -> HtmlNode +viewTaggingSection model headers row rows nav = let translateHeaderText = - Locale.translateApplyTags locale + Locale.translateApplyTags model.locale singleTaggingText = - Locale.translateSingleTagging locale + Locale.translateSingleTagging model.locale batchTaggingText = - Locale.translateBatchTagging locale + Locale.translateBatchTagging model.locale tagActionText = - Locale.translateSelectATagToTag locale + Locale.translateSelectATagToTag model.locale taggingAction tag = - case taggingOption of + case model.taggingMode of SingleTagging -> MapRecordToTag (Single row) tag BatchTagging -> - ShowMatchingRecords headers tag rows + NoOp ( viewTagActionDescription, viewTagCloud_ ) = if List.isEmpty rows then @@ -1187,68 +1132,56 @@ viewTaggingSection locale taggingOption batchTaggingOptions tags headers row row else ( div [ class "uk-margin" ] [ h5 [ class "uk-text-primary" ] [ text tagActionText ] ] - , viewTagCloud (\tag -> taggingAction tag) tags + , viewTagCloud (\tag -> taggingAction tag) model.tags ) ( singleIsActiveTab, viewTab ) = - case taggingOption of + case model.taggingMode of {--tab selection is naive--} SingleTagging -> - ( True, viewManualTaggingTab locale headers row.cells ) + ( True, viewManualTaggingTab model.locale headers row.cells ) BatchTagging -> - ( False, viewBatchTaggingTab locale batchTaggingOptions SearchPatternInput headers rows ) + ( False, viewBatchTaggingTab model SearchPatternInput headers rows ) in - div [] - [ div [ class "uk-position-relative" ] - [ h3 - [ class "uk-heading-line uk-text-center" ] - [ span [ class "uk-text-background uk-text-large" ] - [ text (translateHeaderText (List.length rows)) - ] - ] - , nav - ] + div [ class "uk-card uk-card-primary uk-card-body uk-width-2xlarge" ] + [ nav , div [ class "uk-padding" ] [ div [ class "uk-width-1-1 uk-margin-large" ] [ ul [ class "uk-child-width-expand", attribute "uk-tab" "" ] [ li - [ onClick (SetTaggingOption SingleTagging) + [ onClick (SetTaggingMode SingleTagging) , classList [ ( "uk-active", singleIsActiveTab ) ] ] [ a [ href "#" ] [ text singleTaggingText ] ] , li - [ onClick (SetTaggingOption BatchTagging) + [ onClick (SetTaggingMode BatchTagging) , classList [ ( "uk-active", not singleIsActiveTab ) ] ] [ a [ href "#" ] [ text batchTaggingText ] ] ] ] , viewTab - , viewTagActionDescription , viewTagCloud_ - , hr [ class "uk-divider-icon" ] [] ] ] -viewManualTaggingTab : Locale.Locale -> List ColumnHeadingName -> List String -> Html.Html msg +viewEmptyTabContent = + [ viewInitialFileSelect + ] + + +viewManualTaggingTab : Locale.Locale -> List ColumnHeadingName -> List String -> Html.Html Msg viewManualTaggingTab locale columns records = let content = if List.isEmpty records then - [ text <| Locale.translateNoRecordsToChooseFromSelectAfile locale ] + viewEmptyTabContent else - [ p - [ class "uk-text-meta" ] - [ span - [ class "uk-label uk-text-small" ] - [ text "NOTE" ] - , text <| " " ++ Locale.translateHowManualTaggingWorks locale - ] - , Table.viewSingle + [ Table.viewSingle [] columns (List.map text records) @@ -1257,27 +1190,49 @@ viewManualTaggingTab locale columns records = div [] content -viewBatchTaggingTab : Locale.Locale -> Dict.Dict ColumnHeadingName SearchPattern -> (ColumnHeadingName -> SearchPattern -> Msg) -> List ColumnHeadingName -> List Row -> Html.Html Msg -viewBatchTaggingTab locale batchTaggingOptions inputAction columns records = +viewManualTaggingHelp locale = + p + [ class "uk-text-meta" ] + [ span + [ class "uk-label uk-text-small" ] + [ text "NOTE" ] + , text <| " " ++ Locale.translateHowManualTaggingWorks locale + ] + + +viewBatchTaggingTab : Model -> (ColumnHeadingName -> SearchPattern -> Msg) -> List ColumnHeadingName -> List Row -> Html.Html Msg +viewBatchTaggingTab model inputAction columns records = let + instantSearchResults = + if List.isEmpty model.selectedWorkingData.rows then + text "" + + else + viewRows Table.Unresponsive model.selectedWorkingData.headers model.selectedWorkingData.rows + content = if List.isEmpty records then - [ text <| Locale.translateNoRecordsToChooseFromSelectAfile locale ] + viewEmptyTabContent else - [ p - [ class "uk-text-meta" ] - [ span - [ class "uk-label uk-text-small" ] - [ text "NOTE" ] - , span [ class "uk-text-small uk-text-light" ] [ text <| " " ++ Locale.translateHowBatchTaggingWorks locale ] - ] - , viewBatchTagging locale batchTaggingOptions inputAction columns records + [ viewBatchTagging model.locale model.batchTaggingOptions inputAction columns records + , hr [] [] + , instantSearchResults ] in div [] content +viewBatchTaggingHelp locale = + p + [ class "uk-text-meta" ] + [ span + [ class "uk-label uk-text-small" ] + [ text "NOTE" ] + , span [ class "uk-text-small uk-text-light" ] [ text <| " " ++ Locale.translateHowBatchTaggingWorks locale ] + ] + + viewBatchTagging : Locale.Locale -> Dict ColumnHeadingName SearchPattern -> (ColumnHeadingName -> SearchPattern -> Msg) -> List ColumnHeadingName -> List Row -> Html.Html Msg viewBatchTagging locale batchTaggingOptions inputAction columns records = let @@ -1315,6 +1270,7 @@ viewBatchTaggingInput labelText locale idVal val options action = div [ class "float-button" ] [ Input.viewAutocomplete labelText + "search" val idVal [ placeholder (Locale.translateSelectAKeywordOrRegex locale), onInput action ] @@ -1348,7 +1304,13 @@ viewTaggingIconNav ( history1, history2 ) = ( NavBar.Disabled NavBar.Undo, NoOp, [] ) in NavBar.viewIconNav - [ ( NavBar.ViewTableData, UserClickedViewWorkingDataNavButtonInTaggingSection, [] ) + [ ( NavBar.Import, UserClickedImportFileButton, [] ) + , ( NavBar.Spacer, NoOp, [] ) + , ( NavBar.ViewTableData, UserClickedViewWorkingDataNavButtonInTaggingSection, [] ) + , ( NavBar.ViewTaggedData, NoOp, [] ) + , ( NavBar.Spacer, NoOp, [] ) + , ( NavBar.ViewManageTags, NoOp, [] ) + , ( NavBar.Spacer, NoOp, [] ) , undoButton -- , ( NavBar.Disabled NavBar.Redo, NoOp, [] ) @@ -1403,6 +1365,46 @@ viewMappedRecordsPanel tagTranslation taggedRecordsText headers_ someTables = -- HELPERS +matchRows : Dict String String -> List String -> List Row -> List Row +matchRows batchTaggingOptions headers rows = + rows + |> List.map + (mapRowCellsToHaveColumns headers) + |> List.filter + (\row_ -> + let + matchingCellCount = + List.foldr + (\( column, cell ) count -> + case Dict.get column batchTaggingOptions of + Just searchPattern -> + let + regexPattern = + Regex.fromStringWith { caseInsensitive = True, multiline = False } searchPattern + |> Maybe.withDefault Regex.never + in + if Regex.contains regexPattern cell then + count + 1 + + else + count + + Nothing -> + count + ) + 0 + row_.cells + in + Dict.size batchTaggingOptions > 0 && Dict.size batchTaggingOptions == matchingCellCount + ) + |> List.map mapRowCellsToRemoveColumns + + +unwrapRows : List Table.Row -> List (List String) +unwrapRows records = + List.map .cells records + + getRegexDropdown key = Dict.fromList [ ( "Integers", [ ( "Integer", UserClickedItemInPlaceRegexDropDownList key IntegerRegex ) ] ) @@ -1520,11 +1522,22 @@ createTableDataFromCsv stacking csv model = let currentTableData = Maybe.withDefault (TableData [] []) (List.head model.tableData) + + updatedTableData = + TableData csv.headers <| currentTableData.rows ++ recordsConvertedToRows + + updatedSelectedWorkingData = + matchRows model.batchTaggingOptions updatedTableData.headers updatedTableData.rows + |> TableData updatedTableData.headers in - { model | tableData = [ TableData csv.headers <| currentTableData.rows ++ recordsConvertedToRows ] } + { model | tableData = [ updatedTableData ], selectedWorkingData = updatedSelectedWorkingData } Replace -> - { model | tableData = [ TableData csv.headers recordsConvertedToRows ] } + let + newTableData = + TableData csv.headers recordsConvertedToRows + in + { model | tableData = [ newTableData ], selectedWorkingData = newTableData } setCSVSemicolonsInList : List String -> List String diff --git a/src/elm/NavBar.elm b/src/elm/NavBar.elm index ade3786..6d2709f 100755 --- a/src/elm/NavBar.elm +++ b/src/elm/NavBar.elm @@ -1,7 +1,7 @@ module NavBar exposing (NavItem(..), viewIconNav) import Html exposing (button, div, li, ul) -import Html.Attributes exposing (attribute, class) +import Html.Attributes exposing (attribute, class, style) import Html.Events exposing (onClick) @@ -15,7 +15,11 @@ type NavItem | Forward | Backward | Export + | Import | ViewTableData + | ViewManageTags + | ViewTaggedData + | Spacer | Disabled NavItem @@ -30,6 +34,15 @@ viewIconNavItem attr msg = mapActionToElement : ( NavItem, msg, List (Html.Attribute msg) ) -> Html.Html msg mapActionToElement ( action, msg, attr ) = case action of + Spacer -> + viewIconNavItem [ style "border-left" "1px solid #efefef", style "height" "26px" ] msg + + ViewTaggedData -> + viewIconNavItem (attribute "uk-icon" "icon: database" :: attr) msg + + ViewManageTags -> + viewIconNavItem (attribute "uk-icon" "icon: tag" :: attr) msg + Undo -> viewIconNavItem (attribute "uk-icon" "icon: reply" :: attr) msg @@ -45,6 +58,9 @@ mapActionToElement ( action, msg, attr ) = Export -> viewIconNavItem (attribute "uk-icon" "icon: download" :: attr) msg + Import -> + viewIconNavItem (attribute "uk-icon" "icon: plus" :: attr) msg + ViewTableData -> viewIconNavItem (attribute "uk-icon" "icon: table" :: attr) msg @@ -55,7 +71,7 @@ mapActionToElement ( action, msg, attr ) = viewIconNav : List ( NavItem, msg, List (Html.Attribute msg) ) -> Html.Html msg viewIconNav buttonList = div - [ class "uk-position-top-right icon-nav" ] + [ class "uk-flex uk-flex-right uk-padding uk-padding-remove-top uk-padding-remove-bottom" ] [ ul [ class "uk-iconnav" ] (List.map mapActionToElement