mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-23 10:58:26 +00:00
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:
144
modules/webapp/src/main/elm/Comp/ItemDetail/AddFilesForm.elm
Normal file
144
modules/webapp/src/main/elm/Comp/ItemDetail/AddFilesForm.elm
Normal file
@ -0,0 +1,144 @@
|
||||
module Comp.ItemDetail.AddFilesForm exposing (view)
|
||||
|
||||
import Comp.Dropzone
|
||||
import Comp.ItemDetail.Model exposing (..)
|
||||
import Comp.Progress
|
||||
import Data.DropdownStyle
|
||||
import Dict
|
||||
import File exposing (File)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onCheck, onClick, onInput)
|
||||
import Set
|
||||
import Styles as S
|
||||
import Util.File exposing (makeFileId)
|
||||
import Util.Size
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
div
|
||||
[ classList
|
||||
[ ( "hidden", not model.addFilesOpen )
|
||||
]
|
||||
, class "flex flex-col px-2 py-2 mb-4"
|
||||
, class S.box
|
||||
]
|
||||
[ div [ class "text-lg font-bold" ]
|
||||
[ text "Add more files to this item"
|
||||
]
|
||||
, Html.map AddFilesMsg
|
||||
(Comp.Dropzone.view2 model.addFilesModel)
|
||||
, div [ class "flex flex-row space-x-2 mt-2" ]
|
||||
[ button
|
||||
[ class S.primaryButton
|
||||
, href "#"
|
||||
, onClick AddFilesSubmitUpload
|
||||
]
|
||||
[ text "Submit"
|
||||
]
|
||||
, button
|
||||
[ class S.secondaryButton
|
||||
, href "#"
|
||||
, onClick AddFilesReset
|
||||
]
|
||||
[ text "Reset"
|
||||
]
|
||||
]
|
||||
, div
|
||||
[ classList
|
||||
[ ( S.successMessage, True )
|
||||
, ( "hidden", model.selectedFiles == [] || not (isSuccessAll model) )
|
||||
]
|
||||
, class "mt-2"
|
||||
]
|
||||
[ text "All files have been uploaded. They are being processed, some data "
|
||||
, text "may not be available immediately. "
|
||||
, a
|
||||
[ class S.successMessageLink
|
||||
, href "#"
|
||||
, onClick ReloadItem
|
||||
]
|
||||
[ text "Refresh now"
|
||||
]
|
||||
]
|
||||
, div
|
||||
[ class "flex flex-col mt-2"
|
||||
, classList [ ( "hidden", List.isEmpty model.selectedFiles || isSuccessAll model ) ]
|
||||
]
|
||||
(List.map (renderFileItem model) model.selectedFiles)
|
||||
]
|
||||
|
||||
|
||||
renderFileItem : Model -> File -> Html Msg
|
||||
renderFileItem model file =
|
||||
let
|
||||
name =
|
||||
File.name file
|
||||
|
||||
size =
|
||||
File.size file
|
||||
|> toFloat
|
||||
|> Util.Size.bytesReadable Util.Size.B
|
||||
|
||||
getProgress =
|
||||
let
|
||||
key =
|
||||
makeFileId file
|
||||
in
|
||||
Dict.get key model.loading
|
||||
|> Maybe.withDefault 0
|
||||
in
|
||||
div [ class "flex flex-col" ]
|
||||
[ 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
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
isSuccessAll : Model -> Bool
|
||||
isSuccessAll model =
|
||||
List.map makeFileId model.selectedFiles
|
||||
|> List.all (\id -> Set.member id model.completed)
|
||||
|
||||
|
||||
isIdle : Model -> File -> Bool
|
||||
isIdle model file =
|
||||
not (isLoading model file || isCompleted model file || isError model file)
|
||||
|
||||
|
||||
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
|
429
modules/webapp/src/main/elm/Comp/ItemDetail/EditForm.elm
Normal file
429
modules/webapp/src/main/elm/Comp/ItemDetail/EditForm.elm
Normal file
@ -0,0 +1,429 @@
|
||||
module Comp.ItemDetail.EditForm exposing (formTabs, view2)
|
||||
|
||||
import Comp.CustomFieldMultiInput
|
||||
import Comp.DatePicker
|
||||
import Comp.Dropdown
|
||||
import Comp.ItemDetail.FieldTabState as FTabState
|
||||
import Comp.ItemDetail.Model
|
||||
exposing
|
||||
( Model
|
||||
, Msg(..)
|
||||
, NotesField(..)
|
||||
, SaveNameState(..)
|
||||
, personMatchesOrg
|
||||
)
|
||||
import Comp.KeyInput
|
||||
import Comp.Tabs as TB
|
||||
import Data.DropdownStyle
|
||||
import Data.Fields
|
||||
import Data.Icons as Icons
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Dict
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick, onInput)
|
||||
import Markdown
|
||||
import Page exposing (Page(..))
|
||||
import Set exposing (Set)
|
||||
import Styles as S
|
||||
import Util.Folder
|
||||
import Util.Time
|
||||
|
||||
|
||||
view2 : UiSettings -> Model -> Html Msg
|
||||
view2 settings model =
|
||||
let
|
||||
keyAttr =
|
||||
if settings.itemDetailShortcuts then
|
||||
Comp.KeyInput.eventsM KeyInputMsg
|
||||
|
||||
else
|
||||
[]
|
||||
|
||||
tabStyle =
|
||||
TB.searchMenuStyle
|
||||
|
||||
tabs =
|
||||
formTabs settings model
|
||||
|
||||
allTabNames =
|
||||
List.map .title tabs
|
||||
|> Set.fromList
|
||||
in
|
||||
div (class "flex flex-col relative" :: keyAttr)
|
||||
[ TB.akkordion tabStyle
|
||||
(tabState settings allTabNames model)
|
||||
tabs
|
||||
]
|
||||
|
||||
|
||||
formTabs : UiSettings -> Model -> List (TB.Tab Msg)
|
||||
formTabs settings model =
|
||||
let
|
||||
dds =
|
||||
Data.DropdownStyle.sidebarStyle
|
||||
|
||||
addIconLink tip m =
|
||||
a
|
||||
[ class "float-right"
|
||||
, href "#"
|
||||
, title tip
|
||||
, onClick m
|
||||
, class S.link
|
||||
]
|
||||
[ i [ class "fa fa-plus" ] []
|
||||
]
|
||||
|
||||
editIconLink tip dm m =
|
||||
a
|
||||
[ classList
|
||||
[ ( "hidden", Comp.Dropdown.notSelected dm )
|
||||
]
|
||||
, href "#"
|
||||
, class "float-right mr-2"
|
||||
, class S.link
|
||||
, title tip
|
||||
, onClick m
|
||||
]
|
||||
[ i [ class "fa fa-pencil-alt" ] []
|
||||
]
|
||||
|
||||
fieldVisible field =
|
||||
Data.UiSettings.fieldVisible settings field
|
||||
|
||||
customFieldSettings =
|
||||
Comp.CustomFieldMultiInput.ViewSettings
|
||||
True
|
||||
"field"
|
||||
(\f -> Dict.get f.id model.customFieldSavingIcon)
|
||||
|
||||
optional fields html =
|
||||
if
|
||||
List.map fieldVisible fields
|
||||
|> List.foldl (||) False
|
||||
then
|
||||
html
|
||||
|
||||
else
|
||||
span [ class "invisible hidden" ] []
|
||||
in
|
||||
[ { title = "Name"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div [ class "relative mb-4" ]
|
||||
[ input
|
||||
[ type_ "text"
|
||||
, value model.nameModel
|
||||
, onInput SetName
|
||||
, class S.textInputSidebar
|
||||
, class "pr-10"
|
||||
]
|
||||
[]
|
||||
, span [ class S.inputLeftIconOnly ]
|
||||
[ i
|
||||
[ classList
|
||||
[ ( "text-green-500 fa fa-check", model.nameState == SaveSuccess )
|
||||
, ( "text-red-500 fa fa-exclamation-triangle", model.nameState == SaveFailed )
|
||||
, ( "sync fa fa-circle-notch animate-spin", model.nameState == Saving )
|
||||
]
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Date"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div [ class "mb-4" ]
|
||||
[ div [ class "relative" ]
|
||||
[ Html.map ItemDatePickerMsg
|
||||
(Comp.DatePicker.viewTimeDefault
|
||||
model.itemDate
|
||||
model.itemDatePicker
|
||||
)
|
||||
, a
|
||||
[ class "ui icon button"
|
||||
, href "#"
|
||||
, class S.inputLeftIconLinkSidebar
|
||||
, onClick RemoveDate
|
||||
]
|
||||
[ i [ class "fa fa-trash-alt font-thin" ] []
|
||||
]
|
||||
, Icons.dateIcon2 S.dateInputIcon
|
||||
]
|
||||
, renderItemDateSuggestions model
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Tags"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div [ class "mb-4" ]
|
||||
[ Html.map TagDropdownMsg (Comp.Dropdown.view2 dds settings model.tagModel)
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Folder"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div [ class "mb-4" ]
|
||||
[ Html.map FolderDropdownMsg
|
||||
(Comp.Dropdown.view2
|
||||
dds
|
||||
settings
|
||||
model.folderModel
|
||||
)
|
||||
, div
|
||||
[ classList
|
||||
[ ( S.message, True )
|
||||
, ( "hidden", isFolderMember model )
|
||||
]
|
||||
]
|
||||
[ Markdown.toHtml [] """
|
||||
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.
|
||||
"""
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Custom Fields"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div [ class "mb-4" ]
|
||||
[ Html.map CustomFieldMsg
|
||||
(Comp.CustomFieldMultiInput.view2
|
||||
dds
|
||||
customFieldSettings
|
||||
model.customFieldsModel
|
||||
)
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Due Date"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div [ class "mb-4" ]
|
||||
[ div [ class "relative" ]
|
||||
[ Html.map DueDatePickerMsg
|
||||
(Comp.DatePicker.viewTimeDefault
|
||||
model.dueDate
|
||||
model.dueDatePicker
|
||||
)
|
||||
, a
|
||||
[ class "ui icon button"
|
||||
, href "#"
|
||||
, class S.inputLeftIconLinkSidebar
|
||||
, onClick RemoveDueDate
|
||||
]
|
||||
[ i [ class "fa fa-trash-alt font-thin" ] []
|
||||
]
|
||||
, Icons.dueDateIcon2 S.dateInputIcon
|
||||
]
|
||||
, renderDueDateSuggestions model
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Correspondent"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ optional [ Data.Fields.CorrOrg ] <|
|
||||
div [ class "mb-4" ]
|
||||
[ label [ class S.inputLabel ]
|
||||
[ Icons.organizationIcon2 "mr-2"
|
||||
, text "Organization"
|
||||
, addIconLink "Add new organization" StartCorrOrgModal
|
||||
, editIconLink "Edit organization" model.corrOrgModel StartEditCorrOrgModal
|
||||
]
|
||||
, Html.map OrgDropdownMsg (Comp.Dropdown.view2 dds settings model.corrOrgModel)
|
||||
, renderOrgSuggestions model
|
||||
]
|
||||
, optional [ Data.Fields.CorrPerson ] <|
|
||||
div [ class "mb-4" ]
|
||||
[ label [ class S.inputLabel ]
|
||||
[ Icons.personIcon2 "mr-2"
|
||||
, text "Person"
|
||||
, addIconLink "Add new correspondent person" StartCorrPersonModal
|
||||
, editIconLink "Edit person"
|
||||
model.corrPersonModel
|
||||
(StartEditPersonModal model.corrPersonModel)
|
||||
]
|
||||
, Html.map CorrPersonMsg (Comp.Dropdown.view2 dds settings model.corrPersonModel)
|
||||
, renderCorrPersonSuggestions model
|
||||
, div
|
||||
[ classList
|
||||
[ ( "hidden", personMatchesOrg model )
|
||||
]
|
||||
, class S.message
|
||||
, class "my-2"
|
||||
]
|
||||
[ i [ class "fa fa-info mr-2 " ] []
|
||||
, text "The selected person doesn't belong to the selected organization."
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Concerning"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ optional [ Data.Fields.ConcPerson ] <|
|
||||
div [ class "mb-4" ]
|
||||
[ label [ class S.inputLabel ]
|
||||
[ Icons.personIcon2 "mr-2"
|
||||
, text "Person"
|
||||
, addIconLink "Add new concerning person" StartConcPersonModal
|
||||
, editIconLink "Edit person"
|
||||
model.concPersonModel
|
||||
(StartEditPersonModal model.concPersonModel)
|
||||
]
|
||||
, Html.map ConcPersonMsg
|
||||
(Comp.Dropdown.view2
|
||||
dds
|
||||
settings
|
||||
model.concPersonModel
|
||||
)
|
||||
, renderConcPersonSuggestions model
|
||||
]
|
||||
, optional [ Data.Fields.ConcEquip ] <|
|
||||
div [ class "mb-4" ]
|
||||
[ label [ class S.inputLabel ]
|
||||
[ Icons.equipmentIcon2 "mr-2"
|
||||
, text "Equipment"
|
||||
, addIconLink "Add new equipment" StartEquipModal
|
||||
, editIconLink "Edit equipment"
|
||||
model.concEquipModel
|
||||
StartEditEquipModal
|
||||
]
|
||||
, Html.map ConcEquipMsg
|
||||
(Comp.Dropdown.view2
|
||||
dds
|
||||
settings
|
||||
model.concEquipModel
|
||||
)
|
||||
, renderConcEquipSuggestions model
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Direction"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div [ class "mb-4" ]
|
||||
[ Html.map DirDropdownMsg
|
||||
(Comp.Dropdown.view2
|
||||
dds
|
||||
settings
|
||||
model.directionModel
|
||||
)
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
renderSuggestions : Model -> (a -> String) -> List a -> (a -> Msg) -> Html Msg
|
||||
renderSuggestions model mkName idnames tagger =
|
||||
div
|
||||
[ classList
|
||||
[ ( "hidden", model.item.state /= "created" )
|
||||
]
|
||||
, class "flex flex-col text-sm"
|
||||
]
|
||||
[ div [ class "font-bold my-1" ]
|
||||
[ text "Suggestions"
|
||||
]
|
||||
, ul [ class "list-disc ml-6" ] <|
|
||||
(idnames
|
||||
|> List.map
|
||||
(\p ->
|
||||
li []
|
||||
[ a
|
||||
[ class S.link
|
||||
, href "#"
|
||||
, onClick (tagger p)
|
||||
]
|
||||
[ text (mkName p) ]
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
renderOrgSuggestions : Model -> Html Msg
|
||||
renderOrgSuggestions model =
|
||||
renderSuggestions model
|
||||
.name
|
||||
(List.take 6 model.itemProposals.corrOrg)
|
||||
SetCorrOrgSuggestion
|
||||
|
||||
|
||||
renderCorrPersonSuggestions : Model -> Html Msg
|
||||
renderCorrPersonSuggestions model =
|
||||
renderSuggestions model
|
||||
.name
|
||||
(List.take 6 model.itemProposals.corrPerson)
|
||||
SetCorrPersonSuggestion
|
||||
|
||||
|
||||
renderConcPersonSuggestions : Model -> Html Msg
|
||||
renderConcPersonSuggestions model =
|
||||
renderSuggestions model
|
||||
.name
|
||||
(List.take 6 model.itemProposals.concPerson)
|
||||
SetConcPersonSuggestion
|
||||
|
||||
|
||||
renderConcEquipSuggestions : Model -> Html Msg
|
||||
renderConcEquipSuggestions model =
|
||||
renderSuggestions model
|
||||
.name
|
||||
(List.take 6 model.itemProposals.concEquipment)
|
||||
SetConcEquipSuggestion
|
||||
|
||||
|
||||
renderItemDateSuggestions : Model -> Html Msg
|
||||
renderItemDateSuggestions model =
|
||||
renderSuggestions model
|
||||
Util.Time.formatDate
|
||||
(List.take 6 model.itemProposals.itemDate)
|
||||
SetItemDateSuggestion
|
||||
|
||||
|
||||
renderDueDateSuggestions : Model -> Html Msg
|
||||
renderDueDateSuggestions model =
|
||||
renderSuggestions model
|
||||
Util.Time.formatDate
|
||||
(List.take 6 model.itemProposals.dueDate)
|
||||
SetDueDateSuggestion
|
||||
|
||||
|
||||
|
||||
--- Helpers
|
||||
|
||||
|
||||
isFolderMember : Model -> Bool
|
||||
isFolderMember model =
|
||||
let
|
||||
selected =
|
||||
Comp.Dropdown.getSelected model.folderModel
|
||||
|> List.head
|
||||
|> Maybe.map .id
|
||||
in
|
||||
Util.Folder.isFolderMember model.allFolders selected
|
||||
|
||||
|
||||
tabState : UiSettings -> Set String -> Model -> TB.Tab Msg -> ( TB.State, Msg )
|
||||
tabState settings allNames model =
|
||||
let
|
||||
openTabs =
|
||||
if model.item.state == "created" then
|
||||
allNames
|
||||
|
||||
else
|
||||
model.editMenuTabsOpen
|
||||
in
|
||||
FTabState.tabState settings
|
||||
openTabs
|
||||
model.customFieldsModel
|
||||
(.title >> ToggleAkkordionTab)
|
@ -0,0 +1,62 @@
|
||||
module Comp.ItemDetail.FieldTabState exposing (tabState)
|
||||
|
||||
import Comp.CustomFieldMultiInput
|
||||
import Comp.Tabs as TB
|
||||
import Data.Fields
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Set exposing (Set)
|
||||
|
||||
|
||||
tabState :
|
||||
UiSettings
|
||||
-> Set String
|
||||
-> Comp.CustomFieldMultiInput.Model
|
||||
-> (TB.Tab msg -> msg)
|
||||
-> TB.Tab msg
|
||||
-> ( TB.State, msg )
|
||||
tabState settings openTabs cfmodel toggle tab =
|
||||
let
|
||||
isHidden f =
|
||||
Data.UiSettings.fieldHidden settings f
|
||||
|
||||
hidden =
|
||||
case tab.title of
|
||||
"Tags" ->
|
||||
isHidden Data.Fields.Tag
|
||||
|
||||
"Folder" ->
|
||||
isHidden Data.Fields.Folder
|
||||
|
||||
"Correspondent" ->
|
||||
isHidden Data.Fields.CorrOrg && isHidden Data.Fields.CorrPerson
|
||||
|
||||
"Concerning" ->
|
||||
isHidden Data.Fields.ConcEquip && isHidden Data.Fields.ConcPerson
|
||||
|
||||
"Custom Fields" ->
|
||||
isHidden Data.Fields.CustomFields
|
||||
|| Comp.CustomFieldMultiInput.isEmpty cfmodel
|
||||
|
||||
"Date" ->
|
||||
isHidden Data.Fields.Date
|
||||
|
||||
"Due Date" ->
|
||||
isHidden Data.Fields.DueDate
|
||||
|
||||
"Direction" ->
|
||||
isHidden Data.Fields.Direction
|
||||
|
||||
_ ->
|
||||
False
|
||||
|
||||
state =
|
||||
if hidden then
|
||||
TB.Hidden
|
||||
|
||||
else if Set.member tab.title openTabs then
|
||||
TB.Open
|
||||
|
||||
else
|
||||
TB.Closed
|
||||
in
|
||||
( state, toggle tab )
|
198
modules/webapp/src/main/elm/Comp/ItemDetail/ItemInfoHeader.elm
Normal file
198
modules/webapp/src/main/elm/Comp/ItemDetail/ItemInfoHeader.elm
Normal file
@ -0,0 +1,198 @@
|
||||
module Comp.ItemDetail.ItemInfoHeader exposing (view)
|
||||
|
||||
import Api.Model.IdName exposing (IdName)
|
||||
import Comp.ItemDetail.Model
|
||||
exposing
|
||||
( Model
|
||||
, Msg(..)
|
||||
, NotesField(..)
|
||||
, SaveNameState(..)
|
||||
)
|
||||
import Comp.LinkTarget
|
||||
import Data.Direction
|
||||
import Data.Fields
|
||||
import Data.Icons as Icons
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Page exposing (Page(..))
|
||||
import Styles as S
|
||||
import Util.Maybe
|
||||
import Util.Time
|
||||
|
||||
|
||||
view : UiSettings -> Model -> Html Msg
|
||||
view settings model =
|
||||
let
|
||||
date =
|
||||
( div
|
||||
[ class "ml-2 sm:ml-0 whitespace-nowrap py-1 whitespace-nowrap opacity-75"
|
||||
, title "Item Date"
|
||||
]
|
||||
[ Icons.dateIcon2 "mr-2"
|
||||
, Maybe.withDefault model.item.created model.item.itemDate
|
||||
|> Util.Time.formatDate
|
||||
|> text
|
||||
]
|
||||
, Data.UiSettings.fieldVisible settings Data.Fields.Date
|
||||
)
|
||||
|
||||
itemStyle =
|
||||
"ml-2 sm:ml-4 py-1 whitespace-nowrap "
|
||||
|
||||
linkStyle =
|
||||
"opacity-75 hover:opacity-100"
|
||||
|
||||
duedate =
|
||||
( div
|
||||
[ class "ml-2 sm:ml-4 py-1 max-w-min whitespace-nowrap opacity-100"
|
||||
, class S.basicLabel
|
||||
, title "Due Date"
|
||||
]
|
||||
[ Icons.dueDateIcon2 "mr-2"
|
||||
, Maybe.map Util.Time.formatDate model.item.dueDate
|
||||
|> Maybe.withDefault ""
|
||||
|> text
|
||||
]
|
||||
, Data.UiSettings.fieldVisible settings Data.Fields.DueDate
|
||||
&& Util.Maybe.nonEmpty model.item.dueDate
|
||||
)
|
||||
|
||||
corr =
|
||||
( div
|
||||
[ class itemStyle
|
||||
, title "Correspondent"
|
||||
]
|
||||
(Icons.correspondentIcon2 "mr-2"
|
||||
:: Comp.LinkTarget.makeCorrLink model.item
|
||||
[ ( linkStyle, True ) ]
|
||||
SetLinkTarget
|
||||
)
|
||||
, Data.UiSettings.fieldVisible settings Data.Fields.CorrOrg
|
||||
|| Data.UiSettings.fieldVisible settings Data.Fields.CorrPerson
|
||||
)
|
||||
|
||||
conc =
|
||||
( div
|
||||
[ class itemStyle
|
||||
, title "Concerning"
|
||||
]
|
||||
(Icons.concernedIcon2 "mr-2"
|
||||
:: Comp.LinkTarget.makeConcLink model.item
|
||||
[ ( linkStyle, True ) ]
|
||||
SetLinkTarget
|
||||
)
|
||||
, Data.UiSettings.fieldVisible settings Data.Fields.ConcEquip
|
||||
|| Data.UiSettings.fieldVisible settings Data.Fields.ConcPerson
|
||||
)
|
||||
|
||||
itemfolder =
|
||||
( div
|
||||
[ class itemStyle
|
||||
, title "Folder"
|
||||
]
|
||||
[ Icons.folderIcon2 "mr-2"
|
||||
, Comp.LinkTarget.makeFolderLink model.item
|
||||
[ ( linkStyle, True ) ]
|
||||
SetLinkTarget
|
||||
]
|
||||
, Data.UiSettings.fieldVisible settings Data.Fields.Folder
|
||||
)
|
||||
|
||||
src =
|
||||
( div
|
||||
[ class itemStyle
|
||||
, title "Source"
|
||||
]
|
||||
[ Icons.sourceIcon2 "mr-2"
|
||||
, Comp.LinkTarget.makeSourceLink [ ( linkStyle, True ) ]
|
||||
SetLinkTarget
|
||||
model.item.source
|
||||
]
|
||||
, True
|
||||
)
|
||||
in
|
||||
div [ class "flex flex-col pb-2" ]
|
||||
[ div [ class "flex flex-row items-center text-2xl" ]
|
||||
[ i
|
||||
[ classList
|
||||
[ ( "hidden", Data.UiSettings.fieldHidden settings Data.Fields.Direction )
|
||||
]
|
||||
, class (Data.Direction.iconFromString2 model.item.direction)
|
||||
, class "mr-2"
|
||||
, title model.item.direction
|
||||
]
|
||||
[]
|
||||
, div [ class "flex-grow ml-1 flex flex-col" ]
|
||||
[ div [ class "flex flex-row items-center font-semibold" ]
|
||||
[ text model.item.name
|
||||
, div
|
||||
[ classList
|
||||
[ ( "hidden", model.item.state /= "created" )
|
||||
]
|
||||
, class "ml-3 text-base label bg-blue-500 dark:bg-lightblue-500 text-white rounded-lg"
|
||||
]
|
||||
[ text "New"
|
||||
, i [ class "fa fa-exclamation ml-2" ] []
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, ul [ class "flex flex-col sm:flex-row flex-wrap text-base " ]
|
||||
(List.filter Tuple.second
|
||||
[ date
|
||||
, corr
|
||||
, conc
|
||||
, itemfolder
|
||||
, src
|
||||
, duedate
|
||||
]
|
||||
|> List.map Tuple.first
|
||||
)
|
||||
, div [ class "font-semibold mb-2 mt-3 pr-3" ]
|
||||
(renderTagsAndFields settings model)
|
||||
]
|
||||
|
||||
|
||||
renderTagsAndFields : UiSettings -> Model -> List (Html Msg)
|
||||
renderTagsAndFields settings model =
|
||||
[ div [ class "flex flex-row flex-wrap items-center sm:justify-end" ]
|
||||
(renderTags settings model ++ renderCustomValues settings model)
|
||||
]
|
||||
|
||||
|
||||
renderTags : UiSettings -> Model -> List (Html Msg)
|
||||
renderTags settings model =
|
||||
let
|
||||
tagView t =
|
||||
Comp.LinkTarget.makeTagLink
|
||||
(IdName t.id t.name)
|
||||
[ ( "label inline-flex ml-2 hover:opacity-90 mt-1 items-center", True )
|
||||
, ( Data.UiSettings.tagColorString2 t settings, True )
|
||||
]
|
||||
SetLinkTarget
|
||||
in
|
||||
if Data.UiSettings.fieldHidden settings Data.Fields.Tag || model.item.tags == [] then
|
||||
[]
|
||||
|
||||
else
|
||||
List.map tagView model.item.tags
|
||||
|
||||
|
||||
renderCustomValues : UiSettings -> Model -> List (Html Msg)
|
||||
renderCustomValues settings model =
|
||||
let
|
||||
fieldView cv =
|
||||
Comp.LinkTarget.makeCustomFieldLink2
|
||||
cv
|
||||
[ ( "ml-2 hover:opacity-90 mt-1 " ++ S.basicLabel, True ) ]
|
||||
SetLinkTarget
|
||||
|
||||
labelThenName cv =
|
||||
Maybe.withDefault cv.name cv.label
|
||||
in
|
||||
if Data.UiSettings.fieldHidden settings Data.Fields.CustomFields || model.item.customfields == [] then
|
||||
[]
|
||||
|
||||
else
|
||||
List.map fieldView (List.sortBy labelThenName model.item.customfields)
|
@ -103,6 +103,8 @@ type alias Model =
|
||||
, customFieldThrottle : Throttle Msg
|
||||
, allTags : List Tag
|
||||
, allPersons : Dict String Person
|
||||
, attachmentDropdownOpen : Bool
|
||||
, editMenuTabsOpen : Set String
|
||||
}
|
||||
|
||||
|
||||
@ -134,7 +136,7 @@ emptyModel =
|
||||
, attachMenuOpen = False
|
||||
, menuOpen = False
|
||||
, tagModel =
|
||||
Util.Tag.makeDropdownModel
|
||||
Util.Tag.makeDropdownModel2
|
||||
, directionModel =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption =
|
||||
@ -195,7 +197,7 @@ emptyModel =
|
||||
, pdfNativeView = Nothing
|
||||
, deleteAttachConfirm = Comp.YesNoDimmer.emptyModel
|
||||
, addFilesOpen = False
|
||||
, addFilesModel = Comp.Dropzone.init Comp.Dropzone.defaultSettings
|
||||
, addFilesModel = Comp.Dropzone.init []
|
||||
, selectedFiles = []
|
||||
, completed = Set.empty
|
||||
, errored = Set.empty
|
||||
@ -209,6 +211,8 @@ emptyModel =
|
||||
, customFieldThrottle = Throttle.create 1
|
||||
, allTags = []
|
||||
, allPersons = Dict.empty
|
||||
, attachmentDropdownOpen = False
|
||||
, editMenuTabsOpen = Set.empty
|
||||
}
|
||||
|
||||
|
||||
@ -297,6 +301,9 @@ type Msg
|
||||
| CustomFieldMsg Comp.CustomFieldMultiInput.Msg
|
||||
| CustomFieldSaveResp CustomField String (Result Http.Error BasicResult)
|
||||
| CustomFieldRemoveResp String (Result Http.Error BasicResult)
|
||||
| ToggleAttachmentDropdown
|
||||
| ToggleAkkordionTab String
|
||||
| ToggleOpenAllAkkordionTabs
|
||||
|
||||
|
||||
type SaveNameState
|
||||
|
@ -1,4 +1,4 @@
|
||||
module Comp.ItemDetail.EditMenu exposing
|
||||
module Comp.ItemDetail.MultiEditMenu exposing
|
||||
( Model
|
||||
, Msg
|
||||
, SaveNameState(..)
|
||||
@ -7,6 +7,7 @@ module Comp.ItemDetail.EditMenu exposing
|
||||
, loadModel
|
||||
, update
|
||||
, view
|
||||
, view2
|
||||
)
|
||||
|
||||
import Api
|
||||
@ -14,7 +15,6 @@ import Api.Model.EquipmentList exposing (EquipmentList)
|
||||
import Api.Model.FolderItem exposing (FolderItem)
|
||||
import Api.Model.FolderList exposing (FolderList)
|
||||
import Api.Model.IdName exposing (IdName)
|
||||
import Api.Model.ItemProposals exposing (ItemProposals)
|
||||
import Api.Model.PersonList exposing (PersonList)
|
||||
import Api.Model.ReferenceList exposing (ReferenceList)
|
||||
import Api.Model.Tag exposing (Tag)
|
||||
@ -23,9 +23,12 @@ import Comp.CustomFieldMultiInput
|
||||
import Comp.DatePicker
|
||||
import Comp.DetailEdit
|
||||
import Comp.Dropdown exposing (isDropdownChangeMsg)
|
||||
import Comp.ItemDetail.FieldTabState as FTabState
|
||||
import Comp.ItemDetail.FormChange exposing (FormChange(..))
|
||||
import Comp.Tabs as TB
|
||||
import Data.CustomFieldChange exposing (CustomFieldChange(..))
|
||||
import Data.Direction exposing (Direction)
|
||||
import Data.DropdownStyle
|
||||
import Data.Fields
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.Icons as Icons
|
||||
@ -37,6 +40,8 @@ import Html.Events exposing (onClick, onInput)
|
||||
import Http
|
||||
import Markdown
|
||||
import Page exposing (Page(..))
|
||||
import Set exposing (Set)
|
||||
import Styles as S
|
||||
import Task
|
||||
import Throttle exposing (Throttle)
|
||||
import Time
|
||||
@ -71,7 +76,6 @@ type alias Model =
|
||||
, directionModel : Comp.Dropdown.Model Direction
|
||||
, itemDatePicker : DatePicker
|
||||
, itemDate : Maybe Int
|
||||
, itemProposals : ItemProposals
|
||||
, dueDate : Maybe Int
|
||||
, dueDatePicker : DatePicker
|
||||
, corrOrgModel : Comp.Dropdown.Model IdName
|
||||
@ -81,6 +85,7 @@ type alias Model =
|
||||
, modalEdit : Maybe Comp.DetailEdit.Model
|
||||
, tagEditMode : TagEditMode
|
||||
, customFieldModel : Comp.CustomFieldMultiInput.Model
|
||||
, openTabs : Set String
|
||||
}
|
||||
|
||||
|
||||
@ -107,12 +112,13 @@ type Msg
|
||||
| GetEquipResp (Result Http.Error EquipmentList)
|
||||
| GetFolderResp (Result Http.Error FolderList)
|
||||
| CustomFieldMsg Comp.CustomFieldMultiInput.Msg
|
||||
| ToggleAkkordionTab String
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
{ tagModel =
|
||||
Util.Tag.makeDropdownModel
|
||||
Util.Tag.makeDropdownModel2
|
||||
, directionModel =
|
||||
Comp.Dropdown.makeSingleList
|
||||
{ makeOption =
|
||||
@ -155,12 +161,12 @@ init =
|
||||
, nameSaveThrottle = Throttle.create 1
|
||||
, itemDatePicker = Comp.DatePicker.emptyModel
|
||||
, itemDate = Nothing
|
||||
, itemProposals = Api.Model.ItemProposals.empty
|
||||
, dueDate = Nothing
|
||||
, dueDatePicker = Comp.DatePicker.emptyModel
|
||||
, modalEdit = Nothing
|
||||
, tagEditMode = AddTags
|
||||
, customFieldModel = Comp.CustomFieldMultiInput.initWith []
|
||||
, openTabs = Set.empty
|
||||
}
|
||||
|
||||
|
||||
@ -563,7 +569,7 @@ update flags msg model =
|
||||
CustomFieldMsg lm ->
|
||||
let
|
||||
res =
|
||||
Comp.CustomFieldMultiInput.update lm model.customFieldModel
|
||||
Comp.CustomFieldMultiInput.update flags lm model.customFieldModel
|
||||
|
||||
model_ =
|
||||
{ model | customFieldModel = res.model }
|
||||
@ -587,6 +593,17 @@ update flags msg model =
|
||||
in
|
||||
UpdateResult model_ cmd_ Sub.none change
|
||||
|
||||
ToggleAkkordionTab title ->
|
||||
let
|
||||
tabs =
|
||||
if Set.member title model.openTabs then
|
||||
Set.remove title model.openTabs
|
||||
|
||||
else
|
||||
Set.insert title model.openTabs
|
||||
in
|
||||
UpdateResult { model | openTabs = tabs } Cmd.none Sub.none NoFormChange
|
||||
|
||||
|
||||
nameThrottleSub : Model -> Sub Msg
|
||||
nameThrottleSub model =
|
||||
@ -857,3 +874,286 @@ actionInputDatePicker =
|
||||
Comp.DatePicker.defaultSettings
|
||||
in
|
||||
{ ds | containerClassList = [ ( "ui action input", True ) ] }
|
||||
|
||||
|
||||
|
||||
--- View2
|
||||
|
||||
|
||||
view2 : ViewConfig -> UiSettings -> Model -> Html Msg
|
||||
view2 =
|
||||
renderEditForm2
|
||||
|
||||
|
||||
renderEditForm2 : ViewConfig -> UiSettings -> Model -> Html Msg
|
||||
renderEditForm2 cfg settings model =
|
||||
let
|
||||
fieldVisible field =
|
||||
Data.UiSettings.fieldVisible settings field
|
||||
|
||||
optional fields html =
|
||||
if
|
||||
List.map fieldVisible fields
|
||||
|> List.foldl (||) False
|
||||
then
|
||||
html
|
||||
|
||||
else
|
||||
span [ class "hidden" ] []
|
||||
|
||||
tagModeIcon =
|
||||
case model.tagEditMode of
|
||||
AddTags ->
|
||||
i [ class "fa fa-plus" ] []
|
||||
|
||||
RemoveTags ->
|
||||
i [ class "fa fa-eraser" ] []
|
||||
|
||||
ReplaceTags ->
|
||||
i [ class "fa fa-redo-alt" ] []
|
||||
|
||||
tagModeMsg =
|
||||
case model.tagEditMode of
|
||||
AddTags ->
|
||||
"Tags chosen here are *added* to all selected items."
|
||||
|
||||
RemoveTags ->
|
||||
"Tags chosen here are *removed* from all selected items."
|
||||
|
||||
ReplaceTags ->
|
||||
"Tags chosen here *replace* those on selected items."
|
||||
|
||||
customFieldIcon field =
|
||||
case cfg.customFieldState field.id of
|
||||
SaveSuccess ->
|
||||
Nothing
|
||||
|
||||
SaveFailed ->
|
||||
Just "text-red-500 fa fa-exclamation-triangle"
|
||||
|
||||
Saving ->
|
||||
Just "fa fa-sync-alt animate-spin"
|
||||
|
||||
customFieldSettings =
|
||||
Comp.CustomFieldMultiInput.ViewSettings
|
||||
False
|
||||
"mb-4"
|
||||
customFieldIcon
|
||||
|
||||
dds =
|
||||
Data.DropdownStyle.sidebarStyle
|
||||
|
||||
tabStyle =
|
||||
TB.searchMenuStyle
|
||||
in
|
||||
div [ class cfg.menuClass, class "mt-2" ]
|
||||
[ TB.akkordion
|
||||
tabStyle
|
||||
(tabState settings model)
|
||||
[ { title = "Confirm/Unconfirm item metadata"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div
|
||||
[ class "flex flex-row space-x-4"
|
||||
]
|
||||
[ button
|
||||
[ class S.primaryButton
|
||||
, class "flex-grow"
|
||||
, onClick (ConfirmMsg True)
|
||||
]
|
||||
[ text "Confirm"
|
||||
]
|
||||
, button
|
||||
[ class S.secondaryButton
|
||||
, class "flex-grow"
|
||||
, onClick (ConfirmMsg False)
|
||||
]
|
||||
[ text "Unconfirm"
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Tags"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div [ class "field" ]
|
||||
[ label [ class S.inputLabel ]
|
||||
[ Icons.tagsIcon2 ""
|
||||
, text "Tags"
|
||||
, a
|
||||
[ class "float-right"
|
||||
, class S.link
|
||||
, href "#"
|
||||
, title "Change tag edit mode"
|
||||
, onClick ToggleTagEditMode
|
||||
]
|
||||
[ tagModeIcon
|
||||
]
|
||||
]
|
||||
, Html.map TagDropdownMsg (Comp.Dropdown.view2 dds settings model.tagModel)
|
||||
, Markdown.toHtml [ class "opacity-50 text-sm" ] tagModeMsg
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Folder"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ Html.map FolderDropdownMsg (Comp.Dropdown.view2 dds settings model.folderModel)
|
||||
, div
|
||||
[ classList
|
||||
[ ( S.message, True )
|
||||
, ( "hidden", isFolderMember model )
|
||||
]
|
||||
]
|
||||
[ Markdown.toHtml [] """
|
||||
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.
|
||||
"""
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Custom Fields"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ Html.map CustomFieldMsg
|
||||
(Comp.CustomFieldMultiInput.view2 dds customFieldSettings model.customFieldModel)
|
||||
]
|
||||
}
|
||||
, { title = "Date"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div [ class "relative" ]
|
||||
[ Html.map ItemDatePickerMsg
|
||||
(Comp.DatePicker.viewTime
|
||||
model.itemDate
|
||||
actionInputDatePicker2
|
||||
model.itemDatePicker
|
||||
)
|
||||
, a
|
||||
[ class S.inputLeftIconLinkSidebar
|
||||
, href "#"
|
||||
, onClick RemoveDate
|
||||
]
|
||||
[ i [ class "fa fa-trash-alt font-thin" ] []
|
||||
]
|
||||
, Icons.dateIcon2 S.dateInputIcon
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Due Date"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div [ class "relative" ]
|
||||
[ Html.map DueDatePickerMsg
|
||||
(Comp.DatePicker.viewTime
|
||||
model.dueDate
|
||||
actionInputDatePicker2
|
||||
model.dueDatePicker
|
||||
)
|
||||
, a
|
||||
[ class S.inputLeftIconLinkSidebar
|
||||
, href "#"
|
||||
, onClick RemoveDueDate
|
||||
]
|
||||
[ i [ class "fa fa-trash-alt font-thin" ] []
|
||||
]
|
||||
, Icons.dueDateIcon2 S.dateInputIcon
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Correspondent"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ optional [ Data.Fields.CorrOrg ] <|
|
||||
div [ class "mb-4" ]
|
||||
[ label [ class S.inputLabel ]
|
||||
[ Icons.organizationIcon2 ""
|
||||
, span [ class "ml-2" ]
|
||||
[ text "Organization"
|
||||
]
|
||||
]
|
||||
, Html.map OrgDropdownMsg (Comp.Dropdown.view2 dds settings model.corrOrgModel)
|
||||
]
|
||||
, optional [ Data.Fields.CorrPerson ] <|
|
||||
div [ class "mb-4" ]
|
||||
[ label [ class S.inputLabel ]
|
||||
[ Icons.personIcon2 ""
|
||||
, span [ class "ml-2" ]
|
||||
[ text "Person"
|
||||
]
|
||||
]
|
||||
, Html.map CorrPersonMsg (Comp.Dropdown.view2 dds settings model.corrPersonModel)
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title =
|
||||
"Concerning"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ optional [ Data.Fields.ConcPerson ] <|
|
||||
div [ class "mb-4" ]
|
||||
[ label [ class S.inputLabel ]
|
||||
[ Icons.personIcon2 ""
|
||||
, span [ class "ml-2" ]
|
||||
[ text "Person" ]
|
||||
]
|
||||
, Html.map ConcPersonMsg (Comp.Dropdown.view2 dds settings model.concPersonModel)
|
||||
]
|
||||
, optional [ Data.Fields.ConcEquip ] <|
|
||||
div [ class "mb-4" ]
|
||||
[ label [ class S.inputLabel ]
|
||||
[ Icons.equipmentIcon2 ""
|
||||
, span [ class "ml-2" ]
|
||||
[ text "Equipment" ]
|
||||
]
|
||||
, Html.map ConcEquipMsg (Comp.Dropdown.view2 dds settings model.concEquipModel)
|
||||
]
|
||||
]
|
||||
}
|
||||
, { title = "Direction"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ Html.map DirDropdownMsg (Comp.Dropdown.view2 dds settings model.directionModel)
|
||||
]
|
||||
}
|
||||
, { title = "Name"
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ div [ class "relative" ]
|
||||
[ input
|
||||
[ type_ "text"
|
||||
, value model.nameModel
|
||||
, onInput SetName
|
||||
, class S.textInputSidebar
|
||||
]
|
||||
[]
|
||||
, span [ class S.inputLeftIconOnly ]
|
||||
[ i
|
||||
[ classList
|
||||
[ ( "text-green-500 fa fa-check", cfg.nameState == SaveSuccess )
|
||||
, ( "text-red-500 fa fa-exclamation-triangle", cfg.nameState == SaveFailed )
|
||||
, ( "sync fa fa-circle-notch animate-spin", cfg.nameState == Saving )
|
||||
]
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
tabState : UiSettings -> Model -> TB.Tab Msg -> ( TB.State, Msg )
|
||||
tabState settings model tab =
|
||||
FTabState.tabState settings
|
||||
model.openTabs
|
||||
model.customFieldModel
|
||||
(.title >> ToggleAkkordionTab)
|
||||
tab
|
||||
|
||||
|
||||
actionInputDatePicker2 : DatePicker.Settings
|
||||
actionInputDatePicker2 =
|
||||
Comp.DatePicker.defaultSettings
|
101
modules/webapp/src/main/elm/Comp/ItemDetail/Notes.elm
Normal file
101
modules/webapp/src/main/elm/Comp/ItemDetail/Notes.elm
Normal file
@ -0,0 +1,101 @@
|
||||
module Comp.ItemDetail.Notes exposing (view)
|
||||
|
||||
import Comp.ItemDetail.Model
|
||||
exposing
|
||||
( Model
|
||||
, Msg(..)
|
||||
, NotesField(..)
|
||||
, SaveNameState(..)
|
||||
)
|
||||
import Comp.MarkdownInput
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick)
|
||||
import Markdown
|
||||
import Page exposing (Page(..))
|
||||
import Styles as S
|
||||
import Util.String
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
case model.notesField of
|
||||
ViewNotes ->
|
||||
div [ class "flex flex-col ds-item-detail-notes" ]
|
||||
[ div [ class "flex flex-row items-center border-b dark:border-bluegray-600" ]
|
||||
[ div [ class "flex-grow font-bold text-lg" ]
|
||||
[ text "Notes"
|
||||
]
|
||||
, div [ class "" ]
|
||||
[ a
|
||||
[ class S.link
|
||||
, onClick ToggleEditNotes
|
||||
, href "#"
|
||||
]
|
||||
[ i [ class "fa fa-edit mr-2" ] []
|
||||
, text "Edit"
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "" ]
|
||||
[ Markdown.toHtml [ class "markdown-preview" ]
|
||||
(Maybe.withDefault "" model.item.notes)
|
||||
]
|
||||
]
|
||||
|
||||
EditNotes mm ->
|
||||
let
|
||||
classes act =
|
||||
classList
|
||||
[ ( "opacity-100", act )
|
||||
, ( "opacity-50", not act )
|
||||
]
|
||||
in
|
||||
div [ class "flex flex-col ds-item-detail-notes" ]
|
||||
[ div [ class "flex flex-col" ]
|
||||
[ div [ class "flex flex-row items-center" ]
|
||||
[ div [ class "font-bold text-lg" ]
|
||||
[ text "Notes"
|
||||
]
|
||||
, div [ class "flex flex-grow justify-end text-sm" ]
|
||||
[ Html.map NotesEditMsg
|
||||
(Comp.MarkdownInput.viewEditLink2 classes mm)
|
||||
, span [ class "px-3" ] [ text "•" ]
|
||||
, Html.map NotesEditMsg
|
||||
(Comp.MarkdownInput.viewPreviewLink2 classes mm)
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "flex flex-col h-64" ]
|
||||
[ Html.map NotesEditMsg
|
||||
(Comp.MarkdownInput.viewContent2
|
||||
(Maybe.withDefault "" model.notesModel)
|
||||
mm
|
||||
)
|
||||
, div [ class "text-sm flex justify-end" ]
|
||||
[ Comp.MarkdownInput.viewCheatLink2 S.link mm
|
||||
]
|
||||
, div [ class "flex flex-row mt-1" ]
|
||||
[ a
|
||||
[ class S.primaryButton
|
||||
, href "#"
|
||||
, onClick SaveNotes
|
||||
]
|
||||
[ i [ class "fa fa-save font-thin mr-2" ] []
|
||||
, text "Save"
|
||||
]
|
||||
, a
|
||||
[ classList
|
||||
[ ( "invisible hidden", Util.String.isNothingOrBlank model.item.notes )
|
||||
]
|
||||
, class S.secondaryButton
|
||||
, class "ml-2"
|
||||
, href "#"
|
||||
, onClick ToggleEditNotes
|
||||
]
|
||||
[ i [ class "fa fa-times mr-2" ] []
|
||||
, text "Cancel"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
343
modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm
Normal file
343
modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm
Normal file
@ -0,0 +1,343 @@
|
||||
module Comp.ItemDetail.SingleAttachment exposing (view)
|
||||
|
||||
import Api
|
||||
import Api.Model.Attachment exposing (Attachment)
|
||||
import Comp.AttachmentMeta
|
||||
import Comp.ItemDetail.Model
|
||||
exposing
|
||||
( Model
|
||||
, Msg(..)
|
||||
, NotesField(..)
|
||||
, SaveNameState(..)
|
||||
)
|
||||
import Comp.MenuBar as MB
|
||||
import Comp.YesNoDimmer
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Dict
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick, onInput)
|
||||
import Html5.DragDrop as DD
|
||||
import Page exposing (Page(..))
|
||||
import Styles as S
|
||||
import Util.Maybe
|
||||
import Util.Size
|
||||
import Util.String
|
||||
|
||||
|
||||
view : UiSettings -> Model -> Int -> Attachment -> Html Msg
|
||||
view settings model pos attach =
|
||||
let
|
||||
fileUrl =
|
||||
Api.fileURL attach.id
|
||||
in
|
||||
div
|
||||
[ class "flex flex-col md:relative h-full mb-2"
|
||||
, classList
|
||||
[ ( "hidden", not (attachmentVisible model pos) )
|
||||
]
|
||||
]
|
||||
[ Html.map (DeleteAttachConfirm attach.id)
|
||||
(Comp.YesNoDimmer.viewN
|
||||
True
|
||||
(Comp.YesNoDimmer.defaultSettings2 "Really delete this file?")
|
||||
model.deleteAttachConfirm
|
||||
)
|
||||
, div
|
||||
[ class "flex flex-row px-2 py-2 text-sm"
|
||||
, class S.border
|
||||
]
|
||||
[ attachHeader settings model pos attach
|
||||
]
|
||||
, editAttachmentName model attach
|
||||
, attachmentSelect model pos attach
|
||||
, if isAttachMetaOpen model attach.id then
|
||||
case Dict.get attach.id model.attachMeta of
|
||||
Just am ->
|
||||
Html.map (AttachMetaMsg attach.id)
|
||||
(Comp.AttachmentMeta.view2 am)
|
||||
|
||||
Nothing ->
|
||||
span [ class "hidden" ] []
|
||||
|
||||
else
|
||||
div
|
||||
[ class "flex flex-col relative px-2 pt-2 h-full"
|
||||
, class "border-r border-l border-b dark:border-bluegray-600"
|
||||
, id "ds-pdf-view-parent"
|
||||
, style "max-height" "calc(100vh - 140px)"
|
||||
, style "min-height" "500px"
|
||||
]
|
||||
[ iframe
|
||||
[ if Maybe.withDefault settings.nativePdfPreview model.pdfNativeView then
|
||||
src fileUrl
|
||||
|
||||
else
|
||||
src (fileUrl ++ "/view")
|
||||
, class "absolute h-full w-full top-0 left-0 mx-0 py-0"
|
||||
, id "ds-pdf-view-iframe"
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
{-| attachment header
|
||||
|
||||
- toggle thumbs
|
||||
- name + size
|
||||
- eye icon to open it
|
||||
- menu
|
||||
- rename
|
||||
- meta data
|
||||
- download archive
|
||||
- download
|
||||
- delete
|
||||
- native view
|
||||
|
||||
-}
|
||||
attachHeader : UiSettings -> Model -> Int -> Attachment -> Html Msg
|
||||
attachHeader settings model _ attach =
|
||||
let
|
||||
attachName =
|
||||
Maybe.withDefault "No name" attach.name
|
||||
|
||||
fileUrl =
|
||||
Api.fileURL attach.id
|
||||
|
||||
hasArchive =
|
||||
List.map .id model.item.archives
|
||||
|> List.member attach.id
|
||||
|
||||
multiAttach =
|
||||
List.length model.item.attachments > 1
|
||||
|
||||
attachSelectToggle mobile =
|
||||
a
|
||||
[ href "#"
|
||||
, onClick ToggleAttachMenu
|
||||
, class S.secondaryBasicButton
|
||||
, classList
|
||||
[ ( "bg-gray-200 dark:bg-bluegray-600 ", model.attachMenuOpen )
|
||||
, ( "hidden", not multiAttach )
|
||||
, ( "sm:hidden", multiAttach && mobile )
|
||||
, ( "hidden sm:block", multiAttach && not mobile )
|
||||
]
|
||||
]
|
||||
[ i [ class "fa fa-images font-thin" ] []
|
||||
]
|
||||
in
|
||||
div [ class "flex flex-col sm:flex-row items-center w-full" ]
|
||||
[ attachSelectToggle False
|
||||
, div [ class "ml-2 text-base font-bold flex-grow w-full text-center sm:text-left" ]
|
||||
[ text attachName
|
||||
, text " ("
|
||||
, text (Util.Size.bytesReadable Util.Size.B (toFloat attach.size))
|
||||
, text ")"
|
||||
]
|
||||
, div [ class "flex flex-row justify-end items-center" ]
|
||||
[ attachSelectToggle True
|
||||
, a
|
||||
[ href fileUrl
|
||||
, target "_new"
|
||||
, title "Open file in new tab"
|
||||
, class S.secondaryBasicButton
|
||||
, class "ml-2"
|
||||
]
|
||||
[ i [ class "fa fa-eye font-thin" ] []
|
||||
]
|
||||
, MB.viewItem <|
|
||||
MB.Dropdown
|
||||
{ linkIcon = "fa fa-bars"
|
||||
, linkClass =
|
||||
[ ( "ml-2", True )
|
||||
, ( S.secondaryBasicButton, True )
|
||||
]
|
||||
, toggleMenu = ToggleAttachmentDropdown
|
||||
, menuOpen = model.attachmentDropdownOpen
|
||||
, items =
|
||||
[ { icon = "fa fa-download"
|
||||
, label = "Download file"
|
||||
, attrs =
|
||||
[ download attachName
|
||||
, href fileUrl
|
||||
]
|
||||
}
|
||||
, { icon = "fa fa-file"
|
||||
, label = "Rename file"
|
||||
, attrs =
|
||||
[ href "#"
|
||||
, onClick (EditAttachNameStart attach.id)
|
||||
]
|
||||
}
|
||||
, { icon = "fa fa-file-archive"
|
||||
, label = "Download original archive"
|
||||
, attrs =
|
||||
[ href (fileUrl ++ "/archive")
|
||||
, target "_new"
|
||||
, classList [ ( "hidden", not hasArchive ) ]
|
||||
]
|
||||
}
|
||||
, { icon = "fa fa-external-link-alt"
|
||||
, label = "Original file"
|
||||
, attrs =
|
||||
[ href (fileUrl ++ "/original")
|
||||
, target "_new"
|
||||
, classList [ ( "hidden", not attach.converted ) ]
|
||||
]
|
||||
}
|
||||
, { icon =
|
||||
if Maybe.withDefault settings.nativePdfPreview model.pdfNativeView then
|
||||
"fa fa-toggle-on"
|
||||
|
||||
else
|
||||
"fa fa-toggle-off"
|
||||
, label = "Render pdf by browser"
|
||||
, attrs =
|
||||
[ onClick (TogglePdfNativeView settings.nativePdfPreview)
|
||||
, href "#"
|
||||
]
|
||||
}
|
||||
, { icon =
|
||||
if isAttachMetaOpen model attach.id then
|
||||
"fa fa-toggle-on"
|
||||
|
||||
else
|
||||
"fa fa-toggle-off"
|
||||
, label = "View extracted data"
|
||||
, attrs =
|
||||
[ onClick (AttachMetaClick attach.id)
|
||||
, href "#"
|
||||
]
|
||||
}
|
||||
, { icon = "fa fa-trash"
|
||||
, label = "Delete this file"
|
||||
, attrs =
|
||||
[ onClick (RequestDeleteAttachment attach.id)
|
||||
, href "#"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
attachmentVisible : Model -> Int -> Bool
|
||||
attachmentVisible model pos =
|
||||
not model.sentMailsOpen
|
||||
&& (if model.visibleAttach >= List.length model.item.attachments then
|
||||
pos == 0
|
||||
|
||||
else
|
||||
model.visibleAttach == pos
|
||||
)
|
||||
|
||||
|
||||
isAttachMetaOpen : Model -> String -> Bool
|
||||
isAttachMetaOpen model id =
|
||||
model.attachMetaOpen && (Dict.get id model.attachMeta /= Nothing)
|
||||
|
||||
|
||||
editAttachmentName : Model -> Attachment -> Html Msg
|
||||
editAttachmentName model attach =
|
||||
let
|
||||
am =
|
||||
Util.Maybe.filter (\m -> m.id == attach.id) model.attachRename
|
||||
in
|
||||
case am of
|
||||
Just m ->
|
||||
div [ class "flex flex-row border-l border-r px-2 py-2 dark:border-bluegray-600" ]
|
||||
[ input
|
||||
[ type_ "text"
|
||||
, value m.newName
|
||||
, onInput EditAttachNameSet
|
||||
, class S.textInput
|
||||
, class "mr-2"
|
||||
]
|
||||
[]
|
||||
, button
|
||||
[ class S.primaryButton
|
||||
, onClick EditAttachNameSubmit
|
||||
]
|
||||
[ i [ class "fa fa-check" ] []
|
||||
]
|
||||
, button
|
||||
[ class S.secondaryButton
|
||||
, onClick EditAttachNameCancel
|
||||
]
|
||||
[ i [ class "fa fa-times" ] []
|
||||
]
|
||||
]
|
||||
|
||||
Nothing ->
|
||||
span [ class "hidden" ] []
|
||||
|
||||
|
||||
attachmentSelect : Model -> Int -> Attachment -> Html Msg
|
||||
attachmentSelect model _ _ =
|
||||
div
|
||||
[ class "flex flex-row border-l border-r px-2 py-2 dark:border-bluegray-600 "
|
||||
, class "overflow-x-auto overflow-y-none"
|
||||
, classList
|
||||
[ ( "hidden", not model.attachMenuOpen )
|
||||
]
|
||||
]
|
||||
(List.indexedMap (menuItem model) model.item.attachments)
|
||||
|
||||
|
||||
menuItem : Model -> Int -> Attachment -> Html Msg
|
||||
menuItem model pos attach =
|
||||
let
|
||||
highlight =
|
||||
let
|
||||
dropId =
|
||||
DD.getDropId model.attachDD
|
||||
|
||||
dragId =
|
||||
DD.getDragId model.attachDD
|
||||
|
||||
enable =
|
||||
Just attach.id == dropId && dropId /= dragId
|
||||
in
|
||||
[ ( "bg-gray-300 dark:bg-bluegray-700 current-drop-target", enable )
|
||||
]
|
||||
|
||||
active =
|
||||
model.visibleAttach == pos
|
||||
in
|
||||
a
|
||||
([ classList <|
|
||||
[ ( "border-blue-500 dark:border-lightblue-500", pos == 0 )
|
||||
, ( "dark:border-bluegray-600", pos /= 0 )
|
||||
]
|
||||
++ highlight
|
||||
, class "block flex-col relative border rounded px-1 py-1 mr-2"
|
||||
, class " hover:shadow dark:hover:border-bluegray-500"
|
||||
, href "#"
|
||||
, onClick (SetActiveAttachment pos)
|
||||
]
|
||||
++ DD.draggable AttachDDMsg attach.id
|
||||
++ DD.droppable AttachDDMsg attach.id
|
||||
)
|
||||
[ div
|
||||
[ classList
|
||||
[ ( "hidden", not active )
|
||||
]
|
||||
, class "absolute right-1 top-1 text-blue-400 dark:text-lightblue-400 text-xl"
|
||||
]
|
||||
[ i [ class "fa fa-check-circle ml-1" ] []
|
||||
]
|
||||
, div [ class "" ]
|
||||
[ img
|
||||
[ src (Api.attachmentPreviewURL attach.id)
|
||||
, class "block w-20 mx-auto"
|
||||
]
|
||||
[]
|
||||
]
|
||||
, div [ class "mt-1 text-sm break-all w-28 text-center" ]
|
||||
[ Maybe.map (Util.String.ellipsis 36) attach.name
|
||||
|> Maybe.withDefault "No Name"
|
||||
|> text
|
||||
]
|
||||
]
|
@ -22,6 +22,7 @@ import Comp.DetailEdit
|
||||
import Comp.Dropdown exposing (isDropdownChangeMsg)
|
||||
import Comp.Dropzone
|
||||
import Comp.EquipmentForm
|
||||
import Comp.ItemDetail.EditForm
|
||||
import Comp.ItemDetail.Model
|
||||
exposing
|
||||
( AttachmentRename
|
||||
@ -863,7 +864,10 @@ update key flags inav settings msg model =
|
||||
case Dict.get id model.attachMeta of
|
||||
Just _ ->
|
||||
resultModel
|
||||
{ model | attachMetaOpen = not model.attachMetaOpen }
|
||||
{ model
|
||||
| attachMetaOpen = not model.attachMetaOpen
|
||||
, attachmentDropdownOpen = False
|
||||
}
|
||||
|
||||
Nothing ->
|
||||
let
|
||||
@ -874,7 +878,11 @@ update key flags inav settings msg model =
|
||||
Dict.insert id am model.attachMeta
|
||||
in
|
||||
resultModelCmd
|
||||
( { model | attachMeta = nextMeta, attachMetaOpen = True }
|
||||
( { model
|
||||
| attachMeta = nextMeta
|
||||
, attachMetaOpen = True
|
||||
, attachmentDropdownOpen = False
|
||||
}
|
||||
, Cmd.map (AttachMetaMsg id) ac
|
||||
)
|
||||
|
||||
@ -901,6 +909,7 @@ update key flags inav settings msg model =
|
||||
|
||||
Nothing ->
|
||||
Just (not default)
|
||||
, attachmentDropdownOpen = False
|
||||
}
|
||||
|
||||
DeleteAttachConfirm attachId lmsg ->
|
||||
@ -933,7 +942,7 @@ update key flags inav settings msg model =
|
||||
inav
|
||||
settings
|
||||
(DeleteAttachConfirm id Comp.YesNoDimmer.activate)
|
||||
model
|
||||
{ model | attachmentDropdownOpen = False }
|
||||
|
||||
AddFilesToggle ->
|
||||
resultModel
|
||||
@ -964,7 +973,7 @@ update key flags inav settings msg model =
|
||||
resultModel
|
||||
{ model
|
||||
| selectedFiles = []
|
||||
, addFilesModel = Comp.Dropzone.init Comp.Dropzone.defaultSettings
|
||||
, addFilesModel = Comp.Dropzone.init []
|
||||
, completed = Set.empty
|
||||
, errored = Set.empty
|
||||
, loading = Dict.empty
|
||||
@ -1220,13 +1229,21 @@ update key flags inav settings msg model =
|
||||
in
|
||||
case name of
|
||||
Just n ->
|
||||
resultModel { model | attachRename = Just (AttachmentRename id n) }
|
||||
resultModel
|
||||
{ model
|
||||
| attachRename = Just (AttachmentRename id n)
|
||||
, attachmentDropdownOpen = False
|
||||
}
|
||||
|
||||
Nothing ->
|
||||
resultModel model
|
||||
|
||||
Just _ ->
|
||||
resultModel { model | attachRename = Nothing }
|
||||
resultModel
|
||||
{ model
|
||||
| attachRename = Nothing
|
||||
, attachmentDropdownOpen = False
|
||||
}
|
||||
|
||||
EditAttachNameCancel ->
|
||||
resultModel { model | attachRename = Nothing }
|
||||
@ -1370,7 +1387,7 @@ update key flags inav settings msg model =
|
||||
CustomFieldMsg lm ->
|
||||
let
|
||||
result =
|
||||
Comp.CustomFieldMultiInput.update lm model.customFieldsModel
|
||||
Comp.CustomFieldMultiInput.update flags lm model.customFieldsModel
|
||||
|
||||
cmd_ =
|
||||
Cmd.map CustomFieldMsg result.cmd
|
||||
@ -1460,6 +1477,36 @@ update key flags inav settings msg model =
|
||||
CustomFieldRemoveResp fieldId (Err _) ->
|
||||
resultModel { model | customFieldSavingIcon = Dict.remove fieldId model.customFieldSavingIcon }
|
||||
|
||||
ToggleAttachmentDropdown ->
|
||||
resultModel { model | attachmentDropdownOpen = not model.attachmentDropdownOpen }
|
||||
|
||||
ToggleAkkordionTab title ->
|
||||
let
|
||||
tabs =
|
||||
if Set.member title model.editMenuTabsOpen then
|
||||
Set.remove title model.editMenuTabsOpen
|
||||
|
||||
else
|
||||
Set.insert title model.editMenuTabsOpen
|
||||
in
|
||||
resultModel { model | editMenuTabsOpen = tabs }
|
||||
|
||||
ToggleOpenAllAkkordionTabs ->
|
||||
let
|
||||
allNames =
|
||||
Comp.ItemDetail.EditForm.formTabs settings model
|
||||
|> List.map .title
|
||||
|> Set.fromList
|
||||
|
||||
next =
|
||||
if model.editMenuTabsOpen == allNames then
|
||||
Set.empty
|
||||
|
||||
else
|
||||
allNames
|
||||
in
|
||||
resultModel { model | editMenuTabsOpen = next }
|
||||
|
||||
|
||||
|
||||
--- Helper
|
||||
|
@ -525,7 +525,7 @@ renderItemInfo settings model =
|
||||
, title "Correspondent"
|
||||
]
|
||||
(Icons.correspondentIcon ""
|
||||
:: Comp.LinkTarget.makeCorrLink model.item SetLinkTarget
|
||||
:: Comp.LinkTarget.makeCorrLink model.item [] SetLinkTarget
|
||||
)
|
||||
, Data.UiSettings.fieldVisible settings Data.Fields.CorrOrg
|
||||
|| Data.UiSettings.fieldVisible settings Data.Fields.CorrPerson
|
||||
@ -537,7 +537,7 @@ renderItemInfo settings model =
|
||||
, title "Concerning"
|
||||
]
|
||||
(Icons.concernedIcon
|
||||
:: Comp.LinkTarget.makeConcLink model.item SetLinkTarget
|
||||
:: Comp.LinkTarget.makeConcLink model.item [] SetLinkTarget
|
||||
)
|
||||
, Data.UiSettings.fieldVisible settings Data.Fields.ConcEquip
|
||||
|| Data.UiSettings.fieldVisible settings Data.Fields.ConcPerson
|
||||
@ -549,7 +549,7 @@ renderItemInfo settings model =
|
||||
, title "Folder"
|
||||
]
|
||||
[ Icons.folderIcon ""
|
||||
, Comp.LinkTarget.makeFolderLink model.item SetLinkTarget
|
||||
, Comp.LinkTarget.makeFolderLink model.item [] SetLinkTarget
|
||||
]
|
||||
, Data.UiSettings.fieldVisible settings Data.Fields.Folder
|
||||
)
|
||||
@ -1093,7 +1093,11 @@ renderAddFilesForm model =
|
||||
[ h4 [ class "ui header" ]
|
||||
[ text "Add more files to this item"
|
||||
]
|
||||
, Html.map AddFilesMsg (Comp.Dropzone.view model.addFilesModel)
|
||||
, Html.map AddFilesMsg
|
||||
(Comp.Dropzone.view
|
||||
Comp.Dropzone.defaultSettings
|
||||
model.addFilesModel
|
||||
)
|
||||
, button
|
||||
[ class "ui primary button"
|
||||
, href "#"
|
||||
|
289
modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm
Normal file
289
modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm
Normal file
@ -0,0 +1,289 @@
|
||||
module Comp.ItemDetail.View2 exposing (view)
|
||||
|
||||
import Comp.Basic as B
|
||||
import Comp.DetailEdit
|
||||
import Comp.ItemDetail.AddFilesForm
|
||||
import Comp.ItemDetail.ItemInfoHeader
|
||||
import Comp.ItemDetail.Model
|
||||
exposing
|
||||
( Model
|
||||
, Msg(..)
|
||||
, NotesField(..)
|
||||
, SaveNameState(..)
|
||||
)
|
||||
import Comp.ItemDetail.Notes
|
||||
import Comp.ItemDetail.SingleAttachment
|
||||
import Comp.ItemMail
|
||||
import Comp.MenuBar as MB
|
||||
import Comp.SentMails
|
||||
import Comp.YesNoDimmer
|
||||
import Data.Icons as Icons
|
||||
import Data.ItemNav exposing (ItemNav)
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick)
|
||||
import Page exposing (Page(..))
|
||||
import Styles as S
|
||||
import Util.Time
|
||||
|
||||
|
||||
view : ItemNav -> UiSettings -> Model -> Html Msg
|
||||
view inav settings model =
|
||||
div [ class "flex flex-col h-full" ]
|
||||
[ header settings model
|
||||
, menuBar inav settings model
|
||||
, body inav settings model
|
||||
, Html.map DeleteItemConfirm
|
||||
(Comp.YesNoDimmer.viewN
|
||||
True
|
||||
(Comp.YesNoDimmer.defaultSettings2 "Really delete the complete item?")
|
||||
model.deleteItemConfirm
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
header : UiSettings -> Model -> Html Msg
|
||||
header settings model =
|
||||
div [ class "my-3" ]
|
||||
[ Comp.ItemDetail.ItemInfoHeader.view settings model ]
|
||||
|
||||
|
||||
menuBar : ItemNav -> UiSettings -> Model -> Html Msg
|
||||
menuBar inav settings model =
|
||||
let
|
||||
keyDescr name =
|
||||
if settings.itemDetailShortcuts && model.menuOpen then
|
||||
" Key '" ++ name ++ "'."
|
||||
|
||||
else
|
||||
""
|
||||
in
|
||||
MB.view
|
||||
{ start =
|
||||
[ MB.CustomElement <|
|
||||
a
|
||||
[ class S.secondaryBasicButton
|
||||
, Page.href HomePage
|
||||
, title "Back to search results"
|
||||
]
|
||||
[ i [ class "fa fa-arrow-left" ] []
|
||||
]
|
||||
, MB.CustomElement <|
|
||||
div [ class "inline-flex" ]
|
||||
[ B.genericButton
|
||||
{ label = ""
|
||||
, icon = "fa fa-caret-left"
|
||||
, baseStyle = S.secondaryBasicButtonMain ++ " px-4 py-2 border rounded-l"
|
||||
, activeStyle = S.secondaryBasicButtonHover
|
||||
, handler =
|
||||
Maybe.map ItemDetailPage inav.prev
|
||||
|> Maybe.map Page.href
|
||||
|> Maybe.withDefault (href "#")
|
||||
, disabled = inav.prev == Nothing
|
||||
, attrs =
|
||||
[ title ("Previous item." ++ keyDescr "Ctrl-,")
|
||||
]
|
||||
}
|
||||
, B.genericButton
|
||||
{ label = ""
|
||||
, icon = "fa fa-caret-right"
|
||||
, baseStyle =
|
||||
S.secondaryBasicButtonMain
|
||||
++ " px-4 py-2 border-t border-b border-r rounded-r"
|
||||
, activeStyle = S.secondaryBasicButtonHover
|
||||
, handler =
|
||||
Maybe.map ItemDetailPage inav.next
|
||||
|> Maybe.map Page.href
|
||||
|> Maybe.withDefault (href "#")
|
||||
, disabled = inav.next == Nothing
|
||||
, attrs =
|
||||
[ title ("Next item." ++ keyDescr "Ctrl-.")
|
||||
]
|
||||
}
|
||||
]
|
||||
, MB.CustomElement <|
|
||||
a
|
||||
[ classList
|
||||
[ ( "bg-gray-200 dark:bg-bluegray-600", model.mailOpen )
|
||||
]
|
||||
, title "Send Mail"
|
||||
, onClick ToggleMail
|
||||
, class S.secondaryBasicButton
|
||||
, href "#"
|
||||
]
|
||||
[ i [ class "fa fa-envelope font-thin" ] []
|
||||
]
|
||||
, MB.CustomElement <|
|
||||
a
|
||||
[ classList
|
||||
[ ( "bg-gray-200 dark:bg-bluegray-600", model.addFilesOpen )
|
||||
]
|
||||
, if model.addFilesOpen then
|
||||
title "Close"
|
||||
|
||||
else
|
||||
title "Add more files to this item"
|
||||
, onClick AddFilesToggle
|
||||
, class S.secondaryBasicButton
|
||||
, href "#"
|
||||
]
|
||||
[ Icons.addFilesIcon2 ""
|
||||
]
|
||||
, MB.CustomElement <|
|
||||
a
|
||||
[ class S.primaryButton
|
||||
, href "#"
|
||||
, onClick ConfirmItem
|
||||
, title "Confirm item metadata"
|
||||
, classList [ ( "hidden", model.item.state /= "created" ) ]
|
||||
]
|
||||
[ i [ class "fa fa-check mr-2" ] []
|
||||
, text "Confirm"
|
||||
]
|
||||
]
|
||||
, end =
|
||||
[ MB.CustomElement <|
|
||||
a
|
||||
[ class S.secondaryBasicButton
|
||||
, href "#"
|
||||
, onClick UnconfirmItem
|
||||
, title "Un-confirm item metadata"
|
||||
, classList [ ( "hidden", model.item.state == "created" ) ]
|
||||
]
|
||||
[ i [ class "fa fa-eye-slash font-thin" ] []
|
||||
]
|
||||
, MB.CustomElement <|
|
||||
a
|
||||
[ class S.deleteButton
|
||||
, href "#"
|
||||
, onClick RequestDelete
|
||||
, title "Delete this item"
|
||||
]
|
||||
[ i [ class "fa fa-trash" ] []
|
||||
]
|
||||
]
|
||||
, rootClasses = "mb-2"
|
||||
}
|
||||
|
||||
|
||||
body : ItemNav -> UiSettings -> Model -> Html Msg
|
||||
body inav settings model =
|
||||
div [ class "grid gap-2 grid-cols-1 md:grid-cols-3 h-full" ]
|
||||
[ leftArea settings model
|
||||
, rightArea settings model
|
||||
]
|
||||
|
||||
|
||||
leftArea : UiSettings -> Model -> Html Msg
|
||||
leftArea settings model =
|
||||
div [ class "w-full md:order-first md:mr-2 flex flex-col" ]
|
||||
[ addDetailForm settings model
|
||||
, sendMailForm settings model
|
||||
, Comp.ItemDetail.AddFilesForm.view model
|
||||
, Comp.ItemDetail.Notes.view model
|
||||
, div
|
||||
[ classList
|
||||
[ ( "hidden", Comp.SentMails.isEmpty model.sentMails )
|
||||
]
|
||||
, class "mt-4 "
|
||||
]
|
||||
[ h3 [ class "flex flex-row items-center border-b dark:border-bluegray-600 font-bold text-lg" ]
|
||||
[ text "Sent E-Mails"
|
||||
]
|
||||
, Html.map SentMailsMsg (Comp.SentMails.view2 model.sentMails)
|
||||
]
|
||||
, div [ class "flex-grow" ] []
|
||||
, itemIdInfo model
|
||||
]
|
||||
|
||||
|
||||
rightArea : UiSettings -> Model -> Html Msg
|
||||
rightArea settings model =
|
||||
div [ class "md:col-span-2 h-full" ]
|
||||
(attachmentsBody settings model)
|
||||
|
||||
|
||||
attachmentsBody : UiSettings -> Model -> List (Html Msg)
|
||||
attachmentsBody settings model =
|
||||
List.indexedMap (Comp.ItemDetail.SingleAttachment.view settings model)
|
||||
model.item.attachments
|
||||
|
||||
|
||||
sendMailForm : UiSettings -> Model -> Html Msg
|
||||
sendMailForm settings model =
|
||||
div
|
||||
[ classList
|
||||
[ ( "hidden", not model.mailOpen )
|
||||
]
|
||||
, class S.box
|
||||
, class "mb-4 px-2 py-2"
|
||||
]
|
||||
[ div [ class "text-lg font-bold" ]
|
||||
[ text "Send this item via E-Mail"
|
||||
]
|
||||
, B.loadingDimmer model.mailSending
|
||||
, Html.map ItemMailMsg (Comp.ItemMail.view2 settings model.itemMail)
|
||||
, div
|
||||
[ classList
|
||||
[ ( S.errorMessage
|
||||
, Maybe.map .success model.mailSendResult
|
||||
|> Maybe.map not
|
||||
|> Maybe.withDefault False
|
||||
)
|
||||
, ( S.successMessage
|
||||
, Maybe.map .success model.mailSendResult
|
||||
|> Maybe.withDefault False
|
||||
)
|
||||
, ( "hidden", model.mailSendResult == Nothing )
|
||||
]
|
||||
, class "mt-2"
|
||||
]
|
||||
[ Maybe.map .message model.mailSendResult
|
||||
|> Maybe.withDefault ""
|
||||
|> text
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
itemIdInfo : Model -> Html msg
|
||||
itemIdInfo model =
|
||||
div [ class "flex flex-col opacity-50 text-xs pb-1 mt-3 border-t dark:border-bluegray-600" ]
|
||||
[ div
|
||||
[ class "inline-flex items-center"
|
||||
, title "Item ID"
|
||||
]
|
||||
[ i [ class "fa fa-bullseye mr-2" ] []
|
||||
, text model.item.id
|
||||
]
|
||||
, div
|
||||
[ class "inline-flex items-center"
|
||||
, title "Created on"
|
||||
]
|
||||
[ i [ class "fa fa-sun font-thin mr-2" ] []
|
||||
, Util.Time.formatDateTime model.item.created |> text
|
||||
]
|
||||
, div
|
||||
[ class "inline-flex items-center"
|
||||
, title "Last update on"
|
||||
]
|
||||
[ i [ class "fa fa-pencil-alt mr-2" ] []
|
||||
, Util.Time.formatDateTime model.item.updated |> text
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
addDetailForm : UiSettings -> Model -> Html Msg
|
||||
addDetailForm settings model =
|
||||
case model.modalEdit of
|
||||
Just mm ->
|
||||
div
|
||||
[ class "flex flex-col px-2 py-2 mb-4"
|
||||
, class S.box
|
||||
]
|
||||
[ Comp.DetailEdit.formHeading S.header3 mm
|
||||
, Html.map ModalEditMsg (Comp.DetailEdit.view2 [] settings mm)
|
||||
]
|
||||
|
||||
Nothing ->
|
||||
span [ class "hidden" ] []
|
Reference in New Issue
Block a user