diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index a57308bd..c585b147 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1961,7 +1961,7 @@ paths: - $ref: "#/components/parameters/bookmarkId" delete: operationId: "sec-querybookmark-delete" - tags: [Query Bookmark] + tags: [Query Bookmarks] summary: Delete a bookmark. description: | Deletes a bookmarks by its id. diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 3b9f63d4..5a5cff99 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -11,6 +11,7 @@ module Api exposing , addConcPerson , addCorrOrg , addCorrPerson + , addDashboard , addMember , addShare , addTag @@ -39,6 +40,7 @@ module Api exposing , deleteCustomField , deleteCustomValue , deleteCustomValueMultiple + , deleteDashboard , deleteEquip , deleteFolder , deleteHook @@ -56,6 +58,7 @@ module Api exposing , deleteUser , disableOtp , fileURL + , getAllDashboards , getAttachmentMeta , getBookmarks , getChannels @@ -101,7 +104,9 @@ module Api exposing , itemDetailShare , itemIndexSearch , itemSearch + , itemSearchBookmark , itemSearchStats + , itemSearchStatsBookmark , login , loginSession , logout @@ -124,6 +129,7 @@ module Api exposing , register , removeMember , removeTagsMultiple + , replaceDashboard , reprocessItem , reprocessMultiple , restoreAllItems @@ -275,9 +281,12 @@ import Api.Model.User exposing (User) import Api.Model.UserList exposing (UserList) import Api.Model.UserPass exposing (UserPass) import Api.Model.VersionInfo exposing (VersionInfo) +import Data.AccountScope exposing (AccountScope) import Data.Bookmarks exposing (AllBookmarks, Bookmarks) import Data.ContactType exposing (ContactType) import Data.CustomFieldOrder exposing (CustomFieldOrder) +import Data.Dashboard exposing (Dashboard) +import Data.Dashboards exposing (AllDashboards, Dashboards) import Data.EquipmentOrder exposing (EquipmentOrder) import Data.EventType exposing (EventType) import Data.Flags exposing (Flags) @@ -297,6 +306,7 @@ import Task import Url import Util.File import Util.Http as Http2 +import Util.Result @@ -2028,24 +2038,70 @@ itemIndexSearch flags query receive = } -itemSearch : Flags -> ItemQuery -> (Result Http.Error ItemLightList -> msg) -> Cmd msg -itemSearch flags search receive = - Http2.authPost +itemSearchTask : Flags -> ItemQuery -> Task.Task Http.Error ItemLightList +itemSearchTask flags search = + Http2.authTask { url = flags.config.baseUrl ++ "/api/v1/sec/item/search" + , method = "POST" + , headers = [] , account = getAccount flags , body = Http.jsonBody (Api.Model.ItemQuery.encode search) - , expect = Http.expectJson receive Api.Model.ItemLightList.decoder + , resolver = Http2.jsonResolver Api.Model.ItemLightList.decoder + , timeout = Nothing + } + + +itemSearch : Flags -> ItemQuery -> (Result Http.Error ItemLightList -> msg) -> Cmd msg +itemSearch flags search receive = + itemSearchTask flags search |> Task.attempt receive + + +{-| Same as `itemSearch` but interprets the `query` field as a bookmark id. +-} +itemSearchBookmark : Flags -> ItemQuery -> (Result Http.Error ItemLightList -> msg) -> Cmd msg +itemSearchBookmark flags bmSearch receive = + let + getBookmark = + getBookmarkByIdTask flags bmSearch.query + |> Task.map (\bm -> { bmSearch | query = bm.query }) + + search q = + itemSearchTask flags q + in + Task.andThen search getBookmark + |> Task.attempt receive + + +itemSearchStatsTask : Flags -> ItemQuery -> Task.Task Http.Error SearchStats +itemSearchStatsTask flags search = + Http2.authTask + { url = flags.config.baseUrl ++ "/api/v1/sec/item/searchStats" + , method = "POST" + , headers = [] + , account = getAccount flags + , body = Http.jsonBody (Api.Model.ItemQuery.encode search) + , resolver = Http2.jsonResolver Api.Model.SearchStats.decoder + , timeout = Nothing } itemSearchStats : Flags -> ItemQuery -> (Result Http.Error SearchStats -> msg) -> Cmd msg itemSearchStats flags search receive = - Http2.authPost - { url = flags.config.baseUrl ++ "/api/v1/sec/item/searchStats" - , account = getAccount flags - , body = Http.jsonBody (Api.Model.ItemQuery.encode search) - , expect = Http.expectJson receive Api.Model.SearchStats.decoder - } + itemSearchStatsTask flags search |> Task.attempt receive + + +itemSearchStatsBookmark : Flags -> ItemQuery -> (Result Http.Error SearchStats -> msg) -> Cmd msg +itemSearchStatsBookmark flags search receive = + let + getBookmark = + getBookmarkByIdTask flags search.query + |> Task.map (\bm -> { search | query = bm.query }) + + getStats q = + itemSearchStatsTask flags q + in + Task.andThen getStats getBookmark + |> Task.attempt receive itemDetail : Flags -> String -> (Result Http.Error ItemDetail -> msg) -> Cmd msg @@ -2314,6 +2370,132 @@ saveClientSettings flags settings receive = +--- Dashboards + + +dashboardsUrl : Flags -> AccountScope -> String +dashboardsUrl flags scope = + let + part = + Data.AccountScope.fold "user" "collective" scope + in + flags.config.baseUrl ++ "/api/v1/sec/clientSettings/" ++ part ++ "/webClientDashboards" + + +getDashboardsScopeTask : Flags -> AccountScope -> Task.Task Http.Error Dashboards +getDashboardsScopeTask flags scope = + Http2.authTask + { method = "GET" + , url = dashboardsUrl flags scope + , account = getAccount flags + , body = Http.emptyBody + , resolver = Http2.jsonResolver Data.Dashboards.decoder + , headers = [] + , timeout = Nothing + } + + +pushDashbordsScopeTask : Flags -> AccountScope -> Dashboards -> Task.Task Http.Error BasicResult +pushDashbordsScopeTask flags scope boards = + Http2.authTask + { method = "PUT" + , url = dashboardsUrl flags scope + , account = getAccount flags + , body = Http.jsonBody (Data.Dashboards.encode boards) + , resolver = Http2.jsonResolver Api.Model.BasicResult.decoder + , headers = [] + , timeout = Nothing + } + + +getAllDashboardsTask : Flags -> Task.Task Http.Error AllDashboards +getAllDashboardsTask flags = + let + coll = + getDashboardsScopeTask flags Data.AccountScope.Collective + + user = + getDashboardsScopeTask flags Data.AccountScope.User + in + Task.map2 AllDashboards coll user + + +getAllDashboards : Flags -> (Result Http.Error AllDashboards -> msg) -> Cmd msg +getAllDashboards flags receive = + getAllDashboardsTask flags |> Task.attempt receive + + +saveDashboardTask : Flags -> String -> Dashboard -> AccountScope -> Bool -> Task.Task Http.Error BasicResult +saveDashboardTask flags original board scope isDefault = + let + boardsTask = + getAllDashboardsTask flags + + setDefault all = + if isDefault then + Data.Dashboards.setDefaultAll board.name all + + else + Data.Dashboards.unsetDefaultAll board.name all + + removeOriginal boards = + Data.Dashboards.removeFromAll original boards + + insert all = + Data.Dashboards.insertIn scope board all + + update all = + let + next = + (removeOriginal >> insert >> setDefault) all + + saveU = + if all.user == next.user then + Task.succeed (BasicResult True "") + + else + pushDashbordsScopeTask flags Data.AccountScope.User next.user + + saveC = + if all.collective == next.collective then + Task.succeed (BasicResult True "") + + else + pushDashbordsScopeTask flags Data.AccountScope.Collective next.collective + in + Task.map2 Util.Result.combine saveU saveC + in + Task.andThen update boardsTask + + +addDashboard : Flags -> Dashboard -> AccountScope -> Bool -> (Result Http.Error BasicResult -> msg) -> Cmd msg +addDashboard flags board scope isDefault receive = + saveDashboardTask flags board.name board scope isDefault |> Task.attempt receive + + +replaceDashboard : Flags -> String -> Dashboard -> AccountScope -> Bool -> (Result Http.Error BasicResult -> msg) -> Cmd msg +replaceDashboard flags originalName board scope isDefault receive = + saveDashboardTask flags originalName board scope isDefault |> Task.attempt receive + + +deleteDashboardTask : Flags -> String -> AccountScope -> Task.Task Http.Error BasicResult +deleteDashboardTask flags name scope = + let + boardsTask = + getDashboardsScopeTask flags scope + + remove boards = + Data.Dashboards.remove name boards + in + Task.andThen (remove >> pushDashbordsScopeTask flags scope) boardsTask + + +deleteDashboard : Flags -> String -> AccountScope -> (Result Http.Error BasicResult -> msg) -> Cmd msg +deleteDashboard flags name scope receive = + deleteDashboardTask flags name scope |> Task.attempt receive + + + --- Query Bookmarks @@ -2335,6 +2517,21 @@ getBookmarksTask flags = } +getBookmarkByIdTask : Flags -> String -> Task.Task Http.Error BookmarkedQuery +getBookmarkByIdTask flags id = + let + findBm all = + Data.Bookmarks.findById id all + + mapNotFound maybeBookmark = + Maybe.map Task.succeed maybeBookmark + |> Maybe.withDefault (Task.fail (Http.BadStatus 404)) + in + getBookmarksTask flags + |> Task.map findBm + |> Task.andThen mapNotFound + + getBookmarks : Flags -> (Result Http.Error AllBookmarks -> msg) -> Cmd msg getBookmarks flags receive = let diff --git a/modules/webapp/src/main/elm/App/Data.elm b/modules/webapp/src/main/elm/App/Data.elm index 2aadcab4..0facd89b 100644 --- a/modules/webapp/src/main/elm/App/Data.elm +++ b/modules/webapp/src/main/elm/App/Data.elm @@ -18,21 +18,25 @@ import Api.Model.BasicResult exposing (BasicResult) import Api.Model.VersionInfo exposing (VersionInfo) import Browser exposing (UrlRequest) import Browser.Navigation exposing (Key) +import Data.Dashboard exposing (Dashboard) import Data.Flags exposing (Flags) import Data.ServerEvent exposing (ServerEvent) import Data.UiSettings exposing (StoredUiSettings, UiSettings) import Data.UiTheme exposing (UiTheme) import Http +import Messages import Messages.UiLanguage exposing (UiLanguage) import Page exposing (Page(..)) import Page.CollectiveSettings.Data -import Page.Home.Data +import Page.Dashboard.Data +import Page.Dashboard.DefaultDashboard import Page.ItemDetail.Data import Page.Login.Data import Page.ManageData.Data import Page.NewInvite.Data import Page.Queue.Data import Page.Register.Data +import Page.Search.Data import Page.Share.Data import Page.ShareDetail.Data import Page.Upload.Data @@ -45,7 +49,7 @@ type alias Model = , key : Key , page : Page , version : VersionInfo - , homeModel : Page.Home.Data.Model + , searchModel : Page.Search.Data.Model , loginModel : Page.Login.Data.Model , manageDataModel : Page.ManageData.Data.Model , collSettingsModel : Page.CollectiveSettings.Data.Model @@ -57,6 +61,7 @@ type alias Model = , itemDetailModel : Page.ItemDetail.Data.Model , shareModel : Page.Share.Data.Model , shareDetailModel : Page.ShareDetail.Data.Model + , dashboardModel : Page.Dashboard.Data.Model , navMenuOpen : Bool , userMenuOpen : Bool , subs : Sub Msg @@ -98,18 +103,21 @@ init key url flags_ settings = ( sdm, sdc ) = Page.ShareDetail.Data.init (Page.pageShareDetail page) flags - homeViewMode = + ( dbm, dbc ) = + Page.Dashboard.Data.init flags + + searchViewMode = if settings.searchMenuVisible then - Page.Home.Data.SearchView + Page.Search.Data.SearchView else - Page.Home.Data.SimpleView + Page.Search.Data.SimpleView in ( { flags = flags , key = key , page = page , version = Api.Model.VersionInfo.empty - , homeModel = Page.Home.Data.init flags homeViewMode + , searchModel = Page.Search.Data.init flags searchViewMode , loginModel = loginm , manageDataModel = mdm , collSettingsModel = csm @@ -121,6 +129,7 @@ init key url flags_ settings = , itemDetailModel = Page.ItemDetail.Data.emptyModel , shareModel = shm , shareDetailModel = sdm + , dashboardModel = dbm , navMenuOpen = False , userMenuOpen = False , subs = Sub.none @@ -133,7 +142,8 @@ init key url flags_ settings = , jobsWaiting = 0 } , Cmd.batch - [ Cmd.map UserSettingsMsg uc + [ Cmd.map DashboardMsg dbc + , Cmd.map UserSettingsMsg uc , Cmd.map ManageDataMsg mdc , Cmd.map CollSettingsMsg csc , Cmd.map LoginMsg loginc @@ -171,7 +181,7 @@ type Msg = NavRequest UrlRequest | NavChange Url | VersionResp (Result Http.Error VersionInfo) - | HomeMsg Page.Home.Data.Msg + | SearchMsg Page.Search.Data.Msg | LoginMsg Page.Login.Data.Msg | ManageDataMsg Page.ManageData.Data.Msg | CollSettingsMsg Page.CollectiveSettings.Data.Msg @@ -183,6 +193,7 @@ type Msg | ItemDetailMsg Page.ItemDetail.Data.Msg | ShareMsg Page.Share.Data.Msg | ShareDetailMsg Page.ShareDetail.Data.Msg + | DashboardMsg Page.Dashboard.Data.Msg | Logout | LogoutResp (Result Http.Error ()) | SessionCheckResp (Result Http.Error AuthResult) @@ -201,14 +212,9 @@ type Msg defaultPage : Flags -> Page defaultPage _ = - HomePage + DashboardPage getUiLanguage : Model -> UiLanguage getUiLanguage model = - case model.flags.account of - Just _ -> - model.uiSettings.uiLang - - Nothing -> - model.anonymousUiLang + Data.UiSettings.getUiLanguage model.flags model.uiSettings model.anonymousUiLang diff --git a/modules/webapp/src/main/elm/App/Update.elm b/modules/webapp/src/main/elm/App/Update.elm index a37368d2..268e05cf 100644 --- a/modules/webapp/src/main/elm/App/Update.elm +++ b/modules/webapp/src/main/elm/App/Update.elm @@ -14,7 +14,7 @@ import Api import App.Data exposing (..) import Browser exposing (UrlRequest(..)) import Browser.Navigation as Nav -import Data.Flags exposing (Flags) +import Data.Flags import Data.ServerEvent exposing (ServerEvent(..)) import Data.UiSettings exposing (UiSettings) import Data.UiTheme @@ -22,8 +22,8 @@ import Messages exposing (Messages) import Page exposing (Page(..)) import Page.CollectiveSettings.Data import Page.CollectiveSettings.Update -import Page.Home.Data -import Page.Home.Update +import Page.Dashboard.Data +import Page.Dashboard.Update import Page.ItemDetail.Data import Page.ItemDetail.Update import Page.Login.Data @@ -36,6 +36,8 @@ import Page.Queue.Data import Page.Queue.Update import Page.Register.Data import Page.Register.Update +import Page.Search.Data +import Page.Search.Update import Page.Share.Data import Page.Share.Update import Page.ShareDetail.Data @@ -121,8 +123,8 @@ updateWithSub msg model = SetLanguage lang -> ( { model | anonymousUiLang = lang, langMenuOpen = False }, Cmd.none, Sub.none ) - HomeMsg lm -> - updateHome texts lm model + SearchMsg lm -> + updateSearch texts lm model ShareMsg lm -> updateShare lm model @@ -157,6 +159,9 @@ updateWithSub msg model = ItemDetailMsg m -> updateItemDetail texts m model + DashboardMsg m -> + updateDashboard texts m model + VersionResp (Ok info) -> ( { model | version = info }, Cmd.none, Sub.none ) @@ -318,12 +323,15 @@ updateWithSub msg model = newModel = { model - | showNewItemsArrived = isProcessItem && model.page /= HomePage + | showNewItemsArrived = isProcessItem && not (Page.isSearchPage model.page) , jobsWaiting = max 0 (model.jobsWaiting - 1) } in - if model.page == HomePage && isProcessItem then - updateHome texts Page.Home.Data.RefreshView newModel + if Page.isSearchPage model.page && isProcessItem then + updateSearch texts Page.Search.Data.RefreshView newModel + + else if Page.isDashboardPage model.page && isProcessItem then + updateDashboard texts Page.Dashboard.Data.reloadDashboardData newModel else ( newModel, Cmd.none, Sub.none ) @@ -359,13 +367,31 @@ applyClientSettings texts model settings = , setTheme , Sub.none ) + , updateDashboard texts Page.Dashboard.Data.reloadUiSettings , updateUserSettings texts Page.UserSettings.Data.UpdateSettings - , updateHome texts Page.Home.Data.UiSettingsUpdated + , updateSearch texts Page.Search.Data.UiSettingsUpdated , updateItemDetail texts Page.ItemDetail.Data.UiSettingsUpdated ] { model | uiSettings = settings } +updateDashboard : Messages -> Page.Dashboard.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +updateDashboard texts lmsg model = + let + ( dbm, dbc, dbs ) = + Page.Dashboard.Update.update texts.dashboard + model.uiSettings + model.key + model.flags + lmsg + model.dashboardModel + in + ( { model | dashboardModel = dbm } + , Cmd.map DashboardMsg dbc + , Sub.map DashboardMsg dbs + ) + + updateShareDetail : Page.ShareDetail.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) updateShareDetail lmsg model = case Page.pageShareDetail model.page of @@ -404,7 +430,7 @@ updateItemDetail : Messages -> Page.ItemDetail.Data.Msg -> Model -> ( Model, Cmd updateItemDetail texts lmsg model = let inav = - Page.Home.Data.itemNav model.itemDetailModel.detail.item.id model.homeModel + Page.Search.Data.itemNav model.itemDetailModel.detail.item.id model.searchModel result = Page.ItemDetail.Update.update @@ -421,12 +447,12 @@ updateItemDetail texts lmsg model = } ( hm, hc, hs ) = - updateHome texts (Page.Home.Data.SetLinkTarget result.linkTarget) model_ + updateSearch texts (Page.Search.Data.SetLinkTarget result.linkTarget) model_ ( hm1, hc1, hs1 ) = case result.removedItem of Just removedId -> - updateHome texts (Page.Home.Data.RemoveItem removedId) hm + updateSearch texts (Page.Search.Data.RemoveItem removedId) hm Nothing -> ( hm, hc, hs ) @@ -552,22 +578,22 @@ updateLogin lmsg model = ) -updateHome : Messages -> Page.Home.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) -updateHome texts lmsg model = +updateSearch : Messages -> Page.Search.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +updateSearch texts lmsg model = let - mid = + ( mid, bmId ) = case model.page of - HomePage -> - Util.Maybe.fromString model.itemDetailModel.detail.item.id + SearchPage bId -> + ( Util.Maybe.fromString model.itemDetailModel.detail.item.id, bId ) _ -> - Nothing + ( Nothing, Nothing ) result = - Page.Home.Update.update mid model.key model.flags texts.home model.uiSettings lmsg model.homeModel + Page.Search.Update.update bmId mid model.key model.flags texts.search model.uiSettings lmsg model.searchModel model_ = - { model | homeModel = result.model } + { model | searchModel = result.model } ( lm, lc, ls ) = case result.newSettings of @@ -579,11 +605,11 @@ updateHome texts lmsg model = in ( lm , Cmd.batch - [ Cmd.map HomeMsg result.cmd + [ Cmd.map SearchMsg result.cmd , lc ] , Sub.batch - [ Sub.map HomeMsg result.sub + [ Sub.map SearchMsg result.sub , ls ] ) @@ -611,9 +637,9 @@ initPage model_ page = Messages.get <| App.Data.getUiLanguage model in case page of - HomePage -> + SearchPage _ -> Util.Update.andThen2 - [ updateHome texts Page.Home.Data.Init + [ updateSearch texts Page.Search.Data.Init , updateQueue Page.Queue.Data.StopRefresh ] model @@ -646,7 +672,7 @@ initPage model_ page = UploadPage _ -> Util.Update.andThen2 [ updateQueue Page.Queue.Data.StopRefresh - , updateUpload Page.Upload.Data.Clear + , updateUpload Page.Upload.Data.reset ] model @@ -685,3 +711,6 @@ initPage model_ page = _ -> ( model, Cmd.none, Sub.none ) + + DashboardPage -> + ( model, Cmd.map DashboardMsg (Page.Dashboard.Data.reinitCmd model.flags), Sub.none ) diff --git a/modules/webapp/src/main/elm/App/View2.elm b/modules/webapp/src/main/elm/App/View2.elm index 41d556f3..09e7697d 100644 --- a/modules/webapp/src/main/elm/App/View2.elm +++ b/modules/webapp/src/main/elm/App/View2.elm @@ -11,6 +11,8 @@ import Api.Model.AuthResult exposing (AuthResult) import App.Data exposing (..) import Comp.Basic as B import Data.Flags +import Data.Icons as Icons +import Data.UiSettings import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) @@ -19,14 +21,15 @@ import Messages.App exposing (Texts) import Messages.UiLanguage import Page exposing (Page(..)) import Page.CollectiveSettings.View2 as CollectiveSettings -import Page.Home.Data -import Page.Home.View2 as Home +import Page.Dashboard.View as Dashboard import Page.ItemDetail.View2 as ItemDetail import Page.Login.View2 as Login import Page.ManageData.View2 as ManageData import Page.NewInvite.View2 as NewInvite import Page.Queue.View2 as Queue import Page.Register.View2 as Register +import Page.Search.Data +import Page.Search.View2 as Search import Page.Share.View as Share import Page.ShareDetail.View as ShareDetail import Page.Upload.View2 as Upload @@ -76,7 +79,11 @@ topNavUser auth model = [ class S.infoMessageBase , class "my-2 px-1 py-1 rounded-lg inline-block hover:opacity-50" , classList [ ( "hidden", not model.showNewItemsArrived ) ] - , Page.href HomePage + , if Page.isSearchPage model.page || Page.isDashboardPage model.page then + href "#" + + else + Page.href (SearchPage Nothing) , onClick ToggleShowNewItemsArrived ] [ i [ class "fa fa-exclamation-circle mr-1" ] [] @@ -133,7 +140,7 @@ headerNavItem authenticated model = [ class "inline-flex font-bold items-center px-4" , classList [ ( "hover:bg-blue-200 dark:hover:bg-slate-800", authenticated ) ] , if authenticated then - Page.href HomePage + Page.href DashboardPage else href "#" @@ -160,8 +167,11 @@ mainContent model = , class styleMain ] (case model.page of - HomePage -> - viewHome texts model + DashboardPage -> + viewDashboard texts model + + SearchPage bmId -> + viewSearch texts bmId model CollectiveSettingPage -> viewCollectiveSettings texts model @@ -280,7 +290,7 @@ dataMenu texts _ model = , classList [ ( "hidden", not model.navMenuOpen ) ] ] [ dataPageLink model - HomePage + DashboardPage [] [ img [ class "w-4 inline-block" @@ -288,14 +298,22 @@ dataMenu texts _ model = ] [] , div [ class "inline-block ml-2" ] - [ text texts.items + [ text texts.dashboard ] ] , div [ class "py-1" ] [ hr [ class S.border ] [] ] + , dataPageLink model + (SearchPage Nothing) + [] + [ Icons.searchIcon "w-6" + , span [ class "ml-1" ] + [ text texts.items + ] + ] , dataPageLink model ManageDataPage [] - [ i [ class "fa fa-cubes w-6" ] [] + [ Icons.metadataIcon "w-6" , span [ class "ml-1" ] [ text texts.manageData ] @@ -304,7 +322,7 @@ dataMenu texts _ model = , dataPageLink model (UploadPage Nothing) [] - [ i [ class "fa fa-upload w-6" ] [] + [ Icons.fileUploadIcon "w-6" , span [ class "ml-1" ] [ text texts.uploadFiles ] @@ -345,11 +363,11 @@ dataMenu texts _ model = ] , a [ class dropdownItem - , href "https://docspell.org/docs" + , href Data.UiSettings.documentationSite , target "_new" - , title "Opens https://docspell.org/docs" + , title ("Opens " ++ Data.UiSettings.documentationSite) ] - [ i [ class "fa fa-question-circle w-6" ] [] + [ Icons.documentationIcon "w-6" , span [ class "ml-1" ] [ text texts.help ] , span [ class "float-right" ] [ i [ class "fa fa-external-link-alt w-6" ] [] @@ -467,6 +485,25 @@ dropdownMenu = " absolute right-0 bg-white dark:bg-slate-800 border dark:border-slate-700 dark:text-slate-300 shadow-lg opacity-1 transition duration-200 min-w-max " +viewDashboard : Messages -> Model -> List (Html Msg) +viewDashboard texts model = + [ Html.map DashboardMsg + (Dashboard.viewSidebar texts.dashboard + model.sidebarVisible + model.flags + model.version + model.uiSettings + model.dashboardModel + ) + , Html.map DashboardMsg + (Dashboard.viewContent texts.dashboard + model.flags + model.uiSettings + model.dashboardModel + ) + ] + + viewShare : Messages -> String -> Model -> List (Html Msg) viewShare texts shareId model = [ Html.map ShareMsg @@ -510,20 +547,20 @@ viewShareDetail texts shareId itemId model = ] -viewHome : Messages -> Model -> List (Html Msg) -viewHome texts model = - [ Html.map HomeMsg - (Home.viewSidebar texts.home +viewSearch : Messages -> Maybe String -> Model -> List (Html Msg) +viewSearch texts bmId model = + [ Html.map SearchMsg + (Search.viewSidebar texts.search model.sidebarVisible model.flags model.uiSettings - model.homeModel + model.searchModel ) - , Html.map HomeMsg - (Home.viewContent texts.home + , Html.map SearchMsg + (Search.viewContent texts.search model.flags model.uiSettings - model.homeModel + model.searchModel ) ] @@ -647,7 +684,7 @@ viewItemDetail : Messages -> String -> Model -> List (Html Msg) viewItemDetail texts id model = let inav = - Page.Home.Data.itemNav id model.homeModel + Page.Search.Data.itemNav id model.searchModel in [ Html.map ItemDetailMsg (ItemDetail.viewSidebar texts.itemDetail diff --git a/modules/webapp/src/main/elm/Comp/Basic.elm b/modules/webapp/src/main/elm/Comp/Basic.elm index a7f6874c..03441c5b 100644 --- a/modules/webapp/src/main/elm/Comp/Basic.elm +++ b/modules/webapp/src/main/elm/Comp/Basic.elm @@ -207,7 +207,7 @@ loadingDimmer : { label : String, active : Bool } -> Html msg loadingDimmer cfg = let content = - div [ class "text-gray-200" ] + div [ class "text-gray-200 " ] [ i [ class "fa fa-circle-notch animate-spin" ] [] , span [ class "ml-2" ] [ text cfg.label diff --git a/modules/webapp/src/main/elm/Comp/BookmarkChooser.elm b/modules/webapp/src/main/elm/Comp/BookmarkChooser.elm index d888a65d..26eb6069 100644 --- a/modules/webapp/src/main/elm/Comp/BookmarkChooser.elm +++ b/modules/webapp/src/main/elm/Comp/BookmarkChooser.elm @@ -16,6 +16,7 @@ module Comp.BookmarkChooser exposing , isEmptySelection , update , view + , viewWith ) import Api.Model.BookmarkedQuery exposing (BookmarkedQuery) @@ -114,33 +115,43 @@ update msg model current = --- View -view : Texts -> Model -> Selection -> Html Msg -view texts model selection = +type alias ViewSettings = + { showUser : Bool + , showCollective : Bool + , showShares : Bool + } + + +viewWith : ViewSettings -> Texts -> Model -> Selection -> Html Msg +viewWith cfg texts model selection = let ( user, coll ) = List.partition .personal model.all.bookmarks in div [ class "flex flex-col" ] - [ userBookmarks texts user selection - , collBookmarks texts coll selection - , shares texts model selection + [ userBookmarks cfg.showUser texts user selection + , collBookmarks cfg.showCollective texts coll selection + , shares cfg.showShares texts model selection ] +view : Texts -> Model -> Selection -> Html Msg +view = + viewWith { showUser = True, showCollective = True, showShares = True } + + titleDiv : String -> Html msg titleDiv label = div [ class "text-sm opacity-75 py-0.5 italic" ] [ text label - - --, text " ──" ] -userBookmarks : Texts -> List BookmarkedQuery -> Selection -> Html Msg -userBookmarks texts model sel = +userBookmarks : Bool -> Texts -> List BookmarkedQuery -> Selection -> Html Msg +userBookmarks visible texts model sel = div [ class "mb-2" - , classList [ ( "hidden", model == [] ) ] + , classList [ ( "hidden", model == [] || not visible ) ] ] [ titleDiv texts.userLabel , div [ class "flex flex-col space-y-2 md:space-y-1" ] @@ -148,11 +159,11 @@ userBookmarks texts model sel = ] -collBookmarks : Texts -> List BookmarkedQuery -> Selection -> Html Msg -collBookmarks texts model sel = +collBookmarks : Bool -> Texts -> List BookmarkedQuery -> Selection -> Html Msg +collBookmarks visible texts model sel = div [ class "mb-2" - , classList [ ( "hidden", [] == model ) ] + , classList [ ( "hidden", [] == model || not visible ) ] ] [ titleDiv texts.collectiveLabel , div [ class "flex flex-col space-y-2 md:space-y-1" ] @@ -160,15 +171,15 @@ collBookmarks texts model sel = ] -shares : Texts -> Model -> Selection -> Html Msg -shares texts model sel = +shares : Bool -> Texts -> Model -> Selection -> Html Msg +shares visible texts model sel = let bms = List.map shareToBookmark model.all.shares in div [ class "" - , classList [ ( "hidden", List.isEmpty bms ) ] + , classList [ ( "hidden", List.isEmpty bms || not visible ) ] ] [ titleDiv texts.shareLabel , div [ class "flex flex-col space-y-2 md:space-y-1" ] diff --git a/modules/webapp/src/main/elm/Comp/BoxEdit.elm b/modules/webapp/src/main/elm/Comp/BoxEdit.elm new file mode 100644 index 00000000..ed83f3c2 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BoxEdit.elm @@ -0,0 +1,474 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.BoxEdit exposing + ( BoxAction(..) + , Model + , Msg + , UpdateResult + , init + , update + , view + ) + +import Comp.Basic as B +import Comp.BoxMessageEdit +import Comp.BoxQueryEdit +import Comp.BoxStatsEdit +import Comp.BoxUploadEdit +import Comp.FixedDropdown +import Comp.MenuBar as MB +import Data.Box exposing (Box) +import Data.BoxContent exposing (BoxContent(..)) +import Data.DropdownStyle as DS +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html, div, i, input, label, text) +import Html.Attributes exposing (class, classList, placeholder, type_, value) +import Html.Events exposing (onInput, onMouseEnter, onMouseLeave) +import Messages.Comp.BoxEdit exposing (Texts) +import Styles as S + + +type alias Model = + { box : Box + , content : ContentModel + , colspanModel : Comp.FixedDropdown.Model Int + , focus : Bool + , deleteRequested : Bool + } + + +type ContentModel + = ContentMessage Comp.BoxMessageEdit.Model + | ContentQuery Comp.BoxQueryEdit.Model + | ContentStats Comp.BoxStatsEdit.Model + | ContentUpload Comp.BoxUploadEdit.Model + + +type Msg + = ToggleVisible + | ToggleDecoration + | SetName String + | ColspanMsg (Comp.FixedDropdown.Msg Int) + | MessageMsg Comp.BoxMessageEdit.Msg + | UploadMsg Comp.BoxUploadEdit.Msg + | QueryMsg Comp.BoxQueryEdit.Msg + | StatsMsg Comp.BoxStatsEdit.Msg + | SetFocus Bool + | RequestDelete + | DeleteBox + | CancelDelete + | MoveLeft + | MoveRight + + +init : Flags -> Box -> ( Model, Cmd Msg, Sub Msg ) +init flags box = + let + ( cm, cc, cs ) = + contentInit flags box.content + in + ( { box = box + , content = cm + , colspanModel = Comp.FixedDropdown.init [ 1, 2, 3, 4, 5 ] + , focus = False + , deleteRequested = False + } + , cc + , cs + ) + + +contentInit : Flags -> BoxContent -> ( ContentModel, Cmd Msg, Sub Msg ) +contentInit flags content = + case content of + BoxMessage data -> + ( ContentMessage (Comp.BoxMessageEdit.init data), Cmd.none, Sub.none ) + + BoxUpload data -> + let + ( um, uc ) = + Comp.BoxUploadEdit.init flags data + in + ( ContentUpload um, Cmd.map UploadMsg uc, Sub.none ) + + BoxQuery data -> + let + ( qm, qc, qs ) = + Comp.BoxQueryEdit.init flags data + in + ( ContentQuery qm, Cmd.map QueryMsg qc, Sub.map QueryMsg qs ) + + BoxStats data -> + let + ( qm, qc, qs ) = + Comp.BoxStatsEdit.init flags data + in + ( ContentStats qm, Cmd.map StatsMsg qc, Sub.map StatsMsg qs ) + + + +--- Update + + +type BoxAction + = BoxNoAction + | BoxMoveLeft + | BoxMoveRight + | BoxDelete + + +type alias UpdateResult = + { model : Model + , cmd : Cmd Msg + , sub : Sub Msg + , action : BoxAction + } + + +update : Flags -> Msg -> Model -> UpdateResult +update flags msg model = + case msg of + MessageMsg lm -> + case model.content of + ContentMessage m -> + let + ( mm, data ) = + Comp.BoxMessageEdit.update lm m + + boxn = + model.box + + box_ = + { boxn | content = BoxMessage data } + in + { model = { model | content = ContentMessage mm, box = box_ } + , cmd = Cmd.none + , sub = Sub.none + , action = BoxNoAction + } + + _ -> + unit model + + UploadMsg lm -> + case model.content of + ContentUpload m -> + let + ( um, data ) = + Comp.BoxUploadEdit.update lm m + + boxn = + model.box + + box_ = + { boxn | content = BoxUpload data } + in + { model = { model | content = ContentUpload um, box = box_ } + , cmd = Cmd.none + , sub = Sub.none + , action = BoxNoAction + } + + _ -> + unit model + + QueryMsg lm -> + case model.content of + ContentQuery m -> + let + result = + Comp.BoxQueryEdit.update flags lm m + + boxn = + model.box + + box_ = + { boxn | content = BoxQuery result.data } + in + { model = { model | content = ContentQuery result.model, box = box_ } + , cmd = Cmd.map QueryMsg result.cmd + , sub = Sub.map QueryMsg result.sub + , action = BoxNoAction + } + + _ -> + unit model + + StatsMsg lm -> + case model.content of + ContentStats m -> + let + result = + Comp.BoxStatsEdit.update flags lm m + + boxn = + model.box + + box_ = + { boxn | content = BoxStats result.data } + in + { model = { model | content = ContentStats result.model, box = box_ } + , cmd = Cmd.map StatsMsg result.cmd + , sub = Sub.map StatsMsg result.sub + , action = BoxNoAction + } + + _ -> + unit model + + ColspanMsg lm -> + let + ( cm, num ) = + Comp.FixedDropdown.update lm model.colspanModel + + boxn = + model.box + + box_ = + { boxn | colspan = Maybe.withDefault boxn.colspan num } + in + unit { model | box = box_, colspanModel = cm } + + ToggleVisible -> + let + box = + model.box + + box_ = + { box | visible = not box.visible } + in + unit { model | box = box_ } + + ToggleDecoration -> + let + box = + model.box + + box_ = + { box | decoration = not box.decoration } + in + unit { model | box = box_ } + + SetName name -> + let + box = + model.box + + box_ = + { box | name = name } + in + unit { model | box = box_ } + + SetFocus flag -> + unit { model | focus = flag } + + RequestDelete -> + unit { model | deleteRequested = True } + + DeleteBox -> + UpdateResult model Cmd.none Sub.none BoxDelete + + CancelDelete -> + unit { model | deleteRequested = False } + + MoveLeft -> + UpdateResult model Cmd.none Sub.none BoxMoveLeft + + MoveRight -> + UpdateResult model Cmd.none Sub.none BoxMoveRight + + +unit : Model -> UpdateResult +unit model = + UpdateResult model Cmd.none Sub.none BoxNoAction + + + +--- View + + +view : Texts -> Flags -> UiSettings -> Model -> Html Msg +view texts flags settings model = + div + [ class (S.box ++ "rounded md:relative") + , class " h-full" + , classList [ ( "ring ring-opacity-50 ring-blue-600 dark:ring-sky-600", model.focus ) ] + , onMouseEnter (SetFocus True) + , onMouseLeave (SetFocus False) + ] + [ B.contentDimmer model.deleteRequested + (div [ class "flex flex-col" ] + [ div [ class "text-xl" ] + [ i [ class "fa fa-info-circle mr-2" ] [] + , text texts.reallyDeleteBox + ] + , div [ class "mt-4 flex flex-row items-center space-x-2" ] + [ MB.viewItem <| + MB.DeleteButton + { tagger = DeleteBox + , title = "" + , label = texts.basics.yes + , icon = Just "fa fa-check" + } + , MB.viewItem <| + MB.SecondaryButton + { tagger = CancelDelete + , title = "" + , label = texts.basics.no + , icon = Just "fa fa-times" + } + ] + ] + ) + , boxHeader texts model + , formHeader (texts.boxContent.forContent model.box.content) + , div [ class "mb-4 pl-2" ] + [ metaForm texts flags model + ] + , formHeader texts.contentProperties + , div [ class "pl-4 pr-2 py-2 h-5/6" ] + [ boxContent texts flags settings model + ] + ] + + +formHeader : String -> Html msg +formHeader heading = + div + [ class "mx-2 border-b dark:border-slate-500 text-lg mt-1" + ] + [ text heading + ] + + +metaForm : Texts -> Flags -> Model -> Html Msg +metaForm texts _ model = + let + colspanCfg = + { display = String.fromInt + , icon = \_ -> Nothing + , selectPlaceholder = "" + , style = DS.mainStyle + } + in + div [ class "my-1 px-2 " ] + [ div [] + [ label [ class S.inputLabel ] + [ text texts.basics.name + ] + , input + [ type_ "text" + , placeholder texts.namePlaceholder + , class S.textInput + , value model.box.name + , onInput SetName + ] + [] + ] + , div [ class "mt-1" ] + [ MB.viewItem <| + MB.Checkbox + { tagger = \_ -> ToggleVisible + , label = texts.visible + , value = model.box.visible + , id = "" + } + ] + , div [ class "mt-1" ] + [ MB.viewItem <| + MB.Checkbox + { tagger = \_ -> ToggleDecoration + , label = texts.decorations + , value = model.box.decoration + , id = "" + } + ] + , div [ class "mt-1" ] + [ label [ class S.inputLabel ] + [ text texts.colspan ] + , Html.map ColspanMsg + (Comp.FixedDropdown.viewStyled2 + colspanCfg + False + (Just model.box.colspan) + model.colspanModel + ) + ] + ] + + +boxHeader : Texts -> Model -> Html Msg +boxHeader texts model = + div + [ class "flex flex-row py-1 bg-blue-50 dark:bg-slate-700 rounded-t" + ] + [ div [ class "flex flex-row items-center text-lg tracking-medium italic px-2" ] + [ i + [ class (Data.Box.boxIcon model.box) + , class "mr-2" + ] + [] + , text model.box.name + ] + , div [ class "flex flex-grow justify-end pr-1" ] + [ MB.viewItem <| + MB.CustomButton + { tagger = MoveLeft + , title = texts.moveToLeft + , label = "" + , icon = Just "fa fa-arrow-left" + , inputClass = + [ ( S.secondaryBasicButton, True ) + , ( "text-xs", True ) + ] + } + , MB.viewItem <| + MB.CustomButton + { tagger = MoveRight + , title = texts.moveToRight + , label = "" + , icon = Just "fa fa-arrow-right" + , inputClass = + [ ( S.secondaryBasicButton, True ) + , ( "text-xs mr-3", True ) + ] + } + , MB.viewItem <| + MB.CustomButton + { tagger = RequestDelete + , title = texts.deleteBox + , label = "" + , icon = Just "fa fa-trash" + , inputClass = + [ ( S.deleteButton, True ) + , ( "text-xs", True ) + ] + } + ] + ] + + +boxContent : Texts -> Flags -> UiSettings -> Model -> Html Msg +boxContent texts flags settings model = + case model.content of + ContentMessage m -> + Html.map MessageMsg + (Comp.BoxMessageEdit.view texts.messageEdit m) + + ContentUpload m -> + Html.map UploadMsg + (Comp.BoxUploadEdit.view texts.uploadEdit m) + + ContentQuery m -> + Html.map QueryMsg + (Comp.BoxQueryEdit.view texts.queryEdit settings m) + + ContentStats m -> + Html.map StatsMsg + (Comp.BoxStatsEdit.view texts.statsEdit settings m) diff --git a/modules/webapp/src/main/elm/Comp/BoxMessageEdit.elm b/modules/webapp/src/main/elm/Comp/BoxMessageEdit.elm new file mode 100644 index 00000000..f403c1d2 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BoxMessageEdit.elm @@ -0,0 +1,99 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.BoxMessageEdit exposing (Model, Msg, init, update, view) + +import Data.BoxContent exposing (MessageData) +import Html exposing (Html, div, input, label, text, textarea) +import Html.Attributes exposing (autocomplete, class, name, placeholder, type_, value) +import Html.Events exposing (onInput) +import Messages.Comp.BoxMessageEdit exposing (Texts) +import Styles as S + + +type alias Model = + { data : MessageData + } + + +type Msg + = SetTitle String + | SetBody String + + +init : MessageData -> Model +init data = + { data = data + } + + + +--- Update + + +update : Msg -> Model -> ( Model, MessageData ) +update msg model = + case msg of + SetTitle str -> + let + data = + model.data + + data_ = + { data | title = str } + in + ( { model | data = data_ }, data_ ) + + SetBody str -> + let + data = + model.data + + data_ = + { data | body = str } + in + ( { model | data = data_ }, data_ ) + + + +--- View + + +view : Texts -> Model -> Html Msg +view texts model = + div [] + [ div [] + [ label [ class S.inputLabel ] + [ text texts.titleLabel + ] + , input + [ type_ "text" + , name "message-title" + , autocomplete False + , onInput SetTitle + , value model.data.title + , placeholder texts.titlePlaceholder + , class S.textInput + ] + [] + ] + , div [ class "mt-2" ] + [ label [ class S.inputLabel ] + [ text texts.bodyLabel + ] + , textarea + [ value model.data.body + , onInput SetBody + , class S.textAreaInput + , placeholder texts.bodyPlaceholder + ] + [] + ] + , div [ class "opacity-75 text-sm mt-1" ] + [ text texts.infoText + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/BoxQueryEdit.elm b/modules/webapp/src/main/elm/Comp/BoxQueryEdit.elm new file mode 100644 index 00000000..256f4c43 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BoxQueryEdit.elm @@ -0,0 +1,197 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.BoxQueryEdit exposing (..) + +import Comp.BoxSearchQueryInput +import Comp.IntField +import Comp.ItemColumnDropdown +import Comp.MenuBar as MB +import Data.Bookmarks +import Data.BoxContent exposing (QueryData, SearchQuery(..)) +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html, div, label, text) +import Html.Attributes exposing (class) +import Messages.Comp.BoxQueryEdit exposing (Texts) +import Styles as S + + +type alias Model = + { data : QueryData + , searchQueryModel : Comp.BoxSearchQueryInput.Model + , limitModel : Comp.IntField.Model + , limitValue : Maybe Int + , columnModel : Comp.ItemColumnDropdown.Model + } + + +type Msg + = SearchQueryMsg Comp.BoxSearchQueryInput.Msg + | LimitMsg Comp.IntField.Msg + | ColumnMsg Comp.ItemColumnDropdown.Msg + | ToggleColumnHeaders + + +init : Flags -> QueryData -> ( Model, Cmd Msg, Sub Msg ) +init flags data = + let + ( qm, qc, qs ) = + Comp.BoxSearchQueryInput.init flags data.query Data.Bookmarks.empty + + emptyModel = + { data = data + , searchQueryModel = qm + , limitModel = Comp.IntField.init (Just 1) Nothing False + , limitValue = Just data.limit + , columnModel = Comp.ItemColumnDropdown.init data.columns + } + in + ( emptyModel, Cmd.map SearchQueryMsg qc, Sub.map SearchQueryMsg qs ) + + + +--- Update + + +type alias UpdateResult = + { model : Model + , cmd : Cmd Msg + , sub : Sub Msg + , data : QueryData + } + + +update : Flags -> Msg -> Model -> UpdateResult +update flags msg model = + case msg of + SearchQueryMsg lm -> + let + result = + Comp.BoxSearchQueryInput.update flags lm model.searchQueryModel + + setData data = + { data | query = Maybe.withDefault data.query result.query } + + nextModel = + withData setData { model | searchQueryModel = result.model } + in + { model = nextModel + , cmd = Cmd.map SearchQueryMsg result.cmd + , sub = Sub.map SearchQueryMsg result.sub + , data = nextModel.data + } + + LimitMsg lm -> + let + ( im, n ) = + Comp.IntField.update lm model.limitModel + + data = + model.data + + data_ = + case n of + Just num -> + { data | limit = num } + + Nothing -> + data + in + { model = { model | limitModel = im, limitValue = n, data = data_ } + , cmd = Cmd.none + , sub = Sub.none + , data = data_ + } + + ColumnMsg lm -> + let + ( cm, cc ) = + Comp.ItemColumnDropdown.update lm model.columnModel + + selection = + Comp.ItemColumnDropdown.getSelected cm + + data = + model.data + + data_ = + { data | columns = selection } + in + { model = { model | columnModel = cm, data = data_ } + , cmd = Cmd.map ColumnMsg cc + , sub = Sub.none + , data = data_ + } + + ToggleColumnHeaders -> + let + data = + model.data + + data_ = + { data | showHeaders = not data.showHeaders } + in + { model = { model | data = data_ } + , cmd = Cmd.none + , sub = Sub.none + , data = data_ + } + + +unit : Model -> UpdateResult +unit model = + { model = model + , cmd = Cmd.none + , sub = Sub.none + , data = model.data + } + + +withData : (QueryData -> QueryData) -> Model -> Model +withData modify model = + { model | data = modify model.data } + + + +--- View + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts settings model = + let + limitSettings = + { label = "Limit" + , info = "Show this many results." + , number = model.limitValue + , classes = "" + } + in + div [] + [ Html.map SearchQueryMsg + (Comp.BoxSearchQueryInput.view texts.searchQuery settings model.searchQueryModel) + , div [ class "mt-2" ] + [ Html.map LimitMsg + (Comp.IntField.view limitSettings model.limitModel) + ] + , div [ class "mt-2" ] + [ label [ class S.inputLabel ] + [ text "Columns" + ] + , Html.map ColumnMsg + (Comp.ItemColumnDropdown.view texts.columnDropdown settings model.columnModel) + ] + , div [ class "mt-2" ] + [ MB.viewItem <| + MB.Checkbox + { tagger = \_ -> ToggleColumnHeaders + , label = texts.showColumnHeaders + , value = model.data.showHeaders + , id = "" + } + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/BoxQueryView.elm b/modules/webapp/src/main/elm/Comp/BoxQueryView.elm new file mode 100644 index 00000000..e46c6c41 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BoxQueryView.elm @@ -0,0 +1,212 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.BoxQueryView exposing (Model, Msg, init, reloadData, update, view) + +import Api +import Api.Model.ItemLight exposing (ItemLight) +import Api.Model.ItemLightList exposing (ItemLightList) +import Api.Model.ItemQuery exposing (ItemQuery) +import Comp.Basic +import Comp.ItemColumnView +import Data.BoxContent exposing (QueryData, SearchQuery(..)) +import Data.Flags exposing (Flags) +import Data.ItemColumn as IC exposing (ItemColumn) +import Data.Items +import Data.SearchMode +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html, a, div, i, table, tbody, td, text, th, thead, tr) +import Html.Attributes exposing (class, classList) +import Http +import Messages.Comp.BoxQueryView exposing (Texts) +import Page exposing (Page(..)) +import Styles + + +type alias Model = + { results : ViewResult + , meta : QueryData + } + + +type ViewResult + = Loading + | Loaded ItemLightList + | Failed Http.Error + + +type Msg + = ItemsResp (Result Http.Error ItemLightList) + | ReloadData + + +init : Flags -> QueryData -> ( Model, Cmd Msg ) +init flags data = + ( { results = Loading + , meta = data + } + , dataCmd flags data + ) + + +reloadData : Msg +reloadData = + ReloadData + + + +--- Update + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Bool ) +update flags msg model = + case msg of + ItemsResp (Ok list) -> + ( { model | results = Loaded list }, Cmd.none, False ) + + ItemsResp (Err err) -> + ( { model | results = Failed err }, Cmd.none, False ) + + ReloadData -> + ( model, dataCmd flags model.meta, True ) + + + +--- View + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts settings model = + case model.results of + Loading -> + div [ class "h-24 " ] + [ Comp.Basic.loadingDimmer + { label = "" + , active = True + } + ] + + Failed err -> + div + [ class "py-4" + , class Styles.errorMessage + ] + [ text texts.errorOccurred + , text ": " + , text (texts.httpError err) + ] + + Loaded list -> + if list.groups == [] then + viewEmpty texts + + else + viewItems texts settings model.meta list + + +viewItems : Texts -> UiSettings -> QueryData -> ItemLightList -> Html Msg +viewItems texts settings meta list = + let + items = + Data.Items.flatten list + in + table [ class "w-full divide-y divide-y-2 dark:divide-slate-500" ] + (viewItemHead texts meta ++ [ tbody [ class "divide-y divide-dotted dark:divide-slate-500" ] <| List.map (viewItemRow texts settings meta) items ]) + + +viewItemHead : Texts -> QueryData -> List (Html Msg) +viewItemHead texts meta = + let + ( col1, cols ) = + getColumns meta + in + if not meta.showHeaders then + [] + + else + [ thead [] + [ tr [] + (List.map texts.itemColumn.header (col1 :: cols) + |> List.map (\n -> th [ class "text-left text-sm" ] [ text n ]) + ) + ] + ] + + +viewItemRow : Texts -> UiSettings -> QueryData -> ItemLight -> Html Msg +viewItemRow texts settings meta item = + let + ( col1, cols ) = + getColumns meta + + render col = + Comp.ItemColumnView.renderDiv texts.templateCtx settings col [ class "flex flex-row space-x-1" ] item + + td1 = + td [ class "py-2 px-1" ] + [ a + [ class Styles.link + , Page.href (ItemDetailPage item.id) + ] + [ render col1 + ] + ] + + tdRem index col = + td + [ class "py-2 px-1" + , classList [ ( "hidden md:table-cell", index > 1 ) ] + ] + [ render col + ] + in + tr [] + (td1 :: List.indexedMap tdRem cols) + + +viewEmpty : Texts -> Html Msg +viewEmpty texts = + div [ class "flex justify-center items-center h-full" ] + [ div [ class "px-4 py-4 text-center align-middle text-lg" ] + [ i [ class "fa fa-smile font-thin mr-2" ] [] + , text texts.noResults + ] + ] + + + +--- Helpers + + +getColumns : QueryData -> ( ItemColumn, List ItemColumn ) +getColumns meta = + case meta.columns of + x :: xs -> + ( x, xs ) + + [] -> + ( IC.Name, [ IC.Correspondent, IC.DateShort ] ) + + +mkQuery : String -> QueryData -> ItemQuery +mkQuery q meta = + { query = q + , limit = Just meta.limit + , offset = Nothing + , searchMode = Just <| Data.SearchMode.asString Data.SearchMode.Normal + , withDetails = Just meta.details + } + + +dataCmd : Flags -> QueryData -> Cmd Msg +dataCmd flags data = + case data.query of + SearchQueryString q -> + Api.itemSearch flags (mkQuery q data) ItemsResp + + SearchQueryBookmark bmId -> + Api.itemSearchBookmark flags (mkQuery bmId data) ItemsResp diff --git a/modules/webapp/src/main/elm/Comp/BoxSearchQueryInput.elm b/modules/webapp/src/main/elm/Comp/BoxSearchQueryInput.elm new file mode 100644 index 00000000..217c0bdc --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BoxSearchQueryInput.elm @@ -0,0 +1,299 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.BoxSearchQueryInput exposing + ( Model + , Msg + , UpdateResult + , init + , switchToBookmark + , switchToQuery + , toSearchQuery + , update + , view + ) + +import Api +import Comp.BookmarkDropdown +import Comp.PowerSearchInput +import Data.Bookmarks exposing (AllBookmarks) +import Data.BoxContent exposing (SearchQuery(..)) +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html, div, input, label, span, text) +import Html.Attributes exposing (checked, class, type_) +import Html.Events exposing (onCheck) +import Http +import Messages.Comp.BoxSearchQueryInput exposing (Texts) +import Styles as S + + +type alias Model = + { queryModel : QueryModel + , allBookmarks : AllBookmarks + } + + +type QueryModel + = Search Comp.PowerSearchInput.Model + | Bookmark Comp.BookmarkDropdown.Model + + +toSearchQuery : Model -> Maybe SearchQuery +toSearchQuery model = + case model.queryModel of + Search pm -> + let + qstr = + Maybe.withDefault "" pm.input + in + if qstr == "" || Comp.PowerSearchInput.isValid pm then + Just (SearchQueryString qstr) + + else + Nothing + + Bookmark bm -> + Comp.BookmarkDropdown.getSelectedId bm + |> Maybe.map SearchQueryBookmark + + +type Msg + = GetBookmarksResp (Result Http.Error AllBookmarks) + | BookmarkMsg Comp.BookmarkDropdown.Msg + | PowerSearchMsg Comp.PowerSearchInput.Msg + | SwitchQueryBookmark + | SwitchQuerySearch + + +switchToBookmark : Msg +switchToBookmark = + SwitchQueryBookmark + + +switchToQuery : Msg +switchToQuery = + SwitchQuerySearch + + +init : Flags -> SearchQuery -> AllBookmarks -> ( Model, Cmd Msg, Sub Msg ) +init flags query bookmarks = + let + emptyModel = + { queryModel = Search Comp.PowerSearchInput.init + , allBookmarks = bookmarks + } + in + case query of + SearchQueryBookmark id -> + initQueryBookmark flags emptyModel (Just id) + + SearchQueryString qstr -> + initQuerySearch emptyModel qstr + + +initQueryBookmark : Flags -> Model -> Maybe String -> ( Model, Cmd Msg, Sub Msg ) +initQueryBookmark flags model bookmarkId = + ( { model + | queryModel = + Bookmark + (Comp.BookmarkDropdown.initWith model.allBookmarks bookmarkId) + } + , if model.allBookmarks == Data.Bookmarks.empty then + Api.getBookmarks flags GetBookmarksResp + + else + Cmd.none + , Sub.none + ) + + +initQuerySearch : Model -> String -> ( Model, Cmd Msg, Sub Msg ) +initQuerySearch model qstr = + let + ( qm, qc, qs ) = + Comp.PowerSearchInput.initWith qstr + in + ( { model | queryModel = Search qm } + , Cmd.map PowerSearchMsg qc + , Sub.map PowerSearchMsg qs + ) + + + +--- Update + + +type alias UpdateResult = + { model : Model + , cmd : Cmd Msg + , sub : Sub Msg + , query : Maybe SearchQuery + } + + +update : Flags -> Msg -> Model -> UpdateResult +update flags msg model = + case msg of + GetBookmarksResp (Ok list) -> + let + bmId = + case model.queryModel of + Bookmark bm -> + Comp.BookmarkDropdown.getSelectedId bm + + Search _ -> + Nothing + + nm = + { model | allBookmarks = list } + in + case model.queryModel of + Bookmark _ -> + update flags + SwitchQueryBookmark + { nm + | queryModel = + Bookmark (Comp.BookmarkDropdown.initWith model.allBookmarks bmId) + } + + Search _ -> + unit nm + + GetBookmarksResp (Err _) -> + unit model + + BookmarkMsg lm -> + case model.queryModel of + Bookmark m -> + let + ( bm, bc ) = + Comp.BookmarkDropdown.update lm m + + nextModel = + { model | queryModel = Bookmark bm } + in + { model = nextModel + , cmd = Cmd.map BookmarkMsg bc + , sub = Sub.none + , query = toSearchQuery nextModel + } + + _ -> + unit model + + PowerSearchMsg lm -> + case model.queryModel of + Search m -> + let + result = + Comp.PowerSearchInput.update lm m + + nextModel = + { model | queryModel = Search result.model } + in + { model = nextModel + , cmd = Cmd.map PowerSearchMsg result.cmd + , sub = Sub.map PowerSearchMsg result.subs + , query = toSearchQuery nextModel + } + + _ -> + unit model + + SwitchQueryBookmark -> + let + selected = + case toSearchQuery model of + Just (SearchQueryBookmark id) -> + Just id + + _ -> + Nothing + + ( m, c, s ) = + initQueryBookmark flags model selected + in + UpdateResult m c s (toSearchQuery m) + + SwitchQuerySearch -> + let + qstr = + case toSearchQuery model of + Just (SearchQueryString q) -> + q + + _ -> + "" + + ( m, c, s ) = + initQuerySearch model qstr + in + UpdateResult m c s (toSearchQuery m) + + +unit : Model -> UpdateResult +unit model = + UpdateResult model Cmd.none Sub.none Nothing + + + +--- View + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts settings model = + let + ( isBookmark, isQuery ) = + case model.queryModel of + Bookmark _ -> + ( True, False ) + + Search _ -> + ( False, True ) + + searchSettings = + { placeholder = texts.searchPlaceholder + , extraAttrs = [] + } + in + div [ class "flex flex-col" ] + [ div [ class "flex flex-row space-x-4" ] + [ label [ class "inline-flex items-center" ] + [ input + [ type_ "radio" + , checked isBookmark + , onCheck (\_ -> SwitchQueryBookmark) + , class S.radioInput + ] + [] + , span [ class "ml-2" ] [ text texts.switchToBookmark ] + ] + , label [ class "inline-flex items-center" ] + [ input + [ type_ "radio" + , checked isQuery + , onCheck (\_ -> SwitchQuerySearch) + , class S.radioInput + ] + [] + , span [ class "ml-2" ] [ text texts.switchToQuery ] + ] + ] + , case model.queryModel of + Bookmark m -> + Html.map BookmarkMsg + (Comp.BookmarkDropdown.view texts.bookmarkDropdown settings m) + + Search m -> + div [ class "relative" ] + [ Html.map PowerSearchMsg + (Comp.PowerSearchInput.viewInput searchSettings m) + , Html.map PowerSearchMsg + (Comp.PowerSearchInput.viewResult [] m) + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/BoxStatsEdit.elm b/modules/webapp/src/main/elm/Comp/BoxStatsEdit.elm new file mode 100644 index 00000000..7a24259e --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BoxStatsEdit.elm @@ -0,0 +1,202 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.BoxStatsEdit exposing (..) + +import Comp.BoxSearchQueryInput +import Comp.FixedDropdown +import Comp.MenuBar as MB +import Data.Bookmarks +import Data.BoxContent exposing (QueryData, SearchQuery(..), StatsData, SummaryShow(..)) +import Data.DropdownStyle as DS +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html, div, label, span, text) +import Html.Attributes exposing (class) +import Messages.Comp.BoxStatsEdit exposing (Texts) +import Styles as S + + +type alias Model = + { data : StatsData + , searchQueryModel : Comp.BoxSearchQueryInput.Model + , showModel : Comp.FixedDropdown.Model SummaryShowLabel + , summaryShow : SummaryShow + } + + +type Msg + = SearchQueryMsg Comp.BoxSearchQueryInput.Msg + | ShowMsg (Comp.FixedDropdown.Msg SummaryShowLabel) + | ToggleItemCountVisible + + +type SummaryShowLabel + = ShowFields + | ShowGeneric + + +init : Flags -> StatsData -> ( Model, Cmd Msg, Sub Msg ) +init flags data = + let + ( qm, qc, qs ) = + Comp.BoxSearchQueryInput.init flags data.query Data.Bookmarks.empty + + emptyModel = + { data = data + , searchQueryModel = qm + , showModel = + Comp.FixedDropdown.init + [ ShowFields, ShowGeneric ] + , summaryShow = data.show + } + in + ( emptyModel, Cmd.map SearchQueryMsg qc, Sub.map SearchQueryMsg qs ) + + + +--- Update + + +type alias UpdateResult = + { model : Model + , cmd : Cmd Msg + , sub : Sub Msg + , data : StatsData + } + + +update : Flags -> Msg -> Model -> UpdateResult +update flags msg model = + case msg of + SearchQueryMsg lm -> + let + result = + Comp.BoxSearchQueryInput.update flags lm model.searchQueryModel + + setData data = + { data | query = Maybe.withDefault data.query result.query } + + nextModel = + withData setData { model | searchQueryModel = result.model } + in + { model = nextModel + , cmd = Cmd.map SearchQueryMsg result.cmd + , sub = Sub.map SearchQueryMsg result.sub + , data = nextModel.data + } + + ShowMsg lm -> + let + ( mm, sel ) = + Comp.FixedDropdown.update lm model.showModel + + nextShow = + case ( model.summaryShow, sel ) of + ( SummaryShowFields _, Just ShowGeneric ) -> + SummaryShowGeneral + + ( SummaryShowGeneral, Just ShowFields ) -> + SummaryShowFields False + + _ -> + model.summaryShow + + data = + model.data + + data_ = + { data | show = nextShow } + in + unit { model | showModel = mm, summaryShow = nextShow, data = data_ } + + ToggleItemCountVisible -> + let + nextShow = + case model.summaryShow of + SummaryShowFields flag -> + SummaryShowFields (not flag) + + _ -> + model.summaryShow + + data = + model.data + + data_ = + { data | show = nextShow } + in + unit { model | summaryShow = nextShow, data = data_ } + + +unit : Model -> UpdateResult +unit model = + { model = model + , cmd = Cmd.none + , sub = Sub.none + , data = model.data + } + + +withData : (StatsData -> StatsData) -> Model -> Model +withData modify model = + { model | data = modify model.data } + + + +--- View + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts settings model = + let + showSettings = + { display = + \a -> + case a of + ShowFields -> + texts.fieldStatistics + + ShowGeneric -> + texts.basicNumbers + , icon = \_ -> Nothing + , selectPlaceholder = "" + , style = DS.mainStyle + } + + showLabel = + case model.summaryShow of + SummaryShowFields _ -> + ShowFields + + SummaryShowGeneral -> + ShowGeneric + in + div [] + [ Html.map SearchQueryMsg + (Comp.BoxSearchQueryInput.view texts.searchQuery settings model.searchQueryModel) + , div [ class "mt-2" ] + [ label [ class S.inputLabel ] + [ text texts.showLabel ] + , Html.map ShowMsg + (Comp.FixedDropdown.viewStyled2 showSettings False (Just showLabel) model.showModel) + ] + , div [ class "mt-2" ] + [ case model.summaryShow of + SummaryShowGeneral -> + span [ class "hidden" ] [] + + SummaryShowFields itemCountVisible -> + MB.viewItem <| + MB.Checkbox + { tagger = \_ -> ToggleItemCountVisible + , label = texts.showItemCount + , id = "" + , value = itemCountVisible + } + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/BoxStatsView.elm b/modules/webapp/src/main/elm/Comp/BoxStatsView.elm new file mode 100644 index 00000000..794ca492 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BoxStatsView.elm @@ -0,0 +1,186 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.BoxStatsView exposing (Model, Msg, init, reloadData, update, view) + +import Api +import Api.Model.ItemQuery exposing (ItemQuery) +import Api.Model.SearchStats exposing (SearchStats) +import Comp.Basic +import Comp.SearchStatsView +import Data.BoxContent exposing (SearchQuery(..), StatsData, SummaryShow(..)) +import Data.Flags exposing (Flags) +import Html exposing (Html, div, text) +import Html.Attributes exposing (class) +import Http +import Messages.Comp.BoxStatsView exposing (Texts) +import Styles +import Util.List + + +type alias Model = + { results : ViewResult + , meta : StatsData + } + + +type ViewResult + = Loading + | Loaded SearchStats + | Failed Http.Error + + +type Msg + = StatsResp (Result Http.Error SearchStats) + | ReloadData + + +init : Flags -> StatsData -> ( Model, Cmd Msg ) +init flags data = + ( { results = Loading + , meta = data + } + , dataCmd flags data + ) + + +reloadData : Msg +reloadData = + ReloadData + + + +--- Update + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Bool ) +update flags msg model = + case msg of + StatsResp (Ok stats) -> + ( { model | results = Loaded stats }, Cmd.none, False ) + + StatsResp (Err err) -> + ( { model | results = Failed err }, Cmd.none, False ) + + ReloadData -> + ( model, dataCmd flags model.meta, True ) + + + +--- View + + +view : Texts -> Model -> Html Msg +view texts model = + case model.results of + Loading -> + div [ class "h-24 " ] + [ Comp.Basic.loadingDimmer + { label = "" + , active = True + } + ] + + Failed err -> + div + [ class "py-4" + , class Styles.errorMessage + ] + [ text texts.errorOccurred + , text ": " + , text (texts.httpError err) + ] + + Loaded stats -> + viewStats texts model stats + + +viewStats : Texts -> Model -> SearchStats -> Html Msg +viewStats texts model stats = + case model.meta.show of + SummaryShowFields flag -> + Comp.SearchStatsView.view2 + texts.statsView + flag + "" + stats + + SummaryShowGeneral -> + viewGeneral texts stats + + +viewGeneral : Texts -> SearchStats -> Html Msg +viewGeneral texts stats = + let + tagCount = + List.length stats.tagCloud.items + + fieldCount = + List.length stats.fieldStats + + orgCount = + List.length stats.corrOrgStats + + persCount = + (stats.corrPersStats ++ stats.concPersStats) + |> List.map (.ref >> .id) + |> Util.List.distinct + |> List.length + + equipCount = + List.length stats.concEquipStats + + mklabel name = + div [ class "py-0.5 text-lg" ] [ text name ] + + value num = + div [ class "py-0.5 font-mono text-lg" ] [ text <| String.fromInt num ] + in + div [ class "opacity-90" ] + [ div [ class "flex flex-row" ] + [ div [ class "flex flex-col mr-4" ] + [ mklabel texts.basics.items + , mklabel texts.basics.tags + , mklabel texts.basics.customFields + , mklabel texts.basics.organization + , mklabel texts.basics.person + , mklabel texts.basics.equipment + ] + , div [ class "flex flex-col" ] + [ value stats.count + , value tagCount + , value fieldCount + , value orgCount + , value persCount + , value equipCount + ] + ] + ] + + + +--- Helpers + + +mkQuery : String -> ItemQuery +mkQuery query = + { query = query + , limit = Nothing + , offset = Nothing + , searchMode = Nothing + , withDetails = Nothing + } + + +dataCmd : Flags -> StatsData -> Cmd Msg +dataCmd flags data = + case data.query of + SearchQueryString q -> + Api.itemSearchStats flags (mkQuery q) StatsResp + + SearchQueryBookmark bmId -> + Api.itemSearchStatsBookmark flags (mkQuery bmId) StatsResp diff --git a/modules/webapp/src/main/elm/Comp/BoxUploadEdit.elm b/modules/webapp/src/main/elm/Comp/BoxUploadEdit.elm new file mode 100644 index 00000000..8dd85bfe --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BoxUploadEdit.elm @@ -0,0 +1,113 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.BoxUploadEdit exposing (..) + +import Api +import Api.Model.Source exposing (Source) +import Api.Model.SourceList exposing (SourceList) +import Comp.BoxUploadView exposing (Msg) +import Comp.FixedDropdown +import Data.BoxContent exposing (UploadData) +import Data.DropdownStyle as DS +import Data.Flags exposing (Flags) +import Html exposing (Html, div, label, text) +import Html.Attributes exposing (class) +import Http +import Messages.Comp.BoxUploadEdit exposing (Texts) +import Styles as S + + +type alias Model = + { data : UploadData + , allSources : List Source + , sourceModel : Comp.FixedDropdown.Model Source + } + + +type Msg + = GetSourcesResp (Result Http.Error SourceList) + | SourceMsg (Comp.FixedDropdown.Msg Source) + + +init : Flags -> UploadData -> ( Model, Cmd Msg ) +init flags data = + ( { data = data + , allSources = [] + , sourceModel = Comp.FixedDropdown.init [] + } + , Api.getSources flags GetSourcesResp + ) + + + +--- Update + + +update : Msg -> Model -> ( Model, UploadData ) +update msg model = + case msg of + GetSourcesResp (Ok list) -> + let + all = + List.map .source list.items + |> List.filter .enabled + + dm = + Comp.FixedDropdown.init all + in + ( { model | allSources = all, sourceModel = dm } + , model.data + ) + + GetSourcesResp (Err _) -> + ( model, model.data ) + + SourceMsg lm -> + let + ( dm, sel ) = + Comp.FixedDropdown.update lm model.sourceModel + + ud = + model.data + + ud_ = + { ud | sourceId = Maybe.map .id sel } + in + ( { model | sourceModel = dm, data = ud_ }, ud_ ) + + + +--- View + + +view : Texts -> Model -> Html Msg +view texts model = + let + cfg = + { display = \s -> s.abbrev + , icon = \_ -> Nothing + , selectPlaceholder = texts.sourcePlaceholder + , style = DS.mainStyle + } + + selected = + List.filter (\e -> Just e.id == model.data.sourceId) model.allSources + |> List.head + in + div [] + [ div [] + [ label [ class S.inputLabel ] + [ text texts.sourceLabel + ] + , Html.map SourceMsg + (Comp.FixedDropdown.viewStyled2 cfg False selected model.sourceModel) + ] + , div [ class "mt-1 opacity-75 text-sm" ] + [ text texts.infoText + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/BoxUploadView.elm b/modules/webapp/src/main/elm/Comp/BoxUploadView.elm new file mode 100644 index 00000000..7c2ec5e7 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BoxUploadView.elm @@ -0,0 +1,70 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.BoxUploadView exposing (..) + +import Comp.UploadForm +import Data.BoxContent exposing (UploadData) +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html, div) +import Html.Attributes exposing (class) +import Messages.Comp.BoxUploadView exposing (Texts) + + +type alias Model = + { uploadForm : Comp.UploadForm.Model + , sourceId : Maybe String + } + + +type Msg + = UploadMsg Comp.UploadForm.Msg + + +init : UploadData -> Model +init data = + { uploadForm = Comp.UploadForm.init + , sourceId = data.sourceId + } + + + +--- Update + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +update flags msg model = + case msg of + UploadMsg lm -> + let + ( um, uc, us ) = + Comp.UploadForm.update model.sourceId flags lm model.uploadForm + in + ( { model | uploadForm = um } + , Cmd.map UploadMsg uc + , Sub.map UploadMsg us + ) + + + +--- View + + +view : Texts -> Flags -> UiSettings -> Model -> Html Msg +view texts flags settings model = + let + viewCfg = + { sourceId = model.sourceId + , showForm = False + , lightForm = True + } + in + div [ class "" ] + [ Html.map UploadMsg + (Comp.UploadForm.view texts.uploadForm viewCfg flags settings model.uploadForm) + ] diff --git a/modules/webapp/src/main/elm/Comp/BoxView.elm b/modules/webapp/src/main/elm/Comp/BoxView.elm new file mode 100644 index 00000000..9e508972 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BoxView.elm @@ -0,0 +1,247 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.BoxView exposing (..) + +import Comp.BoxQueryView +import Comp.BoxStatsView +import Comp.BoxUploadView +import Data.Box exposing (Box) +import Data.BoxContent exposing (BoxContent(..), MessageData) +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html, div, i, text) +import Html.Attributes exposing (class, classList) +import Markdown +import Messages.Comp.BoxView exposing (Texts) +import Styles as S + + +type alias Model = + { box : Box + , content : ContentModel + , reloading : Bool + } + + +type ContentModel + = ContentMessage Data.BoxContent.MessageData + | ContentUpload Comp.BoxUploadView.Model + | ContentQuery Comp.BoxQueryView.Model + | ContentStats Comp.BoxStatsView.Model + + +type Msg + = QueryMsg Comp.BoxQueryView.Msg + | StatsMsg Comp.BoxStatsView.Msg + | UploadMsg Comp.BoxUploadView.Msg + | ReloadData + + +init : Flags -> Box -> ( Model, Cmd Msg ) +init flags box = + let + ( cm, cc ) = + contentInit flags box.content + in + ( { box = box + , content = cm + , reloading = False + } + , cc + ) + + +reloadData : Msg +reloadData = + ReloadData + + +contentInit : Flags -> BoxContent -> ( ContentModel, Cmd Msg ) +contentInit flags content = + case content of + BoxMessage data -> + ( ContentMessage data, Cmd.none ) + + BoxUpload data -> + let + qm = + Comp.BoxUploadView.init data + in + ( ContentUpload qm, Cmd.none ) + + BoxQuery data -> + let + ( qm, qc ) = + Comp.BoxQueryView.init flags data + in + ( ContentQuery qm, Cmd.map QueryMsg qc ) + + BoxStats data -> + let + ( sm, sc ) = + Comp.BoxStatsView.init flags data + in + ( ContentStats sm, Cmd.map StatsMsg sc ) + + + +--- Update + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +update flags msg model = + case msg of + QueryMsg lm -> + case model.content of + ContentQuery qm -> + let + ( cm, cc, reloading ) = + Comp.BoxQueryView.update flags lm qm + in + ( { model | content = ContentQuery cm, reloading = reloading } + , Cmd.map QueryMsg cc + , Sub.none + ) + + _ -> + unit model + + StatsMsg lm -> + case model.content of + ContentStats qm -> + let + ( cm, cc, reloading ) = + Comp.BoxStatsView.update flags lm qm + in + ( { model | content = ContentStats cm, reloading = reloading } + , Cmd.map StatsMsg cc + , Sub.none + ) + + _ -> + unit model + + UploadMsg lm -> + case model.content of + ContentUpload qm -> + let + ( cm, cc, cs ) = + Comp.BoxUploadView.update flags lm qm + in + ( { model | content = ContentUpload cm } + , Cmd.map UploadMsg cc + , Sub.map UploadMsg cs + ) + + _ -> + unit model + + ReloadData -> + case model.content of + ContentQuery _ -> + update flags (QueryMsg Comp.BoxQueryView.reloadData) model + + ContentStats _ -> + update flags (StatsMsg Comp.BoxStatsView.reloadData) model + + _ -> + unit model + + +unit : Model -> ( Model, Cmd Msg, Sub Msg ) +unit model = + ( model, Cmd.none, Sub.none ) + + + +--- View + + +view : Texts -> Flags -> UiSettings -> Model -> Html Msg +view texts flags settings model = + div + [ classList [ ( S.box ++ "rounded", model.box.decoration ) ] + , class (spanStyle model.box) + , class "relative h-full" + , classList [ ( "hidden", not model.box.visible ) ] + ] + [ boxLoading model + , boxHeader model + , div [ class "px-2 py-1 h-5/6" ] + [ boxContent texts flags settings model + ] + ] + + +boxLoading : Model -> Html Msg +boxLoading model = + if not model.reloading then + div [ class "hidden" ] [] + + else + div [ class "absolute right-0 top-1 h-6 w-6" ] + [ i [ class "fa fa-spinner animate-spin" ] [] + ] + + +boxHeader : Model -> Html Msg +boxHeader model = + div + [ class "flex flex-row py-1 bg-blue-50 dark:bg-slate-700 rounded-t" + , classList [ ( "hidden", not model.box.decoration || model.box.name == "" ) ] + ] + [ div [ class "flex text-lg tracking-medium italic px-2" ] + [ text model.box.name + ] + ] + + +boxContent : Texts -> Flags -> UiSettings -> Model -> Html Msg +boxContent texts flags settings model = + case model.content of + ContentMessage m -> + messageContent m + + ContentUpload qm -> + Html.map UploadMsg + (Comp.BoxUploadView.view texts.uploadView flags settings qm) + + ContentQuery qm -> + Html.map QueryMsg + (Comp.BoxQueryView.view texts.queryView settings qm) + + ContentStats qm -> + Html.map StatsMsg + (Comp.BoxStatsView.view texts.statsView qm) + + +spanStyle : Box -> String +spanStyle box = + case box.colspan of + 1 -> + "" + + 2 -> + "col-span-1 md:col-span-2" + + 3 -> + "col-span-1 md:col-span-3" + + 4 -> + "col-span-1 md:col-span-4" + + _ -> + "col-span-1 md:col-span-5" + + +messageContent : MessageData -> Html msg +messageContent data = + div [ class "markdown-preview" ] + [ Markdown.toHtml [] data.title + , Markdown.toHtml [] data.body + ] diff --git a/modules/webapp/src/main/elm/Comp/DashboardEdit.elm b/modules/webapp/src/main/elm/Comp/DashboardEdit.elm new file mode 100644 index 00000000..e025079d --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/DashboardEdit.elm @@ -0,0 +1,512 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.DashboardEdit exposing (Model, Msg, getBoard, init, update, view, viewBox) + +import Comp.Basic as B +import Comp.BoxEdit +import Comp.FixedDropdown +import Comp.MenuBar as MB +import Data.AccountScope exposing (AccountScope) +import Data.Box exposing (Box) +import Data.Dashboard exposing (Dashboard) +import Data.DropdownStyle as DS +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Dict exposing (Dict) +import Html exposing (Html, div, i, input, label, span, text) +import Html.Attributes exposing (checked, class, classList, href, placeholder, type_, value) +import Html.Events exposing (onCheck, onClick, onInput) +import Html5.DragDrop as DD +import Messages.Comp.DashboardEdit exposing (Texts) +import Styles as S +import Util.Maybe + + +type alias Model = + { dashboard : Dashboard + , boxModels : Dict Int Comp.BoxEdit.Model + , nameValue : String + , columnsModel : Comp.FixedDropdown.Model Int + , columnsValue : Maybe Int + , gapModel : Comp.FixedDropdown.Model Int + , gapValue : Maybe Int + , defaultDashboard : Bool + , scope : AccountScope + , newBoxMenuOpen : Bool + , boxDragDrop : DD.Model Int Int + } + + +type Msg + = BoxMsg Int Comp.BoxEdit.Msg + | SetName String + | ColumnsMsg (Comp.FixedDropdown.Msg Int) + | GapMsg (Comp.FixedDropdown.Msg Int) + | ToggleNewBoxMenu + | SetScope AccountScope + | ToggleDefault + | PrependNew Box + | DragDropMsg (DD.Msg Int Int) + + +init : Flags -> Dashboard -> AccountScope -> Bool -> ( Model, Cmd Msg, Sub Msg ) +init flags db scope default = + let + ( boxModels, cmdsAndSubs ) = + List.map (Comp.BoxEdit.init flags) db.boxes + |> List.indexedMap + (\a -> + \( bm, bc, bs ) -> + ( bm, ( Cmd.map (BoxMsg a) bc, Sub.map (BoxMsg a) bs ) ) + ) + |> List.unzip + + ( cmds, subs ) = + List.unzip cmdsAndSubs + in + ( { dashboard = db + , nameValue = db.name + , columnsModel = Comp.FixedDropdown.init [ 1, 2, 3, 4, 5 ] + , columnsValue = Just db.columns + , gapModel = Comp.FixedDropdown.init (List.range 0 12) + , gapValue = Just db.gap + , defaultDashboard = default + , scope = scope + , newBoxMenuOpen = False + , boxModels = + List.indexedMap Tuple.pair boxModels + |> Dict.fromList + , boxDragDrop = DD.init + } + , Cmd.batch cmds + , Sub.batch subs + ) + + +getBoard : Model -> ( Dashboard, AccountScope, Bool ) +getBoard model = + ( model.dashboard, model.scope, model.defaultDashboard ) + + + +--- Update + + +type alias UpdateResult = + { model : Model + , cmd : Cmd Msg + , sub : Sub Msg + } + + +update : Flags -> Msg -> Model -> UpdateResult +update flags msg model = + case msg of + BoxMsg index lm -> + case Dict.get index model.boxModels of + Just bm -> + let + result = + Comp.BoxEdit.update flags lm bm + + newBoxes = + applyBoxAction index result.action <| + Dict.insert index result.model model.boxModels + + db = + model.dashboard + + db_ = + { db | boxes = List.map .box (Dict.values newBoxes) } + in + { model = { model | boxModels = newBoxes, dashboard = db_ } + , cmd = Cmd.map (BoxMsg index) result.cmd + , sub = Sub.map (BoxMsg index) result.sub + } + + Nothing -> + unit model + + SetName str -> + let + db = + model.dashboard + + db_ = + { db | name = String.trim str } + in + unit { model | dashboard = db_, nameValue = str } + + ColumnsMsg lm -> + let + ( cm, value ) = + Comp.FixedDropdown.update lm model.columnsModel + + db = + model.dashboard + + db_ = + { db | columns = Maybe.withDefault db.columns value } + in + unit + { model + | columnsValue = Util.Maybe.or [ value, model.columnsValue ] + , columnsModel = cm + , dashboard = db_ + } + + GapMsg lm -> + let + ( gm, value ) = + Comp.FixedDropdown.update lm model.gapModel + + db = + model.dashboard + + db_ = + { db | gap = Maybe.withDefault db.gap value } + in + unit + { model + | gapModel = gm + , gapValue = Util.Maybe.or [ value, model.gapValue ] + , dashboard = db_ + } + + ToggleNewBoxMenu -> + unit { model | newBoxMenuOpen = not model.newBoxMenuOpen } + + PrependNew box -> + let + min = + Dict.keys model.boxModels + |> List.minimum + |> Maybe.withDefault 1 + + index = + min - 1 + + db = + model.dashboard + + db_ = + { db | boxes = box :: db.boxes } + + ( bm, bc, bs ) = + Comp.BoxEdit.init flags box + + newBoxes = + Dict.insert index bm model.boxModels + in + { model = { model | boxModels = newBoxes, dashboard = db_, newBoxMenuOpen = False } + , cmd = Cmd.map (BoxMsg index) bc + , sub = Sub.map (BoxMsg index) bs + } + + DragDropMsg lm -> + let + ( dm, dropped ) = + DD.update lm model.boxDragDrop + + m_ = + { model | boxDragDrop = dm } + + nextModel = + case dropped of + Just ( dragId, dropId, _ ) -> + applyDrop dragId dropId m_ + + Nothing -> + m_ + in + unit nextModel + + SetScope s -> + unit { model | scope = s } + + ToggleDefault -> + unit { model | defaultDashboard = not model.defaultDashboard } + + +unit : Model -> UpdateResult +unit model = + UpdateResult model Cmd.none Sub.none + + +applyBoxAction : + Int + -> Comp.BoxEdit.BoxAction + -> Dict Int Comp.BoxEdit.Model + -> Dict Int Comp.BoxEdit.Model +applyBoxAction index action boxes = + let + swap n1 n2 = + Maybe.map2 + (\e1 -> \e2 -> Dict.insert n2 e1 boxes |> Dict.insert n1 e2) + (Dict.get n1 boxes) + (Dict.get n2 boxes) + |> Maybe.withDefault boxes + in + case action of + Comp.BoxEdit.BoxNoAction -> + boxes + + Comp.BoxEdit.BoxDelete -> + Dict.remove index boxes + + Comp.BoxEdit.BoxMoveLeft -> + swap (index - 1) index + + Comp.BoxEdit.BoxMoveRight -> + swap index (index + 1) + + +applyDrop : Int -> Int -> Model -> Model +applyDrop dragId dropId model = + let + dragEl = + Dict.get dragId model.boxModels + in + if dragId == dropId then + model + + else + case dragEl of + Just box -> + let + withoutDragged = + Dict.remove dragId model.boxModels + + ( begin, end ) = + Dict.partition (\k -> \_ -> k < dropId) withoutDragged + + incKeys = + Dict.toList end + |> List.map (\( k, v ) -> ( k + 1, v )) + |> Dict.fromList + + newBoxes = + Dict.insert dropId box (Dict.union begin incKeys) + + db = + model.dashboard + + db_ = + { db | boxes = List.map .box (Dict.values newBoxes) } + in + { model | boxModels = newBoxes, dashboard = db_ } + + Nothing -> + model + + + +--- View + + +view : Texts -> Flags -> UiSettings -> Model -> Html Msg +view texts flags settings model = + let + boxMenuItem box = + { icon = i [ class (Data.Box.boxIcon box) ] [] + , label = texts.boxContent.forContent box.content + , disabled = False + , attrs = + [ href "#" + , onClick (PrependNew box) + ] + } + in + div [] + [ viewMain texts flags settings model + , div [ class S.formHeader ] + [ text texts.dashboardBoxes + ] + , MB.view + { start = [] + , end = + [ MB.Dropdown + { linkIcon = "fa fa-plus" + , label = texts.newBox + , linkClass = + [ ( S.secondaryBasicButton, True ) + ] + , toggleMenu = ToggleNewBoxMenu + , menuOpen = model.newBoxMenuOpen + , items = + [ boxMenuItem Data.Box.queryBox + , boxMenuItem Data.Box.statsBox + , boxMenuItem Data.Box.messageBox + , boxMenuItem Data.Box.uploadBox + ] + } + ] + , rootClasses = "mb-2" + } + , div + [ class (gridStyle model.dashboard) + ] + (List.map + (viewBox texts flags settings model) + (Dict.toList model.boxModels) + ) + ] + + +viewBox : Texts -> Flags -> UiSettings -> Model -> ( Int, Comp.BoxEdit.Model ) -> Html Msg +viewBox texts flags settings model ( index, box ) = + let + dropId = + DD.getDropId model.boxDragDrop + + dragId = + DD.getDragId model.boxDragDrop + + styles = + [ classList [ ( "opacity-40", dropId == Just index && dropId /= dragId ) ] + , class (spanStyle box.box) + ] + in + div + (DD.draggable DragDropMsg index ++ DD.droppable DragDropMsg index ++ styles) + [ Html.map (BoxMsg index) + (Comp.BoxEdit.view texts.boxView flags settings box) + ] + + +viewMain : Texts -> Flags -> UiSettings -> Model -> Html Msg +viewMain texts _ _ model = + let + columnsSettings = + { display = String.fromInt + , icon = \_ -> Nothing + , selectPlaceholder = "" + , style = DS.mainStyle + } + in + div [ class "my-2 " ] + [ div [ class "flex flex-col" ] + [ div [ class "mt-2" ] + [ label [ class S.inputLabel ] + [ text texts.basics.name + , B.inputRequired + ] + , input + [ type_ "text" + , placeholder texts.namePlaceholder + , class S.textInput + , classList [ ( S.inputErrorBorder, String.trim model.nameValue == "" ) ] + , value model.nameValue + , onInput SetName + ] + [] + ] + , div [ class "mt-2" ] + [ label [ class S.inputLabel ] + [ text texts.columns + ] + , Html.map ColumnsMsg + (Comp.FixedDropdown.viewStyled2 columnsSettings + False + model.columnsValue + model.columnsModel + ) + ] + , div [ class "mt-2" ] + [ label [ class S.inputLabel ] + [ text texts.gap + ] + , Html.map GapMsg + (Comp.FixedDropdown.viewStyled2 columnsSettings + False + model.gapValue + model.gapModel + ) + ] + , div [ class "mt-2" ] + [ div [ class "flex flex-row space-x-4" ] + [ label [ class "inline-flex items-center" ] + [ input + [ type_ "radio" + , checked (Data.AccountScope.isUser model.scope) + , onCheck (\_ -> SetScope Data.AccountScope.User) + , class S.radioInput + ] + [] + , span [ class "ml-2" ] [ text <| texts.accountScope Data.AccountScope.User ] + ] + , label [ class "inline-flex items-center" ] + [ input + [ type_ "radio" + , checked (Data.AccountScope.isCollective model.scope) + , onCheck (\_ -> SetScope Data.AccountScope.Collective) + , class S.radioInput + ] + [] + , span [ class "ml-2" ] + [ text <| texts.accountScope Data.AccountScope.Collective ] + ] + ] + ] + , div [ class "mt-2" ] + [ MB.viewItem <| + MB.Checkbox + { tagger = \_ -> ToggleDefault + , label = texts.defaultDashboard + , id = "" + , value = model.defaultDashboard + } + ] + ] + ] + + + +--- Helpers + + +gridStyle : Dashboard -> String +gridStyle db = + let + colStyle = + case db.columns of + 1 -> + "" + + 2 -> + "md:grid-cols-2" + + 3 -> + "md:grid-cols-3" + + 4 -> + "md:grid-cols-4" + + _ -> + "md:grid-cols-5" + in + "grid gap-4 grid-cols-1 " ++ colStyle + + +spanStyle : Box -> String +spanStyle box = + case box.colspan of + 1 -> + "" + + 2 -> + "col-span-1 md:col-span-2" + + 3 -> + "col-span-1 md:col-span-3" + + 4 -> + "col-span-1 md:col-span-4" + + _ -> + "col-span-1 md:col-span-5" diff --git a/modules/webapp/src/main/elm/Comp/DashboardManage.elm b/modules/webapp/src/main/elm/Comp/DashboardManage.elm new file mode 100644 index 00000000..ee8f63e7 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/DashboardManage.elm @@ -0,0 +1,319 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.DashboardManage exposing (Model, Msg, SubmitAction(..), UpdateResult, init, update, view) + +import Api +import Api.Model.BasicResult exposing (BasicResult) +import Comp.Basic as B +import Comp.DashboardEdit +import Comp.MenuBar as MB +import Data.AccountScope exposing (AccountScope) +import Data.Dashboard exposing (Dashboard) +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html, div, i, text) +import Html.Attributes exposing (class, classList) +import Http +import Messages.Comp.DashboardManage exposing (Texts) +import Styles as S + + +type alias Model = + { edit : Comp.DashboardEdit.Model + , initData : InitData + , deleteRequested : Bool + , formError : Maybe FormError + } + + +type Msg + = SaveDashboard + | Cancel + | DeleteDashboard + | SetRequestDelete Bool + | EditMsg Comp.DashboardEdit.Msg + | DeleteResp (Result Http.Error BasicResult) + | SaveResp String (Result Http.Error BasicResult) + | CreateNew + | CopyCurrent + + +type FormError + = FormInvalid String + | FormHttpError Http.Error + | FormNameEmpty + | FormNameExists + + +type alias InitData = + { flags : Flags + , dashboard : Dashboard + , scope : AccountScope + , isDefault : Bool + } + + +init : InitData -> ( Model, Cmd Msg, Sub Msg ) +init data = + let + ( em, ec, es ) = + Comp.DashboardEdit.init data.flags data.dashboard data.scope data.isDefault + + model = + { edit = em + , initData = data + , deleteRequested = False + , formError = Nothing + } + in + ( model, Cmd.map EditMsg ec, Sub.map EditMsg es ) + + + +--- Update + + +type SubmitAction + = SubmitNone + | SubmitCancel String + | SubmitSaved String + | SubmitDeleted + + +type alias UpdateResult = + { model : Model + , cmd : Cmd Msg + , sub : Sub Msg + , action : SubmitAction + } + + +update : Flags -> (String -> Bool) -> Msg -> Model -> UpdateResult +update flags nameExists msg model = + case msg of + EditMsg lm -> + let + result = + Comp.DashboardEdit.update flags lm model.edit + in + { model = { model | edit = result.model } + , cmd = Cmd.map EditMsg result.cmd + , sub = Sub.map EditMsg result.sub + , action = SubmitNone + } + + CreateNew -> + let + initData = + { flags = flags + , dashboard = Data.Dashboard.empty + , scope = Data.AccountScope.User + , isDefault = False + } + + ( m, c, s ) = + init initData + in + UpdateResult m c s SubmitNone + + CopyCurrent -> + let + ( current, scope, isDefault ) = + Comp.DashboardEdit.getBoard model.edit + + initData = + { flags = flags + , dashboard = { current | name = "" } + , scope = scope + , isDefault = isDefault + } + + ( m, c, s ) = + init initData + in + UpdateResult m c s SubmitNone + + SetRequestDelete flag -> + unit { model | deleteRequested = flag } + + SaveDashboard -> + let + ( tosave, scope, isDefault ) = + Comp.DashboardEdit.getBoard model.edit + + saveCmd = + Api.replaceDashboard flags + model.initData.dashboard.name + tosave + scope + isDefault + (SaveResp tosave.name) + in + if tosave.name == "" then + unit { model | formError = Just FormNameEmpty } + + else if tosave.name /= model.initData.dashboard.name && nameExists tosave.name then + unit { model | formError = Just FormNameExists } + + else + UpdateResult model saveCmd Sub.none SubmitNone + + Cancel -> + unitAction model (SubmitCancel model.initData.dashboard.name) + + DeleteDashboard -> + let + deleteCmd = + Api.deleteDashboard flags model.initData.dashboard.name model.initData.scope DeleteResp + in + UpdateResult model deleteCmd Sub.none SubmitNone + + SaveResp name (Ok result) -> + if result.success then + unitAction model (SubmitSaved name) + + else + unit { model | formError = Just (FormInvalid result.message) } + + SaveResp _ (Err err) -> + unit { model | formError = Just (FormHttpError err) } + + DeleteResp (Ok result) -> + if result.success then + unitAction model SubmitDeleted + + else + unit { model | formError = Just (FormInvalid result.message) } + + DeleteResp (Err err) -> + unit { model | formError = Just (FormHttpError err) } + + +unit : Model -> UpdateResult +unit model = + UpdateResult model Cmd.none Sub.none SubmitNone + + +unitAction : Model -> SubmitAction -> UpdateResult +unitAction model action = + UpdateResult model Cmd.none Sub.none action + + + +--- View + + +type alias ViewSettings = + { showDeleteButton : Bool + , showCopyButton : Bool + } + + +view : Texts -> Flags -> ViewSettings -> UiSettings -> Model -> Html Msg +view texts flags cfg settings model = + div [] + [ B.contentDimmer model.deleteRequested + (div [ class "flex flex-col" ] + [ div [ class "text-xl" ] + [ i [ class "fa fa-info-circle mr-2" ] [] + , text texts.reallyDeleteDashboard + ] + , div [ class "mt-4 flex flex-row items-center space-x-2" ] + [ MB.viewItem <| + MB.DeleteButton + { tagger = DeleteDashboard + , title = "" + , label = texts.basics.yes + , icon = Just "fa fa-check" + } + , MB.viewItem <| + MB.SecondaryButton + { tagger = SetRequestDelete False + , title = "" + , label = texts.basics.no + , icon = Just "fa fa-times" + } + ] + ] + ) + , MB.view + { start = + [ MB.PrimaryButton + { tagger = SaveDashboard + , title = texts.basics.submitThisForm + , icon = Just "fa fa-save" + , label = texts.basics.submit + } + , MB.SecondaryButton + { tagger = Cancel + , title = texts.basics.cancel + , icon = Just "fa fa-times" + , label = texts.basics.cancel + } + ] + , end = + [ MB.BasicButton + { tagger = CreateNew + , title = texts.createDashboard + , icon = Just "fa fa-plus" + , label = texts.createDashboard + } + , MB.CustomButton + { tagger = CopyCurrent + , title = texts.copyDashboard + , icon = Just "fa fa-copy" + , label = texts.copyDashboard + , inputClass = + [ ( S.secondaryBasicButton, True ) + , ( "hidden", not cfg.showCopyButton ) + ] + } + , MB.CustomButton + { tagger = SetRequestDelete True + , title = texts.basics.delete + , icon = Just "fa fa-times" + , label = texts.basics.delete + , inputClass = + [ ( S.deleteButton, True ) + , ( "hidden", not cfg.showDeleteButton ) + ] + } + ] + , rootClasses = "" + } + , div + [ class S.errorMessage + , class "mt-2" + , classList [ ( "hidden", model.formError == Nothing ) ] + ] + [ errorMessage texts model + ] + , div [] + [ Html.map EditMsg + (Comp.DashboardEdit.view texts.dashboardEdit flags settings model.edit) + ] + ] + + +errorMessage : Texts -> Model -> Html Msg +errorMessage texts model = + case model.formError of + Just (FormInvalid errMsg) -> + text errMsg + + Just (FormHttpError err) -> + text (texts.httpError err) + + Just FormNameEmpty -> + text texts.nameEmpty + + Just FormNameExists -> + text texts.nameExists + + Nothing -> + text "" diff --git a/modules/webapp/src/main/elm/Comp/DashboardView.elm b/modules/webapp/src/main/elm/Comp/DashboardView.elm new file mode 100644 index 00000000..28958ef3 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/DashboardView.elm @@ -0,0 +1,137 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.DashboardView exposing (Model, Msg, init, reloadData, update, view, viewBox) + +import Comp.BoxView +import Data.Dashboard exposing (Dashboard) +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Dict exposing (Dict) +import Html exposing (Html, div) +import Html.Attributes exposing (class) +import Messages.Comp.DashboardView exposing (Texts) +import Util.Update + + +type alias Model = + { dashboard : Dashboard + , boxModels : Dict Int Comp.BoxView.Model + } + + +type Msg + = BoxMsg Int Comp.BoxView.Msg + | ReloadData + + +init : Flags -> Dashboard -> ( Model, Cmd Msg ) +init flags db = + let + ( boxModels, cmds ) = + List.map (Comp.BoxView.init flags) db.boxes + |> List.indexedMap (\a -> \( bm, bc ) -> ( bm, Cmd.map (BoxMsg a) bc )) + |> List.unzip + in + ( { dashboard = db + , boxModels = + List.indexedMap Tuple.pair boxModels + |> Dict.fromList + } + , Cmd.batch cmds + ) + + +reloadData : Msg +reloadData = + ReloadData + + + +--- Update + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +update flags msg model = + case msg of + BoxMsg index lm -> + case Dict.get index model.boxModels of + Just bm -> + let + ( cm, cc, cs ) = + Comp.BoxView.update flags lm bm + in + ( { model | boxModels = Dict.insert index cm model.boxModels } + , Cmd.map (BoxMsg index) cc + , Sub.map (BoxMsg index) cs + ) + + Nothing -> + unit model + + ReloadData -> + let + updateAll = + List.map (\index -> BoxMsg index Comp.BoxView.reloadData) (Dict.keys model.boxModels) + |> List.map (\m -> update flags m) + |> Util.Update.andThen2 + in + updateAll model + + +unit : Model -> ( Model, Cmd Msg, Sub Msg ) +unit model = + ( model, Cmd.none, Sub.none ) + + + +--- View + + +view : Texts -> Flags -> UiSettings -> Model -> Html Msg +view texts flags settings model = + div + [ class (gridStyle model.dashboard) + ] + (List.indexedMap (viewBox texts flags settings) <| Dict.values model.boxModels) + + +viewBox : Texts -> Flags -> UiSettings -> Int -> Comp.BoxView.Model -> Html Msg +viewBox texts flags settings index box = + Html.map (BoxMsg index) + (Comp.BoxView.view texts.boxView flags settings box) + + + +--- Helpers + + +{-| note due to tailwinds purging css that is not found in source +files, need to spell them out somewhere - which is done it keep.txt in +this case. +-} +gridStyle : Dashboard -> String +gridStyle db = + let + cappedGap = + min db.gap 12 + + cappedCol = + min db.columns 12 + + gapStyle = + " gap-" ++ String.fromInt cappedGap ++ " " + + colStyle = + case db.columns of + 1 -> + "" + + _ -> + " md:grid-cols-" ++ String.fromInt cappedCol ++ " " + in + "grid grid-cols-1 " ++ gapStyle ++ colStyle diff --git a/modules/webapp/src/main/elm/Comp/DetailEdit.elm b/modules/webapp/src/main/elm/Comp/DetailEdit.elm index 43b5231a..da7ae172 100644 --- a/modules/webapp/src/main/elm/Comp/DetailEdit.elm +++ b/modules/webapp/src/main/elm/Comp/DetailEdit.elm @@ -712,10 +712,10 @@ formHeading texts classes model = (\_ -> texts.addCustomFieldHeader) headIcon = - fold (\_ -> Icons.tagIcon2 "mr-2") - (\_ -> Icons.personIcon2 "mr-2") - (\_ -> Icons.organizationIcon2 "mr-2") - (\_ -> Icons.equipmentIcon2 "mt-2") + fold (\_ -> Icons.tagIcon "mr-2") + (\_ -> Icons.personIcon "mr-2") + (\_ -> Icons.organizationIcon "mr-2") + (\_ -> Icons.equipmentIcon "mt-2") (\_ -> Icons.customFieldIcon2 "mr-2") in div [ class classes ] @@ -738,10 +738,10 @@ viewModal2 texts settings mm = (\_ -> texts.addCustomFieldHeader) headIcon = - fold (\_ -> Icons.tagIcon2 "mr-2") - (\_ -> Icons.personIcon2 "mr-2") - (\_ -> Icons.organizationIcon2 "mr-2") - (\_ -> Icons.equipmentIcon2 "mt-2") + fold (\_ -> Icons.tagIcon "mr-2") + (\_ -> Icons.personIcon "mr-2") + (\_ -> Icons.organizationIcon "mr-2") + (\_ -> Icons.equipmentIcon "mt-2") (\_ -> Icons.customFieldIcon2 "mr-2") in div diff --git a/modules/webapp/src/main/elm/Comp/Dropzone.elm b/modules/webapp/src/main/elm/Comp/Dropzone.elm index ff34f2a8..e4dd621e 100644 --- a/modules/webapp/src/main/elm/Comp/Dropzone.elm +++ b/modules/webapp/src/main/elm/Comp/Dropzone.elm @@ -132,15 +132,21 @@ filterMime model files = --- View2 -view2 : Texts -> Model -> Html Msg -view2 texts model = +type alias ViewSettings = + { light : Bool + } + + +view2 : Texts -> ViewSettings -> Model -> Html Msg +view2 texts cfg model = div [ classList [ ( "bg-opacity-100 bg-blue-100 dark:bg-sky-800", model.state.hover ) - , ( "bg-blue-100 dark:bg-sky-900 bg-opacity-50", not model.state.hover ) + , ( "bg-indigo-100 dark:bg-sky-900 bg-opacity-50", not model.state.hover ) , ( "disabled", not model.state.active ) ] - , class "flex flex-col justify-center items-center py-2 md:py-12 border-0 border-t-2 border-blue-500 dark:border-sky-500 dropzone" + , class "flex flex-col justify-center items-center py-2 md:py-12 dropzone" + , classList [ ( " border-0 border-t-2 border-blue-500 dark:border-sky-500", not cfg.light ) ] , onDragEnter DragEnter , onDragOver DragEnter , onDragLeave DragLeave @@ -168,7 +174,10 @@ view2 texts model = , attrs = [ href "#" ] , disabled = not model.state.active } - , div [ class "text-center opacity-75 text-sm mt-4" ] + , div + [ class "text-center opacity-75 text-sm mt-4" + , classList [ ( "hidden", cfg.light ) ] + ] [ text texts.selectInfo ] ] diff --git a/modules/webapp/src/main/elm/Comp/EquipmentManage.elm b/modules/webapp/src/main/elm/Comp/EquipmentManage.elm index 12e03896..76d3034c 100644 --- a/modules/webapp/src/main/elm/Comp/EquipmentManage.elm +++ b/modules/webapp/src/main/elm/Comp/EquipmentManage.elm @@ -9,6 +9,7 @@ module Comp.EquipmentManage exposing ( Model , Msg(..) , emptyModel + , init , update , view2 ) @@ -70,6 +71,11 @@ emptyModel = } +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( emptyModel, Api.getEquipments flags emptyModel.query emptyModel.order EquipmentResp ) + + type Msg = TableMsg Comp.EquipmentTable.Msg | FormMsg Comp.EquipmentForm.Msg diff --git a/modules/webapp/src/main/elm/Comp/ItemCard.elm b/modules/webapp/src/main/elm/Comp/ItemCard.elm index fd51fc8d..0af6039e 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCard.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCard.elm @@ -418,7 +418,7 @@ viewRow texts cfg settings flags model item = , class "hover:opacity-60" , title texts.basics.folder ] - [ Icons.folderIcon2 "mr-2" + [ Icons.folderIcon "mr-2" , Comp.LinkTarget.makeFolderLink item [ ( "hover:opacity-60", True ) ] SetLinkTarget @@ -592,7 +592,7 @@ metaDataContent2 texts settings item = , class "hover:opacity-60" , title texts.basics.folder ] - [ Icons.folderIcon2 "mr-2" + [ Icons.folderIcon "mr-2" , Comp.LinkTarget.makeFolderLink item [ ( "hover:opacity-60", True ) ] SetLinkTarget diff --git a/modules/webapp/src/main/elm/Comp/ItemColumnDropdown.elm b/modules/webapp/src/main/elm/Comp/ItemColumnDropdown.elm new file mode 100644 index 00000000..5bbbbe9c --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ItemColumnDropdown.elm @@ -0,0 +1,86 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.ItemColumnDropdown exposing (Model, Msg, getSelected, init, update, view) + +import Comp.Dropdown exposing (Option) +import Data.DropdownStyle +import Data.ItemColumn exposing (ItemColumn(..)) +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html) +import Messages.Comp.ItemColumnDropdown exposing (Texts) + + +type Model + = Model (Comp.Dropdown.Model ItemColumn) + + +type Msg + = DropdownMsg (Comp.Dropdown.Msg ItemColumn) + + +init : List ItemColumn -> Model +init selected = + Model <| + Comp.Dropdown.makeMultipleList + { options = Data.ItemColumn.all, selected = selected } + + +getSelected : Model -> List ItemColumn +getSelected (Model dm) = + Comp.Dropdown.getSelected dm + + + +--- Update + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + let + dmodel = + case model of + Model a -> + a + in + case msg of + DropdownMsg lm -> + let + ( dm, dc ) = + Comp.Dropdown.update lm dmodel + in + ( Model dm, Cmd.map DropdownMsg dc ) + + + +--- View + + +itemOption : Texts -> ItemColumn -> Option +itemOption texts item = + { text = texts.column.label item + , additional = "" + } + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts settings model = + let + viewSettings = + { makeOption = itemOption texts + , placeholder = texts.placeholder + , labelColor = \_ -> \_ -> "" + , style = Data.DropdownStyle.mainStyle + } + + dm = + case model of + Model a -> + a + in + Html.map DropdownMsg + (Comp.Dropdown.view2 viewSettings settings dm) diff --git a/modules/webapp/src/main/elm/Comp/ItemColumnView.elm b/modules/webapp/src/main/elm/Comp/ItemColumnView.elm new file mode 100644 index 00000000..0c86f296 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ItemColumnView.elm @@ -0,0 +1,43 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.ItemColumnView exposing (..) + +import Api.Model.ItemLight exposing (ItemLight) +import Data.ItemColumn exposing (ItemColumn(..)) +import Data.ItemTemplate exposing (TemplateContext) +import Data.UiSettings exposing (UiSettings) +import Html exposing (Attribute, Html, div, text) +import Html.Attributes exposing (class) + + +renderDiv : + TemplateContext + -> UiSettings + -> ItemColumn + -> List (Attribute msg) + -> ItemLight + -> Html msg +renderDiv ctx settings col attr item = + case col of + Tags -> + div attr + (List.map + (\t -> + div + [ class "label text-sm" + , class <| Data.UiSettings.tagColorString2 t settings + ] + [ text t.name ] + ) + item.tags + ) + + _ -> + div attr + [ text (Data.ItemColumn.renderString ctx col item) + ] diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/AddFilesForm.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/AddFilesForm.elm index 86451511..4d0090ed 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/AddFilesForm.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/AddFilesForm.elm @@ -24,6 +24,11 @@ import Util.Size view : Texts -> Model -> Html Msg view texts model = + let + dropzoneCfg = + { light = True + } + in div [ classList [ ( "hidden", not model.addFilesOpen ) @@ -35,7 +40,7 @@ view texts model = [ text texts.addMoreFilesToItem ] , Html.map AddFilesMsg - (Comp.Dropzone.view2 texts.dropzone model.addFilesModel) + (Comp.Dropzone.view2 texts.dropzone dropzoneCfg model.addFilesModel) , div [ class "flex flex-row space-x-2 mt-2" ] [ button [ class S.primaryButton diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/EditForm.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/EditForm.elm index ab39e7a6..397b0ce8 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/EditForm.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/EditForm.elm @@ -298,7 +298,7 @@ formTabs texts flags settings model = [ optional [ Data.Fields.CorrOrg ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] - [ Icons.organizationIcon2 "mr-2" + [ Icons.organizationIcon "mr-2" , text texts.basics.organization , addIconLink texts.addNewOrg StartCorrOrgModal , editIconLink texts.editOrg model.corrOrgModel StartEditCorrOrgModal @@ -314,7 +314,7 @@ formTabs texts flags settings model = , optional [ Data.Fields.CorrPerson ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] - [ Icons.personIcon2 "mr-2" + [ Icons.personIcon "mr-2" , text texts.basics.person , addIconLink texts.addNewCorrespondentPerson StartCorrPersonModal , editIconLink texts.editPerson @@ -348,7 +348,7 @@ formTabs texts flags settings model = [ optional [ Data.Fields.ConcPerson ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] - [ Icons.personIcon2 "mr-2" + [ Icons.personIcon "mr-2" , text texts.basics.person , addIconLink texts.addNewConcerningPerson StartConcPersonModal , editIconLink texts.editPerson @@ -366,7 +366,7 @@ formTabs texts flags settings model = , optional [ Data.Fields.ConcEquip ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] - [ Icons.equipmentIcon2 "mr-2" + [ Icons.equipmentIcon "mr-2" , text texts.basics.equipment , addIconLink texts.addNewEquipment StartEquipModal , editIconLink texts.editEquipment diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/ItemInfoHeader.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/ItemInfoHeader.elm index 9aa20ecf..75a85108 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/ItemInfoHeader.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/ItemInfoHeader.elm @@ -98,7 +98,7 @@ view texts settings model = [ class itemStyle , title texts.basics.folder ] - [ Icons.folderIcon2 "mr-2" + [ Icons.folderIcon "mr-2" , Comp.LinkTarget.makeFolderLink model.item [ ( linkStyle, True ) ] SetLinkTarget diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/MultiEditMenu.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/MultiEditMenu.elm index 96789c30..adc27eab 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/MultiEditMenu.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/MultiEditMenu.elm @@ -735,7 +735,7 @@ renderEditForm2 texts flags cfg settings model = , body = [ div [ class "field" ] [ label [ class S.inputLabel ] - [ Icons.tagsIcon2 "" + [ Icons.tagsIcon "" , text texts.basics.tags , a [ class "float-right" @@ -841,7 +841,7 @@ renderEditForm2 texts flags cfg settings model = [ optional [ Data.Fields.CorrOrg ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] - [ Icons.organizationIcon2 "" + [ Icons.organizationIcon "" , span [ class "ml-2" ] [ text texts.basics.organization ] @@ -856,7 +856,7 @@ renderEditForm2 texts flags cfg settings model = , optional [ Data.Fields.CorrPerson ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] - [ Icons.personIcon2 "" + [ Icons.personIcon "" , span [ class "ml-2" ] [ text texts.basics.person ] @@ -878,7 +878,7 @@ renderEditForm2 texts flags cfg settings model = [ optional [ Data.Fields.ConcPerson ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] - [ Icons.personIcon2 "" + [ Icons.personIcon "" , span [ class "ml-2" ] [ text texts.basics.person ] ] @@ -887,7 +887,7 @@ renderEditForm2 texts flags cfg settings model = , optional [ Data.Fields.ConcEquip ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] - [ Icons.equipmentIcon2 "" + [ Icons.equipmentIcon "" , span [ class "ml-2" ] [ text texts.basics.equipment ] ] diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index 26ab704a..6af8466d 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -42,10 +42,7 @@ import Comp.ItemDetail.Model , UpdateResult , ViewMode(..) , initSelectViewModel - , initShowQrModel , isEditNotes - , isShowQrAttach - , isShowQrItem , resultModel , resultModelCmd , resultModelCmdSub @@ -741,7 +738,7 @@ update key flags inav settings msg model = resultModelCmd ( model, Page.set key (ItemDetailPage id) ) Nothing -> - resultModelCmd ( model, Page.set key HomePage ) + resultModelCmd ( model, Page.set key (SearchPage Nothing) ) in { result_ | removedItem = Just removedId } diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm index 781da2e8..0b8af10f 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm @@ -80,7 +80,7 @@ menuBar texts inav settings model = [ MB.CustomElement <| a [ class S.secondaryBasicButton - , Page.href HomePage + , Page.href (SearchPage Nothing) , title texts.backToSearchResults ] [ i [ class "fa fa-arrow-left" ] [] diff --git a/modules/webapp/src/main/elm/Comp/LinkTarget.elm b/modules/webapp/src/main/elm/Comp/LinkTarget.elm index 0ee1f413..0ec457c1 100644 --- a/modules/webapp/src/main/elm/Comp/LinkTarget.elm +++ b/modules/webapp/src/main/elm/Comp/LinkTarget.elm @@ -35,6 +35,7 @@ type LinkTarget | LinkTag IdName | LinkCustomField ItemFieldValue | LinkSource String + | LinkBookmark String | LinkNone diff --git a/modules/webapp/src/main/elm/Comp/MenuBar.elm b/modules/webapp/src/main/elm/Comp/MenuBar.elm index c25fa281..b5819ae2 100644 --- a/modules/webapp/src/main/elm/Comp/MenuBar.elm +++ b/modules/webapp/src/main/elm/Comp/MenuBar.elm @@ -308,18 +308,27 @@ makeButton btnType model = makeCheckbox : CheckboxData msg -> Html msg makeCheckbox model = + let + withId list = + if model.id == "" then + list + + else + id model.id :: list + in div [ class "" ] [ label [ class "inline-flex space-x-2 items-center" , for model.id ] [ input - [ type_ "checkbox" - , onCheck model.tagger - , checked model.value - , class S.checkboxInput - , id model.id - ] + (withId + [ type_ "checkbox" + , onCheck model.tagger + , checked model.value + , class S.checkboxInput + ] + ) [] , span [ class "truncate" ] [ text model.label diff --git a/modules/webapp/src/main/elm/Comp/OrgManage.elm b/modules/webapp/src/main/elm/Comp/OrgManage.elm index 51968145..5dccc8b0 100644 --- a/modules/webapp/src/main/elm/Comp/OrgManage.elm +++ b/modules/webapp/src/main/elm/Comp/OrgManage.elm @@ -9,6 +9,7 @@ module Comp.OrgManage exposing ( Model , Msg(..) , emptyModel + , init , update , view2 ) @@ -71,6 +72,11 @@ emptyModel = } +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( emptyModel, Api.getOrganizations flags emptyModel.query emptyModel.order OrgResp ) + + type Msg = TableMsg Comp.OrgTable.Msg | FormMsg Comp.OrgForm.Msg diff --git a/modules/webapp/src/main/elm/Comp/PersonManage.elm b/modules/webapp/src/main/elm/Comp/PersonManage.elm index 0228b49d..92451a22 100644 --- a/modules/webapp/src/main/elm/Comp/PersonManage.elm +++ b/modules/webapp/src/main/elm/Comp/PersonManage.elm @@ -9,6 +9,7 @@ module Comp.PersonManage exposing ( Model , Msg(..) , emptyModel + , init , update , view2 ) @@ -72,6 +73,11 @@ emptyModel = } +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( emptyModel, Api.getPersons flags emptyModel.query emptyModel.order PersonResp ) + + type Msg = TableMsg Comp.PersonTable.Msg | FormMsg Comp.PersonForm.Msg diff --git a/modules/webapp/src/main/elm/Comp/PowerSearchInput.elm b/modules/webapp/src/main/elm/Comp/PowerSearchInput.elm index 1036452d..9dedaa91 100644 --- a/modules/webapp/src/main/elm/Comp/PowerSearchInput.elm +++ b/modules/webapp/src/main/elm/Comp/PowerSearchInput.elm @@ -11,6 +11,7 @@ module Comp.PowerSearchInput exposing , Msg , ViewSettings , init + , initWith , isValid , setSearchString , update @@ -45,6 +46,15 @@ init = } +initWith : String -> ( Model, Cmd Msg, Sub Msg ) +initWith qstr = + let + result = + update (setSearchString qstr) init + in + ( result.model, result.cmd, result.subs ) + + isValid : Model -> Bool isValid model = model.input /= Nothing && model.result.success diff --git a/modules/webapp/src/main/elm/Comp/SearchMenu.elm b/modules/webapp/src/main/elm/Comp/SearchMenu.elm index aa52bc4c..95271a3c 100644 --- a/modules/webapp/src/main/elm/Comp/SearchMenu.elm +++ b/modules/webapp/src/main/elm/Comp/SearchMenu.elm @@ -59,7 +59,7 @@ import Data.UiSettings exposing (UiSettings) import DatePicker exposing (DatePicker) import Html exposing (..) import Html.Attributes exposing (..) -import Html.Events exposing (onClick, onInput) +import Html.Events exposing (onInput) import Http import Messages.Comp.SearchMenu exposing (Texts) import Set exposing (Set) @@ -385,6 +385,7 @@ type Msg | SetConcEquip IdName | SetFolder IdName | SetTag String + | SetBookmark String | SetCustomField ItemFieldValue | CustomFieldMsg Comp.CustomFieldMultiInput.Msg | SetSource String @@ -432,6 +433,9 @@ linkTargetMsg linkTarget = Comp.LinkTarget.LinkSource str -> Just <| ResetToSource str + Comp.LinkTarget.LinkBookmark id -> + Just <| SetBookmark id + type alias NextState = { model : Model @@ -556,6 +560,22 @@ updateDrop ddm flags settings msg model = SetTag id -> resetAndSet (TagSelectMsg (Comp.TagSelect.toggleTag id)) + SetBookmark id -> + let + nextModel = + resetModel model + + sel = + { bookmarks = Set.singleton id + , shares = Set.empty + } + in + { model = { nextModel | selectedBookmarks = sel } + , cmd = Cmd.none + , stateChange = sel /= model.selectedBookmarks + , dragDrop = DD.DragDropData ddm Nothing + } + GetAllTagsResp (Ok stats) -> let tagSel = @@ -1064,7 +1084,7 @@ updateDrop ddm flags settings msg model = AllBookmarksResp (Ok bm) -> { model = { model | allBookmarks = Comp.BookmarkChooser.init bm } , cmd = Cmd.none - , stateChange = False + , stateChange = model.allBookmarks /= Comp.BookmarkChooser.init bm , dragDrop = DD.DragDropData ddm Nothing } @@ -1082,7 +1102,7 @@ updateDrop ddm flags settings msg model = in { model = { model | allBookmarks = next, selectedBookmarks = sel } , cmd = Cmd.none - , stateChange = sel /= model.selectedBookmarks + , stateChange = sel /= model.selectedBookmarks || model.allBookmarks /= next , dragDrop = DD.DragDropData ddm Nothing } diff --git a/modules/webapp/src/main/elm/Comp/SearchStatsView.elm b/modules/webapp/src/main/elm/Comp/SearchStatsView.elm index d091465e..1d3bb8ad 100644 --- a/modules/webapp/src/main/elm/Comp/SearchStatsView.elm +++ b/modules/webapp/src/main/elm/Comp/SearchStatsView.elm @@ -8,6 +8,7 @@ module Comp.SearchStatsView exposing ( nameOrLabel , sortFields + , view , view2 ) @@ -36,8 +37,13 @@ sortFields fields = --- View2 -view2 : Texts -> String -> SearchStats -> Html msg -view2 texts classes stats = +view : Texts -> String -> SearchStats -> Html msg +view texts classes stats = + view2 texts True classes stats + + +view2 : Texts -> Bool -> String -> SearchStats -> Html msg +view2 texts showCount classes stats = let isNumField f = f.sum > 0 @@ -78,7 +84,10 @@ view2 texts classes stats = in div [ class classes ] [ div [ class "flex flex-col md:flex-row" ] - [ div [ class "px-8 py-4" ] + [ div + [ class "px-8 py-4" + , classList [ ( "hidden", not showCount ) ] + ] [ B.stats { rootClass = "" , valueClass = "text-4xl" diff --git a/modules/webapp/src/main/elm/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Comp/ShareManage.elm index f0b8bca3..2a4915d4 100644 --- a/modules/webapp/src/main/elm/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Comp/ShareManage.elm @@ -85,6 +85,7 @@ init flags = , Cmd.batch [ Cmd.map FormMsg fc , Cmd.map MailMsg mc + , Api.getShares flags "" True LoadSharesResp ] ) diff --git a/modules/webapp/src/main/elm/Comp/SourceManage.elm b/modules/webapp/src/main/elm/Comp/SourceManage.elm index b8581da8..c936069d 100644 --- a/modules/webapp/src/main/elm/Comp/SourceManage.elm +++ b/modules/webapp/src/main/elm/Comp/SourceManage.elm @@ -69,6 +69,7 @@ init flags = [ Cmd.map FormMsg fc , Ports.initClipboard appClipboardData , Ports.initClipboard apiClipboardData + , Api.getSources flags SourceResp ] ) diff --git a/modules/webapp/src/main/elm/Comp/TagManage.elm b/modules/webapp/src/main/elm/Comp/TagManage.elm index 422e3264..7881003b 100644 --- a/modules/webapp/src/main/elm/Comp/TagManage.elm +++ b/modules/webapp/src/main/elm/Comp/TagManage.elm @@ -9,6 +9,7 @@ module Comp.TagManage exposing ( Model , Msg(..) , emptyModel + , init , update , view2 ) @@ -73,6 +74,11 @@ emptyModel = } +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( emptyModel, Api.getTags flags emptyModel.query emptyModel.order (TagResp emptyModel.query) ) + + type Msg = TableMsg Comp.TagTable.Msg | FormMsg Comp.TagForm.Msg diff --git a/modules/webapp/src/main/elm/Comp/TagSelect.elm b/modules/webapp/src/main/elm/Comp/TagSelect.elm index fae8f89e..b1fe5c79 100644 --- a/modules/webapp/src/main/elm/Comp/TagSelect.elm +++ b/modules/webapp/src/main/elm/Comp/TagSelect.elm @@ -546,7 +546,7 @@ viewTagItem2 ddm settings model tag = Data.UiSettings.tagColorFg2 tag.tag settings icon = - getIcon2 state color I.tagIcon2 + getIcon2 state color I.tagIcon dropActive = DD.getDropId ddm == Just (DD.Tag tag.tag.id) @@ -587,7 +587,7 @@ viewCategoryItem2 settings model cat = Data.UiSettings.catColorFg2 settings cat.name icon = - getIcon2 state color I.tagsIcon2 + getIcon2 state color I.tagsIcon in a [ class "flex flex-row items-center" diff --git a/modules/webapp/src/main/elm/Comp/UploadForm.elm b/modules/webapp/src/main/elm/Comp/UploadForm.elm new file mode 100644 index 00000000..0dd01e37 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/UploadForm.elm @@ -0,0 +1,572 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.UploadForm exposing (Model, Msg, init, reset, update, view) + +import Api +import Api.Model.BasicResult exposing (BasicResult) +import Api.Model.ItemUploadMeta +import Comp.Dropzone +import Comp.FixedDropdown +import Comp.Progress +import Data.DropdownStyle as DS +import Data.Flags exposing (Flags) +import Data.Language exposing (Language) +import Data.UiSettings exposing (UiSettings) +import Dict exposing (Dict) +import File exposing (File) +import Html exposing (Html, a, div, h2, h3, i, input, label, p, span, text) +import Html.Attributes exposing (action, checked, class, classList, href, id, type_) +import Html.Events exposing (onCheck, onClick) +import Http +import Messages.Comp.UploadForm exposing (Texts) +import Page exposing (Page(..)) +import Set exposing (Set) +import Styles +import Util.File exposing (makeFileId) +import Util.Maybe +import Util.Size + + +type alias Model = + { incoming : Bool + , singleItem : Bool + , files : List File + , completed : Set String + , errored : Set String + , loading : Dict String Int + , dropzone : Comp.Dropzone.Model + , skipDuplicates : Bool + , languageModel : Comp.FixedDropdown.Model Language + , language : Maybe Language + } + + +type Msg + = SubmitUpload + | SingleUploadResp String (Result Http.Error BasicResult) + | GotProgress String Http.Progress + | ToggleIncoming + | ToggleSingleItem + | Clear + | DropzoneMsg Comp.Dropzone.Msg + | ToggleSkipDuplicates + | LanguageMsg (Comp.FixedDropdown.Msg Language) + + +init : Model +init = + { incoming = True + , singleItem = False + , files = [] + , completed = Set.empty + , errored = Set.empty + , loading = Dict.empty + , dropzone = Comp.Dropzone.init [] + , skipDuplicates = True + , languageModel = + Comp.FixedDropdown.init Data.Language.all + , language = Nothing + } + + +reset : Msg +reset = + Clear + + +isLoading : Model -> File -> Bool +isLoading model file = + Dict.member (makeFileId file) model.loading + + +isCompleted : Model -> File -> Bool +isCompleted model file = + Set.member (makeFileId file) model.completed + + +isError : Model -> File -> Bool +isError model file = + Set.member (makeFileId file) model.errored + + +isIdle : Model -> File -> Bool +isIdle model file = + not (isLoading model file || isCompleted model file || isError model file) + + +uploadAllTracker : String +uploadAllTracker = + "upload-all" + + +isDone : Model -> Bool +isDone model = + List.map makeFileId model.files + |> List.all (\id -> Set.member id model.completed || Set.member id model.errored) + + +isSuccessAll : Model -> Bool +isSuccessAll model = + List.map makeFileId model.files + |> List.all (\id -> Set.member id model.completed) + + +hasErrors : Model -> Bool +hasErrors model = + not (Set.isEmpty model.errored) + + + +--- Update + + +update : Maybe String -> Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +update sourceId flags msg model = + case msg of + ToggleIncoming -> + ( { model | incoming = not model.incoming }, Cmd.none, Sub.none ) + + ToggleSingleItem -> + ( { model | singleItem = not model.singleItem }, Cmd.none, Sub.none ) + + ToggleSkipDuplicates -> + ( { model | skipDuplicates = not model.skipDuplicates }, Cmd.none, Sub.none ) + + SubmitUpload -> + let + emptyMeta = + Api.Model.ItemUploadMeta.empty + + meta = + { emptyMeta + | multiple = not model.singleItem + , skipDuplicates = Just model.skipDuplicates + , direction = + if model.incoming then + Just "incoming" + + else + Just "outgoing" + , language = Maybe.map Data.Language.toIso3 model.language + } + + fileids = + List.map makeFileId model.files + + uploads = + if model.singleItem then + Api.uploadSingle flags + sourceId + meta + uploadAllTracker + model.files + (SingleUploadResp uploadAllTracker) + + else + Cmd.batch (Api.upload flags sourceId meta model.files SingleUploadResp) + + tracker = + if model.singleItem then + Http.track uploadAllTracker (GotProgress uploadAllTracker) + + else + Sub.batch <| List.map (\id -> Http.track id (GotProgress id)) fileids + + ( cm2, _, _ ) = + Comp.Dropzone.update (Comp.Dropzone.setActive False) model.dropzone + + nowLoading = + List.map (\fid -> ( fid, 0 )) fileids + |> Dict.fromList + in + ( { model | loading = nowLoading, dropzone = cm2 }, uploads, tracker ) + + SingleUploadResp fileid (Ok res) -> + let + compl = + if res.success then + setCompleted model fileid + + else + model.completed + + errs = + if not res.success then + setErrored model fileid + + else + model.errored + + load = + if fileid == uploadAllTracker then + Dict.empty + + else + Dict.remove fileid model.loading + in + ( { model | completed = compl, errored = errs, loading = load } + , Cmd.none + , Sub.none + ) + + SingleUploadResp fileid (Err _) -> + let + errs = + setErrored model fileid + + load = + if fileid == uploadAllTracker then + Dict.empty + + else + Dict.remove fileid model.loading + in + ( { model | errored = errs, loading = load }, Cmd.none, Sub.none ) + + GotProgress fileid progress -> + let + percent = + case progress of + Http.Sending p -> + Http.fractionSent p + |> (*) 100 + |> round + + _ -> + 0 + + newLoading = + if model.singleItem then + Dict.insert uploadAllTracker percent model.loading + + else + Dict.insert fileid percent model.loading + in + ( { model | loading = newLoading } + , Cmd.none + , Sub.none + ) + + Clear -> + ( init, Cmd.none, Sub.none ) + + DropzoneMsg m -> + let + ( m2, c2, files ) = + Comp.Dropzone.update m model.dropzone + + nextFiles = + List.append model.files files + in + ( { model | files = nextFiles, dropzone = m2 }, Cmd.map DropzoneMsg c2, Sub.none ) + + LanguageMsg lm -> + let + ( dm, sel ) = + Comp.FixedDropdown.update lm model.languageModel + in + ( { model + | languageModel = dm + , language = Util.Maybe.or [ sel, model.language ] + } + , Cmd.none + , Sub.none + ) + + +setCompleted : Model -> String -> Set String +setCompleted model fileid = + if fileid == uploadAllTracker then + List.map makeFileId model.files |> Set.fromList + + else + Set.insert fileid model.completed + + +setErrored : Model -> String -> Set String +setErrored model fileid = + if fileid == uploadAllTracker then + List.map makeFileId model.files |> Set.fromList + + else + Set.insert fileid model.errored + + + +--- View + + +type alias ViewSettings = + { showForm : Bool + , sourceId : Maybe String + , lightForm : Bool + } + + +view : Texts -> ViewSettings -> Flags -> UiSettings -> Model -> Html Msg +view texts viewCfg _ _ model = + let + showForm = + viewCfg.sourceId == Nothing && viewCfg.showForm + + dropzoneCfg = + { light = viewCfg.lightForm + } + in + div [ class "mx-auto" ] + [ div [ class "px-0 flex flex-col" ] + [ if showForm then + div [ class "mb-4" ] + [ renderForm texts model + ] + + else + span [ class "hidden" ] [] + , div [ class "py-0" ] + [ Html.map DropzoneMsg + (Comp.Dropzone.view2 texts.dropzone dropzoneCfg model.dropzone) + ] + , div [ class "py-4" ] + [ a + [ class Styles.primaryButton + , href "#" + , onClick SubmitUpload + ] + [ text texts.basics.submit + ] + , a + [ class Styles.secondaryButton + , class "ml-2" + , href "#" + , onClick Clear + ] + [ text texts.reset + ] + ] + ] + , renderErrorMsg texts model + , renderSuccessMsg texts (Util.Maybe.nonEmpty viewCfg.sourceId) model + , renderUploads texts model + ] + + +renderForm : Texts -> Model -> Html Msg +renderForm texts model = + let + languageCfg = + { display = texts.languageLabel + , icon = \_ -> Nothing + , style = DS.mainStyleWith "w-40" + , selectPlaceholder = texts.basics.selectPlaceholder + } + in + div [ class "row" ] + [ Html.form [ action "#" ] + [ div [ class "flex flex-col mb-3" ] + [ label [ class "inline-flex items-center" ] + [ input + [ type_ "radio" + , checked model.incoming + , onCheck (\_ -> ToggleIncoming) + , class Styles.radioInput + ] + [] + , span [ class "ml-2" ] [ text texts.basics.incoming ] + ] + , label [ class "inline-flex items-center" ] + [ input + [ type_ "radio" + , checked (not model.incoming) + , onCheck (\_ -> ToggleIncoming) + , class Styles.radioInput + ] + [] + , span [ class "ml-2" ] [ text texts.basics.outgoing ] + ] + ] + , div [ class "flex flex-col mb-3" ] + [ label [ class "inline-flex items-center" ] + [ input + [ type_ "checkbox" + , checked model.singleItem + , onCheck (\_ -> ToggleSingleItem) + , class Styles.checkboxInput + ] + [] + , span [ class "ml-2" ] + [ text texts.allFilesOneItem + ] + ] + ] + , div [ class "flex flex-col mb-3" ] + [ label [ class "inline-flex items-center" ] + [ input + [ type_ "checkbox" + , checked model.skipDuplicates + , onCheck (\_ -> ToggleSkipDuplicates) + , class Styles.checkboxInput + ] + [] + , span [ class "ml-2" ] + [ text texts.skipExistingFiles + ] + ] + ] + , div [ class "flex flex-col mb-3" ] + [ label [ class "inline-flex items-center mb-2" ] + [ span [ class "mr-2" ] [ text (texts.language ++ ":") ] + , Html.map LanguageMsg + (Comp.FixedDropdown.viewStyled2 + languageCfg + False + model.language + model.languageModel + ) + ] + , div [ class "text-gray-400 text-xs" ] + [ text texts.languageInfo + ] + ] + ] + ] + + +renderErrorMsg : Texts -> Model -> Html Msg +renderErrorMsg texts model = + div + [ class "row" + , classList [ ( "hidden", not (isDone model && hasErrors model) ) ] + ] + [ div [ class "mt-4" ] + [ div [ class Styles.errorMessage ] + [ text texts.uploadErrorMessage + ] + ] + ] + + +renderSuccessMsg : Texts -> Bool -> Model -> Html Msg +renderSuccessMsg texts public model = + div + [ class "row" + , classList [ ( "hidden", List.isEmpty model.files || not (isSuccessAll model) ) ] + ] + [ div [ class "mt-4" ] + [ div [ class Styles.successMessage ] + [ h3 [ class Styles.header2, class "text-green-800 dark:text-lime-800" ] + [ i [ class "fa fa-smile font-thin" ] [] + , span [ class "ml-2" ] + [ text texts.successBox.allFilesUploaded + ] + ] + , p + [ classList [ ( "hidden", public ) ] + ] + [ text texts.successBox.line1 + , a + [ class Styles.successMessageLink + , Page.href (SearchPage Nothing) + ] + [ text texts.successBox.itemsPage + ] + , text texts.successBox.line2 + , a + [ class Styles.successMessageLink + , Page.href QueuePage + ] + [ text texts.successBox.processingPage + ] + , text texts.successBox.line3 + ] + , p [] + [ text texts.successBox.resetLine1 + , a + [ class Styles.successMessageLink + , href "#" + , onClick Clear + ] + [ text texts.successBox.reset + ] + , text texts.successBox.resetLine2 + ] + ] + ] + ] + + +renderUploads : Texts -> Model -> Html Msg +renderUploads texts model = + div + [ class "mt-4" + , classList [ ( "hidden", List.isEmpty model.files || isSuccessAll model ) ] + ] + [ h2 [ class Styles.header2 ] + [ text texts.selectedFiles + , text (" (" ++ (List.length model.files |> String.fromInt) ++ ")") + ] + , div [] <| + if model.singleItem then + List.map (renderFileItem model (Just uploadAllTracker)) model.files + + else + List.map (renderFileItem model Nothing) model.files + ] + + +getProgress : Model -> File -> Int +getProgress model file = + let + key = + if model.singleItem then + uploadAllTracker + + else + makeFileId file + in + Dict.get key model.loading + |> Maybe.withDefault 0 + + +renderFileItem : Model -> Maybe String -> File -> Html Msg +renderFileItem model _ file = + let + name = + File.name file + + size = + File.size file + |> toFloat + |> Util.Size.bytesReadable Util.Size.B + in + div [ class "flex flex-col w-full mb-4" ] + [ div [ class "flex flex-row items-center" ] + [ div [ class "inline-flex items-center" ] + [ i + [ classList + [ ( "mr-2 text-lg", True ) + , ( "fa fa-file font-thin", isIdle model file ) + , ( "fa fa-spinner animate-spin ", isLoading model file ) + , ( "fa fa-check ", isCompleted model file ) + , ( "fa fa-bolt", isError model file ) + ] + ] + [] + , div [ class "middle aligned content" ] + [ div [ class "header" ] + [ text name + ] + ] + ] + , div [ class "flex-grow inline-flex justify-end" ] + [ text size + ] + ] + , div [ class "h-4" ] + [ Comp.Progress.progress2 (getProgress model file) + ] + ] diff --git a/modules/webapp/src/main/elm/Data/AccountScope.elm b/modules/webapp/src/main/elm/Data/AccountScope.elm new file mode 100644 index 00000000..1fe287ec --- /dev/null +++ b/modules/webapp/src/main/elm/Data/AccountScope.elm @@ -0,0 +1,33 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Data.AccountScope exposing (..) + + +type AccountScope + = User + | Collective + + +fold : a -> a -> AccountScope -> a +fold user coll scope = + case scope of + User -> + user + + Collective -> + coll + + +isUser : AccountScope -> Bool +isUser scope = + fold True False scope + + +isCollective : AccountScope -> Bool +isCollective scope = + fold False True scope diff --git a/modules/webapp/src/main/elm/Data/Bookmarks.elm b/modules/webapp/src/main/elm/Data/Bookmarks.elm index e9c83c5c..2074f229 100644 --- a/modules/webapp/src/main/elm/Data/Bookmarks.elm +++ b/modules/webapp/src/main/elm/Data/Bookmarks.elm @@ -11,6 +11,7 @@ module Data.Bookmarks exposing , bookmarksDecoder , empty , exists + , findById , sort ) @@ -34,6 +35,12 @@ type alias Bookmarks = List BookmarkedQuery +findById : String -> Bookmarks -> Maybe BookmarkedQuery +findById id all = + List.filter (\e -> e.id == id) all + |> List.head + + {-| Checks wether a bookmark of this name already exists. -} exists : String -> Bookmarks -> Bool diff --git a/modules/webapp/src/main/elm/Data/Box.elm b/modules/webapp/src/main/elm/Data/Box.elm new file mode 100644 index 00000000..f27ec67d --- /dev/null +++ b/modules/webapp/src/main/elm/Data/Box.elm @@ -0,0 +1,81 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Data.Box exposing (Box, boxIcon, decoder, empty, encode, messageBox, queryBox, statsBox, uploadBox) + +import Data.BoxContent exposing (BoxContent(..)) +import Json.Decode as D +import Json.Encode as E + + +type alias Box = + { name : String + , visible : Bool + , decoration : Bool + , colspan : Int + , content : BoxContent + } + + +empty : BoxContent -> Box +empty cnt = + { name = "" + , visible = True + , decoration = True + , colspan = 1 + , content = cnt + } + + +boxIcon : Box -> String +boxIcon box = + Data.BoxContent.boxContentIcon box.content + + +queryBox : Box +queryBox = + empty (BoxQuery Data.BoxContent.emptyQueryData) + + +statsBox : Box +statsBox = + empty (BoxStats Data.BoxContent.emptyStatsData) + + +messageBox : Box +messageBox = + empty (BoxMessage Data.BoxContent.emptyMessageData) + + +uploadBox : Box +uploadBox = + empty (BoxUpload Data.BoxContent.emptyUploadData) + + + +--- JSON + + +decoder : D.Decoder Box +decoder = + D.map5 Box + (D.field "name" D.string) + (D.field "visible" D.bool) + (D.field "decoration" D.bool) + (D.field "colspan" D.int) + (D.field "content" Data.BoxContent.boxContentDecoder) + + +encode : Box -> E.Value +encode box = + E.object + [ ( "name", E.string box.name ) + , ( "visible", E.bool box.visible ) + , ( "decoration", E.bool box.decoration ) + , ( "colspan", E.int box.colspan ) + , ( "content", Data.BoxContent.boxContentEncode box.content ) + ] diff --git a/modules/webapp/src/main/elm/Data/BoxContent.elm b/modules/webapp/src/main/elm/Data/BoxContent.elm new file mode 100644 index 00000000..f5630969 --- /dev/null +++ b/modules/webapp/src/main/elm/Data/BoxContent.elm @@ -0,0 +1,319 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Data.BoxContent exposing + ( BoxContent(..) + , MessageData + , QueryData + , SearchQuery(..) + , StatsData + , SummaryShow(..) + , UploadData + , boxContentDecoder + , boxContentEncode + , boxContentIcon + , emptyMessageData + , emptyQueryData + , emptyStatsData + , emptyUploadData + ) + +import Data.ItemColumn exposing (ItemColumn) +import Html exposing (datalist) +import Json.Decode as D +import Json.Encode as E + + +type BoxContent + = BoxUpload UploadData + | BoxMessage MessageData + | BoxQuery QueryData + | BoxStats StatsData + + +type alias MessageData = + { title : String + , body : String + } + + +emptyMessageData : MessageData +emptyMessageData = + { title = "" + , body = "" + } + + +type alias UploadData = + { sourceId : Maybe String + } + + +emptyUploadData : UploadData +emptyUploadData = + { sourceId = Nothing + } + + +type alias QueryData = + { query : SearchQuery + , limit : Int + , details : Bool + , columns : List ItemColumn + , showHeaders : Bool + } + + +emptyQueryData : QueryData +emptyQueryData = + { query = SearchQueryString "" + , limit = 5 + , details = True + , columns = [] + , showHeaders = True + } + + +type alias StatsData = + { query : SearchQuery + , show : SummaryShow + } + + +emptyStatsData : StatsData +emptyStatsData = + { query = SearchQueryString "" + , show = SummaryShowGeneral + } + + +type SummaryShow + = SummaryShowFields Bool + | SummaryShowGeneral + + +type SearchQuery + = SearchQueryString String + | SearchQueryBookmark String + + +searchQueryAsString : SearchQuery -> String +searchQueryAsString q = + case q of + SearchQueryBookmark id -> + "bookmark:" ++ id + + SearchQueryString str -> + "query:" ++ str + + +searchQueryFromString : String -> Maybe SearchQuery +searchQueryFromString str = + if String.startsWith "bookmark:" str then + Just (SearchQueryBookmark <| String.dropLeft 9 str) + + else if String.startsWith "query:" str then + Just (SearchQueryString <| String.dropLeft 6 str) + + else + Nothing + + +boxContentIcon : BoxContent -> String +boxContentIcon content = + case content of + BoxMessage _ -> + "fa fa-comment-alt font-thin" + + BoxUpload _ -> + "fa fa-file-upload" + + BoxQuery _ -> + "fa fa-search" + + BoxStats _ -> + "fa fa-chart-bar font-thin" + + + +--- JSON + + +boxContentDecoder : D.Decoder BoxContent +boxContentDecoder = + let + from discr = + case String.toLower discr of + "message" -> + D.field "data" <| + D.map BoxMessage messageDataDecoder + + "upload" -> + D.field "data" <| + D.map BoxUpload uploadDataDecoder + + "query" -> + D.field "data" <| + D.map BoxQuery queryDataDecoder + + "stats" -> + D.field "data" <| + D.map BoxStats statsDataDecoder + + _ -> + D.fail ("Unknown box content: " ++ discr) + in + D.andThen from (D.field discriminator D.string) + + +boxContentEncode : BoxContent -> E.Value +boxContentEncode cnt = + case cnt of + BoxMessage data -> + E.object + [ ( discriminator, E.string "message" ) + , ( "data", messageDataEncode data ) + ] + + BoxUpload data -> + E.object + [ ( discriminator, E.string "upload" ) + , ( "data", uploadDataEncode data ) + ] + + BoxQuery data -> + E.object + [ ( discriminator, E.string "query" ) + , ( "data", queryDataEncode data ) + ] + + BoxStats data -> + E.object + [ ( discriminator, E.string "stats" ) + , ( "data", statsDataEncode data ) + ] + + +messageDataDecoder : D.Decoder MessageData +messageDataDecoder = + D.map2 MessageData + (D.field "title" D.string) + (D.field "body" D.string) + + +messageDataEncode : MessageData -> E.Value +messageDataEncode data = + E.object + [ ( "title", E.string data.title ) + , ( "body", E.string data.body ) + ] + + +uploadDataDecoder : D.Decoder UploadData +uploadDataDecoder = + D.map UploadData + (D.maybe (D.field "sourceId" D.string)) + + +uploadDataEncode : UploadData -> E.Value +uploadDataEncode data = + E.object + [ ( "sourceId", Maybe.map E.string data.sourceId |> Maybe.withDefault E.null ) + ] + + +queryDataDecoder : D.Decoder QueryData +queryDataDecoder = + D.map5 QueryData + (D.field "query" searchQueryDecoder) + (D.field "limit" D.int) + (D.field "details" D.bool) + (D.field "columns" <| D.list Data.ItemColumn.decode) + (D.field "showHeaders" D.bool) + + +queryDataEncode : QueryData -> E.Value +queryDataEncode data = + E.object + [ ( "query", searchQueryEncode data.query ) + , ( "limit", E.int data.limit ) + , ( "details", E.bool data.details ) + , ( "columns", E.list Data.ItemColumn.encode data.columns ) + , ( "showHeaders", E.bool data.showHeaders ) + ] + + +statsDataDecoder : D.Decoder StatsData +statsDataDecoder = + D.map2 StatsData + (D.field "query" searchQueryDecoder) + (D.field "show" summaryShowDecoder) + + +statsDataEncode : StatsData -> E.Value +statsDataEncode data = + E.object + [ ( "query", searchQueryEncode data.query ) + , ( "show", summaryShowEncode data.show ) + ] + + +searchQueryDecoder : D.Decoder SearchQuery +searchQueryDecoder = + let + fromString str = + case searchQueryFromString str of + Just q -> + D.succeed q + + Nothing -> + D.fail ("Invalid search query: " ++ str) + in + D.andThen fromString D.string + + +searchQueryEncode : SearchQuery -> E.Value +searchQueryEncode q = + E.string (searchQueryAsString q) + + +summaryShowDecoder : D.Decoder SummaryShow +summaryShowDecoder = + let + decode discr = + case String.toLower discr of + "fields" -> + D.field "showItemCount" D.bool + |> D.map SummaryShowFields + + "general" -> + D.succeed SummaryShowGeneral + + _ -> + D.fail ("Unknown summary show for: " ++ discr) + in + D.andThen decode (D.field discriminator D.string) + + +summaryShowEncode : SummaryShow -> E.Value +summaryShowEncode show = + case show of + SummaryShowFields flag -> + E.object + [ ( discriminator, E.string "fields" ) + , ( "showItemCount", E.bool flag ) + ] + + SummaryShowGeneral -> + E.object + [ ( "discriminator", E.string "general" ) + ] + + +discriminator : String +discriminator = + "discriminator" diff --git a/modules/webapp/src/main/elm/Data/Dashboard.elm b/modules/webapp/src/main/elm/Data/Dashboard.elm new file mode 100644 index 00000000..a057af53 --- /dev/null +++ b/modules/webapp/src/main/elm/Data/Dashboard.elm @@ -0,0 +1,57 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Data.Dashboard exposing (Dashboard, decoder, empty, encode, isEmpty) + +import Data.Box exposing (Box) +import Json.Decode as D +import Json.Encode as E + + +type alias Dashboard = + { name : String + , columns : Int + , gap : Int + , boxes : List Box + } + + +empty : Dashboard +empty = + { name = "" + , columns = 1 + , gap = 2 + , boxes = [] + } + + +isEmpty : Dashboard -> Bool +isEmpty board = + List.isEmpty board.boxes + + + +--- JSON + + +encode : Dashboard -> E.Value +encode b = + E.object + [ ( "name", E.string b.name ) + , ( "columns", E.int b.columns ) + , ( "gap", E.int b.gap ) + , ( "boxes", E.list Data.Box.encode b.boxes ) + ] + + +decoder : D.Decoder Dashboard +decoder = + D.map4 Dashboard + (D.field "name" D.string) + (D.field "columns" D.int) + (D.field "gap" D.int) + (D.field "boxes" <| D.list Data.Box.decoder) diff --git a/modules/webapp/src/main/elm/Data/Dashboards.elm b/modules/webapp/src/main/elm/Data/Dashboards.elm new file mode 100644 index 00000000..b31dd9ed --- /dev/null +++ b/modules/webapp/src/main/elm/Data/Dashboards.elm @@ -0,0 +1,296 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Data.Dashboards exposing + ( AllDashboards + , Dashboards + , countAll + , decoder + , empty + , emptyAll + , encode + , exists + , existsAll + , find + , findInAll + , foldl + , getAllDefault + , getDefault + , getScope + , insert + , insertIn + , isDefaultAll + , isEmpty + , isEmptyAll + , map + , remove + , removeFromAll + , selectBoards + , setDefaultAll + , singleton + , singletonAll + , unsetDefaultAll + ) + +import Data.AccountScope exposing (AccountScope) +import Data.Dashboard exposing (Dashboard) +import Dict exposing (Dict) +import Json.Decode as D +import Json.Encode as E +import Util.Maybe + + +type Dashboards + = Dashboards Info + + +empty : Dashboards +empty = + Dashboards { default = "", boards = Dict.empty } + + +isEmpty : Dashboards -> Bool +isEmpty (Dashboards info) = + Dict.isEmpty info.boards + + +insert : Dashboard -> Dashboards -> Dashboards +insert board (Dashboards info) = + let + nb = + Dict.insert (String.toLower board.name) board info.boards + in + Dashboards { info | boards = nb } + + +singleton : Dashboard -> Dashboards +singleton board = + insert board empty + + +remove : String -> Dashboards -> Dashboards +remove name (Dashboards info) = + let + nb = + Dict.remove (String.toLower name) info.boards + in + Dashboards { info | boards = nb } + + +map : (Dashboard -> a) -> Dashboards -> List a +map f (Dashboards info) = + List.map f (Dict.values info.boards) + + +find : String -> Dashboards -> Maybe Dashboard +find name (Dashboards info) = + Dict.get (String.toLower name) info.boards + + +foldl : (Dashboard -> a -> a) -> a -> Dashboards -> a +foldl f init (Dashboards info) = + List.foldl f init (Dict.values info.boards) + + +exists : String -> Dashboards -> Bool +exists name (Dashboards info) = + Dict.member (String.toLower name) info.boards + + +getDefault : Dashboards -> Maybe Dashboard +getDefault (Dashboards info) = + Dict.get (String.toLower info.default) info.boards + + +isDefault : String -> Dashboards -> Bool +isDefault name (Dashboards info) = + String.toLower name == String.toLower info.default + + +setDefault : String -> Dashboards -> Dashboards +setDefault name (Dashboards info) = + Dashboards { info | default = String.toLower name } + + +unsetDefault : String -> Dashboards -> Dashboards +unsetDefault name dbs = + if isDefault name dbs then + setDefault "" dbs + + else + dbs + + +getFirst : Dashboards -> Maybe Dashboard +getFirst (Dashboards info) = + List.head (Dict.values info.boards) + + + +--- AllDashboards + + +type alias AllDashboards = + { collective : Dashboards + , user : Dashboards + } + + +emptyAll : AllDashboards +emptyAll = + AllDashboards empty empty + + +isEmptyAll : AllDashboards -> Bool +isEmptyAll all = + isEmpty all.collective && isEmpty all.user + + +insertIn : AccountScope -> Dashboard -> AllDashboards -> AllDashboards +insertIn scope board all = + Data.AccountScope.fold + { user = insert board all.user + , collective = all.collective + } + { user = all.user + , collective = insert board all.collective + } + scope + + +selectBoards : AccountScope -> AllDashboards -> Dashboards +selectBoards scope all = + Data.AccountScope.fold all.user all.collective scope + + +getAllDefault : AllDashboards -> Maybe Dashboard +getAllDefault boards = + Util.Maybe.or + [ getDefault boards.user + , getDefault boards.collective + , getFirst boards.user + , getFirst boards.collective + ] + + +existsAll : String -> AllDashboards -> Bool +existsAll name boards = + exists name boards.collective || exists name boards.user + + +singletonAll : Dashboard -> AllDashboards +singletonAll board = + AllDashboards empty (singleton board) + + +isDefaultAll : String -> AllDashboards -> Bool +isDefaultAll name all = + isDefault name all.user || isDefault name all.collective + + +findInAll : String -> AllDashboards -> Maybe Dashboard +findInAll name all = + Util.Maybe.or + [ find name all.user + , find name all.collective + ] + + +removeFromAll : String -> AllDashboards -> AllDashboards +removeFromAll name all = + { user = remove name all.user + , collective = remove name all.collective + } + + +setDefaultAll : String -> AllDashboards -> AllDashboards +setDefaultAll name all = + if isDefaultAll name all then + all + + else + { user = setDefault name all.user + , collective = setDefault name all.collective + } + + +unsetDefaultAll : String -> AllDashboards -> AllDashboards +unsetDefaultAll name all = + if isDefaultAll name all then + { user = unsetDefault name all.user + , collective = unsetDefault name all.collective + } + + else + all + + +getScope : String -> AllDashboards -> Maybe AccountScope +getScope name all = + if exists name all.user then + Just Data.AccountScope.User + + else if exists name all.collective then + Just Data.AccountScope.Collective + + else + Nothing + + +countAll : AllDashboards -> Int +countAll all = + List.sum + [ foldl (\_ -> \n -> n + 1) 0 all.user + , foldl (\_ -> \n -> n + 1) 0 all.collective + ] + + + +--- Helper + + +type alias Info = + { boards : Dict String Dashboard + , default : String + } + + + +--- JSON + + +decoder : D.Decoder Dashboards +decoder = + D.oneOf + [ D.map Dashboards infoDecoder + , emptyObjectDecoder + ] + + +encode : Dashboards -> E.Value +encode (Dashboards info) = + infoEncode info + + +infoDecoder : D.Decoder Info +infoDecoder = + D.map2 Info + (D.field "boards" <| D.dict Data.Dashboard.decoder) + (D.field "default" D.string) + + +emptyObjectDecoder : D.Decoder Dashboards +emptyObjectDecoder = + D.dict (D.fail "non-empty") |> D.map (\_ -> empty) + + +infoEncode : Info -> E.Value +infoEncode info = + E.object + [ ( "boards", E.dict identity Data.Dashboard.encode info.boards ) + , ( "default", E.string info.default ) + ] diff --git a/modules/webapp/src/main/elm/Data/Icons.elm b/modules/webapp/src/main/elm/Data/Icons.elm index 7a878d16..2a769a37 100644 --- a/modules/webapp/src/main/elm/Data/Icons.elm +++ b/modules/webapp/src/main/elm/Data/Icons.elm @@ -25,6 +25,7 @@ module Data.Icons exposing , customFieldTypeIcon , customFieldTypeIconString , customFieldTypeIconString2 + , dashboardIcon , date , date2 , dateIcon @@ -33,31 +34,30 @@ module Data.Icons exposing , direction2 , directionIcon , directionIcon2 + , documentationIcon , dueDate , dueDate2 , dueDateIcon , dueDateIcon2 - , editNotes - , editNotesIcon + , editIcon , equipment - , equipment2 , equipmentIcon - , equipmentIcon2 + , fileUploadIcon , folder - , folder2 , folderIcon - , folderIcon2 , gotifyIcon , itemDatesIcon , matrixIcon + , metadata + , metadataIcon + , notificationHooks + , notificationHooksIcon , organization - , organization2 , organizationIcon - , organizationIcon2 + , periodicTasks + , periodicTasksIcon , person - , person2 , personIcon - , personIcon2 , search , searchIcon , share @@ -67,11 +67,9 @@ module Data.Icons exposing , source2 , sourceIcon2 , tag - , tag2 , tagIcon - , tagIcon2 - , tags2 - , tagsIcon2 + , tags + , tagsIcon , trash , trashIcon ) @@ -83,6 +81,56 @@ import Svg import Svg.Attributes as SA +documentation : String +documentation = + "fa fa-question-circle" + + +documentationIcon : String -> Html msg +documentationIcon classes = + i [ class classes, class documentation ] [] + + +dashboard : String +dashboard = + "fa fa-house-user" + + +dashboardIcon : String -> Html msg +dashboardIcon classes = + i [ class classes, class dashboard ] [] + + +periodicTasks : String +periodicTasks = + "fa fa-history" + + +periodicTasksIcon : String -> Html msg +periodicTasksIcon classes = + i [ class classes, class periodicTasks ] [] + + +notificationHooks : String +notificationHooks = + "fa fa-comment font-thin" + + +notificationHooksIcon : String -> Html msg +notificationHooksIcon classes = + i [ class classes, class notificationHooks ] [] + + +metadata : String +metadata = + "fa fa-cubes" + + +metadataIcon : String -> Html msg +metadataIcon classes = + i [ class classes, class metadata ] [] + + trash : String trash = "fa fa-trash-alt text-red-500 dark:text-orange-600" @@ -112,6 +160,16 @@ source2 = "fa fa-upload" +fileUpload : String +fileUpload = + "fa fa-file-upload" + + +fileUploadIcon : String -> Html msg +fileUploadIcon classes = + i [ class classes, class fileUpload ] [] + + sourceIcon2 : String -> Html msg sourceIcon2 classes = i [ class (source2 ++ " " ++ classes) ] [] @@ -203,7 +261,7 @@ customFieldIcon2 classes = search : String search = - "search icon" + "fa fa-search" searchIcon : String -> Html msg @@ -213,11 +271,6 @@ searchIcon classes = folder : String folder = - "folder outline icon" - - -folder2 : String -folder2 = "fa fa-folder font-thin " @@ -226,11 +279,6 @@ folderIcon classes = i [ class (folder ++ " " ++ classes) ] [] -folderIcon2 : String -> Html msg -folderIcon2 classes = - i [ class (folder2 ++ " " ++ classes) ] [] - - concerned : String concerned = "crosshairs icon" @@ -329,14 +377,14 @@ dueDateIcon2 classes = i [ class (dueDate2 ++ " " ++ classes) ] [] -editNotes : String -editNotes = - "comment alternate outline icon" +edit : String +edit = + "fa fa-edit font-thin" -editNotesIcon : Html msg -editNotesIcon = - i [ class editNotes ] [] +editIcon : String -> Html msg +editIcon classes = + i [ class edit, class classes ] [] addFiles2 : String @@ -361,11 +409,6 @@ showQrIcon classes = tag : String tag = - "tag icon" - - -tag2 : String -tag2 = "fa fa-tag" @@ -374,19 +417,14 @@ tagIcon classes = i [ class (tag ++ " " ++ classes) ] [] -tagIcon2 : String -> Html msg -tagIcon2 classes = - i [ class (tag2 ++ " " ++ classes) ] [] - - -tags2 : String -tags2 = +tags : String +tags = "fa fa-tags" -tagsIcon2 : String -> Html msg -tagsIcon2 classes = - i [ class (tags2 ++ " " ++ classes) ] [] +tagsIcon : String -> Html msg +tagsIcon classes = + i [ class (tags ++ " " ++ classes) ] [] direction : String @@ -411,11 +449,6 @@ directionIcon2 classes = person : String person = - "user icon" - - -person2 : String -person2 = "fa fa-user" @@ -424,18 +457,8 @@ personIcon classes = i [ class (person ++ " " ++ classes) ] [] -personIcon2 : String -> Html msg -personIcon2 classes = - i [ class (person2 ++ " " ++ classes) ] [] - - organization : String organization = - "factory icon" - - -organization2 : String -organization2 = "fa fa-industry" @@ -444,18 +467,8 @@ organizationIcon classes = i [ class (organization ++ " " ++ classes) ] [] -organizationIcon2 : String -> Html msg -organizationIcon2 classes = - i [ class (organization2 ++ " " ++ classes) ] [] - - equipment : String equipment = - "box icon" - - -equipment2 : String -equipment2 = "fa fa-box" @@ -464,11 +477,6 @@ equipmentIcon classes = i [ class (equipment ++ " " ++ classes) ] [] -equipmentIcon2 : String -> Html msg -equipmentIcon2 classes = - i [ class (equipment2 ++ " " ++ classes) ] [] - - matrixIcon : String -> Html msg matrixIcon classes = Svg.svg diff --git a/modules/webapp/src/main/elm/Data/ItemColumn.elm b/modules/webapp/src/main/elm/Data/ItemColumn.elm new file mode 100644 index 00000000..231df4f1 --- /dev/null +++ b/modules/webapp/src/main/elm/Data/ItemColumn.elm @@ -0,0 +1,146 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Data.ItemColumn exposing (..) + +import Api.Model.ItemLight exposing (ItemLight) +import Data.ItemTemplate as IT exposing (TemplateContext) +import Json.Decode as D +import Json.Encode as E + + +type ItemColumn + = Name + | DateLong + | DateShort + | DueDateLong + | DueDateShort + | Folder + | Correspondent + | Concerning + | Tags + + +all : List ItemColumn +all = + [ Name, DateLong, DateShort, DueDateLong, DueDateShort, Folder, Correspondent, Concerning, Tags ] + + +renderString : TemplateContext -> ItemColumn -> ItemLight -> String +renderString ctx col item = + case col of + Name -> + IT.render IT.name ctx item + + DateShort -> + IT.render IT.dateShort ctx item + + DateLong -> + IT.render IT.dateLong ctx item + + DueDateShort -> + IT.render IT.dueDateShort ctx item + + DueDateLong -> + IT.render IT.dueDateLong ctx item + + Folder -> + IT.render IT.folder ctx item + + Correspondent -> + IT.render IT.correspondent ctx item + + Concerning -> + IT.render IT.concerning ctx item + + Tags -> + List.map .name item.tags + |> String.join ", " + + +asString : ItemColumn -> String +asString col = + case col of + Name -> + "name" + + DateShort -> + "dateshort" + + DateLong -> + "datelong" + + DueDateShort -> + "duedateshort" + + DueDateLong -> + "duedatelong" + + Folder -> + "folder" + + Correspondent -> + "correspondent" + + Concerning -> + "concerning" + + Tags -> + "tags" + + +fromString : String -> Maybe ItemColumn +fromString str = + case String.toLower str of + "name" -> + Just Name + + "dateshort" -> + Just DateShort + + "datelong" -> + Just DateLong + + "duedateshort" -> + Just DueDateShort + + "duedatelong" -> + Just DueDateLong + + "folder" -> + Just Folder + + "correspondent" -> + Just Correspondent + + "concerning" -> + Just Concerning + + "tags" -> + Just Tags + + _ -> + Nothing + + +encode : ItemColumn -> E.Value +encode col = + asString col |> E.string + + +decode : D.Decoder ItemColumn +decode = + let + from str = + case fromString str of + Just col -> + D.succeed col + + Nothing -> + D.fail ("Invalid column: " ++ str) + in + D.andThen from D.string diff --git a/modules/webapp/src/main/elm/Data/UiSettings.elm b/modules/webapp/src/main/elm/Data/UiSettings.elm index 50e1a540..30a6c129 100644 --- a/modules/webapp/src/main/elm/Data/UiSettings.elm +++ b/modules/webapp/src/main/elm/Data/UiSettings.elm @@ -16,8 +16,10 @@ module Data.UiSettings exposing , catColorFg2 , catColorString2 , defaults + , documentationSite , fieldHidden , fieldVisible + , getUiLanguage , merge , mergeDefaults , pdfUrl @@ -93,6 +95,9 @@ storedUiSettingsDecoder = maybeString = Decode.maybe Decode.string + + def = + defaults in Decode.succeed StoredUiSettings |> P.optional "itemSearchPageSize" maybeInt Nothing @@ -104,19 +109,19 @@ storedUiSettingsDecoder = |> P.optional "searchMenuTagCount" maybeInt Nothing |> P.optional "searchMenuTagCatCount" maybeInt Nothing |> P.optional "formFields" (Decode.maybe <| Decode.list Decode.string) Nothing - |> P.optional "itemDetailShortcuts" Decode.bool False - |> P.optional "searchMenuVisible" Decode.bool False - |> P.optional "editMenuVisible" Decode.bool False + |> P.optional "itemDetailShortcuts" Decode.bool def.itemDetailShortcuts + |> P.optional "searchMenuVisible" Decode.bool def.searchMenuVisible + |> P.optional "editMenuVisible" Decode.bool def.editMenuVisible |> P.optional "cardPreviewSize" maybeString Nothing |> P.optional "cardTitleTemplate" maybeString Nothing |> P.optional "cardSubtitleTemplate" maybeString Nothing - |> P.optional "searchStatsVisible" Decode.bool False - |> P.optional "cardPreviewFullWidth" Decode.bool False + |> P.optional "searchStatsVisible" Decode.bool def.searchStatsVisible + |> P.optional "cardPreviewFullWidth" Decode.bool def.cardPreviewFullWidth |> P.optional "uiTheme" maybeString Nothing - |> P.optional "sideMenuVisible" Decode.bool False - |> P.optional "powerSearchEnabled" Decode.bool False + |> P.optional "sideMenuVisible" Decode.bool def.sideMenuVisible + |> P.optional "powerSearchEnabled" Decode.bool def.powerSearchEnabled |> P.optional "uiLang" maybeString Nothing - |> P.optional "itemSearchShowGroups" Decode.bool True + |> P.optional "itemSearchShowGroups" Decode.bool def.itemSearchShowGroups |> P.optional "itemSearchArrange" maybeString Nothing @@ -444,6 +449,21 @@ pdfUrl settings flags originalUrl = Data.Pdf.serverUrl originalUrl +getUiLanguage : Flags -> UiSettings -> UiLanguage -> UiLanguage +getUiLanguage flags settings default = + case flags.account of + Just _ -> + settings.uiLang + + Nothing -> + default + + +documentationSite : String +documentationSite = + "https://docspell.org/docs" + + --- Helpers diff --git a/modules/webapp/src/main/elm/Messages.elm b/modules/webapp/src/main/elm/Messages.elm index 24399b15..d395eada 100644 --- a/modules/webapp/src/main/elm/Messages.elm +++ b/modules/webapp/src/main/elm/Messages.elm @@ -14,13 +14,14 @@ module Messages exposing import Messages.App import Messages.Page.CollectiveSettings -import Messages.Page.Home +import Messages.Page.Dashboard import Messages.Page.ItemDetail import Messages.Page.Login import Messages.Page.ManageData import Messages.Page.NewInvite import Messages.Page.Queue import Messages.Page.Register +import Messages.Page.Search import Messages.Page.Share import Messages.Page.ShareDetail import Messages.Page.Upload @@ -45,9 +46,10 @@ type alias Messages = , queue : Messages.Page.Queue.Texts , userSettings : Messages.Page.UserSettings.Texts , manageData : Messages.Page.ManageData.Texts - , home : Messages.Page.Home.Texts + , search : Messages.Page.Search.Texts , share : Messages.Page.Share.Texts , shareDetail : Messages.Page.ShareDetail.Texts + , dashboard : Messages.Page.Dashboard.Texts } @@ -112,9 +114,10 @@ gb = , queue = Messages.Page.Queue.gb , userSettings = Messages.Page.UserSettings.gb , manageData = Messages.Page.ManageData.gb - , home = Messages.Page.Home.gb + , search = Messages.Page.Search.gb , share = Messages.Page.Share.gb , shareDetail = Messages.Page.ShareDetail.gb + , dashboard = Messages.Page.Dashboard.gb } @@ -134,7 +137,8 @@ de = , queue = Messages.Page.Queue.de , userSettings = Messages.Page.UserSettings.de , manageData = Messages.Page.ManageData.de - , home = Messages.Page.Home.de + , search = Messages.Page.Search.de , share = Messages.Page.Share.de , shareDetail = Messages.Page.ShareDetail.de + , dashboard = Messages.Page.Dashboard.de } diff --git a/modules/webapp/src/main/elm/Messages/App.elm b/modules/webapp/src/main/elm/Messages/App.elm index 06d32a73..72fefaef 100644 --- a/modules/webapp/src/main/elm/Messages/App.elm +++ b/modules/webapp/src/main/elm/Messages/App.elm @@ -24,6 +24,7 @@ type alias Texts = , newInvites : String , help : String , newItemsArrived : String + , dashboard : String } @@ -40,6 +41,7 @@ gb = , newInvites = "New Invites" , help = "Help" , newItemsArrived = "New items arrived!" + , dashboard = "Dashboard" } @@ -56,4 +58,5 @@ de = , newInvites = "Neue Einladung" , help = "Hilfe (English)" , newItemsArrived = "Neue Dokumente eingetroffen!" + , dashboard = "Dashboard" } diff --git a/modules/webapp/src/main/elm/Messages/Basics.elm b/modules/webapp/src/main/elm/Messages/Basics.elm index 4b857f25..3dabacf0 100644 --- a/modules/webapp/src/main/elm/Messages/Basics.elm +++ b/modules/webapp/src/main/elm/Messages/Basics.elm @@ -45,6 +45,10 @@ type alias Texts = , customFields : String , direction : String , folderNotOwnerWarning : String + , shares : String + , sources : String + , periodicQueries : String + , notificationHooks : String } @@ -87,6 +91,10 @@ You are **not a member** of this folder. This item will be **hidden** from any search now. Use a folder where you are a member of to make this item visible. This message will disappear then. """ + , shares = "Shares" + , sources = "Sources" + , periodicQueries = "Periodic Queries" + , notificationHooks = "Webhooks" } @@ -130,4 +138,8 @@ URL hochgeladen werden, sind für dich in der Suche *nicht* sichtbar. Nutze lieber einen Ordner, dem Du als Mitglied zugeordnet bist. Diese Nachricht verschwindet dann. """ + , shares = "Freigaben" + , sources = "Quellen" + , periodicQueries = "Periodische Abfragen" + , notificationHooks = "Webhooks" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BookmarkChooser.elm b/modules/webapp/src/main/elm/Messages/Comp/BookmarkChooser.elm index a878a8e0..ff5c2efe 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/BookmarkChooser.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/BookmarkChooser.elm @@ -11,7 +11,9 @@ module Messages.Comp.BookmarkChooser exposing , gb ) +import Data.AccountScope exposing (AccountScope(..)) import Messages.Basics +import Messages.Data.AccountScope type alias Texts = @@ -25,8 +27,8 @@ type alias Texts = gb : Texts gb = { basics = Messages.Basics.gb - , userLabel = "Personal" - , collectiveLabel = "Collective" + , userLabel = Messages.Data.AccountScope.gb User + , collectiveLabel = Messages.Data.AccountScope.gb Collective , shareLabel = "Shares" } @@ -34,7 +36,7 @@ gb = de : Texts de = { basics = Messages.Basics.de - , userLabel = "Persönlich" - , collectiveLabel = "Kollektiv" + , userLabel = Messages.Data.AccountScope.de User + , collectiveLabel = Messages.Data.AccountScope.de Collective , shareLabel = "Freigaben" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BoxEdit.elm b/modules/webapp/src/main/elm/Messages/Comp/BoxEdit.elm new file mode 100644 index 00000000..8a9c02b1 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BoxEdit.elm @@ -0,0 +1,74 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BoxEdit exposing (Texts, de, gb) + +import Messages.Basics +import Messages.Comp.BoxMessageEdit +import Messages.Comp.BoxQueryEdit +import Messages.Comp.BoxStatsEdit +import Messages.Comp.BoxUploadEdit +import Messages.Data.BoxContent + + +type alias Texts = + { messageEdit : Messages.Comp.BoxMessageEdit.Texts + , uploadEdit : Messages.Comp.BoxUploadEdit.Texts + , queryEdit : Messages.Comp.BoxQueryEdit.Texts + , statsEdit : Messages.Comp.BoxStatsEdit.Texts + , boxContent : Messages.Data.BoxContent.Texts + , basics : Messages.Basics.Texts + , namePlaceholder : String + , visible : String + , decorations : String + , colspan : String + , contentProperties : String + , reallyDeleteBox : String + , moveToLeft : String + , moveToRight : String + , deleteBox : String + } + + +gb : Texts +gb = + { messageEdit = Messages.Comp.BoxMessageEdit.gb + , uploadEdit = Messages.Comp.BoxUploadEdit.gb + , queryEdit = Messages.Comp.BoxQueryEdit.gb + , statsEdit = Messages.Comp.BoxStatsEdit.gb + , boxContent = Messages.Data.BoxContent.gb + , basics = Messages.Basics.gb + , namePlaceholder = "Box name" + , visible = "Visible" + , decorations = "Box decorations" + , colspan = "Column span" + , contentProperties = "Content" + , reallyDeleteBox = "Really delete this box?" + , moveToLeft = "Move to left" + , moveToRight = "Move to right" + , deleteBox = "Delete box" + } + + +de : Texts +de = + { messageEdit = Messages.Comp.BoxMessageEdit.de + , uploadEdit = Messages.Comp.BoxUploadEdit.de + , queryEdit = Messages.Comp.BoxQueryEdit.de + , statsEdit = Messages.Comp.BoxStatsEdit.de + , boxContent = Messages.Data.BoxContent.de + , basics = Messages.Basics.de + , namePlaceholder = "Boxname" + , visible = "Sichtbar" + , decorations = "Kachel-Dekoration anzeigen" + , colspan = "Spalten überspannen" + , contentProperties = "Inhalt" + , reallyDeleteBox = "Die Kachel wirklich entfernen?" + , moveToLeft = "Nach links verschieben" + , moveToRight = "Nach rechts verschieben" + , deleteBox = "Kachel entfernen" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BoxMessageEdit.elm b/modules/webapp/src/main/elm/Messages/Comp/BoxMessageEdit.elm new file mode 100644 index 00000000..9c58f779 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BoxMessageEdit.elm @@ -0,0 +1,37 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BoxMessageEdit exposing (Texts, de, gb) + + +type alias Texts = + { titleLabel : String + , titlePlaceholder : String + , bodyLabel : String + , bodyPlaceholder : String + , infoText : String + } + + +gb : Texts +gb = + { titleLabel = "Title" + , titlePlaceholder = "Message title…" + , bodyLabel = "Body" + , bodyPlaceholder = "Message body…" + , infoText = "Markdown can be used in both fields for simple formatting." + } + + +de : Texts +de = + { titleLabel = "Titel" + , titlePlaceholder = "Titel…" + , bodyLabel = "Nachricht" + , bodyPlaceholder = "Text…" + , infoText = "Markdown kann in beiden Feldern für einfache Formatierung verwendet werden." + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BoxQueryEdit.elm b/modules/webapp/src/main/elm/Messages/Comp/BoxQueryEdit.elm new file mode 100644 index 00000000..90bf45af --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BoxQueryEdit.elm @@ -0,0 +1,34 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BoxQueryEdit exposing (Texts, de, gb) + +import Messages.Comp.BoxSearchQueryInput +import Messages.Comp.ItemColumnDropdown + + +type alias Texts = + { columnDropdown : Messages.Comp.ItemColumnDropdown.Texts + , searchQuery : Messages.Comp.BoxSearchQueryInput.Texts + , showColumnHeaders : String + } + + +gb : Texts +gb = + { columnDropdown = Messages.Comp.ItemColumnDropdown.gb + , searchQuery = Messages.Comp.BoxSearchQueryInput.gb + , showColumnHeaders = "Show column headers" + } + + +de : Texts +de = + { columnDropdown = Messages.Comp.ItemColumnDropdown.de + , searchQuery = Messages.Comp.BoxSearchQueryInput.de + , showColumnHeaders = "Spaltennamen anzeigen" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BoxQueryView.elm b/modules/webapp/src/main/elm/Messages/Comp/BoxQueryView.elm new file mode 100644 index 00000000..ab9ff4e9 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BoxQueryView.elm @@ -0,0 +1,57 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BoxQueryView exposing (Texts, de, gb) + +import Data.ItemTemplate as IT +import Http +import Messages.Basics +import Messages.Comp.HttpError +import Messages.Data.Direction +import Messages.Data.ItemColumn +import Messages.DateFormat as DF +import Messages.UiLanguage + + +type alias Texts = + { httpError : Http.Error -> String + , errorOccurred : String + , basics : Messages.Basics.Texts + , noResults : String + , templateCtx : IT.TemplateContext + , itemColumn : Messages.Data.ItemColumn.Texts + } + + +gb : Texts +gb = + { httpError = Messages.Comp.HttpError.gb + , errorOccurred = "Error retrieving data." + , basics = Messages.Basics.gb + , noResults = "No items found." + , templateCtx = + { dateFormatLong = DF.formatDateLong Messages.UiLanguage.English + , dateFormatShort = DF.formatDateShort Messages.UiLanguage.English + , directionLabel = Messages.Data.Direction.gb + } + , itemColumn = Messages.Data.ItemColumn.gb + } + + +de : Texts +de = + { httpError = Messages.Comp.HttpError.de + , errorOccurred = "Fehler beim Laden der Daten." + , basics = Messages.Basics.de + , noResults = "Keine Dokumente gefunden." + , templateCtx = + { dateFormatLong = DF.formatDateLong Messages.UiLanguage.German + , dateFormatShort = DF.formatDateShort Messages.UiLanguage.German + , directionLabel = Messages.Data.Direction.de + } + , itemColumn = Messages.Data.ItemColumn.de + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BoxSearchQueryInput.elm b/modules/webapp/src/main/elm/Messages/Comp/BoxSearchQueryInput.elm new file mode 100644 index 00000000..5e202e0d --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BoxSearchQueryInput.elm @@ -0,0 +1,36 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BoxSearchQueryInput exposing (Texts, de, gb) + +import Messages.Comp.BookmarkDropdown + + +type alias Texts = + { bookmarkDropdown : Messages.Comp.BookmarkDropdown.Texts + , switchToBookmark : String + , switchToQuery : String + , searchPlaceholder : String + } + + +gb : Texts +gb = + { bookmarkDropdown = Messages.Comp.BookmarkDropdown.gb + , switchToBookmark = "Bookmarks" + , switchToQuery = "Search query" + , searchPlaceholder = "Search…" + } + + +de : Texts +de = + { bookmarkDropdown = Messages.Comp.BookmarkDropdown.de + , switchToBookmark = "Bookmarks" + , switchToQuery = "Suchabfrage" + , searchPlaceholder = "Abfrage…" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BoxStatsEdit.elm b/modules/webapp/src/main/elm/Messages/Comp/BoxStatsEdit.elm new file mode 100644 index 00000000..412583db --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BoxStatsEdit.elm @@ -0,0 +1,39 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BoxStatsEdit exposing (Texts, de, gb) + +import Messages.Comp.BoxSearchQueryInput + + +type alias Texts = + { searchQuery : Messages.Comp.BoxSearchQueryInput.Texts + , fieldStatistics : String + , basicNumbers : String + , showLabel : String + , showItemCount : String + } + + +gb : Texts +gb = + { searchQuery = Messages.Comp.BoxSearchQueryInput.gb + , fieldStatistics = "Field statistics" + , basicNumbers = "Basic numbers" + , showLabel = "Display" + , showItemCount = "Show item count" + } + + +de : Texts +de = + { searchQuery = Messages.Comp.BoxSearchQueryInput.de + , fieldStatistics = "Benutzerfeld Statistiken" + , basicNumbers = "Allgemeine Zahlen" + , showLabel = "Anzeige" + , showItemCount = "Gesamtanzahl Dokumente mit anzeigen" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BoxStatsView.elm b/modules/webapp/src/main/elm/Messages/Comp/BoxStatsView.elm new file mode 100644 index 00000000..2ff0eda7 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BoxStatsView.elm @@ -0,0 +1,39 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BoxStatsView exposing (Texts, de, gb) + +import Http +import Messages.Basics +import Messages.Comp.HttpError +import Messages.Comp.SearchStatsView + + +type alias Texts = + { httpError : Http.Error -> String + , errorOccurred : String + , statsView : Messages.Comp.SearchStatsView.Texts + , basics : Messages.Basics.Texts + } + + +gb : Texts +gb = + { httpError = Messages.Comp.HttpError.gb + , errorOccurred = "Error retrieving data." + , statsView = Messages.Comp.SearchStatsView.gb + , basics = Messages.Basics.gb + } + + +de : Texts +de = + { httpError = Messages.Comp.HttpError.de + , errorOccurred = "Fehler beim Laden der Daten." + , statsView = Messages.Comp.SearchStatsView.de + , basics = Messages.Basics.de + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BoxUploadEdit.elm b/modules/webapp/src/main/elm/Messages/Comp/BoxUploadEdit.elm new file mode 100644 index 00000000..b423afdc --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BoxUploadEdit.elm @@ -0,0 +1,31 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BoxUploadEdit exposing (Texts, de, gb) + + +type alias Texts = + { sourceLabel : String + , sourcePlaceholder : String + , infoText : String + } + + +gb : Texts +gb = + { sourceLabel = "Source" + , sourcePlaceholder = "Choose source…" + , infoText = "Optionally choose a source otherwise default settings apply to all uploads." + } + + +de : Texts +de = + { sourceLabel = "Quelle" + , sourcePlaceholder = "Quelle…" + , infoText = "Optional kann eine Quelle als Einstellung gewählt werden, sonst werden Standardeinstellungen verwendet." + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BoxUploadView.elm b/modules/webapp/src/main/elm/Messages/Comp/BoxUploadView.elm new file mode 100644 index 00000000..1cc464ec --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BoxUploadView.elm @@ -0,0 +1,30 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BoxUploadView exposing (Texts, de, gb) + +import Messages.Comp.UploadForm + + +type alias Texts = + { uploadForm : Messages.Comp.UploadForm.Texts + , moreOptions : String + } + + +gb : Texts +gb = + { uploadForm = Messages.Comp.UploadForm.gb + , moreOptions = "More options…" + } + + +de : Texts +de = + { uploadForm = Messages.Comp.UploadForm.de + , moreOptions = "More options…" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BoxView.elm b/modules/webapp/src/main/elm/Messages/Comp/BoxView.elm new file mode 100644 index 00000000..d4bbfa1d --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BoxView.elm @@ -0,0 +1,35 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BoxView exposing (Texts, de, gb) + +import Messages.Comp.BoxQueryView +import Messages.Comp.BoxStatsView +import Messages.Comp.BoxUploadView + + +type alias Texts = + { queryView : Messages.Comp.BoxQueryView.Texts + , statsView : Messages.Comp.BoxStatsView.Texts + , uploadView : Messages.Comp.BoxUploadView.Texts + } + + +gb : Texts +gb = + { queryView = Messages.Comp.BoxQueryView.gb + , statsView = Messages.Comp.BoxStatsView.gb + , uploadView = Messages.Comp.BoxUploadView.gb + } + + +de : Texts +de = + { queryView = Messages.Comp.BoxQueryView.de + , statsView = Messages.Comp.BoxStatsView.de + , uploadView = Messages.Comp.BoxUploadView.de + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/DashboardEdit.elm b/modules/webapp/src/main/elm/Messages/Comp/DashboardEdit.elm new file mode 100644 index 00000000..8c4249a9 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/DashboardEdit.elm @@ -0,0 +1,57 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.DashboardEdit exposing (Texts, de, gb) + +import Messages.Basics +import Messages.Comp.BoxEdit +import Messages.Data.AccountScope +import Messages.Data.BoxContent + + +type alias Texts = + { boxView : Messages.Comp.BoxEdit.Texts + , boxContent : Messages.Data.BoxContent.Texts + , basics : Messages.Basics.Texts + , accountScope : Messages.Data.AccountScope.Texts + , namePlaceholder : String + , columns : String + , dashboardBoxes : String + , newBox : String + , defaultDashboard : String + , gap : String + } + + +gb : Texts +gb = + { boxView = Messages.Comp.BoxEdit.gb + , boxContent = Messages.Data.BoxContent.gb + , basics = Messages.Basics.gb + , accountScope = Messages.Data.AccountScope.gb + , namePlaceholder = "Dashboard name" + , columns = "Columns" + , dashboardBoxes = "Dashboard Boxes" + , newBox = "New box" + , defaultDashboard = "Default Dashboard" + , gap = "Gap" + } + + +de : Texts +de = + { boxView = Messages.Comp.BoxEdit.de + , boxContent = Messages.Data.BoxContent.de + , basics = Messages.Basics.de + , accountScope = Messages.Data.AccountScope.de + , namePlaceholder = "Dashboardname" + , columns = "Spalten" + , dashboardBoxes = "Dashboard Kacheln" + , newBox = "Neue Kachel" + , defaultDashboard = "Standard Dashboard" + , gap = "Abstand" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/DashboardManage.elm b/modules/webapp/src/main/elm/Messages/Comp/DashboardManage.elm new file mode 100644 index 00000000..03253b8c --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/DashboardManage.elm @@ -0,0 +1,51 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.DashboardManage exposing (Texts, de, gb) + +import Http +import Messages.Basics +import Messages.Comp.DashboardEdit +import Messages.Comp.HttpError + + +type alias Texts = + { basics : Messages.Basics.Texts + , dashboardEdit : Messages.Comp.DashboardEdit.Texts + , httpError : Http.Error -> String + , reallyDeleteDashboard : String + , nameEmpty : String + , nameExists : String + , createDashboard : String + , copyDashboard : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , dashboardEdit = Messages.Comp.DashboardEdit.gb + , httpError = Messages.Comp.HttpError.gb + , reallyDeleteDashboard = "Really delete this dashboard?" + , nameEmpty = "The name must not be empty." + , nameExists = "The name is already in use." + , createDashboard = "New" + , copyDashboard = "Copy" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , dashboardEdit = Messages.Comp.DashboardEdit.de + , httpError = Messages.Comp.HttpError.de + , reallyDeleteDashboard = "Das Dashboard wirklich entfernen?" + , nameEmpty = "Ein Name muss angegeben werden." + , nameExists = "Der Name wird bereits verwendet." + , createDashboard = "Neu" + , copyDashboard = "Kopie" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/DashboardView.elm b/modules/webapp/src/main/elm/Messages/Comp/DashboardView.elm new file mode 100644 index 00000000..53f292a0 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/DashboardView.elm @@ -0,0 +1,27 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.DashboardView exposing (Texts, de, gb) + +import Messages.Comp.BoxView + + +type alias Texts = + { boxView : Messages.Comp.BoxView.Texts + } + + +gb : Texts +gb = + { boxView = Messages.Comp.BoxView.gb + } + + +de : Texts +de = + { boxView = Messages.Comp.BoxView.de + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemColumnDropdown.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemColumnDropdown.elm new file mode 100644 index 00000000..fd66b3b1 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/ItemColumnDropdown.elm @@ -0,0 +1,38 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.ItemColumnDropdown exposing + ( Texts + , de + , gb + ) + +import Messages.Basics +import Messages.Data.ItemColumn + + +type alias Texts = + { basics : Messages.Basics.Texts + , column : Messages.Data.ItemColumn.Texts + , placeholder : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , column = Messages.Data.ItemColumn.gb + , placeholder = "Choose…" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , column = Messages.Data.ItemColumn.de + , placeholder = "Wähle…" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/UploadForm.elm b/modules/webapp/src/main/elm/Messages/Comp/UploadForm.elm new file mode 100644 index 00000000..502aa1a2 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/UploadForm.elm @@ -0,0 +1,98 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.UploadForm exposing (Texts, de, gb) + +import Data.Language exposing (Language) +import Messages.Basics +import Messages.Comp.Dropzone +import Messages.Data.Language + + +type alias Texts = + { basics : Messages.Basics.Texts + , dropzone : Messages.Comp.Dropzone.Texts + , reset : String + , allFilesOneItem : String + , skipExistingFiles : String + , language : String + , languageInfo : String + , uploadErrorMessage : String + , successBox : + { allFilesUploaded : String + , line1 : String + , itemsPage : String + , line2 : String + , processingPage : String + , line3 : String + , resetLine1 : String + , reset : String + , resetLine2 : String + } + , selectedFiles : String + , languageLabel : Language -> String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , dropzone = Messages.Comp.Dropzone.gb + , reset = "Reset" + , allFilesOneItem = "All files are one single item" + , skipExistingFiles = "Skip files already present in docspell" + , language = "Language" + , languageInfo = + "Used for text extraction and analysis. The collective's " + ++ "default language is used if not specified here." + , uploadErrorMessage = "There were errors uploading some files." + , successBox = + { allFilesUploaded = "All files uploaded" + , line1 = + "Your files have been successfully uploaded. " + ++ "They are now being processed. Check the " + , itemsPage = "Items Page" + , line2 = " later where the files will arrive eventually. Or go to the " + , processingPage = "Processing Page" + , line3 = " to view the current processing state." + , resetLine1 = " Click " + , reset = "Reset" + , resetLine2 = " to upload more files." + } + , selectedFiles = "Selected Files" + , languageLabel = Messages.Data.Language.gb + } + + +de : Texts +de = + { basics = Messages.Basics.de + , dropzone = Messages.Comp.Dropzone.de + , reset = "Zurücksetzen" + , allFilesOneItem = "Alle Dateien sind ein Dokument" + , skipExistingFiles = "Lasse Dateien aus, die schon in Docspell sind" + , language = "Sprache" + , languageInfo = + "Wird für Texterkennung und -analyse verwendet. Die Standardsprache des Kollektivs " + ++ "wird verwendet, falls hier nicht angegeben." + , uploadErrorMessage = "Es gab Fehler beim Hochladen der Dateien." + , successBox = + { allFilesUploaded = "Alle Dateien hochgeladen" + , line1 = + "Deine Dateien wurden erfolgreich hochgeladen und sie werden nun verarbeitet. " + ++ "Gehe nachher zur " + , itemsPage = "Hauptseite" + , line2 = " wo die Dateien als Dokumente erscheinen werden oder gehe zur " + , processingPage = "Verarbeitungsseite," + , line3 = " welche einen Einblick in den aktuellen Status gibt." + , resetLine1 = " Klicke " + , reset = "Zurücksetzen" + , resetLine2 = " um weitere Dateien hochzuladen." + } + , selectedFiles = "Ausgewählte Dateien" + , languageLabel = Messages.Data.Language.de + } diff --git a/modules/webapp/src/main/elm/Messages/Data/AccountScope.elm b/modules/webapp/src/main/elm/Messages/Data/AccountScope.elm new file mode 100644 index 00000000..5f135ecb --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Data/AccountScope.elm @@ -0,0 +1,24 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Data.AccountScope exposing (Texts, de, gb) + +import Data.AccountScope exposing (AccountScope) + + +type alias Texts = + AccountScope -> String + + +gb : Texts +gb = + Data.AccountScope.fold "Personal" "Collective" + + +de : Texts +de = + Data.AccountScope.fold "Persönlich" "Kollektiv" diff --git a/modules/webapp/src/main/elm/Messages/Data/BoxContent.elm b/modules/webapp/src/main/elm/Messages/Data/BoxContent.elm new file mode 100644 index 00000000..4942c72a --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Data/BoxContent.elm @@ -0,0 +1,61 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Data.BoxContent exposing (Texts, de, gb) + +import Data.BoxContent exposing (BoxContent(..)) + + +type alias Texts = + { forContent : BoxContent -> String + , queryBox : String + , statsBox : String + , messageBox : String + , uploadBox : String + } + + +gb : Texts +gb = + updateForContent + { forContent = \_ -> "" + , queryBox = "Query box" + , statsBox = "Statistics box" + , messageBox = "Message box" + , uploadBox = "Upload box" + } + + +de : Texts +de = + updateForContent + { forContent = \_ -> "" + , queryBox = "Suchabfrage Kachel" + , statsBox = "Statistik Kachel" + , messageBox = "Mitteilung Kachel" + , uploadBox = "Datei hochladen Kachel" + } + + +updateForContent : Texts -> Texts +updateForContent init = + { init + | forContent = + \cnt -> + case cnt of + BoxMessage _ -> + init.messageBox + + BoxUpload _ -> + init.uploadBox + + BoxQuery _ -> + init.queryBox + + BoxStats _ -> + init.statsBox + } diff --git a/modules/webapp/src/main/elm/Messages/Data/ItemColumn.elm b/modules/webapp/src/main/elm/Messages/Data/ItemColumn.elm new file mode 100644 index 00000000..9d91fb91 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Data/ItemColumn.elm @@ -0,0 +1,122 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Data.ItemColumn exposing (Texts, de, gb) + +import Data.ItemColumn exposing (ItemColumn(..)) + + +type alias Texts = + { header : ItemColumn -> String + , label : ItemColumn -> String + } + + +gb : Texts +gb = + let + headerName col = + case col of + Name -> + "Name" + + DateLong -> + "Date" + + DateShort -> + "Date" + + DueDateLong -> + "Due date" + + DueDateShort -> + "Due date" + + Folder -> + "Folder" + + Correspondent -> + "Correspondent" + + Concerning -> + "Concerning" + + Tags -> + "Tags" + in + { header = headerName + , label = + \col -> + case col of + DateShort -> + headerName col ++ " (short)" + + DateLong -> + headerName col ++ " (long)" + + DueDateShort -> + headerName col ++ " (short)" + + DueDateLong -> + headerName col ++ " (long)" + + _ -> + headerName col + } + + +de : Texts +de = + let + headerName col = + case col of + Name -> + "Name" + + DateLong -> + "Datum" + + DateShort -> + "Datum" + + DueDateLong -> + "Fälligkeitsdatum" + + DueDateShort -> + "Fälligkeitsdatum" + + Folder -> + "Ordner" + + Correspondent -> + "Korrespondent" + + Concerning -> + "Betreffend" + + Tags -> + "Tags" + in + { header = headerName + , label = + \col -> + case col of + DateShort -> + headerName col ++ " (kurz)" + + DateLong -> + headerName col ++ " (lang)" + + DueDateShort -> + headerName col ++ " (kurz)" + + DueDateLong -> + headerName col ++ " (lang)" + + _ -> + headerName col + } diff --git a/modules/webapp/src/main/elm/Messages/DateFormat.elm b/modules/webapp/src/main/elm/Messages/DateFormat.elm index 01ac198f..6b247b8f 100644 --- a/modules/webapp/src/main/elm/Messages/DateFormat.elm +++ b/modules/webapp/src/main/elm/Messages/DateFormat.elm @@ -10,6 +10,7 @@ module Messages.DateFormat exposing , formatDateLong , formatDateShort , formatDateTimeLong + , formatDateTimeShort ) import DateFormat exposing (Token) @@ -68,6 +69,11 @@ formatDateShort lang millis = format lang .dateShort millis +formatDateTimeShort : UiLanguage -> Int -> String +formatDateTimeShort lang millis = + format lang .dateTimeShort millis + + --- Language Definitions diff --git a/modules/webapp/src/main/elm/Messages/Page/CollectiveSettings.elm b/modules/webapp/src/main/elm/Messages/Page/CollectiveSettings.elm index 4ca75f93..45a5caa3 100644 --- a/modules/webapp/src/main/elm/Messages/Page/CollectiveSettings.elm +++ b/modules/webapp/src/main/elm/Messages/Page/CollectiveSettings.elm @@ -29,10 +29,8 @@ type alias Texts = , httpError : Http.Error -> String , collectiveSettings : String , insights : String - , sources : String , settings : String , users : String - , shares : String , user : String , collective : String , size : String @@ -51,10 +49,8 @@ gb = , httpError = Messages.Comp.HttpError.gb , collectiveSettings = "Collective Settings" , insights = "Insights" - , sources = "Sources" , settings = "Settings" , users = "Users" - , shares = "Shares" , user = "User" , collective = "Collective" , size = "Size" @@ -73,10 +69,8 @@ de = , httpError = Messages.Comp.HttpError.de , collectiveSettings = "Kollektiveinstellungen" , insights = "Statistiken" - , sources = "Quellen" , settings = "Einstellungen" , users = "Benutzer" - , shares = "Freigaben" , user = "Benutzer" , collective = "Kollektiv" , size = "Größe" diff --git a/modules/webapp/src/main/elm/Messages/Page/Dashboard.elm b/modules/webapp/src/main/elm/Messages/Page/Dashboard.elm new file mode 100644 index 00000000..c991382c --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Page/Dashboard.elm @@ -0,0 +1,117 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Page.Dashboard exposing (Texts, de, gb) + +import Messages.Basics +import Messages.Comp.BookmarkChooser +import Messages.Comp.DashboardManage +import Messages.Comp.DashboardView +import Messages.Comp.EquipmentManage +import Messages.Comp.FolderManage +import Messages.Comp.NotificationHookManage +import Messages.Comp.OrgManage +import Messages.Comp.PeriodicQueryTaskManage +import Messages.Comp.PersonManage +import Messages.Comp.ShareManage +import Messages.Comp.SourceManage +import Messages.Comp.TagManage +import Messages.Comp.UploadForm +import Messages.Data.AccountScope +import Messages.Page.DefaultDashboard + + +type alias Texts = + { basics : Messages.Basics.Texts + , bookmarkChooser : Messages.Comp.BookmarkChooser.Texts + , notificationHookManage : Messages.Comp.NotificationHookManage.Texts + , periodicQueryManage : Messages.Comp.PeriodicQueryTaskManage.Texts + , sourceManage : Messages.Comp.SourceManage.Texts + , shareManage : Messages.Comp.ShareManage.Texts + , organizationManage : Messages.Comp.OrgManage.Texts + , personManage : Messages.Comp.PersonManage.Texts + , equipManage : Messages.Comp.EquipmentManage.Texts + , tagManage : Messages.Comp.TagManage.Texts + , folderManage : Messages.Comp.FolderManage.Texts + , uploadForm : Messages.Comp.UploadForm.Texts + , dashboard : Messages.Comp.DashboardView.Texts + , dashboardManage : Messages.Comp.DashboardManage.Texts + , defaultDashboard : Messages.Page.DefaultDashboard.Texts + , accountScope : Messages.Data.AccountScope.Texts + , manage : String + , dashboardLink : String + , bookmarks : String + , misc : String + , settings : String + , documentation : String + , uploadFiles : String + , editDashboard : String + , dashboards : String + , predefinedMessage : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , bookmarkChooser = Messages.Comp.BookmarkChooser.gb + , notificationHookManage = Messages.Comp.NotificationHookManage.gb + , periodicQueryManage = Messages.Comp.PeriodicQueryTaskManage.gb + , sourceManage = Messages.Comp.SourceManage.gb + , shareManage = Messages.Comp.ShareManage.gb + , organizationManage = Messages.Comp.OrgManage.gb + , personManage = Messages.Comp.PersonManage.gb + , equipManage = Messages.Comp.EquipmentManage.gb + , tagManage = Messages.Comp.TagManage.gb + , folderManage = Messages.Comp.FolderManage.gb + , uploadForm = Messages.Comp.UploadForm.gb + , dashboard = Messages.Comp.DashboardView.gb + , dashboardManage = Messages.Comp.DashboardManage.gb + , defaultDashboard = Messages.Page.DefaultDashboard.gb + , accountScope = Messages.Data.AccountScope.gb + , manage = "Manage" + , dashboardLink = "Dasbhoard" + , bookmarks = "Bookmarks" + , misc = "Misc" + , settings = "Settings" + , documentation = "Documentation" + , uploadFiles = "Upload documents" + , editDashboard = "Edit Dashboard" + , dashboards = "Dashboards" + , predefinedMessage = "This dashboard is predefined one that cannot be deleted. It is replaced with the first one you save." + } + + +de : Texts +de = + { basics = Messages.Basics.de + , bookmarkChooser = Messages.Comp.BookmarkChooser.de + , notificationHookManage = Messages.Comp.NotificationHookManage.de + , periodicQueryManage = Messages.Comp.PeriodicQueryTaskManage.de + , sourceManage = Messages.Comp.SourceManage.de + , shareManage = Messages.Comp.ShareManage.de + , organizationManage = Messages.Comp.OrgManage.de + , personManage = Messages.Comp.PersonManage.de + , equipManage = Messages.Comp.EquipmentManage.de + , tagManage = Messages.Comp.TagManage.de + , folderManage = Messages.Comp.FolderManage.de + , uploadForm = Messages.Comp.UploadForm.de + , dashboard = Messages.Comp.DashboardView.de + , dashboardManage = Messages.Comp.DashboardManage.de + , defaultDashboard = Messages.Page.DefaultDashboard.de + , accountScope = Messages.Data.AccountScope.de + , manage = "Verwalten" + , dashboardLink = "Dasbhoard" + , bookmarks = "Bookmarks" + , misc = "Anderes" + , settings = "Einstellungen" + , documentation = "Dokumentation" + , uploadFiles = "Dokumente hochladen" + , editDashboard = "Dashboard ändern" + , dashboards = "Dashboards" + , predefinedMessage = "Dieses Dashboard ist vordefiniert und kann nicht entfernt werden. Es wird durch ein gespeichertes ersetzt." + } diff --git a/modules/webapp/src/main/elm/Messages/Page/DefaultDashboard.elm b/modules/webapp/src/main/elm/Messages/Page/DefaultDashboard.elm new file mode 100644 index 00000000..62b1052a --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Page/DefaultDashboard.elm @@ -0,0 +1,48 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Page.DefaultDashboard exposing (Texts, de, gb) + +import Messages.Basics + + +type alias Texts = + { basics : Messages.Basics.Texts + , default : String + , welcomeName : String + , welcomeTitle : String + , welcomeBody : String + , summaryName : String + , dueInDays : Int -> String + , newDocsName : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , default = "Default" + , welcomeName = "Welcome Message" + , welcomeTitle = "# Welcome to Docspell" + , welcomeBody = "Docspell keeps your documents organized." + , summaryName = "Summary" + , dueInDays = \n -> "Due in " ++ String.fromInt n ++ " days" + , newDocsName = "New Documents" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , default = "Standard" + , welcomeName = "Willkommens-Nachricht" + , welcomeTitle = "# Willkommen zu Docspell" + , welcomeBody = "Docspell behält die Übersicht über deine Dokumene." + , summaryName = "Zahlen" + , dueInDays = \n -> "Fällig in " ++ String.fromInt n ++ " Tagen" + , newDocsName = "Neue Dokumente" + } diff --git a/modules/webapp/src/main/elm/Messages/Page/Home.elm b/modules/webapp/src/main/elm/Messages/Page/Search.elm similarity index 96% rename from modules/webapp/src/main/elm/Messages/Page/Home.elm rename to modules/webapp/src/main/elm/Messages/Page/Search.elm index 38b5386f..2bde1975 100644 --- a/modules/webapp/src/main/elm/Messages/Page/Home.elm +++ b/modules/webapp/src/main/elm/Messages/Page/Search.elm @@ -5,7 +5,7 @@ -} -module Messages.Page.Home exposing +module Messages.Page.Search exposing ( Texts , de , gb @@ -17,14 +17,14 @@ import Messages.Comp.ItemCardList import Messages.Comp.ItemMerge import Messages.Comp.PublishItems import Messages.Comp.SearchStatsView -import Messages.Page.HomeSideMenu +import Messages.Page.SearchSideMenu type alias Texts = { basics : Messages.Basics.Texts , itemCardList : Messages.Comp.ItemCardList.Texts , searchStatsView : Messages.Comp.SearchStatsView.Texts - , sideMenu : Messages.Page.HomeSideMenu.Texts + , sideMenu : Messages.Page.SearchSideMenu.Texts , itemMerge : Messages.Comp.ItemMerge.Texts , publishItems : Messages.Comp.PublishItems.Texts , bookmarkManage : Messages.Comp.BookmarkQueryManage.Texts @@ -66,7 +66,7 @@ gb = { basics = Messages.Basics.gb , itemCardList = Messages.Comp.ItemCardList.gb , searchStatsView = Messages.Comp.SearchStatsView.gb - , sideMenu = Messages.Page.HomeSideMenu.gb + , sideMenu = Messages.Page.SearchSideMenu.gb , itemMerge = Messages.Comp.ItemMerge.gb , publishItems = Messages.Comp.PublishItems.gb , bookmarkManage = Messages.Comp.BookmarkQueryManage.gb @@ -108,7 +108,7 @@ de = { basics = Messages.Basics.de , itemCardList = Messages.Comp.ItemCardList.de , searchStatsView = Messages.Comp.SearchStatsView.de - , sideMenu = Messages.Page.HomeSideMenu.de + , sideMenu = Messages.Page.SearchSideMenu.de , itemMerge = Messages.Comp.ItemMerge.de , publishItems = Messages.Comp.PublishItems.de , bookmarkManage = Messages.Comp.BookmarkQueryManage.de diff --git a/modules/webapp/src/main/elm/Messages/Page/HomeSideMenu.elm b/modules/webapp/src/main/elm/Messages/Page/SearchSideMenu.elm similarity index 96% rename from modules/webapp/src/main/elm/Messages/Page/HomeSideMenu.elm rename to modules/webapp/src/main/elm/Messages/Page/SearchSideMenu.elm index 0d4bac38..b87949c5 100644 --- a/modules/webapp/src/main/elm/Messages/Page/HomeSideMenu.elm +++ b/modules/webapp/src/main/elm/Messages/Page/SearchSideMenu.elm @@ -5,7 +5,7 @@ -} -module Messages.Page.HomeSideMenu exposing +module Messages.Page.SearchSideMenu exposing ( Texts , de , gb diff --git a/modules/webapp/src/main/elm/Messages/Page/Upload.elm b/modules/webapp/src/main/elm/Messages/Page/Upload.elm index 6f4d3973..b694403b 100644 --- a/modules/webapp/src/main/elm/Messages/Page/Upload.elm +++ b/modules/webapp/src/main/elm/Messages/Page/Upload.elm @@ -11,92 +11,21 @@ module Messages.Page.Upload exposing , gb ) -import Data.Language exposing (Language) -import Messages.Basics -import Messages.Comp.Dropzone -import Messages.Data.Language +import Messages.Comp.UploadForm type alias Texts = - { basics : Messages.Basics.Texts - , dropzone : Messages.Comp.Dropzone.Texts - , reset : String - , allFilesOneItem : String - , skipExistingFiles : String - , language : String - , languageInfo : String - , uploadErrorMessage : String - , successBox : - { allFilesUploaded : String - , line1 : String - , itemsPage : String - , line2 : String - , processingPage : String - , line3 : String - , resetLine1 : String - , reset : String - , resetLine2 : String - } - , selectedFiles : String - , languageLabel : Language -> String + { uploadForm : Messages.Comp.UploadForm.Texts } gb : Texts gb = - { basics = Messages.Basics.gb - , dropzone = Messages.Comp.Dropzone.gb - , reset = "Reset" - , allFilesOneItem = "All files are one single item" - , skipExistingFiles = "Skip files already present in docspell" - , language = "Language" - , languageInfo = - "Used for text extraction and analysis. The collective's " - ++ "default language is used if not specified here." - , uploadErrorMessage = "There were errors uploading some files." - , successBox = - { allFilesUploaded = "All files uploaded" - , line1 = - "Your files have been successfully uploaded. " - ++ "They are now being processed. Check the " - , itemsPage = "Items Page" - , line2 = " later where the files will arrive eventually. Or go to the " - , processingPage = "Processing Page" - , line3 = " to view the current processing state." - , resetLine1 = " Click " - , reset = "Reset" - , resetLine2 = " to upload more files." - } - , selectedFiles = "Selected Files" - , languageLabel = Messages.Data.Language.gb + { uploadForm = Messages.Comp.UploadForm.gb } de : Texts de = - { basics = Messages.Basics.de - , dropzone = Messages.Comp.Dropzone.de - , reset = "Zurücksetzen" - , allFilesOneItem = "Alle Dateien sind ein Dokument" - , skipExistingFiles = "Lasse Dateien aus, die schon in Docspell sind" - , language = "Sprache" - , languageInfo = - "Wird für Texterkennung und -analyse verwendet. Die Standardsprache des Kollektivs " - ++ "wird verwendet, falls hier nicht angegeben." - , uploadErrorMessage = "Es gab Fehler beim Hochladen der Dateien." - , successBox = - { allFilesUploaded = "Alle Dateien hochgeladen" - , line1 = - "Deine Dateien wurden erfolgreich hochgeladen und sie werden nun verarbeitet. " - ++ "Gehe nachher zur " - , itemsPage = "Hauptseite" - , line2 = " wo die Dateien als Dokumente erscheinen werden oder gehe zur " - , processingPage = "Verarbeitungsseite," - , line3 = " welche einen Einblick in den aktuellen Status gibt." - , resetLine1 = " Klicke " - , reset = "Zurücksetzen" - , resetLine2 = " um weitere Dateien hochzuladen." - } - , selectedFiles = "Ausgewählte Dateien" - , languageLabel = Messages.Data.Language.de + { uploadForm = Messages.Comp.UploadForm.de } diff --git a/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm b/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm index dcef8b6c..99879774 100644 --- a/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm +++ b/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm @@ -11,6 +11,7 @@ module Messages.Page.UserSettings exposing , gb ) +import Messages.Basics import Messages.Comp.ChangePasswordForm import Messages.Comp.DueItemsTaskManage import Messages.Comp.EmailSettingsManage @@ -24,7 +25,8 @@ import Messages.Comp.UiSettingsManage type alias Texts = - { changePasswordForm : Messages.Comp.ChangePasswordForm.Texts + { basics : Messages.Basics.Texts + , changePasswordForm : Messages.Comp.ChangePasswordForm.Texts , uiSettingsManage : Messages.Comp.UiSettingsManage.Texts , emailSettingsManage : Messages.Comp.EmailSettingsManage.Texts , imapSettingsManage : Messages.Comp.ImapSettingsManage.Texts @@ -46,8 +48,6 @@ type alias Texts = , scanMailboxInfo1 : String , scanMailboxInfo2 : String , otpMenu : String - , webhooks : String - , genericQueries : String , dueItems : String , notificationInfoText : String , webhookInfoText : String @@ -60,7 +60,8 @@ type alias Texts = gb : Texts gb = - { changePasswordForm = Messages.Comp.ChangePasswordForm.gb + { basics = Messages.Basics.gb + , changePasswordForm = Messages.Comp.ChangePasswordForm.gb , uiSettingsManage = Messages.Comp.UiSettingsManage.gb , emailSettingsManage = Messages.Comp.EmailSettingsManage.gb , imapSettingsManage = Messages.Comp.ImapSettingsManage.gb @@ -96,8 +97,6 @@ gb = adjust the schedule to avoid reading over the same mails again.""" , otpMenu = "Two Factor Authentication" - , webhooks = "Webhooks" - , genericQueries = "Generic Queries" , dueItems = "Due Items Query" , notificationInfoText = """ @@ -125,7 +124,8 @@ must be created before. de : Texts de = - { changePasswordForm = Messages.Comp.ChangePasswordForm.de + { basics = Messages.Basics.de + , changePasswordForm = Messages.Comp.ChangePasswordForm.de , uiSettingsManage = Messages.Comp.UiSettingsManage.de , emailSettingsManage = Messages.Comp.EmailSettingsManage.de , imapSettingsManage = Messages.Comp.ImapSettingsManage.de @@ -161,8 +161,6 @@ E-Mail-Einstellungen (IMAP) notwendig.""" gleichen E-Mails möglichst nicht noch einmal eingelesen werden.""" , otpMenu = "Zwei-Faktor-Authentifizierung" - , webhooks = "Webhooks" - , genericQueries = "Periodische Abfragen" , dueItems = "Fällige Dokumente" , notificationInfoText = """ diff --git a/modules/webapp/src/main/elm/Page.elm b/modules/webapp/src/main/elm/Page.elm index a14e5295..6a177a7b 100644 --- a/modules/webapp/src/main/elm/Page.elm +++ b/modules/webapp/src/main/elm/Page.elm @@ -13,7 +13,9 @@ module Page exposing , goto , hasSidebar , href + , isDashboardPage , isOpen + , isSearchPage , isSecured , loginPage , loginPageReferrer @@ -51,7 +53,7 @@ emptyLoginData = type Page - = HomePage + = SearchPage (Maybe String) | LoginPage LoginData | ManageDataPage | CollectiveSettingPage @@ -63,12 +65,16 @@ type Page | ItemDetailPage String | SharePage String | ShareDetailPage String String + | DashboardPage isSecured : Page -> Bool isSecured page = case page of - HomePage -> + DashboardPage -> + True + + SearchPage _ -> True LoginPage _ -> @@ -138,11 +144,34 @@ loginPage p = LoginPage { emptyLoginData | referrer = Just p } +isSearchPage : Page -> Bool +isSearchPage page = + case page of + SearchPage _ -> + True + + _ -> + False + + +isDashboardPage : Page -> Bool +isDashboardPage page = + case page of + DashboardPage -> + True + + _ -> + False + + pageName : Page -> String pageName page = case page of - HomePage -> - "Home" + DashboardPage -> + "dashboard" + + SearchPage _ -> + "Search" LoginPage _ -> "Login" @@ -226,8 +255,16 @@ uploadId page = pageToString : Page -> String pageToString page = case page of - HomePage -> - "/app/home" + DashboardPage -> + "/app/dashboard" + + SearchPage bmId -> + case bmId of + Just id -> + "/app/search?bm=" ++ id + + Nothing -> + "/app/search" LoginPage data -> case data.referrer of @@ -312,12 +349,14 @@ pathPrefix = parser : Parser (Page -> a) a parser = oneOf - [ Parser.map HomePage + [ Parser.map DashboardPage (oneOf [ Parser.top - , s pathPrefix s "home" + , s pathPrefix + , s pathPrefix s "dashboard" ] ) + , Parser.map SearchPage (s pathPrefix s "search" Query.string "bm") , Parser.map LoginPage (s pathPrefix s "login" loginPageParser) , Parser.map ManageDataPage (s pathPrefix s "managedata") , Parser.map CollectiveSettingPage (s pathPrefix s "csettings") diff --git a/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm b/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm index 7a09d909..f2fad1f3 100644 --- a/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm +++ b/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm @@ -59,7 +59,7 @@ viewSidebar texts visible _ _ model = [ Icons.sourceIcon2 "" , span [ class "ml-3" ] - [ text texts.sources ] + [ text texts.basics.sources ] ] , a [ href "#" @@ -70,7 +70,7 @@ viewSidebar texts visible _ _ model = [ Icons.shareIcon "" , span [ class "ml-3" ] - [ text texts.shares ] + [ text texts.basics.shares ] ] , a [ href "#" @@ -238,7 +238,7 @@ viewSources texts flags settings model = ] [ Icons.sourceIcon2 "" , div [ class "ml-3" ] - [ text texts.sources + [ text texts.basics.sources ] ] , Html.map SourceMsg (Comp.SourceManage.view2 texts.sourceManage flags settings model.sourceModel) @@ -253,7 +253,7 @@ viewShares texts settings flags model = ] [ Icons.shareIcon "" , div [ class "ml-3" ] - [ text texts.shares + [ text texts.basics.shares ] ] , Html.map ShareMsg (Comp.ShareManage.view texts.shareManage settings flags model.shareModel) diff --git a/modules/webapp/src/main/elm/Page/Dashboard/Data.elm b/modules/webapp/src/main/elm/Page/Dashboard/Data.elm new file mode 100644 index 00000000..22817738 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Dashboard/Data.elm @@ -0,0 +1,200 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Page.Dashboard.Data exposing + ( Content(..) + , Model + , Msg(..) + , PageError(..) + , SideMenuModel + , init + , isDashboardDefault + , isDashboardVisible + , isHomeContent + , reinitCmd + , reloadDashboardData + , reloadUiSettings + ) + +import Api +import Comp.BookmarkChooser +import Comp.DashboardManage +import Comp.DashboardView +import Comp.EquipmentManage +import Comp.FolderManage +import Comp.NotificationHookManage +import Comp.OrgManage +import Comp.PeriodicQueryTaskManage +import Comp.PersonManage +import Comp.ShareManage +import Comp.SourceManage +import Comp.TagManage +import Comp.UploadForm +import Data.Bookmarks exposing (AllBookmarks) +import Data.Dashboard exposing (Dashboard) +import Data.Dashboards exposing (AllDashboards) +import Data.Flags exposing (Flags) +import Http + + +type alias SideMenuModel = + { bookmarkChooser : Comp.BookmarkChooser.Model + } + + +type alias Model = + { sideMenu : SideMenuModel + , content : Content + , pageError : Maybe PageError + , dashboards : AllDashboards + , isPredefined : Bool + } + + +type Msg + = GetBookmarksResp AllBookmarks + | GetAllDashboardsResp (Maybe Msg) (Result Http.Error AllDashboards) + | BookmarkMsg Comp.BookmarkChooser.Msg + | NotificationHookMsg Comp.NotificationHookManage.Msg + | PeriodicQueryMsg Comp.PeriodicQueryTaskManage.Msg + | SourceMsg Comp.SourceManage.Msg + | ShareMsg Comp.ShareManage.Msg + | OrganizationMsg Comp.OrgManage.Msg + | PersonMsg Comp.PersonManage.Msg + | EquipmentMsg Comp.EquipmentManage.Msg + | TagMsg Comp.TagManage.Msg + | FolderMsg Comp.FolderManage.Msg + | UploadMsg Comp.UploadForm.Msg + | DashboardMsg Comp.DashboardView.Msg + | DashboardManageMsg Comp.DashboardManage.Msg + | InitNotificationHook + | InitPeriodicQuery + | InitSource + | InitShare + | InitOrganization + | InitPerson + | InitEquipment + | InitTags + | InitFolder + | InitUpload + | InitEditDashboard + | ReloadDashboardData + | HardReloadDashboard + | SetDashboard Dashboard + | SetDashboardByName String + | SetDefaultDashboard + + +init : Flags -> ( Model, Cmd Msg ) +init flags = + let + ( dm, dc ) = + Comp.DashboardView.init flags Data.Dashboard.empty + in + ( { sideMenu = + { bookmarkChooser = Comp.BookmarkChooser.init Data.Bookmarks.empty + } + , content = Home dm + , pageError = Nothing + , dashboards = Data.Dashboards.emptyAll + , isPredefined = True + } + , Cmd.batch + [ initCmd flags + , Cmd.map DashboardMsg dc + ] + ) + + +initCmd : Flags -> Cmd Msg +initCmd flags = + makeInitCmd flags SetDefaultDashboard + + +reinitCmd : Flags -> Cmd Msg +reinitCmd flags = + makeInitCmd flags ReloadDashboardData + + +makeInitCmd : Flags -> Msg -> Cmd Msg +makeInitCmd flags nextMsg = + let + ignoreBookmarkError r = + Result.withDefault Data.Bookmarks.empty r + |> GetBookmarksResp + in + Cmd.batch + [ Api.getBookmarks flags ignoreBookmarkError + , Api.getAllDashboards flags (GetAllDashboardsResp (Just nextMsg)) + ] + + +reloadDashboardData : Msg +reloadDashboardData = + ReloadDashboardData + + +reloadUiSettings : Msg +reloadUiSettings = + HardReloadDashboard + + + +--- Content + + +type Content + = Home Comp.DashboardView.Model + | Webhook Comp.NotificationHookManage.Model + | PeriodicQuery Comp.PeriodicQueryTaskManage.Model + | Source Comp.SourceManage.Model + | Share Comp.ShareManage.Model + | Organization Comp.OrgManage.Model + | Person Comp.PersonManage.Model + | Equipment Comp.EquipmentManage.Model + | Tags Comp.TagManage.Model + | Folder Comp.FolderManage.Model + | Upload Comp.UploadForm.Model + | Edit Comp.DashboardManage.Model + + +isHomeContent : Content -> Bool +isHomeContent cnt = + case cnt of + Home _ -> + True + + _ -> + False + + +isDashboardVisible : Model -> String -> Bool +isDashboardVisible model name = + case model.content of + Home m -> + m.dashboard.name == name + + Edit m -> + m.initData.dashboard.name == name + + _ -> + False + + +isDashboardDefault : Model -> String -> Bool +isDashboardDefault model name = + Data.Dashboards.isDefaultAll name model.dashboards + + + +--- Errors + + +type PageError + = PageErrorHttp Http.Error + | PageErrorNoDashboard + | PageErrorInvalid String diff --git a/modules/webapp/src/main/elm/Page/Dashboard/DefaultDashboard.elm b/modules/webapp/src/main/elm/Page/Dashboard/DefaultDashboard.elm new file mode 100644 index 00000000..29ecb6ad --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Dashboard/DefaultDashboard.elm @@ -0,0 +1,143 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Page.Dashboard.DefaultDashboard exposing (getDefaultDashboard, value) + +import Data.Box exposing (Box) +import Data.BoxContent exposing (BoxContent(..), SearchQuery(..), SummaryShow(..)) +import Data.Dashboard exposing (Dashboard) +import Data.Flags exposing (Flags) +import Data.ItemColumn as IC +import Data.UiSettings exposing (UiSettings) +import Messages +import Messages.Page.DefaultDashboard exposing (Texts) +import Messages.UiLanguage + + +value : Texts -> Dashboard +value texts = + { name = texts.default + , columns = 4 + , gap = 2 + , boxes = + [ messageBox texts + , fieldStats + , newDocuments texts + , dueDocuments texts + , upload + , summary texts + ] + } + + +getDefaultDashboard : Flags -> UiSettings -> Dashboard +getDefaultDashboard flags settings = + let + lang = + Data.UiSettings.getUiLanguage flags settings Messages.UiLanguage.English + + texts = + Messages.get lang + in + value texts.dashboard.defaultDashboard + + + +--- Boxes + + +messageBox : Texts -> Box +messageBox texts = + { name = texts.welcomeName + , visible = True + , decoration = False + , colspan = 4 + , content = + BoxMessage + { title = texts.welcomeTitle + , body = texts.welcomeBody + } + } + + +newDocuments : Texts -> Box +newDocuments texts = + { name = texts.newDocsName + , visible = True + , decoration = True + , colspan = 2 + , content = + BoxQuery + { query = SearchQueryString "inbox:yes" + , limit = 5 + , details = True + , showHeaders = False + , columns = [] + } + } + + +dueDocuments : Texts -> Box +dueDocuments texts = + { name = texts.dueInDays 10 + , visible = True + , decoration = True + , colspan = 2 + , content = + BoxQuery + { query = SearchQueryString "due>today;-10d due Box +summary texts = + { name = texts.summaryName + , visible = True + , decoration = True + , colspan = 1 + , content = + BoxStats + { query = SearchQueryString "" + , show = SummaryShowGeneral + } + } + + +fieldStats : Box +fieldStats = + { name = "" + , visible = True + , decoration = False + , colspan = 4 + , content = + BoxStats + { query = SearchQueryString "" + , show = SummaryShowFields False + } + } + + +upload : Box +upload = + { name = "" + , visible = True + , decoration = True + , colspan = 3 + , content = + BoxUpload + { sourceId = Nothing + } + } diff --git a/modules/webapp/src/main/elm/Page/Dashboard/SideMenu.elm b/modules/webapp/src/main/elm/Page/Dashboard/SideMenu.elm new file mode 100644 index 00000000..b6c1d4b1 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Dashboard/SideMenu.elm @@ -0,0 +1,180 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Page.Dashboard.SideMenu exposing (view) + +import Api.Model.VersionInfo exposing (VersionInfo) +import Comp.BookmarkChooser +import Data.AccountScope +import Data.Dashboard exposing (Dashboard) +import Data.Dashboards +import Data.Icons as Icons +import Data.UiSettings exposing (UiSettings) +import Html exposing (Attribute, Html, a, div, h3, i, span, text) +import Html.Attributes exposing (class, classList, href, target, title) +import Html.Events exposing (onClick) +import Messages.Page.Dashboard exposing (Texts) +import Page exposing (Page(..)) +import Page.Dashboard.Data exposing (Model, Msg(..), isDashboardDefault, isDashboardVisible, isHomeContent) +import Styles as S + + +view : Texts -> VersionInfo -> UiSettings -> Model -> Html Msg +view texts versionInfo _ model = + div [ class "flex flex-col flex-grow" ] + [ div [ class "mt-2" ] + [ menuLink [ onClick SetDefaultDashboard, href "#" ] (Icons.dashboardIcon "") texts.dashboardLink + , menuLink [ Page.href (SearchPage Nothing) ] (Icons.searchIcon "") texts.basics.items + , menuLink [ onClick InitUpload, href "#" ] (Icons.fileUploadIcon "") texts.uploadFiles + ] + , h3 + [ class S.header3 + , class "italic mt-3" + ] + [ text texts.bookmarks + ] + , div [ class "ml-2" ] + [ Html.map BookmarkMsg + (Comp.BookmarkChooser.viewWith + { showUser = True, showCollective = True, showShares = False } + texts.bookmarkChooser + model.sideMenu.bookmarkChooser + Comp.BookmarkChooser.emptySelection + ) + ] + , h3 + [ class S.header3 + , class "italic mt-3" + ] + [ text texts.settings + ] + , div [ class "ml-2 mb-2" ] + [ menuLink [ onClick InitNotificationHook, href "#" ] (Icons.notificationHooksIcon "") texts.basics.notificationHooks + , menuLink [ onClick InitPeriodicQuery, href "#" ] (Icons.periodicTasksIcon "") texts.basics.periodicQueries + , menuLink [ onClick InitSource, href "#" ] (Icons.sourceIcon2 "") texts.basics.sources + , menuLink [ onClick InitShare, href "#" ] (Icons.shareIcon "") texts.basics.shares + ] + , h3 + [ class S.header3 + , class "italic mt-3" + ] + [ text texts.manage + ] + , div [ class "ml-2 mb-2" ] + [ menuLink [ onClick InitOrganization, href "#" ] (Icons.organizationIcon "") texts.basics.organization + , menuLink [ onClick InitPerson, href "#" ] (Icons.personIcon "") texts.basics.person + , menuLink [ onClick InitEquipment, href "#" ] (Icons.equipmentIcon "") texts.basics.equipment + , menuLink [ onClick InitTags, href "#" ] (Icons.tagsIcon "") texts.basics.tags + , menuLink [ onClick InitFolder, href "#" ] (Icons.folderIcon "") texts.basics.folder + ] + , h3 + [ class S.header3 + , class "italic mt-3" + , classList [ ( "hidden", Data.Dashboards.countAll model.dashboards <= 1 ) ] + ] + [ text texts.dashboards + ] + , div + [ class "ml-2" + , classList [ ( "hidden", Data.Dashboards.countAll model.dashboards <= 1 ) ] + ] + [ titleDiv <| texts.accountScope Data.AccountScope.User + , div + [ classList [ ( "hidden", Data.Dashboards.isEmpty model.dashboards.user ) ] + ] + (Data.Dashboards.map (dashboardLink texts model) model.dashboards.user) + , titleDiv <| texts.accountScope Data.AccountScope.Collective + , div + [ classList [ ( "hidden", Data.Dashboards.isEmpty model.dashboards.collective ) ] + ] + (Data.Dashboards.map (dashboardLink texts model) model.dashboards.collective) + ] + , h3 + [ class S.header3 + , class "italic mt-3" + ] + [ text texts.misc + ] + , div [ class "ml-2" ] + [ menuLink + [ onClick InitEditDashboard + , classList [ ( "hidden", not (isHomeContent model.content) ) ] + , href "#" + ] + (Icons.editIcon "") + texts.editDashboard + , div [ class "mt-2 opacity-75" ] + [ menuLink [ href Data.UiSettings.documentationSite, target "_blank" ] + (Icons.documentationIcon "") + texts.documentation + ] + ] + , div [ class "flex flex-grow items-end" ] + [ div [ class "text-center text-xs w-full opacity-50" ] + [ text "Docspell " + , text versionInfo.version + ] + ] + ] + + +titleDiv : String -> Html msg +titleDiv label = + div [ class "text-sm opacity-75 py-0.5 italic" ] + [ text label + ] + + +menuLinkStyle : String +menuLinkStyle = + "my-1 flex flex-row items-center rounded px-1 py-1 hover:bg-blue-100 dark:hover:bg-slate-600" + + +menuLink : List (Attribute Msg) -> Html Msg -> String -> Html Msg +menuLink attrs icon label = + a + (attrs ++ [ class menuLinkStyle ]) + [ icon + , span [ class "ml-2" ] + [ text label + ] + ] + + +dashboardLink : Texts -> Model -> Dashboard -> Html Msg +dashboardLink texts model db = + let + ( visible, default ) = + ( isDashboardVisible model db.name + , isDashboardDefault model db.name + ) + in + a + [ class menuLinkStyle + , classList [ ( "italic", visible ) ] + , href "#" + , onClick (SetDashboard db) + ] + [ if visible then + i [ class "fa fa-check mr-2" ] [] + + else + i [ class "fa fa-columns mr-2" ] [] + , div [ class "flex flex-row flex-grow space-x-1" ] + [ div [ class "flex flex-grow" ] + [ text db.name + ] + , div [ class "opacity-50" ] + [ i + [ classList [ ( "hidden", not default ) ] + , class "fa fa-house-user" + , title texts.defaultDashboard.default + ] + [] + ] + ] + ] diff --git a/modules/webapp/src/main/elm/Page/Dashboard/Update.elm b/modules/webapp/src/main/elm/Page/Dashboard/Update.elm new file mode 100644 index 00000000..f0988cc5 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Dashboard/Update.elm @@ -0,0 +1,451 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Page.Dashboard.Update exposing (update) + +import Api +import Browser.Navigation as Nav +import Comp.BookmarkChooser +import Comp.DashboardManage +import Comp.DashboardView +import Comp.EquipmentManage +import Comp.FolderManage +import Comp.NotificationHookManage +import Comp.OrgManage +import Comp.PeriodicQueryTaskManage +import Comp.PersonManage +import Comp.ShareManage +import Comp.SourceManage +import Comp.TagManage +import Comp.UploadForm +import Data.AccountScope +import Data.Dashboards +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Messages.Page.Dashboard exposing (Texts) +import Page exposing (Page(..)) +import Page.Dashboard.Data exposing (..) +import Page.Dashboard.DefaultDashboard +import Set + + +update : Texts -> UiSettings -> Nav.Key -> Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +update texts settings navKey flags msg model = + let + nextRun amsg = + nextRunModel amsg model + + nextRunModel amsg amodel = + update texts settings navKey flags amsg amodel + in + case msg of + GetBookmarksResp list -> + let + sideMenu = + model.sideMenu + in + unit + { model | sideMenu = { sideMenu | bookmarkChooser = Comp.BookmarkChooser.init list } } + + GetAllDashboardsResp next (Ok boards) -> + let + nextModel = + if Data.Dashboards.isEmptyAll boards then + { model + | dashboards = + Data.Dashboards.singletonAll <| + Page.Dashboard.DefaultDashboard.value texts.defaultDashboard + , isPredefined = True + , pageError = Nothing + } + + else + { model | dashboards = boards, isPredefined = False, pageError = Nothing } + in + case next of + Just nextMsg -> + nextRunModel nextMsg nextModel + + Nothing -> + unit nextModel + + GetAllDashboardsResp _ (Err err) -> + unit { model | pageError = Just <| PageErrorHttp err } + + BookmarkMsg lm -> + let + sideMenu = + model.sideMenu + + ( bm, sel ) = + Comp.BookmarkChooser.update + lm + sideMenu.bookmarkChooser + Comp.BookmarkChooser.emptySelection + + bmId = + Set.toList sel.bookmarks |> List.head + in + ( { model | sideMenu = { sideMenu | bookmarkChooser = bm } } + , Page.set navKey (SearchPage bmId) + , Sub.none + ) + + ReloadDashboardData -> + let + lm = + DashboardMsg Comp.DashboardView.reloadData + in + update texts settings navKey flags lm model + + HardReloadDashboard -> + case model.content of + Home dm -> + let + board = + dm.dashboard + + ( dm_, dc ) = + Comp.DashboardView.init flags board + in + ( { model | content = Home dm_ }, Cmd.map DashboardMsg dc, Sub.none ) + + _ -> + unit model + + SetDashboard db -> + let + isVisible = + case model.content of + Home dm -> + dm.dashboard.name == db.name + + _ -> + False + in + if isVisible then + update texts settings navKey flags ReloadDashboardData model + + else + let + ( dbm, dbc ) = + Comp.DashboardView.init flags db + in + ( { model | content = Home dbm, pageError = Nothing } + , Cmd.map DashboardMsg dbc + , Sub.none + ) + + SetDefaultDashboard -> + case Data.Dashboards.getAllDefault model.dashboards of + Just db -> + nextRun (SetDashboard db) + + Nothing -> + unit model + + SetDashboardByName name -> + case Data.Dashboards.findInAll name model.dashboards of + Just db -> + nextRun (SetDashboard db) + + Nothing -> + unit model + + InitNotificationHook -> + let + ( nhm, nhc ) = + Comp.NotificationHookManage.init flags + in + ( { model | content = Webhook nhm }, Cmd.map NotificationHookMsg nhc, Sub.none ) + + InitPeriodicQuery -> + let + ( pqm, pqc ) = + Comp.PeriodicQueryTaskManage.init flags + in + ( { model | content = PeriodicQuery pqm }, Cmd.map PeriodicQueryMsg pqc, Sub.none ) + + InitSource -> + let + ( sm, sc ) = + Comp.SourceManage.init flags + in + ( { model | content = Source sm }, Cmd.map SourceMsg sc, Sub.none ) + + InitShare -> + let + ( sm, sc ) = + Comp.ShareManage.init flags + in + ( { model | content = Share sm }, Cmd.map ShareMsg sc, Sub.none ) + + InitOrganization -> + let + ( om, oc ) = + Comp.OrgManage.init flags + in + ( { model | content = Organization om }, Cmd.map OrganizationMsg oc, Sub.none ) + + InitPerson -> + let + ( pm, pc ) = + Comp.PersonManage.init flags + in + ( { model | content = Person pm }, Cmd.map PersonMsg pc, Sub.none ) + + InitEquipment -> + let + ( em, ec ) = + Comp.EquipmentManage.init flags + in + ( { model | content = Equipment em }, Cmd.map EquipmentMsg ec, Sub.none ) + + InitTags -> + let + ( tm, tc ) = + Comp.TagManage.init flags + in + ( { model | content = Tags tm }, Cmd.map TagMsg tc, Sub.none ) + + InitFolder -> + let + ( fm, fc ) = + Comp.FolderManage.init flags + in + ( { model | content = Folder fm }, Cmd.map FolderMsg fc, Sub.none ) + + InitUpload -> + let + um = + Comp.UploadForm.init + in + ( { model | content = Upload um }, Cmd.none, Sub.none ) + + InitEditDashboard -> + case model.content of + Home m -> + let + default = + Data.Dashboards.isDefaultAll m.dashboard.name model.dashboards + + scope = + Data.Dashboards.getScope m.dashboard.name model.dashboards + |> Maybe.withDefault Data.AccountScope.User + + ( dm, dc, ds ) = + Comp.DashboardManage.init + { flags = flags + , dashboard = m.dashboard + , scope = scope + , isDefault = default + } + in + ( { model | content = Edit dm } + , Cmd.map DashboardManageMsg dc + , Sub.map DashboardManageMsg ds + ) + + _ -> + unit model + + NotificationHookMsg lm -> + case model.content of + Webhook nhm -> + let + ( nhm_, nhc ) = + Comp.NotificationHookManage.update flags lm nhm + in + ( { model | content = Webhook nhm_ }, Cmd.map NotificationHookMsg nhc, Sub.none ) + + _ -> + unit model + + PeriodicQueryMsg lm -> + case model.content of + PeriodicQuery pqm -> + let + ( pqm_, pqc, pqs ) = + Comp.PeriodicQueryTaskManage.update flags lm pqm + in + ( { model | content = PeriodicQuery pqm_ } + , Cmd.map PeriodicQueryMsg pqc + , Sub.map PeriodicQueryMsg pqs + ) + + _ -> + unit model + + SourceMsg lm -> + case model.content of + Source m -> + let + ( sm, sc ) = + Comp.SourceManage.update flags lm m + in + ( { model | content = Source sm }, Cmd.map SourceMsg sc, Sub.none ) + + _ -> + unit model + + ShareMsg lm -> + case model.content of + Share m -> + let + ( sm, sc, subs ) = + Comp.ShareManage.update texts.shareManage flags lm m + in + ( { model | content = Share sm } + , Cmd.map ShareMsg sc + , Sub.map ShareMsg subs + ) + + _ -> + unit model + + OrganizationMsg lm -> + case model.content of + Organization m -> + let + ( om, oc ) = + Comp.OrgManage.update flags lm m + in + ( { model | content = Organization om }, Cmd.map OrganizationMsg oc, Sub.none ) + + _ -> + unit model + + PersonMsg lm -> + case model.content of + Person m -> + let + ( pm, pc ) = + Comp.PersonManage.update flags lm m + in + ( { model | content = Person pm }, Cmd.map PersonMsg pc, Sub.none ) + + _ -> + unit model + + EquipmentMsg lm -> + case model.content of + Equipment m -> + let + ( em, ec ) = + Comp.EquipmentManage.update flags lm m + in + ( { model | content = Equipment em }, Cmd.map EquipmentMsg ec, Sub.none ) + + _ -> + unit model + + TagMsg lm -> + case model.content of + Tags m -> + let + ( tm, tc ) = + Comp.TagManage.update flags lm m + in + ( { model | content = Tags tm }, Cmd.map TagMsg tc, Sub.none ) + + _ -> + unit model + + FolderMsg lm -> + case model.content of + Folder m -> + let + ( fm, fc ) = + Comp.FolderManage.update flags lm m + in + ( { model | content = Folder fm }, Cmd.map FolderMsg fc, Sub.none ) + + _ -> + unit model + + UploadMsg lm -> + case model.content of + Upload m -> + let + ( um, uc, us ) = + Comp.UploadForm.update Nothing flags lm m + in + ( { model | content = Upload um }, Cmd.map UploadMsg uc, Sub.map UploadMsg us ) + + _ -> + unit model + + DashboardMsg lm -> + case model.content of + Home m -> + let + ( dm, dc, ds ) = + Comp.DashboardView.update flags lm m + in + ( { model | content = Home dm }, Cmd.map DashboardMsg dc, Sub.map DashboardMsg ds ) + + _ -> + unit model + + DashboardManageMsg lm -> + case model.content of + Edit m -> + let + nameExists name = + Data.Dashboards.existsAll name model.dashboards + + result = + Comp.DashboardManage.update flags nameExists lm m + in + case result.action of + Comp.DashboardManage.SubmitNone -> + ( { model | content = Edit result.model } + , Cmd.map DashboardManageMsg result.cmd + , Sub.map DashboardManageMsg result.sub + ) + + Comp.DashboardManage.SubmitSaved name -> + ( { model | content = Edit result.model } + , Cmd.batch + [ Cmd.map DashboardManageMsg result.cmd + , getDashboards flags (Just <| SetDashboardByName name) + ] + , Sub.map DashboardManageMsg result.sub + ) + + Comp.DashboardManage.SubmitCancel name -> + case Data.Dashboards.findInAll name model.dashboards of + Just db -> + update texts settings navKey flags (SetDashboard db) model + + Nothing -> + ( { model | content = Edit result.model } + , Cmd.map DashboardManageMsg result.cmd + , Sub.map DashboardManageMsg result.sub + ) + + Comp.DashboardManage.SubmitDeleted -> + ( { model | content = Edit result.model } + , Cmd.batch + [ Cmd.map DashboardManageMsg result.cmd + , getDashboards flags (Just SetDefaultDashboard) + ] + , Sub.map DashboardManageMsg result.sub + ) + + _ -> + unit model + + +unit : Model -> ( Model, Cmd Msg, Sub Msg ) +unit m = + ( m, Cmd.none, Sub.none ) + + +getDashboards : Flags -> Maybe Msg -> Cmd Msg +getDashboards flags nextMsg = + Api.getAllDashboards flags (GetAllDashboardsResp nextMsg) diff --git a/modules/webapp/src/main/elm/Page/Dashboard/View.elm b/modules/webapp/src/main/elm/Page/Dashboard/View.elm new file mode 100644 index 00000000..d6f556cf --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Dashboard/View.elm @@ -0,0 +1,226 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Page.Dashboard.View exposing (viewContent, viewSidebar) + +import Api.Model.VersionInfo exposing (VersionInfo) +import Comp.DashboardManage +import Comp.DashboardView +import Comp.EquipmentManage +import Comp.FolderManage +import Comp.NotificationHookManage +import Comp.OrgManage +import Comp.PeriodicQueryTaskManage +import Comp.PersonManage +import Comp.ShareManage +import Comp.SourceManage +import Comp.TagManage +import Comp.UploadForm +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Messages.Page.Dashboard exposing (Texts) +import Page.Dashboard.Data exposing (..) +import Page.Dashboard.SideMenu as SideMenu +import Styles as S + + +viewSidebar : Texts -> Bool -> Flags -> VersionInfo -> UiSettings -> Model -> Html Msg +viewSidebar texts visible _ versionInfo settings model = + div + [ id "sidebar" + , class S.sidebar + , class S.sidebarBg + , classList [ ( "hidden", not visible ) ] + ] + [ SideMenu.view texts versionInfo settings model + ] + + +viewContent : Texts -> Flags -> UiSettings -> Model -> Html Msg +viewContent texts flags settings model = + let + editSettings = + { showDeleteButton = not model.isPredefined + , showCopyButton = not model.isPredefined + } + in + div + [ id "content" + , class S.content + ] + [ case model.content of + Home m -> + div [ class "mt-1" ] + [ Html.map DashboardMsg + (Comp.DashboardView.view texts.dashboard flags settings m) + ] + + Edit m -> + div [ class "mt-1" ] + [ div + [ class S.infoMessage + , class "my-1" + , classList [ ( "hidden", not model.isPredefined ) ] + ] + [ text texts.predefinedMessage ] + , Html.map DashboardManageMsg + (Comp.DashboardManage.view texts.dashboardManage flags editSettings settings m) + ] + + Webhook m -> + viewHookManage texts settings m + + PeriodicQuery m -> + viewPeriodicQuery texts settings m + + Source m -> + viewSource texts flags settings m + + Share m -> + viewShare texts flags settings m + + Organization m -> + viewOrganization texts settings m + + Person m -> + viewPerson texts settings m + + Equipment m -> + viewEquipment texts m + + Tags m -> + viewTags texts settings m + + Folder m -> + viewFolder texts flags m + + Upload m -> + viewUplod texts flags settings m + ] + + + +--- Helpers + + +viewUplod : Texts -> Flags -> UiSettings -> Comp.UploadForm.Model -> Html Msg +viewUplod texts flags settings model = + let + viewCfg = + { showForm = True + , sourceId = Nothing + , lightForm = False + } + in + div [] + [ h1 [ class S.header1 ] + [ text texts.uploadFiles + ] + , Html.map UploadMsg <| + Comp.UploadForm.view texts.uploadForm viewCfg flags settings model + ] + + +viewFolder : Texts -> Flags -> Comp.FolderManage.Model -> Html Msg +viewFolder texts flags model = + div [] + [ h1 [ class S.header1 ] + [ text texts.basics.folder + ] + , Html.map FolderMsg <| + Comp.FolderManage.view2 texts.folderManage flags model + ] + + +viewTags : Texts -> UiSettings -> Comp.TagManage.Model -> Html Msg +viewTags texts settings model = + div [] + [ h1 [ class S.header1 ] + [ text texts.basics.tags + ] + , Html.map TagMsg <| + Comp.TagManage.view2 texts.tagManage settings model + ] + + +viewEquipment : Texts -> Comp.EquipmentManage.Model -> Html Msg +viewEquipment texts model = + div [] + [ h1 [ class S.header1 ] + [ text texts.basics.equipment + ] + , Html.map EquipmentMsg <| + Comp.EquipmentManage.view2 texts.equipManage model + ] + + +viewPerson : Texts -> UiSettings -> Comp.PersonManage.Model -> Html Msg +viewPerson texts settings model = + div [] + [ h1 [ class S.header1 ] + [ text texts.basics.person + ] + , Html.map PersonMsg <| + Comp.PersonManage.view2 texts.personManage settings model + ] + + +viewOrganization : Texts -> UiSettings -> Comp.OrgManage.Model -> Html Msg +viewOrganization texts settings model = + div [] + [ h1 [ class S.header1 ] + [ text texts.basics.organization + ] + , Html.map OrganizationMsg <| + Comp.OrgManage.view2 texts.organizationManage settings model + ] + + +viewShare : Texts -> Flags -> UiSettings -> Comp.ShareManage.Model -> Html Msg +viewShare texts flags settings model = + div [] + [ h1 [ class S.header1 ] + [ text texts.basics.shares + ] + , Html.map ShareMsg <| + Comp.ShareManage.view texts.shareManage settings flags model + ] + + +viewSource : Texts -> Flags -> UiSettings -> Comp.SourceManage.Model -> Html Msg +viewSource texts flags settings model = + div [] + [ h1 [ class S.header1 ] + [ text texts.basics.sources + ] + , Html.map SourceMsg <| + Comp.SourceManage.view2 texts.sourceManage flags settings model + ] + + +viewPeriodicQuery : Texts -> UiSettings -> Comp.PeriodicQueryTaskManage.Model -> Html Msg +viewPeriodicQuery texts settings model = + div [] + [ h1 [ class S.header1 ] + [ text texts.basics.periodicQueries + ] + , Html.map PeriodicQueryMsg <| + Comp.PeriodicQueryTaskManage.view texts.periodicQueryManage settings model + ] + + +viewHookManage : Texts -> UiSettings -> Comp.NotificationHookManage.Model -> Html Msg +viewHookManage texts settings model = + div [] + [ h1 [ class S.header1 ] + [ text texts.basics.notificationHooks + ] + , Html.map NotificationHookMsg <| + Comp.NotificationHookManage.view texts.notificationHookManage settings model + ] diff --git a/modules/webapp/src/main/elm/Page/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Page/ItemDetail/Update.elm index 5f7c3bb7..5e4e7128 100644 --- a/modules/webapp/src/main/elm/Page/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Page/ItemDetail/Update.elm @@ -60,7 +60,7 @@ update key flags inav settings msg model = Cmd.none _ -> - Page.set key HomePage + Page.set key (SearchPage Nothing) in { model = { model | detail = result.model } , cmd = Cmd.batch [ pageSwitch, Cmd.map ItemDetailMsg result.cmd ] diff --git a/modules/webapp/src/main/elm/Page/Login/Update.elm b/modules/webapp/src/main/elm/Page/Login/Update.elm index d65290bb..6e2d013c 100644 --- a/modules/webapp/src/main/elm/Page/Login/Update.elm +++ b/modules/webapp/src/main/elm/Page/Login/Update.elm @@ -53,7 +53,7 @@ update loginData flags msg model = AuthResp (Ok lr) -> let gotoRef = - Maybe.withDefault HomePage loginData.referrer |> Page.goto + Maybe.withDefault DashboardPage loginData.referrer |> Page.goto in if lr.success && not lr.requireSecondFactor then ( { model | formState = AuthSuccess lr, password = "" } diff --git a/modules/webapp/src/main/elm/Page/ManageData/View2.elm b/modules/webapp/src/main/elm/Page/ManageData/View2.elm index f4947bf1..2354a7c3 100644 --- a/modules/webapp/src/main/elm/Page/ManageData/View2.elm +++ b/modules/webapp/src/main/elm/Page/ManageData/View2.elm @@ -46,7 +46,7 @@ viewSidebar texts visible _ settings model = , class S.sidebarLink , menuEntryActive model TagTab ] - [ Icons.tagIcon2 "" + [ Icons.tagIcon "" , span [ class "ml-3" ] [ text texts.basics.tags @@ -58,7 +58,7 @@ viewSidebar texts visible _ settings model = , menuEntryActive model EquipTab , class S.sidebarLink ] - [ Icons.equipmentIcon2 "" + [ Icons.equipmentIcon "" , span [ class "ml-3" ] [ text texts.basics.equipment @@ -70,7 +70,7 @@ viewSidebar texts visible _ settings model = , menuEntryActive model OrgTab , class S.sidebarLink ] - [ Icons.organizationIcon2 "" + [ Icons.organizationIcon "" , span [ class "ml-3" ] [ text texts.basics.organization @@ -82,7 +82,7 @@ viewSidebar texts visible _ settings model = , menuEntryActive model PersonTab , class S.sidebarLink ] - [ Icons.personIcon2 "" + [ Icons.personIcon "" , span [ class "ml-3" ] [ text texts.basics.person @@ -99,7 +99,7 @@ viewSidebar texts visible _ settings model = , menuEntryActive model FolderTab , class S.sidebarLink ] - [ Icons.folderIcon2 "" + [ Icons.folderIcon "" , span [ class "ml-3" ] [ text texts.basics.folder @@ -186,7 +186,7 @@ viewTags texts settings model = [ class S.header1 , class "inline-flex items-center" ] - [ Icons.tagIcon2 "" + [ Icons.tagIcon "" , div [ class "ml-2" ] [ text texts.basics.tags ] @@ -206,7 +206,7 @@ viewEquip texts model = [ class S.header1 , class "inline-flex items-center" ] - [ Icons.equipmentIcon2 "" + [ Icons.equipmentIcon "" , div [ class "ml-2" ] [ text texts.basics.equipment ] @@ -224,7 +224,7 @@ viewOrg texts settings model = [ class S.header1 , class "inline-flex items-center" ] - [ Icons.organizationIcon2 "" + [ Icons.organizationIcon "" , div [ class "ml-2" ] [ text texts.basics.organization ] @@ -243,7 +243,7 @@ viewPerson texts settings model = [ class S.header1 , class "inline-flex items-center" ] - [ Icons.personIcon2 "" + [ Icons.personIcon "" , div [ class "ml-2" ] [ text texts.basics.person ] @@ -262,7 +262,7 @@ viewFolder texts flags _ model = [ class S.header1 , class "inline-flex items-center" ] - [ Icons.folderIcon2 "" + [ Icons.folderIcon "" , div [ class "ml-2" ] diff --git a/modules/webapp/src/main/elm/Page/Home/Data.elm b/modules/webapp/src/main/elm/Page/Search/Data.elm similarity index 98% rename from modules/webapp/src/main/elm/Page/Home/Data.elm rename to modules/webapp/src/main/elm/Page/Search/Data.elm index b55d31aa..06d6bc05 100644 --- a/modules/webapp/src/main/elm/Page/Home/Data.elm +++ b/modules/webapp/src/main/elm/Page/Search/Data.elm @@ -5,7 +5,7 @@ -} -module Page.Home.Data exposing +module Page.Search.Data exposing ( ConfirmModalValue(..) , Model , Msg(..) @@ -48,7 +48,6 @@ import Data.Items import Data.UiSettings exposing (UiSettings) import Http import Set exposing (Set) -import Throttle exposing (Throttle) import Util.Html exposing (KeyCode(..)) import Util.ItemDragDrop as DD @@ -61,7 +60,6 @@ type alias Model = , searchOffset : Int , moreAvailable : Bool , moreInProgress : Bool - , throttle : Throttle Msg , searchTypeDropdownValue : SearchType , lastSearchType : SearchType , dragDropData : DD.DragDropData @@ -129,7 +127,6 @@ init flags viewMode = , searchOffset = 0 , moreAvailable = True , moreInProgress = False - , throttle = Throttle.create 1 , searchTypeDropdownValue = if Comp.SearchMenu.isFulltextSearch searchMenuModel then ContentOnlySearch @@ -199,6 +196,7 @@ editActive model = type Msg = Init + | DoNothing | SearchMenuMsg Comp.SearchMenu.Msg | ResetSearch | ItemCardListMsg Comp.ItemCardList.Msg @@ -208,7 +206,6 @@ type Msg | ToggleSearchMenu | ToggleSelectView | LoadMore - | UpdateThrottle | SetBasicSearch String | ToggleSearchType | KeyUpSearchbarMsg (Maybe KeyCode) diff --git a/modules/webapp/src/main/elm/Page/Home/SideMenu.elm b/modules/webapp/src/main/elm/Page/Search/SideMenu.elm similarity index 97% rename from modules/webapp/src/main/elm/Page/Home/SideMenu.elm rename to modules/webapp/src/main/elm/Page/Search/SideMenu.elm index 9402f1d8..65b5d61f 100644 --- a/modules/webapp/src/main/elm/Page/Home/SideMenu.elm +++ b/modules/webapp/src/main/elm/Page/Search/SideMenu.elm @@ -5,7 +5,7 @@ -} -module Page.Home.SideMenu exposing (view) +module Page.Search.SideMenu exposing (view) import Comp.Basic as B import Comp.ItemDetail.MultiEditMenu @@ -16,8 +16,8 @@ import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) -import Messages.Page.HomeSideMenu exposing (Texts) -import Page.Home.Data exposing (..) +import Messages.Page.SearchSideMenu exposing (Texts) +import Page.Search.Data exposing (..) import Set import Styles as S diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Search/Update.elm similarity index 94% rename from modules/webapp/src/main/elm/Page/Home/Update.elm rename to modules/webapp/src/main/elm/Page/Search/Update.elm index aa99403a..6db4874d 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Search/Update.elm @@ -5,7 +5,7 @@ -} -module Page.Home.Update exposing +module Page.Search.Update exposing ( UpdateResult , update ) @@ -28,15 +28,13 @@ import Data.ItemSelection import Data.Items import Data.SearchMode exposing (SearchMode) import Data.UiSettings exposing (UiSettings) -import Messages.Page.Home exposing (Texts) +import Messages.Page.Search exposing (Texts) import Page exposing (Page(..)) -import Page.Home.Data exposing (..) +import Page.Search.Data exposing (..) import Process import Scroll import Set exposing (Set) import Task -import Throttle -import Time import Util.Html exposing (KeyCode(..)) import Util.ItemDragDrop as DD import Util.Update @@ -50,8 +48,8 @@ type alias UpdateResult = } -update : Maybe String -> Nav.Key -> Flags -> Texts -> UiSettings -> Msg -> Model -> UpdateResult -update mId key flags texts settings msg model = +update : Maybe String -> Maybe String -> Nav.Key -> Flags -> Texts -> UiSettings -> Msg -> Model -> UpdateResult +update bookmarkId mId key flags texts settings msg model = case msg of Init -> let @@ -62,20 +60,28 @@ update mId key flags texts settings msg model = , offset = 0 , scroll = True } + + setBookmark = + Maybe.map (\bmId -> SearchMenuMsg <| Comp.SearchMenu.SetBookmark bmId) bookmarkId + |> Maybe.withDefault DoNothing in makeResult <| Util.Update.andThen3 - [ update mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.Init) + [ update bookmarkId mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.Init) + , update bookmarkId mId key flags texts settings setBookmark , doSearch searchParam ] model + DoNothing -> + UpdateResult model Cmd.none Sub.none Nothing + ResetSearch -> let nm = { model | searchOffset = 0, powerSearchInput = Comp.PowerSearchInput.init } in - update mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.ResetForm) nm + update bookmarkId mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.ResetForm) nm SearchMenuMsg m -> let @@ -121,7 +127,7 @@ update mId key flags texts settings msg model = SetLinkTarget lt -> case linkTargetMsg lt of Just m -> - update mId key flags texts settings m model + update bookmarkId mId key flags texts settings m model Nothing -> makeResult ( model, Cmd.none, Sub.none ) @@ -193,7 +199,7 @@ update mId key flags texts settings msg model = in makeResult <| Util.Update.andThen3 - [ update mId key flags texts settings (ItemCardListMsg (Comp.ItemCardList.SetResults list)) + [ update bookmarkId mId key flags texts settings (ItemCardListMsg (Comp.ItemCardList.SetResults list)) , if scroll then scrollToCard mId @@ -215,7 +221,7 @@ update mId key flags texts settings msg model = , moreAvailable = list.groups /= [] } in - update mId key flags texts settings (ItemCardListMsg (Comp.ItemCardList.AddResults list)) m + update bookmarkId mId key flags texts settings (ItemCardListMsg (Comp.ItemCardList.AddResults list)) m ItemSearchAddResp (Err _) -> withSub @@ -319,30 +325,23 @@ update mId key flags texts settings msg model = else withSub ( model, Cmd.none ) - UpdateThrottle -> - let - ( newThrottle, cmd ) = - Throttle.update model.throttle - in - withSub ( { model | throttle = newThrottle }, cmd ) - SetBasicSearch str -> let smMsg = SearchMenuMsg (Comp.SearchMenu.SetTextSearch str) in - update mId key flags texts settings smMsg model + update bookmarkId mId key flags texts settings smMsg model ToggleSearchType -> case model.searchTypeDropdownValue of BasicSearch -> - update mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.SetFulltextSearch) model + update bookmarkId mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.SetFulltextSearch) model ContentOnlySearch -> - update mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.SetNamesSearch) model + update bookmarkId mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.SetNamesSearch) model KeyUpSearchbarMsg (Just Enter) -> - update mId key flags texts settings (DoSearch model.searchTypeDropdownValue) model + update bookmarkId mId key flags texts settings (DoSearch model.searchTypeDropdownValue) model KeyUpSearchbarMsg _ -> withSub ( model, Cmd.none ) @@ -653,7 +652,8 @@ update mId key flags texts settings msg model = { model | viewMode = nextView } in if result.outcome == Comp.ItemMerge.OutcomeMerged then - update mId + update bookmarkId + mId key flags texts @@ -733,7 +733,8 @@ update mId key flags texts settings msg model = { model | viewMode = nextView } in if result.outcome == Comp.PublishItems.OutcomeDone then - update mId + update bookmarkId + mId key flags texts @@ -853,7 +854,7 @@ update mId key flags texts settings msg model = model_ = { model | viewMode = viewMode } in - update mId key flags texts settings (DoSearch model.lastSearchType) model_ + update bookmarkId mId key flags texts settings (DoSearch model.lastSearchType) model_ SearchStatsResp result -> let @@ -863,7 +864,7 @@ update mId key flags texts settings msg model = stats = Result.withDefault model.searchStats result in - update mId key flags texts settings lm { model | searchStats = stats } + update bookmarkId mId key flags texts settings lm { model | searchStats = stats } TogglePreviewFullWidth -> let @@ -905,16 +906,16 @@ update mId key flags texts settings msg model = makeResult ( model_, cmd_, Sub.map PowerSearchMsg result.subs ) Comp.PowerSearchInput.SubmitSearch -> - update mId key flags texts settings (DoSearch model_.searchTypeDropdownValue) model_ + update bookmarkId mId key flags texts settings (DoSearch model_.searchTypeDropdownValue) model_ KeyUpPowerSearchbarMsg (Just Enter) -> - update mId key flags texts settings (DoSearch model.searchTypeDropdownValue) model + update bookmarkId mId key flags texts settings (DoSearch model.searchTypeDropdownValue) model KeyUpPowerSearchbarMsg _ -> withSub ( model, Cmd.none ) RemoveItem id -> - update mId key flags texts settings (ItemCardListMsg (Comp.ItemCardList.RemoveItem id)) model + update bookmarkId mId key flags texts settings (ItemCardListMsg (Comp.ItemCardList.RemoveItem id)) model TogglePublishCurrentQueryView -> case createQuery model of @@ -1146,18 +1147,14 @@ doSearch param model = searchCmd = doSearchCmd param_ model - - ( newThrottle, cmd ) = - Throttle.try searchCmd model.throttle in withSub ( { model - | searchInProgress = cmd /= Cmd.none + | searchInProgress = True , searchOffset = 0 - , throttle = newThrottle , lastSearchType = param.searchType } - , cmd + , searchCmd ) @@ -1190,9 +1187,7 @@ withSub ( m, c ) = makeResult ( m , c - , Throttle.ifNeeded - (Time.every 500 (\_ -> UpdateThrottle)) - m.throttle + , Sub.none ) diff --git a/modules/webapp/src/main/elm/Page/Home/View2.elm b/modules/webapp/src/main/elm/Page/Search/View2.elm similarity index 98% rename from modules/webapp/src/main/elm/Page/Home/View2.elm rename to modules/webapp/src/main/elm/Page/Search/View2.elm index f87bd94f..8977bae4 100644 --- a/modules/webapp/src/main/elm/Page/Home/View2.elm +++ b/modules/webapp/src/main/elm/Page/Search/View2.elm @@ -5,7 +5,7 @@ -} -module Page.Home.View2 exposing (viewContent, viewSidebar) +module Page.Search.View2 exposing (viewContent, viewSidebar) import Api import Comp.Basic as B @@ -27,10 +27,10 @@ import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick, onInput) -import Messages.Page.Home exposing (Texts) +import Messages.Page.Search exposing (Texts) import Page exposing (Page(..)) -import Page.Home.Data exposing (..) -import Page.Home.SideMenu +import Page.Search.Data exposing (..) +import Page.Search.SideMenu import Set import Styles as S import Util.Html @@ -44,7 +44,7 @@ viewSidebar texts visible flags settings model = , class S.sidebarBg , classList [ ( "hidden", not visible ) ] ] - [ Page.Home.SideMenu.view texts.sideMenu flags settings model + [ Page.Search.SideMenu.view texts.sideMenu flags settings model ] @@ -563,7 +563,7 @@ editMenuBar texts model svm = searchStats : Texts -> Flags -> UiSettings -> Model -> List (Html Msg) searchStats texts _ settings model = if settings.searchStatsVisible then - [ Comp.SearchStatsView.view2 texts.searchStatsView "my-2" model.searchStats + [ Comp.SearchStatsView.view texts.searchStatsView "my-2" model.searchStats ] else diff --git a/modules/webapp/src/main/elm/Page/Share/Data.elm b/modules/webapp/src/main/elm/Page/Share/Data.elm index 0ae8e160..22bf249b 100644 --- a/modules/webapp/src/main/elm/Page/Share/Data.elm +++ b/modules/webapp/src/main/elm/Page/Share/Data.elm @@ -19,7 +19,7 @@ import Comp.SharePasswordForm import Data.Flags exposing (Flags) import Data.ItemArrange exposing (ItemArrange) import Http -import Page.Home.Data exposing (Msg(..)) +import Page.Search.Data exposing (Msg(..)) import Set exposing (Set) import Util.Html exposing (KeyCode) diff --git a/modules/webapp/src/main/elm/Page/ShareDetail/View.elm b/modules/webapp/src/main/elm/Page/ShareDetail/View.elm index 73442e26..210a0c12 100644 --- a/modules/webapp/src/main/elm/Page/ShareDetail/View.elm +++ b/modules/webapp/src/main/elm/Page/ShareDetail/View.elm @@ -143,7 +143,7 @@ itemData texts flags model shareId itemId = ] , div [ class boxStyle ] [ div [ class headerStyle ] - [ Icons.tagsIcon2 "mr-2 ml-2" + [ Icons.tagsIcon "mr-2 ml-2" , text texts.tagsAndFields ] , div [ class "flex flex-row items-center flex-wrap font-medium my-1" ] diff --git a/modules/webapp/src/main/elm/Page/Upload/Data.elm b/modules/webapp/src/main/elm/Page/Upload/Data.elm index 0c72ca82..db41be9a 100644 --- a/modules/webapp/src/main/elm/Page/Upload/Data.elm +++ b/modules/webapp/src/main/elm/Page/Upload/Data.elm @@ -9,106 +9,27 @@ module Page.Upload.Data exposing ( Model , Msg(..) , emptyModel - , hasErrors - , isCompleted - , isDone - , isError - , isIdle - , isLoading - , isSuccessAll - , uploadAllTracker + , reset ) -import Api.Model.BasicResult exposing (BasicResult) -import Comp.Dropzone -import Comp.FixedDropdown -import Data.Language exposing (Language) -import Dict exposing (Dict) -import File exposing (File) -import Http -import Set exposing (Set) -import Util.File exposing (makeFileId) +import Comp.UploadForm type alias Model = - { incoming : Bool - , singleItem : Bool - , files : List File - , completed : Set String - , errored : Set String - , loading : Dict String Int - , dropzone : Comp.Dropzone.Model - , skipDuplicates : Bool - , languageModel : Comp.FixedDropdown.Model Language - , language : Maybe Language + { uploadForm : Comp.UploadForm.Model } emptyModel : Model emptyModel = - { incoming = True - , singleItem = False - , files = [] - , completed = Set.empty - , errored = Set.empty - , loading = Dict.empty - , dropzone = Comp.Dropzone.init [] - , skipDuplicates = True - , languageModel = - Comp.FixedDropdown.init Data.Language.all - , language = Nothing + { uploadForm = Comp.UploadForm.init } type Msg - = SubmitUpload - | SingleUploadResp String (Result Http.Error BasicResult) - | GotProgress String Http.Progress - | ToggleIncoming - | ToggleSingleItem - | Clear - | DropzoneMsg Comp.Dropzone.Msg - | ToggleSkipDuplicates - | LanguageMsg (Comp.FixedDropdown.Msg Language) + = UploadMsg Comp.UploadForm.Msg -isLoading : Model -> File -> Bool -isLoading model file = - Dict.member (makeFileId file) model.loading - - -isCompleted : Model -> File -> Bool -isCompleted model file = - Set.member (makeFileId file) model.completed - - -isError : Model -> File -> Bool -isError model file = - Set.member (makeFileId file) model.errored - - -isIdle : Model -> File -> Bool -isIdle model file = - not (isLoading model file || isCompleted model file || isError model file) - - -uploadAllTracker : String -uploadAllTracker = - "upload-all" - - -isDone : Model -> Bool -isDone model = - List.map makeFileId model.files - |> List.all (\id -> Set.member id model.completed || Set.member id model.errored) - - -isSuccessAll : Model -> Bool -isSuccessAll model = - List.map makeFileId model.files - |> List.all (\id -> Set.member id model.completed) - - -hasErrors : Model -> Bool -hasErrors model = - not (Set.isEmpty model.errored) +reset : Msg +reset = + UploadMsg Comp.UploadForm.reset diff --git a/modules/webapp/src/main/elm/Page/Upload/Update.elm b/modules/webapp/src/main/elm/Page/Upload/Update.elm index 8142f1ad..74d67819 100644 --- a/modules/webapp/src/main/elm/Page/Upload/Update.elm +++ b/modules/webapp/src/main/elm/Page/Upload/Update.elm @@ -7,187 +7,17 @@ module Page.Upload.Update exposing (update) -import Api -import Api.Model.ItemUploadMeta -import Comp.Dropzone -import Comp.FixedDropdown +import Comp.UploadForm import Data.Flags exposing (Flags) -import Data.Language -import Dict -import Http import Page.Upload.Data exposing (..) -import Set exposing (Set) -import Util.File exposing (makeFileId) -import Util.Maybe update : Maybe String -> Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) update sourceId flags msg model = case msg of - ToggleIncoming -> - ( { model | incoming = not model.incoming }, Cmd.none, Sub.none ) - - ToggleSingleItem -> - ( { model | singleItem = not model.singleItem }, Cmd.none, Sub.none ) - - ToggleSkipDuplicates -> - ( { model | skipDuplicates = not model.skipDuplicates }, Cmd.none, Sub.none ) - - SubmitUpload -> + UploadMsg lm -> let - emptyMeta = - Api.Model.ItemUploadMeta.empty - - meta = - { emptyMeta - | multiple = not model.singleItem - , skipDuplicates = Just model.skipDuplicates - , direction = - if model.incoming then - Just "incoming" - - else - Just "outgoing" - , language = Maybe.map Data.Language.toIso3 model.language - } - - fileids = - List.map makeFileId model.files - - uploads = - if model.singleItem then - Api.uploadSingle flags - sourceId - meta - uploadAllTracker - model.files - (SingleUploadResp uploadAllTracker) - - else - Cmd.batch (Api.upload flags sourceId meta model.files SingleUploadResp) - - tracker = - if model.singleItem then - Http.track uploadAllTracker (GotProgress uploadAllTracker) - - else - Sub.batch <| List.map (\id -> Http.track id (GotProgress id)) fileids - - ( cm2, _, _ ) = - Comp.Dropzone.update (Comp.Dropzone.setActive False) model.dropzone - - nowLoading = - List.map (\fid -> ( fid, 0 )) fileids - |> Dict.fromList + ( um, uc, us ) = + Comp.UploadForm.update sourceId flags lm model.uploadForm in - ( { model | loading = nowLoading, dropzone = cm2 }, uploads, tracker ) - - SingleUploadResp fileid (Ok res) -> - let - compl = - if res.success then - setCompleted model fileid - - else - model.completed - - errs = - if not res.success then - setErrored model fileid - - else - model.errored - - load = - if fileid == uploadAllTracker then - Dict.empty - - else - Dict.remove fileid model.loading - in - ( { model | completed = compl, errored = errs, loading = load } - , Cmd.none - , Sub.none - ) - - SingleUploadResp fileid (Err _) -> - let - errs = - setErrored model fileid - - load = - if fileid == uploadAllTracker then - Dict.empty - - else - Dict.remove fileid model.loading - in - ( { model | errored = errs, loading = load }, Cmd.none, Sub.none ) - - GotProgress fileid progress -> - let - percent = - case progress of - Http.Sending p -> - Http.fractionSent p - |> (*) 100 - |> round - - _ -> - 0 - - newLoading = - if model.singleItem then - Dict.insert uploadAllTracker percent model.loading - - else - Dict.insert fileid percent model.loading - in - ( { model | loading = newLoading } - , Cmd.none - , Sub.none - ) - - Clear -> - ( emptyModel, Cmd.none, Sub.none ) - - DropzoneMsg m -> - let - ( m2, c2, files ) = - Comp.Dropzone.update m model.dropzone - - nextFiles = - List.append model.files files - in - ( { model | files = nextFiles, dropzone = m2 }, Cmd.map DropzoneMsg c2, Sub.none ) - - LanguageMsg lm -> - let - ( dm, sel ) = - Comp.FixedDropdown.update lm model.languageModel - in - ( { model - | languageModel = dm - , language = Util.Maybe.or [ sel, model.language ] - } - , Cmd.none - , Sub.none - ) - - -setCompleted : Model -> String -> Set String -setCompleted model fileid = - if fileid == uploadAllTracker then - List.map makeFileId model.files |> Set.fromList - - else - Set.insert fileid model.completed - - -setErrored : Model -> String -> Set String -setErrored model fileid = - if fileid == uploadAllTracker then - List.map makeFileId model.files |> Set.fromList - - else - Set.insert fileid model.errored + ( { model | uploadForm = um }, Cmd.map UploadMsg uc, Sub.map UploadMsg us ) diff --git a/modules/webapp/src/main/elm/Page/Upload/View2.elm b/modules/webapp/src/main/elm/Page/Upload/View2.elm index 964142e2..95ebc502 100644 --- a/modules/webapp/src/main/elm/Page/Upload/View2.elm +++ b/modules/webapp/src/main/elm/Page/Upload/View2.elm @@ -7,24 +7,15 @@ module Page.Upload.View2 exposing (viewContent, viewSidebar) -import Comp.Dropzone -import Comp.FixedDropdown -import Comp.Progress -import Data.DropdownStyle as DS +import Comp.UploadForm import Data.Flags exposing (Flags) import Data.UiSettings exposing (UiSettings) -import Dict -import File exposing (File) import Html exposing (..) import Html.Attributes exposing (..) -import Html.Events exposing (onCheck, onClick) import Messages.Page.Upload exposing (Texts) import Page exposing (Page(..)) import Page.Upload.Data exposing (..) -import Styles as S -import Util.File exposing (makeFileId) -import Util.Maybe -import Util.Size +import Styles viewSidebar : Maybe String -> Bool -> Flags -> UiSettings -> Model -> Html Msg @@ -37,261 +28,19 @@ viewSidebar _ _ _ _ _ = viewContent : Texts -> Maybe String -> Flags -> UiSettings -> Model -> Html Msg -viewContent texts mid _ _ model = - div - [ id "content" - , class S.content - ] - [ div [ class "container mx-auto" ] - [ div [ class "px-0 flex flex-col" ] - [ div [ class "py-4" ] - [ if mid == Nothing then - renderForm texts model - - else - span [ class "hidden" ] [] - ] - , div [ class "py-0" ] - [ Html.map DropzoneMsg - (Comp.Dropzone.view2 texts.dropzone model.dropzone) - ] - , div [ class "py-4" ] - [ a - [ class S.primaryButton - , href "#" - , onClick SubmitUpload - ] - [ text texts.basics.submit - ] - , a - [ class S.secondaryButton - , class "ml-2" - , href "#" - , onClick Clear - ] - [ text texts.reset - ] - ] - ] - , renderErrorMsg texts model - , renderSuccessMsg texts (Util.Maybe.nonEmpty mid) model - , renderUploads texts model - ] - ] - - -renderForm : Texts -> Model -> Html Msg -renderForm texts model = +viewContent texts sourceId flags settings model = let - languageCfg = - { display = texts.languageLabel - , icon = \_ -> Nothing - , style = DS.mainStyleWith "w-40" - , selectPlaceholder = texts.basics.selectPlaceholder + viewCfg = + { sourceId = sourceId + , showForm = True + , lightForm = False } in - div [ class "row" ] - [ Html.form [ action "#" ] - [ div [ class "flex flex-col mb-3" ] - [ label [ class "inline-flex items-center" ] - [ input - [ type_ "radio" - , checked model.incoming - , onCheck (\_ -> ToggleIncoming) - , class S.radioInput - ] - [] - , span [ class "ml-2" ] [ text texts.basics.incoming ] - ] - , label [ class "inline-flex items-center" ] - [ input - [ type_ "radio" - , checked (not model.incoming) - , onCheck (\_ -> ToggleIncoming) - , class S.radioInput - ] - [] - , span [ class "ml-2" ] [ text texts.basics.outgoing ] - ] - ] - , div [ class "flex flex-col mb-3" ] - [ label [ class "inline-flex items-center" ] - [ input - [ type_ "checkbox" - , checked model.singleItem - , onCheck (\_ -> ToggleSingleItem) - , class S.checkboxInput - ] - [] - , span [ class "ml-2" ] - [ text texts.allFilesOneItem - ] - ] - ] - , div [ class "flex flex-col mb-3" ] - [ label [ class "inline-flex items-center" ] - [ input - [ type_ "checkbox" - , checked model.skipDuplicates - , onCheck (\_ -> ToggleSkipDuplicates) - , class S.checkboxInput - ] - [] - , span [ class "ml-2" ] - [ text texts.skipExistingFiles - ] - ] - ] - , div [ class "flex flex-col mb-3" ] - [ label [ class "inline-flex items-center mb-2" ] - [ span [ class "mr-2" ] [ text (texts.language ++ ":") ] - , Html.map LanguageMsg - (Comp.FixedDropdown.viewStyled2 - languageCfg - False - model.language - model.languageModel - ) - ] - , div [ class "text-gray-400 text-xs" ] - [ text texts.languageInfo - ] - ] - ] - ] - - -renderErrorMsg : Texts -> Model -> Html Msg -renderErrorMsg texts model = div - [ class "row" - , classList [ ( "hidden", not (isDone model && hasErrors model) ) ] + [ id "content" + , class Styles.content + , class "mt-4" ] - [ div [ class "mt-4" ] - [ div [ class S.errorMessage ] - [ text texts.uploadErrorMessage - ] - ] - ] - - -renderSuccessMsg : Texts -> Bool -> Model -> Html Msg -renderSuccessMsg texts public model = - div - [ class "row" - , classList [ ( "hidden", List.isEmpty model.files || not (isSuccessAll model) ) ] - ] - [ div [ class "mt-4" ] - [ div [ class S.successMessage ] - [ h3 [ class S.header2, class "text-green-800 dark:text-lime-800" ] - [ i [ class "fa fa-smile font-thin" ] [] - , span [ class "ml-2" ] - [ text texts.successBox.allFilesUploaded - ] - ] - , p - [ classList [ ( "hidden", public ) ] - ] - [ text texts.successBox.line1 - , a - [ class S.successMessageLink - , Page.href HomePage - ] - [ text texts.successBox.itemsPage - ] - , text texts.successBox.line2 - , a - [ class S.successMessageLink - , Page.href QueuePage - ] - [ text texts.successBox.processingPage - ] - , text texts.successBox.line3 - ] - , p [] - [ text texts.successBox.resetLine1 - , a - [ class S.successMessageLink - , href "#" - , onClick Clear - ] - [ text texts.successBox.reset - ] - , text texts.successBox.resetLine2 - ] - ] - ] - ] - - -renderUploads : Texts -> Model -> Html Msg -renderUploads texts model = - div - [ class "mt-4" - , classList [ ( "hidden", List.isEmpty model.files || isSuccessAll model ) ] - ] - [ h2 [ class S.header2 ] - [ text texts.selectedFiles - , text (" (" ++ (List.length model.files |> String.fromInt) ++ ")") - ] - , div [] <| - if model.singleItem then - List.map (renderFileItem model (Just uploadAllTracker)) model.files - - else - List.map (renderFileItem model Nothing) model.files - ] - - -getProgress : Model -> File -> Int -getProgress model file = - let - key = - if model.singleItem then - uploadAllTracker - - else - makeFileId file - in - Dict.get key model.loading - |> Maybe.withDefault 0 - - -renderFileItem : Model -> Maybe String -> File -> Html Msg -renderFileItem model _ file = - let - name = - File.name file - - size = - File.size file - |> toFloat - |> Util.Size.bytesReadable Util.Size.B - in - div [ class "flex flex-col w-full mb-4" ] - [ div [ class "flex flex-row items-center" ] - [ div [ class "inline-flex items-center" ] - [ i - [ classList - [ ( "mr-2 text-lg", True ) - , ( "fa fa-file font-thin", isIdle model file ) - , ( "fa fa-spinner animate-spin ", isLoading model file ) - , ( "fa fa-check ", isCompleted model file ) - , ( "fa fa-bolt", isError model file ) - ] - ] - [] - , div [ class "middle aligned content" ] - [ div [ class "header" ] - [ text name - ] - ] - ] - , div [ class "flex-grow inline-flex justify-end" ] - [ text size - ] - ] - , div [ class "h-4" ] - [ Comp.Progress.progress2 (getProgress model file) - ] + [ Html.map UploadMsg + (Comp.UploadForm.view texts.uploadForm viewCfg flags settings model.uploadForm) ] diff --git a/modules/webapp/src/main/elm/Page/UserSettings/View2.elm b/modules/webapp/src/main/elm/Page/UserSettings/View2.elm index 50193b85..019ec898 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/View2.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/View2.elm @@ -18,6 +18,7 @@ import Comp.PeriodicQueryTaskManage import Comp.ScanMailboxManage import Comp.UiSettingsManage import Data.Flags exposing (Flags) +import Data.Icons as Icons import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) @@ -78,7 +79,7 @@ viewSidebar texts visible _ _ model = , menuEntryActive model NotificationTab , class S.sidebarLink ] - [ i [ class "fa fa-comment font-thin" ] [] + [ Icons.notificationHooksIcon "" , span [ class "ml-3" ] [ text texts.notifications ] @@ -96,7 +97,7 @@ viewSidebar texts visible _ _ model = [ i [ class "fa fa-bell" ] [] , span [ class "ml-3" ] - [ text texts.webhooks ] + [ text texts.basics.notificationHooks ] ] , a [ href "#" @@ -118,7 +119,7 @@ viewSidebar texts visible _ _ model = [ i [ class "fa fa-history" ] [] , span [ class "ml-3" ] - [ text texts.genericQueries ] + [ text texts.basics.periodicQueries ] ] ] ] @@ -395,7 +396,7 @@ viewNotificationInfo texts settings model = [ i [ class "fa fa-bell" ] [] , span [ class "ml-3" ] - [ text texts.webhooks ] + [ text texts.basics.notificationHooks ] ] , div [ class "ml-3 text-sm opacity-50" ] [ text texts.webhookInfoText @@ -422,10 +423,10 @@ viewNotificationInfo texts settings model = , onClick (SetTab NotificationQueriesTab) , class S.link ] - [ i [ class "fa fa-history" ] [] + [ Icons.periodicTasksIcon "" , span [ class "ml-3" ] - [ text texts.genericQueries ] + [ text texts.basics.periodicQueries ] ] , div [ class "ml-3 text-sm opacity-50" ] [ text texts.periodicQueryInfoText @@ -464,7 +465,7 @@ viewNotificationQueries texts settings model = ] [ i [ class "fa fa-history" ] [] , div [ class "ml-3" ] - [ text texts.genericQueries + [ text texts.basics.periodicQueries ] ] , Markdown.toHtml [ class "opacity-80 text-lg mb-3 markdown-preview" ] texts.periodicQueryInfoText @@ -484,7 +485,7 @@ viewNotificationHooks texts settings model = ] [ i [ class "fa fa-bell" ] [] , div [ class "ml-3" ] - [ text texts.webhooks + [ text texts.basics.notificationHooks ] ] , Markdown.toHtml [ class "opacity-80 text-lg mb-3 markdown-preview" ] texts.webhookInfoText diff --git a/modules/webapp/src/main/elm/Styles.elm b/modules/webapp/src/main/elm/Styles.elm index 314f8dc7..88fd0438 100644 --- a/modules/webapp/src/main/elm/Styles.elm +++ b/modules/webapp/src/main/elm/Styles.elm @@ -253,7 +253,7 @@ deleteLabel = link : String link = - " text-blue-400 hover:text-blue-500 dark:text-sky-200 dark:hover:text-sky-100 cursor-pointer " + " text-blue-600 hover:text-blue-500 dark:text-sky-200 dark:hover:text-sky-100 cursor-pointer " inputErrorBorder : String diff --git a/modules/webapp/src/main/elm/Util/Result.elm b/modules/webapp/src/main/elm/Util/Result.elm index cc8844cc..b93432cd 100644 --- a/modules/webapp/src/main/elm/Util/Result.elm +++ b/modules/webapp/src/main/elm/Util/Result.elm @@ -5,7 +5,10 @@ -} -module Util.Result exposing (fold) +module Util.Result exposing (combine, fold) + +import Api.Model.BasicResult exposing (BasicResult) +import Set fold : (a -> x) -> (b -> x) -> Result b a -> x @@ -16,3 +19,12 @@ fold fa fb rba = Err b -> fb b + + +combine : BasicResult -> BasicResult -> BasicResult +combine r1 r2 = + BasicResult (r1.success && r2.success) + (Set.fromList [ r1.message, r2.message ] + |> Set.toList + |> String.join ", " + ) diff --git a/modules/webapp/src/main/styles/keep.txt b/modules/webapp/src/main/styles/keep.txt index 02ecc86f..d7588992 100644 --- a/modules/webapp/src/main/styles/keep.txt +++ b/modules/webapp/src/main/styles/keep.txt @@ -33,3 +33,31 @@ them using string concatenation, which cannot be detected by postcss. elm-datepicker--other-month ds-card-search-hl strong + + +gap-0 +gap-1 +gap-2 +gap-3 +gap-4 +gap-5 +gap-6 +gap-7 +gap-8 +gap-9 +gap-10 +gap-11 +gap-12 + +md:grid-cols-1 +md:grid-cols-2 +md:grid-cols-3 +md:grid-cols-4 +md:grid-cols-5 +md:grid-cols-6 +md:grid-cols-7 +md:grid-cols-8 +md:grid-cols-9 +md:grid-cols-10 +md:grid-cols-11 +md:grid-cols-12 diff --git a/website/site/content/docs/dev/translation.md b/website/site/content/docs/dev/translation.md index 27445404..e4520e65 100644 --- a/website/site/content/docs/dev/translation.md +++ b/website/site/content/docs/dev/translation.md @@ -247,7 +247,7 @@ de = , queue = Messages.Page.Queue.gb , userSettings = Messages.Page.UserSettings.gb , manageData = Messages.Page.ManageData.gb - , home = Messages.Page.Home.gb + , search = Messages.Page.Search.gb } ``` @@ -340,7 +340,7 @@ de = , queue = Messages.Page.Queue.gb , userSettings = Messages.Page.UserSettings.gb , manageData = Messages.Page.ManageData.gb - , home = Messages.Page.Home.gb + , search = Messages.Page.Search.gb } ``` diff --git a/website/site/content/docs/features/_index.md b/website/site/content/docs/features/_index.md index 03db3604..1f305af1 100644 --- a/website/site/content/docs/features/_index.md +++ b/website/site/content/docs/features/_index.md @@ -20,6 +20,7 @@ description = "A list of features and limitations." - A powerful [query language](@/docs/query/_index.md) to find documents - use [bookmarks](@/docs/webapp/bookmarks.md) to save more complex queries +- customizable [dashboads](@/docs/webapp/dashboards.md) as the main page - Non-destructive: all your uploaded files are never modified and can always be downloaded untouched - Organize files using tags, folders, [Custom diff --git a/website/site/content/docs/query/_index.md b/website/site/content/docs/query/_index.md index ecfac32a..ff59e37a 100644 --- a/website/site/content/docs/query/_index.md +++ b/website/site/content/docs/query/_index.md @@ -10,7 +10,8 @@ mktoc = true Docspell uses a query language to provide a powerful way to search for your documents. It is targeted at "power users" and it needs to be -enabled explicitely in your user settings. +enabled explicitely in your user settings to be used on the search +page.
diff --git a/website/site/content/docs/webapp/dashboards-01.png b/website/site/content/docs/webapp/dashboards-01.png new file mode 100644 index 00000000..8aeaee2d Binary files /dev/null and b/website/site/content/docs/webapp/dashboards-01.png differ diff --git a/website/site/content/docs/webapp/dashboards-01d.png b/website/site/content/docs/webapp/dashboards-01d.png new file mode 100644 index 00000000..695694c8 Binary files /dev/null and b/website/site/content/docs/webapp/dashboards-01d.png differ diff --git a/website/site/content/docs/webapp/dashboards-02.png b/website/site/content/docs/webapp/dashboards-02.png new file mode 100644 index 00000000..928d02fb Binary files /dev/null and b/website/site/content/docs/webapp/dashboards-02.png differ diff --git a/website/site/content/docs/webapp/dashboards-03.png b/website/site/content/docs/webapp/dashboards-03.png new file mode 100644 index 00000000..0feed509 Binary files /dev/null and b/website/site/content/docs/webapp/dashboards-03.png differ diff --git a/website/site/content/docs/webapp/dashboards-04.png b/website/site/content/docs/webapp/dashboards-04.png new file mode 100644 index 00000000..9df746fd Binary files /dev/null and b/website/site/content/docs/webapp/dashboards-04.png differ diff --git a/website/site/content/docs/webapp/dashboards-05.png b/website/site/content/docs/webapp/dashboards-05.png new file mode 100644 index 00000000..21efa0c0 Binary files /dev/null and b/website/site/content/docs/webapp/dashboards-05.png differ diff --git a/website/site/content/docs/webapp/dashboards.md b/website/site/content/docs/webapp/dashboards.md new file mode 100644 index 00000000..72d61b7c --- /dev/null +++ b/website/site/content/docs/webapp/dashboards.md @@ -0,0 +1,124 @@ ++++ +title = "Dashboards" +weight = 5 +[extra] +mktoc = true ++++ + +The main page shows a dashboard that can be configured to show some +aspects of your documents. The following shows the default dashboard +that is bundled in the application: + +{{ figure(file="dashboards-01.png") }} + +It shows a predefined set of information, which can be customized. You +can create multiple dashboards and switch between them, you can also +define one as the "default" which is shown when the page loads. + + +# Side menu + +The side menu contains a list of useful links. The first loads the +default dashboard. All others (and more) are available through the +top-right menus as well. + +The _Bookmarks_ section shows all your bookmarks and a click takes you +directly to the search page with the corresponding bookmark active. + +The _Settings_ section contains some links to useful settings and the +_Manage_ section has links to metadata that can be managed separately. +These links are fixed and cannot be changed. + +# Dasbhoard properties + +The main component on this page is the "dashboard". A dashboard has +the following properties (all required): + +- _name_: which must be unique among all dashboards +- _columns_: the dashboard is a grid that has this number + of columns, where each box can span any number of columns +- _gap_: defines the gap between boxes +- _scope_: (Personal or Collective) whether it is a personal or a + collective dashboard. Personal dashboards can only be seen by its + user, whereas collective dashboards can be used by all users of a + collective. Every user can change/delete collective dashboards. +- _default_ wether this dashboard is the default one and will be + displayed when the page is loaded +- a list of _boxes_ that define the content + +# Customizing + +When clicking on _Edit Dashboard_ down in the side menu, the currently +viewed dashboard changes into a form that show the dashboards +properties. You can change the content by changing, moving, adding and +removing boxes and the dashboard properties. + +## Editing dashboard properties + +After clicking _Edit Dashboard_ the dashboard changes into a form: + +{{ figure(file="dashboards-02.png") }} + +Note the message on the top: it indicates that this dashboard is the +bundled one that is used only if there are no custom ones available. +That's why you can't delete it, for example. As soon as you click +save, the configuration is stored at the server and the bundled +dashboard will not show up again - until you delete all of your custom +ones. + +A dashboard must have a unique name, in this example the name is just +left as is. You can change things by using the forms and you can +reorder the boxes using the arrow buttons or drag and drop. When +satisfied, click _Submit_. In the example, the last two boxes are +removed and box decorations are enabled for the field overview box. + +{{ figure(file="dashboards-03.png") }} + +When you now edit this dasbhoard again, the message is gone and you +can change the dashboard and also delete it. You can also create a new +dashboard or copy the current one. + +{{ figure(file="dashboards-04.png") }} + +In this example, the dashboard was copied, then the message was +changed and it was set to the default dashboard. This is how it looks +now: + +{{ figure(file="dashboards-05.png") }} + +When there is more than one dashboard, the side menu shows all of +them. The little house icon indicates whether this is the default +dashboard. You can click on any dashboard in order to load it. + +## Content + +### Message + +The simplest form is a message box that displays a title and a body. +Both fields support markdown for basic formatting. + +### Upload + +The upload box can be used to show a file upload form. It can be +optionally configured with a +[source](@/docs/webapp/uploading.md#anonymous-upload). If no source is +configured, it is just the main upload form using all default settings +(skip duplicates, using the collective langauge etc). + +### Stats + +The stats box can display basic statistics given a [search +query](@/docs/query/_index.md) or a +[bookmark](@/docs/webapp/bookmarks.md). The query can be empty, which +means to not restrict the results. + +It is possible to display the numbers in a simple table or show an +overview of some custom fields. + +### Query + +The query box can be used to display a table of documents returned +from a [search query](@/docs/query/_index.md) or a +[bookmark](@/docs/webapp/bookmarks.md). You can choose from a set of +columns what to display. The first column will always be rendered as a +link to the corresponding item. diff --git a/website/site/content/docs/webapp/uploading.md b/website/site/content/docs/webapp/uploading.md index 015d931f..c98182b5 100644 --- a/website/site/content/docs/webapp/uploading.md +++ b/website/site/content/docs/webapp/uploading.md @@ -1,6 +1,6 @@ +++ title = "Uploads" -weight = 0 +weight = 8 +++ This page describes, how files can get into docspell. Technically,