From 643afd180991b84bc2e6990f5e863770c594e8ab Mon Sep 17 00:00:00 2001 From: eikek Date: Mon, 31 Oct 2022 22:14:40 +0100 Subject: [PATCH] Allow to change extracted text of attachments Closes: #1775 --- .../src/main/resources/docspell-openapi.yml | 66 ++++++++++ .../restserver/routes/AttachmentRoutes.scala | 37 +++++- modules/webapp/src/main/elm/Api.elm | 16 +++ .../src/main/elm/Comp/AttachmentMeta.elm | 123 +++++++++++++++--- .../src/main/elm/Comp/ItemDetail/Update.elm | 10 +- 5 files changed, 232 insertions(+), 20 deletions(-) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index b4c45699..1b3ded2f 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -4823,6 +4823,72 @@ paths: application/json: schema: $ref: "#/components/schemas/BasicResult" + /sec/attachment/{id}/extracted-text: + get: + operationId: "sec-attachment-get-extracted-text" + tags: [ Attachment ] + summary: Get the extracted text of the given attachment. + description: | + Returns the extracted text of the attachment with the given + id. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 422: + description: BadRequest + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/OptionalText" + delete: + operationId: "sec-attachment-delete-extracted-text" + summary: Removes extracted text for the given attachment. + description: | + Removes any extracted text for the given attachment. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 422: + description: BadRequest + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + post: + operationId: "sec-attachment-post-extracted-text" + tags: [ Attachment ] + summary: Changes the extracted text for an attachment + description: | + Changes the extracted text of the attachment with the given + id. The attachment must be part of an item that belongs to the + collective of the current user. This can be used to correct + poor ocr-ed files. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OptionalText" + responses: + 422: + description: BadRequest + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" /sec/attachments/delete: post: diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala index d1e4ac60..ac43b3ba 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala @@ -6,8 +6,9 @@ package docspell.restserver.routes +import cats.data.OptionT import cats.effect._ -import cats.implicits._ +import cats.syntax.all._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken @@ -157,6 +158,40 @@ object AttachmentRoutes { resp <- Ok(Conversions.basicResult(res, "Name updated.")) } yield resp + case req @ POST -> Root / Ident(id) / "extracted-text" => + (for { + itemId <- OptionT( + backend.itemSearch.findAttachment(id, user.account.collectiveId) + ).map(_.ra.itemId) + nn <- OptionT.liftF(req.as[OptionalText]) + newText = nn.text.getOrElse("").pure[F] + _ <- OptionT.liftF( + backend.attachment + .setExtractedText(user.account.collectiveId, itemId, id, newText) + ) + resp <- OptionT.liftF(Ok(BasicResult(true, "Extracted text updated."))) + } yield resp).getOrElseF(NotFound(BasicResult(false, "Attachment not found"))) + + case DELETE -> Root / Ident(id) / "extracted-text" => + (for { + itemId <- OptionT( + backend.itemSearch.findAttachment(id, user.account.collectiveId) + ).map(_.ra.itemId) + _ <- OptionT.liftF( + backend.attachment + .setExtractedText(user.account.collectiveId, itemId, id, "".pure[F]) + ) + resp <- OptionT.liftF(Ok(BasicResult(true, "Extracted text cleared."))) + } yield resp).getOrElseF(NotFound()) + + case GET -> Root / Ident(id) / "extracted-text" => + (for { + meta <- OptionT( + backend.itemSearch.findAttachmentMeta(id, user.account.collectiveId) + ) + resp <- OptionT.liftF(Ok(OptionalText(meta.content))) + } yield resp).getOrElseF(NotFound(BasicResult(false, "Attachment not found"))) + case DELETE -> Root / Ident(id) => for { n <- backend.item.deleteAttachment(id, user.account.collectiveId) diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index ff22e2a6..3ed94ba0 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -159,6 +159,7 @@ module Api exposing , searchShare , searchShareStats , sendMail + , setAttachmentExtractedText , setAttachmentName , setCollectiveSettings , setConcEquip @@ -2049,6 +2050,21 @@ setAttachmentName flags attachId newName receive = } +setAttachmentExtractedText : + Flags + -> String + -> Maybe String + -> (Result Http.Error BasicResult -> msg) + -> Cmd msg +setAttachmentExtractedText flags attachId newName receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/attachment/" ++ attachId ++ "/extracted-text" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.OptionalText.encode (OptionalText newName)) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + moveAttachmentBefore : Flags -> String diff --git a/modules/webapp/src/main/elm/Comp/AttachmentMeta.elm b/modules/webapp/src/main/elm/Comp/AttachmentMeta.elm index 624f1196..d1fc6683 100644 --- a/modules/webapp/src/main/elm/Comp/AttachmentMeta.elm +++ b/modules/webapp/src/main/elm/Comp/AttachmentMeta.elm @@ -15,12 +15,14 @@ module Comp.AttachmentMeta exposing import Api import Api.Model.AttachmentMeta exposing (AttachmentMeta) +import Api.Model.BasicResult exposing (BasicResult) import Api.Model.ItemProposals exposing (ItemProposals) import Api.Model.Label exposing (Label) import Comp.Basic as B import Data.Flags exposing (Flags) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick, onInput) import Http import Messages.Comp.AttachmentMeta exposing (Texts) import Styles as S @@ -28,13 +30,20 @@ import Styles as S type alias Model = { id : String - , meta : DataResult AttachmentMeta + , meta : DataResult } -type DataResult a +type alias EditModel = + { meta : AttachmentMeta + , text : String + } + + +type DataResult = NotAvailable - | Success a + | Success AttachmentMeta + | Editing EditModel | HttpFailure Http.Error @@ -54,16 +63,64 @@ init flags id = type Msg = MetaResp (Result Http.Error AttachmentMeta) + | SaveResp String (Result Http.Error BasicResult) + | ToggleEdit + | SaveExtractedText + | SetExtractedText String -update : Msg -> Model -> Model -update msg model = +update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update flags msg model = case msg of MetaResp (Ok am) -> - { model | meta = Success am } + ( { model | meta = Success am }, Cmd.none ) MetaResp (Err err) -> - { model | meta = HttpFailure err } + ( { model | meta = HttpFailure err }, Cmd.none ) + + SaveResp newText (Ok result) -> + if result.success then + case model.meta of + Editing { meta } -> + ( { model | meta = Success { meta | content = newText } }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + else + ( model, Cmd.none ) + + SaveResp _ (Err err) -> + ( { model | meta = HttpFailure err }, Cmd.none ) + + ToggleEdit -> + case model.meta of + Editing m -> + ( { model | meta = Success m.meta }, Cmd.none ) + + Success m -> + ( { model | meta = Editing { meta = m, text = m.content } }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + SaveExtractedText -> + case model.meta of + Editing em -> + ( model + , Api.setAttachmentExtractedText flags model.id (Just em.text) (SaveResp em.text) + ) + + _ -> + ( model, Cmd.none ) + + SetExtractedText txt -> + case model.meta of + Editing em -> + ( { model | meta = Editing { em | text = txt } }, Cmd.none ) + + _ -> + ( model, Cmd.none ) @@ -89,19 +146,55 @@ view2 texts attrs model = ] Success data -> - viewData2 texts data + viewData2 texts data Nothing + + Editing em -> + viewData2 texts em.meta (Just em.text) ] -viewData2 : Texts -> AttachmentMeta -> Html Msg -viewData2 texts meta = +viewData2 : Texts -> AttachmentMeta -> Maybe String -> Html Msg +viewData2 texts meta maybeText = div [ class "flex flex-col" ] - [ div [ class "text-lg font-bold" ] - [ text texts.content - ] - , div [ class "px-2 py-2 text-sm bg-yellow-50 dark:bg-stone-800 break-words whitespace-pre max-h-80 overflow-auto" ] - [ text meta.content + [ div [ class "flex flex-row items-center" ] + [ div [ class "text-lg font-bold flex flex-grow" ] + [ text texts.content + ] + , case maybeText of + Nothing -> + div [ class "flex text-sm" ] + [ a [ href "#", class S.link, onClick ToggleEdit ] + [ i [ class "fa fa-edit pr-1" ] [] + , text texts.basics.edit + ] + ] + + Just _ -> + div [ class "flex text-sm" ] + [ a [ href "#", class S.link, onClick ToggleEdit ] + [ text texts.basics.cancel + ] + , span [ class "px-2" ] [ text "•" ] + , a [ href "#", class S.link, onClick SaveExtractedText ] + [ i [ class "fa fa-save pr-1" ] [] + , text texts.basics.submit + ] + ] ] + , case maybeText of + Nothing -> + div [ class "px-2 py-2 text-sm bg-yellow-50 dark:bg-stone-800 break-words whitespace-pre max-h-80 overflow-auto" ] + [ text meta.content + ] + + Just em -> + textarea + [ class "px-2 py-2 text-sm bg-yellow-50 dark:bg-stone-800 break-words whitespace-pre h-80 overflow-auto" + , value em + , onInput SetExtractedText + , rows 10 + ] + [] , div [ class "text-lg font-bold mt-2" ] [ text texts.labels ] diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index 5128c5c9..98dc030e 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -991,11 +991,13 @@ update inav env msg model = case Dict.get id model.attachMeta of Just cm -> let - am = - Comp.AttachmentMeta.update lmsg cm + ( am, ac ) = + Comp.AttachmentMeta.update env.flags lmsg cm in - resultModel - { model | attachMeta = Dict.insert id am model.attachMeta } + resultModelCmd + ( { model | attachMeta = Dict.insert id am model.attachMeta } + , Cmd.map (AttachMetaMsg id) ac + ) Nothing -> resultModel model