mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-10-31 17:50:11 +00:00 
			
		
		
		
	| @@ -1,5 +1,6 @@ | ||||
| package docspell.backend.ops | ||||
|  | ||||
| import cats.data.NonEmptyList | ||||
| import cats.data.OptionT | ||||
| import cats.effect.{Effect, Resource} | ||||
| import cats.implicits._ | ||||
| @@ -13,62 +14,124 @@ import docspell.store.queue.JobQueue | ||||
| import docspell.store.records._ | ||||
| import docspell.store.{AddResult, Store} | ||||
|  | ||||
| import doobie._ | ||||
| import doobie.implicits._ | ||||
| import org.log4s.getLogger | ||||
|  | ||||
| trait OItem[F[_]] { | ||||
|  | ||||
|   /** Sets the given tags (removing all existing ones). */ | ||||
|   def setTags(item: Ident, tagIds: List[Ident], collective: Ident): F[AddResult] | ||||
|   def setTags(item: Ident, tagIds: List[Ident], collective: Ident): F[UpdateResult] | ||||
|  | ||||
|   /** Sets tags for multiple items. The tags of the items will be | ||||
|     * replaced with the given ones. Same as `setTags` but for multiple | ||||
|     * items. | ||||
|     */ | ||||
|   def setTagsMultipleItems( | ||||
|       items: NonEmptyList[Ident], | ||||
|       tags: List[Ident], | ||||
|       collective: Ident | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   /** Create a new tag and add it to the item. */ | ||||
|   def addNewTag(item: Ident, tag: RTag): F[AddResult] | ||||
|  | ||||
|   /** Apply all tags to the given item. Tags must exist, but can be IDs or names. */ | ||||
|   /** Apply all tags to the given item. Tags must exist, but can be IDs | ||||
|     * or names. Existing tags on the item are left unchanged. | ||||
|     */ | ||||
|   def linkTags(item: Ident, tags: List[String], collective: Ident): F[UpdateResult] | ||||
|  | ||||
|   def linkTagsMultipleItems( | ||||
|       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] | ||||
|  | ||||
|   def setDirection(item: Ident, direction: Direction, collective: Ident): F[AddResult] | ||||
|   def setDirection( | ||||
|       item: NonEmptyList[Ident], | ||||
|       direction: Direction, | ||||
|       collective: Ident | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   def setFolder(item: Ident, folder: Option[Ident], collective: Ident): F[AddResult] | ||||
|   def setFolder(item: Ident, folder: Option[Ident], collective: Ident): F[UpdateResult] | ||||
|  | ||||
|   def setCorrOrg(item: Ident, org: Option[Ident], collective: Ident): F[AddResult] | ||||
|   def setFolderMultiple( | ||||
|       items: NonEmptyList[Ident], | ||||
|       folder: Option[Ident], | ||||
|       collective: Ident | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   def setCorrOrg( | ||||
|       items: NonEmptyList[Ident], | ||||
|       org: Option[Ident], | ||||
|       collective: Ident | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   def addCorrOrg(item: Ident, org: OOrganization.OrgAndContacts): F[AddResult] | ||||
|  | ||||
|   def setCorrPerson(item: Ident, person: Option[Ident], collective: Ident): F[AddResult] | ||||
|   def setCorrPerson( | ||||
|       items: NonEmptyList[Ident], | ||||
|       person: Option[Ident], | ||||
|       collective: Ident | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   def addCorrPerson(item: Ident, person: OOrganization.PersonAndContacts): F[AddResult] | ||||
|  | ||||
|   def setConcPerson(item: Ident, person: Option[Ident], collective: Ident): F[AddResult] | ||||
|   def setConcPerson( | ||||
|       items: NonEmptyList[Ident], | ||||
|       person: Option[Ident], | ||||
|       collective: Ident | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   def addConcPerson(item: Ident, person: OOrganization.PersonAndContacts): F[AddResult] | ||||
|  | ||||
|   def setConcEquip(item: Ident, equip: Option[Ident], collective: Ident): F[AddResult] | ||||
|   def setConcEquip( | ||||
|       items: NonEmptyList[Ident], | ||||
|       equip: Option[Ident], | ||||
|       collective: Ident | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   def addConcEquip(item: Ident, equip: REquipment): F[AddResult] | ||||
|  | ||||
|   def setNotes(item: Ident, notes: Option[String], collective: Ident): F[AddResult] | ||||
|   def setNotes(item: Ident, notes: Option[String], collective: Ident): F[UpdateResult] | ||||
|  | ||||
|   def setName(item: Ident, name: String, collective: Ident): F[AddResult] | ||||
|   def setName(item: Ident, name: String, collective: Ident): F[UpdateResult] | ||||
|  | ||||
|   def setState(item: Ident, state: ItemState, collective: Ident): F[AddResult] | ||||
|   def setNameMultiple( | ||||
|       items: NonEmptyList[Ident], | ||||
|       name: String, | ||||
|       collective: Ident | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   def setItemDate(item: Ident, date: Option[Timestamp], collective: Ident): F[AddResult] | ||||
|   def setState(item: Ident, state: ItemState, collective: Ident): F[AddResult] = | ||||
|     setStates(NonEmptyList.of(item), state, collective) | ||||
|  | ||||
|   def setItemDueDate( | ||||
|       item: Ident, | ||||
|       date: Option[Timestamp], | ||||
|   def setStates( | ||||
|       item: NonEmptyList[Ident], | ||||
|       state: ItemState, | ||||
|       collective: Ident | ||||
|   ): F[AddResult] | ||||
|  | ||||
|   def setItemDate( | ||||
|       item: NonEmptyList[Ident], | ||||
|       date: Option[Timestamp], | ||||
|       collective: Ident | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   def setItemDueDate( | ||||
|       item: NonEmptyList[Ident], | ||||
|       date: Option[Timestamp], | ||||
|       collective: Ident | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   def getProposals(item: Ident, collective: Ident): F[MetaProposalList] | ||||
|  | ||||
|   def deleteItem(itemId: Ident, collective: Ident): F[Int] | ||||
|  | ||||
|   def deleteItemMultiple(items: NonEmptyList[Ident], collective: Ident): F[Int] | ||||
|  | ||||
|   def deleteAttachment(id: Ident, collective: Ident): F[Int] | ||||
|  | ||||
|   def moveAttachmentBefore(itemId: Ident, source: Ident, target: Ident): F[AddResult] | ||||
| @@ -77,7 +140,7 @@ trait OItem[F[_]] { | ||||
|       attachId: Ident, | ||||
|       name: Option[String], | ||||
|       collective: Ident | ||||
|   ): F[AddResult] | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   /** Submits the item for re-processing. The list of attachment ids can | ||||
|     * be used to only re-process a subset of the item's attachments. | ||||
| @@ -91,6 +154,12 @@ trait OItem[F[_]] { | ||||
|       notifyJoex: Boolean | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   def reprocessAll( | ||||
|       items: NonEmptyList[Ident], | ||||
|       account: AccountId, | ||||
|       notifyJoex: Boolean | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   /** Submits a task that finds all non-converted pdfs and triggers | ||||
|     * converting them using ocrmypdf. Each file is converted by a | ||||
|     * separate task. | ||||
| @@ -130,21 +199,30 @@ object OItem { | ||||
|             item: Ident, | ||||
|             tags: List[String], | ||||
|             collective: Ident | ||||
|         ): F[UpdateResult] = | ||||
|           linkTagsMultipleItems(NonEmptyList.of(item), tags, collective) | ||||
|  | ||||
|         def linkTagsMultipleItems( | ||||
|             items: NonEmptyList[Ident], | ||||
|             tags: List[String], | ||||
|             collective: Ident | ||||
|         ): F[UpdateResult] = | ||||
|           tags.distinct match { | ||||
|             case Nil => UpdateResult.success.pure[F] | ||||
|             case kws => | ||||
|               val db = | ||||
|             case ws => | ||||
|               store.transact { | ||||
|                 (for { | ||||
|                   _     <- OptionT(RItem.checkByIdAndCollective(item, collective)) | ||||
|                   given <- OptionT.liftF(RTag.findAllByNameOrId(kws, collective)) | ||||
|                   exist <- OptionT.liftF(RTagItem.findAllIn(item, given.map(_.tagId))) | ||||
|                   itemIds <- OptionT | ||||
|                     .liftF(RItem.filterItems(items, collective)) | ||||
|                     .filter(_.nonEmpty) | ||||
|                   given <- OptionT.liftF(RTag.findAllByNameOrId(ws, collective)) | ||||
|                   _ <- OptionT.liftF( | ||||
|                     RTagItem.setAllTags(item, given.map(_.tagId).diff(exist.map(_.tagId))) | ||||
|                     itemIds.traverse(item => | ||||
|                       RTagItem.appendTags(item, given.map(_.tagId).toList) | ||||
|                     ) | ||||
|                   ) | ||||
|                 } yield UpdateResult.success).getOrElse(UpdateResult.notFound) | ||||
|  | ||||
|               store.transact(db) | ||||
|               } | ||||
|           } | ||||
|  | ||||
|         def toggleTags( | ||||
| @@ -169,20 +247,23 @@ object OItem { | ||||
|               store.transact(db) | ||||
|           } | ||||
|  | ||||
|         def setTags(item: Ident, tagIds: List[Ident], collective: Ident): F[AddResult] = { | ||||
|           val db = for { | ||||
|             cid <- RItem.getCollective(item) | ||||
|             nd <- | ||||
|               if (cid.contains(collective)) RTagItem.deleteItemTags(item) | ||||
|               else 0.pure[ConnectionIO] | ||||
|             ni <- | ||||
|               if (tagIds.nonEmpty && cid.contains(collective)) | ||||
|                 RTagItem.insertItemTags(item, tagIds) | ||||
|               else 0.pure[ConnectionIO] | ||||
|           } yield nd + ni | ||||
|         def setTags( | ||||
|             item: Ident, | ||||
|             tagIds: List[Ident], | ||||
|             collective: Ident | ||||
|         ): F[UpdateResult] = | ||||
|           setTagsMultipleItems(NonEmptyList.of(item), tagIds, collective) | ||||
|  | ||||
|           store.transact(db).attempt.map(AddResult.fromUpdate) | ||||
|         } | ||||
|         def setTagsMultipleItems( | ||||
|             items: NonEmptyList[Ident], | ||||
|             tags: List[Ident], | ||||
|             collective: Ident | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult.fromUpdate(store.transact(for { | ||||
|             k   <- RTagItem.deleteItemTags(items, collective) | ||||
|             res <- items.traverse(i => RTagItem.setAllTags(i, tags)) | ||||
|             n = res.fold | ||||
|           } yield k + n)) | ||||
|  | ||||
|         def addNewTag(item: Ident, tag: RTag): F[AddResult] = | ||||
|           (for { | ||||
| @@ -192,7 +273,7 @@ object OItem { | ||||
|             _ <- addres match { | ||||
|               case AddResult.Success => | ||||
|                 OptionT.liftF( | ||||
|                   store.transact(RTagItem.insertItemTags(item, List(tag.tagId))) | ||||
|                   store.transact(RTagItem.setAllTags(item, List(tag.tagId))) | ||||
|                 ) | ||||
|               case AddResult.EntityExists(_) => | ||||
|                 OptionT.pure[F](0) | ||||
| @@ -203,33 +284,59 @@ object OItem { | ||||
|             .getOrElse(AddResult.Failure(new Exception("Collective mismatch"))) | ||||
|  | ||||
|         def setDirection( | ||||
|             item: Ident, | ||||
|             items: NonEmptyList[Ident], | ||||
|             direction: Direction, | ||||
|             collective: Ident | ||||
|         ): F[AddResult] = | ||||
|           store | ||||
|             .transact(RItem.updateDirection(item, collective, direction)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult.fromUpdate( | ||||
|             store | ||||
|               .transact(RItem.updateDirection(items, collective, direction)) | ||||
|           ) | ||||
|  | ||||
|         def setFolder( | ||||
|             item: Ident, | ||||
|             folder: Option[Ident], | ||||
|             collective: Ident | ||||
|         ): F[AddResult] = | ||||
|           store | ||||
|             .transact(RItem.updateFolder(item, collective, folder)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult | ||||
|             .fromUpdate( | ||||
|               store | ||||
|                 .transact(RItem.updateFolder(item, collective, folder)) | ||||
|             ) | ||||
|             .flatTap( | ||||
|               onSuccessIgnoreError(fts.updateFolder(logger, item, collective, folder)) | ||||
|             ) | ||||
|  | ||||
|         def setCorrOrg(item: Ident, org: Option[Ident], collective: Ident): F[AddResult] = | ||||
|           store | ||||
|             .transact(RItem.updateCorrOrg(item, collective, org)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|         def setFolderMultiple( | ||||
|             items: NonEmptyList[Ident], | ||||
|             folder: Option[Ident], | ||||
|             collective: Ident | ||||
|         ): F[UpdateResult] = | ||||
|           for { | ||||
|             results <- items.traverse(i => setFolder(i, folder, collective)) | ||||
|             err <- results.traverse { | ||||
|               case UpdateResult.NotFound => | ||||
|                 logger.info("An item was not found when updating the folder") *> 0.pure[F] | ||||
|               case UpdateResult.Failure(err) => | ||||
|                 logger.error(err)("An item failed to update its folder") *> 1.pure[F] | ||||
|               case UpdateResult.Success => | ||||
|                 0.pure[F] | ||||
|             } | ||||
|             res = | ||||
|               if (results.size == err.fold) | ||||
|                 UpdateResult.failure(new Exception("All items failed to update")) | ||||
|               else UpdateResult.success | ||||
|           } yield res | ||||
|  | ||||
|         def setCorrOrg( | ||||
|             items: NonEmptyList[Ident], | ||||
|             org: Option[Ident], | ||||
|             collective: Ident | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult.fromUpdate( | ||||
|             store | ||||
|               .transact(RItem.updateCorrOrg(items, collective, org)) | ||||
|           ) | ||||
|  | ||||
|         def addCorrOrg(item: Ident, org: OOrganization.OrgAndContacts): F[AddResult] = | ||||
|           (for { | ||||
| @@ -240,7 +347,11 @@ object OItem { | ||||
|               case AddResult.Success => | ||||
|                 OptionT.liftF( | ||||
|                   store.transact( | ||||
|                     RItem.updateCorrOrg(item, org.org.cid, Some(org.org.oid)) | ||||
|                     RItem.updateCorrOrg( | ||||
|                       NonEmptyList.of(item), | ||||
|                       org.org.cid, | ||||
|                       Some(org.org.oid) | ||||
|                     ) | ||||
|                   ) | ||||
|                 ) | ||||
|               case AddResult.EntityExists(_) => | ||||
| @@ -252,14 +363,14 @@ object OItem { | ||||
|             .getOrElse(AddResult.Failure(new Exception("Collective mismatch"))) | ||||
|  | ||||
|         def setCorrPerson( | ||||
|             item: Ident, | ||||
|             items: NonEmptyList[Ident], | ||||
|             person: Option[Ident], | ||||
|             collective: Ident | ||||
|         ): F[AddResult] = | ||||
|           store | ||||
|             .transact(RItem.updateCorrPerson(item, collective, person)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult.fromUpdate( | ||||
|             store | ||||
|               .transact(RItem.updateCorrPerson(items, collective, person)) | ||||
|           ) | ||||
|  | ||||
|         def addCorrPerson( | ||||
|             item: Ident, | ||||
| @@ -274,7 +385,11 @@ object OItem { | ||||
|                 OptionT.liftF( | ||||
|                   store.transact( | ||||
|                     RItem | ||||
|                       .updateCorrPerson(item, person.person.cid, Some(person.person.pid)) | ||||
|                       .updateCorrPerson( | ||||
|                         NonEmptyList.of(item), | ||||
|                         person.person.cid, | ||||
|                         Some(person.person.pid) | ||||
|                       ) | ||||
|                   ) | ||||
|                 ) | ||||
|               case AddResult.EntityExists(_) => | ||||
| @@ -286,14 +401,14 @@ object OItem { | ||||
|             .getOrElse(AddResult.Failure(new Exception("Collective mismatch"))) | ||||
|  | ||||
|         def setConcPerson( | ||||
|             item: Ident, | ||||
|             items: NonEmptyList[Ident], | ||||
|             person: Option[Ident], | ||||
|             collective: Ident | ||||
|         ): F[AddResult] = | ||||
|           store | ||||
|             .transact(RItem.updateConcPerson(item, collective, person)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult.fromUpdate( | ||||
|             store | ||||
|               .transact(RItem.updateConcPerson(items, collective, person)) | ||||
|           ) | ||||
|  | ||||
|         def addConcPerson( | ||||
|             item: Ident, | ||||
| @@ -308,7 +423,11 @@ object OItem { | ||||
|                 OptionT.liftF( | ||||
|                   store.transact( | ||||
|                     RItem | ||||
|                       .updateConcPerson(item, person.person.cid, Some(person.person.pid)) | ||||
|                       .updateConcPerson( | ||||
|                         NonEmptyList.of(item), | ||||
|                         person.person.cid, | ||||
|                         Some(person.person.pid) | ||||
|                       ) | ||||
|                   ) | ||||
|                 ) | ||||
|               case AddResult.EntityExists(_) => | ||||
| @@ -320,14 +439,14 @@ object OItem { | ||||
|             .getOrElse(AddResult.Failure(new Exception("Collective mismatch"))) | ||||
|  | ||||
|         def setConcEquip( | ||||
|             item: Ident, | ||||
|             items: NonEmptyList[Ident], | ||||
|             equip: Option[Ident], | ||||
|             collective: Ident | ||||
|         ): F[AddResult] = | ||||
|           store | ||||
|             .transact(RItem.updateConcEquip(item, collective, equip)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult.fromUpdate( | ||||
|             store | ||||
|               .transact(RItem.updateConcEquip(items, collective, equip)) | ||||
|           ) | ||||
|  | ||||
|         def addConcEquip(item: Ident, equip: REquipment): F[AddResult] = | ||||
|           (for { | ||||
| @@ -338,7 +457,8 @@ object OItem { | ||||
|               case AddResult.Success => | ||||
|                 OptionT.liftF( | ||||
|                   store.transact( | ||||
|                     RItem.updateConcEquip(item, equip.cid, Some(equip.eid)) | ||||
|                     RItem | ||||
|                       .updateConcEquip(NonEmptyList.of(item), equip.cid, Some(equip.eid)) | ||||
|                   ) | ||||
|                 ) | ||||
|               case AddResult.EntityExists(_) => | ||||
| @@ -353,55 +473,89 @@ object OItem { | ||||
|             item: Ident, | ||||
|             notes: Option[String], | ||||
|             collective: Ident | ||||
|         ): F[AddResult] = | ||||
|           store | ||||
|             .transact(RItem.updateNotes(item, collective, notes)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult | ||||
|             .fromUpdate( | ||||
|               store | ||||
|                 .transact(RItem.updateNotes(item, collective, notes)) | ||||
|             ) | ||||
|             .flatTap( | ||||
|               onSuccessIgnoreError(fts.updateItemNotes(logger, item, collective, notes)) | ||||
|             ) | ||||
|  | ||||
|         def setName(item: Ident, name: String, collective: Ident): F[AddResult] = | ||||
|           store | ||||
|             .transact(RItem.updateName(item, collective, name)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|         def setName(item: Ident, name: String, collective: Ident): F[UpdateResult] = | ||||
|           UpdateResult | ||||
|             .fromUpdate( | ||||
|               store | ||||
|                 .transact(RItem.updateName(item, collective, name)) | ||||
|             ) | ||||
|             .flatTap( | ||||
|               onSuccessIgnoreError(fts.updateItemName(logger, item, collective, name)) | ||||
|             ) | ||||
|  | ||||
|         def setState(item: Ident, state: ItemState, collective: Ident): F[AddResult] = | ||||
|         def setNameMultiple( | ||||
|             items: NonEmptyList[Ident], | ||||
|             name: String, | ||||
|             collective: Ident | ||||
|         ): F[UpdateResult] = | ||||
|           for { | ||||
|             results <- items.traverse(i => setName(i, name, collective)) | ||||
|             err <- results.traverse { | ||||
|               case UpdateResult.NotFound => | ||||
|                 logger.info("An item was not found when updating the name") *> 0.pure[F] | ||||
|               case UpdateResult.Failure(err) => | ||||
|                 logger.error(err)("An item failed to update its name") *> 1.pure[F] | ||||
|               case UpdateResult.Success => | ||||
|                 0.pure[F] | ||||
|             } | ||||
|             res = | ||||
|               if (results.size == err.fold) | ||||
|                 UpdateResult.failure(new Exception("All items failed to update")) | ||||
|               else UpdateResult.success | ||||
|           } yield res | ||||
|  | ||||
|         def setStates( | ||||
|             items: NonEmptyList[Ident], | ||||
|             state: ItemState, | ||||
|             collective: Ident | ||||
|         ): F[AddResult] = | ||||
|           store | ||||
|             .transact(RItem.updateStateForCollective(item, state, collective)) | ||||
|             .transact(RItem.updateStateForCollective(items, state, collective)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|  | ||||
|         def setItemDate( | ||||
|             item: Ident, | ||||
|             items: NonEmptyList[Ident], | ||||
|             date: Option[Timestamp], | ||||
|             collective: Ident | ||||
|         ): F[AddResult] = | ||||
|           store | ||||
|             .transact(RItem.updateDate(item, collective, date)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult.fromUpdate( | ||||
|             store | ||||
|               .transact(RItem.updateDate(items, collective, date)) | ||||
|           ) | ||||
|  | ||||
|         def setItemDueDate( | ||||
|             item: Ident, | ||||
|             items: NonEmptyList[Ident], | ||||
|             date: Option[Timestamp], | ||||
|             collective: Ident | ||||
|         ): F[AddResult] = | ||||
|           store | ||||
|             .transact(RItem.updateDueDate(item, collective, date)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult.fromUpdate( | ||||
|             store | ||||
|               .transact(RItem.updateDueDate(items, collective, date)) | ||||
|           ) | ||||
|  | ||||
|         def deleteItem(itemId: Ident, collective: Ident): F[Int] = | ||||
|           QItem | ||||
|             .delete(store)(itemId, collective) | ||||
|             .flatTap(_ => fts.removeItem(logger, itemId)) | ||||
|  | ||||
|         def deleteItemMultiple(items: NonEmptyList[Ident], collective: Ident): F[Int] = | ||||
|           for { | ||||
|             itemIds <- store.transact(RItem.filterItems(items, collective)) | ||||
|             results <- itemIds.traverse(item => deleteItem(item, collective)) | ||||
|             n = results.fold(0)(_ + _) | ||||
|           } yield n | ||||
|  | ||||
|         def getProposals(item: Ident, collective: Ident): F[MetaProposalList] = | ||||
|           store.transact(QAttachment.getMetaProposals(item, collective)) | ||||
|  | ||||
| @@ -414,11 +568,12 @@ object OItem { | ||||
|             attachId: Ident, | ||||
|             name: Option[String], | ||||
|             collective: Ident | ||||
|         ): F[AddResult] = | ||||
|           store | ||||
|             .transact(RAttachment.updateName(attachId, collective, name)) | ||||
|             .attempt | ||||
|             .map(AddResult.fromUpdate) | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult | ||||
|             .fromUpdate( | ||||
|               store | ||||
|                 .transact(RAttachment.updateName(attachId, collective, name)) | ||||
|             ) | ||||
|             .flatTap( | ||||
|               onSuccessIgnoreError( | ||||
|                 OptionT(store.transact(RAttachment.findItemId(attachId))) | ||||
| @@ -447,6 +602,20 @@ object OItem { | ||||
|             _ <- OptionT.liftF(if (notifyJoex) joex.notifyAllNodes else ().pure[F]) | ||||
|           } yield UpdateResult.success).getOrElse(UpdateResult.notFound) | ||||
|  | ||||
|         def reprocessAll( | ||||
|             items: NonEmptyList[Ident], | ||||
|             account: AccountId, | ||||
|             notifyJoex: Boolean | ||||
|         ): F[UpdateResult] = | ||||
|           UpdateResult.fromUpdate(for { | ||||
|             items <- store.transact(RItem.filterItems(items, account.collective)) | ||||
|             jobs <- items | ||||
|               .map(item => ReProcessItemArgs(item, Nil)) | ||||
|               .traverse(arg => JobFactory.reprocessItem[F](arg, account, Priority.Low)) | ||||
|             _ <- queue.insertAllIfNew(jobs) | ||||
|             _ <- if (notifyJoex) joex.notifyAllNodes else ().pure[F] | ||||
|           } yield items.size) | ||||
|  | ||||
|         def convertAllPdf( | ||||
|             collective: Option[Ident], | ||||
|             account: AccountId, | ||||
| @@ -458,17 +627,17 @@ object OItem { | ||||
|             _   <- if (notifyJoex) joex.notifyAllNodes else ().pure[F] | ||||
|           } yield UpdateResult.success | ||||
|  | ||||
|         private def onSuccessIgnoreError(update: F[Unit])(ar: AddResult): F[Unit] = | ||||
|         private def onSuccessIgnoreError(update: F[Unit])(ar: UpdateResult): F[Unit] = | ||||
|           ar match { | ||||
|             case AddResult.Success => | ||||
|             case UpdateResult.Success => | ||||
|               update.attempt.flatMap { | ||||
|                 case Right(()) => ().pure[F] | ||||
|                 case Left(ex) => | ||||
|                   logger.warn(s"Error updating full-text index: ${ex.getMessage}") | ||||
|               } | ||||
|             case AddResult.Failure(_) => | ||||
|             case UpdateResult.Failure(_) => | ||||
|               ().pure[F] | ||||
|             case AddResult.EntityExists(_) => | ||||
|             case UpdateResult.NotFound => | ||||
|               ().pure[F] | ||||
|           } | ||||
|       }) | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package docspell.joex.process | ||||
|  | ||||
| import cats.data.NonEmptyList | ||||
| import cats.effect.Sync | ||||
| import cats.implicits._ | ||||
|  | ||||
| @@ -65,22 +66,38 @@ object LinkProposal { | ||||
|       case MetaProposalType.CorrOrg => | ||||
|         ctx.logger.debug(s"Updating item organization with: ${value.id}") *> | ||||
|           ctx.store.transact( | ||||
|             RItem.updateCorrOrg(itemId, ctx.args.meta.collective, Some(value)) | ||||
|             RItem.updateCorrOrg( | ||||
|               NonEmptyList.of(itemId), | ||||
|               ctx.args.meta.collective, | ||||
|               Some(value) | ||||
|             ) | ||||
|           ) | ||||
|       case MetaProposalType.ConcPerson => | ||||
|         ctx.logger.debug(s"Updating item concerning person with: $value") *> | ||||
|           ctx.store.transact( | ||||
|             RItem.updateConcPerson(itemId, ctx.args.meta.collective, Some(value)) | ||||
|             RItem.updateConcPerson( | ||||
|               NonEmptyList.of(itemId), | ||||
|               ctx.args.meta.collective, | ||||
|               Some(value) | ||||
|             ) | ||||
|           ) | ||||
|       case MetaProposalType.CorrPerson => | ||||
|         ctx.logger.debug(s"Updating item correspondent person with: $value") *> | ||||
|           ctx.store.transact( | ||||
|             RItem.updateCorrPerson(itemId, ctx.args.meta.collective, Some(value)) | ||||
|             RItem.updateCorrPerson( | ||||
|               NonEmptyList.of(itemId), | ||||
|               ctx.args.meta.collective, | ||||
|               Some(value) | ||||
|             ) | ||||
|           ) | ||||
|       case MetaProposalType.ConcEquip => | ||||
|         ctx.logger.debug(s"Updating item concerning equipment with: $value") *> | ||||
|           ctx.store.transact( | ||||
|             RItem.updateConcEquip(itemId, ctx.args.meta.collective, Some(value)) | ||||
|             RItem.updateConcEquip( | ||||
|               NonEmptyList.of(itemId), | ||||
|               ctx.args.meta.collective, | ||||
|               Some(value) | ||||
|             ) | ||||
|           ) | ||||
|       case MetaProposalType.DocDate => | ||||
|         MetaProposal.parseDate(value) match { | ||||
| @@ -88,7 +105,11 @@ object LinkProposal { | ||||
|             val ts = Timestamp.from(ld.atStartOfDay(Timestamp.UTC)) | ||||
|             ctx.logger.debug(s"Updating item date ${value.id}") *> | ||||
|               ctx.store.transact( | ||||
|                 RItem.updateDate(itemId, ctx.args.meta.collective, Some(ts)) | ||||
|                 RItem.updateDate( | ||||
|                   NonEmptyList.of(itemId), | ||||
|                   ctx.args.meta.collective, | ||||
|                   Some(ts) | ||||
|                 ) | ||||
|               ) | ||||
|           case None => | ||||
|             ctx.logger.info(s"Cannot read value '${value.id}' into a date.") *> | ||||
| @@ -100,7 +121,11 @@ object LinkProposal { | ||||
|             val ts = Timestamp.from(ld.atStartOfDay(Timestamp.UTC)) | ||||
|             ctx.logger.debug(s"Updating item due-date suggestion ${value.id}") *> | ||||
|               ctx.store.transact( | ||||
|                 RItem.updateDueDate(itemId, ctx.args.meta.collective, Some(ts)) | ||||
|                 RItem.updateDueDate( | ||||
|                   NonEmptyList.of(itemId), | ||||
|                   ctx.args.meta.collective, | ||||
|                   Some(ts) | ||||
|                 ) | ||||
|               ) | ||||
|           case None => | ||||
|             ctx.logger.info(s"Cannot read value '${value.id}' into a date.") *> | ||||
|   | ||||
| @@ -1384,7 +1384,9 @@ paths: | ||||
|       tags: [ Item ] | ||||
|       summary: Set new set of tags. | ||||
|       description: | | ||||
|         Update the tags associated to an item. | ||||
|         Update the tags associated to an item. This will remove all | ||||
|         existing ones and sets the given tags, such that after this | ||||
|         returns, the item has exactly the tags as given. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       parameters: | ||||
| @@ -1845,6 +1847,7 @@ paths: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/ItemProposals" | ||||
|  | ||||
|   /sec/item/{itemId}/reprocess: | ||||
|     post: | ||||
|       tags: [ Item ] | ||||
| @@ -1895,6 +1898,359 @@ paths: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|  | ||||
|   /sec/items/deleteAll: | ||||
|     post: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Delete multiple items. | ||||
|       description: | | ||||
|         Given a list of item 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/items/tags: | ||||
|     post: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Add tags to multiple items | ||||
|       description: | | ||||
|         Add the given tags to all given items. The tags that are | ||||
|         currently attached to the items are not changed. If there are | ||||
|         new tags in the given list, then they are added. Otherwise, | ||||
|         the item is left unchanged. | ||||
|       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) | ||||
|       summary: Sets tags to multiple items | ||||
|       description: | | ||||
|         Sets the given tags to all given items. If the tag list is | ||||
|         empty, then all tags are removed from the items. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/ItemsAndRefs" | ||||
|       responses: | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/items/name: | ||||
|     put: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Change the name of multiple items | ||||
|       description: | | ||||
|         Sets the name of multiple items at once. The name must not be | ||||
|         empty. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/ItemsAndName" | ||||
|       responses: | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/items/folder: | ||||
|     put: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Sets a folder to multiple items. | ||||
|       description: | | ||||
|         Given a folder id, sets it on all given items. If the folder | ||||
|         reference is not present, the folder is removed from all | ||||
|         items. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/ItemsAndRef" | ||||
|       responses: | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/items/direction: | ||||
|     put: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Set the direction of multiple items | ||||
|       description: | | ||||
|         Given multiple item ids and a direction value, sets it to all | ||||
|         items. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/ItemsAndDirection" | ||||
|       responses: | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/items/date: | ||||
|     put: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Set the date of multiple items | ||||
|       description: | | ||||
|         Given multiple item ids and a date, sets it to all items as | ||||
|         the item date. If no date is present, remove the date from the | ||||
|         items. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/ItemsAndDate" | ||||
|       responses: | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/items/duedate: | ||||
|     put: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Set the direction of multiple items | ||||
|       description: | | ||||
|         Given multiple item ids and a date value, sets it to all items | ||||
|         as the due date. If the date is missing, remove the due-date | ||||
|         from the items. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/ItemsAndDate" | ||||
|       responses: | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/items/corrOrg: | ||||
|     put: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Sets an organization to multiple items. | ||||
|       description: | | ||||
|         Given an organization id, sets it on all given items. If the | ||||
|         organization is missing, the reference is removed from all | ||||
|         items. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/ItemsAndRef" | ||||
|       responses: | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/items/corrPerson: | ||||
|     put: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Sets an correspondent person to multiple items. | ||||
|       description: | | ||||
|         Given an person id, sets it on all given items as | ||||
|         correspondent person. If the person is missing, the reference | ||||
|         is removed from all items. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/ItemsAndRef" | ||||
|       responses: | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/items/concPerson: | ||||
|     put: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Sets an concerning person to multiple items. | ||||
|       description: | | ||||
|         Given an person id, sets it on all given items as concerning | ||||
|         person. If the person is missing, it is removed from all | ||||
|         items. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/ItemsAndRef" | ||||
|       responses: | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/items/concEquipment: | ||||
|     put: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Sets an equipment to multiple items. | ||||
|       description: | | ||||
|         Given an equipment id, sets it on all given items. If no | ||||
|         equipment is given, the reference is removed from all given | ||||
|         items. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/ItemsAndRef" | ||||
|       responses: | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/items/confirm: | ||||
|     put: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Confirm multiple items. | ||||
|       description: | | ||||
|         Given a list of item ids, confirm 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/items/unconfirm: | ||||
|     put: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Un-confirm multiple items. | ||||
|       description: | | ||||
|         Given a list of item ids, un-confirm 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/items/reprocess: | ||||
|     post: | ||||
|       tags: | ||||
|         - Item (Multi Edit) | ||||
|       summary: Submit multiple items to re-processing | ||||
|       description: | | ||||
|         Given a list of item-ids, submits all these items for | ||||
|         reprocessing. All attachments of these items will be | ||||
|         reprocessed. Item metadata is not changed. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/IdList" | ||||
|  | ||||
|       responses: | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|  | ||||
|   /sec/attachment/{id}: | ||||
|     delete: | ||||
|       tags: [ Attachment ] | ||||
| @@ -2702,6 +3058,84 @@ paths: | ||||
|  | ||||
| components: | ||||
|   schemas: | ||||
|     ItemsAndRefs: | ||||
|       description: | | ||||
|         Holds a list of item ids and a list of ids of some other | ||||
|         related entity (e.g. tags). | ||||
|       required: | ||||
|         - items | ||||
|         - refs | ||||
|       properties: | ||||
|         items: | ||||
|           type: array | ||||
|           items: | ||||
|             type: string | ||||
|             format: ident | ||||
|         refs: | ||||
|           type: array | ||||
|           items: | ||||
|             type: string | ||||
|             format: ident | ||||
|     ItemsAndRef: | ||||
|       description: | | ||||
|         Holds a list of item ids and a single optional id of some | ||||
|         other related entity (e.g. person, org). | ||||
|       required: | ||||
|         - items | ||||
|       properties: | ||||
|         items: | ||||
|           type: array | ||||
|           items: | ||||
|             type: string | ||||
|             format: ident | ||||
|         ref: | ||||
|           type: string | ||||
|           format: ident | ||||
|     ItemsAndName: | ||||
|       description: | | ||||
|         Holds a list of item ids and an item name. | ||||
|       required: | ||||
|         - items | ||||
|         - name | ||||
|       properties: | ||||
|         items: | ||||
|           type: array | ||||
|           items: | ||||
|             type: string | ||||
|             format: ident | ||||
|         name: | ||||
|           type: string | ||||
|     ItemsAndDirection: | ||||
|       description: | | ||||
|         Holds a list of item ids and a direction value. | ||||
|       required: | ||||
|         - items | ||||
|         - direction | ||||
|       properties: | ||||
|         items: | ||||
|           type: array | ||||
|           items: | ||||
|             type: string | ||||
|             format: ident | ||||
|         direction: | ||||
|           type: string | ||||
|           format: direction | ||||
|     ItemsAndDate: | ||||
|       description: | | ||||
|         Holds a list of item ids and a date value. | ||||
|       required: | ||||
|         - items | ||||
|       properties: | ||||
|         items: | ||||
|           type: array | ||||
|           items: | ||||
|             type: string | ||||
|             format: ident | ||||
|         date: | ||||
|           type: integer | ||||
|           format: date-time | ||||
|  | ||||
|  | ||||
|     JobPriority: | ||||
|       description: | | ||||
|         Transfer the priority of a job. | ||||
| @@ -3828,7 +4262,7 @@ components: | ||||
|           format: date-time | ||||
|     ReferenceList: | ||||
|       description: | ||||
|         Listing of items. | ||||
|         Listing of entities with their id and a name. | ||||
|       required: | ||||
|         - items | ||||
|       properties: | ||||
| @@ -4077,6 +4511,8 @@ components: | ||||
|         dueDateUntil: | ||||
|           type: integer | ||||
|           format: date-time | ||||
|         itemSubset: | ||||
|           $ref: "#/components/schemas/IdList" | ||||
|     ItemLight: | ||||
|       description: | | ||||
|         An item with only a few important properties. | ||||
|   | ||||
| @@ -73,6 +73,7 @@ object RestServer { | ||||
|       "collective"              -> CollectiveRoutes(restApp.backend, token), | ||||
|       "queue"                   -> JobQueueRoutes(restApp.backend, token), | ||||
|       "item"                    -> ItemRoutes(cfg, restApp.backend, token), | ||||
|       "items"                   -> ItemMultiRoutes(restApp.backend, token), | ||||
|       "attachment"              -> AttachmentRoutes(restApp.backend, token), | ||||
|       "upload"                  -> UploadRoutes.secured(restApp.backend, cfg, token), | ||||
|       "checkfile"               -> CheckFileRoutes.secured(restApp.backend, token), | ||||
|   | ||||
| @@ -134,7 +134,9 @@ trait Conversions { | ||||
|       m.dueDateFrom, | ||||
|       m.dueDateUntil, | ||||
|       m.allNames, | ||||
|       None, | ||||
|       m.itemSubset | ||||
|         .map(_.ids.flatMap(i => Ident.fromString(i).toOption).toSet) | ||||
|         .filter(_.nonEmpty), | ||||
|       None | ||||
|     ) | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,203 @@ | ||||
| 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.common.{Ident, ItemState} | ||||
| import docspell.restapi.model._ | ||||
| import docspell.restserver.conv.Conversions | ||||
|  | ||||
| import io.circe.DecodingFailure | ||||
| import org.http4s.HttpRoutes | ||||
| import org.http4s.circe.CirceEntityDecoder._ | ||||
| import org.http4s.circe.CirceEntityEncoder._ | ||||
| import org.http4s.dsl.Http4sDsl | ||||
|  | ||||
| object ItemMultiRoutes { | ||||
|  | ||||
|   def apply[F[_]: Effect]( | ||||
|       backend: BackendApp[F], | ||||
|       user: AuthToken | ||||
|   ): HttpRoutes[F] = { | ||||
|     val dsl = new Http4sDsl[F] {} | ||||
|     import dsl._ | ||||
|  | ||||
|     HttpRoutes.of { | ||||
|       case req @ PUT -> Root / "confirm" => | ||||
|         for { | ||||
|           json <- req.as[IdList] | ||||
|           data <- readIds[F](json.ids) | ||||
|           res <- backend.item.setStates( | ||||
|             data, | ||||
|             ItemState.Confirmed, | ||||
|             user.account.collective | ||||
|           ) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Item data confirmed")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root / "unconfirm" => | ||||
|         for { | ||||
|           json <- req.as[IdList] | ||||
|           data <- readIds[F](json.ids) | ||||
|           res <- backend.item.setStates( | ||||
|             data, | ||||
|             ItemState.Created, | ||||
|             user.account.collective | ||||
|           ) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Item back to created.")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root / "tags" => | ||||
|         for { | ||||
|           json  <- req.as[ItemsAndRefs] | ||||
|           items <- readIds[F](json.items) | ||||
|           tags  <- json.refs.traverse(readId[F]) | ||||
|           res   <- backend.item.setTagsMultipleItems(items, tags, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Tags updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ POST -> Root / "tags" => | ||||
|         for { | ||||
|           json  <- req.as[ItemsAndRefs] | ||||
|           items <- readIds[F](json.items) | ||||
|           res <- backend.item.linkTagsMultipleItems( | ||||
|             items, | ||||
|             json.refs, | ||||
|             user.account.collective | ||||
|           ) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Tags added.")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root / "name" => | ||||
|         for { | ||||
|           json  <- req.as[ItemsAndName] | ||||
|           items <- readIds[F](json.items) | ||||
|           res <- backend.item.setNameMultiple( | ||||
|             items, | ||||
|             json.name.notEmpty.getOrElse(""), | ||||
|             user.account.collective | ||||
|           ) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Name updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root / "folder" => | ||||
|         for { | ||||
|           json  <- req.as[ItemsAndRef] | ||||
|           items <- readIds[F](json.items) | ||||
|           res   <- backend.item.setFolderMultiple(items, json.ref, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Folder updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root / "direction" => | ||||
|         for { | ||||
|           json  <- req.as[ItemsAndDirection] | ||||
|           items <- readIds[F](json.items) | ||||
|           res   <- backend.item.setDirection(items, json.direction, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Direction updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root / "date" => | ||||
|         for { | ||||
|           json  <- req.as[ItemsAndDate] | ||||
|           items <- readIds[F](json.items) | ||||
|           res   <- backend.item.setItemDate(items, json.date, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Item date updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root / "duedate" => | ||||
|         for { | ||||
|           json  <- req.as[ItemsAndDate] | ||||
|           items <- readIds[F](json.items) | ||||
|           res   <- backend.item.setItemDueDate(items, json.date, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Item due date updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root / "corrOrg" => | ||||
|         for { | ||||
|           json  <- req.as[ItemsAndRef] | ||||
|           items <- readIds[F](json.items) | ||||
|           res   <- backend.item.setCorrOrg(items, json.ref, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Correspondent organization updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root / "corrPerson" => | ||||
|         for { | ||||
|           json  <- req.as[ItemsAndRef] | ||||
|           items <- readIds[F](json.items) | ||||
|           res   <- backend.item.setCorrPerson(items, json.ref, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Correspondent person updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root / "concPerson" => | ||||
|         for { | ||||
|           json  <- req.as[ItemsAndRef] | ||||
|           items <- readIds[F](json.items) | ||||
|           res   <- backend.item.setConcPerson(items, json.ref, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Concerned person updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root / "concEquipment" => | ||||
|         for { | ||||
|           json  <- req.as[ItemsAndRef] | ||||
|           items <- readIds[F](json.items) | ||||
|           res   <- backend.item.setConcEquip(items, json.ref, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Concerned equipment updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ POST -> Root / "reprocess" => | ||||
|         for { | ||||
|           json  <- req.as[IdList] | ||||
|           items <- readIds[F](json.ids) | ||||
|           res   <- backend.item.reprocessAll(items, user.account, true) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Re-process task(s) submitted.")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ POST -> Root / "deleteAll" => | ||||
|         for { | ||||
|           json  <- req.as[IdList] | ||||
|           items <- readIds[F](json.ids) | ||||
|           n     <- backend.item.deleteItemMultiple(items, user.account.collective) | ||||
|           res = BasicResult( | ||||
|             n > 0, | ||||
|             if (n > 0) "Item(s) deleted" else "Item deletion failed." | ||||
|           ) | ||||
|           resp <- Ok(res) | ||||
|         } yield resp | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   implicit final class OptionString(opt: Option[String]) { | ||||
|     def notEmpty: Option[String] = | ||||
|       opt.map(_.trim).filter(_.nonEmpty) | ||||
|   } | ||||
|   implicit final class StringOps(str: String) { | ||||
|     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) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| package docspell.restserver.routes | ||||
|  | ||||
| import cats.data.NonEmptyList | ||||
| import cats.effect._ | ||||
| import cats.implicits._ | ||||
|  | ||||
| @@ -165,8 +166,12 @@ object ItemRoutes { | ||||
|  | ||||
|       case req @ PUT -> Root / Ident(id) / "direction" => | ||||
|         for { | ||||
|           dir  <- req.as[DirectionValue] | ||||
|           res  <- backend.item.setDirection(id, dir.direction, user.account.collective) | ||||
|           dir <- req.as[DirectionValue] | ||||
|           res <- backend.item.setDirection( | ||||
|             NonEmptyList.of(id), | ||||
|             dir.direction, | ||||
|             user.account.collective | ||||
|           ) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Direction updated")) | ||||
|         } yield resp | ||||
|  | ||||
| @@ -180,8 +185,12 @@ object ItemRoutes { | ||||
|       case req @ PUT -> Root / Ident(id) / "corrOrg" => | ||||
|         for { | ||||
|           idref <- req.as[OptionalId] | ||||
|           res   <- backend.item.setCorrOrg(id, idref.id, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Correspondent organization updated")) | ||||
|           res <- backend.item.setCorrOrg( | ||||
|             NonEmptyList.of(id), | ||||
|             idref.id, | ||||
|             user.account.collective | ||||
|           ) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Correspondent organization updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ POST -> Root / Ident(id) / "corrOrg" => | ||||
| @@ -195,8 +204,12 @@ object ItemRoutes { | ||||
|       case req @ PUT -> Root / Ident(id) / "corrPerson" => | ||||
|         for { | ||||
|           idref <- req.as[OptionalId] | ||||
|           res   <- backend.item.setCorrPerson(id, idref.id, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Correspondent person updated")) | ||||
|           res <- backend.item.setCorrPerson( | ||||
|             NonEmptyList.of(id), | ||||
|             idref.id, | ||||
|             user.account.collective | ||||
|           ) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Correspondent person updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ POST -> Root / Ident(id) / "corrPerson" => | ||||
| @@ -210,8 +223,12 @@ object ItemRoutes { | ||||
|       case req @ PUT -> Root / Ident(id) / "concPerson" => | ||||
|         for { | ||||
|           idref <- req.as[OptionalId] | ||||
|           res   <- backend.item.setConcPerson(id, idref.id, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Concerned person updated")) | ||||
|           res <- backend.item.setConcPerson( | ||||
|             NonEmptyList.of(id), | ||||
|             idref.id, | ||||
|             user.account.collective | ||||
|           ) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Concerned person updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ POST -> Root / Ident(id) / "concPerson" => | ||||
| @@ -225,8 +242,12 @@ object ItemRoutes { | ||||
|       case req @ PUT -> Root / Ident(id) / "concEquipment" => | ||||
|         for { | ||||
|           idref <- req.as[OptionalId] | ||||
|           res   <- backend.item.setConcEquip(id, idref.id, user.account.collective) | ||||
|           resp  <- Ok(Conversions.basicResult(res, "Concerned equipment updated")) | ||||
|           res <- backend.item.setConcEquip( | ||||
|             NonEmptyList.of(id), | ||||
|             idref.id, | ||||
|             user.account.collective | ||||
|           ) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ POST -> Root / Ident(id) / "concEquipment" => | ||||
| @@ -259,7 +280,11 @@ object ItemRoutes { | ||||
|         for { | ||||
|           date <- req.as[OptionalDate] | ||||
|           _    <- logger.fdebug(s"Setting item due date to ${date.date}") | ||||
|           res  <- backend.item.setItemDueDate(id, date.date, user.account.collective) | ||||
|           res <- backend.item.setItemDueDate( | ||||
|             NonEmptyList.of(id), | ||||
|             date.date, | ||||
|             user.account.collective | ||||
|           ) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Item due date updated")) | ||||
|         } yield resp | ||||
|  | ||||
| @@ -267,7 +292,11 @@ object ItemRoutes { | ||||
|         for { | ||||
|           date <- req.as[OptionalDate] | ||||
|           _    <- logger.fdebug(s"Setting item date to ${date.date}") | ||||
|           res  <- backend.item.setItemDate(id, date.date, user.account.collective) | ||||
|           res <- backend.item.setItemDate( | ||||
|             NonEmptyList.of(id), | ||||
|             date.date, | ||||
|             user.account.collective | ||||
|           ) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Item date updated")) | ||||
|         } yield resp | ||||
|  | ||||
|   | ||||
| @@ -57,7 +57,12 @@ case class Column(name: String, ns: String = "", alias: String = "") { | ||||
|     f ++ fr"IN (" ++ commas(values) ++ fr")" | ||||
|  | ||||
|   def isIn[A: Put](values: NonEmptyList[A]): Fragment = | ||||
|     isIn(values.map(a => sql"$a").toList) | ||||
|     values.tail match { | ||||
|       case Nil => | ||||
|         is(values.head) | ||||
|       case _ => | ||||
|         isIn(values.map(a => sql"$a").toList) | ||||
|     } | ||||
|  | ||||
|   def isLowerIn[A: Put](values: NonEmptyList[A]): Fragment = | ||||
|     fr"lower(" ++ f ++ fr") IN (" ++ commas(values.map(a => sql"$a").toList) ++ fr")" | ||||
|   | ||||
| @@ -132,7 +132,7 @@ object RItem { | ||||
|     } yield n | ||||
|  | ||||
|   def updateStateForCollective( | ||||
|       itemId: Ident, | ||||
|       itemIds: NonEmptyList[Ident], | ||||
|       itemState: ItemState, | ||||
|       coll: Ident | ||||
|   ): ConnectionIO[Int] = | ||||
| @@ -140,27 +140,35 @@ object RItem { | ||||
|       t <- currentTime | ||||
|       n <- updateRow( | ||||
|         table, | ||||
|         and(id.is(itemId), cid.is(coll)), | ||||
|         and(id.isIn(itemIds), cid.is(coll)), | ||||
|         commas(state.setTo(itemState), updated.setTo(t)) | ||||
|       ).update.run | ||||
|     } yield n | ||||
|  | ||||
|   def updateDirection(itemId: Ident, coll: Ident, dir: Direction): ConnectionIO[Int] = | ||||
|   def updateDirection( | ||||
|       itemIds: NonEmptyList[Ident], | ||||
|       coll: Ident, | ||||
|       dir: Direction | ||||
|   ): ConnectionIO[Int] = | ||||
|     for { | ||||
|       t <- currentTime | ||||
|       n <- updateRow( | ||||
|         table, | ||||
|         and(id.is(itemId), cid.is(coll)), | ||||
|         and(id.isIn(itemIds), cid.is(coll)), | ||||
|         commas(incoming.setTo(dir), updated.setTo(t)) | ||||
|       ).update.run | ||||
|     } yield n | ||||
|  | ||||
|   def updateCorrOrg(itemId: Ident, coll: Ident, org: Option[Ident]): ConnectionIO[Int] = | ||||
|   def updateCorrOrg( | ||||
|       itemIds: NonEmptyList[Ident], | ||||
|       coll: Ident, | ||||
|       org: Option[Ident] | ||||
|   ): ConnectionIO[Int] = | ||||
|     for { | ||||
|       t <- currentTime | ||||
|       n <- updateRow( | ||||
|         table, | ||||
|         and(id.is(itemId), cid.is(coll)), | ||||
|         and(id.isIn(itemIds), cid.is(coll)), | ||||
|         commas(corrOrg.setTo(org), updated.setTo(t)) | ||||
|       ).update.run | ||||
|     } yield n | ||||
| @@ -176,7 +184,7 @@ object RItem { | ||||
|     } yield n | ||||
|  | ||||
|   def updateCorrPerson( | ||||
|       itemId: Ident, | ||||
|       itemIds: NonEmptyList[Ident], | ||||
|       coll: Ident, | ||||
|       person: Option[Ident] | ||||
|   ): ConnectionIO[Int] = | ||||
| @@ -184,7 +192,7 @@ object RItem { | ||||
|       t <- currentTime | ||||
|       n <- updateRow( | ||||
|         table, | ||||
|         and(id.is(itemId), cid.is(coll)), | ||||
|         and(id.isIn(itemIds), cid.is(coll)), | ||||
|         commas(corrPerson.setTo(person), updated.setTo(t)) | ||||
|       ).update.run | ||||
|     } yield n | ||||
| @@ -200,7 +208,7 @@ object RItem { | ||||
|     } yield n | ||||
|  | ||||
|   def updateConcPerson( | ||||
|       itemId: Ident, | ||||
|       itemIds: NonEmptyList[Ident], | ||||
|       coll: Ident, | ||||
|       person: Option[Ident] | ||||
|   ): ConnectionIO[Int] = | ||||
| @@ -208,7 +216,7 @@ object RItem { | ||||
|       t <- currentTime | ||||
|       n <- updateRow( | ||||
|         table, | ||||
|         and(id.is(itemId), cid.is(coll)), | ||||
|         and(id.isIn(itemIds), cid.is(coll)), | ||||
|         commas(concPerson.setTo(person), updated.setTo(t)) | ||||
|       ).update.run | ||||
|     } yield n | ||||
| @@ -224,7 +232,7 @@ object RItem { | ||||
|     } yield n | ||||
|  | ||||
|   def updateConcEquip( | ||||
|       itemId: Ident, | ||||
|       itemIds: NonEmptyList[Ident], | ||||
|       coll: Ident, | ||||
|       equip: Option[Ident] | ||||
|   ): ConnectionIO[Int] = | ||||
| @@ -232,7 +240,7 @@ object RItem { | ||||
|       t <- currentTime | ||||
|       n <- updateRow( | ||||
|         table, | ||||
|         and(id.is(itemId), cid.is(coll)), | ||||
|         and(id.isIn(itemIds), cid.is(coll)), | ||||
|         commas(concEquipment.setTo(equip), updated.setTo(t)) | ||||
|       ).update.run | ||||
|     } yield n | ||||
| @@ -281,18 +289,8 @@ object RItem { | ||||
|       ).update.run | ||||
|     } yield n | ||||
|  | ||||
|   def updateDate(itemId: Ident, coll: Ident, date: Option[Timestamp]): ConnectionIO[Int] = | ||||
|     for { | ||||
|       t <- currentTime | ||||
|       n <- updateRow( | ||||
|         table, | ||||
|         and(id.is(itemId), cid.is(coll)), | ||||
|         commas(itemDate.setTo(date), updated.setTo(t)) | ||||
|       ).update.run | ||||
|     } yield n | ||||
|  | ||||
|   def updateDueDate( | ||||
|       itemId: Ident, | ||||
|   def updateDate( | ||||
|       itemIds: NonEmptyList[Ident], | ||||
|       coll: Ident, | ||||
|       date: Option[Timestamp] | ||||
|   ): ConnectionIO[Int] = | ||||
| @@ -300,7 +298,21 @@ object RItem { | ||||
|       t <- currentTime | ||||
|       n <- updateRow( | ||||
|         table, | ||||
|         and(id.is(itemId), cid.is(coll)), | ||||
|         and(id.isIn(itemIds), cid.is(coll)), | ||||
|         commas(itemDate.setTo(date), updated.setTo(t)) | ||||
|       ).update.run | ||||
|     } yield n | ||||
|  | ||||
|   def updateDueDate( | ||||
|       itemIds: NonEmptyList[Ident], | ||||
|       coll: Ident, | ||||
|       date: Option[Timestamp] | ||||
|   ): ConnectionIO[Int] = | ||||
|     for { | ||||
|       t <- currentTime | ||||
|       n <- updateRow( | ||||
|         table, | ||||
|         and(id.isIn(itemIds), cid.is(coll)), | ||||
|         commas(dueDate.setTo(date), updated.setTo(t)) | ||||
|       ).update.run | ||||
|     } yield n | ||||
| @@ -324,4 +336,10 @@ object RItem { | ||||
|     val empty: Option[Ident] = None | ||||
|     updateRow(table, folder.is(folderId), folder.setTo(empty)).update.run | ||||
|   } | ||||
|  | ||||
|   def filterItemsFragment(items: NonEmptyList[Ident], coll: Ident): Fragment = | ||||
|     selectSimple(Seq(id), table, and(cid.is(coll), id.isIn(items))) | ||||
|  | ||||
|   def filterItems(items: NonEmptyList[Ident], coll: Ident): ConnectionIO[Vector[Ident]] = | ||||
|     filterItemsFragment(items, coll).query[Ident].to[Vector] | ||||
| } | ||||
|   | ||||
| @@ -30,18 +30,17 @@ object RTagItem { | ||||
|   def deleteItemTags(item: Ident): ConnectionIO[Int] = | ||||
|     deleteFrom(table, itemId.is(item)).update.run | ||||
|  | ||||
|   def deleteItemTags(items: NonEmptyList[Ident], cid: Ident): ConnectionIO[Int] = { | ||||
|     val itemsFiltered = | ||||
|       RItem.filterItemsFragment(items, cid) | ||||
|     val sql = fr"DELETE FROM" ++ table ++ fr"WHERE" ++ itemId.isIn(itemsFiltered) | ||||
|  | ||||
|     sql.update.run | ||||
|   } | ||||
|  | ||||
|   def deleteTag(tid: Ident): ConnectionIO[Int] = | ||||
|     deleteFrom(table, tagId.is(tid)).update.run | ||||
|  | ||||
|   def insertItemTags(item: Ident, tags: Seq[Ident]): ConnectionIO[Int] = | ||||
|     for { | ||||
|       tagValues <- tags.toList.traverse(id => | ||||
|         Ident.randomId[ConnectionIO].map(rid => RTagItem(rid, item, id)) | ||||
|       ) | ||||
|       tagFrag = tagValues.map(v => fr"${v.tagItemId},${v.itemId},${v.tagId}") | ||||
|       ins <- insertRows(table, all, tagFrag).update.run | ||||
|     } yield ins | ||||
|  | ||||
|   def findByItem(item: Ident): ConnectionIO[Vector[RTagItem]] = | ||||
|     selectSimple(all, table, itemId.is(item)).query[RTagItem].to[Vector] | ||||
|  | ||||
| @@ -76,4 +75,12 @@ object RTagItem { | ||||
|           entities.map(v => fr"${v.tagItemId},${v.itemId},${v.tagId}") | ||||
|         ).update.run | ||||
|       } yield n | ||||
|  | ||||
|   def appendTags(item: Ident, tags: List[Ident]): ConnectionIO[Int] = | ||||
|     for { | ||||
|       existing <- findByItem(item) | ||||
|       toadd = tags.toSet.diff(existing.map(_.tagId).toSet) | ||||
|       n <- setAllTags(item, toadd.toSeq) | ||||
|     } yield n | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ module Api exposing | ||||
|     , addCorrPerson | ||||
|     , addMember | ||||
|     , addTag | ||||
|     , addTagsMultiple | ||||
|     , cancelJob | ||||
|     , changeFolderName | ||||
|     , changePassword | ||||
| @@ -14,6 +15,7 @@ module Api exposing | ||||
|     , createNewFolder | ||||
|     , createNotifyDueItems | ||||
|     , createScanMailbox | ||||
|     , deleteAllItems | ||||
|     , deleteAttachment | ||||
|     , deleteEquip | ||||
|     , deleteFolder | ||||
| @@ -76,18 +78,28 @@ module Api exposing | ||||
|     , setAttachmentName | ||||
|     , setCollectiveSettings | ||||
|     , setConcEquip | ||||
|     , setConcEquipmentMultiple | ||||
|     , setConcPerson | ||||
|     , setConcPersonMultiple | ||||
|     , setConfirmed | ||||
|     , setCorrOrg | ||||
|     , setCorrOrgMultiple | ||||
|     , setCorrPerson | ||||
|     , setCorrPersonMultiple | ||||
|     , setDateMultiple | ||||
|     , setDirection | ||||
|     , setDirectionMultiple | ||||
|     , setDueDateMultiple | ||||
|     , setFolder | ||||
|     , setFolderMultiple | ||||
|     , setItemDate | ||||
|     , setItemDueDate | ||||
|     , setItemName | ||||
|     , setItemNotes | ||||
|     , setJobPrio | ||||
|     , setNameMultiple | ||||
|     , setTags | ||||
|     , setTagsMultiple | ||||
|     , setUnconfirmed | ||||
|     , startClassifier | ||||
|     , startOnceNotifyDueItems | ||||
| @@ -119,6 +131,7 @@ import Api.Model.EquipmentList exposing (EquipmentList) | ||||
| import Api.Model.FolderDetail exposing (FolderDetail) | ||||
| import Api.Model.FolderList exposing (FolderList) | ||||
| import Api.Model.GenInvite exposing (GenInvite) | ||||
| import Api.Model.IdList exposing (IdList) | ||||
| import Api.Model.IdResult exposing (IdResult) | ||||
| import Api.Model.ImapSettings exposing (ImapSettings) | ||||
| import Api.Model.ImapSettingsList exposing (ImapSettingsList) | ||||
| @@ -130,6 +143,11 @@ import Api.Model.ItemLightList exposing (ItemLightList) | ||||
| import Api.Model.ItemProposals exposing (ItemProposals) | ||||
| import Api.Model.ItemSearch exposing (ItemSearch) | ||||
| import Api.Model.ItemUploadMeta exposing (ItemUploadMeta) | ||||
| import Api.Model.ItemsAndDate exposing (ItemsAndDate) | ||||
| import Api.Model.ItemsAndDirection exposing (ItemsAndDirection) | ||||
| import Api.Model.ItemsAndName exposing (ItemsAndName) | ||||
| import Api.Model.ItemsAndRef exposing (ItemsAndRef) | ||||
| import Api.Model.ItemsAndRefs exposing (ItemsAndRefs) | ||||
| import Api.Model.JobPriority exposing (JobPriority) | ||||
| import Api.Model.JobQueueState exposing (JobQueueState) | ||||
| import Api.Model.MoveAttachment exposing (MoveAttachment) | ||||
| @@ -166,6 +184,7 @@ import Data.Priority exposing (Priority) | ||||
| import File exposing (File) | ||||
| import Http | ||||
| import Json.Encode as JsonEncode | ||||
| import Set exposing (Set) | ||||
| import Task | ||||
| import Url | ||||
| import Util.File | ||||
| @@ -1262,6 +1281,178 @@ getJobQueueStateTask flags = | ||||
|  | ||||
|  | ||||
|  | ||||
| --- Item (Mulit Edit) | ||||
|  | ||||
|  | ||||
| setTagsMultiple : | ||||
|     Flags | ||||
|     -> ItemsAndRefs | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| setTagsMultiple flags data receive = | ||||
|     Http2.authPut | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/tags" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemsAndRefs.encode data) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| addTagsMultiple : | ||||
|     Flags | ||||
|     -> ItemsAndRefs | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| addTagsMultiple flags data receive = | ||||
|     Http2.authPost | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/tags" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemsAndRefs.encode data) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| setNameMultiple : | ||||
|     Flags | ||||
|     -> ItemsAndName | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| setNameMultiple flags data receive = | ||||
|     Http2.authPut | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/name" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemsAndName.encode data) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| setFolderMultiple : | ||||
|     Flags | ||||
|     -> ItemsAndRef | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| setFolderMultiple flags data receive = | ||||
|     Http2.authPut | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/folder" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemsAndRef.encode data) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| setDirectionMultiple : | ||||
|     Flags | ||||
|     -> ItemsAndDirection | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| setDirectionMultiple flags data receive = | ||||
|     Http2.authPut | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/direction" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemsAndDirection.encode data) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| setDateMultiple : | ||||
|     Flags | ||||
|     -> ItemsAndDate | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| setDateMultiple flags data receive = | ||||
|     Http2.authPut | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/date" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemsAndDate.encode data) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| setDueDateMultiple : | ||||
|     Flags | ||||
|     -> ItemsAndDate | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| setDueDateMultiple flags data receive = | ||||
|     Http2.authPut | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/duedate" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemsAndDate.encode data) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| setCorrOrgMultiple : | ||||
|     Flags | ||||
|     -> ItemsAndRef | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| setCorrOrgMultiple flags data receive = | ||||
|     Http2.authPut | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/corrOrg" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemsAndRef.encode data) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| setCorrPersonMultiple : | ||||
|     Flags | ||||
|     -> ItemsAndRef | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| setCorrPersonMultiple flags data receive = | ||||
|     Http2.authPut | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/corrPerson" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemsAndRef.encode data) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| setConcPersonMultiple : | ||||
|     Flags | ||||
|     -> ItemsAndRef | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| setConcPersonMultiple flags data receive = | ||||
|     Http2.authPut | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/concPerson" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemsAndRef.encode data) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| setConcEquipmentMultiple : | ||||
|     Flags | ||||
|     -> ItemsAndRef | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| setConcEquipmentMultiple flags data receive = | ||||
|     Http2.authPut | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/concEquipment" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemsAndRef.encode data) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| deleteAllItems : | ||||
|     Flags | ||||
|     -> Set String | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| deleteAllItems flags ids receive = | ||||
|     Http2.authPost | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/items/deleteAll" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.IdList.encode (IdList (Set.toList ids))) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
|  | ||||
| --- Item | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| module Comp.ItemCardList exposing | ||||
|     ( Model | ||||
|     , Msg(..) | ||||
|     , ViewConfig | ||||
|     , init | ||||
|     , nextItem | ||||
|     , prevItem | ||||
| @@ -17,12 +18,16 @@ import Data.Direction | ||||
| import Data.Fields | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.Icons as Icons | ||||
| import Data.ItemSelection exposing (ItemSelection) | ||||
| import Data.Items | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| import Html exposing (..) | ||||
| import Html.Attributes exposing (..) | ||||
| import Html.Events exposing (onClick) | ||||
| import Markdown | ||||
| import Page exposing (Page(..)) | ||||
| import Set exposing (Set) | ||||
| import Util.Html | ||||
| import Util.ItemDragDrop as DD | ||||
| import Util.List | ||||
| import Util.String | ||||
| @@ -38,6 +43,7 @@ type Msg | ||||
|     = SetResults ItemLightList | ||||
|     | AddResults ItemLightList | ||||
|     | ItemDDMsg DD.Msg | ||||
|     | ToggleSelectItem (Set String) String | ||||
|  | ||||
|  | ||||
| init : Model | ||||
| @@ -75,6 +81,7 @@ type alias UpdateResult = | ||||
|     { model : Model | ||||
|     , cmd : Cmd Msg | ||||
|     , dragModel : DD.Model | ||||
|     , selection : ItemSelection | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -91,51 +98,78 @@ updateDrag dm _ msg model = | ||||
|                 newModel = | ||||
|                     { model | results = list } | ||||
|             in | ||||
|             UpdateResult newModel Cmd.none dm | ||||
|             UpdateResult newModel Cmd.none dm Data.ItemSelection.Inactive | ||||
|  | ||||
|         AddResults list -> | ||||
|             if list.groups == [] then | ||||
|                 UpdateResult model Cmd.none dm | ||||
|                 UpdateResult model Cmd.none dm Data.ItemSelection.Inactive | ||||
|  | ||||
|             else | ||||
|                 let | ||||
|                     newModel = | ||||
|                         { model | results = Data.Items.concat model.results list } | ||||
|                 in | ||||
|                 UpdateResult newModel Cmd.none dm | ||||
|                 UpdateResult newModel Cmd.none dm Data.ItemSelection.Inactive | ||||
|  | ||||
|         ItemDDMsg lm -> | ||||
|             let | ||||
|                 ddd = | ||||
|                     DD.update lm dm | ||||
|             in | ||||
|             UpdateResult model Cmd.none ddd.model | ||||
|             UpdateResult model Cmd.none ddd.model Data.ItemSelection.Inactive | ||||
|  | ||||
|         ToggleSelectItem ids id -> | ||||
|             let | ||||
|                 newSet = | ||||
|                     if Set.member id ids then | ||||
|                         Set.remove id ids | ||||
|  | ||||
|                     else | ||||
|                         Set.insert id ids | ||||
|             in | ||||
|             UpdateResult model Cmd.none dm (Data.ItemSelection.Active newSet) | ||||
|  | ||||
|  | ||||
|  | ||||
| --- View | ||||
|  | ||||
|  | ||||
| view : Maybe String -> UiSettings -> Model -> Html Msg | ||||
| view current settings model = | ||||
| type alias ViewConfig = | ||||
|     { current : Maybe String | ||||
|     , selection : ItemSelection | ||||
|     } | ||||
|  | ||||
|  | ||||
| isSelected : ViewConfig -> String -> Bool | ||||
| isSelected cfg id = | ||||
|     case cfg.selection of | ||||
|         Data.ItemSelection.Active ids -> | ||||
|             Set.member id ids | ||||
|  | ||||
|         Data.ItemSelection.Inactive -> | ||||
|             False | ||||
|  | ||||
|  | ||||
| view : ViewConfig -> UiSettings -> Model -> Html Msg | ||||
| view cfg settings model = | ||||
|     div [ class "ui container" ] | ||||
|         (List.map (viewGroup current settings) model.results.groups) | ||||
|         (List.map (viewGroup cfg settings) model.results.groups) | ||||
|  | ||||
|  | ||||
| viewGroup : Maybe String -> UiSettings -> ItemLightGroup -> Html Msg | ||||
| viewGroup current settings group = | ||||
| viewGroup : ViewConfig -> UiSettings -> ItemLightGroup -> Html Msg | ||||
| viewGroup cfg settings group = | ||||
|     div [ class "item-group" ] | ||||
|         [ div [ class "ui horizontal divider header item-list" ] | ||||
|             [ i [ class "calendar alternate outline icon" ] [] | ||||
|             , text group.name | ||||
|             ] | ||||
|         , div [ class "ui stackable three cards" ] | ||||
|             (List.map (viewItem current settings) group.items) | ||||
|             (List.map (viewItem cfg settings) group.items) | ||||
|         ] | ||||
|  | ||||
|  | ||||
| viewItem : Maybe String -> UiSettings -> ItemLight -> Html Msg | ||||
| viewItem current settings item = | ||||
| viewItem : ViewConfig -> UiSettings -> ItemLight -> Html Msg | ||||
| viewItem cfg settings item = | ||||
|     let | ||||
|         dirIcon = | ||||
|             i [ class (Data.Direction.iconFromMaybe item.direction) ] [] | ||||
| @@ -163,43 +197,69 @@ viewItem current settings item = | ||||
|         isConfirmed = | ||||
|             item.state /= "created" | ||||
|  | ||||
|         newColor = | ||||
|             "blue" | ||||
|         cardColor = | ||||
|             if isSelected cfg item.id then | ||||
|                 "purple" | ||||
|  | ||||
|             else if not isConfirmed then | ||||
|                 "blue" | ||||
|  | ||||
|             else | ||||
|                 "" | ||||
|  | ||||
|         fieldHidden f = | ||||
|             Data.UiSettings.fieldHidden settings f | ||||
|  | ||||
|         cardAction = | ||||
|             case cfg.selection of | ||||
|                 Data.ItemSelection.Inactive -> | ||||
|                     Page.href (ItemDetailPage item.id) | ||||
|  | ||||
|                 Data.ItemSelection.Active ids -> | ||||
|                     onClick (ToggleSelectItem ids item.id) | ||||
|     in | ||||
|     a | ||||
|         ([ classList | ||||
|             [ ( "ui fluid card", True ) | ||||
|             , ( newColor, not isConfirmed ) | ||||
|             , ( "current", current == Just item.id ) | ||||
|             , ( cardColor, True ) | ||||
|             , ( "current", cfg.current == Just item.id ) | ||||
|             ] | ||||
|          , id item.id | ||||
|          , Page.href (ItemDetailPage item.id) | ||||
|          , href "#" | ||||
|          , cardAction | ||||
|          ] | ||||
|             ++ DD.draggable ItemDDMsg item.id | ||||
|         ) | ||||
|         [ div [ class "content" ] | ||||
|             [ if fieldHidden Data.Fields.Direction then | ||||
|                 div [ class "header" ] | ||||
|                     [ Util.String.underscoreToSpace item.name |> text | ||||
|                     ] | ||||
|             [ case cfg.selection of | ||||
|                 Data.ItemSelection.Active ids -> | ||||
|                     div [ class "header" ] | ||||
|                         [ Util.Html.checkbox (Set.member item.id ids) | ||||
|                         , dirIcon | ||||
|                         , Util.String.underscoreToSpace item.name | ||||
|                             |> text | ||||
|                         ] | ||||
|  | ||||
|               else | ||||
|                 div | ||||
|                     [ class "header" | ||||
|                     , Data.Direction.labelFromMaybe item.direction | ||||
|                         |> title | ||||
|                     ] | ||||
|                     [ dirIcon | ||||
|                     , Util.String.underscoreToSpace item.name | ||||
|                         |> text | ||||
|                     ] | ||||
|                 Data.ItemSelection.Inactive -> | ||||
|                     if fieldHidden Data.Fields.Direction then | ||||
|                         div [ class "header" ] | ||||
|                             [ Util.String.underscoreToSpace item.name |> text | ||||
|                             ] | ||||
|  | ||||
|                     else | ||||
|                         div | ||||
|                             [ class "header" | ||||
|                             , Data.Direction.labelFromMaybe item.direction | ||||
|                                 |> title | ||||
|                             ] | ||||
|                             [ dirIcon | ||||
|                             , Util.String.underscoreToSpace item.name | ||||
|                                 |> text | ||||
|                             ] | ||||
|             , div | ||||
|                 [ classList | ||||
|                     [ ( "ui right corner label", True ) | ||||
|                     , ( newColor, True ) | ||||
|                     , ( cardColor, True ) | ||||
|                     , ( "invisible", isConfirmed ) | ||||
|                     ] | ||||
|                 , title "New" | ||||
|   | ||||
							
								
								
									
										693
									
								
								modules/webapp/src/main/elm/Comp/ItemDetail/EditMenu.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										693
									
								
								modules/webapp/src/main/elm/Comp/ItemDetail/EditMenu.elm
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,693 @@ | ||||
| module Comp.ItemDetail.EditMenu exposing | ||||
|     ( Model | ||||
|     , Msg | ||||
|     , SaveNameState(..) | ||||
|     , defaultViewConfig | ||||
|     , init | ||||
|     , loadModel | ||||
|     , update | ||||
|     , view | ||||
|     ) | ||||
|  | ||||
| import Api | ||||
| 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.ItemProposals exposing (ItemProposals) | ||||
| import Api.Model.ReferenceList exposing (ReferenceList) | ||||
| import Api.Model.Tag exposing (Tag) | ||||
| import Api.Model.TagList exposing (TagList) | ||||
| import Comp.DatePicker | ||||
| import Comp.DetailEdit | ||||
| import Comp.Dropdown exposing (isDropdownChangeMsg) | ||||
| import Comp.ItemDetail.FormChange exposing (FormChange(..)) | ||||
| import Data.Direction exposing (Direction) | ||||
| import Data.Fields | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.Icons as Icons | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| import DatePicker exposing (DatePicker) | ||||
| import Html exposing (..) | ||||
| import Html.Attributes exposing (..) | ||||
| import Html.Events exposing (onClick, onInput) | ||||
| import Http | ||||
| import Markdown | ||||
| import Page exposing (Page(..)) | ||||
| import Task | ||||
| import Throttle exposing (Throttle) | ||||
| import Time | ||||
| import Util.Folder exposing (mkFolderOption) | ||||
| import Util.List | ||||
| import Util.Maybe | ||||
| import Util.Tag | ||||
|  | ||||
|  | ||||
|  | ||||
| --- Model | ||||
|  | ||||
|  | ||||
| type SaveNameState | ||||
|     = Saving | ||||
|     | SaveSuccess | ||||
|     | SaveFailed | ||||
|  | ||||
|  | ||||
| type alias Model = | ||||
|     { tagModel : Comp.Dropdown.Model Tag | ||||
|     , nameModel : String | ||||
|     , nameSaveThrottle : Throttle Msg | ||||
|     , folderModel : Comp.Dropdown.Model IdName | ||||
|     , allFolders : List FolderItem | ||||
|     , directionModel : Comp.Dropdown.Model Direction | ||||
|     , itemDatePicker : DatePicker | ||||
|     , itemDate : Maybe Int | ||||
|     , itemProposals : ItemProposals | ||||
|     , dueDate : Maybe Int | ||||
|     , dueDatePicker : DatePicker | ||||
|     , corrOrgModel : Comp.Dropdown.Model IdName | ||||
|     , corrPersonModel : Comp.Dropdown.Model IdName | ||||
|     , concPersonModel : Comp.Dropdown.Model IdName | ||||
|     , concEquipModel : Comp.Dropdown.Model IdName | ||||
|     , modalEdit : Maybe Comp.DetailEdit.Model | ||||
|     } | ||||
|  | ||||
|  | ||||
| type Msg | ||||
|     = ItemDatePickerMsg Comp.DatePicker.Msg | ||||
|     | DueDatePickerMsg Comp.DatePicker.Msg | ||||
|     | SetName String | ||||
|     | SaveName | ||||
|     | UpdateThrottle | ||||
|     | RemoveDueDate | ||||
|     | RemoveDate | ||||
|     | FolderDropdownMsg (Comp.Dropdown.Msg IdName) | ||||
|     | TagDropdownMsg (Comp.Dropdown.Msg Tag) | ||||
|     | DirDropdownMsg (Comp.Dropdown.Msg Direction) | ||||
|     | OrgDropdownMsg (Comp.Dropdown.Msg IdName) | ||||
|     | CorrPersonMsg (Comp.Dropdown.Msg IdName) | ||||
|     | ConcPersonMsg (Comp.Dropdown.Msg IdName) | ||||
|     | ConcEquipMsg (Comp.Dropdown.Msg IdName) | ||||
|     | GetTagsResp (Result Http.Error TagList) | ||||
|     | GetOrgResp (Result Http.Error ReferenceList) | ||||
|     | GetPersonResp (Result Http.Error ReferenceList) | ||||
|     | GetEquipResp (Result Http.Error EquipmentList) | ||||
|     | GetFolderResp (Result Http.Error FolderList) | ||||
|  | ||||
|  | ||||
| init : Model | ||||
| init = | ||||
|     { tagModel = | ||||
|         Util.Tag.makeDropdownModel | ||||
|     , directionModel = | ||||
|         Comp.Dropdown.makeSingleList | ||||
|             { makeOption = | ||||
|                 \entry -> | ||||
|                     { value = Data.Direction.toString entry | ||||
|                     , text = Data.Direction.toString entry | ||||
|                     , additional = "" | ||||
|                     } | ||||
|             , options = Data.Direction.all | ||||
|             , placeholder = "Choose a direction…" | ||||
|             , selected = Nothing | ||||
|             } | ||||
|     , corrOrgModel = | ||||
|         Comp.Dropdown.makeSingle | ||||
|             { makeOption = \e -> { value = e.id, text = e.name, additional = "" } | ||||
|             , placeholder = "" | ||||
|             } | ||||
|     , corrPersonModel = | ||||
|         Comp.Dropdown.makeSingle | ||||
|             { makeOption = \e -> { value = e.id, text = e.name, additional = "" } | ||||
|             , placeholder = "" | ||||
|             } | ||||
|     , concPersonModel = | ||||
|         Comp.Dropdown.makeSingle | ||||
|             { makeOption = \e -> { value = e.id, text = e.name, additional = "" } | ||||
|             , placeholder = "" | ||||
|             } | ||||
|     , concEquipModel = | ||||
|         Comp.Dropdown.makeSingle | ||||
|             { makeOption = \e -> { value = e.id, text = e.name, additional = "" } | ||||
|             , placeholder = "" | ||||
|             } | ||||
|     , folderModel = | ||||
|         Comp.Dropdown.makeSingle | ||||
|             { makeOption = \e -> { value = e.id, text = e.name, additional = "" } | ||||
|             , placeholder = "" | ||||
|             } | ||||
|     , allFolders = [] | ||||
|     , nameModel = "" | ||||
|     , nameSaveThrottle = Throttle.create 1 | ||||
|     , itemDatePicker = Comp.DatePicker.emptyModel | ||||
|     , itemDate = Nothing | ||||
|     , itemProposals = Api.Model.ItemProposals.empty | ||||
|     , dueDate = Nothing | ||||
|     , dueDatePicker = Comp.DatePicker.emptyModel | ||||
|     , modalEdit = Nothing | ||||
|     } | ||||
|  | ||||
|  | ||||
| loadModel : Flags -> Cmd Msg | ||||
| loadModel flags = | ||||
|     let | ||||
|         ( _, dpc ) = | ||||
|             Comp.DatePicker.init | ||||
|     in | ||||
|     Cmd.batch | ||||
|         [ Api.getTags flags "" GetTagsResp | ||||
|         , Api.getOrgLight flags GetOrgResp | ||||
|         , Api.getPersonsLight flags GetPersonResp | ||||
|         , Api.getEquipments flags "" GetEquipResp | ||||
|         , Api.getFolders flags "" False GetFolderResp | ||||
|         , Cmd.map ItemDatePickerMsg dpc | ||||
|         , Cmd.map DueDatePickerMsg dpc | ||||
|         ] | ||||
|  | ||||
|  | ||||
| isFolderMember : Model -> Bool | ||||
| isFolderMember model = | ||||
|     let | ||||
|         selected = | ||||
|             Comp.Dropdown.getSelected model.folderModel | ||||
|                 |> List.head | ||||
|                 |> Maybe.map .id | ||||
|     in | ||||
|     Util.Folder.isFolderMember model.allFolders selected | ||||
|  | ||||
|  | ||||
|  | ||||
| --- Update | ||||
|  | ||||
|  | ||||
| type alias UpdateResult = | ||||
|     { model : Model | ||||
|     , cmd : Cmd Msg | ||||
|     , sub : Sub Msg | ||||
|     , change : FormChange | ||||
|     } | ||||
|  | ||||
|  | ||||
| resultNoCmd : FormChange -> Model -> UpdateResult | ||||
| resultNoCmd change model = | ||||
|     UpdateResult model Cmd.none Sub.none change | ||||
|  | ||||
|  | ||||
| resultNone : Model -> UpdateResult | ||||
| resultNone model = | ||||
|     resultNoCmd NoFormChange model | ||||
|  | ||||
|  | ||||
| update : Flags -> Msg -> Model -> UpdateResult | ||||
| update flags msg model = | ||||
|     case msg of | ||||
|         TagDropdownMsg m -> | ||||
|             let | ||||
|                 ( m2, _ ) = | ||||
|                     Comp.Dropdown.update m model.tagModel | ||||
|  | ||||
|                 newModel = | ||||
|                     { model | tagModel = m2 } | ||||
|  | ||||
|                 change = | ||||
|                     if isDropdownChangeMsg m then | ||||
|                         Comp.Dropdown.getSelected newModel.tagModel | ||||
|                             |> Util.List.distinct | ||||
|                             |> List.map (\t -> IdName t.id t.name) | ||||
|                             |> ReferenceList | ||||
|                             |> TagChange | ||||
|  | ||||
|                     else | ||||
|                         NoFormChange | ||||
|             in | ||||
|             resultNoCmd change newModel | ||||
|  | ||||
|         GetTagsResp (Ok tags) -> | ||||
|             let | ||||
|                 tagList = | ||||
|                     Comp.Dropdown.SetOptions tags.items | ||||
|             in | ||||
|             update flags (TagDropdownMsg tagList) model | ||||
|  | ||||
|         GetTagsResp (Err _) -> | ||||
|             resultNone model | ||||
|  | ||||
|         FolderDropdownMsg m -> | ||||
|             let | ||||
|                 ( m2, _ ) = | ||||
|                     Comp.Dropdown.update m model.folderModel | ||||
|  | ||||
|                 newModel = | ||||
|                     { model | folderModel = m2 } | ||||
|  | ||||
|                 idref = | ||||
|                     Comp.Dropdown.getSelected m2 |> List.head | ||||
|  | ||||
|                 change = | ||||
|                     if isDropdownChangeMsg m then | ||||
|                         FolderChange idref | ||||
|  | ||||
|                     else | ||||
|                         NoFormChange | ||||
|             in | ||||
|             resultNoCmd change newModel | ||||
|  | ||||
|         GetFolderResp (Ok fs) -> | ||||
|             let | ||||
|                 model_ = | ||||
|                     { model | ||||
|                         | allFolders = fs.items | ||||
|                         , folderModel = | ||||
|                             Comp.Dropdown.setMkOption | ||||
|                                 (mkFolderOption flags fs.items) | ||||
|                                 model.folderModel | ||||
|                     } | ||||
|  | ||||
|                 mkIdName fitem = | ||||
|                     IdName fitem.id fitem.name | ||||
|  | ||||
|                 opts = | ||||
|                     fs.items | ||||
|                         |> List.map mkIdName | ||||
|                         |> Comp.Dropdown.SetOptions | ||||
|             in | ||||
|             update flags (FolderDropdownMsg opts) model_ | ||||
|  | ||||
|         GetFolderResp (Err _) -> | ||||
|             resultNone model | ||||
|  | ||||
|         DirDropdownMsg m -> | ||||
|             let | ||||
|                 ( m2, _ ) = | ||||
|                     Comp.Dropdown.update m model.directionModel | ||||
|  | ||||
|                 newModel = | ||||
|                     { model | directionModel = m2 } | ||||
|  | ||||
|                 change = | ||||
|                     if isDropdownChangeMsg m then | ||||
|                         let | ||||
|                             dir = | ||||
|                                 Comp.Dropdown.getSelected m2 |> List.head | ||||
|                         in | ||||
|                         case dir of | ||||
|                             Just d -> | ||||
|                                 DirectionChange d | ||||
|  | ||||
|                             Nothing -> | ||||
|                                 NoFormChange | ||||
|  | ||||
|                     else | ||||
|                         NoFormChange | ||||
|             in | ||||
|             resultNoCmd change newModel | ||||
|  | ||||
|         OrgDropdownMsg m -> | ||||
|             let | ||||
|                 ( m2, _ ) = | ||||
|                     Comp.Dropdown.update m model.corrOrgModel | ||||
|  | ||||
|                 newModel = | ||||
|                     { model | corrOrgModel = m2 } | ||||
|  | ||||
|                 idref = | ||||
|                     Comp.Dropdown.getSelected m2 |> List.head | ||||
|  | ||||
|                 change = | ||||
|                     if isDropdownChangeMsg m then | ||||
|                         OrgChange idref | ||||
|  | ||||
|                     else | ||||
|                         NoFormChange | ||||
|             in | ||||
|             resultNoCmd change newModel | ||||
|  | ||||
|         GetOrgResp (Ok orgs) -> | ||||
|             let | ||||
|                 opts = | ||||
|                     Comp.Dropdown.SetOptions orgs.items | ||||
|             in | ||||
|             update flags (OrgDropdownMsg opts) model | ||||
|  | ||||
|         GetOrgResp (Err _) -> | ||||
|             resultNone model | ||||
|  | ||||
|         CorrPersonMsg m -> | ||||
|             let | ||||
|                 ( m2, _ ) = | ||||
|                     Comp.Dropdown.update m model.corrPersonModel | ||||
|  | ||||
|                 newModel = | ||||
|                     { model | corrPersonModel = m2 } | ||||
|  | ||||
|                 idref = | ||||
|                     Comp.Dropdown.getSelected m2 |> List.head | ||||
|  | ||||
|                 change = | ||||
|                     if isDropdownChangeMsg m then | ||||
|                         CorrPersonChange idref | ||||
|  | ||||
|                     else | ||||
|                         NoFormChange | ||||
|             in | ||||
|             resultNoCmd change newModel | ||||
|  | ||||
|         ConcPersonMsg m -> | ||||
|             let | ||||
|                 ( m2, _ ) = | ||||
|                     Comp.Dropdown.update m model.concPersonModel | ||||
|  | ||||
|                 newModel = | ||||
|                     { model | concPersonModel = m2 } | ||||
|  | ||||
|                 idref = | ||||
|                     Comp.Dropdown.getSelected m2 |> List.head | ||||
|  | ||||
|                 change = | ||||
|                     if isDropdownChangeMsg m then | ||||
|                         ConcPersonChange idref | ||||
|  | ||||
|                     else | ||||
|                         NoFormChange | ||||
|             in | ||||
|             resultNoCmd change newModel | ||||
|  | ||||
|         GetPersonResp (Ok ps) -> | ||||
|             let | ||||
|                 opts = | ||||
|                     Comp.Dropdown.SetOptions ps.items | ||||
|  | ||||
|                 res1 = | ||||
|                     update flags (CorrPersonMsg opts) model | ||||
|  | ||||
|                 res2 = | ||||
|                     update flags (ConcPersonMsg opts) res1.model | ||||
|             in | ||||
|             res2 | ||||
|  | ||||
|         GetPersonResp (Err _) -> | ||||
|             resultNone model | ||||
|  | ||||
|         ConcEquipMsg m -> | ||||
|             let | ||||
|                 ( m2, _ ) = | ||||
|                     Comp.Dropdown.update m model.concEquipModel | ||||
|  | ||||
|                 newModel = | ||||
|                     { model | concEquipModel = m2 } | ||||
|  | ||||
|                 idref = | ||||
|                     Comp.Dropdown.getSelected m2 |> List.head | ||||
|  | ||||
|                 change = | ||||
|                     if isDropdownChangeMsg m then | ||||
|                         EquipChange idref | ||||
|  | ||||
|                     else | ||||
|                         NoFormChange | ||||
|             in | ||||
|             resultNoCmd change newModel | ||||
|  | ||||
|         GetEquipResp (Ok equips) -> | ||||
|             let | ||||
|                 opts = | ||||
|                     Comp.Dropdown.SetOptions | ||||
|                         (List.map (\e -> IdName e.id e.name) | ||||
|                             equips.items | ||||
|                         ) | ||||
|             in | ||||
|             update flags (ConcEquipMsg opts) model | ||||
|  | ||||
|         GetEquipResp (Err _) -> | ||||
|             resultNone model | ||||
|  | ||||
|         ItemDatePickerMsg m -> | ||||
|             let | ||||
|                 ( dp, event ) = | ||||
|                     Comp.DatePicker.updateDefault m model.itemDatePicker | ||||
|             in | ||||
|             case event of | ||||
|                 DatePicker.Picked date -> | ||||
|                     let | ||||
|                         newModel = | ||||
|                             { model | itemDatePicker = dp, itemDate = Just (Comp.DatePicker.midOfDay date) } | ||||
|                     in | ||||
|                     resultNoCmd (ItemDateChange newModel.itemDate) newModel | ||||
|  | ||||
|                 _ -> | ||||
|                     resultNone { model | itemDatePicker = dp } | ||||
|  | ||||
|         RemoveDate -> | ||||
|             resultNoCmd (ItemDateChange Nothing) { model | itemDate = Nothing } | ||||
|  | ||||
|         DueDatePickerMsg m -> | ||||
|             let | ||||
|                 ( dp, event ) = | ||||
|                     Comp.DatePicker.updateDefault m model.dueDatePicker | ||||
|             in | ||||
|             case event of | ||||
|                 DatePicker.Picked date -> | ||||
|                     let | ||||
|                         newModel = | ||||
|                             { model | dueDatePicker = dp, dueDate = Just (Comp.DatePicker.midOfDay date) } | ||||
|                     in | ||||
|                     resultNoCmd (DueDateChange newModel.dueDate) newModel | ||||
|  | ||||
|                 _ -> | ||||
|                     resultNone { model | dueDatePicker = dp } | ||||
|  | ||||
|         RemoveDueDate -> | ||||
|             resultNoCmd (DueDateChange Nothing) { model | dueDate = Nothing } | ||||
|  | ||||
|         SetName str -> | ||||
|             case Util.Maybe.fromString str of | ||||
|                 Just newName -> | ||||
|                     let | ||||
|                         cmd_ = | ||||
|                             Task.succeed () | ||||
|                                 |> Task.perform (\_ -> SaveName) | ||||
|  | ||||
|                         ( newThrottle, cmd ) = | ||||
|                             Throttle.try cmd_ model.nameSaveThrottle | ||||
|  | ||||
|                         newModel = | ||||
|                             { model | ||||
|                                 | nameSaveThrottle = newThrottle | ||||
|                                 , nameModel = newName | ||||
|                             } | ||||
|  | ||||
|                         sub = | ||||
|                             nameThrottleSub newModel | ||||
|                     in | ||||
|                     UpdateResult newModel cmd sub NoFormChange | ||||
|  | ||||
|                 Nothing -> | ||||
|                     resultNone { model | nameModel = str } | ||||
|  | ||||
|         SaveName -> | ||||
|             case Util.Maybe.fromString model.nameModel of | ||||
|                 Just n -> | ||||
|                     resultNoCmd (NameChange n) model | ||||
|  | ||||
|                 Nothing -> | ||||
|                     resultNone model | ||||
|  | ||||
|         UpdateThrottle -> | ||||
|             let | ||||
|                 ( newThrottle, cmd ) = | ||||
|                     Throttle.update model.nameSaveThrottle | ||||
|  | ||||
|                 newModel = | ||||
|                     { model | nameSaveThrottle = newThrottle } | ||||
|  | ||||
|                 sub = | ||||
|                     nameThrottleSub newModel | ||||
|             in | ||||
|             UpdateResult newModel cmd sub NoFormChange | ||||
|  | ||||
|  | ||||
| nameThrottleSub : Model -> Sub Msg | ||||
| nameThrottleSub model = | ||||
|     Throttle.ifNeeded | ||||
|         (Time.every 400 (\_ -> UpdateThrottle)) | ||||
|         model.nameSaveThrottle | ||||
|  | ||||
|  | ||||
|  | ||||
| --- View | ||||
|  | ||||
|  | ||||
| type alias ViewConfig = | ||||
|     { menuClass : String | ||||
|     , nameState : SaveNameState | ||||
|     } | ||||
|  | ||||
|  | ||||
| defaultViewConfig : ViewConfig | ||||
| defaultViewConfig = | ||||
|     { menuClass = "ui vertical segment" | ||||
|     , nameState = SaveSuccess | ||||
|     } | ||||
|  | ||||
|  | ||||
| view : ViewConfig -> UiSettings -> Model -> Html Msg | ||||
| view = | ||||
|     renderEditForm | ||||
|  | ||||
|  | ||||
| renderEditForm : ViewConfig -> UiSettings -> Model -> Html Msg | ||||
| renderEditForm cfg settings model = | ||||
|     let | ||||
|         fieldVisible field = | ||||
|             Data.UiSettings.fieldVisible settings field | ||||
|  | ||||
|         optional fields html = | ||||
|             if | ||||
|                 List.map fieldVisible fields | ||||
|                     |> List.foldl (||) False | ||||
|             then | ||||
|                 html | ||||
|  | ||||
|             else | ||||
|                 span [ class "invisible hidden" ] [] | ||||
|     in | ||||
|     div [ class cfg.menuClass ] | ||||
|         [ div [ class "ui form warning" ] | ||||
|             [ optional [ Data.Fields.Tag ] <| | ||||
|                 div [ class "field" ] | ||||
|                     [ label [] | ||||
|                         [ Icons.tagsIcon "grey" | ||||
|                         , text "Tags" | ||||
|                         ] | ||||
|                     , Html.map TagDropdownMsg (Comp.Dropdown.view settings model.tagModel) | ||||
|                     ] | ||||
|             , div [ class " field" ] | ||||
|                 [ label [] [ text "Name" ] | ||||
|                 , div [ class "ui icon input" ] | ||||
|                     [ input [ type_ "text", value model.nameModel, onInput SetName ] [] | ||||
|                     , i | ||||
|                         [ classList | ||||
|                             [ ( "green check icon", cfg.nameState == SaveSuccess ) | ||||
|                             , ( "red exclamation triangle icon", cfg.nameState == SaveFailed ) | ||||
|                             , ( "sync loading icon", cfg.nameState == Saving ) | ||||
|                             ] | ||||
|                         ] | ||||
|                         [] | ||||
|                     ] | ||||
|                 ] | ||||
|             , optional [ Data.Fields.Folder ] <| | ||||
|                 div [ class "field" ] | ||||
|                     [ label [] | ||||
|                         [ Icons.folderIcon "grey" | ||||
|                         , text "Folder" | ||||
|                         ] | ||||
|                     , Html.map FolderDropdownMsg (Comp.Dropdown.view settings model.folderModel) | ||||
|                     , div | ||||
|                         [ classList | ||||
|                             [ ( "ui warning message", True ) | ||||
|                             , ( "hidden", isFolderMember model ) | ||||
|                             ] | ||||
|                         ] | ||||
|                         [ Markdown.toHtml [] """ | ||||
| You are **not a member** of this folder. This item will be **hidden** | ||||
| from any search now. Use a folder where you are a member of to make this | ||||
| item visible. This message will disappear then. | ||||
|                       """ | ||||
|                         ] | ||||
|                     ] | ||||
|             , optional [ Data.Fields.Direction ] <| | ||||
|                 div [ class "field" ] | ||||
|                     [ label [] | ||||
|                         [ Icons.directionIcon "grey" | ||||
|                         , text "Direction" | ||||
|                         ] | ||||
|                     , Html.map DirDropdownMsg (Comp.Dropdown.view settings model.directionModel) | ||||
|                     ] | ||||
|             , optional [ Data.Fields.Date ] <| | ||||
|                 div [ class "field" ] | ||||
|                     [ label [] | ||||
|                         [ Icons.dateIcon "grey" | ||||
|                         , text "Date" | ||||
|                         ] | ||||
|                     , div [ class "ui action input" ] | ||||
|                         [ Html.map ItemDatePickerMsg | ||||
|                             (Comp.DatePicker.viewTime | ||||
|                                 model.itemDate | ||||
|                                 actionInputDatePicker | ||||
|                                 model.itemDatePicker | ||||
|                             ) | ||||
|                         , a [ class "ui icon button", href "", onClick RemoveDate ] | ||||
|                             [ i [ class "trash alternate outline icon" ] [] | ||||
|                             ] | ||||
|                         ] | ||||
|                     ] | ||||
|             , optional [ Data.Fields.DueDate ] <| | ||||
|                 div [ class " field" ] | ||||
|                     [ label [] | ||||
|                         [ Icons.dueDateIcon "grey" | ||||
|                         , text "Due Date" | ||||
|                         ] | ||||
|                     , div [ class "ui action input" ] | ||||
|                         [ Html.map DueDatePickerMsg | ||||
|                             (Comp.DatePicker.viewTime | ||||
|                                 model.dueDate | ||||
|                                 actionInputDatePicker | ||||
|                                 model.dueDatePicker | ||||
|                             ) | ||||
|                         , a [ class "ui icon button", href "", onClick RemoveDueDate ] | ||||
|                             [ i [ class "trash alternate outline icon" ] [] ] | ||||
|                         ] | ||||
|                     ] | ||||
|             , optional [ Data.Fields.CorrOrg, Data.Fields.CorrPerson ] <| | ||||
|                 h4 [ class "ui dividing header" ] | ||||
|                     [ Icons.correspondentIcon "" | ||||
|                     , text "Correspondent" | ||||
|                     ] | ||||
|             , optional [ Data.Fields.CorrOrg ] <| | ||||
|                 div [ class "field" ] | ||||
|                     [ label [] | ||||
|                         [ Icons.organizationIcon "grey" | ||||
|                         , text "Organization" | ||||
|                         ] | ||||
|                     , Html.map OrgDropdownMsg (Comp.Dropdown.view settings model.corrOrgModel) | ||||
|                     ] | ||||
|             , optional [ Data.Fields.CorrPerson ] <| | ||||
|                 div [ class "field" ] | ||||
|                     [ label [] | ||||
|                         [ Icons.personIcon "grey" | ||||
|                         , text "Person" | ||||
|                         ] | ||||
|                     , Html.map CorrPersonMsg (Comp.Dropdown.view settings model.corrPersonModel) | ||||
|                     ] | ||||
|             , optional [ Data.Fields.ConcPerson, Data.Fields.ConcEquip ] <| | ||||
|                 h4 [ class "ui dividing header" ] | ||||
|                     [ Icons.concernedIcon | ||||
|                     , text "Concerning" | ||||
|                     ] | ||||
|             , optional [ Data.Fields.ConcPerson ] <| | ||||
|                 div [ class "field" ] | ||||
|                     [ label [] | ||||
|                         [ Icons.personIcon "grey" | ||||
|                         , text "Person" | ||||
|                         ] | ||||
|                     , Html.map ConcPersonMsg (Comp.Dropdown.view settings model.concPersonModel) | ||||
|                     ] | ||||
|             , optional [ Data.Fields.ConcEquip ] <| | ||||
|                 div [ class "field" ] | ||||
|                     [ label [] | ||||
|                         [ Icons.equipmentIcon "grey" | ||||
|                         , text "Equipment" | ||||
|                         ] | ||||
|                     , Html.map ConcEquipMsg (Comp.Dropdown.view settings model.concEquipModel) | ||||
|                     ] | ||||
|             ] | ||||
|         ] | ||||
|  | ||||
|  | ||||
| actionInputDatePicker : DatePicker.Settings | ||||
| actionInputDatePicker = | ||||
|     let | ||||
|         ds = | ||||
|             Comp.DatePicker.defaultSettings | ||||
|     in | ||||
|     { ds | containerClassList = [ ( "ui action input", True ) ] } | ||||
							
								
								
									
										118
									
								
								modules/webapp/src/main/elm/Comp/ItemDetail/FormChange.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								modules/webapp/src/main/elm/Comp/ItemDetail/FormChange.elm
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| module Comp.ItemDetail.FormChange exposing | ||||
|     ( FormChange(..) | ||||
|     , multiUpdate | ||||
|     ) | ||||
|  | ||||
| import Api | ||||
| import Api.Model.BasicResult exposing (BasicResult) | ||||
| import Api.Model.IdName exposing (IdName) | ||||
| import Api.Model.ItemsAndDate exposing (ItemsAndDate) | ||||
| import Api.Model.ItemsAndDirection exposing (ItemsAndDirection) | ||||
| import Api.Model.ItemsAndName exposing (ItemsAndName) | ||||
| import Api.Model.ItemsAndRef exposing (ItemsAndRef) | ||||
| import Api.Model.ItemsAndRefs exposing (ItemsAndRefs) | ||||
| import Api.Model.ReferenceList exposing (ReferenceList) | ||||
| import Data.Direction exposing (Direction) | ||||
| import Data.Flags exposing (Flags) | ||||
| import Http | ||||
| import Set exposing (Set) | ||||
|  | ||||
|  | ||||
| type FormChange | ||||
|     = NoFormChange | ||||
|     | TagChange ReferenceList | ||||
|     | FolderChange (Maybe IdName) | ||||
|     | DirectionChange Direction | ||||
|     | OrgChange (Maybe IdName) | ||||
|     | CorrPersonChange (Maybe IdName) | ||||
|     | ConcPersonChange (Maybe IdName) | ||||
|     | EquipChange (Maybe IdName) | ||||
|     | ItemDateChange (Maybe Int) | ||||
|     | DueDateChange (Maybe Int) | ||||
|     | NameChange String | ||||
|  | ||||
|  | ||||
| multiUpdate : | ||||
|     Flags | ||||
|     -> Set String | ||||
|     -> FormChange | ||||
|     -> (Result Http.Error BasicResult -> msg) | ||||
|     -> Cmd msg | ||||
| multiUpdate flags ids change receive = | ||||
|     let | ||||
|         items = | ||||
|             Set.toList ids | ||||
|     in | ||||
|     case change of | ||||
|         TagChange tags -> | ||||
|             let | ||||
|                 data = | ||||
|                     ItemsAndRefs items (List.map .id tags.items) | ||||
|             in | ||||
|             Api.setTagsMultiple flags data receive | ||||
|  | ||||
|         NameChange name -> | ||||
|             let | ||||
|                 data = | ||||
|                     ItemsAndName items name | ||||
|             in | ||||
|             Api.setNameMultiple flags data receive | ||||
|  | ||||
|         FolderChange id -> | ||||
|             let | ||||
|                 data = | ||||
|                     ItemsAndRef items (Maybe.map .id id) | ||||
|             in | ||||
|             Api.setFolderMultiple flags data receive | ||||
|  | ||||
|         DirectionChange dir -> | ||||
|             let | ||||
|                 data = | ||||
|                     ItemsAndDirection items (Data.Direction.toString dir) | ||||
|             in | ||||
|             Api.setDirectionMultiple flags data receive | ||||
|  | ||||
|         ItemDateChange date -> | ||||
|             let | ||||
|                 data = | ||||
|                     ItemsAndDate items date | ||||
|             in | ||||
|             Api.setDateMultiple flags data receive | ||||
|  | ||||
|         DueDateChange date -> | ||||
|             let | ||||
|                 data = | ||||
|                     ItemsAndDate items date | ||||
|             in | ||||
|             Api.setDueDateMultiple flags data receive | ||||
|  | ||||
|         OrgChange ref -> | ||||
|             let | ||||
|                 data = | ||||
|                     ItemsAndRef items (Maybe.map .id ref) | ||||
|             in | ||||
|             Api.setCorrOrgMultiple flags data receive | ||||
|  | ||||
|         CorrPersonChange ref -> | ||||
|             let | ||||
|                 data = | ||||
|                     ItemsAndRef items (Maybe.map .id ref) | ||||
|             in | ||||
|             Api.setCorrPersonMultiple flags data receive | ||||
|  | ||||
|         ConcPersonChange ref -> | ||||
|             let | ||||
|                 data = | ||||
|                     ItemsAndRef items (Maybe.map .id ref) | ||||
|             in | ||||
|             Api.setConcPersonMultiple flags data receive | ||||
|  | ||||
|         EquipChange ref -> | ||||
|             let | ||||
|                 data = | ||||
|                     ItemsAndRef items (Maybe.map .id ref) | ||||
|             in | ||||
|             Api.setConcEquipmentMultiple flags data receive | ||||
|  | ||||
|         NoFormChange -> | ||||
|             Cmd.none | ||||
| @@ -24,7 +24,6 @@ import File exposing (File) | ||||
| import Html exposing (..) | ||||
| import Html.Attributes exposing (..) | ||||
| import Html.Events exposing (onCheck, onClick, onInput) | ||||
| import Html5.DragDrop as DD | ||||
| import Markdown | ||||
| import Page exposing (Page(..)) | ||||
| import Set | ||||
|   | ||||
| @@ -6,6 +6,8 @@ module Comp.YesNoDimmer exposing | ||||
|     , defaultSettings | ||||
|     , disable | ||||
|     , emptyModel | ||||
|     , initActive | ||||
|     , initInactive | ||||
|     , update | ||||
|     , view | ||||
|     , view2 | ||||
| @@ -27,6 +29,18 @@ emptyModel = | ||||
|     } | ||||
|  | ||||
|  | ||||
| initInactive : Model | ||||
| initInactive = | ||||
|     { active = False | ||||
|     } | ||||
|  | ||||
|  | ||||
| initActive : Model | ||||
| initActive = | ||||
|     { active = True | ||||
|     } | ||||
|  | ||||
|  | ||||
| type Msg | ||||
|     = Activate | ||||
|     | Disable | ||||
| @@ -40,6 +54,7 @@ type alias Settings = | ||||
|     , confirmButton : String | ||||
|     , cancelButton : String | ||||
|     , invertedDimmer : Bool | ||||
|     , extraClass : String | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -51,6 +66,7 @@ defaultSettings = | ||||
|     , confirmButton = "Yes, do it!" | ||||
|     , cancelButton = "No" | ||||
|     , invertedDimmer = False | ||||
|     , extraClass = "" | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -87,6 +103,7 @@ view2 active settings model = | ||||
|     div | ||||
|         [ classList | ||||
|             [ ( "ui dimmer", True ) | ||||
|             , ( settings.extraClass, True ) | ||||
|             , ( "inverted", settings.invertedDimmer ) | ||||
|             , ( "active", active && model.active ) | ||||
|             ] | ||||
|   | ||||
							
								
								
									
										32
									
								
								modules/webapp/src/main/elm/Data/ItemSelection.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								modules/webapp/src/main/elm/Data/ItemSelection.elm
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| module Data.ItemSelection exposing | ||||
|     ( ItemSelection(..) | ||||
|     , isActive | ||||
|     , isSelected | ||||
|     ) | ||||
|  | ||||
| import Set exposing (Set) | ||||
|  | ||||
|  | ||||
| type ItemSelection | ||||
|     = Inactive | ||||
|     | Active (Set String) | ||||
|  | ||||
|  | ||||
| isSelected : String -> ItemSelection -> Bool | ||||
| isSelected id set = | ||||
|     case set of | ||||
|         Inactive -> | ||||
|             False | ||||
|  | ||||
|         Active ids -> | ||||
|             Set.member id ids | ||||
|  | ||||
|  | ||||
| isActive : ItemSelection -> Bool | ||||
| isActive sel = | ||||
|     case sel of | ||||
|         Active _ -> | ||||
|             True | ||||
|  | ||||
|         Inactive -> | ||||
|             False | ||||
| @@ -1,12 +1,16 @@ | ||||
| module Data.Items exposing | ||||
|     ( concat | ||||
|     , first | ||||
|     , idSet | ||||
|     , length | ||||
|     , replaceIn | ||||
|     ) | ||||
|  | ||||
| import Api.Model.ItemLight exposing (ItemLight) | ||||
| import Api.Model.ItemLightGroup exposing (ItemLightGroup) | ||||
| import Api.Model.ItemLightList exposing (ItemLightList) | ||||
| import Dict exposing (Dict) | ||||
| import Set exposing (Set) | ||||
| import Util.List | ||||
|  | ||||
|  | ||||
| @@ -65,3 +69,54 @@ lastGroup : ItemLightList -> Maybe ItemLightGroup | ||||
| lastGroup list = | ||||
|     List.reverse list.groups | ||||
|         |> List.head | ||||
|  | ||||
|  | ||||
| idSet : ItemLightList -> Set String | ||||
| idSet items = | ||||
|     List.map idSetGroup items.groups | ||||
|         |> List.foldl Set.union Set.empty | ||||
|  | ||||
|  | ||||
| idSetGroup : ItemLightGroup -> Set String | ||||
| idSetGroup group = | ||||
|     List.map .id group.items | ||||
|         |> Set.fromList | ||||
|  | ||||
|  | ||||
| replaceIn : ItemLightList -> ItemLightList -> ItemLightList | ||||
| replaceIn origin replacements = | ||||
|     let | ||||
|         newItems = | ||||
|             mkItemDict replacements | ||||
|  | ||||
|         replaceItem item = | ||||
|             case Dict.get item.id newItems of | ||||
|                 Just ni -> | ||||
|                     ni | ||||
|  | ||||
|                 Nothing -> | ||||
|                     item | ||||
|  | ||||
|         replaceGroup g = | ||||
|             List.map replaceItem g.items | ||||
|                 |> ItemLightGroup g.name | ||||
|     in | ||||
|     List.map replaceGroup origin.groups | ||||
|         |> ItemLightList | ||||
|  | ||||
|  | ||||
|  | ||||
| --- Helper | ||||
|  | ||||
|  | ||||
| mkItemDict : ItemLightList -> Dict String ItemLight | ||||
| mkItemDict list = | ||||
|     let | ||||
|         insertItems : Dict String ItemLight -> List ItemLight -> Dict String ItemLight | ||||
|         insertItems dict items = | ||||
|             List.foldl (\i -> \d -> Dict.insert i.id i d) dict items | ||||
|  | ||||
|         insertGroup dict groups = | ||||
|             List.foldl (\g -> \d -> insertItems d g.items) dict groups | ||||
|     in | ||||
|     insertGroup Dict.empty list.groups | ||||
|   | ||||
| @@ -2,26 +2,36 @@ module Page.Home.Data exposing | ||||
|     ( Model | ||||
|     , Msg(..) | ||||
|     , SearchType(..) | ||||
|     , SelectActionMode(..) | ||||
|     , SelectViewModel | ||||
|     , ViewMode(..) | ||||
|     , defaultSearchType | ||||
|     , doSearchCmd | ||||
|     , init | ||||
|     , initSelectViewModel | ||||
|     , itemNav | ||||
|     , menuCollapsed | ||||
|     , resultsBelowLimit | ||||
|     , searchTypeString | ||||
|     , selectActive | ||||
|     ) | ||||
|  | ||||
| import Api | ||||
| import Api.Model.BasicResult exposing (BasicResult) | ||||
| import Api.Model.ItemLightList exposing (ItemLightList) | ||||
| import Api.Model.ItemSearch | ||||
| import Browser.Dom as Dom | ||||
| import Comp.FixedDropdown | ||||
| import Comp.ItemCardList | ||||
| import Comp.ItemDetail.EditMenu | ||||
| import Comp.SearchMenu | ||||
| import Comp.YesNoDimmer | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.ItemNav exposing (ItemNav) | ||||
| import Data.Items | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| import Http | ||||
| import Set exposing (Set) | ||||
| import Throttle exposing (Throttle) | ||||
| import Util.Html exposing (KeyCode(..)) | ||||
| import Util.ItemDragDrop as DD | ||||
| @@ -31,7 +41,7 @@ type alias Model = | ||||
|     { searchMenuModel : Comp.SearchMenu.Model | ||||
|     , itemListModel : Comp.ItemCardList.Model | ||||
|     , searchInProgress : Bool | ||||
|     , menuCollapsed : Bool | ||||
|     , viewMode : ViewMode | ||||
|     , searchOffset : Int | ||||
|     , moreAvailable : Bool | ||||
|     , moreInProgress : Bool | ||||
| @@ -45,6 +55,29 @@ type alias Model = | ||||
|     } | ||||
|  | ||||
|  | ||||
| type alias SelectViewModel = | ||||
|     { ids : Set String | ||||
|     , action : SelectActionMode | ||||
|     , deleteAllConfirm : Comp.YesNoDimmer.Model | ||||
|     , editModel : Comp.ItemDetail.EditMenu.Model | ||||
|     } | ||||
|  | ||||
|  | ||||
| initSelectViewModel : SelectViewModel | ||||
| initSelectViewModel = | ||||
|     { ids = Set.empty | ||||
|     , action = NoneAction | ||||
|     , deleteAllConfirm = Comp.YesNoDimmer.initActive | ||||
|     , editModel = Comp.ItemDetail.EditMenu.init | ||||
|     } | ||||
|  | ||||
|  | ||||
| type ViewMode | ||||
|     = SimpleView | ||||
|     | SearchView | ||||
|     | SelectView SelectViewModel | ||||
|  | ||||
|  | ||||
| init : Flags -> Model | ||||
| init flags = | ||||
|     let | ||||
| @@ -58,7 +91,6 @@ init flags = | ||||
|     { searchMenuModel = Comp.SearchMenu.init | ||||
|     , itemListModel = Comp.ItemCardList.init | ||||
|     , searchInProgress = False | ||||
|     , menuCollapsed = True | ||||
|     , searchOffset = 0 | ||||
|     , moreAvailable = True | ||||
|     , moreInProgress = False | ||||
| @@ -72,6 +104,7 @@ init flags = | ||||
|     , dragDropData = | ||||
|         DD.DragDropData DD.init Nothing | ||||
|     , scrollToCard = Nothing | ||||
|     , viewMode = SimpleView | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -84,6 +117,32 @@ defaultSearchType flags = | ||||
|         BasicSearch | ||||
|  | ||||
|  | ||||
| menuCollapsed : Model -> Bool | ||||
| menuCollapsed model = | ||||
|     case model.viewMode of | ||||
|         SimpleView -> | ||||
|             True | ||||
|  | ||||
|         SearchView -> | ||||
|             False | ||||
|  | ||||
|         SelectView _ -> | ||||
|             False | ||||
|  | ||||
|  | ||||
| selectActive : Model -> Bool | ||||
| selectActive model = | ||||
|     case model.viewMode of | ||||
|         SimpleView -> | ||||
|             False | ||||
|  | ||||
|         SearchView -> | ||||
|             False | ||||
|  | ||||
|         SelectView _ -> | ||||
|             True | ||||
|  | ||||
|  | ||||
| type Msg | ||||
|     = Init | ||||
|     | SearchMenuMsg Comp.SearchMenu.Msg | ||||
| @@ -93,6 +152,7 @@ type Msg | ||||
|     | ItemSearchAddResp (Result Http.Error ItemLightList) | ||||
|     | DoSearch | ||||
|     | ToggleSearchMenu | ||||
|     | ToggleSelectView | ||||
|     | LoadMore | ||||
|     | UpdateThrottle | ||||
|     | SetBasicSearch String | ||||
| @@ -101,6 +161,15 @@ type Msg | ||||
|     | SetContentOnly String | ||||
|     | ScrollResult (Result Dom.Error ()) | ||||
|     | ClearItemDetailId | ||||
|     | SelectAllItems | ||||
|     | SelectNoItems | ||||
|     | RequestDeleteSelected | ||||
|     | DeleteSelectedConfirmMsg Comp.YesNoDimmer.Msg | ||||
|     | EditSelectedItems | ||||
|     | EditMenuMsg Comp.ItemDetail.EditMenu.Msg | ||||
|     | MultiUpdateResp (Result Http.Error BasicResult) | ||||
|     | ReplaceChangedItemsResp (Result Http.Error ItemLightList) | ||||
|     | DeleteAllResp (Result Http.Error BasicResult) | ||||
|  | ||||
|  | ||||
| type SearchType | ||||
| @@ -109,6 +178,12 @@ type SearchType | ||||
|     | ContentOnlySearch | ||||
|  | ||||
|  | ||||
| type SelectActionMode | ||||
|     = NoneAction | ||||
|     | DeleteSelected | ||||
|     | EditSelected | ||||
|  | ||||
|  | ||||
| searchTypeString : SearchType -> String | ||||
| searchTypeString st = | ||||
|     case st of | ||||
|   | ||||
| @@ -1,15 +1,25 @@ | ||||
| module Page.Home.Update exposing (update) | ||||
|  | ||||
| import Api | ||||
| import Api.Model.IdList exposing (IdList) | ||||
| import Api.Model.ItemLightList exposing (ItemLightList) | ||||
| import Api.Model.ItemSearch exposing (ItemSearch) | ||||
| import Browser.Navigation as Nav | ||||
| import Comp.FixedDropdown | ||||
| import Comp.ItemCardList | ||||
| import Comp.ItemDetail.EditMenu | ||||
| import Comp.ItemDetail.FormChange | ||||
| import Comp.SearchMenu | ||||
| import Comp.YesNoDimmer | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.ItemSelection | ||||
| import Data.Items | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| import Page exposing (Page(..)) | ||||
| import Page.Home.Data exposing (..) | ||||
| import Process | ||||
| import Scroll | ||||
| import Set exposing (Set) | ||||
| import Task | ||||
| import Throttle | ||||
| import Time | ||||
| @@ -82,10 +92,19 @@ update mId key flags settings msg model = | ||||
|                         flags | ||||
|                         m | ||||
|                         model.itemListModel | ||||
|  | ||||
|                 nextView = | ||||
|                     case ( model.viewMode, result.selection ) of | ||||
|                         ( SelectView svm, Data.ItemSelection.Active ids ) -> | ||||
|                             SelectView { svm | ids = ids } | ||||
|  | ||||
|                         ( v, _ ) -> | ||||
|                             v | ||||
|             in | ||||
|             withSub | ||||
|                 ( { model | ||||
|                     | itemListModel = result.model | ||||
|                     , viewMode = nextView | ||||
|                     , dragDropData = DD.DragDropData result.dragModel Nothing | ||||
|                   } | ||||
|                 , Cmd.batch [ Cmd.map ItemCardListMsg result.cmd ] | ||||
| @@ -159,11 +178,43 @@ update mId key flags settings msg model = | ||||
|                 doSearch flags settings False nm | ||||
|  | ||||
|         ToggleSearchMenu -> | ||||
|             let | ||||
|                 nextView = | ||||
|                     case model.viewMode of | ||||
|                         SimpleView -> | ||||
|                             SearchView | ||||
|  | ||||
|                         SearchView -> | ||||
|                             SimpleView | ||||
|  | ||||
|                         SelectView _ -> | ||||
|                             SimpleView | ||||
|             in | ||||
|             withSub | ||||
|                 ( { model | menuCollapsed = not model.menuCollapsed } | ||||
|                 ( { model | viewMode = nextView } | ||||
|                 , Cmd.none | ||||
|                 ) | ||||
|  | ||||
|         ToggleSelectView -> | ||||
|             let | ||||
|                 ( nextView, cmd ) = | ||||
|                     case model.viewMode of | ||||
|                         SimpleView -> | ||||
|                             ( SelectView initSelectViewModel, loadEditModel flags ) | ||||
|  | ||||
|                         SearchView -> | ||||
|                             ( SelectView initSelectViewModel, loadEditModel flags ) | ||||
|  | ||||
|                         SelectView _ -> | ||||
|                             ( SearchView, Cmd.none ) | ||||
|             in | ||||
|             withSub | ||||
|                 ( { model | ||||
|                     | viewMode = nextView | ||||
|                   } | ||||
|                 , cmd | ||||
|                 ) | ||||
|  | ||||
|         LoadMore -> | ||||
|             if model.moreAvailable then | ||||
|                 doSearchMore flags settings model |> withSub | ||||
| @@ -253,11 +304,223 @@ update mId key flags settings msg model = | ||||
|         ClearItemDetailId -> | ||||
|             noSub ( { model | scrollToCard = Nothing }, Cmd.none ) | ||||
|  | ||||
|         SelectAllItems -> | ||||
|             case model.viewMode of | ||||
|                 SelectView svm -> | ||||
|                     let | ||||
|                         visible = | ||||
|                             Data.Items.idSet model.itemListModel.results | ||||
|  | ||||
|                         svm_ = | ||||
|                             { svm | ids = Set.union svm.ids visible } | ||||
|                     in | ||||
|                     noSub | ||||
|                         ( { model | viewMode = SelectView svm_ } | ||||
|                         , Cmd.none | ||||
|                         ) | ||||
|  | ||||
|                 _ -> | ||||
|                     noSub ( model, Cmd.none ) | ||||
|  | ||||
|         SelectNoItems -> | ||||
|             case model.viewMode of | ||||
|                 SelectView svm -> | ||||
|                     let | ||||
|                         svm_ = | ||||
|                             { svm | ids = Set.empty } | ||||
|                     in | ||||
|                     noSub | ||||
|                         ( { model | viewMode = SelectView svm_ } | ||||
|                         , Cmd.none | ||||
|                         ) | ||||
|  | ||||
|                 _ -> | ||||
|                     noSub ( model, Cmd.none ) | ||||
|  | ||||
|         DeleteSelectedConfirmMsg lmsg -> | ||||
|             case model.viewMode of | ||||
|                 SelectView svm -> | ||||
|                     let | ||||
|                         ( confirmModel, confirmed ) = | ||||
|                             Comp.YesNoDimmer.update lmsg svm.deleteAllConfirm | ||||
|  | ||||
|                         cmd = | ||||
|                             if confirmed then | ||||
|                                 Api.deleteAllItems flags svm.ids DeleteAllResp | ||||
|  | ||||
|                             else | ||||
|                                 Cmd.none | ||||
|  | ||||
|                         act = | ||||
|                             if confirmModel.active || confirmed then | ||||
|                                 DeleteSelected | ||||
|  | ||||
|                             else | ||||
|                                 NoneAction | ||||
|                     in | ||||
|                     noSub | ||||
|                         ( { model | ||||
|                             | viewMode = | ||||
|                                 SelectView | ||||
|                                     { svm | ||||
|                                         | deleteAllConfirm = confirmModel | ||||
|                                         , action = act | ||||
|                                     } | ||||
|                           } | ||||
|                         , cmd | ||||
|                         ) | ||||
|  | ||||
|                 _ -> | ||||
|                     noSub ( model, Cmd.none ) | ||||
|  | ||||
|         DeleteAllResp (Ok res) -> | ||||
|             if res.success then | ||||
|                 let | ||||
|                     nm = | ||||
|                         { model | viewMode = SearchView } | ||||
|                 in | ||||
|                 doSearch flags settings False nm | ||||
|  | ||||
|             else | ||||
|                 noSub ( model, Cmd.none ) | ||||
|  | ||||
|         DeleteAllResp (Err _) -> | ||||
|             noSub ( model, Cmd.none ) | ||||
|  | ||||
|         RequestDeleteSelected -> | ||||
|             case model.viewMode of | ||||
|                 SelectView svm -> | ||||
|                     if svm.ids == Set.empty then | ||||
|                         noSub ( model, Cmd.none ) | ||||
|  | ||||
|                     else | ||||
|                         let | ||||
|                             lmsg = | ||||
|                                 DeleteSelectedConfirmMsg Comp.YesNoDimmer.activate | ||||
|  | ||||
|                             model_ = | ||||
|                                 { model | viewMode = SelectView { svm | action = DeleteSelected } } | ||||
|                         in | ||||
|                         update mId key flags settings lmsg model_ | ||||
|  | ||||
|                 _ -> | ||||
|                     noSub ( model, Cmd.none ) | ||||
|  | ||||
|         EditSelectedItems -> | ||||
|             case model.viewMode of | ||||
|                 SelectView svm -> | ||||
|                     if svm.action == EditSelected then | ||||
|                         noSub | ||||
|                             ( { model | viewMode = SelectView { svm | action = NoneAction } } | ||||
|                             , Cmd.none | ||||
|                             ) | ||||
|  | ||||
|                     else if svm.ids == Set.empty then | ||||
|                         noSub ( model, Cmd.none ) | ||||
|  | ||||
|                     else | ||||
|                         noSub | ||||
|                             ( { model | viewMode = SelectView { svm | action = EditSelected } } | ||||
|                             , Cmd.none | ||||
|                             ) | ||||
|  | ||||
|                 _ -> | ||||
|                     noSub ( model, Cmd.none ) | ||||
|  | ||||
|         EditMenuMsg lmsg -> | ||||
|             case model.viewMode of | ||||
|                 SelectView svm -> | ||||
|                     let | ||||
|                         res = | ||||
|                             Comp.ItemDetail.EditMenu.update flags lmsg svm.editModel | ||||
|  | ||||
|                         svm_ = | ||||
|                             { svm | editModel = res.model } | ||||
|  | ||||
|                         cmd_ = | ||||
|                             Cmd.map EditMenuMsg res.cmd | ||||
|  | ||||
|                         sub_ = | ||||
|                             Sub.map EditMenuMsg res.sub | ||||
|  | ||||
|                         upCmd = | ||||
|                             Comp.ItemDetail.FormChange.multiUpdate flags | ||||
|                                 svm.ids | ||||
|                                 res.change | ||||
|                                 MultiUpdateResp | ||||
|                     in | ||||
|                     ( { model | viewMode = SelectView svm_ } | ||||
|                     , Cmd.batch [ cmd_, upCmd ] | ||||
|                     , sub_ | ||||
|                     ) | ||||
|  | ||||
|                 _ -> | ||||
|                     noSub ( model, Cmd.none ) | ||||
|  | ||||
|         MultiUpdateResp (Ok res) -> | ||||
|             if res.success then | ||||
|                 case model.viewMode of | ||||
|                     SelectView svm -> | ||||
|                         -- replace changed items in the view | ||||
|                         noSub ( model, loadChangedItems flags svm.ids ) | ||||
|  | ||||
|                     _ -> | ||||
|                         noSub ( model, Cmd.none ) | ||||
|  | ||||
|             else | ||||
|                 noSub ( model, Cmd.none ) | ||||
|  | ||||
|         MultiUpdateResp (Err _) -> | ||||
|             noSub ( model, Cmd.none ) | ||||
|  | ||||
|         ReplaceChangedItemsResp (Ok items) -> | ||||
|             noSub ( replaceItems model items, Cmd.none ) | ||||
|  | ||||
|         ReplaceChangedItemsResp (Err _) -> | ||||
|             noSub ( model, Cmd.none ) | ||||
|  | ||||
|  | ||||
|  | ||||
| --- Helpers | ||||
|  | ||||
|  | ||||
| replaceItems : Model -> ItemLightList -> Model | ||||
| replaceItems model newItems = | ||||
|     let | ||||
|         listModel = | ||||
|             model.itemListModel | ||||
|  | ||||
|         changed = | ||||
|             Data.Items.replaceIn listModel.results newItems | ||||
|  | ||||
|         newList = | ||||
|             { listModel | results = changed } | ||||
|     in | ||||
|     { model | itemListModel = newList } | ||||
|  | ||||
|  | ||||
| loadChangedItems : Flags -> Set String -> Cmd Msg | ||||
| loadChangedItems flags ids = | ||||
|     if Set.isEmpty ids then | ||||
|         Cmd.none | ||||
|  | ||||
|     else | ||||
|         let | ||||
|             searchInit = | ||||
|                 Api.Model.ItemSearch.empty | ||||
|  | ||||
|             idList = | ||||
|                 IdList (Set.toList ids) | ||||
|  | ||||
|             search = | ||||
|                 { searchInit | ||||
|                     | itemSubset = Just idList | ||||
|                     , limit = Set.size ids | ||||
|                 } | ||||
|         in | ||||
|         Api.itemSearch flags search ReplaceChangedItemsResp | ||||
|  | ||||
|  | ||||
| scrollToCard : Maybe String -> Model -> ( Model, Cmd Msg, Sub Msg ) | ||||
| scrollToCard mId model = | ||||
|     let | ||||
| @@ -275,12 +538,17 @@ scrollToCard mId model = | ||||
|             ( model, Cmd.none, Sub.none ) | ||||
|  | ||||
|  | ||||
| loadEditModel : Flags -> Cmd Msg | ||||
| loadEditModel flags = | ||||
|     Cmd.map EditMenuMsg (Comp.ItemDetail.EditMenu.loadModel flags) | ||||
|  | ||||
|  | ||||
| doSearch : Flags -> UiSettings -> Bool -> Model -> ( Model, Cmd Msg, Sub Msg ) | ||||
| doSearch flags settings scroll model = | ||||
|     let | ||||
|         stype = | ||||
|             if | ||||
|                 not model.menuCollapsed | ||||
|                 not (menuCollapsed model) | ||||
|                     || Util.String.isNothingOrBlank model.contentOnlySearch | ||||
|             then | ||||
|                 BasicSearch | ||||
|   | ||||
| @@ -3,26 +3,51 @@ module Page.Home.View exposing (view) | ||||
| import Api.Model.ItemSearch | ||||
| import Comp.FixedDropdown | ||||
| import Comp.ItemCardList | ||||
| import Comp.ItemDetail.EditMenu | ||||
| import Comp.SearchMenu | ||||
| import Comp.YesNoDimmer | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.ItemSelection | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| import Html exposing (..) | ||||
| import Html.Attributes exposing (..) | ||||
| import Html.Events exposing (onClick, onInput) | ||||
| import Page exposing (Page(..)) | ||||
| import Page.Home.Data exposing (..) | ||||
| import Set | ||||
| import Util.Html | ||||
|  | ||||
|  | ||||
| view : Flags -> UiSettings -> Model -> Html Msg | ||||
| view flags settings model = | ||||
|     let | ||||
|         itemViewCfg = | ||||
|             case model.viewMode of | ||||
|                 SelectView svm -> | ||||
|                     Comp.ItemCardList.ViewConfig | ||||
|                         model.scrollToCard | ||||
|                         (Data.ItemSelection.Active svm.ids) | ||||
|  | ||||
|                 _ -> | ||||
|                     Comp.ItemCardList.ViewConfig | ||||
|                         model.scrollToCard | ||||
|                         Data.ItemSelection.Inactive | ||||
|  | ||||
|         selectAction = | ||||
|             case model.viewMode of | ||||
|                 SelectView svm -> | ||||
|                     svm.action | ||||
|  | ||||
|                 _ -> | ||||
|                     NoneAction | ||||
|     in | ||||
|     div [ class "home-page ui padded grid" ] | ||||
|         [ div | ||||
|             [ classList | ||||
|                 [ ( "sixteen wide mobile six wide tablet four wide computer search-menu column" | ||||
|                   , True | ||||
|                   ) | ||||
|                 , ( "invisible hidden", model.menuCollapsed ) | ||||
|                 , ( "invisible hidden", menuCollapsed model ) | ||||
|                 ] | ||||
|             ] | ||||
|             [ div | ||||
| @@ -38,6 +63,17 @@ view flags settings model = | ||||
|                     ] | ||||
|                 , div [ class "right floated menu" ] | ||||
|                     [ a | ||||
|                         [ classList | ||||
|                             [ ( "borderless item", True ) | ||||
|                             , ( "active", selectActive model ) | ||||
|                             ] | ||||
|                         , href "#" | ||||
|                         , title "Toggle select items" | ||||
|                         , onClick ToggleSelectView | ||||
|                         ] | ||||
|                         [ i [ class "tasks icon" ] [] | ||||
|                         ] | ||||
|                     , a | ||||
|                         [ class "borderless item" | ||||
|                         , onClick ResetSearch | ||||
|                         , title "Reset form" | ||||
| @@ -63,26 +99,30 @@ view flags settings model = | ||||
|                     ] | ||||
|                 ] | ||||
|             , div [ class "" ] | ||||
|                 [ Html.map SearchMenuMsg | ||||
|                     (Comp.SearchMenu.viewDrop model.dragDropData | ||||
|                         flags | ||||
|                         settings | ||||
|                         model.searchMenuModel | ||||
|                     ) | ||||
|                 ] | ||||
|                 (viewLeftMenu flags settings model) | ||||
|             ] | ||||
|         , div | ||||
|             [ classList | ||||
|                 [ ( "sixteen wide mobile ten wide tablet twelve wide computer column" | ||||
|                   , not model.menuCollapsed | ||||
|                   , not (menuCollapsed model) | ||||
|                   ) | ||||
|                 , ( "sixteen wide column", model.menuCollapsed ) | ||||
|                 , ( "sixteen wide column", menuCollapsed model ) | ||||
|                 , ( "item-card-list", True ) | ||||
|                 ] | ||||
|             ] | ||||
|             [ viewSearchBar flags model | ||||
|             [ viewBar flags model | ||||
|             , case model.viewMode of | ||||
|                 SelectView svm -> | ||||
|                     Html.map DeleteSelectedConfirmMsg | ||||
|                         (Comp.YesNoDimmer.view2 (selectAction == DeleteSelected) | ||||
|                             deleteAllDimmer | ||||
|                             svm.deleteAllConfirm | ||||
|                         ) | ||||
|  | ||||
|                 _ -> | ||||
|                     span [ class "invisible" ] [] | ||||
|             , Html.map ItemCardListMsg | ||||
|                 (Comp.ItemCardList.view model.scrollToCard settings model.itemListModel) | ||||
|                 (Comp.ItemCardList.view itemViewCfg settings model.itemListModel) | ||||
|             ] | ||||
|         , div | ||||
|             [ classList | ||||
| @@ -117,6 +157,113 @@ view flags settings model = | ||||
|         ] | ||||
|  | ||||
|  | ||||
| viewLeftMenu : Flags -> UiSettings -> Model -> List (Html Msg) | ||||
| viewLeftMenu flags settings model = | ||||
|     let | ||||
|         searchMenu = | ||||
|             [ Html.map SearchMenuMsg | ||||
|                 (Comp.SearchMenu.viewDrop model.dragDropData | ||||
|                     flags | ||||
|                     settings | ||||
|                     model.searchMenuModel | ||||
|                 ) | ||||
|             ] | ||||
|     in | ||||
|     case model.viewMode of | ||||
|         SelectView svm -> | ||||
|             case svm.action of | ||||
|                 EditSelected -> | ||||
|                     let | ||||
|                         cfg = | ||||
|                             Comp.ItemDetail.EditMenu.defaultViewConfig | ||||
|                     in | ||||
|                     [ div [ class "ui dividing header" ] | ||||
|                         [ text "Multi-Edit" | ||||
|                         ] | ||||
|                     , div [ class "ui info message" ] | ||||
|                         [ text "Note that a change here immediatly affects all selected items on the right!" | ||||
|                         ] | ||||
|                     , Html.map EditMenuMsg | ||||
|                         (Comp.ItemDetail.EditMenu.view cfg settings svm.editModel) | ||||
|                     ] | ||||
|  | ||||
|                 _ -> | ||||
|                     searchMenu | ||||
|  | ||||
|         _ -> | ||||
|             searchMenu | ||||
|  | ||||
|  | ||||
| viewBar : Flags -> Model -> Html Msg | ||||
| viewBar flags model = | ||||
|     case model.viewMode of | ||||
|         SimpleView -> | ||||
|             viewSearchBar flags model | ||||
|  | ||||
|         SearchView -> | ||||
|             div [ class "hidden invisible" ] [] | ||||
|  | ||||
|         SelectView svm -> | ||||
|             viewActionBar flags svm model | ||||
|  | ||||
|  | ||||
| viewActionBar : Flags -> SelectViewModel -> Model -> Html Msg | ||||
| viewActionBar _ svm model = | ||||
|     let | ||||
|         selectCount = | ||||
|             Set.size svm.ids |> String.fromInt | ||||
|     in | ||||
|     div | ||||
|         [ class "ui ablue-comp icon menu" | ||||
|         ] | ||||
|         [ a | ||||
|             [ classList | ||||
|                 [ ( "borderless item", True ) | ||||
|                 , ( "active", svm.action == EditSelected ) | ||||
|                 ] | ||||
|             , href "#" | ||||
|             , title <| "Edit " ++ selectCount ++ " selected items" | ||||
|             , onClick EditSelectedItems | ||||
|             ] | ||||
|             [ i [ class "ui edit icon" ] [] | ||||
|             ] | ||||
|         , a | ||||
|             [ classList | ||||
|                 [ ( "borderless item", True ) | ||||
|                 , ( "active", svm.action == DeleteSelected ) | ||||
|                 ] | ||||
|             , href "#" | ||||
|             , title <| "Delete " ++ selectCount ++ " selected items" | ||||
|             , onClick RequestDeleteSelected | ||||
|             ] | ||||
|             [ i [ class "trash icon" ] [] | ||||
|             ] | ||||
|         , div [ class "right menu" ] | ||||
|             [ a | ||||
|                 [ class "item" | ||||
|                 , href "#" | ||||
|                 , onClick SelectAllItems | ||||
|                 , title "Select all" | ||||
|                 ] | ||||
|                 [ i [ class "check square outline icon" ] [] | ||||
|                 ] | ||||
|             , a | ||||
|                 [ class "borderless item" | ||||
|                 , href "#" | ||||
|                 , title "Select none" | ||||
|                 , onClick SelectNoItems | ||||
|                 ] | ||||
|                 [ i [ class "square outline icon" ] [] | ||||
|                 ] | ||||
|             , div [ class "borderless label item" ] | ||||
|                 [ div [ class "ui circular purple icon label" ] | ||||
|                     [ text selectCount | ||||
|                     ] | ||||
|                 ] | ||||
|             ] | ||||
|         ] | ||||
|  | ||||
|  | ||||
| viewSearchBar : Flags -> Model -> Html Msg | ||||
| viewSearchBar flags model = | ||||
|     let | ||||
| @@ -145,7 +292,7 @@ viewSearchBar flags model = | ||||
|     in | ||||
|     div | ||||
|         [ classList | ||||
|             [ ( "invisible hidden", not model.menuCollapsed ) | ||||
|             [ ( "invisible hidden", not (menuCollapsed model) ) | ||||
|             , ( "ui secondary stackable menu container", True ) | ||||
|             ] | ||||
|         ] | ||||
| @@ -221,3 +368,15 @@ hasMoreSearch model = | ||||
|                     Api.Model.ItemSearch.empty | ||||
|     in | ||||
|     is_ /= Api.Model.ItemSearch.empty | ||||
|  | ||||
|  | ||||
| deleteAllDimmer : Comp.YesNoDimmer.Settings | ||||
| deleteAllDimmer = | ||||
|     { message = "Really delete all selected items?" | ||||
|     , headerIcon = "exclamation icon" | ||||
|     , headerClass = "ui inverted icon header" | ||||
|     , confirmButton = "Yes" | ||||
|     , cancelButton = "No" | ||||
|     , invertedDimmer = False | ||||
|     , extraClass = "top aligned" | ||||
|     } | ||||
|   | ||||
| @@ -31,6 +31,9 @@ | ||||
|     margin: 0 1em; | ||||
| } | ||||
|  | ||||
| .default-layout .ui.icon.menu .label.item { | ||||
|     padding: 0 0.5em 0 0; | ||||
| } | ||||
|  | ||||
| .default-layout .right-float { | ||||
|     float: right; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user