Add routes and upload form to item detail

This commit is contained in:
Eike Kettner 2020-05-23 19:42:36 +02:00
parent f4949446e3
commit a5ca3b0325
6 changed files with 456 additions and 18 deletions
modules
restapi/src/main/resources
restserver/src/main/scala/docspell/restserver/routes
webapp/src/main/elm

@ -87,12 +87,6 @@ paths:
The upload meta data can be used to tell, whether multiple
files are one item, or if each file should become a single
item. By default, each file will be a one item.
Only certain file types are supported:
* application/pdf
Support for more types might be added.
parameters:
- $ref: "#/components/parameters/id"
requestBody:
@ -115,6 +109,48 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
/open/upload/item/{itemId}/{id}:
post:
tags: [ Upload ]
summary: Upload files to docspell.
description: |
Upload a file to docspell for processing. The id is a *source
id* configured by a collective. Files are submitted for
processing which eventually resuts in an item in the inbox of
the corresponding collective. This endpoint associates the
files to an existing item identified by its `itemId`.
The request must be a `multipart/form-data` request, where the
first part has name `meta`, is optional and may contain upload
metadata as JSON. Checkout the structure `ItemUploadMeta` at
the end if it is not shown here. Other parts specify the
files. Multiple files can be specified, but at least on is
required.
Upload meta data is ignored.
parameters:
- $ref: "#/components/parameters/id"
- $ref: "#/components/parameters/itemId"
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
meta:
$ref: "#/components/schemas/ItemUploadMeta"
file:
type: array
items:
type: string
format: binary
responses:
200:
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
/sec/checkfile/{checksum}:
get:
tags: [ Upload ]
@ -155,12 +191,6 @@ paths:
The upload meta data can be used to tell, whether multiple
files are one item, or if each file should become a single
item. By default, each file will be a one item.
Only certain file types are supported:
* application/pdf
Support for more types might be added.
security:
- authTokenHeader: []
requestBody:
@ -183,6 +213,50 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
/sec/upload/{itemId}:
post:
tags: [ Upload ]
summary: Upload files to docspell.
description: |
Upload files to docspell for processing. This route is meant
for authenticated users that upload files to their account.
This endpoint will associate the files to an existing item
identified by its `itemId`.
Everything else is the same as with the
`/open/upload/item/{itemId}/{id}` endpoint.
The request must be a "multipart/form-data" request, where the
first part is optional and may contain upload metadata as
JSON. Other parts specify the files. Multiple files can be
specified, but at least on is required.
The upload meta data is ignored, since the item already
exists.
security:
- authTokenHeader: []
parameters:
- $ref: "#/components/parameters/itemId"
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
meta:
$ref: "#/components/schemas/ItemUploadMeta"
file:
type: array
items:
type: string
format: binary
responses:
200:
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
/open/signup/register:
post:
tags: [ Registration ]
@ -3156,6 +3230,13 @@ components:
required: true
schema:
type: string
itemId:
name: itemId
in: path
description: An identifier for an item
required: true
schema:
type: string
full:
name: full
in: query

@ -39,6 +39,19 @@ object UploadRoutes {
result <- backend.upload.submit(updata, user.account, true, None)
res <- Ok(basicResult(result))
} yield res
case req @ POST -> Root / "item" / Ident(itemId) =>
for {
multipart <- req.as[Multipart[F]]
updata <- readMultipart(
multipart,
logger,
Priority.High,
cfg.backend.files.validMimeTypes
)
result <- backend.upload.submit(updata, user.account, true, Some(itemId))
res <- Ok(basicResult(result))
} yield res
}
}
@ -59,6 +72,19 @@ object UploadRoutes {
result <- backend.upload.submit(updata, id, true, None)
res <- Ok(basicResult(result))
} yield res
case req @ POST -> Root / "item" / Ident(itemId) / Ident(id) =>
for {
multipart <- req.as[Multipart[F]]
updata <- readMultipart(
multipart,
logger,
Priority.Low,
cfg.backend.files.validMimeTypes
)
result <- backend.upload.submit(updata, id, true, Some(itemId))
res <- Ok(basicResult(result))
} yield res
}
}
}

@ -71,6 +71,7 @@ module Api exposing
, submitNotifyDueItems
, updateScanMailbox
, upload
, uploadAmend
, uploadSingle
, versionInfo
)
@ -429,7 +430,42 @@ createImapSettings flags mname ems receive =
--- Upload
upload : Flags -> Maybe String -> ItemUploadMeta -> List File -> (String -> Result Http.Error BasicResult -> msg) -> List (Cmd msg)
uploadAmend :
Flags
-> String
-> List File
-> (String -> Result Http.Error BasicResult -> msg)
-> List (Cmd msg)
uploadAmend flags itemId files receive =
let
mkReq file =
let
fid =
Util.File.makeFileId file
path =
"/api/v1/sec/upload/item/" ++ itemId
in
Http2.authPostTrack
{ url = flags.config.baseUrl ++ path
, account = getAccount flags
, body =
Http.multipartBody <|
[ Http.filePart "file[]" file ]
, expect = Http.expectJson (receive fid) Api.Model.BasicResult.decoder
, tracker = fid
}
in
List.map mkReq files
upload :
Flags
-> Maybe String
-> ItemUploadMeta
-> List File
-> (String -> Result Http.Error BasicResult -> msg)
-> List (Cmd msg)
upload flags sourceId meta files receive =
let
metaStr =
@ -457,7 +493,14 @@ upload flags sourceId meta files receive =
List.map mkReq files
uploadSingle : Flags -> Maybe String -> ItemUploadMeta -> String -> List File -> (Result Http.Error BasicResult -> msg) -> Cmd msg
uploadSingle :
Flags
-> Maybe String
-> ItemUploadMeta
-> String
-> List File
-> (Result Http.Error BasicResult -> msg)
-> Cmd msg
uploadSingle flags sourceId meta track files receive =
let
metaStr =

@ -25,6 +25,7 @@ import Browser.Navigation as Nav
import Comp.AttachmentMeta
import Comp.DatePicker
import Comp.Dropdown exposing (isDropdownChangeMsg)
import Comp.Dropzone
import Comp.ItemMail
import Comp.MarkdownInput
import Comp.SentMails
@ -34,12 +35,16 @@ import Data.Flags exposing (Flags)
import Data.Icons as Icons
import DatePicker exposing (DatePicker)
import Dict exposing (Dict)
import File exposing (File)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onCheck, onClick, onInput)
import Http
import Markdown
import Page exposing (Page(..))
import Ports
import Set exposing (Set)
import Util.File exposing (makeFileId)
import Util.Http
import Util.List
import Util.Maybe
@ -77,6 +82,12 @@ type alias Model =
, attachMetaOpen : Bool
, pdfNativeView : Bool
, deleteAttachConfirm : Comp.YesNoDimmer.Model
, addFilesOpen : Bool
, addFilesModel : Comp.Dropzone.Model
, selectedFiles : List File
, completed : Set String
, errored : Set String
, loading : Set String
}
@ -165,6 +176,12 @@ emptyModel =
, attachMetaOpen = False
, pdfNativeView = False
, deleteAttachConfirm = Comp.YesNoDimmer.emptyModel
, addFilesOpen = False
, addFilesModel = Comp.Dropzone.init Comp.Dropzone.defaultSettings
, selectedFiles = []
, completed = Set.empty
, errored = Set.empty
, loading = Set.empty
}
@ -221,6 +238,12 @@ type Msg
| RequestDeleteAttachment String
| DeleteAttachConfirm String Comp.YesNoDimmer.Msg
| DeleteAttachResp (Result Http.Error BasicResult)
| AddFilesToggle
| AddFilesMsg Comp.Dropzone.Msg
| AddFilesSubmitUpload
| AddFilesUploadResp String (Result Http.Error BasicResult)
| AddFilesProgress String Http.Progress
| AddFilesReset
@ -334,6 +357,42 @@ setDueDate flags model date =
Api.setItemDueDate flags model.item.id (OptionalDate date) SaveResp
isLoading : Model -> File -> Bool
isLoading model file =
Set.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
isIdle : Model -> File -> Bool
isIdle model file =
not (isLoading model file || isCompleted model file || isError model file)
setCompleted : Model -> String -> Set String
setCompleted model fileid =
Set.insert fileid model.completed
setErrored : Model -> String -> Set String
setErrored model fileid =
Set.insert fileid model.errored
isSuccessAll : Model -> Bool
isSuccessAll model =
List.map makeFileId model.selectedFiles
|> List.all (\id -> Set.member id model.completed)
update : Nav.Key -> Flags -> Maybe String -> Msg -> Model -> ( Model, Cmd Msg )
update key flags next msg model =
case msg of
@ -430,6 +489,9 @@ update key flags next msg model =
)
m5
( m7, c7 ) =
update key flags next AddFilesReset m6
proposalCmd =
if item.state == "created" then
Api.getItemProposals flags item.id GetProposalResp
@ -437,7 +499,7 @@ update key flags next msg model =
else
Cmd.none
in
( { m6
( { m7
| item = item
, nameModel = item.name
, notesModel = item.notes
@ -453,6 +515,7 @@ update key flags next msg model =
, c4
, c5
, c6
, c7
, getOptions flags
, proposalCmd
, Api.getSentMails flags item.id SentMailsResp
@ -980,6 +1043,107 @@ update key flags next msg model =
(DeleteAttachConfirm id Comp.YesNoDimmer.activate)
model
AddFilesToggle ->
( { model | addFilesOpen = not model.addFilesOpen }
, Cmd.none
)
AddFilesMsg lm ->
let
( dm, dc, df ) =
Comp.Dropzone.update lm model.addFilesModel
nextFiles =
model.selectedFiles ++ df
in
( { model | addFilesModel = dm, selectedFiles = nextFiles }
, Cmd.map AddFilesMsg dc
)
AddFilesReset ->
( { model
| selectedFiles = []
, addFilesModel = Comp.Dropzone.init Comp.Dropzone.defaultSettings
, completed = Set.empty
, errored = Set.empty
, loading = Set.empty
}
, Cmd.none
)
AddFilesSubmitUpload ->
let
fileids =
List.map makeFileId model.selectedFiles
uploads =
Cmd.batch (Api.uploadAmend flags model.item.id model.selectedFiles AddFilesUploadResp)
tracker =
Sub.batch <| List.map (\id -> Http.track id (AddFilesProgress id)) fileids
( cm2, _, _ ) =
Comp.Dropzone.update (Comp.Dropzone.setActive False) model.addFilesModel
in
( { model | loading = Set.fromList fileids, addFilesModel = cm2 }, uploads )
AddFilesUploadResp fileid (Ok res) ->
let
compl =
if res.success then
setCompleted model fileid
else
model.completed
errs =
if not res.success then
setErrored model fileid
else
model.errored
load =
Set.remove fileid model.loading
newModel =
{ model | completed = compl, errored = errs, loading = load }
in
( newModel
, Ports.setProgress ( fileid, 100 )
)
AddFilesUploadResp fileid (Err _) ->
let
errs =
setErrored model fileid
load =
Set.remove fileid model.loading
in
( { model | errored = errs, loading = load }, Cmd.none )
AddFilesProgress fileid progress ->
let
percent =
case progress of
Http.Sending p ->
Http.fractionSent p
|> (*) 100
|> round
_ ->
0
updateBars =
if percent == 0 then
Cmd.none
else
Ports.setProgress ( fileid, percent )
in
( model, updateBars )
-- view
@ -1001,7 +1165,7 @@ view inav model =
, div
[ classList
[ ( "ui ablue-comp menu", True )
, ( "top attached", model.mailOpen )
, ( "top attached", model.mailOpen || model.addFilesOpen )
]
]
[ a [ class "item", Page.href HomePage ]
@ -1066,8 +1230,24 @@ view inav model =
]
[ Icons.editNotesIcon
]
, a
[ classList
[ ( "toggle item", True )
, ( "active", model.addFilesOpen )
]
, if model.addFilesOpen then
title "Close"
else
title "Add Files"
, onClick AddFilesToggle
, href "#"
]
[ Icons.addFilesIcon
]
]
, renderMailForm model
, renderAddFilesForm model
, div [ class "ui grid" ]
[ Html.map DeleteItemConfirm (Comp.YesNoDimmer.view model.deleteItemConfirm)
, div
@ -1756,3 +1936,94 @@ renderMailForm model =
|> text
]
]
renderAddFilesForm : Model -> Html Msg
renderAddFilesForm model =
div
[ classList
[ ( "ui bottom attached segment", True )
, ( "invisible hidden", not model.addFilesOpen )
]
]
[ h4 [ class "ui header" ]
[ text "Add more files to this item"
]
, Html.map AddFilesMsg (Comp.Dropzone.view model.addFilesModel)
, button
[ class "ui primary button"
, href "#"
, onClick AddFilesSubmitUpload
]
[ text "Submit"
]
, button
[ class "ui secondary button"
, href "#"
, onClick AddFilesReset
]
[ text "Reset"
]
, div
[ classList
[ ( "ui success message", True )
, ( "invisible hidden", model.selectedFiles == [] || not (isSuccessAll model) )
]
]
[ text "All files have been uploaded. They are being processed, some data "
, text "may not be available immediately. "
, a
[ class "link"
, href "#"
, onClick ReloadItem
]
[ text "Refresh now"
]
]
, div [ class "ui items" ]
(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
in
div [ class "item" ]
[ i
[ classList
[ ( "large", True )
, ( "file outline icon", isIdle model file )
, ( "loading spinner icon", isLoading model file )
, ( "green check icon", isCompleted model file )
, ( "red bolt icon", isError model file )
]
]
[]
, div [ class "middle aligned content" ]
[ div [ class "header" ]
[ text name
]
, div [ class "right floated meta" ]
[ text size
]
, div [ class "description" ]
[ div
[ classList
[ ( "ui small indicating progress", True )
]
, id (makeFileId file)
]
[ div [ class "bar" ]
[]
]
]
]
]

@ -1,5 +1,7 @@
module Data.Icons exposing
( concerned
( addFiles
, addFilesIcon
, concerned
, concernedIcon
, correspondent
, correspondentIcon
@ -51,3 +53,13 @@ editNotes =
editNotesIcon : Html msg
editNotesIcon =
i [ class editNotes ] []
addFiles : String
addFiles =
"file plus icon"
addFilesIcon : Html msg
addFilesIcon =
i [ class addFiles ] []

@ -41,7 +41,12 @@ update sourceId flags msg model =
uploads =
if model.singleItem then
Api.uploadSingle flags sourceId meta uploadAllTracker model.files (SingleUploadResp uploadAllTracker)
Api.uploadSingle flags
sourceId
meta
uploadAllTracker
model.files
(SingleUploadResp uploadAllTracker)
else
Cmd.batch (Api.upload flags sourceId meta model.files SingleUploadResp)