First version of new ui based on tailwind

This drops fomantic-ui as css toolkit and introduces tailwindcss. With
tailwind there are no predefined components, but it's very easy to
create those. So customizing the look&feel is much simpler, most of
the time no additional css is needed.

This requires a complete rewrite of the markup + styles. Luckily all
logic can be kept as is. The now old ui is not removed, it is still
available by using a request header `Docspell-Ui` with a value of `1`
for the old ui and `2` for the new ui.

Another addition is "dev mode", where docspell serves assets with a
no-cache header, to disable browser caching. This makes developing a
lot easier.
This commit is contained in:
Eike Kettner
2021-01-29 20:48:27 +01:00
parent 442b76c5af
commit dd935454c9
140 changed files with 15077 additions and 214 deletions

View File

@ -0,0 +1,265 @@
module Page.CollectiveSettings.View2 exposing (viewContent, viewSidebar)
import Api.Model.TagCount exposing (TagCount)
import Comp.Basic as B
import Comp.CollectiveSettingsForm
import Comp.SourceManage
import Comp.UserManage
import Data.Flags exposing (Flags)
import Data.Icons as Icons
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Page.CollectiveSettings.Data exposing (..)
import Styles as S
import Util.Maybe
import Util.Size
viewSidebar : Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar visible _ _ model =
div
[ id "sidebar"
, class S.sidebar
, class S.sidebarBg
, classList [ ( "hidden", not visible ) ]
]
[ div [ class "" ]
[ h1 [ class S.header1 ]
[ text "Collective Settings"
]
]
, div [ class "flex flex-col my-2" ]
[ a
[ href "#"
, onClick (SetTab InsightsTab)
, menuEntryActive model InsightsTab
, class S.sidebarLink
]
[ i [ class "fa fa-chart-bar" ] []
, span
[ class "ml-3" ]
[ text "Insights" ]
]
, a
[ href "#"
, onClick (SetTab SourceTab)
, class S.sidebarLink
, menuEntryActive model SourceTab
]
[ Icons.sourceIcon2 ""
, span
[ class "ml-3" ]
[ text "Sources" ]
]
, a
[ href "#"
, onClick (SetTab SettingsTab)
, menuEntryActive model SettingsTab
, class S.sidebarLink
]
[ i [ class "fa fa-cog" ] []
, span
[ class "ml-3" ]
[ text "Settings" ]
]
, a
[ href "#"
, onClick (SetTab UserTab)
, menuEntryActive model UserTab
, class S.sidebarLink
]
[ i [ class "fa fa-user" ] []
, span
[ class "ml-3" ]
[ text "Users" ]
]
]
]
viewContent : Flags -> UiSettings -> Model -> Html Msg
viewContent flags settings model =
div
[ id "content"
, class S.content
]
(case model.currentTab of
Just UserTab ->
viewUsers settings model
Just SettingsTab ->
viewSettings flags settings model
Just InsightsTab ->
viewInsights flags model
Just SourceTab ->
viewSources flags settings model
Nothing ->
[]
)
--- Helper
menuEntryActive : Model -> Tab -> Attribute msg
menuEntryActive model tab =
if model.currentTab == Just tab then
class S.sidebarMenuItemActive
else
class ""
viewInsights : Flags -> Model -> List (Html Msg)
viewInsights flags model =
let
( coll, user ) =
Maybe.map (\a -> ( a.collective, a.user )) flags.account
|> Maybe.withDefault ( "", "" )
in
[ h1 [ class S.header1 ]
[ i [ class "fa fa-chart-bar font-thin" ] []
, span [ class "ml-2" ]
[ text "Insights"
]
]
, div [ class "mb-4" ]
[ hr [ class S.border ] []
]
, h2 [ class S.header3 ]
[ div [ class "flex flex-row space-x-6" ]
[ div
[ class ""
, title "Collective"
]
[ i [ class "fa fa-users" ] []
, span [ class "ml-2" ]
[ text coll
]
]
, div
[ class ""
, title "User"
]
[ i [ class "fa fa-user font-thin" ] []
, span [ class "ml-2" ]
[ text user
]
]
]
]
, div
[ class "py-2"
]
[ h4 [ class S.header3 ]
[ text "Items"
]
, div [ class "flex px-4 flex-wrap" ]
[ stats (String.fromInt (model.insights.incomingCount + model.insights.outgoingCount)) "Items"
, stats (String.fromInt model.insights.incomingCount) "Incoming"
, stats (String.fromInt model.insights.outgoingCount) "Outgoing"
]
]
, div
[ class "py-2"
]
[ h4 [ class S.header3 ]
[ text "Size"
]
, div [ class "flex px-4 flex-wrap" ]
[ stats (toFloat model.insights.itemSize |> Util.Size.bytesReadable Util.Size.B) "Size"
]
]
, div
[ class "py-2"
]
[ h4 [ class S.header3 ]
[ text "Tags"
]
, div [ class "flex px-4 flex-wrap" ]
(List.map makeTagStats
(List.sortBy .count model.insights.tagCloud.items
|> List.reverse
)
)
]
]
stats : String -> String -> Html msg
stats value label =
B.stats
{ rootClass = "mb-4"
, valueClass = "text-6xl"
, value = value
, label = label
}
makeTagStats : TagCount -> Html Msg
makeTagStats nc =
stats (String.fromInt nc.count) nc.tag.name
viewSources : Flags -> UiSettings -> Model -> List (Html Msg)
viewSources flags settings model =
[ h1
[ class S.header1
, class "inline-flex items-center"
]
[ Icons.sourceIcon2 ""
, div [ class "ml-3" ]
[ text "Sources"
]
]
, Html.map SourceMsg (Comp.SourceManage.view2 flags settings model.sourceModel)
]
viewUsers : UiSettings -> Model -> List (Html Msg)
viewUsers settings model =
[ h1
[ class S.header1
, class "inline-flex items-center"
]
[ i [ class "fa fa-user" ] []
, div [ class "ml-3" ]
[ text "Users"
]
]
, Html.map UserMsg (Comp.UserManage.view2 settings model.userModel)
]
viewSettings : Flags -> UiSettings -> Model -> List (Html Msg)
viewSettings flags settings model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ i [ class "fa fa-cog" ] []
, span [ class "ml-3" ]
[ text "Collective Settings"
]
]
, Html.map SettingsFormMsg
(Comp.CollectiveSettingsForm.view2 flags settings model.settingsModel)
, div
[ classList
[ ( "hidden", Util.Maybe.isEmpty model.submitResult )
, ( S.successMessage, Maybe.map .success model.submitResult |> Maybe.withDefault False )
, ( S.errorMessage, Maybe.map .success model.submitResult |> Maybe.map not |> Maybe.withDefault False )
]
, class "mt-2"
]
[ Maybe.map .message model.submitResult
|> Maybe.withDefault ""
|> text
]
]

View File

@ -7,6 +7,7 @@ module Page.Home.Data exposing
, SelectViewModel
, ViewMode(..)
, doSearchCmd
, editActive
, init
, initSelectViewModel
, itemNav
@ -23,8 +24,8 @@ import Api.Model.SearchStats exposing (SearchStats)
import Browser.Dom as Dom
import Comp.FixedDropdown
import Comp.ItemCardList
import Comp.ItemDetail.EditMenu exposing (SaveNameState(..))
import Comp.ItemDetail.FormChange exposing (FormChange)
import Comp.ItemDetail.MultiEditMenu exposing (SaveNameState(..))
import Comp.LinkTarget exposing (LinkTarget)
import Comp.SearchMenu
import Comp.YesNoDimmer
@ -61,7 +62,7 @@ type alias SelectViewModel =
{ ids : Set String
, action : SelectActionMode
, deleteAllConfirm : Comp.YesNoDimmer.Model
, editModel : Comp.ItemDetail.EditMenu.Model
, editModel : Comp.ItemDetail.MultiEditMenu.Model
, saveNameState : SaveNameState
, saveCustomFieldState : Set String
}
@ -72,7 +73,7 @@ initSelectViewModel =
{ ids = Set.empty
, action = NoneAction
, deleteAllConfirm = Comp.YesNoDimmer.initActive
, editModel = Comp.ItemDetail.EditMenu.init
, editModel = Comp.ItemDetail.MultiEditMenu.init
, saveNameState = SaveSuccess
, saveCustomFieldState = Set.empty
}
@ -148,6 +149,19 @@ selectActive model =
True
editActive : Model -> Bool
editActive model =
case model.viewMode of
SimpleView ->
False
SearchView ->
False
SelectView svm ->
svm.action == EditSelected
type Msg
= Init
| SearchMenuMsg Comp.SearchMenu.Msg
@ -162,6 +176,7 @@ type Msg
| UpdateThrottle
| SetBasicSearch String
| SearchTypeMsg (Comp.FixedDropdown.Msg SearchType)
| ToggleSearchType
| KeyUpSearchbarMsg (Maybe KeyCode)
| ScrollResult (Result Dom.Error ())
| ClearItemDetailId
@ -170,13 +185,14 @@ type Msg
| RequestDeleteSelected
| DeleteSelectedConfirmMsg Comp.YesNoDimmer.Msg
| EditSelectedItems
| EditMenuMsg Comp.ItemDetail.EditMenu.Msg
| EditMenuMsg Comp.ItemDetail.MultiEditMenu.Msg
| MultiUpdateResp FormChange (Result Http.Error BasicResult)
| ReplaceChangedItemsResp (Result Http.Error ItemLightList)
| DeleteAllResp (Result Http.Error BasicResult)
| UiSettingsUpdated
| SetLinkTarget LinkTarget
| SearchStatsResp (Result Http.Error SearchStats)
| TogglePreviewFullWidth
type SearchType
@ -206,7 +222,7 @@ searchTypeString st =
"Names"
ContentOnlySearch ->
"Contents Only"
"Contents"
itemNav : String -> Model -> ItemNav

View File

@ -0,0 +1,131 @@
module Page.Home.SideMenu exposing (view)
import Comp.Basic as B
import Comp.ItemDetail.MultiEditMenu
import Comp.MenuBar as MB
import Comp.SearchMenu
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Page.Home.Data exposing (..)
import Set
import Styles as S
view : Flags -> UiSettings -> Model -> Html Msg
view flags settings model =
div
[ class "flex flex-col"
]
[ MB.viewSide
{ end =
[ MB.CustomButton
{ tagger = ToggleSelectView
, label = ""
, icon = Just "fa fa-tasks"
, title = "Edit Mode"
, inputClass =
[ ( S.secondaryBasicButton, True )
, ( "bg-gray-200 dark:bg-bluegray-600", selectActive model )
]
}
, MB.CustomButton
{ tagger = ResetSearch
, label = ""
, icon = Just "fa fa-sync"
, title = "Reset search form"
, inputClass = [ ( S.secondaryBasicButton, True ) ]
}
]
, start = []
, rootClasses = "text-sm w-full bg-blue-50 pt-2 hidden"
}
, div [ class "flex flex-col" ]
(case model.viewMode of
SelectView svm ->
case svm.action of
EditSelected ->
viewEditMenu svm settings
_ ->
viewSearch flags settings model
_ ->
viewSearch flags settings model
)
]
viewSearch : Flags -> UiSettings -> Model -> List (Html Msg)
viewSearch flags settings model =
[ MB.viewSide
{ start =
[ MB.CustomElement <|
B.secondaryBasicButton
{ label = ""
, icon = "fa fa-expand-alt"
, handler = onClick (SearchMenuMsg Comp.SearchMenu.ToggleOpenAllAkkordionTabs)
, attrs = [ href "#" ]
, disabled = False
}
]
, end = []
, rootClasses = "my-1 text-xs hidden sm:flex"
}
, Html.map SearchMenuMsg
(Comp.SearchMenu.viewDrop2 model.dragDropData
flags
settings
model.searchMenuModel
)
]
viewEditMenu : SelectViewModel -> UiSettings -> List (Html Msg)
viewEditMenu svm settings =
let
cfg_ =
Comp.ItemDetail.MultiEditMenu.defaultViewConfig
cfg =
{ cfg_
| nameState = svm.saveNameState
, customFieldState =
\fId ->
if Set.member fId svm.saveCustomFieldState then
Comp.ItemDetail.MultiEditMenu.Saving
else
Comp.ItemDetail.MultiEditMenu.SaveSuccess
}
in
[ div [ class S.header2 ]
[ i [ class "fa fa-edit" ] []
, span [ class "ml-2" ]
[ text "Multi-Edit"
]
]
, div [ class S.infoMessage ]
[ text "Note that a change here immediatly affects all selected items on the right!"
]
, MB.viewSide
{ start =
[ MB.CustomElement <|
B.secondaryButton
{ label = "Close"
, disabled = False
, icon = "fa fa-times"
, handler = onClick ToggleSelectView
, attrs =
[ href "#"
]
}
]
, end = []
, rootClasses = "mt-2 text-sm"
}
, Html.map EditMenuMsg
(Comp.ItemDetail.MultiEditMenu.view2 cfg settings svm.editModel)
]

View File

@ -7,8 +7,8 @@ import Api.Model.ItemSearch
import Browser.Navigation as Nav
import Comp.FixedDropdown
import Comp.ItemCardList
import Comp.ItemDetail.EditMenu exposing (SaveNameState(..))
import Comp.ItemDetail.FormChange exposing (FormChange(..))
import Comp.ItemDetail.MultiEditMenu exposing (SaveNameState(..))
import Comp.LinkTarget exposing (LinkTarget)
import Comp.SearchMenu
import Comp.YesNoDimmer
@ -18,6 +18,7 @@ import Data.Items
import Data.UiSettings exposing (UiSettings)
import Page exposing (Page(..))
import Page.Home.Data exposing (..)
import Ports
import Process
import Scroll
import Set exposing (Set)
@ -268,6 +269,14 @@ update mId key flags settings msg model =
in
update mId key flags settings smMsg model
ToggleSearchType ->
case model.searchTypeDropdownValue of
BasicSearch ->
update mId key flags settings (SearchMenuMsg Comp.SearchMenu.SetFulltextSearch) model
ContentOnlySearch ->
update mId key flags settings (SearchMenuMsg Comp.SearchMenu.SetNamesSearch) model
SearchTypeMsg lm ->
let
( sm, mv ) =
@ -452,7 +461,7 @@ update mId key flags settings msg model =
SelectView svm ->
let
res =
Comp.ItemDetail.EditMenu.update flags lmsg svm.editModel
Comp.ItemDetail.MultiEditMenu.update flags lmsg svm.editModel
svm_ =
{ svm
@ -560,6 +569,16 @@ update mId key flags settings msg model =
in
update mId key flags settings lm { model | searchStats = stats }
TogglePreviewFullWidth ->
let
newSettings =
{ settings | cardPreviewFullWidth = not settings.cardPreviewFullWidth }
cmd =
Ports.storeUiSettings flags newSettings
in
noSub ( model, cmd )
--- Helpers
@ -663,7 +682,7 @@ scrollToCard mId model =
loadEditModel : Flags -> Cmd Msg
loadEditModel flags =
Cmd.map EditMenuMsg (Comp.ItemDetail.EditMenu.loadModel flags)
Cmd.map EditMenuMsg (Comp.ItemDetail.MultiEditMenu.loadModel flags)
doSearch : SearchParam -> Model -> ( Model, Cmd Msg, Sub Msg )

View File

@ -3,7 +3,7 @@ module Page.Home.View exposing (view)
import Api.Model.ItemSearch
import Comp.FixedDropdown
import Comp.ItemCardList
import Comp.ItemDetail.EditMenu
import Comp.ItemDetail.MultiEditMenu
import Comp.SearchMenu
import Comp.SearchStatsView
import Comp.YesNoDimmer
@ -191,7 +191,7 @@ viewLeftMenu flags settings model =
EditSelected ->
let
cfg_ =
Comp.ItemDetail.EditMenu.defaultViewConfig
Comp.ItemDetail.MultiEditMenu.defaultViewConfig
cfg =
{ cfg_
@ -199,10 +199,10 @@ viewLeftMenu flags settings model =
, customFieldState =
\fId ->
if Set.member fId svm.saveCustomFieldState then
Comp.ItemDetail.EditMenu.Saving
Comp.ItemDetail.MultiEditMenu.Saving
else
Comp.ItemDetail.EditMenu.SaveSuccess
Comp.ItemDetail.MultiEditMenu.SaveSuccess
}
in
[ div [ class "ui dividing header" ]
@ -212,7 +212,7 @@ viewLeftMenu flags settings model =
[ text "Note that a change here immediatly affects all selected items on the right!"
]
, Html.map EditMenuMsg
(Comp.ItemDetail.EditMenu.view cfg settings svm.editModel)
(Comp.ItemDetail.MultiEditMenu.view cfg settings svm.editModel)
]
_ ->
@ -412,6 +412,5 @@ deleteAllDimmer =
, headerClass = "ui inverted icon header"
, confirmButton = "Yes"
, cancelButton = "No"
, invertedDimmer = False
, extraClass = "top aligned"
}

View File

@ -0,0 +1,320 @@
module Page.Home.View2 exposing (viewContent, viewSidebar)
import Comp.Basic as B
import Comp.ItemCardList
import Comp.MenuBar as MB
import Comp.SearchMenu
import Comp.SearchStatsView
import Comp.YesNoDimmer
import Data.Flags exposing (Flags)
import Data.ItemSelection
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)
import Page exposing (Page(..))
import Page.Home.Data exposing (..)
import Page.Home.SideMenu
import Set
import Styles as S
import Util.Html
viewSidebar : Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar visible flags settings model =
div
[ id "sidebar"
, class S.sidebar
, class S.sidebarBg
, classList [ ( "hidden", not visible ) ]
]
[ Page.Home.SideMenu.view flags settings model
]
viewContent : Flags -> UiSettings -> Model -> Html Msg
viewContent flags settings model =
div
[ id "item-card-list" -- this id is used in scroll-to-card
, class S.content
]
(searchStats flags settings model
++ itemsBar flags settings model
++ itemCardList flags settings model
++ deleteSelectedDimmer model
)
--- Helpers
deleteSelectedDimmer : Model -> List (Html Msg)
deleteSelectedDimmer model =
let
selectAction =
case model.viewMode of
SelectView svm ->
svm.action
_ ->
NoneAction
deleteAllDimmer : Comp.YesNoDimmer.Settings
deleteAllDimmer =
Comp.YesNoDimmer.defaultSettings2 "Really delete all selected items?"
in
case model.viewMode of
SelectView svm ->
[ Html.map DeleteSelectedConfirmMsg
(Comp.YesNoDimmer.viewN
(selectAction == DeleteSelected)
deleteAllDimmer
svm.deleteAllConfirm
)
]
_ ->
[]
itemsBar : Flags -> UiSettings -> Model -> List (Html Msg)
itemsBar flags settings model =
case model.viewMode of
SimpleView ->
[ defaultMenuBar flags settings model ]
SearchView ->
[ defaultMenuBar flags settings model ]
SelectView svm ->
[ editMenuBar model svm ]
defaultMenuBar : Flags -> UiSettings -> Model -> Html Msg
defaultMenuBar flags settings model =
let
btnStyle =
S.secondaryBasicButton ++ " text-sm"
searchInput =
Comp.SearchMenu.textSearchString
model.searchMenuModel.textSearchModel
in
MB.view
{ end =
[ MB.CustomElement <|
B.secondaryBasicButton
{ label = ""
, icon =
if model.searchInProgress then
"fa fa-sync animate-spin"
else
"fa fa-sync"
, disabled = model.searchInProgress
, handler = onClick ResetSearch
, attrs = [ href "#" ]
}
, MB.CustomButton
{ tagger = ToggleSelectView
, label = ""
, icon = Just "fa fa-tasks"
, title = "Select Mode"
, inputClass =
[ ( btnStyle, True )
, ( "bg-gray-200 dark:bg-bluegray-600", selectActive model )
]
}
]
, start =
[ MB.CustomElement <|
div
[ class "relative flex flex-row" ]
[ input
[ type_ "text"
, placeholder
(case model.searchTypeDropdownValue of
ContentOnlySearch ->
"Content search"
BasicSearch ->
"Search in names"
)
, onInput SetBasicSearch
, Util.Html.onKeyUpCode KeyUpSearchbarMsg
, Maybe.map value searchInput
|> Maybe.withDefault (value "")
, class (String.replace "rounded" "" S.textInput)
, class "py-1 text-sm border-r-0 rounded-l"
]
[]
, a
[ class S.secondaryBasicButtonPlain
, class "text-sm px-4 py-2 border rounded-r"
, href "#"
, onClick ToggleSearchType
]
[ i [ class "fa fa-exchange-alt" ] []
]
]
, MB.CustomButton
{ tagger = TogglePreviewFullWidth
, label = ""
, icon = Just "fa fa-expand"
, title =
if settings.cardPreviewFullWidth then
"Full height preview"
else
"Full width preview"
, inputClass =
[ ( btnStyle, True )
, ( "hidden sm:inline-block", False )
, ( "bg-gray-200 dark:bg-bluegray-600", settings.cardPreviewFullWidth )
]
}
]
, rootClasses = "mb-2 pt-1 dark:bg-bluegray-700 items-center text-sm"
}
editMenuBar : Model -> SelectViewModel -> Html Msg
editMenuBar model svm =
let
selectCount =
Set.size svm.ids |> String.fromInt
btnStyle =
S.secondaryBasicButton ++ " text-sm"
in
MB.view
{ start =
[ MB.CustomButton
{ tagger = EditSelectedItems
, label = ""
, icon = Just "fa fa-edit"
, title = "Edit " ++ selectCount ++ " selected items"
, inputClass =
[ ( btnStyle, True )
, ( "bg-gray-200 dark:bg-bluegray-600", svm.action == EditSelected )
]
}
, MB.CustomButton
{ tagger = RequestDeleteSelected
, label = ""
, icon = Just "fa fa-trash"
, title = "Delete " ++ selectCount ++ " selected items"
, inputClass =
[ ( btnStyle, True )
, ( "bg-gray-200 dark:bg-bluegray-600", svm.action == DeleteSelected )
]
}
]
, end =
[ MB.CustomButton
{ tagger = SelectAllItems
, label = ""
, icon = Just "fa fa-check-square font-thin"
, title = "Select all visible"
, inputClass =
[ ( btnStyle, True )
]
}
, MB.CustomButton
{ tagger = SelectNoItems
, label = ""
, icon = Just "fa fa-square font-thin"
, title = "Select none"
, inputClass =
[ ( btnStyle, True )
]
}
, MB.TextLabel
{ icon = ""
, label = selectCount
, class = "px-4 py-2 w-10 rounded-full font-bold bg-blue-100 dark:bg-lightblue-600 "
}
, MB.CustomButton
{ tagger = ResetSearch
, label = ""
, icon = Just "fa fa-sync"
, title = "Reset search form"
, inputClass =
[ ( btnStyle, True )
, ( "hidden sm:block", True )
]
}
, MB.CustomButton
{ tagger = ToggleSelectView
, label = ""
, icon = Just "fa fa-tasks"
, title = "Exit Select Mode"
, inputClass =
[ ( btnStyle, True )
, ( "bg-gray-200 dark:bg-bluegray-600", selectActive model )
]
}
]
, rootClasses = "mb-2 pt-2 sticky top-0 text-sm"
}
searchStats : Flags -> UiSettings -> Model -> List (Html Msg)
searchStats _ settings model =
if settings.searchStatsVisible then
[ Comp.SearchStatsView.view2 "my-2" model.searchStats
]
else
[]
itemCardList : Flags -> UiSettings -> Model -> List (Html Msg)
itemCardList flags settings model =
let
itemViewCfg =
case model.viewMode of
SelectView svm ->
Comp.ItemCardList.ViewConfig
model.scrollToCard
(Data.ItemSelection.Active svm.ids)
_ ->
Comp.ItemCardList.ViewConfig
model.scrollToCard
Data.ItemSelection.Inactive
in
[ Html.map ItemCardListMsg
(Comp.ItemCardList.view2 itemViewCfg settings model.itemListModel)
, loadMore model
]
loadMore : Model -> Html Msg
loadMore model =
let
inactive =
not model.moreAvailable || model.moreInProgress || model.searchInProgress
in
div
[ class "h-40 flex flex-col items-center justify-center w-full"
]
[ B.secondaryBasicButton
{ label =
if model.moreAvailable then
"Load more"
else
"That's all"
, icon =
if model.moreInProgress then
"fa fa-circle-notch animate-spin"
else
"fa fa-angle-double-down"
, handler = onClick LoadMore
, disabled = inactive
, attrs = []
}
]

View File

@ -0,0 +1,63 @@
module Page.ItemDetail.View2 exposing (viewContent, viewSidebar)
import Comp.Basic as B
import Comp.ItemDetail
import Comp.ItemDetail.EditForm
import Comp.ItemDetail.Model
import Comp.MenuBar as MB
import Data.Flags exposing (Flags)
import Data.ItemNav exposing (ItemNav)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Page.ItemDetail.Data exposing (..)
import Styles as S
viewSidebar : Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar visible _ settings model =
div
[ id "sidebar"
, class S.sidebar
, class S.sidebarBg
, classList [ ( "hidden", not visible ) ]
]
[ div
[ class S.header2
, class "font-bold mt-2"
]
[ i [ class "fa fa-pencil-alt mr-2" ] []
, text "Edit Metadata"
]
, MB.viewSide
{ start =
[ MB.CustomElement <|
B.secondaryBasicButton
{ label = ""
, icon = "fa fa-expand-alt"
, disabled = model.detail.item.state == "created"
, handler = onClick (ItemDetailMsg Comp.ItemDetail.Model.ToggleOpenAllAkkordionTabs)
, attrs =
[ title "Collapse/Expand"
, href "#"
]
}
]
, end = []
, rootClasses = "text-sm mb-3 "
}
, Html.map ItemDetailMsg
(Comp.ItemDetail.EditForm.view2 settings model.detail)
]
viewContent : ItemNav -> Flags -> UiSettings -> Model -> Html Msg
viewContent inav _ settings model =
div
[ id "content"
, class S.content
]
[ Html.map ItemDetailMsg
(Comp.ItemDetail.view2 inav settings model.detail)
]

View File

@ -0,0 +1,157 @@
module Page.Login.View2 exposing (viewContent, viewSidebar)
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onCheck, onInput, onSubmit)
import Page exposing (Page(..))
import Page.Login.Data exposing (..)
import Styles as S
viewSidebar : Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar _ _ _ _ =
div
[ id "sidebar"
, class "hidden"
]
[]
viewContent : Flags -> UiSettings -> Model -> Html Msg
viewContent flags _ model =
div
[ id "content"
, class "h-full flex flex-col items-center justify-center w-full"
, class S.content
]
[ div [ class ("flex flex-col px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md " ++ S.box) ]
[ div [ class "self-center" ]
[ img
[ class "w-16 py-2"
, src (flags.config.docspellAssetPath ++ "/img/logo-96.png")
]
[]
]
, div [ class "font-medium self-center text-xl sm:text-2xl" ]
[ text "Login to Docspell"
]
, Html.form
[ action "#"
, onSubmit Authenticate
, autocomplete False
]
[ div [ class "flex flex-col mt-6" ]
[ label
[ for "username"
, class S.inputLabel
]
[ text "Username"
]
, div [ class "relative" ]
[ div [ class S.inputIcon ]
[ i [ class "fa fa-user" ] []
]
, input
[ type_ "text"
, name "username"
, autocomplete False
, onInput SetUsername
, value model.username
, autofocus True
, class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput)
, placeholder "Collective / Login"
]
[]
]
]
, div [ class "flex flex-col my-3" ]
[ label
[ for "password"
, class S.inputLabel
]
[ text "Password"
]
, div [ class "relative" ]
[ div [ class S.inputIcon ]
[ i [ class "fa fa-lock" ] []
]
, input
[ type_ "password"
, name "password"
, autocomplete False
, onInput SetPassword
, value model.password
, class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput)
, placeholder "Password"
]
[]
]
]
, div [ class "flex flex-col my-3" ]
[ label
[ class "inline-flex items-center"
, for "rememberme"
]
[ input
[ id "rememberme"
, type_ "checkbox"
, onCheck (\_ -> ToggleRememberMe)
, checked model.rememberMe
, name "rememberme"
, class S.checkboxInput
]
[]
, span
[ class "mb-1 ml-2 text-xs sm:text-sm tracking-wide my-1"
]
[ text "Remember Me"
]
]
]
, div [ class "flex flex-col my-3" ]
[ button
[ type_ "submit"
, class S.primaryButton
]
[ text "Login"
]
]
, resultMessage model
, div
[ class "flex justify-end text-sm pt-4"
, classList [ ( "hidden", flags.config.signupMode == "closed" ) ]
]
[ span []
[ text "No account?"
]
, a
[ Page.href RegisterPage
, class ("ml-2" ++ S.link)
]
[ i [ class "fa fa-user-plus mr-1" ] []
, text "Sign up"
]
]
]
]
]
resultMessage : Model -> Html Msg
resultMessage model =
case model.result of
Just r ->
if r.success then
div [ class ("my-2" ++ S.successMessage) ]
[ text "Login successful."
]
else
div [ class ("my-2" ++ S.errorMessage) ]
[ text r.message
]
Nothing ->
span [ class "hidden" ] []

View File

@ -26,16 +26,20 @@ type alias Model =
init : Flags -> ( Model, Cmd Msg )
init _ =
( { currentTab = Nothing
, tagManageModel = Comp.TagManage.emptyModel
init flags =
let
( m2, c2 ) =
Comp.TagManage.update flags Comp.TagManage.LoadTags Comp.TagManage.emptyModel
in
( { currentTab = Just TagTab
, tagManageModel = m2
, equipManageModel = Comp.EquipmentManage.emptyModel
, orgManageModel = Comp.OrgManage.emptyModel
, personManageModel = Comp.PersonManage.emptyModel
, folderManageModel = Comp.FolderManage.empty
, fieldManageModel = Comp.CustomFieldManage.empty
}
, Cmd.none
, Cmd.map TagManageMsg c2
)

View File

@ -0,0 +1,245 @@
module Page.ManageData.View2 exposing (viewContent, viewSidebar)
import Comp.CustomFieldManage
import Comp.EquipmentManage
import Comp.FolderManage
import Comp.OrgManage
import Comp.PersonManage
import Comp.TagManage
import Data.Fields
import Data.Flags exposing (Flags)
import Data.Icons as Icons
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Page.ManageData.Data exposing (..)
import Styles as S
viewSidebar : Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar visible _ settings model =
div
[ id "sidebar"
, class S.sidebar
, class S.sidebarBg
, classList [ ( "hidden", not visible ) ]
]
[ div [ class "" ]
[ h1 [ class S.header1 ]
[ text "Manage Data"
]
]
, div [ class "flex flex-col my-2" ]
[ a
[ href "#"
, onClick (SetTab TagTab)
, class S.sidebarLink
, menuEntryActive model TagTab
]
[ Icons.tagIcon2 ""
, span
[ class "ml-3" ]
[ text "Tags" ]
]
, a
[ href "#"
, onClick (SetTab EquipTab)
, menuEntryActive model EquipTab
, class S.sidebarLink
]
[ Icons.equipmentIcon2 ""
, span
[ class "ml-3" ]
[ text "Equipment" ]
]
, a
[ href "#"
, onClick (SetTab OrgTab)
, menuEntryActive model OrgTab
, class S.sidebarLink
]
[ Icons.organizationIcon2 ""
, span
[ class "ml-3" ]
[ text "Organization" ]
]
, a
[ href "#"
, onClick (SetTab PersonTab)
, menuEntryActive model PersonTab
, class S.sidebarLink
]
[ Icons.personIcon2 ""
, span
[ class "ml-3" ]
[ text "Person" ]
]
, a
[ href "#"
, classList
[ ( "hidden"
, Data.UiSettings.fieldHidden settings Data.Fields.Folder
)
]
, onClick (SetTab FolderTab)
, menuEntryActive model FolderTab
, class S.sidebarLink
]
[ Icons.folderIcon2 ""
, span
[ class "ml-3" ]
[ text "Folder" ]
]
, a
[ href "#"
, classList
[ ( "invisible hidden"
, Data.UiSettings.fieldHidden settings Data.Fields.CustomFields
)
]
, onClick (SetTab CustomFieldTab)
, menuEntryActive model CustomFieldTab
, class S.sidebarLink
]
[ Icons.customFieldIcon2 ""
, span
[ class "ml-3" ]
[ text "Custom Fields" ]
]
]
]
viewContent : Flags -> UiSettings -> Model -> Html Msg
viewContent flags settings model =
div
[ id "content"
, class S.content
]
(case model.currentTab of
Just TagTab ->
viewTags model
Just EquipTab ->
viewEquip model
Just OrgTab ->
viewOrg settings model
Just PersonTab ->
viewPerson settings model
Just FolderTab ->
viewFolder flags settings model
Just CustomFieldTab ->
viewCustomFields flags settings model
Nothing ->
[]
)
menuEntryActive : Model -> Tab -> Attribute msg
menuEntryActive model tab =
if model.currentTab == Just tab then
class S.sidebarMenuItemActive
else
class ""
viewTags : Model -> List (Html Msg)
viewTags model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ Icons.tagIcon2 ""
, div [ class "ml-2" ]
[ text "Tags"
]
]
, Html.map TagManageMsg (Comp.TagManage.view2 model.tagManageModel)
]
viewEquip : Model -> List (Html Msg)
viewEquip model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ Icons.equipmentIcon2 ""
, div [ class "ml-2" ]
[ text "Equipment"
]
]
, Html.map EquipManageMsg (Comp.EquipmentManage.view2 model.equipManageModel)
]
viewOrg : UiSettings -> Model -> List (Html Msg)
viewOrg settings model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ Icons.organizationIcon2 ""
, div [ class "ml-2" ]
[ text "Organizations"
]
]
, Html.map OrgManageMsg (Comp.OrgManage.view2 settings model.orgManageModel)
]
viewPerson : UiSettings -> Model -> List (Html Msg)
viewPerson settings model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ Icons.personIcon2 ""
, div [ class "ml-2" ]
[ text "Person"
]
]
, Html.map PersonManageMsg
(Comp.PersonManage.view2 settings model.personManageModel)
]
viewFolder : Flags -> UiSettings -> Model -> List (Html Msg)
viewFolder flags _ model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ Icons.folderIcon2 ""
, div
[ class "ml-2"
]
[ text "Folder"
]
]
, Html.map FolderMsg
(Comp.FolderManage.view2 flags model.folderManageModel)
]
viewCustomFields : Flags -> UiSettings -> Model -> List (Html Msg)
viewCustomFields flags _ model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ Icons.customFieldIcon2 ""
, div [ class "ml-2" ]
[ text "Custom Fields"
]
]
, Html.map CustomFieldMsg
(Comp.CustomFieldManage.view2 flags model.fieldManageModel)
]

View File

@ -0,0 +1,137 @@
module Page.NewInvite.View2 exposing (viewContent, viewSidebar)
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput, onSubmit)
import Page.NewInvite.Data exposing (..)
import Styles as S
viewSidebar : Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar _ _ _ _ =
div
[ id "sidebar"
, class "hidden"
]
[]
viewContent : Flags -> UiSettings -> Model -> Html Msg
viewContent flags _ model =
div
[ id "content"
, class "flex flex-col md:w-3/5 px-2"
, class S.content
]
[ h1 [ class S.header1 ] [ text "Create new invitations" ]
, inviteMessage flags
, div [ class " py-2 mt-6 rounded" ]
[ Html.form
[ action "#"
, onSubmit GenerateInvite
, autocomplete False
]
[ div [ class "flex flex-col" ]
[ label
[ for "invitekey"
, class "mb-1 text-xs sm:text-sm tracking-wide "
]
[ text "Invitation key"
]
, div [ class "relative" ]
[ div [ class "inline-flex items-center justify-center absolute left-0 top-0 h-full w-10 text-gray-400 dark:text-bluegray-400 " ]
[ i [ class "fa fa-key" ] []
]
, input
[ id "email"
, type_ "password"
, name "invitekey"
, autocomplete False
, onInput SetPassword
, value model.password
, autofocus True
, class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput)
, placeholder "Password"
]
[]
]
]
, div [ class "flex flex-col my-3" ]
[ div [ class "flex flex-row space-x-2" ]
[ button
[ type_ "submit"
, class (S.primaryButton ++ "inline-flex")
]
[ text "Submit"
]
, a
[ class S.secondaryButton
, href "#"
, onClick Reset
]
[ text "Reset"
]
]
]
, resultMessage model
]
]
]
resultMessage : Model -> Html Msg
resultMessage model =
div
[ classList
[ ( S.errorMessage, isFailed model.result )
, ( S.successMessage, isSuccess model.result )
, ( "hidden", model.result == Empty )
]
]
[ case model.result of
Failed m ->
p [] [ text m ]
Success r ->
div [ class "" ]
[ p []
[ text r.message
, text " Invitation Key:"
]
, pre [ class "text-center font-mono mt-4" ]
[ Maybe.withDefault "" r.key |> text
]
]
Empty ->
span [ class "hidden" ] []
]
inviteMessage : Flags -> Html Msg
inviteMessage flags =
div
[ class (S.message ++ "text-sm")
, classList
[ ( "hidden", flags.config.signupMode /= "invite" )
]
]
[ p []
[ text
"""Docspell requires an invite when signing up. You can
create these invites here and send them to friends so
they can signup with docspell."""
]
, p []
[ text
"""Each invite can only be used once. You'll need to
create one key for each person you want to invite."""
]
, p []
[ text
"""Creating an invite requires providing the password
from the configuration."""
]
]

View File

@ -1,6 +1,7 @@
module Page.Queue.Data exposing
( Model
, Msg(..)
, QueueView(..)
, emptyModel
, getDuration
, getRunningTime
@ -27,9 +28,18 @@ type alias Model =
, showLog : Maybe JobDetail
, deleteConfirm : Comp.YesNoDimmer.Model
, cancelJobRequest : Maybe String
, queueView : QueueView
}
type QueueView
= CurrentJobs
| QueueAll
| QueueWaiting
| QueueError
| QueueSuccess
emptyModel : Model
emptyModel =
{ state = Api.Model.JobQueueState.empty
@ -41,6 +51,7 @@ emptyModel =
, showLog = Nothing
, deleteConfirm = Comp.YesNoDimmer.emptyModel
, cancelJobRequest = Nothing
, queueView = CurrentJobs
}
@ -55,6 +66,7 @@ type Msg
| DimmerMsg JobDetail Comp.YesNoDimmer.Msg
| CancelResp (Result Http.Error BasicResult)
| ChangePrio String Priority
| SetQueueView QueueView
getRunningTime : Model -> JobDetail -> Maybe String

View File

@ -86,6 +86,9 @@ update flags msg model =
ChangePrio id prio ->
( model, Api.setJobPrio flags id prio CancelResp )
SetQueueView v ->
( { model | queueView = v }, Cmd.none )
getNewTime : Cmd Msg
getNewTime =

View File

@ -0,0 +1,440 @@
module Page.Queue.View2 exposing (viewContent, viewSidebar)
import Api.Model.JobDetail exposing (JobDetail)
import Api.Model.JobLogEvent exposing (JobLogEvent)
import Comp.Progress
import Comp.YesNoDimmer
import Data.Flags exposing (Flags)
import Data.Priority
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Page.Queue.Data exposing (..)
import Styles as S
import Util.Time exposing (formatDateTime, formatIsoDateTime)
viewSidebar : Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar visible _ _ model =
let
tabLink cls v icon label =
a
[ href "#"
, class S.sidebarLink
, class cls
, classList [ ( "bg-blue-100 dark:bg-bluegray-600", model.queueView == v ) ]
, onClick (SetQueueView v)
]
[ i [ class icon ]
[]
, span
[ class "ml-3" ]
[ text label ]
]
in
div
[ id "sidebar"
, class S.sidebar
, class S.sidebarBg
, classList [ ( "hidden", not visible ) ]
]
[ div [ class "" ]
[ h1 [ class S.header1 ]
[ text "Processing Queue"
]
]
, div [ class "flex flex-col my-2" ]
[ tabLink "" CurrentJobs "fa fa-play-circle" "Currently Running"
, tabLink "" QueueAll "fa fa-hourglass-half" "Queue"
, tabLink "ml-8" QueueWaiting "fa fa-clock" "Waiting"
, tabLink "ml-8" QueueError "fa fa-bolt" "Errored"
, tabLink "ml-8" QueueSuccess "fa fa-check" "Success"
]
]
viewContent : Flags -> UiSettings -> Model -> Html Msg
viewContent _ _ model =
let
gridStyle =
"grid gap-4 grid-cols-1 md:grid-cols-2"
isState state job =
state == job.state
message str =
div [ class "h-28 flex flex-col items-center justify-center w-full" ]
[ div [ class S.header2 ]
[ text str
]
]
in
div
[ class "py-2"
, class S.content
]
[ case model.showLog of
Just job ->
renderJobLog job
Nothing ->
span [ class "hidden" ] []
, case model.queueView of
CurrentJobs ->
if List.isEmpty model.state.progress then
message "No jobs currently running."
else
div [ class "flex flex-col space-y-2" ]
(List.map (renderProgressCard model) model.state.progress)
QueueAll ->
if List.isEmpty model.state.completed && List.isEmpty model.state.completed then
message "No jobs to display."
else
div [ class gridStyle ]
(List.map (renderInfoCard model)
(model.state.queued ++ model.state.completed)
)
QueueWaiting ->
if List.isEmpty model.state.queued then
message "No waiting jobs."
else
div [ class gridStyle ]
(List.map (renderInfoCard model) model.state.queued)
QueueError ->
let
items =
List.filter (isState "failed") model.state.completed
in
if List.isEmpty items then
message "No failed jobs to display."
else
div [ class gridStyle ]
(List.map (renderInfoCard model) items)
QueueSuccess ->
let
items =
List.filter (isState "success") model.state.completed
in
if List.isEmpty items then
message "No succesfull jobs to display."
else
div [ class gridStyle ]
(List.map (renderInfoCard model) items)
]
renderJobLog : JobDetail -> Html Msg
renderJobLog job =
div
[ class " absolute top-12 left-0 w-full h-full-12 z-40 flex flex-col items-center px-4 py-2 "
, class "bg-white bg-opacity-80 dark:bg-black dark:bg-bluegray-900 dark:bg-opacity-90"
]
[ div [ class (S.box ++ "py-2 px-2 flex flex-col w-full") ]
[ div [ class "flex flex-row mb-4 px-2" ]
[ span [ class "font-semibold" ]
[ text job.name
]
, div [ class "flex-grow flex flex-row justify-end" ]
[ a
[ href "#"
, class S.link
, onClick QuitShowLog
]
[ i [ class "fa fa-times" ] []
]
]
]
, div [ class styleJobLog ]
(List.map renderLogLine job.logs)
]
]
renderProgressCard : Model -> JobDetail -> Html Msg
renderProgressCard model job =
div [ class (S.box ++ "px-2 flex flex-col") ]
[ Comp.Progress.topAttachedIndicating job.progress
, Html.map (DimmerMsg job)
(Comp.YesNoDimmer.viewN
(model.cancelJobRequest == Just job.id)
dimmerSettings
model.deleteConfirm
)
, div [ class "py-2 flex flex-row x-space-2 items-center" ]
[ div [ class "flex flex-row items-center py-0.5" ]
[ i [ class "fa fa-circle-notch animate-spin" ] []
, span [ class "ml-2" ]
[ text job.name
]
]
, div [ class "flex-grow flex flex-row items-center justify-end" ]
[ div [ class S.basicLabel ]
[ text job.state
, div [ class "ml-3" ]
[ Maybe.withDefault "" job.worker |> text
]
]
, div [ class (S.basicLabel ++ "ml-2") ]
[ i [ class "fa fa-clock" ] []
, div [ class "ml-3" ]
[ getDuration model job |> Maybe.withDefault "-:-" |> text
]
]
]
]
, div [ class "py-2", id "joblog" ]
[ div [ class styleJobLog ]
(List.map renderLogLine job.logs)
]
, div [ class "py-2 flex flex-row justify-end" ]
[ button [ class S.secondaryButton, onClick (RequestCancelJob job) ]
[ text "Cancel"
]
]
]
styleJobLog : String
styleJobLog =
"bg-gray-900 text-xs leading-5 px-2 py-1 font-mono text-gray-100 overflow-auto max-h-96 rounded"
renderLogLine : JobLogEvent -> Html Msg
renderLogLine log =
let
lineStyle =
case String.toLower log.level of
"info" ->
""
"debug" ->
"opacity-50"
"warn" ->
"text-yellow-400"
"error" ->
"text-red-400"
_ ->
""
in
span [ class lineStyle ]
[ formatIsoDateTime log.time |> text
, text ": "
, text log.message
, br [] []
]
isFinal : JobDetail -> Bool
isFinal job =
case job.state of
"failed" ->
True
"success" ->
True
"cancelled" ->
True
_ ->
False
dimmerSettings : Comp.YesNoDimmer.Settings
dimmerSettings =
let
defaults =
Comp.YesNoDimmer.defaultSettings
in
{ defaults
| headerClass = "text-lg text-white"
, headerIcon = ""
, extraClass = "rounded"
, message = "Cancel/Delete this job?"
}
renderInfoCard : Model -> JobDetail -> Html Msg
renderInfoCard model job =
let
prio =
Data.Priority.fromString job.priority
|> Maybe.withDefault Data.Priority.Low
color solid =
jobStateColor job solid
labelStyle solid =
" label min-h-6 inline text-xs font-semibold " ++ color solid ++ " "
in
div
[ class (S.box ++ "px-4 py-4 flex flex-col rounded relative")
]
[ Html.map (DimmerMsg job)
(Comp.YesNoDimmer.viewN
(model.cancelJobRequest == Just job.id)
dimmerSettings
model.deleteConfirm
)
, div [ class "flex flex-row" ]
[ div [ class "flex-grow items-center" ]
[ i
[ classList
[ ( "fa fa-check", job.state == "success" )
, ( "fa fa-redo", job.state == "stuck" )
, ( "fa fa-bolt", job.state == "failed" )
, ( "fa fa-meh-outline", job.state == "canceled" )
, ( "fa fa-cog", not (isFinal job) && job.state /= "stuck" )
]
, class "justify-center"
]
[]
, div [ class (labelStyle True ++ " ml-2") ]
[ text job.state
]
, div [ class "ml-2 break-all hidden sm:inline-block" ]
[ text job.name
]
]
, div [ class "flex flex-row space-x-2" ]
[ a
[ onClick (ShowLog job)
, href "#"
, class S.link
, classList [ ( "hidden", not (isFinal job || job.state == "stuck") ) ]
]
[ i [ class "fa fa-file", title "Show log" ] []
]
, a
[ title "Remove"
, href "#"
, class S.link
, onClick (RequestCancelJob job)
]
[ i
[ class "fa fa-times"
]
[]
]
, div
[ classList [ ( "hidden", isFinal job ) ]
]
[ div [ class "font-mono" ]
[ getDuration model job |> Maybe.withDefault "3:12" |> text
]
]
]
]
, div [ class "sm:hidden mt-1 break-all" ]
[ text job.name
]
, div [ class "my-2" ]
[ hr [ class S.border ] []
]
, div [ class "flex flex-row space-x-2 items-center flex-wrap" ]
[ div [ class "flex flex-row justify-start " ]
[ div [ class "text-xs font-semibold" ]
[ Util.Time.formatDateTime job.submitted |> text
]
]
, div [ class "flex-grow flex flex-row justify-end space-x-2 flex-wrap" ]
[ div
[ class (labelStyle False)
, classList [ ( "hidden", not (isFinal job) ) ]
]
[ i [ class "fa fa-clock mr-3" ] []
, span []
[ getDuration model job |> Maybe.withDefault "-:-" |> text
]
]
, div [ class (labelStyle False) ]
[ span [ class "mr-3" ]
[ text "Retries"
]
, span []
[ job.retries |> String.fromInt |> text
]
]
, case job.state of
"waiting" ->
a
[ class (labelStyle False)
, onClick (ChangePrio job.id (Data.Priority.next prio))
, href "#"
, title "Change priority of this job"
]
[ i [ class "sort numeric up icon" ] []
, text "Prio"
, div [ class "detail" ]
[ code []
[ Data.Priority.fromString job.priority
|> Maybe.map Data.Priority.toName
|> Maybe.withDefault job.priority
|> text
]
]
]
_ ->
div
[ class (labelStyle False)
]
[ span [ class "mr-3" ]
[ text "Prio"
]
, code [ class "font-mono" ]
[ Data.Priority.fromString job.priority
|> Maybe.map Data.Priority.toName
|> Maybe.withDefault job.priority
|> text
]
]
]
]
]
jobStateColor : JobDetail -> Bool -> String
jobStateColor job solid =
case job.state of
"success" ->
if solid then
S.greenSolidLabel
else
S.greenBasicLabel
"failed" ->
if solid then
S.redSolidLabel
else
S.redBasicLabel
"canceled" ->
"text-orange-500 border-orange-500"
"stuck" ->
"text-purple-500 border-purple-500"
"scheduled" ->
"text-blue-500 border-blue-500"
"waiting" ->
"text-gray-500 border-gray-500"
_ ->
""

View File

@ -0,0 +1,259 @@
module Page.Register.View2 exposing (viewContent, viewSidebar)
import Comp.Basic as B
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput, onSubmit)
import Page exposing (Page(..))
import Page.Register.Data exposing (..)
import Styles as S
viewSidebar : Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar _ _ _ _ =
div
[ id "sidebar"
, class "hidden"
]
[]
viewContent : Flags -> UiSettings -> Model -> Html Msg
viewContent flags _ model =
div
[ id "content"
, class "h-full flex flex-col items-center justify-center w-full"
, class S.content
]
[ div [ class ("flex flex-col px-4 sm:px-6 md:px-8 lg:px-10 lg:w-2/5 py-8 rounded-md " ++ S.box) ]
[ div [ class "self-center" ]
[ img
[ class "w-16 py-2"
, src (flags.config.docspellAssetPath ++ "/img/logo-96.png")
]
[]
]
, div [ class "font-medium self-center text-xl sm:text-2xl" ]
[ text "Signup to Docspell"
]
, Html.form
[ action "#"
, onSubmit RegisterSubmit
, autocomplete False
]
[ div [ class "flex flex-col mt-6" ]
[ label
[ for "username"
, class S.inputLabel
]
[ text "Collective ID"
, B.inputRequired
]
, div [ class "relative" ]
[ div [ class S.inputIcon ]
[ i [ class "fa fa-users" ] []
]
, input
[ type_ "text"
, name "collective"
, autocomplete False
, onInput SetCollId
, value model.collId
, autofocus True
, class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput)
, placeholder "Collective"
]
[]
]
]
, div [ class "flex flex-col my-3" ]
[ label
[ for "user"
, class S.inputLabel
]
[ text "User Login"
, B.inputRequired
]
, div [ class "relative" ]
[ div [ class S.inputIcon ]
[ i [ class "fa fa-user" ] []
]
, input
[ type_ "text"
, name "user"
, autocomplete False
, onInput SetLogin
, value model.login
, class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput)
, placeholder "Username"
]
[]
]
]
, div [ class "flex flex-col my-3" ]
[ label
[ for "passw1"
, class S.inputLabel
]
[ text "Password"
, B.inputRequired
]
, div [ class "relative" ]
[ div [ class S.inputIcon ]
[ i
[ class "fa"
, if model.showPass1 then
class "fa-lock-open"
else
class "fa-lock"
]
[]
]
, input
[ type_ <|
if model.showPass1 then
"text"
else
"password"
, name "passw1"
, autocomplete False
, onInput SetPass1
, value model.pass1
, class ("pl-10 pr-10 py-2 rounded-lg" ++ S.textInput)
, placeholder "Password"
]
[]
, a
[ class S.inputLeftIconLink
, onClick ToggleShowPass1
, href "#"
]
[ i [ class "fa fa-eye" ] []
]
]
]
, div [ class "flex flex-col my-3" ]
[ label
[ for "passw2"
, class S.inputLabel
]
[ text "Password (repeat)"
, B.inputRequired
]
, div [ class "relative" ]
[ div [ class S.inputIcon ]
[ i
[ class "fa"
, if model.showPass2 then
class "fa-lock-open"
else
class "fa-lock"
]
[]
]
, input
[ type_ <|
if model.showPass2 then
"text"
else
"password"
, name "passw2"
, autocomplete False
, onInput SetPass2
, value model.pass2
, class ("pl-10 pr-10 py-2 rounded-lg" ++ S.textInput)
, placeholder "Password (repeat)"
]
[]
, a
[ class S.inputLeftIconLink
, onClick ToggleShowPass2
, href "#"
]
[ i [ class "fa fa-eye" ] []
]
]
]
, div
[ class "flex flex-col my-3"
, classList [ ( "hidden", flags.config.signupMode /= "invite" ) ]
]
[ label
[ for "invitekey"
, class S.inputLabel
]
[ text "Invitation Key"
, B.inputRequired
]
, div [ class "relative" ]
[ div [ class S.inputIcon ]
[ i [ class "fa fa-key" ] []
]
, input
[ type_ "text"
, name "invitekey"
, autocomplete False
, onInput SetInvite
, model.invite |> Maybe.withDefault "" |> value
, class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput)
, placeholder "Invitation Key"
]
[]
]
]
, div [ class "flex flex-col my-3" ]
[ button
[ type_ "submit"
, class S.primaryButton
]
[ text "Submit"
]
]
, resultMessage model
, div
[ class "flex justify-end text-sm pt-4"
, classList [ ( "hidden", flags.config.signupMode == "closed" ) ]
]
[ span []
[ text "Already signed up?"
]
, a
[ Page.href (LoginPage Nothing)
, class ("ml-2" ++ S.link)
]
[ i [ class "fa fa-user-plus mr-1" ] []
, text "Sign in"
]
]
]
]
]
resultMessage : Model -> Html Msg
resultMessage model =
case model.result of
Just r ->
if r.success then
div [ class S.successMessage ]
[ text "Registration successful."
]
else
div [ class S.errorMessage ]
[ text r.message
]
Nothing ->
if List.isEmpty model.errorMsg then
span [ class "hidden" ] []
else
div [ class S.errorMessage ]
(List.map (\s -> div [] [ text s ]) model.errorMsg)

View File

@ -38,22 +38,6 @@ type alias Model =
}
dropzoneSettings : Comp.Dropzone.Settings
dropzoneSettings =
let
ds =
Comp.Dropzone.defaultSettings
in
{ ds
| classList =
\m ->
[ ( "ui attached blue placeholder segment dropzone", True )
, ( "dragging", m.hover )
, ( "disabled", not m.active )
]
}
mkLanguageItem : Language -> Comp.FixedDropdown.Item Language
mkLanguageItem lang =
Comp.FixedDropdown.Item lang (Data.Language.toName lang)
@ -67,7 +51,7 @@ emptyModel =
, completed = Set.empty
, errored = Set.empty
, loading = Dict.empty
, dropzone = Comp.Dropzone.init dropzoneSettings
, dropzone = Comp.Dropzone.init []
, skipDuplicates = True
, languageModel =
Comp.FixedDropdown.init

View File

@ -23,7 +23,7 @@ view mid model =
[ div [ class "ui top attached segment" ]
[ renderForm model
]
, Html.map DropzoneMsg (Comp.Dropzone.view model.dropzone)
, Html.map DropzoneMsg (Comp.Dropzone.view dropzoneSettings model.dropzone)
, div [ class "ui bottom attached segment" ]
[ a [ class "ui primary button", href "#", onClick SubmitUpload ]
[ text "Submit"
@ -50,6 +50,22 @@ view mid model =
]
dropzoneSettings : Comp.Dropzone.Settings
dropzoneSettings =
let
ds =
Comp.Dropzone.defaultSettings
in
{ ds
| classList =
\m ->
[ ( "ui attached blue placeholder segment dropzone", True )
, ( "dragging", m.hover )
, ( "disabled", not m.active )
]
}
renderErrorMsg : Model -> Html Msg
renderErrorMsg _ =
div [ class "row" ]

View File

@ -0,0 +1,280 @@
module Page.Upload.View2 exposing (viewContent, viewSidebar)
import Comp.Dropzone
import Comp.FixedDropdown
import Comp.Progress
import Data.DropdownStyle as DS
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 Page exposing (Page(..))
import Page.Upload.Data exposing (..)
import Styles as S
import Util.File exposing (makeFileId)
import Util.Maybe
import Util.Size
viewSidebar : Maybe String -> Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar _ _ _ _ _ =
div
[ id "sidebar"
, class "hidden"
]
[]
viewContent : Maybe String -> Flags -> UiSettings -> Model -> Html Msg
viewContent mid _ _ model =
div
[ id "content"
, class S.content
]
[ div [ class "px-0 flex flex-col" ]
[ div [ class "py-4" ]
[ renderForm model
]
, div [ class "py-0" ]
[ Html.map DropzoneMsg
(Comp.Dropzone.view2 model.dropzone)
]
, div [ class "py-4" ]
[ a
[ class S.primaryButton
, href "#"
, onClick SubmitUpload
]
[ text "Submit"
]
, a
[ class S.secondaryButton
, class "ml-2"
, href "#"
, onClick Clear
]
[ text "Reset"
]
]
]
, renderErrorMsg model
, renderSuccessMsg (Util.Maybe.nonEmpty mid) model
, renderUploads model
]
renderForm : Model -> Html Msg
renderForm model =
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 "Incoming" ]
]
, label [ class "inline-flex items-center" ]
[ input
[ type_ "radio"
, checked (not model.incoming)
, onCheck (\_ -> ToggleIncoming)
, class S.radioInput
]
[]
, span [ class "ml-2" ] [ text "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 "All files are one single item"
]
]
]
, 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 "Skip files already present in docspell"
]
]
]
, div [ class "flex flex-col mb-3" ]
[ label [ class "inline-flex items-center mb-2" ]
[ span [ class "mr-2" ] [ text "Language:" ]
, Html.map LanguageMsg
(Comp.FixedDropdown.viewStyled2
(DS.mainStyleWith "w-40")
False
(Maybe.map mkLanguageItem model.language)
model.languageModel
)
]
, div [ class "text-gray-400 text-xs" ]
[ text "Used for text extraction and analysis. The collective's "
, text "default language is used if not specified here."
]
]
]
]
renderErrorMsg : Model -> Html Msg
renderErrorMsg model =
div
[ class "row"
, classList [ ( "hidden", not (isDone model && hasErrors model) ) ]
]
[ div [ class "mt-4" ]
[ div [ class S.errorMessage ]
[ text "There were errors uploading some files."
]
]
]
renderSuccessMsg : Bool -> Model -> Html Msg
renderSuccessMsg 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 "All files uploaded"
]
]
, p
[ classList [ ( "hidden", public ) ]
]
[ text "Your files have been successfully uploaded. "
, text "They are now being processed. Check the "
, a
[ class S.successMessageLink
, Page.href HomePage
]
[ text "Items page"
]
, text " later where the files will arrive eventually. Or go to the "
, a
[ class S.successMessageLink
, Page.href QueuePage
]
[ text "Processing Page"
]
, text " to view the current processing state."
]
, p []
[ text "Click "
, a
[ class S.successMessageLink
, href "#"
, onClick Clear
]
[ text "Reset"
]
, text " to upload more files."
]
]
]
]
renderUploads : Model -> Html Msg
renderUploads model =
div
[ class "mt-4"
, classList [ ( "hidden", List.isEmpty model.files || isSuccessAll model ) ]
]
[ div [ class "sixteen wide column" ]
[ div [ class "ui basic segment" ]
[ h2 [ class S.header2 ]
[ text "Selected Files"
]
, div [ class "ui items" ] <|
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)
]
]

View File

@ -32,7 +32,7 @@ init flags settings =
( um, uc ) =
Comp.UiSettingsManage.init flags settings
in
( { currentTab = Nothing
( { currentTab = Just UiSettingsTab
, changePassModel = Comp.ChangePasswordForm.emptyModel
, emailSettingsModel = Comp.EmailSettingsManage.emptyModel
, imapSettingsModel = Comp.ImapSettingsManage.emptyModel

View File

@ -0,0 +1,282 @@
module Page.UserSettings.View2 exposing (viewContent, viewSidebar)
import Comp.ChangePasswordForm
import Comp.EmailSettingsManage
import Comp.ImapSettingsManage
import Comp.NotificationManage
import Comp.ScanMailboxManage
import Comp.UiSettingsManage
import Data.Flags exposing (Flags)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Page.UserSettings.Data exposing (..)
import Styles as S
viewSidebar : Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar visible _ _ model =
div
[ id "sidebar"
, class S.sidebar
, class S.sidebarBg
, classList [ ( "hidden", not visible ) ]
]
[ div [ class "" ]
[ h1 [ class S.header1 ]
[ text "User Settings"
]
]
, div [ class "flex flex-col my-2" ]
[ a
[ href "#"
, onClick (SetTab UiSettingsTab)
, menuEntryActive model UiSettingsTab
, class S.sidebarLink
]
[ i [ class "fa fa-cog" ] []
, span
[ class "ml-3" ]
[ text "UI Settings" ]
]
, a
[ href "#"
, onClick (SetTab NotificationTab)
, menuEntryActive model NotificationTab
, class S.sidebarLink
]
[ i [ class "fa fa-bullhorn" ] []
, span
[ class "ml-3" ]
[ text "Notifications" ]
]
, a
[ href "#"
, onClick (SetTab ScanMailboxTab)
, menuEntryActive model ScanMailboxTab
, class S.sidebarLink
]
[ i [ class "fa fa-envelope-open font-thin" ] []
, span
[ class "ml-3" ]
[ text "Scan Mailbox" ]
]
, a
[ href "#"
, onClick (SetTab EmailSettingsTab)
, class S.sidebarLink
, menuEntryActive model EmailSettingsTab
]
[ i [ class "fa fa-envelope" ] []
, span
[ class "ml-3" ]
[ text "E-Mail Settings (SMTP)" ]
]
, a
[ href "#"
, onClick (SetTab ImapSettingsTab)
, menuEntryActive model ImapSettingsTab
, class S.sidebarLink
]
[ i [ class "fa fa-envelope" ] []
, span
[ class "ml-3" ]
[ text "E-Mail Settings (IMAP)" ]
]
, a
[ href "#"
, onClick (SetTab ChangePassTab)
, menuEntryActive model ChangePassTab
, class S.sidebarLink
]
[ i [ class "fa fa-user-secret" ] []
, span
[ class "ml-3" ]
[ text "CHange Password" ]
]
]
]
viewContent : Flags -> UiSettings -> Model -> Html Msg
viewContent flags settings model =
div
[ id "content"
, class S.content
]
(case model.currentTab of
Just ChangePassTab ->
viewChangePassword model
Just EmailSettingsTab ->
viewEmailSettings settings model
Just NotificationTab ->
viewNotificationManage settings model
Just ImapSettingsTab ->
viewImapSettings settings model
Just ScanMailboxTab ->
viewScanMailboxManage settings model
Just UiSettingsTab ->
viewUiSettings flags settings model
Nothing ->
[]
)
--- Helper
menuEntryActive : Model -> Tab -> Attribute msg
menuEntryActive model tab =
if model.currentTab == Just tab then
class S.sidebarMenuItemActive
else
class ""
viewChangePassword : Model -> List (Html Msg)
viewChangePassword model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ i [ class "fa fa-user-secret" ] []
, div [ class "ml-3" ]
[ text "Change Password"
]
]
, Html.map ChangePassMsg (Comp.ChangePasswordForm.view2 model.changePassModel)
]
viewUiSettings : Flags -> UiSettings -> Model -> List (Html Msg)
viewUiSettings flags settings model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ i [ class "fa fa-cog" ] []
, span [ class "ml-3" ]
[ text "UI Settings"
]
]
, p [ class "opacity-75 text-lg mb-4" ]
[ 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.view2
flags
settings
""
model.uiSettingsModel
)
]
viewEmailSettings : UiSettings -> Model -> List (Html Msg)
viewEmailSettings settings model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ i [ class "fa fa-envelope" ] []
, div [ class "ml-3" ]
[ text "E-Mail Settings (Smtp)"
]
]
, Html.map EmailSettingsMsg
(Comp.EmailSettingsManage.view2
settings
model.emailSettingsModel
)
]
viewImapSettings : UiSettings -> Model -> List (Html Msg)
viewImapSettings settings model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ i [ class "fa fa-envelope" ] []
, div [ class "ml-3" ]
[ text "E-Mail Settings (Imap)"
]
]
, Html.map ImapSettingsMsg
(Comp.ImapSettingsManage.view2
settings
model.imapSettingsModel
)
]
viewNotificationManage : UiSettings -> Model -> List (Html Msg)
viewNotificationManage settings model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ i [ class "fa fa-bullhorn" ] []
, div [ class "ml-3" ]
[ text "Notification"
]
]
, p [ class "opacity-80 text-lg mb-3" ]
[ text """
Docspell can notify you once the due dates of your items
come closer. Notification is done via e-mail. You need to
provide a connection in your e-mail settings."""
]
, p [ class "opacity-80 text-lg mb-3" ]
[ text "Docspell finds all items that are due in "
, em [ class "font-italic" ] [ text "Remind Days" ]
, text " days and sends this list via e-mail."
]
, Html.map NotificationMsg
(Comp.NotificationManage.view2 settings model.notificationModel)
]
viewScanMailboxManage : UiSettings -> Model -> List (Html Msg)
viewScanMailboxManage settings model =
[ h2
[ class S.header1
, class "inline-flex items-center"
]
[ i [ class "fa fa-envelope-open font-thin" ] []
, div [ class "ml-3" ]
[ text "Scan Mailbox"
]
]
, p [ class "opacity-80 text-lg mb-3" ]
[ text "Docspell can scan folders of your mailbox to import your mails. "
, text "You need to provide a connection in "
, text "your e-mail (imap) settings."
]
, p [ class "opacity-80 text-lg mb-3 hidden" ]
[ text """
Docspell goes through all configured folders and imports
mails matching the search criteria. Mails are skipped if
they were imported in a previous run and the corresponding
items still exist. After submitting a mail into docspell,
you can choose to move it to another folder, to delete it
or to just leave it there. In the latter case you should
adjust the schedule to avoid reading over the same mails
again."""
]
, Html.map ScanMailboxMsg
(Comp.ScanMailboxManage.view2
settings
model.scanMailboxModel
)
]