From 86443e10a6dbf008dc3d2bb60e7aa9e505a8e492 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Sat, 11 Jul 2020 12:00:19 +0200 Subject: [PATCH] Set the folder of an item --- .../scala/docspell/backend/ops/OItem.scala | 12 +++ .../src/main/resources/docspell-openapi.yml | 27 +++++++ .../restserver/conv/Conversions.scala | 1 + .../restserver/routes/ItemRoutes.scala | 7 ++ .../scala/docspell/store/queries/QItem.scala | 10 ++- .../scala/docspell/store/records/RItem.scala | 14 +++- modules/webapp/src/main/elm/Api.elm | 11 +++ .../webapp/src/main/elm/Comp/ItemDetail.elm | 79 ++++++++++++++++++- 8 files changed, 155 insertions(+), 6 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala index 7c170230..f51e7bd3 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala @@ -24,6 +24,8 @@ trait OItem[F[_]] { def setDirection(item: Ident, direction: Direction, collective: Ident): F[AddResult] + def setFolder(item: Ident, folder: Option[Ident], collective: Ident): F[AddResult] + def setCorrOrg(item: Ident, org: Option[Ident], collective: Ident): F[AddResult] def addCorrOrg(item: Ident, org: OOrganization.OrgAndContacts): F[AddResult] @@ -131,6 +133,16 @@ object OItem { .attempt .map(AddResult.fromUpdate) + def setFolder( + item: Ident, + folder: Option[Ident], + collective: Ident + ): F[AddResult] = + store + .transact(RItem.updateFolder(item, collective, folder)) + .attempt + .map(AddResult.fromUpdate) + def setCorrOrg(item: Ident, org: Option[Ident], collective: Ident): F[AddResult] = store .transact(RItem.updateCorrOrg(item, collective, org)) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index bcdeef5c..4a94d772 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1365,6 +1365,31 @@ paths: application/json: schema: $ref: "#/components/schemas/BasicResult" + /sec/item/{id}/folder: + put: + tags: [ Item ] + summary: Set a folder for this item. + description: | + Updates the folder property for this item to "place" the item + into a folder. If the request contains an empty object or an + `id` property of `null`, the item is moved into the "public" + or "root" folder. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OptionalId" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" /sec/item/{id}/corrOrg: put: tags: [ Item ] @@ -3167,6 +3192,8 @@ components: $ref: "#/components/schemas/IdName" inReplyTo: $ref: "#/components/schemas/IdName" + folder: + $ref: "#/components/schemas/IdName" dueDate: type: integer format: date-time diff --git a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala index 271fae25..4f093c40 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala @@ -85,6 +85,7 @@ trait Conversions { data.concPerson.map(p => IdName(p.pid, p.name)), data.concEquip.map(e => IdName(e.eid, e.name)), data.inReplyTo.map(mkIdName), + data.folder.map(mkIdName), data.item.dueDate, data.item.notes, data.attachments.map((mkAttachment(data) _).tupled).toList, diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala index ba1003d5..d32ba6b8 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -149,6 +149,13 @@ object ItemRoutes { resp <- Ok(Conversions.basicResult(res, "Direction updated")) } yield resp + case req @ PUT -> Root / Ident(id) / "folder" => + for { + idref <- req.as[OptionalId] + res <- backend.item.setFolder(id, idref.id, user.account.collective) + resp <- Ok(Conversions.basicResult(res, "Folder updated")) + } yield resp + case req @ PUT -> Root / Ident(id) / "corrOrg" => for { idref <- req.as[OptionalId] diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index 84c81e9a..657f10ec 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -66,6 +66,7 @@ object QItem { concPerson: Option[RPerson], concEquip: Option[REquipment], inReplyTo: Option[IdRef], + folder: Option[IdRef], tags: Vector[RTag], attachments: Vector[(RAttachment, FileMeta)], sources: Vector[(RAttachmentSource, FileMeta)], @@ -83,10 +84,11 @@ object QItem { val P1C = RPerson.Columns.all.map(_.prefix("p1")) val EC = REquipment.Columns.all.map(_.prefix("e")) val ICC = List(RItem.Columns.id, RItem.Columns.name).map(_.prefix("ref")) + val FC = List(RFolder.Columns.id, RFolder.Columns.name).map(_.prefix("f")) val cq = selectSimple( - IC ++ OC ++ P0C ++ P1C ++ EC ++ ICC, + IC ++ OC ++ P0C ++ P1C ++ EC ++ ICC ++ FC, RItem.table ++ fr"i", Fragment.empty ) ++ @@ -105,6 +107,9 @@ object QItem { fr"LEFT JOIN" ++ RItem.table ++ fr"ref ON" ++ RItem.Columns.inReplyTo .prefix("i") .is(RItem.Columns.id.prefix("ref")) ++ + fr"LEFT JOIN" ++ RFolder.table ++ fr"f ON" ++ RItem.Columns.folder + .prefix("i") + .is(RFolder.Columns.id.prefix("f")) ++ fr"WHERE" ++ RItem.Columns.id.prefix("i").is(id) val q = cq @@ -115,6 +120,7 @@ object QItem { Option[RPerson], Option[RPerson], Option[REquipment], + Option[IdRef], Option[IdRef] ) ] @@ -132,7 +138,7 @@ object QItem { arch <- archives ts <- tags } yield data.map(d => - ItemData(d._1, d._2, d._3, d._4, d._5, d._6, ts, att, srcs, arch) + ItemData(d._1, d._2, d._3, d._4, d._5, d._6, d._7, ts, att, srcs, arch) ) } diff --git a/modules/store/src/main/scala/docspell/store/records/RItem.scala b/modules/store/src/main/scala/docspell/store/records/RItem.scala index b0c197ce..ea40ec30 100644 --- a/modules/store/src/main/scala/docspell/store/records/RItem.scala +++ b/modules/store/src/main/scala/docspell/store/records/RItem.scala @@ -82,7 +82,7 @@ object RItem { val created = Column("created") val updated = Column("updated") val notes = Column("notes") - val folder = Column("folder_id") + val folder = Column("folder_id") val all = List( id, cid, @@ -243,7 +243,17 @@ object RItem { n <- updateRow( table, and(cid.is(coll), concEquipment.is(Some(currentEquip))), - commas(concPerson.setTo(None: Option[Ident]), updated.setTo(t)) + commas(concEquipment.setTo(None: Option[Ident]), updated.setTo(t)) + ).update.run + } yield n + + def updateFolder(itemId: Ident, coll: Ident, folderId: Option[Ident]): ConnectionIO[Int] = + for { + t <- currentTime + n <- updateRow( + table, + and(cid.is(coll), id.is(itemId)), + commas(folder.setTo(folderId), updated.setTo(t)) ).update.run } yield n diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index b23fa1dd..2934547d 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -77,6 +77,7 @@ module Api exposing , setCorrOrg , setCorrPerson , setDirection + , setFolder , setItemDate , setItemDueDate , setItemName @@ -1262,6 +1263,16 @@ setDirection flags item dir receive = } +setFolder : Flags -> String -> OptionalId -> (Result Http.Error BasicResult -> msg) -> Cmd msg +setFolder flags item id receive = + Http2.authPut + { url = flags.config.baseUrl ++ "/api/v1/sec/item/" ++ item ++ "/folder" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.OptionalId.encode id) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + setCorrOrg : Flags -> String -> OptionalId -> (Result Http.Error BasicResult -> msg) -> Cmd msg setCorrOrg flags item id receive = Http2.authPut diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail.elm b/modules/webapp/src/main/elm/Comp/ItemDetail.elm index 718e5d80..67127543 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail.elm @@ -11,6 +11,8 @@ import Api.Model.Attachment exposing (Attachment) import Api.Model.BasicResult exposing (BasicResult) import Api.Model.DirectionValue exposing (DirectionValue) import Api.Model.EquipmentList exposing (EquipmentList) +import Api.Model.FolderItem exposing (FolderItem) +import Api.Model.FolderList exposing (FolderList) import Api.Model.IdName exposing (IdName) import Api.Model.ItemDetail exposing (ItemDetail) import Api.Model.ItemProposals exposing (ItemProposals) @@ -71,6 +73,7 @@ type alias Model = , corrPersonModel : Comp.Dropdown.Model IdName , concPersonModel : Comp.Dropdown.Model IdName , concEquipModel : Comp.Dropdown.Model IdName + , folderModel : Comp.Dropdown.Model IdName , nameModel : String , notesModel : Maybe String , notesField : NotesField @@ -165,6 +168,11 @@ emptyModel = { makeOption = \e -> { value = e.id, text = e.name } , placeholder = "" } + , folderModel = + Comp.Dropdown.makeSingle + { makeOption = \e -> { value = e.id, text = e.name } + , placeholder = "" + } , nameModel = "" , notesModel = Nothing , notesField = ViewNotes @@ -268,6 +276,8 @@ type Msg | EditAttachNameSet String | EditAttachNameSubmit | EditAttachNameResp (Result Http.Error BasicResult) + | GetFolderResp (Result Http.Error FolderList) + | FolderDropdownMsg (Comp.Dropdown.Msg IdName) @@ -281,6 +291,7 @@ getOptions flags = , Api.getOrgLight flags GetOrgResp , Api.getPersonsLight flags GetPersonResp , Api.getEquipments flags "" GetEquipResp + , Api.getFolders flags "" False GetFolderResp ] @@ -310,6 +321,16 @@ setDirection flags model = Cmd.none +setFolder : Flags -> Model -> Maybe IdName -> Cmd Msg +setFolder flags model mref = + let + idref = + Maybe.map .id mref + |> OptionalId + in + Api.setFolder flags model.item.id idref SaveResp + + setCorrOrg : Flags -> Model -> Maybe IdName -> Cmd Msg setCorrOrg flags model mref = let @@ -523,6 +544,20 @@ update key flags next msg model = ( m7, c7, s7 ) = update key flags next AddFilesReset m6 + ( m8, c8, s8 ) = + update key + flags + next + (FolderDropdownMsg + (Comp.Dropdown.SetSelection + (item.folder + |> Maybe.map List.singleton + |> Maybe.withDefault [] + ) + ) + ) + m7 + proposalCmd = if item.state == "created" then Api.getItemProposals flags item.id GetProposalResp @@ -530,7 +565,7 @@ update key flags next msg model = else Cmd.none in - ( { m7 + ( { m8 | item = item , nameModel = item.name , notesModel = item.notes @@ -548,11 +583,12 @@ update key flags next msg model = , c5 , c6 , c7 + , c8 , getOptions flags , proposalCmd , Api.getSentMails flags item.id SentMailsResp ] - , Sub.batch [ s1, s2, s3, s4, s5, s6, s7 ] + , Sub.batch [ s1, s2, s3, s4, s5, s6, s7, s8 ] ) SetActiveAttachment pos -> @@ -575,6 +611,26 @@ update key flags next msg model = else noSub ( model, Api.itemDetail flags model.item.id GetItemResp ) + FolderDropdownMsg m -> + let + ( m2, c2 ) = + Comp.Dropdown.update m model.folderModel + + newModel = + { model | folderModel = m2 } + + idref = + Comp.Dropdown.getSelected m2 |> List.head + + save = + if isDropdownChangeMsg m then + setFolder flags newModel idref + + else + Cmd.none + in + noSub ( newModel, Cmd.batch [ save, Cmd.map FolderDropdownMsg c2 ] ) + TagDropdownMsg m -> let ( m2, c2 ) = @@ -827,6 +883,18 @@ update key flags next msg model = SetDueDateSuggestion date -> noSub ( model, setDueDate flags model (Just date) ) + GetFolderResp (Ok fs) -> + let + opts = + fs.items + |> List.map (\e -> IdName e.id e.name) + |> Comp.Dropdown.SetOptions + in + update key flags next (FolderDropdownMsg opts) model + + GetFolderResp (Err _) -> + noSub ( model, Cmd.none ) + GetTagsResp (Ok tags) -> let tagList = @@ -2082,6 +2150,13 @@ renderEditForm settings model = ] ] ] + , div [ class "field" ] + [ label [] + [ Icons.folderIcon "grey" + , text "Folder" + ] + , Html.map FolderDropdownMsg (Comp.Dropdown.view settings model.folderModel) + ] , div [ class "field" ] [ label [] [ Icons.directionIcon "grey"