diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 6d46e15b..aca743d5 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -2334,10 +2334,16 @@ getBookmarks flags receive = user = getBookmarksTask flags Data.BookmarkedQuery.User - combine bc bu = - AllBookmarks bc bu + shares = + getSharesTask flags "" False + + activeShare s = + s.enabled && s.name /= Nothing + + combine bc bu bs = + AllBookmarks bc bu (List.filter activeShare bs.items) in - Task.map2 combine coll user + Task.map3 combine coll user shares |> Task.attempt receive @@ -2436,10 +2442,12 @@ disableOtp flags otp receive = --- Share -getShares : Flags -> String -> Bool -> (Result Http.Error ShareList -> msg) -> Cmd msg -getShares flags query owning receive = - Http2.authGet - { url = +getSharesTask : Flags -> String -> Bool -> Task.Task Http.Error ShareList +getSharesTask flags query owning = + Http2.authTask + { method = + "GET" + , url = flags.config.baseUrl ++ "/api/v1/sec/share?q=" ++ Url.percentEncode query @@ -2450,10 +2458,18 @@ getShares flags query owning receive = "" ) , account = getAccount flags - , expect = Http.expectJson receive Api.Model.ShareList.decoder + , body = Http.emptyBody + , resolver = Http2.jsonResolver Api.Model.ShareList.decoder + , headers = [] + , timeout = Nothing } +getShares : Flags -> String -> Bool -> (Result Http.Error ShareList -> msg) -> Cmd msg +getShares flags query owning receive = + getSharesTask flags query owning |> Task.attempt receive + + getShare : Flags -> String -> (Result Http.Error ShareDetail -> msg) -> Cmd msg getShare flags id receive = Http2.authGet diff --git a/modules/webapp/src/main/elm/Comp/BookmarkChooser.elm b/modules/webapp/src/main/elm/Comp/BookmarkChooser.elm new file mode 100644 index 00000000..90b8ce5c --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BookmarkChooser.elm @@ -0,0 +1,202 @@ +module Comp.BookmarkChooser exposing + ( Model + , Msg + , Selection + , emptySelection + , getQueries + , init + , isEmpty + , isEmptySelection + , update + , view + ) + +import Api.Model.ShareDetail exposing (ShareDetail) +import Data.BookmarkedQuery exposing (AllBookmarks, BookmarkedQuery) +import Data.Icons as Icons +import Html exposing (Html, a, div, i, span, text) +import Html.Attributes exposing (class, classList, href) +import Html.Events exposing (onClick) +import Messages.Comp.BookmarkChooser exposing (Texts) +import Set exposing (Set) + + +type alias Model = + { all : AllBookmarks + } + + +init : AllBookmarks -> Model +init all = + { all = all + } + + +isEmpty : Model -> Bool +isEmpty model = + model.all == Data.BookmarkedQuery.allBookmarksEmpty + + +type alias Selection = + { user : Set String + , collective : Set String + , shares : Set String + } + + +emptySelection : Selection +emptySelection = + { user = Set.empty, collective = Set.empty, shares = Set.empty } + + +isEmptySelection : Selection -> Bool +isEmptySelection sel = + sel == emptySelection + + +type Kind + = User + | Collective + | Share + + +type Msg + = Toggle Kind String + + +getQueries : Model -> Selection -> List BookmarkedQuery +getQueries model sel = + let + member set bm = + Set.member bm.name set + + filterBookmarks f bms = + Data.BookmarkedQuery.filter f bms |> Data.BookmarkedQuery.map identity + in + List.concat + [ filterBookmarks (member sel.user) model.all.user + , filterBookmarks (member sel.collective) model.all.collective + , List.map shareToBookmark model.all.shares + |> List.filter (member sel.shares) + ] + + + +--- Update + + +update : Msg -> Model -> Selection -> ( Model, Selection ) +update msg model current = + let + toggle name set = + if Set.member name set then + Set.remove name set + + else + Set.insert name set + in + case msg of + Toggle kind name -> + case kind of + User -> + ( model, { current | user = toggle name current.user } ) + + Collective -> + ( model, { current | collective = toggle name current.collective } ) + + Share -> + ( model, { current | shares = toggle name current.shares } ) + + + +--- View + + +view : Texts -> Model -> Selection -> Html Msg +view texts model selection = + div [ class "flex flex-col" ] + [ userBookmarks texts model selection + , collBookmarks texts model selection + , shares texts model selection + ] + + +userBookmarks : Texts -> Model -> Selection -> Html Msg +userBookmarks texts model sel = + div + [ class "mb-2" + , classList [ ( "hidden", Data.BookmarkedQuery.emptyBookmarks == model.all.user ) ] + ] + [ div [ class " text-sm font-semibold py-0.5 " ] + [ text texts.userLabel + ] + , div [ class "flex flex-col space-y-2 md:space-y-1" ] + (Data.BookmarkedQuery.map (mkItem "fa fa-bookmark" sel User) model.all.user) + ] + + +collBookmarks : Texts -> Model -> Selection -> Html Msg +collBookmarks texts model sel = + div + [ class "mb-2" + , classList [ ( "hidden", Data.BookmarkedQuery.emptyBookmarks == model.all.collective ) ] + ] + [ div [ class " text-sm font-semibold py-0.5 " ] + [ text texts.collectiveLabel + ] + , div [ class "flex flex-col space-y-2 md:space-y-1" ] + (Data.BookmarkedQuery.map (mkItem "fa fa-bookmark font-light" sel Collective) model.all.collective) + ] + + +shares : Texts -> Model -> Selection -> Html Msg +shares texts model sel = + let + bms = + List.map shareToBookmark model.all.shares + in + div + [ class "" + , classList [ ( "hidden", List.isEmpty bms ) ] + ] + [ div [ class " text-sm font-semibold py-0.5 " ] + [ text texts.shareLabel + ] + , div [ class "flex flex-col space-y-2 md:space-y-1" ] + (List.map (mkItem Icons.share sel Share) bms) + ] + + +mkItem : String -> Selection -> Kind -> BookmarkedQuery -> Html Msg +mkItem icon sel kind bm = + a + [ class "flex flex-row items-center rounded px-1 py-1 hover:bg-blue-100 dark:hover:bg-slate-600" + , href "#" + , onClick (Toggle kind bm.name) + ] + [ if isSelected sel kind bm.name then + i [ class "fa fa-check" ] [] + + else + i [ class icon ] [] + , span [ class "ml-2" ] [ text bm.name ] + ] + + +isSelected : Selection -> Kind -> String -> Bool +isSelected sel kind name = + Set.member name <| + case kind of + User -> + sel.user + + Collective -> + sel.collective + + Share -> + sel.shares + + +shareToBookmark : ShareDetail -> BookmarkedQuery +shareToBookmark share = + BookmarkedQuery (Maybe.withDefault "-" share.name) share.query diff --git a/modules/webapp/src/main/elm/Comp/SearchMenu.elm b/modules/webapp/src/main/elm/Comp/SearchMenu.elm index 019987d7..4256aa48 100644 --- a/modules/webapp/src/main/elm/Comp/SearchMenu.elm +++ b/modules/webapp/src/main/elm/Comp/SearchMenu.elm @@ -16,6 +16,7 @@ module Comp.SearchMenu exposing , isFulltextSearch , isNamesSearch , linkTargetMsg + , refreshBookmarks , setFromStats , textSearchString , update @@ -33,6 +34,7 @@ import Api.Model.ItemQuery exposing (ItemQuery) import Api.Model.PersonList exposing (PersonList) import Api.Model.ReferenceList exposing (ReferenceList) import Api.Model.SearchStats exposing (SearchStats) +import Comp.BookmarkChooser import Comp.CustomFieldMultiInput import Comp.DatePicker import Comp.Dropdown exposing (isDropdownChangeMsg) @@ -41,6 +43,7 @@ import Comp.LinkTarget exposing (LinkTarget) import Comp.MenuBar as MB import Comp.Tabs import Comp.TagSelect +import Data.BookmarkedQuery exposing (AllBookmarks) import Data.CustomFieldChange exposing (CustomFieldValueCollect) import Data.Direction exposing (Direction) import Data.DropdownStyle as DS @@ -96,6 +99,8 @@ type alias Model = , customFieldModel : Comp.CustomFieldMultiInput.Model , customValues : CustomFieldValueCollect , sourceModel : Maybe String + , allBookmarks : Comp.BookmarkChooser.Model + , selectedBookmarks : Comp.BookmarkChooser.Selection , openTabs : Set String , searchMode : SearchMode } @@ -141,6 +146,8 @@ init flags = , customFieldModel = Comp.CustomFieldMultiInput.initWith [] , customValues = Data.CustomFieldChange.emptyCollect , sourceModel = Nothing + , allBookmarks = Comp.BookmarkChooser.init Data.BookmarkedQuery.allBookmarksEmpty + , selectedBookmarks = Comp.BookmarkChooser.emptySelection , openTabs = Set.fromList [ "Tags", "Inbox" ] , searchMode = Data.SearchMode.Normal } @@ -243,6 +250,10 @@ getItemQuery model = textSearch = textSearchValue model.textSearchModel + + bookmarks = + List.map .query (Comp.BookmarkChooser.getQueries model.allBookmarks model.selectedBookmarks) + |> List.map Q.Fragment in Q.and [ when model.inboxCheckbox (Q.Inbox True) @@ -289,6 +300,7 @@ getItemQuery model = |> Maybe.map Q.Dir , textSearch.fullText |> Maybe.map Q.Contents + , whenNotEmpty bookmarks Q.And ] @@ -333,6 +345,7 @@ resetModel model = model.customFieldModel , customValues = Data.CustomFieldChange.emptyCollect , sourceModel = Nothing + , selectedBookmarks = Comp.BookmarkChooser.emptySelection , searchMode = Data.SearchMode.Normal } @@ -380,6 +393,8 @@ type Msg | GetAllTagsResp (Result Http.Error SearchStats) | ToggleAkkordionTab String | ToggleOpenAllAkkordionTabs + | AllBookmarksResp (Result Http.Error AllBookmarks) + | SelectBookmarkMsg Comp.BookmarkChooser.Msg setFromStats : SearchStats -> Msg @@ -426,6 +441,11 @@ type alias NextState = } +refreshBookmarks : Flags -> Cmd Msg +refreshBookmarks flags = + Api.getBookmarks flags AllBookmarksResp + + update : Flags -> UiSettings -> Msg -> Model -> NextState update = updateDrop DD.init @@ -488,6 +508,7 @@ updateDrop ddm flags settings msg model = , Api.getPersons flags "" Data.PersonOrder.NameAsc GetPersonResp , Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags) , cdp + , Api.getBookmarks flags AllBookmarksResp ] , stateChange = False , dragDrop = DD.DragDropData ddm Nothing @@ -1040,6 +1061,31 @@ updateDrop ddm flags settings msg model = , dragDrop = DD.DragDropData ddm Nothing } + AllBookmarksResp (Ok bm) -> + { model = { model | allBookmarks = Comp.BookmarkChooser.init bm } + , cmd = Cmd.none + , stateChange = False + , dragDrop = DD.DragDropData ddm Nothing + } + + AllBookmarksResp (Err err) -> + { model = model + , cmd = Cmd.none + , stateChange = False + , dragDrop = DD.DragDropData ddm Nothing + } + + SelectBookmarkMsg lm -> + let + ( next, sel ) = + Comp.BookmarkChooser.update lm model.allBookmarks model.selectedBookmarks + in + { model = { model | allBookmarks = next, selectedBookmarks = sel } + , cmd = Cmd.none + , stateChange = sel /= model.selectedBookmarks + , dragDrop = DD.DragDropData ddm Nothing + } + --- View2 @@ -1064,6 +1110,7 @@ viewDrop2 texts ddd flags cfg settings model = type SearchTab = TabInbox + | TabBookmarks | TabTags | TabTagCategories | TabFolder @@ -1080,6 +1127,7 @@ type SearchTab allTabs : List SearchTab allTabs = [ TabInbox + , TabBookmarks , TabTags , TabTagCategories , TabFolder @@ -1100,6 +1148,9 @@ tabName tab = TabInbox -> "inbox" + TabBookmarks -> + "bookmarks" + TabTags -> "tags" @@ -1140,6 +1191,9 @@ findTab tab = "inbox" -> Just TabInbox + "bookmarks" -> + Just TabBookmarks + "tags" -> Just TabTags @@ -1215,6 +1269,16 @@ tabLook settings model tab = TabInbox -> activeWhen model.inboxCheckbox + TabBookmarks -> + if Comp.BookmarkChooser.isEmpty model.allBookmarks then + Comp.Tabs.Hidden + + else if not <| Comp.BookmarkChooser.isEmptySelection model.selectedBookmarks then + Comp.Tabs.Active + + else + Comp.Tabs.Normal + TabTags -> hiddenOr [ Data.Fields.Tag ] (activeWhenNotEmpty model.tagSelection.includeTags model.tagSelection.excludeTags) @@ -1329,52 +1393,15 @@ searchTabs texts ddd flags settings model = , label = texts.inbox , tagger = \_ -> ToggleInbox } - , div [ class "mt-2 hidden" ] - [ label [ class S.inputLabel ] - [ text - (case model.textSearchModel of - Fulltext _ -> - texts.fulltextSearch - - Names _ -> - texts.searchInNames - ) - , a - [ classList - [ ( "hidden", not flags.config.fullTextSearchEnabled ) - ] - , class "float-right" - , class S.link - , href "#" - , onClick SwapTextSearch - , title texts.switchSearchModes - ] - [ i [ class "fa fa-exchange-alt" ] [] - ] - ] - , input - [ type_ "text" - , onInput SetTextSearch - , Util.Html.onKeyUpCode KeyUpMsg - , textSearchString model.textSearchModel |> Maybe.withDefault "" |> value - , case model.textSearchModel of - Fulltext _ -> - placeholder texts.contentSearch - - Names _ -> - placeholder texts.searchInNamesPlaceholder - , class S.textInputSidebar - ] - [] - , span [ class "opacity-50 text-sm" ] - [ case model.textSearchModel of - Fulltext _ -> - text texts.fulltextSearchInfo - - Names _ -> - text texts.nameSearchInfo - ] - ] + ] + } + , { name = tabName TabBookmarks + , title = texts.bookmarks + , titleRight = [] + , info = Nothing + , body = + [ Html.map SelectBookmarkMsg + (Comp.BookmarkChooser.view texts.bookmarkChooser model.allBookmarks model.selectedBookmarks) ] } , { name = tabName TabTags diff --git a/modules/webapp/src/main/elm/Data/BookmarkedQuery.elm b/modules/webapp/src/main/elm/Data/BookmarkedQuery.elm index 4c2c21cc..ea5e8358 100644 --- a/modules/webapp/src/main/elm/Data/BookmarkedQuery.elm +++ b/modules/webapp/src/main/elm/Data/BookmarkedQuery.elm @@ -5,13 +5,17 @@ module Data.BookmarkedQuery exposing , Bookmarks , Location(..) , add + , allBookmarksEmpty , bookmarksDecoder , bookmarksEncode , emptyBookmarks , exists + , filter + , map , remove ) +import Api.Model.ShareDetail exposing (ShareDetail) import Json.Decode as D import Json.Encode as E @@ -52,6 +56,20 @@ type Bookmarks = Bookmarks (List BookmarkedQuery) +map : (BookmarkedQuery -> a) -> Bookmarks -> List a +map f bms = + case bms of + Bookmarks items -> + List.map f items + + +filter : (BookmarkedQuery -> Bool) -> Bookmarks -> Bookmarks +filter f bms = + case bms of + Bookmarks items -> + Bookmarks <| List.filter f items + + emptyBookmarks : Bookmarks emptyBookmarks = Bookmarks [] @@ -60,9 +78,15 @@ emptyBookmarks = type alias AllBookmarks = { collective : Bookmarks , user : Bookmarks + , shares : List ShareDetail } +allBookmarksEmpty : AllBookmarks +allBookmarksEmpty = + AllBookmarks emptyBookmarks emptyBookmarks [] + + {-| Checks wether a bookmark of this name already exists. -} exists : String -> Bookmarks -> Bool diff --git a/modules/webapp/src/main/elm/Data/ItemQuery.elm b/modules/webapp/src/main/elm/Data/ItemQuery.elm index bf6d3979..36370a6b 100644 --- a/modules/webapp/src/main/elm/Data/ItemQuery.elm +++ b/modules/webapp/src/main/elm/Data/ItemQuery.elm @@ -71,7 +71,7 @@ and list = Nothing es -> - Just (And es) + Just (unwrap (And es)) request : SearchMode -> Maybe ItemQuery -> RQ.ItemQuery @@ -90,6 +90,32 @@ renderMaybe mq = |> Maybe.withDefault "" +unwrap : ItemQuery -> ItemQuery +unwrap query = + case query of + And inner -> + case inner of + first :: [] -> + unwrap first + + _ -> + And (List.map unwrap inner) + + Or inner -> + case inner of + first :: [] -> + unwrap first + + _ -> + Or (List.map unwrap inner) + + Not (Not inner) -> + unwrap inner + + _ -> + query + + render : ItemQuery -> String render q = let @@ -118,7 +144,7 @@ render q = String.replace "\"" "\\\"" >> surround "\"" in - case q of + case unwrap q of And inner -> List.map render inner |> String.join " " diff --git a/modules/webapp/src/main/elm/Messages/Comp/BookmarkChooser.elm b/modules/webapp/src/main/elm/Messages/Comp/BookmarkChooser.elm new file mode 100644 index 00000000..a878a8e0 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BookmarkChooser.elm @@ -0,0 +1,40 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BookmarkChooser exposing + ( Texts + , de + , gb + ) + +import Messages.Basics + + +type alias Texts = + { basics : Messages.Basics.Texts + , userLabel : String + , collectiveLabel : String + , shareLabel : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , userLabel = "Personal" + , collectiveLabel = "Collective" + , shareLabel = "Shares" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , userLabel = "Persönlich" + , collectiveLabel = "Kollektiv" + , shareLabel = "Freigaben" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/SearchMenu.elm b/modules/webapp/src/main/elm/Messages/Comp/SearchMenu.elm index 23ee8342..e526ac19 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/SearchMenu.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/SearchMenu.elm @@ -13,6 +13,7 @@ module Messages.Comp.SearchMenu exposing import Data.Direction exposing (Direction) import Messages.Basics +import Messages.Comp.BookmarkChooser import Messages.Comp.CustomFieldMultiInput import Messages.Comp.FolderSelect import Messages.Comp.TagSelect @@ -24,6 +25,7 @@ type alias Texts = , customFieldMultiInput : Messages.Comp.CustomFieldMultiInput.Texts , tagSelect : Messages.Comp.TagSelect.Texts , folderSelect : Messages.Comp.FolderSelect.Texts + , bookmarkChooser : Messages.Comp.BookmarkChooser.Texts , chooseDirection : String , choosePerson : String , chooseEquipment : String @@ -47,6 +49,7 @@ type alias Texts = , searchInItemSource : String , direction : Direction -> String , trashcan : String + , bookmarks : String } @@ -56,6 +59,7 @@ gb = , customFieldMultiInput = Messages.Comp.CustomFieldMultiInput.gb , tagSelect = Messages.Comp.TagSelect.gb , folderSelect = Messages.Comp.FolderSelect.gb + , bookmarkChooser = Messages.Comp.BookmarkChooser.gb , chooseDirection = "Choose a direction…" , choosePerson = "Choose a person" , chooseEquipment = "Choose an equipment" @@ -79,6 +83,7 @@ gb = , searchInItemSource = "Search in item source…" , direction = Messages.Data.Direction.gb , trashcan = "Trash" + , bookmarks = "Bookmarks" } @@ -88,6 +93,7 @@ de = , customFieldMultiInput = Messages.Comp.CustomFieldMultiInput.de , tagSelect = Messages.Comp.TagSelect.de , folderSelect = Messages.Comp.FolderSelect.de + , bookmarkChooser = Messages.Comp.BookmarkChooser.de , chooseDirection = "Wähle eine Richtung…" , choosePerson = "Wähle eine Person…" , chooseEquipment = "Wähle eine Ausstattung" @@ -111,4 +117,5 @@ de = , searchInItemSource = "Suche in Dokumentquelle…" , direction = Messages.Data.Direction.de , trashcan = "Papierkorb" + , bookmarks = "Bookmarks" } diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index e06ec59d..aa99403a 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -966,10 +966,20 @@ update mId key flags texts settings msg model = else BookmarkQuery res.model + + refreshCmd = + if res.outcome == Comp.BookmarkQueryManage.Done then + Cmd.map SearchMenuMsg (Comp.SearchMenu.refreshBookmarks flags) + + else + Cmd.none in makeResult ( { model | topWidgetModel = nextModel } - , Cmd.map BookmarkQueryMsg res.cmd + , Cmd.batch + [ Cmd.map BookmarkQueryMsg res.cmd + , refreshCmd + ] , Sub.map BookmarkQueryMsg res.sub ) @@ -991,7 +1001,10 @@ update mId key flags texts settings msg model = ) Comp.PublishItems.OutcomeDone -> - noSub ( { model | viewMode = SearchView }, Cmd.none ) + noSub + ( { model | viewMode = SearchView } + , Cmd.map SearchMenuMsg (Comp.SearchMenu.refreshBookmarks flags) + ) _ -> noSub ( model, Cmd.none )