Introduce ui settings and let user set page size for item search

This commit is contained in:
Eike Kettner 2020-06-07 00:51:11 +02:00
parent 6abdb95f02
commit 79fc5a30a1
13 changed files with 530 additions and 66 deletions

View File

@ -11,6 +11,7 @@ import Api.Model.VersionInfo exposing (VersionInfo)
import Browser exposing (UrlRequest)
import Browser.Navigation exposing (Key)
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings)
import Http
import Page exposing (Page(..))
import Page.CollectiveSettings.Data
@ -90,6 +91,7 @@ type Msg
| LogoutResp (Result Http.Error ())
| SessionCheckResp (Result Http.Error AuthResult)
| ToggleNavMenu
| GetUiSettings UiSettings
isSignedIn : Flags -> Bool

View File

@ -40,7 +40,7 @@ update msg model =
( m, c, s ) =
updateWithSub msg model
in
( { m | subs = s }, c )
( { m | subs = Sub.batch [ m.subs, s ] }, c )
updateWithSub : Msg -> Model -> ( Model, Cmd Msg, Sub Msg )
@ -92,7 +92,10 @@ updateWithSub msg model =
)
LogoutResp _ ->
( { model | loginModel = Page.Login.Data.emptyModel }, Page.goto (LoginPage Nothing), Sub.none )
( { model | loginModel = Page.Login.Data.emptyModel }
, Page.goto (LoginPage Nothing)
, Sub.none
)
SessionCheckResp res ->
case res of
@ -171,6 +174,14 @@ updateWithSub msg model =
ToggleNavMenu ->
( { model | navMenuOpen = not model.navMenuOpen }, Cmd.none, Sub.none )
GetUiSettings settings ->
Util.Update.andThen1
[ updateUserSettings (Page.UserSettings.Data.GetUiSettings settings)
, updateHome (Page.Home.Data.GetUiSettings settings)
]
model
|> noSub
updateItemDetail : Page.ItemDetail.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg )
updateItemDetail lmsg model =
@ -241,10 +252,17 @@ updateQueue lmsg model =
updateUserSettings : Page.UserSettings.Data.Msg -> Model -> ( Model, Cmd Msg )
updateUserSettings lmsg model =
let
( lm, lc ) =
( lm, lc, ls ) =
Page.UserSettings.Update.update model.flags lmsg model.userSettingsModel
in
( { model | userSettingsModel = lm }
( { model
| userSettingsModel = lm
, subs =
Sub.batch
[ model.subs
, Sub.map UserSettingsMsg ls
]
}
, Cmd.map UserSettingsMsg lc
)

View File

@ -0,0 +1,93 @@
module Comp.UiSettingsForm exposing
( Model
, Msg
, init
, initWith
, update
, view
)
import Comp.IntField
import Data.UiSettings exposing (StoredUiSettings, UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
type alias Model =
{ defaults : UiSettings
, input : StoredUiSettings
, searchPageSizeModel : Comp.IntField.Model
}
initWith : UiSettings -> Model
initWith defaults =
{ defaults = defaults
, input = Data.UiSettings.toStoredUiSettings defaults
, searchPageSizeModel =
Comp.IntField.init
(Just 10)
(Just 500)
False
"Item search page"
}
init : Model
init =
initWith Data.UiSettings.defaults
changeInput : (StoredUiSettings -> StoredUiSettings) -> Model -> StoredUiSettings
changeInput change model =
change model.input
type Msg
= SearchPageSizeMsg Comp.IntField.Msg
getSettings : Model -> UiSettings
getSettings model =
Data.UiSettings.merge model.input model.defaults
--- Update
update : Msg -> Model -> ( Model, Maybe UiSettings )
update msg model =
case msg of
SearchPageSizeMsg lm ->
let
( m, n ) =
Comp.IntField.update lm model.searchPageSizeModel
model_ =
{ model
| searchPageSizeModel = m
, input = changeInput (\s -> { s | itemSearchPageSize = n }) model
}
nextSettings =
Maybe.map (\_ -> getSettings model_) n
in
( model_, nextSettings )
--- View
view : Model -> Html Msg
view model =
div [ class "ui form" ]
[ Html.map SearchPageSizeMsg
(Comp.IntField.viewWithInfo
"Maximum results in one page when searching items."
model.input.itemSearchPageSize
""
model.searchPageSizeModel
)
]

View File

@ -0,0 +1,119 @@
module Comp.UiSettingsManage exposing
( Model
, Msg
, init
, update
, view
)
import Api.Model.BasicResult exposing (BasicResult)
import Comp.UiSettingsForm
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Ports
type alias Model =
{ formModel : Comp.UiSettingsForm.Model
, settings : Maybe UiSettings
, message : Maybe BasicResult
}
type Msg
= UiSettingsFormMsg Comp.UiSettingsForm.Msg
| Submit
| SettingsSaved
init : UiSettings -> Model
init defaults =
{ formModel = Comp.UiSettingsForm.initWith defaults
, settings = Nothing
, message = Nothing
}
--- update
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg )
update flags msg model =
case msg of
UiSettingsFormMsg lm ->
let
( m_, sett ) =
Comp.UiSettingsForm.update lm model.formModel
in
( { model
| formModel = m_
, settings = sett
, message = Nothing
}
, Cmd.none
, Sub.none
)
Submit ->
case model.settings of
Just s ->
( { model | message = Nothing }
, Ports.storeUiSettings flags s
, Ports.onUiSettingsSaved SettingsSaved
)
Nothing ->
( { model | message = Just (BasicResult False "Settings unchanged or invalid.") }
, Cmd.none
, Sub.none
)
SettingsSaved ->
( { model | message = Just (BasicResult True "Settings saved.") }
, Cmd.none
, Sub.none
)
--- View
isError : Model -> Bool
isError model =
Maybe.map .success model.message == Just False
isSuccess : Model -> Bool
isSuccess model =
Maybe.map .success model.message == Just True
view : String -> Model -> Html Msg
view classes model =
div [ class classes ]
[ Html.map UiSettingsFormMsg (Comp.UiSettingsForm.view model.formModel)
, div [ class "ui divider" ] []
, button
[ class "ui primary button"
, onClick Submit
]
[ text "Submit"
]
, div
[ classList
[ ( "ui message", True )
, ( "success", isSuccess model )
, ( "error", isError model )
, ( "hidden invisible", model.message == Nothing )
]
]
[ Maybe.map .message model.message
|> Maybe.withDefault ""
|> text
]
]

View File

@ -0,0 +1,63 @@
module Data.UiSettings exposing
( StoredUiSettings
, UiSettings
, defaults
, merge
, mergeDefaults
, toStoredUiSettings
)
{-| Settings for the web ui. All fields should be optional, since it
is loaded from local storage.
Making fields optional, allows it to evolve without breaking previous
versions. Also if a user is logged out, an empty object is send to
force default settings.
-}
type alias StoredUiSettings =
{ itemSearchPageSize : Maybe Int
}
{-| Settings for the web ui. These fields are all mandatory, since
there is always a default value.
When loaded from local storage, all optional fields can fallback to a
default value, converting the StoredUiSettings into a UiSettings.
-}
type alias UiSettings =
{ itemSearchPageSize : Int
}
defaults : UiSettings
defaults =
{ itemSearchPageSize = 90
}
merge : StoredUiSettings -> UiSettings -> UiSettings
merge given fallback =
{ itemSearchPageSize =
choose given.itemSearchPageSize fallback.itemSearchPageSize
}
mergeDefaults : StoredUiSettings -> UiSettings
mergeDefaults given =
merge given defaults
toStoredUiSettings : UiSettings -> StoredUiSettings
toStoredUiSettings settings =
{ itemSearchPageSize = Just settings.itemSearchPageSize
}
choose : Maybe a -> a -> a
choose m1 m2 =
Maybe.withDefault m2 m1

View File

@ -11,6 +11,7 @@ import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Page
import Ports
import Url exposing (Url)
@ -59,7 +60,12 @@ init flags url key =
Cmd.none
in
( m
, Cmd.batch [ cmd, Api.versionInfo flags VersionResp, sessionCheck ]
, Cmd.batch
[ cmd
, Api.versionInfo flags VersionResp
, sessionCheck
, Ports.getUiSettings flags
]
)
@ -76,4 +82,7 @@ viewDoc model =
subscriptions : Model -> Sub Msg
subscriptions model =
model.subs
Sub.batch
[ model.subs
, Ports.loadUiSettings GetUiSettings
]

View File

@ -6,7 +6,6 @@ module Page.Home.Data exposing
, init
, itemNav
, resultsBelowLimit
, searchLimit
)
import Api
@ -15,6 +14,7 @@ import Comp.ItemCardList
import Comp.SearchMenu
import Data.Flags exposing (Flags)
import Data.Items
import Data.UiSettings exposing (UiSettings)
import Http
@ -27,6 +27,7 @@ type alias Model =
, searchOffset : Int
, moreAvailable : Bool
, moreInProgress : Bool
, uiSettings : UiSettings
}
@ -40,6 +41,7 @@ init _ =
, searchOffset = 0
, moreAvailable = True
, moreInProgress = False
, uiSettings = Data.UiSettings.defaults
}
@ -49,9 +51,11 @@ type Msg
| ResetSearch
| ItemCardListMsg Comp.ItemCardList.Msg
| ItemSearchResp (Result Http.Error ItemLightList)
| ItemSearchAddResp (Result Http.Error ItemLightList)
| DoSearch
| ToggleSearchMenu
| LoadMore
| GetUiSettings UiSettings
type ViewMode
@ -73,24 +77,23 @@ itemNav id model =
}
searchLimit : Int
searchLimit =
90
doSearchCmd : Flags -> Int -> Comp.SearchMenu.Model -> Cmd Msg
doSearchCmd : Flags -> Int -> Model -> Cmd Msg
doSearchCmd flags offset model =
let
smask =
Comp.SearchMenu.getItemSearch model
Comp.SearchMenu.getItemSearch model.searchMenuModel
mask =
{ smask
| limit = searchLimit
| limit = model.uiSettings.itemSearchPageSize
, offset = offset
}
in
Api.itemSearch flags mask ItemSearchResp
if offset == 0 then
Api.itemSearch flags mask ItemSearchResp
else
Api.itemSearch flags mask ItemSearchAddResp
resultsBelowLimit : Model -> Bool
@ -99,4 +102,4 @@ resultsBelowLimit model =
len =
Data.Items.length model.itemListModel.results
in
len < searchLimit
len < model.uiSettings.itemSearchPageSize

View File

@ -15,7 +15,6 @@ update key flags msg model =
Init ->
Util.Update.andThen1
[ update key flags (SearchMenuMsg Comp.SearchMenu.Init)
, doSearch flags
]
model
@ -63,7 +62,22 @@ update key flags msg model =
ItemSearchResp (Ok list) ->
let
noff =
model.searchOffset + searchLimit
model.uiSettings.itemSearchPageSize
m =
{ model
| searchInProgress = False
, searchOffset = noff
, viewMode = Listing
, moreAvailable = list.groups /= []
}
in
update key flags (ItemCardListMsg (Comp.ItemCardList.SetResults list)) m
ItemSearchAddResp (Ok list) ->
let
noff =
model.searchOffset + model.uiSettings.itemSearchPageSize
m =
{ model
@ -74,11 +88,14 @@ update key flags msg model =
, moreAvailable = list.groups /= []
}
in
if model.searchOffset == 0 then
update key flags (ItemCardListMsg (Comp.ItemCardList.SetResults list)) m
update key flags (ItemCardListMsg (Comp.ItemCardList.AddResults list)) m
else
update key flags (ItemCardListMsg (Comp.ItemCardList.AddResults list)) m
ItemSearchAddResp (Err _) ->
( { model
| moreInProgress = False
}
, Cmd.none
)
ItemSearchResp (Err _) ->
( { model
@ -106,12 +123,19 @@ update key flags msg model =
else
( model, Cmd.none )
GetUiSettings settings ->
let
m_ =
{ model | uiSettings = settings }
in
doSearch flags m_
doSearch : Flags -> Model -> ( Model, Cmd Msg )
doSearch flags model =
let
cmd =
doSearchCmd flags 0 model.searchMenuModel
doSearchCmd flags 0 model
in
( { model
| searchInProgress = True
@ -126,7 +150,7 @@ doSearchMore : Flags -> Model -> ( Model, Cmd Msg )
doSearchMore flags model =
let
cmd =
doSearchCmd flags model.searchOffset model.searchMenuModel
doSearchCmd flags model.searchOffset model
in
( { model | moreInProgress = True, viewMode = Listing }
, cmd

View File

@ -10,7 +10,9 @@ import Comp.EmailSettingsManage
import Comp.ImapSettingsManage
import Comp.NotificationForm
import Comp.ScanMailboxManage
import Comp.UiSettingsManage
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings)
type alias Model =
@ -20,6 +22,7 @@ type alias Model =
, imapSettingsModel : Comp.ImapSettingsManage.Model
, notificationModel : Comp.NotificationForm.Model
, scanMailboxModel : Comp.ScanMailboxManage.Model
, uiSettingsModel : Comp.UiSettingsManage.Model
}
@ -31,6 +34,7 @@ emptyModel flags =
, imapSettingsModel = Comp.ImapSettingsManage.emptyModel
, notificationModel = Tuple.first (Comp.NotificationForm.init flags)
, scanMailboxModel = Tuple.first (Comp.ScanMailboxManage.init flags)
, uiSettingsModel = Comp.UiSettingsManage.init Data.UiSettings.defaults
}
@ -40,6 +44,7 @@ type Tab
| ImapSettingsTab
| NotificationTab
| ScanMailboxTab
| UiSettingsTab
type Msg
@ -49,3 +54,5 @@ type Msg
| NotificationMsg Comp.NotificationForm.Msg
| ImapSettingsMsg Comp.ImapSettingsManage.Msg
| ScanMailboxMsg Comp.ScanMailboxManage.Msg
| GetUiSettings UiSettings
| UiSettingsMsg Comp.UiSettingsManage.Msg

View File

@ -5,75 +5,76 @@ import Comp.EmailSettingsManage
import Comp.ImapSettingsManage
import Comp.NotificationForm
import Comp.ScanMailboxManage
import Comp.UiSettingsManage
import Data.Flags exposing (Flags)
import Page.UserSettings.Data exposing (..)
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg )
update flags msg model =
case msg of
SetTab t ->
let
m =
{ model | currentTab = Just t }
( m2, cmd ) =
case t of
EmailSettingsTab ->
let
( em, c ) =
Comp.EmailSettingsManage.init flags
in
( { m | emailSettingsModel = em }, Cmd.map EmailSettingsMsg c )
ImapSettingsTab ->
let
( em, c ) =
Comp.ImapSettingsManage.init flags
in
( { m | imapSettingsModel = em }, Cmd.map ImapSettingsMsg c )
ChangePassTab ->
( m, Cmd.none )
NotificationTab ->
let
initCmd =
Cmd.map NotificationMsg
(Tuple.second (Comp.NotificationForm.init flags))
in
( m, initCmd )
ScanMailboxTab ->
let
initCmd =
Cmd.map ScanMailboxMsg
(Tuple.second (Comp.ScanMailboxManage.init flags))
in
( m, initCmd )
in
( m2, cmd )
case t of
EmailSettingsTab ->
let
( em, c ) =
Comp.EmailSettingsManage.init flags
in
( { m | emailSettingsModel = em }, Cmd.map EmailSettingsMsg c, Sub.none )
ImapSettingsTab ->
let
( em, c ) =
Comp.ImapSettingsManage.init flags
in
( { m | imapSettingsModel = em }, Cmd.map ImapSettingsMsg c, Sub.none )
ChangePassTab ->
( m, Cmd.none, Sub.none )
NotificationTab ->
let
initCmd =
Cmd.map NotificationMsg
(Tuple.second (Comp.NotificationForm.init flags))
in
( m, initCmd, Sub.none )
ScanMailboxTab ->
let
initCmd =
Cmd.map ScanMailboxMsg
(Tuple.second (Comp.ScanMailboxManage.init flags))
in
( m, initCmd, Sub.none )
UiSettingsTab ->
( m, Cmd.none, Sub.none )
ChangePassMsg m ->
let
( m2, c2 ) =
Comp.ChangePasswordForm.update flags m model.changePassModel
in
( { model | changePassModel = m2 }, Cmd.map ChangePassMsg c2 )
( { model | changePassModel = m2 }, Cmd.map ChangePassMsg c2, Sub.none )
EmailSettingsMsg m ->
let
( m2, c2 ) =
Comp.EmailSettingsManage.update flags m model.emailSettingsModel
in
( { model | emailSettingsModel = m2 }, Cmd.map EmailSettingsMsg c2 )
( { model | emailSettingsModel = m2 }, Cmd.map EmailSettingsMsg c2, Sub.none )
ImapSettingsMsg m ->
let
( m2, c2 ) =
Comp.ImapSettingsManage.update flags m model.imapSettingsModel
in
( { model | imapSettingsModel = m2 }, Cmd.map ImapSettingsMsg c2 )
( { model | imapSettingsModel = m2 }, Cmd.map ImapSettingsMsg c2, Sub.none )
NotificationMsg lm ->
let
@ -82,6 +83,7 @@ update flags msg model =
in
( { model | notificationModel = m2 }
, Cmd.map NotificationMsg c2
, Sub.none
)
ScanMailboxMsg lm ->
@ -91,4 +93,21 @@ update flags msg model =
in
( { model | scanMailboxModel = m2 }
, Cmd.map ScanMailboxMsg c2
, Sub.none
)
GetUiSettings settings ->
( { model | uiSettingsModel = Comp.UiSettingsManage.init settings }
, Cmd.none
, Sub.none
)
UiSettingsMsg lm ->
let
( m2, c2, s2 ) =
Comp.UiSettingsManage.update flags lm model.uiSettingsModel
in
( { model | uiSettingsModel = m2 }
, Cmd.map UiSettingsMsg c2
, Sub.map UiSettingsMsg s2
)

View File

@ -5,6 +5,7 @@ import Comp.EmailSettingsManage
import Comp.ImapSettingsManage
import Comp.NotificationForm
import Comp.ScanMailboxManage
import Comp.UiSettingsManage
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
@ -26,6 +27,7 @@ view model =
, makeTab model ImapSettingsTab "E-Mail Settings (IMAP)" "mail icon"
, makeTab model NotificationTab "Notification Task" "bullhorn icon"
, makeTab model ScanMailboxTab "Scan Mailbox Task" "envelope open outline icon"
, makeTab model UiSettingsTab "UI Settings" "cog icon"
]
]
]
@ -47,6 +49,9 @@ view model =
Just ScanMailboxTab ->
viewScanMailboxManage model
Just UiSettingsTab ->
viewUiSettings model
Nothing ->
[]
)
@ -66,6 +71,20 @@ makeTab model tab header icon =
]
viewUiSettings : Model -> List (Html Msg)
viewUiSettings model =
[ h2 [ class "ui header" ]
[ i [ class "cog icon" ] []
, text "UI Settings"
]
, p []
[ text "These settings only affect the web ui. They are stored in the browser, "
, text "so they are separated between browsers and devices."
]
, Html.map UiSettingsMsg (Comp.UiSettingsManage.view "ui segment" model.uiSettingsModel)
]
viewEmailSettings : Model -> List (Html Msg)
viewEmailSettings model =
[ h2 [ class "ui header" ]

View File

@ -1,14 +1,22 @@
port module Ports exposing
( removeAccount
( getUiSettings
, loadUiSettings
, onUiSettingsSaved
, removeAccount
, scrollToElem
, setAccount
, setAllProgress
, setProgress
, storeUiSettings
)
import Api.Model.AuthResult exposing (AuthResult)
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (StoredUiSettings, UiSettings)
{-| Save the result of authentication to local storage.
-}
port setAccount : AuthResult -> Cmd msg
@ -22,3 +30,45 @@ port setAllProgress : ( String, Int ) -> Cmd msg
port scrollToElem : String -> Cmd msg
port saveUiSettings : ( AuthResult, UiSettings ) -> Cmd msg
port receiveUiSettings : (StoredUiSettings -> msg) -> Sub msg
port requestUiSettings : ( AuthResult, UiSettings ) -> Cmd msg
port uiSettingsSaved : (() -> msg) -> Sub msg
onUiSettingsSaved : msg -> Sub msg
onUiSettingsSaved m =
uiSettingsSaved (\_ -> m)
storeUiSettings : Flags -> UiSettings -> Cmd msg
storeUiSettings flags settings =
case flags.account of
Just ar ->
saveUiSettings ( ar, settings )
Nothing ->
Cmd.none
loadUiSettings : (UiSettings -> msg) -> Sub msg
loadUiSettings tagger =
receiveUiSettings (Data.UiSettings.mergeDefaults >> tagger)
getUiSettings : Flags -> Cmd msg
getUiSettings flags =
case flags.account of
Just ar ->
requestUiSettings ( ar, Data.UiSettings.defaults )
Nothing ->
Cmd.none

View File

@ -5,6 +5,7 @@ var elmApp = Elm.Main.init({
flags: elmFlags
});
elmApp.ports.setAccount.subscribe(function(authResult) {
console.log("Add account from local storage");
localStorage.setItem("account", JSON.stringify(authResult));
@ -45,3 +46,40 @@ elmApp.ports.scrollToElem.subscribe(function(id) {
}, 20);
}
});
elmApp.ports.saveUiSettings.subscribe(function(args) {
if (Array.isArray(args) && args.length == 2) {
var authResult = args[0];
var settings = args[1];
if (authResult && settings) {
var key = authResult.collective + "/" + authResult.user + "/uiSettings";
console.log("Save ui settings to local storage");
localStorage.setItem(key, JSON.stringify(settings));
elmApp.ports.receiveUiSettings.send(settings);
elmApp.ports.uiSettingsSaved.send(null);
}
}
});
elmApp.ports.requestUiSettings.subscribe(function(args) {
console.log("Requesting ui settings");
if (Array.isArray(args) && args.length == 2) {
var account = args[0];
var defaults = args[1];
var collective = account ? account.collective : null;
var user = account ? account.user : null;
if (collective && user) {
var key = collective + "/" + user + "/uiSettings";
var settings = localStorage.getItem(key);
var data = settings ? JSON.parse(settings) : null;
if (data && defaults) {
$.extend(defaults, data);
elmApp.ports.receiveUiSettings.send(defaults);
} else if (defaults) {
elmApp.ports.receiveUiSettings.send(defaults);
}
} else if (defaults) {
elmApp.ports.receiveUiSettings.send(defaults);
}
}
});