mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-04-07 06:39:32 +00:00
Add routes and upload form to item detail
This commit is contained in:
parent
f4949446e3
commit
a5ca3b0325
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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user