mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-06 15:15:58 +00:00
commit
324476c1a9
13
Changelog.md
13
Changelog.md
@ -8,18 +8,21 @@
|
|||||||
periodically to import your mails.
|
periodically to import your mails.
|
||||||
- New feature "Integration Endpoint". Allows an admin to upload files
|
- New feature "Integration Endpoint". Allows an admin to upload files
|
||||||
to any collective using a separate endpoint.
|
to any collective using a separate endpoint.
|
||||||
|
- New feature: add files to existing items.
|
||||||
|
- The document list on the front-page has been rewritten. The table is
|
||||||
|
removed and documents are now presented in a “card view”.
|
||||||
|
- Amend the mail-to-pdf conversion to include the e-mail date.
|
||||||
|
- When processing e-mails, set the item date automatically from the
|
||||||
|
received-date in the mail.
|
||||||
|
- Fixes regarding character encodings when reading e-mails.
|
||||||
- Fix the `find-by-checksum` route that, given a sha256 checksum,
|
- Fix the `find-by-checksum` route that, given a sha256 checksum,
|
||||||
returns whether there is such a file in docspell. It falsely
|
returns whether there is such a file in docspell. It falsely
|
||||||
returned `false` although documents existed.
|
returned `false` although documents existed.
|
||||||
- Amend the mail-to-pdf conversion to include the e-mail date.
|
|
||||||
- Fix webapp for mobile devices.
|
- Fix webapp for mobile devices.
|
||||||
- The document list on the front-page has been rewritten. The table is
|
|
||||||
removed and documents are now presented in a “card view”.
|
|
||||||
- Fix the search menu to remember dates in fields. When going back
|
- Fix the search menu to remember dates in fields. When going back
|
||||||
from an item detail to the front-page, the search menu remembers the
|
from an item detail to the front-page, the search menu remembers the
|
||||||
last state, but dates were cleared.
|
last state, but dates were cleared.
|
||||||
- More fixes regarding character encodings when reading e-mails.
|
- Fix redirecting `/` only to `/app`.
|
||||||
- Fix redirecting `/` to `/app`.
|
|
||||||
|
|
||||||
### Configuration Changes
|
### Configuration Changes
|
||||||
|
|
||||||
|
@ -19,6 +19,15 @@ private[analysis] object Tld {
|
|||||||
".edu",
|
".edu",
|
||||||
".gov",
|
".gov",
|
||||||
".mil",
|
".mil",
|
||||||
|
".info",
|
||||||
|
".app",
|
||||||
|
".bar",
|
||||||
|
".biz",
|
||||||
|
".club",
|
||||||
|
".coop",
|
||||||
|
".icu",
|
||||||
|
".name",
|
||||||
|
".xyz",
|
||||||
".ad",
|
".ad",
|
||||||
".ae",
|
".ae",
|
||||||
".al",
|
".al",
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
package docspell.backend.ops
|
package docspell.backend.ops
|
||||||
|
|
||||||
import bitpeace.MimetypeHint
|
import bitpeace.MimetypeHint
|
||||||
import cats.implicits._
|
import cats.Functor
|
||||||
|
import cats.data.{EitherT, OptionT}
|
||||||
import cats.effect._
|
import cats.effect._
|
||||||
|
import cats.implicits._
|
||||||
import docspell.backend.Config
|
import docspell.backend.Config
|
||||||
import fs2.Stream
|
import fs2.Stream
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.common.syntax.all._
|
import docspell.common.syntax.all._
|
||||||
import docspell.store.Store
|
import docspell.store.Store
|
||||||
import docspell.store.queue.JobQueue
|
import docspell.store.queue.JobQueue
|
||||||
import docspell.store.records.{RCollective, RJob, RSource}
|
import docspell.store.records._
|
||||||
import org.log4s._
|
import org.log4s._
|
||||||
|
|
||||||
trait OUpload[F[_]] {
|
trait OUpload[F[_]] {
|
||||||
@ -17,14 +19,29 @@ trait OUpload[F[_]] {
|
|||||||
def submit(
|
def submit(
|
||||||
data: OUpload.UploadData[F],
|
data: OUpload.UploadData[F],
|
||||||
account: AccountId,
|
account: AccountId,
|
||||||
notifyJoex: Boolean
|
notifyJoex: Boolean,
|
||||||
|
itemId: Option[Ident]
|
||||||
): F[OUpload.UploadResult]
|
): F[OUpload.UploadResult]
|
||||||
|
|
||||||
def submit(
|
def submit(
|
||||||
data: OUpload.UploadData[F],
|
data: OUpload.UploadData[F],
|
||||||
sourceId: Ident,
|
sourceId: Ident,
|
||||||
notifyJoex: Boolean
|
notifyJoex: Boolean,
|
||||||
|
itemId: Option[Ident]
|
||||||
): F[OUpload.UploadResult]
|
): F[OUpload.UploadResult]
|
||||||
|
|
||||||
|
final def submitEither(
|
||||||
|
data: OUpload.UploadData[F],
|
||||||
|
accOrSrc: Either[Ident, AccountId],
|
||||||
|
notifyJoex: Boolean,
|
||||||
|
itemId: Option[Ident]
|
||||||
|
): F[OUpload.UploadResult] =
|
||||||
|
accOrSrc match {
|
||||||
|
case Right(acc) =>
|
||||||
|
submit(data, acc, notifyJoex, itemId)
|
||||||
|
case Left(srcId) =>
|
||||||
|
submit(data, srcId, notifyJoex, itemId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object OUpload {
|
object OUpload {
|
||||||
@ -52,11 +69,32 @@ object OUpload {
|
|||||||
|
|
||||||
sealed trait UploadResult
|
sealed trait UploadResult
|
||||||
object UploadResult {
|
object UploadResult {
|
||||||
|
|
||||||
|
/** File(s) have been successfully submitted. */
|
||||||
case object Success extends UploadResult
|
case object Success extends UploadResult
|
||||||
|
|
||||||
|
def success: UploadResult = Success
|
||||||
|
|
||||||
|
/** There were no files to submit. */
|
||||||
case object NoFiles extends UploadResult
|
case object NoFiles extends UploadResult
|
||||||
|
|
||||||
|
def noFiles: UploadResult = NoFiles
|
||||||
|
|
||||||
|
/** A source (`RSource') could not be found for a given source-id. */
|
||||||
case object NoSource extends UploadResult
|
case object NoSource extends UploadResult
|
||||||
|
|
||||||
|
def noSource: UploadResult = NoSource
|
||||||
|
|
||||||
|
/** When adding files to an item, no item was found using the given
|
||||||
|
* item-id. */
|
||||||
|
case object NoItem extends UploadResult
|
||||||
|
|
||||||
|
def noItem: UploadResult = NoItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def right[F[_]: Functor, A](a: F[A]): EitherT[F, UploadResult, A] =
|
||||||
|
EitherT.right(a)
|
||||||
|
|
||||||
def apply[F[_]: Sync](
|
def apply[F[_]: Sync](
|
||||||
store: Store[F],
|
store: Store[F],
|
||||||
queue: JobQueue[F],
|
queue: JobQueue[F],
|
||||||
@ -68,14 +106,17 @@ object OUpload {
|
|||||||
def submit(
|
def submit(
|
||||||
data: OUpload.UploadData[F],
|
data: OUpload.UploadData[F],
|
||||||
account: AccountId,
|
account: AccountId,
|
||||||
notifyJoex: Boolean
|
notifyJoex: Boolean,
|
||||||
|
itemId: Option[Ident]
|
||||||
): F[OUpload.UploadResult] =
|
): F[OUpload.UploadResult] =
|
||||||
for {
|
(for {
|
||||||
files <- data.files.traverse(saveFile).map(_.flatten)
|
_ <- checkExistingItem(itemId, account.collective)
|
||||||
pred <- checkFileList(files)
|
files <- right(data.files.traverse(saveFile).map(_.flatten))
|
||||||
lang <- store.transact(RCollective.findLanguage(account.collective))
|
_ <- checkFileList(files)
|
||||||
|
lang <- right(store.transact(RCollective.findLanguage(account.collective)))
|
||||||
meta = ProcessItemArgs.ProcessMeta(
|
meta = ProcessItemArgs.ProcessMeta(
|
||||||
account.collective,
|
account.collective,
|
||||||
|
itemId,
|
||||||
lang.getOrElse(Language.German),
|
lang.getOrElse(Language.German),
|
||||||
data.meta.direction,
|
data.meta.direction,
|
||||||
data.meta.sourceAbbrev,
|
data.meta.sourceAbbrev,
|
||||||
@ -84,29 +125,31 @@ object OUpload {
|
|||||||
args =
|
args =
|
||||||
if (data.multiple) files.map(f => ProcessItemArgs(meta, List(f)))
|
if (data.multiple) files.map(f => ProcessItemArgs(meta, List(f)))
|
||||||
else Vector(ProcessItemArgs(meta, files.toList))
|
else Vector(ProcessItemArgs(meta, files.toList))
|
||||||
job <- pred.traverse(_ => makeJobs(args, account, data.priority, data.tracker))
|
jobs <- right(makeJobs(args, account, data.priority, data.tracker))
|
||||||
_ <- logger.fdebug(s"Storing jobs: $job")
|
_ <- right(logger.fdebug(s"Storing jobs: $jobs"))
|
||||||
res <- job.traverse(submitJobs(notifyJoex))
|
res <- right(submitJobs(notifyJoex)(jobs))
|
||||||
_ <- store.transact(
|
_ <- right(
|
||||||
|
store.transact(
|
||||||
RSource.incrementCounter(data.meta.sourceAbbrev, account.collective)
|
RSource.incrementCounter(data.meta.sourceAbbrev, account.collective)
|
||||||
)
|
)
|
||||||
} yield res.fold(identity, identity)
|
)
|
||||||
|
} yield res).fold(identity, identity)
|
||||||
|
|
||||||
def submit(
|
def submit(
|
||||||
data: OUpload.UploadData[F],
|
data: OUpload.UploadData[F],
|
||||||
sourceId: Ident,
|
sourceId: Ident,
|
||||||
notifyJoex: Boolean
|
notifyJoex: Boolean,
|
||||||
|
itemId: Option[Ident]
|
||||||
): F[OUpload.UploadResult] =
|
): F[OUpload.UploadResult] =
|
||||||
for {
|
(for {
|
||||||
sOpt <-
|
src <- OptionT(store.transact(RSource.find(sourceId)))
|
||||||
store
|
updata = data.copy(
|
||||||
.transact(RSource.find(sourceId))
|
meta = data.meta.copy(sourceAbbrev = src.abbrev),
|
||||||
.map(_.toRight(UploadResult.NoSource))
|
priority = src.priority
|
||||||
abbrev = sOpt.map(_.abbrev).toOption.getOrElse(data.meta.sourceAbbrev)
|
)
|
||||||
updata = data.copy(meta = data.meta.copy(sourceAbbrev = abbrev))
|
accId = AccountId(src.cid, src.sid)
|
||||||
accId = sOpt.map(source => AccountId(source.cid, source.sid))
|
result <- OptionT.liftF(submit(updata, accId, notifyJoex, itemId))
|
||||||
result <- accId.traverse(acc => submit(updata, acc, notifyJoex))
|
} yield result).getOrElse(UploadResult.noSource)
|
||||||
} yield result.fold(identity, identity)
|
|
||||||
|
|
||||||
private def submitJobs(
|
private def submitJobs(
|
||||||
notifyJoex: Boolean
|
notifyJoex: Boolean
|
||||||
@ -117,6 +160,7 @@ object OUpload {
|
|||||||
_ <- if (notifyJoex) joex.notifyAllNodes else ().pure[F]
|
_ <- if (notifyJoex) joex.notifyAllNodes else ().pure[F]
|
||||||
} yield UploadResult.Success
|
} yield UploadResult.Success
|
||||||
|
|
||||||
|
/** Saves the file into the database. */
|
||||||
private def saveFile(file: File[F]): F[Option[ProcessItemArgs.File]] =
|
private def saveFile(file: File[F]): F[Option[ProcessItemArgs.File]] =
|
||||||
logger.finfo(s"Receiving file $file") *>
|
logger.finfo(s"Receiving file $file") *>
|
||||||
store.bitpeace
|
store.bitpeace
|
||||||
@ -135,10 +179,24 @@ object OUpload {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private def checkExistingItem(
|
||||||
|
itemId: Option[Ident],
|
||||||
|
coll: Ident
|
||||||
|
): EitherT[F, UploadResult, Unit] =
|
||||||
|
itemId match {
|
||||||
|
case None =>
|
||||||
|
right(().pure[F])
|
||||||
|
case Some(id) =>
|
||||||
|
OptionT(store.transact(RItem.findByIdAndCollective(id, coll)))
|
||||||
|
.toRight(UploadResult.noItem)
|
||||||
|
.map(_ => ())
|
||||||
|
}
|
||||||
|
|
||||||
private def checkFileList(
|
private def checkFileList(
|
||||||
files: Seq[ProcessItemArgs.File]
|
files: Seq[ProcessItemArgs.File]
|
||||||
): F[Either[UploadResult, Unit]] =
|
): EitherT[F, UploadResult, Unit] =
|
||||||
Sync[F].pure(if (files.isEmpty) Left(UploadResult.NoFiles) else Right(()))
|
if (files.isEmpty) EitherT.left(UploadResult.noFiles.pure[F])
|
||||||
|
else right(().pure[F])
|
||||||
|
|
||||||
private def makeJobs(
|
private def makeJobs(
|
||||||
args: Vector[ProcessItemArgs],
|
args: Vector[ProcessItemArgs],
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
package docspell.common
|
package docspell.common
|
||||||
|
|
||||||
import io.circe.{Decoder, Encoder}
|
import io.circe.{Decoder, Encoder}
|
||||||
|
import cats.data.NonEmptyList
|
||||||
|
|
||||||
sealed trait ItemState { self: Product =>
|
sealed trait ItemState { self: Product =>
|
||||||
|
|
||||||
final def name: String =
|
final def name: String =
|
||||||
productPrefix.toLowerCase
|
productPrefix.toLowerCase
|
||||||
|
|
||||||
|
def isValid: Boolean =
|
||||||
|
ItemState.validStates.exists(_ == this)
|
||||||
|
|
||||||
|
def isInvalid: Boolean =
|
||||||
|
ItemState.invalidStates.exists(_ == this)
|
||||||
}
|
}
|
||||||
|
|
||||||
object ItemState {
|
object ItemState {
|
||||||
@ -24,8 +31,11 @@ object ItemState {
|
|||||||
case _ => Left(s"Invalid item state: $str")
|
case _ => Left(s"Invalid item state: $str")
|
||||||
}
|
}
|
||||||
|
|
||||||
val validStates: Seq[ItemState] =
|
val validStates: NonEmptyList[ItemState] =
|
||||||
Seq(Created, Confirmed)
|
NonEmptyList.of(Created, Confirmed)
|
||||||
|
|
||||||
|
val invalidStates: NonEmptyList[ItemState] =
|
||||||
|
NonEmptyList.of(Premature, Processing)
|
||||||
|
|
||||||
def unsafe(str: String): ItemState =
|
def unsafe(str: String): ItemState =
|
||||||
fromString(str).fold(sys.error, identity)
|
fromString(str).fold(sys.error, identity)
|
||||||
|
@ -4,6 +4,14 @@ import io.circe._, io.circe.generic.semiauto._
|
|||||||
import docspell.common.syntax.all._
|
import docspell.common.syntax.all._
|
||||||
import ProcessItemArgs._
|
import ProcessItemArgs._
|
||||||
|
|
||||||
|
/** Arguments to the process-item task.
|
||||||
|
*
|
||||||
|
* This task is run for each new file to create a new item from it or
|
||||||
|
* to add this file as an attachment to an existing item.
|
||||||
|
*
|
||||||
|
* If the `itemId' is set to some value, the item is tried to load to
|
||||||
|
* ammend with the given files. Otherwise a new item is created.
|
||||||
|
*/
|
||||||
case class ProcessItemArgs(meta: ProcessMeta, files: List[File]) {
|
case class ProcessItemArgs(meta: ProcessMeta, files: List[File]) {
|
||||||
|
|
||||||
def makeSubject: String =
|
def makeSubject: String =
|
||||||
@ -22,6 +30,7 @@ object ProcessItemArgs {
|
|||||||
|
|
||||||
case class ProcessMeta(
|
case class ProcessMeta(
|
||||||
collective: Ident,
|
collective: Ident,
|
||||||
|
itemId: Option[Ident],
|
||||||
language: Language,
|
language: Language,
|
||||||
direction: Option[Direction],
|
direction: Option[Direction],
|
||||||
sourceAbbrev: String,
|
sourceAbbrev: String,
|
||||||
|
@ -77,7 +77,7 @@ object JoexAppImpl {
|
|||||||
.withTask(
|
.withTask(
|
||||||
JobTask.json(
|
JobTask.json(
|
||||||
ProcessItemArgs.taskName,
|
ProcessItemArgs.taskName,
|
||||||
ItemHandler[F](cfg),
|
ItemHandler.newItem[F](cfg),
|
||||||
ItemHandler.onCancel[F]
|
ItemHandler.onCancel[F]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -71,7 +71,7 @@ object NotifyDueItemsTask {
|
|||||||
QItem.Query
|
QItem.Query
|
||||||
.empty(ctx.args.account.collective)
|
.empty(ctx.args.account.collective)
|
||||||
.copy(
|
.copy(
|
||||||
states = ItemState.validStates,
|
states = ItemState.validStates.toList,
|
||||||
tagsInclude = ctx.args.tagsInclude,
|
tagsInclude = ctx.args.tagsInclude,
|
||||||
tagsExclude = ctx.args.tagsExclude,
|
tagsExclude = ctx.args.tagsExclude,
|
||||||
dueDateFrom = ctx.args.daysBack.map(back => now - Duration.days(back.toLong)),
|
dueDateFrom = ctx.args.daysBack.map(back => now - Duration.days(back.toLong)),
|
||||||
|
@ -62,7 +62,7 @@ object ConvertPdf {
|
|||||||
Conversion.create[F](cfg, sanitizeHtml, ctx.blocker, ctx.logger).use { conv =>
|
Conversion.create[F](cfg, sanitizeHtml, ctx.blocker, ctx.logger).use { conv =>
|
||||||
mime match {
|
mime match {
|
||||||
case mt if mt.baseEqual(Mimetype.`application/pdf`) =>
|
case mt if mt.baseEqual(Mimetype.`application/pdf`) =>
|
||||||
ctx.logger.info("Not going to convert a PDF file into a PDF.") *>
|
ctx.logger.debug(s"Not going to convert a PDF file ${ra.name} into a PDF.") *>
|
||||||
(ra, None: Option[RAttachmentMeta]).pure[F]
|
(ra, None: Option[RAttachmentMeta]).pure[F]
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
|
@ -31,6 +31,9 @@ object CreateItem {
|
|||||||
.contains(fm.mimetype.baseType)
|
.contains(fm.mimetype.baseType)
|
||||||
|
|
||||||
def fileMetas(itemId: Ident, now: Timestamp) =
|
def fileMetas(itemId: Ident, now: Timestamp) =
|
||||||
|
Stream
|
||||||
|
.eval(ctx.store.transact(RAttachment.nextPosition(itemId)))
|
||||||
|
.flatMap { offset =>
|
||||||
Stream
|
Stream
|
||||||
.emits(ctx.args.files)
|
.emits(ctx.args.files)
|
||||||
.flatMap(f => ctx.store.bitpeace.get(f.fileMetaId.id).map(fm => (f, fm)))
|
.flatMap(f => ctx.store.bitpeace.get(f.fileMetaId.id).map(fm => (f, fm)))
|
||||||
@ -41,35 +44,63 @@ object CreateItem {
|
|||||||
Ident
|
Ident
|
||||||
.randomId[F]
|
.randomId[F]
|
||||||
.map(id =>
|
.map(id =>
|
||||||
RAttachment(id, itemId, f.fileMetaId, index.toInt, now, f.name)
|
RAttachment(
|
||||||
|
id,
|
||||||
|
itemId,
|
||||||
|
f.fileMetaId,
|
||||||
|
index.toInt + offset,
|
||||||
|
now,
|
||||||
|
f.name
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
.compile
|
.compile
|
||||||
.toVector
|
.toVector
|
||||||
|
|
||||||
val item = RItem.newItem[F](
|
val loadItemOrInsertNew =
|
||||||
|
ctx.args.meta.itemId match {
|
||||||
|
case Some(id) =>
|
||||||
|
(for {
|
||||||
|
_ <- OptionT.liftF(
|
||||||
|
ctx.logger.info(
|
||||||
|
s"Loading item with id ${id.id} to ammend"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
item <- OptionT(
|
||||||
|
ctx.store
|
||||||
|
.transact(RItem.findByIdAndCollective(id, ctx.args.meta.collective))
|
||||||
|
)
|
||||||
|
} yield (1, item))
|
||||||
|
.getOrElseF(Sync[F].raiseError(new Exception(s"Item not found.")))
|
||||||
|
case None =>
|
||||||
|
for {
|
||||||
|
_ <- ctx.logger.info(
|
||||||
|
s"Creating new item with ${ctx.args.files.size} attachment(s)"
|
||||||
|
)
|
||||||
|
item <- RItem.newItem[F](
|
||||||
ctx.args.meta.collective,
|
ctx.args.meta.collective,
|
||||||
ctx.args.makeSubject,
|
ctx.args.makeSubject,
|
||||||
ctx.args.meta.sourceAbbrev,
|
ctx.args.meta.sourceAbbrev,
|
||||||
ctx.args.meta.direction.getOrElse(Direction.Incoming),
|
ctx.args.meta.direction.getOrElse(Direction.Incoming),
|
||||||
ItemState.Premature
|
ItemState.Premature
|
||||||
)
|
)
|
||||||
|
n <- ctx.store.transact(RItem.insert(item))
|
||||||
|
} yield (n, item)
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
_ <- ctx.logger.info(
|
|
||||||
s"Creating new item with ${ctx.args.files.size} attachment(s)"
|
|
||||||
)
|
|
||||||
time <- Duration.stopTime[F]
|
time <- Duration.stopTime[F]
|
||||||
it <- item
|
it <- loadItemOrInsertNew
|
||||||
n <- ctx.store.transact(RItem.insert(it))
|
_ <- if (it._1 != 1) storeItemError[F](ctx) else ().pure[F]
|
||||||
_ <- if (n != 1) storeItemError[F](ctx) else ().pure[F]
|
now <- Timestamp.current[F]
|
||||||
fm <- fileMetas(it.id, it.created)
|
fm <- fileMetas(it._2.id, now)
|
||||||
k <- fm.traverse(insertAttachment(ctx))
|
k <- fm.traverse(insertAttachment(ctx))
|
||||||
_ <- logDifferences(ctx, fm, k.sum)
|
_ <- logDifferences(ctx, fm, k.sum)
|
||||||
dur <- time
|
dur <- time
|
||||||
_ <- ctx.logger.info(s"Creating item finished in ${dur.formatExact}")
|
_ <- ctx.logger.info(s"Creating item finished in ${dur.formatExact}")
|
||||||
} yield ItemData(
|
} yield ItemData(
|
||||||
it,
|
it._2,
|
||||||
fm,
|
fm,
|
||||||
Vector.empty,
|
Vector.empty,
|
||||||
Vector.empty,
|
Vector.empty,
|
||||||
@ -86,10 +117,11 @@ object CreateItem {
|
|||||||
} yield n)
|
} yield n)
|
||||||
}
|
}
|
||||||
|
|
||||||
def findExisting[F[_]: Sync]: Task[F, ProcessItemArgs, Option[ItemData]] =
|
private def findExisting[F[_]: Sync]: Task[F, ProcessItemArgs, Option[ItemData]] =
|
||||||
Task { ctx =>
|
Task { ctx =>
|
||||||
|
val fileMetaIds = ctx.args.files.map(_.fileMetaId).toSet
|
||||||
for {
|
for {
|
||||||
cand <- ctx.store.transact(QItem.findByFileIds(ctx.args.files.map(_.fileMetaId)))
|
cand <- ctx.store.transact(QItem.findByFileIds(fileMetaIds.toSeq))
|
||||||
_ <-
|
_ <-
|
||||||
if (cand.nonEmpty) ctx.logger.warn("Found existing item with these files.")
|
if (cand.nonEmpty) ctx.logger.warn("Found existing item with these files.")
|
||||||
else ().pure[F]
|
else ().pure[F]
|
||||||
@ -99,8 +131,11 @@ object CreateItem {
|
|||||||
ctx.logger.warn(s"Removed ${ht.sum} items with same attachments")
|
ctx.logger.warn(s"Removed ${ht.sum} items with same attachments")
|
||||||
else ().pure[F]
|
else ().pure[F]
|
||||||
rms <- OptionT(
|
rms <- OptionT(
|
||||||
|
//load attachments but only those mentioned in the task's arguments
|
||||||
cand.headOption.traverse(ri =>
|
cand.headOption.traverse(ri =>
|
||||||
ctx.store.transact(RAttachment.findByItemAndCollective(ri.id, ri.cid))
|
ctx.store
|
||||||
|
.transact(RAttachment.findByItemAndCollective(ri.id, ri.cid))
|
||||||
|
.map(_.filter(r => fileMetaIds.contains(r.fileId)))
|
||||||
)
|
)
|
||||||
).getOrElse(Vector.empty)
|
).getOrElse(Vector.empty)
|
||||||
orig <- rms.traverse(a =>
|
orig <- rms.traverse(a =>
|
||||||
|
@ -13,6 +13,8 @@ import docspell.store.records._
|
|||||||
import docspell.files.Zip
|
import docspell.files.Zip
|
||||||
import cats.kernel.Monoid
|
import cats.kernel.Monoid
|
||||||
import emil.Mail
|
import emil.Mail
|
||||||
|
import cats.kernel.Order
|
||||||
|
import cats.data.NonEmptyList
|
||||||
|
|
||||||
/** Goes through all attachments and extracts archive files, like zip
|
/** Goes through all attachments and extracts archive files, like zip
|
||||||
* files. The process is recursive, until all archives have been
|
* files. The process is recursive, until all archives have been
|
||||||
@ -46,22 +48,37 @@ object ExtractArchive {
|
|||||||
archive: Option[RAttachmentArchive]
|
archive: Option[RAttachmentArchive]
|
||||||
): Task[F, ProcessItemArgs, (Option[RAttachmentArchive], ItemData)] =
|
): Task[F, ProcessItemArgs, (Option[RAttachmentArchive], ItemData)] =
|
||||||
Task { ctx =>
|
Task { ctx =>
|
||||||
def extract(ra: RAttachment) =
|
def extract(ra: RAttachment, pos: Int): F[Extracted] =
|
||||||
findMime(ctx)(ra).flatMap(m => extractSafe(ctx, archive)(ra, m))
|
findMime(ctx)(ra).flatMap(m => extractSafe(ctx, archive)(ra, pos, m))
|
||||||
|
|
||||||
for {
|
for {
|
||||||
ras <- item.attachments.traverse(extract)
|
lastPos <- ctx.store.transact(RAttachment.nextPosition(item.item.id))
|
||||||
nra = ras.flatMap(_.files).zipWithIndex.map(t => t._1.copy(position = t._2))
|
extracts <-
|
||||||
_ <- nra.traverse(storeAttachment(ctx))
|
item.attachments.zipWithIndex
|
||||||
naa = ras.flatMap(_.archives)
|
.traverse(t => extract(t._1, lastPos + t._2))
|
||||||
|
.map(Monoid[Extracted].combineAll)
|
||||||
|
.map(fixPositions)
|
||||||
|
nra = extracts.files
|
||||||
|
_ <- extracts.files.traverse(storeAttachment(ctx))
|
||||||
|
naa = extracts.archives
|
||||||
_ <- naa.traverse(storeArchive(ctx))
|
_ <- naa.traverse(storeArchive(ctx))
|
||||||
} yield naa.headOption -> item.copy(
|
} yield naa.headOption -> item.copy(
|
||||||
attachments = nra,
|
attachments = nra,
|
||||||
originFile = item.originFile ++ nra.map(a => a.id -> a.fileId).toMap,
|
originFile = item.originFile ++ nra.map(a => a.id -> a.fileId).toMap,
|
||||||
givenMeta = item.givenMeta.fillEmptyFrom(Monoid[Extracted].combineAll(ras).meta)
|
givenMeta = item.givenMeta.fillEmptyFrom(extracts.meta)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** After all files have been extracted, the `extract' contains the
|
||||||
|
* whole (combined) result. This fixes positions of the attachments
|
||||||
|
* such that the elements of an archive are "spliced" into the
|
||||||
|
* attachment list at the position of the archive. If there is no
|
||||||
|
* archive, positions don't need to be fixed.
|
||||||
|
*/
|
||||||
|
private def fixPositions(extract: Extracted): Extracted =
|
||||||
|
if (extract.archives.isEmpty) extract
|
||||||
|
else extract.updatePositions
|
||||||
|
|
||||||
def findMime[F[_]: Functor](ctx: Context[F, _])(ra: RAttachment): F[Mimetype] =
|
def findMime[F[_]: Functor](ctx: Context[F, _])(ra: RAttachment): F[Mimetype] =
|
||||||
OptionT(ctx.store.transact(RFileMeta.findById(ra.fileId)))
|
OptionT(ctx.store.transact(RFileMeta.findById(ra.fileId)))
|
||||||
.map(_.mimetype)
|
.map(_.mimetype)
|
||||||
@ -70,21 +87,21 @@ object ExtractArchive {
|
|||||||
def extractSafe[F[_]: ConcurrentEffect: ContextShift](
|
def extractSafe[F[_]: ConcurrentEffect: ContextShift](
|
||||||
ctx: Context[F, ProcessItemArgs],
|
ctx: Context[F, ProcessItemArgs],
|
||||||
archive: Option[RAttachmentArchive]
|
archive: Option[RAttachmentArchive]
|
||||||
)(ra: RAttachment, mime: Mimetype): F[Extracted] =
|
)(ra: RAttachment, pos: Int, mime: Mimetype): F[Extracted] =
|
||||||
mime match {
|
mime match {
|
||||||
case Mimetype("application", "zip", _) if ra.name.exists(_.endsWith(".zip")) =>
|
case Mimetype("application", "zip", _) if ra.name.exists(_.endsWith(".zip")) =>
|
||||||
ctx.logger.info(s"Extracting zip archive ${ra.name.getOrElse("<noname>")}.") *>
|
ctx.logger.info(s"Extracting zip archive ${ra.name.getOrElse("<noname>")}.") *>
|
||||||
extractZip(ctx, archive)(ra)
|
extractZip(ctx, archive)(ra, pos)
|
||||||
.flatTap(_ => cleanupParents(ctx, ra, archive))
|
.flatTap(_ => cleanupParents(ctx, ra, archive))
|
||||||
|
|
||||||
case Mimetype("message", "rfc822", _) =>
|
case Mimetype("message", "rfc822", _) =>
|
||||||
ctx.logger.info(s"Reading e-mail ${ra.name.getOrElse("<noname>")}") *>
|
ctx.logger.info(s"Reading e-mail ${ra.name.getOrElse("<noname>")}") *>
|
||||||
extractMail(ctx, archive)(ra)
|
extractMail(ctx, archive)(ra, pos)
|
||||||
.flatTap(_ => cleanupParents(ctx, ra, archive))
|
.flatTap(_ => cleanupParents(ctx, ra, archive))
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
ctx.logger.debug(s"Not an archive: ${mime.asString}") *>
|
ctx.logger.debug(s"Not an archive: ${mime.asString}") *>
|
||||||
Extracted.noArchive(ra).pure[F]
|
Extracted.noArchive(ra, pos, 0).pure[F]
|
||||||
}
|
}
|
||||||
|
|
||||||
def cleanupParents[F[_]: Sync](
|
def cleanupParents[F[_]: Sync](
|
||||||
@ -114,7 +131,7 @@ object ExtractArchive {
|
|||||||
def extractZip[F[_]: ConcurrentEffect: ContextShift](
|
def extractZip[F[_]: ConcurrentEffect: ContextShift](
|
||||||
ctx: Context[F, _],
|
ctx: Context[F, _],
|
||||||
archive: Option[RAttachmentArchive]
|
archive: Option[RAttachmentArchive]
|
||||||
)(ra: RAttachment): F[Extracted] = {
|
)(ra: RAttachment, pos: Int): F[Extracted] = {
|
||||||
val zipData = ctx.store.bitpeace
|
val zipData = ctx.store.bitpeace
|
||||||
.get(ra.fileId.id)
|
.get(ra.fileId.id)
|
||||||
.unNoneTerminate
|
.unNoneTerminate
|
||||||
@ -122,7 +139,8 @@ object ExtractArchive {
|
|||||||
|
|
||||||
zipData
|
zipData
|
||||||
.through(Zip.unzipP[F](8192, ctx.blocker))
|
.through(Zip.unzipP[F](8192, ctx.blocker))
|
||||||
.flatMap(handleEntry(ctx, ra, archive, None))
|
.zipWithIndex
|
||||||
|
.flatMap(handleEntry(ctx, ra, pos, archive, None))
|
||||||
.foldMonoid
|
.foldMonoid
|
||||||
.compile
|
.compile
|
||||||
.lastOrError
|
.lastOrError
|
||||||
@ -131,7 +149,7 @@ object ExtractArchive {
|
|||||||
def extractMail[F[_]: ConcurrentEffect: ContextShift](
|
def extractMail[F[_]: ConcurrentEffect: ContextShift](
|
||||||
ctx: Context[F, _],
|
ctx: Context[F, _],
|
||||||
archive: Option[RAttachmentArchive]
|
archive: Option[RAttachmentArchive]
|
||||||
)(ra: RAttachment): F[Extracted] = {
|
)(ra: RAttachment, pos: Int): F[Extracted] = {
|
||||||
val email: Stream[F, Byte] = ctx.store.bitpeace
|
val email: Stream[F, Byte] = ctx.store.bitpeace
|
||||||
.get(ra.fileId.id)
|
.get(ra.fileId.id)
|
||||||
.unNoneTerminate
|
.unNoneTerminate
|
||||||
@ -149,7 +167,8 @@ object ExtractArchive {
|
|||||||
|
|
||||||
ReadMail
|
ReadMail
|
||||||
.mailToEntries(ctx.logger)(mail)
|
.mailToEntries(ctx.logger)(mail)
|
||||||
.flatMap(handleEntry(ctx, ra, archive, mId)) ++ Stream.eval(givenMeta)
|
.zipWithIndex
|
||||||
|
.flatMap(handleEntry(ctx, ra, pos, archive, mId)) ++ Stream.eval(givenMeta)
|
||||||
}
|
}
|
||||||
.foldMonoid
|
.foldMonoid
|
||||||
.compile
|
.compile
|
||||||
@ -165,11 +184,13 @@ object ExtractArchive {
|
|||||||
def handleEntry[F[_]: Sync](
|
def handleEntry[F[_]: Sync](
|
||||||
ctx: Context[F, _],
|
ctx: Context[F, _],
|
||||||
ra: RAttachment,
|
ra: RAttachment,
|
||||||
|
pos: Int,
|
||||||
archive: Option[RAttachmentArchive],
|
archive: Option[RAttachmentArchive],
|
||||||
messageId: Option[String]
|
messageId: Option[String]
|
||||||
)(
|
)(
|
||||||
entry: Binary[F]
|
tentry: (Binary[F], Long)
|
||||||
): Stream[F, Extracted] = {
|
): Stream[F, Extracted] = {
|
||||||
|
val (entry, subPos) = tentry
|
||||||
val mimeHint = MimetypeHint.filename(entry.name).withAdvertised(entry.mime.asString)
|
val mimeHint = MimetypeHint.filename(entry.name).withAdvertised(entry.mime.asString)
|
||||||
val fileMeta = ctx.store.bitpeace.saveNew(entry.data, 8192, mimeHint)
|
val fileMeta = ctx.store.bitpeace.saveNew(entry.data, 8192, mimeHint)
|
||||||
Stream.eval(ctx.logger.debug(s"Extracted ${entry.name}. Storing as attachment.")) >>
|
Stream.eval(ctx.logger.debug(s"Extracted ${entry.name}. Storing as attachment.")) >>
|
||||||
@ -179,12 +200,12 @@ object ExtractArchive {
|
|||||||
id,
|
id,
|
||||||
ra.itemId,
|
ra.itemId,
|
||||||
Ident.unsafe(fm.id),
|
Ident.unsafe(fm.id),
|
||||||
0, //position is updated afterwards
|
pos,
|
||||||
ra.created,
|
ra.created,
|
||||||
Option(entry.name).map(_.trim).filter(_.nonEmpty)
|
Option(entry.name).map(_.trim).filter(_.nonEmpty)
|
||||||
)
|
)
|
||||||
val aa = archive.getOrElse(RAttachmentArchive.of(ra, messageId)).copy(id = id)
|
val aa = archive.getOrElse(RAttachmentArchive.of(ra, messageId)).copy(id = id)
|
||||||
Extracted.of(nra, aa)
|
Extracted.of(nra, aa, pos, subPos.toInt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,28 +225,67 @@ object ExtractArchive {
|
|||||||
case class Extracted(
|
case class Extracted(
|
||||||
files: Vector[RAttachment],
|
files: Vector[RAttachment],
|
||||||
archives: Vector[RAttachmentArchive],
|
archives: Vector[RAttachmentArchive],
|
||||||
meta: MetaProposalList
|
meta: MetaProposalList,
|
||||||
|
positions: List[Extracted.Pos]
|
||||||
) {
|
) {
|
||||||
def ++(e: Extracted) =
|
def ++(e: Extracted) =
|
||||||
Extracted(files ++ e.files, archives ++ e.archives, meta.fillEmptyFrom(e.meta))
|
Extracted(
|
||||||
|
files ++ e.files,
|
||||||
|
archives ++ e.archives,
|
||||||
|
meta.fillEmptyFrom(e.meta),
|
||||||
|
positions ++ e.positions
|
||||||
|
)
|
||||||
|
|
||||||
def setMeta(m: MetaProposal): Extracted =
|
def setMeta(m: MetaProposal): Extracted =
|
||||||
setMeta(MetaProposalList.of(m))
|
setMeta(MetaProposalList.of(m))
|
||||||
|
|
||||||
def setMeta(ml: MetaProposalList): Extracted =
|
def setMeta(ml: MetaProposalList): Extracted =
|
||||||
Extracted(files, archives, meta.fillEmptyFrom(ml))
|
Extracted(files, archives, meta.fillEmptyFrom(ml), positions)
|
||||||
|
|
||||||
|
def updatePositions: Extracted =
|
||||||
|
NonEmptyList.fromList(positions) match {
|
||||||
|
case None =>
|
||||||
|
this
|
||||||
|
case Some(nel) =>
|
||||||
|
val sorted = nel.sorted
|
||||||
|
println(s"---------------------------- $sorted ")
|
||||||
|
val offset = sorted.head.first
|
||||||
|
val pos =
|
||||||
|
sorted.zipWithIndex.map({ case (p, i) => p.id -> (i + offset) }).toList.toMap
|
||||||
|
val nf =
|
||||||
|
files.map(f => pos.get(f.id).map(n => f.copy(position = n)).getOrElse(f))
|
||||||
|
copy(files = nf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
object Extracted {
|
object Extracted {
|
||||||
val empty = Extracted(Vector.empty, Vector.empty, MetaProposalList.empty)
|
val empty =
|
||||||
|
Extracted(Vector.empty, Vector.empty, MetaProposalList.empty, Nil)
|
||||||
|
|
||||||
def noArchive(ra: RAttachment): Extracted =
|
def noArchive(ra: RAttachment, pos: Int, subPos: Int): Extracted =
|
||||||
Extracted(Vector(ra), Vector.empty, MetaProposalList.empty)
|
Extracted(
|
||||||
|
Vector(ra),
|
||||||
|
Vector.empty,
|
||||||
|
MetaProposalList.empty,
|
||||||
|
List(Pos(ra.id, pos, subPos))
|
||||||
|
)
|
||||||
|
|
||||||
def of(ra: RAttachment, aa: RAttachmentArchive): Extracted =
|
def of(ra: RAttachment, aa: RAttachmentArchive, pos: Int, subPos: Int): Extracted =
|
||||||
Extracted(Vector(ra), Vector(aa), MetaProposalList.empty)
|
Extracted(
|
||||||
|
Vector(ra),
|
||||||
|
Vector(aa),
|
||||||
|
MetaProposalList.empty,
|
||||||
|
List(Pos(ra.id, pos, subPos))
|
||||||
|
)
|
||||||
|
|
||||||
implicit val extractedMonoid: Monoid[Extracted] =
|
implicit val extractedMonoid: Monoid[Extracted] =
|
||||||
Monoid.instance(empty, _ ++ _)
|
Monoid.instance(empty, _ ++ _)
|
||||||
|
|
||||||
|
case class Pos(id: Ident, first: Int, second: Int)
|
||||||
|
|
||||||
|
object Pos {
|
||||||
|
implicit val ordering: Order[Pos] =
|
||||||
|
Order.whenEqual(Order.by(_.first), Order.by(_.second))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,20 @@ package docspell.joex.process
|
|||||||
|
|
||||||
import cats.implicits._
|
import cats.implicits._
|
||||||
import cats.effect._
|
import cats.effect._
|
||||||
|
import fs2.Stream
|
||||||
import docspell.common.{ItemState, ProcessItemArgs}
|
import docspell.common.{ItemState, ProcessItemArgs}
|
||||||
import docspell.joex.Config
|
import docspell.joex.Config
|
||||||
import docspell.joex.scheduler.{Context, Task}
|
import docspell.joex.scheduler.Task
|
||||||
import docspell.store.queries.QItem
|
import docspell.store.queries.QItem
|
||||||
import docspell.store.records.{RItem, RJob}
|
import docspell.store.records.RItem
|
||||||
|
|
||||||
object ItemHandler {
|
object ItemHandler {
|
||||||
def onCancel[F[_]: Sync: ContextShift]: Task[F, ProcessItemArgs, Unit] =
|
def onCancel[F[_]: Sync: ContextShift]: Task[F, ProcessItemArgs, Unit] =
|
||||||
logWarn("Now cancelling. Deleting potentially created data.").flatMap(_ =>
|
logWarn("Now cancelling. Deleting potentially created data.").flatMap(_ =>
|
||||||
deleteByFileIds
|
deleteByFileIds.flatMap(_ => deleteFiles)
|
||||||
)
|
)
|
||||||
|
|
||||||
def apply[F[_]: ConcurrentEffect: ContextShift](
|
def newItem[F[_]: ConcurrentEffect: ContextShift](
|
||||||
cfg: Config
|
cfg: Config
|
||||||
): Task[F, ProcessItemArgs, Unit] =
|
): Task[F, ProcessItemArgs, Unit] =
|
||||||
CreateItem[F]
|
CreateItem[F]
|
||||||
@ -25,18 +26,19 @@ object ItemHandler {
|
|||||||
def itemStateTask[F[_]: Sync, A](
|
def itemStateTask[F[_]: Sync, A](
|
||||||
state: ItemState
|
state: ItemState
|
||||||
)(data: ItemData): Task[F, A, ItemData] =
|
)(data: ItemData): Task[F, A, ItemData] =
|
||||||
Task(ctx => ctx.store.transact(RItem.updateState(data.item.id, state)).map(_ => data))
|
Task(ctx =>
|
||||||
|
ctx.store
|
||||||
|
.transact(RItem.updateState(data.item.id, state, ItemState.invalidStates))
|
||||||
|
.map(_ => data)
|
||||||
|
)
|
||||||
|
|
||||||
def isLastRetry[F[_]: Sync, A](ctx: Context[F, A]): F[Boolean] =
|
def isLastRetry[F[_]: Sync]: Task[F, ProcessItemArgs, Boolean] =
|
||||||
for {
|
Task(_.isLastRetry)
|
||||||
current <- ctx.store.transact(RJob.getRetries(ctx.jobId))
|
|
||||||
last = ctx.config.retries == current.getOrElse(0)
|
|
||||||
} yield last
|
|
||||||
|
|
||||||
def safeProcess[F[_]: ConcurrentEffect: ContextShift](
|
def safeProcess[F[_]: ConcurrentEffect: ContextShift](
|
||||||
cfg: Config
|
cfg: Config
|
||||||
)(data: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
)(data: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
||||||
Task(isLastRetry[F, ProcessItemArgs] _).flatMap {
|
isLastRetry[F].flatMap {
|
||||||
case true =>
|
case true =>
|
||||||
ProcessItem[F](cfg)(data).attempt.flatMap({
|
ProcessItem[F](cfg)(data).attempt.flatMap({
|
||||||
case Right(d) =>
|
case Right(d) =>
|
||||||
@ -60,6 +62,15 @@ object ItemHandler {
|
|||||||
} yield ()
|
} yield ()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def deleteFiles[F[_]: Sync]: Task[F, ProcessItemArgs, Unit] =
|
||||||
|
Task(ctx =>
|
||||||
|
Stream
|
||||||
|
.emits(ctx.args.files.map(_.fileMetaId.id))
|
||||||
|
.flatMap(id => ctx.store.bitpeace.delete(id).attempt.drain)
|
||||||
|
.compile
|
||||||
|
.drain
|
||||||
|
)
|
||||||
|
|
||||||
private def logWarn[F[_]](msg: => String): Task[F, ProcessItemArgs, Unit] =
|
private def logWarn[F[_]](msg: => String): Task[F, ProcessItemArgs, Unit] =
|
||||||
Task(_.logger.warn(msg))
|
Task(_.logger.warn(msg))
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,11 @@ import docspell.store.records.RItem
|
|||||||
object LinkProposal {
|
object LinkProposal {
|
||||||
|
|
||||||
def apply[F[_]: Sync](data: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
def apply[F[_]: Sync](data: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
||||||
|
if (data.item.state.isValid)
|
||||||
|
Task
|
||||||
|
.log[F, ProcessItemArgs](_.debug(s"Not linking proposals on existing item"))
|
||||||
|
.map(_ => data)
|
||||||
|
else
|
||||||
Task { ctx =>
|
Task { ctx =>
|
||||||
// sort by weight; order of equal weights is not important, just
|
// sort by weight; order of equal weights is not important, just
|
||||||
// choose one others are then suggestions
|
// choose one others are then suggestions
|
||||||
@ -40,8 +45,9 @@ object LinkProposal {
|
|||||||
Result.single(mpt)
|
Result.single(mpt)
|
||||||
)
|
)
|
||||||
case Some(a) =>
|
case Some(a) =>
|
||||||
|
val ids = a.values.map(_.ref.id.id)
|
||||||
ctx.logger.info(
|
ctx.logger.info(
|
||||||
s"Found many (${a.size}, ${a.values.map(_.ref.id.id)}) candidates for ${a.proposalType}. Setting first."
|
s"Found many (${a.size}, ${ids}) candidates for ${a.proposalType}. Setting first."
|
||||||
) *>
|
) *>
|
||||||
setItemMeta(data.item.id, ctx, a.proposalType, a.values.head.ref.id).map(_ =>
|
setItemMeta(data.item.id, ctx, a.proposalType, a.values.head.ref.id).map(_ =>
|
||||||
Result.multiple(mpt)
|
Result.multiple(mpt)
|
||||||
|
@ -12,11 +12,13 @@ object ProcessItem {
|
|||||||
cfg: Config
|
cfg: Config
|
||||||
)(item: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
)(item: ItemData): Task[F, ProcessItemArgs, ItemData] =
|
||||||
ExtractArchive(item)
|
ExtractArchive(item)
|
||||||
|
.flatMap(Task.setProgress(20))
|
||||||
.flatMap(ConvertPdf(cfg.convert, _))
|
.flatMap(ConvertPdf(cfg.convert, _))
|
||||||
|
.flatMap(Task.setProgress(40))
|
||||||
.flatMap(TextExtraction(cfg.extraction, _))
|
.flatMap(TextExtraction(cfg.extraction, _))
|
||||||
.flatMap(Task.setProgress(50))
|
.flatMap(Task.setProgress(60))
|
||||||
.flatMap(analysisOnly[F](cfg.textAnalysis))
|
.flatMap(analysisOnly[F](cfg.textAnalysis))
|
||||||
.flatMap(Task.setProgress(75))
|
.flatMap(Task.setProgress(80))
|
||||||
.flatMap(LinkProposal[F])
|
.flatMap(LinkProposal[F])
|
||||||
.flatMap(Task.setProgress(99))
|
.flatMap(Task.setProgress(99))
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ object TextExtraction {
|
|||||||
rm => rm.setContentIfEmpty(txt.map(_.trim).filter(_.nonEmpty))
|
rm => rm.setContentIfEmpty(txt.map(_.trim).filter(_.nonEmpty))
|
||||||
)
|
)
|
||||||
est <- dst
|
est <- dst
|
||||||
_ <- ctx.logger.debug(
|
_ <- ctx.logger.info(
|
||||||
s"Extracting text for attachment ${stripAttachmentName(ra)} finished in ${est.formatExact}"
|
s"Extracting text for attachment ${stripAttachmentName(ra)} finished in ${est.formatExact}"
|
||||||
)
|
)
|
||||||
} yield meta
|
} yield meta
|
||||||
|
@ -259,7 +259,7 @@ object ScanMailboxTask {
|
|||||||
priority = Priority.Low,
|
priority = Priority.Low,
|
||||||
tracker = None
|
tracker = None
|
||||||
)
|
)
|
||||||
res <- upload.submit(data, ctx.args.account, false)
|
res <- upload.submit(data, ctx.args.account, false, None)
|
||||||
} yield res
|
} yield res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package docspell.joex.scheduler
|
package docspell.joex.scheduler
|
||||||
|
|
||||||
import cats.Functor
|
import cats.{Applicative, Functor}
|
||||||
import cats.effect.{Blocker, Concurrent}
|
import cats.effect._
|
||||||
import cats.implicits._
|
import cats.implicits._
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.store.Store
|
import docspell.store.Store
|
||||||
@ -23,6 +23,12 @@ trait Context[F[_], A] { self =>
|
|||||||
|
|
||||||
def store: Store[F]
|
def store: Store[F]
|
||||||
|
|
||||||
|
final def isLastRetry(implicit ev: Applicative[F]): F[Boolean] =
|
||||||
|
for {
|
||||||
|
current <- store.transact(RJob.getRetries(jobId))
|
||||||
|
last = config.retries == current.getOrElse(0)
|
||||||
|
} yield last
|
||||||
|
|
||||||
def blocker: Blocker
|
def blocker: Blocker
|
||||||
|
|
||||||
def map[C](f: A => C)(implicit F: Functor[F]): Context[F, C] =
|
def map[C](f: A => C)(implicit F: Functor[F]): Context[F, C] =
|
||||||
|
@ -87,12 +87,6 @@ paths:
|
|||||||
The upload meta data can be used to tell, whether multiple
|
The upload meta data can be used to tell, whether multiple
|
||||||
files are one item, or if each file should become a single
|
files are one item, or if each file should become a single
|
||||||
item. By default, each file will be a one item.
|
item. By default, each file will be a one item.
|
||||||
|
|
||||||
Only certain file types are supported:
|
|
||||||
|
|
||||||
* application/pdf
|
|
||||||
|
|
||||||
Support for more types might be added.
|
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: "#/components/parameters/id"
|
- $ref: "#/components/parameters/id"
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -115,6 +109,48 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/BasicResult"
|
$ref: "#/components/schemas/BasicResult"
|
||||||
|
/open/upload/item/{itemId}/{id}:
|
||||||
|
post:
|
||||||
|
tags: [ Upload ]
|
||||||
|
summary: Upload files to docspell.
|
||||||
|
description: |
|
||||||
|
Upload a file to docspell for processing. The id is a *source
|
||||||
|
id* configured by a collective. Files are submitted for
|
||||||
|
processing which eventually resuts in an item in the inbox of
|
||||||
|
the corresponding collective. This endpoint associates the
|
||||||
|
files to an existing item identified by its `itemId`.
|
||||||
|
|
||||||
|
The request must be a `multipart/form-data` request, where the
|
||||||
|
first part has name `meta`, is optional and may contain upload
|
||||||
|
metadata as JSON. Checkout the structure `ItemUploadMeta` at
|
||||||
|
the end if it is not shown here. Other parts specify the
|
||||||
|
files. Multiple files can be specified, but at least on is
|
||||||
|
required.
|
||||||
|
|
||||||
|
Upload meta data is ignored.
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/id"
|
||||||
|
- $ref: "#/components/parameters/itemId"
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
meta:
|
||||||
|
$ref: "#/components/schemas/ItemUploadMeta"
|
||||||
|
file:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/BasicResult"
|
||||||
/sec/checkfile/{checksum}:
|
/sec/checkfile/{checksum}:
|
||||||
get:
|
get:
|
||||||
tags: [ Upload ]
|
tags: [ Upload ]
|
||||||
@ -155,12 +191,6 @@ paths:
|
|||||||
The upload meta data can be used to tell, whether multiple
|
The upload meta data can be used to tell, whether multiple
|
||||||
files are one item, or if each file should become a single
|
files are one item, or if each file should become a single
|
||||||
item. By default, each file will be a one item.
|
item. By default, each file will be a one item.
|
||||||
|
|
||||||
Only certain file types are supported:
|
|
||||||
|
|
||||||
* application/pdf
|
|
||||||
|
|
||||||
Support for more types might be added.
|
|
||||||
security:
|
security:
|
||||||
- authTokenHeader: []
|
- authTokenHeader: []
|
||||||
requestBody:
|
requestBody:
|
||||||
@ -183,6 +213,50 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/BasicResult"
|
$ref: "#/components/schemas/BasicResult"
|
||||||
|
/sec/upload/{itemId}:
|
||||||
|
post:
|
||||||
|
tags: [ Upload ]
|
||||||
|
summary: Upload files to docspell.
|
||||||
|
description: |
|
||||||
|
Upload files to docspell for processing. This route is meant
|
||||||
|
for authenticated users that upload files to their account.
|
||||||
|
This endpoint will associate the files to an existing item
|
||||||
|
identified by its `itemId`.
|
||||||
|
|
||||||
|
Everything else is the same as with the
|
||||||
|
`/open/upload/item/{itemId}/{id}` endpoint.
|
||||||
|
|
||||||
|
The request must be a "multipart/form-data" request, where the
|
||||||
|
first part is optional and may contain upload metadata as
|
||||||
|
JSON. Other parts specify the files. Multiple files can be
|
||||||
|
specified, but at least on is required.
|
||||||
|
|
||||||
|
The upload meta data is ignored, since the item already
|
||||||
|
exists.
|
||||||
|
security:
|
||||||
|
- authTokenHeader: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/itemId"
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
meta:
|
||||||
|
$ref: "#/components/schemas/ItemUploadMeta"
|
||||||
|
file:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/BasicResult"
|
||||||
/open/signup/register:
|
/open/signup/register:
|
||||||
post:
|
post:
|
||||||
tags: [ Registration ]
|
tags: [ Registration ]
|
||||||
@ -3156,6 +3230,13 @@ components:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
itemId:
|
||||||
|
name: itemId
|
||||||
|
in: path
|
||||||
|
description: An identifier for an item
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
full:
|
full:
|
||||||
name: full
|
name: full
|
||||||
in: query
|
in: query
|
||||||
|
@ -109,7 +109,7 @@ trait Conversions {
|
|||||||
coll,
|
coll,
|
||||||
m.name,
|
m.name,
|
||||||
if (m.inbox) Seq(ItemState.Created)
|
if (m.inbox) Seq(ItemState.Created)
|
||||||
else ItemState.validStates,
|
else ItemState.validStates.toList,
|
||||||
m.direction,
|
m.direction,
|
||||||
m.corrPerson,
|
m.corrPerson,
|
||||||
m.corrOrg,
|
m.corrOrg,
|
||||||
@ -470,6 +470,7 @@ trait Conversions {
|
|||||||
case UploadResult.Success => BasicResult(true, "Files submitted.")
|
case UploadResult.Success => BasicResult(true, "Files submitted.")
|
||||||
case UploadResult.NoFiles => BasicResult(false, "There were no files to submit.")
|
case UploadResult.NoFiles => BasicResult(false, "There were no files to submit.")
|
||||||
case UploadResult.NoSource => BasicResult(false, "The source id is not valid.")
|
case UploadResult.NoSource => BasicResult(false, "The source id is not valid.")
|
||||||
|
case UploadResult.NoItem => BasicResult(false, "The item could not be found.")
|
||||||
}
|
}
|
||||||
|
|
||||||
def basicResult(cr: PassChangeResult): BasicResult =
|
def basicResult(cr: PassChangeResult): BasicResult =
|
||||||
|
@ -80,7 +80,7 @@ object IntegrationEndpointRoutes {
|
|||||||
cfg.backend.files.validMimeTypes
|
cfg.backend.files.validMimeTypes
|
||||||
)
|
)
|
||||||
account = AccountId(coll, Ident.unsafe("docspell-system"))
|
account = AccountId(coll, Ident.unsafe("docspell-system"))
|
||||||
result <- backend.upload.submit(updata, account, true)
|
result <- backend.upload.submit(updata, account, true, None)
|
||||||
res <- Ok(basicResult(result))
|
res <- Ok(basicResult(result))
|
||||||
} yield res
|
} yield res
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,13 @@ package docspell.restserver.routes
|
|||||||
|
|
||||||
import cats.effect._
|
import cats.effect._
|
||||||
import cats.implicits._
|
import cats.implicits._
|
||||||
|
import docspell.common._
|
||||||
import docspell.backend.BackendApp
|
import docspell.backend.BackendApp
|
||||||
import docspell.backend.auth.AuthToken
|
import docspell.backend.auth.AuthToken
|
||||||
import docspell.common.{Ident, Priority}
|
|
||||||
import docspell.restserver.Config
|
import docspell.restserver.Config
|
||||||
import docspell.restserver.conv.Conversions._
|
import docspell.restserver.conv.Conversions._
|
||||||
import docspell.restserver.http4s.ResponseGenerator
|
import docspell.restserver.http4s.ResponseGenerator
|
||||||
import org.http4s.HttpRoutes
|
import org.http4s._
|
||||||
import org.http4s.circe.CirceEntityEncoder._
|
import org.http4s.circe.CirceEntityEncoder._
|
||||||
import org.http4s.EntityDecoder._
|
import org.http4s.EntityDecoder._
|
||||||
import org.http4s.dsl.Http4sDsl
|
import org.http4s.dsl.Http4sDsl
|
||||||
@ -26,19 +26,14 @@ object UploadRoutes {
|
|||||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
|
val submitting = submitFiles[F](backend, cfg, Right(user.account)) _
|
||||||
|
|
||||||
HttpRoutes.of {
|
HttpRoutes.of {
|
||||||
case req @ POST -> Root / "item" =>
|
case req @ POST -> Root / "item" =>
|
||||||
for {
|
submitting(req, None, Priority.High, dsl)
|
||||||
multipart <- req.as[Multipart[F]]
|
|
||||||
updata <- readMultipart(
|
case req @ POST -> Root / "item" / Ident(itemId) =>
|
||||||
multipart,
|
submitting(req, Some(itemId), Priority.High, dsl)
|
||||||
logger,
|
|
||||||
Priority.High,
|
|
||||||
cfg.backend.files.validMimeTypes
|
|
||||||
)
|
|
||||||
result <- backend.upload.submit(updata, user.account, true)
|
|
||||||
res <- Ok(basicResult(result))
|
|
||||||
} yield res
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,17 +43,35 @@ object UploadRoutes {
|
|||||||
|
|
||||||
HttpRoutes.of {
|
HttpRoutes.of {
|
||||||
case req @ POST -> Root / "item" / Ident(id) =>
|
case req @ POST -> Root / "item" / Ident(id) =>
|
||||||
|
submitFiles(backend, cfg, Left(id))(req, None, Priority.Low, dsl)
|
||||||
|
|
||||||
|
case req @ POST -> Root / "item" / Ident(itemId) / Ident(id) =>
|
||||||
|
submitFiles(backend, cfg, Left(id))(req, Some(itemId), Priority.Low, dsl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def submitFiles[F[_]: Effect](
|
||||||
|
backend: BackendApp[F],
|
||||||
|
cfg: Config,
|
||||||
|
accOrSrc: Either[Ident, AccountId]
|
||||||
|
)(
|
||||||
|
req: Request[F],
|
||||||
|
itemId: Option[Ident],
|
||||||
|
prio: Priority,
|
||||||
|
dsl: Http4sDsl[F]
|
||||||
|
): F[Response[F]] = {
|
||||||
|
import dsl._
|
||||||
|
|
||||||
for {
|
for {
|
||||||
multipart <- req.as[Multipart[F]]
|
multipart <- req.as[Multipart[F]]
|
||||||
updata <- readMultipart(
|
updata <- readMultipart(
|
||||||
multipart,
|
multipart,
|
||||||
logger,
|
logger,
|
||||||
Priority.Low,
|
prio,
|
||||||
cfg.backend.files.validMimeTypes
|
cfg.backend.files.validMimeTypes
|
||||||
)
|
)
|
||||||
result <- backend.upload.submit(updata, id, true)
|
result <- backend.upload.submitEither(updata, accOrSrc, true, itemId)
|
||||||
res <- Ok(basicResult(result))
|
res <- Ok(basicResult(result))
|
||||||
} yield res
|
} yield res
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -101,4 +101,6 @@ case class Column(name: String, ns: String = "", alias: String = "") {
|
|||||||
def asc: Fragment =
|
def asc: Fragment =
|
||||||
f ++ fr"asc"
|
f ++ fr"asc"
|
||||||
|
|
||||||
|
def max: Fragment =
|
||||||
|
fr"MAX(" ++ f ++ fr")"
|
||||||
}
|
}
|
||||||
|
@ -287,7 +287,7 @@ object QItem {
|
|||||||
n <- store.transact(RItem.deleteByIdAndCollective(itemId, collective))
|
n <- store.transact(RItem.deleteByIdAndCollective(itemId, collective))
|
||||||
} yield tn + rn + n
|
} yield tn + rn + n
|
||||||
|
|
||||||
def findByFileIds(fileMetaIds: List[Ident]): ConnectionIO[Vector[RItem]] = {
|
def findByFileIds(fileMetaIds: Seq[Ident]): ConnectionIO[Vector[RItem]] = {
|
||||||
val IC = RItem.Columns
|
val IC = RItem.Columns
|
||||||
val AC = RAttachment.Columns
|
val AC = RAttachment.Columns
|
||||||
val q =
|
val q =
|
||||||
|
@ -38,6 +38,11 @@ object RAttachment {
|
|||||||
fr"${v.id},${v.itemId},${v.fileId.id},${v.position},${v.created},${v.name}"
|
fr"${v.id},${v.itemId},${v.fileId.id},${v.position},${v.created},${v.name}"
|
||||||
).update.run
|
).update.run
|
||||||
|
|
||||||
|
def nextPosition(id: Ident): ConnectionIO[Int] =
|
||||||
|
for {
|
||||||
|
max <- selectSimple(position.max, table, itemId.is(id)).query[Option[Int]].unique
|
||||||
|
} yield max.map(_ + 1).getOrElse(0)
|
||||||
|
|
||||||
def updateFileIdAndName(
|
def updateFileIdAndName(
|
||||||
attachId: Ident,
|
attachId: Ident,
|
||||||
fId: Ident,
|
fId: Ident,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
package docspell.store.records
|
package docspell.store.records
|
||||||
|
|
||||||
import cats.implicits._
|
import cats.data.NonEmptyList
|
||||||
import cats.effect.Sync
|
import cats.effect.Sync
|
||||||
|
import cats.implicits._
|
||||||
import doobie._
|
import doobie._
|
||||||
import doobie.implicits._
|
import doobie.implicits._
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
@ -110,12 +111,16 @@ object RItem {
|
|||||||
def getCollective(itemId: Ident): ConnectionIO[Option[Ident]] =
|
def getCollective(itemId: Ident): ConnectionIO[Option[Ident]] =
|
||||||
selectSimple(List(cid), table, id.is(itemId)).query[Ident].option
|
selectSimple(List(cid), table, id.is(itemId)).query[Ident].option
|
||||||
|
|
||||||
def updateState(itemId: Ident, itemState: ItemState): ConnectionIO[Int] =
|
def updateState(
|
||||||
|
itemId: Ident,
|
||||||
|
itemState: ItemState,
|
||||||
|
existing: NonEmptyList[ItemState]
|
||||||
|
): ConnectionIO[Int] =
|
||||||
for {
|
for {
|
||||||
t <- currentTime
|
t <- currentTime
|
||||||
n <- updateRow(
|
n <- updateRow(
|
||||||
table,
|
table,
|
||||||
id.is(itemId),
|
and(id.is(itemId), state.isIn(existing)),
|
||||||
commas(state.setTo(itemState), updated.setTo(t))
|
commas(state.setTo(itemState), updated.setTo(t))
|
||||||
).update.run
|
).update.run
|
||||||
} yield n
|
} yield n
|
||||||
@ -285,4 +290,7 @@ object RItem {
|
|||||||
|
|
||||||
def existsById(itemId: Ident): ConnectionIO[Boolean] =
|
def existsById(itemId: Ident): ConnectionIO[Boolean] =
|
||||||
selectCount(id, table, id.is(itemId)).query[Int].unique.map(_ > 0)
|
selectCount(id, table, id.is(itemId)).query[Int].unique.map(_ > 0)
|
||||||
|
|
||||||
|
def findByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[RItem]] =
|
||||||
|
selectSimple(all, table, and(id.is(itemId), cid.is(coll))).query[RItem].option
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,7 @@ module Api exposing
|
|||||||
, submitNotifyDueItems
|
, submitNotifyDueItems
|
||||||
, updateScanMailbox
|
, updateScanMailbox
|
||||||
, upload
|
, upload
|
||||||
|
, uploadAmend
|
||||||
, uploadSingle
|
, uploadSingle
|
||||||
, versionInfo
|
, versionInfo
|
||||||
)
|
)
|
||||||
@ -429,7 +430,42 @@ createImapSettings flags mname ems receive =
|
|||||||
--- Upload
|
--- Upload
|
||||||
|
|
||||||
|
|
||||||
upload : Flags -> Maybe String -> ItemUploadMeta -> List File -> (String -> Result Http.Error BasicResult -> msg) -> List (Cmd msg)
|
uploadAmend :
|
||||||
|
Flags
|
||||||
|
-> String
|
||||||
|
-> List File
|
||||||
|
-> (String -> Result Http.Error BasicResult -> msg)
|
||||||
|
-> List (Cmd msg)
|
||||||
|
uploadAmend flags itemId files receive =
|
||||||
|
let
|
||||||
|
mkReq file =
|
||||||
|
let
|
||||||
|
fid =
|
||||||
|
Util.File.makeFileId file
|
||||||
|
|
||||||
|
path =
|
||||||
|
"/api/v1/sec/upload/item/" ++ itemId
|
||||||
|
in
|
||||||
|
Http2.authPostTrack
|
||||||
|
{ url = flags.config.baseUrl ++ path
|
||||||
|
, account = getAccount flags
|
||||||
|
, body =
|
||||||
|
Http.multipartBody <|
|
||||||
|
[ Http.filePart "file[]" file ]
|
||||||
|
, expect = Http.expectJson (receive fid) Api.Model.BasicResult.decoder
|
||||||
|
, tracker = fid
|
||||||
|
}
|
||||||
|
in
|
||||||
|
List.map mkReq files
|
||||||
|
|
||||||
|
|
||||||
|
upload :
|
||||||
|
Flags
|
||||||
|
-> Maybe String
|
||||||
|
-> ItemUploadMeta
|
||||||
|
-> List File
|
||||||
|
-> (String -> Result Http.Error BasicResult -> msg)
|
||||||
|
-> List (Cmd msg)
|
||||||
upload flags sourceId meta files receive =
|
upload flags sourceId meta files receive =
|
||||||
let
|
let
|
||||||
metaStr =
|
metaStr =
|
||||||
@ -457,7 +493,14 @@ upload flags sourceId meta files receive =
|
|||||||
List.map mkReq files
|
List.map mkReq files
|
||||||
|
|
||||||
|
|
||||||
uploadSingle : Flags -> Maybe String -> ItemUploadMeta -> String -> List File -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
uploadSingle :
|
||||||
|
Flags
|
||||||
|
-> Maybe String
|
||||||
|
-> ItemUploadMeta
|
||||||
|
-> String
|
||||||
|
-> List File
|
||||||
|
-> (Result Http.Error BasicResult -> msg)
|
||||||
|
-> Cmd msg
|
||||||
uploadSingle flags sourceId meta track files receive =
|
uploadSingle flags sourceId meta track files receive =
|
||||||
let
|
let
|
||||||
metaStr =
|
metaStr =
|
||||||
|
@ -74,7 +74,7 @@ updateWithSub msg model =
|
|||||||
updateNewInvite m model |> noSub
|
updateNewInvite m model |> noSub
|
||||||
|
|
||||||
ItemDetailMsg m ->
|
ItemDetailMsg m ->
|
||||||
updateItemDetail m model |> noSub
|
updateItemDetail m model
|
||||||
|
|
||||||
VersionResp (Ok info) ->
|
VersionResp (Ok info) ->
|
||||||
( { model | version = info }, Cmd.none ) |> noSub
|
( { model | version = info }, Cmd.none ) |> noSub
|
||||||
@ -172,17 +172,20 @@ updateWithSub msg model =
|
|||||||
( { model | navMenuOpen = not model.navMenuOpen }, Cmd.none, Sub.none )
|
( { model | navMenuOpen = not model.navMenuOpen }, Cmd.none, Sub.none )
|
||||||
|
|
||||||
|
|
||||||
updateItemDetail : Page.ItemDetail.Data.Msg -> Model -> ( Model, Cmd Msg )
|
updateItemDetail : Page.ItemDetail.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg )
|
||||||
updateItemDetail lmsg model =
|
updateItemDetail lmsg model =
|
||||||
let
|
let
|
||||||
inav =
|
inav =
|
||||||
Page.Home.Data.itemNav model.itemDetailModel.detail.item.id model.homeModel
|
Page.Home.Data.itemNav model.itemDetailModel.detail.item.id model.homeModel
|
||||||
|
|
||||||
( lm, lc ) =
|
( lm, lc, ls ) =
|
||||||
Page.ItemDetail.Update.update model.key model.flags inav.next lmsg model.itemDetailModel
|
Page.ItemDetail.Update.update model.key model.flags inav.next lmsg model.itemDetailModel
|
||||||
in
|
in
|
||||||
( { model | itemDetailModel = lm }
|
( { model
|
||||||
|
| itemDetailModel = lm
|
||||||
|
}
|
||||||
, Cmd.map ItemDetailMsg lc
|
, Cmd.map ItemDetailMsg lc
|
||||||
|
, Sub.map ItemDetailMsg ls
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -341,7 +344,19 @@ initPage model page =
|
|||||||
updateQueue Page.Queue.Data.StopRefresh model
|
updateQueue Page.Queue.Data.StopRefresh model
|
||||||
|
|
||||||
ItemDetailPage id ->
|
ItemDetailPage id ->
|
||||||
updateItemDetail (Page.ItemDetail.Data.Init id) model
|
let
|
||||||
|
updateDetail m__ =
|
||||||
|
let
|
||||||
|
( m, c, s ) =
|
||||||
|
updateItemDetail (Page.ItemDetail.Data.Init id) m__
|
||||||
|
in
|
||||||
|
( { m | subs = Sub.batch [ m.subs, s ] }, c )
|
||||||
|
in
|
||||||
|
Util.Update.andThen1
|
||||||
|
[ updateDetail
|
||||||
|
, updateQueue Page.Queue.Data.StopRefresh
|
||||||
|
]
|
||||||
|
model
|
||||||
|
|
||||||
|
|
||||||
noSub : ( Model, Cmd Msg ) -> ( Model, Cmd Msg, Sub Msg )
|
noSub : ( Model, Cmd Msg ) -> ( Model, Cmd Msg, Sub Msg )
|
||||||
|
@ -136,6 +136,12 @@ view model =
|
|||||||
[ i [ class "folder open icon" ] []
|
[ i [ class "folder open icon" ] []
|
||||||
, text "Select ..."
|
, text "Select ..."
|
||||||
]
|
]
|
||||||
|
, div [ class "ui center aligned text container" ]
|
||||||
|
[ span [ class "small-info" ]
|
||||||
|
[ text "Choose document files (pdf, docx, txt, html, …). "
|
||||||
|
, text "Archives (zip and eml) are extracted."
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import Browser.Navigation as Nav
|
|||||||
import Comp.AttachmentMeta
|
import Comp.AttachmentMeta
|
||||||
import Comp.DatePicker
|
import Comp.DatePicker
|
||||||
import Comp.Dropdown exposing (isDropdownChangeMsg)
|
import Comp.Dropdown exposing (isDropdownChangeMsg)
|
||||||
|
import Comp.Dropzone
|
||||||
import Comp.ItemMail
|
import Comp.ItemMail
|
||||||
import Comp.MarkdownInput
|
import Comp.MarkdownInput
|
||||||
import Comp.SentMails
|
import Comp.SentMails
|
||||||
@ -34,12 +35,16 @@ import Data.Flags exposing (Flags)
|
|||||||
import Data.Icons as Icons
|
import Data.Icons as Icons
|
||||||
import DatePicker exposing (DatePicker)
|
import DatePicker exposing (DatePicker)
|
||||||
import Dict exposing (Dict)
|
import Dict exposing (Dict)
|
||||||
|
import File exposing (File)
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes exposing (..)
|
import Html.Attributes exposing (..)
|
||||||
import Html.Events exposing (onCheck, onClick, onInput)
|
import Html.Events exposing (onCheck, onClick, onInput)
|
||||||
import Http
|
import Http
|
||||||
import Markdown
|
import Markdown
|
||||||
import Page exposing (Page(..))
|
import Page exposing (Page(..))
|
||||||
|
import Ports
|
||||||
|
import Set exposing (Set)
|
||||||
|
import Util.File exposing (makeFileId)
|
||||||
import Util.Http
|
import Util.Http
|
||||||
import Util.List
|
import Util.List
|
||||||
import Util.Maybe
|
import Util.Maybe
|
||||||
@ -77,6 +82,12 @@ type alias Model =
|
|||||||
, attachMetaOpen : Bool
|
, attachMetaOpen : Bool
|
||||||
, pdfNativeView : Bool
|
, pdfNativeView : Bool
|
||||||
, deleteAttachConfirm : Comp.YesNoDimmer.Model
|
, deleteAttachConfirm : Comp.YesNoDimmer.Model
|
||||||
|
, addFilesOpen : Bool
|
||||||
|
, addFilesModel : Comp.Dropzone.Model
|
||||||
|
, selectedFiles : List File
|
||||||
|
, completed : Set String
|
||||||
|
, errored : Set String
|
||||||
|
, loading : Set String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -165,6 +176,12 @@ emptyModel =
|
|||||||
, attachMetaOpen = False
|
, attachMetaOpen = False
|
||||||
, pdfNativeView = False
|
, pdfNativeView = False
|
||||||
, deleteAttachConfirm = Comp.YesNoDimmer.emptyModel
|
, deleteAttachConfirm = Comp.YesNoDimmer.emptyModel
|
||||||
|
, addFilesOpen = False
|
||||||
|
, addFilesModel = Comp.Dropzone.init Comp.Dropzone.defaultSettings
|
||||||
|
, selectedFiles = []
|
||||||
|
, completed = Set.empty
|
||||||
|
, errored = Set.empty
|
||||||
|
, loading = Set.empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -221,6 +238,12 @@ type Msg
|
|||||||
| RequestDeleteAttachment String
|
| RequestDeleteAttachment String
|
||||||
| DeleteAttachConfirm String Comp.YesNoDimmer.Msg
|
| DeleteAttachConfirm String Comp.YesNoDimmer.Msg
|
||||||
| DeleteAttachResp (Result Http.Error BasicResult)
|
| DeleteAttachResp (Result Http.Error BasicResult)
|
||||||
|
| AddFilesToggle
|
||||||
|
| AddFilesMsg Comp.Dropzone.Msg
|
||||||
|
| AddFilesSubmitUpload
|
||||||
|
| AddFilesUploadResp String (Result Http.Error BasicResult)
|
||||||
|
| AddFilesProgress String Http.Progress
|
||||||
|
| AddFilesReset
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -334,7 +357,48 @@ setDueDate flags model date =
|
|||||||
Api.setItemDueDate flags model.item.id (OptionalDate date) SaveResp
|
Api.setItemDueDate flags model.item.id (OptionalDate date) SaveResp
|
||||||
|
|
||||||
|
|
||||||
update : Nav.Key -> Flags -> Maybe String -> Msg -> Model -> ( Model, Cmd Msg )
|
isLoading : Model -> File -> Bool
|
||||||
|
isLoading model file =
|
||||||
|
Set.member (makeFileId file) model.loading
|
||||||
|
|
||||||
|
|
||||||
|
isCompleted : Model -> File -> Bool
|
||||||
|
isCompleted model file =
|
||||||
|
Set.member (makeFileId file) model.completed
|
||||||
|
|
||||||
|
|
||||||
|
isError : Model -> File -> Bool
|
||||||
|
isError model file =
|
||||||
|
Set.member (makeFileId file) model.errored
|
||||||
|
|
||||||
|
|
||||||
|
isIdle : Model -> File -> Bool
|
||||||
|
isIdle model file =
|
||||||
|
not (isLoading model file || isCompleted model file || isError model file)
|
||||||
|
|
||||||
|
|
||||||
|
setCompleted : Model -> String -> Set String
|
||||||
|
setCompleted model fileid =
|
||||||
|
Set.insert fileid model.completed
|
||||||
|
|
||||||
|
|
||||||
|
setErrored : Model -> String -> Set String
|
||||||
|
setErrored model fileid =
|
||||||
|
Set.insert fileid model.errored
|
||||||
|
|
||||||
|
|
||||||
|
isSuccessAll : Model -> Bool
|
||||||
|
isSuccessAll model =
|
||||||
|
List.map makeFileId model.selectedFiles
|
||||||
|
|> List.all (\id -> Set.member id model.completed)
|
||||||
|
|
||||||
|
|
||||||
|
noSub : ( Model, Cmd Msg ) -> ( Model, Cmd Msg, Sub Msg )
|
||||||
|
noSub ( m, c ) =
|
||||||
|
( m, c, Sub.none )
|
||||||
|
|
||||||
|
|
||||||
|
update : Nav.Key -> Flags -> Maybe String -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg )
|
||||||
update key flags next msg model =
|
update key flags next msg model =
|
||||||
case msg of
|
case msg of
|
||||||
Init ->
|
Init ->
|
||||||
@ -345,6 +409,7 @@ update key flags next msg model =
|
|||||||
( im, ic ) =
|
( im, ic ) =
|
||||||
Comp.ItemMail.init flags
|
Comp.ItemMail.init flags
|
||||||
in
|
in
|
||||||
|
noSub
|
||||||
( { model | itemDatePicker = dp, dueDatePicker = dp, itemMail = im, visibleAttach = 0 }
|
( { model | itemDatePicker = dp, dueDatePicker = dp, itemMail = im, visibleAttach = 0 }
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ getOptions flags
|
[ getOptions flags
|
||||||
@ -357,10 +422,10 @@ update key flags next msg model =
|
|||||||
|
|
||||||
SetItem item ->
|
SetItem item ->
|
||||||
let
|
let
|
||||||
( m1, c1 ) =
|
( m1, c1, s1 ) =
|
||||||
update key flags next (TagDropdownMsg (Comp.Dropdown.SetSelection item.tags)) model
|
update key flags next (TagDropdownMsg (Comp.Dropdown.SetSelection item.tags)) model
|
||||||
|
|
||||||
( m2, c2 ) =
|
( m2, c2, s2 ) =
|
||||||
update key
|
update key
|
||||||
flags
|
flags
|
||||||
next
|
next
|
||||||
@ -374,7 +439,7 @@ update key flags next msg model =
|
|||||||
)
|
)
|
||||||
m1
|
m1
|
||||||
|
|
||||||
( m3, c3 ) =
|
( m3, c3, s3 ) =
|
||||||
update key
|
update key
|
||||||
flags
|
flags
|
||||||
next
|
next
|
||||||
@ -388,7 +453,7 @@ update key flags next msg model =
|
|||||||
)
|
)
|
||||||
m2
|
m2
|
||||||
|
|
||||||
( m4, c4 ) =
|
( m4, c4, s4 ) =
|
||||||
update key
|
update key
|
||||||
flags
|
flags
|
||||||
next
|
next
|
||||||
@ -402,7 +467,7 @@ update key flags next msg model =
|
|||||||
)
|
)
|
||||||
m3
|
m3
|
||||||
|
|
||||||
( m5, c5 ) =
|
( m5, c5, s5 ) =
|
||||||
update key
|
update key
|
||||||
flags
|
flags
|
||||||
next
|
next
|
||||||
@ -416,7 +481,7 @@ update key flags next msg model =
|
|||||||
)
|
)
|
||||||
m4
|
m4
|
||||||
|
|
||||||
( m6, c6 ) =
|
( m6, c6, s6 ) =
|
||||||
update key
|
update key
|
||||||
flags
|
flags
|
||||||
next
|
next
|
||||||
@ -430,6 +495,9 @@ update key flags next msg model =
|
|||||||
)
|
)
|
||||||
m5
|
m5
|
||||||
|
|
||||||
|
( m7, c7, s7 ) =
|
||||||
|
update key flags next AddFilesReset m6
|
||||||
|
|
||||||
proposalCmd =
|
proposalCmd =
|
||||||
if item.state == "created" then
|
if item.state == "created" then
|
||||||
Api.getItemProposals flags item.id GetProposalResp
|
Api.getItemProposals flags item.id GetProposalResp
|
||||||
@ -437,7 +505,7 @@ update key flags next msg model =
|
|||||||
else
|
else
|
||||||
Cmd.none
|
Cmd.none
|
||||||
in
|
in
|
||||||
( { m6
|
( { m7
|
||||||
| item = item
|
| item = item
|
||||||
, nameModel = item.name
|
, nameModel = item.name
|
||||||
, notesModel = item.notes
|
, notesModel = item.notes
|
||||||
@ -453,24 +521,26 @@ update key flags next msg model =
|
|||||||
, c4
|
, c4
|
||||||
, c5
|
, c5
|
||||||
, c6
|
, c6
|
||||||
|
, c7
|
||||||
, getOptions flags
|
, getOptions flags
|
||||||
, proposalCmd
|
, proposalCmd
|
||||||
, Api.getSentMails flags item.id SentMailsResp
|
, Api.getSentMails flags item.id SentMailsResp
|
||||||
]
|
]
|
||||||
|
, Sub.batch [ s1, s2, s3, s4, s5, s6, s7 ]
|
||||||
)
|
)
|
||||||
|
|
||||||
SetActiveAttachment pos ->
|
SetActiveAttachment pos ->
|
||||||
( { model | visibleAttach = pos, sentMailsOpen = False }, Cmd.none )
|
noSub ( { model | visibleAttach = pos, sentMailsOpen = False }, Cmd.none )
|
||||||
|
|
||||||
ToggleMenu ->
|
ToggleMenu ->
|
||||||
( { model | menuOpen = not model.menuOpen }, Cmd.none )
|
noSub ( { model | menuOpen = not model.menuOpen }, Cmd.none )
|
||||||
|
|
||||||
ReloadItem ->
|
ReloadItem ->
|
||||||
if model.item.id == "" then
|
if model.item.id == "" then
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
else
|
else
|
||||||
( model, Api.itemDetail flags model.item.id GetItemResp )
|
noSub ( model, Api.itemDetail flags model.item.id GetItemResp )
|
||||||
|
|
||||||
TagDropdownMsg m ->
|
TagDropdownMsg m ->
|
||||||
let
|
let
|
||||||
@ -487,7 +557,7 @@ update key flags next msg model =
|
|||||||
else
|
else
|
||||||
Cmd.none
|
Cmd.none
|
||||||
in
|
in
|
||||||
( newModel, Cmd.batch [ save, Cmd.map TagDropdownMsg c2 ] )
|
noSub ( newModel, Cmd.batch [ save, Cmd.map TagDropdownMsg c2 ] )
|
||||||
|
|
||||||
DirDropdownMsg m ->
|
DirDropdownMsg m ->
|
||||||
let
|
let
|
||||||
@ -504,7 +574,7 @@ update key flags next msg model =
|
|||||||
else
|
else
|
||||||
Cmd.none
|
Cmd.none
|
||||||
in
|
in
|
||||||
( newModel, Cmd.batch [ save, Cmd.map DirDropdownMsg c2 ] )
|
noSub ( newModel, Cmd.batch [ save, Cmd.map DirDropdownMsg c2 ] )
|
||||||
|
|
||||||
OrgDropdownMsg m ->
|
OrgDropdownMsg m ->
|
||||||
let
|
let
|
||||||
@ -524,7 +594,7 @@ update key flags next msg model =
|
|||||||
else
|
else
|
||||||
Cmd.none
|
Cmd.none
|
||||||
in
|
in
|
||||||
( newModel, Cmd.batch [ save, Cmd.map OrgDropdownMsg c2 ] )
|
noSub ( newModel, Cmd.batch [ save, Cmd.map OrgDropdownMsg c2 ] )
|
||||||
|
|
||||||
CorrPersonMsg m ->
|
CorrPersonMsg m ->
|
||||||
let
|
let
|
||||||
@ -544,7 +614,7 @@ update key flags next msg model =
|
|||||||
else
|
else
|
||||||
Cmd.none
|
Cmd.none
|
||||||
in
|
in
|
||||||
( newModel, Cmd.batch [ save, Cmd.map CorrPersonMsg c2 ] )
|
noSub ( newModel, Cmd.batch [ save, Cmd.map CorrPersonMsg c2 ] )
|
||||||
|
|
||||||
ConcPersonMsg m ->
|
ConcPersonMsg m ->
|
||||||
let
|
let
|
||||||
@ -564,7 +634,7 @@ update key flags next msg model =
|
|||||||
else
|
else
|
||||||
Cmd.none
|
Cmd.none
|
||||||
in
|
in
|
||||||
( newModel, Cmd.batch [ save, Cmd.map ConcPersonMsg c2 ] )
|
noSub ( newModel, Cmd.batch [ save, Cmd.map ConcPersonMsg c2 ] )
|
||||||
|
|
||||||
ConcEquipMsg m ->
|
ConcEquipMsg m ->
|
||||||
let
|
let
|
||||||
@ -584,20 +654,22 @@ update key flags next msg model =
|
|||||||
else
|
else
|
||||||
Cmd.none
|
Cmd.none
|
||||||
in
|
in
|
||||||
( newModel, Cmd.batch [ save, Cmd.map ConcEquipMsg c2 ] )
|
noSub ( newModel, Cmd.batch [ save, Cmd.map ConcEquipMsg c2 ] )
|
||||||
|
|
||||||
SetName str ->
|
SetName str ->
|
||||||
( { model | nameModel = str }, Cmd.none )
|
noSub ( { model | nameModel = str }, Cmd.none )
|
||||||
|
|
||||||
SaveName ->
|
SaveName ->
|
||||||
( model, setName flags model )
|
noSub ( model, setName flags model )
|
||||||
|
|
||||||
SetNotes str ->
|
SetNotes str ->
|
||||||
|
noSub
|
||||||
( { model | notesModel = Util.Maybe.fromString str }
|
( { model | notesModel = Util.Maybe.fromString str }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
ToggleNotes ->
|
ToggleNotes ->
|
||||||
|
noSub
|
||||||
( { model
|
( { model
|
||||||
| notesField =
|
| notesField =
|
||||||
if model.notesField == ViewNotes then
|
if model.notesField == ViewNotes then
|
||||||
@ -610,6 +682,7 @@ update key flags next msg model =
|
|||||||
)
|
)
|
||||||
|
|
||||||
ToggleEditNotes ->
|
ToggleEditNotes ->
|
||||||
|
noSub
|
||||||
( { model
|
( { model
|
||||||
| notesField =
|
| notesField =
|
||||||
if isEditNotes model.notesField then
|
if isEditNotes model.notesField then
|
||||||
@ -628,21 +701,25 @@ update key flags next msg model =
|
|||||||
( lm2, str ) =
|
( lm2, str ) =
|
||||||
Comp.MarkdownInput.update (Maybe.withDefault "" model.notesModel) lm em
|
Comp.MarkdownInput.update (Maybe.withDefault "" model.notesModel) lm em
|
||||||
in
|
in
|
||||||
|
noSub
|
||||||
( { model | notesField = EditNotes lm2, notesModel = Util.Maybe.fromString str }
|
( { model | notesField = EditNotes lm2, notesModel = Util.Maybe.fromString str }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
_ ->
|
HideNotes ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
|
ViewNotes ->
|
||||||
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
SaveNotes ->
|
SaveNotes ->
|
||||||
( model, setNotes flags model )
|
noSub ( model, setNotes flags model )
|
||||||
|
|
||||||
ConfirmItem ->
|
ConfirmItem ->
|
||||||
( model, Api.setConfirmed flags model.item.id SaveResp )
|
noSub ( model, Api.setConfirmed flags model.item.id SaveResp )
|
||||||
|
|
||||||
UnconfirmItem ->
|
UnconfirmItem ->
|
||||||
( model, Api.setUnconfirmed flags model.item.id SaveResp )
|
noSub ( model, Api.setUnconfirmed flags model.item.id SaveResp )
|
||||||
|
|
||||||
ItemDatePickerMsg m ->
|
ItemDatePickerMsg m ->
|
||||||
let
|
let
|
||||||
@ -655,13 +732,13 @@ update key flags next msg model =
|
|||||||
newModel =
|
newModel =
|
||||||
{ model | itemDatePicker = dp, itemDate = Just (Comp.DatePicker.midOfDay date) }
|
{ model | itemDatePicker = dp, itemDate = Just (Comp.DatePicker.midOfDay date) }
|
||||||
in
|
in
|
||||||
( newModel, setDate flags newModel newModel.itemDate )
|
noSub ( newModel, setDate flags newModel newModel.itemDate )
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
( { model | itemDatePicker = dp }, Cmd.none )
|
noSub ( { model | itemDatePicker = dp }, Cmd.none )
|
||||||
|
|
||||||
RemoveDate ->
|
RemoveDate ->
|
||||||
( { model | itemDate = Nothing }, setDate flags model Nothing )
|
noSub ( { model | itemDate = Nothing }, setDate flags model Nothing )
|
||||||
|
|
||||||
DueDatePickerMsg m ->
|
DueDatePickerMsg m ->
|
||||||
let
|
let
|
||||||
@ -674,13 +751,13 @@ update key flags next msg model =
|
|||||||
newModel =
|
newModel =
|
||||||
{ model | dueDatePicker = dp, dueDate = Just (Comp.DatePicker.midOfDay date) }
|
{ model | dueDatePicker = dp, dueDate = Just (Comp.DatePicker.midOfDay date) }
|
||||||
in
|
in
|
||||||
( newModel, setDueDate flags newModel newModel.dueDate )
|
noSub ( newModel, setDueDate flags newModel newModel.dueDate )
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
( { model | dueDatePicker = dp }, Cmd.none )
|
noSub ( { model | dueDatePicker = dp }, Cmd.none )
|
||||||
|
|
||||||
RemoveDueDate ->
|
RemoveDueDate ->
|
||||||
( { model | dueDate = Nothing }, setDueDate flags model Nothing )
|
noSub ( { model | dueDate = Nothing }, setDueDate flags model Nothing )
|
||||||
|
|
||||||
DeleteItemConfirm m ->
|
DeleteItemConfirm m ->
|
||||||
let
|
let
|
||||||
@ -694,41 +771,41 @@ update key flags next msg model =
|
|||||||
else
|
else
|
||||||
Cmd.none
|
Cmd.none
|
||||||
in
|
in
|
||||||
( { model | deleteItemConfirm = cm }, cmd )
|
noSub ( { model | deleteItemConfirm = cm }, cmd )
|
||||||
|
|
||||||
RequestDelete ->
|
RequestDelete ->
|
||||||
update key flags next (DeleteItemConfirm Comp.YesNoDimmer.activate) model
|
update key flags next (DeleteItemConfirm Comp.YesNoDimmer.activate) model
|
||||||
|
|
||||||
SetCorrOrgSuggestion idname ->
|
SetCorrOrgSuggestion idname ->
|
||||||
( model, setCorrOrg flags model (Just idname) )
|
noSub ( model, setCorrOrg flags model (Just idname) )
|
||||||
|
|
||||||
SetCorrPersonSuggestion idname ->
|
SetCorrPersonSuggestion idname ->
|
||||||
( model, setCorrPerson flags model (Just idname) )
|
noSub ( model, setCorrPerson flags model (Just idname) )
|
||||||
|
|
||||||
SetConcPersonSuggestion idname ->
|
SetConcPersonSuggestion idname ->
|
||||||
( model, setConcPerson flags model (Just idname) )
|
noSub ( model, setConcPerson flags model (Just idname) )
|
||||||
|
|
||||||
SetConcEquipSuggestion idname ->
|
SetConcEquipSuggestion idname ->
|
||||||
( model, setConcEquip flags model (Just idname) )
|
noSub ( model, setConcEquip flags model (Just idname) )
|
||||||
|
|
||||||
SetItemDateSuggestion date ->
|
SetItemDateSuggestion date ->
|
||||||
( model, setDate flags model (Just date) )
|
noSub ( model, setDate flags model (Just date) )
|
||||||
|
|
||||||
SetDueDateSuggestion date ->
|
SetDueDateSuggestion date ->
|
||||||
( model, setDueDate flags model (Just date) )
|
noSub ( model, setDueDate flags model (Just date) )
|
||||||
|
|
||||||
GetTagsResp (Ok tags) ->
|
GetTagsResp (Ok tags) ->
|
||||||
let
|
let
|
||||||
tagList =
|
tagList =
|
||||||
Comp.Dropdown.SetOptions tags.items
|
Comp.Dropdown.SetOptions tags.items
|
||||||
|
|
||||||
( m1, c1 ) =
|
( m1, c1, s1 ) =
|
||||||
update key flags next (TagDropdownMsg tagList) model
|
update key flags next (TagDropdownMsg tagList) model
|
||||||
in
|
in
|
||||||
( m1, c1 )
|
( m1, c1, s1 )
|
||||||
|
|
||||||
GetTagsResp (Err _) ->
|
GetTagsResp (Err _) ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
GetOrgResp (Ok orgs) ->
|
GetOrgResp (Ok orgs) ->
|
||||||
let
|
let
|
||||||
@ -738,23 +815,23 @@ update key flags next msg model =
|
|||||||
update key flags next (OrgDropdownMsg opts) model
|
update key flags next (OrgDropdownMsg opts) model
|
||||||
|
|
||||||
GetOrgResp (Err _) ->
|
GetOrgResp (Err _) ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
GetPersonResp (Ok ps) ->
|
GetPersonResp (Ok ps) ->
|
||||||
let
|
let
|
||||||
opts =
|
opts =
|
||||||
Comp.Dropdown.SetOptions ps.items
|
Comp.Dropdown.SetOptions ps.items
|
||||||
|
|
||||||
( m1, c1 ) =
|
( m1, c1, s1 ) =
|
||||||
update key flags next (CorrPersonMsg opts) model
|
update key flags next (CorrPersonMsg opts) model
|
||||||
|
|
||||||
( m2, c2 ) =
|
( m2, c2, s2 ) =
|
||||||
update key flags next (ConcPersonMsg opts) m1
|
update key flags next (ConcPersonMsg opts) m1
|
||||||
in
|
in
|
||||||
( m2, Cmd.batch [ c1, c2 ] )
|
( m2, Cmd.batch [ c1, c2 ], Sub.batch [ s1, s2 ] )
|
||||||
|
|
||||||
GetPersonResp (Err _) ->
|
GetPersonResp (Err _) ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
GetEquipResp (Ok equips) ->
|
GetEquipResp (Ok equips) ->
|
||||||
let
|
let
|
||||||
@ -767,44 +844,44 @@ update key flags next msg model =
|
|||||||
update key flags next (ConcEquipMsg opts) model
|
update key flags next (ConcEquipMsg opts) model
|
||||||
|
|
||||||
GetEquipResp (Err _) ->
|
GetEquipResp (Err _) ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
SaveResp (Ok res) ->
|
SaveResp (Ok res) ->
|
||||||
if res.success then
|
if res.success then
|
||||||
( model, Api.itemDetail flags model.item.id GetItemResp )
|
noSub ( model, Api.itemDetail flags model.item.id GetItemResp )
|
||||||
|
|
||||||
else
|
else
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
SaveResp (Err _) ->
|
SaveResp (Err _) ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
DeleteResp (Ok res) ->
|
DeleteResp (Ok res) ->
|
||||||
if res.success then
|
if res.success then
|
||||||
case next of
|
case next of
|
||||||
Just id ->
|
Just id ->
|
||||||
( model, Page.set key (ItemDetailPage id) )
|
noSub ( model, Page.set key (ItemDetailPage id) )
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
( model, Page.set key HomePage )
|
noSub ( model, Page.set key HomePage )
|
||||||
|
|
||||||
else
|
else
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
DeleteResp (Err _) ->
|
DeleteResp (Err _) ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
GetItemResp (Ok item) ->
|
GetItemResp (Ok item) ->
|
||||||
update key flags next (SetItem item) model
|
update key flags next (SetItem item) model
|
||||||
|
|
||||||
GetItemResp (Err _) ->
|
GetItemResp (Err _) ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
GetProposalResp (Ok ip) ->
|
GetProposalResp (Ok ip) ->
|
||||||
( { model | itemProposals = ip }, Cmd.none )
|
noSub ( { model | itemProposals = ip }, Cmd.none )
|
||||||
|
|
||||||
GetProposalResp (Err _) ->
|
GetProposalResp (Err _) ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
ItemMailMsg m ->
|
ItemMailMsg m ->
|
||||||
let
|
let
|
||||||
@ -813,9 +890,10 @@ update key flags next msg model =
|
|||||||
in
|
in
|
||||||
case fa of
|
case fa of
|
||||||
Comp.ItemMail.FormNone ->
|
Comp.ItemMail.FormNone ->
|
||||||
( { model | itemMail = im }, Cmd.map ItemMailMsg ic )
|
noSub ( { model | itemMail = im }, Cmd.map ItemMailMsg ic )
|
||||||
|
|
||||||
Comp.ItemMail.FormCancel ->
|
Comp.ItemMail.FormCancel ->
|
||||||
|
noSub
|
||||||
( { model
|
( { model
|
||||||
| itemMail = Comp.ItemMail.clear im
|
| itemMail = Comp.ItemMail.clear im
|
||||||
, mailOpen = False
|
, mailOpen = False
|
||||||
@ -832,6 +910,7 @@ update key flags next msg model =
|
|||||||
, conn = sm.conn
|
, conn = sm.conn
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
|
noSub
|
||||||
( { model | mailSending = True }
|
( { model | mailSending = True }
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ Cmd.map ItemMailMsg ic
|
[ Cmd.map ItemMailMsg ic
|
||||||
@ -851,6 +930,7 @@ update key flags next msg model =
|
|||||||
else
|
else
|
||||||
Nothing
|
Nothing
|
||||||
in
|
in
|
||||||
|
noSub
|
||||||
( { model
|
( { model
|
||||||
| mailOpen = newOpen
|
| mailOpen = newOpen
|
||||||
, mailSendResult = sendResult
|
, mailSendResult = sendResult
|
||||||
@ -867,6 +947,7 @@ update key flags next msg model =
|
|||||||
else
|
else
|
||||||
model.itemMail
|
model.itemMail
|
||||||
in
|
in
|
||||||
|
noSub
|
||||||
( { model
|
( { model
|
||||||
| itemMail = mm
|
| itemMail = mm
|
||||||
, mailSending = False
|
, mailSending = False
|
||||||
@ -884,6 +965,7 @@ update key flags next msg model =
|
|||||||
errmsg =
|
errmsg =
|
||||||
Util.Http.errorToString err
|
Util.Http.errorToString err
|
||||||
in
|
in
|
||||||
|
noSub
|
||||||
( { model
|
( { model
|
||||||
| mailSendResult = Just (BasicResult False errmsg)
|
| mailSendResult = Just (BasicResult False errmsg)
|
||||||
, mailSending = False
|
, mailSending = False
|
||||||
@ -896,24 +978,25 @@ update key flags next msg model =
|
|||||||
sm =
|
sm =
|
||||||
Comp.SentMails.update m model.sentMails
|
Comp.SentMails.update m model.sentMails
|
||||||
in
|
in
|
||||||
( { model | sentMails = sm }, Cmd.none )
|
noSub ( { model | sentMails = sm }, Cmd.none )
|
||||||
|
|
||||||
ToggleSentMails ->
|
ToggleSentMails ->
|
||||||
( { model | sentMailsOpen = not model.sentMailsOpen, visibleAttach = -1 }, Cmd.none )
|
noSub ( { model | sentMailsOpen = not model.sentMailsOpen, visibleAttach = -1 }, Cmd.none )
|
||||||
|
|
||||||
SentMailsResp (Ok list) ->
|
SentMailsResp (Ok list) ->
|
||||||
let
|
let
|
||||||
sm =
|
sm =
|
||||||
Comp.SentMails.initMails list.items
|
Comp.SentMails.initMails list.items
|
||||||
in
|
in
|
||||||
( { model | sentMails = sm }, Cmd.none )
|
noSub ( { model | sentMails = sm }, Cmd.none )
|
||||||
|
|
||||||
SentMailsResp (Err _) ->
|
SentMailsResp (Err _) ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
AttachMetaClick id ->
|
AttachMetaClick id ->
|
||||||
case Dict.get id model.attachMeta of
|
case Dict.get id model.attachMeta of
|
||||||
Just _ ->
|
Just _ ->
|
||||||
|
noSub
|
||||||
( { model | attachMetaOpen = not model.attachMetaOpen }
|
( { model | attachMetaOpen = not model.attachMetaOpen }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
@ -926,6 +1009,7 @@ update key flags next msg model =
|
|||||||
nextMeta =
|
nextMeta =
|
||||||
Dict.insert id am model.attachMeta
|
Dict.insert id am model.attachMeta
|
||||||
in
|
in
|
||||||
|
noSub
|
||||||
( { model | attachMeta = nextMeta, attachMetaOpen = True }
|
( { model | attachMeta = nextMeta, attachMetaOpen = True }
|
||||||
, Cmd.map (AttachMetaMsg id) ac
|
, Cmd.map (AttachMetaMsg id) ac
|
||||||
)
|
)
|
||||||
@ -937,14 +1021,16 @@ update key flags next msg model =
|
|||||||
am =
|
am =
|
||||||
Comp.AttachmentMeta.update lmsg cm
|
Comp.AttachmentMeta.update lmsg cm
|
||||||
in
|
in
|
||||||
|
noSub
|
||||||
( { model | attachMeta = Dict.insert id am model.attachMeta }
|
( { model | attachMeta = Dict.insert id am model.attachMeta }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
TogglePdfNativeView ->
|
TogglePdfNativeView ->
|
||||||
|
noSub
|
||||||
( { model | pdfNativeView = not model.pdfNativeView }
|
( { model | pdfNativeView = not model.pdfNativeView }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
@ -961,17 +1047,17 @@ update key flags next msg model =
|
|||||||
else
|
else
|
||||||
Cmd.none
|
Cmd.none
|
||||||
in
|
in
|
||||||
( { model | deleteAttachConfirm = cm }, cmd )
|
noSub ( { model | deleteAttachConfirm = cm }, cmd )
|
||||||
|
|
||||||
DeleteAttachResp (Ok res) ->
|
DeleteAttachResp (Ok res) ->
|
||||||
if res.success then
|
if res.success then
|
||||||
update key flags next ReloadItem model
|
update key flags next ReloadItem model
|
||||||
|
|
||||||
else
|
else
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
DeleteAttachResp (Err _) ->
|
DeleteAttachResp (Err _) ->
|
||||||
( model, Cmd.none )
|
noSub ( model, Cmd.none )
|
||||||
|
|
||||||
RequestDeleteAttachment id ->
|
RequestDeleteAttachment id ->
|
||||||
update key
|
update key
|
||||||
@ -980,6 +1066,114 @@ update key flags next msg model =
|
|||||||
(DeleteAttachConfirm id Comp.YesNoDimmer.activate)
|
(DeleteAttachConfirm id Comp.YesNoDimmer.activate)
|
||||||
model
|
model
|
||||||
|
|
||||||
|
AddFilesToggle ->
|
||||||
|
noSub
|
||||||
|
( { model | addFilesOpen = not model.addFilesOpen }
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
AddFilesMsg lm ->
|
||||||
|
let
|
||||||
|
( dm, dc, df ) =
|
||||||
|
Comp.Dropzone.update lm model.addFilesModel
|
||||||
|
|
||||||
|
nextFiles =
|
||||||
|
model.selectedFiles ++ df
|
||||||
|
in
|
||||||
|
noSub
|
||||||
|
( { model | addFilesModel = dm, selectedFiles = nextFiles }
|
||||||
|
, Cmd.map AddFilesMsg dc
|
||||||
|
)
|
||||||
|
|
||||||
|
AddFilesReset ->
|
||||||
|
noSub
|
||||||
|
( { model
|
||||||
|
| selectedFiles = []
|
||||||
|
, addFilesModel = Comp.Dropzone.init Comp.Dropzone.defaultSettings
|
||||||
|
, completed = Set.empty
|
||||||
|
, errored = Set.empty
|
||||||
|
, loading = Set.empty
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
AddFilesSubmitUpload ->
|
||||||
|
let
|
||||||
|
fileids =
|
||||||
|
List.map makeFileId model.selectedFiles
|
||||||
|
|
||||||
|
uploads =
|
||||||
|
Cmd.batch (Api.uploadAmend flags model.item.id model.selectedFiles AddFilesUploadResp)
|
||||||
|
|
||||||
|
tracker =
|
||||||
|
Sub.batch <| List.map (\id -> Http.track id (AddFilesProgress id)) fileids
|
||||||
|
|
||||||
|
( cm2, _, _ ) =
|
||||||
|
Comp.Dropzone.update (Comp.Dropzone.setActive False) model.addFilesModel
|
||||||
|
in
|
||||||
|
( { model | loading = Set.fromList fileids, addFilesModel = cm2 }
|
||||||
|
, uploads
|
||||||
|
, tracker
|
||||||
|
)
|
||||||
|
|
||||||
|
AddFilesUploadResp fileid (Ok res) ->
|
||||||
|
let
|
||||||
|
compl =
|
||||||
|
if res.success then
|
||||||
|
setCompleted model fileid
|
||||||
|
|
||||||
|
else
|
||||||
|
model.completed
|
||||||
|
|
||||||
|
errs =
|
||||||
|
if not res.success then
|
||||||
|
setErrored model fileid
|
||||||
|
|
||||||
|
else
|
||||||
|
model.errored
|
||||||
|
|
||||||
|
load =
|
||||||
|
Set.remove fileid model.loading
|
||||||
|
|
||||||
|
newModel =
|
||||||
|
{ model | completed = compl, errored = errs, loading = load }
|
||||||
|
in
|
||||||
|
noSub
|
||||||
|
( newModel
|
||||||
|
, Ports.setProgress ( fileid, 100 )
|
||||||
|
)
|
||||||
|
|
||||||
|
AddFilesUploadResp fileid (Err _) ->
|
||||||
|
let
|
||||||
|
errs =
|
||||||
|
setErrored model fileid
|
||||||
|
|
||||||
|
load =
|
||||||
|
Set.remove fileid model.loading
|
||||||
|
in
|
||||||
|
noSub ( { model | errored = errs, loading = load }, Cmd.none )
|
||||||
|
|
||||||
|
AddFilesProgress fileid progress ->
|
||||||
|
let
|
||||||
|
percent =
|
||||||
|
case progress of
|
||||||
|
Http.Sending p ->
|
||||||
|
Http.fractionSent p
|
||||||
|
|> (*) 100
|
||||||
|
|> round
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
0
|
||||||
|
|
||||||
|
updateBars =
|
||||||
|
if percent == 0 then
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
else
|
||||||
|
Ports.setProgress ( fileid, percent )
|
||||||
|
in
|
||||||
|
noSub ( model, updateBars )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- view
|
-- view
|
||||||
@ -1001,7 +1195,11 @@ view inav model =
|
|||||||
, div
|
, div
|
||||||
[ classList
|
[ classList
|
||||||
[ ( "ui ablue-comp menu", True )
|
[ ( "ui ablue-comp menu", True )
|
||||||
, ( "top attached", model.mailOpen )
|
, ( "top attached"
|
||||||
|
, model.mailOpen
|
||||||
|
|| model.addFilesOpen
|
||||||
|
|| isEditNotes model.notesField
|
||||||
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
[ a [ class "item", Page.href HomePage ]
|
[ a [ class "item", Page.href HomePage ]
|
||||||
@ -1066,8 +1264,25 @@ view inav model =
|
|||||||
]
|
]
|
||||||
[ Icons.editNotesIcon
|
[ Icons.editNotesIcon
|
||||||
]
|
]
|
||||||
|
, a
|
||||||
|
[ classList
|
||||||
|
[ ( "toggle item", True )
|
||||||
|
, ( "active", model.addFilesOpen )
|
||||||
|
]
|
||||||
|
, if model.addFilesOpen then
|
||||||
|
title "Close"
|
||||||
|
|
||||||
|
else
|
||||||
|
title "Add Files"
|
||||||
|
, onClick AddFilesToggle
|
||||||
|
, href "#"
|
||||||
|
]
|
||||||
|
[ Icons.addFilesIcon
|
||||||
|
]
|
||||||
]
|
]
|
||||||
, renderMailForm model
|
, renderMailForm model
|
||||||
|
, renderAddFilesForm model
|
||||||
|
, renderNotes model
|
||||||
, div [ class "ui grid" ]
|
, div [ class "ui grid" ]
|
||||||
[ Html.map DeleteItemConfirm (Comp.YesNoDimmer.view model.deleteItemConfirm)
|
[ Html.map DeleteItemConfirm (Comp.YesNoDimmer.view model.deleteItemConfirm)
|
||||||
, div
|
, div
|
||||||
@ -1091,8 +1306,7 @@ view inav model =
|
|||||||
]
|
]
|
||||||
<|
|
<|
|
||||||
List.concat
|
List.concat
|
||||||
[ renderNotes model
|
[ [ renderAttachmentsTabMenu model
|
||||||
, [ renderAttachmentsTabMenu model
|
|
||||||
]
|
]
|
||||||
, renderAttachmentsTabBody model
|
, renderAttachmentsTabBody model
|
||||||
, renderIdInfo model
|
, renderIdInfo model
|
||||||
@ -1117,16 +1331,16 @@ renderIdInfo model =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
renderNotes : Model -> List (Html Msg)
|
renderNotes : Model -> Html Msg
|
||||||
renderNotes model =
|
renderNotes model =
|
||||||
case model.notesField of
|
case model.notesField of
|
||||||
HideNotes ->
|
HideNotes ->
|
||||||
case model.item.notes of
|
case model.item.notes of
|
||||||
Nothing ->
|
Nothing ->
|
||||||
[]
|
span [ class "invisible hidden" ] []
|
||||||
|
|
||||||
Just _ ->
|
Just _ ->
|
||||||
[ div [ class "ui segment" ]
|
div [ class "ui segment" ]
|
||||||
[ a
|
[ a
|
||||||
[ class "ui top left attached label"
|
[ class "ui top left attached label"
|
||||||
, onClick ToggleNotes
|
, onClick ToggleNotes
|
||||||
@ -1136,15 +1350,14 @@ renderNotes model =
|
|||||||
, text "Show notes…"
|
, text "Show notes…"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
|
||||||
|
|
||||||
ViewNotes ->
|
ViewNotes ->
|
||||||
case model.item.notes of
|
case model.item.notes of
|
||||||
Nothing ->
|
Nothing ->
|
||||||
[]
|
span [ class "hidden invisible" ] []
|
||||||
|
|
||||||
Just str ->
|
Just str ->
|
||||||
[ div [ class "ui segment" ]
|
div [ class "ui raised segment item-notes-display" ]
|
||||||
[ Markdown.toHtml [ class "item-notes" ] str
|
[ Markdown.toHtml [ class "item-notes" ] str
|
||||||
, a
|
, a
|
||||||
[ class "ui left corner label"
|
[ class "ui left corner label"
|
||||||
@ -1154,10 +1367,9 @@ renderNotes model =
|
|||||||
[ i [ class "eye slash icon" ] []
|
[ i [ class "eye slash icon" ] []
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
|
||||||
|
|
||||||
EditNotes mm ->
|
EditNotes mm ->
|
||||||
[ div [ class "ui segment" ]
|
div [ class "ui bottom attached segment" ]
|
||||||
[ Html.map NotesEditMsg (Comp.MarkdownInput.view (Maybe.withDefault "" model.notesModel) mm)
|
[ Html.map NotesEditMsg (Comp.MarkdownInput.view (Maybe.withDefault "" model.notesModel) mm)
|
||||||
, div [ class "ui secondary menu" ]
|
, div [ class "ui secondary menu" ]
|
||||||
[ a
|
[ a
|
||||||
@ -1178,7 +1390,6 @@ renderNotes model =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
attachmentVisible : Model -> Int -> Bool
|
attachmentVisible : Model -> Int -> Bool
|
||||||
@ -1722,7 +1933,10 @@ renderMailForm model =
|
|||||||
, ( "invisible hidden", not model.mailOpen )
|
, ( "invisible hidden", not model.mailOpen )
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
[ div
|
[ h4 [ class "ui header" ]
|
||||||
|
[ text "Send this item via E-Mail"
|
||||||
|
]
|
||||||
|
, div
|
||||||
[ classList
|
[ classList
|
||||||
[ ( "ui dimmer", True )
|
[ ( "ui dimmer", True )
|
||||||
, ( "active", model.mailSending )
|
, ( "active", model.mailSending )
|
||||||
@ -1732,9 +1946,6 @@ renderMailForm model =
|
|||||||
[ text "Sending …"
|
[ text "Sending …"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
, h4 [ class "ui header" ]
|
|
||||||
[ text "Send this item via E-Mail"
|
|
||||||
]
|
|
||||||
, Html.map ItemMailMsg (Comp.ItemMail.view model.itemMail)
|
, Html.map ItemMailMsg (Comp.ItemMail.view model.itemMail)
|
||||||
, div
|
, div
|
||||||
[ classList
|
[ classList
|
||||||
@ -1756,3 +1967,94 @@ renderMailForm model =
|
|||||||
|> text
|
|> text
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
renderAddFilesForm : Model -> Html Msg
|
||||||
|
renderAddFilesForm model =
|
||||||
|
div
|
||||||
|
[ classList
|
||||||
|
[ ( "ui bottom attached segment", True )
|
||||||
|
, ( "invisible hidden", not model.addFilesOpen )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ h4 [ class "ui header" ]
|
||||||
|
[ text "Add more files to this item"
|
||||||
|
]
|
||||||
|
, Html.map AddFilesMsg (Comp.Dropzone.view model.addFilesModel)
|
||||||
|
, button
|
||||||
|
[ class "ui primary button"
|
||||||
|
, href "#"
|
||||||
|
, onClick AddFilesSubmitUpload
|
||||||
|
]
|
||||||
|
[ text "Submit"
|
||||||
|
]
|
||||||
|
, button
|
||||||
|
[ class "ui secondary button"
|
||||||
|
, href "#"
|
||||||
|
, onClick AddFilesReset
|
||||||
|
]
|
||||||
|
[ text "Reset"
|
||||||
|
]
|
||||||
|
, div
|
||||||
|
[ classList
|
||||||
|
[ ( "ui success message", True )
|
||||||
|
, ( "invisible hidden", model.selectedFiles == [] || not (isSuccessAll model) )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ text "All files have been uploaded. They are being processed, some data "
|
||||||
|
, text "may not be available immediately. "
|
||||||
|
, a
|
||||||
|
[ class "link"
|
||||||
|
, href "#"
|
||||||
|
, onClick ReloadItem
|
||||||
|
]
|
||||||
|
[ text "Refresh now"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "ui items" ]
|
||||||
|
(List.map (renderFileItem model) model.selectedFiles)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
renderFileItem : Model -> File -> Html Msg
|
||||||
|
renderFileItem model file =
|
||||||
|
let
|
||||||
|
name =
|
||||||
|
File.name file
|
||||||
|
|
||||||
|
size =
|
||||||
|
File.size file
|
||||||
|
|> toFloat
|
||||||
|
|> Util.Size.bytesReadable Util.Size.B
|
||||||
|
in
|
||||||
|
div [ class "item" ]
|
||||||
|
[ i
|
||||||
|
[ classList
|
||||||
|
[ ( "large", True )
|
||||||
|
, ( "file outline icon", isIdle model file )
|
||||||
|
, ( "loading spinner icon", isLoading model file )
|
||||||
|
, ( "green check icon", isCompleted model file )
|
||||||
|
, ( "red bolt icon", isError model file )
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, div [ class "middle aligned content" ]
|
||||||
|
[ div [ class "header" ]
|
||||||
|
[ text name
|
||||||
|
]
|
||||||
|
, div [ class "right floated meta" ]
|
||||||
|
[ text size
|
||||||
|
]
|
||||||
|
, div [ class "description" ]
|
||||||
|
[ div
|
||||||
|
[ classList
|
||||||
|
[ ( "ui small indicating progress", True )
|
||||||
|
]
|
||||||
|
, id (makeFileId file)
|
||||||
|
]
|
||||||
|
[ div [ class "bar" ]
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
module Data.Icons exposing
|
module Data.Icons exposing
|
||||||
( concerned
|
( addFiles
|
||||||
|
, addFilesIcon
|
||||||
|
, concerned
|
||||||
, concernedIcon
|
, concernedIcon
|
||||||
, correspondent
|
, correspondent
|
||||||
, correspondentIcon
|
, correspondentIcon
|
||||||
@ -51,3 +53,13 @@ editNotes =
|
|||||||
editNotesIcon : Html msg
|
editNotesIcon : Html msg
|
||||||
editNotesIcon =
|
editNotesIcon =
|
||||||
i [ class editNotes ] []
|
i [ class editNotes ] []
|
||||||
|
|
||||||
|
|
||||||
|
addFiles : String
|
||||||
|
addFiles =
|
||||||
|
"file plus icon"
|
||||||
|
|
||||||
|
|
||||||
|
addFilesIcon : Html msg
|
||||||
|
addFilesIcon =
|
||||||
|
i [ class addFiles ] []
|
||||||
|
@ -58,7 +58,9 @@ init flags url key =
|
|||||||
Nothing ->
|
Nothing ->
|
||||||
Cmd.none
|
Cmd.none
|
||||||
in
|
in
|
||||||
( m, Cmd.batch [ cmd, Api.versionInfo flags VersionResp, sessionCheck ] )
|
( m
|
||||||
|
, Cmd.batch [ cmd, Api.versionInfo flags VersionResp, sessionCheck ]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
viewDoc : Model -> Document Msg
|
viewDoc : Model -> Document Msg
|
||||||
|
@ -7,25 +7,27 @@ import Data.Flags exposing (Flags)
|
|||||||
import Page.ItemDetail.Data exposing (Model, Msg(..))
|
import Page.ItemDetail.Data exposing (Model, Msg(..))
|
||||||
|
|
||||||
|
|
||||||
update : Nav.Key -> Flags -> Maybe String -> Msg -> Model -> ( Model, Cmd Msg )
|
update : Nav.Key -> Flags -> Maybe String -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg )
|
||||||
update key flags next msg model =
|
update key flags next msg model =
|
||||||
case msg of
|
case msg of
|
||||||
Init id ->
|
Init id ->
|
||||||
let
|
let
|
||||||
( lm, lc ) =
|
( lm, lc, ls ) =
|
||||||
Comp.ItemDetail.update key flags next Comp.ItemDetail.Init model.detail
|
Comp.ItemDetail.update key flags next Comp.ItemDetail.Init model.detail
|
||||||
in
|
in
|
||||||
( { model | detail = lm }
|
( { model | detail = lm }
|
||||||
, Cmd.batch [ Api.itemDetail flags id ItemResp, Cmd.map ItemDetailMsg lc ]
|
, Cmd.batch [ Api.itemDetail flags id ItemResp, Cmd.map ItemDetailMsg lc ]
|
||||||
|
, Sub.map ItemDetailMsg ls
|
||||||
)
|
)
|
||||||
|
|
||||||
ItemDetailMsg lmsg ->
|
ItemDetailMsg lmsg ->
|
||||||
let
|
let
|
||||||
( lm, lc ) =
|
( lm, lc, ls ) =
|
||||||
Comp.ItemDetail.update key flags next lmsg model.detail
|
Comp.ItemDetail.update key flags next lmsg model.detail
|
||||||
in
|
in
|
||||||
( { model | detail = lm }
|
( { model | detail = lm }
|
||||||
, Cmd.map ItemDetailMsg lc
|
, Cmd.map ItemDetailMsg lc
|
||||||
|
, Sub.map ItemDetailMsg ls
|
||||||
)
|
)
|
||||||
|
|
||||||
ItemResp (Ok item) ->
|
ItemResp (Ok item) ->
|
||||||
@ -36,4 +38,4 @@ update key flags next msg model =
|
|||||||
update key flags next (ItemDetailMsg lmsg) model
|
update key flags next (ItemDetailMsg lmsg) model
|
||||||
|
|
||||||
ItemResp (Err _) ->
|
ItemResp (Err _) ->
|
||||||
( model, Cmd.none )
|
( model, Cmd.none, Sub.none )
|
||||||
|
@ -41,7 +41,12 @@ update sourceId flags msg model =
|
|||||||
|
|
||||||
uploads =
|
uploads =
|
||||||
if model.singleItem then
|
if model.singleItem then
|
||||||
Api.uploadSingle flags sourceId meta uploadAllTracker model.files (SingleUploadResp uploadAllTracker)
|
Api.uploadSingle flags
|
||||||
|
sourceId
|
||||||
|
meta
|
||||||
|
uploadAllTracker
|
||||||
|
model.files
|
||||||
|
(SingleUploadResp uploadAllTracker)
|
||||||
|
|
||||||
else
|
else
|
||||||
Cmd.batch (Api.upload flags sourceId meta model.files SingleUploadResp)
|
Cmd.batch (Api.upload flags sourceId meta model.files SingleUploadResp)
|
||||||
|
@ -70,6 +70,9 @@
|
|||||||
.default-layout .ui.segment .item-notes {
|
.default-layout .ui.segment .item-notes {
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
}
|
}
|
||||||
|
.default-layout .ui.segment.item-notes-display {
|
||||||
|
background: rgba(246, 255, 158, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
.default-layout .extracted-text {
|
.default-layout .extracted-text {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user