mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
Link items it detail view
This commit is contained in:
@ -13,6 +13,7 @@ module Api exposing
|
||||
, addCorrPerson
|
||||
, addDashboard
|
||||
, addMember
|
||||
, addRelatedItems
|
||||
, addShare
|
||||
, addTag
|
||||
, addTagsMultiple
|
||||
@ -91,6 +92,7 @@ module Api exposing
|
||||
, getPersonFull
|
||||
, getPersons
|
||||
, getPersonsLight
|
||||
, getRelatedItems
|
||||
, getScanMailbox
|
||||
, getSentMails
|
||||
, getShare
|
||||
@ -130,6 +132,8 @@ module Api exposing
|
||||
, refreshSession
|
||||
, register
|
||||
, removeMember
|
||||
, removeRelatedItem
|
||||
, removeRelatedItems
|
||||
, removeTagsMultiple
|
||||
, replaceDashboard
|
||||
, reprocessItem
|
||||
@ -227,7 +231,9 @@ import Api.Model.ImapSettingsList exposing (ImapSettingsList)
|
||||
import Api.Model.InviteResult exposing (InviteResult)
|
||||
import Api.Model.ItemDetail exposing (ItemDetail)
|
||||
import Api.Model.ItemInsights exposing (ItemInsights)
|
||||
import Api.Model.ItemLightGroup exposing (ItemLightGroup)
|
||||
import Api.Model.ItemLightList exposing (ItemLightList)
|
||||
import Api.Model.ItemLinkData exposing (ItemLinkData)
|
||||
import Api.Model.ItemProposals exposing (ItemProposals)
|
||||
import Api.Model.ItemQuery exposing (ItemQuery)
|
||||
import Api.Model.ItemUploadMeta exposing (ItemUploadMeta)
|
||||
@ -3007,6 +3013,48 @@ verifyJsonFilter flags query receive =
|
||||
|
||||
|
||||
|
||||
--- Item Links
|
||||
|
||||
|
||||
getRelatedItems : Flags -> String -> (Result Http.Error ItemLightGroup -> msg) -> Cmd msg
|
||||
getRelatedItems flags itemId receive =
|
||||
Http2.authGet
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/itemlink/" ++ itemId
|
||||
, account = getAccount flags
|
||||
, expect = Http.expectJson receive Api.Model.ItemLightGroup.decoder
|
||||
}
|
||||
|
||||
|
||||
addRelatedItems : Flags -> ItemLinkData -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
addRelatedItems flags data receive =
|
||||
Http2.authPost
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/itemlink/addAll"
|
||||
, account = getAccount flags
|
||||
, body = Http.jsonBody (Api.Model.ItemLinkData.encode data)
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
removeRelatedItems : Flags -> ItemLinkData -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
removeRelatedItems flags data receive =
|
||||
Http2.authPost
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/itemlink/removeAll"
|
||||
, account = getAccount flags
|
||||
, body = Http.jsonBody (Api.Model.ItemLinkData.encode data)
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
removeRelatedItem : Flags -> String -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
removeRelatedItem flags item1 item2 receive =
|
||||
Http2.authDelete
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/itemlink/" ++ item1 ++ "/" ++ item2
|
||||
, account = getAccount flags
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
|
||||
--- Helper
|
||||
|
||||
|
||||
|
@ -48,6 +48,7 @@ import Comp.DatePicker
|
||||
import Comp.DetailEdit
|
||||
import Comp.Dropdown
|
||||
import Comp.Dropzone
|
||||
import Comp.ItemLinkForm
|
||||
import Comp.ItemMail
|
||||
import Comp.KeyInput
|
||||
import Comp.LinkTarget exposing (LinkTarget)
|
||||
@ -121,6 +122,7 @@ type alias Model =
|
||||
, editMenuTabsOpen : Set String
|
||||
, viewMode : ViewMode
|
||||
, showQrModel : ShowQrModel
|
||||
, itemLinkModel : Comp.ItemLinkForm.Model
|
||||
}
|
||||
|
||||
|
||||
@ -256,6 +258,7 @@ emptyModel =
|
||||
, editMenuTabsOpen = Set.empty
|
||||
, viewMode = SimpleView
|
||||
, showQrModel = initShowQrModel
|
||||
, itemLinkModel = Comp.ItemLinkForm.emptyModel
|
||||
}
|
||||
|
||||
|
||||
@ -369,6 +372,7 @@ type Msg
|
||||
| PrintElement String
|
||||
| SetNameMsg Comp.SimpleTextInput.Msg
|
||||
| ToggleSelectItem
|
||||
| ItemLinkFormMsg Comp.ItemLinkForm.Msg
|
||||
|
||||
|
||||
type SaveNameState
|
||||
|
@ -46,6 +46,7 @@ import Comp.ItemDetail.Model
|
||||
, resultModelCmd
|
||||
, resultModelCmdSub
|
||||
)
|
||||
import Comp.ItemLinkForm
|
||||
import Comp.ItemMail
|
||||
import Comp.KeyInput
|
||||
import Comp.LinkTarget
|
||||
@ -95,6 +96,13 @@ update inav env msg model =
|
||||
|
||||
( cm, cc ) =
|
||||
Comp.CustomFieldMultiInput.init env.flags
|
||||
|
||||
( ilm, ilc ) =
|
||||
if model.item.id == "" then
|
||||
( model.itemLinkModel, Cmd.none )
|
||||
|
||||
else
|
||||
Comp.ItemLinkForm.init env.flags model.item.id
|
||||
in
|
||||
resultModelCmd
|
||||
( { model
|
||||
@ -104,6 +112,7 @@ update inav env msg model =
|
||||
, visibleAttach = 0
|
||||
, attachMenuOpen = False
|
||||
, customFieldsModel = cm
|
||||
, itemLinkModel = ilm
|
||||
}
|
||||
, Cmd.batch
|
||||
[ getOptions env.flags
|
||||
@ -111,6 +120,7 @@ update inav env msg model =
|
||||
, Cmd.map DueDatePickerMsg dpc
|
||||
, Cmd.map ItemMailMsg ic
|
||||
, Cmd.map CustomFieldMsg cc
|
||||
, Cmd.map ItemLinkFormMsg ilc
|
||||
, Api.getSentMails env.flags model.item.id SentMailsResp
|
||||
]
|
||||
)
|
||||
@ -217,6 +227,9 @@ update inav env msg model =
|
||||
else
|
||||
Cmd.none
|
||||
|
||||
( ilm, ilc ) =
|
||||
Comp.ItemLinkForm.init env.flags item.id
|
||||
|
||||
lastModel =
|
||||
res9.model
|
||||
in
|
||||
@ -237,6 +250,7 @@ update inav env msg model =
|
||||
, dueDate = item.dueDate
|
||||
, visibleAttach = 0
|
||||
, modalEdit = Nothing
|
||||
, itemLinkModel = ilm
|
||||
}
|
||||
, cmd =
|
||||
Cmd.batch
|
||||
@ -254,6 +268,7 @@ update inav env msg model =
|
||||
, Api.getSentMails env.flags item.id SentMailsResp
|
||||
, Api.getPersons env.flags "" Data.PersonOrder.NameAsc GetPersonResp
|
||||
, Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd env.flags)
|
||||
, Cmd.map ItemLinkFormMsg ilc
|
||||
]
|
||||
, sub =
|
||||
Sub.batch
|
||||
@ -1613,6 +1628,17 @@ update inav env msg model =
|
||||
in
|
||||
{ res | selectionChange = newSelection }
|
||||
|
||||
ItemLinkFormMsg lm ->
|
||||
let
|
||||
( ilm, ilc, ils ) =
|
||||
Comp.ItemLinkForm.update env.flags lm model.itemLinkModel
|
||||
in
|
||||
resultModelCmdSub
|
||||
( { model | itemLinkModel = ilm }
|
||||
, Cmd.map ItemLinkFormMsg ilc
|
||||
, Sub.map ItemLinkFormMsg ils
|
||||
)
|
||||
|
||||
|
||||
|
||||
--- Helper
|
||||
|
@ -24,6 +24,7 @@ import Comp.ItemDetail.Model
|
||||
import Comp.ItemDetail.Notes
|
||||
import Comp.ItemDetail.ShowQrCode
|
||||
import Comp.ItemDetail.SingleAttachment
|
||||
import Comp.ItemLinkForm
|
||||
import Comp.ItemMail
|
||||
import Comp.MenuBar as MB
|
||||
import Comp.SentMails
|
||||
@ -45,8 +46,6 @@ view : Texts -> ItemNav -> Env.View -> Model -> Html Msg
|
||||
view texts inav env model =
|
||||
div [ class "flex flex-col h-full" ]
|
||||
[ header texts inav env model
|
||||
|
||||
-- , menuBar texts inav settings model
|
||||
, body texts env.flags inav env.settings model
|
||||
, itemModal texts model
|
||||
]
|
||||
@ -407,12 +406,18 @@ itemActions texts flags settings model classes =
|
||||
|
||||
|
||||
notesAndSentMails : Texts -> Flags -> UiSettings -> Model -> String -> Html Msg
|
||||
notesAndSentMails texts _ _ model classes =
|
||||
notesAndSentMails texts _ settings model classes =
|
||||
div
|
||||
[ class "w-full md:mr-2 flex flex-col"
|
||||
, class classes
|
||||
]
|
||||
[ Comp.ItemDetail.Notes.view texts.notes model
|
||||
, div [ class "mb-4 mt-4" ]
|
||||
[ div [ class "font-bold text-lg" ]
|
||||
[ text texts.relatedItems
|
||||
]
|
||||
, Html.map ItemLinkFormMsg (Comp.ItemLinkForm.view texts.itemLinkForm settings model.itemLinkModel)
|
||||
]
|
||||
, div
|
||||
[ classList
|
||||
[ ( "hidden", Comp.SentMails.isEmpty model.sentMails )
|
||||
|
304
modules/webapp/src/main/elm/Comp/ItemLinkForm.elm
Normal file
304
modules/webapp/src/main/elm/Comp/ItemLinkForm.elm
Normal file
@ -0,0 +1,304 @@
|
||||
module Comp.ItemLinkForm exposing (Model, Msg, emptyModel, init, update, view)
|
||||
|
||||
import Api
|
||||
import Api.Model.BasicResult exposing (BasicResult)
|
||||
import Api.Model.ItemLight exposing (ItemLight)
|
||||
import Api.Model.ItemLightGroup exposing (ItemLightGroup)
|
||||
import Comp.ItemSearchInput
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.ItemQuery as IQ
|
||||
import Data.ItemTemplate as IT
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Html exposing (Html, a, div, i, text)
|
||||
import Html.Attributes exposing (class, classList, href, title)
|
||||
import Html.Events exposing (onClick)
|
||||
import Http
|
||||
import Messages.Comp.ItemLinkForm exposing (Texts)
|
||||
import Page exposing (Page(..))
|
||||
import Styles as S
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ itemSearchModel : Comp.ItemSearchInput.Model
|
||||
, relatedItems : List ItemLight
|
||||
, targetItemId : String
|
||||
, editMode : EditMode
|
||||
, formState : FormState
|
||||
}
|
||||
|
||||
|
||||
type EditMode
|
||||
= AddRelated
|
||||
| RemoveRelated
|
||||
|
||||
|
||||
type FormState
|
||||
= FormOk
|
||||
| FormHttpError Http.Error
|
||||
| FormError String
|
||||
|
||||
|
||||
emptyModel : Model
|
||||
emptyModel =
|
||||
let
|
||||
cfg =
|
||||
Comp.ItemSearchInput.defaultConfig
|
||||
in
|
||||
{ itemSearchModel = Comp.ItemSearchInput.init cfg
|
||||
, relatedItems = []
|
||||
, targetItemId = ""
|
||||
, editMode = AddRelated
|
||||
, formState = FormOk
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= ItemSearchMsg Comp.ItemSearchInput.Msg
|
||||
| RelatedItemsResp (Result Http.Error ItemLightGroup)
|
||||
| UpdateRelatedResp (Result Http.Error BasicResult)
|
||||
| DeleteRelatedItem ItemLight
|
||||
| ToggleEditMode
|
||||
|
||||
|
||||
init : Flags -> String -> ( Model, Cmd Msg )
|
||||
init flags itemId =
|
||||
let
|
||||
searchCfg =
|
||||
Comp.ItemSearchInput.defaultConfig
|
||||
in
|
||||
( { itemSearchModel = Comp.ItemSearchInput.init searchCfg
|
||||
, relatedItems = []
|
||||
, targetItemId = itemId
|
||||
, editMode = AddRelated
|
||||
, formState = FormOk
|
||||
}
|
||||
, initCmd flags itemId
|
||||
)
|
||||
|
||||
|
||||
initCmd : Flags -> String -> Cmd Msg
|
||||
initCmd flags itemId =
|
||||
Api.getRelatedItems flags itemId RelatedItemsResp
|
||||
|
||||
|
||||
excludeResults : Model -> Maybe IQ.ItemQuery
|
||||
excludeResults model =
|
||||
let
|
||||
relatedIds =
|
||||
List.map .id model.relatedItems
|
||||
|
||||
all =
|
||||
if model.targetItemId == "" then
|
||||
relatedIds
|
||||
|
||||
else
|
||||
model.targetItemId :: relatedIds
|
||||
in
|
||||
case all of
|
||||
[] ->
|
||||
Nothing
|
||||
|
||||
ids ->
|
||||
Just <| IQ.Not (IQ.ItemIdIn ids)
|
||||
|
||||
|
||||
|
||||
--- Update
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg )
|
||||
update flags msg model =
|
||||
case msg of
|
||||
ItemSearchMsg lm ->
|
||||
case model.editMode of
|
||||
AddRelated ->
|
||||
let
|
||||
result =
|
||||
Comp.ItemSearchInput.update flags (excludeResults model) lm model.itemSearchModel
|
||||
|
||||
cmd =
|
||||
case result.selected of
|
||||
Just item ->
|
||||
if model.targetItemId == "" then
|
||||
Cmd.none
|
||||
|
||||
else
|
||||
Api.addRelatedItems flags
|
||||
{ item = model.targetItemId
|
||||
, related = [ item.id ]
|
||||
}
|
||||
UpdateRelatedResp
|
||||
|
||||
Nothing ->
|
||||
Cmd.none
|
||||
in
|
||||
( { model | itemSearchModel = result.model }
|
||||
, Cmd.batch
|
||||
[ Cmd.map ItemSearchMsg result.cmd
|
||||
, cmd
|
||||
]
|
||||
, Sub.map ItemSearchMsg result.sub
|
||||
)
|
||||
|
||||
RemoveRelated ->
|
||||
( model, Cmd.none, Sub.none )
|
||||
|
||||
RelatedItemsResp (Ok list) ->
|
||||
( { model
|
||||
| relatedItems = list.items
|
||||
, formState = FormOk
|
||||
, editMode =
|
||||
if List.isEmpty list.items then
|
||||
AddRelated
|
||||
|
||||
else
|
||||
model.editMode
|
||||
}
|
||||
, Cmd.none
|
||||
, Sub.none
|
||||
)
|
||||
|
||||
RelatedItemsResp (Err err) ->
|
||||
( { model | formState = FormHttpError err }, Cmd.none, Sub.none )
|
||||
|
||||
UpdateRelatedResp (Ok res) ->
|
||||
if res.success then
|
||||
( { model | formState = FormOk }
|
||||
, initCmd flags model.targetItemId
|
||||
, Sub.none
|
||||
)
|
||||
|
||||
else
|
||||
( { model | formState = FormError res.message }, Cmd.none, Sub.none )
|
||||
|
||||
UpdateRelatedResp (Err err) ->
|
||||
( { model | formState = FormHttpError err }, Cmd.none, Sub.none )
|
||||
|
||||
ToggleEditMode ->
|
||||
let
|
||||
next =
|
||||
if model.editMode == RemoveRelated then
|
||||
AddRelated
|
||||
|
||||
else
|
||||
RemoveRelated
|
||||
in
|
||||
( { model | editMode = next }, Cmd.none, Sub.none )
|
||||
|
||||
DeleteRelatedItem item ->
|
||||
case model.editMode of
|
||||
RemoveRelated ->
|
||||
if model.targetItemId == "" then
|
||||
( model, Cmd.none, Sub.none )
|
||||
|
||||
else
|
||||
( model, Api.removeRelatedItem flags model.targetItemId item.id UpdateRelatedResp, Sub.none )
|
||||
|
||||
AddRelated ->
|
||||
( model, Cmd.none, Sub.none )
|
||||
|
||||
|
||||
|
||||
--- View
|
||||
|
||||
|
||||
view : Texts -> UiSettings -> Model -> Html Msg
|
||||
view texts settings model =
|
||||
div
|
||||
[ classList
|
||||
[ ( "bg-red-100 bg-opacity-25", model.editMode == RemoveRelated )
|
||||
, ( "dark:bg-orange-80 dark:bg-opacity-10", model.editMode == RemoveRelated )
|
||||
]
|
||||
]
|
||||
[ div [ class "relative" ]
|
||||
[ Html.map ItemSearchMsg
|
||||
(Comp.ItemSearchInput.view texts.itemSearchInput
|
||||
settings
|
||||
model.itemSearchModel
|
||||
[ class "text-sm py-1 pr-6"
|
||||
, classList [ ( "disabled", model.editMode == RemoveRelated ) ]
|
||||
]
|
||||
)
|
||||
, a
|
||||
[ classList
|
||||
[ ( "hidden", Comp.ItemSearchInput.hasFocus model.itemSearchModel )
|
||||
, ( "bg-red-600 text-white dark:bg-orange-500 dark:text-slate-900 ", model.editMode == RemoveRelated )
|
||||
, ( "opacity-50", model.editMode == AddRelated )
|
||||
, ( S.deleteButtonBase, model.editMode == AddRelated )
|
||||
]
|
||||
, class " absolute right-0 top-0 rounded-r py-1 px-2 h-full block text-sm"
|
||||
, href "#"
|
||||
, onClick ToggleEditMode
|
||||
]
|
||||
[ i [ class "fa fa-trash " ] []
|
||||
]
|
||||
, div
|
||||
[ class "absolute right-0 top-0 py-1 mr-1 w-4"
|
||||
, classList [ ( "hidden", not (Comp.ItemSearchInput.isSearching model.itemSearchModel) ) ]
|
||||
]
|
||||
[ i [ class "fa fa-circle-notch animate-spin" ] []
|
||||
]
|
||||
]
|
||||
, case model.formState of
|
||||
FormOk ->
|
||||
viewRelatedItems texts settings model
|
||||
|
||||
FormHttpError err ->
|
||||
div [ class S.errorText ]
|
||||
[ text <| texts.httpError err
|
||||
]
|
||||
|
||||
FormError msg ->
|
||||
div [ class S.errorText ]
|
||||
[ text msg
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewRelatedItems : Texts -> UiSettings -> Model -> Html Msg
|
||||
viewRelatedItems texts settings model =
|
||||
div [ class "px-1.5 pb-0.5" ]
|
||||
(List.map (viewItem texts settings model) model.relatedItems)
|
||||
|
||||
|
||||
viewItem : Texts -> UiSettings -> Model -> ItemLight -> Html Msg
|
||||
viewItem texts _ model item =
|
||||
let
|
||||
mainTpl =
|
||||
IT.name
|
||||
|
||||
tooltipTpl =
|
||||
IT.concat
|
||||
[ IT.dateShort
|
||||
, IT.literal ", "
|
||||
, IT.correspondent
|
||||
]
|
||||
|
||||
tctx =
|
||||
{ dateFormatLong = texts.dateFormatLong
|
||||
, dateFormatShort = texts.dateFormatShort
|
||||
, directionLabel = texts.directionLabel
|
||||
}
|
||||
in
|
||||
case model.editMode of
|
||||
AddRelated ->
|
||||
a
|
||||
[ class "flex items-center my-2"
|
||||
, class S.link
|
||||
, Page.href (ItemDetailPage item.id)
|
||||
, title <| IT.render tooltipTpl tctx item
|
||||
]
|
||||
[ i [ class "fa fa-link text-xs mr-1" ] []
|
||||
, IT.render mainTpl tctx item |> text
|
||||
]
|
||||
|
||||
RemoveRelated ->
|
||||
a
|
||||
[ class "flex items-center my-2"
|
||||
, class " text-red-600 hover:text-red-500 dark:text-orange-400 dark:hover:text-orange-300 "
|
||||
, href "#"
|
||||
, onClick (DeleteRelatedItem item)
|
||||
]
|
||||
[ i [ class "fa fa-trash mr-2" ] []
|
||||
, IT.render mainTpl tctx item |> text
|
||||
]
|
406
modules/webapp/src/main/elm/Comp/ItemSearchInput.elm
Normal file
406
modules/webapp/src/main/elm/Comp/ItemSearchInput.elm
Normal file
@ -0,0 +1,406 @@
|
||||
module Comp.ItemSearchInput exposing (Config, Model, Msg, defaultConfig, hasFocus, init, isSearching, update, view)
|
||||
|
||||
import Api
|
||||
import Api.Model.ItemLight exposing (ItemLight)
|
||||
import Api.Model.ItemLightList exposing (ItemLightList)
|
||||
import Comp.SimpleTextInput
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.ItemQuery as IQ
|
||||
import Data.Items
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Html exposing (Attribute, Html, a, div, span, text)
|
||||
import Html.Attributes exposing (class, classList, href, placeholder)
|
||||
import Html.Events exposing (onBlur, onClick, onFocus)
|
||||
import Http
|
||||
import Messages.Comp.ItemSearchInput exposing (Texts)
|
||||
import Process
|
||||
import Styles as S
|
||||
import Task
|
||||
import Util.Html
|
||||
import Util.List
|
||||
import Util.String
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ searchModel : Comp.SimpleTextInput.Model
|
||||
, config : Config
|
||||
, results : List ItemLight
|
||||
, searchProgress : Bool
|
||||
, menuState : MenuState
|
||||
, focus : Bool
|
||||
, errorState : ErrorState
|
||||
}
|
||||
|
||||
|
||||
type alias MenuState =
|
||||
{ open : Bool
|
||||
, active : Maybe String
|
||||
}
|
||||
|
||||
|
||||
type ErrorState
|
||||
= NoError
|
||||
| HttpError Http.Error
|
||||
|
||||
|
||||
type alias Config =
|
||||
{ makeQuery : String -> IQ.ItemQuery
|
||||
, limit : Int
|
||||
}
|
||||
|
||||
|
||||
defaultConfig : Config
|
||||
defaultConfig =
|
||||
{ limit = 15
|
||||
, makeQuery = defaultMakeQuery
|
||||
}
|
||||
|
||||
|
||||
defaultMakeQuery : String -> IQ.ItemQuery
|
||||
defaultMakeQuery str =
|
||||
let
|
||||
qstr =
|
||||
Util.String.appendIfAbsent "*" str
|
||||
in
|
||||
IQ.Or
|
||||
[ IQ.ItemIdMatch qstr
|
||||
, IQ.AllNames qstr
|
||||
]
|
||||
|
||||
|
||||
init : Config -> Model
|
||||
init cfg =
|
||||
let
|
||||
textCfg =
|
||||
{ delay = 200
|
||||
, setOnTyping = True
|
||||
, setOnEnter = True
|
||||
, setOnBlur = False
|
||||
}
|
||||
in
|
||||
{ searchModel = Comp.SimpleTextInput.init textCfg Nothing
|
||||
, config = cfg
|
||||
, results = []
|
||||
, searchProgress = False
|
||||
, menuState =
|
||||
{ open = False
|
||||
, active = Nothing
|
||||
}
|
||||
, errorState = NoError
|
||||
, focus = False
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= SetSearchMsg Comp.SimpleTextInput.Msg
|
||||
| SearchResultResp (Result Http.Error ItemLightList)
|
||||
| SelectItem ItemLight
|
||||
| FocusGained
|
||||
| FocusRemoved Bool
|
||||
|
||||
|
||||
type alias UpdateResult =
|
||||
{ model : Model
|
||||
, cmd : Cmd Msg
|
||||
, sub : Sub Msg
|
||||
, selected : Maybe ItemLight
|
||||
}
|
||||
|
||||
|
||||
isSearching : Model -> Bool
|
||||
isSearching model =
|
||||
model.searchProgress
|
||||
|
||||
|
||||
hasFocus : Model -> Bool
|
||||
hasFocus model =
|
||||
model.focus
|
||||
|
||||
|
||||
|
||||
--- Update
|
||||
|
||||
|
||||
unit : Model -> UpdateResult
|
||||
unit model =
|
||||
UpdateResult model Cmd.none Sub.none Nothing
|
||||
|
||||
|
||||
update : Flags -> Maybe IQ.ItemQuery -> Msg -> Model -> UpdateResult
|
||||
update flags addQuery msg model =
|
||||
case msg of
|
||||
SetSearchMsg lm ->
|
||||
let
|
||||
res =
|
||||
Comp.SimpleTextInput.update lm model.searchModel
|
||||
|
||||
findActiveItem results =
|
||||
Maybe.andThen (\id -> List.filter (\e -> e.id == id) results |> List.head) model.menuState.active
|
||||
|
||||
( mm, selectAction ) =
|
||||
case res.keyPressed of
|
||||
Just Util.Html.ESC ->
|
||||
( setMenuOpen False model, False )
|
||||
|
||||
Just Util.Html.Enter ->
|
||||
if model.menuState.open then
|
||||
( model, True )
|
||||
|
||||
else
|
||||
( setMenuOpen True model, False )
|
||||
|
||||
Just Util.Html.Up ->
|
||||
( setActivePrev model, False )
|
||||
|
||||
Just Util.Html.Down ->
|
||||
( setActiveNext model, False )
|
||||
|
||||
_ ->
|
||||
( model, False )
|
||||
|
||||
( model_, searchCmd ) =
|
||||
case res.change of
|
||||
Comp.SimpleTextInput.ValueUnchanged ->
|
||||
( mm, Cmd.none )
|
||||
|
||||
Comp.SimpleTextInput.ValueUpdated v ->
|
||||
let
|
||||
cmd =
|
||||
makeSearchCmd flags model addQuery v
|
||||
in
|
||||
( { mm | searchProgress = cmd /= Cmd.none }, cmd )
|
||||
in
|
||||
if selectAction then
|
||||
findActiveItem model.results
|
||||
|> Maybe.map SelectItem
|
||||
|> Maybe.map (\m -> update flags addQuery m model)
|
||||
|> Maybe.withDefault (unit model)
|
||||
|
||||
else
|
||||
{ model = { model_ | searchModel = res.model }
|
||||
, cmd = Cmd.batch [ Cmd.map SetSearchMsg res.cmd, searchCmd ]
|
||||
, sub = Sub.map SetSearchMsg res.sub
|
||||
, selected = Nothing
|
||||
}
|
||||
|
||||
SearchResultResp (Ok list) ->
|
||||
unit
|
||||
{ model
|
||||
| results = Data.Items.flatten list
|
||||
, errorState = NoError
|
||||
, searchProgress = False
|
||||
}
|
||||
|
||||
SearchResultResp (Err err) ->
|
||||
unit { model | errorState = HttpError err, searchProgress = False }
|
||||
|
||||
SelectItem item ->
|
||||
let
|
||||
ms =
|
||||
model.menuState
|
||||
|
||||
( searchModel, sub ) =
|
||||
Comp.SimpleTextInput.setValue model.searchModel ""
|
||||
|
||||
res =
|
||||
unit
|
||||
{ model
|
||||
| menuState = { ms | open = False }
|
||||
, searchModel = searchModel
|
||||
}
|
||||
in
|
||||
{ res | selected = Just item, sub = Sub.map SetSearchMsg sub }
|
||||
|
||||
FocusGained ->
|
||||
unit (setMenuOpen True model |> setFocus True)
|
||||
|
||||
FocusRemoved flag ->
|
||||
if flag then
|
||||
unit (setMenuOpen False model |> setFocus False)
|
||||
|
||||
else
|
||||
{ model = model
|
||||
, cmd =
|
||||
Process.sleep 100
|
||||
|> Task.perform (\_ -> FocusRemoved True)
|
||||
, sub = Sub.none
|
||||
, selected = Nothing
|
||||
}
|
||||
|
||||
|
||||
makeSearchCmd : Flags -> Model -> Maybe IQ.ItemQuery -> Maybe String -> Cmd Msg
|
||||
makeSearchCmd flags model addQuery str =
|
||||
let
|
||||
itemQuery =
|
||||
IQ.and
|
||||
[ addQuery
|
||||
, Maybe.map model.config.makeQuery str
|
||||
]
|
||||
|
||||
qstr =
|
||||
IQ.renderMaybe itemQuery
|
||||
|
||||
q =
|
||||
{ offset = Nothing
|
||||
, limit = Just model.config.limit
|
||||
, withDetails = Just False
|
||||
, searchMode = Nothing
|
||||
, query = qstr
|
||||
}
|
||||
in
|
||||
if str == Nothing then
|
||||
Cmd.none
|
||||
|
||||
else
|
||||
Api.itemSearch flags q SearchResultResp
|
||||
|
||||
|
||||
setMenuOpen : Bool -> Model -> Model
|
||||
setMenuOpen flag model =
|
||||
let
|
||||
ms =
|
||||
model.menuState
|
||||
in
|
||||
{ model | menuState = { ms | open = flag } }
|
||||
|
||||
|
||||
setFocus : Bool -> Model -> Model
|
||||
setFocus flag model =
|
||||
{ model | focus = flag }
|
||||
|
||||
|
||||
setActiveNext : Model -> Model
|
||||
setActiveNext model =
|
||||
let
|
||||
find ms =
|
||||
case ms.active of
|
||||
Just id ->
|
||||
Util.List.findNext (\e -> e.id == id) model.results
|
||||
|
||||
Nothing ->
|
||||
List.head model.results
|
||||
|
||||
set ms act =
|
||||
{ ms | active = act }
|
||||
|
||||
updateMs =
|
||||
find >> Maybe.map .id >> set model.menuState
|
||||
in
|
||||
if model.menuState.open then
|
||||
{ model | menuState = updateMs model.menuState }
|
||||
|
||||
else
|
||||
model
|
||||
|
||||
|
||||
setActivePrev : Model -> Model
|
||||
setActivePrev model =
|
||||
let
|
||||
find ms =
|
||||
case ms.active of
|
||||
Just id ->
|
||||
Util.List.findPrev (\e -> e.id == id) model.results
|
||||
|
||||
Nothing ->
|
||||
List.reverse model.results |> List.head
|
||||
|
||||
set ms act =
|
||||
{ ms | active = act }
|
||||
|
||||
updateMs =
|
||||
find >> Maybe.map .id >> set model.menuState
|
||||
in
|
||||
if model.menuState.open then
|
||||
{ model | menuState = updateMs model.menuState }
|
||||
|
||||
else
|
||||
model
|
||||
|
||||
|
||||
|
||||
--- View
|
||||
|
||||
|
||||
view : Texts -> UiSettings -> Model -> List (Attribute Msg) -> Html Msg
|
||||
view texts settings model attrs =
|
||||
let
|
||||
inputAttrs =
|
||||
[ class S.textInput
|
||||
, onFocus FocusGained
|
||||
, onBlur (FocusRemoved False)
|
||||
, placeholder texts.placeholder
|
||||
]
|
||||
in
|
||||
div
|
||||
[ class "relative"
|
||||
]
|
||||
[ Comp.SimpleTextInput.viewMap SetSearchMsg
|
||||
(inputAttrs ++ attrs)
|
||||
model.searchModel
|
||||
, renderResultMenu texts settings model
|
||||
]
|
||||
|
||||
|
||||
renderResultMenu : Texts -> UiSettings -> Model -> Html Msg
|
||||
renderResultMenu texts _ model =
|
||||
div
|
||||
[ class "z-50 max-h-96 overflow-y-auto"
|
||||
, class dropdownMenu
|
||||
, classList [ ( "hidden", not model.menuState.open ) ]
|
||||
]
|
||||
(case model.errorState of
|
||||
HttpError err ->
|
||||
[ div
|
||||
[ class dropdownItem
|
||||
, class S.errorText
|
||||
]
|
||||
[ text <| texts.httpError err
|
||||
]
|
||||
]
|
||||
|
||||
NoError ->
|
||||
case model.results of
|
||||
[] ->
|
||||
[ div [ class dropdownItem ]
|
||||
[ span [ class "italic" ]
|
||||
[ text texts.noResults
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
_ ->
|
||||
List.map (renderResultItem model) model.results
|
||||
)
|
||||
|
||||
|
||||
renderResultItem : Model -> ItemLight -> Html Msg
|
||||
renderResultItem model item =
|
||||
let
|
||||
active =
|
||||
model.menuState.active == Just item.id
|
||||
in
|
||||
a
|
||||
[ classList
|
||||
[ ( dropdownItem, not active )
|
||||
, ( activeItem, active )
|
||||
]
|
||||
, href "#"
|
||||
, onClick (SelectItem item)
|
||||
]
|
||||
[ text item.name
|
||||
]
|
||||
|
||||
|
||||
dropdownMenu : String
|
||||
dropdownMenu =
|
||||
" absolute left-0 bg-white dark:bg-slate-800 border dark:border-slate-700 dark:text-slate-300 shadow-lg opacity-1 transition duration-200 w-full "
|
||||
|
||||
|
||||
dropdownItem : String
|
||||
dropdownItem =
|
||||
"transition-colors duration-200 items-center block px-4 py-2 text-normal hover:bg-gray-200 dark:hover:bg-slate-700 dark:hover:text-slate-50"
|
||||
|
||||
|
||||
activeItem : String
|
||||
activeItem =
|
||||
"transition-colors duration-200 items-center block px-4 py-2 text-normal bg-gray-200 dark:bg-slate-700 dark:text-slate-50"
|
@ -119,6 +119,7 @@ type alias Result =
|
||||
, change : ValueChange
|
||||
, cmd : Cmd Msg
|
||||
, sub : Sub Msg
|
||||
, keyPressed : Maybe KeyCode
|
||||
}
|
||||
|
||||
|
||||
@ -144,6 +145,7 @@ update msg (Model model) =
|
||||
, change = ValueUnchanged
|
||||
, cmd = cmd
|
||||
, sub = makeSub model newThrottle
|
||||
, keyPressed = Nothing
|
||||
}
|
||||
|
||||
UpdateThrottle ->
|
||||
@ -155,6 +157,7 @@ update msg (Model model) =
|
||||
, change = ValueUnchanged
|
||||
, cmd = cmd
|
||||
, sub = makeSub model newThrottle
|
||||
, keyPressed = Nothing
|
||||
}
|
||||
|
||||
DelayedSet ->
|
||||
@ -172,14 +175,22 @@ update msg (Model model) =
|
||||
unit model
|
||||
|
||||
KeyPressed (Just Util.Html.Enter) ->
|
||||
let
|
||||
res =
|
||||
if model.cfg.setOnEnter then
|
||||
publishChange model
|
||||
|
||||
else
|
||||
unit model
|
||||
in
|
||||
{ res | keyPressed = Just Util.Html.Enter }
|
||||
|
||||
KeyPressed _ ->
|
||||
KeyPressed kc ->
|
||||
let
|
||||
res =
|
||||
unit model
|
||||
in
|
||||
{ res | keyPressed = kc }
|
||||
|
||||
|
||||
publishChange : InnerModel -> Result
|
||||
@ -192,6 +203,7 @@ publishChange model =
|
||||
(ValueUpdated model.value)
|
||||
Cmd.none
|
||||
(makeSub model model.throttle)
|
||||
Nothing
|
||||
|
||||
|
||||
unit : InnerModel -> Result
|
||||
@ -200,6 +212,7 @@ unit model =
|
||||
, change = ValueUnchanged
|
||||
, cmd = Cmd.none
|
||||
, sub = makeSub model model.throttle
|
||||
, keyPressed = Nothing
|
||||
}
|
||||
|
||||
|
||||
|
@ -22,6 +22,7 @@ import Api.Model.CustomFieldValue exposing (CustomFieldValue)
|
||||
import Api.Model.ItemQuery as RQ
|
||||
import Data.Direction exposing (Direction)
|
||||
import Data.SearchMode exposing (SearchMode)
|
||||
import Util.String
|
||||
|
||||
|
||||
type TagMatch
|
||||
@ -58,6 +59,7 @@ type ItemQuery
|
||||
| Source AttrMatch String
|
||||
| Dir Direction
|
||||
| ItemIdIn (List String)
|
||||
| ItemIdMatch String
|
||||
| ItemName AttrMatch String
|
||||
| AllNames String
|
||||
| Contents String
|
||||
@ -207,6 +209,13 @@ render q =
|
||||
ItemIdIn ids ->
|
||||
"id~=" ++ String.join "," ids
|
||||
|
||||
ItemIdMatch id ->
|
||||
if String.length id == 47 then
|
||||
"id" ++ attrMatch Eq ++ id
|
||||
|
||||
else
|
||||
"id" ++ attrMatch Like ++ Util.String.appendIfAbsent "*" id
|
||||
|
||||
ItemName m str ->
|
||||
"name" ++ attrMatch m ++ quoteStr str
|
||||
|
||||
|
@ -13,6 +13,7 @@ module Data.ItemTemplate exposing
|
||||
, concat
|
||||
, concerning
|
||||
, corrOrg
|
||||
, corrOrgOrPerson
|
||||
, corrPerson
|
||||
, correspondent
|
||||
, dateLong
|
||||
@ -229,6 +230,11 @@ correspondent =
|
||||
combine ", " corrOrg corrPerson
|
||||
|
||||
|
||||
corrOrgOrPerson : ItemTemplate
|
||||
corrOrgOrPerson =
|
||||
firstNonEmpty [ corrOrg, corrPerson ]
|
||||
|
||||
|
||||
concPerson : ItemTemplate
|
||||
concPerson =
|
||||
from (.concPerson >> getName)
|
||||
|
@ -21,6 +21,7 @@ import Messages.Comp.ItemDetail.ConfirmModal
|
||||
import Messages.Comp.ItemDetail.ItemInfoHeader
|
||||
import Messages.Comp.ItemDetail.Notes
|
||||
import Messages.Comp.ItemDetail.SingleAttachment
|
||||
import Messages.Comp.ItemLinkForm
|
||||
import Messages.Comp.ItemMail
|
||||
import Messages.Comp.SentMails
|
||||
import Messages.DateFormat as DF
|
||||
@ -36,6 +37,7 @@ type alias Texts =
|
||||
, itemMail : Messages.Comp.ItemMail.Texts
|
||||
, detailEdit : Messages.Comp.DetailEdit.Texts
|
||||
, confirmModal : Messages.Comp.ItemDetail.ConfirmModal.Texts
|
||||
, itemLinkForm : Messages.Comp.ItemLinkForm.Texts
|
||||
, httpError : Http.Error -> String
|
||||
, key : String
|
||||
, backToSearchResults : String
|
||||
@ -61,6 +63,7 @@ type alias Texts =
|
||||
, close : String
|
||||
, selectItem : String
|
||||
, deselectItem : String
|
||||
, relatedItems : String
|
||||
}
|
||||
|
||||
|
||||
@ -74,6 +77,7 @@ gb tz =
|
||||
, itemMail = Messages.Comp.ItemMail.gb
|
||||
, detailEdit = Messages.Comp.DetailEdit.gb
|
||||
, confirmModal = Messages.Comp.ItemDetail.ConfirmModal.gb
|
||||
, itemLinkForm = Messages.Comp.ItemLinkForm.gb tz
|
||||
, httpError = Messages.Comp.HttpError.gb
|
||||
, key = "Key"
|
||||
, backToSearchResults = "Back to search results"
|
||||
@ -99,6 +103,7 @@ gb tz =
|
||||
, close = "Close"
|
||||
, selectItem = "Select this item"
|
||||
, deselectItem = "Deselect this item"
|
||||
, relatedItems = "Linked items"
|
||||
}
|
||||
|
||||
|
||||
@ -112,6 +117,7 @@ de tz =
|
||||
, itemMail = Messages.Comp.ItemMail.de
|
||||
, detailEdit = Messages.Comp.DetailEdit.de
|
||||
, confirmModal = Messages.Comp.ItemDetail.ConfirmModal.de
|
||||
, itemLinkForm = Messages.Comp.ItemLinkForm.de tz
|
||||
, httpError = Messages.Comp.HttpError.de
|
||||
, key = "Taste"
|
||||
, backToSearchResults = "Zurück zur Suche"
|
||||
@ -137,6 +143,7 @@ de tz =
|
||||
, close = "Schließen"
|
||||
, selectItem = "Zur Auswahl hinzufügen"
|
||||
, deselectItem = "Aus Auswahl entfernen"
|
||||
, relatedItems = "Verknüpfte Dokumente"
|
||||
}
|
||||
|
||||
|
||||
@ -150,6 +157,7 @@ fr tz =
|
||||
, itemMail = Messages.Comp.ItemMail.fr
|
||||
, detailEdit = Messages.Comp.DetailEdit.fr
|
||||
, confirmModal = Messages.Comp.ItemDetail.ConfirmModal.fr
|
||||
, itemLinkForm = Messages.Comp.ItemLinkForm.fr tz
|
||||
, httpError = Messages.Comp.HttpError.fr
|
||||
, key = "Clé"
|
||||
, backToSearchResults = "Retour aux résultat de recherche"
|
||||
@ -175,4 +183,5 @@ fr tz =
|
||||
, close = "Fermer"
|
||||
, selectItem = "Sélectionner ce document"
|
||||
, deselectItem = "Désélectionner ce document"
|
||||
, relatedItems = "Documents associés"
|
||||
}
|
||||
|
49
modules/webapp/src/main/elm/Messages/Comp/ItemLinkForm.elm
Normal file
49
modules/webapp/src/main/elm/Messages/Comp/ItemLinkForm.elm
Normal file
@ -0,0 +1,49 @@
|
||||
module Messages.Comp.ItemLinkForm exposing (Texts, de, fr, gb)
|
||||
|
||||
import Data.Direction exposing (Direction)
|
||||
import Data.TimeZone exposing (TimeZone)
|
||||
import Http
|
||||
import Messages.Comp.HttpError
|
||||
import Messages.Comp.ItemSearchInput
|
||||
import Messages.Data.Direction
|
||||
import Messages.DateFormat as DF
|
||||
import Messages.UiLanguage exposing (UiLanguage(..))
|
||||
|
||||
|
||||
type alias Texts =
|
||||
{ dateFormatLong : Int -> String
|
||||
, dateFormatShort : Int -> String
|
||||
, directionLabel : Direction -> String
|
||||
, itemSearchInput : Messages.Comp.ItemSearchInput.Texts
|
||||
, httpError : Http.Error -> String
|
||||
}
|
||||
|
||||
|
||||
gb : TimeZone -> Texts
|
||||
gb tz =
|
||||
{ dateFormatLong = DF.formatDateLong English tz
|
||||
, dateFormatShort = DF.formatDateShort English tz
|
||||
, directionLabel = Messages.Data.Direction.gb
|
||||
, itemSearchInput = Messages.Comp.ItemSearchInput.gb
|
||||
, httpError = Messages.Comp.HttpError.gb
|
||||
}
|
||||
|
||||
|
||||
de : TimeZone -> Texts
|
||||
de tz =
|
||||
{ dateFormatLong = DF.formatDateLong German tz
|
||||
, dateFormatShort = DF.formatDateShort German tz
|
||||
, directionLabel = Messages.Data.Direction.de
|
||||
, itemSearchInput = Messages.Comp.ItemSearchInput.de
|
||||
, httpError = Messages.Comp.HttpError.de
|
||||
}
|
||||
|
||||
|
||||
fr : TimeZone -> Texts
|
||||
fr tz =
|
||||
{ dateFormatLong = DF.formatDateLong French tz
|
||||
, dateFormatShort = DF.formatDateShort French tz
|
||||
, directionLabel = Messages.Data.Direction.fr
|
||||
, itemSearchInput = Messages.Comp.ItemSearchInput.fr
|
||||
, httpError = Messages.Comp.HttpError.fr
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
module Messages.Comp.ItemSearchInput exposing (Texts, de, fr, gb)
|
||||
|
||||
import Http
|
||||
import Messages.Basics
|
||||
import Messages.Comp.HttpError
|
||||
|
||||
|
||||
type alias Texts =
|
||||
{ noResults : String
|
||||
, placeholder : String
|
||||
, httpError : Http.Error -> String
|
||||
}
|
||||
|
||||
|
||||
gb : Texts
|
||||
gb =
|
||||
{ noResults = "No results"
|
||||
, placeholder = Messages.Basics.gb.searchPlaceholder
|
||||
, httpError = Messages.Comp.HttpError.gb
|
||||
}
|
||||
|
||||
|
||||
de : Texts
|
||||
de =
|
||||
{ noResults = "Keine Resultate"
|
||||
, placeholder = Messages.Basics.de.searchPlaceholder
|
||||
, httpError = Messages.Comp.HttpError.de
|
||||
}
|
||||
|
||||
|
||||
fr : Texts
|
||||
fr =
|
||||
{ noResults = "Aucun document trouvé"
|
||||
, placeholder = Messages.Basics.fr.searchPlaceholder
|
||||
, httpError = Messages.Comp.HttpError.fr
|
||||
}
|
@ -233,9 +233,14 @@ deleteButton =
|
||||
deleteButtonMain ++ deleteButtonHover
|
||||
|
||||
|
||||
deleteButtonBase : String
|
||||
deleteButtonBase =
|
||||
" my-auto whitespace-nowrap border border-red-500 dark:border-lightred-500 text-red-500 dark:text-orange-500 text-center "
|
||||
|
||||
|
||||
deleteButtonMain : String
|
||||
deleteButtonMain =
|
||||
" rounded my-auto whitespace-nowrap border border-red-500 dark:border-lightred-500 text-red-500 dark:text-orange-500 text-center px-4 py-2 shadow-none focus:outline-none focus:ring focus:ring-opacity-75 "
|
||||
" rounded px-4 py-2 shadow-none focus:outline-none focus:ring focus:ring-opacity-75 " ++ deleteButtonBase
|
||||
|
||||
|
||||
deleteButtonHover : String
|
||||
|
@ -6,7 +6,8 @@
|
||||
|
||||
|
||||
module Util.String exposing
|
||||
( crazyEncode
|
||||
( appendIfAbsent
|
||||
, crazyEncode
|
||||
, ellipsis
|
||||
, isBlank
|
||||
, isNothingOrBlank
|
||||
@ -15,6 +16,7 @@ module Util.String exposing
|
||||
)
|
||||
|
||||
import Base64
|
||||
import Html exposing (strong)
|
||||
|
||||
|
||||
crazyEncode : String -> String
|
||||
@ -66,3 +68,12 @@ isNothingOrBlank : Maybe String -> Bool
|
||||
isNothingOrBlank ms =
|
||||
Maybe.map isBlank ms
|
||||
|> Maybe.withDefault True
|
||||
|
||||
|
||||
appendIfAbsent : String -> String -> String
|
||||
appendIfAbsent suffix str =
|
||||
if String.endsWith suffix str then
|
||||
str
|
||||
|
||||
else
|
||||
str ++ suffix
|
||||
|
Reference in New Issue
Block a user