From 037d8e818da8af7da1df9b45f79889c12836169a Mon Sep 17 00:00:00 2001 From: Stefan Scheidewig Date: Thu, 15 Apr 2021 17:59:39 +0200 Subject: [PATCH 01/10] Added a route definition to bulk-delete attachments --- .../src/main/resources/docspell-openapi.yml | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 59dc743f..01f4bade 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -2789,6 +2789,28 @@ paths: schema: $ref: "#/components/schemas/BasicResult" + /sec/attachments/delete: + post: + tags: + - Attachment (Multi Edit) + summary: Delete multiple attachments. + description: | + Given a list of attachment ids, deletes all of them. + security: + - authTokenHeader: [ ] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/IdList" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + /sec/queue/state: get: tags: [ Job Queue ] From 93f772351a3f125416c7b18b3ceec5a064a36da5 Mon Sep 17 00:00:00 2001 From: Stefan Scheidewig Date: Thu, 15 Apr 2021 18:03:58 +0200 Subject: [PATCH 02/10] Made multi id read logic sharable --- .../restserver/conv/MultiIdSupport.scala | 31 +++++++++++++++++ .../restserver/routes/ItemMultiRoutes.scala | 33 ++----------------- 2 files changed, 34 insertions(+), 30 deletions(-) create mode 100644 modules/restserver/src/main/scala/docspell/restserver/conv/MultiIdSupport.scala diff --git a/modules/restserver/src/main/scala/docspell/restserver/conv/MultiIdSupport.scala b/modules/restserver/src/main/scala/docspell/restserver/conv/MultiIdSupport.scala new file mode 100644 index 00000000..4dbc895b --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/conv/MultiIdSupport.scala @@ -0,0 +1,31 @@ +package docspell.restserver.conv + +import cats.data.NonEmptyList +import cats.implicits._ +import cats.{ApplicativeError, MonadError} +import docspell.common.Ident +import io.circe.DecodingFailure + +trait MultiIdSupport { + + protected def readId[F[_]]( + id: String + )(implicit F: ApplicativeError[F, Throwable]): F[Ident] = + Ident + .fromString(id) + .fold( + err => F.raiseError(DecodingFailure(err, Nil)), + F.pure + ) + + protected def readIds[F[_]](ids: List[String])(implicit + F: MonadError[F, Throwable] + ): F[NonEmptyList[Ident]] = + ids.traverse(readId[F]).map(NonEmptyList.fromList).flatMap { + case Some(nel) => nel.pure[F] + case None => + F.raiseError( + DecodingFailure("Empty list found, at least one element required", Nil) + ) + } +} 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 4b15689c..270a44a9 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala @@ -1,25 +1,19 @@ package docspell.restserver.routes -import cats.ApplicativeError -import cats.MonadError -import cats.data.NonEmptyList import cats.effect._ import cats.implicits._ - import docspell.backend.BackendApp import docspell.backend.auth.AuthToken import docspell.backend.ops.OCustomFields.{RemoveValue, SetValue} -import docspell.common.{Ident, ItemState} +import docspell.common.ItemState import docspell.restapi.model._ -import docspell.restserver.conv.Conversions - -import io.circe.DecodingFailure +import docspell.restserver.conv.{Conversions, MultiIdSupport} import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl -object ItemMultiRoutes { +object ItemMultiRoutes extends MultiIdSupport { def apply[F[_]: Effect]( backend: BackendApp[F], @@ -215,25 +209,4 @@ object ItemMultiRoutes { def notEmpty: Option[String] = Option(str).notEmpty } - - private def readId[F[_]]( - id: String - )(implicit F: ApplicativeError[F, Throwable]): F[Ident] = - Ident - .fromString(id) - .fold( - err => F.raiseError(DecodingFailure(err, Nil)), - F.pure - ) - - private def readIds[F[_]](ids: List[String])(implicit - F: MonadError[F, Throwable] - ): F[NonEmptyList[Ident]] = - ids.traverse(readId[F]).map(NonEmptyList.fromList).flatMap { - case Some(nel) => nel.pure[F] - case None => - F.raiseError( - DecodingFailure("Empty list found, at least one element required", Nil) - ) - } } From fa3431202052c5f03f58262c55b9601bdc121427 Mon Sep 17 00:00:00 2001 From: Stefan Scheidewig Date: Thu, 15 Apr 2021 18:05:01 +0200 Subject: [PATCH 03/10] Implemented endpoint to delete multiple attachments --- .../scala/docspell/backend/ops/OItem.scala | 27 ++++++++++--- .../docspell/restserver/RestServer.scala | 1 + .../routes/AttachmentMultiRoutes.scala | 38 +++++++++++++++++++ .../docspell/store/records/RAttachment.scala | 24 ++++++++---- 4 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala 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 53acd38d..5d8a6143 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala @@ -1,19 +1,15 @@ package docspell.backend.ops -import cats.data.NonEmptyList -import cats.data.OptionT +import cats.data.{NonEmptyList, OptionT} import cats.effect.{Effect, Resource} import cats.implicits._ - import docspell.backend.JobFactory import docspell.common._ import docspell.ftsclient.FtsClient -import docspell.store.UpdateResult import docspell.store.queries.{QAttachment, QItem, QMoveAttachment} import docspell.store.queue.JobQueue import docspell.store.records._ -import docspell.store.{AddResult, Store} - +import docspell.store.{AddResult, Store, UpdateResult} import doobie.implicits._ import org.log4s.getLogger @@ -140,6 +136,11 @@ trait OItem[F[_]] { def deleteAttachment(id: Ident, collective: Ident): F[Int] + def deleteAttachmentMultiple( + attachments: NonEmptyList[Ident], + collective: Ident + ): F[Int] + def moveAttachmentBefore(itemId: Ident, source: Ident, target: Ident): F[AddResult] def setAttachmentName( @@ -602,6 +603,20 @@ object OItem { .deleteSingleAttachment(store)(id, collective) .flatTap(_ => fts.removeAttachment(logger, id)) + def deleteAttachmentMultiple( + attachments: NonEmptyList[Ident], + collective: Ident + ): F[Int] = + for { + attachmentIds <- store.transact( + RAttachment.filterAttachments(attachments, collective) + ) + results <- attachmentIds.traverse(attachment => + deleteAttachment(attachment, collective) + ) + n = results.sum + } yield n + def setAttachmentName( attachId: Ident, name: Option[String], diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala index 7fc0cdfb..30e5733e 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala @@ -80,6 +80,7 @@ object RestServer { "item" -> ItemRoutes(cfg, pools.blocker, restApp.backend, token), "items" -> ItemMultiRoutes(restApp.backend, token), "attachment" -> AttachmentRoutes(pools.blocker, restApp.backend, token), + "attachments" -> AttachmentMultiRoutes(restApp.backend, token), "upload" -> UploadRoutes.secured(restApp.backend, cfg, token), "checkfile" -> CheckFileRoutes.secured(restApp.backend, token), "email/send" -> MailSendRoutes(restApp.backend, token), diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala new file mode 100644 index 00000000..fede7636 --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala @@ -0,0 +1,38 @@ +package docspell.restserver.routes + +import cats.effect.Effect +import cats.implicits._ +import docspell.backend.BackendApp +import docspell.backend.auth.AuthToken +import docspell.restapi.model._ +import docspell.restserver.conv.MultiIdSupport +import org.http4s.HttpRoutes +import org.http4s.circe.CirceEntityDecoder._ +import org.http4s.circe.CirceEntityEncoder._ +import org.http4s.dsl.Http4sDsl + +object AttachmentMultiRoutes extends MultiIdSupport { + + def apply[F[_]: Effect]( + backend: BackendApp[F], + user: AuthToken + ): HttpRoutes[F] = { + + val dsl = new Http4sDsl[F] {} + import dsl._ + + HttpRoutes.of { case req @ POST -> Root / "delete" => + for { + json <- req.as[IdList] + attachments <- readIds[F](json.ids) + n <- backend.item.deleteAttachmentMultiple(attachments, user.account.collective) + res = BasicResult( + n > 0, + if (n > 0) "Attachment(s) deleted" else "Attachment deletion failed." + ) + resp <- Ok(res) + } yield resp + } + } + +} diff --git a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala index e781eb89..ec892df7 100644 --- a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala +++ b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala @@ -1,16 +1,14 @@ package docspell.store.records +import bitpeace.FileMeta import cats.data.NonEmptyList import cats.implicits._ -import fs2.Stream - import docspell.common._ import docspell.store.qb.DSL._ import docspell.store.qb._ - -import bitpeace.FileMeta import doobie._ import doobie.implicits._ +import fs2.Stream case class RAttachment( id: Ident, @@ -98,7 +96,6 @@ object RAttachment { run(select(T.all), from(T), T.id === attachId).query[RAttachment].option def findMeta(attachId: Ident): ConnectionIO[Option[FileMeta]] = { - import bitpeace.sql._ val m = RFileMeta.as("m") val a = RAttachment.as("a") @@ -191,7 +188,6 @@ object RAttachment { id: Ident, coll: Ident ): ConnectionIO[Vector[(RAttachment, FileMeta)]] = { - import bitpeace.sql._ val a = RAttachment.as("a") val m = RFileMeta.as("m") @@ -206,7 +202,6 @@ object RAttachment { } def findByItemWithMeta(id: Ident): ConnectionIO[Vector[(RAttachment, FileMeta)]] = { - import bitpeace.sql._ val a = RAttachment.as("a") val m = RFileMeta.as("m") @@ -301,4 +296,19 @@ object RAttachment { coll.map(cid => i.cid === cid) ).build.query[RAttachment].streamWithChunkSize(chunkSize) } + + def filterAttachments( + attachments: NonEmptyList[Ident], + coll: Ident + ): ConnectionIO[Vector[Ident]] = { + val a = RAttachment.as("a") + val i = RItem.as("i") + + Select( + select(a.id), + from(a) + .innerJoin(i, i.id === a.itemId), + i.cid === coll && a.id.in(attachments) + ).build.query[Ident].to[Vector] + } } From 6149a2ab89d378a178bb050acf670841a5fbfd1d Mon Sep 17 00:00:00 2001 From: Stefan Scheidewig Date: Thu, 15 Apr 2021 18:34:54 +0200 Subject: [PATCH 04/10] Restored unused imports to make it compile again --- .../main/scala/docspell/store/records/RAttachment.scala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala index ec892df7..bbb7a00d 100644 --- a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala +++ b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala @@ -1,14 +1,16 @@ package docspell.store.records -import bitpeace.FileMeta import cats.data.NonEmptyList import cats.implicits._ +import fs2.Stream + import docspell.common._ import docspell.store.qb.DSL._ import docspell.store.qb._ + +import bitpeace.FileMeta import doobie._ import doobie.implicits._ -import fs2.Stream case class RAttachment( id: Ident, @@ -96,6 +98,7 @@ object RAttachment { run(select(T.all), from(T), T.id === attachId).query[RAttachment].option def findMeta(attachId: Ident): ConnectionIO[Option[FileMeta]] = { + import bitpeace.sql._ val m = RFileMeta.as("m") val a = RAttachment.as("a") @@ -188,6 +191,7 @@ object RAttachment { id: Ident, coll: Ident ): ConnectionIO[Vector[(RAttachment, FileMeta)]] = { + import bitpeace.sql._ val a = RAttachment.as("a") val m = RFileMeta.as("m") @@ -202,6 +206,7 @@ object RAttachment { } def findByItemWithMeta(id: Ident): ConnectionIO[Vector[(RAttachment, FileMeta)]] = { + import bitpeace.sql._ val a = RAttachment.as("a") val m = RFileMeta.as("m") From 558197e415b58a9fe2cc6c24af44f9c53bd8d4ac Mon Sep 17 00:00:00 2001 From: Stefan Scheidewig Date: Thu, 15 Apr 2021 20:49:34 +0200 Subject: [PATCH 05/10] Fixed the imports --- modules/backend/src/main/scala/docspell/backend/ops/OItem.scala | 2 ++ .../main/scala/docspell/restserver/conv/MultiIdSupport.scala | 2 ++ .../docspell/restserver/routes/AttachmentMultiRoutes.scala | 2 ++ .../main/scala/docspell/restserver/routes/ItemMultiRoutes.scala | 2 ++ 4 files changed, 8 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 5d8a6143..0cbf8b44 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala @@ -3,6 +3,7 @@ package docspell.backend.ops import cats.data.{NonEmptyList, OptionT} import cats.effect.{Effect, Resource} import cats.implicits._ + import docspell.backend.JobFactory import docspell.common._ import docspell.ftsclient.FtsClient @@ -10,6 +11,7 @@ import docspell.store.queries.{QAttachment, QItem, QMoveAttachment} import docspell.store.queue.JobQueue import docspell.store.records._ import docspell.store.{AddResult, Store, UpdateResult} + import doobie.implicits._ import org.log4s.getLogger diff --git a/modules/restserver/src/main/scala/docspell/restserver/conv/MultiIdSupport.scala b/modules/restserver/src/main/scala/docspell/restserver/conv/MultiIdSupport.scala index 4dbc895b..bb020c25 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/conv/MultiIdSupport.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/conv/MultiIdSupport.scala @@ -3,7 +3,9 @@ package docspell.restserver.conv import cats.data.NonEmptyList import cats.implicits._ import cats.{ApplicativeError, MonadError} + import docspell.common.Ident + import io.circe.DecodingFailure trait MultiIdSupport { diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala index fede7636..e2f125fd 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala @@ -2,10 +2,12 @@ package docspell.restserver.routes import cats.effect.Effect import cats.implicits._ + import docspell.backend.BackendApp import docspell.backend.auth.AuthToken import docspell.restapi.model._ import docspell.restserver.conv.MultiIdSupport + import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ 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 270a44a9..42a80d9d 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala @@ -2,12 +2,14 @@ package docspell.restserver.routes import cats.effect._ import cats.implicits._ + import docspell.backend.BackendApp import docspell.backend.auth.AuthToken import docspell.backend.ops.OCustomFields.{RemoveValue, SetValue} import docspell.common.ItemState import docspell.restapi.model._ import docspell.restserver.conv.{Conversions, MultiIdSupport} + import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ From a9c02e9e88db46ee01d90a565a3b14f510252609 Mon Sep 17 00:00:00 2001 From: Stefan Scheidewig Date: Sat, 17 Apr 2021 13:04:30 +0200 Subject: [PATCH 06/10] Prepared multiselect view mode for attachment list --- .../src/main/elm/Comp/ItemDetail/Model.elm | 23 ++++++++ .../elm/Comp/ItemDetail/SingleAttachment.elm | 55 ++++++++++++++++--- .../src/main/elm/Comp/ItemDetail/Update.elm | 26 +++++++-- .../Comp/ItemDetail/SingleAttachment.elm | 6 ++ 4 files changed, 98 insertions(+), 12 deletions(-) diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm index 02e2e2d8..7a6db775 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm @@ -5,12 +5,14 @@ module Comp.ItemDetail.Model exposing , NotesField(..) , SaveNameState(..) , UpdateResult + , ViewMode(..) , emptyModel , isEditNotes , personMatchesOrg , resultModel , resultModelCmd , resultModelCmdSub + , initSelectViewModel ) import Api.Model.BasicResult exposing (BasicResult) @@ -105,8 +107,21 @@ type alias Model = , allPersons : Dict String Person , attachmentDropdownOpen : Bool , editMenuTabsOpen : Set String + , viewMode : ViewMode } +type ViewMode + = SimpleView + | SelectView SelectViewModel + +type alias SelectViewModel = + { ids : Set String + , action : SelectActionMode + } + +type SelectActionMode + = NoneAction + | DeleteSelected type NotesField = ViewNotes @@ -185,6 +200,13 @@ emptyModel = , allPersons = Dict.empty , attachmentDropdownOpen = False , editMenuTabsOpen = Set.empty + , viewMode = SimpleView + } + +initSelectViewModel : SelectViewModel +initSelectViewModel = + { ids = Set.empty + , action = NoneAction } @@ -283,6 +305,7 @@ type Msg | ReprocessFileResp (Result Http.Error BasicResult) | RequestReprocessItem | ReprocessItemConfirmed + | ToggleSelectView type SaveNameState diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm index 75c51dfc..fe49677c 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm @@ -4,13 +4,7 @@ import Api import Api.Model.Attachment exposing (Attachment) import Comp.AttachmentMeta import Comp.ConfirmModal -import Comp.ItemDetail.Model - exposing - ( Model - , Msg(..) - , NotesField(..) - , SaveNameState(..) - ) +import Comp.ItemDetail.Model exposing (Model, Msg(..), NotesField(..), SaveNameState(..), ViewMode(..)) import Comp.MenuBar as MB import Data.UiSettings exposing (UiSettings) import Dict @@ -19,7 +13,6 @@ import Html.Attributes exposing (..) import Html.Events exposing (onClick, onInput) import Html5.DragDrop as DD import Messages.Comp.ItemDetail.SingleAttachment exposing (Texts) -import Page exposing (Page(..)) import Styles as S import Util.Maybe import Util.Size @@ -87,6 +80,7 @@ view texts settings model pos attach = - toggle thumbs - name + size - eye icon to open it + - toggle multi select - menu - rename - meta data @@ -112,6 +106,26 @@ attachHeader texts settings model _ attach = multiAttach = List.length model.item.attachments > 1 + selectPossible = + multiAttach && model.attachMenuOpen + + selectView = + case model.viewMode of + SelectView _ -> + True + _ -> + False + + selectToggleText = + case model.viewMode of + SelectView _ -> + texts.exitSelectMode + _ -> + texts.selectModeTitle + + noAttachmentsSelected = + List.isEmpty model.item.attachments + attachSelectToggle mobile = a [ href "#" @@ -143,15 +157,40 @@ attachHeader texts settings model _ attach = , title texts.openFileInNewTab , class S.secondaryBasicButton , class "ml-2" + , classList [ ( "hidden", selectView ) ] ] [ i [ class "fa fa-eye font-thin" ] [] ] + , a + [ classList [ (S.secondaryBasicButton ++ " text-sm", True) + , ( "bg-gray-200 dark:bg-bluegray-600", selectView ) + , ( "hidden", not selectPossible ) + , ( "ml-2", True ) + ] + , href "#" + , title selectToggleText + , onClick ToggleSelectView + ] + [ i [ class "fa fa-tasks" ] [] + ] + , a + [ classList [ ( S.deleteButton, True ) + , ( "disabled", noAttachmentsSelected ) + , ( "hidden", not selectPossible || not selectView ) + , ( "ml-2", True ) + ] + , href "#" + , title texts.deleteAttachments + ] + [ i [ class "fa fa-trash" ] [] + ] , MB.viewItem <| MB.Dropdown { linkIcon = "fa fa-bars" , linkClass = [ ( "ml-2", True ) , ( S.secondaryBasicButton, True ) + , ( "hidden", selectView ) ] , toggleMenu = ToggleAttachmentDropdown , menuOpen = model.attachmentDropdownOpen diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index c8ebf07a..544ed354 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -23,7 +23,6 @@ import Comp.DetailEdit import Comp.Dropdown exposing (isDropdownChangeMsg) import Comp.Dropzone import Comp.EquipmentForm -import Comp.ItemDetail.EditForm import Comp.ItemDetail.FieldTabState as FTabState import Comp.ItemDetail.Model exposing @@ -54,11 +53,10 @@ import Data.PersonUse import Data.UiSettings exposing (UiSettings) import DatePicker import Dict -import Html exposing (..) -import Html.Attributes exposing (..) import Html5.DragDrop as DD import Http import Page exposing (Page(..)) +import Comp.ItemDetail.Model exposing (ViewMode(..), initSelectViewModel) import Set exposing (Set) import Throttle import Time @@ -1360,7 +1358,10 @@ update key flags inav settings msg model = withSub ( model_, Cmd.none ) ToggleAttachMenu -> - resultModel { model | attachMenuOpen = not model.attachMenuOpen } + resultModel { model + | attachMenuOpen = not model.attachMenuOpen + , viewMode = SimpleView + } UiSettingsUpdated -> let @@ -1571,6 +1572,23 @@ update key flags inav settings msg model = in resultModelCmd ( { model | itemModal = Nothing }, cmd ) + ToggleSelectView -> + let + ( nextView, cmd ) = + case model.viewMode of + SimpleView -> + ( SelectView initSelectViewModel, Cmd.none ) + + SelectView _ -> + ( SimpleView, Cmd.none ) + in + withSub + ( { model + | viewMode = nextView + } + , cmd + ) + --- Helper diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm index 4acdef22..ec52d724 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm @@ -15,6 +15,9 @@ type alias Texts = , viewExtractedData : String , reprocessFile : String , deleteThisFile : String + , selectModeTitle : String + , exitSelectMode : String + , deleteAttachments: String } @@ -31,4 +34,7 @@ gb = , viewExtractedData = "View extracted data" , reprocessFile = "Re-process this file" , deleteThisFile = "Delete this file" + , selectModeTitle = "Select Mode" + , exitSelectMode = "Exit Select Mode" + , deleteAttachments = "Delete attachments" } From 1db5eaf5ee710b01f7f712c4cd41b222a909119d Mon Sep 17 00:00:00 2001 From: Stefan Scheidewig Date: Sat, 17 Apr 2021 16:43:24 +0200 Subject: [PATCH 07/10] Attachments selectable --- .../src/main/elm/Comp/ItemDetail/Model.elm | 3 ++ .../elm/Comp/ItemDetail/SingleAttachment.elm | 33 ++++++++++++++++--- .../src/main/elm/Comp/ItemDetail/Update.elm | 14 ++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm index 7a6db775..30085f5c 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm @@ -108,6 +108,7 @@ type alias Model = , attachmentDropdownOpen : Bool , editMenuTabsOpen : Set String , viewMode : ViewMode + , selectedAttachments: Set String } type ViewMode @@ -201,6 +202,7 @@ emptyModel = , attachmentDropdownOpen = False , editMenuTabsOpen = Set.empty , viewMode = SimpleView + , selectedAttachments = Set.empty } initSelectViewModel : SelectViewModel @@ -216,6 +218,7 @@ type Msg | Init | SetItem ItemDetail | SetActiveAttachment Int + | ToggleAttachment String | TagDropdownMsg (Comp.Dropdown.Msg Tag) | DirDropdownMsg (Comp.Dropdown.Msg Direction) | OrgDropdownMsg (Comp.Dropdown.Msg IdName) diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm index fe49677c..2a91ecd9 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm @@ -13,6 +13,7 @@ import Html.Attributes exposing (..) import Html.Events exposing (onClick, onInput) import Html5.DragDrop as DD import Messages.Comp.ItemDetail.SingleAttachment exposing (Texts) +import Set import Styles as S import Util.Maybe import Util.Size @@ -349,8 +350,30 @@ menuItem texts model pos attach = [ ( "bg-gray-300 dark:bg-bluegray-700 current-drop-target", enable ) ] - active = - model.visibleAttach == pos + iconClass = + case model.viewMode of + SelectView _ -> + if Set.member attach.id model.selectedAttachments then + "fa fa-check-circle ml-1" + else + "fa fa-circle ml-1" + _ -> + "fa fa-check-circle ml-1" + + visible = + case model.viewMode of + SelectView _ -> + True + _ -> + model.visibleAttach == pos + + msg = + case model.viewMode of + SelectView _ -> + (ToggleAttachment attach.id) + _ -> + (SetActiveAttachment pos) + in a ([ classList <| @@ -361,18 +384,18 @@ menuItem texts model pos attach = , class "flex flex-col relative border rounded px-1 py-1 mr-2" , class " hover:shadow dark:hover:border-bluegray-500" , href "#" - , onClick (SetActiveAttachment pos) + , onClick msg ] ++ DD.draggable AttachDDMsg attach.id ++ DD.droppable AttachDDMsg attach.id ) [ div [ classList - [ ( "hidden", not active ) + [ ( "hidden", not visible ) ] , class "absolute right-1 top-1 text-blue-400 dark:text-lightblue-400 text-xl" ] - [ i [ class "fa fa-check-circle ml-1" ] [] + [ i [ class iconClass ] [] ] , div [ class "flex-grow" ] [ img diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index 544ed354..c5bf374a 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -280,6 +280,18 @@ update key flags inav settings msg model = , attachRename = Nothing } + ToggleAttachment id -> + if Set.member id model.selectedAttachments then + resultModel + { model + | selectedAttachments = Set.remove id model.selectedAttachments + } + else + resultModel + { model + | selectedAttachments = Set.insert id model.selectedAttachments + } + ToggleMenu -> resultModel { model | menuOpen = not model.menuOpen } @@ -1361,6 +1373,7 @@ update key flags inav settings msg model = resultModel { model | attachMenuOpen = not model.attachMenuOpen , viewMode = SimpleView + , selectedAttachments = Set.empty } UiSettingsUpdated -> @@ -1585,6 +1598,7 @@ update key flags inav settings msg model = withSub ( { model | viewMode = nextView + , selectedAttachments = Set.empty } , cmd ) From decae84aec71458a429c8622483fdd8a9f188a67 Mon Sep 17 00:00:00 2001 From: Stefan Scheidewig Date: Sun, 18 Apr 2021 18:38:38 +0200 Subject: [PATCH 08/10] Using SelectViewModel, bulk deletion of item attachments --- modules/webapp/src/main/elm/Api.elm | 15 ++++ .../src/main/elm/Comp/ItemDetail/Model.elm | 7 +- .../elm/Comp/ItemDetail/SingleAttachment.elm | 5 +- .../src/main/elm/Comp/ItemDetail/Update.elm | 82 +++++++++++++------ 4 files changed, 79 insertions(+), 30 deletions(-) diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 968f8041..2f2ea00a 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -19,6 +19,7 @@ module Api exposing , createScanMailbox , deleteAllItems , deleteAttachment + , deleteAttachments , deleteCustomField , deleteCustomValue , deleteCustomValueMultiple @@ -609,6 +610,20 @@ deleteAttachment flags attachId receive = , expect = Http.expectJson receive Api.Model.BasicResult.decoder } +--- Delete Attachments + +deleteAttachments : + Flags + -> Set String + -> (Result Http.Error BasicResult -> msg) + -> Cmd msg +deleteAttachments flags attachIds receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/attachments/delete" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.IdList.encode (Set.toList attachIds |> IdList)) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } --- Attachment Metadata diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm index 30085f5c..af0e5f86 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm @@ -4,15 +4,16 @@ module Comp.ItemDetail.Model exposing , Msg(..) , NotesField(..) , SaveNameState(..) + , SelectActionMode(..) , UpdateResult , ViewMode(..) , emptyModel + , initSelectViewModel , isEditNotes , personMatchesOrg , resultModel , resultModelCmd , resultModelCmdSub - , initSelectViewModel ) import Api.Model.BasicResult exposing (BasicResult) @@ -108,7 +109,6 @@ type alias Model = , attachmentDropdownOpen : Bool , editMenuTabsOpen : Set String , viewMode : ViewMode - , selectedAttachments: Set String } type ViewMode @@ -202,7 +202,6 @@ emptyModel = , attachmentDropdownOpen = False , editMenuTabsOpen = Set.empty , viewMode = SimpleView - , selectedAttachments = Set.empty } initSelectViewModel : SelectViewModel @@ -264,6 +263,8 @@ type Msg | TogglePdfNativeView Bool | RequestDeleteAttachment String | DeleteAttachConfirmed String + | RequestDeleteSelected + | DeleteSelectedConfirmed | AttachModalCancelled | DeleteAttachResp (Result Http.Error BasicResult) | AddFilesToggle diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm index 2a91ecd9..abec8078 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm @@ -182,6 +182,7 @@ attachHeader texts settings model _ attach = ] , href "#" , title texts.deleteAttachments + , onClick RequestDeleteSelected ] [ i [ class "fa fa-trash" ] [] ] @@ -352,8 +353,8 @@ menuItem texts model pos attach = iconClass = case model.viewMode of - SelectView _ -> - if Set.member attach.id model.selectedAttachments then + SelectView svm -> + if Set.member attach.id svm.ids then "fa fa-check-circle ml-1" else "fa fa-circle ml-1" diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index c5bf374a..95c7513f 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -24,19 +24,7 @@ import Comp.Dropdown exposing (isDropdownChangeMsg) import Comp.Dropzone import Comp.EquipmentForm import Comp.ItemDetail.FieldTabState as FTabState -import Comp.ItemDetail.Model - exposing - ( AttachmentRename - , Model - , Msg(..) - , NotesField(..) - , SaveNameState(..) - , UpdateResult - , isEditNotes - , resultModel - , resultModelCmd - , resultModelCmdSub - ) +import Comp.ItemDetail.Model exposing (AttachmentRename, Model, Msg(..), NotesField(..), SaveNameState(..), SelectActionMode(..), UpdateResult, isEditNotes, resultModel, resultModelCmd, resultModelCmdSub) import Comp.ItemMail import Comp.KeyInput import Comp.LinkTarget @@ -281,16 +269,19 @@ update key flags inav settings msg model = } ToggleAttachment id -> - if Set.member id model.selectedAttachments then - resultModel - { model - | selectedAttachments = Set.remove id model.selectedAttachments - } - else - resultModel - { model - | selectedAttachments = Set.insert id model.selectedAttachments - } + case model.viewMode of + SelectView svm -> + let + svm_ = + if Set.member id svm.ids then + { svm | ids = Set.remove id svm.ids } + else + { svm | ids = Set.insert id svm.ids } + in + resultModel + { model | viewMode = SelectView svm_ } + _ -> + resultModel model ToggleMenu -> resultModel @@ -948,6 +939,49 @@ update key flags inav settings msg model = in resultModel model_ + RequestDeleteSelected -> + case model.viewMode of + SelectView svm -> + if Set.isEmpty svm.ids then + resultModel model + + else + let + confirmModal = + Comp.ConfirmModal.defaultSettings + DeleteSelectedConfirmed + AttachModalCancelled + "Ok" + "Cancel" + "Really delete these files?" + + model_ = + { model + | viewMode = + SelectView + { svm + | action = DeleteSelected + } + , attachModal = Just confirmModal + } + in + resultModel model_ + + _ -> + resultModel model + + DeleteSelectedConfirmed -> + case model.viewMode of + SelectView svm -> + let + cmd = + Api.deleteAttachments flags svm.ids DeleteAttachResp + in + resultModelCmd ( { model | attachModal = Nothing, viewMode = SimpleView }, cmd ) + + _ -> + resultModel model + AddFilesToggle -> resultModel { model @@ -1373,7 +1407,6 @@ update key flags inav settings msg model = resultModel { model | attachMenuOpen = not model.attachMenuOpen , viewMode = SimpleView - , selectedAttachments = Set.empty } UiSettingsUpdated -> @@ -1598,7 +1631,6 @@ update key flags inav settings msg model = withSub ( { model | viewMode = nextView - , selectedAttachments = Set.empty } , cmd ) From dd743cf2730dbc5a10bf4422ee076ca3909e5b3b Mon Sep 17 00:00:00 2001 From: Stefan Scheidewig Date: Sun, 18 Apr 2021 22:27:50 +0200 Subject: [PATCH 09/10] Reformatted changed source files with elm-format --- modules/webapp/src/main/elm/Api.elm | 4 +++ .../src/main/elm/Comp/ItemDetail/Model.elm | 5 +++ .../elm/Comp/ItemDetail/SingleAttachment.elm | 33 +++++++++++-------- .../src/main/elm/Comp/ItemDetail/Update.elm | 14 ++++---- .../Comp/ItemDetail/SingleAttachment.elm | 2 +- 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 2f2ea00a..2ecf5171 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -610,8 +610,11 @@ deleteAttachment flags attachId receive = , expect = Http.expectJson receive Api.Model.BasicResult.decoder } + + --- Delete Attachments + deleteAttachments : Flags -> Set String @@ -626,6 +629,7 @@ deleteAttachments flags attachIds receive = } + --- Attachment Metadata diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm index af0e5f86..04bb61ae 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm @@ -111,19 +111,23 @@ type alias Model = , viewMode : ViewMode } + type ViewMode = SimpleView | SelectView SelectViewModel + type alias SelectViewModel = { ids : Set String , action : SelectActionMode } + type SelectActionMode = NoneAction | DeleteSelected + type NotesField = ViewNotes | EditNotes Comp.MarkdownInput.Model @@ -204,6 +208,7 @@ emptyModel = , viewMode = SimpleView } + initSelectViewModel : SelectViewModel initSelectViewModel = { ids = Set.empty diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm index abec8078..a1e87c4d 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm @@ -114,6 +114,7 @@ attachHeader texts settings model _ attach = case model.viewMode of SelectView _ -> True + _ -> False @@ -121,6 +122,7 @@ attachHeader texts settings model _ attach = case model.viewMode of SelectView _ -> texts.exitSelectMode + _ -> texts.selectModeTitle @@ -163,11 +165,12 @@ attachHeader texts settings model _ attach = [ i [ class "fa fa-eye font-thin" ] [] ] , a - [ classList [ (S.secondaryBasicButton ++ " text-sm", True) - , ( "bg-gray-200 dark:bg-bluegray-600", selectView ) - , ( "hidden", not selectPossible ) - , ( "ml-2", True ) - ] + [ classList + [ ( S.secondaryBasicButton ++ " text-sm", True ) + , ( "bg-gray-200 dark:bg-bluegray-600", selectView ) + , ( "hidden", not selectPossible ) + , ( "ml-2", True ) + ] , href "#" , title selectToggleText , onClick ToggleSelectView @@ -175,11 +178,12 @@ attachHeader texts settings model _ attach = [ i [ class "fa fa-tasks" ] [] ] , a - [ classList [ ( S.deleteButton, True ) - , ( "disabled", noAttachmentsSelected ) - , ( "hidden", not selectPossible || not selectView ) - , ( "ml-2", True ) - ] + [ classList + [ ( S.deleteButton, True ) + , ( "disabled", noAttachmentsSelected ) + , ( "hidden", not selectPossible || not selectView ) + , ( "ml-2", True ) + ] , href "#" , title texts.deleteAttachments , onClick RequestDeleteSelected @@ -356,8 +360,10 @@ menuItem texts model pos attach = SelectView svm -> if Set.member attach.id svm.ids then "fa fa-check-circle ml-1" + else "fa fa-circle ml-1" + _ -> "fa fa-check-circle ml-1" @@ -365,16 +371,17 @@ menuItem texts model pos attach = case model.viewMode of SelectView _ -> True + _ -> model.visibleAttach == pos msg = case model.viewMode of SelectView _ -> - (ToggleAttachment attach.id) - _ -> - (SetActiveAttachment pos) + ToggleAttachment attach.id + _ -> + SetActiveAttachment pos in a ([ classList <| diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index 95c7513f..65e56c05 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -24,7 +24,7 @@ import Comp.Dropdown exposing (isDropdownChangeMsg) import Comp.Dropzone import Comp.EquipmentForm import Comp.ItemDetail.FieldTabState as FTabState -import Comp.ItemDetail.Model exposing (AttachmentRename, Model, Msg(..), NotesField(..), SaveNameState(..), SelectActionMode(..), UpdateResult, isEditNotes, resultModel, resultModelCmd, resultModelCmdSub) +import Comp.ItemDetail.Model exposing (AttachmentRename, Model, Msg(..), NotesField(..), SaveNameState(..), SelectActionMode(..), UpdateResult, ViewMode(..), initSelectViewModel, isEditNotes, resultModel, resultModelCmd, resultModelCmdSub) import Comp.ItemMail import Comp.KeyInput import Comp.LinkTarget @@ -44,7 +44,6 @@ import Dict import Html5.DragDrop as DD import Http import Page exposing (Page(..)) -import Comp.ItemDetail.Model exposing (ViewMode(..), initSelectViewModel) import Set exposing (Set) import Throttle import Time @@ -275,11 +274,13 @@ update key flags inav settings msg model = svm_ = if Set.member id svm.ids then { svm | ids = Set.remove id svm.ids } + else { svm | ids = Set.insert id svm.ids } in resultModel { model | viewMode = SelectView svm_ } + _ -> resultModel model @@ -1404,10 +1405,11 @@ update key flags inav settings msg model = withSub ( model_, Cmd.none ) ToggleAttachMenu -> - resultModel { model - | attachMenuOpen = not model.attachMenuOpen - , viewMode = SimpleView - } + resultModel + { model + | attachMenuOpen = not model.attachMenuOpen + , viewMode = SimpleView + } UiSettingsUpdated -> let diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm index ec52d724..d7e12583 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ItemDetail/SingleAttachment.elm @@ -17,7 +17,7 @@ type alias Texts = , deleteThisFile : String , selectModeTitle : String , exitSelectMode : String - , deleteAttachments: String + , deleteAttachments : String } From 5faf0e5a0eec09a2f4021726f68d23202e521ccf Mon Sep 17 00:00:00 2001 From: Stefan Scheidewig Date: Sun, 18 Apr 2021 22:31:17 +0200 Subject: [PATCH 10/10] Made viewMode pattern matches exhaustive --- .../src/main/elm/Comp/ItemDetail/SingleAttachment.elm | 10 +++++----- modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm index a1e87c4d..046a36f8 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm @@ -115,7 +115,7 @@ attachHeader texts settings model _ attach = SelectView _ -> True - _ -> + SimpleView -> False selectToggleText = @@ -123,7 +123,7 @@ attachHeader texts settings model _ attach = SelectView _ -> texts.exitSelectMode - _ -> + SimpleView -> texts.selectModeTitle noAttachmentsSelected = @@ -364,7 +364,7 @@ menuItem texts model pos attach = else "fa fa-circle ml-1" - _ -> + SimpleView -> "fa fa-check-circle ml-1" visible = @@ -372,7 +372,7 @@ menuItem texts model pos attach = SelectView _ -> True - _ -> + SimpleView -> model.visibleAttach == pos msg = @@ -380,7 +380,7 @@ menuItem texts model pos attach = SelectView _ -> ToggleAttachment attach.id - _ -> + SimpleView -> SetActiveAttachment pos in a diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index 65e56c05..435145b4 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -281,7 +281,7 @@ update key flags inav settings msg model = resultModel { model | viewMode = SelectView svm_ } - _ -> + SimpleView -> resultModel model ToggleMenu -> @@ -968,7 +968,7 @@ update key flags inav settings msg model = in resultModel model_ - _ -> + SimpleView -> resultModel model DeleteSelectedConfirmed -> @@ -980,7 +980,7 @@ update key flags inav settings msg model = in resultModelCmd ( { model | attachModal = Nothing, viewMode = SimpleView }, cmd ) - _ -> + SimpleView -> resultModel model AddFilesToggle ->