From f4c79c72ae7a742a66137b47fc65297bfe827517 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Sat, 31 Oct 2020 12:03:05 +0100 Subject: [PATCH 1/3] Allow to remove tags from multiple items --- .../scala/docspell/backend/ops/OItem.scala | 29 +++++++++++++++++++ .../src/main/resources/docspell-openapi.yml | 23 +++++++++++++++ .../restserver/routes/ItemMultiRoutes.scala | 12 ++++++++ 3 files changed, 64 insertions(+) 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 fd3e5344..492d613a 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala @@ -46,6 +46,12 @@ trait OItem[F[_]] { collective: Ident ): F[UpdateResult] + def removeTagsMultipleItems( + items: NonEmptyList[Ident], + tags: List[String], + collective: Ident + ): F[UpdateResult] + /** Toggles tags of the given item. Tags must exist, but can be IDs or names. */ def toggleTags(item: Ident, tags: List[String], collective: Ident): F[UpdateResult] @@ -225,6 +231,29 @@ object OItem { } } + def removeTagsMultipleItems( + items: NonEmptyList[Ident], + tags: List[String], + collective: Ident + ): F[UpdateResult] = + tags.distinct match { + case Nil => UpdateResult.success.pure[F] + case ws => + store.transact { + (for { + itemIds <- OptionT + .liftF(RItem.filterItems(items, collective)) + .filter(_.nonEmpty) + given <- OptionT.liftF(RTag.findAllByNameOrId(ws, collective)) + _ <- OptionT.liftF( + itemIds.traverse(item => + RTagItem.removeAllTags(item, given.map(_.tagId).toList) + ) + ) + } yield UpdateResult.success).getOrElse(UpdateResult.notFound) + } + } + def toggleTags( item: Ident, tags: List[String], diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 2eabe73f..881511be 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1945,6 +1945,29 @@ paths: application/json: schema: $ref: "#/components/schemas/BasicResult" + + /sec/items/tagsremove: + post: + tags: + - Item (Multi Edit) + summary: Remove tags from multiple items + description: | + Remove the given tags from all given items. + security: + - authTokenHeader: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ItemsAndRefs" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + put: tags: - Item (Multi Edit) diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala index 3cb50f6e..7b1dd931 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala @@ -73,6 +73,18 @@ object ItemMultiRoutes { resp <- Ok(Conversions.basicResult(res, "Tags added.")) } yield resp + case req @ POST -> Root / "tagsremove" => + for { + json <- req.as[ItemsAndRefs] + items <- readIds[F](json.items) + res <- backend.item.removeTagsMultipleItems( + items, + json.refs, + user.account.collective + ) + resp <- Ok(Conversions.basicResult(res, "Tags removed")) + } yield resp + case req @ PUT -> Root / "name" => for { json <- req.as[ItemsAndName] From a965605a9ec9dfa4b80498e0ce10a71df4ab2075 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Sat, 31 Oct 2020 14:41:25 +0100 Subject: [PATCH 2/3] Improve tag multi-edit --- modules/webapp/src/main/elm/Api.elm | 15 ++++ .../src/main/elm/Comp/ItemDetail/EditMenu.elm | 71 ++++++++++++++++++- .../main/elm/Comp/ItemDetail/FormChange.elm | 20 +++++- 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 53a9982a..6ce34ec1 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -75,6 +75,7 @@ module Api exposing , refreshSession , register , removeMember + , removeTagsMultiple , sendMail , setAttachmentName , setCollectiveSettings @@ -1342,6 +1343,20 @@ addTagsMultiple flags data receive = } +removeTagsMultiple : + Flags + -> ItemsAndRefs + -> (Result Http.Error BasicResult -> msg) + -> Cmd msg +removeTagsMultiple flags data receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/items/tagsremove" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.ItemsAndRefs.encode data) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + setNameMultiple : Flags -> ItemsAndName diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/EditMenu.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/EditMenu.elm index dd707a0e..161b5871 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/EditMenu.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/EditMenu.elm @@ -53,6 +53,12 @@ type SaveNameState | SaveFailed +type TagEditMode + = AddTags + | RemoveTags + | ReplaceTags + + type alias Model = { tagModel : Comp.Dropdown.Model Tag , nameModel : String @@ -70,6 +76,7 @@ type alias Model = , concPersonModel : Comp.Dropdown.Model IdName , concEquipModel : Comp.Dropdown.Model IdName , modalEdit : Maybe Comp.DetailEdit.Model + , tagEditMode : TagEditMode } @@ -82,6 +89,7 @@ type Msg | RemoveDueDate | RemoveDate | ConfirmMsg Bool + | ToggleTagEditMode | FolderDropdownMsg (Comp.Dropdown.Msg IdName) | TagDropdownMsg (Comp.Dropdown.Msg Tag) | DirDropdownMsg (Comp.Dropdown.Msg Direction) @@ -146,6 +154,7 @@ init = , dueDate = Nothing , dueDatePicker = Comp.DatePicker.emptyModel , modalEdit = Nothing + , tagEditMode = AddTags } @@ -213,19 +222,48 @@ update flags msg model = newModel = { model | tagModel = m2 } + mkChange list = + case model.tagEditMode of + AddTags -> + AddTagChange list + + RemoveTags -> + RemoveTagChange list + + ReplaceTags -> + ReplaceTagChange list + change = if isDropdownChangeMsg m then Comp.Dropdown.getSelected newModel.tagModel |> Util.List.distinct |> List.map (\t -> IdName t.id t.name) |> ReferenceList - |> TagChange + |> mkChange else NoFormChange in resultNoCmd change newModel + ToggleTagEditMode -> + let + ( m2, _ ) = + Comp.Dropdown.update (Comp.Dropdown.SetSelection []) model.tagModel + + newModel = + { model | tagModel = m2 } + in + case model.tagEditMode of + AddTags -> + resultNone { newModel | tagEditMode = RemoveTags } + + RemoveTags -> + resultNone { newModel | tagEditMode = ReplaceTags } + + ReplaceTags -> + resultNone { newModel | tagEditMode = AddTags } + GetTagsResp (Ok tags) -> let tagList = @@ -554,6 +592,28 @@ renderEditForm cfg settings model = else span [ class "invisible hidden" ] [] + + tagModeIcon = + case model.tagEditMode of + AddTags -> + i [ class "grey plus link icon" ] [] + + RemoveTags -> + i [ class "grey eraser link icon" ] [] + + ReplaceTags -> + i [ class "grey redo alternate link icon" ] [] + + tagModeMsg = + case model.tagEditMode of + AddTags -> + "Tags chosen here are *added* to all selected items." + + RemoveTags -> + "Tags chosen here are *removed* from all selected items." + + ReplaceTags -> + "Tags chosen here *replace* those on selected items." in div [ class cfg.menuClass ] [ div [ class "ui form warning" ] @@ -581,8 +641,17 @@ renderEditForm cfg settings model = [ label [] [ Icons.tagsIcon "grey" , text "Tags" + , a + [ class "right-float" + , href "#" + , title "Change tag edit mode" + , onClick ToggleTagEditMode + ] + [ tagModeIcon + ] ] , Html.map TagDropdownMsg (Comp.Dropdown.view settings model.tagModel) + , Markdown.toHtml [ class "small-info" ] tagModeMsg ] , div [ class " field" ] [ label [] [ text "Name" ] diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/FormChange.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/FormChange.elm index 802a24b1..b7521ba4 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/FormChange.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/FormChange.elm @@ -20,7 +20,9 @@ import Set exposing (Set) type FormChange = NoFormChange - | TagChange ReferenceList + | AddTagChange ReferenceList + | ReplaceTagChange ReferenceList + | RemoveTagChange ReferenceList | FolderChange (Maybe IdName) | DirectionChange Direction | OrgChange (Maybe IdName) @@ -45,13 +47,27 @@ multiUpdate flags ids change receive = Set.toList ids in case change of - TagChange tags -> + ReplaceTagChange tags -> let data = ItemsAndRefs items (List.map .id tags.items) in Api.setTagsMultiple flags data receive + AddTagChange tags -> + let + data = + ItemsAndRefs items (List.map .id tags.items) + in + Api.addTagsMultiple flags data receive + + RemoveTagChange tags -> + let + data = + ItemsAndRefs items (List.map .id tags.items) + in + Api.removeTagsMultiple flags data receive + NameChange name -> let data = From ff3ea883ba40880f669c0fb3cd090df3a9238f28 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Sat, 31 Oct 2020 14:44:24 +0100 Subject: [PATCH 3/3] Update changelog --- Changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index b63672a5..d75f54e1 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,7 +8,7 @@ This release contains many bug fixes, thank you all so much for helping out! There is also a new feature and some more scripts in tools. -- Edit/delete multiple items at once (#253) +- Edit/delete multiple items at once (#253, #412) - Show/hide side menus via ui settings (#351) - Adds two more scripts to the `tools/` section (thanks to @totti4ever): @@ -36,6 +36,7 @@ tools. - Routes for managing multiple items: - `/sec/items/deleteAll` - `/sec/items/tags` + - `/sec/items/tagsremove` - `/sec/items/name` - `/sec/items/folder` - `/sec/items/direction`