diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 13249814..c8f43b96 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -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 diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm index b0f577be..726166fb 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm @@ -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 diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index fa5ac06a..f5884061 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -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 diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm index acc8d06a..54a1d3d9 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/View2.elm @@ -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 ) diff --git a/modules/webapp/src/main/elm/Comp/ItemLinkForm.elm b/modules/webapp/src/main/elm/Comp/ItemLinkForm.elm new file mode 100644 index 00000000..924867da --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ItemLinkForm.elm @@ -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 + ] diff --git a/modules/webapp/src/main/elm/Comp/ItemSearchInput.elm b/modules/webapp/src/main/elm/Comp/ItemSearchInput.elm new file mode 100644 index 00000000..4777d3f4 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ItemSearchInput.elm @@ -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" diff --git a/modules/webapp/src/main/elm/Comp/SimpleTextInput.elm b/modules/webapp/src/main/elm/Comp/SimpleTextInput.elm index ce48747c..a830198c 100644 --- a/modules/webapp/src/main/elm/Comp/SimpleTextInput.elm +++ b/modules/webapp/src/main/elm/Comp/SimpleTextInput.elm @@ -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) -> - if model.cfg.setOnEnter then - publishChange model + let + res = + if model.cfg.setOnEnter then + publishChange model - else - unit model + else + unit model + in + { res | keyPressed = Just Util.Html.Enter } - KeyPressed _ -> - unit model + 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 } diff --git a/modules/webapp/src/main/elm/Data/ItemQuery.elm b/modules/webapp/src/main/elm/Data/ItemQuery.elm index 36370a6b..2d957716 100644 --- a/modules/webapp/src/main/elm/Data/ItemQuery.elm +++ b/modules/webapp/src/main/elm/Data/ItemQuery.elm @@ -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 diff --git a/modules/webapp/src/main/elm/Data/ItemTemplate.elm b/modules/webapp/src/main/elm/Data/ItemTemplate.elm index 32771eee..132e2b9e 100644 --- a/modules/webapp/src/main/elm/Data/ItemTemplate.elm +++ b/modules/webapp/src/main/elm/Data/ItemTemplate.elm @@ -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) diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm index 9940350c..3ea89606 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail.elm @@ -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" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemLinkForm.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemLinkForm.elm new file mode 100644 index 00000000..b460252e --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/ItemLinkForm.elm @@ -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 + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemSearchInput.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemSearchInput.elm new file mode 100644 index 00000000..2fe5d23f --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/ItemSearchInput.elm @@ -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 + } diff --git a/modules/webapp/src/main/elm/Styles.elm b/modules/webapp/src/main/elm/Styles.elm index 894526c2..48a866c4 100644 --- a/modules/webapp/src/main/elm/Styles.elm +++ b/modules/webapp/src/main/elm/Styles.elm @@ -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 diff --git a/modules/webapp/src/main/elm/Util/String.elm b/modules/webapp/src/main/elm/Util/String.elm index 6f0cd492..54cfa9ea 100644 --- a/modules/webapp/src/main/elm/Util/String.elm +++ b/modules/webapp/src/main/elm/Util/String.elm @@ -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