From de1baf725f8e6b1e355b9319326b17dc85b96d00 Mon Sep 17 00:00:00 2001 From: eikek Date: Fri, 1 Oct 2021 01:42:20 +0200 Subject: [PATCH 01/37] Generate a query string given an expression Initialize share record and improve tests. --- .../main/scala/docspell/query/ItemQuery.scala | 8 +- .../docspell/query/ItemQueryParser.scala | 24 +- .../docspell/query/internal/BasicParser.scala | 2 +- .../docspell/query/internal/ExprString.scala | 244 +++++++++++++++ .../docspell/query/internal/ExprUtil.scala | 23 +- .../scala/docspell/query/ItemQueryGen.scala | 287 ++++++++++++++++++ .../query/internal/ExprStringTest.scala | 69 +++++ .../query/internal/ItemQueryParserTest.scala | 12 +- .../docspell/store/impl/DoobieMeta.scala | 6 + .../scala/docspell/store/records/RShare.scala | 59 ++++ 10 files changed, 718 insertions(+), 16 deletions(-) create mode 100644 modules/query/shared/src/main/scala/docspell/query/internal/ExprString.scala create mode 100644 modules/query/shared/src/test/scala/docspell/query/ItemQueryGen.scala create mode 100644 modules/query/shared/src/test/scala/docspell/query/internal/ExprStringTest.scala create mode 100644 modules/store/src/main/scala/docspell/store/records/RShare.scala diff --git a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala index 0fc73acb..be0e5135 100644 --- a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala +++ b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala @@ -123,9 +123,11 @@ object ItemQuery { final case class ChecksumMatch(checksum: String) extends Expr final case class AttachId(id: String) extends Expr - final case object ValidItemStates extends Expr - final case object Trashed extends Expr - final case object ValidItemsOrTrashed extends Expr + /** A "private" expression is only visible in code, but cannot be parsed. */ + sealed trait PrivateExpr extends Expr + final case object ValidItemStates extends PrivateExpr + final case object Trashed extends PrivateExpr + final case object ValidItemsOrTrashed extends PrivateExpr // things that can be expressed with terms above sealed trait MacroExpr extends Expr { diff --git a/modules/query/shared/src/main/scala/docspell/query/ItemQueryParser.scala b/modules/query/shared/src/main/scala/docspell/query/ItemQueryParser.scala index c4c17801..d571cf63 100644 --- a/modules/query/shared/src/main/scala/docspell/query/ItemQueryParser.scala +++ b/modules/query/shared/src/main/scala/docspell/query/ItemQueryParser.scala @@ -8,12 +8,23 @@ package docspell.query import cats.data.NonEmptyList -import docspell.query.internal.ExprParser -import docspell.query.internal.ExprUtil +import docspell.query.internal.{ExprParser, ExprString, ExprUtil} object ItemQueryParser { + val PrivateExprError = ExprString.PrivateExprError + type PrivateExprError = ExprString.PrivateExprError + def parse(input: String): Either[ParseFailure, ItemQuery] = + parse0(input, expandMacros = true) + + def parseKeepMacros(input: String): Either[ParseFailure, ItemQuery] = + parse0(input, expandMacros = false) + + private def parse0( + input: String, + expandMacros: Boolean + ): Either[ParseFailure, ItemQuery] = if (input.isEmpty) Left( ParseFailure("", 0, NonEmptyList.of(ParseFailure.SimpleMessage(0, "No input."))) @@ -24,9 +35,16 @@ object ItemQueryParser { .parseQuery(in) .left .map(ParseFailure.fromError(in)) - .map(q => q.copy(expr = ExprUtil.reduce(q.expr))) + .map(q => q.copy(expr = ExprUtil.reduce(expandMacros)(q.expr))) } def parseUnsafe(input: String): ItemQuery = parse(input).fold(m => sys.error(m.render), identity) + + def asString(q: ItemQuery.Expr): Either[PrivateExprError, String] = + ExprString(q) + + def unsafeAsString(q: ItemQuery.Expr): String = + asString(q).fold(f => sys.error(s"Cannot expose private query part: $f"), identity) + } diff --git a/modules/query/shared/src/main/scala/docspell/query/internal/BasicParser.scala b/modules/query/shared/src/main/scala/docspell/query/internal/BasicParser.scala index fd2d9207..48bd98df 100644 --- a/modules/query/shared/src/main/scala/docspell/query/internal/BasicParser.scala +++ b/modules/query/shared/src/main/scala/docspell/query/internal/BasicParser.scala @@ -24,7 +24,7 @@ object BasicParser { ) private[this] val identChars: Set[Char] = - (('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_.").toSet + (('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_.@").toSet val parenAnd: P[Unit] = P.stringIn(List("(&", "(and")).void <* ws0 diff --git a/modules/query/shared/src/main/scala/docspell/query/internal/ExprString.scala b/modules/query/shared/src/main/scala/docspell/query/internal/ExprString.scala new file mode 100644 index 00000000..d3c5def4 --- /dev/null +++ b/modules/query/shared/src/main/scala/docspell/query/internal/ExprString.scala @@ -0,0 +1,244 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.query.internal + +import java.time.Period + +import docspell.query.Date +import docspell.query.Date.DateLiteral +import docspell.query.ItemQuery.Attr._ +import docspell.query.ItemQuery.Expr._ +import docspell.query.ItemQuery._ +import docspell.query.internal.{Constants => C} + +/** Creates the string representation for a given expression. The returned string can be + * parsed back to the expression using `ExprParser`. Note that expressions obtained from + * the `ItemQueryParser` have macros already expanded. + * + * It may fail when the expression contains non-public parts. Every expression that has + * been created by parsing a string, can be transformed back to a string. But an + * expression created via code may contain parts that cannot be transformed to a string. + */ +object ExprString { + + final case class PrivateExprError(expr: Expr.PrivateExpr) + type Result = Either[PrivateExprError, String] + + def apply(expr: Expr): Result = + expr match { + case AndExpr(inner) => + val es = inner.traverse(ExprString.apply) + es.map(_.toList.mkString(" ")).map(els => s"(& $els )") + + case OrExpr(inner) => + val es = inner.traverse(ExprString.apply) + es.map(_.toList.mkString(" ")).map(els => s"(| $els )") + + case NotExpr(inner) => + inner match { + case NotExpr(inner2) => + apply(inner2) + case _ => + apply(inner).map(n => s"!$n") + } + + case m: MacroExpr => + Right(macroStr(m)) + + case DirectionExpr(v) => + Right(s"${C.incoming}${C.like}${v}") + + case InboxExpr(v) => + Right(s"${C.inbox}${C.like}${v}") + + case InExpr(attr, values) => + val els = values.map(quote).toList.mkString(",") + Right(s"${attrStr(attr)}${C.in}$els") + + case InDateExpr(attr, values) => + val els = values.map(dateStr).toList.mkString(",") + Right(s"${attrStr(attr)}${C.in}$els") + + case TagsMatch(op, values) => + val els = values.map(quote).toList.mkString(",") + Right(s"${C.tag}${tagOpStr(op)}$els") + + case TagIdsMatch(op, values) => + val els = values.map(quote).toList.mkString(",") + Right(s"${C.tagId}${tagOpStr(op)}$els") + + case Exists(field) => + Right(s"${C.exist}${C.like}${attrStr(field)}") + + case Fulltext(v) => + Right(s"${C.content}${C.like}${quote(v)}") + + case SimpleExpr(op, prop) => + prop match { + case Property.StringProperty(attr, value) => + Right(s"${stringAttr(attr)}${opStr(op)}${quote(value)}") + case Property.DateProperty(attr, value) => + Right(s"${dateAttr(attr)}${opStr(op)}${dateStr(value)}") + case Property.IntProperty(attr, value) => + Right(s"${attrStr(attr)}${opStr(op)}$value") + } + + case TagCategoryMatch(op, values) => + val els = values.map(quote).toList.mkString(",") + Right(s"${C.cat}${tagOpStr(op)}$els") + + case CustomFieldMatch(name, op, value) => + Right(s"${C.customField}:$name${opStr(op)}${quote(value)}") + + case CustomFieldIdMatch(id, op, value) => + Right(s"${C.customFieldId}:$id${opStr(op)}${quote(value)}") + + case ChecksumMatch(cs) => + Right(s"${C.checksum}${C.like}$cs") + + case AttachId(aid) => + Right(s"${C.attachId}${C.eqs}$aid") + + case pe: PrivateExpr => + // There is no parser equivalent for this + Left(PrivateExprError(pe)) + } + + private[internal] def macroStr(expr: Expr.MacroExpr): String = + expr match { + case Expr.NamesMacro(name) => + s"${C.names}:${quote(name)}" + case Expr.YearMacro(_, year) => + s"${C.year}:$year" //currently, only for Attr.Date + case Expr.ConcMacro(term) => + s"${C.conc}:${quote(term)}" + case Expr.CorrMacro(term) => + s"${C.corr}:${quote(term)}" + case Expr.DateRangeMacro(attr, left, right) => + val name = attr match { + case Attr.CreatedDate => + C.createdIn + case Attr.Date => + C.dateIn + case Attr.DueDate => + C.dueIn + } + (left, right) match { + case (_: Date.DateLiteral, Date.Calc(date, calc, period)) => + s"$name:${dateStr(date)};${calcStr(calc)}${periodStr(period)}" + + case (Date.Calc(date, calc, period), _: DateLiteral) => + s"$name:${dateStr(date)};${calcStr(calc)}${periodStr(period)}" + + case (Date.Calc(d1, _, p1), Date.Calc(_, _, _)) => + s"$name:${dateStr(d1)};/${periodStr(p1)}" + + case (_: DateLiteral, _: DateLiteral) => + sys.error("Invalid date range") + } + } + + private[internal] def dateStr(date: Date): String = + date match { + case Date.Today => + "today" + case Date.Local(ld) => + f"${ld.getYear}-${ld.getMonthValue}%02d-${ld.getDayOfMonth}%02d" + + case Date.Millis(ms) => + s"ms$ms" + + case Date.Calc(date, calc, period) => + val ds = dateStr(date) + s"$ds;${calcStr(calc)}${periodStr(period)}" + } + + private[internal] def calcStr(c: Date.CalcDirection): String = + c match { + case Date.CalcDirection.Plus => "+" + case Date.CalcDirection.Minus => "-" + } + + private[internal] def periodStr(p: Period): String = + if (p.toTotalMonths == 0) s"${p.getDays}d" + else s"${p.toTotalMonths}m" + + private[internal] def attrStr(attr: Attr): String = + attr match { + case a: StringAttr => stringAttr(a) + case a: DateAttr => dateAttr(a) + case a: IntAttr => intAttr(a) + } + + private[internal] def intAttr(attr: IntAttr): String = + attr match { + case AttachCount => + Constants.attachCount + } + + private[internal] def dateAttr(attr: DateAttr): String = + attr match { + case Attr.Date => + Constants.date + case DueDate => + Constants.due + case CreatedDate => + Constants.created + } + + private[internal] def stringAttr(attr: StringAttr): String = + attr match { + case Attr.ItemName => + Constants.name + case Attr.ItemId => + Constants.id + case Attr.ItemSource => + Constants.source + case Attr.ItemNotes => + Constants.notes + case Correspondent.OrgId => + Constants.corrOrgId + case Correspondent.OrgName => + Constants.corrOrgName + case Correspondent.PersonId => + Constants.corrPersId + case Correspondent.PersonName => + Constants.corrPersName + case Concerning.EquipId => + Constants.concEquipId + case Concerning.EquipName => + Constants.concEquipName + case Concerning.PersonId => + Constants.concPersId + case Concerning.PersonName => + Constants.concPersName + case Folder.FolderName => + Constants.folder + case Folder.FolderId => + Constants.folderId + } + + private[internal] def opStr(op: Operator): String = + op match { + case Operator.Like => Constants.like.toString + case Operator.Gte => Constants.gte + case Operator.Lte => Constants.lte + case Operator.Eq => Constants.eqs.toString + case Operator.Lt => Constants.lt.toString + case Operator.Gt => Constants.gt.toString + case Operator.Neq => Constants.neq + } + + private[internal] def tagOpStr(op: TagOperator): String = + op match { + case TagOperator.AllMatch => C.eqs.toString + case TagOperator.AnyMatch => C.like.toString + } + + private def quote(s: String): String = + s"\"$s\"" +} diff --git a/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala b/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala index 4f985be5..6b22ef96 100644 --- a/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala +++ b/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala @@ -13,35 +13,42 @@ import docspell.query.ItemQuery._ object ExprUtil { + def reduce(expr: Expr): Expr = + reduce(expandMacros = true)(expr) + /** Does some basic transformation, like unfolding nested and trees containing one value * etc. */ - def reduce(expr: Expr): Expr = + def reduce(expandMacros: Boolean)(expr: Expr): Expr = expr match { case AndExpr(inner) => val nodes = spliceAnd(inner) - if (nodes.tail.isEmpty) reduce(nodes.head) - else AndExpr(nodes.map(reduce)) + if (nodes.tail.isEmpty) reduce(expandMacros)(nodes.head) + else AndExpr(nodes.map(reduce(expandMacros))) case OrExpr(inner) => val nodes = spliceOr(inner) - if (nodes.tail.isEmpty) reduce(nodes.head) - else OrExpr(nodes.map(reduce)) + if (nodes.tail.isEmpty) reduce(expandMacros)(nodes.head) + else OrExpr(nodes.map(reduce(expandMacros))) case NotExpr(inner) => inner match { case NotExpr(inner2) => - reduce(inner2) + reduce(expandMacros)(inner2) case InboxExpr(flag) => InboxExpr(!flag) case DirectionExpr(flag) => DirectionExpr(!flag) case _ => - NotExpr(reduce(inner)) + NotExpr(reduce(expandMacros)(inner)) } case m: MacroExpr => - reduce(m.body) + if (expandMacros) { + reduce(expandMacros)(m.body) + } else { + m + } case DirectionExpr(_) => expr diff --git a/modules/query/shared/src/test/scala/docspell/query/ItemQueryGen.scala b/modules/query/shared/src/test/scala/docspell/query/ItemQueryGen.scala new file mode 100644 index 00000000..4f6a6982 --- /dev/null +++ b/modules/query/shared/src/test/scala/docspell/query/ItemQueryGen.scala @@ -0,0 +1,287 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.query + +import java.time.{Instant, Period, ZoneOffset} + +import cats.data.NonEmptyList + +import docspell.query.ItemQuery.Expr.TagIdsMatch +import docspell.query.ItemQuery._ + +import org.scalacheck.Gen + +/** Generator for syntactically valid queries. */ +object ItemQueryGen { + + def exprGen: Gen[Expr] = + Gen.oneOf( + simpleExprGen, + existsExprGen, + inExprGen, + inDateExprGen, + inboxExprGen, + directionExprGen, + tagIdsMatchExprGen, + tagMatchExprGen, + tagCatMatchExpr, + customFieldMatchExprGen, + customFieldIdMatchExprGen, + fulltextExprGen, + checksumMatchExprGen, + attachIdExprGen, + namesMacroGen, + corrMacroGen, + concMacroGen, + yearMacroGen, + dateRangeMacro, + Gen.lzy(andExprGen(exprGen)), + Gen.lzy(orExprGen(exprGen)), + Gen.lzy(notExprGen(exprGen)) + ) + + def andExprGen(g: Gen[Expr]): Gen[Expr.AndExpr] = + nelGen(g).map(Expr.AndExpr) + + def orExprGen(g: Gen[Expr]): Gen[Expr.OrExpr] = + nelGen(g).map(Expr.OrExpr) + + // avoid generating nested not expressions, they are already flattened by the parser + // and only occur artificially + def notExprGen(g: Gen[Expr]): Gen[Expr] = + g.map { + case Expr.NotExpr(inner) => inner + case e => Expr.NotExpr(e) + } + + val opGen: Gen[Operator] = + Gen.oneOf( + Operator.Like, + Operator.Gte, + Operator.Lt, + Operator.Gt, + Operator.Lte, + Operator.Eq, + Operator.Neq + ) + + val tagOpGen: Gen[TagOperator] = + Gen.oneOf(TagOperator.AllMatch, TagOperator.AnyMatch) + + val stringAttrGen: Gen[Attr.StringAttr] = + Gen.oneOf( + Attr.Concerning.EquipName, + Attr.Concerning.EquipId, + Attr.Concerning.PersonName, + Attr.Concerning.PersonId, + Attr.Correspondent.OrgName, + Attr.Correspondent.OrgId, + Attr.Correspondent.PersonName, + Attr.Correspondent.PersonId, + Attr.ItemId, + Attr.ItemName, + Attr.ItemSource, + Attr.ItemNotes, + Attr.Folder.FolderId, + Attr.Folder.FolderName + ) + + val dateAttrGen: Gen[Attr.DateAttr] = + Gen.oneOf(Attr.Date, Attr.DueDate, Attr.CreatedDate) + + val intAttrGen: Gen[Attr.IntAttr] = + Gen.const(Attr.AttachCount) + + val attrGen: Gen[Attr] = + Gen.oneOf(stringAttrGen, dateAttrGen, intAttrGen) + + private val valueChars = + Gen.oneOf(Gen.alphaNumChar, Gen.oneOf(" /{}*?-:@#$~+%…_[]^!ß")) + + private val stringValueGen: Gen[String] = + Gen.choose(1, 20).flatMap(n => Gen.stringOfN(n, valueChars)) + + private val intValueGen: Gen[Int] = + Gen.choose(1900, 9999) + + private val identGen: Gen[String] = + Gen + .choose(3, 12) + .flatMap(n => + Gen.stringOfN( + n, + Gen.oneOf((('A' to 'Z') ++ ('a' to 'z') ++ ('0' to '9') ++ "-_.@").toSet) + ) + ) + + private def nelGen[T](gen: Gen[T]): Gen[NonEmptyList[T]] = + for { + head <- gen + tail <- Gen.choose(0, 9).flatMap(n => Gen.listOfN(n, gen)) + } yield NonEmptyList(head, tail) + + private val dateMillisGen: Gen[Long] = + Gen.choose(0, Instant.parse("2100-12-24T20:00:00Z").toEpochMilli) + + val localDateGen: Gen[Date.Local] = + dateMillisGen + .map(ms => Instant.ofEpochMilli(ms).atOffset(ZoneOffset.UTC).toLocalDate) + .map(Date.Local) + + val millisDateGen: Gen[Date.Millis] = + dateMillisGen.map(Date.Millis) + + val dateLiteralGen: Gen[Date.DateLiteral] = + Gen.oneOf( + localDateGen, + millisDateGen, + Gen.const(Date.Today) + ) + + val periodGen: Gen[Period] = + for { + mOrD <- Gen.oneOf(a => Period.ofDays(a), a => Period.ofMonths(a)) + num <- Gen.choose(1, 30) + } yield mOrD(num) + + val calcGen: Gen[Date.CalcDirection] = + Gen.oneOf(Date.CalcDirection.Plus, Date.CalcDirection.Minus) + + val dateCalcGen: Gen[Date.Calc] = + for { + dl <- dateLiteralGen + calc <- calcGen + period <- periodGen + } yield Date.Calc(dl, calc, period) + + val dateValueGen: Gen[Date] = + Gen.oneOf(dateLiteralGen, dateCalcGen) + + val stringPropGen: Gen[Property.StringProperty] = + for { + attr <- stringAttrGen + sval <- stringValueGen + } yield Property.StringProperty(attr, sval) + + val intPropGen: Gen[Property.IntProperty] = + for { + attr <- intAttrGen + ival <- intValueGen + } yield Property.IntProperty(attr, ival) + + val datePropGen: Gen[Property.DateProperty] = + for { + attr <- dateAttrGen + dv <- dateValueGen + } yield Property.DateProperty(attr, dv) + + val propertyGen: Gen[Property] = + Gen.oneOf(stringPropGen, datePropGen, intPropGen) + + val simpleExprGen: Gen[Expr.SimpleExpr] = + for { + op <- opGen + prop <- propertyGen + } yield Expr.SimpleExpr(op, prop) + + val existsExprGen: Gen[Expr.Exists] = + attrGen.map(Expr.Exists) + + val inExprGen: Gen[Expr.InExpr] = + for { + attr <- stringAttrGen + vals <- nelGen(stringValueGen) + } yield Expr.InExpr(attr, vals) + + val inDateExprGen: Gen[Expr.InDateExpr] = + for { + attr <- dateAttrGen + vals <- nelGen(dateValueGen) + } yield Expr.InDateExpr(attr, vals) + + val inboxExprGen: Gen[Expr.InboxExpr] = + Gen.oneOf(true, false).map(Expr.InboxExpr) + + val directionExprGen: Gen[Expr.DirectionExpr] = + Gen.oneOf(true, false).map(Expr.DirectionExpr) + + val tagIdsMatchExprGen: Gen[Expr.TagIdsMatch] = + for { + op <- tagOpGen + vals <- nelGen(stringValueGen) + } yield TagIdsMatch(op, vals) + + val tagMatchExprGen: Gen[Expr.TagsMatch] = + for { + op <- tagOpGen + vals <- nelGen(stringValueGen) + } yield Expr.TagsMatch(op, vals) + + val tagCatMatchExpr: Gen[Expr.TagCategoryMatch] = + for { + op <- tagOpGen + vals <- nelGen(stringValueGen) + } yield Expr.TagCategoryMatch(op, vals) + + val customFieldMatchExprGen: Gen[Expr.CustomFieldMatch] = + for { + name <- identGen + op <- opGen + value <- stringValueGen + } yield Expr.CustomFieldMatch(name, op, value) + + val customFieldIdMatchExprGen: Gen[Expr.CustomFieldIdMatch] = + for { + name <- identGen + op <- opGen + value <- identGen + } yield Expr.CustomFieldIdMatch(name, op, value) + + val fulltextExprGen: Gen[Expr.Fulltext] = + Gen + .choose(3, 20) + .flatMap(n => Gen.stringOfN(n, valueChars)) + .map(Expr.Fulltext) + + val checksumMatchExprGen: Gen[Expr.ChecksumMatch] = + Gen.stringOfN(64, Gen.hexChar).map(Expr.ChecksumMatch) + + val attachIdExprGen: Gen[Expr.AttachId] = + identGen.map(Expr.AttachId) + + val namesMacroGen: Gen[Expr.NamesMacro] = + stringValueGen.map(Expr.NamesMacro) + + val concMacroGen: Gen[Expr.ConcMacro] = + stringValueGen.map(Expr.ConcMacro) + + val corrMacroGen: Gen[Expr.CorrMacro] = + stringValueGen.map(Expr.CorrMacro) + + val yearMacroGen: Gen[Expr.YearMacro] = + Gen.choose(1900, 9999).map(Expr.YearMacro(Attr.Date, _)) + + val dateRangeMacro: Gen[Expr.DateRangeMacro] = + for { + attr <- dateAttrGen + dl <- dateLiteralGen + p <- periodGen + calc <- Gen.option(calcGen) + range = calc match { + case Some(c @ Date.CalcDirection.Plus) => + Expr.DateRangeMacro(attr, dl, Date.Calc(dl, c, p)) + case Some(c @ Date.CalcDirection.Minus) => + Expr.DateRangeMacro(attr, Date.Calc(dl, c, p), dl) + case None => + Expr.DateRangeMacro( + attr, + Date.Calc(dl, Date.CalcDirection.Minus, p), + Date.Calc(dl, Date.CalcDirection.Plus, p) + ) + } + } yield range +} diff --git a/modules/query/shared/src/test/scala/docspell/query/internal/ExprStringTest.scala b/modules/query/shared/src/test/scala/docspell/query/internal/ExprStringTest.scala new file mode 100644 index 00000000..99fe673f --- /dev/null +++ b/modules/query/shared/src/test/scala/docspell/query/internal/ExprStringTest.scala @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.query.internal + +import java.time.{LocalDate, Period} + +import docspell.query.ItemQuery._ +import docspell.query.{Date, ItemQueryGen, ParseFailure} + +import munit.{FunSuite, ScalaCheckSuite} +import org.scalacheck.Prop.forAll + +class ExprStringTest extends FunSuite with ScalaCheckSuite { + + // parses the query without reducing and expanding macros + def singleParse(s: String): Expr = + ExprParser + .parseQuery(s) + .left + .map(ParseFailure.fromError(s)) + .fold(f => sys.error(f.render), _.expr) + + def exprString(expr: Expr): String = + ExprString(expr).fold(f => sys.error(f.toString), identity) + + test("macro: name") { + val str = exprString(Expr.NamesMacro("test")) + val q = singleParse(str) + assertEquals(str, "names:\"test\"") + assertEquals(q, Expr.NamesMacro("test")) + } + + test("macro: year") { + val str = exprString(Expr.YearMacro(Attr.Date, 1990)) + val q = singleParse(str) + assertEquals(str, "year:1990") + assertEquals(q, Expr.YearMacro(Attr.Date, 1990)) + } + + test("macro: daterange") { + val range = Expr.DateRangeMacro( + attr = Attr.Date, + left = Date.Calc( + date = Date.Local( + date = LocalDate.of(2076, 12, 9) + ), + calc = Date.CalcDirection.Minus, + period = Period.ofMonths(27) + ), + right = Date.Local(LocalDate.of(2076, 12, 9)) + ) + val str = exprString(range) + val q = singleParse(str) + assertEquals(str, "dateIn:2076-12-09;-27m") + assertEquals(q, range) + } + + property("generate expr and parse it") { + forAll(ItemQueryGen.exprGen) { expr => + val str = exprString(expr) + val q = singleParse(str) + assertEquals(q, expr) + } + } +} diff --git a/modules/query/shared/src/test/scala/docspell/query/internal/ItemQueryParserTest.scala b/modules/query/shared/src/test/scala/docspell/query/internal/ItemQueryParserTest.scala index 37de9db0..3326a764 100644 --- a/modules/query/shared/src/test/scala/docspell/query/internal/ItemQueryParserTest.scala +++ b/modules/query/shared/src/test/scala/docspell/query/internal/ItemQueryParserTest.scala @@ -8,7 +8,7 @@ package docspell.query.internal import cats.implicits._ -import docspell.query.ItemQueryParser +import docspell.query.{ItemQuery, ItemQueryParser} import munit._ @@ -64,4 +64,14 @@ class ItemQueryParserTest extends FunSuite { ItemQueryParser.parseUnsafe("(| name:hello date:2021-02 name:world name:hello )") assertEquals(expect.copy(raw = raw.some), q) } + + test("f.id:name=value") { + val raw = "f.id:QsuGW@=\"dAHBstXJd0\"" + val q = ItemQueryParser.parseUnsafe(raw) + val expect = + ItemQuery.Expr.CustomFieldIdMatch("QsuGW@", ItemQuery.Operator.Eq, "dAHBstXJd0") + + assertEquals(q.expr, expect) + + } } diff --git a/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala b/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala index 1d69da70..4c94b1c2 100644 --- a/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala +++ b/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala @@ -11,6 +11,7 @@ import java.time.{Instant, LocalDate} import docspell.common._ import docspell.common.syntax.all._ +import docspell.query.{ItemQuery, ItemQueryParser} import docspell.totp.Key import com.github.eikek.calev.CalEvent @@ -142,6 +143,11 @@ trait DoobieMeta extends EmilDoobieMeta { implicit val metaByteSize: Meta[ByteSize] = Meta[Long].timap(ByteSize.apply)(_.bytes) + + implicit val metaItemQuery: Meta[ItemQuery] = + Meta[String].timap(s => ItemQueryParser.parseUnsafe(s))(q => + q.raw.getOrElse(ItemQueryParser.unsafeAsString(q.expr)) + ) } object DoobieMeta extends DoobieMeta { diff --git a/modules/store/src/main/scala/docspell/store/records/RShare.scala b/modules/store/src/main/scala/docspell/store/records/RShare.scala new file mode 100644 index 00000000..72f67de7 --- /dev/null +++ b/modules/store/src/main/scala/docspell/store/records/RShare.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.store.records + +import cats.data.NonEmptyList + +import docspell.common._ +import docspell.query.ItemQuery +import docspell.store.qb._ + +final case class RShare( + id: Ident, + cid: Ident, + query: ItemQuery, + enabled: Boolean, + password: Option[Password], + publishedAt: Timestamp, + publishedUntil: Timestamp, + views: Int, + lastAccess: Option[Timestamp] +) {} + +object RShare { + + final case class Table(alias: Option[String]) extends TableDef { + val tableName = "item_share"; + + val id = Column[Ident]("id", this) + val cid = Column[Ident]("cid", this) + val query = Column[ItemQuery]("query", this) + val enabled = Column[Boolean]("enabled", this) + val password = Column[Password]("password", this) + val publishedAt = Column[Timestamp]("published_at", this) + val publishedUntil = Column[Timestamp]("published_until", this) + val views = Column[Int]("views", this) + val lastAccess = Column[Timestamp]("last_access", this) + + val all: NonEmptyList[Column[_]] = + NonEmptyList.of( + id, + cid, + query, + enabled, + password, + publishedAt, + publishedUntil, + views, + lastAccess + ) + } + + val T: Table = Table(None) + def as(alias: String): Table = Table(Some(alias)) + +} From c7d587bea404a7dc31e13a7c9adbfcaf15410547 Mon Sep 17 00:00:00 2001 From: eikek Date: Sat, 2 Oct 2021 15:16:02 +0200 Subject: [PATCH 02/37] Basic management of shares --- build.sbt | 15 +- .../scala/docspell/backend/BackendApp.scala | 3 + .../scala/docspell/backend/ops/OShare.scala | 116 ++++++ .../scala/docspell/common/Timestamp.scala | 3 + .../src/main/resources/docspell-openapi.yml | 178 ++++++++- .../restapi/codec/ItemQueryJson.scala | 24 ++ .../docspell/restserver/RestServer.scala | 1 + .../restserver/routes/ShareRoutes.scala | 105 ++++++ .../db/migration/h2/V1.27.1__item_share.sql | 13 + .../migration/mariadb/V1.27.1__item_share.sql | 13 + .../postgresql/V1.27.1__item_share.sql | 13 + .../src/main/scala/docspell/store/Store.scala | 2 + .../scala/docspell/store/impl/StoreImpl.scala | 5 + .../scala/docspell/store/records/RShare.scala | 62 +++- modules/webapp/src/main/elm/Api.elm | 59 +++ .../webapp/src/main/elm/Comp/DatePicker.elm | 2 +- .../webapp/src/main/elm/Comp/ShareForm.elm | 282 ++++++++++++++ .../webapp/src/main/elm/Comp/ShareManage.elm | 349 ++++++++++++++++++ .../webapp/src/main/elm/Comp/ShareTable.elm | 87 +++++ modules/webapp/src/main/elm/Data/Icons.elm | 20 +- .../src/main/elm/Messages/Comp/ShareForm.elm | 46 +++ .../main/elm/Messages/Comp/ShareManage.elm | 74 ++++ .../src/main/elm/Messages/Comp/ShareTable.elm | 42 +++ .../elm/Messages/Page/CollectiveSettings.elm | 7 + .../main/elm/Page/CollectiveSettings/Data.elm | 9 + .../elm/Page/CollectiveSettings/Update.elm | 11 + .../elm/Page/CollectiveSettings/View2.elm | 30 ++ 27 files changed, 1551 insertions(+), 20 deletions(-) create mode 100644 modules/backend/src/main/scala/docspell/backend/ops/OShare.scala create mode 100644 modules/restapi/src/main/scala/docspell/restapi/codec/ItemQueryJson.scala create mode 100644 modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala create mode 100644 modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql create mode 100644 modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql create mode 100644 modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql create mode 100644 modules/webapp/src/main/elm/Comp/ShareForm.elm create mode 100644 modules/webapp/src/main/elm/Comp/ShareManage.elm create mode 100644 modules/webapp/src/main/elm/Comp/ShareTable.elm create mode 100644 modules/webapp/src/main/elm/Messages/Comp/ShareForm.elm create mode 100644 modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm create mode 100644 modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm diff --git a/build.sbt b/build.sbt index c4837e71..9e1b0777 100644 --- a/build.sbt +++ b/build.sbt @@ -260,6 +260,18 @@ val openapiScalaSettings = Seq( .copy(typeDef = TypeDef("AccountSource", Imports("docspell.common.AccountSource")) ) + case "itemquery" => + field => + field + .copy(typeDef = + TypeDef( + "ItemQuery", + Imports( + "docspell.query.ItemQuery", + "docspell.restapi.codec.ItemQueryJson._" + ) + ) + ) }) ) @@ -367,6 +379,7 @@ val store = project .settings(testSettingsMUnit) .settings( name := "docspell-store", + addCompilerPlugin(Dependencies.kindProjectorPlugin), libraryDependencies ++= Dependencies.doobie ++ Dependencies.binny ++ @@ -472,7 +485,7 @@ val restapi = project openapiSpec := (Compile / resourceDirectory).value / "docspell-openapi.yml", openapiStaticGen := OpenApiDocGenerator.Redoc ) - .dependsOn(common) + .dependsOn(common, query.jvm) val joexapi = project .in(file("modules/joexapi")) diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala index fd12ec40..5a6fa482 100644 --- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala +++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala @@ -48,6 +48,7 @@ trait BackendApp[F[_]] { def simpleSearch: OSimpleSearch[F] def clientSettings: OClientSettings[F] def totp: OTotp[F] + def share: OShare[F] } object BackendApp { @@ -85,6 +86,7 @@ object BackendApp { customFieldsImpl <- OCustomFields(store) simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl) clientSettingsImpl <- OClientSettings(store) + shareImpl <- Resource.pure(OShare(store)) } yield new BackendApp[F] { val login = loginImpl val signup = signupImpl @@ -107,6 +109,7 @@ object BackendApp { val simpleSearch = simpleSearchImpl val clientSettings = clientSettingsImpl val totp = totpImpl + val share = shareImpl } def apply[F[_]: Async]( diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala new file mode 100644 index 00000000..68b86f11 --- /dev/null +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -0,0 +1,116 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.backend.ops + +import cats.data.OptionT +import cats.effect._ +import cats.implicits._ + +import docspell.backend.PasswordCrypt +import docspell.common._ +import docspell.query.ItemQuery +import docspell.store.Store +import docspell.store.records.RShare + +trait OShare[F[_]] { + + def findAll(collective: Ident): F[List[RShare]] + + def delete(id: Ident, collective: Ident): F[Boolean] + + def addNew(share: OShare.NewShare): F[OShare.ChangeResult] + + def findOne(id: Ident, collective: Ident): OptionT[F, RShare] + + def update( + id: Ident, + share: OShare.NewShare, + removePassword: Boolean + ): F[OShare.ChangeResult] +} + +object OShare { + + final case class NewShare( + cid: Ident, + name: Option[String], + query: ItemQuery, + enabled: Boolean, + password: Option[Password], + publishUntil: Timestamp + ) + + sealed trait ChangeResult + object ChangeResult { + final case class Success(id: Ident) extends ChangeResult + case object PublishUntilInPast extends ChangeResult + + def success(id: Ident): ChangeResult = Success(id) + def publishUntilInPast: ChangeResult = PublishUntilInPast + } + + def apply[F[_]: Async](store: Store[F]): OShare[F] = + new OShare[F] { + def findAll(collective: Ident): F[List[RShare]] = + store.transact(RShare.findAllByCollective(collective)) + + def delete(id: Ident, collective: Ident): F[Boolean] = + store.transact(RShare.deleteByIdAndCid(id, collective)).map(_ > 0) + + def addNew(share: NewShare): F[ChangeResult] = + for { + curTime <- Timestamp.current[F] + id <- Ident.randomId[F] + pass = share.password.map(PasswordCrypt.crypt) + record = RShare( + id, + share.cid, + share.name, + share.query, + share.enabled, + pass, + curTime, + share.publishUntil, + 0, + None + ) + res <- + if (share.publishUntil < curTime) ChangeResult.publishUntilInPast.pure[F] + else store.transact(RShare.insert(record)).map(_ => ChangeResult.success(id)) + } yield res + + def update( + id: Ident, + share: OShare.NewShare, + removePassword: Boolean + ): F[ChangeResult] = + for { + curTime <- Timestamp.current[F] + record = RShare( + id, + share.cid, + share.name, + share.query, + share.enabled, + share.password.map(PasswordCrypt.crypt), + Timestamp.Epoch, + share.publishUntil, + 0, + None + ) + res <- + if (share.publishUntil < curTime) ChangeResult.publishUntilInPast.pure[F] + else + store + .transact(RShare.updateData(record, removePassword)) + .map(_ => ChangeResult.success(id)) + } yield res + + def findOne(id: Ident, collective: Ident): OptionT[F, RShare] = + RShare.findOne(id, collective).mapK(store.transform) + } +} diff --git a/modules/common/src/main/scala/docspell/common/Timestamp.scala b/modules/common/src/main/scala/docspell/common/Timestamp.scala index d55e5fc4..b9aa104f 100644 --- a/modules/common/src/main/scala/docspell/common/Timestamp.scala +++ b/modules/common/src/main/scala/docspell/common/Timestamp.scala @@ -51,6 +51,9 @@ case class Timestamp(value: Instant) { def <(other: Timestamp): Boolean = this.value.isBefore(other.value) + + def >(other: Timestamp): Boolean = + this.value.isAfter(other.value) } object Timestamp { diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index c5e95d29..f01cd129 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1711,6 +1711,96 @@ paths: schema: $ref: "#/components/schemas/BasicResult" + /sec/share: + get: + operationId: "sec-share-get-all" + tags: [ Share ] + summary: Get a list of shares + description: | + Return a list of all shares for this collective. + security: + - authTokenHeader: [] + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/ShareList" + post: + operationId: "sec-share-new" + tags: [ Share ] + summary: Create a new share. + description: | + Create a new share. + security: + - authTokenHeader: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ShareData" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/IdResult" + /sec/share/{shareId}: + parameters: + - $ref: "#/components/parameters/shareId" + get: + operationId: "sec-share-get" + tags: [Share] + summary: Get details to a single share. + description: | + Given the id of a share, returns some details about it. + security: + - authTokenHeader: [] + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/ShareDetail" + put: + operationId: "sec-share-update" + tags: [ Share ] + summary: Update an existing share. + description: | + Updates an existing share. + security: + - authTokenHeader: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ShareData" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + delete: + operationId: "sec-share-delete-by-id" + tags: [ Share ] + summary: Delete a share. + description: | + Deletes a share + security: + - authTokenHeader: [] + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + /sec/item/search: get: operationId: "sec-item-search-by-get" @@ -4096,6 +4186,83 @@ paths: components: schemas: + ShareData: + description: | + Editable data for a share. + required: + - query + - enabled + - publishUntil + properties: + name: + type: string + query: + type: string + format: itemquery + enabled: + type: boolean + password: + type: string + format: password + publishUntil: + type: integer + format: date-time + removePassword: + type: boolean + description: | + For an update request, this can control whether to delete + the password. Otherwise if the password is not set, it + will not be changed. When adding a new share, this has no + effect. + + ShareDetail: + description: | + Details for an existing share. + required: + - id + - query + - enabled + - publishAt + - publishUntil + - password + - views + properties: + id: + type: string + format: ident + query: + type: string + format: itemquery + name: + type: string + enabled: + type: boolean + publishAt: + type: integer + format: date-time + publishUntil: + type: integer + format: date-time + password: + type: boolean + views: + type: integer + format: int32 + lastAccess: + type: integer + format: date-time + + ShareList: + description: | + A list of shares. + required: + - items + properties: + items: + type: array + items: + $ref: "#/components/schemas/ShareDetail" + DeleteUserData: description: | An excerpt of data that would be deleted when deleting the @@ -6121,8 +6288,8 @@ components: type: string IdResult: description: | - Some basic result of an operation with an ID as payload. If - success if `false` the id is not usable. + Some basic result of an operation with an ID as payload, if + success is true. If success is `false` the id is not usable. required: - success - message @@ -6257,6 +6424,13 @@ components: required: true schema: type: string + shareId: + name: shareId + in: path + description: An identifier for a share + required: true + schema: + type: string username: name: username in: path diff --git a/modules/restapi/src/main/scala/docspell/restapi/codec/ItemQueryJson.scala b/modules/restapi/src/main/scala/docspell/restapi/codec/ItemQueryJson.scala new file mode 100644 index 00000000..096c0ba5 --- /dev/null +++ b/modules/restapi/src/main/scala/docspell/restapi/codec/ItemQueryJson.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.restapi.codec + +import docspell.query.{ItemQuery, ItemQueryParser} + +import io.circe.{Decoder, Encoder} + +trait ItemQueryJson { + + implicit val itemQueryDecoder: Decoder[ItemQuery] = + Decoder.decodeString.emap(str => ItemQueryParser.parse(str).left.map(_.render)) + + implicit val itemQueryEncoder: Encoder[ItemQuery] = + Encoder.encodeString.contramap(q => + q.raw.getOrElse(ItemQueryParser.unsafeAsString(q.expr)) + ) +} + +object ItemQueryJson extends ItemQueryJson diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala index 3cc244fb..6c620a0b 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala @@ -94,6 +94,7 @@ object RestServer { "email/send" -> MailSendRoutes(restApp.backend, token), "email/settings" -> MailSettingsRoutes(restApp.backend, token), "email/sent" -> SentMailRoutes(restApp.backend, token), + "share" -> ShareRoutes.manage(restApp.backend, token), "usertask/notifydueitems" -> NotifyDueItemsRoutes(cfg, restApp.backend, token), "usertask/scanmailbox" -> ScanMailboxRoutes(restApp.backend, token), "calevent/check" -> CalEventCheckRoutes(), diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala new file mode 100644 index 00000000..060ef30c --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.restserver.routes + +import cats.data.OptionT +import cats.effect._ +import cats.implicits._ + +import docspell.backend.BackendApp +import docspell.backend.auth.AuthToken +import docspell.backend.ops.OShare +import docspell.common.Ident +import docspell.restapi.model._ +import docspell.restserver.http4s.ResponseGenerator +import docspell.store.records.RShare + +import org.http4s.HttpRoutes +import org.http4s.circe.CirceEntityDecoder._ +import org.http4s.circe.CirceEntityEncoder._ +import org.http4s.dsl.Http4sDsl + +object ShareRoutes { + + def manage[F[_]: Async](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = { + val dsl = new Http4sDsl[F] with ResponseGenerator[F] {} + import dsl._ + + HttpRoutes.of { + case GET -> Root => + for { + all <- backend.share.findAll(user.account.collective) + res <- Ok(ShareList(all.map(mkShareDetail))) + } yield res + + case req @ POST -> Root => + for { + data <- req.as[ShareData] + share = mkNewShare(data, user) + res <- backend.share.addNew(share) + resp <- Ok(mkIdResult(res, "New share created.")) + } yield resp + + case GET -> Root / Ident(id) => + (for { + share <- backend.share.findOne(id, user.account.collective) + resp <- OptionT.liftF(Ok(mkShareDetail(share))) + } yield resp).getOrElseF(NotFound()) + + case req @ PUT -> Root / Ident(id) => + for { + data <- req.as[ShareData] + share = mkNewShare(data, user) + updated <- backend.share.update(id, share, data.removePassword.getOrElse(false)) + resp <- Ok(mkBasicResult(updated, "Share updated.")) + } yield resp + + case DELETE -> Root / Ident(id) => + for { + del <- backend.share.delete(id, user.account.collective) + resp <- Ok(BasicResult(del, if (del) "Share deleted." else "Deleting failed.")) + } yield resp + } + } + + def mkNewShare(data: ShareData, user: AuthToken): OShare.NewShare = + OShare.NewShare( + user.account.collective, + data.name, + data.query, + data.enabled, + data.password, + data.publishUntil + ) + + def mkIdResult(r: OShare.ChangeResult, msg: => String): IdResult = + r match { + case OShare.ChangeResult.Success(id) => IdResult(true, msg, id) + case OShare.ChangeResult.PublishUntilInPast => + IdResult(false, "Until date must not be in the past", Ident.unsafe("")) + } + + def mkBasicResult(r: OShare.ChangeResult, msg: => String): BasicResult = + r match { + case OShare.ChangeResult.Success(_) => BasicResult(true, msg) + case OShare.ChangeResult.PublishUntilInPast => + BasicResult(false, "Until date must not be in the past") + } + + def mkShareDetail(r: RShare): ShareDetail = + ShareDetail( + r.id, + r.query, + r.name, + r.enabled, + r.publishAt, + r.publishUntil, + r.password.isDefined, + r.views, + r.lastAccess + ) +} diff --git a/modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql b/modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql new file mode 100644 index 00000000..7e252c14 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql @@ -0,0 +1,13 @@ +CREATE TABLE "item_share" ( + "id" varchar(254) not null primary key, + "cid" varchar(254) not null, + "name" varchar(254), + "query" varchar(2000) not null, + "enabled" boolean not null, + "pass" varchar(254), + "publish_at" timestamp not null, + "publish_until" timestamp not null, + "views" int not null, + "last_access" timestamp, + foreign key ("cid") references "collective"("cid") on delete cascade +) diff --git a/modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql b/modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql new file mode 100644 index 00000000..fb74d283 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql @@ -0,0 +1,13 @@ +CREATE TABLE `item_share` ( + `id` varchar(254) not null primary key, + `cid` varchar(254) not null, + `name` varchar(254), + `query` varchar(2000) not null, + `enabled` boolean not null, + `pass` varchar(254), + `publish_at` timestamp not null, + `publish_until` timestamp not null, + `views` int not null, + `last_access` timestamp, + foreign key (`cid`) references `collective`(`cid`) on delete cascade +) diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql new file mode 100644 index 00000000..7e252c14 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql @@ -0,0 +1,13 @@ +CREATE TABLE "item_share" ( + "id" varchar(254) not null primary key, + "cid" varchar(254) not null, + "name" varchar(254), + "query" varchar(2000) not null, + "enabled" boolean not null, + "pass" varchar(254), + "publish_at" timestamp not null, + "publish_until" timestamp not null, + "views" int not null, + "last_access" timestamp, + foreign key ("cid") references "collective"("cid") on delete cascade +) diff --git a/modules/store/src/main/scala/docspell/store/Store.scala b/modules/store/src/main/scala/docspell/store/Store.scala index f68e353c..338b177a 100644 --- a/modules/store/src/main/scala/docspell/store/Store.scala +++ b/modules/store/src/main/scala/docspell/store/Store.scala @@ -9,6 +9,7 @@ package docspell.store import scala.concurrent.ExecutionContext import cats.effect._ +import cats.~> import fs2._ import docspell.store.file.FileStore @@ -19,6 +20,7 @@ import doobie._ import doobie.hikari.HikariTransactor trait Store[F[_]] { + def transform: ConnectionIO ~> F def transact[A](prg: ConnectionIO[A]): F[A] diff --git a/modules/store/src/main/scala/docspell/store/impl/StoreImpl.scala b/modules/store/src/main/scala/docspell/store/impl/StoreImpl.scala index f0f622bb..50c856b1 100644 --- a/modules/store/src/main/scala/docspell/store/impl/StoreImpl.scala +++ b/modules/store/src/main/scala/docspell/store/impl/StoreImpl.scala @@ -6,8 +6,10 @@ package docspell.store.impl +import cats.arrow.FunctionK import cats.effect.Async import cats.implicits._ +import cats.~> import docspell.store.file.FileStore import docspell.store.migrate.FlywayMigrate @@ -22,6 +24,9 @@ final class StoreImpl[F[_]: Async]( xa: Transactor[F] ) extends Store[F] { + def transform: ConnectionIO ~> F = + FunctionK.lift(transact) + def migrate: F[Int] = FlywayMigrate.run[F](jdbc).map(_.migrationsExecuted) diff --git a/modules/store/src/main/scala/docspell/store/records/RShare.scala b/modules/store/src/main/scala/docspell/store/records/RShare.scala index 72f67de7..af0b1e40 100644 --- a/modules/store/src/main/scala/docspell/store/records/RShare.scala +++ b/modules/store/src/main/scala/docspell/store/records/RShare.scala @@ -6,20 +6,25 @@ package docspell.store.records -import cats.data.NonEmptyList +import cats.data.{NonEmptyList, OptionT} import docspell.common._ import docspell.query.ItemQuery +import docspell.store.qb.DSL._ import docspell.store.qb._ +import doobie._ +import doobie.implicits._ + final case class RShare( id: Ident, cid: Ident, + name: Option[String], query: ItemQuery, enabled: Boolean, password: Option[Password], - publishedAt: Timestamp, - publishedUntil: Timestamp, + publishAt: Timestamp, + publishUntil: Timestamp, views: Int, lastAccess: Option[Timestamp] ) {} @@ -31,11 +36,12 @@ object RShare { val id = Column[Ident]("id", this) val cid = Column[Ident]("cid", this) + val name = Column[String]("name", this) val query = Column[ItemQuery]("query", this) val enabled = Column[Boolean]("enabled", this) - val password = Column[Password]("password", this) - val publishedAt = Column[Timestamp]("published_at", this) - val publishedUntil = Column[Timestamp]("published_until", this) + val password = Column[Password]("pass", this) + val publishedAt = Column[Timestamp]("publish_at", this) + val publishedUntil = Column[Timestamp]("publish_until", this) val views = Column[Int]("views", this) val lastAccess = Column[Timestamp]("last_access", this) @@ -43,6 +49,7 @@ object RShare { NonEmptyList.of( id, cid, + name, query, enabled, password, @@ -56,4 +63,47 @@ object RShare { val T: Table = Table(None) def as(alias: String): Table = Table(Some(alias)) + def insert(r: RShare): ConnectionIO[Int] = + DML.insert( + T, + T.all, + fr"${r.id},${r.cid},${r.name},${r.query},${r.enabled},${r.password},${r.publishAt},${r.publishUntil},${r.views},${r.lastAccess}" + ) + + def incAccess(id: Ident): ConnectionIO[Int] = + for { + curTime <- Timestamp.current[ConnectionIO] + n <- DML.update( + T, + T.id === id, + DML.set(T.views.increment(1), T.lastAccess.setTo(curTime)) + ) + } yield n + + def updateData(r: RShare, removePassword: Boolean): ConnectionIO[Int] = + DML.update( + T, + T.id === r.id && T.cid === r.cid, + DML.set( + T.name.setTo(r.name), + T.query.setTo(r.query), + T.enabled.setTo(r.enabled), + T.publishedUntil.setTo(r.publishUntil) + ) ++ (if (r.password.isDefined || removePassword) + List(T.password.setTo(r.password)) + else Nil) + ) + + def findOne(id: Ident, cid: Ident): OptionT[ConnectionIO, RShare] = + OptionT( + Select(select(T.all), from(T), T.id === id && T.cid === cid).build + .query[RShare] + .option + ) + + def findAllByCollective(cid: Ident): ConnectionIO[List[RShare]] = + Select(select(T.all), from(T), T.cid === cid).build.query[RShare].to[List] + + def deleteByIdAndCid(id: Ident, cid: Ident): ConnectionIO[Int] = + DML.delete(T, T.id === id && T.cid === cid) } diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 5ae8945d..a9619b1e 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -11,6 +11,7 @@ module Api exposing , addCorrOrg , addCorrPerson , addMember + , addShare , addTag , addTagsMultiple , attachmentPreviewURL @@ -40,6 +41,7 @@ module Api exposing , deleteOrg , deletePerson , deleteScanMailbox + , deleteShare , deleteSource , deleteTag , deleteUser @@ -72,6 +74,8 @@ module Api exposing , getPersonsLight , getScanMailbox , getSentMails + , getShare + , getShares , getSources , getTagCloud , getTags @@ -147,6 +151,7 @@ module Api exposing , unconfirmMultiple , updateNotifyDueItems , updateScanMailbox + , updateShare , upload , uploadAmend , uploadSingle @@ -215,6 +220,9 @@ import Api.Model.ScanMailboxSettingsList exposing (ScanMailboxSettingsList) import Api.Model.SearchStats exposing (SearchStats) import Api.Model.SecondFactor exposing (SecondFactor) import Api.Model.SentMails exposing (SentMails) +import Api.Model.ShareData exposing (ShareData) +import Api.Model.ShareDetail exposing (ShareDetail) +import Api.Model.ShareList exposing (ShareList) import Api.Model.SimpleMail exposing (SimpleMail) import Api.Model.SourceAndTags exposing (SourceAndTags) import Api.Model.SourceList exposing (SourceList) @@ -2206,6 +2214,57 @@ disableOtp flags otp receive = +--- Share + + +getShares : Flags -> (Result Http.Error ShareList -> msg) -> Cmd msg +getShares flags receive = + Http2.authGet + { url = flags.config.baseUrl ++ "/api/v1/sec/share" + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.ShareList.decoder + } + + +getShare : Flags -> String -> (Result Http.Error ShareDetail -> msg) -> Cmd msg +getShare flags id receive = + Http2.authGet + { url = flags.config.baseUrl ++ "/api/v1/sec/share/" ++ id + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.ShareDetail.decoder + } + + +addShare : Flags -> ShareData -> (Result Http.Error IdResult -> msg) -> Cmd msg +addShare flags share receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/share" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.ShareData.encode share) + , expect = Http.expectJson receive Api.Model.IdResult.decoder + } + + +updateShare : Flags -> String -> ShareData -> (Result Http.Error BasicResult -> msg) -> Cmd msg +updateShare flags id share receive = + Http2.authPut + { url = flags.config.baseUrl ++ "/api/v1/sec/share/" ++ id + , account = getAccount flags + , body = Http.jsonBody (Api.Model.ShareData.encode share) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + +deleteShare : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg +deleteShare flags id receive = + Http2.authDelete + { url = flags.config.baseUrl ++ "/api/v1/sec/share/" ++ id + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + + --- Helper diff --git a/modules/webapp/src/main/elm/Comp/DatePicker.elm b/modules/webapp/src/main/elm/Comp/DatePicker.elm index 3804c296..e0383651 100644 --- a/modules/webapp/src/main/elm/Comp/DatePicker.elm +++ b/modules/webapp/src/main/elm/Comp/DatePicker.elm @@ -37,7 +37,7 @@ init = emptyModel : DatePicker emptyModel = - DatePicker.initFromDate (Date.fromCalendarDate 2019 Aug 21) + DatePicker.initFromDate (Date.fromCalendarDate 2021 Oct 31) defaultSettings : Settings diff --git a/modules/webapp/src/main/elm/Comp/ShareForm.elm b/modules/webapp/src/main/elm/Comp/ShareForm.elm new file mode 100644 index 00000000..4f2d39cf --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ShareForm.elm @@ -0,0 +1,282 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.ShareForm exposing (Model, Msg, getShare, init, setShare, update, view) + +import Api.Model.ShareData exposing (ShareData) +import Api.Model.ShareDetail exposing (ShareDetail) +import Comp.Basic as B +import Comp.DatePicker +import Comp.PasswordInput +import Data.Flags exposing (Flags) +import DatePicker exposing (DatePicker) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onCheck, onInput) +import Messages.Comp.ShareForm exposing (Texts) +import Styles as S +import Util.Maybe + + +type alias Model = + { share : ShareDetail + , name : Maybe String + , query : String + , enabled : Bool + , passwordModel : Comp.PasswordInput.Model + , password : Maybe String + , passwordSet : Bool + , clearPassword : Bool + , untilModel : DatePicker + , untilDate : Maybe Int + } + + +init : ( Model, Cmd Msg ) +init = + let + ( dp, dpc ) = + Comp.DatePicker.init + in + ( { share = Api.Model.ShareDetail.empty + , name = Nothing + , query = "" + , enabled = False + , passwordModel = Comp.PasswordInput.init + , password = Nothing + , passwordSet = False + , clearPassword = False + , untilModel = dp + , untilDate = Nothing + } + , Cmd.map UntilDateMsg dpc + ) + + +isValid : Model -> Bool +isValid model = + model.query /= "" && model.untilDate /= Nothing + + +type Msg + = SetName String + | SetQuery String + | SetShare ShareDetail + | ToggleEnabled + | ToggleClearPassword + | PasswordMsg Comp.PasswordInput.Msg + | UntilDateMsg Comp.DatePicker.Msg + + +setShare : ShareDetail -> Msg +setShare share = + SetShare share + + +getShare : Model -> Maybe ( String, ShareData ) +getShare model = + if isValid model then + Just + ( model.share.id + , { name = model.name + , query = model.query + , enabled = model.enabled + , password = model.password + , removePassword = + if model.share.id == "" then + Nothing + + else + Just model.clearPassword + , publishUntil = Maybe.withDefault 0 model.untilDate + } + ) + + else + Nothing + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update _ msg model = + case msg of + SetShare s -> + ( { model + | share = s + , name = s.name + , query = s.query + , enabled = s.enabled + , password = Nothing + , passwordSet = s.password + , clearPassword = False + , untilDate = + if s.publishUntil > 0 then + Just s.publishUntil + + else + Nothing + } + , Cmd.none + ) + + SetName n -> + ( { model | name = Util.Maybe.fromString n }, Cmd.none ) + + SetQuery n -> + ( { model | query = n }, Cmd.none ) + + ToggleEnabled -> + ( { model | enabled = not model.enabled }, Cmd.none ) + + ToggleClearPassword -> + ( { model | clearPassword = not model.clearPassword }, Cmd.none ) + + PasswordMsg lm -> + let + ( pm, pw ) = + Comp.PasswordInput.update lm model.passwordModel + in + ( { model + | passwordModel = pm + , password = pw + } + , Cmd.none + ) + + UntilDateMsg lm -> + let + ( dp, event ) = + Comp.DatePicker.updateDefault lm model.untilModel + + nextDate = + case event of + DatePicker.Picked date -> + Just (Comp.DatePicker.endOfDay date) + + _ -> + Nothing + in + ( { model | untilModel = dp, untilDate = nextDate } + , Cmd.none + ) + + + +--- View + + +view : Texts -> Model -> Html Msg +view texts model = + div + [ class "flex flex-col" ] + [ div [ class "mb-4" ] + [ label + [ for "sharename" + , class S.inputLabel + ] + [ text texts.basics.name + ] + , input + [ type_ "text" + , onInput SetName + , placeholder texts.basics.name + , value <| Maybe.withDefault "" model.name + , id "sharename" + , class S.textInput + ] + [] + ] + , div [ class "mb-4" ] + [ label + [ for "sharequery" + , class S.inputLabel + ] + [ text texts.queryLabel + , B.inputRequired + ] + , input + [ type_ "text" + , onInput SetQuery + , placeholder texts.queryLabel + , value model.query + , id "sharequery" + , class S.textInput + , classList + [ ( S.inputErrorBorder + , not (isValid model) + ) + ] + ] + [] + ] + , div [ class "mb-4" ] + [ label + [ class "inline-flex items-center" + , for "source-enabled" + ] + [ input + [ type_ "checkbox" + , onCheck (\_ -> ToggleEnabled) + , checked model.enabled + , class S.checkboxInput + , id "source-enabled" + ] + [] + , span [ class "ml-2" ] + [ text texts.enabled + ] + ] + ] + , div [ class "mb-4" ] + [ label + [ class S.inputLabel + ] + [ text texts.password + ] + , Html.map PasswordMsg + (Comp.PasswordInput.view2 + { placeholder = texts.password } + model.password + False + model.passwordModel + ) + , div + [ class "mb-2" + , classList [ ( "hidden", not model.passwordSet ) ] + ] + [ label + [ class "inline-flex items-center" + , for "clear-password" + ] + [ input + [ type_ "checkbox" + , onCheck (\_ -> ToggleClearPassword) + , checked model.clearPassword + , class S.checkboxInput + , id "clear-password" + ] + [] + , span [ class "ml-2" ] + [ text texts.clearPassword + ] + ] + ] + ] + , div [ class "mb-2 max-w-sm" ] + [ label [ class S.inputLabel ] + [ text texts.publishUntil + , B.inputRequired + ] + , div [ class "relative" ] + [ Html.map UntilDateMsg + (Comp.DatePicker.viewTimeDefault + model.untilDate + model.untilModel + ) + , i [ class S.dateInputIcon, class "fa fa-calendar" ] [] + ] + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Comp/ShareManage.elm new file mode 100644 index 00000000..4fc431af --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ShareManage.elm @@ -0,0 +1,349 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.ShareManage exposing (Model, Msg, init, loadShares, update, view) + +import Api +import Api.Model.BasicResult exposing (BasicResult) +import Api.Model.IdResult exposing (IdResult) +import Api.Model.ShareDetail exposing (ShareDetail) +import Api.Model.ShareList exposing (ShareList) +import Comp.Basic as B +import Comp.ItemDetail.Model exposing (Msg(..)) +import Comp.MenuBar as MB +import Comp.ShareForm +import Comp.ShareTable +import Data.Flags exposing (Flags) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) +import Http +import Messages.Comp.ShareManage exposing (Texts) +import Styles as S + + +type FormError + = FormErrorNone + | FormErrorHttp Http.Error + | FormErrorInvalid + | FormErrorSubmit String + + +type ViewMode + = Table + | Form + + +type DeleteConfirm + = DeleteConfirmOff + | DeleteConfirmOn + + +type alias Model = + { viewMode : ViewMode + , shares : List ShareDetail + , formModel : Comp.ShareForm.Model + , loading : Bool + , formError : FormError + , deleteConfirm : DeleteConfirm + } + + +init : ( Model, Cmd Msg ) +init = + let + ( fm, fc ) = + Comp.ShareForm.init + in + ( { viewMode = Table + , shares = [] + , formModel = fm + , loading = False + , formError = FormErrorNone + , deleteConfirm = DeleteConfirmOff + } + , Cmd.map FormMsg fc + ) + + +type Msg + = LoadShares + | TableMsg Comp.ShareTable.Msg + | FormMsg Comp.ShareForm.Msg + | InitNewShare + | SetViewMode ViewMode + | Submit + | RequestDelete + | CancelDelete + | DeleteShareNow String + | LoadSharesResp (Result Http.Error ShareList) + | AddShareResp (Result Http.Error IdResult) + | UpdateShareResp (Result Http.Error BasicResult) + | GetShareResp (Result Http.Error ShareDetail) + | DeleteShareResp (Result Http.Error BasicResult) + + +loadShares : Msg +loadShares = + LoadShares + + + +--- update + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update flags msg model = + case msg of + InitNewShare -> + let + nm = + { model | viewMode = Form, formError = FormErrorNone } + + share = + Api.Model.ShareDetail.empty + in + update flags (FormMsg (Comp.ShareForm.setShare share)) nm + + SetViewMode vm -> + ( { model | viewMode = vm, formError = FormErrorNone } + , if vm == Table then + Api.getShares flags LoadSharesResp + + else + Cmd.none + ) + + FormMsg lm -> + let + ( fm, fc ) = + Comp.ShareForm.update flags lm model.formModel + in + ( { model | formModel = fm }, Cmd.map FormMsg fc ) + + TableMsg lm -> + let + action = + Comp.ShareTable.update lm + + nextModel = + { model | viewMode = Form, formError = FormErrorNone } + in + case action of + Comp.ShareTable.Edit share -> + update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel + + RequestDelete -> + ( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none ) + + CancelDelete -> + ( { model | deleteConfirm = DeleteConfirmOff }, Cmd.none ) + + DeleteShareNow id -> + ( { model | deleteConfirm = DeleteConfirmOff, loading = True } + , Api.deleteShare flags id DeleteShareResp + ) + + LoadShares -> + ( { model | loading = True }, Api.getShares flags LoadSharesResp ) + + LoadSharesResp (Ok list) -> + ( { model | loading = False, shares = list.items, formError = FormErrorNone }, Cmd.none ) + + LoadSharesResp (Err err) -> + ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none ) + + Submit -> + case Comp.ShareForm.getShare model.formModel of + Just ( id, data ) -> + if id == "" then + ( { model | loading = True }, Api.addShare flags data AddShareResp ) + + else + ( { model | loading = True }, Api.updateShare flags id data UpdateShareResp ) + + Nothing -> + ( { model | formError = FormErrorInvalid }, Cmd.none ) + + AddShareResp (Ok res) -> + if res.success then + ( model, Api.getShare flags res.id GetShareResp ) + + else + ( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none ) + + AddShareResp (Err err) -> + ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none ) + + UpdateShareResp (Ok res) -> + if res.success then + ( model, Api.getShare flags model.formModel.share.id GetShareResp ) + + else + ( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none ) + + UpdateShareResp (Err err) -> + ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none ) + + GetShareResp (Ok share) -> + let + nextModel = + { model | formError = FormErrorNone, loading = False } + in + update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel + + GetShareResp (Err err) -> + ( { model | formError = FormErrorHttp err }, Cmd.none ) + + DeleteShareResp (Ok res) -> + if res.success then + update flags (SetViewMode Table) { model | loading = False } + + else + ( { model | formError = FormErrorSubmit res.message, loading = False }, Cmd.none ) + + DeleteShareResp (Err err) -> + ( { model | formError = FormErrorHttp err, loading = False }, Cmd.none ) + + + +--- view + + +view : Texts -> Flags -> Model -> Html Msg +view texts _ model = + if model.viewMode == Table then + viewTable texts model + + else + viewForm texts model + + +viewTable : Texts -> Model -> Html Msg +viewTable texts model = + div [ class "flex flex-col" ] + [ MB.view + { start = + [] + , end = + [ MB.PrimaryButton + { tagger = InitNewShare + , title = texts.createNewShare + , icon = Just "fa fa-plus" + , label = texts.newShare + } + ] + , rootClasses = "mb-4" + } + , Html.map TableMsg (Comp.ShareTable.view texts.shareTable model.shares) + , B.loadingDimmer + { label = "" + , active = model.loading + } + ] + + +viewForm : Texts -> Model -> Html Msg +viewForm texts model = + let + newShare = + model.formModel.share.id == "" + in + Html.form [ class "relative" ] + [ if newShare then + h1 [ class S.header2 ] + [ text texts.createNewShare + ] + + else + h1 [ class S.header2 ] + [ text <| Maybe.withDefault texts.noName model.formModel.share.name + , div [ class "opacity-50 text-sm" ] + [ text "Id: " + , text model.formModel.share.id + ] + ] + , MB.view + { start = + [ MB.PrimaryButton + { tagger = Submit + , title = "Submit this form" + , icon = Just "fa fa-save" + , label = texts.basics.submit + } + , MB.SecondaryButton + { tagger = SetViewMode Table + , title = texts.basics.backToList + , icon = Just "fa fa-arrow-left" + , label = texts.basics.cancel + } + ] + , end = + if not newShare then + [ MB.DeleteButton + { tagger = RequestDelete + , title = texts.deleteThisShare + , icon = Just "fa fa-trash" + , label = texts.basics.delete + } + ] + + else + [] + , rootClasses = "mb-4" + } + , div + [ classList + [ ( "hidden", model.formError == FormErrorNone ) + ] + , class "my-2" + , class S.errorMessage + ] + [ case model.formError of + FormErrorNone -> + text "" + + FormErrorHttp err -> + text (texts.httpError err) + + FormErrorInvalid -> + text texts.correctFormErrors + + FormErrorSubmit m -> + text m + ] + , Html.map FormMsg (Comp.ShareForm.view texts.shareForm model.formModel) + , B.loadingDimmer + { active = model.loading + , label = texts.basics.loading + } + , B.contentDimmer + (model.deleteConfirm == DeleteConfirmOn) + (div [ class "flex flex-col" ] + [ div [ class "text-lg" ] + [ i [ class "fa fa-info-circle mr-2" ] [] + , text texts.reallyDeleteShare + ] + , div [ class "mt-4 flex flex-row items-center" ] + [ B.deleteButton + { label = texts.basics.yes + , icon = "fa fa-check" + , disabled = False + , handler = onClick (DeleteShareNow model.formModel.share.id) + , attrs = [ href "#" ] + } + , B.secondaryButton + { label = texts.basics.no + , icon = "fa fa-times" + , disabled = False + , handler = onClick CancelDelete + , attrs = [ href "#", class "ml-2" ] + } + ] + ] + ) + ] diff --git a/modules/webapp/src/main/elm/Comp/ShareTable.elm b/modules/webapp/src/main/elm/Comp/ShareTable.elm new file mode 100644 index 00000000..940f64c5 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ShareTable.elm @@ -0,0 +1,87 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.ShareTable exposing + ( Msg(..) + , SelectAction(..) + , update + , view + ) + +import Api.Model.ShareDetail exposing (ShareDetail) +import Comp.Basic as B +import Html exposing (..) +import Html.Attributes exposing (..) +import Messages.Comp.ShareTable exposing (Texts) +import Styles as S +import Util.Html +import Util.String + + +type Msg + = Select ShareDetail + + +type SelectAction + = Edit ShareDetail + + +update : Msg -> SelectAction +update msg = + case msg of + Select share -> + Edit share + + + +--- View + + +view : Texts -> List ShareDetail -> Html Msg +view texts shares = + table [ class S.tableMain ] + [ thead [] + [ tr [] + [ th [ class "" ] [] + , th [ class "text-left" ] + [ text texts.basics.id + ] + , th [ class "text-left" ] + [ text texts.basics.name + ] + , th [ class "text-center" ] + [ text texts.enabled + ] + , th [ class "text-center" ] + [ text texts.publishUntil + ] + ] + ] + , tbody [] + (List.map (renderShareLine texts) shares) + ] + + +renderShareLine : Texts -> ShareDetail -> Html Msg +renderShareLine texts share = + tr + [ class S.tableRow + ] + [ B.editLinkTableCell texts.basics.edit (Select share) + , td [ class "text-left py-4 md:py-2" ] + [ text (Util.String.ellipsis 8 share.id) + ] + , td [ class "text-left py-4 md:py-2" ] + [ text (Maybe.withDefault "-" share.name) + ] + , td [ class "w-px px-2 text-center" ] + [ Util.Html.checkbox2 share.enabled + ] + , td [ class "hidden sm:table-cell text-center" ] + [ texts.formatDateTime share.publishUntil |> text + ] + ] diff --git a/modules/webapp/src/main/elm/Data/Icons.elm b/modules/webapp/src/main/elm/Data/Icons.elm index f2455c8d..76a5f360 100644 --- a/modules/webapp/src/main/elm/Data/Icons.elm +++ b/modules/webapp/src/main/elm/Data/Icons.elm @@ -58,11 +58,11 @@ module Data.Icons exposing , personIcon2 , search , searchIcon + , share + , shareIcon , showQr , showQrIcon - , source , source2 - , sourceIcon , sourceIcon2 , tag , tag2 @@ -79,9 +79,14 @@ import Html exposing (Html, i) import Html.Attributes exposing (class) -source : String -source = - "upload icon" +share : String +share = + "fa fa-share-alt" + + +shareIcon : String -> Html msg +shareIcon classes = + i [ class (classes ++ " " ++ share) ] [] source2 : String @@ -89,11 +94,6 @@ source2 = "fa fa-upload" -sourceIcon : String -> Html msg -sourceIcon classes = - i [ class (source ++ " " ++ classes) ] [] - - sourceIcon2 : String -> Html msg sourceIcon2 classes = i [ class (source2 ++ " " ++ classes) ] [] diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareForm.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareForm.elm new file mode 100644 index 00000000..44a9bcb5 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareForm.elm @@ -0,0 +1,46 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.ShareForm exposing + ( Texts + , de + , gb + ) + +import Messages.Basics + + +type alias Texts = + { basics : Messages.Basics.Texts + , queryLabel : String + , enabled : String + , password : String + , publishUntil : String + , clearPassword : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , queryLabel = "Query" + , enabled = "Enabled" + , password = "Password" + , publishUntil = "Publish Until" + , clearPassword = "Remove password" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , queryLabel = "Abfrage" + , enabled = "Aktiv" + , password = "Passwort" + , publishUntil = "Publiziert bis" + , clearPassword = "Passwort entfernen" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm new file mode 100644 index 00000000..66b27e6e --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm @@ -0,0 +1,74 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.ShareManage exposing + ( Texts + , de + , gb + ) + +import Http +import Messages.Basics +import Messages.Comp.HttpError +import Messages.Comp.ShareForm +import Messages.Comp.ShareTable + + +type alias Texts = + { basics : Messages.Basics.Texts + , shareTable : Messages.Comp.ShareTable.Texts + , shareForm : Messages.Comp.ShareForm.Texts + , httpError : Http.Error -> String + , newShare : String + , copyToClipboard : String + , openInNewTab : String + , publicUrl : String + , reallyDeleteShare : String + , createNewShare : String + , deleteThisShare : String + , errorGeneratingQR : String + , correctFormErrors : String + , noName : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , httpError = Messages.Comp.HttpError.gb + , shareTable = Messages.Comp.ShareTable.gb + , shareForm = Messages.Comp.ShareForm.gb + , newShare = "New share" + , copyToClipboard = "Copy to clipboard" + , openInNewTab = "Open in new tab/window" + , publicUrl = "Public URL" + , reallyDeleteShare = "Really delete this share?" + , createNewShare = "Create new share" + , deleteThisShare = "Delete this share" + , errorGeneratingQR = "Error generating QR Code" + , correctFormErrors = "Please correct the errors in the form." + , noName = "No Name" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , shareTable = Messages.Comp.ShareTable.de + , shareForm = Messages.Comp.ShareForm.de + , httpError = Messages.Comp.HttpError.de + , newShare = "Neue Freigabe" + , copyToClipboard = "In die Zwischenablage kopieren" + , openInNewTab = "Im neuen Tab/Fenster öffnen" + , publicUrl = "Öffentliche URL" + , reallyDeleteShare = "Diese Freigabe wirklich entfernen?" + , createNewShare = "Neue Freigabe erstellen" + , deleteThisShare = "Freigabe löschen" + , errorGeneratingQR = "Fehler beim Generieren des QR-Code" + , correctFormErrors = "Bitte korrigiere die Fehler im Formular." + , noName = "Ohne Name" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm new file mode 100644 index 00000000..7b68fcc4 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm @@ -0,0 +1,42 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.ShareTable exposing + ( Texts + , de + , gb + ) + +import Messages.Basics +import Messages.DateFormat as DF +import Messages.UiLanguage + + +type alias Texts = + { basics : Messages.Basics.Texts + , formatDateTime : Int -> String + , enabled : String + , publishUntil : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.English + , enabled = "Enabled" + , publishUntil = "Publish Until" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.German + , enabled = "Aktiv" + , publishUntil = "Publiziert bis" + } diff --git a/modules/webapp/src/main/elm/Messages/Page/CollectiveSettings.elm b/modules/webapp/src/main/elm/Messages/Page/CollectiveSettings.elm index dc36ab98..4ca75f93 100644 --- a/modules/webapp/src/main/elm/Messages/Page/CollectiveSettings.elm +++ b/modules/webapp/src/main/elm/Messages/Page/CollectiveSettings.elm @@ -15,6 +15,7 @@ import Http import Messages.Basics import Messages.Comp.CollectiveSettingsForm import Messages.Comp.HttpError +import Messages.Comp.ShareManage import Messages.Comp.SourceManage import Messages.Comp.UserManage @@ -24,12 +25,14 @@ type alias Texts = , userManage : Messages.Comp.UserManage.Texts , collectiveSettingsForm : Messages.Comp.CollectiveSettingsForm.Texts , sourceManage : Messages.Comp.SourceManage.Texts + , shareManage : Messages.Comp.ShareManage.Texts , httpError : Http.Error -> String , collectiveSettings : String , insights : String , sources : String , settings : String , users : String + , shares : String , user : String , collective : String , size : String @@ -44,12 +47,14 @@ gb = , userManage = Messages.Comp.UserManage.gb , collectiveSettingsForm = Messages.Comp.CollectiveSettingsForm.gb , sourceManage = Messages.Comp.SourceManage.gb + , shareManage = Messages.Comp.ShareManage.gb , httpError = Messages.Comp.HttpError.gb , collectiveSettings = "Collective Settings" , insights = "Insights" , sources = "Sources" , settings = "Settings" , users = "Users" + , shares = "Shares" , user = "User" , collective = "Collective" , size = "Size" @@ -64,12 +69,14 @@ de = , userManage = Messages.Comp.UserManage.de , collectiveSettingsForm = Messages.Comp.CollectiveSettingsForm.de , sourceManage = Messages.Comp.SourceManage.de + , shareManage = Messages.Comp.ShareManage.de , httpError = Messages.Comp.HttpError.de , collectiveSettings = "Kollektiveinstellungen" , insights = "Statistiken" , sources = "Quellen" , settings = "Einstellungen" , users = "Benutzer" + , shares = "Freigaben" , user = "Benutzer" , collective = "Kollektiv" , size = "Größe" diff --git a/modules/webapp/src/main/elm/Page/CollectiveSettings/Data.elm b/modules/webapp/src/main/elm/Page/CollectiveSettings/Data.elm index d24b9494..e940ade4 100644 --- a/modules/webapp/src/main/elm/Page/CollectiveSettings/Data.elm +++ b/modules/webapp/src/main/elm/Page/CollectiveSettings/Data.elm @@ -17,6 +17,7 @@ import Api.Model.BasicResult exposing (BasicResult) import Api.Model.CollectiveSettings exposing (CollectiveSettings) import Api.Model.ItemInsights exposing (ItemInsights) import Comp.CollectiveSettingsForm +import Comp.ShareManage import Comp.SourceManage import Comp.UserManage import Data.Flags exposing (Flags) @@ -28,6 +29,7 @@ type alias Model = , sourceModel : Comp.SourceManage.Model , userModel : Comp.UserManage.Model , settingsModel : Comp.CollectiveSettingsForm.Model + , shareModel : Comp.ShareManage.Model , insights : ItemInsights , formState : FormState } @@ -48,10 +50,14 @@ init flags = ( cm, cc ) = Comp.CollectiveSettingsForm.init flags Api.Model.CollectiveSettings.empty + + ( shm, shc ) = + Comp.ShareManage.init in ( { currentTab = Just InsightsTab , sourceModel = sm , userModel = Comp.UserManage.emptyModel + , shareModel = shm , settingsModel = cm , insights = Api.Model.ItemInsights.empty , formState = InitialState @@ -59,6 +65,7 @@ init flags = , Cmd.batch [ Cmd.map SourceMsg sc , Cmd.map SettingsFormMsg cc + , Cmd.map ShareMsg shc ] ) @@ -68,6 +75,7 @@ type Tab | UserTab | InsightsTab | SettingsTab + | ShareTab type Msg @@ -79,3 +87,4 @@ type Msg | GetInsightsResp (Result Http.Error ItemInsights) | CollectiveSettingsResp (Result Http.Error CollectiveSettings) | SubmitResp (Result Http.Error BasicResult) + | ShareMsg Comp.ShareManage.Msg diff --git a/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm b/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm index b3b55b88..1e711acd 100644 --- a/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm +++ b/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm @@ -9,6 +9,7 @@ module Page.CollectiveSettings.Update exposing (update) import Api import Comp.CollectiveSettingsForm +import Comp.ShareManage import Comp.SourceManage import Comp.UserManage import Data.Flags exposing (Flags) @@ -36,6 +37,9 @@ update flags msg model = SettingsTab -> update flags Init m + ShareTab -> + update flags (ShareMsg Comp.ShareManage.loadShares) m + SourceMsg m -> let ( m2, c2 ) = @@ -43,6 +47,13 @@ update flags msg model = in ( { model | sourceModel = m2 }, Cmd.map SourceMsg c2 ) + ShareMsg lm -> + let + ( sm, sc ) = + Comp.ShareManage.update flags lm model.shareModel + in + ( { model | shareModel = sm }, Cmd.map ShareMsg sc ) + UserMsg m -> let ( m2, c2 ) = diff --git a/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm b/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm index 4454e30f..7c31f7ff 100644 --- a/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm +++ b/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm @@ -10,6 +10,7 @@ module Page.CollectiveSettings.View2 exposing (viewContent, viewSidebar) import Api.Model.TagCount exposing (TagCount) import Comp.Basic as B import Comp.CollectiveSettingsForm +import Comp.ShareManage import Comp.SourceManage import Comp.UserManage import Data.Flags exposing (Flags) @@ -60,6 +61,17 @@ viewSidebar texts visible _ _ model = [ class "ml-3" ] [ text texts.sources ] ] + , a + [ href "#" + , onClick (SetTab ShareTab) + , class S.sidebarLink + , menuEntryActive model ShareTab + ] + [ Icons.shareIcon "" + , span + [ class "ml-3" ] + [ text texts.shares ] + ] , a [ href "#" , onClick (SetTab SettingsTab) @@ -105,6 +117,9 @@ viewContent texts flags settings model = Just SourceTab -> viewSources texts flags settings model + Just ShareTab -> + viewShares texts flags model + Nothing -> [] ) @@ -230,6 +245,21 @@ viewSources texts flags settings model = ] +viewShares : Texts -> Flags -> Model -> List (Html Msg) +viewShares texts flags model = + [ h1 + [ class S.header1 + , class "inline-flex items-center" + ] + [ Icons.shareIcon "" + , div [ class "ml-3" ] + [ text texts.shares + ] + ] + , Html.map ShareMsg (Comp.ShareManage.view texts.shareManage flags model.shareModel) + ] + + viewUsers : Texts -> UiSettings -> Model -> List (Html Msg) viewUsers texts settings model = [ h1 From 4ef9d6c3ffca9bcf9201bd1a9df7b22d459bc04d Mon Sep 17 00:00:00 2001 From: eikek Date: Sat, 2 Oct 2021 23:05:19 +0200 Subject: [PATCH 03/37] Add expired flag to share details --- .../src/main/resources/docspell-openapi.yml | 3 +++ .../docspell/restserver/routes/ShareRoutes.scala | 14 ++++++++------ .../main/scala/docspell/store/records/RShare.scala | 6 +++++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index f01cd129..ac2cc363 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -4226,6 +4226,7 @@ components: - publishUntil - password - views + - expired properties: id: type: string @@ -4243,6 +4244,8 @@ components: publishUntil: type: integer format: date-time + expired: + type: boolean password: type: boolean views: diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala index 060ef30c..846bc7bc 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala @@ -9,15 +9,13 @@ package docspell.restserver.routes import cats.data.OptionT import cats.effect._ import cats.implicits._ - import docspell.backend.BackendApp import docspell.backend.auth.AuthToken import docspell.backend.ops.OShare -import docspell.common.Ident +import docspell.common.{Ident, Timestamp} import docspell.restapi.model._ import docspell.restserver.http4s.ResponseGenerator import docspell.store.records.RShare - import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ @@ -33,7 +31,8 @@ object ShareRoutes { case GET -> Root => for { all <- backend.share.findAll(user.account.collective) - res <- Ok(ShareList(all.map(mkShareDetail))) + now <- Timestamp.current[F] + res <- Ok(ShareList(all.map(mkShareDetail(now)))) } yield res case req @ POST -> Root => @@ -47,7 +46,8 @@ object ShareRoutes { case GET -> Root / Ident(id) => (for { share <- backend.share.findOne(id, user.account.collective) - resp <- OptionT.liftF(Ok(mkShareDetail(share))) + now <- OptionT.liftF(Timestamp.current[F]) + resp <- OptionT.liftF(Ok(mkShareDetail(now)(share))) } yield resp).getOrElseF(NotFound()) case req @ PUT -> Root / Ident(id) => @@ -90,7 +90,7 @@ object ShareRoutes { BasicResult(false, "Until date must not be in the past") } - def mkShareDetail(r: RShare): ShareDetail = + def mkShareDetail(now: Timestamp)(r: RShare): ShareDetail = ShareDetail( r.id, r.query, @@ -98,8 +98,10 @@ object ShareRoutes { r.enabled, r.publishAt, r.publishUntil, + now > r.publishUntil, r.password.isDefined, r.views, r.lastAccess ) + } diff --git a/modules/store/src/main/scala/docspell/store/records/RShare.scala b/modules/store/src/main/scala/docspell/store/records/RShare.scala index af0b1e40..4cef0929 100644 --- a/modules/store/src/main/scala/docspell/store/records/RShare.scala +++ b/modules/store/src/main/scala/docspell/store/records/RShare.scala @@ -102,7 +102,11 @@ object RShare { ) def findAllByCollective(cid: Ident): ConnectionIO[List[RShare]] = - Select(select(T.all), from(T), T.cid === cid).build.query[RShare].to[List] + Select(select(T.all), from(T), T.cid === cid) + .orderBy(T.publishedAt.desc) + .build + .query[RShare] + .to[List] def deleteByIdAndCid(id: Ident, cid: Ident): ConnectionIO[Int] = DML.delete(T, T.id === id && T.cid === cid) From 189009325e08bda7a1c8ab931970453eb6fddeeb Mon Sep 17 00:00:00 2001 From: eikek Date: Sat, 2 Oct 2021 23:08:01 +0200 Subject: [PATCH 04/37] Update tailwind to 2.2.16 --- modules/webapp/package-lock.json | 392 +++++++++++++++--------------- modules/webapp/package.json | 2 +- modules/webapp/tailwind.config.js | 2 +- 3 files changed, 204 insertions(+), 192 deletions(-) diff --git a/modules/webapp/package-lock.json b/modules/webapp/package-lock.json index 702326f0..6f8016f3 100644 --- a/modules/webapp/package-lock.json +++ b/modules/webapp/package-lock.json @@ -32,14 +32,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz", "integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==" }, - "@fullhuman/postcss-purgecss": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz", - "integrity": "sha512-kwOXw8fZ0Lt1QmeOOrd+o4Ibvp4UTEBFQbzvWldjlKv5n+G9sXfIPn1hh63IQIL8K8vbvv1oYMJiIUbuy9bGaA==", - "requires": { - "purgecss": "^3.1.3" - } - }, "@nodelib/fs.scandir": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", @@ -128,16 +120,16 @@ "picomatch": "^2.0.4" } }, + "arg": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", + "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==" + }, "array-union": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==" }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" - }, "autoprefixer": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.5.tgz", @@ -517,9 +509,9 @@ } }, "didyoumean": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz", - "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, "dir-glob": { "version": "3.0.1", @@ -604,16 +596,15 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "fast-glob": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", - "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", + "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" + "micromatch": "^4.0.4" } }, "fastq": { @@ -643,11 +634,10 @@ "integrity": "sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA==" }, "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", "requires": { - "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" @@ -680,9 +670,9 @@ "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==" }, "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -692,38 +682,6 @@ "path-is-absolute": "^1.0.0" } }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - }, - "dependencies": { - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "requires": { - "is-glob": "^2.0.0" - } - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "requires": { - "is-extglob": "^1.0.0" - } - } - } - }, "glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -913,11 +871,6 @@ "has": "^1.0.3" } }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1015,11 +968,6 @@ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" }, - "lodash.toarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", - "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" - }, "lodash.topath": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", @@ -1041,12 +989,19 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "requires": { "braces": "^3.0.1", - "picomatch": "^2.0.5" + "picomatch": "^2.2.3" + }, + "dependencies": { + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" + } } }, "mini-svg-data-uri": { @@ -1068,9 +1023,9 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "modern-normalize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.0.0.tgz", - "integrity": "sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", + "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==" }, "nanoid": { "version": "3.1.22", @@ -1078,11 +1033,11 @@ "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==" }, "node-emoji": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", - "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", "requires": { - "lodash.toarray": "^4.4.0" + "lodash": "^4.17.21" } }, "normalize-path": { @@ -1108,15 +1063,10 @@ "boolbase": "^1.0.0" } }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, "object-hash": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz", - "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" }, "once": { "version": "1.4.0", @@ -1146,32 +1096,6 @@ } } }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "requires": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - }, - "dependencies": { - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "requires": { - "is-extglob": "^1.0.0" - } - } - } - }, "parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -1303,42 +1227,6 @@ "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.0.tgz", "integrity": "sha512-hybnScTaZM2iEA6kzVQ6Spozy7kVdLw+lGw8hftLlBEzt93uzXoltkYp9u0tI8xbfhxDLTOOzHsHQCkYdmzRUg==" }, - "postcss-functions": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz", - "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=", - "requires": { - "glob": "^7.1.2", - "object-assign": "^4.1.1", - "postcss": "^6.0.9", - "postcss-value-parser": "^3.3.0" - }, - "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "postcss-import": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.1.tgz", @@ -1449,11 +1337,22 @@ } }, "postcss-nested": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.5.tgz", - "integrity": "sha512-GSRXYz5bccobpTzLQZXOnSOfKl6TwVr5CyAQJUPub4nuRJSOECK5AqurxVgmtxP48p0Kc/ndY/YyS1yqldX0Ew==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", "requires": { - "postcss-selector-parser": "^6.0.4" + "postcss-selector-parser": "^6.0.6" + }, + "dependencies": { + "postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + } } }, "postcss-normalize-charset": { @@ -1619,9 +1518,9 @@ "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" }, "purgecss": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-3.1.3.tgz", - "integrity": "sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.0.3.tgz", + "integrity": "sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==", "requires": { "commander": "^6.0.0", "glob": "^7.0.0", @@ -1700,6 +1599,14 @@ "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, "run-parallel": { "version": "1.1.10", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", @@ -1829,37 +1736,42 @@ } }, "tailwindcss": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.1.1.tgz", - "integrity": "sha512-zZ6axGqpSZOCBS7wITm/WNHkBzDt5CIZlDlx0eCVldwTxFPELCVGbgh7Xpb3/kZp3cUxOmK7bZUjqhuMrbN6xQ==", + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.16.tgz", + "integrity": "sha512-EireCtpQyyJ4Xz8NYzHafBoy4baCOO96flM0+HgtsFcIQ9KFy/YBK3GEtlnD+rXen0e4xm8t3WiUcKBJmN6yjg==", "requires": { - "@fullhuman/postcss-purgecss": "^3.1.3", + "arg": "^5.0.1", "bytes": "^3.0.0", - "chalk": "^4.1.0", - "chokidar": "^3.5.1", - "color": "^3.1.3", + "chalk": "^4.1.2", + "chokidar": "^3.5.2", + "color": "^4.0.1", + "cosmiconfig": "^7.0.1", "detective": "^5.2.0", - "didyoumean": "^1.2.1", + "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.5", - "fs-extra": "^9.1.0", + "fast-glob": "^3.2.7", + "fs-extra": "^10.0.0", + "glob-parent": "^6.0.1", "html-tags": "^3.1.0", + "is-color-stop": "^1.1.0", + "is-glob": "^4.0.1", "lodash": "^4.17.21", "lodash.topath": "^4.5.2", - "modern-normalize": "^1.0.0", - "node-emoji": "^1.8.1", + "modern-normalize": "^1.1.0", + "node-emoji": "^1.11.0", "normalize-path": "^3.0.0", - "object-hash": "^2.1.1", - "parse-glob": "^3.0.4", - "postcss-functions": "^3", + "object-hash": "^2.2.0", "postcss-js": "^3.0.3", - "postcss-nested": "5.0.5", - "postcss-selector-parser": "^6.0.4", + "postcss-load-config": "^3.1.0", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.6", "postcss-value-parser": "^4.1.0", "pretty-hrtime": "^1.0.3", + "purgecss": "^4.0.3", "quick-lru": "^5.1.1", "reduce-css-calc": "^2.1.8", - "resolve": "^1.20.0" + "resolve": "^1.20.0", + "tmp": "^0.2.1" }, "dependencies": { "ansi-styles": { @@ -1870,15 +1782,58 @@ "color-convert": "^2.0.1" } }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "color": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/color/-/color-4.0.1.tgz", + "integrity": "sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==", + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.6.0" + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1892,17 +1847,66 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "color-string": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", + "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", "requires": { - "has-flag": "^4.0.0" + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "requires": { + "is-glob": "^4.0.3" + }, + "dependencies": { + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + } + } + }, + "postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" } } } @@ -1912,6 +1916,14 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + } + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/modules/webapp/package.json b/modules/webapp/package.json index fa662612..d5c85f4b 100644 --- a/modules/webapp/package.json +++ b/modules/webapp/package.json @@ -11,6 +11,6 @@ "postcss": "^8.2.9", "postcss-cli": "^9.0.0", "postcss-import": "^14.0.1", - "tailwindcss": "^2.1.1" + "tailwindcss": "^2.2.16" } } diff --git a/modules/webapp/tailwind.config.js b/modules/webapp/tailwind.config.js index 8dcb72c4..3578b787 100644 --- a/modules/webapp/tailwind.config.js +++ b/modules/webapp/tailwind.config.js @@ -19,7 +19,7 @@ module.exports = { orange: colors.orange, teal: colors.teal, lime: colors.lime, - lightblue: colors.lightBlue + lightblue: colors.sky } } }, From aa21e7a74cce60965fe3647cb4897db4e07a12ae Mon Sep 17 00:00:00 2001 From: eikek Date: Sat, 2 Oct 2021 23:46:58 +0200 Subject: [PATCH 05/37] Create shares from search and select view --- .../webapp/src/main/elm/Comp/PublishItems.elm | 292 ++++++++++++++++++ .../webapp/src/main/elm/Comp/ShareForm.elm | 35 ++- .../webapp/src/main/elm/Comp/ShareManage.elm | 228 ++++++++------ .../webapp/src/main/elm/Comp/ShareTable.elm | 11 +- .../webapp/src/main/elm/Comp/ShareView.elm | 184 +++++++++++ .../main/elm/Messages/Comp/PublishItems.elm | 82 +++++ .../main/elm/Messages/Comp/ShareManage.elm | 7 + .../src/main/elm/Messages/Comp/ShareTable.elm | 6 +- .../src/main/elm/Messages/Comp/ShareView.elm | 66 ++++ .../src/main/elm/Messages/Page/Home.elm | 19 ++ .../webapp/src/main/elm/Page/Home/Data.elm | 32 +- .../webapp/src/main/elm/Page/Home/Update.elm | 118 +++++++ .../webapp/src/main/elm/Page/Home/View2.elm | 92 +++++- modules/webapp/src/main/elm/Styles.elm | 5 + 14 files changed, 1046 insertions(+), 131 deletions(-) create mode 100644 modules/webapp/src/main/elm/Comp/PublishItems.elm create mode 100644 modules/webapp/src/main/elm/Comp/ShareView.elm create mode 100644 modules/webapp/src/main/elm/Messages/Comp/PublishItems.elm create mode 100644 modules/webapp/src/main/elm/Messages/Comp/ShareView.elm diff --git a/modules/webapp/src/main/elm/Comp/PublishItems.elm b/modules/webapp/src/main/elm/Comp/PublishItems.elm new file mode 100644 index 00000000..2a491f15 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/PublishItems.elm @@ -0,0 +1,292 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.PublishItems exposing + ( Model + , Msg + , Outcome(..) + , init + , initQuery + , update + , view + ) + +import Api +import Api.Model.IdResult exposing (IdResult) +import Api.Model.ShareDetail exposing (ShareDetail) +import Comp.Basic as B +import Comp.MenuBar as MB +import Comp.ShareForm +import Comp.ShareView +import Data.Flags exposing (Flags) +import Data.Icons as Icons +import Data.ItemQuery exposing (ItemQuery) +import Data.SearchMode exposing (SearchMode) +import Html exposing (..) +import Html.Attributes exposing (..) +import Http +import Messages.Comp.PublishItems exposing (Texts) +import Ports +import Styles as S + + + +--- Model + + +type ViewMode + = ViewModeEdit + | ViewModeInfo ShareDetail + + +type FormError + = FormErrorNone + | FormErrorHttp Http.Error + | FormErrorInvalid + | FormErrorSubmit String + + +type alias Model = + { formModel : Comp.ShareForm.Model + , viewMode : ViewMode + , formError : FormError + , loading : Bool + } + + +init : ( Model, Cmd Msg ) +init = + let + ( fm, fc ) = + Comp.ShareForm.init + in + ( { formModel = fm + , viewMode = ViewModeEdit + , formError = FormErrorNone + , loading = False + } + , Cmd.map FormMsg fc + ) + + +initQuery : ItemQuery -> ( Model, Cmd Msg ) +initQuery query = + let + ( fm, fc ) = + Comp.ShareForm.initQuery (Data.ItemQuery.render query) + in + ( { formModel = fm + , viewMode = ViewModeEdit + , formError = FormErrorNone + , loading = False + } + , Cmd.map FormMsg fc + ) + + + +--- Update + + +type Msg + = FormMsg Comp.ShareForm.Msg + | CancelPublish + | SubmitPublish + | PublishResp (Result Http.Error IdResult) + | GetShareResp (Result Http.Error ShareDetail) + + +type Outcome + = OutcomeDone + | OutcomeInProgress + + +type alias UpdateResult = + { model : Model + , cmd : Cmd Msg + , outcome : Outcome + } + + +update : Flags -> Msg -> Model -> UpdateResult +update flags msg model = + case msg of + CancelPublish -> + { model = model + , cmd = Cmd.none + , outcome = OutcomeDone + } + + FormMsg lm -> + let + ( fm, fc ) = + Comp.ShareForm.update flags lm model.formModel + in + { model = { model | formModel = fm } + , cmd = Cmd.map FormMsg fc + , outcome = OutcomeInProgress + } + + SubmitPublish -> + case Comp.ShareForm.getShare model.formModel of + Just ( _, data ) -> + { model = { model | loading = True } + , cmd = Api.addShare flags data PublishResp + , outcome = OutcomeInProgress + } + + Nothing -> + { model = { model | formError = FormErrorInvalid } + , cmd = Cmd.none + , outcome = OutcomeInProgress + } + + PublishResp (Ok res) -> + if res.success then + { model = model + , cmd = Api.getShare flags res.id GetShareResp + , outcome = OutcomeInProgress + } + + else + { model = { model | formError = FormErrorSubmit res.message, loading = False } + , cmd = Cmd.none + , outcome = OutcomeInProgress + } + + PublishResp (Err err) -> + { model = { model | formError = FormErrorHttp err, loading = False } + , cmd = Cmd.none + , outcome = OutcomeInProgress + } + + GetShareResp (Ok share) -> + { model = + { model + | formError = FormErrorNone + , loading = False + , viewMode = ViewModeInfo share + } + , cmd = Ports.initClipboard (Comp.ShareView.clipboardData share) + , outcome = OutcomeInProgress + } + + GetShareResp (Err err) -> + { model = { model | formError = FormErrorHttp err, loading = False } + , cmd = Cmd.none + , outcome = OutcomeInProgress + } + + + +--- View + + +view : Texts -> Flags -> Model -> Html Msg +view texts flags model = + div [] + [ B.loadingDimmer + { active = model.loading + , label = "" + } + , case model.viewMode of + ViewModeEdit -> + viewForm texts model + + ViewModeInfo share -> + viewInfo texts flags model share + ] + + +viewInfo : Texts -> Flags -> Model -> ShareDetail -> Html Msg +viewInfo texts flags model share = + let + cfg = + { mainClasses = "" + , showAccessData = False + } + in + div [ class "px-2 mb-4" ] + [ h1 [ class S.header1 ] + [ text texts.title + ] + , div + [ class S.infoMessage + ] + [ text texts.infoText + ] + , MB.view <| + { start = + [ MB.SecondaryButton + { tagger = CancelPublish + , title = texts.cancelPublishTitle + , icon = Just "fa fa-arrow-left" + , label = texts.doneLabel + } + ] + , end = [] + , rootClasses = "my-4" + } + , div [] + [ Comp.ShareView.view cfg texts.shareView flags share + ] + ] + + +viewForm : Texts -> Model -> Html Msg +viewForm texts model = + div [ class "px-2 mb-4" ] + [ h1 [ class S.header1 ] + [ text texts.title + ] + , div + [ class S.infoMessage + ] + [ text texts.infoText + ] + , MB.view <| + { start = + [ MB.PrimaryButton + { tagger = SubmitPublish + , title = texts.submitPublishTitle + , icon = Just Icons.share + , label = texts.submitPublish + } + , MB.SecondaryButton + { tagger = CancelPublish + , title = texts.cancelPublishTitle + , icon = Just "fa fa-times" + , label = texts.cancelPublish + } + ] + , end = [] + , rootClasses = "my-4" + } + , div [] + [ Html.map FormMsg (Comp.ShareForm.view texts.shareForm model.formModel) + ] + , div + [ classList + [ ( "hidden", model.formError == FormErrorNone ) + ] + , class "my-2" + , class S.errorMessage + ] + [ case model.formError of + FormErrorNone -> + text "" + + FormErrorHttp err -> + text (texts.httpError err) + + FormErrorInvalid -> + text texts.correctFormErrors + + FormErrorSubmit m -> + text m + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/ShareForm.elm b/modules/webapp/src/main/elm/Comp/ShareForm.elm index 4f2d39cf..d07e74a4 100644 --- a/modules/webapp/src/main/elm/Comp/ShareForm.elm +++ b/modules/webapp/src/main/elm/Comp/ShareForm.elm @@ -5,7 +5,7 @@ -} -module Comp.ShareForm exposing (Model, Msg, getShare, init, setShare, update, view) +module Comp.ShareForm exposing (Model, Msg, getShare, init, initQuery, setShare, update, view) import Api.Model.ShareData exposing (ShareData) import Api.Model.ShareDetail exposing (ShareDetail) @@ -36,16 +36,16 @@ type alias Model = } -init : ( Model, Cmd Msg ) -init = +initQuery : String -> ( Model, Cmd Msg ) +initQuery q = let ( dp, dpc ) = Comp.DatePicker.init in ( { share = Api.Model.ShareDetail.empty , name = Nothing - , query = "" - , enabled = False + , query = q + , enabled = True , passwordModel = Comp.PasswordInput.init , password = Nothing , passwordSet = False @@ -57,6 +57,11 @@ init = ) +init : ( Model, Cmd Msg ) +init = + initQuery "" + + isValid : Model -> Bool isValid model = model.query /= "" && model.untilDate /= Nothing @@ -206,7 +211,7 @@ view texts model = , class S.textInput , classList [ ( S.inputErrorBorder - , not (isValid model) + , model.query == "" ) ] ] @@ -265,12 +270,16 @@ view texts model = ] ] ] - , div [ class "mb-2 max-w-sm" ] + , div + [ class "mb-2 max-w-sm" + ] [ label [ class S.inputLabel ] [ text texts.publishUntil , B.inputRequired ] - , div [ class "relative" ] + , div + [ class "relative" + ] [ Html.map UntilDateMsg (Comp.DatePicker.viewTimeDefault model.untilDate @@ -278,5 +287,15 @@ view texts model = ) , i [ class S.dateInputIcon, class "fa fa-calendar" ] [] ] + , div + [ classList + [ ( "hidden" + , model.untilDate /= Nothing + ) + ] + , class "mt-1" + , class S.errorText + ] + [ text "This field is required." ] ] ] diff --git a/modules/webapp/src/main/elm/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Comp/ShareManage.elm index 4fc431af..472680c4 100644 --- a/modules/webapp/src/main/elm/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Comp/ShareManage.elm @@ -17,12 +17,14 @@ import Comp.ItemDetail.Model exposing (Msg(..)) import Comp.MenuBar as MB import Comp.ShareForm import Comp.ShareTable +import Comp.ShareView import Data.Flags exposing (Flags) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) import Http import Messages.Comp.ShareManage exposing (Texts) +import Ports import Styles as S @@ -107,7 +109,7 @@ update flags msg model = share = Api.Model.ShareDetail.empty in - update flags (FormMsg (Comp.ShareForm.setShare share)) nm + update flags (FormMsg (Comp.ShareForm.setShare { share | enabled = True })) nm SetViewMode vm -> ( { model | viewMode = vm, formError = FormErrorNone } @@ -129,13 +131,10 @@ update flags msg model = let action = Comp.ShareTable.update lm - - nextModel = - { model | viewMode = Form, formError = FormErrorNone } in case action of Comp.ShareTable.Edit share -> - update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel + setShare share flags model RequestDelete -> ( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none ) @@ -190,11 +189,7 @@ update flags msg model = ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none ) GetShareResp (Ok share) -> - let - nextModel = - { model | formError = FormErrorNone, loading = False } - in - update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel + setShare share flags model GetShareResp (Err err) -> ( { model | formError = FormErrorHttp err }, Cmd.none ) @@ -210,17 +205,32 @@ update flags msg model = ( { model | formError = FormErrorHttp err, loading = False }, Cmd.none ) +setShare : ShareDetail -> Flags -> Model -> ( Model, Cmd Msg ) +setShare share flags model = + let + nextModel = + { model | formError = FormErrorNone, viewMode = Form, loading = False } + + initClipboard = + Ports.initClipboard (Comp.ShareView.clipboardData share) + + ( nm, nc ) = + update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel + in + ( nm, Cmd.batch [ initClipboard, nc ] ) + + --- view view : Texts -> Flags -> Model -> Html Msg -view texts _ model = +view texts flags model = if model.viewMode == Table then viewTable texts model else - viewForm texts model + viewForm texts flags model viewTable : Texts -> Model -> Html Msg @@ -247,103 +257,119 @@ viewTable texts model = ] -viewForm : Texts -> Model -> Html Msg -viewForm texts model = +viewForm : Texts -> Flags -> Model -> Html Msg +viewForm texts flags model = let newShare = model.formModel.share.id == "" in - Html.form [ class "relative" ] - [ if newShare then - h1 [ class S.header2 ] - [ text texts.createNewShare - ] - - else - h1 [ class S.header2 ] - [ text <| Maybe.withDefault texts.noName model.formModel.share.name - , div [ class "opacity-50 text-sm" ] - [ text "Id: " - , text model.formModel.share.id + div [ class "relative" ] + [ Html.form [] + [ if newShare then + h1 [ class S.header2 ] + [ text texts.createNewShare ] - ] - , MB.view - { start = - [ MB.PrimaryButton - { tagger = Submit - , title = "Submit this form" - , icon = Just "fa fa-save" - , label = texts.basics.submit - } - , MB.SecondaryButton - { tagger = SetViewMode Table - , title = texts.basics.backToList - , icon = Just "fa fa-arrow-left" - , label = texts.basics.cancel - } - ] - , end = - if not newShare then - [ MB.DeleteButton - { tagger = RequestDelete - , title = texts.deleteThisShare - , icon = Just "fa fa-trash" - , label = texts.basics.delete + + else + h1 [ class S.header2 ] + [ text <| Maybe.withDefault texts.noName model.formModel.share.name + , div [ class "opacity-50 text-sm" ] + [ text "Id: " + , text model.formModel.share.id + ] + ] + , MB.view + { start = + [ MB.PrimaryButton + { tagger = Submit + , title = "Submit this form" + , icon = Just "fa fa-save" + , label = texts.basics.submit + } + , MB.SecondaryButton + { tagger = SetViewMode Table + , title = texts.basics.backToList + , icon = Just "fa fa-arrow-left" + , label = texts.basics.cancel } ] + , end = + if not newShare then + [ MB.DeleteButton + { tagger = RequestDelete + , title = texts.deleteThisShare + , icon = Just "fa fa-trash" + , label = texts.basics.delete + } + ] - else - [] - , rootClasses = "mb-4" - } - , div - [ classList - [ ( "hidden", model.formError == FormErrorNone ) + else + [] + , rootClasses = "mb-4" + } + , div + [ classList + [ ( "hidden", model.formError == FormErrorNone ) + ] + , class "my-2" + , class S.errorMessage ] - , class "my-2" - , class S.errorMessage + [ case model.formError of + FormErrorNone -> + text "" + + FormErrorHttp err -> + text (texts.httpError err) + + FormErrorInvalid -> + text texts.correctFormErrors + + FormErrorSubmit m -> + text m + ] + , Html.map FormMsg (Comp.ShareForm.view texts.shareForm model.formModel) + , B.loadingDimmer + { active = model.loading + , label = texts.basics.loading + } + , B.contentDimmer + (model.deleteConfirm == DeleteConfirmOn) + (div [ class "flex flex-col" ] + [ div [ class "text-lg" ] + [ i [ class "fa fa-info-circle mr-2" ] [] + , text texts.reallyDeleteShare + ] + , div [ class "mt-4 flex flex-row items-center" ] + [ B.deleteButton + { label = texts.basics.yes + , icon = "fa fa-check" + , disabled = False + , handler = onClick (DeleteShareNow model.formModel.share.id) + , attrs = [ href "#" ] + } + , B.secondaryButton + { label = texts.basics.no + , icon = "fa fa-times" + , disabled = False + , handler = onClick CancelDelete + , attrs = [ href "#", class "ml-2" ] + } + ] + ] + ) ] - [ case model.formError of - FormErrorNone -> - text "" - - FormErrorHttp err -> - text (texts.httpError err) - - FormErrorInvalid -> - text texts.correctFormErrors - - FormErrorSubmit m -> - text m - ] - , Html.map FormMsg (Comp.ShareForm.view texts.shareForm model.formModel) - , B.loadingDimmer - { active = model.loading - , label = texts.basics.loading - } - , B.contentDimmer - (model.deleteConfirm == DeleteConfirmOn) - (div [ class "flex flex-col" ] - [ div [ class "text-lg" ] - [ i [ class "fa fa-info-circle mr-2" ] [] - , text texts.reallyDeleteShare - ] - , div [ class "mt-4 flex flex-row items-center" ] - [ B.deleteButton - { label = texts.basics.yes - , icon = "fa fa-check" - , disabled = False - , handler = onClick (DeleteShareNow model.formModel.share.id) - , attrs = [ href "#" ] - } - , B.secondaryButton - { label = texts.basics.no - , icon = "fa fa-times" - , disabled = False - , handler = onClick CancelDelete - , attrs = [ href "#", class "ml-2" ] - } - ] - ] - ) + , shareInfo texts flags model.formModel.share + ] + + +shareInfo : Texts -> Flags -> ShareDetail -> Html Msg +shareInfo texts flags share = + div + [ class "mt-6" + , classList [ ( "hidden", share.id == "" ) ] + ] + [ h2 [ class S.header2 ] + [ text texts.shareInformation + ] + , Comp.ShareView.viewDefault texts.shareView flags share ] diff --git a/modules/webapp/src/main/elm/Comp/ShareTable.elm b/modules/webapp/src/main/elm/Comp/ShareTable.elm index 940f64c5..b62f39b5 100644 --- a/modules/webapp/src/main/elm/Comp/ShareTable.elm +++ b/modules/webapp/src/main/elm/Comp/ShareTable.elm @@ -54,7 +54,7 @@ view texts shares = [ text texts.basics.name ] , th [ class "text-center" ] - [ text texts.enabled + [ text texts.active ] , th [ class "text-center" ] [ text texts.publishUntil @@ -79,7 +79,14 @@ renderShareLine texts share = [ text (Maybe.withDefault "-" share.name) ] , td [ class "w-px px-2 text-center" ] - [ Util.Html.checkbox2 share.enabled + [ if not share.enabled then + i [ class "fa fa-ban" ] [] + + else if share.expired then + i [ class "fa fa-bolt text-red-600 dark:text-orange-800" ] [] + + else + i [ class "fa fa-check" ] [] ] , td [ class "hidden sm:table-cell text-center" ] [ texts.formatDateTime share.publishUntil |> text diff --git a/modules/webapp/src/main/elm/Comp/ShareView.elm b/modules/webapp/src/main/elm/Comp/ShareView.elm new file mode 100644 index 00000000..f7d4962f --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ShareView.elm @@ -0,0 +1,184 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.ShareView exposing (ViewSettings, clipboardData, view, viewDefault) + +import Api.Model.ShareDetail exposing (ShareDetail) +import Data.Flags exposing (Flags) +import Html exposing (..) +import Html.Attributes exposing (..) +import Messages.Comp.ShareView exposing (Texts) +import QRCode +import Styles as S + + +type alias ViewSettings = + { mainClasses : String + , showAccessData : Bool + } + + +view : ViewSettings -> Texts -> Flags -> ShareDetail -> Html msg +view cfg texts flags share = + if not share.enabled then + viewDisabled cfg texts share + + else if share.expired then + viewExpired cfg texts share + + else + viewActive cfg texts flags share + + +viewDefault : Texts -> Flags -> ShareDetail -> Html msg +viewDefault = + view + { mainClasses = "" + , showAccessData = True + } + + +clipboardData : ShareDetail -> ( String, String ) +clipboardData share = + ( "app-share-" ++ share.id, "#app-share-url-copy-to-clipboard-btn-" ++ share.id ) + + + +--- Helper + + +viewActive : ViewSettings -> Texts -> Flags -> ShareDetail -> Html msg +viewActive cfg texts flags share = + let + clipboard = + clipboardData share + + appUrl = + flags.config.baseUrl ++ "/app/share/" ++ share.id + + styleUrl = + "truncate px-2 py-2 border-0 border-t border-b border-r font-mono text-sm my-auto rounded-r border-gray-400 dark:border-bluegray-500" + + infoLine hidden icon label value = + div + [ class "flex flex-row items-center" + , classList [ ( "hidden", hidden ) ] + ] + [ div [ class "flex mr-3" ] + [ i [ class icon ] [] + ] + , div [ class "flex flex-col" ] + [ div [ class "-mb-1" ] + [ text value + ] + , div [ class "opacity-50 text-sm" ] + [ text label + ] + ] + ] + in + div + [ class cfg.mainClasses + , class "flex flex-col sm:flex-row " + ] + [ div [ class "flex" ] + [ div + [ class S.border + , class S.qrCode + ] + [ qrCodeView texts appUrl + ] + ] + , div + [ class "flex flex-col ml-3 pr-2" + + -- hack for the qr code that is 265px + , style "max-width" "calc(100% - 265px)" + ] + [ div [ class "font-medium text-2xl" ] + [ text <| Maybe.withDefault texts.noName share.name + ] + , div [ class "my-2" ] + [ div [ class "flex flex-row" ] + [ a + [ class S.secondaryBasicButtonPlain + , class "rounded-l border text-sm px-4 py-2" + , title texts.copyToClipboard + , href "#" + , Tuple.second clipboard + |> String.dropLeft 1 + |> id + , attribute "data-clipboard-target" ("#" ++ Tuple.first clipboard) + ] + [ i [ class "fa fa-copy" ] [] + ] + , a + [ class S.secondaryBasicButtonPlain + , class "px-4 py-2 border-0 border-t border-b border-r text-sm" + , href appUrl + , target "_blank" + , title texts.openInNewTab + ] + [ i [ class "fa fa-external-link-alt" ] [] + ] + , div + [ id (Tuple.first clipboard) + , class styleUrl + ] + [ text appUrl + ] + ] + ] + , div [ class "text-lg flex flex-col" ] + [ infoLine False "fa fa-calendar" texts.publishUntil (texts.date share.publishUntil) + , infoLine False + (if share.password then + "fa fa-lock" + + else + "fa fa-lock-open" + ) + texts.passwordProtected + (if share.password then + texts.basics.yes + + else + texts.basics.no + ) + , infoLine + (not cfg.showAccessData) + "fa fa-eye" + texts.views + (String.fromInt share.views) + , infoLine + (not cfg.showAccessData) + "fa fa-calendar-check font-thin" + texts.lastAccess + (Maybe.map texts.date share.lastAccess |> Maybe.withDefault "-") + ] + ] + ] + + +viewExpired : ViewSettings -> Texts -> ShareDetail -> Html msg +viewExpired cfg texts share = + div [ class S.warnMessage ] + [ text texts.expiredInfo ] + + +viewDisabled : ViewSettings -> Texts -> ShareDetail -> Html msg +viewDisabled cfg texts share = + div [ class S.warnMessage ] + [ text texts.disabledInfo ] + + +qrCodeView : Texts -> String -> Html msg +qrCodeView texts message = + QRCode.encode message + |> Result.map QRCode.toSvg + |> Result.withDefault + (Html.text texts.qrCodeError) diff --git a/modules/webapp/src/main/elm/Messages/Comp/PublishItems.elm b/modules/webapp/src/main/elm/Messages/Comp/PublishItems.elm new file mode 100644 index 00000000..269f68ef --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/PublishItems.elm @@ -0,0 +1,82 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.PublishItems exposing + ( Texts + , de + , gb + ) + +import Http +import Messages.Basics +import Messages.Comp.HttpError +import Messages.Comp.ShareForm +import Messages.Comp.ShareView +import Messages.DateFormat +import Messages.UiLanguage + + +type alias Texts = + { basics : Messages.Basics.Texts + , httpError : Http.Error -> String + , shareForm : Messages.Comp.ShareForm.Texts + , shareView : Messages.Comp.ShareView.Texts + , title : String + , infoText : String + , formatDateLong : Int -> String + , formatDateShort : Int -> String + , submitPublish : String + , cancelPublish : String + , submitPublishTitle : String + , cancelPublishTitle : String + , publishSuccessful : String + , publishInProcess : String + , correctFormErrors : String + , doneLabel : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , httpError = Messages.Comp.HttpError.gb + , shareForm = Messages.Comp.ShareForm.gb + , shareView = Messages.Comp.ShareView.gb + , title = "Publish Items" + , infoText = "Publishing items creates a cryptic link, which can be used by everyone to see the selected documents. This link cannot be guessed, but is public! It exists for a certain amount of time and can be further protected using a password." + , formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.English + , formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.English + , submitPublish = "Publish" + , submitPublishTitle = "Publish the documents now" + , cancelPublish = "Cancel" + , cancelPublishTitle = "Back to select view" + , publishSuccessful = "Items published successfully" + , publishInProcess = "Items are published …" + , correctFormErrors = "Please correct the errors in the form." + , doneLabel = "Done" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , httpError = Messages.Comp.HttpError.de + , shareForm = Messages.Comp.ShareForm.de + , shareView = Messages.Comp.ShareView.de + , title = "Dokumente publizieren" + , infoText = "Beim Publizieren der Dokumente wird ein kryptischer Link erzeugt, mit welchem jeder die dahinter publizierten Dokumente einsehen kann. Dieser Link kann nicht erraten werden, ist aber öffentlich. Er ist zeitlich begrenzt und kann zusätzlich mit einem Passwort geschützt werden." + , formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.German + , formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.German + , submitPublish = "Publizieren" + , submitPublishTitle = "Dokumente jetzt publizieren" + , cancelPublish = "Abbrechen" + , cancelPublishTitle = "Zurück zur Auswahl" + , publishSuccessful = "Die Dokumente wurden erfolgreich publiziert." + , publishInProcess = "Dokumente werden publiziert…" + , correctFormErrors = "Bitte korrigiere die Fehler im Formular." + , doneLabel = "Fertig" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm index 66b27e6e..c415d3c8 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm @@ -16,12 +16,14 @@ import Messages.Basics import Messages.Comp.HttpError import Messages.Comp.ShareForm import Messages.Comp.ShareTable +import Messages.Comp.ShareView type alias Texts = { basics : Messages.Basics.Texts , shareTable : Messages.Comp.ShareTable.Texts , shareForm : Messages.Comp.ShareForm.Texts + , shareView : Messages.Comp.ShareView.Texts , httpError : Http.Error -> String , newShare : String , copyToClipboard : String @@ -33,6 +35,7 @@ type alias Texts = , errorGeneratingQR : String , correctFormErrors : String , noName : String + , shareInformation : String } @@ -42,6 +45,7 @@ gb = , httpError = Messages.Comp.HttpError.gb , shareTable = Messages.Comp.ShareTable.gb , shareForm = Messages.Comp.ShareForm.gb + , shareView = Messages.Comp.ShareView.gb , newShare = "New share" , copyToClipboard = "Copy to clipboard" , openInNewTab = "Open in new tab/window" @@ -52,6 +56,7 @@ gb = , errorGeneratingQR = "Error generating QR Code" , correctFormErrors = "Please correct the errors in the form." , noName = "No Name" + , shareInformation = "Share Information" } @@ -60,6 +65,7 @@ de = { basics = Messages.Basics.de , shareTable = Messages.Comp.ShareTable.de , shareForm = Messages.Comp.ShareForm.de + , shareView = Messages.Comp.ShareView.de , httpError = Messages.Comp.HttpError.de , newShare = "Neue Freigabe" , copyToClipboard = "In die Zwischenablage kopieren" @@ -71,4 +77,5 @@ de = , errorGeneratingQR = "Fehler beim Generieren des QR-Code" , correctFormErrors = "Bitte korrigiere die Fehler im Formular." , noName = "Ohne Name" + , shareInformation = "Informationen zur Freigabe" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm index 7b68fcc4..5b87e47e 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm @@ -19,7 +19,7 @@ import Messages.UiLanguage type alias Texts = { basics : Messages.Basics.Texts , formatDateTime : Int -> String - , enabled : String + , active : String , publishUntil : String } @@ -28,7 +28,7 @@ gb : Texts gb = { basics = Messages.Basics.gb , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.English - , enabled = "Enabled" + , active = "Active" , publishUntil = "Publish Until" } @@ -37,6 +37,6 @@ de : Texts de = { basics = Messages.Basics.de , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.German - , enabled = "Aktiv" + , active = "Aktiv" , publishUntil = "Publiziert bis" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareView.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareView.elm new file mode 100644 index 00000000..86f15c07 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareView.elm @@ -0,0 +1,66 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.ShareView exposing + ( Texts + , de + , gb + ) + +import Messages.Basics +import Messages.DateFormat as DF +import Messages.UiLanguage + + +type alias Texts = + { basics : Messages.Basics.Texts + , date : Int -> String + , qrCodeError : String + , expiredInfo : String + , disabledInfo : String + , noName : String + , copyToClipboard : String + , openInNewTab : String + , publishUntil : String + , passwordProtected : String + , views : String + , lastAccess : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , date = DF.formatDateLong Messages.UiLanguage.English + , qrCodeError = "Error generating QR Code." + , expiredInfo = "This share has expired." + , disabledInfo = "This share is disabled." + , noName = "No Name" + , copyToClipboard = "Copy to clipboard" + , openInNewTab = "Open in new tab/window" + , publishUntil = "Published Until" + , passwordProtected = "Password protected" + , views = "Views" + , lastAccess = "Last Access" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , date = DF.formatDateLong Messages.UiLanguage.German + , qrCodeError = "Fehler beim Erzeugen des QR-Codes." + , expiredInfo = "Diese Freigabe ist abgelaufen." + , disabledInfo = "Diese Freigae ist nicht aktiv." + , noName = "Ohne Name" + , copyToClipboard = "In die Zwischenablage kopieren" + , openInNewTab = "Im neuen Tab/Fenster öffnen" + , publishUntil = "Publiziert bis" + , passwordProtected = "Passwordgeschützt" + , views = "Aufrufe" + , lastAccess = "Letzter Zugriff" + } diff --git a/modules/webapp/src/main/elm/Messages/Page/Home.elm b/modules/webapp/src/main/elm/Messages/Page/Home.elm index f51c5202..dada7a27 100644 --- a/modules/webapp/src/main/elm/Messages/Page/Home.elm +++ b/modules/webapp/src/main/elm/Messages/Page/Home.elm @@ -14,6 +14,7 @@ module Messages.Page.Home exposing import Messages.Basics import Messages.Comp.ItemCardList import Messages.Comp.ItemMerge +import Messages.Comp.PublishItems import Messages.Comp.SearchStatsView import Messages.Page.HomeSideMenu @@ -24,6 +25,7 @@ type alias Texts = , searchStatsView : Messages.Comp.SearchStatsView.Texts , sideMenu : Messages.Page.HomeSideMenu.Texts , itemMerge : Messages.Comp.ItemMerge.Texts + , publishItems : Messages.Comp.PublishItems.Texts , contentSearch : String , searchInNames : String , selectModeTitle : String @@ -42,6 +44,11 @@ type alias Texts = , resetSearchForm : String , exitSelectMode : String , mergeItemsTitle : Int -> String + , publishItemsTitle : Int -> String + , publishCurrentQueryTitle : String + , nothingSelectedToShare : String + , loadMore : String + , thatsAll : String } @@ -52,6 +59,7 @@ gb = , searchStatsView = Messages.Comp.SearchStatsView.gb , sideMenu = Messages.Page.HomeSideMenu.gb , itemMerge = Messages.Comp.ItemMerge.gb + , publishItems = Messages.Comp.PublishItems.gb , contentSearch = "Content search…" , searchInNames = "Search in names…" , selectModeTitle = "Select Mode" @@ -70,6 +78,11 @@ gb = , resetSearchForm = "Reset search form" , exitSelectMode = "Exit Select Mode" , mergeItemsTitle = \n -> "Merge " ++ String.fromInt n ++ " selected items" + , publishItemsTitle = \n -> "Publish " ++ String.fromInt n ++ " selected items" + , publishCurrentQueryTitle = "Publish current results" + , nothingSelectedToShare = "Sharing everything doesn't work. You need to apply some criteria." + , loadMore = "Load more…" + , thatsAll = "That's all" } @@ -80,6 +93,7 @@ de = , searchStatsView = Messages.Comp.SearchStatsView.de , sideMenu = Messages.Page.HomeSideMenu.de , itemMerge = Messages.Comp.ItemMerge.de + , publishItems = Messages.Comp.PublishItems.de , contentSearch = "Volltextsuche…" , searchInNames = "Suche in Namen…" , selectModeTitle = "Auswahlmodus" @@ -98,4 +112,9 @@ de = , resetSearchForm = "Suchformular zurücksetzen" , exitSelectMode = "Auswahlmodus verlassen" , mergeItemsTitle = \n -> String.fromInt n ++ " gewählte Dokumente zusammenführen" + , publishItemsTitle = \n -> String.fromInt n ++ " gewählte Dokumente publizieren" + , publishCurrentQueryTitle = "Aktuelle Ansicht publizieren" + , nothingSelectedToShare = "Alles kann nicht geteilt werden; es muss etwas gesucht werden." + , loadMore = "Mehr laden…" + , thatsAll = "Mehr gibt es nicht" } diff --git a/modules/webapp/src/main/elm/Page/Home/Data.elm b/modules/webapp/src/main/elm/Page/Home/Data.elm index cb60492d..c4b65df0 100644 --- a/modules/webapp/src/main/elm/Page/Home/Data.elm +++ b/modules/webapp/src/main/elm/Page/Home/Data.elm @@ -14,6 +14,7 @@ module Page.Home.Data exposing , SelectActionMode(..) , SelectViewModel , ViewMode(..) + , createQuery , doSearchCmd , editActive , init @@ -36,6 +37,7 @@ import Comp.ItemDetail.MultiEditMenu exposing (SaveNameState(..)) import Comp.ItemMerge import Comp.LinkTarget exposing (LinkTarget) import Comp.PowerSearchInput +import Comp.PublishItems import Comp.SearchMenu import Data.Flags exposing (Flags) import Data.ItemNav exposing (ItemNav) @@ -79,6 +81,7 @@ type alias SelectViewModel = , confirmModal : Maybe ConfirmModalValue , editModel : Comp.ItemDetail.MultiEditMenu.Model , mergeModel : Comp.ItemMerge.Model + , publishModel : Comp.PublishItems.Model , saveNameState : SaveNameState , saveCustomFieldState : Set String } @@ -91,6 +94,7 @@ initSelectViewModel = , confirmModal = Nothing , editModel = Comp.ItemDetail.MultiEditMenu.init , mergeModel = Comp.ItemMerge.init [] + , publishModel = Tuple.first Comp.PublishItems.init , saveNameState = SaveSuccess , saveCustomFieldState = Set.empty } @@ -100,6 +104,7 @@ type ViewMode = SimpleView | SearchView | SelectView SelectViewModel + | PublishView Comp.PublishItems.Model init : Flags -> ViewMode -> Model @@ -143,6 +148,9 @@ menuCollapsed model = SelectView _ -> False + PublishView _ -> + False + selectActive : Model -> Bool selectActive model = @@ -153,6 +161,9 @@ selectActive model = SearchView -> False + PublishView _ -> + False + SelectView _ -> True @@ -166,6 +177,9 @@ editActive model = SearchView -> False + PublishView _ -> + False + SelectView svm -> svm.action == EditSelected @@ -211,6 +225,10 @@ type Msg | RemoveItem String | MergeSelectedItems | MergeItemsMsg Comp.ItemMerge.Msg + | PublishSelectedItems + | PublishItemsMsg Comp.PublishItems.Msg + | TogglePublishCurrentQueryView + | PublishViewMsg Comp.PublishItems.Msg type SearchType @@ -225,6 +243,7 @@ type SelectActionMode | ReprocessSelected | RestoreSelected | MergeSelected + | PublishSelected type alias SearchParam = @@ -251,10 +270,7 @@ doSearchDefaultCmd param model = let smask = Q.request model.searchMenuModel.searchMode <| - Q.and - [ Comp.SearchMenu.getItemQuery model.searchMenuModel - , Maybe.map Q.Fragment model.powerSearchInput.input - ] + createQuery model mask = { smask @@ -272,6 +288,14 @@ doSearchDefaultCmd param model = Api.itemSearch param.flags mask ItemSearchAddResp +createQuery : Model -> Maybe Q.ItemQuery +createQuery model = + Q.and + [ Comp.SearchMenu.getItemQuery model.searchMenuModel + , Maybe.map Q.Fragment model.powerSearchInput.input + ] + + resultsBelowLimit : UiSettings -> Model -> Bool resultsBelowLimit settings model = let diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index 2ba19438..00ebfe9a 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -19,6 +19,7 @@ import Comp.ItemDetail.MultiEditMenu exposing (SaveNameState(..)) import Comp.ItemMerge import Comp.LinkTarget exposing (LinkTarget) import Comp.PowerSearchInput +import Comp.PublishItems import Comp.SearchMenu import Data.Flags exposing (Flags) import Data.ItemQuery as Q @@ -237,6 +238,9 @@ update mId key flags settings msg model = SelectView _ -> SimpleView + + PublishView q -> + PublishView q in withSub ( { model | viewMode = nextView } @@ -255,6 +259,9 @@ update mId key flags settings msg model = SelectView _ -> ( SearchView, Cmd.none ) + + PublishView q -> + ( PublishView q, Cmd.none ) in withSub ( { model @@ -620,6 +627,85 @@ update mId key flags settings msg model = _ -> noSub ( model, Cmd.none ) + PublishSelectedItems -> + case model.viewMode of + SelectView svm -> + if svm.action == PublishSelected then + let + ( mm, mc ) = + Comp.PublishItems.init + in + noSub + ( { model + | viewMode = + SelectView + { svm + | action = NoneAction + , publishModel = mm + } + } + , Cmd.map PublishItemsMsg mc + ) + + else if svm.ids == Set.empty then + noSub ( model, Cmd.none ) + + else + let + ( mm, mc ) = + Comp.PublishItems.initQuery + (Q.ItemIdIn (Set.toList svm.ids)) + in + noSub + ( { model + | viewMode = + SelectView + { svm + | action = PublishSelected + , publishModel = mm + } + } + , Cmd.map PublishItemsMsg mc + ) + + _ -> + noSub ( model, Cmd.none ) + + PublishItemsMsg lmsg -> + case model.viewMode of + SelectView svm -> + let + result = + Comp.PublishItems.update flags lmsg svm.publishModel + + nextView = + case result.outcome of + Comp.PublishItems.OutcomeDone -> + SelectView { svm | action = NoneAction } + + Comp.PublishItems.OutcomeInProgress -> + SelectView { svm | publishModel = result.model } + + model_ = + { model | viewMode = nextView } + in + if result.outcome == Comp.PublishItems.OutcomeDone then + update mId + key + flags + settings + (DoSearch model.searchTypeDropdownValue) + model_ + + else + noSub + ( model_ + , Cmd.map PublishItemsMsg result.cmd + ) + + _ -> + noSub ( model, Cmd.none ) + EditMenuMsg lmsg -> case model.viewMode of SelectView svm -> @@ -786,6 +872,38 @@ update mId key flags settings msg model = RemoveItem id -> update mId key flags settings (ItemCardListMsg (Comp.ItemCardList.RemoveItem id)) model + TogglePublishCurrentQueryView -> + case createQuery model of + Just q -> + let + ( pm, pc ) = + Comp.PublishItems.initQuery q + in + noSub ( { model | viewMode = PublishView pm }, Cmd.map PublishViewMsg pc ) + + Nothing -> + noSub ( model, Cmd.none ) + + PublishViewMsg lmsg -> + case model.viewMode of + PublishView inPM -> + let + result = + Comp.PublishItems.update flags lmsg inPM + in + case result.outcome of + Comp.PublishItems.OutcomeInProgress -> + noSub + ( { model | viewMode = PublishView result.model } + , Cmd.map PublishViewMsg result.cmd + ) + + Comp.PublishItems.OutcomeDone -> + noSub ( { model | viewMode = SearchView }, Cmd.none ) + + _ -> + noSub ( model, Cmd.none ) + --- Helpers diff --git a/modules/webapp/src/main/elm/Page/Home/View2.elm b/modules/webapp/src/main/elm/Page/Home/View2.elm index 40dd3b3c..63f39957 100644 --- a/modules/webapp/src/main/elm/Page/Home/View2.elm +++ b/modules/webapp/src/main/elm/Page/Home/View2.elm @@ -13,9 +13,12 @@ import Comp.ItemCardList import Comp.ItemMerge import Comp.MenuBar as MB import Comp.PowerSearchInput +import Comp.PublishItems import Comp.SearchMenu import Comp.SearchStatsView import Data.Flags exposing (Flags) +import Data.Icons as Icons +import Data.ItemQuery as Q import Data.ItemSelection import Data.SearchMode import Data.UiSettings exposing (UiSettings) @@ -63,29 +66,52 @@ viewContent texts flags settings model = mainView : Texts -> Flags -> UiSettings -> Model -> List (Html Msg) mainView texts flags settings model = let - mergeView = + otherView = case model.viewMode of SelectView svm -> case svm.action of MergeSelected -> - Just svm + Just + [ div [ class "sm:relative mb-2" ] + (itemMergeView texts settings svm) + ] + + PublishSelected -> + Just + [ div [ class "sm:relative mb-2" ] + (itemPublishView texts flags svm) + ] _ -> Nothing - _ -> + PublishView pm -> + Just + [ div [ class "sm:relative mb-2" ] + (publishResults texts flags model pm) + ] + + SimpleView -> + Nothing + + SearchView -> Nothing in - case mergeView of - Just svm -> - [ div [ class "sm:relative mb-2" ] - (itemMergeView texts settings svm) - ] + case otherView of + Just body -> + body Nothing -> itemCardList texts flags settings model +itemPublishView : Texts -> Flags -> SelectViewModel -> List (Html Msg) +itemPublishView texts flags svm = + [ Html.map PublishItemsMsg + (Comp.PublishItems.view texts.publishItems flags svm.publishModel) + ] + + itemMergeView : Texts -> UiSettings -> SelectViewModel -> List (Html Msg) itemMergeView texts settings svm = [ Html.map MergeItemsMsg @@ -93,6 +119,13 @@ itemMergeView texts settings svm = ] +publishResults : Texts -> Flags -> Model -> Comp.PublishItems.Model -> List (Html Msg) +publishResults texts flags model pm = + [ Html.map PublishViewMsg + (Comp.PublishItems.view texts.publishItems flags pm) + ] + + confirmModal : Texts -> Model -> List (Html Msg) confirmModal texts model = let @@ -148,6 +181,9 @@ itemsBar texts flags settings model = SelectView svm -> [ editMenuBar texts model svm ] + PublishView query -> + [ defaultMenuBar texts flags settings model ] + defaultMenuBar : Texts -> Flags -> UiSettings -> Model -> Html Msg defaultMenuBar texts flags settings model = @@ -215,6 +251,25 @@ defaultMenuBar texts flags settings model = MB.view { end = [ MB.CustomElement <| + B.secondaryBasicButton + { label = "" + , icon = Icons.share + , disabled = createQuery model == Nothing + , handler = onClick TogglePublishCurrentQueryView + , attrs = + [ title <| + if createQuery model == Nothing then + texts.nothingSelectedToShare + + else + texts.publishCurrentQueryTitle + , classList + [ ( btnStyle, True ) + ] + , href "#" + ] + } + , MB.CustomElement <| B.secondaryBasicButton { label = "" , icon = @@ -332,6 +387,17 @@ editMenuBar texts model svm = , ( "hidden", model.searchMenuModel.searchMode == Data.SearchMode.Trashed ) ] } + , MB.CustomButton + { tagger = PublishSelectedItems + , label = "" + , icon = Just Icons.share + , title = texts.publishItemsTitle selectCount + , inputClass = + [ ( btnStyle, True ) + , ( "bg-gray-200 dark:bg-bluegray-600", svm.action == PublishSelected ) + , ( "hidden", model.searchMenuModel.searchMode == Data.SearchMode.Trashed ) + ] + } ] , end = [ MB.CustomButton @@ -413,12 +479,12 @@ itemCardList texts _ settings model = settings model.itemListModel ) - , loadMore settings model + , loadMore texts settings model ] -loadMore : UiSettings -> Model -> Html Msg -loadMore settings model = +loadMore : Texts -> UiSettings -> Model -> Html Msg +loadMore texts settings model = let inactive = not model.moreAvailable || model.moreInProgress || model.searchInProgress @@ -430,10 +496,10 @@ loadMore settings model = [ B.secondaryBasicButton { label = if model.moreAvailable then - "Load more…" + texts.loadMore else - "That's all" + texts.thatsAll , icon = if model.moreInProgress then "fa fa-circle-notch animate-spin" diff --git a/modules/webapp/src/main/elm/Styles.elm b/modules/webapp/src/main/elm/Styles.elm index c7686ffa..f33ba301 100644 --- a/modules/webapp/src/main/elm/Styles.elm +++ b/modules/webapp/src/main/elm/Styles.elm @@ -48,6 +48,11 @@ errorMessage = " border border-red-600 bg-red-50 text-red-600 dark:border-orange-800 dark:bg-orange-300 dark:text-orange-800 px-2 py-2 rounded " +errorText : String +errorText = + " text-red-600 dark:text-orange-800 " + + warnMessage : String warnMessage = warnMessageColors ++ " border dark:bg-opacity-25 px-2 py-2 rounded " From 97922340d9b8799c3da84ea495281ea10f217d4e Mon Sep 17 00:00:00 2001 From: eikek Date: Sun, 3 Oct 2021 01:32:56 +0200 Subject: [PATCH 06/37] Share page skeleton --- modules/webapp/src/main/elm/App/Data.elm | 8 ++++ modules/webapp/src/main/elm/App/Update.elm | 25 +++++++++++ modules/webapp/src/main/elm/App/View2.elm | 41 +++++++++++++++---- modules/webapp/src/main/elm/Data/Flags.elm | 20 +++++++++ modules/webapp/src/main/elm/Messages.elm | 4 ++ .../src/main/elm/Messages/Page/Share.elm | 22 ++++++++++ modules/webapp/src/main/elm/Page.elm | 25 +++++++++++ .../webapp/src/main/elm/Page/Share/Data.elm | 32 +++++++++++++++ .../webapp/src/main/elm/Page/Share/Update.elm | 23 +++++++++++ .../webapp/src/main/elm/Page/Share/View.elm | 38 +++++++++++++++++ 10 files changed, 231 insertions(+), 7 deletions(-) create mode 100644 modules/webapp/src/main/elm/Messages/Page/Share.elm create mode 100644 modules/webapp/src/main/elm/Page/Share/Data.elm create mode 100644 modules/webapp/src/main/elm/Page/Share/Update.elm create mode 100644 modules/webapp/src/main/elm/Page/Share/View.elm diff --git a/modules/webapp/src/main/elm/App/Data.elm b/modules/webapp/src/main/elm/App/Data.elm index 055eb456..36713a54 100644 --- a/modules/webapp/src/main/elm/App/Data.elm +++ b/modules/webapp/src/main/elm/App/Data.elm @@ -32,6 +32,7 @@ import Page.ManageData.Data import Page.NewInvite.Data import Page.Queue.Data import Page.Register.Data +import Page.Share.Data import Page.Upload.Data import Page.UserSettings.Data import Url exposing (Url) @@ -52,6 +53,7 @@ type alias Model = , uploadModel : Page.Upload.Data.Model , newInviteModel : Page.NewInvite.Data.Model , itemDetailModel : Page.ItemDetail.Data.Model + , shareModel : Page.Share.Data.Model , navMenuOpen : Bool , userMenuOpen : Bool , subs : Sub Msg @@ -85,6 +87,9 @@ init key url flags_ settings = ( loginm, loginc ) = Page.Login.Data.init flags (Page.loginPageReferrer page) + ( shm, shc ) = + Page.Share.Data.init (Page.shareId page) flags + homeViewMode = if settings.searchMenuVisible then Page.Home.Data.SearchView @@ -106,6 +111,7 @@ init key url flags_ settings = , uploadModel = Page.Upload.Data.emptyModel , newInviteModel = Page.NewInvite.Data.emptyModel , itemDetailModel = Page.ItemDetail.Data.emptyModel + , shareModel = shm , navMenuOpen = False , userMenuOpen = False , subs = Sub.none @@ -120,6 +126,7 @@ init key url flags_ settings = , Cmd.map ManageDataMsg mdc , Cmd.map CollSettingsMsg csc , Cmd.map LoginMsg loginc + , Cmd.map ShareMsg shc ] ) @@ -162,6 +169,7 @@ type Msg | UploadMsg Page.Upload.Data.Msg | NewInviteMsg Page.NewInvite.Data.Msg | ItemDetailMsg Page.ItemDetail.Data.Msg + | ShareMsg Page.Share.Data.Msg | Logout | LogoutResp (Result Http.Error ()) | SessionCheckResp (Result Http.Error AuthResult) diff --git a/modules/webapp/src/main/elm/App/Update.elm b/modules/webapp/src/main/elm/App/Update.elm index bd760a70..5408c581 100644 --- a/modules/webapp/src/main/elm/App/Update.elm +++ b/modules/webapp/src/main/elm/App/Update.elm @@ -34,6 +34,8 @@ import Page.Queue.Data import Page.Queue.Update import Page.Register.Data import Page.Register.Update +import Page.Share.Data +import Page.Share.Update import Page.Upload.Data import Page.Upload.Update import Page.UserSettings.Data @@ -114,6 +116,9 @@ updateWithSub msg model = HomeMsg lm -> updateHome lm model + ShareMsg lm -> + updateShare lm model + LoginMsg lm -> updateLogin lm model @@ -313,6 +318,23 @@ applyClientSettings model settings = { model | uiSettings = settings } +updateShare : Page.Share.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +updateShare lmsg model = + case Page.shareId model.page of + Just id -> + let + result = + Page.Share.Update.update model.flags id lmsg model.shareModel + in + ( { model | shareModel = result.model } + , Cmd.map ShareMsg result.cmd + , Sub.map ShareMsg result.sub + ) + + Nothing -> + ( model, Cmd.none, Sub.none ) + + updateItemDetail : Page.ItemDetail.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) updateItemDetail lmsg model = let @@ -568,3 +590,6 @@ initPage model_ page = , updateQueue Page.Queue.Data.StopRefresh ] model + + SharePage _ -> + ( model, Cmd.none, Sub.none ) diff --git a/modules/webapp/src/main/elm/App/View2.elm b/modules/webapp/src/main/elm/App/View2.elm index d2b277b7..d80be6e3 100644 --- a/modules/webapp/src/main/elm/App/View2.elm +++ b/modules/webapp/src/main/elm/App/View2.elm @@ -27,6 +27,7 @@ import Page.ManageData.View2 as ManageData import Page.NewInvite.View2 as NewInvite import Page.Queue.View2 as Queue import Page.Register.View2 as Register +import Page.Share.View as Share import Page.Upload.View2 as Upload import Page.UserSettings.View2 as UserSettings import Styles as S @@ -41,13 +42,9 @@ view model = topNavbar : Model -> Html Msg topNavbar model = - case model.flags.account of + case Data.Flags.getAccount model.flags of Just acc -> - if acc.success then - topNavUser acc model - - else - topNavAnon model + topNavUser acc model Nothing -> topNavAnon model @@ -86,7 +83,16 @@ topNavAnon model = [ id "top-nav" , class styleTopNav ] - [ headerNavItem model + [ B.genericButton + { label = "" + , icon = "fa fa-bars" + , handler = onClick ToggleSidebar + , disabled = not (Page.hasSidebar model.page) + , attrs = [ href "#" ] + , baseStyle = "font-bold inline-flex items-center px-4 py-2" + , activeStyle = "hover:bg-blue-200 dark:hover:bg-bluegray-800 w-12" + } + , headerNavItem model , div [ class "flex flex-grow justify-end" ] [ langMenu model , a @@ -157,6 +163,9 @@ mainContent model = ItemDetailPage id -> viewItemDetail texts id model + + SharePage id -> + viewShare texts id model ) @@ -411,6 +420,24 @@ dropdownMenu = " absolute right-0 bg-white dark:bg-bluegray-800 border dark:border-bluegray-700 dark:text-bluegray-300 shadow-lg opacity-1 transition duration-200 min-w-max " +viewShare : Messages -> String -> Model -> List (Html Msg) +viewShare texts shareId model = + [ Html.map ShareMsg + (Share.viewSidebar texts.share + model.sidebarVisible + model.flags + model.uiSettings + model.shareModel + ) + , Html.map ShareMsg + (Share.viewContent texts.share + model.flags + model.uiSettings + model.shareModel + ) + ] + + viewHome : Messages -> Model -> List (Html Msg) viewHome texts model = [ Html.map HomeMsg diff --git a/modules/webapp/src/main/elm/Data/Flags.elm b/modules/webapp/src/main/elm/Data/Flags.elm index ea23b52f..e605bd50 100644 --- a/modules/webapp/src/main/elm/Data/Flags.elm +++ b/modules/webapp/src/main/elm/Data/Flags.elm @@ -9,7 +9,9 @@ module Data.Flags exposing ( Config , Flags , accountString + , getAccount , getToken + , isAuthenticated , withAccount , withoutAccount ) @@ -43,6 +45,24 @@ type alias Flags = } +isAuthenticated : Flags -> Bool +isAuthenticated flags = + getAccount flags /= Nothing + + +getAccount : Flags -> Maybe AuthResult +getAccount flags = + Maybe.andThen + (\ar -> + if ar.success then + Just ar + + else + Nothing + ) + flags.account + + getToken : Flags -> Maybe String getToken flags = flags.account diff --git a/modules/webapp/src/main/elm/Messages.elm b/modules/webapp/src/main/elm/Messages.elm index a0671289..9809f1b4 100644 --- a/modules/webapp/src/main/elm/Messages.elm +++ b/modules/webapp/src/main/elm/Messages.elm @@ -21,6 +21,7 @@ import Messages.Page.ManageData import Messages.Page.NewInvite import Messages.Page.Queue import Messages.Page.Register +import Messages.Page.Share import Messages.Page.Upload import Messages.Page.UserSettings import Messages.UiLanguage exposing (UiLanguage(..)) @@ -44,6 +45,7 @@ type alias Messages = , userSettings : Messages.Page.UserSettings.Texts , manageData : Messages.Page.ManageData.Texts , home : Messages.Page.Home.Texts + , share : Messages.Page.Share.Texts } @@ -109,6 +111,7 @@ gb = , userSettings = Messages.Page.UserSettings.gb , manageData = Messages.Page.ManageData.gb , home = Messages.Page.Home.gb + , share = Messages.Page.Share.gb } @@ -129,4 +132,5 @@ de = , userSettings = Messages.Page.UserSettings.de , manageData = Messages.Page.ManageData.de , home = Messages.Page.Home.de + , share = Messages.Page.Share.de } diff --git a/modules/webapp/src/main/elm/Messages/Page/Share.elm b/modules/webapp/src/main/elm/Messages/Page/Share.elm new file mode 100644 index 00000000..b6044543 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Page/Share.elm @@ -0,0 +1,22 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Page.Share exposing (..) + + +type alias Texts = + {} + + +gb : Texts +gb = + {} + + +de : Texts +de = + {} diff --git a/modules/webapp/src/main/elm/Page.elm b/modules/webapp/src/main/elm/Page.elm index 92ff29d9..667fe7aa 100644 --- a/modules/webapp/src/main/elm/Page.elm +++ b/modules/webapp/src/main/elm/Page.elm @@ -21,6 +21,7 @@ module Page exposing , pageName , pageToString , set + , shareId , uploadId ) @@ -59,6 +60,7 @@ type Page | UploadPage (Maybe String) | NewInvitePage | ItemDetailPage String + | SharePage String isSecured : Page -> Bool @@ -94,6 +96,9 @@ isSecured page = ItemDetailPage _ -> True + SharePage _ -> + False + {-| Currently, all secured pages have a sidebar, except UploadPage. -} @@ -103,6 +108,9 @@ hasSidebar page = UploadPage _ -> False + SharePage _ -> + True + _ -> isSecured page @@ -160,6 +168,9 @@ pageName page = ItemDetailPage _ -> "Item" + SharePage _ -> + "Share" + loginPageReferrer : Page -> LoginData loginPageReferrer page = @@ -171,6 +182,16 @@ loginPageReferrer page = emptyLoginData +shareId : Page -> Maybe String +shareId page = + case page of + SharePage id -> + Just id + + _ -> + Nothing + + uploadId : Page -> Maybe String uploadId page = case page of @@ -224,6 +245,9 @@ pageToString page = ItemDetailPage id -> "/app/item/" ++ id + SharePage id -> + "/app/share/" ++ id + pageFromString : String -> Maybe Page pageFromString str = @@ -280,6 +304,7 @@ parser = , Parser.map (UploadPage Nothing) (s pathPrefix s "upload") , Parser.map NewInvitePage (s pathPrefix s "newinvite") , Parser.map ItemDetailPage (s pathPrefix s "item" string) + , Parser.map SharePage (s pathPrefix s "share" string) ] diff --git a/modules/webapp/src/main/elm/Page/Share/Data.elm b/modules/webapp/src/main/elm/Page/Share/Data.elm new file mode 100644 index 00000000..a0aa5f76 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Share/Data.elm @@ -0,0 +1,32 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Page.Share.Data exposing (Model, Msg, init) + +import Data.Flags exposing (Flags) + + +type alias Model = + {} + + +init : Maybe String -> Flags -> ( Model, Cmd Msg ) +init shareId flags = + case shareId of + Just id -> + let + _ = + Debug.log "share" id + in + ( {}, Cmd.none ) + + Nothing -> + ( {}, Cmd.none ) + + +type Msg + = Msg diff --git a/modules/webapp/src/main/elm/Page/Share/Update.elm b/modules/webapp/src/main/elm/Page/Share/Update.elm new file mode 100644 index 00000000..0f1dadbb --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Share/Update.elm @@ -0,0 +1,23 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Page.Share.Update exposing (UpdateResult, update) + +import Data.Flags exposing (Flags) +import Page.Share.Data exposing (..) + + +type alias UpdateResult = + { model : Model + , cmd : Cmd Msg + , sub : Sub Msg + } + + +update : Flags -> String -> Msg -> Model -> UpdateResult +update flags shareId msg model = + UpdateResult model Cmd.none Sub.none diff --git a/modules/webapp/src/main/elm/Page/Share/View.elm b/modules/webapp/src/main/elm/Page/Share/View.elm new file mode 100644 index 00000000..0d5cf016 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Share/View.elm @@ -0,0 +1,38 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Page.Share.View exposing (viewContent, viewSidebar) + +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Messages.Page.Share exposing (Texts) +import Page.Share.Data exposing (..) +import Styles as S + + +viewSidebar : Texts -> Bool -> Flags -> UiSettings -> Model -> Html Msg +viewSidebar _ visible _ _ _ = + div + [ id "sidebar" + , classList [ ( "hidden", not visible ) ] + ] + [ text "sidebar" ] + + +viewContent : Texts -> Flags -> UiSettings -> Model -> Html Msg +viewContent texts flags _ model = + div + [ id "content" + , class "h-full flex flex-col" + , class S.content + ] + [ h1 [ class S.header1 ] + [ text "Share Page!" + ] + ] From f4596db63da0c28396b57a88146a330997890766 Mon Sep 17 00:00:00 2001 From: eikek Date: Sun, 3 Oct 2021 23:56:59 +0200 Subject: [PATCH 07/37] Authorize share access --- .../docspell/backend/auth/ShareToken.scala | 52 +++++++++++++ .../docspell/backend/auth/TokenUtil.scala | 15 +++- .../scala/docspell/backend/ops/OShare.scala | 74 +++++++++++++++++++ .../main/scala/docspell/common/Password.scala | 11 +++ .../scala/docspell/common/Timestamp.scala | 3 + .../src/main/resources/docspell-openapi.yml | 63 ++++++++++++++++ .../docspell/restserver/RestServer.scala | 19 ++++- .../restserver/auth/ShareCookieData.scala | 69 +++++++++++++++++ .../{AdminRoutes.scala => AdminAuth.scala} | 6 +- .../restserver/routes/ShareAuth.scala | 73 ++++++++++++++++++ .../restserver/routes/ShareRoutes.scala | 32 +++++++- .../restserver/routes/ShareSearchRoutes.scala | 29 ++++++++ .../scala/docspell/store/records/RShare.scala | 22 ++++++ 13 files changed, 457 insertions(+), 11 deletions(-) create mode 100644 modules/backend/src/main/scala/docspell/backend/auth/ShareToken.scala create mode 100644 modules/restserver/src/main/scala/docspell/restserver/auth/ShareCookieData.scala rename modules/restserver/src/main/scala/docspell/restserver/routes/{AdminRoutes.scala => AdminAuth.scala} (92%) create mode 100644 modules/restserver/src/main/scala/docspell/restserver/routes/ShareAuth.scala create mode 100644 modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala diff --git a/modules/backend/src/main/scala/docspell/backend/auth/ShareToken.scala b/modules/backend/src/main/scala/docspell/backend/auth/ShareToken.scala new file mode 100644 index 00000000..c26124d6 --- /dev/null +++ b/modules/backend/src/main/scala/docspell/backend/auth/ShareToken.scala @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.backend.auth + +import cats.effect._ +import cats.implicits._ + +import docspell.backend.Common +import docspell.common.{Ident, Timestamp} + +import scodec.bits.ByteVector + +/** Can be used as an authenticator to access data behind a share. */ +final case class ShareToken(created: Timestamp, id: Ident, salt: String, sig: String) { + def asString = s"${created.toMillis}-${TokenUtil.b64enc(id.id)}-$salt-$sig" + + def sigValid(key: ByteVector): Boolean = { + val newSig = TokenUtil.sign(this, key) + TokenUtil.constTimeEq(sig, newSig) + } + def sigInvalid(key: ByteVector): Boolean = + !sigValid(key) +} + +object ShareToken { + + def fromString(s: String): Either[String, ShareToken] = + s.split("-", 4) match { + case Array(ms, id, salt, sig) => + for { + created <- ms.toLongOption.toRight("Invalid timestamp") + idStr <- TokenUtil.b64dec(id).toRight("Cannot read authenticator data") + shareId <- Ident.fromString(idStr) + } yield ShareToken(Timestamp.ofMillis(created), shareId, salt, sig) + + case _ => + Left("Invalid authenticator") + } + + def create[F[_]: Sync](shareId: Ident, key: ByteVector): F[ShareToken] = + for { + now <- Timestamp.current[F] + salt <- Common.genSaltString[F] + cd = ShareToken(now, shareId, salt, "") + sig = TokenUtil.sign(cd, key) + } yield cd.copy(sig = sig) + +} diff --git a/modules/backend/src/main/scala/docspell/backend/auth/TokenUtil.scala b/modules/backend/src/main/scala/docspell/backend/auth/TokenUtil.scala index 7958ed0a..9bba4823 100644 --- a/modules/backend/src/main/scala/docspell/backend/auth/TokenUtil.scala +++ b/modules/backend/src/main/scala/docspell/backend/auth/TokenUtil.scala @@ -18,17 +18,24 @@ private[auth] object TokenUtil { def sign(cd: RememberToken, key: ByteVector): String = { val raw = cd.nowMillis.toString + cd.rememberId.id + cd.salt - val mac = Mac.getInstance("HmacSHA1") - mac.init(new SecretKeySpec(key.toArray, "HmacSHA1")) - ByteVector.view(mac.doFinal(raw.getBytes(utf8))).toBase64 + signRaw(raw, key) } def sign(cd: AuthToken, key: ByteVector): String = { val raw = cd.nowMillis.toString + cd.account.asString + cd.requireSecondFactor + cd.salt + signRaw(raw, key) + } + + def sign(sd: ShareToken, key: ByteVector): String = { + val raw = s"${sd.created.toMillis}${sd.id.id}${sd.salt}" + signRaw(raw, key) + } + + private def signRaw(data: String, key: ByteVector): String = { val mac = Mac.getInstance("HmacSHA1") mac.init(new SecretKeySpec(key.toArray, "HmacSHA1")) - ByteVector.view(mac.doFinal(raw.getBytes(utf8))).toBase64 + ByteVector.view(mac.doFinal(data.getBytes(utf8))).toBase64 } def b64enc(s: String): String = diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index 68b86f11..005938a0 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -11,11 +11,15 @@ import cats.effect._ import cats.implicits._ import docspell.backend.PasswordCrypt +import docspell.backend.auth.ShareToken +import docspell.backend.ops.OShare.VerifyResult import docspell.common._ import docspell.query.ItemQuery import docspell.store.Store import docspell.store.records.RShare +import scodec.bits.ByteVector + trait OShare[F[_]] { def findAll(collective: Ident): F[List[RShare]] @@ -31,10 +35,32 @@ trait OShare[F[_]] { share: OShare.NewShare, removePassword: Boolean ): F[OShare.ChangeResult] + + def verify(key: ByteVector)(id: Ident, password: Option[Password]): F[VerifyResult] + def verifyToken(key: ByteVector)(token: String): F[VerifyResult] } object OShare { + sealed trait VerifyResult { + def toEither: Either[String, ShareToken] = + this match { + case VerifyResult.Success(token) => Right(token) + case _ => Left("Authentication failed.") + } + } + object VerifyResult { + case class Success(token: ShareToken) extends VerifyResult + case object NotFound extends VerifyResult + case object PasswordMismatch extends VerifyResult + case object InvalidToken extends VerifyResult + + def success(token: ShareToken): VerifyResult = Success(token) + def notFound: VerifyResult = NotFound + def passwordMismatch: VerifyResult = PasswordMismatch + def invalidToken: VerifyResult = InvalidToken + } + final case class NewShare( cid: Ident, name: Option[String], @@ -55,6 +81,8 @@ object OShare { def apply[F[_]: Async](store: Store[F]): OShare[F] = new OShare[F] { + private[this] val logger = Logger.log4s[F](org.log4s.getLogger) + def findAll(collective: Ident): F[List[RShare]] = store.transact(RShare.findAllByCollective(collective)) @@ -112,5 +140,51 @@ object OShare { def findOne(id: Ident, collective: Ident): OptionT[F, RShare] = RShare.findOne(id, collective).mapK(store.transform) + + def verify( + key: ByteVector + )(id: Ident, password: Option[Password]): F[VerifyResult] = + RShare + .findCurrentActive(id) + .mapK(store.transform) + .semiflatMap { share => + val pwCheck = + share.password.map(encPw => password.exists(PasswordCrypt.check(_, encPw))) + + // add the password (if existing) to the server secret key; this way the token + // invalidates when the user changes the password + val shareKey = + share.password.map(pw => key ++ pw.asByteVector).getOrElse(key) + + val token = ShareToken.create(id, shareKey) + pwCheck match { + case Some(true) => token.map(VerifyResult.success) + case None => token.map(VerifyResult.success) + case Some(false) => VerifyResult.passwordMismatch.pure[F] + } + } + .getOrElse(VerifyResult.notFound) + + def verifyToken(key: ByteVector)(token: String): F[VerifyResult] = + ShareToken.fromString(token) match { + case Right(st) => + RShare + .findActivePassword(st.id) + .mapK(store.transform) + .semiflatMap { password => + val shareKey = + password.map(pw => key ++ pw.asByteVector).getOrElse(key) + if (st.sigValid(shareKey)) VerifyResult.success(st).pure[F] + else + logger.info( + s"Signature failure for share: ${st.id.id}" + ) *> VerifyResult.invalidToken.pure[F] + } + .getOrElse(VerifyResult.notFound) + + case Left(err) => + logger.debug(s"Invalid session token: $err") *> + VerifyResult.invalidToken.pure[F] + } } } diff --git a/modules/common/src/main/scala/docspell/common/Password.scala b/modules/common/src/main/scala/docspell/common/Password.scala index da83364c..7c2daeb0 100644 --- a/modules/common/src/main/scala/docspell/common/Password.scala +++ b/modules/common/src/main/scala/docspell/common/Password.scala @@ -6,18 +6,29 @@ package docspell.common +import java.nio.charset.StandardCharsets + import cats.effect.Sync import cats.implicits._ import io.circe.{Decoder, Encoder} +import scodec.bits.ByteVector final class Password(val pass: String) extends AnyVal { def isEmpty: Boolean = pass.isEmpty + def nonEmpty: Boolean = pass.nonEmpty + def length: Int = pass.length + + def asByteVector: ByteVector = + ByteVector.view(pass.getBytes(StandardCharsets.UTF_8)) override def toString: String = if (pass.isEmpty) "" else "***" + def compare(other: Password): Boolean = + this.pass.zip(other.pass).forall { case (a, b) => a == b } && + this.nonEmpty && this.length == other.length } object Password { diff --git a/modules/common/src/main/scala/docspell/common/Timestamp.scala b/modules/common/src/main/scala/docspell/common/Timestamp.scala index b9aa104f..c056c2a8 100644 --- a/modules/common/src/main/scala/docspell/common/Timestamp.scala +++ b/modules/common/src/main/scala/docspell/common/Timestamp.scala @@ -70,6 +70,9 @@ object Timestamp { def atUtc(ldt: LocalDateTime): Timestamp = from(ldt.atZone(UTC)) + def ofMillis(ms: Long): Timestamp = + Timestamp(Instant.ofEpochMilli(ms)) + def daysBetween(ts0: Timestamp, ts1: Timestamp): Long = ChronoUnit.DAYS.between(ts0.toUtcDate, ts1.toUtcDate) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index ac2cc363..f1e5176b 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -538,6 +538,37 @@ paths: application/json: schema: $ref: "#/components/schemas/InviteResult" + + /open/share/verify: + post: + operationId: "open-share-verify" + tags: [ Share ] + summary: Verify a secret for a share + description: | + Given the share id and optionally a password, it verifies the + correctness of the given data. As a result, a token is + returned that must be used with all `share/*` routes. If the + password is missing, but required, the response indicates + this. Then the requests needs to be replayed with the correct + password to retrieve the token. + + The token is also added as a session cookie to the response. + + The token is used to avoid passing the user define password + with every request. + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ShareSecret" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/ShareVerifyResult" + /sec/auth/session: post: operationId: "sec-auth-session" @@ -4186,6 +4217,38 @@ paths: components: schemas: + ShareSecret: + description: | + The secret (the share id + optional password) to access a + share. + required: + - shareId + properties: + shareId: + type: string + format: ident + password: + type: string + format: password + + ShareVerifyResult: + description: | + The data returned when verifying a `ShareSecret`. + required: + - success + - token + - passwordRequired + - message + properties: + success: + type: boolean + token: + type: string + passwordRequired: + type: boolean + message: + type: string + ShareData: description: | Editable data for a share. diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala index 6c620a0b..cceacfdd 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala @@ -10,7 +10,7 @@ import cats.effect._ import cats.implicits._ import fs2.Stream -import docspell.backend.auth.AuthToken +import docspell.backend.auth.{AuthToken, ShareToken} import docspell.common._ import docspell.oidc.CodeFlowRoutes import docspell.restserver.auth.OpenId @@ -44,9 +44,12 @@ object RestServer { "/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token => securedRoutes(cfg, restApp, token) }, - "/api/v1/admin" -> AdminRoutes(cfg.adminEndpoint) { + "/api/v1/admin" -> AdminAuth(cfg.adminEndpoint) { adminRoutes(cfg, restApp) }, + "/api/v1/share" -> ShareAuth(restApp.backend.share, cfg.auth) { token => + shareRoutes(cfg, restApp, token) + }, "/api/doc" -> templates.doc, "/app/assets" -> EnvMiddleware(WebjarRoutes.appRoutes[F]), "/app" -> EnvMiddleware(templates.app), @@ -120,7 +123,8 @@ object RestServer { "signup" -> RegisterRoutes(restApp.backend, cfg), "upload" -> UploadRoutes.open(restApp.backend, cfg), "checkfile" -> CheckFileRoutes.open(restApp.backend), - "integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg) + "integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg), + "share" -> ShareRoutes.verify(restApp.backend, cfg) ) def adminRoutes[F[_]: Async](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] = @@ -132,6 +136,15 @@ object RestServer { "attachments" -> AttachmentRoutes.admin(restApp.backend) ) + def shareRoutes[F[_]: Async]( + cfg: Config, + restApp: RestApp[F], + token: ShareToken + ): HttpRoutes[F] = + Router( + "search" -> ShareSearchRoutes(restApp.backend, cfg, token) + ) + def redirectTo[F[_]: Async](path: String): HttpRoutes[F] = { val dsl = new Http4sDsl[F] {} import dsl._ diff --git a/modules/restserver/src/main/scala/docspell/restserver/auth/ShareCookieData.scala b/modules/restserver/src/main/scala/docspell/restserver/auth/ShareCookieData.scala new file mode 100644 index 00000000..0c3b0bdf --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/auth/ShareCookieData.scala @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.restserver.auth + +import docspell.backend.auth.ShareToken +import docspell.common._ + +import org.http4s._ +import org.typelevel.ci.CIString + +final case class ShareCookieData(token: ShareToken) { + def asString: String = token.asString + + def asCookie(baseUrl: LenientUri): ResponseCookie = { + val sec = baseUrl.scheme.exists(_.endsWith("s")) + val path = baseUrl.path / "api" / "v1" + ResponseCookie( + name = ShareCookieData.cookieName, + content = asString, + domain = None, + path = Some(path.asString), + httpOnly = true, + secure = sec, + maxAge = None, + expires = None + ) + } + + def addCookie[F[_]](baseUrl: LenientUri)( + resp: Response[F] + ): Response[F] = + resp.addCookie(asCookie(baseUrl)) +} + +object ShareCookieData { + val cookieName = "docspell_share" + val headerName = "Docspell-Share-Auth" + + def fromCookie[F[_]](req: Request[F]): Option[String] = + for { + header <- req.headers.get[headers.Cookie] + cookie <- header.values.toList.find(_.name == cookieName) + } yield cookie.content + + def fromHeader[F[_]](req: Request[F]): Option[String] = + req.headers + .get(CIString(headerName)) + .map(_.head.value) + + def fromRequest[F[_]](req: Request[F]): Option[String] = + fromCookie(req).orElse(fromHeader(req)) + + def delete(baseUrl: LenientUri): ResponseCookie = + ResponseCookie( + name = cookieName, + content = "", + domain = None, + path = Some(baseUrl.path / "api" / "v1").map(_.asString), + httpOnly = true, + secure = baseUrl.scheme.exists(_.endsWith("s")), + maxAge = None, + expires = None + ) + +} diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AdminRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AdminAuth.scala similarity index 92% rename from modules/restserver/src/main/scala/docspell/restserver/routes/AdminRoutes.scala rename to modules/restserver/src/main/scala/docspell/restserver/routes/AdminAuth.scala index 59491091..333f8d10 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/AdminRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AdminAuth.scala @@ -10,6 +10,7 @@ import cats.data.{Kleisli, OptionT} import cats.effect._ import cats.implicits._ +import docspell.common.Password import docspell.restserver.Config import docspell.restserver.http4s.Responses @@ -19,7 +20,7 @@ import org.http4s.dsl.Http4sDsl import org.http4s.server._ import org.typelevel.ci.CIString -object AdminRoutes { +object AdminAuth { private val adminHeader = CIString("Docspell-Admin-Secret") def apply[F[_]: Async](cfg: Config.AdminEndpoint)( @@ -55,6 +56,5 @@ object AdminRoutes { req.headers.get(adminHeader).map(_.head.value) private def compareSecret(s1: String)(s2: String): Boolean = - s1.length > 0 && s1.length == s2.length && - s1.zip(s2).forall { case (a, b) => a == b } + Password(s1).compare(Password(s2)) } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAuth.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAuth.scala new file mode 100644 index 00000000..ad2c41ab --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAuth.scala @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.restserver.routes + +import cats.data.{Kleisli, OptionT} +import cats.effect._ +import cats.implicits._ + +import docspell.backend.auth.{Login, ShareToken} +import docspell.backend.ops.OShare +import docspell.backend.ops.OShare.VerifyResult +import docspell.restserver.auth.ShareCookieData + +import org.http4s._ +import org.http4s.circe.CirceEntityEncoder._ +import org.http4s.dsl.Http4sDsl +import org.http4s.server._ + +object ShareAuth { + + def authenticateRequest[F[_]: Async]( + validate: String => F[VerifyResult] + )(req: Request[F]): F[OShare.VerifyResult] = + ShareCookieData.fromRequest(req) match { + case Some(tokenStr) => + validate(tokenStr) + case None => + VerifyResult.notFound.pure[F] + } + + private def getToken[F[_]: Async]( + auth: String => F[VerifyResult] + ): Kleisli[F, Request[F], Either[String, ShareToken]] = + Kleisli(r => authenticateRequest(auth)(r).map(_.toEither)) + + def of[F[_]: Async](S: OShare[F], cfg: Login.Config)( + pf: PartialFunction[AuthedRequest[F, ShareToken], F[Response[F]]] + ): HttpRoutes[F] = { + val dsl: Http4sDsl[F] = new Http4sDsl[F] {} + import dsl._ + + val authUser = getToken[F](S.verifyToken(cfg.serverSecret)) + + val onFailure: AuthedRoutes[String, F] = + Kleisli(req => OptionT.liftF(Forbidden(req.context))) + + val middleware: AuthMiddleware[F, ShareToken] = + AuthMiddleware(authUser, onFailure) + + middleware(AuthedRoutes.of(pf)) + } + + def apply[F[_]: Async](S: OShare[F], cfg: Login.Config)( + f: ShareToken => HttpRoutes[F] + ): HttpRoutes[F] = { + val dsl: Http4sDsl[F] = new Http4sDsl[F] {} + import dsl._ + + val authUser = getToken[F](S.verifyToken(cfg.serverSecret)) + + val onFailure: AuthedRoutes[String, F] = + Kleisli(req => OptionT.liftF(Forbidden(req.context))) + + val middleware: AuthMiddleware[F, ShareToken] = + AuthMiddleware(authUser, onFailure) + + middleware(AuthedRoutes(authReq => f(authReq.context).run(authReq.req))) + } +} diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala index 846bc7bc..1e5947a6 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala @@ -9,13 +9,18 @@ package docspell.restserver.routes import cats.data.OptionT import cats.effect._ import cats.implicits._ + import docspell.backend.BackendApp import docspell.backend.auth.AuthToken import docspell.backend.ops.OShare +import docspell.backend.ops.OShare.VerifyResult import docspell.common.{Ident, Timestamp} import docspell.restapi.model._ -import docspell.restserver.http4s.ResponseGenerator +import docspell.restserver.Config +import docspell.restserver.auth.ShareCookieData +import docspell.restserver.http4s.{ClientRequestInfo, ResponseGenerator} import docspell.store.records.RShare + import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ @@ -66,6 +71,31 @@ object ShareRoutes { } } + def verify[F[_]: Async](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = { + val dsl = new Http4sDsl[F] with ResponseGenerator[F] {} + import dsl._ + + HttpRoutes.of { case req @ POST -> Root / "verify" => + for { + secret <- req.as[ShareSecret] + res <- backend.share + .verify(cfg.auth.serverSecret)(secret.shareId, secret.password) + resp <- res match { + case VerifyResult.Success(token) => + val cd = ShareCookieData(token) + Ok(ShareVerifyResult(true, token.asString, false, "Success")) + .map(cd.addCookie(ClientRequestInfo.getBaseUrl(cfg, req))) + case VerifyResult.PasswordMismatch => + Ok(ShareVerifyResult(false, "", true, "Failed")) + case VerifyResult.NotFound => + Ok(ShareVerifyResult(false, "", false, "Failed")) + case VerifyResult.InvalidToken => + Ok(ShareVerifyResult(false, "", false, "Failed")) + } + } yield resp + } + } + def mkNewShare(data: ShareData, user: AuthToken): OShare.NewShare = OShare.NewShare( user.account.collective, diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala new file mode 100644 index 00000000..720b5d2f --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.restserver.routes + +import cats.effect._ + +import docspell.backend.BackendApp +import docspell.backend.auth.ShareToken +import docspell.common.Logger +import docspell.restserver.Config + +import org.http4s.HttpRoutes + +object ShareSearchRoutes { + + def apply[F[_]: Async]( + backend: BackendApp[F], + cfg: Config, + token: ShareToken + ): HttpRoutes[F] = { + val logger = Logger.log4s[F](org.log4s.getLogger) + logger.trace(s"$backend $cfg $token") + ??? + } +} diff --git a/modules/store/src/main/scala/docspell/store/records/RShare.scala b/modules/store/src/main/scala/docspell/store/records/RShare.scala index 4cef0929..7d6ae9bd 100644 --- a/modules/store/src/main/scala/docspell/store/records/RShare.scala +++ b/modules/store/src/main/scala/docspell/store/records/RShare.scala @@ -101,6 +101,28 @@ object RShare { .option ) + private def activeCondition(t: Table, id: Ident, current: Timestamp): Condition = + t.id === id && t.enabled === true && t.publishedUntil > current + + def findActive(id: Ident, current: Timestamp): OptionT[ConnectionIO, RShare] = + OptionT( + Select( + select(T.all), + from(T), + activeCondition(T, id, current) + ).build.query[RShare].option + ) + + def findCurrentActive(id: Ident): OptionT[ConnectionIO, RShare] = + OptionT.liftF(Timestamp.current[ConnectionIO]).flatMap(now => findActive(id, now)) + + def findActivePassword(id: Ident): OptionT[ConnectionIO, Option[Password]] = + OptionT(Timestamp.current[ConnectionIO].flatMap { now => + Select(select(T.password), from(T), activeCondition(T, id, now)).build + .query[Option[Password]] + .option + }) + def findAllByCollective(cid: Ident): ConnectionIO[List[RShare]] = Select(select(T.all), from(T), T.cid === cid) .orderBy(T.publishedAt.desc) From a2865561160af9aaed469083e2f2ff2783c3986c Mon Sep 17 00:00:00 2001 From: eikek Date: Mon, 4 Oct 2021 09:46:08 +0200 Subject: [PATCH 08/37] Initial impl of search route --- .../scala/docspell/backend/ops/OShare.scala | 31 ++++++++++--- .../src/main/resources/docspell-openapi.yml | 33 ++++++++++++++ .../restserver/routes/ShareRoutes.scala | 10 ++--- .../restserver/routes/ShareSearchRoutes.scala | 43 +++++++++++++++++-- 4 files changed, 102 insertions(+), 15 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index 005938a0..d0b82171 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -12,7 +12,7 @@ import cats.implicits._ import docspell.backend.PasswordCrypt import docspell.backend.auth.ShareToken -import docspell.backend.ops.OShare.VerifyResult +import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} import docspell.common._ import docspell.query.ItemQuery import docspell.store.Store @@ -36,26 +36,37 @@ trait OShare[F[_]] { removePassword: Boolean ): F[OShare.ChangeResult] + // --- + + /** Verifies the given id and password and returns a authorization token on success. */ def verify(key: ByteVector)(id: Ident, password: Option[Password]): F[VerifyResult] + + /** Verifies the authorization token. */ def verifyToken(key: ByteVector)(token: String): F[VerifyResult] + + def findShareQuery(id: Ident): OptionT[F, ShareQuery] } object OShare { + final case class ShareQuery(id: Ident, cid: Ident, query: ItemQuery) sealed trait VerifyResult { def toEither: Either[String, ShareToken] = this match { - case VerifyResult.Success(token) => Right(token) - case _ => Left("Authentication failed.") + case VerifyResult.Success(token, _) => + Right(token) + case _ => Left("Authentication failed.") } } object VerifyResult { - case class Success(token: ShareToken) extends VerifyResult + case class Success(token: ShareToken, shareName: Option[String]) extends VerifyResult case object NotFound extends VerifyResult case object PasswordMismatch extends VerifyResult case object InvalidToken extends VerifyResult - def success(token: ShareToken): VerifyResult = Success(token) + def success(token: ShareToken): VerifyResult = Success(token, None) + def success(token: ShareToken, name: Option[String]): VerifyResult = + Success(token, name) def notFound: VerifyResult = NotFound def passwordMismatch: VerifyResult = PasswordMismatch def invalidToken: VerifyResult = InvalidToken @@ -158,8 +169,8 @@ object OShare { val token = ShareToken.create(id, shareKey) pwCheck match { - case Some(true) => token.map(VerifyResult.success) - case None => token.map(VerifyResult.success) + case Some(true) => token.map(t => VerifyResult.success(t, share.name)) + case None => token.map(t => VerifyResult.success(t, share.name)) case Some(false) => VerifyResult.passwordMismatch.pure[F] } } @@ -186,5 +197,11 @@ object OShare { logger.debug(s"Invalid session token: $err") *> VerifyResult.invalidToken.pure[F] } + + def findShareQuery(id: Ident): OptionT[F, ShareQuery] = + RShare + .findCurrentActive(id) + .mapK(store.transform) + .map(share => ShareQuery(share.id, share.cid, share.query)) } } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index f1e5176b..2f563116 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1558,6 +1558,30 @@ paths: schema: $ref: "#/components/schemas/BasicResult" + /share/search: + post: + operationId: "share-search" + tags: [Share] + summary: Performs a search in a share. + description: | + Allows to run a search query in the shared documents. The + input data structure is the same as with a standard query. The + `searchMode` parameter is ignored here. + security: + - shareTokenHeader: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ItemQuery" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/ItemLightList" + /admin/user/resetPassword: post: operationId: "admin-user-reset-password" @@ -4248,6 +4272,11 @@ components: type: boolean message: type: string + name: + type: string + description: | + The name of the share if it exists. Only valid to use when + `success` is `true`. ShareData: description: | @@ -6475,6 +6504,10 @@ components: type: apiKey in: header name: Docspell-Admin-Secret + shareTokenHeader: + type: apiKey + in: header + name: Docspell-Share-Auth parameters: id: name: id diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala index 1e5947a6..5e9b13b5 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala @@ -81,16 +81,16 @@ object ShareRoutes { res <- backend.share .verify(cfg.auth.serverSecret)(secret.shareId, secret.password) resp <- res match { - case VerifyResult.Success(token) => + case VerifyResult.Success(token, name) => val cd = ShareCookieData(token) - Ok(ShareVerifyResult(true, token.asString, false, "Success")) + Ok(ShareVerifyResult(true, token.asString, false, "Success", name)) .map(cd.addCookie(ClientRequestInfo.getBaseUrl(cfg, req))) case VerifyResult.PasswordMismatch => - Ok(ShareVerifyResult(false, "", true, "Failed")) + Ok(ShareVerifyResult(false, "", true, "Failed", None)) case VerifyResult.NotFound => - Ok(ShareVerifyResult(false, "", false, "Failed")) + Ok(ShareVerifyResult(false, "", false, "Failed", None)) case VerifyResult.InvalidToken => - Ok(ShareVerifyResult(false, "", false, "Failed")) + Ok(ShareVerifyResult(false, "", false, "Failed", None)) } } yield resp } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala index 720b5d2f..a0ffa3a9 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala @@ -7,13 +7,20 @@ package docspell.restserver.routes import cats.effect._ +import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.ShareToken -import docspell.common.Logger +import docspell.backend.ops.OSimpleSearch +import docspell.common._ +import docspell.restapi.model.ItemQuery import docspell.restserver.Config +import docspell.store.qb.Batch +import docspell.store.queries.Query import org.http4s.HttpRoutes +import org.http4s.circe.CirceEntityDecoder._ +import org.http4s.dsl.Http4sDsl object ShareSearchRoutes { @@ -23,7 +30,37 @@ object ShareSearchRoutes { token: ShareToken ): HttpRoutes[F] = { val logger = Logger.log4s[F](org.log4s.getLogger) - logger.trace(s"$backend $cfg $token") - ??? + + val dsl = new Http4sDsl[F] {} + import dsl._ + + HttpRoutes.of { case req @ POST -> Root => + backend.share + .findShareQuery(token.id) + .semiflatMap { share => + for { + userQuery <- req.as[ItemQuery] + batch = Batch( + userQuery.offset.getOrElse(0), + userQuery.limit.getOrElse(cfg.maxItemPageSize) + ).restrictLimitTo( + cfg.maxItemPageSize + ) + itemQuery = ItemQueryString(userQuery.query) + settings = OSimpleSearch.Settings( + batch, + cfg.fullTextSearch.enabled, + userQuery.withDetails.getOrElse(false), + cfg.maxNoteLength, + searchMode = SearchMode.Normal + ) + account = AccountId(share.cid, Ident.unsafe("")) + fixQuery = Query.Fix(account, Some(share.query.expr), None) + _ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}") + resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery) + } yield resp + } + .getOrElseF(NotFound()) + } } } From 83dd675e4f4525f53abe102c8fa597e5ddf4ceda Mon Sep 17 00:00:00 2001 From: eikek Date: Tue, 5 Oct 2021 01:06:52 +0200 Subject: [PATCH 09/37] Basic search view for shares --- modules/webapp/src/main/elm/Api.elm | 24 +++ modules/webapp/src/main/elm/App/Update.elm | 2 +- modules/webapp/src/main/elm/App/View2.elm | 1 + modules/webapp/src/main/elm/Data/Items.elm | 6 + .../src/main/elm/Messages/Page/Share.elm | 31 +++- .../webapp/src/main/elm/Page/Share/Data.elm | 73 +++++++- .../src/main/elm/Page/Share/Menubar.elm | 60 +++++++ .../src/main/elm/Page/Share/Results.elm | 23 +++ .../src/main/elm/Page/Share/Sidebar.elm | 32 ++++ .../webapp/src/main/elm/Page/Share/Update.elm | 169 +++++++++++++++++- .../webapp/src/main/elm/Page/Share/View.elm | 137 +++++++++++++- modules/webapp/src/main/elm/Util/Http.elm | 42 +++++ 12 files changed, 577 insertions(+), 23 deletions(-) create mode 100644 modules/webapp/src/main/elm/Page/Share/Menubar.elm create mode 100644 modules/webapp/src/main/elm/Page/Share/Results.elm create mode 100644 modules/webapp/src/main/elm/Page/Share/Sidebar.elm diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index a9619b1e..b5323195 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -113,6 +113,7 @@ module Api exposing , restoreAllItems , restoreItem , saveClientSettings + , searchShare , sendMail , setAttachmentName , setCollectiveSettings @@ -155,6 +156,7 @@ module Api exposing , upload , uploadAmend , uploadSingle + , verifyShare , versionInfo ) @@ -223,6 +225,8 @@ import Api.Model.SentMails exposing (SentMails) import Api.Model.ShareData exposing (ShareData) import Api.Model.ShareDetail exposing (ShareDetail) import Api.Model.ShareList exposing (ShareList) +import Api.Model.ShareSecret exposing (ShareSecret) +import Api.Model.ShareVerifyResult exposing (ShareVerifyResult) import Api.Model.SimpleMail exposing (SimpleMail) import Api.Model.SourceAndTags exposing (SourceAndTags) import Api.Model.SourceList exposing (SourceList) @@ -2264,6 +2268,26 @@ deleteShare flags id receive = } +verifyShare : Flags -> ShareSecret -> (Result Http.Error ShareVerifyResult -> msg) -> Cmd msg +verifyShare flags secret receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/open/share/verify" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.ShareSecret.encode secret) + , expect = Http.expectJson receive Api.Model.ShareVerifyResult.decoder + } + + +searchShare : Flags -> String -> ItemQuery -> (Result Http.Error ItemLightList -> msg) -> Cmd msg +searchShare flags token search receive = + Http2.sharePost + { url = flags.config.baseUrl ++ "/api/v1/share/search" + , token = token + , body = Http.jsonBody (Api.Model.ItemQuery.encode search) + , expect = Http.expectJson receive Api.Model.ItemLightList.decoder + } + + --- Helper diff --git a/modules/webapp/src/main/elm/App/Update.elm b/modules/webapp/src/main/elm/App/Update.elm index 5408c581..2b051d84 100644 --- a/modules/webapp/src/main/elm/App/Update.elm +++ b/modules/webapp/src/main/elm/App/Update.elm @@ -324,7 +324,7 @@ updateShare lmsg model = Just id -> let result = - Page.Share.Update.update model.flags id lmsg model.shareModel + Page.Share.Update.update model.flags model.uiSettings id lmsg model.shareModel in ( { model | shareModel = result.model } , Cmd.map ShareMsg result.cmd diff --git a/modules/webapp/src/main/elm/App/View2.elm b/modules/webapp/src/main/elm/App/View2.elm index d80be6e3..06b99462 100644 --- a/modules/webapp/src/main/elm/App/View2.elm +++ b/modules/webapp/src/main/elm/App/View2.elm @@ -432,6 +432,7 @@ viewShare texts shareId model = , Html.map ShareMsg (Share.viewContent texts.share model.flags + model.version model.uiSettings model.shareModel ) diff --git a/modules/webapp/src/main/elm/Data/Items.elm b/modules/webapp/src/main/elm/Data/Items.elm index a2459b7b..0940781c 100644 --- a/modules/webapp/src/main/elm/Data/Items.elm +++ b/modules/webapp/src/main/elm/Data/Items.elm @@ -8,6 +8,7 @@ module Data.Items exposing ( concat , first + , flatten , idSet , length , replaceIn @@ -21,6 +22,11 @@ import Set exposing (Set) import Util.List +flatten : ItemLightList -> List ItemLight +flatten list = + List.concatMap .items list.groups + + concat : ItemLightList -> ItemLightList -> ItemLightList concat l0 l1 = let diff --git a/modules/webapp/src/main/elm/Messages/Page/Share.elm b/modules/webapp/src/main/elm/Messages/Page/Share.elm index b6044543..cee461cd 100644 --- a/modules/webapp/src/main/elm/Messages/Page/Share.elm +++ b/modules/webapp/src/main/elm/Messages/Page/Share.elm @@ -7,16 +7,41 @@ module Messages.Page.Share exposing (..) +import Messages.Basics +import Messages.Comp.ItemCardList +import Messages.Comp.SearchMenu + type alias Texts = - {} + { searchMenu : Messages.Comp.SearchMenu.Texts + , basics : Messages.Basics.Texts + , itemCardList : Messages.Comp.ItemCardList.Texts + , passwordRequired : String + , password : String + , passwordSubmitButton : String + , passwordFailed : String + } gb : Texts gb = - {} + { searchMenu = Messages.Comp.SearchMenu.gb + , basics = Messages.Basics.gb + , itemCardList = Messages.Comp.ItemCardList.gb + , passwordRequired = "Password required" + , password = "Password" + , passwordSubmitButton = "Submit" + , passwordFailed = "Das Passwort ist falsch" + } de : Texts de = - {} + { searchMenu = Messages.Comp.SearchMenu.de + , basics = Messages.Basics.de + , itemCardList = Messages.Comp.ItemCardList.de + , passwordRequired = "Passwort benötigt" + , password = "Passwort" + , passwordSubmitButton = "Submit" + , passwordFailed = "Password is wrong" + } diff --git a/modules/webapp/src/main/elm/Page/Share/Data.elm b/modules/webapp/src/main/elm/Page/Share/Data.elm index a0aa5f76..0689e9dd 100644 --- a/modules/webapp/src/main/elm/Page/Share/Data.elm +++ b/modules/webapp/src/main/elm/Page/Share/Data.elm @@ -5,28 +5,83 @@ -} -module Page.Share.Data exposing (Model, Msg, init) +module Page.Share.Data exposing (Mode(..), Model, Msg(..), PageError(..), init) +import Api +import Api.Model.ItemLightList exposing (ItemLightList) +import Api.Model.ShareSecret exposing (ShareSecret) +import Api.Model.ShareVerifyResult exposing (ShareVerifyResult) +import Comp.ItemCardList +import Comp.PowerSearchInput +import Comp.SearchMenu import Data.Flags exposing (Flags) +import Http + + +type Mode + = ModeInitial + | ModePassword + | ModeShare + + +type PageError + = PageErrorNone + | PageErrorHttp Http.Error + | PageErrorAuthFail + + +type alias PasswordModel = + { password : String + , passwordFailed : Bool + } type alias Model = - {} + { mode : Mode + , verifyResult : ShareVerifyResult + , passwordModel : PasswordModel + , pageError : PageError + , items : ItemLightList + , searchMenuModel : Comp.SearchMenu.Model + , powerSearchInput : Comp.PowerSearchInput.Model + , searchInProgress : Bool + , itemListModel : Comp.ItemCardList.Model + } + + +emptyModel : Flags -> Model +emptyModel flags = + { mode = ModeInitial + , verifyResult = Api.Model.ShareVerifyResult.empty + , passwordModel = + { password = "" + , passwordFailed = False + } + , pageError = PageErrorNone + , items = Api.Model.ItemLightList.empty + , searchMenuModel = Comp.SearchMenu.init flags + , powerSearchInput = Comp.PowerSearchInput.init + , searchInProgress = False + , itemListModel = Comp.ItemCardList.init + } init : Maybe String -> Flags -> ( Model, Cmd Msg ) init shareId flags = case shareId of Just id -> - let - _ = - Debug.log "share" id - in - ( {}, Cmd.none ) + ( emptyModel flags, Api.verifyShare flags (ShareSecret id Nothing) VerifyResp ) Nothing -> - ( {}, Cmd.none ) + ( emptyModel flags, Cmd.none ) type Msg - = Msg + = VerifyResp (Result Http.Error ShareVerifyResult) + | SearchResp (Result Http.Error ItemLightList) + | SetPassword String + | SubmitPassword + | SearchMenuMsg Comp.SearchMenu.Msg + | PowerSearchMsg Comp.PowerSearchInput.Msg + | ResetSearch + | ItemListMsg Comp.ItemCardList.Msg diff --git a/modules/webapp/src/main/elm/Page/Share/Menubar.elm b/modules/webapp/src/main/elm/Page/Share/Menubar.elm new file mode 100644 index 00000000..10751839 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Share/Menubar.elm @@ -0,0 +1,60 @@ +module Page.Share.Menubar exposing (view) + +import Comp.Basic as B +import Comp.MenuBar as MB +import Comp.PowerSearchInput +import Comp.SearchMenu +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) +import Messages.Page.Share exposing (Texts) +import Page.Share.Data exposing (Model, Msg(..)) +import Styles as S + + +view : Texts -> Model -> Html Msg +view texts model = + let + btnStyle = + S.secondaryBasicButton ++ " text-sm" + + searchInput = + Comp.SearchMenu.textSearchString + model.searchMenuModel.textSearchModel + + powerSearchBar = + div + [ class "relative flex flex-grow flex-row" ] + [ Html.map PowerSearchMsg + (Comp.PowerSearchInput.viewInput + { placeholder = texts.basics.searchPlaceholder + , extraAttrs = [] + } + model.powerSearchInput + ) + , Html.map PowerSearchMsg + (Comp.PowerSearchInput.viewResult [] model.powerSearchInput) + ] + in + MB.view + { end = + [ MB.CustomElement <| + B.secondaryBasicButton + { label = "" + , icon = + if model.searchInProgress then + "fa fa-sync animate-spin" + + else + "fa fa-sync" + , disabled = model.searchInProgress + , handler = onClick ResetSearch + , attrs = [ href "#" ] + } + ] + , start = + [ MB.CustomElement <| + powerSearchBar + ] + , rootClasses = "mb-2 pt-1 dark:bg-bluegray-700 items-center text-sm" + } diff --git a/modules/webapp/src/main/elm/Page/Share/Results.elm b/modules/webapp/src/main/elm/Page/Share/Results.elm new file mode 100644 index 00000000..b513e936 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Share/Results.elm @@ -0,0 +1,23 @@ +module Page.Share.Results exposing (view) + +import Comp.ItemCardList +import Data.ItemSelection +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Messages.Page.Share exposing (Texts) +import Page.Share.Data exposing (Model, Msg(..)) + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts settings model = + let + viewCfg = + { current = Nothing + , selection = Data.ItemSelection.Inactive + } + in + div [] + [ Html.map ItemListMsg + (Comp.ItemCardList.view2 texts.itemCardList viewCfg settings model.itemListModel) + ] diff --git a/modules/webapp/src/main/elm/Page/Share/Sidebar.elm b/modules/webapp/src/main/elm/Page/Share/Sidebar.elm new file mode 100644 index 00000000..abd0d8f4 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Share/Sidebar.elm @@ -0,0 +1,32 @@ +module Page.Share.Sidebar exposing (..) + +import Comp.SearchMenu +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Messages.Page.Share exposing (Texts) +import Page.Share.Data exposing (Model, Msg(..)) +import Util.ItemDragDrop + + +view : Texts -> Flags -> UiSettings -> Model -> Html Msg +view texts flags settings model = + div + [ class "flex flex-col" + ] + [ Html.map SearchMenuMsg + (Comp.SearchMenu.viewDrop2 texts.searchMenu + ddDummy + flags + settings + model.searchMenuModel + ) + ] + + +ddDummy : Util.ItemDragDrop.DragDropData +ddDummy = + { model = Util.ItemDragDrop.init + , dropped = Nothing + } diff --git a/modules/webapp/src/main/elm/Page/Share/Update.elm b/modules/webapp/src/main/elm/Page/Share/Update.elm index 0f1dadbb..f7be7d99 100644 --- a/modules/webapp/src/main/elm/Page/Share/Update.elm +++ b/modules/webapp/src/main/elm/Page/Share/Update.elm @@ -7,7 +7,15 @@ module Page.Share.Update exposing (UpdateResult, update) +import Api +import Api.Model.ItemQuery +import Comp.ItemCardList +import Comp.PowerSearchInput +import Comp.SearchMenu import Data.Flags exposing (Flags) +import Data.ItemQuery as Q +import Data.SearchMode +import Data.UiSettings exposing (UiSettings) import Page.Share.Data exposing (..) @@ -18,6 +26,161 @@ type alias UpdateResult = } -update : Flags -> String -> Msg -> Model -> UpdateResult -update flags shareId msg model = - UpdateResult model Cmd.none Sub.none +update : Flags -> UiSettings -> String -> Msg -> Model -> UpdateResult +update flags settings shareId msg model = + case msg of + VerifyResp (Ok res) -> + if res.success then + let + eq = + Api.Model.ItemQuery.empty + + iq = + { eq | withDetails = Just True } + in + noSub + ( { model + | pageError = PageErrorNone + , mode = ModeShare + , verifyResult = res + , searchInProgress = True + } + , makeSearchCmd flags model + ) + + else if res.passwordRequired then + if model.mode == ModePassword then + noSub + ( { model + | pageError = PageErrorNone + , passwordModel = + { password = "" + , passwordFailed = True + } + } + , Cmd.none + ) + + else + noSub + ( { model + | pageError = PageErrorNone + , mode = ModePassword + } + , Cmd.none + ) + + else + noSub + ( { model | pageError = PageErrorAuthFail } + , Cmd.none + ) + + VerifyResp (Err err) -> + noSub ( { model | pageError = PageErrorHttp err }, Cmd.none ) + + SearchResp (Ok list) -> + update flags + settings + shareId + (ItemListMsg (Comp.ItemCardList.SetResults list)) + { model | searchInProgress = False } + + SearchResp (Err err) -> + noSub ( { model | pageError = PageErrorHttp err, searchInProgress = False }, Cmd.none ) + + SetPassword pw -> + let + pm = + model.passwordModel + in + noSub ( { model | passwordModel = { pm | password = pw } }, Cmd.none ) + + SubmitPassword -> + let + secret = + { shareId = shareId + , password = Just model.passwordModel.password + } + in + noSub ( model, Api.verifyShare flags secret VerifyResp ) + + SearchMenuMsg lm -> + let + res = + Comp.SearchMenu.update flags settings lm model.searchMenuModel + + nextModel = + { model | searchMenuModel = res.model } + + ( initSearch, searchCmd ) = + if res.stateChange && not model.searchInProgress then + ( True, makeSearchCmd flags nextModel ) + + else + ( False, Cmd.none ) + in + noSub + ( { nextModel | searchInProgress = initSearch } + , Cmd.batch [ Cmd.map SearchMenuMsg res.cmd, searchCmd ] + ) + + PowerSearchMsg lm -> + let + res = + Comp.PowerSearchInput.update lm model.powerSearchInput + + nextModel = + { model | powerSearchInput = res.model } + + ( initSearch, searchCmd ) = + case res.action of + Comp.PowerSearchInput.NoAction -> + ( False, Cmd.none ) + + Comp.PowerSearchInput.SubmitSearch -> + ( True, makeSearchCmd flags nextModel ) + in + { model = { nextModel | searchInProgress = initSearch } + , cmd = Cmd.batch [ Cmd.map PowerSearchMsg res.cmd, searchCmd ] + , sub = Sub.map PowerSearchMsg res.subs + } + + ResetSearch -> + let + nm = + { model | powerSearchInput = Comp.PowerSearchInput.init } + in + update flags settings shareId (SearchMenuMsg Comp.SearchMenu.ResetForm) nm + + ItemListMsg lm -> + let + ( im, ic ) = + Comp.ItemCardList.update flags lm model.itemListModel + in + noSub ( { model | itemListModel = im }, Cmd.map ItemListMsg ic ) + + +noSub : ( Model, Cmd Msg ) -> UpdateResult +noSub ( m, c ) = + UpdateResult m c Sub.none + + +makeSearchCmd : Flags -> Model -> Cmd Msg +makeSearchCmd flags model = + let + xq = + Q.and + [ Comp.SearchMenu.getItemQuery model.searchMenuModel + , Maybe.map Q.Fragment model.powerSearchInput.input + ] + + request mq = + { offset = Nothing + , limit = Nothing + , withDetails = Just True + , query = Q.renderMaybe mq + , searchMode = Just (Data.SearchMode.asString Data.SearchMode.Normal) + } + in + Api.searchShare flags model.verifyResult.token (request xq) SearchResp diff --git a/modules/webapp/src/main/elm/Page/Share/View.elm b/modules/webapp/src/main/elm/Page/Share/View.elm index 0d5cf016..5c0f941b 100644 --- a/modules/webapp/src/main/elm/Page/Share/View.elm +++ b/modules/webapp/src/main/elm/Page/Share/View.elm @@ -7,32 +7,155 @@ module Page.Share.View exposing (viewContent, viewSidebar) +import Api.Model.VersionInfo exposing (VersionInfo) +import Comp.Basic as B import Data.Flags exposing (Flags) +import Data.Items import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onInput, onSubmit) import Messages.Page.Share exposing (Texts) import Page.Share.Data exposing (..) +import Page.Share.Menubar as Menubar +import Page.Share.Results as Results +import Page.Share.Sidebar as Sidebar import Styles as S viewSidebar : Texts -> Bool -> Flags -> UiSettings -> Model -> Html Msg -viewSidebar _ visible _ _ _ = +viewSidebar texts visible flags settings model = div [ id "sidebar" - , classList [ ( "hidden", not visible ) ] + , class S.sidebar + , class S.sidebarBg + , classList [ ( "hidden", not visible || model.mode /= ModeShare ) ] + ] + [ Sidebar.view texts flags settings model ] - [ text "sidebar" ] -viewContent : Texts -> Flags -> UiSettings -> Model -> Html Msg -viewContent texts flags _ model = +viewContent : Texts -> Flags -> VersionInfo -> UiSettings -> Model -> Html Msg +viewContent texts flags versionInfo uiSettings model = + case model.mode of + ModeInitial -> + div + [ id "content" + , class "h-full w-full flex flex-col text-5xl" + , class S.content + ] + [ B.loadingDimmer + { active = model.pageError == PageErrorNone + , label = "" + } + ] + + ModePassword -> + passwordContent texts flags versionInfo model + + ModeShare -> + mainContent texts flags uiSettings model + + + +--- Helpers + + +mainContent : Texts -> Flags -> UiSettings -> Model -> Html Msg +mainContent texts _ settings model = div [ id "content" , class "h-full flex flex-col" , class S.content ] - [ h1 [ class S.header1 ] - [ text "Share Page!" + [ h1 + [ class S.header1 + , classList [ ( "hidden", model.verifyResult.name == Nothing ) ] + ] + [ text <| Maybe.withDefault "" model.verifyResult.name + ] + , Menubar.view texts model + , Results.view texts settings model + ] + + +passwordContent : Texts -> Flags -> VersionInfo -> Model -> Html Msg +passwordContent texts flags versionInfo model = + div + [ id "content" + , class "h-full flex flex-col items-center justify-center w-full" + , class S.content + ] + [ div [ class ("flex flex-col px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md " ++ S.box) ] + [ div [ class "self-center" ] + [ img + [ class "w-16 py-2" + , src (flags.config.docspellAssetPath ++ "/img/logo-96.png") + ] + [] + ] + , div [ class "font-medium self-center text-xl sm:text-2xl" ] + [ text texts.passwordRequired + ] + , Html.form + [ action "#" + , onSubmit SubmitPassword + , autocomplete False + ] + [ div [ class "flex flex-col my-3" ] + [ label + [ for "password" + , class S.inputLabel + ] + [ text texts.password + ] + , div [ class "relative" ] + [ div [ class S.inputIcon ] + [ i [ class "fa fa-lock" ] [] + ] + , input + [ type_ "password" + , name "password" + , autocomplete False + , autofocus True + , tabindex 1 + , onInput SetPassword + , value model.passwordModel.password + , class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput) + , placeholder texts.password + ] + [] + ] + ] + , div [ class "flex flex-col my-3" ] + [ button + [ type_ "submit" + , class S.primaryButton + ] + [ text texts.passwordSubmitButton + ] + ] + , div + [ class S.errorMessage + , classList [ ( "hidden", not model.passwordModel.passwordFailed ) ] + ] + [ text texts.passwordFailed + ] + ] + ] + , a + [ class "inline-flex items-center mt-4 text-xs opacity-50 hover:opacity-90" + , href "https://docspell.org" + , target "_new" + ] + [ img + [ src (flags.config.docspellAssetPath ++ "/img/logo-mc-96.png") + , class "w-3 h-3 mr-1" + ] + [] + , span [] + [ text "Docspell " + , text versionInfo.version + ] ] ] diff --git a/modules/webapp/src/main/elm/Util/Http.elm b/modules/webapp/src/main/elm/Util/Http.elm index 550cbd7a..dd965b23 100644 --- a/modules/webapp/src/main/elm/Util/Http.elm +++ b/modules/webapp/src/main/elm/Util/Http.elm @@ -14,6 +14,7 @@ module Util.Http exposing , authTask , executeIn , jsonResolver + , sharePost ) import Api.Model.AuthResult exposing (AuthResult) @@ -49,6 +50,28 @@ authReq req = } +shareReq : + { url : String + , token : String + , method : String + , headers : List Http.Header + , body : Http.Body + , expect : Http.Expect msg + , tracker : Maybe String + } + -> Cmd msg +shareReq req = + Http.request + { url = req.url + , method = req.method + , headers = Http.header "Docspell-Share-Auth" req.token :: req.headers + , expect = req.expect + , body = req.body + , timeout = Nothing + , tracker = req.tracker + } + + authPost : { url : String , account : AuthResult @@ -68,6 +91,25 @@ authPost req = } +sharePost : + { url : String + , token : String + , body : Http.Body + , expect : Http.Expect msg + } + -> Cmd msg +sharePost req = + shareReq + { url = req.url + , token = req.token + , body = req.body + , expect = req.expect + , method = "POST" + , headers = [] + , tracker = Nothing + } + + authPostTrack : { url : String , account : AuthResult From 7b0f378558ed7610f7d46743936c08e00bf1e719 Mon Sep 17 00:00:00 2001 From: eikek Date: Tue, 5 Oct 2021 01:44:50 +0200 Subject: [PATCH 10/37] Refactor to allow internal card links into search menu Also allows to exchange the preview-url in the item card --- modules/webapp/src/main/elm/Api.elm | 12 +++++++ modules/webapp/src/main/elm/Comp/ItemCard.elm | 15 +++++---- .../webapp/src/main/elm/Comp/ItemCardList.elm | 9 +++-- .../webapp/src/main/elm/Comp/SearchMenu.elm | 33 +++++++++++++++++++ .../webapp/src/main/elm/Page/Home/Update.elm | 28 +--------------- .../webapp/src/main/elm/Page/Home/View2.elm | 11 +++++++ .../src/main/elm/Page/Share/Menubar.elm | 7 ++++ .../src/main/elm/Page/Share/Results.elm | 10 ++++++ .../src/main/elm/Page/Share/Sidebar.elm | 7 ++++ .../webapp/src/main/elm/Page/Share/Update.elm | 18 ++++++++-- 10 files changed, 111 insertions(+), 39 deletions(-) diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index b5323195..226b3740 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -141,6 +141,8 @@ module Api exposing , setTags , setTagsMultiple , setUnconfirmed + , shareAttachmentPreviewURL + , shareItemBasePreviewURL , startClassifier , startEmptyTrash , startOnceNotifyDueItems @@ -2288,6 +2290,16 @@ searchShare flags token search receive = } +shareAttachmentPreviewURL : String -> String +shareAttachmentPreviewURL id = + "/api/v1/share/attachment/" ++ id ++ "/preview?withFallback=true" + + +shareItemBasePreviewURL : String -> String +shareItemBasePreviewURL itemId = + "/api/v1/share/item/" ++ itemId ++ "/preview?withFallback=true" + + --- Helper diff --git a/modules/webapp/src/main/elm/Comp/ItemCard.elm b/modules/webapp/src/main/elm/Comp/ItemCard.elm index 9fd19ff3..b4ce9786 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCard.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCard.elm @@ -56,6 +56,8 @@ type Msg type alias ViewConfig = { selection : ItemSelection , extraClasses : String + , previewUrl : AttachmentLight -> String + , previewUrlFallback : ItemLight -> String } @@ -160,7 +162,7 @@ view2 texts cfg settings model item = "text-blue-500 dark:text-lightblue-500" else if isDeleted then - "text-red-600 dark:text-orange-600" + "text-red-600 dark:text-orange-600" else "" @@ -210,7 +212,7 @@ view2 texts cfg settings model item = [] else - [ previewImage2 settings cardAction model item + [ previewImage2 cfg settings cardAction model item ] ) ++ [ mainContent2 texts cardAction cardColor isCreated isDeleted settings cfg item @@ -443,16 +445,15 @@ mainTagsAndFields2 settings item = (renderFields ++ renderTags) -previewImage2 : UiSettings -> List (Attribute Msg) -> Model -> ItemLight -> Html Msg -previewImage2 settings cardAction model item = +previewImage2 : ViewConfig -> UiSettings -> List (Attribute Msg) -> Model -> ItemLight -> Html Msg +previewImage2 cfg settings cardAction model item = let mainAttach = currentAttachment model item previewUrl = - Maybe.map .id mainAttach - |> Maybe.map Api.attachmentPreviewURL - |> Maybe.withDefault (Api.itemBasePreviewURL item.id) + Maybe.map cfg.previewUrl mainAttach + |> Maybe.withDefault (cfg.previewUrlFallback item) in a ([ class "overflow-hidden block bg-gray-50 dark:bg-bluegray-700 dark:bg-opacity-40 border-gray-400 dark:hover:border-bluegray-500 rounded-t-lg" diff --git a/modules/webapp/src/main/elm/Comp/ItemCardList.elm b/modules/webapp/src/main/elm/Comp/ItemCardList.elm index 0d0c97e0..6c66004d 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCardList.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCardList.elm @@ -17,6 +17,7 @@ module Comp.ItemCardList exposing , view2 ) +import Api.Model.AttachmentLight exposing (AttachmentLight) import Api.Model.ItemLight exposing (ItemLight) import Api.Model.ItemLightGroup exposing (ItemLightGroup) import Api.Model.ItemLightList exposing (ItemLightList) @@ -72,13 +73,13 @@ prevItem model id = --- Update -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, LinkTarget ) update flags msg model = let res = updateDrag DD.init flags msg model in - ( res.model, res.cmd ) + ( res.model, res.cmd, res.linkTarget ) type alias UpdateResult = @@ -161,6 +162,8 @@ updateDrag dm _ msg model = type alias ViewConfig = { current : Maybe String , selection : ItemSelection + , previewUrl : AttachmentLight -> String + , previewUrlFallback : ItemLight -> String } @@ -216,7 +219,7 @@ viewItem2 texts model cfg settings item = "" vvcfg = - Comp.ItemCard.ViewConfig cfg.selection currentClass + Comp.ItemCard.ViewConfig cfg.selection currentClass cfg.previewUrl cfg.previewUrlFallback cardModel = Dict.get item.id model.itemCards diff --git a/modules/webapp/src/main/elm/Comp/SearchMenu.elm b/modules/webapp/src/main/elm/Comp/SearchMenu.elm index e29d52aa..5a2aa5f7 100644 --- a/modules/webapp/src/main/elm/Comp/SearchMenu.elm +++ b/modules/webapp/src/main/elm/Comp/SearchMenu.elm @@ -14,6 +14,7 @@ module Comp.SearchMenu exposing , init , isFulltextSearch , isNamesSearch + , linkTargetMsg , textSearchString , update , updateDrop @@ -34,6 +35,7 @@ import Comp.CustomFieldMultiInput import Comp.DatePicker import Comp.Dropdown exposing (isDropdownChangeMsg) import Comp.FolderSelect +import Comp.LinkTarget exposing (LinkTarget) import Comp.MenuBar as MB import Comp.Tabs import Comp.TagSelect @@ -377,6 +379,37 @@ type Msg | ToggleOpenAllAkkordionTabs +linkTargetMsg : LinkTarget -> Maybe Msg +linkTargetMsg linkTarget = + case linkTarget of + Comp.LinkTarget.LinkNone -> + Nothing + + Comp.LinkTarget.LinkCorrOrg id -> + Just <| SetCorrOrg id + + Comp.LinkTarget.LinkCorrPerson id -> + Just <| SetCorrPerson id + + Comp.LinkTarget.LinkConcPerson id -> + Just <| SetConcPerson id + + Comp.LinkTarget.LinkConcEquip id -> + Just <| SetConcEquip id + + Comp.LinkTarget.LinkFolder id -> + Just <| SetFolder id + + Comp.LinkTarget.LinkTag id -> + Just <| SetTag id.id + + Comp.LinkTarget.LinkCustomField id -> + Just <| SetCustomField id + + Comp.LinkTarget.LinkSource str -> + Just <| ResetToSource str + + type alias NextState = { model : Model , cmd : Cmd Msg diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index 00ebfe9a..edebe2f0 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -1035,33 +1035,7 @@ doSearch param model = linkTargetMsg : LinkTarget -> Maybe Msg linkTargetMsg linkTarget = - case linkTarget of - Comp.LinkTarget.LinkNone -> - Nothing - - Comp.LinkTarget.LinkCorrOrg id -> - Just <| SearchMenuMsg (Comp.SearchMenu.SetCorrOrg id) - - Comp.LinkTarget.LinkCorrPerson id -> - Just <| SearchMenuMsg (Comp.SearchMenu.SetCorrPerson id) - - Comp.LinkTarget.LinkConcPerson id -> - Just <| SearchMenuMsg (Comp.SearchMenu.SetConcPerson id) - - Comp.LinkTarget.LinkConcEquip id -> - Just <| SearchMenuMsg (Comp.SearchMenu.SetConcEquip id) - - Comp.LinkTarget.LinkFolder id -> - Just <| SearchMenuMsg (Comp.SearchMenu.SetFolder id) - - Comp.LinkTarget.LinkTag id -> - Just <| SearchMenuMsg (Comp.SearchMenu.SetTag id.id) - - Comp.LinkTarget.LinkCustomField id -> - Just <| SearchMenuMsg (Comp.SearchMenu.SetCustomField id) - - Comp.LinkTarget.LinkSource str -> - Just <| SearchMenuMsg (Comp.SearchMenu.ResetToSource str) + Maybe.map SearchMenuMsg (Comp.SearchMenu.linkTargetMsg linkTarget) doSearchMore : Flags -> UiSettings -> Model -> ( Model, Cmd Msg ) diff --git a/modules/webapp/src/main/elm/Page/Home/View2.elm b/modules/webapp/src/main/elm/Page/Home/View2.elm index 63f39957..b0ca349e 100644 --- a/modules/webapp/src/main/elm/Page/Home/View2.elm +++ b/modules/webapp/src/main/elm/Page/Home/View2.elm @@ -7,6 +7,7 @@ module Page.Home.View2 exposing (viewContent, viewSidebar) +import Api import Comp.Basic as B import Comp.ConfirmModal import Comp.ItemCardList @@ -461,17 +462,27 @@ searchStats texts _ settings model = itemCardList : Texts -> Flags -> UiSettings -> Model -> List (Html Msg) itemCardList texts _ settings model = let + previewUrl attach = + Api.attachmentPreviewURL attach.id + + previewUrlFallback item = + Api.itemBasePreviewURL item.id + itemViewCfg = case model.viewMode of SelectView svm -> Comp.ItemCardList.ViewConfig model.scrollToCard (Data.ItemSelection.Active svm.ids) + previewUrl + previewUrlFallback _ -> Comp.ItemCardList.ViewConfig model.scrollToCard Data.ItemSelection.Inactive + previewUrl + previewUrlFallback in [ Html.map ItemCardListMsg (Comp.ItemCardList.view2 texts.itemCardList diff --git a/modules/webapp/src/main/elm/Page/Share/Menubar.elm b/modules/webapp/src/main/elm/Page/Share/Menubar.elm index 10751839..eb490c37 100644 --- a/modules/webapp/src/main/elm/Page/Share/Menubar.elm +++ b/modules/webapp/src/main/elm/Page/Share/Menubar.elm @@ -1,3 +1,10 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + module Page.Share.Menubar exposing (view) import Comp.Basic as B diff --git a/modules/webapp/src/main/elm/Page/Share/Results.elm b/modules/webapp/src/main/elm/Page/Share/Results.elm index b513e936..c50af9e8 100644 --- a/modules/webapp/src/main/elm/Page/Share/Results.elm +++ b/modules/webapp/src/main/elm/Page/Share/Results.elm @@ -1,5 +1,13 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + module Page.Share.Results exposing (view) +import Api import Comp.ItemCardList import Data.ItemSelection import Data.UiSettings exposing (UiSettings) @@ -15,6 +23,8 @@ view texts settings model = viewCfg = { current = Nothing , selection = Data.ItemSelection.Inactive + , previewUrl = \attach -> Api.shareAttachmentPreviewURL attach.id + , previewUrlFallback = \item -> Api.shareItemBasePreviewURL item.id } in div [] diff --git a/modules/webapp/src/main/elm/Page/Share/Sidebar.elm b/modules/webapp/src/main/elm/Page/Share/Sidebar.elm index abd0d8f4..31a8ee1b 100644 --- a/modules/webapp/src/main/elm/Page/Share/Sidebar.elm +++ b/modules/webapp/src/main/elm/Page/Share/Sidebar.elm @@ -1,3 +1,10 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + module Page.Share.Sidebar exposing (..) import Comp.SearchMenu diff --git a/modules/webapp/src/main/elm/Page/Share/Update.elm b/modules/webapp/src/main/elm/Page/Share/Update.elm index f7be7d99..56070017 100644 --- a/modules/webapp/src/main/elm/Page/Share/Update.elm +++ b/modules/webapp/src/main/elm/Page/Share/Update.elm @@ -10,6 +10,7 @@ module Page.Share.Update exposing (UpdateResult, update) import Api import Api.Model.ItemQuery import Comp.ItemCardList +import Comp.LinkTarget exposing (LinkTarget) import Comp.PowerSearchInput import Comp.SearchMenu import Data.Flags exposing (Flags) @@ -17,6 +18,7 @@ import Data.ItemQuery as Q import Data.SearchMode import Data.UiSettings exposing (UiSettings) import Page.Share.Data exposing (..) +import Util.Update type alias UpdateResult = @@ -155,10 +157,17 @@ update flags settings shareId msg model = ItemListMsg lm -> let - ( im, ic ) = + ( im, ic, linkTarget ) = Comp.ItemCardList.update flags lm model.itemListModel + + searchMsg = + Maybe.map Util.Update.cmdUnit (linkTargetMsg linkTarget) + |> Maybe.withDefault Cmd.none in - noSub ( { model | itemListModel = im }, Cmd.map ItemListMsg ic ) + noSub + ( { model | itemListModel = im } + , Cmd.batch [ Cmd.map ItemListMsg ic, searchMsg ] + ) noSub : ( Model, Cmd Msg ) -> UpdateResult @@ -184,3 +193,8 @@ makeSearchCmd flags model = } in Api.searchShare flags model.verifyResult.token (request xq) SearchResp + + +linkTargetMsg : LinkTarget -> Maybe Msg +linkTargetMsg linkTarget = + Maybe.map SearchMenuMsg (Comp.SearchMenu.linkTargetMsg linkTarget) From e52271f9cdbc9e99144e2d36f94baa1f01996d32 Mon Sep 17 00:00:00 2001 From: eikek Date: Tue, 5 Oct 2021 09:24:11 +0200 Subject: [PATCH 11/37] Implement share preview image --- .../scala/docspell/backend/BackendApp.scala | 2 +- .../scala/docspell/backend/ops/OShare.scala | 43 +++++++++++++++++-- .../docspell/restserver/RestServer.scala | 3 +- .../restserver/http4s/BinaryUtil.scala | 34 ++++++++++++++- .../restserver/routes/AttachmentRoutes.scala | 19 +------- .../restserver/routes/ItemRoutes.scala | 2 +- .../routes/ShareAttachmentRoutes.scala | 37 ++++++++++++++++ .../restserver/routes/ShareSearchRoutes.scala | 2 +- 8 files changed, 115 insertions(+), 27 deletions(-) create mode 100644 modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala index 5a6fa482..1e58654c 100644 --- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala +++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala @@ -86,7 +86,7 @@ object BackendApp { customFieldsImpl <- OCustomFields(store) simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl) clientSettingsImpl <- OClientSettings(store) - shareImpl <- Resource.pure(OShare(store)) + shareImpl <- Resource.pure(OShare(store, itemSearchImpl)) } yield new BackendApp[F] { val login = loginImpl val signup = signupImpl diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index d0b82171..77b882f2 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -9,15 +9,15 @@ package docspell.backend.ops import cats.data.OptionT import cats.effect._ import cats.implicits._ - import docspell.backend.PasswordCrypt import docspell.backend.auth.ShareToken +import docspell.backend.ops.OItemSearch.{AttachmentPreviewData, Batch, Query} import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} import docspell.common._ import docspell.query.ItemQuery +import docspell.query.ItemQuery.Expr.AttachId import docspell.store.Store import docspell.store.records.RShare - import scodec.bits.ByteVector trait OShare[F[_]] { @@ -45,10 +45,21 @@ trait OShare[F[_]] { def verifyToken(key: ByteVector)(token: String): F[VerifyResult] def findShareQuery(id: Ident): OptionT[F, ShareQuery] + + def findAttachmentPreview( + attachId: Ident, + shareId: Ident + ): OptionT[F, AttachmentPreviewData[F]] + } object OShare { - final case class ShareQuery(id: Ident, cid: Ident, query: ItemQuery) + final case class ShareQuery(id: Ident, cid: Ident, query: ItemQuery) { + + //TODO + def asAccount: AccountId = + AccountId(cid, Ident.unsafe("")) + } sealed trait VerifyResult { def toEither: Either[String, ShareToken] = @@ -90,7 +101,7 @@ object OShare { def publishUntilInPast: ChangeResult = PublishUntilInPast } - def apply[F[_]: Async](store: Store[F]): OShare[F] = + def apply[F[_]: Async](store: Store[F], itemSearch: OItemSearch[F]): OShare[F] = new OShare[F] { private[this] val logger = Logger.log4s[F](org.log4s.getLogger) @@ -203,5 +214,29 @@ object OShare { .findCurrentActive(id) .mapK(store.transform) .map(share => ShareQuery(share.id, share.cid, share.query)) + + def findAttachmentPreview( + attachId: Ident, + shareId: Ident + ): OptionT[F, AttachmentPreviewData[F]] = + for { + sq <- findShareQuery(shareId) + account = sq.asAccount + checkQuery = Query( + Query.Fix(account, Some(sq.query.expr), None), + Query.QueryExpr(AttachId(attachId.id)) + ) + checkRes <- OptionT.liftF(itemSearch.findItems(0)(checkQuery, Batch.limit(1))) + res <- + if (checkRes.isEmpty) + OptionT + .liftF( + logger.info( + s"Attempt to load unshared attachment '${attachId.id}' for share: ${shareId.id}" + ) + ) + .mapFilter(_ => None) + else OptionT(itemSearch.findAttachmentPreview(attachId, sq.cid)) + } yield res } } diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala index cceacfdd..2e66c65f 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala @@ -142,7 +142,8 @@ object RestServer { token: ShareToken ): HttpRoutes[F] = Router( - "search" -> ShareSearchRoutes(restApp.backend, cfg, token) + "search" -> ShareSearchRoutes(restApp.backend, cfg, token), + "attachment" -> ShareAttachmentRoutes(restApp.backend, token) ) def redirectTo[F[_]: Async](path: String): HttpRoutes[F] = { diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala index e97eda8f..91baf47e 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala @@ -10,19 +10,49 @@ import cats.data.NonEmptyList import cats.data.OptionT import cats.effect._ import cats.implicits._ - +import docspell.backend.ops.OItemSearch.AttachmentPreviewData import docspell.backend.ops._ +import docspell.restapi.model.BasicResult import docspell.store.records.RFileMeta +import docspell.restserver.http4s.{QueryParam => QP} import org.http4s._ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl -import org.http4s.headers.ETag.EntityTag import org.http4s.headers._ +import org.http4s.headers.ETag.EntityTag import org.typelevel.ci.CIString object BinaryUtil { + def respond[F[_]: Async](dsl: Http4sDsl[F], req: Request[F])( + fileData: Option[AttachmentPreviewData[F]] + ): F[Response[F]] = { + import dsl._ + def notFound = + NotFound(BasicResult(false, "Not found")) + + QP.WithFallback.unapply(req.multiParams) match { + case Some(bool) => + val fallback = bool.getOrElse(false) + val inm = req.headers.get[`If-None-Match`].flatMap(_.tags) + val matches = matchETag(fileData.map(_.meta), inm) + + fileData + .map { data => + if (matches) withResponseHeaders(dsl, NotModified())(data) + else makeByteResp(dsl)(data) + } + .getOrElse( + if (fallback) BinaryUtil.noPreview(req.some).getOrElseF(notFound) + else notFound + ) + + case None => + BadRequest(BasicResult(false, "Invalid query parameter 'withFallback'")) + } + } + def withResponseHeaders[F[_]: Sync](dsl: Http4sDsl[F], resp: F[Response[F]])( data: OItemSearch.BinaryData[F] ): F[Response[F]] = { diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala index 9b5d52aa..63b818cd 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala @@ -17,7 +17,6 @@ import docspell.common.MakePreviewArgs import docspell.restapi.model._ import docspell.restserver.conv.Conversions import docspell.restserver.http4s.BinaryUtil -import docspell.restserver.http4s.{QueryParam => QP} import docspell.restserver.webapp.Webjars import org.http4s._ @@ -115,25 +114,11 @@ object AttachmentRoutes { .getOrElse(NotFound(BasicResult(false, "Not found"))) } yield resp - case req @ GET -> Root / Ident(id) / "preview" :? QP.WithFallback(flag) => - def notFound = - NotFound(BasicResult(false, "Not found")) + case req @ GET -> Root / Ident(id) / "preview" => for { fileData <- backend.itemSearch.findAttachmentPreview(id, user.account.collective) - inm = req.headers.get[`If-None-Match`].flatMap(_.tags) - matches = BinaryUtil.matchETag(fileData.map(_.meta), inm) - fallback = flag.getOrElse(false) - resp <- - fileData - .map { data => - if (matches) withResponseHeaders(NotModified())(data) - else makeByteResp(data) - } - .getOrElse( - if (fallback) BinaryUtil.noPreview(req.some).getOrElseF(notFound) - else notFound - ) + resp <- BinaryUtil.respond(dsl, req)(fileData) } yield resp case HEAD -> Root / Ident(id) / "preview" => diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala index 8ea503bd..5277d0c8 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -452,7 +452,7 @@ object ItemRoutes { } } - private def searchItemStats[F[_]: Sync]( + def searchItemStats[F[_]: Sync]( backend: BackendApp[F], dsl: Http4sDsl[F] )( diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala new file mode 100644 index 00000000..af02bf16 --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.restserver.routes + +import cats.effect._ +import cats.implicits._ + +import docspell.backend.BackendApp +import docspell.backend.auth.ShareToken +import docspell.common._ +import docspell.restserver.http4s.BinaryUtil + +import org.http4s.HttpRoutes +import org.http4s.dsl.Http4sDsl + +object ShareAttachmentRoutes { + + def apply[F[_]: Async]( + backend: BackendApp[F], + token: ShareToken + ): HttpRoutes[F] = { + val dsl = new Http4sDsl[F] {} + import dsl._ + + HttpRoutes.of { case req @ GET -> Root / Ident(id) / "preview" => + for { + fileData <- + backend.share.findAttachmentPreview(id, token.id).value + resp <- BinaryUtil.respond(dsl, req)(fileData) + } yield resp + } + } +} diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala index a0ffa3a9..39a45412 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala @@ -54,7 +54,7 @@ object ShareSearchRoutes { cfg.maxNoteLength, searchMode = SearchMode.Normal ) - account = AccountId(share.cid, Ident.unsafe("")) + account = share.asAccount fixQuery = Query.Fix(account, Some(share.query.expr), None) _ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}") resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery) From e961a5ac10f5854e829c05d61c4ace39dca56b7d Mon Sep 17 00:00:00 2001 From: eikek Date: Tue, 5 Oct 2021 10:27:21 +0200 Subject: [PATCH 12/37] Use search stats to populate search menu --- .../scala/docspell/backend/BackendApp.scala | 2 +- .../scala/docspell/backend/ops/OShare.scala | 31 +++++- .../src/main/resources/docspell-openapi.yml | 70 ++++++++++++- .../restserver/http4s/BinaryUtil.scala | 14 ++- .../restserver/routes/AttachmentRoutes.scala | 5 +- .../restserver/routes/ItemRoutes.scala | 11 ++- .../routes/ShareAttachmentRoutes.scala | 20 ++-- .../restserver/routes/ShareSearchRoutes.scala | 99 +++++++++++++------ .../store/queries/SearchSummary.scala | 12 ++- modules/webapp/src/main/elm/Api.elm | 13 ++- .../webapp/src/main/elm/Comp/SearchMenu.elm | 6 ++ .../webapp/src/main/elm/Comp/TagSelect.elm | 7 ++ .../webapp/src/main/elm/Page/Share/Data.elm | 4 +- .../webapp/src/main/elm/Page/Share/Update.elm | 18 +++- 14 files changed, 257 insertions(+), 55 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala index 1e58654c..9037e138 100644 --- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala +++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala @@ -86,7 +86,7 @@ object BackendApp { customFieldsImpl <- OCustomFields(store) simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl) clientSettingsImpl <- OClientSettings(store) - shareImpl <- Resource.pure(OShare(store, itemSearchImpl)) + shareImpl <- Resource.pure(OShare(store, itemSearchImpl, simpleSearchImpl)) } yield new BackendApp[F] { val login = loginImpl val signup = signupImpl diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index 77b882f2..0f064c36 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -9,15 +9,19 @@ package docspell.backend.ops import cats.data.OptionT import cats.effect._ import cats.implicits._ + import docspell.backend.PasswordCrypt import docspell.backend.auth.ShareToken import docspell.backend.ops.OItemSearch.{AttachmentPreviewData, Batch, Query} import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} +import docspell.backend.ops.OSimpleSearch.StringSearchResult import docspell.common._ import docspell.query.ItemQuery import docspell.query.ItemQuery.Expr.AttachId import docspell.store.Store +import docspell.store.queries.SearchSummary import docspell.store.records.RShare + import scodec.bits.ByteVector trait OShare[F[_]] { @@ -51,6 +55,9 @@ trait OShare[F[_]] { shareId: Ident ): OptionT[F, AttachmentPreviewData[F]] + def searchSummary( + settings: OSimpleSearch.StatsSettings + )(shareId: Ident, q: ItemQueryString): OptionT[F, StringSearchResult[SearchSummary]] } object OShare { @@ -101,7 +108,11 @@ object OShare { def publishUntilInPast: ChangeResult = PublishUntilInPast } - def apply[F[_]: Async](store: Store[F], itemSearch: OItemSearch[F]): OShare[F] = + def apply[F[_]: Async]( + store: Store[F], + itemSearch: OItemSearch[F], + simpleSearch: OSimpleSearch[F] + ): OShare[F] = new OShare[F] { private[this] val logger = Logger.log4s[F](org.log4s.getLogger) @@ -238,5 +249,23 @@ object OShare { .mapFilter(_ => None) else OptionT(itemSearch.findAttachmentPreview(attachId, sq.cid)) } yield res + + def searchSummary( + settings: OSimpleSearch.StatsSettings + )( + shareId: Ident, + q: ItemQueryString + ): OptionT[F, StringSearchResult[SearchSummary]] = + findShareQuery(shareId) + .semiflatMap { share => + val fix = Query.Fix(share.asAccount, Some(share.query.expr), None) + simpleSearch + .searchSummaryByString(settings)(fix, q) + .map { + case StringSearchResult.Success(summary) => + StringSearchResult.Success(summary.onlyExisting) + case other => other + } + } } } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 2f563116..46a4766a 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1558,9 +1558,9 @@ paths: schema: $ref: "#/components/schemas/BasicResult" - /share/search: + /share/search/query: post: - operationId: "share-search" + operationId: "share-search-query" tags: [Share] summary: Performs a search in a share. description: | @@ -1581,6 +1581,72 @@ paths: application/json: schema: $ref: "#/components/schemas/ItemLightList" + /share/search/stats: + post: + operationId: "share-search-stats" + tags: [ Share ] + summary: Get basic statistics about search results. + description: | + Instead of returning the results of a query, uses it to return + a summary, constraint to the share. + security: + - shareTokenHeader: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ItemQuery" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/SearchStats" + /share/attachment/{id}/preview: + head: + operationId: "share-attach-check-preview" + tags: [ Attachment ] + summary: Get the headers to a preview image of an attachment file. + description: | + Checks if an image file showing a preview of the attachment is + available. If not available, a 404 is returned. + security: + - shareTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + 404: + description: NotFound + get: + operationId: "share-attach-get-preview" + tags: [ Attachment ] + summary: Get a preview image of an attachment file. + description: | + Gets a image file showing a preview of the attachment. Usually + it is a small image of the first page of the document.If not + available, a 404 is returned. However, if the query parameter + `withFallback` is `true`, a fallback preview image is + returned. You can also use the `HEAD` method to check for + existence. + + The attachment must be in the search results of the current + share. + security: + - shareTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/withFallback" + responses: + 200: + description: Ok + content: + application/octet-stream: + schema: + type: string + format: binary /admin/user/resetPassword: post: diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala index 91baf47e..208847a6 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala @@ -10,17 +10,18 @@ import cats.data.NonEmptyList import cats.data.OptionT import cats.effect._ import cats.implicits._ + import docspell.backend.ops.OItemSearch.AttachmentPreviewData import docspell.backend.ops._ import docspell.restapi.model.BasicResult -import docspell.store.records.RFileMeta import docspell.restserver.http4s.{QueryParam => QP} +import docspell.store.records.RFileMeta import org.http4s._ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl -import org.http4s.headers._ import org.http4s.headers.ETag.EntityTag +import org.http4s.headers._ import org.typelevel.ci.CIString object BinaryUtil { @@ -53,6 +54,15 @@ object BinaryUtil { } } + def respondHead[F[_]: Async]( + dsl: Http4sDsl[F] + )(fileData: Option[AttachmentPreviewData[F]]): F[Response[F]] = { + import dsl._ + fileData + .map(data => withResponseHeaders(dsl, Ok())(data)) + .getOrElse(NotFound(BasicResult(false, "Not found"))) + } + def withResponseHeaders[F[_]: Sync](dsl: Http4sDsl[F], resp: F[Response[F]])( data: OItemSearch.BinaryData[F] ): F[Response[F]] = { diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala index 63b818cd..cf4e23f7 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala @@ -125,10 +125,7 @@ object AttachmentRoutes { for { fileData <- backend.itemSearch.findAttachmentPreview(id, user.account.collective) - resp <- - fileData - .map(data => withResponseHeaders(Ok())(data)) - .getOrElse(NotFound(BasicResult(false, "Not found"))) + resp <- BinaryUtil.respondHead(dsl)(fileData) } yield resp case POST -> Root / Ident(id) / "preview" => diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala index 5277d0c8..5db15101 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -28,11 +28,11 @@ import docspell.restserver.http4s.BinaryUtil import docspell.restserver.http4s.Responses import docspell.restserver.http4s.{QueryParam => QP} -import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl import org.http4s.headers._ +import org.http4s.{HttpRoutes, Response} import org.log4s._ object ItemRoutes { @@ -415,7 +415,11 @@ object ItemRoutes { def searchItems[F[_]: Sync]( backend: BackendApp[F], dsl: Http4sDsl[F] - )(settings: OSimpleSearch.Settings, fixQuery: Query.Fix, itemQuery: ItemQueryString) = { + )( + settings: OSimpleSearch.Settings, + fixQuery: Query.Fix, + itemQuery: ItemQueryString + ): F[Response[F]] = { import dsl._ def convertFts(res: OSimpleSearch.Items.FtsItems): ItemLightList = @@ -459,7 +463,7 @@ object ItemRoutes { settings: OSimpleSearch.StatsSettings, fixQuery: Query.Fix, itemQuery: ItemQueryString - ) = { + ): F[Response[F]] = { import dsl._ backend.simpleSearch @@ -479,7 +483,6 @@ object ItemRoutes { case StringSearchResult.ParseFailed(pf) => BadRequest(BasicResult(false, s"Error reading query: ${pf.render}")) } - } implicit final class OptionString(opt: Option[String]) { diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala index af02bf16..d763530b 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala @@ -26,12 +26,20 @@ object ShareAttachmentRoutes { val dsl = new Http4sDsl[F] {} import dsl._ - HttpRoutes.of { case req @ GET -> Root / Ident(id) / "preview" => - for { - fileData <- - backend.share.findAttachmentPreview(id, token.id).value - resp <- BinaryUtil.respond(dsl, req)(fileData) - } yield resp + HttpRoutes.of { + case req @ GET -> Root / Ident(id) / "preview" => + for { + fileData <- + backend.share.findAttachmentPreview(id, token.id).value + resp <- BinaryUtil.respond(dsl, req)(fileData) + } yield resp + + case HEAD -> Root / Ident(id) / "preview" => + for { + fileData <- + backend.share.findAttachmentPreview(id, token.id).value + resp <- BinaryUtil.respondHead(dsl)(fileData) + } yield resp } } } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala index 39a45412..96202f14 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala @@ -12,15 +12,19 @@ import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.ShareToken import docspell.backend.ops.OSimpleSearch +import docspell.backend.ops.OSimpleSearch.StringSearchResult import docspell.common._ -import docspell.restapi.model.ItemQuery +import docspell.query.FulltextExtract.Result.{TooMany, UnsupportedPosition} +import docspell.restapi.model._ import docspell.restserver.Config +import docspell.restserver.conv.Conversions import docspell.store.qb.Batch -import docspell.store.queries.Query +import docspell.store.queries.{Query, SearchSummary} -import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityDecoder._ +import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl +import org.http4s.{HttpRoutes, Response} object ShareSearchRoutes { @@ -34,33 +38,68 @@ object ShareSearchRoutes { val dsl = new Http4sDsl[F] {} import dsl._ - HttpRoutes.of { case req @ POST -> Root => - backend.share - .findShareQuery(token.id) - .semiflatMap { share => - for { - userQuery <- req.as[ItemQuery] - batch = Batch( - userQuery.offset.getOrElse(0), - userQuery.limit.getOrElse(cfg.maxItemPageSize) - ).restrictLimitTo( - cfg.maxItemPageSize - ) - itemQuery = ItemQueryString(userQuery.query) - settings = OSimpleSearch.Settings( - batch, - cfg.fullTextSearch.enabled, - userQuery.withDetails.getOrElse(false), - cfg.maxNoteLength, - searchMode = SearchMode.Normal - ) - account = share.asAccount - fixQuery = Query.Fix(account, Some(share.query.expr), None) - _ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}") - resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery) - } yield resp - } - .getOrElseF(NotFound()) + HttpRoutes.of { + case req @ POST -> Root / "query" => + backend.share + .findShareQuery(token.id) + .semiflatMap { share => + for { + userQuery <- req.as[ItemQuery] + batch = Batch( + userQuery.offset.getOrElse(0), + userQuery.limit.getOrElse(cfg.maxItemPageSize) + ).restrictLimitTo( + cfg.maxItemPageSize + ) + itemQuery = ItemQueryString(userQuery.query) + settings = OSimpleSearch.Settings( + batch, + cfg.fullTextSearch.enabled, + userQuery.withDetails.getOrElse(false), + cfg.maxNoteLength, + searchMode = SearchMode.Normal + ) + account = share.asAccount + fixQuery = Query.Fix(account, Some(share.query.expr), None) + _ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}") + resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery) + } yield resp + } + .getOrElseF(NotFound()) + + case req @ POST -> Root / "stats" => + for { + userQuery <- req.as[ItemQuery] + itemQuery = ItemQueryString(userQuery.query) + settings = OSimpleSearch.StatsSettings( + useFTS = cfg.fullTextSearch.enabled, + searchMode = userQuery.searchMode.getOrElse(SearchMode.Normal) + ) + stats <- backend.share.searchSummary(settings)(token.id, itemQuery).value + resp <- stats.map(mkSummaryResponse(dsl)).getOrElse(NotFound()) + } yield resp } } + + def mkSummaryResponse[F[_]: Sync]( + dsl: Http4sDsl[F] + )(r: StringSearchResult[SearchSummary]): F[Response[F]] = { + import dsl._ + r match { + case StringSearchResult.Success(summary) => + Ok(Conversions.mkSearchStats(summary)) + case StringSearchResult.FulltextMismatch(TooMany) => + BadRequest(BasicResult(false, "Fulltext search is not possible in this share.")) + case StringSearchResult.FulltextMismatch(UnsupportedPosition) => + BadRequest( + BasicResult( + false, + "Fulltext search must be in root position or inside the first AND." + ) + ) + case StringSearchResult.ParseFailed(pf) => + BadRequest(BasicResult(false, s"Error reading query: ${pf.render}")) + } + } + } diff --git a/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala b/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala index 0b6a1b1c..1eeaef2e 100644 --- a/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala +++ b/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala @@ -12,4 +12,14 @@ case class SearchSummary( cats: List[CategoryCount], fields: List[FieldStats], folders: List[FolderCount] -) +) { + + def onlyExisting: SearchSummary = + SearchSummary( + count, + tags.filter(_.count > 0), + cats.filter(_.count > 0), + fields.filter(_.count > 0), + folders.filter(_.count > 0) + ) +} diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 226b3740..a049bce3 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -114,6 +114,7 @@ module Api exposing , restoreItem , saveClientSettings , searchShare + , searchShareStats , sendMail , setAttachmentName , setCollectiveSettings @@ -2283,13 +2284,23 @@ verifyShare flags secret receive = searchShare : Flags -> String -> ItemQuery -> (Result Http.Error ItemLightList -> msg) -> Cmd msg searchShare flags token search receive = Http2.sharePost - { url = flags.config.baseUrl ++ "/api/v1/share/search" + { url = flags.config.baseUrl ++ "/api/v1/share/search/query" , token = token , body = Http.jsonBody (Api.Model.ItemQuery.encode search) , expect = Http.expectJson receive Api.Model.ItemLightList.decoder } +searchShareStats : Flags -> String -> ItemQuery -> (Result Http.Error SearchStats -> msg) -> Cmd msg +searchShareStats flags token search receive = + Http2.sharePost + { url = flags.config.baseUrl ++ "/api/v1/share/search/stats" + , token = token + , body = Http.jsonBody (Api.Model.ItemQuery.encode search) + , expect = Http.expectJson receive Api.Model.SearchStats.decoder + } + + shareAttachmentPreviewURL : String -> String shareAttachmentPreviewURL id = "/api/v1/share/attachment/" ++ id ++ "/preview?withFallback=true" diff --git a/modules/webapp/src/main/elm/Comp/SearchMenu.elm b/modules/webapp/src/main/elm/Comp/SearchMenu.elm index 5a2aa5f7..c70a1c3d 100644 --- a/modules/webapp/src/main/elm/Comp/SearchMenu.elm +++ b/modules/webapp/src/main/elm/Comp/SearchMenu.elm @@ -15,6 +15,7 @@ module Comp.SearchMenu exposing , isFulltextSearch , isNamesSearch , linkTargetMsg + , setFromStats , textSearchString , update , updateDrop @@ -379,6 +380,11 @@ type Msg | ToggleOpenAllAkkordionTabs +setFromStats : SearchStats -> Msg +setFromStats stats = + GetStatsResp (Ok stats) + + linkTargetMsg : LinkTarget -> Maybe Msg linkTargetMsg linkTarget = case linkTarget of diff --git a/modules/webapp/src/main/elm/Comp/TagSelect.elm b/modules/webapp/src/main/elm/Comp/TagSelect.elm index 62f2a65d..3fe96ac1 100644 --- a/modules/webapp/src/main/elm/Comp/TagSelect.elm +++ b/modules/webapp/src/main/elm/Comp/TagSelect.elm @@ -245,6 +245,12 @@ makeWorkModel sel model = } +noEmptyTags : Model -> Bool +noEmptyTags model = + Dict.filter (\k -> \v -> v.count == 0) model.availableTags + |> Dict.isEmpty + + type Msg = ToggleTag String | ToggleCat String @@ -422,6 +428,7 @@ viewTagsDrop2 texts ddm wm settings model = [ a [ class S.secondaryBasicButtonPlain , class "border rounded flex-none px-1 py-1" + , classList [ ( "hidden", noEmptyTags model ) ] , href "#" , onClick ToggleShowEmpty ] diff --git a/modules/webapp/src/main/elm/Page/Share/Data.elm b/modules/webapp/src/main/elm/Page/Share/Data.elm index 0689e9dd..eb47a41c 100644 --- a/modules/webapp/src/main/elm/Page/Share/Data.elm +++ b/modules/webapp/src/main/elm/Page/Share/Data.elm @@ -9,6 +9,7 @@ module Page.Share.Data exposing (Mode(..), Model, Msg(..), PageError(..), init) import Api import Api.Model.ItemLightList exposing (ItemLightList) +import Api.Model.SearchStats exposing (SearchStats) import Api.Model.ShareSecret exposing (ShareSecret) import Api.Model.ShareVerifyResult exposing (ShareVerifyResult) import Comp.ItemCardList @@ -41,7 +42,6 @@ type alias Model = , verifyResult : ShareVerifyResult , passwordModel : PasswordModel , pageError : PageError - , items : ItemLightList , searchMenuModel : Comp.SearchMenu.Model , powerSearchInput : Comp.PowerSearchInput.Model , searchInProgress : Bool @@ -58,7 +58,6 @@ emptyModel flags = , passwordFailed = False } , pageError = PageErrorNone - , items = Api.Model.ItemLightList.empty , searchMenuModel = Comp.SearchMenu.init flags , powerSearchInput = Comp.PowerSearchInput.init , searchInProgress = False @@ -79,6 +78,7 @@ init shareId flags = type Msg = VerifyResp (Result Http.Error ShareVerifyResult) | SearchResp (Result Http.Error ItemLightList) + | StatsResp (Result Http.Error SearchStats) | SetPassword String | SubmitPassword | SearchMenuMsg Comp.SearchMenu.Msg diff --git a/modules/webapp/src/main/elm/Page/Share/Update.elm b/modules/webapp/src/main/elm/Page/Share/Update.elm index 56070017..01b7ec73 100644 --- a/modules/webapp/src/main/elm/Page/Share/Update.elm +++ b/modules/webapp/src/main/elm/Page/Share/Update.elm @@ -91,6 +91,16 @@ update flags settings shareId msg model = SearchResp (Err err) -> noSub ( { model | pageError = PageErrorHttp err, searchInProgress = False }, Cmd.none ) + StatsResp (Ok stats) -> + update flags + settings + shareId + (SearchMenuMsg (Comp.SearchMenu.setFromStats stats)) + model + + StatsResp (Err err) -> + noSub ( { model | pageError = PageErrorHttp err }, Cmd.none ) + SetPassword pw -> let pm = @@ -191,8 +201,14 @@ makeSearchCmd flags model = , query = Q.renderMaybe mq , searchMode = Just (Data.SearchMode.asString Data.SearchMode.Normal) } + + searchCmd = + Api.searchShare flags model.verifyResult.token (request xq) SearchResp + + statsCmd = + Api.searchShareStats flags model.verifyResult.token (request xq) StatsResp in - Api.searchShare flags model.verifyResult.token (request xq) SearchResp + Cmd.batch [ searchCmd, statsCmd ] linkTargetMsg : LinkTarget -> Maybe Msg From 813797756cffe1d0f89b7618639b2dabf2123601 Mon Sep 17 00:00:00 2001 From: eikek Date: Tue, 5 Oct 2021 13:50:31 +0200 Subject: [PATCH 13/37] Extend search stats to fully populate search menu Refs: #856 --- .../src/main/resources/docspell-openapi.yml | 34 +++++++++++++ .../restserver/conv/Conversions.scala | 14 ++++-- .../docspell/store/queries/IdRefCount.scala | 5 ++ .../scala/docspell/store/queries/QItem.scala | 50 ++++++++++++++++++- .../store/queries/SearchSummary.scala | 12 ++++- .../main/elm/Comp/CustomFieldMultiInput.elm | 6 +++ .../webapp/src/main/elm/Comp/SearchMenu.elm | 42 ++++++++++++++++ .../webapp/src/main/elm/Util/CustomField.elm | 12 +++++ 8 files changed, 167 insertions(+), 8 deletions(-) create mode 100644 modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 46a4766a..0c0dd351 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -5305,6 +5305,10 @@ components: - tagCategoryCloud - fieldStats - folderStats + - corrOrgStats + - corrPersStats + - concPersStats + - concEquipStats properties: count: type: integer @@ -5321,6 +5325,23 @@ components: type: array items: $ref: "#/components/schemas/FolderStats" + corrOrgStats: + type: array + items: + $ref: "#/components/schemas/IdRefStats" + corrPersStats: + type: array + items: + $ref: "#/components/schemas/IdRefStats" + concPersStats: + type: array + items: + $ref: "#/components/schemas/IdRefStats" + concEquipStats: + type: array + items: + $ref: "#/components/schemas/IdRefStats" + ItemInsights: description: | Information about the items in docspell. @@ -5454,6 +5475,19 @@ components: type: integer format: int32 + IdRefStats: + description: | + Counting some objects that have an id and a name. + required: + - ref + - count + properties: + ref: + $ref: "#/components/schemas/IdName" + count: + type: integer + format: int32 + AttachmentMeta: description: | Extracted meta data of an attachment. diff --git a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala index 7cc03c6b..be89e955 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala @@ -7,11 +7,9 @@ package docspell.restserver.conv import java.time.{LocalDate, ZoneId} - import cats.effect.{Async, Sync} import cats.implicits._ import fs2.Stream - import docspell.backend.ops.OCollective.{InsightData, PassChangeResult} import docspell.backend.ops.OCustomFields.SetValueResult import docspell.backend.ops.OJob.JobCancelResult @@ -22,10 +20,9 @@ import docspell.common.syntax.all._ import docspell.ftsclient.FtsResult import docspell.restapi.model._ import docspell.restserver.conv.Conversions._ -import docspell.store.queries.{AttachmentLight => QAttachmentLight} +import docspell.store.queries.{AttachmentLight => QAttachmentLight, IdRefCount} import docspell.store.records._ import docspell.store.{AddResult, UpdateResult} - import org.http4s.headers.`Content-Type` import org.http4s.multipart.Multipart import org.log4s.Logger @@ -38,9 +35,16 @@ trait Conversions { mkTagCloud(sum.tags), mkTagCategoryCloud(sum.cats), sum.fields.map(mkFieldStats), - sum.folders.map(mkFolderStats) + sum.folders.map(mkFolderStats), + sum.corrOrgs.map(mkIdRefStats), + sum.corrPers.map(mkIdRefStats), + sum.concPers.map(mkIdRefStats), + sum.concEquip.map(mkIdRefStats) ) + def mkIdRefStats(s: IdRefCount): IdRefStats = + IdRefStats(mkIdName(s.ref), s.count) + def mkFolderStats(fs: docspell.store.queries.FolderCount): FolderStats = FolderStats(fs.id, fs.name, mkIdName(fs.owner), fs.count) diff --git a/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala b/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala new file mode 100644 index 00000000..20c2fbdf --- /dev/null +++ b/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala @@ -0,0 +1,5 @@ +package docspell.store.queries + +import docspell.common._ + +final case class IdRefCount(ref: IdRef, count: Int) {} diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index 2671fcaa..623b68e0 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -192,7 +192,21 @@ object QItem { cats <- searchTagCategorySummary(today)(q) fields <- searchFieldSummary(today)(q) folders <- searchFolderSummary(today)(q) - } yield SearchSummary(count, tags, cats, fields, folders) + orgs <- searchCorrOrgSummary(today)(q) + corrPers <- searchCorrPersonSummary(today)(q) + concPers <- searchConcPersonSummary(today)(q) + concEquip <- searchConcEquipSummary(today)(q) + } yield SearchSummary( + count, + tags, + cats, + fields, + folders, + orgs, + corrPers, + concPers, + concEquip + ) def searchTagCategorySummary( today: LocalDate @@ -251,6 +265,40 @@ object QItem { .query[Int] .unique + def searchCorrOrgSummary(today: LocalDate)(q: Query): ConnectionIO[List[IdRefCount]] = + searchIdRefSummary(org.oid, org.name, i.corrOrg, today)(q) + + def searchCorrPersonSummary(today: LocalDate)( + q: Query + ): ConnectionIO[List[IdRefCount]] = + searchIdRefSummary(pers0.pid, pers0.name, i.corrPerson, today)(q) + + def searchConcPersonSummary(today: LocalDate)( + q: Query + ): ConnectionIO[List[IdRefCount]] = + searchIdRefSummary(pers1.pid, pers1.name, i.concPerson, today)(q) + + def searchConcEquipSummary(today: LocalDate)( + q: Query + ): ConnectionIO[List[IdRefCount]] = + searchIdRefSummary(equip.eid, equip.name, i.concEquipment, today)(q) + + private def searchIdRefSummary( + idCol: Column[Ident], + nameCol: Column[String], + fkCol: Column[Ident], + today: LocalDate + )(q: Query): ConnectionIO[List[IdRefCount]] = + findItemsBase(q.fix, today, 0).unwrap + .withSelect(select(idCol, nameCol).append(count(idCol).as("num"))) + .changeWhere(c => + c && fkCol.isNotNull && queryCondition(today, q.fix.account.collective, q.cond) + ) + .groupBy(idCol, nameCol) + .build + .query[IdRefCount] + .to[List] + def searchFolderSummary(today: LocalDate)(q: Query): ConnectionIO[List[FolderCount]] = { val fu = RUser.as("fu") findItemsBase(q.fix, today, 0).unwrap diff --git a/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala b/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala index 1eeaef2e..c6bff383 100644 --- a/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala +++ b/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala @@ -11,7 +11,11 @@ case class SearchSummary( tags: List[TagCount], cats: List[CategoryCount], fields: List[FieldStats], - folders: List[FolderCount] + folders: List[FolderCount], + corrOrgs: List[IdRefCount], + corrPers: List[IdRefCount], + concPers: List[IdRefCount], + concEquip: List[IdRefCount] ) { def onlyExisting: SearchSummary = @@ -20,6 +24,10 @@ case class SearchSummary( tags.filter(_.count > 0), cats.filter(_.count > 0), fields.filter(_.count > 0), - folders.filter(_.count > 0) + folders.filter(_.count > 0), + corrOrgs = corrOrgs.filter(_.count > 0), + corrPers = corrPers.filter(_.count > 0), + concPers = concPers.filter(_.count > 0), + concEquip = concEquip.filter(_.count > 0) ) } diff --git a/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm b/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm index 27d11480..6a60260e 100644 --- a/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm +++ b/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm @@ -16,6 +16,7 @@ module Comp.CustomFieldMultiInput exposing , isEmpty , nonEmpty , reset + , setOptions , setValues , update , updateSearch @@ -125,6 +126,11 @@ setValues values = SetValues values +setOptions : List CustomField -> Msg +setOptions fields = + CustomFieldResp (Ok (CustomFieldList fields)) + + reset : Model -> Model reset model = let diff --git a/modules/webapp/src/main/elm/Comp/SearchMenu.elm b/modules/webapp/src/main/elm/Comp/SearchMenu.elm index c70a1c3d..890cf25b 100644 --- a/modules/webapp/src/main/elm/Comp/SearchMenu.elm +++ b/modules/webapp/src/main/elm/Comp/SearchMenu.elm @@ -60,6 +60,7 @@ import Http import Messages.Comp.SearchMenu exposing (Texts) import Set exposing (Set) import Styles as S +import Util.CustomField import Util.Html exposing (KeyCode(..)) import Util.ItemDragDrop as DD import Util.Maybe @@ -564,6 +565,42 @@ updateDrop ddm flags settings msg model = selectModel = Comp.TagSelect.modifyCount model.tagSelectModel tagCount catCount + orgOpts = + Comp.Dropdown.update (Comp.Dropdown.SetOptions (List.map .ref stats.corrOrgStats)) + model.orgModel + |> Tuple.first + + corrPersOpts = + Comp.Dropdown.update (Comp.Dropdown.SetOptions (List.map .ref stats.corrPersStats)) + model.corrPersonModel + |> Tuple.first + + concPersOpts = + Comp.Dropdown.update (Comp.Dropdown.SetOptions (List.map .ref stats.concPersStats)) + model.concPersonModel + |> Tuple.first + + concEquipOpts = + let + mkEquip ref = + Equipment ref.id ref.name 0 Nothing "" + in + Comp.Dropdown.update + (Comp.Dropdown.SetOptions + (List.map (.ref >> mkEquip) stats.concEquipStats) + ) + model.concEquipmentModel + |> Tuple.first + + fields = + Util.CustomField.statsToFields stats + + fieldOpts = + Comp.CustomFieldMultiInput.update flags + (Comp.CustomFieldMultiInput.setOptions fields) + model.customFieldModel + |> .model + model_ = { model | tagSelectModel = selectModel @@ -571,6 +608,11 @@ updateDrop ddm flags settings msg model = Comp.FolderSelect.modify model.selectedFolder model.folderList stats.folderStats + , orgModel = orgOpts + , corrPersonModel = corrPersOpts + , concPersonModel = concPersOpts + , concEquipmentModel = concEquipOpts + , customFieldModel = fieldOpts } in { model = model_ diff --git a/modules/webapp/src/main/elm/Util/CustomField.elm b/modules/webapp/src/main/elm/Util/CustomField.elm index fc121f62..cfe58d92 100644 --- a/modules/webapp/src/main/elm/Util/CustomField.elm +++ b/modules/webapp/src/main/elm/Util/CustomField.elm @@ -10,9 +10,12 @@ module Util.CustomField exposing , nameOrLabel , renderValue , renderValue2 + , statsToFields ) +import Api.Model.CustomField exposing (CustomField) import Api.Model.ItemFieldValue exposing (ItemFieldValue) +import Api.Model.SearchStats exposing (SearchStats) import Data.CustomFieldType import Data.Icons as Icons import Html exposing (..) @@ -20,6 +23,15 @@ import Html.Attributes exposing (..) import Html.Events exposing (onClick) +statsToFields : SearchStats -> List CustomField +statsToFields stats = + let + mkField fs = + CustomField fs.id fs.name fs.label fs.ftype fs.count 0 + in + List.map mkField stats.fieldStats + + {-| This is how the server wants the value to a bool custom field -} boolValue : Bool -> String From 4ad90b76b49d4b6b93681357c9973fc44290ec68 Mon Sep 17 00:00:00 2001 From: eikek Date: Wed, 6 Oct 2021 00:26:53 +0200 Subject: [PATCH 14/37] Fix tag menu when restricting results When search results are restricted in a share view, tags may disappear and thus the tags from the beginning need to be kept. --- .../webapp/src/main/elm/Comp/SearchMenu.elm | 23 ++++++------ .../webapp/src/main/elm/Comp/TagSelect.elm | 35 +++++++++++++++++++ .../src/main/elm/Page/Home/SideMenu.elm | 8 ++++- .../src/main/elm/Page/Share/Sidebar.elm | 15 ++++++++ 4 files changed, 70 insertions(+), 11 deletions(-) diff --git a/modules/webapp/src/main/elm/Comp/SearchMenu.elm b/modules/webapp/src/main/elm/Comp/SearchMenu.elm index 890cf25b..019987d7 100644 --- a/modules/webapp/src/main/elm/Comp/SearchMenu.elm +++ b/modules/webapp/src/main/elm/Comp/SearchMenu.elm @@ -9,6 +9,7 @@ module Comp.SearchMenu exposing ( Model , Msg(..) , NextState + , SearchTab(..) , TextSearchModel , getItemQuery , init @@ -563,7 +564,7 @@ updateDrop ddm flags settings msg model = List.sortBy .count stats.tagCategoryCloud.items selectModel = - Comp.TagSelect.modifyCount model.tagSelectModel tagCount catCount + Comp.TagSelect.modifyCountKeepExisting model.tagSelectModel tagCount catCount orgOpts = Comp.Dropdown.update (Comp.Dropdown.SetOptions (List.map .ref stats.corrOrgStats)) @@ -1044,15 +1045,20 @@ updateDrop ddm flags settings msg model = --- View2 -viewDrop2 : Texts -> DD.DragDropData -> Flags -> UiSettings -> Model -> Html Msg -viewDrop2 texts ddd flags settings model = +type alias ViewConfig = + { overrideTabLook : SearchTab -> Comp.Tabs.Look -> Comp.Tabs.Look + } + + +viewDrop2 : Texts -> DD.DragDropData -> Flags -> ViewConfig -> UiSettings -> Model -> Html Msg +viewDrop2 texts ddd flags cfg settings model = let akkordionStyle = Comp.Tabs.searchMenuStyle in Comp.Tabs.akkordion akkordionStyle - (searchTabState settings model) + (searchTabState settings cfg model) (searchTabs texts ddd flags settings model) @@ -1254,12 +1260,9 @@ tabLook settings model tab = Comp.Tabs.Normal -searchTabState : UiSettings -> Model -> Comp.Tabs.Tab Msg -> ( Comp.Tabs.State, Msg ) -searchTabState settings model tab = +searchTabState : UiSettings -> ViewConfig -> Model -> Comp.Tabs.Tab Msg -> ( Comp.Tabs.State, Msg ) +searchTabState settings cfg model tab = let - isHidden f = - Data.UiSettings.fieldHidden settings f - searchTab = findTab tab @@ -1273,7 +1276,7 @@ searchTabState settings model tab = state = { folded = folded , look = - Maybe.map (tabLook settings model) searchTab + Maybe.map (\t -> tabLook settings model t |> cfg.overrideTabLook t) searchTab |> Maybe.withDefault Comp.Tabs.Normal } in diff --git a/modules/webapp/src/main/elm/Comp/TagSelect.elm b/modules/webapp/src/main/elm/Comp/TagSelect.elm index 3fe96ac1..07d76380 100644 --- a/modules/webapp/src/main/elm/Comp/TagSelect.elm +++ b/modules/webapp/src/main/elm/Comp/TagSelect.elm @@ -16,6 +16,7 @@ module Comp.TagSelect exposing , makeWorkModel , modifyAll , modifyCount + , modifyCountKeepExisting , reset , toggleTag , update @@ -99,6 +100,40 @@ modifyCount model tags cats = } +modifyCountKeepExisting : Model -> List TagCount -> List NameCount -> Model +modifyCountKeepExisting model tags cats = + let + tagZeros : Dict String TagCount + tagZeros = + Dict.map (\_ -> \tc -> TagCount tc.tag 0) model.availableTags + + tagAvail = + List.foldl (\tc -> \dict -> Dict.insert tc.tag.id tc dict) tagZeros tags + + tcs = + Dict.values tagAvail + + catcs = + List.filterMap (\e -> Maybe.map (\k -> CategoryCount k e.count) e.name) cats + + catZeros : Dict String CategoryCount + catZeros = + Dict.map (\_ -> \cc -> CategoryCount cc.name 0) model.availableCats + + catAvail = + List.foldl (\cc -> \dict -> Dict.insert cc.name cc dict) catZeros catcs + + ccs = + Dict.values catAvail + in + { model + | tagCounts = tcs + , availableTags = tagAvail + , categoryCounts = ccs + , availableCats = catAvail + } + + reset : Model -> Model reset model = { model diff --git a/modules/webapp/src/main/elm/Page/Home/SideMenu.elm b/modules/webapp/src/main/elm/Page/Home/SideMenu.elm index f5690952..5ad19c47 100644 --- a/modules/webapp/src/main/elm/Page/Home/SideMenu.elm +++ b/modules/webapp/src/main/elm/Page/Home/SideMenu.elm @@ -82,10 +82,16 @@ viewSearch texts flags settings model = , end = [] , rootClasses = "my-1 text-xs hidden sm:flex" } - , Html.map SearchMenuMsg + , let + searchMenuCfg = + { overrideTabLook = \_ -> identity + } + in + Html.map SearchMenuMsg (Comp.SearchMenu.viewDrop2 texts.searchMenu model.dragDropData flags + searchMenuCfg settings model.searchMenuModel ) diff --git a/modules/webapp/src/main/elm/Page/Share/Sidebar.elm b/modules/webapp/src/main/elm/Page/Share/Sidebar.elm index 31a8ee1b..11c31457 100644 --- a/modules/webapp/src/main/elm/Page/Share/Sidebar.elm +++ b/modules/webapp/src/main/elm/Page/Share/Sidebar.elm @@ -8,6 +8,7 @@ module Page.Share.Sidebar exposing (..) import Comp.SearchMenu +import Comp.Tabs import Data.Flags exposing (Flags) import Data.UiSettings exposing (UiSettings) import Html exposing (..) @@ -19,6 +20,19 @@ import Util.ItemDragDrop view : Texts -> Flags -> UiSettings -> Model -> Html Msg view texts flags settings model = + let + hideTrashTab tab default = + case tab of + Comp.SearchMenu.TabTrashed -> + Comp.Tabs.Hidden + + _ -> + default + + searchMenuCfg = + { overrideTabLook = hideTrashTab + } + in div [ class "flex flex-col" ] @@ -26,6 +40,7 @@ view texts flags settings model = (Comp.SearchMenu.viewDrop2 texts.searchMenu ddDummy flags + searchMenuCfg settings model.searchMenuModel ) From 9eb2f9c6fe9ee1104e1d2631ad05433ce60bb44c Mon Sep 17 00:00:00 2001 From: eikek Date: Wed, 6 Oct 2021 00:48:48 +0200 Subject: [PATCH 15/37] Implement binary routes for shares --- .../scala/docspell/backend/ops/OShare.scala | 48 ++++++++++++------- .../restserver/conv/Conversions.scala | 3 ++ .../restserver/http4s/BinaryUtil.scala | 29 ++++++++++- .../restserver/routes/AttachmentRoutes.scala | 19 ++------ .../routes/ShareAttachmentRoutes.scala | 33 ++++++++++--- .../docspell/store/queries/IdRefCount.scala | 6 +++ 6 files changed, 97 insertions(+), 41 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index 0f064c36..75e899a6 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -12,7 +12,7 @@ import cats.implicits._ import docspell.backend.PasswordCrypt import docspell.backend.auth.ShareToken -import docspell.backend.ops.OItemSearch.{AttachmentPreviewData, Batch, Query} +import docspell.backend.ops.OItemSearch._ import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} import docspell.backend.ops.OSimpleSearch.StringSearchResult import docspell.common._ @@ -55,6 +55,8 @@ trait OShare[F[_]] { shareId: Ident ): OptionT[F, AttachmentPreviewData[F]] + def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] + def searchSummary( settings: OSimpleSearch.StatsSettings )(shareId: Ident, q: ItemQueryString): OptionT[F, StringSearchResult[SearchSummary]] @@ -232,24 +234,36 @@ object OShare { ): OptionT[F, AttachmentPreviewData[F]] = for { sq <- findShareQuery(shareId) - account = sq.asAccount - checkQuery = Query( - Query.Fix(account, Some(sq.query.expr), None), - Query.QueryExpr(AttachId(attachId.id)) - ) - checkRes <- OptionT.liftF(itemSearch.findItems(0)(checkQuery, Batch.limit(1))) - res <- - if (checkRes.isEmpty) - OptionT - .liftF( - logger.info( - s"Attempt to load unshared attachment '${attachId.id}' for share: ${shareId.id}" - ) - ) - .mapFilter(_ => None) - else OptionT(itemSearch.findAttachmentPreview(attachId, sq.cid)) + _ <- checkAttachment(sq, attachId) + res <- OptionT(itemSearch.findAttachmentPreview(attachId, sq.cid)) } yield res + def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] = + for { + sq <- findShareQuery(shareId) + _ <- checkAttachment(sq, attachId) + res <- OptionT(itemSearch.findAttachment(attachId, sq.cid)) + } yield res + + /** Check whether the attachment with the given id is in the results of the given + * share + */ + private def checkAttachment(sq: ShareQuery, attachId: Ident): OptionT[F, Unit] = { + val checkQuery = Query( + Query.Fix(sq.asAccount, Some(sq.query.expr), None), + Query.QueryExpr(AttachId(attachId.id)) + ) + OptionT( + itemSearch + .findItems(0)(checkQuery, Batch.limit(1)) + .map(_.headOption.map(_ => ())) + ).flatTapNone( + logger.info( + s"Attempt to load unshared attachment '${attachId.id}' via share: ${sq.id.id}" + ) + ) + } + def searchSummary( settings: OSimpleSearch.StatsSettings )( diff --git a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala index be89e955..03142eaf 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala @@ -7,9 +7,11 @@ package docspell.restserver.conv import java.time.{LocalDate, ZoneId} + import cats.effect.{Async, Sync} import cats.implicits._ import fs2.Stream + import docspell.backend.ops.OCollective.{InsightData, PassChangeResult} import docspell.backend.ops.OCustomFields.SetValueResult import docspell.backend.ops.OJob.JobCancelResult @@ -23,6 +25,7 @@ import docspell.restserver.conv.Conversions._ import docspell.store.queries.{AttachmentLight => QAttachmentLight, IdRefCount} import docspell.store.records._ import docspell.store.{AddResult, UpdateResult} + import org.http4s.headers.`Content-Type` import org.http4s.multipart.Multipart import org.log4s.Logger diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala index 208847a6..7ebdb9b3 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala @@ -11,7 +11,7 @@ import cats.data.OptionT import cats.effect._ import cats.implicits._ -import docspell.backend.ops.OItemSearch.AttachmentPreviewData +import docspell.backend.ops.OItemSearch.{AttachmentData, AttachmentPreviewData} import docspell.backend.ops._ import docspell.restapi.model.BasicResult import docspell.restserver.http4s.{QueryParam => QP} @@ -27,6 +27,31 @@ import org.typelevel.ci.CIString object BinaryUtil { def respond[F[_]: Async](dsl: Http4sDsl[F], req: Request[F])( + fileData: Option[AttachmentData[F]] + ): F[Response[F]] = { + import dsl._ + + val inm = req.headers.get[`If-None-Match`].flatMap(_.tags) + val matches = BinaryUtil.matchETag(fileData.map(_.meta), inm) + fileData + .map { data => + if (matches) withResponseHeaders(dsl, NotModified())(data) + else makeByteResp(dsl)(data) + } + .getOrElse(NotFound(BasicResult(false, "Not found"))) + } + + def respondHead[F[_]: Async](dsl: Http4sDsl[F])( + fileData: Option[AttachmentData[F]] + ): F[Response[F]] = { + import dsl._ + + fileData + .map(data => withResponseHeaders(dsl, Ok())(data)) + .getOrElse(NotFound(BasicResult(false, "Not found"))) + } + + def respondPreview[F[_]: Async](dsl: Http4sDsl[F], req: Request[F])( fileData: Option[AttachmentPreviewData[F]] ): F[Response[F]] = { import dsl._ @@ -54,7 +79,7 @@ object BinaryUtil { } } - def respondHead[F[_]: Async]( + def respondPreviewHead[F[_]: Async]( dsl: Http4sDsl[F] )(fileData: Option[AttachmentPreviewData[F]]): F[Response[F]] = { import dsl._ diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala index cf4e23f7..a7bc4b82 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala @@ -46,24 +46,13 @@ object AttachmentRoutes { case HEAD -> Root / Ident(id) => for { fileData <- backend.itemSearch.findAttachment(id, user.account.collective) - resp <- - fileData - .map(data => withResponseHeaders(Ok())(data)) - .getOrElse(NotFound(BasicResult(false, "Not found"))) + resp <- BinaryUtil.respondHead(dsl)(fileData) } yield resp case req @ GET -> Root / Ident(id) => for { fileData <- backend.itemSearch.findAttachment(id, user.account.collective) - inm = req.headers.get[`If-None-Match`].flatMap(_.tags) - matches = BinaryUtil.matchETag(fileData.map(_.meta), inm) - resp <- - fileData - .map { data => - if (matches) withResponseHeaders(NotModified())(data) - else makeByteResp(data) - } - .getOrElse(NotFound(BasicResult(false, "Not found"))) + resp <- BinaryUtil.respond[F](dsl, req)(fileData) } yield resp case HEAD -> Root / Ident(id) / "original" => @@ -118,14 +107,14 @@ object AttachmentRoutes { for { fileData <- backend.itemSearch.findAttachmentPreview(id, user.account.collective) - resp <- BinaryUtil.respond(dsl, req)(fileData) + resp <- BinaryUtil.respondPreview(dsl, req)(fileData) } yield resp case HEAD -> Root / Ident(id) / "preview" => for { fileData <- backend.itemSearch.findAttachmentPreview(id, user.account.collective) - resp <- BinaryUtil.respondHead(dsl)(fileData) + resp <- BinaryUtil.respondPreviewHead(dsl)(fileData) } yield resp case POST -> Root / Ident(id) / "preview" => diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala index d763530b..b93a1381 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala @@ -13,9 +13,11 @@ import docspell.backend.BackendApp import docspell.backend.auth.ShareToken import docspell.common._ import docspell.restserver.http4s.BinaryUtil +import docspell.restserver.webapp.Webjars -import org.http4s.HttpRoutes +import org.http4s._ import org.http4s.dsl.Http4sDsl +import org.http4s.headers._ object ShareAttachmentRoutes { @@ -27,18 +29,35 @@ object ShareAttachmentRoutes { import dsl._ HttpRoutes.of { + case HEAD -> Root / Ident(id) => + for { + fileData <- backend.share.findAttachment(id, token.id).value + resp <- BinaryUtil.respondHead(dsl)(fileData) + } yield resp + + case req @ GET -> Root / Ident(id) => + for { + fileData <- backend.share.findAttachment(id, token.id).value + resp <- BinaryUtil.respond(dsl, req)(fileData) + } yield resp + + case GET -> Root / Ident(id) / "view" => + // this route exists to provide a stable url + // it redirects currently to viewerjs + val attachUrl = s"/api/v1/share/attachment/${id.id}" + val path = s"/app/assets${Webjars.viewerjs}/index.html#$attachUrl" + SeeOther(Location(Uri(path = Uri.Path.unsafeFromString(path)))) + case req @ GET -> Root / Ident(id) / "preview" => for { - fileData <- - backend.share.findAttachmentPreview(id, token.id).value - resp <- BinaryUtil.respond(dsl, req)(fileData) + fileData <- backend.share.findAttachmentPreview(id, token.id).value + resp <- BinaryUtil.respondPreview(dsl, req)(fileData) } yield resp case HEAD -> Root / Ident(id) / "preview" => for { - fileData <- - backend.share.findAttachmentPreview(id, token.id).value - resp <- BinaryUtil.respondHead(dsl)(fileData) + fileData <- backend.share.findAttachmentPreview(id, token.id).value + resp <- BinaryUtil.respondPreviewHead(dsl)(fileData) } yield resp } } diff --git a/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala b/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala index 20c2fbdf..5ac6682d 100644 --- a/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala +++ b/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala @@ -1,3 +1,9 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + package docspell.store.queries import docspell.common._ From c62b8526be8a86ce82218a88a110c5b51c8b9c32 Mon Sep 17 00:00:00 2001 From: eikek Date: Wed, 6 Oct 2021 01:14:30 +0200 Subject: [PATCH 16/37] View attachments from a share --- modules/webapp/src/main/elm/Api.elm | 6 ++++++ modules/webapp/src/main/elm/Comp/ItemCard.elm | 16 ++++++++-------- .../webapp/src/main/elm/Comp/ItemCardList.elm | 3 ++- modules/webapp/src/main/elm/Page/Home/View2.elm | 2 ++ .../webapp/src/main/elm/Page/Share/Results.elm | 1 + 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index a049bce3..bb6b94e7 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -143,6 +143,7 @@ module Api exposing , setTagsMultiple , setUnconfirmed , shareAttachmentPreviewURL + , shareFileURL , shareItemBasePreviewURL , startClassifier , startEmptyTrash @@ -2311,6 +2312,11 @@ shareItemBasePreviewURL itemId = "/api/v1/share/item/" ++ itemId ++ "/preview?withFallback=true" +shareFileURL : String -> String +shareFileURL attachId = + "/api/v1/share/attachment/" ++ attachId + + --- Helper diff --git a/modules/webapp/src/main/elm/Comp/ItemCard.elm b/modules/webapp/src/main/elm/Comp/ItemCard.elm index b4ce9786..15d66ce5 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCard.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCard.elm @@ -58,6 +58,7 @@ type alias ViewConfig = , extraClasses : String , previewUrl : AttachmentLight -> String , previewUrlFallback : ItemLight -> String + , attachUrl : AttachmentLight -> String } @@ -219,7 +220,7 @@ view2 texts cfg settings model item = , metaDataContent2 texts settings item , notesContent2 settings item , fulltextResultsContent2 item - , previewMenu2 texts settings model item (currentAttachment model item) + , previewMenu2 texts settings cfg model item (currentAttachment model item) , selectedDimmer ] ) @@ -473,8 +474,8 @@ previewImage2 cfg settings cardAction model item = ] -previewMenu2 : Texts -> UiSettings -> Model -> ItemLight -> Maybe AttachmentLight -> Html Msg -previewMenu2 texts settings model item mainAttach = +previewMenu2 : Texts -> UiSettings -> ViewConfig -> Model -> ItemLight -> Maybe AttachmentLight -> Html Msg +previewMenu2 texts settings cfg model item mainAttach = let pageCount = Maybe.andThen .pageCount mainAttach @@ -486,16 +487,15 @@ previewMenu2 texts settings model item mainAttach = fieldHidden f = Data.UiSettings.fieldHidden settings f - mkAttachUrl id = + mkAttachUrl attach = if settings.nativePdfPreview then - Api.fileURL id + cfg.attachUrl attach else - Api.fileURL id ++ "/view" + cfg.attachUrl attach ++ "/view" attachUrl = - Maybe.map .id mainAttach - |> Maybe.map mkAttachUrl + Maybe.map mkAttachUrl mainAttach |> Maybe.withDefault "/api/v1/sec/attachment/none" dueDate = diff --git a/modules/webapp/src/main/elm/Comp/ItemCardList.elm b/modules/webapp/src/main/elm/Comp/ItemCardList.elm index 6c66004d..a23894e2 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCardList.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCardList.elm @@ -164,6 +164,7 @@ type alias ViewConfig = , selection : ItemSelection , previewUrl : AttachmentLight -> String , previewUrlFallback : ItemLight -> String + , attachUrl : AttachmentLight -> String } @@ -219,7 +220,7 @@ viewItem2 texts model cfg settings item = "" vvcfg = - Comp.ItemCard.ViewConfig cfg.selection currentClass cfg.previewUrl cfg.previewUrlFallback + Comp.ItemCard.ViewConfig cfg.selection currentClass cfg.previewUrl cfg.previewUrlFallback cfg.attachUrl cardModel = Dict.get item.id model.itemCards diff --git a/modules/webapp/src/main/elm/Page/Home/View2.elm b/modules/webapp/src/main/elm/Page/Home/View2.elm index b0ca349e..506ae853 100644 --- a/modules/webapp/src/main/elm/Page/Home/View2.elm +++ b/modules/webapp/src/main/elm/Page/Home/View2.elm @@ -476,6 +476,7 @@ itemCardList texts _ settings model = (Data.ItemSelection.Active svm.ids) previewUrl previewUrlFallback + (.id >> Api.fileURL) _ -> Comp.ItemCardList.ViewConfig @@ -483,6 +484,7 @@ itemCardList texts _ settings model = Data.ItemSelection.Inactive previewUrl previewUrlFallback + (.id >> Api.fileURL) in [ Html.map ItemCardListMsg (Comp.ItemCardList.view2 texts.itemCardList diff --git a/modules/webapp/src/main/elm/Page/Share/Results.elm b/modules/webapp/src/main/elm/Page/Share/Results.elm index c50af9e8..5bd69400 100644 --- a/modules/webapp/src/main/elm/Page/Share/Results.elm +++ b/modules/webapp/src/main/elm/Page/Share/Results.elm @@ -25,6 +25,7 @@ view texts settings model = , selection = Data.ItemSelection.Inactive , previewUrl = \attach -> Api.shareAttachmentPreviewURL attach.id , previewUrlFallback = \item -> Api.shareItemBasePreviewURL item.id + , attachUrl = .id >> Api.shareFileURL } in div [] From 1a10216e3d8821bfb97392026f47b9016375164c Mon Sep 17 00:00:00 2001 From: eikek Date: Wed, 6 Oct 2021 09:36:38 +0200 Subject: [PATCH 17/37] Get item details from a share --- .../scala/docspell/backend/ops/OShare.scala | 20 +++- .../main/scala/docspell/query/ItemQuery.scala | 4 + .../src/main/resources/docspell-openapi.yml | 91 +++++++++++++++++++ .../docspell/restserver/RestServer.scala | 3 +- .../restserver/routes/ShareItemRoutes.scala | 41 +++++++++ 5 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 modules/restserver/src/main/scala/docspell/restserver/routes/ShareItemRoutes.scala diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index 75e899a6..57a2c236 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -17,6 +17,7 @@ import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} import docspell.backend.ops.OSimpleSearch.StringSearchResult import docspell.common._ import docspell.query.ItemQuery +import docspell.query.ItemQuery.Expr import docspell.query.ItemQuery.Expr.AttachId import docspell.store.Store import docspell.store.queries.SearchSummary @@ -57,6 +58,8 @@ trait OShare[F[_]] { def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] + def findItem(itemId: Ident, shareId: Ident): OptionT[F, ItemData] + def searchSummary( settings: OSimpleSearch.StatsSettings )(shareId: Ident, q: ItemQueryString): OptionT[F, StringSearchResult[SearchSummary]] @@ -234,24 +237,31 @@ object OShare { ): OptionT[F, AttachmentPreviewData[F]] = for { sq <- findShareQuery(shareId) - _ <- checkAttachment(sq, attachId) + _ <- checkAttachment(sq, AttachId(attachId.id)) res <- OptionT(itemSearch.findAttachmentPreview(attachId, sq.cid)) } yield res def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] = for { sq <- findShareQuery(shareId) - _ <- checkAttachment(sq, attachId) + _ <- checkAttachment(sq, AttachId(attachId.id)) res <- OptionT(itemSearch.findAttachment(attachId, sq.cid)) } yield res + def findItem(itemId: Ident, shareId: Ident): OptionT[F, ItemData] = + for { + sq <- findShareQuery(shareId) + _ <- checkAttachment(sq, Expr.itemIdEq(itemId.id)) + res <- OptionT(itemSearch.findItem(itemId, sq.cid)) + } yield res + /** Check whether the attachment with the given id is in the results of the given * share */ - private def checkAttachment(sq: ShareQuery, attachId: Ident): OptionT[F, Unit] = { + private def checkAttachment(sq: ShareQuery, idExpr: Expr): OptionT[F, Unit] = { val checkQuery = Query( Query.Fix(sq.asAccount, Some(sq.query.expr), None), - Query.QueryExpr(AttachId(attachId.id)) + Query.QueryExpr(idExpr) ) OptionT( itemSearch @@ -259,7 +269,7 @@ object OShare { .map(_.headOption.map(_ => ())) ).flatTapNone( logger.info( - s"Attempt to load unshared attachment '${attachId.id}' via share: ${sq.id.id}" + s"Attempt to load unshared data '$idExpr' via share: ${sq.id.id}" ) ) } diff --git a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala index be0e5135..c9466ac0 100644 --- a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala +++ b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala @@ -188,6 +188,10 @@ object ItemQuery { def date(op: Operator, attr: DateAttr, value: Date): SimpleExpr = SimpleExpr(op, Property(attr, value)) + + def itemIdEq(itemId1: String, moreIds: String*): Expr = + if (moreIds.isEmpty) string(Operator.Eq, Attr.ItemId, itemId1) + else InExpr(Attr.ItemId, Nel(itemId1, moreIds.toList)) } } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 0c0dd351..8745c23c 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1603,6 +1603,97 @@ paths: application/json: schema: $ref: "#/components/schemas/SearchStats" + /share/item/{id}: + get: + operationId: "share-item-get" + tags: [ Share ] + summary: Get details about an item. + description: | + Get detailed information about an item. + security: + - shareTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/ItemDetail" + /share/attachment/{id}: + head: + operationId: "share-attach-head" + tags: [ Share ] + summary: Get headers to an attachment file. + description: | + Get information about the binary file belonging to the + attachment with the given id. + security: + - shareTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + headers: + Content-Type: + schema: + type: string + Content-Length: + schema: + type: integer + format: int64 + ETag: + schema: + type: string + Content-Disposition: + schema: + type: string + get: + operationId: "share-attach-get" + tags: [ Share ] + summary: Get an attachment file. + description: | + Get the binary file belonging to the attachment with the given + id. The binary is a pdf file. If conversion failed, then the + original file is returned. + security: + - shareTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + content: + application/octet-stream: + schema: + type: string + format: binary + /share/attachment/{id}/view: + get: + operationId: "share-attach-show-viewerjs" + tags: [ Share ] + summary: A javascript rendered view of the pdf attachment + description: | + This provides a preview of the attachment rendered in a + browser. + + It currently uses a third-party javascript library (viewerjs) + to display the preview. This works by redirecting to the + viewerjs url with the attachment url as parameter. Note that + the resulting url that is redirected to is not stable. It may + change from version to version. This route, however, is meant + to provide a stable url for the preview. + security: + - shareTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 303: + description: See Other + 200: + description: Ok /share/attachment/{id}/preview: head: operationId: "share-attach-check-preview" diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala index 2e66c65f..9881fdcc 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala @@ -143,7 +143,8 @@ object RestServer { ): HttpRoutes[F] = Router( "search" -> ShareSearchRoutes(restApp.backend, cfg, token), - "attachment" -> ShareAttachmentRoutes(restApp.backend, token) + "attachment" -> ShareAttachmentRoutes(restApp.backend, token), + "item" -> ShareItemRoutes(restApp.backend, token) ) def redirectTo[F[_]: Async](path: String): HttpRoutes[F] = { diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareItemRoutes.scala new file mode 100644 index 00000000..38c3d041 --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareItemRoutes.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.restserver.routes +import cats.effect._ +import cats.implicits._ + +import docspell.backend.BackendApp +import docspell.backend.auth.ShareToken +import docspell.common._ +import docspell.restapi.model.BasicResult +import docspell.restserver.conv.Conversions + +import org.http4s._ +import org.http4s.circe.CirceEntityEncoder._ +import org.http4s.dsl.Http4sDsl + +object ShareItemRoutes { + + def apply[F[_]: Async]( + backend: BackendApp[F], + token: ShareToken + ): HttpRoutes[F] = { + val dsl = new Http4sDsl[F] {} + import dsl._ + + HttpRoutes.of { case GET -> Root / Ident(id) => + for { + item <- backend.share.findItem(id, token.id).value + result = item.map(Conversions.mkItemDetail) + resp <- + result + .map(r => Ok(r)) + .getOrElse(NotFound(BasicResult(false, "Not found."))) + } yield resp + } + } +} From b6187bb88d9e2a24395a984c11ba8d30f0274c96 Mon Sep 17 00:00:00 2001 From: eikek Date: Wed, 6 Oct 2021 11:04:18 +0200 Subject: [PATCH 18/37] Outline share detail page --- modules/webapp/src/main/elm/Api.elm | 10 ++ modules/webapp/src/main/elm/App/Data.elm | 10 +- modules/webapp/src/main/elm/App/Update.elm | 36 +++- modules/webapp/src/main/elm/App/View2.elm | 26 +++ modules/webapp/src/main/elm/Comp/ItemCard.elm | 5 +- .../webapp/src/main/elm/Comp/ItemCardList.elm | 3 +- .../src/main/elm/Comp/SharePasswordForm.elm | 156 ++++++++++++++++++ modules/webapp/src/main/elm/Messages.elm | 4 + .../elm/Messages/Comp/SharePasswordForm.elm | 33 ++++ .../src/main/elm/Messages/Page/Share.elm | 16 +- .../main/elm/Messages/Page/ShareDetail.elm | 20 +++ modules/webapp/src/main/elm/Page.elm | 28 +++- .../webapp/src/main/elm/Page/Home/View2.elm | 23 ++- .../webapp/src/main/elm/Page/Share/Data.elm | 17 +- .../src/main/elm/Page/Share/Results.elm | 6 +- .../webapp/src/main/elm/Page/Share/Update.elm | 52 +++--- .../webapp/src/main/elm/Page/Share/View.elm | 87 +--------- .../src/main/elm/Page/ShareDetail/Data.elm | 56 +++++++ .../src/main/elm/Page/ShareDetail/Update.elm | 64 +++++++ .../src/main/elm/Page/ShareDetail/View.elm | 108 ++++++++++++ modules/webapp/src/main/elm/Util/Http.elm | 19 +++ 21 files changed, 622 insertions(+), 157 deletions(-) create mode 100644 modules/webapp/src/main/elm/Comp/SharePasswordForm.elm create mode 100644 modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm create mode 100644 modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm create mode 100644 modules/webapp/src/main/elm/Page/ShareDetail/Data.elm create mode 100644 modules/webapp/src/main/elm/Page/ShareDetail/Update.elm create mode 100644 modules/webapp/src/main/elm/Page/ShareDetail/View.elm diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index bb6b94e7..42b16e20 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -83,6 +83,7 @@ module Api exposing , initOtp , itemBasePreviewURL , itemDetail + , itemDetailShare , itemIndexSearch , itemSearch , itemSearchStats @@ -2302,6 +2303,15 @@ searchShareStats flags token search receive = } +itemDetailShare : Flags -> String -> String -> (Result Http.Error ItemDetail -> msg) -> Cmd msg +itemDetailShare flags token itemId receive = + Http2.shareGet + { url = flags.config.baseUrl ++ "/api/v1/share/item/" ++ itemId + , token = token + , expect = Http.expectJson receive Api.Model.ItemDetail.decoder + } + + shareAttachmentPreviewURL : String -> String shareAttachmentPreviewURL id = "/api/v1/share/attachment/" ++ id ++ "/preview?withFallback=true" diff --git a/modules/webapp/src/main/elm/App/Data.elm b/modules/webapp/src/main/elm/App/Data.elm index 36713a54..c64c192a 100644 --- a/modules/webapp/src/main/elm/App/Data.elm +++ b/modules/webapp/src/main/elm/App/Data.elm @@ -33,6 +33,7 @@ import Page.NewInvite.Data import Page.Queue.Data import Page.Register.Data import Page.Share.Data +import Page.ShareDetail.Data import Page.Upload.Data import Page.UserSettings.Data import Url exposing (Url) @@ -54,6 +55,7 @@ type alias Model = , newInviteModel : Page.NewInvite.Data.Model , itemDetailModel : Page.ItemDetail.Data.Model , shareModel : Page.Share.Data.Model + , shareDetailModel : Page.ShareDetail.Data.Model , navMenuOpen : Bool , userMenuOpen : Bool , subs : Sub Msg @@ -88,7 +90,10 @@ init key url flags_ settings = Page.Login.Data.init flags (Page.loginPageReferrer page) ( shm, shc ) = - Page.Share.Data.init (Page.shareId page) flags + Page.Share.Data.init (Page.pageShareId page) flags + + ( sdm, sdc ) = + Page.ShareDetail.Data.init (Page.pageShareDetail page) flags homeViewMode = if settings.searchMenuVisible then @@ -112,6 +117,7 @@ init key url flags_ settings = , newInviteModel = Page.NewInvite.Data.emptyModel , itemDetailModel = Page.ItemDetail.Data.emptyModel , shareModel = shm + , shareDetailModel = sdm , navMenuOpen = False , userMenuOpen = False , subs = Sub.none @@ -127,6 +133,7 @@ init key url flags_ settings = , Cmd.map CollSettingsMsg csc , Cmd.map LoginMsg loginc , Cmd.map ShareMsg shc + , Cmd.map ShareDetailMsg sdc ] ) @@ -170,6 +177,7 @@ type Msg | NewInviteMsg Page.NewInvite.Data.Msg | ItemDetailMsg Page.ItemDetail.Data.Msg | ShareMsg Page.Share.Data.Msg + | ShareDetailMsg Page.ShareDetail.Data.Msg | Logout | LogoutResp (Result Http.Error ()) | SessionCheckResp (Result Http.Error AuthResult) diff --git a/modules/webapp/src/main/elm/App/Update.elm b/modules/webapp/src/main/elm/App/Update.elm index 2b051d84..19a689cb 100644 --- a/modules/webapp/src/main/elm/App/Update.elm +++ b/modules/webapp/src/main/elm/App/Update.elm @@ -36,6 +36,8 @@ import Page.Register.Data import Page.Register.Update import Page.Share.Data import Page.Share.Update +import Page.ShareDetail.Data +import Page.ShareDetail.Update import Page.Upload.Data import Page.Upload.Update import Page.UserSettings.Data @@ -119,6 +121,9 @@ updateWithSub msg model = ShareMsg lm -> updateShare lm model + ShareDetailMsg lm -> + updateShareDetail lm model + LoginMsg lm -> updateLogin lm model @@ -318,9 +323,26 @@ applyClientSettings model settings = { model | uiSettings = settings } +updateShareDetail : Page.ShareDetail.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +updateShareDetail lmsg model = + case Page.pageShareDetail model.page of + Just ( shareId, itemId ) -> + let + ( m, c ) = + Page.ShareDetail.Update.update shareId itemId model.flags lmsg model.shareDetailModel + in + ( { model | shareDetailModel = m } + , Cmd.map ShareDetailMsg c + , Sub.none + ) + + Nothing -> + ( model, Cmd.none, Sub.none ) + + updateShare : Page.Share.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) updateShare lmsg model = - case Page.shareId model.page of + case Page.pageShareId model.page of Just id -> let result = @@ -593,3 +615,15 @@ initPage model_ page = SharePage _ -> ( model, Cmd.none, Sub.none ) + + ShareDetailPage _ _ -> + case model_.page of + SharePage _ -> + let + verifyResult = + model.shareModel.verifyResult + in + updateShareDetail (Page.ShareDetail.Data.VerifyResp (Ok verifyResult)) model + + _ -> + ( model, Cmd.none, Sub.none ) diff --git a/modules/webapp/src/main/elm/App/View2.elm b/modules/webapp/src/main/elm/App/View2.elm index 06b99462..3dcaba5a 100644 --- a/modules/webapp/src/main/elm/App/View2.elm +++ b/modules/webapp/src/main/elm/App/View2.elm @@ -28,6 +28,7 @@ import Page.NewInvite.View2 as NewInvite import Page.Queue.View2 as Queue import Page.Register.View2 as Register import Page.Share.View as Share +import Page.ShareDetail.View as ShareDetail import Page.Upload.View2 as Upload import Page.UserSettings.View2 as UserSettings import Styles as S @@ -166,6 +167,9 @@ mainContent model = SharePage id -> viewShare texts id model + + ShareDetailPage shareId itemId -> + viewShareDetail texts shareId itemId model ) @@ -434,11 +438,33 @@ viewShare texts shareId model = model.flags model.version model.uiSettings + shareId model.shareModel ) ] +viewShareDetail : Messages -> String -> String -> Model -> List (Html Msg) +viewShareDetail texts shareId itemId model = + [ Html.map ShareDetailMsg + (ShareDetail.viewSidebar texts.shareDetail + model.sidebarVisible + model.flags + model.uiSettings + model.shareDetailModel + ) + , Html.map ShareDetailMsg + (ShareDetail.viewContent texts.shareDetail + model.flags + model.uiSettings + model.version + shareId + itemId + model.shareDetailModel + ) + ] + + viewHome : Messages -> Model -> List (Html Msg) viewHome texts model = [ Html.map HomeMsg diff --git a/modules/webapp/src/main/elm/Comp/ItemCard.elm b/modules/webapp/src/main/elm/Comp/ItemCard.elm index 15d66ce5..554cb1c5 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCard.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCard.elm @@ -59,6 +59,7 @@ type alias ViewConfig = , previewUrl : AttachmentLight -> String , previewUrlFallback : ItemLight -> String , attachUrl : AttachmentLight -> String + , detailPage : ItemLight -> Page } @@ -174,7 +175,7 @@ view2 texts cfg settings model item = cardAction = case cfg.selection of Data.ItemSelection.Inactive -> - [ Page.href (ItemDetailPage item.id) + [ Page.href (cfg.detailPage item) ] Data.ItemSelection.Active ids -> @@ -530,7 +531,7 @@ previewMenu2 texts settings cfg model item mainAttach = , a [ class S.secondaryBasicButtonPlain , class "px-2 py-1 border rounded ml-2" - , Page.href (ItemDetailPage item.id) + , Page.href (cfg.detailPage item) , title texts.gotoDetail ] [ i [ class "fa fa-edit" ] [] diff --git a/modules/webapp/src/main/elm/Comp/ItemCardList.elm b/modules/webapp/src/main/elm/Comp/ItemCardList.elm index a23894e2..1986411d 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCardList.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCardList.elm @@ -165,6 +165,7 @@ type alias ViewConfig = , previewUrl : AttachmentLight -> String , previewUrlFallback : ItemLight -> String , attachUrl : AttachmentLight -> String + , detailPage : ItemLight -> Page } @@ -220,7 +221,7 @@ viewItem2 texts model cfg settings item = "" vvcfg = - Comp.ItemCard.ViewConfig cfg.selection currentClass cfg.previewUrl cfg.previewUrlFallback cfg.attachUrl + Comp.ItemCard.ViewConfig cfg.selection currentClass cfg.previewUrl cfg.previewUrlFallback cfg.attachUrl cfg.detailPage cardModel = Dict.get item.id model.itemCards diff --git a/modules/webapp/src/main/elm/Comp/SharePasswordForm.elm b/modules/webapp/src/main/elm/Comp/SharePasswordForm.elm new file mode 100644 index 00000000..2c17a16a --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/SharePasswordForm.elm @@ -0,0 +1,156 @@ +module Comp.SharePasswordForm exposing (Model, Msg, init, update, view) + +import Api +import Api.Model.ShareVerifyResult exposing (ShareVerifyResult) +import Api.Model.VersionInfo exposing (VersionInfo) +import Data.Flags exposing (Flags) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onInput, onSubmit) +import Http +import Messages.Comp.SharePasswordForm exposing (Texts) +import Styles as S + + +type CompError + = CompErrorNone + | CompErrorPasswordFailed + | CompErrorHttp Http.Error + + +type alias Model = + { password : String + , compError : CompError + } + + +init : Model +init = + { password = "" + , compError = CompErrorNone + } + + +type Msg + = SetPassword String + | SubmitPassword + | VerifyResp (Result Http.Error ShareVerifyResult) + + + +--- update + + +update : String -> Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe ShareVerifyResult ) +update shareId flags msg model = + case msg of + SetPassword pw -> + ( { model | password = pw }, Cmd.none, Nothing ) + + SubmitPassword -> + let + secret = + { shareId = shareId + , password = Just model.password + } + in + ( model, Api.verifyShare flags secret VerifyResp, Nothing ) + + VerifyResp (Ok res) -> + if res.success then + ( { model | password = "", compError = CompErrorNone }, Cmd.none, Just res ) + + else + ( { model | password = "", compError = CompErrorPasswordFailed }, Cmd.none, Nothing ) + + VerifyResp (Err err) -> + ( { model | password = "", compError = CompErrorHttp err }, Cmd.none, Nothing ) + + + +--- view + + +view : Texts -> Flags -> VersionInfo -> Model -> Html Msg +view texts flags versionInfo model = + div [ class "flex flex-col items-center" ] + [ div [ class ("flex flex-col px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md " ++ S.box) ] + [ div [ class "self-center" ] + [ img + [ class "w-16 py-2" + , src (flags.config.docspellAssetPath ++ "/img/logo-96.png") + ] + [] + ] + , div [ class "font-medium self-center text-xl sm:text-2xl" ] + [ text texts.passwordRequired + ] + , Html.form + [ action "#" + , onSubmit SubmitPassword + , autocomplete False + ] + [ div [ class "flex flex-col my-3" ] + [ label + [ for "password" + , class S.inputLabel + ] + [ text texts.password + ] + , div [ class "relative" ] + [ div [ class S.inputIcon ] + [ i [ class "fa fa-lock" ] [] + ] + , input + [ type_ "password" + , name "password" + , autocomplete False + , autofocus True + , tabindex 1 + , onInput SetPassword + , value model.password + , class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput) + , placeholder texts.password + ] + [] + ] + ] + , div [ class "flex flex-col my-3" ] + [ button + [ type_ "submit" + , class S.primaryButton + ] + [ text texts.passwordSubmitButton + ] + ] + , case model.compError of + CompErrorNone -> + span [ class "hidden" ] [] + + CompErrorHttp err -> + div [ class S.errorMessage ] + [ text (texts.httpError err) + ] + + CompErrorPasswordFailed -> + div [ class S.errorMessage ] + [ text texts.passwordFailed + ] + ] + ] + , a + [ class "inline-flex items-center mt-4 text-xs opacity-50 hover:opacity-90" + , href "https://docspell.org" + , target "_new" + ] + [ img + [ src (flags.config.docspellAssetPath ++ "/img/logo-mc-96.png") + , class "w-3 h-3 mr-1" + ] + [] + , span [] + [ text "Docspell " + , text versionInfo.version + ] + ] + ] diff --git a/modules/webapp/src/main/elm/Messages.elm b/modules/webapp/src/main/elm/Messages.elm index 9809f1b4..24399b15 100644 --- a/modules/webapp/src/main/elm/Messages.elm +++ b/modules/webapp/src/main/elm/Messages.elm @@ -22,6 +22,7 @@ import Messages.Page.NewInvite import Messages.Page.Queue import Messages.Page.Register import Messages.Page.Share +import Messages.Page.ShareDetail import Messages.Page.Upload import Messages.Page.UserSettings import Messages.UiLanguage exposing (UiLanguage(..)) @@ -46,6 +47,7 @@ type alias Messages = , manageData : Messages.Page.ManageData.Texts , home : Messages.Page.Home.Texts , share : Messages.Page.Share.Texts + , shareDetail : Messages.Page.ShareDetail.Texts } @@ -112,6 +114,7 @@ gb = , manageData = Messages.Page.ManageData.gb , home = Messages.Page.Home.gb , share = Messages.Page.Share.gb + , shareDetail = Messages.Page.ShareDetail.gb } @@ -133,4 +136,5 @@ de = , manageData = Messages.Page.ManageData.de , home = Messages.Page.Home.de , share = Messages.Page.Share.de + , shareDetail = Messages.Page.ShareDetail.de } diff --git a/modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm b/modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm new file mode 100644 index 00000000..8688a4e9 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm @@ -0,0 +1,33 @@ +module Messages.Comp.SharePasswordForm exposing (Texts, de, gb) + +import Http +import Messages.Comp.HttpError + + +type alias Texts = + { httpError : Http.Error -> String + , passwordRequired : String + , password : String + , passwordSubmitButton : String + , passwordFailed : String + } + + +gb : Texts +gb = + { httpError = Messages.Comp.HttpError.gb + , passwordRequired = "Password required" + , password = "Password" + , passwordSubmitButton = "Submit" + , passwordFailed = "Das Passwort ist falsch" + } + + +de : Texts +de = + { httpError = Messages.Comp.HttpError.de + , passwordRequired = "Passwort benötigt" + , password = "Passwort" + , passwordSubmitButton = "Submit" + , passwordFailed = "Password is wrong" + } diff --git a/modules/webapp/src/main/elm/Messages/Page/Share.elm b/modules/webapp/src/main/elm/Messages/Page/Share.elm index cee461cd..f6b73d31 100644 --- a/modules/webapp/src/main/elm/Messages/Page/Share.elm +++ b/modules/webapp/src/main/elm/Messages/Page/Share.elm @@ -10,16 +10,14 @@ module Messages.Page.Share exposing (..) import Messages.Basics import Messages.Comp.ItemCardList import Messages.Comp.SearchMenu +import Messages.Comp.SharePasswordForm type alias Texts = { searchMenu : Messages.Comp.SearchMenu.Texts , basics : Messages.Basics.Texts , itemCardList : Messages.Comp.ItemCardList.Texts - , passwordRequired : String - , password : String - , passwordSubmitButton : String - , passwordFailed : String + , passwordForm : Messages.Comp.SharePasswordForm.Texts } @@ -28,10 +26,7 @@ gb = { searchMenu = Messages.Comp.SearchMenu.gb , basics = Messages.Basics.gb , itemCardList = Messages.Comp.ItemCardList.gb - , passwordRequired = "Password required" - , password = "Password" - , passwordSubmitButton = "Submit" - , passwordFailed = "Das Passwort ist falsch" + , passwordForm = Messages.Comp.SharePasswordForm.gb } @@ -40,8 +35,5 @@ de = { searchMenu = Messages.Comp.SearchMenu.de , basics = Messages.Basics.de , itemCardList = Messages.Comp.ItemCardList.de - , passwordRequired = "Passwort benötigt" - , password = "Passwort" - , passwordSubmitButton = "Submit" - , passwordFailed = "Password is wrong" + , passwordForm = Messages.Comp.SharePasswordForm.de } diff --git a/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm b/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm new file mode 100644 index 00000000..71ab9536 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm @@ -0,0 +1,20 @@ +module Messages.Page.ShareDetail exposing (..) + +import Messages.Comp.SharePasswordForm + + +type alias Texts = + { passwordForm : Messages.Comp.SharePasswordForm.Texts + } + + +gb : Texts +gb = + { passwordForm = Messages.Comp.SharePasswordForm.gb + } + + +de : Texts +de = + { passwordForm = Messages.Comp.SharePasswordForm.de + } diff --git a/modules/webapp/src/main/elm/Page.elm b/modules/webapp/src/main/elm/Page.elm index 667fe7aa..2f42ee2e 100644 --- a/modules/webapp/src/main/elm/Page.elm +++ b/modules/webapp/src/main/elm/Page.elm @@ -19,9 +19,10 @@ module Page exposing , loginPageReferrer , pageFromString , pageName + , pageShareDetail + , pageShareId , pageToString , set - , shareId , uploadId ) @@ -61,6 +62,7 @@ type Page | NewInvitePage | ItemDetailPage String | SharePage String + | ShareDetailPage String String isSecured : Page -> Bool @@ -99,6 +101,9 @@ isSecured page = SharePage _ -> False + ShareDetailPage _ _ -> + False + {-| Currently, all secured pages have a sidebar, except UploadPage. -} @@ -171,6 +176,9 @@ pageName page = SharePage _ -> "Share" + ShareDetailPage _ _ -> + "Share Detail" + loginPageReferrer : Page -> LoginData loginPageReferrer page = @@ -182,8 +190,8 @@ loginPageReferrer page = emptyLoginData -shareId : Page -> Maybe String -shareId page = +pageShareId : Page -> Maybe String +pageShareId page = case page of SharePage id -> Just id @@ -192,6 +200,16 @@ shareId page = Nothing +pageShareDetail : Page -> Maybe ( String, String ) +pageShareDetail page = + case page of + ShareDetailPage shareId itemId -> + Just ( shareId, itemId ) + + _ -> + Nothing + + uploadId : Page -> Maybe String uploadId page = case page of @@ -248,6 +266,9 @@ pageToString page = SharePage id -> "/app/share/" ++ id + ShareDetailPage shareId itemId -> + "/app/share/" ++ shareId ++ "/" ++ itemId + pageFromString : String -> Maybe Page pageFromString str = @@ -304,6 +325,7 @@ parser = , Parser.map (UploadPage Nothing) (s pathPrefix s "upload") , Parser.map NewInvitePage (s pathPrefix s "newinvite") , Parser.map ItemDetailPage (s pathPrefix s "item" string) + , Parser.map ShareDetailPage (s pathPrefix s "share" string string) , Parser.map SharePage (s pathPrefix s "share" string) ] diff --git a/modules/webapp/src/main/elm/Page/Home/View2.elm b/modules/webapp/src/main/elm/Page/Home/View2.elm index 506ae853..da26662d 100644 --- a/modules/webapp/src/main/elm/Page/Home/View2.elm +++ b/modules/webapp/src/main/elm/Page/Home/View2.elm @@ -468,23 +468,22 @@ itemCardList texts _ settings model = previewUrlFallback item = Api.itemBasePreviewURL item.id + viewCfg sel = + Comp.ItemCardList.ViewConfig + model.scrollToCard + sel + previewUrl + previewUrlFallback + (.id >> Api.fileURL) + (.id >> ItemDetailPage) + itemViewCfg = case model.viewMode of SelectView svm -> - Comp.ItemCardList.ViewConfig - model.scrollToCard - (Data.ItemSelection.Active svm.ids) - previewUrl - previewUrlFallback - (.id >> Api.fileURL) + viewCfg (Data.ItemSelection.Active svm.ids) _ -> - Comp.ItemCardList.ViewConfig - model.scrollToCard - Data.ItemSelection.Inactive - previewUrl - previewUrlFallback - (.id >> Api.fileURL) + viewCfg Data.ItemSelection.Inactive in [ Html.map ItemCardListMsg (Comp.ItemCardList.view2 texts.itemCardList diff --git a/modules/webapp/src/main/elm/Page/Share/Data.elm b/modules/webapp/src/main/elm/Page/Share/Data.elm index eb47a41c..4be43360 100644 --- a/modules/webapp/src/main/elm/Page/Share/Data.elm +++ b/modules/webapp/src/main/elm/Page/Share/Data.elm @@ -15,6 +15,7 @@ import Api.Model.ShareVerifyResult exposing (ShareVerifyResult) import Comp.ItemCardList import Comp.PowerSearchInput import Comp.SearchMenu +import Comp.SharePasswordForm import Data.Flags exposing (Flags) import Http @@ -31,16 +32,10 @@ type PageError | PageErrorAuthFail -type alias PasswordModel = - { password : String - , passwordFailed : Bool - } - - type alias Model = { mode : Mode , verifyResult : ShareVerifyResult - , passwordModel : PasswordModel + , passwordModel : Comp.SharePasswordForm.Model , pageError : PageError , searchMenuModel : Comp.SearchMenu.Model , powerSearchInput : Comp.PowerSearchInput.Model @@ -53,10 +48,7 @@ emptyModel : Flags -> Model emptyModel flags = { mode = ModeInitial , verifyResult = Api.Model.ShareVerifyResult.empty - , passwordModel = - { password = "" - , passwordFailed = False - } + , passwordModel = Comp.SharePasswordForm.init , pageError = PageErrorNone , searchMenuModel = Comp.SearchMenu.init flags , powerSearchInput = Comp.PowerSearchInput.init @@ -79,8 +71,7 @@ type Msg = VerifyResp (Result Http.Error ShareVerifyResult) | SearchResp (Result Http.Error ItemLightList) | StatsResp (Result Http.Error SearchStats) - | SetPassword String - | SubmitPassword + | PasswordMsg Comp.SharePasswordForm.Msg | SearchMenuMsg Comp.SearchMenu.Msg | PowerSearchMsg Comp.PowerSearchInput.Msg | ResetSearch diff --git a/modules/webapp/src/main/elm/Page/Share/Results.elm b/modules/webapp/src/main/elm/Page/Share/Results.elm index 5bd69400..e47d8583 100644 --- a/modules/webapp/src/main/elm/Page/Share/Results.elm +++ b/modules/webapp/src/main/elm/Page/Share/Results.elm @@ -14,11 +14,12 @@ import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) import Messages.Page.Share exposing (Texts) +import Page exposing (Page(..)) import Page.Share.Data exposing (Model, Msg(..)) -view : Texts -> UiSettings -> Model -> Html Msg -view texts settings model = +view : Texts -> UiSettings -> String -> Model -> Html Msg +view texts settings shareId model = let viewCfg = { current = Nothing @@ -26,6 +27,7 @@ view texts settings model = , previewUrl = \attach -> Api.shareAttachmentPreviewURL attach.id , previewUrlFallback = \item -> Api.shareItemBasePreviewURL item.id , attachUrl = .id >> Api.shareFileURL + , detailPage = \item -> ShareDetailPage shareId item.id } in div [] diff --git a/modules/webapp/src/main/elm/Page/Share/Update.elm b/modules/webapp/src/main/elm/Page/Share/Update.elm index 01b7ec73..c37a9776 100644 --- a/modules/webapp/src/main/elm/Page/Share/Update.elm +++ b/modules/webapp/src/main/elm/Page/Share/Update.elm @@ -13,6 +13,7 @@ import Comp.ItemCardList import Comp.LinkTarget exposing (LinkTarget) import Comp.PowerSearchInput import Comp.SearchMenu +import Comp.SharePasswordForm import Data.Flags exposing (Flags) import Data.ItemQuery as Q import Data.SearchMode @@ -51,26 +52,13 @@ update flags settings shareId msg model = ) else if res.passwordRequired then - if model.mode == ModePassword then - noSub - ( { model - | pageError = PageErrorNone - , passwordModel = - { password = "" - , passwordFailed = True - } - } - , Cmd.none - ) - - else - noSub - ( { model - | pageError = PageErrorNone - , mode = ModePassword - } - , Cmd.none - ) + noSub + ( { model + | pageError = PageErrorNone + , mode = ModePassword + } + , Cmd.none + ) else noSub @@ -101,21 +89,21 @@ update flags settings shareId msg model = StatsResp (Err err) -> noSub ( { model | pageError = PageErrorHttp err }, Cmd.none ) - SetPassword pw -> + PasswordMsg lmsg -> let - pm = - model.passwordModel + ( m, c, res ) = + Comp.SharePasswordForm.update shareId flags lmsg model.passwordModel in - noSub ( { model | passwordModel = { pm | password = pw } }, Cmd.none ) + case res of + Just verifyResult -> + update flags + settings + shareId + (VerifyResp (Ok verifyResult)) + model - SubmitPassword -> - let - secret = - { shareId = shareId - , password = Just model.passwordModel.password - } - in - noSub ( model, Api.verifyShare flags secret VerifyResp ) + Nothing -> + noSub ( { model | passwordModel = m }, Cmd.map PasswordMsg c ) SearchMenuMsg lm -> let diff --git a/modules/webapp/src/main/elm/Page/Share/View.elm b/modules/webapp/src/main/elm/Page/Share/View.elm index 5c0f941b..baaad70a 100644 --- a/modules/webapp/src/main/elm/Page/Share/View.elm +++ b/modules/webapp/src/main/elm/Page/Share/View.elm @@ -9,6 +9,7 @@ module Page.Share.View exposing (viewContent, viewSidebar) import Api.Model.VersionInfo exposing (VersionInfo) import Comp.Basic as B +import Comp.SharePasswordForm import Data.Flags exposing (Flags) import Data.Items import Data.UiSettings exposing (UiSettings) @@ -35,8 +36,8 @@ viewSidebar texts visible flags settings model = ] -viewContent : Texts -> Flags -> VersionInfo -> UiSettings -> Model -> Html Msg -viewContent texts flags versionInfo uiSettings model = +viewContent : Texts -> Flags -> VersionInfo -> UiSettings -> String -> Model -> Html Msg +viewContent texts flags versionInfo uiSettings shareId model = case model.mode of ModeInitial -> div @@ -54,15 +55,15 @@ viewContent texts flags versionInfo uiSettings model = passwordContent texts flags versionInfo model ModeShare -> - mainContent texts flags uiSettings model + mainContent texts flags uiSettings shareId model --- Helpers -mainContent : Texts -> Flags -> UiSettings -> Model -> Html Msg -mainContent texts _ settings model = +mainContent : Texts -> Flags -> UiSettings -> String -> Model -> Html Msg +mainContent texts _ settings shareId model = div [ id "content" , class "h-full flex flex-col" @@ -75,7 +76,7 @@ mainContent texts _ settings model = [ text <| Maybe.withDefault "" model.verifyResult.name ] , Menubar.view texts model - , Results.view texts settings model + , Results.view texts settings shareId model ] @@ -86,76 +87,6 @@ passwordContent texts flags versionInfo model = , class "h-full flex flex-col items-center justify-center w-full" , class S.content ] - [ div [ class ("flex flex-col px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md " ++ S.box) ] - [ div [ class "self-center" ] - [ img - [ class "w-16 py-2" - , src (flags.config.docspellAssetPath ++ "/img/logo-96.png") - ] - [] - ] - , div [ class "font-medium self-center text-xl sm:text-2xl" ] - [ text texts.passwordRequired - ] - , Html.form - [ action "#" - , onSubmit SubmitPassword - , autocomplete False - ] - [ div [ class "flex flex-col my-3" ] - [ label - [ for "password" - , class S.inputLabel - ] - [ text texts.password - ] - , div [ class "relative" ] - [ div [ class S.inputIcon ] - [ i [ class "fa fa-lock" ] [] - ] - , input - [ type_ "password" - , name "password" - , autocomplete False - , autofocus True - , tabindex 1 - , onInput SetPassword - , value model.passwordModel.password - , class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput) - , placeholder texts.password - ] - [] - ] - ] - , div [ class "flex flex-col my-3" ] - [ button - [ type_ "submit" - , class S.primaryButton - ] - [ text texts.passwordSubmitButton - ] - ] - , div - [ class S.errorMessage - , classList [ ( "hidden", not model.passwordModel.passwordFailed ) ] - ] - [ text texts.passwordFailed - ] - ] - ] - , a - [ class "inline-flex items-center mt-4 text-xs opacity-50 hover:opacity-90" - , href "https://docspell.org" - , target "_new" - ] - [ img - [ src (flags.config.docspellAssetPath ++ "/img/logo-mc-96.png") - , class "w-3 h-3 mr-1" - ] - [] - , span [] - [ text "Docspell " - , text versionInfo.version - ] - ] + [ Html.map PasswordMsg + (Comp.SharePasswordForm.view texts.passwordForm flags versionInfo model.passwordModel) ] diff --git a/modules/webapp/src/main/elm/Page/ShareDetail/Data.elm b/modules/webapp/src/main/elm/Page/ShareDetail/Data.elm new file mode 100644 index 00000000..08c412bd --- /dev/null +++ b/modules/webapp/src/main/elm/Page/ShareDetail/Data.elm @@ -0,0 +1,56 @@ +module Page.ShareDetail.Data exposing (Model, Msg(..), PageError(..), ViewMode(..), init) + +import Api +import Api.Model.ItemDetail exposing (ItemDetail) +import Api.Model.ShareSecret exposing (ShareSecret) +import Api.Model.ShareVerifyResult exposing (ShareVerifyResult) +import Comp.SharePasswordForm +import Data.Flags exposing (Flags) +import Http + + +type ViewMode + = ViewNormal + | ViewPassword + | ViewLoading + + +type PageError + = PageErrorNone + | PageErrorHttp Http.Error + | PageErrorAuthFail + + +type alias Model = + { item : ItemDetail + , verifyResult : ShareVerifyResult + , passwordModel : Comp.SharePasswordForm.Model + , viewMode : ViewMode + , pageError : PageError + } + + +type Msg + = VerifyResp (Result Http.Error ShareVerifyResult) + | GetItemResp (Result Http.Error ItemDetail) + | PasswordMsg Comp.SharePasswordForm.Msg + + +emptyModel : ViewMode -> Model +emptyModel vm = + { item = Api.Model.ItemDetail.empty + , verifyResult = Api.Model.ShareVerifyResult.empty + , passwordModel = Comp.SharePasswordForm.init + , viewMode = vm + , pageError = PageErrorNone + } + + +init : Maybe ( String, String ) -> Flags -> ( Model, Cmd Msg ) +init mids flags = + case mids of + Just ( shareId, _ ) -> + ( emptyModel ViewLoading, Api.verifyShare flags (ShareSecret shareId Nothing) VerifyResp ) + + Nothing -> + ( emptyModel ViewLoading, Cmd.none ) diff --git a/modules/webapp/src/main/elm/Page/ShareDetail/Update.elm b/modules/webapp/src/main/elm/Page/ShareDetail/Update.elm new file mode 100644 index 00000000..40aa5757 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/ShareDetail/Update.elm @@ -0,0 +1,64 @@ +module Page.ShareDetail.Update exposing (update) + +import Api +import Comp.SharePasswordForm +import Data.Flags exposing (Flags) +import Page.ShareDetail.Data exposing (..) + + +update : String -> String -> Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update shareId itemId flags msg model = + case msg of + VerifyResp (Ok res) -> + if res.success then + ( { model + | pageError = PageErrorNone + , viewMode = ViewLoading + , verifyResult = res + } + , Api.itemDetailShare flags res.token itemId GetItemResp + ) + + else if res.passwordRequired then + ( { model + | pageError = PageErrorNone + , viewMode = ViewPassword + } + , Cmd.none + ) + + else + ( { model | pageError = PageErrorAuthFail } + , Cmd.none + ) + + VerifyResp (Err err) -> + ( { model | pageError = PageErrorHttp err }, Cmd.none ) + + GetItemResp (Ok item) -> + ( { model + | item = item + , viewMode = ViewNormal + , pageError = PageErrorNone + } + , Cmd.none + ) + + GetItemResp (Err err) -> + ( { model | viewMode = ViewNormal, pageError = PageErrorHttp err }, Cmd.none ) + + PasswordMsg lmsg -> + let + ( m, c, res ) = + Comp.SharePasswordForm.update shareId flags lmsg model.passwordModel + in + case res of + Just verifyResult -> + update shareId + itemId + flags + (VerifyResp (Ok verifyResult)) + model + + Nothing -> + ( { model | passwordModel = m }, Cmd.map PasswordMsg c ) diff --git a/modules/webapp/src/main/elm/Page/ShareDetail/View.elm b/modules/webapp/src/main/elm/Page/ShareDetail/View.elm new file mode 100644 index 00000000..aad6fa3f --- /dev/null +++ b/modules/webapp/src/main/elm/Page/ShareDetail/View.elm @@ -0,0 +1,108 @@ +module Page.ShareDetail.View exposing (viewContent, viewSidebar) + +import Api.Model.VersionInfo exposing (VersionInfo) +import Comp.Basic as B +import Comp.SharePasswordForm +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Messages.Page.ShareDetail exposing (Texts) +import Page exposing (Page(..)) +import Page.ShareDetail.Data exposing (..) +import Styles as S + + +viewSidebar : Texts -> Bool -> Flags -> UiSettings -> Model -> Html Msg +viewSidebar texts visible flags settings model = + div + [ id "sidebar" + , class "hidden" + ] + [] + + +viewContent : Texts -> Flags -> UiSettings -> VersionInfo -> String -> String -> Model -> Html Msg +viewContent texts flags uiSettings versionInfo shareId itemId model = + case model.viewMode of + ViewLoading -> + div + [ id "content" + , class "h-full w-full flex flex-col text-5xl" + , class S.content + ] + [ B.loadingDimmer + { active = model.pageError == PageErrorNone + , label = "" + } + ] + + ViewPassword -> + passwordContent texts flags versionInfo model + + ViewNormal -> + mainContent texts flags uiSettings shareId model + + + +--- Helper + + +mainContent : Texts -> Flags -> UiSettings -> String -> Model -> Html Msg +mainContent texts flags settings shareId model = + div + [ class "flex flex-col" + , class S.content + ] + [ itemHead texts shareId model + , div [ class "flex flex-col sm:flex-row" ] + [ itemData texts model + , itemPreview texts flags settings model + ] + ] + + +itemData : Texts -> Model -> Html Msg +itemData texts model = + div [ class "flex" ] + [] + + +{-| Using ItemDetail Model to be able to reuse SingleAttachment component +-} +itemPreview : Texts -> Flags -> UiSettings -> Model -> Html Msg +itemPreview texts flags settings model = + div [ class "flex flex-grow" ] + [] + + +itemHead : Texts -> String -> Model -> Html Msg +itemHead texts shareId model = + div [ class "flex flex-col sm:flex-row" ] + [ div [ class "flex flex-grow items-center" ] + [ h1 [ class S.header1 ] + [ text model.item.name + ] + ] + , div [ class "flex flex-row items-center justify-end" ] + [ B.secondaryBasicButton + { label = "Close" + , icon = "fa fa-times" + , disabled = False + , handler = Page.href (SharePage shareId) + , attrs = [] + } + ] + ] + + +passwordContent : Texts -> Flags -> VersionInfo -> Model -> Html Msg +passwordContent texts flags versionInfo model = + div + [ id "content" + , class "h-full flex flex-col items-center justify-center w-full" + , class S.content + ] + [ Html.map PasswordMsg + (Comp.SharePasswordForm.view texts.passwordForm flags versionInfo model.passwordModel) + ] diff --git a/modules/webapp/src/main/elm/Util/Http.elm b/modules/webapp/src/main/elm/Util/Http.elm index dd965b23..5088f892 100644 --- a/modules/webapp/src/main/elm/Util/Http.elm +++ b/modules/webapp/src/main/elm/Util/Http.elm @@ -14,6 +14,7 @@ module Util.Http exposing , authTask , executeIn , jsonResolver + , shareGet , sharePost ) @@ -167,6 +168,24 @@ authGet req = } +shareGet : + { url : String + , token : String + , expect : Http.Expect msg + } + -> Cmd msg +shareGet req = + shareReq + { url = req.url + , token = req.token + , body = Http.emptyBody + , expect = req.expect + , method = "GET" + , headers = [] + , tracker = Nothing + } + + authDelete : { url : String , account : AuthResult From f216c472ee632bab8cad58ae555f90f8f839a6d3 Mon Sep 17 00:00:00 2001 From: eikek Date: Wed, 6 Oct 2021 23:20:16 +0200 Subject: [PATCH 19/37] Detect how to display pdf files Closes: #1099 --- .../restserver/src/main/templates/index.html | 13 ++++ modules/webapp/src/main/elm/Comp/ItemCard.elm | 17 ++--- .../webapp/src/main/elm/Comp/ItemCardList.elm | 18 ++--- .../src/main/elm/Comp/ItemDetail/Model.elm | 3 - .../elm/Comp/ItemDetail/SingleAttachment.elm | 20 +----- .../src/main/elm/Comp/ItemDetail/Update.elm | 13 ---- .../src/main/elm/Comp/UiSettingsForm.elm | 56 ++++++++++------ modules/webapp/src/main/elm/Data/Flags.elm | 1 + modules/webapp/src/main/elm/Data/Pdf.elm | 67 +++++++++++++++++++ .../webapp/src/main/elm/Data/UiSettings.elm | 38 ++++++++--- .../main/elm/Messages/Comp/UiSettingsForm.elm | 5 ++ .../src/main/elm/Messages/Data/PdfMode.elm | 39 +++++++++++ .../webapp/src/main/elm/Page/Home/View2.elm | 3 +- .../src/main/elm/Page/Share/Results.elm | 7 +- .../webapp/src/main/elm/Page/Share/View.elm | 4 +- 15 files changed, 217 insertions(+), 87 deletions(-) create mode 100644 modules/webapp/src/main/elm/Data/Pdf.elm create mode 100644 modules/webapp/src/main/elm/Messages/Data/PdfMode.elm diff --git a/modules/restserver/src/main/templates/index.html b/modules/restserver/src/main/templates/index.html index 6ae141fb..2a61f28c 100644 --- a/modules/restserver/src/main/templates/index.html +++ b/modules/restserver/src/main/templates/index.html @@ -43,8 +43,21 @@ // this is required for transitioning; elm fails to parse the account account["requireSecondFactor"] = false; } + + // hack to guess if the browser can display PDFs natively. It + // seems that almost all browsers allow to query the + // navigator.mimeTypes array, except firefox. + var ua = navigator.userAgent.toLowerCase(); + var pdfSupported = false; + if (ua.indexOf("firefox") > -1) { + pdfSupported = ua.indexOf("mobile") == -1; + } else { + pdfSupported = "application/pdf" in navigator.mimeTypes; + } + var elmFlags = { "account": account, + "pdfSupported": pdfSupported, "config": {{{flagsJson}}} }; diff --git a/modules/webapp/src/main/elm/Comp/ItemCard.elm b/modules/webapp/src/main/elm/Comp/ItemCard.elm index 554cb1c5..093afbc2 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCard.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCard.elm @@ -22,6 +22,7 @@ import Api.Model.ItemLight exposing (ItemLight) import Comp.LinkTarget exposing (LinkTarget(..)) import Data.Direction import Data.Fields +import Data.Flags exposing (Flags) import Data.Icons as Icons import Data.ItemSelection exposing (ItemSelection) import Data.ItemTemplate as IT @@ -150,8 +151,8 @@ update ddm msg model = --- View2 -view2 : Texts -> ViewConfig -> UiSettings -> Model -> ItemLight -> Html Msg -view2 texts cfg settings model item = +view2 : Texts -> ViewConfig -> UiSettings -> Flags -> Model -> ItemLight -> Html Msg +view2 texts cfg settings flags model item = let isCreated = item.state == "created" @@ -221,7 +222,7 @@ view2 texts cfg settings model item = , metaDataContent2 texts settings item , notesContent2 settings item , fulltextResultsContent2 item - , previewMenu2 texts settings cfg model item (currentAttachment model item) + , previewMenu2 texts settings flags cfg model item (currentAttachment model item) , selectedDimmer ] ) @@ -475,8 +476,8 @@ previewImage2 cfg settings cardAction model item = ] -previewMenu2 : Texts -> UiSettings -> ViewConfig -> Model -> ItemLight -> Maybe AttachmentLight -> Html Msg -previewMenu2 texts settings cfg model item mainAttach = +previewMenu2 : Texts -> UiSettings -> Flags -> ViewConfig -> Model -> ItemLight -> Maybe AttachmentLight -> Html Msg +previewMenu2 texts settings flags cfg model item mainAttach = let pageCount = Maybe.andThen .pageCount mainAttach @@ -489,11 +490,7 @@ previewMenu2 texts settings cfg model item mainAttach = Data.UiSettings.fieldHidden settings f mkAttachUrl attach = - if settings.nativePdfPreview then - cfg.attachUrl attach - - else - cfg.attachUrl attach ++ "/view" + Data.UiSettings.pdfUrl settings flags (cfg.attachUrl attach) attachUrl = Maybe.map mkAttachUrl mainAttach diff --git a/modules/webapp/src/main/elm/Comp/ItemCardList.elm b/modules/webapp/src/main/elm/Comp/ItemCardList.elm index 1986411d..df8b7af4 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCardList.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCardList.elm @@ -169,19 +169,19 @@ type alias ViewConfig = } -view2 : Texts -> ViewConfig -> UiSettings -> Model -> Html Msg -view2 texts cfg settings model = +view2 : Texts -> ViewConfig -> UiSettings -> Flags -> Model -> Html Msg +view2 texts cfg settings flags model = div [ classList [ ( "ds-item-list", True ) , ( "ds-multi-select-mode", isMultiSelectMode cfg ) ] ] - (List.map (viewGroup2 texts model cfg settings) model.results.groups) + (List.map (viewGroup2 texts model cfg settings flags) model.results.groups) -viewGroup2 : Texts -> Model -> ViewConfig -> UiSettings -> ItemLightGroup -> Html Msg -viewGroup2 texts model cfg settings group = +viewGroup2 : Texts -> Model -> ViewConfig -> UiSettings -> Flags -> ItemLightGroup -> Html Msg +viewGroup2 texts model cfg settings flags group = div [ class "ds-item-group" ] [ div [ class "flex py-1 mt-2 mb-2 flex flex-row items-center" @@ -206,12 +206,12 @@ viewGroup2 texts model cfg settings group = [] ] , div [ class "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-2" ] - (List.map (viewItem2 texts model cfg settings) group.items) + (List.map (viewItem2 texts model cfg settings flags) group.items) ] -viewItem2 : Texts -> Model -> ViewConfig -> UiSettings -> ItemLight -> Html Msg -viewItem2 texts model cfg settings item = +viewItem2 : Texts -> Model -> ViewConfig -> UiSettings -> Flags -> ItemLight -> Html Msg +viewItem2 texts model cfg settings flags item = let currentClass = if cfg.current == Just item.id then @@ -228,7 +228,7 @@ viewItem2 texts model cfg settings item = |> Maybe.withDefault Comp.ItemCard.init cardHtml = - Comp.ItemCard.view2 texts.itemCard vvcfg settings cardModel item + Comp.ItemCard.view2 texts.itemCard vvcfg settings flags cardModel item in Html.map (ItemCardMsg item) cardHtml diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm index 1302791d..d995ef7d 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm @@ -100,7 +100,6 @@ type alias Model = , sentMailsOpen : Bool , attachMeta : Dict String Comp.AttachmentMeta.Model , attachMetaOpen : Bool - , pdfNativeView : Maybe Bool , attachModal : Maybe ConfirmModalValue , addFilesOpen : Bool , addFilesModel : Comp.Dropzone.Model @@ -236,7 +235,6 @@ emptyModel = , sentMailsOpen = False , attachMeta = Dict.empty , attachMetaOpen = False - , pdfNativeView = Nothing , attachModal = Nothing , addFilesOpen = False , addFilesModel = Comp.Dropzone.init [] @@ -316,7 +314,6 @@ type Msg | SentMailsResp (Result Http.Error SentMails) | AttachMetaClick String | AttachMetaMsg String Comp.AttachmentMeta.Msg - | TogglePdfNativeView Bool | RequestDeleteAttachment String | DeleteAttachConfirmed String | RequestDeleteSelected diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm index 61143847..52f935f9 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm @@ -85,12 +85,8 @@ view texts flags settings model pos attach = , style "max-height" "calc(100vh - 140px)" , style "min-height" "500px" ] - [ iframe - [ if Maybe.withDefault settings.nativePdfPreview model.pdfNativeView then - src fileUrl - - else - src (fileUrl ++ "/view") + [ embed + [ src <| Data.UiSettings.pdfUrl settings flags fileUrl , class "absolute h-full w-full top-0 left-0 mx-0 py-0" , id "ds-pdf-view-iframe" ] @@ -254,18 +250,6 @@ attachHeader texts settings model _ attach = , classList [ ( "hidden", not attach.converted ) ] ] } - , { icon = - if Maybe.withDefault settings.nativePdfPreview model.pdfNativeView then - "fa fa-toggle-on" - - else - "fa fa-toggle-off" - , label = texts.renderPdfByBrowser - , attrs = - [ onClick (TogglePdfNativeView settings.nativePdfPreview) - , href "#" - ] - } , { icon = if isAttachMetaOpen model attach.id then "fa fa-toggle-on" diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index 4299e632..c6b41490 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -913,19 +913,6 @@ update key flags inav settings msg model = Nothing -> resultModel model - TogglePdfNativeView default -> - resultModel - { model - | pdfNativeView = - case model.pdfNativeView of - Just flag -> - Just (not flag) - - Nothing -> - Just (not default) - , attachmentDropdownOpen = False - } - DeleteAttachConfirmed attachId -> let cmd = diff --git a/modules/webapp/src/main/elm/Comp/UiSettingsForm.elm b/modules/webapp/src/main/elm/Comp/UiSettingsForm.elm index 4e4b1632..ecfb6781 100644 --- a/modules/webapp/src/main/elm/Comp/UiSettingsForm.elm +++ b/modules/webapp/src/main/elm/Comp/UiSettingsForm.elm @@ -28,6 +28,7 @@ import Data.DropdownStyle as DS import Data.Fields exposing (Field) import Data.Flags exposing (Flags) import Data.ItemTemplate as IT exposing (ItemTemplate) +import Data.Pdf exposing (PdfMode) import Data.TagOrder import Data.UiSettings exposing (ItemPattern, Pos(..), UiSettings) import Dict exposing (Dict) @@ -50,7 +51,8 @@ type alias Model = , searchPageSizeModel : Comp.IntField.Model , tagColors : Dict String Color , tagColorModel : Comp.ColorTagger.Model - , nativePdfPreview : Bool + , pdfMode : PdfMode + , pdfModeModel : Comp.FixedDropdown.Model PdfMode , itemSearchNoteLength : Maybe Int , searchNoteLengthModel : Comp.IntField.Model , searchMenuFolderCount : Maybe Int @@ -122,7 +124,8 @@ init flags settings = Comp.ColorTagger.init [] Data.Color.all - , nativePdfPreview = settings.nativePdfPreview + , pdfMode = settings.pdfMode + , pdfModeModel = Comp.FixedDropdown.init Data.Pdf.allModes , itemSearchNoteLength = Just settings.itemSearchNoteLength , searchNoteLengthModel = Comp.IntField.init @@ -169,7 +172,6 @@ type Msg = SearchPageSizeMsg Comp.IntField.Msg | TagColorMsg Comp.ColorTagger.Msg | GetTagsResp (Result Http.Error TagList) - | TogglePdfPreview | NoteLengthMsg Comp.IntField.Msg | SearchMenuFolderMsg Comp.IntField.Msg | SearchMenuTagMsg Comp.IntField.Msg @@ -185,6 +187,7 @@ type Msg | ToggleSideMenuVisible | TogglePowerSearch | UiLangMsg (Comp.FixedDropdown.Msg UiLanguage) + | PdfModeMsg (Comp.FixedDropdown.Msg PdfMode) @@ -290,15 +293,6 @@ update sett msg model = in ( model_, nextSettings ) - TogglePdfPreview -> - let - flag = - not model.nativePdfPreview - in - ( { model | nativePdfPreview = flag } - , Just { sett | nativePdfPreview = flag } - ) - GetTagsResp (Ok tl) -> let categories = @@ -463,6 +457,22 @@ update sett msg model = Just { sett | uiLang = newLang } ) + PdfModeMsg lm -> + let + ( m, sel ) = + Comp.FixedDropdown.update lm model.pdfModeModel + + newMode = + Maybe.withDefault model.pdfMode sel + in + ( { model | pdfModeModel = m, pdfMode = newMode } + , if newMode == model.pdfMode then + Nothing + + else + Just { sett | pdfMode = newMode } + ) + --- View2 @@ -516,6 +526,13 @@ settingFormTabs texts flags _ model = , style = DS.mainStyle , selectPlaceholder = texts.basics.selectPlaceholder } + + pdfModeCfg = + { display = texts.pdfMode + , icon = \_ -> Nothing + , style = DS.mainStyle + , selectPlaceholder = texts.basics.selectPlaceholder + } in [ { name = "general" , title = texts.general @@ -689,13 +706,14 @@ settingFormTabs texts flags _ model = , info = Nothing , body = [ div [ class "mb-4" ] - [ MB.viewItem <| - MB.Checkbox - { tagger = \_ -> TogglePdfPreview - , label = texts.browserNativePdfView - , value = model.nativePdfPreview - , id = "uisetting-pdfpreview-toggle" - } + [ label [ class S.inputLabel ] [ text texts.browserNativePdfView ] + , Html.map PdfModeMsg + (Comp.FixedDropdown.viewStyled2 + pdfModeCfg + False + (Just model.pdfMode) + model.pdfModeModel + ) ] , div [ class "mb-4" ] [ MB.viewItem <| diff --git a/modules/webapp/src/main/elm/Data/Flags.elm b/modules/webapp/src/main/elm/Data/Flags.elm index e605bd50..5ebfa9c7 100644 --- a/modules/webapp/src/main/elm/Data/Flags.elm +++ b/modules/webapp/src/main/elm/Data/Flags.elm @@ -41,6 +41,7 @@ type alias Config = type alias Flags = { account : Maybe AuthResult + , pdfSupported : Bool , config : Config } diff --git a/modules/webapp/src/main/elm/Data/Pdf.elm b/modules/webapp/src/main/elm/Data/Pdf.elm new file mode 100644 index 00000000..e4c691b8 --- /dev/null +++ b/modules/webapp/src/main/elm/Data/Pdf.elm @@ -0,0 +1,67 @@ +module Data.Pdf exposing (PdfMode(..), allModes, asString, detectUrl, fromString, serverUrl) + +{-| Makes use of the fact, that docspell uses a `/view` suffix on the +path to provide a browser independent PDF view. +-} + +import Data.Flags exposing (Flags) +import Html exposing (..) +import Html.Attributes exposing (..) + + +type PdfMode + = Detect + | Native + | Server + + +allModes : List PdfMode +allModes = + [ Detect, Native, Server ] + + +asString : PdfMode -> String +asString mode = + case mode of + Detect -> + "detect" + + Native -> + "native" + + Server -> + "server" + + +fromString : String -> Maybe PdfMode +fromString str = + case String.toLower str of + "detect" -> + Just Detect + + "native" -> + Just Native + + "server" -> + Just Server + + _ -> + Nothing + + +serverUrl : String -> String +serverUrl url = + if String.endsWith "/" url then + url ++ "view" + + else + url ++ "/view" + + +detectUrl : Flags -> String -> String +detectUrl flags url = + if flags.pdfSupported then + url + + else + serverUrl url diff --git a/modules/webapp/src/main/elm/Data/UiSettings.elm b/modules/webapp/src/main/elm/Data/UiSettings.elm index fa7180df..3b5d4d80 100644 --- a/modules/webapp/src/main/elm/Data/UiSettings.elm +++ b/modules/webapp/src/main/elm/Data/UiSettings.elm @@ -20,6 +20,7 @@ module Data.UiSettings exposing , fieldVisible , merge , mergeDefaults + , pdfUrl , posFromString , posToString , storedUiSettingsDecoder @@ -34,7 +35,9 @@ import Api.Model.Tag exposing (Tag) import Data.BasicSize exposing (BasicSize) import Data.Color exposing (Color) import Data.Fields exposing (Field) +import Data.Flags exposing (Flags) import Data.ItemTemplate exposing (ItemTemplate) +import Data.Pdf exposing (PdfMode) import Data.UiTheme exposing (UiTheme) import Dict exposing (Dict) import Html exposing (Attribute) @@ -57,7 +60,7 @@ force default settings. type alias StoredUiSettings = { itemSearchPageSize : Maybe Int , tagCategoryColors : List ( String, String ) - , nativePdfPreview : Bool + , pdfMode : Maybe String , itemSearchNoteLength : Maybe Int , itemDetailNotesPosition : Maybe String , searchMenuFolderCount : Maybe Int @@ -91,7 +94,7 @@ storedUiSettingsDecoder = Decode.succeed StoredUiSettings |> P.optional "itemSearchPageSize" maybeInt Nothing |> P.optional "tagCategoryColors" (Decode.keyValuePairs Decode.string) [] - |> P.optional "nativePdfPreview" Decode.bool False + |> P.optional "pdfMode" maybeString Nothing |> P.optional "itemSearchNoteLength" maybeInt Nothing |> P.optional "itemDetailNotesPosition" maybeString Nothing |> P.optional "searchMenuFolderCount" maybeInt Nothing @@ -121,7 +124,7 @@ storedUiSettingsEncode value = Encode.object [ ( "itemSearchPageSize", maybeEnc Encode.int value.itemSearchPageSize ) , ( "tagCategoryColors", Encode.dict identity Encode.string (Dict.fromList value.tagCategoryColors) ) - , ( "nativePdfPreview", Encode.bool value.nativePdfPreview ) + , ( "pdfMode", maybeEnc Encode.string value.pdfMode ) , ( "itemSearchNoteLength", maybeEnc Encode.int value.itemSearchNoteLength ) , ( "itemDetailNotesPosition", maybeEnc Encode.string value.itemDetailNotesPosition ) , ( "searchMenuFolderCount", maybeEnc Encode.int value.searchMenuFolderCount ) @@ -146,14 +149,15 @@ storedUiSettingsEncode value = {-| Settings for the web ui. These fields are all mandatory, since there is always a default value. -When loaded from local storage, all optional fields can fallback to a -default value, converting the StoredUiSettings into a UiSettings. +When loaded from local storage or the server, all optional fields can +fallback to a default value, converting the StoredUiSettings into a +UiSettings. -} type alias UiSettings = { itemSearchPageSize : Int , tagCategoryColors : Dict String Color - , nativePdfPreview : Bool + , pdfMode : PdfMode , itemSearchNoteLength : Int , itemDetailNotesPosition : Pos , searchMenuFolderCount : Int @@ -219,7 +223,7 @@ defaults : UiSettings defaults = { itemSearchPageSize = 60 , tagCategoryColors = Dict.empty - , nativePdfPreview = False + , pdfMode = Data.Pdf.Detect , itemSearchNoteLength = 0 , itemDetailNotesPosition = Bottom , searchMenuFolderCount = 3 @@ -259,7 +263,10 @@ merge given fallback = |> Dict.map (\_ -> Maybe.withDefault Data.Color.Grey) ) fallback.tagCategoryColors - , nativePdfPreview = given.nativePdfPreview + , pdfMode = + given.pdfMode + |> Maybe.andThen Data.Pdf.fromString + |> Maybe.withDefault fallback.pdfMode , itemSearchNoteLength = choose given.itemSearchNoteLength fallback.itemSearchNoteLength , itemDetailNotesPosition = @@ -313,7 +320,7 @@ toStoredUiSettings settings = , tagCategoryColors = Dict.map (\_ -> Data.Color.toString) settings.tagCategoryColors |> Dict.toList - , nativePdfPreview = settings.nativePdfPreview + , pdfMode = Just (Data.Pdf.asString settings.pdfMode) , itemSearchNoteLength = Just settings.itemSearchNoteLength , itemDetailNotesPosition = Just (posToString settings.itemDetailNotesPosition) , searchMenuFolderCount = Just settings.searchMenuFolderCount @@ -407,6 +414,19 @@ cardPreviewSize2 settings = "max-h-80" +pdfUrl : UiSettings -> Flags -> String -> String +pdfUrl settings flags originalUrl = + case settings.pdfMode of + Data.Pdf.Detect -> + Data.Pdf.detectUrl flags originalUrl + + Data.Pdf.Native -> + originalUrl + + Data.Pdf.Server -> + Data.Pdf.serverUrl originalUrl + + --- Helpers diff --git a/modules/webapp/src/main/elm/Messages/Comp/UiSettingsForm.elm b/modules/webapp/src/main/elm/Messages/Comp/UiSettingsForm.elm index 929128bd..a09151a3 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/UiSettingsForm.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/UiSettingsForm.elm @@ -13,9 +13,11 @@ module Messages.Comp.UiSettingsForm exposing import Data.Color exposing (Color) import Data.Fields exposing (Field) +import Data.Pdf exposing (PdfMode) import Messages.Basics import Messages.Data.Color import Messages.Data.Fields +import Messages.Data.PdfMode type alias Texts = @@ -53,6 +55,7 @@ type alias Texts = , fieldsInfo : String , fieldLabel : Field -> String , templateHelpMessage : String + , pdfMode : PdfMode -> String } @@ -127,6 +130,7 @@ for example `{{corrOrg|corrPerson|-}}` would render the organization and if that is not present the person. If both are absent a dash `-` is rendered. """ + , pdfMode = Messages.Data.PdfMode.gb } @@ -203,4 +207,5 @@ verknüpft werden, bis zur ersten die einen Wert enthält. Zum Beispiel: oder, wenn diese leer ist, die Person. Sind beide leer wird ein `-` dargestellt. """ + , pdfMode = Messages.Data.PdfMode.de } diff --git a/modules/webapp/src/main/elm/Messages/Data/PdfMode.elm b/modules/webapp/src/main/elm/Messages/Data/PdfMode.elm new file mode 100644 index 00000000..73748076 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Data/PdfMode.elm @@ -0,0 +1,39 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Data.PdfMode exposing + ( de + , gb + ) + +import Data.Pdf exposing (PdfMode(..)) + + +gb : PdfMode -> String +gb st = + case st of + Detect -> + "Detect automatically" + + Native -> + "Use the browser's native PDF view" + + Server -> + "Use cross-browser fallback" + + +de : PdfMode -> String +de st = + case st of + Detect -> + "Automatisch ermitteln" + + Native -> + "Browsernative Darstellung" + + Server -> + "Browserübergreifende Ersatzdarstellung" diff --git a/modules/webapp/src/main/elm/Page/Home/View2.elm b/modules/webapp/src/main/elm/Page/Home/View2.elm index da26662d..515c8c51 100644 --- a/modules/webapp/src/main/elm/Page/Home/View2.elm +++ b/modules/webapp/src/main/elm/Page/Home/View2.elm @@ -460,7 +460,7 @@ searchStats texts _ settings model = itemCardList : Texts -> Flags -> UiSettings -> Model -> List (Html Msg) -itemCardList texts _ settings model = +itemCardList texts flags settings model = let previewUrl attach = Api.attachmentPreviewURL attach.id @@ -489,6 +489,7 @@ itemCardList texts _ settings model = (Comp.ItemCardList.view2 texts.itemCardList itemViewCfg settings + flags model.itemListModel ) , loadMore texts settings model diff --git a/modules/webapp/src/main/elm/Page/Share/Results.elm b/modules/webapp/src/main/elm/Page/Share/Results.elm index e47d8583..6785663c 100644 --- a/modules/webapp/src/main/elm/Page/Share/Results.elm +++ b/modules/webapp/src/main/elm/Page/Share/Results.elm @@ -9,6 +9,7 @@ module Page.Share.Results exposing (view) import Api import Comp.ItemCardList +import Data.Flags exposing (Flags) import Data.ItemSelection import Data.UiSettings exposing (UiSettings) import Html exposing (..) @@ -18,8 +19,8 @@ import Page exposing (Page(..)) import Page.Share.Data exposing (Model, Msg(..)) -view : Texts -> UiSettings -> String -> Model -> Html Msg -view texts settings shareId model = +view : Texts -> UiSettings -> Flags -> String -> Model -> Html Msg +view texts settings flags shareId model = let viewCfg = { current = Nothing @@ -32,5 +33,5 @@ view texts settings shareId model = in div [] [ Html.map ItemListMsg - (Comp.ItemCardList.view2 texts.itemCardList viewCfg settings model.itemListModel) + (Comp.ItemCardList.view2 texts.itemCardList viewCfg settings flags model.itemListModel) ] diff --git a/modules/webapp/src/main/elm/Page/Share/View.elm b/modules/webapp/src/main/elm/Page/Share/View.elm index baaad70a..49bf8803 100644 --- a/modules/webapp/src/main/elm/Page/Share/View.elm +++ b/modules/webapp/src/main/elm/Page/Share/View.elm @@ -63,7 +63,7 @@ viewContent texts flags versionInfo uiSettings shareId model = mainContent : Texts -> Flags -> UiSettings -> String -> Model -> Html Msg -mainContent texts _ settings shareId model = +mainContent texts flags settings shareId model = div [ id "content" , class "h-full flex flex-col" @@ -76,7 +76,7 @@ mainContent texts _ settings shareId model = [ text <| Maybe.withDefault "" model.verifyResult.name ] , Menubar.view texts model - , Results.view texts settings shareId model + , Results.view texts settings flags shareId model ] From f25d40b493ecd1027f4d8dd8b7f19d086825b50e Mon Sep 17 00:00:00 2001 From: eikek Date: Thu, 7 Oct 2021 01:33:59 +0200 Subject: [PATCH 20/37] First simple item detail version for a share --- modules/webapp/src/main/elm/Data/Icons.elm | 12 -- .../main/elm/Messages/Page/ShareDetail.elm | 8 ++ .../src/main/elm/Page/ShareDetail/View.elm | 105 ++++++++++++++++-- modules/webapp/src/main/elm/Styles.elm | 2 +- modules/webapp/src/main/elm/Util/Item.elm | 35 ++++++ 5 files changed, 141 insertions(+), 21 deletions(-) diff --git a/modules/webapp/src/main/elm/Data/Icons.elm b/modules/webapp/src/main/elm/Data/Icons.elm index 76a5f360..dbe79fc0 100644 --- a/modules/webapp/src/main/elm/Data/Icons.elm +++ b/modules/webapp/src/main/elm/Data/Icons.elm @@ -68,9 +68,7 @@ module Data.Icons exposing , tag2 , tagIcon , tagIcon2 - , tags , tags2 - , tagsIcon , tagsIcon2 ) @@ -361,16 +359,6 @@ tagIcon2 classes = i [ class (tag2 ++ " " ++ classes) ] [] -tags : String -tags = - "tags icon" - - -tagsIcon : String -> Html msg -tagsIcon classes = - i [ class (tags ++ " " ++ classes) ] [] - - tags2 : String tags2 = "fa fa-tags" diff --git a/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm b/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm index 71ab9536..a9392d9a 100644 --- a/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm +++ b/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm @@ -1,20 +1,28 @@ module Messages.Page.ShareDetail exposing (..) import Messages.Comp.SharePasswordForm +import Messages.DateFormat +import Messages.UiLanguage exposing (UiLanguage(..)) type alias Texts = { passwordForm : Messages.Comp.SharePasswordForm.Texts + , formatDateLong : Int -> String + , formatDateShort : Int -> String } gb : Texts gb = { passwordForm = Messages.Comp.SharePasswordForm.gb + , formatDateLong = Messages.DateFormat.formatDateLong English + , formatDateShort = Messages.DateFormat.formatDateShort English } de : Texts de = { passwordForm = Messages.Comp.SharePasswordForm.de + , formatDateLong = Messages.DateFormat.formatDateLong German + , formatDateShort = Messages.DateFormat.formatDateShort German } diff --git a/modules/webapp/src/main/elm/Page/ShareDetail/View.elm b/modules/webapp/src/main/elm/Page/ShareDetail/View.elm index aad6fa3f..7d06fc65 100644 --- a/modules/webapp/src/main/elm/Page/ShareDetail/View.elm +++ b/modules/webapp/src/main/elm/Page/ShareDetail/View.elm @@ -1,9 +1,12 @@ module Page.ShareDetail.View exposing (viewContent, viewSidebar) +import Api import Api.Model.VersionInfo exposing (VersionInfo) import Comp.Basic as B import Comp.SharePasswordForm import Data.Flags exposing (Flags) +import Data.Icons as Icons +import Data.ItemTemplate as IT exposing (ItemTemplate) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) @@ -11,6 +14,8 @@ import Messages.Page.ShareDetail exposing (Texts) import Page exposing (Page(..)) import Page.ShareDetail.Data exposing (..) import Styles as S +import Util.CustomField +import Util.Item viewSidebar : Texts -> Bool -> Flags -> UiSettings -> Model -> Html Msg @@ -55,7 +60,7 @@ mainContent texts flags settings shareId model = , class S.content ] [ itemHead texts shareId model - , div [ class "flex flex-col sm:flex-row" ] + , div [ class "flex flex-col sm:flex-row sm:space-x-4 relative h-full" ] [ itemData texts model , itemPreview texts flags settings model ] @@ -64,16 +69,92 @@ mainContent texts flags settings shareId model = itemData : Texts -> Model -> Html Msg itemData texts model = - div [ class "flex" ] - [] + let + boxStyle = + "mb-4 sm:mb-6 max-w-sm" + + headerStyle = + "py-2 bg-blue-50 hover:bg-blue-100 dark:bg-bluegray-700 dark:hover:bg-opacity-100 dark:hover:bg-bluegray-600 text-lg font-medium rounded-lg" + + showTag tag = + div + [ class "flex ml-2 mt-1 font-semibold hover:opacity-75" + , class S.basicLabel + ] + [ i [ class "fa fa-tag mr-2" ] [] + , text tag.name + ] + + showField = + Util.CustomField.renderValue2 + [ ( S.basicLabel, True ) + , ( "flex ml-2 mt-1 font-semibold hover:opacity-75", True ) + ] + Nothing + in + div [ class "flex flex-col pr-2 sm:w-1/3" ] + [ div [ class boxStyle ] + [ div [ class headerStyle ] + [ Icons.dateIcon2 "mr-2 ml-2" + , text "Date" + ] + , div [ class "text-lg ml-2" ] + [ Util.Item.toItemLight model.item + |> IT.render IT.dateLong (templateCtx texts) + |> text + ] + ] + , div [ class boxStyle ] + [ div [ class headerStyle ] + [ Icons.tagsIcon2 "mr-2 ml-2" + , text "Tags & Fields" + ] + , div [ class "flex flex-row items-center flex-wrap font-medium my-1" ] + (List.map showTag model.item.tags ++ List.map showField model.item.customfields) + ] + , div [ class boxStyle ] + [ div [ class headerStyle ] + [ Icons.correspondentIcon2 "mr-2 ml-2" + , text "Correspondent" + ] + , div [ class "text-lg ml-2" ] + [ Util.Item.toItemLight model.item + |> IT.render IT.correspondent (templateCtx texts) + |> text + ] + ] + , div [ class boxStyle ] + [ div [ class headerStyle ] + [ Icons.concernedIcon2 "mr-2 ml-2" + , text "Concerning" + ] + , div [ class "text-lg ml-2" ] + [ Util.Item.toItemLight model.item + |> IT.render IT.concerning (templateCtx texts) + |> text + ] + ] + ] -{-| Using ItemDetail Model to be able to reuse SingleAttachment component --} itemPreview : Texts -> Flags -> UiSettings -> Model -> Html Msg itemPreview texts flags settings model = - div [ class "flex flex-grow" ] - [] + let + id = + List.head model.item.attachments + |> Maybe.map .id + |> Maybe.withDefault "" + in + div + [ class "flex flex-grow" + , style "min-height" "500px" + ] + [ embed + [ src (Data.UiSettings.pdfUrl settings flags (Api.shareFileURL id)) + , class " h-full w-full mx-0 py-0" + ] + [] + ] itemHead : Texts -> String -> Model -> Html Msg @@ -84,7 +165,7 @@ itemHead texts shareId model = [ text model.item.name ] ] - , div [ class "flex flex-row items-center justify-end" ] + , div [ class "flex flex-row items-center justify-end mb-2 sm:mb-0" ] [ B.secondaryBasicButton { label = "Close" , icon = "fa fa-times" @@ -106,3 +187,11 @@ passwordContent texts flags versionInfo model = [ Html.map PasswordMsg (Comp.SharePasswordForm.view texts.passwordForm flags versionInfo model.passwordModel) ] + + +templateCtx : Texts -> IT.TemplateContext +templateCtx texts = + { dateFormatLong = texts.formatDateLong + , dateFormatShort = texts.formatDateShort + , directionLabel = \_ -> "" + } diff --git a/modules/webapp/src/main/elm/Styles.elm b/modules/webapp/src/main/elm/Styles.elm index f33ba301..2c8167dd 100644 --- a/modules/webapp/src/main/elm/Styles.elm +++ b/modules/webapp/src/main/elm/Styles.elm @@ -325,7 +325,7 @@ border2 = header1 : String header1 = - " text-3xl mt-3 mb-5 font-semibold tracking-wide break-all" + " text-3xl mt-3 mb-3 sm:mb-5 font-semibold tracking-wide break-all" header2 : String diff --git a/modules/webapp/src/main/elm/Util/Item.elm b/modules/webapp/src/main/elm/Util/Item.elm index c14a3b8d..3a8c8ea3 100644 --- a/modules/webapp/src/main/elm/Util/Item.elm +++ b/modules/webapp/src/main/elm/Util/Item.elm @@ -8,14 +8,49 @@ module Util.Item exposing ( concTemplate , corrTemplate + , toItemLight ) +import Api.Model.Attachment exposing (Attachment) +import Api.Model.AttachmentLight exposing (AttachmentLight) +import Api.Model.ItemDetail exposing (ItemDetail) import Api.Model.ItemLight exposing (ItemLight) import Data.Fields import Data.ItemTemplate as IT exposing (ItemTemplate) import Data.UiSettings exposing (UiSettings) +toItemLight : ItemDetail -> ItemLight +toItemLight detail = + { id = detail.id + , name = detail.name + , state = detail.state + , date = Maybe.withDefault detail.created detail.itemDate + , dueDate = detail.dueDate + , source = detail.source + , direction = Just detail.direction + , corrOrg = detail.corrOrg + , corrPerson = detail.corrPerson + , concPerson = detail.concPerson + , concEquipment = detail.concEquipment + , folder = detail.folder + , attachments = List.indexedMap toAttachmentLight detail.attachments + , tags = detail.tags + , customfields = detail.customfields + , notes = detail.notes + , highlighting = [] + } + + +toAttachmentLight : Int -> Attachment -> AttachmentLight +toAttachmentLight index attach = + { id = attach.id + , position = index + , name = attach.name + , pageCount = Nothing + } + + corrTemplate : UiSettings -> ItemTemplate corrTemplate settings = let From 02cbd95e0d8e7761671eb9e17cb1a5453d126395 Mon Sep 17 00:00:00 2001 From: eikek Date: Thu, 7 Oct 2021 15:39:32 +0200 Subject: [PATCH 21/37] Increment share access on verify --- .../backend/src/main/scala/docspell/backend/ops/OShare.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index 57a2c236..4b8ae0f6 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -194,7 +194,9 @@ object OShare { val shareKey = share.password.map(pw => key ++ pw.asByteVector).getOrElse(key) - val token = ShareToken.create(id, shareKey) + val token = ShareToken + .create(id, shareKey) + .flatTap(_ => store.transact(RShare.incAccess(share.id))) pwCheck match { case Some(true) => token.map(t => VerifyResult.success(t, share.name)) case None => token.map(t => VerifyResult.success(t, share.name)) From 006791deb42245b765fb64a796875379e79c1215 Mon Sep 17 00:00:00 2001 From: eikek Date: Thu, 7 Oct 2021 15:41:08 +0200 Subject: [PATCH 22/37] Fix curl command in exim conf --- tools/exim/exim.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/exim/exim.conf b/tools/exim/exim.conf index e2d7d6b4..25926737 100644 --- a/tools/exim/exim.conf +++ b/tools/exim/exim.conf @@ -28,7 +28,7 @@ require verify = recipient require message = Recipient unknown - condition = ${run{/usr/bin/curl --out /dev/null --silent --fail -H "Docspell-Integration: ${env{DS_HEADER}{$value} fail}" "${env{DS_URL}{$value} fail}/api/v1/open/integration/item/$local_part"}{yes}{no}} + condition = ${run{/usr/bin/curl --output /dev/null --silent --fail -H "Docspell-Integration: ${env{DS_HEADER}{$value} fail}" "${env{DS_URL}{$value} fail}/api/v1/open/integration/item/$local_part"}{yes}{no}} warn message = Reverse lookup failed !verify = reverse_host_lookup @@ -48,7 +48,7 @@ local_users: begin transports docspell: driver = pipe - command = /usr/bin/curl --out /dev/null --silent --fail -H "Docspell-Integration: ${env{DS_HEADER}{$value} fail}" -F "file=@-;filename=\"$h_subject:\"" "${env{DS_URL}{$value} fail}/api/v1/open/integration/item/$local_part" + command = /usr/bin/curl --output /dev/null --silent --fail -H "Docspell-Integration: ${env{DS_HEADER}{$value} fail}" -F "file=@-;filename=\"$h_subject:\"" "${env{DS_URL}{$value} fail}/api/v1/open/integration/item/$local_part" return_fail_output user = nobody delivery_date_add From 7cbdf919f43dde2ab7a90e258b7f2422a5b7f77c Mon Sep 17 00:00:00 2001 From: eikek Date: Thu, 7 Oct 2021 22:02:31 +0200 Subject: [PATCH 23/37] Show item detail for a shared item --- modules/webapp/elm.json | 4 +- modules/webapp/src/main/elm/App/Update.elm | 15 +- modules/webapp/src/main/elm/App/View2.elm | 2 + .../main/elm/Comp/ItemDetail/ShowQrCode.elm | 5 +- modules/webapp/src/main/elm/Comp/OtpSetup.elm | 5 +- .../webapp/src/main/elm/Comp/ShareView.elm | 5 +- .../webapp/src/main/elm/Comp/SourceManage.elm | 5 +- modules/webapp/src/main/elm/Comp/UrlCopy.elm | 95 +++++++ .../src/main/elm/Messages/Page/Share.elm | 8 + .../main/elm/Messages/Page/ShareDetail.elm | 26 ++ modules/webapp/src/main/elm/Page.elm | 3 + .../webapp/src/main/elm/Page/Share/Data.elm | 17 +- .../webapp/src/main/elm/Page/Share/View.elm | 33 ++- .../src/main/elm/Page/ShareDetail/Data.elm | 14 +- .../src/main/elm/Page/ShareDetail/Update.elm | 26 +- .../src/main/elm/Page/ShareDetail/View.elm | 241 +++++++++++++++--- modules/webapp/src/main/elm/Styles.elm | 7 +- modules/webapp/src/main/webjar/docspell.js | 4 +- 18 files changed, 458 insertions(+), 57 deletions(-) create mode 100644 modules/webapp/src/main/elm/Comp/UrlCopy.elm diff --git a/modules/webapp/elm.json b/modules/webapp/elm.json index 047c5cda..b1071ec9 100644 --- a/modules/webapp/elm.json +++ b/modules/webapp/elm.json @@ -16,12 +16,13 @@ "elm/html": "1.0.0", "elm/http": "2.0.0", "elm/json": "1.1.3", + "elm/svg": "1.0.1", "elm/time": "1.0.0", "elm/url": "1.0.0", "elm-explorations/markdown": "1.0.0", "justinmimbs/date": "3.1.2", "norpan/elm-html5-drag-drop": "3.1.4", - "pablohirafuji/elm-qrcode": "3.3.1", + "pablohirafuji/elm-qrcode": "4.0.1", "ryannhg/date-format": "2.3.0", "truqu/elm-base64": "2.0.4", "ursi/elm-scroll": "1.0.0", @@ -33,7 +34,6 @@ "elm/bytes": "1.0.8", "elm/parser": "1.1.0", "elm/regex": "1.0.0", - "elm/svg": "1.0.1", "elm/virtual-dom": "1.0.2", "elm-community/list-extra": "8.2.4", "folkertdev/elm-flate": "2.0.4", diff --git a/modules/webapp/src/main/elm/App/Update.elm b/modules/webapp/src/main/elm/App/Update.elm index 19a689cb..66002c92 100644 --- a/modules/webapp/src/main/elm/App/Update.elm +++ b/modules/webapp/src/main/elm/App/Update.elm @@ -613,8 +613,19 @@ initPage model_ page = ] model - SharePage _ -> - ( model, Cmd.none, Sub.none ) + SharePage id -> + let + cmd = + Cmd.map ShareMsg (Page.Share.Data.initCmd id model.flags) + + shareModel = + model.shareModel + in + if shareModel.initialized then + ( model, Cmd.none, Sub.none ) + + else + ( { model | shareModel = { shareModel | initialized = True } }, cmd, Sub.none ) ShareDetailPage _ _ -> case model_.page of diff --git a/modules/webapp/src/main/elm/App/View2.elm b/modules/webapp/src/main/elm/App/View2.elm index 3dcaba5a..6e5652a5 100644 --- a/modules/webapp/src/main/elm/App/View2.elm +++ b/modules/webapp/src/main/elm/App/View2.elm @@ -451,6 +451,8 @@ viewShareDetail texts shareId itemId model = model.sidebarVisible model.flags model.uiSettings + shareId + itemId model.shareDetailModel ) , Html.map ShareDetailMsg diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/ShowQrCode.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/ShowQrCode.elm index cfd1ae22..a6e3a96f 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/ShowQrCode.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/ShowQrCode.elm @@ -17,6 +17,7 @@ import Html.Attributes exposing (..) import Html.Events exposing (onClick) import QRCode import Styles as S +import Svg.Attributes as SvgA view : Flags -> String -> Model -> UrlId -> Html Msg @@ -111,7 +112,7 @@ type UrlId qrCodeView : String -> Html msg qrCodeView message = - QRCode.encode message - |> Result.map QRCode.toSvg + QRCode.fromString message + |> Result.map (QRCode.toSvg [ SvgA.class "w-64 h-64" ]) |> Result.withDefault (text "Error generating QR code") diff --git a/modules/webapp/src/main/elm/Comp/OtpSetup.elm b/modules/webapp/src/main/elm/Comp/OtpSetup.elm index 46db0f75..c972eb95 100644 --- a/modules/webapp/src/main/elm/Comp/OtpSetup.elm +++ b/modules/webapp/src/main/elm/Comp/OtpSetup.elm @@ -23,6 +23,7 @@ import Markdown import Messages.Comp.OtpSetup exposing (Texts) import QRCode import Styles as S +import Svg.Attributes as SvgA type Model @@ -389,8 +390,8 @@ viewDisabled texts model = qrCodeView : Texts -> String -> Html msg qrCodeView texts message = - QRCode.encode message - |> Result.map QRCode.toSvg + QRCode.fromString message + |> Result.map (QRCode.toSvg [ SvgA.class "w-64 h-64" ]) |> Result.withDefault (Html.text texts.errorGeneratingQR) diff --git a/modules/webapp/src/main/elm/Comp/ShareView.elm b/modules/webapp/src/main/elm/Comp/ShareView.elm index f7d4962f..dbb33752 100644 --- a/modules/webapp/src/main/elm/Comp/ShareView.elm +++ b/modules/webapp/src/main/elm/Comp/ShareView.elm @@ -14,6 +14,7 @@ import Html.Attributes exposing (..) import Messages.Comp.ShareView exposing (Texts) import QRCode import Styles as S +import Svg.Attributes as SvgA type alias ViewSettings = @@ -178,7 +179,7 @@ viewDisabled cfg texts share = qrCodeView : Texts -> String -> Html msg qrCodeView texts message = - QRCode.encode message - |> Result.map QRCode.toSvg + QRCode.fromString message + |> Result.map (QRCode.toSvg [ SvgA.class "w-64 h-64" ]) |> Result.withDefault (Html.text texts.qrCodeError) diff --git a/modules/webapp/src/main/elm/Comp/SourceManage.elm b/modules/webapp/src/main/elm/Comp/SourceManage.elm index 5ce42136..629ac18e 100644 --- a/modules/webapp/src/main/elm/Comp/SourceManage.elm +++ b/modules/webapp/src/main/elm/Comp/SourceManage.elm @@ -32,6 +32,7 @@ import Messages.Comp.SourceManage exposing (Texts) import Ports import QRCode import Styles as S +import Svg.Attributes as SvgA type alias Model = @@ -226,8 +227,8 @@ update flags msg model = qrCodeView : Texts -> String -> Html msg qrCodeView texts message = - QRCode.encode message - |> Result.map QRCode.toSvg + QRCode.fromString message + |> Result.map (QRCode.toSvg [ SvgA.class "w-64 h-64" ]) |> Result.withDefault (Html.text texts.errorGeneratingQR) diff --git a/modules/webapp/src/main/elm/Comp/UrlCopy.elm b/modules/webapp/src/main/elm/Comp/UrlCopy.elm new file mode 100644 index 00000000..2a6f048b --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/UrlCopy.elm @@ -0,0 +1,95 @@ +module Comp.UrlCopy exposing (..) + +import Comp.Basic as B +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) +import Ports +import QRCode +import Styles as S +import Svg.Attributes as SvgA + + +type Msg + = Print String + + +update : Msg -> Cmd msg +update msg = + case msg of + Print id -> + Ports.printElement id + + +initCopy : String -> Cmd msg +initCopy data = + Ports.initClipboard <| clipboardData data + + +clipboardData : String -> ( String, String ) +clipboardData data = + ( "share-url", "#button-share-url" ) + + +view : String -> Html Msg +view data = + let + ( elementId, buttonId ) = + clipboardData data + + btnId = + String.dropLeft 1 buttonId + + printId = + "print-qr-code" + in + div [ class "flex flex-col items-center" ] + [ div + [ class S.border + , class S.qrCode + , id printId + ] + [ qrCodeView data + ] + , div + [ class "flex w-64" + ] + [ p + [ id elementId + , class "font-mono text-xs py-2 mx-auto break-all" + ] + [ text data + ] + ] + , div [ class "flex flex-row mt-1 space-x-2 items-center w-full" ] + [ B.primaryButton + { label = "Copy" + , icon = "fa fa-copy" + , handler = href "#" + , disabled = False + , attrs = + [ id btnId + , class "flex flex-grow items-center justify-center" + , attribute "data-clipboard-target" ("#" ++ elementId) + ] + } + , B.primaryButton + { label = "Print" + , icon = "fa fa-print" + , handler = onClick (Print printId) + , disabled = False + , attrs = + [ href "#" + , class "flex flex-grow items-center justify-center" + ] + } + ] + ] + + +qrCodeView : String -> Html msg +qrCodeView message = + QRCode.fromString message + |> Result.map (QRCode.toSvg [ SvgA.class "w-64 h-64" ]) + |> Result.withDefault + (text "Error generating QR code") diff --git a/modules/webapp/src/main/elm/Messages/Page/Share.elm b/modules/webapp/src/main/elm/Messages/Page/Share.elm index f6b73d31..20884777 100644 --- a/modules/webapp/src/main/elm/Messages/Page/Share.elm +++ b/modules/webapp/src/main/elm/Messages/Page/Share.elm @@ -7,7 +7,9 @@ module Messages.Page.Share exposing (..) +import Http import Messages.Basics +import Messages.Comp.HttpError import Messages.Comp.ItemCardList import Messages.Comp.SearchMenu import Messages.Comp.SharePasswordForm @@ -18,6 +20,8 @@ type alias Texts = , basics : Messages.Basics.Texts , itemCardList : Messages.Comp.ItemCardList.Texts , passwordForm : Messages.Comp.SharePasswordForm.Texts + , httpError : Http.Error -> String + , authFailed : String } @@ -27,6 +31,8 @@ gb = , basics = Messages.Basics.gb , itemCardList = Messages.Comp.ItemCardList.gb , passwordForm = Messages.Comp.SharePasswordForm.gb + , authFailed = "This share does not exist." + , httpError = Messages.Comp.HttpError.gb } @@ -36,4 +42,6 @@ de = , basics = Messages.Basics.de , itemCardList = Messages.Comp.ItemCardList.de , passwordForm = Messages.Comp.SharePasswordForm.de + , authFailed = "Diese Freigabe existiert nicht." + , httpError = Messages.Comp.HttpError.de } diff --git a/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm b/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm index a9392d9a..70397991 100644 --- a/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm +++ b/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm @@ -1,28 +1,54 @@ module Messages.Page.ShareDetail exposing (..) +import Data.Fields exposing (Field) +import Http +import Messages.Basics +import Messages.Comp.HttpError import Messages.Comp.SharePasswordForm +import Messages.Data.Fields import Messages.DateFormat import Messages.UiLanguage exposing (UiLanguage(..)) type alias Texts = { passwordForm : Messages.Comp.SharePasswordForm.Texts + , basics : Messages.Basics.Texts + , field : Field -> String , formatDateLong : Int -> String , formatDateShort : Int -> String + , httpError : Http.Error -> String + , authFailed : String + , tagsAndFields : String + , noName : String + , unconfirmed : String } gb : Texts gb = { passwordForm = Messages.Comp.SharePasswordForm.gb + , basics = Messages.Basics.gb + , field = Messages.Data.Fields.gb , formatDateLong = Messages.DateFormat.formatDateLong English , formatDateShort = Messages.DateFormat.formatDateShort English + , authFailed = "This share does not exist." + , httpError = Messages.Comp.HttpError.gb + , tagsAndFields = "Tags & Fields" + , noName = "No name" + , unconfirmed = "Unconfirmed" } de : Texts de = { passwordForm = Messages.Comp.SharePasswordForm.de + , basics = Messages.Basics.de + , field = Messages.Data.Fields.de , formatDateLong = Messages.DateFormat.formatDateLong German , formatDateShort = Messages.DateFormat.formatDateShort German + , authFailed = "Diese Freigabe existiert nicht." + , httpError = Messages.Comp.HttpError.de + , tagsAndFields = "Tags & Felder" + , noName = "Kein Name" + , unconfirmed = "Nicht bestätigt" } diff --git a/modules/webapp/src/main/elm/Page.elm b/modules/webapp/src/main/elm/Page.elm index 2f42ee2e..a14e5295 100644 --- a/modules/webapp/src/main/elm/Page.elm +++ b/modules/webapp/src/main/elm/Page.elm @@ -116,6 +116,9 @@ hasSidebar page = SharePage _ -> True + ShareDetailPage _ _ -> + True + _ -> isSecured page diff --git a/modules/webapp/src/main/elm/Page/Share/Data.elm b/modules/webapp/src/main/elm/Page/Share/Data.elm index 4be43360..505f4908 100644 --- a/modules/webapp/src/main/elm/Page/Share/Data.elm +++ b/modules/webapp/src/main/elm/Page/Share/Data.elm @@ -5,7 +5,7 @@ -} -module Page.Share.Data exposing (Mode(..), Model, Msg(..), PageError(..), init) +module Page.Share.Data exposing (Mode(..), Model, Msg(..), PageError(..), init, initCmd) import Api import Api.Model.ItemLightList exposing (ItemLightList) @@ -41,6 +41,7 @@ type alias Model = , powerSearchInput : Comp.PowerSearchInput.Model , searchInProgress : Bool , itemListModel : Comp.ItemCardList.Model + , initialized : Bool } @@ -54,17 +55,27 @@ emptyModel flags = , powerSearchInput = Comp.PowerSearchInput.init , searchInProgress = False , itemListModel = Comp.ItemCardList.init + , initialized = False } init : Maybe String -> Flags -> ( Model, Cmd Msg ) init shareId flags = + let + em = + emptyModel flags + in case shareId of Just id -> - ( emptyModel flags, Api.verifyShare flags (ShareSecret id Nothing) VerifyResp ) + ( { em | initialized = True }, Api.verifyShare flags (ShareSecret id Nothing) VerifyResp ) Nothing -> - ( emptyModel flags, Cmd.none ) + ( em, Cmd.none ) + + +initCmd : String -> Flags -> Cmd Msg +initCmd shareId flags = + Api.verifyShare flags (ShareSecret shareId Nothing) VerifyResp type Msg diff --git a/modules/webapp/src/main/elm/Page/Share/View.elm b/modules/webapp/src/main/elm/Page/Share/View.elm index 49bf8803..924b4631 100644 --- a/modules/webapp/src/main/elm/Page/Share/View.elm +++ b/modules/webapp/src/main/elm/Page/Share/View.elm @@ -42,13 +42,18 @@ viewContent texts flags versionInfo uiSettings shareId model = ModeInitial -> div [ id "content" - , class "h-full w-full flex flex-col text-5xl" + , class "h-full w-full flex flex-col" , class S.content ] - [ B.loadingDimmer - { active = model.pageError == PageErrorNone - , label = "" - } + [ div [ class " text-5xl" ] + [ B.loadingDimmer + { active = model.pageError == PageErrorNone + , label = "" + } + ] + , div [ class "my-4 text-lg" ] + [ errorMessage texts model + ] ] ModePassword -> @@ -76,10 +81,28 @@ mainContent texts flags settings shareId model = [ text <| Maybe.withDefault "" model.verifyResult.name ] , Menubar.view texts model + , errorMessage texts model , Results.view texts settings flags shareId model ] +errorMessage : Texts -> Model -> Html Msg +errorMessage texts model = + case model.pageError of + PageErrorNone -> + span [ class "hidden" ] [] + + PageErrorAuthFail -> + div [ class S.errorMessage ] + [ text texts.authFailed + ] + + PageErrorHttp err -> + div [ class S.errorMessage ] + [ text (texts.httpError err) + ] + + passwordContent : Texts -> Flags -> VersionInfo -> Model -> Html Msg passwordContent texts flags versionInfo model = div diff --git a/modules/webapp/src/main/elm/Page/ShareDetail/Data.elm b/modules/webapp/src/main/elm/Page/ShareDetail/Data.elm index 08c412bd..0f262dab 100644 --- a/modules/webapp/src/main/elm/Page/ShareDetail/Data.elm +++ b/modules/webapp/src/main/elm/Page/ShareDetail/Data.elm @@ -5,6 +5,7 @@ import Api.Model.ItemDetail exposing (ItemDetail) import Api.Model.ShareSecret exposing (ShareSecret) import Api.Model.ShareVerifyResult exposing (ShareVerifyResult) import Comp.SharePasswordForm +import Comp.UrlCopy import Data.Flags exposing (Flags) import Http @@ -27,6 +28,8 @@ type alias Model = , passwordModel : Comp.SharePasswordForm.Model , viewMode : ViewMode , pageError : PageError + , attachMenuOpen : Bool + , visibleAttach : Int } @@ -34,6 +37,9 @@ type Msg = VerifyResp (Result Http.Error ShareVerifyResult) | GetItemResp (Result Http.Error ItemDetail) | PasswordMsg Comp.SharePasswordForm.Msg + | SelectActiveAttachment Int + | ToggleSelectAttach + | UrlCopyMsg Comp.UrlCopy.Msg emptyModel : ViewMode -> Model @@ -43,14 +49,18 @@ emptyModel vm = , passwordModel = Comp.SharePasswordForm.init , viewMode = vm , pageError = PageErrorNone + , attachMenuOpen = False + , visibleAttach = 0 } init : Maybe ( String, String ) -> Flags -> ( Model, Cmd Msg ) init mids flags = case mids of - Just ( shareId, _ ) -> - ( emptyModel ViewLoading, Api.verifyShare flags (ShareSecret shareId Nothing) VerifyResp ) + Just ( shareId, itemId ) -> + ( emptyModel ViewLoading + , Api.verifyShare flags (ShareSecret shareId Nothing) VerifyResp + ) Nothing -> ( emptyModel ViewLoading, Cmd.none ) diff --git a/modules/webapp/src/main/elm/Page/ShareDetail/Update.elm b/modules/webapp/src/main/elm/Page/ShareDetail/Update.elm index 40aa5757..93ddabe2 100644 --- a/modules/webapp/src/main/elm/Page/ShareDetail/Update.elm +++ b/modules/webapp/src/main/elm/Page/ShareDetail/Update.elm @@ -2,7 +2,9 @@ module Page.ShareDetail.Update exposing (update) import Api import Comp.SharePasswordForm +import Comp.UrlCopy import Data.Flags exposing (Flags) +import Page exposing (Page(..)) import Page.ShareDetail.Data exposing (..) @@ -36,12 +38,16 @@ update shareId itemId flags msg model = ( { model | pageError = PageErrorHttp err }, Cmd.none ) GetItemResp (Ok item) -> + let + url = + Page.pageToString (ShareDetailPage shareId itemId) + in ( { model | item = item , viewMode = ViewNormal , pageError = PageErrorNone } - , Cmd.none + , Comp.UrlCopy.initCopy url ) GetItemResp (Err err) -> @@ -62,3 +68,21 @@ update shareId itemId flags msg model = Nothing -> ( { model | passwordModel = m }, Cmd.map PasswordMsg c ) + + SelectActiveAttachment pos -> + ( { model + | visibleAttach = pos + , attachMenuOpen = False + } + , Cmd.none + ) + + ToggleSelectAttach -> + ( { model | attachMenuOpen = not model.attachMenuOpen }, Cmd.none ) + + UrlCopyMsg lm -> + let + cmd = + Comp.UrlCopy.update lm + in + ( model, cmd ) diff --git a/modules/webapp/src/main/elm/Page/ShareDetail/View.elm b/modules/webapp/src/main/elm/Page/ShareDetail/View.elm index 7d06fc65..56ef7cbb 100644 --- a/modules/webapp/src/main/elm/Page/ShareDetail/View.elm +++ b/modules/webapp/src/main/elm/Page/ShareDetail/View.elm @@ -1,30 +1,41 @@ module Page.ShareDetail.View exposing (viewContent, viewSidebar) import Api +import Api.Model.Attachment exposing (Attachment) import Api.Model.VersionInfo exposing (VersionInfo) import Comp.Basic as B import Comp.SharePasswordForm +import Comp.UrlCopy +import Data.Fields import Data.Flags exposing (Flags) import Data.Icons as Icons import Data.ItemTemplate as IT exposing (ItemTemplate) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick) import Messages.Page.ShareDetail exposing (Texts) import Page exposing (Page(..)) import Page.ShareDetail.Data exposing (..) import Styles as S import Util.CustomField import Util.Item +import Util.List +import Util.Size +import Util.String -viewSidebar : Texts -> Bool -> Flags -> UiSettings -> Model -> Html Msg -viewSidebar texts visible flags settings model = +viewSidebar : Texts -> Bool -> Flags -> UiSettings -> String -> String -> Model -> Html Msg +viewSidebar texts visible flags settings shareId itemId model = div [ id "sidebar" - , class "hidden" + , classList [ ( "hidden", not visible ) ] + , class S.sidebar + ] + [ div [ class "pt-2" ] + [ itemData texts flags model shareId itemId + ] ] - [] viewContent : Texts -> Flags -> UiSettings -> VersionInfo -> String -> String -> Model -> Html Msg @@ -36,10 +47,15 @@ viewContent texts flags uiSettings versionInfo shareId itemId model = , class "h-full w-full flex flex-col text-5xl" , class S.content ] - [ B.loadingDimmer - { active = model.pageError == PageErrorNone - , label = "" - } + [ div [ class "text-5xl" ] + [ B.loadingDimmer + { active = model.pageError == PageErrorNone + , label = "" + } + ] + , div [ class "my-4 text-lg" ] + [ errorMessage texts model + ] ] ViewPassword -> @@ -60,18 +76,18 @@ mainContent texts flags settings shareId model = , class S.content ] [ itemHead texts shareId model - , div [ class "flex flex-col sm:flex-row sm:space-x-4 relative h-full" ] - [ itemData texts model - , itemPreview texts flags settings model + , errorMessage texts model + , div [ class "relative h-full" ] + [ itemPreview texts flags settings model ] ] -itemData : Texts -> Model -> Html Msg -itemData texts model = +itemData : Texts -> Flags -> Model -> String -> String -> Html Msg +itemData texts flags model shareId itemId = let boxStyle = - "mb-4 sm:mb-6 max-w-sm" + "mb-4 sm:mb-6" headerStyle = "py-2 bg-blue-50 hover:bg-blue-100 dark:bg-bluegray-700 dark:hover:bg-opacity-100 dark:hover:bg-bluegray-600 text-lg font-medium rounded-lg" @@ -92,11 +108,11 @@ itemData texts model = ] Nothing in - div [ class "flex flex-col pr-2 sm:w-1/3" ] + div [ class "flex flex-col" ] [ div [ class boxStyle ] [ div [ class headerStyle ] [ Icons.dateIcon2 "mr-2 ml-2" - , text "Date" + , text (texts.field Data.Fields.Date) ] , div [ class "text-lg ml-2" ] [ Util.Item.toItemLight model.item @@ -104,10 +120,24 @@ itemData texts model = |> text ] ] + , div + [ class boxStyle + , classList [ ( "hidden", model.item.dueDate == Nothing ) ] + ] + [ div [ class headerStyle ] + [ Icons.dueDateIcon2 "mr-2 ml-2" + , text (texts.field Data.Fields.DueDate) + ] + , div [ class "text-lg ml-2" ] + [ Util.Item.toItemLight model.item + |> IT.render IT.dueDateLong (templateCtx texts) + |> text + ] + ] , div [ class boxStyle ] [ div [ class headerStyle ] [ Icons.tagsIcon2 "mr-2 ml-2" - , text "Tags & Fields" + , text texts.tagsAndFields ] , div [ class "flex flex-row items-center flex-wrap font-medium my-1" ] (List.map showTag model.item.tags ++ List.map showField model.item.customfields) @@ -115,7 +145,7 @@ itemData texts model = , div [ class boxStyle ] [ div [ class headerStyle ] [ Icons.correspondentIcon2 "mr-2 ml-2" - , text "Correspondent" + , text texts.basics.correspondent ] , div [ class "text-lg ml-2" ] [ Util.Item.toItemLight model.item @@ -126,7 +156,7 @@ itemData texts model = , div [ class boxStyle ] [ div [ class headerStyle ] [ Icons.concernedIcon2 "mr-2 ml-2" - , text "Concerning" + , text texts.basics.concerning ] , div [ class "text-lg ml-2" ] [ Util.Item.toItemLight model.item @@ -134,40 +164,110 @@ itemData texts model = |> text ] ] + , div [ class boxStyle ] + [ div [ class headerStyle ] + [ i [ class "fa fa-copy mr-2 ml-2" ] [] + , text "Copy URL" + ] + , div [ class "flex flex-col items-center py-2" ] + [ Html.map UrlCopyMsg + (Comp.UrlCopy.view + (flags.config.baseUrl + ++ Page.pageToString + (ShareDetailPage shareId itemId) + ) + ) + ] + ] ] itemPreview : Texts -> Flags -> UiSettings -> Model -> Html Msg itemPreview texts flags settings model = let - id = - List.head model.item.attachments - |> Maybe.map .id - |> Maybe.withDefault "" + attach = + Util.List.get model.item.attachments model.visibleAttach + |> Maybe.withDefault Api.Model.Attachment.empty + + attachName = + Maybe.withDefault (texts.noName ++ ".pdf") attach.name in div - [ class "flex flex-grow" - , style "min-height" "500px" + [ class "flex flex-grow flex-col h-full border-t dark:border-bluegray-600" ] - [ embed - [ src (Data.UiSettings.pdfUrl settings flags (Api.shareFileURL id)) - , class " h-full w-full mx-0 py-0" + [ div [ class "flex flex-col sm:flex-row items-center py-1 px-1 border-l border-r dark:border-bluegray-600" ] + [ div [ class "text-base font-bold flex-grow w-full text-center sm:text-left break-all" ] + [ text attachName + , text " (" + , text (Util.Size.bytesReadable Util.Size.B (toFloat attach.size)) + , text ")" + ] + , div [ class "flex flex-row space-x-2" ] + [ B.secondaryBasicButton + { label = "" + , icon = "fa fa-eye" + , disabled = False + , handler = href (Api.shareFileURL attach.id) + , attrs = + [ target "_new" + ] + } + , B.secondaryBasicButton + { label = "" + , icon = "fa fa-download" + , disabled = False + , handler = href (Api.shareFileURL attach.id) + , attrs = + [ download attachName + ] + } + , B.secondaryBasicButton + { label = "" + , icon = "fa fa-ellipsis-v" + , disabled = False + , handler = onClick ToggleSelectAttach + , attrs = + [ href "#" + , classList [ ( "hidden", List.length model.item.attachments <= 1 ) ] + ] + } + ] + ] + , attachmentSelect texts model + , div + [ class "flex w-full h-full mb-4 border-b border-l border-r dark:border-bluegray-600" + , style "min-height" "500px" + ] + [ embed + [ src (Data.UiSettings.pdfUrl settings flags (Api.shareFileURL attach.id)) + , class " h-full w-full mx-0 py-0" + ] + [] ] - [] ] itemHead : Texts -> String -> Model -> Html Msg itemHead texts shareId model = - div [ class "flex flex-col sm:flex-row" ] + div [ class "flex flex-col sm:flex-row mt-1" ] [ div [ class "flex flex-grow items-center" ] - [ h1 [ class S.header1 ] + [ h1 + [ class S.header1 + , class "items-center flex flex-row" + ] [ text model.item.name + , span + [ classList [ ( "hidden", model.item.state /= "created" ) ] + , class S.blueBasicLabel + , class "inline ml-4 text-sm" + ] + [ text texts.unconfirmed + ] ] ] , div [ class "flex flex-row items-center justify-end mb-2 sm:mb-0" ] [ B.secondaryBasicButton - { label = "Close" + { label = texts.basics.back , icon = "fa fa-times" , disabled = False , handler = Page.href (SharePage shareId) @@ -189,6 +289,83 @@ passwordContent texts flags versionInfo model = ] +attachmentSelect : Texts -> Model -> Html Msg +attachmentSelect texts model = + div + [ class "flex flex-row border-l border-t border-r px-2 py-2 dark:border-bluegray-600 " + , class "overflow-x-auto overflow-y-none" + , classList + [ ( "hidden", not model.attachMenuOpen ) + ] + ] + (List.indexedMap (menuItem texts model) model.item.attachments) + + +menuItem : Texts -> Model -> Int -> Attachment -> Html Msg +menuItem texts model pos attach = + let + iconClass = + "fa fa-circle ml-1" + + visible = + model.visibleAttach == pos + in + a + [ classList <| + [ ( "border-blue-500 dark:border-lightblue-500", pos == 0 ) + , ( "dark:border-bluegray-600", pos /= 0 ) + ] + , class "flex flex-col relative border rounded px-1 py-1 mr-2" + , class " hover:shadow dark:hover:border-bluegray-500" + , href "#" + , onClick (SelectActiveAttachment pos) + ] + [ div + [ classList + [ ( "hidden", not visible ) + ] + , class "absolute right-1 top-1 text-blue-400 dark:text-lightblue-400 text-xl" + ] + [ i [ class iconClass ] [] + ] + , div [ class "flex-grow" ] + [ img + [ src (Api.shareAttachmentPreviewURL attach.id) + , class "block w-20 mx-auto" + ] + [] + ] + , div [ class "mt-1 text-sm break-all w-28 text-center" ] + [ Maybe.map (Util.String.ellipsis 36) attach.name + |> Maybe.withDefault texts.noName + |> text + ] + ] + + +errorMessage : Texts -> Model -> Html Msg +errorMessage texts model = + case model.pageError of + PageErrorNone -> + span [ class "hidden" ] [] + + PageErrorAuthFail -> + div + [ class S.errorMessage + , class "my-4" + ] + [ text texts.authFailed + ] + + PageErrorHttp err -> + div + [ class S.errorMessage + , class "my-4" + ] + [ text (texts.httpError err) + ] + + templateCtx : Texts -> IT.TemplateContext templateCtx texts = { dateFormatLong = texts.formatDateLong diff --git a/modules/webapp/src/main/elm/Styles.elm b/modules/webapp/src/main/elm/Styles.elm index 2c8167dd..645d2324 100644 --- a/modules/webapp/src/main/elm/Styles.elm +++ b/modules/webapp/src/main/elm/Styles.elm @@ -10,7 +10,7 @@ module Styles exposing (..) sidebar : String sidebar = - " flex flex-col flex-none md:w-80 w-full min-h-max px-2 dark:text-gray-200 shadow overflow-y-auto h-full transition-opacity transition-duration-200 scrollbar-thin scrollbar-light-sidebar dark:scrollbar-dark-sidebar" + " flex flex-col flex-none md:w-80 w-full min-h-max px-2 dark:text-gray-200 overflow-y-auto h-full transition-opacity transition-duration-200 scrollbar-thin scrollbar-light-sidebar dark:scrollbar-dark-sidebar" sidebarBg : String @@ -100,6 +100,11 @@ basicLabel = " label border-gray-600 text-gray-600 dark:border-bluegray-300 dark:text-bluegray-300 " +blueBasicLabel : String +blueBasicLabel = + " label border-blue-500 text-blue-500 dark:border-lightblue-200 dark:text-lightblue-200 " + + --- Primary Button diff --git a/modules/webapp/src/main/webjar/docspell.js b/modules/webapp/src/main/webjar/docspell.js index a5518163..ef4ef03c 100644 --- a/modules/webapp/src/main/webjar/docspell.js +++ b/modules/webapp/src/main/webjar/docspell.js @@ -127,8 +127,10 @@ elmApp.ports.printElement.subscribe(function(id) { w.document.write(''); } w.document.write(''); + w.document.write(''); + w.document.write(''); w.document.write(''); } } From fe77f7245a5f54038bbc2b91ae3b7f715a42b85c Mon Sep 17 00:00:00 2001 From: eikek Date: Thu, 7 Oct 2021 22:11:16 +0200 Subject: [PATCH 24/37] Fix navbar link for anonymous --- modules/webapp/src/main/elm/App/View2.elm | 27 +++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/modules/webapp/src/main/elm/App/View2.elm b/modules/webapp/src/main/elm/App/View2.elm index 6e5652a5..75159ed1 100644 --- a/modules/webapp/src/main/elm/App/View2.elm +++ b/modules/webapp/src/main/elm/App/View2.elm @@ -70,7 +70,7 @@ topNavUser auth model = , baseStyle = "font-bold inline-flex items-center px-4 py-2" , activeStyle = "hover:bg-blue-200 dark:hover:bg-bluegray-800 w-12" } - , headerNavItem model + , headerNavItem True model , div [ class "flex flex-grow justify-end" ] [ userMenu texts.app auth model , dataMenu texts.app auth model @@ -93,7 +93,7 @@ topNavAnon model = , baseStyle = "font-bold inline-flex items-center px-4 py-2" , activeStyle = "hover:bg-blue-200 dark:hover:bg-bluegray-800 w-12" } - , headerNavItem model + , headerNavItem False model , div [ class "flex flex-grow justify-end" ] [ langMenu model , a @@ -107,11 +107,24 @@ topNavAnon model = ] -headerNavItem : Model -> Html Msg -headerNavItem model = - a - [ class "inline-flex font-bold hover:bg-blue-200 dark:hover:bg-bluegray-800 items-center px-4" - , Page.href HomePage +headerNavItem : Bool -> Model -> Html Msg +headerNavItem authenticated model = + let + tag = + if authenticated then + a + + else + div + in + tag + [ class "inline-flex font-bold items-center px-4" + , classList [ ( "hover:bg-blue-200 dark:hover:bg-bluegray-800", authenticated ) ] + , if authenticated then + Page.href HomePage + + else + href "#" ] [ img [ src (model.flags.config.docspellAssetPath ++ "/img/logo-96.png") From 40aa2d41025b5218a6a8e5abd762953ee72802f3 Mon Sep 17 00:00:00 2001 From: eikek Date: Thu, 7 Oct 2021 23:51:09 +0200 Subject: [PATCH 25/37] Use powersearch input element in share form --- modules/webapp/src/main/elm/App/Update.elm | 4 +- .../elm/Comp/ItemDetail/SingleAttachment.elm | 9 +- .../src/main/elm/Comp/PowerSearchInput.elm | 12 +++ .../webapp/src/main/elm/Comp/PublishItems.elm | 12 ++- .../webapp/src/main/elm/Comp/ShareForm.elm | 89 +++++++++++++------ .../webapp/src/main/elm/Comp/ShareManage.elm | 54 ++++++----- .../elm/Page/CollectiveSettings/Update.elm | 22 +++-- 7 files changed, 136 insertions(+), 66 deletions(-) diff --git a/modules/webapp/src/main/elm/App/Update.elm b/modules/webapp/src/main/elm/App/Update.elm index 66002c92..de687444 100644 --- a/modules/webapp/src/main/elm/App/Update.elm +++ b/modules/webapp/src/main/elm/App/Update.elm @@ -478,14 +478,14 @@ updateUserSettings lmsg model = updateCollSettings : Page.CollectiveSettings.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) updateCollSettings lmsg model = let - ( lm, lc ) = + ( lm, lc, ls ) = Page.CollectiveSettings.Update.update model.flags lmsg model.collSettingsModel in ( { model | collSettingsModel = lm } , Cmd.map CollSettingsMsg lc - , Sub.none + , Sub.map CollSettingsMsg ls ) diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm index 52f935f9..56d63635 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/SingleAttachment.elm @@ -153,6 +153,7 @@ attachHeader texts settings model _ attach = [ href "#" , onClick ToggleAttachMenu , class S.secondaryBasicButton + , class "mr-2" , classList [ ( "bg-gray-200 dark:bg-bluegray-600 ", model.attachMenuOpen ) , ( "hidden", not multiAttach ) @@ -160,12 +161,16 @@ attachHeader texts settings model _ attach = , ( "hidden sm:block", multiAttach && not mobile ) ] ] - [ i [ class "fa fa-images font-thin" ] [] + [ if model.attachMenuOpen then + i [ class "fa fa-chevron-up" ] [] + + else + i [ class "fa fa-chevron-down" ] [] ] in div [ class "flex flex-col sm:flex-row items-center w-full" ] [ attachSelectToggle False - , div [ class "ml-2 text-base font-bold flex-grow w-full text-center sm:text-left break-all" ] + , div [ class "text-base font-bold flex-grow w-full text-center sm:text-left break-all" ] [ text attachName , text " (" , text (Util.Size.bytesReadable Util.Size.B (toFloat attach.size)) diff --git a/modules/webapp/src/main/elm/Comp/PowerSearchInput.elm b/modules/webapp/src/main/elm/Comp/PowerSearchInput.elm index e06ef8c9..1036452d 100644 --- a/modules/webapp/src/main/elm/Comp/PowerSearchInput.elm +++ b/modules/webapp/src/main/elm/Comp/PowerSearchInput.elm @@ -11,6 +11,8 @@ module Comp.PowerSearchInput exposing , Msg , ViewSettings , init + , isValid + , setSearchString , update , viewInput , viewResult @@ -43,6 +45,11 @@ init = } +isValid : Model -> Bool +isValid model = + model.input /= Nothing && model.result.success + + type Msg = SetSearch String | KeyUpMsg (Maybe KeyCode) @@ -63,6 +70,11 @@ type alias Result = } +setSearchString : String -> Msg +setSearchString q = + SetSearch q + + --- Update diff --git a/modules/webapp/src/main/elm/Comp/PublishItems.elm b/modules/webapp/src/main/elm/Comp/PublishItems.elm index 2a491f15..a098d29a 100644 --- a/modules/webapp/src/main/elm/Comp/PublishItems.elm +++ b/modules/webapp/src/main/elm/Comp/PublishItems.elm @@ -108,6 +108,7 @@ type Outcome type alias UpdateResult = { model : Model , cmd : Cmd Msg + , sub : Sub Msg , outcome : Outcome } @@ -118,16 +119,18 @@ update flags msg model = CancelPublish -> { model = model , cmd = Cmd.none + , sub = Sub.none , outcome = OutcomeDone } FormMsg lm -> let - ( fm, fc ) = + ( fm, fc, fs ) = Comp.ShareForm.update flags lm model.formModel in { model = { model | formModel = fm } , cmd = Cmd.map FormMsg fc + , sub = Sub.map FormMsg fs , outcome = OutcomeInProgress } @@ -136,12 +139,14 @@ update flags msg model = Just ( _, data ) -> { model = { model | loading = True } , cmd = Api.addShare flags data PublishResp + , sub = Sub.none , outcome = OutcomeInProgress } Nothing -> { model = { model | formError = FormErrorInvalid } , cmd = Cmd.none + , sub = Sub.none , outcome = OutcomeInProgress } @@ -149,18 +154,21 @@ update flags msg model = if res.success then { model = model , cmd = Api.getShare flags res.id GetShareResp + , sub = Sub.none , outcome = OutcomeInProgress } else { model = { model | formError = FormErrorSubmit res.message, loading = False } , cmd = Cmd.none + , sub = Sub.none , outcome = OutcomeInProgress } PublishResp (Err err) -> { model = { model | formError = FormErrorHttp err, loading = False } , cmd = Cmd.none + , sub = Sub.none , outcome = OutcomeInProgress } @@ -172,12 +180,14 @@ update flags msg model = , viewMode = ViewModeInfo share } , cmd = Ports.initClipboard (Comp.ShareView.clipboardData share) + , sub = Sub.none , outcome = OutcomeInProgress } GetShareResp (Err err) -> { model = { model | formError = FormErrorHttp err, loading = False } , cmd = Cmd.none + , sub = Sub.none , outcome = OutcomeInProgress } diff --git a/modules/webapp/src/main/elm/Comp/ShareForm.elm b/modules/webapp/src/main/elm/Comp/ShareForm.elm index d07e74a4..ee08a7d8 100644 --- a/modules/webapp/src/main/elm/Comp/ShareForm.elm +++ b/modules/webapp/src/main/elm/Comp/ShareForm.elm @@ -12,6 +12,7 @@ import Api.Model.ShareDetail exposing (ShareDetail) import Comp.Basic as B import Comp.DatePicker import Comp.PasswordInput +import Comp.PowerSearchInput import Data.Flags exposing (Flags) import DatePicker exposing (DatePicker) import Html exposing (..) @@ -25,7 +26,7 @@ import Util.Maybe type alias Model = { share : ShareDetail , name : Maybe String - , query : String + , queryModel : Comp.PowerSearchInput.Model , enabled : Bool , passwordModel : Comp.PasswordInput.Model , password : Maybe String @@ -41,10 +42,15 @@ initQuery q = let ( dp, dpc ) = Comp.DatePicker.init + + res = + Comp.PowerSearchInput.update + (Comp.PowerSearchInput.setSearchString q) + Comp.PowerSearchInput.init in ( { share = Api.Model.ShareDetail.empty , name = Nothing - , query = q + , queryModel = res.model , enabled = True , passwordModel = Comp.PasswordInput.init , password = Nothing @@ -53,7 +59,10 @@ initQuery q = , untilModel = dp , untilDate = Nothing } - , Cmd.map UntilDateMsg dpc + , Cmd.batch + [ Cmd.map UntilDateMsg dpc + , Cmd.map QueryMsg res.cmd + ] ) @@ -64,17 +73,19 @@ init = isValid : Model -> Bool isValid model = - model.query /= "" && model.untilDate /= Nothing + Comp.PowerSearchInput.isValid model.queryModel + && model.untilDate + /= Nothing type Msg = SetName String - | SetQuery String | SetShare ShareDetail | ToggleEnabled | ToggleClearPassword | PasswordMsg Comp.PasswordInput.Msg | UntilDateMsg Comp.DatePicker.Msg + | QueryMsg Comp.PowerSearchInput.Msg setShare : ShareDetail -> Msg @@ -88,7 +99,9 @@ getShare model = Just ( model.share.id , { name = model.name - , query = model.query + , query = + model.queryModel.input + |> Maybe.withDefault "" , enabled = model.enabled , password = model.password , removePassword = @@ -105,14 +118,20 @@ getShare model = Nothing -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) update _ msg model = case msg of SetShare s -> + let + res = + Comp.PowerSearchInput.update + (Comp.PowerSearchInput.setSearchString s.query) + model.queryModel + in ( { model | share = s , name = s.name - , query = s.query + , queryModel = res.model , enabled = s.enabled , password = Nothing , passwordSet = s.password @@ -124,20 +143,18 @@ update _ msg model = else Nothing } - , Cmd.none + , Cmd.map QueryMsg res.cmd + , Sub.map QueryMsg res.subs ) SetName n -> - ( { model | name = Util.Maybe.fromString n }, Cmd.none ) - - SetQuery n -> - ( { model | query = n }, Cmd.none ) + ( { model | name = Util.Maybe.fromString n }, Cmd.none, Sub.none ) ToggleEnabled -> - ( { model | enabled = not model.enabled }, Cmd.none ) + ( { model | enabled = not model.enabled }, Cmd.none, Sub.none ) ToggleClearPassword -> - ( { model | clearPassword = not model.clearPassword }, Cmd.none ) + ( { model | clearPassword = not model.clearPassword }, Cmd.none, Sub.none ) PasswordMsg lm -> let @@ -149,6 +166,7 @@ update _ msg model = , password = pw } , Cmd.none + , Sub.none ) UntilDateMsg lm -> @@ -166,6 +184,17 @@ update _ msg model = in ( { model | untilModel = dp, untilDate = nextDate } , Cmd.none + , Sub.none + ) + + QueryMsg lm -> + let + res = + Comp.PowerSearchInput.update lm model.queryModel + in + ( { model | queryModel = res.model } + , Cmd.map QueryMsg res.cmd + , Sub.map QueryMsg res.subs ) @@ -175,6 +204,21 @@ update _ msg model = view : Texts -> Model -> Html Msg view texts model = + let + queryInput = + div + [ class "relative flex flex-grow flex-row" ] + [ Html.map QueryMsg + (Comp.PowerSearchInput.viewInput + { placeholder = texts.queryLabel + , extraAttrs = [] + } + model.queryModel + ) + , Html.map QueryMsg + (Comp.PowerSearchInput.viewResult [] model.queryModel) + ] + in div [ class "flex flex-col" ] [ div [ class "mb-4" ] @@ -202,20 +246,7 @@ view texts model = [ text texts.queryLabel , B.inputRequired ] - , input - [ type_ "text" - , onInput SetQuery - , placeholder texts.queryLabel - , value model.query - , id "sharequery" - , class S.textInput - , classList - [ ( S.inputErrorBorder - , model.query == "" - ) - ] - ] - [] + , queryInput ] , div [ class "mb-4" ] [ label diff --git a/modules/webapp/src/main/elm/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Comp/ShareManage.elm index 472680c4..c3f31e64 100644 --- a/modules/webapp/src/main/elm/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Comp/ShareManage.elm @@ -98,7 +98,7 @@ loadShares = --- update -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) update flags msg model = case msg of InitNewShare -> @@ -118,14 +118,18 @@ update flags msg model = else Cmd.none + , Sub.none ) FormMsg lm -> let - ( fm, fc ) = + ( fm, fc, fs ) = Comp.ShareForm.update flags lm model.formModel in - ( { model | formModel = fm }, Cmd.map FormMsg fc ) + ( { model | formModel = fm, formError = FormErrorNone } + , Cmd.map FormMsg fc + , Sub.map FormMsg fs + ) TableMsg lm -> let @@ -137,75 +141,79 @@ update flags msg model = setShare share flags model RequestDelete -> - ( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none ) + ( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none, Sub.none ) CancelDelete -> - ( { model | deleteConfirm = DeleteConfirmOff }, Cmd.none ) + ( { model | deleteConfirm = DeleteConfirmOff }, Cmd.none, Sub.none ) DeleteShareNow id -> ( { model | deleteConfirm = DeleteConfirmOff, loading = True } , Api.deleteShare flags id DeleteShareResp + , Sub.none ) LoadShares -> - ( { model | loading = True }, Api.getShares flags LoadSharesResp ) + ( { model | loading = True }, Api.getShares flags LoadSharesResp, Sub.none ) LoadSharesResp (Ok list) -> - ( { model | loading = False, shares = list.items, formError = FormErrorNone }, Cmd.none ) + ( { model | loading = False, shares = list.items, formError = FormErrorNone } + , Cmd.none + , Sub.none + ) LoadSharesResp (Err err) -> - ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none ) + ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) Submit -> case Comp.ShareForm.getShare model.formModel of Just ( id, data ) -> if id == "" then - ( { model | loading = True }, Api.addShare flags data AddShareResp ) + ( { model | loading = True }, Api.addShare flags data AddShareResp, Sub.none ) else - ( { model | loading = True }, Api.updateShare flags id data UpdateShareResp ) + ( { model | loading = True }, Api.updateShare flags id data UpdateShareResp, Sub.none ) Nothing -> - ( { model | formError = FormErrorInvalid }, Cmd.none ) + ( { model | formError = FormErrorInvalid }, Cmd.none, Sub.none ) AddShareResp (Ok res) -> if res.success then - ( model, Api.getShare flags res.id GetShareResp ) + ( model, Api.getShare flags res.id GetShareResp, Sub.none ) else - ( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none ) + ( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none, Sub.none ) AddShareResp (Err err) -> - ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none ) + ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) UpdateShareResp (Ok res) -> if res.success then - ( model, Api.getShare flags model.formModel.share.id GetShareResp ) + ( model, Api.getShare flags model.formModel.share.id GetShareResp, Sub.none ) else - ( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none ) + ( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none, Sub.none ) UpdateShareResp (Err err) -> - ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none ) + ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) GetShareResp (Ok share) -> setShare share flags model GetShareResp (Err err) -> - ( { model | formError = FormErrorHttp err }, Cmd.none ) + ( { model | formError = FormErrorHttp err }, Cmd.none, Sub.none ) DeleteShareResp (Ok res) -> if res.success then update flags (SetViewMode Table) { model | loading = False } else - ( { model | formError = FormErrorSubmit res.message, loading = False }, Cmd.none ) + ( { model | formError = FormErrorSubmit res.message, loading = False }, Cmd.none, Sub.none ) DeleteShareResp (Err err) -> - ( { model | formError = FormErrorHttp err, loading = False }, Cmd.none ) + ( { model | formError = FormErrorHttp err, loading = False }, Cmd.none, Sub.none ) -setShare : ShareDetail -> Flags -> Model -> ( Model, Cmd Msg ) +setShare : ShareDetail -> Flags -> Model -> ( Model, Cmd Msg, Sub Msg ) setShare share flags model = let nextModel = @@ -214,10 +222,10 @@ setShare share flags model = initClipboard = Ports.initClipboard (Comp.ShareView.clipboardData share) - ( nm, nc ) = + ( nm, nc, ns ) = update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel in - ( nm, Cmd.batch [ initClipboard, nc ] ) + ( nm, Cmd.batch [ initClipboard, nc ], ns ) diff --git a/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm b/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm index 1e711acd..b8a63d74 100644 --- a/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm +++ b/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm @@ -16,7 +16,7 @@ import Data.Flags exposing (Flags) import Page.CollectiveSettings.Data exposing (..) -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) update flags msg model = case msg of SetTab t -> @@ -45,21 +45,21 @@ update flags msg model = ( m2, c2 ) = Comp.SourceManage.update flags m model.sourceModel in - ( { model | sourceModel = m2 }, Cmd.map SourceMsg c2 ) + ( { model | sourceModel = m2 }, Cmd.map SourceMsg c2, Sub.none ) ShareMsg lm -> let - ( sm, sc ) = + ( sm, sc, ss ) = Comp.ShareManage.update flags lm model.shareModel in - ( { model | shareModel = sm }, Cmd.map ShareMsg sc ) + ( { model | shareModel = sm }, Cmd.map ShareMsg sc, Sub.map ShareMsg ss ) UserMsg m -> let ( m2, c2 ) = Comp.UserManage.update flags m model.userModel in - ( { model | userModel = m2 }, Cmd.map UserMsg c2 ) + ( { model | userModel = m2 }, Cmd.map UserMsg c2, Sub.none ) SettingsFormMsg m -> let @@ -76,6 +76,7 @@ update flags msg model = in ( { model | settingsModel = m2, formState = InitialState } , Cmd.batch [ cmd, Cmd.map SettingsFormMsg c2 ] + , Sub.none ) Init -> @@ -84,13 +85,14 @@ update flags msg model = [ Api.getInsights flags GetInsightsResp , Api.getCollectiveSettings flags CollectiveSettingsResp ] + , Sub.none ) GetInsightsResp (Ok data) -> - ( { model | insights = data }, Cmd.none ) + ( { model | insights = data }, Cmd.none, Sub.none ) GetInsightsResp (Err _) -> - ( model, Cmd.none ) + ( model, Cmd.none, Sub.none ) CollectiveSettingsResp (Ok data) -> let @@ -99,10 +101,11 @@ update flags msg model = in ( { model | settingsModel = cm } , Cmd.map SettingsFormMsg cc + , Sub.none ) CollectiveSettingsResp (Err _) -> - ( model, Cmd.none ) + ( model, Cmd.none, Sub.none ) SubmitResp (Ok res) -> ( { model @@ -114,7 +117,8 @@ update flags msg model = SubmitFailed res.message } , Cmd.none + , Sub.none ) SubmitResp (Err err) -> - ( { model | formState = SubmitError err }, Cmd.none ) + ( { model | formState = SubmitError err }, Cmd.none, Sub.none ) From 09242fddb25cf5449ec6ec95b9dc6411297cd7e6 Mon Sep 17 00:00:00 2001 From: eikek Date: Thu, 7 Oct 2021 23:54:05 +0200 Subject: [PATCH 26/37] Fix swapped translation --- .../webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm b/modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm index 8688a4e9..8b0a7f13 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm @@ -19,7 +19,7 @@ gb = , passwordRequired = "Password required" , password = "Password" , passwordSubmitButton = "Submit" - , passwordFailed = "Das Passwort ist falsch" + , passwordFailed = "Password is wrong" } @@ -29,5 +29,5 @@ de = , passwordRequired = "Passwort benötigt" , password = "Passwort" , passwordSubmitButton = "Submit" - , passwordFailed = "Password is wrong" + , passwordFailed = "Das Passwort ist falsch" } From 337293128dc9f18d42d14fc2642444ecae4fc2ea Mon Sep 17 00:00:00 2001 From: eikek Date: Fri, 8 Oct 2021 09:51:35 +0200 Subject: [PATCH 27/37] Add route to send mail for a share --- .../scala/docspell/backend/BackendApp.scala | 4 +- .../scala/docspell/backend/ops/OMail.scala | 16 +++++ .../scala/docspell/backend/ops/OShare.scala | 72 ++++++++++++++++++- .../docspell/backend/ops/SendResult.scala | 26 ------- .../src/main/resources/docspell-openapi.yml | 56 +++++++++++++++ .../restserver/routes/MailSendRoutes.scala | 3 +- .../restserver/routes/ShareRoutes.scala | 31 +++++++- 7 files changed, 175 insertions(+), 33 deletions(-) delete mode 100644 modules/backend/src/main/scala/docspell/backend/ops/SendResult.scala diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala index 9037e138..3881d537 100644 --- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala +++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala @@ -86,7 +86,9 @@ object BackendApp { customFieldsImpl <- OCustomFields(store) simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl) clientSettingsImpl <- OClientSettings(store) - shareImpl <- Resource.pure(OShare(store, itemSearchImpl, simpleSearchImpl)) + shareImpl <- Resource.pure( + OShare(store, itemSearchImpl, simpleSearchImpl, javaEmil) + ) } yield new BackendApp[F] { val login = loginImpl val signup = signupImpl diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala index 8d9debfe..368477d0 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala @@ -51,6 +51,22 @@ trait OMail[F[_]] { } object OMail { + sealed trait SendResult + + object SendResult { + + /** Mail was successfully sent and stored to db. */ + case class Success(id: Ident) extends SendResult + + /** There was a failure sending the mail. The mail is then not saved to db. */ + case class SendFailure(ex: Throwable) extends SendResult + + /** The mail was successfully sent, but storing to db failed. */ + case class StoreFailure(ex: Throwable) extends SendResult + + /** Something could not be found required for sending (mail configs, items etc). */ + case object NotFound extends SendResult + } case class Sent( id: Ident, diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index 4b8ae0f6..e8dae28f 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -13,7 +13,7 @@ import cats.implicits._ import docspell.backend.PasswordCrypt import docspell.backend.auth.ShareToken import docspell.backend.ops.OItemSearch._ -import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} +import docspell.backend.ops.OShare._ import docspell.backend.ops.OSimpleSearch.StringSearchResult import docspell.common._ import docspell.query.ItemQuery @@ -21,8 +21,9 @@ import docspell.query.ItemQuery.Expr import docspell.query.ItemQuery.Expr.AttachId import docspell.store.Store import docspell.store.queries.SearchSummary -import docspell.store.records.RShare +import docspell.store.records.{RShare, RUserEmail} +import emil._ import scodec.bits.ByteVector trait OShare[F[_]] { @@ -63,9 +64,33 @@ trait OShare[F[_]] { def searchSummary( settings: OSimpleSearch.StatsSettings )(shareId: Ident, q: ItemQueryString): OptionT[F, StringSearchResult[SearchSummary]] + + def sendMail(account: AccountId, connection: Ident, mail: ShareMail): F[SendResult] } object OShare { + final case class ShareMail( + shareId: Ident, + subject: String, + recipients: List[MailAddress], + cc: List[MailAddress], + bcc: List[MailAddress], + body: String + ) + + sealed trait SendResult + object SendResult { + + /** Mail was successfully sent and stored to db. */ + case class Success(msgId: String) extends SendResult + + /** There was a failure sending the mail. The mail is then not saved to db. */ + case class SendFailure(ex: Throwable) extends SendResult + + /** Something could not be found required for sending (mail configs, items etc). */ + case object NotFound extends SendResult + } + final case class ShareQuery(id: Ident, cid: Ident, query: ItemQuery) { //TODO @@ -116,7 +141,8 @@ object OShare { def apply[F[_]: Async]( store: Store[F], itemSearch: OItemSearch[F], - simpleSearch: OSimpleSearch[F] + simpleSearch: OSimpleSearch[F], + emil: Emil[F] ): OShare[F] = new OShare[F] { private[this] val logger = Logger.log4s[F](org.log4s.getLogger) @@ -293,5 +319,45 @@ object OShare { case other => other } } + + def sendMail( + account: AccountId, + connection: Ident, + mail: ShareMail + ): F[SendResult] = { + val getSmtpSettings: OptionT[F, RUserEmail] = + OptionT(store.transact(RUserEmail.getByName(account, connection))) + + def createMail(sett: RUserEmail): OptionT[F, Mail[F]] = { + import _root_.emil.builder._ + + OptionT.pure( + MailBuilder.build( + From(sett.mailFrom), + Tos(mail.recipients), + Ccs(mail.cc), + Bccs(mail.bcc), + XMailer.emil, + Subject(mail.subject), + TextBody[F](mail.body) + ) + ) + } + + def sendMail(cfg: MailConfig, mail: Mail[F]): F[Either[SendResult, String]] = + emil(cfg).send(mail).map(_.head).attempt.map(_.left.map(SendResult.SendFailure)) + + (for { + _ <- RShare + .findCurrentActive(mail.shareId) + .filter(_.cid == account.collective) + .mapK(store.transform) + mailCfg <- getSmtpSettings + mail <- createMail(mailCfg) + mid <- OptionT.liftF(sendMail(mailCfg.toMailConfig, mail)) + conv = mid.fold(identity, id => SendResult.Success(id)) + } yield conv).getOrElse(SendResult.NotFound) + } + } } diff --git a/modules/backend/src/main/scala/docspell/backend/ops/SendResult.scala b/modules/backend/src/main/scala/docspell/backend/ops/SendResult.scala deleted file mode 100644 index 97feed6b..00000000 --- a/modules/backend/src/main/scala/docspell/backend/ops/SendResult.scala +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020 Eike K. & Contributors - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package docspell.backend.ops - -import docspell.common._ - -sealed trait SendResult - -object SendResult { - - /** Mail was successfully sent and stored to db. */ - case class Success(id: Ident) extends SendResult - - /** There was a failure sending the mail. The mail is then not saved to db. */ - case class SendFailure(ex: Throwable) extends SendResult - - /** The mail was successfully sent, but storing to db failed. */ - case class StoreFailure(ex: Throwable) extends SendResult - - /** Something could not be found required for sending (mail configs, items etc). */ - case object NotFound extends SendResult -} diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 8745c23c..e5ce7c9e 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1959,6 +1959,32 @@ paths: application/json: schema: $ref: "#/components/schemas/IdResult" + /sec/share/email/send/{name}: + post: + operationId: "sec-share-email-send" + tags: [ Share, E-Mail ] + summary: Send an email. + description: | + Sends an email as specified in the body of the request. + + An existing shareId must be given with the request, no matter + the content of the mail. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/name" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SimpleShareMail" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" /sec/share/{shareId}: parameters: - $ref: "#/components/parameters/shareId" @@ -5283,6 +5309,36 @@ components: items: type: string format: ident + SimpleShareMail: + description: | + A simple e-mail related to a share. + required: + - shareId + - recipients + - cc + - bcc + - subject + - body + properties: + shareId: + type: string + format: ident + recipients: + type: array + items: + type: string + cc: + type: array + items: + type: string + bcc: + type: array + items: + type: string + subject: + type: string + body: + type: string EmailSettingsList: description: | A list of user email settings. diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala index d8591aa0..798dd719 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala @@ -11,8 +11,7 @@ import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken -import docspell.backend.ops.OMail.{AttachSelection, ItemMail} -import docspell.backend.ops.SendResult +import docspell.backend.ops.OMail.{AttachSelection, ItemMail, SendResult} import docspell.common._ import docspell.restapi.model._ diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala index 5e9b13b5..92830d2d 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala @@ -13,7 +13,7 @@ import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken import docspell.backend.ops.OShare -import docspell.backend.ops.OShare.VerifyResult +import docspell.backend.ops.OShare.{SendResult, ShareMail, VerifyResult} import docspell.common.{Ident, Timestamp} import docspell.restapi.model._ import docspell.restserver.Config @@ -21,6 +21,8 @@ import docspell.restserver.auth.ShareCookieData import docspell.restserver.http4s.{ClientRequestInfo, ResponseGenerator} import docspell.store.records.RShare +import emil.MailAddress +import emil.javamail.syntax._ import org.http4s.HttpRoutes import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ @@ -68,6 +70,17 @@ object ShareRoutes { del <- backend.share.delete(id, user.account.collective) resp <- Ok(BasicResult(del, if (del) "Share deleted." else "Deleting failed.")) } yield resp + + case req @ POST -> Root / "email" / "send" / Ident(name) => + for { + in <- req.as[SimpleShareMail] + mail = convertIn(in) + res <- mail.traverse(m => backend.share.sendMail(user.account, name, m)) + resp <- res.fold( + err => Ok(BasicResult(false, s"Invalid mail data: $err")), + res => Ok(convertOut(res)) + ) + } yield resp } } @@ -134,4 +147,20 @@ object ShareRoutes { r.lastAccess ) + def convertIn(s: SimpleShareMail): Either[String, ShareMail] = + for { + rec <- s.recipients.traverse(MailAddress.parse) + cc <- s.cc.traverse(MailAddress.parse) + bcc <- s.bcc.traverse(MailAddress.parse) + } yield ShareMail(s.shareId, s.subject, rec, cc, bcc, s.body) + + def convertOut(res: SendResult): BasicResult = + res match { + case SendResult.Success(_) => + BasicResult(true, "Mail sent.") + case SendResult.SendFailure(ex) => + BasicResult(false, s"Mail sending failed: ${ex.getMessage}") + case SendResult.NotFound => + BasicResult(false, s"There was no mail-connection or item found.") + } } From 16ccddab9f2092f891f5fae4dd91c6523bb4cd45 Mon Sep 17 00:00:00 2001 From: eikek Date: Fri, 8 Oct 2021 10:15:19 +0200 Subject: [PATCH 28/37] Add mail form when creating shares --- modules/webapp/src/main/elm/Api.elm | 16 ++ modules/webapp/src/main/elm/Comp/ItemMail.elm | 71 ++++++- .../webapp/src/main/elm/Comp/PublishItems.elm | 71 +++++-- .../webapp/src/main/elm/Comp/ShareMail.elm | 183 ++++++++++++++++++ .../webapp/src/main/elm/Comp/ShareManage.elm | 74 +++++-- .../src/main/elm/Comp/SharePasswordForm.elm | 7 + modules/webapp/src/main/elm/Comp/UrlCopy.elm | 7 + modules/webapp/src/main/elm/Data/Pdf.elm | 7 + .../src/main/elm/Messages/Comp/ItemMail.elm | 4 +- .../main/elm/Messages/Comp/PublishItems.elm | 7 + .../src/main/elm/Messages/Comp/ShareMail.elm | 60 ++++++ .../main/elm/Messages/Comp/ShareManage.elm | 7 + .../elm/Messages/Comp/SharePasswordForm.elm | 7 + .../main/elm/Messages/Page/ShareDetail.elm | 7 + .../main/elm/Page/CollectiveSettings/Data.elm | 2 +- .../elm/Page/CollectiveSettings/View2.elm | 8 +- .../webapp/src/main/elm/Page/Home/Data.elm | 6 +- .../webapp/src/main/elm/Page/Home/Update.elm | 10 +- .../webapp/src/main/elm/Page/Home/View2.elm | 16 +- .../src/main/elm/Page/ShareDetail/Data.elm | 7 + .../src/main/elm/Page/ShareDetail/Update.elm | 7 + .../src/main/elm/Page/ShareDetail/View.elm | 7 + 22 files changed, 535 insertions(+), 56 deletions(-) create mode 100644 modules/webapp/src/main/elm/Comp/ShareMail.elm create mode 100644 modules/webapp/src/main/elm/Messages/Comp/ShareMail.elm diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 42b16e20..2051fe23 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -146,6 +146,7 @@ module Api exposing , shareAttachmentPreviewURL , shareFileURL , shareItemBasePreviewURL + , shareSendMail , startClassifier , startEmptyTrash , startOnceNotifyDueItems @@ -233,6 +234,7 @@ import Api.Model.ShareList exposing (ShareList) import Api.Model.ShareSecret exposing (ShareSecret) import Api.Model.ShareVerifyResult exposing (ShareVerifyResult) import Api.Model.SimpleMail exposing (SimpleMail) +import Api.Model.SimpleShareMail exposing (SimpleShareMail) import Api.Model.SourceAndTags exposing (SourceAndTags) import Api.Model.SourceList exposing (SourceList) import Api.Model.SourceTagIn @@ -2312,6 +2314,20 @@ itemDetailShare flags token itemId receive = } +shareSendMail : + Flags + -> { conn : String, mail : SimpleShareMail } + -> (Result Http.Error BasicResult -> msg) + -> Cmd msg +shareSendMail flags opts receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/share/email/send/" ++ opts.conn + , account = getAccount flags + , body = Http.jsonBody (Api.Model.SimpleShareMail.encode opts.mail) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + shareAttachmentPreviewURL : String -> String shareAttachmentPreviewURL id = "/api/v1/share/attachment/" ++ id ++ "/preview?withFallback=true" diff --git a/modules/webapp/src/main/elm/Comp/ItemMail.elm b/modules/webapp/src/main/elm/Comp/ItemMail.elm index e135c45f..18b2fc57 100644 --- a/modules/webapp/src/main/elm/Comp/ItemMail.elm +++ b/modules/webapp/src/main/elm/Comp/ItemMail.elm @@ -12,7 +12,9 @@ module Comp.ItemMail exposing , clear , emptyModel , init + , setMailInfo , update + , view , view2 ) @@ -28,7 +30,7 @@ import Data.Flags exposing (Flags) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) -import Html.Events exposing (onClick, onInput) +import Html.Events exposing (onClick, onFocus, onInput) import Http import Messages.Comp.ItemMail exposing (Texts) import Styles as S @@ -61,6 +63,7 @@ type Msg | CCRecipientMsg Comp.EmailInput.Msg | BCCRecipientMsg Comp.EmailInput.Msg | SetBody String + | SetSubjectBody String String | ConnMsg (Comp.Dropdown.Msg String) | ConnResp (Result Http.Error EmailSettingsList) | ToggleAttachAll @@ -112,12 +115,20 @@ clear model = } +setMailInfo : String -> String -> Msg +setMailInfo subject body = + SetSubjectBody subject body + + update : Flags -> Msg -> Model -> ( Model, Cmd Msg, FormAction ) update flags msg model = case msg of SetSubject str -> ( { model | subject = str }, Cmd.none, FormNone ) + SetSubjectBody subj body -> + ( { model | subject = subj, body = body }, Cmd.none, FormNone ) + RecipientMsg m -> let ( em, ec, rec ) = @@ -239,8 +250,31 @@ isValid model = --- View2 +type alias ViewConfig = + { withAttachments : Bool + , subjectTemplate : Maybe String + , bodyTemplate : Maybe String + , textAreaClass : String + , showCancel : Bool + } + + view2 : Texts -> UiSettings -> Model -> Html Msg view2 texts settings model = + let + cfg = + { withAttachments = True + , subjectTemplate = Nothing + , bodyTemplate = Nothing + , textAreaClass = "" + , showCancel = True + } + in + view texts settings cfg model + + +view : Texts -> UiSettings -> ViewConfig -> Model -> Html Msg +view texts settings cfg model = let dds = Data.DropdownStyle.mainStyle @@ -323,6 +357,11 @@ view2 texts settings model = [ type_ "text" , class S.textInput , onInput SetSubject + , if model.subject == "" then + onFocus (SetSubject <| Maybe.withDefault "" cfg.subjectTemplate) + + else + class "" , value model.subject ] [] @@ -334,18 +373,27 @@ view2 texts settings model = ] , textarea [ onInput SetBody - , value model.body + , if model.body == "" then + value <| Maybe.withDefault "" cfg.bodyTemplate + + else + value model.body , class S.textAreaInput + , class cfg.textAreaClass ] [] ] - , MB.viewItem <| - MB.Checkbox - { tagger = \_ -> ToggleAttachAll - , label = texts.includeAllAttachments - , value = model.attachAll - , id = "item-send-mail-attach-all" - } + , if cfg.withAttachments then + MB.viewItem <| + MB.Checkbox + { tagger = \_ -> ToggleAttachAll + , label = texts.includeAllAttachments + , value = model.attachAll + , id = "item-send-mail-attach-all" + } + + else + span [ class "hidden" ] [] , div [ class "flex flex-row space-x-2" ] [ B.primaryButton { label = texts.sendLabel @@ -358,7 +406,10 @@ view2 texts settings model = { label = texts.basics.cancel , icon = "fa fa-times" , handler = onClick Cancel - , attrs = [ href "#" ] + , attrs = + [ href "#" + , classList [ ( "hidden", not cfg.showCancel ) ] + ] , disabled = False } ] diff --git a/modules/webapp/src/main/elm/Comp/PublishItems.elm b/modules/webapp/src/main/elm/Comp/PublishItems.elm index a098d29a..6e0aede9 100644 --- a/modules/webapp/src/main/elm/Comp/PublishItems.elm +++ b/modules/webapp/src/main/elm/Comp/PublishItems.elm @@ -21,11 +21,13 @@ import Api.Model.ShareDetail exposing (ShareDetail) import Comp.Basic as B import Comp.MenuBar as MB import Comp.ShareForm +import Comp.ShareMail import Comp.ShareView import Data.Flags exposing (Flags) import Data.Icons as Icons import Data.ItemQuery exposing (ItemQuery) import Data.SearchMode exposing (SearchMode) +import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) import Http @@ -52,39 +54,54 @@ type FormError type alias Model = { formModel : Comp.ShareForm.Model + , mailModel : Comp.ShareMail.Model , viewMode : ViewMode , formError : FormError , loading : Bool } -init : ( Model, Cmd Msg ) -init = +init : Flags -> ( Model, Cmd Msg ) +init flags = let ( fm, fc ) = Comp.ShareForm.init + + ( mm, mc ) = + Comp.ShareMail.init flags in ( { formModel = fm + , mailModel = mm , viewMode = ViewModeEdit , formError = FormErrorNone , loading = False } - , Cmd.map FormMsg fc + , Cmd.batch + [ Cmd.map FormMsg fc + , Cmd.map MailMsg mc + ] ) -initQuery : ItemQuery -> ( Model, Cmd Msg ) -initQuery query = +initQuery : Flags -> ItemQuery -> ( Model, Cmd Msg ) +initQuery flags query = let ( fm, fc ) = Comp.ShareForm.initQuery (Data.ItemQuery.render query) + + ( mm, mc ) = + Comp.ShareMail.init flags in ( { formModel = fm + , mailModel = mm , viewMode = ViewModeEdit , formError = FormErrorNone , loading = False } - , Cmd.map FormMsg fc + , Cmd.batch + [ Cmd.map FormMsg fc + , Cmd.map MailMsg mc + ] ) @@ -94,6 +111,7 @@ initQuery query = type Msg = FormMsg Comp.ShareForm.Msg + | MailMsg Comp.ShareMail.Msg | CancelPublish | SubmitPublish | PublishResp (Result Http.Error IdResult) @@ -134,6 +152,17 @@ update flags msg model = , outcome = OutcomeInProgress } + MailMsg lm -> + let + ( mm, mc ) = + Comp.ShareMail.update flags lm model.mailModel + in + { model = { model | mailModel = mm } + , cmd = Cmd.map MailMsg mc + , sub = Sub.none + , outcome = OutcomeInProgress + } + SubmitPublish -> case Comp.ShareForm.getShare model.formModel of Just ( _, data ) -> @@ -173,13 +202,22 @@ update flags msg model = } GetShareResp (Ok share) -> + let + ( mm, mc ) = + Comp.ShareMail.update flags (Comp.ShareMail.setMailInfo share) model.mailModel + in { model = { model | formError = FormErrorNone , loading = False , viewMode = ViewModeInfo share + , mailModel = mm } - , cmd = Ports.initClipboard (Comp.ShareView.clipboardData share) + , cmd = + Cmd.batch + [ Ports.initClipboard (Comp.ShareView.clipboardData share) + , Cmd.map MailMsg mc + ] , sub = Sub.none , outcome = OutcomeInProgress } @@ -196,8 +234,8 @@ update flags msg model = --- View -view : Texts -> Flags -> Model -> Html Msg -view texts flags model = +view : Texts -> UiSettings -> Flags -> Model -> Html Msg +view texts settings flags model = div [] [ B.loadingDimmer { active = model.loading @@ -208,12 +246,12 @@ view texts flags model = viewForm texts model ViewModeInfo share -> - viewInfo texts flags model share + viewInfo texts settings flags model share ] -viewInfo : Texts -> Flags -> Model -> ShareDetail -> Html Msg -viewInfo texts flags model share = +viewInfo : Texts -> UiSettings -> Flags -> Model -> ShareDetail -> Html Msg +viewInfo texts settings flags model share = let cfg = { mainClasses = "" @@ -244,6 +282,15 @@ viewInfo texts flags model share = , div [] [ Comp.ShareView.view cfg texts.shareView flags share ] + , div [ class "flex flex-col mt-6" ] + [ div + [ class S.header2 + ] + [ text texts.sendViaMail + ] + , Html.map MailMsg + (Comp.ShareMail.view texts.shareMail flags settings model.mailModel) + ] ] diff --git a/modules/webapp/src/main/elm/Comp/ShareMail.elm b/modules/webapp/src/main/elm/Comp/ShareMail.elm new file mode 100644 index 00000000..201a7cd0 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ShareMail.elm @@ -0,0 +1,183 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.ShareMail exposing (Model, Msg, init, setMailInfo, update, view) + +import Api +import Api.Model.BasicResult exposing (BasicResult) +import Api.Model.ShareDetail exposing (ShareDetail) +import Comp.Basic as B +import Comp.ItemMail +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Http +import Messages.Comp.ShareMail exposing (Texts) +import Page exposing (Page(..)) +import Styles as S + + +type FormState + = FormStateNone + | FormStateSubmit String + | FormStateHttp Http.Error + | FormStateSent + + +type alias Model = + { mailModel : Comp.ItemMail.Model + , share : ShareDetail + , sending : Bool + , formState : FormState + } + + +init : Flags -> ( Model, Cmd Msg ) +init flags = + let + ( mm, mc ) = + Comp.ItemMail.init flags + in + ( { mailModel = mm + , share = Api.Model.ShareDetail.empty + , sending = False + , formState = FormStateNone + } + , Cmd.map MailMsg mc + ) + + +type Msg + = MailMsg Comp.ItemMail.Msg + | SetMailInfo ShareDetail + | SendMailResp (Result Http.Error BasicResult) + + + +--- Update + + +setMailInfo : ShareDetail -> Msg +setMailInfo share = + SetMailInfo share + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update flags msg model = + case msg of + MailMsg lm -> + let + ( mm, mc, fa ) = + Comp.ItemMail.update flags lm model.mailModel + + defaultResult = + ( { model | mailModel = mm }, Cmd.map MailMsg mc ) + in + case fa of + Comp.ItemMail.FormSend sm -> + let + mail = + { mail = + { shareId = model.share.id + , recipients = sm.mail.recipients + , cc = sm.mail.cc + , bcc = sm.mail.bcc + , subject = sm.mail.subject + , body = sm.mail.body + } + , conn = sm.conn + } + in + ( { model | sending = True, mailModel = mm } + , Cmd.batch + [ Cmd.map MailMsg mc + , Api.shareSendMail flags mail SendMailResp + ] + ) + + Comp.ItemMail.FormNone -> + defaultResult + + Comp.ItemMail.FormCancel -> + defaultResult + + SetMailInfo share -> + ( { model | share = share }, Cmd.none ) + + SendMailResp (Ok res) -> + if res.success then + ( { model + | formState = FormStateSent + , mailModel = Comp.ItemMail.clear model.mailModel + , sending = False + } + , Cmd.none + ) + + else + ( { model + | formState = FormStateSubmit res.message + , sending = False + } + , Cmd.none + ) + + SendMailResp (Err err) -> + ( { model | formState = FormStateHttp err }, Cmd.none ) + + + +--- View + + +view : Texts -> Flags -> UiSettings -> Model -> Html Msg +view texts flags settings model = + let + url = + flags.config.baseUrl ++ (Page.pageToString <| SharePage model.share.id) + + subject = + texts.subjectTemplate model.share.name + + body = + texts.bodyTemplate url + + cfg = + { withAttachments = False + , subjectTemplate = Just subject + , bodyTemplate = Just body + , textAreaClass = "h-52" + , showCancel = False + } + in + div [ class "relative" ] + [ case model.formState of + FormStateNone -> + span [ class "hidden" ] [] + + FormStateSubmit msg -> + div [ class S.errorMessage ] + [ text msg + ] + + FormStateHttp err -> + div [ class S.errorMessage ] + [ text (texts.httpError err) + ] + + FormStateSent -> + div [ class S.successMessage ] + [ text "Mail sent." + ] + , Html.map MailMsg + (Comp.ItemMail.view texts.itemMail settings cfg model.mailModel) + , B.loadingDimmer + { active = model.sending + , label = "" + } + ] diff --git a/modules/webapp/src/main/elm/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Comp/ShareManage.elm index c3f31e64..f5d03315 100644 --- a/modules/webapp/src/main/elm/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Comp/ShareManage.elm @@ -16,14 +16,17 @@ import Comp.Basic as B import Comp.ItemDetail.Model exposing (Msg(..)) import Comp.MenuBar as MB import Comp.ShareForm +import Comp.ShareMail import Comp.ShareTable import Comp.ShareView import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) import Http import Messages.Comp.ShareManage exposing (Texts) +import Page exposing (Page(..)) import Ports import Styles as S @@ -49,26 +52,34 @@ type alias Model = { viewMode : ViewMode , shares : List ShareDetail , formModel : Comp.ShareForm.Model + , mailModel : Comp.ShareMail.Model , loading : Bool , formError : FormError , deleteConfirm : DeleteConfirm } -init : ( Model, Cmd Msg ) -init = +init : Flags -> ( Model, Cmd Msg ) +init flags = let ( fm, fc ) = Comp.ShareForm.init + + ( mm, mc ) = + Comp.ShareMail.init flags in ( { viewMode = Table , shares = [] , formModel = fm + , mailModel = mm , loading = False , formError = FormErrorNone , deleteConfirm = DeleteConfirmOff } - , Cmd.map FormMsg fc + , Cmd.batch + [ Cmd.map FormMsg fc + , Cmd.map MailMsg mc + ] ) @@ -76,6 +87,7 @@ type Msg = LoadShares | TableMsg Comp.ShareTable.Msg | FormMsg Comp.ShareForm.Msg + | MailMsg Comp.ShareMail.Msg | InitNewShare | SetViewMode ViewMode | Submit @@ -212,10 +224,20 @@ update flags msg model = DeleteShareResp (Err err) -> ( { model | formError = FormErrorHttp err, loading = False }, Cmd.none, Sub.none ) + MailMsg lm -> + let + ( mm, mc ) = + Comp.ShareMail.update flags lm model.mailModel + in + ( { model | mailModel = mm }, Cmd.map MailMsg mc, Sub.none ) + setShare : ShareDetail -> Flags -> Model -> ( Model, Cmd Msg, Sub Msg ) setShare share flags model = let + shareUrl = + flags.config.baseUrl ++ Page.pageToString (SharePage share.id) + nextModel = { model | formError = FormErrorNone, viewMode = Form, loading = False } @@ -224,21 +246,24 @@ setShare share flags model = ( nm, nc, ns ) = update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel + + ( nm2, nc2, ns2 ) = + update flags (MailMsg <| Comp.ShareMail.setMailInfo share) nm in - ( nm, Cmd.batch [ initClipboard, nc ], ns ) + ( nm2, Cmd.batch [ initClipboard, nc, nc2 ], Sub.batch [ ns, ns2 ] ) --- view -view : Texts -> Flags -> Model -> Html Msg -view texts flags model = +view : Texts -> UiSettings -> Flags -> Model -> Html Msg +view texts settings flags model = if model.viewMode == Table then viewTable texts model else - viewForm texts flags model + viewForm texts settings flags model viewTable : Texts -> Model -> Html Msg @@ -265,13 +290,13 @@ viewTable texts model = ] -viewForm : Texts -> Flags -> Model -> Html Msg -viewForm texts flags model = +viewForm : Texts -> UiSettings -> Flags -> Model -> Html Msg +viewForm texts settings flags model = let newShare = model.formModel.share.id == "" in - div [ class "relative" ] + div [] [ Html.form [] [ if newShare then h1 [ class S.header2 ] @@ -367,6 +392,7 @@ viewForm texts flags model = ) ] , shareInfo texts flags model.formModel.share + , shareSendMail texts flags settings model ] @@ -376,8 +402,34 @@ shareInfo texts flags share = [ class "mt-6" , classList [ ( "hidden", share.id == "" ) ] ] - [ h2 [ class S.header2 ] + [ h2 + [ class S.header2 + , class "border-b-2 dark:border-bluegray-600" + ] [ text texts.shareInformation ] , Comp.ShareView.viewDefault texts.shareView flags share ] + + +shareSendMail : Texts -> Flags -> UiSettings -> Model -> Html Msg +shareSendMail texts flags settings model = + let + share = + model.formModel.share + in + div + [ class "mt-8 mb-2" + , classList [ ( "hidden", share.id == "" || not share.enabled || share.expired ) ] + ] + [ h2 + [ class S.header2 + , class "border-b-2 dark:border-bluegray-600" + ] + [ text "Send via E-Mail" + ] + , div [ class "px-2 py-2 dark:border-bluegray-600" ] + [ Html.map MailMsg + (Comp.ShareMail.view texts.shareMail flags settings model.mailModel) + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/SharePasswordForm.elm b/modules/webapp/src/main/elm/Comp/SharePasswordForm.elm index 2c17a16a..d280c732 100644 --- a/modules/webapp/src/main/elm/Comp/SharePasswordForm.elm +++ b/modules/webapp/src/main/elm/Comp/SharePasswordForm.elm @@ -1,3 +1,10 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + module Comp.SharePasswordForm exposing (Model, Msg, init, update, view) import Api diff --git a/modules/webapp/src/main/elm/Comp/UrlCopy.elm b/modules/webapp/src/main/elm/Comp/UrlCopy.elm index 2a6f048b..ac42d239 100644 --- a/modules/webapp/src/main/elm/Comp/UrlCopy.elm +++ b/modules/webapp/src/main/elm/Comp/UrlCopy.elm @@ -1,3 +1,10 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + module Comp.UrlCopy exposing (..) import Comp.Basic as B diff --git a/modules/webapp/src/main/elm/Data/Pdf.elm b/modules/webapp/src/main/elm/Data/Pdf.elm index e4c691b8..9943dbe1 100644 --- a/modules/webapp/src/main/elm/Data/Pdf.elm +++ b/modules/webapp/src/main/elm/Data/Pdf.elm @@ -1,3 +1,10 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + module Data.Pdf exposing (PdfMode(..), allModes, asString, detectUrl, fromString, serverUrl) {-| Makes use of the fact, that docspell uses a `/view` suffix on the diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemMail.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemMail.elm index 78a538d2..c3ad7854 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ItemMail.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ItemMail.elm @@ -39,8 +39,8 @@ gb = , selectConnection = "Select connection..." , sendVia = "Send via" , recipients = "Recipient(s)" - , ccRecipients = "CC recipient(s)" - , bccRecipients = "BCC recipient(s)..." + , ccRecipients = "CC" + , bccRecipients = "BCC" , subject = "Subject" , body = "Body" , includeAllAttachments = "Include all item attachments" diff --git a/modules/webapp/src/main/elm/Messages/Comp/PublishItems.elm b/modules/webapp/src/main/elm/Messages/Comp/PublishItems.elm index 269f68ef..b23703a8 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/PublishItems.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/PublishItems.elm @@ -15,6 +15,7 @@ import Http import Messages.Basics import Messages.Comp.HttpError import Messages.Comp.ShareForm +import Messages.Comp.ShareMail import Messages.Comp.ShareView import Messages.DateFormat import Messages.UiLanguage @@ -25,6 +26,7 @@ type alias Texts = , httpError : Http.Error -> String , shareForm : Messages.Comp.ShareForm.Texts , shareView : Messages.Comp.ShareView.Texts + , shareMail : Messages.Comp.ShareMail.Texts , title : String , infoText : String , formatDateLong : Int -> String @@ -37,6 +39,7 @@ type alias Texts = , publishInProcess : String , correctFormErrors : String , doneLabel : String + , sendViaMail : String } @@ -46,6 +49,7 @@ gb = , httpError = Messages.Comp.HttpError.gb , shareForm = Messages.Comp.ShareForm.gb , shareView = Messages.Comp.ShareView.gb + , shareMail = Messages.Comp.ShareMail.gb , title = "Publish Items" , infoText = "Publishing items creates a cryptic link, which can be used by everyone to see the selected documents. This link cannot be guessed, but is public! It exists for a certain amount of time and can be further protected using a password." , formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.English @@ -58,6 +62,7 @@ gb = , publishInProcess = "Items are published …" , correctFormErrors = "Please correct the errors in the form." , doneLabel = "Done" + , sendViaMail = "Send via E-Mail" } @@ -67,6 +72,7 @@ de = , httpError = Messages.Comp.HttpError.de , shareForm = Messages.Comp.ShareForm.de , shareView = Messages.Comp.ShareView.de + , shareMail = Messages.Comp.ShareMail.de , title = "Dokumente publizieren" , infoText = "Beim Publizieren der Dokumente wird ein kryptischer Link erzeugt, mit welchem jeder die dahinter publizierten Dokumente einsehen kann. Dieser Link kann nicht erraten werden, ist aber öffentlich. Er ist zeitlich begrenzt und kann zusätzlich mit einem Passwort geschützt werden." , formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.German @@ -79,4 +85,5 @@ de = , publishInProcess = "Dokumente werden publiziert…" , correctFormErrors = "Bitte korrigiere die Fehler im Formular." , doneLabel = "Fertig" + , sendViaMail = "Per E-Mail versenden" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareMail.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareMail.elm new file mode 100644 index 00000000..8b56bbb7 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareMail.elm @@ -0,0 +1,60 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.ShareMail exposing + ( Texts + , de + , gb + ) + +import Http +import Messages.Basics +import Messages.Comp.HttpError +import Messages.Comp.ItemMail + + +type alias Texts = + { basics : Messages.Basics.Texts + , itemMail : Messages.Comp.ItemMail.Texts + , httpError : Http.Error -> String + , subjectTemplate : Maybe String -> String + , bodyTemplate : String -> String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , httpError = Messages.Comp.HttpError.gb + , itemMail = Messages.Comp.ItemMail.gb + , subjectTemplate = \mt -> "Shared Documents" ++ (Maybe.map (\n -> ": " ++ n) mt |> Maybe.withDefault "") + , bodyTemplate = \url -> """Hi, + +you can find the documents here: + + """ ++ url ++ """ + +Kind regards +""" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , httpError = Messages.Comp.HttpError.de + , itemMail = Messages.Comp.ItemMail.de + , subjectTemplate = \mt -> "Freigegebene Dokumente" ++ (Maybe.map (\n -> ": " ++ n) mt |> Maybe.withDefault "") + , bodyTemplate = \url -> """Hallo, + +die freigegebenen Dokumente befinden sich hier: + + """ ++ url ++ """ + +Freundliche Grüße +""" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm index c415d3c8..d90824be 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm @@ -15,6 +15,7 @@ import Http import Messages.Basics import Messages.Comp.HttpError import Messages.Comp.ShareForm +import Messages.Comp.ShareMail import Messages.Comp.ShareTable import Messages.Comp.ShareView @@ -24,6 +25,7 @@ type alias Texts = , shareTable : Messages.Comp.ShareTable.Texts , shareForm : Messages.Comp.ShareForm.Texts , shareView : Messages.Comp.ShareView.Texts + , shareMail : Messages.Comp.ShareMail.Texts , httpError : Http.Error -> String , newShare : String , copyToClipboard : String @@ -36,6 +38,7 @@ type alias Texts = , correctFormErrors : String , noName : String , shareInformation : String + , sendMail : String } @@ -46,6 +49,7 @@ gb = , shareTable = Messages.Comp.ShareTable.gb , shareForm = Messages.Comp.ShareForm.gb , shareView = Messages.Comp.ShareView.gb + , shareMail = Messages.Comp.ShareMail.gb , newShare = "New share" , copyToClipboard = "Copy to clipboard" , openInNewTab = "Open in new tab/window" @@ -57,6 +61,7 @@ gb = , correctFormErrors = "Please correct the errors in the form." , noName = "No Name" , shareInformation = "Share Information" + , sendMail = "Send via E-Mail" } @@ -67,6 +72,7 @@ de = , shareForm = Messages.Comp.ShareForm.de , shareView = Messages.Comp.ShareView.de , httpError = Messages.Comp.HttpError.de + , shareMail = Messages.Comp.ShareMail.de , newShare = "Neue Freigabe" , copyToClipboard = "In die Zwischenablage kopieren" , openInNewTab = "Im neuen Tab/Fenster öffnen" @@ -78,4 +84,5 @@ de = , correctFormErrors = "Bitte korrigiere die Fehler im Formular." , noName = "Ohne Name" , shareInformation = "Informationen zur Freigabe" + , sendMail = "Per E-Mail versenden" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm b/modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm index 8b0a7f13..889495c8 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/SharePasswordForm.elm @@ -1,3 +1,10 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + module Messages.Comp.SharePasswordForm exposing (Texts, de, gb) import Http diff --git a/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm b/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm index 70397991..33408d3f 100644 --- a/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm +++ b/modules/webapp/src/main/elm/Messages/Page/ShareDetail.elm @@ -1,3 +1,10 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + module Messages.Page.ShareDetail exposing (..) import Data.Fields exposing (Field) diff --git a/modules/webapp/src/main/elm/Page/CollectiveSettings/Data.elm b/modules/webapp/src/main/elm/Page/CollectiveSettings/Data.elm index e940ade4..286913b1 100644 --- a/modules/webapp/src/main/elm/Page/CollectiveSettings/Data.elm +++ b/modules/webapp/src/main/elm/Page/CollectiveSettings/Data.elm @@ -52,7 +52,7 @@ init flags = Comp.CollectiveSettingsForm.init flags Api.Model.CollectiveSettings.empty ( shm, shc ) = - Comp.ShareManage.init + Comp.ShareManage.init flags in ( { currentTab = Just InsightsTab , sourceModel = sm diff --git a/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm b/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm index 7c31f7ff..7a09d909 100644 --- a/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm +++ b/modules/webapp/src/main/elm/Page/CollectiveSettings/View2.elm @@ -118,7 +118,7 @@ viewContent texts flags settings model = viewSources texts flags settings model Just ShareTab -> - viewShares texts flags model + viewShares texts settings flags model Nothing -> [] @@ -245,8 +245,8 @@ viewSources texts flags settings model = ] -viewShares : Texts -> Flags -> Model -> List (Html Msg) -viewShares texts flags model = +viewShares : Texts -> UiSettings -> Flags -> Model -> List (Html Msg) +viewShares texts settings flags model = [ h1 [ class S.header1 , class "inline-flex items-center" @@ -256,7 +256,7 @@ viewShares texts flags model = [ text texts.shares ] ] - , Html.map ShareMsg (Comp.ShareManage.view texts.shareManage flags model.shareModel) + , Html.map ShareMsg (Comp.ShareManage.view texts.shareManage settings flags model.shareModel) ] diff --git a/modules/webapp/src/main/elm/Page/Home/Data.elm b/modules/webapp/src/main/elm/Page/Home/Data.elm index c4b65df0..0a2deed7 100644 --- a/modules/webapp/src/main/elm/Page/Home/Data.elm +++ b/modules/webapp/src/main/elm/Page/Home/Data.elm @@ -87,14 +87,14 @@ type alias SelectViewModel = } -initSelectViewModel : SelectViewModel -initSelectViewModel = +initSelectViewModel : Flags -> SelectViewModel +initSelectViewModel flags = { ids = Set.empty , action = NoneAction , confirmModal = Nothing , editModel = Comp.ItemDetail.MultiEditMenu.init , mergeModel = Comp.ItemMerge.init [] - , publishModel = Tuple.first Comp.PublishItems.init + , publishModel = Tuple.first (Comp.PublishItems.init flags) , saveNameState = SaveSuccess , saveCustomFieldState = Set.empty } diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index edebe2f0..4848b838 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -252,10 +252,10 @@ update mId key flags settings msg model = ( nextView, cmd ) = case model.viewMode of SimpleView -> - ( SelectView initSelectViewModel, loadEditModel flags ) + ( SelectView <| initSelectViewModel flags, loadEditModel flags ) SearchView -> - ( SelectView initSelectViewModel, loadEditModel flags ) + ( SelectView <| initSelectViewModel flags, loadEditModel flags ) SelectView _ -> ( SearchView, Cmd.none ) @@ -633,7 +633,7 @@ update mId key flags settings msg model = if svm.action == PublishSelected then let ( mm, mc ) = - Comp.PublishItems.init + Comp.PublishItems.init flags in noSub ( { model @@ -653,7 +653,7 @@ update mId key flags settings msg model = else let ( mm, mc ) = - Comp.PublishItems.initQuery + Comp.PublishItems.initQuery flags (Q.ItemIdIn (Set.toList svm.ids)) in noSub @@ -877,7 +877,7 @@ update mId key flags settings msg model = Just q -> let ( pm, pc ) = - Comp.PublishItems.initQuery q + Comp.PublishItems.initQuery flags q in noSub ( { model | viewMode = PublishView pm }, Cmd.map PublishViewMsg pc ) diff --git a/modules/webapp/src/main/elm/Page/Home/View2.elm b/modules/webapp/src/main/elm/Page/Home/View2.elm index 515c8c51..e58f4132 100644 --- a/modules/webapp/src/main/elm/Page/Home/View2.elm +++ b/modules/webapp/src/main/elm/Page/Home/View2.elm @@ -80,7 +80,7 @@ mainView texts flags settings model = PublishSelected -> Just [ div [ class "sm:relative mb-2" ] - (itemPublishView texts flags svm) + (itemPublishView texts settings flags svm) ] _ -> @@ -89,7 +89,7 @@ mainView texts flags settings model = PublishView pm -> Just [ div [ class "sm:relative mb-2" ] - (publishResults texts flags model pm) + (publishResults texts settings flags model pm) ] SimpleView -> @@ -106,10 +106,10 @@ mainView texts flags settings model = itemCardList texts flags settings model -itemPublishView : Texts -> Flags -> SelectViewModel -> List (Html Msg) -itemPublishView texts flags svm = +itemPublishView : Texts -> UiSettings -> Flags -> SelectViewModel -> List (Html Msg) +itemPublishView texts settings flags svm = [ Html.map PublishItemsMsg - (Comp.PublishItems.view texts.publishItems flags svm.publishModel) + (Comp.PublishItems.view texts.publishItems settings flags svm.publishModel) ] @@ -120,10 +120,10 @@ itemMergeView texts settings svm = ] -publishResults : Texts -> Flags -> Model -> Comp.PublishItems.Model -> List (Html Msg) -publishResults texts flags model pm = +publishResults : Texts -> UiSettings -> Flags -> Model -> Comp.PublishItems.Model -> List (Html Msg) +publishResults texts settings flags model pm = [ Html.map PublishViewMsg - (Comp.PublishItems.view texts.publishItems flags pm) + (Comp.PublishItems.view texts.publishItems settings flags pm) ] diff --git a/modules/webapp/src/main/elm/Page/ShareDetail/Data.elm b/modules/webapp/src/main/elm/Page/ShareDetail/Data.elm index 0f262dab..2d6e1b3a 100644 --- a/modules/webapp/src/main/elm/Page/ShareDetail/Data.elm +++ b/modules/webapp/src/main/elm/Page/ShareDetail/Data.elm @@ -1,3 +1,10 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + module Page.ShareDetail.Data exposing (Model, Msg(..), PageError(..), ViewMode(..), init) import Api diff --git a/modules/webapp/src/main/elm/Page/ShareDetail/Update.elm b/modules/webapp/src/main/elm/Page/ShareDetail/Update.elm index 93ddabe2..efa880d3 100644 --- a/modules/webapp/src/main/elm/Page/ShareDetail/Update.elm +++ b/modules/webapp/src/main/elm/Page/ShareDetail/Update.elm @@ -1,3 +1,10 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + module Page.ShareDetail.Update exposing (update) import Api diff --git a/modules/webapp/src/main/elm/Page/ShareDetail/View.elm b/modules/webapp/src/main/elm/Page/ShareDetail/View.elm index 56ef7cbb..3ef1f3ea 100644 --- a/modules/webapp/src/main/elm/Page/ShareDetail/View.elm +++ b/modules/webapp/src/main/elm/Page/ShareDetail/View.elm @@ -1,3 +1,10 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + module Page.ShareDetail.View exposing (viewContent, viewSidebar) import Api From 9009ebcb3901a713f8e88cf43ae734c14fd47690 Mon Sep 17 00:00:00 2001 From: eikek Date: Sun, 10 Oct 2021 11:48:08 +0200 Subject: [PATCH 29/37] Prefill share mail form To have access to the translated content, the messages must be given to the update function. There is no way to set the values in the view. --- modules/webapp/src/main/elm/App/Update.elm | 67 +++++++++++-------- modules/webapp/src/main/elm/Comp/ItemMail.elm | 15 +---- .../webapp/src/main/elm/Comp/PublishItems.elm | 9 ++- .../webapp/src/main/elm/Comp/ShareMail.elm | 32 +++++---- .../webapp/src/main/elm/Comp/ShareManage.elm | 22 +++--- .../elm/Page/CollectiveSettings/Update.elm | 17 ++--- .../webapp/src/main/elm/Page/Home/Update.elm | 39 ++++++----- 7 files changed, 102 insertions(+), 99 deletions(-) diff --git a/modules/webapp/src/main/elm/App/Update.elm b/modules/webapp/src/main/elm/App/Update.elm index de687444..47d7ad9b 100644 --- a/modules/webapp/src/main/elm/App/Update.elm +++ b/modules/webapp/src/main/elm/App/Update.elm @@ -17,6 +17,7 @@ import Browser.Navigation as Nav import Data.Flags import Data.UiSettings exposing (UiSettings) import Data.UiTheme +import Messages exposing (Messages) import Page exposing (Page(..)) import Page.CollectiveSettings.Data import Page.CollectiveSettings.Update @@ -59,6 +60,10 @@ update msg model = updateWithSub : Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) updateWithSub msg model = + let + texts = + Messages.get <| App.Data.getUiLanguage model + in case msg of ToggleSidebar -> ( { model | sidebarVisible = not model.sidebarVisible }, Cmd.none, Sub.none ) @@ -98,7 +103,7 @@ updateWithSub msg model = ClientSettingsSaveResp settings (Ok res) -> if res.success then - applyClientSettings model settings + applyClientSettings texts model settings else ( model, Cmd.none, Sub.none ) @@ -116,7 +121,7 @@ updateWithSub msg model = ( { model | anonymousUiLang = lang, langMenuOpen = False }, Cmd.none, Sub.none ) HomeMsg lm -> - updateHome lm model + updateHome texts lm model ShareMsg lm -> updateShare lm model @@ -131,10 +136,10 @@ updateWithSub msg model = updateManageData lm model CollSettingsMsg m -> - updateCollSettings m model + updateCollSettings texts m model UserSettingsMsg m -> - updateUserSettings m model + updateUserSettings texts m model QueueMsg m -> updateQueue m model @@ -149,7 +154,7 @@ updateWithSub msg model = updateNewInvite m model ItemDetailMsg m -> - updateItemDetail m model + updateItemDetail texts m model VersionResp (Ok info) -> ( { model | version = info }, Cmd.none, Sub.none ) @@ -291,7 +296,7 @@ updateWithSub msg model = ) GetUiSettings (Ok settings) -> - applyClientSettings model settings + applyClientSettings texts model settings GetUiSettings (Err _) -> ( model, Cmd.none, Sub.none ) @@ -301,11 +306,11 @@ updateWithSub msg model = lm = Page.UserSettings.Data.ReceiveBrowserSettings sett in - updateUserSettings lm model + updateUserSettings texts lm model -applyClientSettings : Model -> UiSettings -> ( Model, Cmd Msg, Sub Msg ) -applyClientSettings model settings = +applyClientSettings : Messages -> Model -> UiSettings -> ( Model, Cmd Msg, Sub Msg ) +applyClientSettings texts model settings = let setTheme = Ports.setUiTheme settings.uiTheme @@ -316,9 +321,9 @@ applyClientSettings model settings = , setTheme , Sub.none ) - , updateUserSettings Page.UserSettings.Data.UpdateSettings - , updateHome Page.Home.Data.UiSettingsUpdated - , updateItemDetail Page.ItemDetail.Data.UiSettingsUpdated + , updateUserSettings texts Page.UserSettings.Data.UpdateSettings + , updateHome texts Page.Home.Data.UiSettingsUpdated + , updateItemDetail texts Page.ItemDetail.Data.UiSettingsUpdated ] { model | uiSettings = settings } @@ -357,8 +362,8 @@ updateShare lmsg model = ( model, Cmd.none, Sub.none ) -updateItemDetail : Page.ItemDetail.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) -updateItemDetail lmsg model = +updateItemDetail : Messages -> Page.ItemDetail.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +updateItemDetail texts lmsg model = let inav = Page.Home.Data.itemNav model.itemDetailModel.detail.item.id model.homeModel @@ -378,12 +383,12 @@ updateItemDetail lmsg model = } ( hm, hc, hs ) = - updateHome (Page.Home.Data.SetLinkTarget result.linkTarget) model_ + updateHome texts (Page.Home.Data.SetLinkTarget result.linkTarget) model_ ( hm1, hc1, hs1 ) = case result.removedItem of Just removedId -> - updateHome (Page.Home.Data.RemoveItem removedId) hm + updateHome texts (Page.Home.Data.RemoveItem removedId) hm Nothing -> ( hm, hc, hs ) @@ -446,8 +451,8 @@ updateQueue lmsg model = ) -updateUserSettings : Page.UserSettings.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) -updateUserSettings lmsg model = +updateUserSettings : Messages -> Page.UserSettings.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +updateUserSettings texts lmsg model = let result = Page.UserSettings.Update.update model.flags model.uiSettings lmsg model.userSettingsModel @@ -458,7 +463,7 @@ updateUserSettings lmsg model = ( lm2, lc2, s2 ) = case result.newSettings of Just sett -> - applyClientSettings model_ sett + applyClientSettings texts model_ sett Nothing -> ( model_, Cmd.none, Sub.none ) @@ -475,11 +480,12 @@ updateUserSettings lmsg model = ) -updateCollSettings : Page.CollectiveSettings.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) -updateCollSettings lmsg model = +updateCollSettings : Messages -> Page.CollectiveSettings.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +updateCollSettings texts lmsg model = let ( lm, lc, ls ) = - Page.CollectiveSettings.Update.update model.flags + Page.CollectiveSettings.Update.update texts.collectiveSettings + model.flags lmsg model.collSettingsModel in @@ -508,8 +514,8 @@ updateLogin lmsg model = ) -updateHome : Page.Home.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) -updateHome lmsg model = +updateHome : Messages -> Page.Home.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +updateHome texts lmsg model = let mid = case model.page of @@ -520,7 +526,7 @@ updateHome lmsg model = Nothing result = - Page.Home.Update.update mid model.key model.flags model.uiSettings lmsg model.homeModel + Page.Home.Update.update mid model.key model.flags texts.home model.uiSettings lmsg model.homeModel model_ = { model | homeModel = result.model } @@ -528,7 +534,7 @@ updateHome lmsg model = ( lm, lc, ls ) = case result.newSettings of Just sett -> - applyClientSettings model_ sett + applyClientSettings texts model_ sett Nothing -> ( model_, Cmd.none, Sub.none ) @@ -562,11 +568,14 @@ initPage model_ page = let model = { model_ | page = page } + + texts = + Messages.get <| App.Data.getUiLanguage model in case page of HomePage -> Util.Update.andThen2 - [ updateHome Page.Home.Data.Init + [ updateHome texts Page.Home.Data.Init , updateQueue Page.Queue.Data.StopRefresh ] model @@ -580,7 +589,7 @@ initPage model_ page = CollectiveSettingPage -> Util.Update.andThen2 [ updateQueue Page.Queue.Data.StopRefresh - , updateCollSettings Page.CollectiveSettings.Data.Init + , updateCollSettings texts Page.CollectiveSettings.Data.Init ] model @@ -608,7 +617,7 @@ initPage model_ page = ItemDetailPage id -> Util.Update.andThen2 - [ updateItemDetail (Page.ItemDetail.Data.Init id) + [ updateItemDetail texts (Page.ItemDetail.Data.Init id) , updateQueue Page.Queue.Data.StopRefresh ] model diff --git a/modules/webapp/src/main/elm/Comp/ItemMail.elm b/modules/webapp/src/main/elm/Comp/ItemMail.elm index 18b2fc57..540e65c0 100644 --- a/modules/webapp/src/main/elm/Comp/ItemMail.elm +++ b/modules/webapp/src/main/elm/Comp/ItemMail.elm @@ -252,8 +252,6 @@ isValid model = type alias ViewConfig = { withAttachments : Bool - , subjectTemplate : Maybe String - , bodyTemplate : Maybe String , textAreaClass : String , showCancel : Bool } @@ -264,8 +262,6 @@ view2 texts settings model = let cfg = { withAttachments = True - , subjectTemplate = Nothing - , bodyTemplate = Nothing , textAreaClass = "" , showCancel = True } @@ -357,11 +353,6 @@ view texts settings cfg model = [ type_ "text" , class S.textInput , onInput SetSubject - , if model.subject == "" then - onFocus (SetSubject <| Maybe.withDefault "" cfg.subjectTemplate) - - else - class "" , value model.subject ] [] @@ -373,11 +364,7 @@ view texts settings cfg model = ] , textarea [ onInput SetBody - , if model.body == "" then - value <| Maybe.withDefault "" cfg.bodyTemplate - - else - value model.body + , value model.body , class S.textAreaInput , class cfg.textAreaClass ] diff --git a/modules/webapp/src/main/elm/Comp/PublishItems.elm b/modules/webapp/src/main/elm/Comp/PublishItems.elm index 6e0aede9..98d18084 100644 --- a/modules/webapp/src/main/elm/Comp/PublishItems.elm +++ b/modules/webapp/src/main/elm/Comp/PublishItems.elm @@ -26,7 +26,6 @@ import Comp.ShareView import Data.Flags exposing (Flags) import Data.Icons as Icons import Data.ItemQuery exposing (ItemQuery) -import Data.SearchMode exposing (SearchMode) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) @@ -131,8 +130,8 @@ type alias UpdateResult = } -update : Flags -> Msg -> Model -> UpdateResult -update flags msg model = +update : Texts -> Flags -> Msg -> Model -> UpdateResult +update texts flags msg model = case msg of CancelPublish -> { model = model @@ -155,7 +154,7 @@ update flags msg model = MailMsg lm -> let ( mm, mc ) = - Comp.ShareMail.update flags lm model.mailModel + Comp.ShareMail.update texts.shareMail flags lm model.mailModel in { model = { model | mailModel = mm } , cmd = Cmd.map MailMsg mc @@ -204,7 +203,7 @@ update flags msg model = GetShareResp (Ok share) -> let ( mm, mc ) = - Comp.ShareMail.update flags (Comp.ShareMail.setMailInfo share) model.mailModel + Comp.ShareMail.update texts.shareMail flags (Comp.ShareMail.setMailInfo share) model.mailModel in { model = { model diff --git a/modules/webapp/src/main/elm/Comp/ShareMail.elm b/modules/webapp/src/main/elm/Comp/ShareMail.elm index 201a7cd0..f28f2d70 100644 --- a/modules/webapp/src/main/elm/Comp/ShareMail.elm +++ b/modules/webapp/src/main/elm/Comp/ShareMail.elm @@ -67,8 +67,8 @@ setMailInfo share = SetMailInfo share -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) -update flags msg model = +update : Texts -> Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update texts flags msg model = case msg of MailMsg lm -> let @@ -107,7 +107,22 @@ update flags msg model = defaultResult SetMailInfo share -> - ( { model | share = share }, Cmd.none ) + let + url = + flags.config.baseUrl ++ Page.pageToString (SharePage share.id) + + name = + share.name + + lm = + Comp.ItemMail.setMailInfo + (texts.subjectTemplate name) + (texts.bodyTemplate url) + + nm = + { model | share = share } + in + update texts flags (MailMsg lm) nm SendMailResp (Ok res) -> if res.success then @@ -138,19 +153,8 @@ update flags msg model = view : Texts -> Flags -> UiSettings -> Model -> Html Msg view texts flags settings model = let - url = - flags.config.baseUrl ++ (Page.pageToString <| SharePage model.share.id) - - subject = - texts.subjectTemplate model.share.name - - body = - texts.bodyTemplate url - cfg = { withAttachments = False - , subjectTemplate = Just subject - , bodyTemplate = Just body , textAreaClass = "h-52" , showCancel = False } diff --git a/modules/webapp/src/main/elm/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Comp/ShareManage.elm index f5d03315..be079129 100644 --- a/modules/webapp/src/main/elm/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Comp/ShareManage.elm @@ -110,8 +110,8 @@ loadShares = --- update -update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) -update flags msg model = +update : Texts -> Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +update texts flags msg model = case msg of InitNewShare -> let @@ -121,7 +121,7 @@ update flags msg model = share = Api.Model.ShareDetail.empty in - update flags (FormMsg (Comp.ShareForm.setShare { share | enabled = True })) nm + update texts flags (FormMsg (Comp.ShareForm.setShare { share | enabled = True })) nm SetViewMode vm -> ( { model | viewMode = vm, formError = FormErrorNone } @@ -150,7 +150,7 @@ update flags msg model = in case action of Comp.ShareTable.Edit share -> - setShare share flags model + setShare texts share flags model RequestDelete -> ( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none, Sub.none ) @@ -209,14 +209,14 @@ update flags msg model = ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) GetShareResp (Ok share) -> - setShare share flags model + setShare texts share flags model GetShareResp (Err err) -> ( { model | formError = FormErrorHttp err }, Cmd.none, Sub.none ) DeleteShareResp (Ok res) -> if res.success then - update flags (SetViewMode Table) { model | loading = False } + update texts flags (SetViewMode Table) { model | loading = False } else ( { model | formError = FormErrorSubmit res.message, loading = False }, Cmd.none, Sub.none ) @@ -227,13 +227,13 @@ update flags msg model = MailMsg lm -> let ( mm, mc ) = - Comp.ShareMail.update flags lm model.mailModel + Comp.ShareMail.update texts.shareMail flags lm model.mailModel in ( { model | mailModel = mm }, Cmd.map MailMsg mc, Sub.none ) -setShare : ShareDetail -> Flags -> Model -> ( Model, Cmd Msg, Sub Msg ) -setShare share flags model = +setShare : Texts -> ShareDetail -> Flags -> Model -> ( Model, Cmd Msg, Sub Msg ) +setShare texts share flags model = let shareUrl = flags.config.baseUrl ++ Page.pageToString (SharePage share.id) @@ -245,10 +245,10 @@ setShare share flags model = Ports.initClipboard (Comp.ShareView.clipboardData share) ( nm, nc, ns ) = - update flags (FormMsg <| Comp.ShareForm.setShare share) nextModel + update texts flags (FormMsg <| Comp.ShareForm.setShare share) nextModel ( nm2, nc2, ns2 ) = - update flags (MailMsg <| Comp.ShareMail.setMailInfo share) nm + update texts flags (MailMsg <| Comp.ShareMail.setMailInfo share) nm in ( nm2, Cmd.batch [ initClipboard, nc, nc2 ], Sub.batch [ ns, ns2 ] ) diff --git a/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm b/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm index b8a63d74..519971c0 100644 --- a/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm +++ b/modules/webapp/src/main/elm/Page/CollectiveSettings/Update.elm @@ -13,11 +13,12 @@ import Comp.ShareManage import Comp.SourceManage import Comp.UserManage import Data.Flags exposing (Flags) +import Messages.Page.CollectiveSettings exposing (Texts) import Page.CollectiveSettings.Data exposing (..) -update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) -update flags msg model = +update : Texts -> Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +update texts flags msg model = case msg of SetTab t -> let @@ -26,19 +27,19 @@ update flags msg model = in case t of SourceTab -> - update flags (SourceMsg Comp.SourceManage.LoadSources) m + update texts flags (SourceMsg Comp.SourceManage.LoadSources) m UserTab -> - update flags (UserMsg Comp.UserManage.LoadUsers) m + update texts flags (UserMsg Comp.UserManage.LoadUsers) m InsightsTab -> - update flags Init m + update texts flags Init m SettingsTab -> - update flags Init m + update texts flags Init m ShareTab -> - update flags (ShareMsg Comp.ShareManage.loadShares) m + update texts flags (ShareMsg Comp.ShareManage.loadShares) m SourceMsg m -> let @@ -50,7 +51,7 @@ update flags msg model = ShareMsg lm -> let ( sm, sc, ss ) = - Comp.ShareManage.update flags lm model.shareModel + Comp.ShareManage.update texts.shareManage flags lm model.shareModel in ( { model | shareModel = sm }, Cmd.map ShareMsg sc, Sub.map ShareMsg ss ) diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index 4848b838..27ddf0fd 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -27,6 +27,7 @@ import Data.ItemSelection import Data.Items import Data.SearchMode exposing (SearchMode) import Data.UiSettings exposing (UiSettings) +import Messages.Page.Home exposing (Texts) import Page exposing (Page(..)) import Page.Home.Data exposing (..) import Process @@ -48,8 +49,8 @@ type alias UpdateResult = } -update : Maybe String -> Nav.Key -> Flags -> UiSettings -> Msg -> Model -> UpdateResult -update mId key flags settings msg model = +update : Maybe String -> Nav.Key -> Flags -> Texts -> UiSettings -> Msg -> Model -> UpdateResult +update mId key flags texts settings msg model = case msg of Init -> let @@ -63,7 +64,7 @@ update mId key flags settings msg model = in makeResult <| Util.Update.andThen3 - [ update mId key flags settings (SearchMenuMsg Comp.SearchMenu.Init) + [ update mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.Init) , doSearch searchParam ] model @@ -73,7 +74,7 @@ update mId key flags settings msg model = nm = { model | searchOffset = 0, powerSearchInput = Comp.PowerSearchInput.init } in - update mId key flags settings (SearchMenuMsg Comp.SearchMenu.ResetForm) nm + update mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.ResetForm) nm SearchMenuMsg m -> let @@ -119,7 +120,7 @@ update mId key flags settings msg model = SetLinkTarget lt -> case linkTargetMsg lt of Just m -> - update mId key flags settings m model + update mId key flags texts settings m model Nothing -> makeResult ( model, Cmd.none, Sub.none ) @@ -167,7 +168,7 @@ update mId key flags settings msg model = in makeResult <| Util.Update.andThen3 - [ update mId key flags settings (ItemCardListMsg (Comp.ItemCardList.SetResults list)) + [ update mId key flags texts settings (ItemCardListMsg (Comp.ItemCardList.SetResults list)) , if scroll then scrollToCard mId @@ -189,7 +190,7 @@ update mId key flags settings msg model = , moreAvailable = list.groups /= [] } in - update mId key flags settings (ItemCardListMsg (Comp.ItemCardList.AddResults list)) m + update mId key flags texts settings (ItemCardListMsg (Comp.ItemCardList.AddResults list)) m ItemSearchAddResp (Err _) -> withSub @@ -289,18 +290,18 @@ update mId key flags settings msg model = smMsg = SearchMenuMsg (Comp.SearchMenu.SetTextSearch str) in - update mId key flags settings smMsg model + update mId key flags texts settings smMsg model ToggleSearchType -> case model.searchTypeDropdownValue of BasicSearch -> - update mId key flags settings (SearchMenuMsg Comp.SearchMenu.SetFulltextSearch) model + update mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.SetFulltextSearch) model ContentOnlySearch -> - update mId key flags settings (SearchMenuMsg Comp.SearchMenu.SetNamesSearch) model + update mId key flags texts settings (SearchMenuMsg Comp.SearchMenu.SetNamesSearch) model KeyUpSearchbarMsg (Just Enter) -> - update mId key flags settings (DoSearch model.searchTypeDropdownValue) model + update mId key flags texts settings (DoSearch model.searchTypeDropdownValue) model KeyUpSearchbarMsg _ -> withSub ( model, Cmd.none ) @@ -614,6 +615,7 @@ update mId key flags settings msg model = update mId key flags + texts settings (DoSearch model.searchTypeDropdownValue) model_ @@ -676,7 +678,7 @@ update mId key flags settings msg model = SelectView svm -> let result = - Comp.PublishItems.update flags lmsg svm.publishModel + Comp.PublishItems.update texts.publishItems flags lmsg svm.publishModel nextView = case result.outcome of @@ -693,6 +695,7 @@ update mId key flags settings msg model = update mId key flags + texts settings (DoSearch model.searchTypeDropdownValue) model_ @@ -809,7 +812,7 @@ update mId key flags settings msg model = model_ = { model | viewMode = viewMode } in - update mId key flags settings (DoSearch model.lastSearchType) model_ + update mId key flags texts settings (DoSearch model.lastSearchType) model_ SearchStatsResp result -> let @@ -819,7 +822,7 @@ update mId key flags settings msg model = stats = Result.withDefault model.searchStats result in - update mId key flags settings lm { model | searchStats = stats } + update mId key flags texts settings lm { model | searchStats = stats } TogglePreviewFullWidth -> let @@ -861,16 +864,16 @@ update mId key flags settings msg model = makeResult ( model_, cmd_, Sub.map PowerSearchMsg result.subs ) Comp.PowerSearchInput.SubmitSearch -> - update mId key flags settings (DoSearch model_.searchTypeDropdownValue) model_ + update mId key flags texts settings (DoSearch model_.searchTypeDropdownValue) model_ KeyUpPowerSearchbarMsg (Just Enter) -> - update mId key flags settings (DoSearch model.searchTypeDropdownValue) model + update mId key flags texts settings (DoSearch model.searchTypeDropdownValue) model KeyUpPowerSearchbarMsg _ -> withSub ( model, Cmd.none ) RemoveItem id -> - update mId key flags settings (ItemCardListMsg (Comp.ItemCardList.RemoveItem id)) model + update mId key flags texts settings (ItemCardListMsg (Comp.ItemCardList.RemoveItem id)) model TogglePublishCurrentQueryView -> case createQuery model of @@ -889,7 +892,7 @@ update mId key flags settings msg model = PublishView inPM -> let result = - Comp.PublishItems.update flags lmsg inPM + Comp.PublishItems.update texts.publishItems flags lmsg inPM in case result.outcome of Comp.PublishItems.OutcomeInProgress -> From 2ac0b84e5226c2e018852cd648d67ae28f6e612b Mon Sep 17 00:00:00 2001 From: eikek Date: Sat, 23 Oct 2021 23:29:36 +0200 Subject: [PATCH 30/37] Link shares to the user, not the collective The user is required when searching because of folders (sadly), so the share is connected to the user. --- .../scala/docspell/backend/ops/OShare.scala | 68 ++++++++----- .../scala/docspell/common/AccountId.scala | 1 + .../src/main/resources/docspell-openapi.yml | 13 +++ .../restserver/http4s/QueryParam.scala | 3 +- .../restserver/routes/ShareRoutes.scala | 44 +++++---- .../restserver/routes/ShareSearchRoutes.scala | 2 +- .../db/migration/h2/V1.27.1__item_share.sql | 4 +- .../migration/mariadb/V1.27.1__item_share.sql | 4 +- .../postgresql/V1.27.1__item_share.sql | 4 +- .../scala/docspell/store/records/RShare.scala | 76 ++++++++++----- .../scala/docspell/store/records/RUser.scala | 8 +- modules/webapp/package-lock.json | 23 +---- modules/webapp/src/main/elm/Api.elm | 15 ++- .../webapp/src/main/elm/Comp/ShareManage.elm | 96 ++++++++++++++++--- .../webapp/src/main/elm/Comp/ShareTable.elm | 8 +- .../main/elm/Messages/Comp/ShareManage.elm | 6 ++ .../src/main/elm/Messages/Comp/ShareTable.elm | 3 + 17 files changed, 268 insertions(+), 110 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index e8dae28f..ba27ea70 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -21,20 +21,24 @@ import docspell.query.ItemQuery.Expr import docspell.query.ItemQuery.Expr.AttachId import docspell.store.Store import docspell.store.queries.SearchSummary -import docspell.store.records.{RShare, RUserEmail} +import docspell.store.records._ import emil._ import scodec.bits.ByteVector trait OShare[F[_]] { - def findAll(collective: Ident): F[List[RShare]] + def findAll( + collective: Ident, + ownerLogin: Option[Ident], + query: Option[String] + ): F[List[ShareData]] def delete(id: Ident, collective: Ident): F[Boolean] def addNew(share: OShare.NewShare): F[OShare.ChangeResult] - def findOne(id: Ident, collective: Ident): OptionT[F, RShare] + def findOne(id: Ident, collective: Ident): OptionT[F, ShareData] def update( id: Ident, @@ -91,12 +95,7 @@ object OShare { case object NotFound extends SendResult } - final case class ShareQuery(id: Ident, cid: Ident, query: ItemQuery) { - - //TODO - def asAccount: AccountId = - AccountId(cid, Ident.unsafe("")) - } + final case class ShareQuery(id: Ident, account: AccountId, query: ItemQuery) sealed trait VerifyResult { def toEither: Either[String, ShareToken] = @@ -121,7 +120,7 @@ object OShare { } final case class NewShare( - cid: Ident, + account: AccountId, name: Option[String], query: ItemQuery, enabled: Boolean, @@ -133,11 +132,15 @@ object OShare { object ChangeResult { final case class Success(id: Ident) extends ChangeResult case object PublishUntilInPast extends ChangeResult + case object NotFound extends ChangeResult def success(id: Ident): ChangeResult = Success(id) def publishUntilInPast: ChangeResult = PublishUntilInPast + def notFound: ChangeResult = NotFound } + final case class ShareData(share: RShare, user: RUser) + def apply[F[_]: Async]( store: Store[F], itemSearch: OItemSearch[F], @@ -147,8 +150,14 @@ object OShare { new OShare[F] { private[this] val logger = Logger.log4s[F](org.log4s.getLogger) - def findAll(collective: Ident): F[List[RShare]] = - store.transact(RShare.findAllByCollective(collective)) + def findAll( + collective: Ident, + ownerLogin: Option[Ident], + query: Option[String] + ): F[List[ShareData]] = + store + .transact(RShare.findAllByCollective(collective, ownerLogin, query)) + .map(_.map(ShareData.tupled)) def delete(id: Ident, collective: Ident): F[Boolean] = store.transact(RShare.deleteByIdAndCid(id, collective)).map(_ > 0) @@ -157,10 +166,11 @@ object OShare { for { curTime <- Timestamp.current[F] id <- Ident.randomId[F] + user <- store.transact(RUser.findByAccount(share.account)) pass = share.password.map(PasswordCrypt.crypt) record = RShare( id, - share.cid, + user.map(_.uid).getOrElse(Ident.unsafe("-error-no-user-")), share.name, share.query, share.enabled, @@ -182,9 +192,10 @@ object OShare { ): F[ChangeResult] = for { curTime <- Timestamp.current[F] + user <- store.transact(RUser.findByAccount(share.account)) record = RShare( id, - share.cid, + user.map(_.uid).getOrElse(Ident.unsafe("-error-no-user-")), share.name, share.query, share.enabled, @@ -199,11 +210,14 @@ object OShare { else store .transact(RShare.updateData(record, removePassword)) - .map(_ => ChangeResult.success(id)) + .map(n => if (n > 0) ChangeResult.success(id) else ChangeResult.notFound) } yield res - def findOne(id: Ident, collective: Ident): OptionT[F, RShare] = - RShare.findOne(id, collective).mapK(store.transform) + def findOne(id: Ident, collective: Ident): OptionT[F, ShareData] = + RShare + .findOne(id, collective) + .mapK(store.transform) + .map(ShareData.tupled) def verify( key: ByteVector @@ -211,7 +225,7 @@ object OShare { RShare .findCurrentActive(id) .mapK(store.transform) - .semiflatMap { share => + .semiflatMap { case (share, _) => val pwCheck = share.password.map(encPw => password.exists(PasswordCrypt.check(_, encPw))) @@ -257,7 +271,9 @@ object OShare { RShare .findCurrentActive(id) .mapK(store.transform) - .map(share => ShareQuery(share.id, share.cid, share.query)) + .map { case (share, user) => + ShareQuery(share.id, user.accountId, share.query) + } def findAttachmentPreview( attachId: Ident, @@ -266,21 +282,23 @@ object OShare { for { sq <- findShareQuery(shareId) _ <- checkAttachment(sq, AttachId(attachId.id)) - res <- OptionT(itemSearch.findAttachmentPreview(attachId, sq.cid)) + res <- OptionT( + itemSearch.findAttachmentPreview(attachId, sq.account.collective) + ) } yield res def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] = for { sq <- findShareQuery(shareId) _ <- checkAttachment(sq, AttachId(attachId.id)) - res <- OptionT(itemSearch.findAttachment(attachId, sq.cid)) + res <- OptionT(itemSearch.findAttachment(attachId, sq.account.collective)) } yield res def findItem(itemId: Ident, shareId: Ident): OptionT[F, ItemData] = for { sq <- findShareQuery(shareId) _ <- checkAttachment(sq, Expr.itemIdEq(itemId.id)) - res <- OptionT(itemSearch.findItem(itemId, sq.cid)) + res <- OptionT(itemSearch.findItem(itemId, sq.account.collective)) } yield res /** Check whether the attachment with the given id is in the results of the given @@ -288,7 +306,7 @@ object OShare { */ private def checkAttachment(sq: ShareQuery, idExpr: Expr): OptionT[F, Unit] = { val checkQuery = Query( - Query.Fix(sq.asAccount, Some(sq.query.expr), None), + Query.Fix(sq.account, Some(sq.query.expr), None), Query.QueryExpr(idExpr) ) OptionT( @@ -310,7 +328,7 @@ object OShare { ): OptionT[F, StringSearchResult[SearchSummary]] = findShareQuery(shareId) .semiflatMap { share => - val fix = Query.Fix(share.asAccount, Some(share.query.expr), None) + val fix = Query.Fix(share.account, Some(share.query.expr), None) simpleSearch .searchSummaryByString(settings)(fix, q) .map { @@ -350,7 +368,7 @@ object OShare { (for { _ <- RShare .findCurrentActive(mail.shareId) - .filter(_.cid == account.collective) + .filter(_._2.cid == account.collective) .mapK(store.transform) mailCfg <- getSmtpSettings mail <- createMail(mailCfg) diff --git a/modules/common/src/main/scala/docspell/common/AccountId.scala b/modules/common/src/main/scala/docspell/common/AccountId.scala index bd8f1fb1..e8479966 100644 --- a/modules/common/src/main/scala/docspell/common/AccountId.scala +++ b/modules/common/src/main/scala/docspell/common/AccountId.scala @@ -8,6 +8,7 @@ package docspell.common import io.circe._ +/** The collective and user name. */ case class AccountId(collective: Ident, user: Ident) { def asString = if (collective == user) user.id diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index e5ce7c9e..f7e250fa 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1932,6 +1932,9 @@ paths: Return a list of all shares for this collective. security: - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/q" + - $ref: "#/components/parameters/owningShare" responses: 200: description: Ok @@ -4496,6 +4499,7 @@ components: required: - id - query + - owner - enabled - publishAt - publishUntil @@ -4509,6 +4513,8 @@ components: query: type: string format: itemquery + owner: + $ref: "#/components/schemas/IdName" name: type: string enabled: @@ -6805,6 +6811,13 @@ components: required: false schema: type: boolean + owningShare: + name: owning + in: query + description: Return my own shares only + required: false + schema: + type: boolean checksum: name: checksum in: path diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala index 102325da..041814cf 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala @@ -16,7 +16,7 @@ import docspell.common.SearchMode import org.http4s.ParseFailure import org.http4s.QueryParamDecoder -import org.http4s.dsl.impl.OptionalQueryParamDecoderMatcher +import org.http4s.dsl.impl.{FlagQueryParamMatcher, OptionalQueryParamDecoderMatcher} object QueryParam { case class QueryString(q: String) @@ -67,6 +67,7 @@ object QueryParam { object FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full") object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning") + object OwningFlag extends FlagQueryParamMatcher("owning") object ContactKindOpt extends OptionalQueryParamDecoderMatcher[ContactKind]("kind") diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala index 92830d2d..4106642f 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala @@ -18,8 +18,7 @@ import docspell.common.{Ident, Timestamp} import docspell.restapi.model._ import docspell.restserver.Config import docspell.restserver.auth.ShareCookieData -import docspell.restserver.http4s.{ClientRequestInfo, ResponseGenerator} -import docspell.store.records.RShare +import docspell.restserver.http4s.{ClientRequestInfo, QueryParam => QP, ResponseGenerator} import emil.MailAddress import emil.javamail.syntax._ @@ -35,9 +34,10 @@ object ShareRoutes { import dsl._ HttpRoutes.of { - case GET -> Root => + case GET -> Root :? QP.Query(q) :? QP.OwningFlag(owning) => + val login = if (owning) Some(user.account.user) else None for { - all <- backend.share.findAll(user.account.collective) + all <- backend.share.findAll(user.account.collective, login, q) now <- Timestamp.current[F] res <- Ok(ShareList(all.map(mkShareDetail(now)))) } yield res @@ -111,7 +111,7 @@ object ShareRoutes { def mkNewShare(data: ShareData, user: AuthToken): OShare.NewShare = OShare.NewShare( - user.account.collective, + user.account, data.name, data.query, data.enabled, @@ -124,6 +124,12 @@ object ShareRoutes { case OShare.ChangeResult.Success(id) => IdResult(true, msg, id) case OShare.ChangeResult.PublishUntilInPast => IdResult(false, "Until date must not be in the past", Ident.unsafe("")) + case OShare.ChangeResult.NotFound => + IdResult( + false, + "Share not found or not owner. Only the owner can update a share.", + Ident.unsafe("") + ) } def mkBasicResult(r: OShare.ChangeResult, msg: => String): BasicResult = @@ -131,20 +137,26 @@ object ShareRoutes { case OShare.ChangeResult.Success(_) => BasicResult(true, msg) case OShare.ChangeResult.PublishUntilInPast => BasicResult(false, "Until date must not be in the past") + case OShare.ChangeResult.NotFound => + BasicResult( + false, + "Share not found or not owner. Only the owner can update a share." + ) } - def mkShareDetail(now: Timestamp)(r: RShare): ShareDetail = + def mkShareDetail(now: Timestamp)(r: OShare.ShareData): ShareDetail = ShareDetail( - r.id, - r.query, - r.name, - r.enabled, - r.publishAt, - r.publishUntil, - now > r.publishUntil, - r.password.isDefined, - r.views, - r.lastAccess + r.share.id, + r.share.query, + IdName(r.user.uid, r.user.login.id), + r.share.name, + r.share.enabled, + r.share.publishAt, + r.share.publishUntil, + now > r.share.publishUntil, + r.share.password.isDefined, + r.share.views, + r.share.lastAccess ) def convertIn(s: SimpleShareMail): Either[String, ShareMail] = diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala index 96202f14..64e14a5e 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala @@ -59,7 +59,7 @@ object ShareSearchRoutes { cfg.maxNoteLength, searchMode = SearchMode.Normal ) - account = share.asAccount + account = share.account fixQuery = Query.Fix(account, Some(share.query.expr), None) _ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}") resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery) diff --git a/modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql b/modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql index 7e252c14..9765afc1 100644 --- a/modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql +++ b/modules/store/src/main/resources/db/migration/h2/V1.27.1__item_share.sql @@ -1,6 +1,6 @@ CREATE TABLE "item_share" ( "id" varchar(254) not null primary key, - "cid" varchar(254) not null, + "user_id" varchar(254) not null, "name" varchar(254), "query" varchar(2000) not null, "enabled" boolean not null, @@ -9,5 +9,5 @@ CREATE TABLE "item_share" ( "publish_until" timestamp not null, "views" int not null, "last_access" timestamp, - foreign key ("cid") references "collective"("cid") on delete cascade + foreign key ("user_id") references "user_"("uid") on delete cascade ) diff --git a/modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql b/modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql index fb74d283..714aabbb 100644 --- a/modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql +++ b/modules/store/src/main/resources/db/migration/mariadb/V1.27.1__item_share.sql @@ -1,6 +1,6 @@ CREATE TABLE `item_share` ( `id` varchar(254) not null primary key, - `cid` varchar(254) not null, + `user_id` varchar(254) not null, `name` varchar(254), `query` varchar(2000) not null, `enabled` boolean not null, @@ -9,5 +9,5 @@ CREATE TABLE `item_share` ( `publish_until` timestamp not null, `views` int not null, `last_access` timestamp, - foreign key (`cid`) references `collective`(`cid`) on delete cascade + foreign key (`user_id`) references `user_`(`uid`) on delete cascade ) diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql index 7e252c14..9765afc1 100644 --- a/modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.27.1__item_share.sql @@ -1,6 +1,6 @@ CREATE TABLE "item_share" ( "id" varchar(254) not null primary key, - "cid" varchar(254) not null, + "user_id" varchar(254) not null, "name" varchar(254), "query" varchar(2000) not null, "enabled" boolean not null, @@ -9,5 +9,5 @@ CREATE TABLE "item_share" ( "publish_until" timestamp not null, "views" int not null, "last_access" timestamp, - foreign key ("cid") references "collective"("cid") on delete cascade + foreign key ("user_id") references "user_"("uid") on delete cascade ) diff --git a/modules/store/src/main/scala/docspell/store/records/RShare.scala b/modules/store/src/main/scala/docspell/store/records/RShare.scala index 7d6ae9bd..5ddfdb6b 100644 --- a/modules/store/src/main/scala/docspell/store/records/RShare.scala +++ b/modules/store/src/main/scala/docspell/store/records/RShare.scala @@ -18,7 +18,7 @@ import doobie.implicits._ final case class RShare( id: Ident, - cid: Ident, + userId: Ident, name: Option[String], query: ItemQuery, enabled: Boolean, @@ -35,7 +35,7 @@ object RShare { val tableName = "item_share"; val id = Column[Ident]("id", this) - val cid = Column[Ident]("cid", this) + val userId = Column[Ident]("user_id", this) val name = Column[String]("name", this) val query = Column[ItemQuery]("query", this) val enabled = Column[Boolean]("enabled", this) @@ -48,7 +48,7 @@ object RShare { val all: NonEmptyList[Column[_]] = NonEmptyList.of( id, - cid, + userId, name, query, enabled, @@ -67,7 +67,7 @@ object RShare { DML.insert( T, T.all, - fr"${r.id},${r.cid},${r.name},${r.query},${r.enabled},${r.password},${r.publishAt},${r.publishUntil},${r.views},${r.lastAccess}" + fr"${r.id},${r.userId},${r.name},${r.query},${r.enabled},${r.password},${r.publishAt},${r.publishUntil},${r.views},${r.lastAccess}" ) def incAccess(id: Ident): ConnectionIO[Int] = @@ -83,7 +83,7 @@ object RShare { def updateData(r: RShare, removePassword: Boolean): ConnectionIO[Int] = DML.update( T, - T.id === r.id && T.cid === r.cid, + T.id === r.id && T.userId === r.userId, DML.set( T.name.setTo(r.name), T.query.setTo(r.query), @@ -94,26 +94,41 @@ object RShare { else Nil) ) - def findOne(id: Ident, cid: Ident): OptionT[ConnectionIO, RShare] = + def findOne(id: Ident, cid: Ident): OptionT[ConnectionIO, (RShare, RUser)] = { + val s = RShare.as("s") + val u = RUser.as("u") + OptionT( - Select(select(T.all), from(T), T.id === id && T.cid === cid).build - .query[RShare] + Select( + select(s.all, u.all), + from(s).innerJoin(u, u.uid === s.userId), + s.id === id && u.cid === cid + ).build + .query[(RShare, RUser)] .option ) + } private def activeCondition(t: Table, id: Ident, current: Timestamp): Condition = t.id === id && t.enabled === true && t.publishedUntil > current - def findActive(id: Ident, current: Timestamp): OptionT[ConnectionIO, RShare] = + def findActive( + id: Ident, + current: Timestamp + ): OptionT[ConnectionIO, (RShare, RUser)] = { + val s = RShare.as("s") + val u = RUser.as("u") + OptionT( Select( - select(T.all), - from(T), - activeCondition(T, id, current) - ).build.query[RShare].option + select(s.all, u.all), + from(s).innerJoin(u, s.userId === u.uid), + activeCondition(s, id, current) + ).build.query[(RShare, RUser)].option ) + } - def findCurrentActive(id: Ident): OptionT[ConnectionIO, RShare] = + def findCurrentActive(id: Ident): OptionT[ConnectionIO, (RShare, RUser)] = OptionT.liftF(Timestamp.current[ConnectionIO]).flatMap(now => findActive(id, now)) def findActivePassword(id: Ident): OptionT[ConnectionIO, Option[Password]] = @@ -123,13 +138,30 @@ object RShare { .option }) - def findAllByCollective(cid: Ident): ConnectionIO[List[RShare]] = - Select(select(T.all), from(T), T.cid === cid) - .orderBy(T.publishedAt.desc) - .build - .query[RShare] - .to[List] + def findAllByCollective( + cid: Ident, + ownerLogin: Option[Ident], + q: Option[String] + ): ConnectionIO[List[(RShare, RUser)]] = { + val s = RShare.as("s") + val u = RUser.as("u") - def deleteByIdAndCid(id: Ident, cid: Ident): ConnectionIO[Int] = - DML.delete(T, T.id === id && T.cid === cid) + val ownerQ = ownerLogin.map(name => u.login === name) + val nameQ = q.map(n => s.name.like(s"%$n%")) + + Select( + select(s.all, u.all), + from(s).innerJoin(u, u.uid === s.userId), + u.cid === cid &&? ownerQ &&? nameQ + ) + .orderBy(s.publishedAt.desc) + .build + .query[(RShare, RUser)] + .to[List] + } + + def deleteByIdAndCid(id: Ident, cid: Ident): ConnectionIO[Int] = { + val u = RUser.T + DML.delete(T, T.id === id && T.userId.in(Select(u.uid.s, from(u), u.cid === cid))) + } } diff --git a/modules/store/src/main/scala/docspell/store/records/RUser.scala b/modules/store/src/main/scala/docspell/store/records/RUser.scala index dc8f66d8..dbd4051c 100644 --- a/modules/store/src/main/scala/docspell/store/records/RUser.scala +++ b/modules/store/src/main/scala/docspell/store/records/RUser.scala @@ -26,7 +26,13 @@ case class RUser( loginCount: Int, lastLogin: Option[Timestamp], created: Timestamp -) {} +) { + def accountId: AccountId = + AccountId(cid, login) + + def idRef: IdRef = + IdRef(uid, login.id) +} object RUser { diff --git a/modules/webapp/package-lock.json b/modules/webapp/package-lock.json index 6f8016f3..4801d04f 100644 --- a/modules/webapp/package-lock.json +++ b/modules/webapp/package-lock.json @@ -153,20 +153,8 @@ "electron-to-chromium": "^1.3.719", "escalade": "^3.1.1", "node-releases": "^1.1.71" - }, - "dependencies": { - "caniuse-lite": { - "version": "1.0.30001230", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz", - "integrity": "sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==" - } } }, - "caniuse-lite": { - "version": "1.0.30001204", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz", - "integrity": "sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ==" - }, "colorette": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", @@ -228,11 +216,6 @@ "node-releases": "^1.1.71" }, "dependencies": { - "caniuse-lite": { - "version": "1.0.30001230", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz", - "integrity": "sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==" - }, "colorette": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", @@ -272,9 +255,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001208", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz", - "integrity": "sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==" + "version": "1.0.30001271", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz", + "integrity": "sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==" }, "chalk": { "version": "2.4.2", diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 2051fe23..bb89794d 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -2228,10 +2228,19 @@ disableOtp flags otp receive = --- Share -getShares : Flags -> (Result Http.Error ShareList -> msg) -> Cmd msg -getShares flags receive = +getShares : Flags -> String -> Bool -> (Result Http.Error ShareList -> msg) -> Cmd msg +getShares flags query owning receive = Http2.authGet - { url = flags.config.baseUrl ++ "/api/v1/sec/share" + { url = + flags.config.baseUrl + ++ "/api/v1/sec/share?q=" + ++ Url.percentEncode query + ++ (if owning then + "&owning" + + else + "" + ) , account = getAccount flags , expect = Http.expectJson receive Api.Model.ShareList.decoder } diff --git a/modules/webapp/src/main/elm/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Comp/ShareManage.elm index be079129..e1c33def 100644 --- a/modules/webapp/src/main/elm/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Comp/ShareManage.elm @@ -56,6 +56,8 @@ type alias Model = , loading : Bool , formError : FormError , deleteConfirm : DeleteConfirm + , query : String + , owningOnly : Bool } @@ -75,6 +77,8 @@ init flags = , loading = False , formError = FormErrorNone , deleteConfirm = DeleteConfirmOff + , query = "" + , owningOnly = True } , Cmd.batch [ Cmd.map FormMsg fc @@ -90,6 +94,8 @@ type Msg | MailMsg Comp.ShareMail.Msg | InitNewShare | SetViewMode ViewMode + | SetQuery String + | ToggleOwningOnly | Submit | RequestDelete | CancelDelete @@ -126,7 +132,7 @@ update texts flags msg model = SetViewMode vm -> ( { model | viewMode = vm, formError = FormErrorNone } , if vm == Table then - Api.getShares flags LoadSharesResp + Api.getShares flags model.query model.owningOnly LoadSharesResp else Cmd.none @@ -165,7 +171,10 @@ update texts flags msg model = ) LoadShares -> - ( { model | loading = True }, Api.getShares flags LoadSharesResp, Sub.none ) + ( { model | loading = True } + , Api.getShares flags model.query model.owningOnly LoadSharesResp + , Sub.none + ) LoadSharesResp (Ok list) -> ( { model | loading = False, shares = list.items, formError = FormErrorNone } @@ -231,6 +240,26 @@ update texts flags msg model = in ( { model | mailModel = mm }, Cmd.map MailMsg mc, Sub.none ) + SetQuery q -> + let + nm = + { model | query = q } + in + ( nm + , Api.getShares flags nm.query nm.owningOnly LoadSharesResp + , Sub.none + ) + + ToggleOwningOnly -> + let + nm = + { model | owningOnly = not model.owningOnly } + in + ( nm + , Api.getShares flags nm.query nm.owningOnly LoadSharesResp + , Sub.none + ) + setShare : Texts -> ShareDetail -> Flags -> Model -> ( Model, Cmd Msg, Sub Msg ) setShare texts share flags model = @@ -271,7 +300,19 @@ viewTable texts model = div [ class "flex flex-col" ] [ MB.view { start = - [] + [ MB.TextInput + { tagger = SetQuery + , value = model.query + , placeholder = texts.basics.searchPlaceholder + , icon = Just "fa fa-search" + } + , MB.Checkbox + { tagger = \_ -> ToggleOwningOnly + , label = texts.showOwningSharesOnly + , value = model.owningOnly + , id = "share-toggle-owner" + } + ] , end = [ MB.PrimaryButton { tagger = InitNewShare @@ -295,6 +336,11 @@ viewForm texts settings flags model = let newShare = model.formModel.share.id == "" + + isOwner = + Maybe.map .user flags.account + |> Maybe.map ((==) model.formModel.share.owner.name) + |> Maybe.withDefault False in div [] [ Html.form [] @@ -305,20 +351,34 @@ viewForm texts settings flags model = else h1 [ class S.header2 ] - [ text <| Maybe.withDefault texts.noName model.formModel.share.name - , div [ class "opacity-50 text-sm" ] - [ text "Id: " - , text model.formModel.share.id + [ div [ class "flex flex-row items-center" ] + [ div + [ class "flex text-sm opacity-75 label mr-3" + , classList [ ( "hidden", isOwner ) ] + ] + [ i [ class "fa fa-user mr-2" ] [] + , text model.formModel.share.owner.name + ] + , text <| Maybe.withDefault texts.noName model.formModel.share.name + ] + , div [ class "flex flex-row items-center" ] + [ div [ class "opacity-50 text-sm flex-grow" ] + [ text "Id: " + , text model.formModel.share.id + ] ] ] , MB.view { start = - [ MB.PrimaryButton - { tagger = Submit - , title = "Submit this form" - , icon = Just "fa fa-save" - , label = texts.basics.submit - } + [ MB.CustomElement <| + B.primaryButton + { handler = onClick Submit + , title = "Submit this form" + , icon = "fa fa-save" + , label = texts.basics.submit + , disabled = not isOwner + , attrs = [ href "#" ] + } , MB.SecondaryButton { tagger = SetViewMode Table , title = texts.basics.backToList @@ -360,7 +420,15 @@ viewForm texts settings flags model = FormErrorSubmit m -> text m ] - , Html.map FormMsg (Comp.ShareForm.view texts.shareForm model.formModel) + , div + [ classList [ ( "hidden", isOwner ) ] + , class S.infoMessage + ] + [ text texts.notOwnerInfo + ] + , div [ classList [ ( "hidden", not isOwner ) ] ] + [ Html.map FormMsg (Comp.ShareForm.view texts.shareForm model.formModel) + ] , B.loadingDimmer { active = model.loading , label = texts.basics.loading diff --git a/modules/webapp/src/main/elm/Comp/ShareTable.elm b/modules/webapp/src/main/elm/Comp/ShareTable.elm index b62f39b5..1082567d 100644 --- a/modules/webapp/src/main/elm/Comp/ShareTable.elm +++ b/modules/webapp/src/main/elm/Comp/ShareTable.elm @@ -56,7 +56,10 @@ view texts shares = , th [ class "text-center" ] [ text texts.active ] - , th [ class "text-center" ] + , th [ class "hidden sm:table-cell text-center" ] + [ text texts.user + ] + , th [ class "hidden sm:table-cell text-center" ] [ text texts.publishUntil ] ] @@ -88,6 +91,9 @@ renderShareLine texts share = else i [ class "fa fa-check" ] [] ] + , td [ class "hidden sm:table-cell text-center" ] + [ text share.owner.name + ] , td [ class "hidden sm:table-cell text-center" ] [ texts.formatDateTime share.publishUntil |> text ] diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm index d90824be..773ea5b3 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm @@ -39,6 +39,8 @@ type alias Texts = , noName : String , shareInformation : String , sendMail : String + , notOwnerInfo : String + , showOwningSharesOnly : String } @@ -62,6 +64,8 @@ gb = , noName = "No Name" , shareInformation = "Share Information" , sendMail = "Send via E-Mail" + , notOwnerInfo = "Only the user who created this share can edit its properties." + , showOwningSharesOnly = "Show my shares only" } @@ -85,4 +89,6 @@ de = , noName = "Ohne Name" , shareInformation = "Informationen zur Freigabe" , sendMail = "Per E-Mail versenden" + , notOwnerInfo = "Nur der Benutzer, der diese Freigabe erstellt hat, kann diese auch ändern." + , showOwningSharesOnly = "Nur meine Freigaben anzeigen" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm index 5b87e47e..170876ff 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareTable.elm @@ -21,6 +21,7 @@ type alias Texts = , formatDateTime : Int -> String , active : String , publishUntil : String + , user : String } @@ -30,6 +31,7 @@ gb = , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.English , active = "Active" , publishUntil = "Publish Until" + , user = "User" } @@ -39,4 +41,5 @@ de = , formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.German , active = "Aktiv" , publishUntil = "Publiziert bis" + , user = "Benutzer" } From 6696aba4817861e00b0d754d90ebc8b502c9cca2 Mon Sep 17 00:00:00 2001 From: eikek Date: Sat, 23 Oct 2021 23:42:02 +0200 Subject: [PATCH 31/37] Show user shares when asking to delete user --- .../restapi/src/main/resources/docspell-openapi.yml | 4 ++++ .../docspell/restserver/routes/UserRoutes.scala | 4 +++- .../main/scala/docspell/store/queries/QUser.scala | 12 ++++++++++-- modules/webapp/src/main/elm/Comp/UserManage.elm | 12 ++++++++---- .../webapp/src/main/elm/Messages/Comp/UserManage.elm | 12 ++++++++++++ 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index f7e250fa..f6714ac4 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -4554,6 +4554,7 @@ components: required: - folders - sentMails + - shares properties: folders: type: array @@ -4563,6 +4564,9 @@ components: sentMails: type: integer format: int32 + shares: + type: integer + format: int32 SecondFactor: description: | diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/UserRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/UserRoutes.scala index 35944fca..ef4d75d8 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/UserRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/UserRoutes.scala @@ -72,7 +72,9 @@ object UserRoutes { data <- backend.collective.getDeleteUserData( AccountId(user.account.collective, username) ) - resp <- Ok(DeleteUserData(data.ownedFolders.map(_.id), data.sentMails)) + resp <- Ok( + DeleteUserData(data.ownedFolders.map(_.id), data.sentMails, data.shares) + ) } yield resp } } diff --git a/modules/store/src/main/scala/docspell/store/queries/QUser.scala b/modules/store/src/main/scala/docspell/store/queries/QUser.scala index c261b670..6000016a 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QUser.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QUser.scala @@ -20,7 +20,8 @@ object QUser { final case class UserData( ownedFolders: List[Ident], - sentMails: Int + sentMails: Int, + shares: Int ) def getUserData(accountId: AccountId): ConnectionIO[UserData] = { @@ -28,6 +29,7 @@ object QUser { val mail = RSentMail.as("m") val mitem = RSentMailItem.as("mi") val user = RUser.as("u") + val share = RShare.as("s") for { uid <- loadUserId(accountId).map(_.getOrElse(Ident.unsafe(""))) @@ -43,7 +45,13 @@ object QUser { .innerJoin(user, user.uid === mail.uid), user.login === accountId.user && user.cid === accountId.collective ).query[Int].unique - } yield UserData(folders, mails) + shares <- run( + select(count(share.id)), + from(share) + .innerJoin(user, user.uid === share.userId), + user.login === accountId.user && user.cid === accountId.collective + ).query[Int].unique + } yield UserData(folders, mails, shares) } def deleteUserAndData(accountId: AccountId): ConnectionIO[Int] = diff --git a/modules/webapp/src/main/elm/Comp/UserManage.elm b/modules/webapp/src/main/elm/Comp/UserManage.elm index 15efbaf8..47a6d0a6 100644 --- a/modules/webapp/src/main/elm/Comp/UserManage.elm +++ b/modules/webapp/src/main/elm/Comp/UserManage.elm @@ -295,7 +295,7 @@ renderDeleteConfirm texts settings model = DimmerUserData data -> let empty = - List.isEmpty data.folders && data.sentMails == 0 + List.isEmpty data.folders && data.sentMails == 0 && data.shares == 0 folderNames = String.join ", " data.folders @@ -312,16 +312,20 @@ renderDeleteConfirm texts settings model = [ div [] [ text texts.reallyDeleteUser , text " " - , text "The following data will be deleted:" + , text (texts.deleteFollowingData ++ ":") ] , ul [ class "list-inside list-disc" ] [ li [ classList [ ( "hidden", List.isEmpty data.folders ) ] ] - [ text "Folders: " + [ text (texts.folders ++ ": ") , text folderNames ] , li [ classList [ ( "hidden", data.sentMails == 0 ) ] ] [ text (String.fromInt data.sentMails) - , text " sent mails" + , text (" " ++ texts.sentMails) + ] + , li [ classList [ ( "hidden", data.shares == 0 ) ] ] + [ text (String.fromInt data.shares) + , text (" " ++ texts.shares) ] ] ] diff --git a/modules/webapp/src/main/elm/Messages/Comp/UserManage.elm b/modules/webapp/src/main/elm/Messages/Comp/UserManage.elm index 2c59fd72..5f1fd5f2 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/UserManage.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/UserManage.elm @@ -31,6 +31,10 @@ type alias Texts = , deleteThisUser : String , pleaseCorrectErrors : String , notDeleteCurrentUser : String + , folders : String + , sentMails : String + , shares : String + , deleteFollowingData : String } @@ -48,6 +52,10 @@ gb = , deleteThisUser = "Delete this user" , pleaseCorrectErrors = "Please correct the errors in the form." , notDeleteCurrentUser = "You can't delete the user you are currently logged in with." + , folders = "Folders" + , sentMails = "sent mails" + , shares = "shares" + , deleteFollowingData = "The following data will be deleted" } @@ -65,4 +73,8 @@ de = , deleteThisUser = "Benutzer löschen" , pleaseCorrectErrors = "Bitte korrigiere die Fehler im Formular." , notDeleteCurrentUser = "Der aktuelle Benutzer kann nicht gelöscht werden." + , folders = "Ordner" + , sentMails = "gesendete E-Mails" + , shares = "Freigaben" + , deleteFollowingData = "Die folgenden Daten werden auch gelöscht" } From eaccb607324bf6a674840823a082b2030357c71a Mon Sep 17 00:00:00 2001 From: eikek Date: Sun, 24 Oct 2021 00:03:09 +0200 Subject: [PATCH 32/37] Fix date field background for sidebar and main content --- modules/webapp/src/main/styles/index.css | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/webapp/src/main/styles/index.css b/modules/webapp/src/main/styles/index.css index 274ad20a..aecfb8fb 100644 --- a/modules/webapp/src/main/styles/index.css +++ b/modules/webapp/src/main/styles/index.css @@ -18,7 +18,14 @@ } .elm-datepicker--input { - @apply pl-10 placeholder-gray-400 bg-blue-50 dark:text-bluegray-200 dark:bg-bluegray-700 dark:border-bluegray-500 border-gray-500 rounded w-full; + @apply pl-10 rounded w-full placeholder-gray-400 dark:text-bluegray-200 dark:border-bluegray-500; + } + #sidebar .elm-datepicker--input { + @apply dark:bg-bluegray-700 border-gray-500 bg-blue-50; + } + + #content .elm-datepicker--input { + @apply dark:bg-bluegray-800 border-gray-400; } .elm-datepicker--container { From f5bb85c61eb68257e111f6560cf4fd224ba7e2e9 Mon Sep 17 00:00:00 2001 From: eikek Date: Sun, 24 Oct 2021 00:37:53 +0200 Subject: [PATCH 33/37] Improve share email form --- modules/webapp/src/main/elm/Comp/ItemMail.elm | 10 ++++++ .../webapp/src/main/elm/Comp/PublishItems.elm | 35 ++++++++++++++++--- .../webapp/src/main/elm/Comp/ShareMail.elm | 10 ++++-- .../webapp/src/main/elm/Comp/ShareManage.elm | 26 +++++++++++--- .../src/main/elm/Messages/Comp/ShareMail.elm | 3 ++ .../main/elm/Messages/Comp/ShareManage.elm | 6 ++-- 6 files changed, 74 insertions(+), 16 deletions(-) diff --git a/modules/webapp/src/main/elm/Comp/ItemMail.elm b/modules/webapp/src/main/elm/Comp/ItemMail.elm index 540e65c0..617325a5 100644 --- a/modules/webapp/src/main/elm/Comp/ItemMail.elm +++ b/modules/webapp/src/main/elm/Comp/ItemMail.elm @@ -10,6 +10,7 @@ module Comp.ItemMail exposing , Model , Msg , clear + , clearRecipients , emptyModel , init , setMailInfo @@ -115,6 +116,15 @@ clear model = } +clearRecipients : Model -> Model +clearRecipients model = + { model + | recipients = [] + , ccRecipients = [] + , bccRecipients = [] + } + + setMailInfo : String -> String -> Msg setMailInfo subject body = SetSubjectBody subject body diff --git a/modules/webapp/src/main/elm/Comp/PublishItems.elm b/modules/webapp/src/main/elm/Comp/PublishItems.elm index 98d18084..5679f80a 100644 --- a/modules/webapp/src/main/elm/Comp/PublishItems.elm +++ b/modules/webapp/src/main/elm/Comp/PublishItems.elm @@ -29,6 +29,7 @@ import Data.ItemQuery exposing (ItemQuery) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick) import Http import Messages.Comp.PublishItems exposing (Texts) import Ports @@ -57,6 +58,7 @@ type alias Model = , viewMode : ViewMode , formError : FormError , loading : Bool + , mailVisible : Bool } @@ -74,6 +76,7 @@ init flags = , viewMode = ViewModeEdit , formError = FormErrorNone , loading = False + , mailVisible = False } , Cmd.batch [ Cmd.map FormMsg fc @@ -96,6 +99,7 @@ initQuery flags query = , viewMode = ViewModeEdit , formError = FormErrorNone , loading = False + , mailVisible = False } , Cmd.batch [ Cmd.map FormMsg fc @@ -115,6 +119,7 @@ type Msg | SubmitPublish | PublishResp (Result Http.Error IdResult) | GetShareResp (Result Http.Error ShareDetail) + | ToggleMailVisible type Outcome @@ -210,6 +215,7 @@ update texts flags msg model = | formError = FormErrorNone , loading = False , viewMode = ViewModeInfo share + , mailVisible = False , mailModel = mm } , cmd = @@ -228,6 +234,13 @@ update texts flags msg model = , outcome = OutcomeInProgress } + ToggleMailVisible -> + { model = { model | mailVisible = not model.mailVisible } + , cmd = Cmd.none + , sub = Sub.none + , outcome = OutcomeInProgress + } + --- View @@ -281,14 +294,26 @@ viewInfo texts settings flags model share = , div [] [ Comp.ShareView.view cfg texts.shareView flags share ] - , div [ class "flex flex-col mt-6" ] - [ div + , div + [ class "flex flex-col mt-6" + ] + [ a [ class S.header2 + , class "inline-block w-full" + , href "#" + , onClick ToggleMailVisible ] - [ text texts.sendViaMail + [ if model.mailVisible then + i [ class "fa fa-caret-down mr-2" ] [] + + else + i [ class "fa fa-caret-right mr-2" ] [] + , text texts.sendViaMail + ] + , div [ classList [ ( "hidden", not model.mailVisible ) ] ] + [ Html.map MailMsg + (Comp.ShareMail.view texts.shareMail flags settings model.mailModel) ] - , Html.map MailMsg - (Comp.ShareMail.view texts.shareMail flags settings model.mailModel) ] ] diff --git a/modules/webapp/src/main/elm/Comp/ShareMail.elm b/modules/webapp/src/main/elm/Comp/ShareMail.elm index f28f2d70..79701eb3 100644 --- a/modules/webapp/src/main/elm/Comp/ShareMail.elm +++ b/modules/webapp/src/main/elm/Comp/ShareMail.elm @@ -120,7 +120,11 @@ update texts flags msg model = (texts.bodyTemplate url) nm = - { model | share = share } + { model + | share = share + , mailModel = Comp.ItemMail.clearRecipients model.mailModel + , formState = FormStateNone + } in update texts flags (MailMsg lm) nm @@ -128,7 +132,7 @@ update texts flags msg model = if res.success then ( { model | formState = FormStateSent - , mailModel = Comp.ItemMail.clear model.mailModel + , mailModel = Comp.ItemMail.clearRecipients model.mailModel , sending = False } , Cmd.none @@ -176,7 +180,7 @@ view texts flags settings model = FormStateSent -> div [ class S.successMessage ] - [ text "Mail sent." + [ text texts.mailSent ] , Html.map MailMsg (Comp.ItemMail.view texts.itemMail settings cfg model.mailModel) diff --git a/modules/webapp/src/main/elm/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Comp/ShareManage.elm index e1c33def..19bdce86 100644 --- a/modules/webapp/src/main/elm/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Comp/ShareManage.elm @@ -58,6 +58,7 @@ type alias Model = , deleteConfirm : DeleteConfirm , query : String , owningOnly : Bool + , sendMailVisible : Bool } @@ -79,6 +80,7 @@ init flags = , deleteConfirm = DeleteConfirmOff , query = "" , owningOnly = True + , sendMailVisible = False } , Cmd.batch [ Cmd.map FormMsg fc @@ -96,6 +98,7 @@ type Msg | SetViewMode ViewMode | SetQuery String | ToggleOwningOnly + | ToggleSendMailVisible | Submit | RequestDelete | CancelDelete @@ -260,6 +263,9 @@ update texts flags msg model = , Sub.none ) + ToggleSendMailVisible -> + ( { model | sendMailVisible = not model.sendMailVisible }, Cmd.none, Sub.none ) + setShare : Texts -> ShareDetail -> Flags -> Model -> ( Model, Cmd Msg, Sub Msg ) setShare texts share flags model = @@ -268,7 +274,7 @@ setShare texts share flags model = flags.config.baseUrl ++ Page.pageToString (SharePage share.id) nextModel = - { model | formError = FormErrorNone, viewMode = Form, loading = False } + { model | formError = FormErrorNone, viewMode = Form, loading = False, sendMailVisible = False } initClipboard = Ports.initClipboard (Comp.ShareView.clipboardData share) @@ -490,13 +496,23 @@ shareSendMail texts flags settings model = [ class "mt-8 mb-2" , classList [ ( "hidden", share.id == "" || not share.enabled || share.expired ) ] ] - [ h2 + [ a [ class S.header2 - , class "border-b-2 dark:border-bluegray-600" + , class "border-b-2 dark:border-bluegray-600 w-full inline-block" + , href "#" + , onClick ToggleSendMailVisible ] - [ text "Send via E-Mail" + [ if model.sendMailVisible then + i [ class "fa fa-caret-down mr-2" ] [] + + else + i [ class "fa fa-caret-right mr-2" ] [] + , text texts.sendViaMail + ] + , div + [ class "px-2 py-2 dark:border-bluegray-600" + , classList [ ( "hidden", not model.sendMailVisible ) ] ] - , div [ class "px-2 py-2 dark:border-bluegray-600" ] [ Html.map MailMsg (Comp.ShareMail.view texts.shareMail flags settings model.mailModel) ] diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareMail.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareMail.elm index 8b56bbb7..04d49ba1 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ShareMail.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareMail.elm @@ -23,6 +23,7 @@ type alias Texts = , httpError : Http.Error -> String , subjectTemplate : Maybe String -> String , bodyTemplate : String -> String + , mailSent : String } @@ -40,6 +41,7 @@ you can find the documents here: Kind regards """ + , mailSent = "Mail sent." } @@ -57,4 +59,5 @@ die freigegebenen Dokumente befinden sich hier: Freundliche Grüße """ + , mailSent = "E-Mail gesendet." } diff --git a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm index 773ea5b3..9de0104e 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ShareManage.elm @@ -38,7 +38,7 @@ type alias Texts = , correctFormErrors : String , noName : String , shareInformation : String - , sendMail : String + , sendViaMail : String , notOwnerInfo : String , showOwningSharesOnly : String } @@ -63,7 +63,7 @@ gb = , correctFormErrors = "Please correct the errors in the form." , noName = "No Name" , shareInformation = "Share Information" - , sendMail = "Send via E-Mail" + , sendViaMail = "Send via E-Mail" , notOwnerInfo = "Only the user who created this share can edit its properties." , showOwningSharesOnly = "Show my shares only" } @@ -88,7 +88,7 @@ de = , correctFormErrors = "Bitte korrigiere die Fehler im Formular." , noName = "Ohne Name" , shareInformation = "Informationen zur Freigabe" - , sendMail = "Per E-Mail versenden" + , sendViaMail = "Per E-Mail versenden" , notOwnerInfo = "Nur der Benutzer, der diese Freigabe erstellt hat, kann diese auch ändern." , showOwningSharesOnly = "Nur meine Freigaben anzeigen" } From 28993e27e5ecedfaf78effdcf44aa7765c262e59 Mon Sep 17 00:00:00 2001 From: eikek Date: Sun, 24 Oct 2021 00:53:56 +0200 Subject: [PATCH 34/37] Dropdown cc and bcc recipients in mail form --- modules/webapp/src/main/elm/Comp/ItemMail.elm | 29 +++++++++++++++++-- .../src/main/elm/Messages/Comp/ItemMail.elm | 6 ++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/modules/webapp/src/main/elm/Comp/ItemMail.elm b/modules/webapp/src/main/elm/Comp/ItemMail.elm index 617325a5..e7a47b80 100644 --- a/modules/webapp/src/main/elm/Comp/ItemMail.elm +++ b/modules/webapp/src/main/elm/Comp/ItemMail.elm @@ -49,6 +49,7 @@ type alias Model = , body : String , attachAll : Bool , formError : FormError + , showCC : Bool } @@ -65,6 +66,7 @@ type Msg | BCCRecipientMsg Comp.EmailInput.Msg | SetBody String | SetSubjectBody String String + | ToggleShowCC | ConnMsg (Comp.Dropdown.Msg String) | ConnResp (Result Http.Error EmailSettingsList) | ToggleAttachAll @@ -97,6 +99,7 @@ emptyModel = , body = "" , attachAll = True , formError = FormErrorNone + , showCC = False } @@ -189,6 +192,9 @@ update flags msg model = ToggleAttachAll -> ( { model | attachAll = not model.attachAll }, Cmd.none, FormNone ) + ToggleShowCC -> + ( { model | showCC = not model.showCC }, Cmd.none, FormNone ) + ConnResp (Ok list) -> let names = @@ -324,9 +330,22 @@ view texts settings cfg model = , div [ class "mb-4" ] [ label [ class S.inputLabel + , class "flex flex-row" ] [ text texts.recipients , B.inputRequired + , a + [ class S.link + , class "justify-end flex flex-grow" + , onClick ToggleShowCC + , href "#" + ] + [ if model.showCC then + text texts.lessRecipients + + else + text texts.moreRecipients + ] ] , Html.map RecipientMsg (Comp.EmailInput.view2 { style = dds, placeholder = appendDots texts.recipients } @@ -334,7 +353,10 @@ view texts settings cfg model = model.recipientsModel ) ] - , div [ class "mb-4" ] + , div + [ class "mb-4" + , classList [ ( "hidden", not model.showCC ) ] + ] [ label [ class S.inputLabel ] [ text texts.ccRecipients ] @@ -344,7 +366,10 @@ view texts settings cfg model = model.ccRecipientsModel ) ] - , div [ class "mb-4" ] + , div + [ class "mb-4" + , classList [ ( "hidden", not model.showCC ) ] + ] [ label [ class S.inputLabel ] [ text texts.bccRecipients ] diff --git a/modules/webapp/src/main/elm/Messages/Comp/ItemMail.elm b/modules/webapp/src/main/elm/Messages/Comp/ItemMail.elm index c3ad7854..f50885bd 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/ItemMail.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/ItemMail.elm @@ -29,6 +29,8 @@ type alias Texts = , includeAllAttachments : String , connectionMissing : String , sendLabel : String + , moreRecipients : String + , lessRecipients : String } @@ -46,6 +48,8 @@ gb = , includeAllAttachments = "Include all item attachments" , connectionMissing = "No E-Mail connections configured. Goto user settings to add one." , sendLabel = "Send" + , moreRecipients = "More…" + , lessRecipients = "Less…" } @@ -63,4 +67,6 @@ de = , includeAllAttachments = "Alle Anhänge mit einfügen" , connectionMissing = "Keine E-Mail-Verbindung definiert. Gehe zu den Benutzereinstellungen und füge eine hinzu." , sendLabel = "Senden" + , moreRecipients = "Weitere…" + , lessRecipients = "Weniger…" } From 99f3be9c0ddc2a8c8d4183857d558e5da8ad7de6 Mon Sep 17 00:00:00 2001 From: eikek Date: Sun, 24 Oct 2021 01:48:14 +0200 Subject: [PATCH 35/37] Allow simple search on share page --- .../src/main/elm/Messages/Page/Share.elm | 9 ++ .../webapp/src/main/elm/Page/Share/Data.elm | 15 +++- .../src/main/elm/Page/Share/Menubar.elm | 85 +++++++++++++++---- .../webapp/src/main/elm/Page/Share/Update.elm | 41 ++++++++- 4 files changed, 128 insertions(+), 22 deletions(-) diff --git a/modules/webapp/src/main/elm/Messages/Page/Share.elm b/modules/webapp/src/main/elm/Messages/Page/Share.elm index 20884777..53061b7d 100644 --- a/modules/webapp/src/main/elm/Messages/Page/Share.elm +++ b/modules/webapp/src/main/elm/Messages/Page/Share.elm @@ -22,6 +22,9 @@ type alias Texts = , passwordForm : Messages.Comp.SharePasswordForm.Texts , httpError : Http.Error -> String , authFailed : String + , fulltextPlaceholder : String + , powerSearchPlaceholder : String + , extendedSearch : String } @@ -33,6 +36,9 @@ gb = , passwordForm = Messages.Comp.SharePasswordForm.gb , authFailed = "This share does not exist." , httpError = Messages.Comp.HttpError.gb + , fulltextPlaceholder = "Fulltext search…" + , powerSearchPlaceholder = "Extended search…" + , extendedSearch = "Extended search query" } @@ -44,4 +50,7 @@ de = , passwordForm = Messages.Comp.SharePasswordForm.de , authFailed = "Diese Freigabe existiert nicht." , httpError = Messages.Comp.HttpError.de + , fulltextPlaceholder = "Volltextsuche…" + , powerSearchPlaceholder = "Erweiterte Suche…" + , extendedSearch = "Erweiterte Suchanfrage" } diff --git a/modules/webapp/src/main/elm/Page/Share/Data.elm b/modules/webapp/src/main/elm/Page/Share/Data.elm index 505f4908..bea33497 100644 --- a/modules/webapp/src/main/elm/Page/Share/Data.elm +++ b/modules/webapp/src/main/elm/Page/Share/Data.elm @@ -5,7 +5,7 @@ -} -module Page.Share.Data exposing (Mode(..), Model, Msg(..), PageError(..), init, initCmd) +module Page.Share.Data exposing (Mode(..), Model, Msg(..), PageError(..), SearchBarMode(..), init, initCmd) import Api import Api.Model.ItemLightList exposing (ItemLightList) @@ -18,6 +18,7 @@ import Comp.SearchMenu import Comp.SharePasswordForm import Data.Flags exposing (Flags) import Http +import Util.Html exposing (KeyCode) type Mode @@ -32,6 +33,11 @@ type PageError | PageErrorAuthFail +type SearchBarMode + = SearchBarNormal + | SearchBarContent + + type alias Model = { mode : Mode , verifyResult : ShareVerifyResult @@ -42,6 +48,8 @@ type alias Model = , searchInProgress : Bool , itemListModel : Comp.ItemCardList.Model , initialized : Bool + , contentSearch : Maybe String + , searchMode : SearchBarMode } @@ -56,6 +64,8 @@ emptyModel flags = , searchInProgress = False , itemListModel = Comp.ItemCardList.init , initialized = False + , contentSearch = Nothing + , searchMode = SearchBarContent } @@ -87,3 +97,6 @@ type Msg | PowerSearchMsg Comp.PowerSearchInput.Msg | ResetSearch | ItemListMsg Comp.ItemCardList.Msg + | ToggleSearchBar + | SetContentSearch String + | ContentSearchKey (Maybe KeyCode) diff --git a/modules/webapp/src/main/elm/Page/Share/Menubar.elm b/modules/webapp/src/main/elm/Page/Share/Menubar.elm index eb490c37..1bfcc08c 100644 --- a/modules/webapp/src/main/elm/Page/Share/Menubar.elm +++ b/modules/webapp/src/main/elm/Page/Share/Menubar.elm @@ -13,10 +13,11 @@ import Comp.PowerSearchInput import Comp.SearchMenu import Html exposing (..) import Html.Attributes exposing (..) -import Html.Events exposing (onClick) +import Html.Events exposing (onClick, onInput) import Messages.Page.Share exposing (Texts) -import Page.Share.Data exposing (Model, Msg(..)) +import Page.Share.Data exposing (Model, Msg(..), SearchBarMode(..)) import Styles as S +import Util.Html view : Texts -> Model -> Html Msg @@ -30,21 +31,73 @@ view texts model = model.searchMenuModel.textSearchModel powerSearchBar = - div - [ class "relative flex flex-grow flex-row" ] - [ Html.map PowerSearchMsg - (Comp.PowerSearchInput.viewInput - { placeholder = texts.basics.searchPlaceholder - , extraAttrs = [] - } - model.powerSearchInput - ) - , Html.map PowerSearchMsg - (Comp.PowerSearchInput.viewResult [] model.powerSearchInput) + div [ class "flex-grow flex flex-col relative" ] + [ div + [ class "relative flex flex-grow flex-row" ] + [ Html.map PowerSearchMsg + (Comp.PowerSearchInput.viewInput + { placeholder = texts.powerSearchPlaceholder + , extraAttrs = [] + } + model.powerSearchInput + ) + , Html.map PowerSearchMsg + (Comp.PowerSearchInput.viewResult [] model.powerSearchInput) + ] + , div + [ class "opacity-60 text-xs -mt-1.5 absolute -bottom-4" + ] + [ text "Use an " + , a + [ href "https://docspell.org/docs/query/#structure" + , target "_new" + , class S.link + , class "mx-1" + ] + [ i [ class "fa fa-external-link-alt mr-1" ] [] + , text "extended search" + ] + , text " syntax." + ] + ] + + contentSearchBar = + div [ class "flex flex-grow" ] + [ input + [ type_ "text" + , class S.textInput + , class "text-sm" + , placeholder texts.fulltextPlaceholder + , onInput SetContentSearch + , value (Maybe.withDefault "" model.contentSearch) + , Util.Html.onKeyUpCode ContentSearchKey + ] + [] ] in MB.view - { end = + { start = + [ MB.CustomElement <| + case model.searchMode of + SearchBarContent -> + contentSearchBar + + SearchBarNormal -> + powerSearchBar + , MB.CustomElement <| + B.secondaryBasicButton + { label = "" + , icon = "fa fa-search-plus" + , disabled = False + , handler = onClick ToggleSearchBar + , attrs = + [ href "#" + , title texts.extendedSearch + , classList [ ( "bg-gray-200 dark:bg-bluegray-600", model.searchMode == SearchBarNormal ) ] + ] + } + ] + , end = [ MB.CustomElement <| B.secondaryBasicButton { label = "" @@ -59,9 +112,5 @@ view texts model = , attrs = [ href "#" ] } ] - , start = - [ MB.CustomElement <| - powerSearchBar - ] , rootClasses = "mb-2 pt-1 dark:bg-bluegray-700 items-center text-sm" } diff --git a/modules/webapp/src/main/elm/Page/Share/Update.elm b/modules/webapp/src/main/elm/Page/Share/Update.elm index c37a9776..e7f6c852 100644 --- a/modules/webapp/src/main/elm/Page/Share/Update.elm +++ b/modules/webapp/src/main/elm/Page/Share/Update.elm @@ -19,6 +19,8 @@ import Data.ItemQuery as Q import Data.SearchMode import Data.UiSettings exposing (UiSettings) import Page.Share.Data exposing (..) +import Util.Html +import Util.Maybe import Util.Update @@ -74,7 +76,7 @@ update flags settings shareId msg model = settings shareId (ItemListMsg (Comp.ItemCardList.SetResults list)) - { model | searchInProgress = False } + { model | searchInProgress = False, pageError = PageErrorNone } SearchResp (Err err) -> noSub ( { model | pageError = PageErrorHttp err, searchInProgress = False }, Cmd.none ) @@ -149,7 +151,11 @@ update flags settings shareId msg model = ResetSearch -> let nm = - { model | powerSearchInput = Comp.PowerSearchInput.init } + { model + | powerSearchInput = Comp.PowerSearchInput.init + , contentSearch = Nothing + , pageError = PageErrorNone + } in update flags settings shareId (SearchMenuMsg Comp.SearchMenu.ResetForm) nm @@ -167,6 +173,29 @@ update flags settings shareId msg model = , Cmd.batch [ Cmd.map ItemListMsg ic, searchMsg ] ) + ToggleSearchBar -> + noSub + ( { model + | searchMode = + case model.searchMode of + SearchBarContent -> + SearchBarNormal + + SearchBarNormal -> + SearchBarContent + } + , Cmd.none + ) + + SetContentSearch q -> + noSub ( { model | contentSearch = Util.Maybe.fromString q }, Cmd.none ) + + ContentSearchKey (Just Util.Html.Enter) -> + noSub ( model, makeSearchCmd flags model ) + + ContentSearchKey _ -> + noSub ( model, Cmd.none ) + noSub : ( Model, Cmd Msg ) -> UpdateResult noSub ( m, c ) = @@ -179,7 +208,13 @@ makeSearchCmd flags model = xq = Q.and [ Comp.SearchMenu.getItemQuery model.searchMenuModel - , Maybe.map Q.Fragment model.powerSearchInput.input + , Maybe.map Q.Fragment <| + case model.searchMode of + SearchBarNormal -> + model.powerSearchInput.input + + SearchBarContent -> + Maybe.map (Q.Contents >> Q.render) model.contentSearch ] request mq = From 208f7e644533f9f11ccc605b56211a4b7ea42b64 Mon Sep 17 00:00:00 2001 From: eikek Date: Sun, 24 Oct 2021 12:53:41 +0200 Subject: [PATCH 36/37] Update npm packages --- modules/webapp/package-lock.json | 1167 ++++++++++++------------------ modules/webapp/package.json | 16 +- 2 files changed, 462 insertions(+), 721 deletions(-) diff --git a/modules/webapp/package-lock.json b/modules/webapp/package-lock.json index 4801d04f..bcd14273 100644 --- a/modules/webapp/package-lock.json +++ b/modules/webapp/package-lock.json @@ -5,68 +5,114 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", "requires": { - "@babel/highlight": "^7.10.4" + "@babel/highlight": "^7.14.5" } }, "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==" }, "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.14.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@fortawesome/fontawesome-free": { - "version": "5.15.3", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz", - "integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==" + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", + "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==" }, "@nodelib/fs.scandir": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", - "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "requires": { - "@nodelib/fs.stat": "2.0.4", + "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "@nodelib/fs.stat": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", - "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" }, "@nodelib/fs.walk": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", - "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "requires": { - "@nodelib/fs.scandir": "2.1.4", + "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "@tailwindcss/forms": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.2.tgz", - "integrity": "sha512-aj2/rJsGb2whAZ/BQWHWWQRSbhH0r/l1ozOByiv+ZNjBD84GMvb5dhAyfpeasFky+EJrAwX5eaqft8NQMZFWvA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.4.tgz", + "integrity": "sha512-vlAoBifNJUkagB+PAdW4aHMe4pKmSLroH398UPgIogBFc91D2VlHUxe4pjxQhiJl0Nfw53sHSJSQBSTQBZP3vA==", "requires": { "mini-svg-data-uri": "^1.2.3" } }, "@trysound/sax": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.1.1.tgz", - "integrity": "sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow==" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" }, "@types/parse-json": { "version": "4.0.0", @@ -104,17 +150,17 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -131,45 +177,16 @@ "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==" }, "autoprefixer": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.5.tgz", - "integrity": "sha512-7H4AJZXvSsn62SqZyJCP+1AWwOuoYpUfK6ot9vm0e87XD6mT8lDywc9D9OTJPMULyGcvmIxzTAMeG2Cc+YX+fA==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.7.tgz", + "integrity": "sha512-EmGpu0nnQVmMhX8ROoJ7Mx8mKYPlcUHuxkwrRYEYMz85lu7H09v8w6R1P0JPdn/hKU32GjpLBFEOuIlDWCRWvg==", "requires": { - "browserslist": "^4.16.3", - "caniuse-lite": "^1.0.30001196", - "colorette": "^1.2.2", - "fraction.js": "^4.0.13", + "browserslist": "^4.17.3", + "caniuse-lite": "^1.0.30001264", + "fraction.js": "^4.1.1", "normalize-range": "^0.1.2", + "picocolors": "^0.2.1", "postcss-value-parser": "^4.1.0" - }, - "dependencies": { - "browserslist": { - "version": "4.16.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.5.tgz", - "integrity": "sha512-C2HAjrM1AI/djrpAUU/tr4pml1DqLIzJKSLDBXBrNErl9ZCCTXdhwxdJjYc16953+mBWf7Lw+uUJgpgb8cN71A==", - "requires": { - "caniuse-lite": "^1.0.30001214", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.719", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" - } - }, - "colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" - }, - "electron-to-chromium": { - "version": "1.3.739", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.739.tgz", - "integrity": "sha512-+LPJVRsN7hGZ9EIUUiWCpO7l4E3qBYHNadazlucBfsXBbccDFNKUBAgzE68FnkWGJPwD/AfKhSzL+G+Iqb8A4A==" - }, - "node-releases": { - "version": "1.1.72", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", - "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==" - } } }, "balanced-match": { @@ -205,31 +222,21 @@ } }, "browserslist": { - "version": "4.16.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.5.tgz", - "integrity": "sha512-C2HAjrM1AI/djrpAUU/tr4pml1DqLIzJKSLDBXBrNErl9ZCCTXdhwxdJjYc16953+mBWf7Lw+uUJgpgb8cN71A==", + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.5.tgz", + "integrity": "sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA==", "requires": { - "caniuse-lite": "^1.0.30001214", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.719", + "caniuse-lite": "^1.0.30001271", + "electron-to-chromium": "^1.3.878", "escalade": "^3.1.1", - "node-releases": "^1.1.71" + "node-releases": "^2.0.1", + "picocolors": "^1.0.0" }, "dependencies": { - "colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" - }, - "electron-to-chromium": { - "version": "1.3.739", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.739.tgz", - "integrity": "sha512-+LPJVRsN7hGZ9EIUUiWCpO7l4E3qBYHNadazlucBfsXBbccDFNKUBAgzE68FnkWGJPwD/AfKhSzL+G+Iqb8A4A==" - }, - "node-releases": { - "version": "1.1.72", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", - "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==" + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" } } }, @@ -238,6 +245,11 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, "camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -260,38 +272,27 @@ "integrity": "sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==" }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, "chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", "requires": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.3.1", - "glob-parent": "~5.1.0", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" + "readdirp": "~3.6.0" } }, "cliui": { @@ -305,45 +306,45 @@ } }, "color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", - "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/color/-/color-4.0.1.tgz", + "integrity": "sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==", "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.4" + "color-convert": "^2.0.1", + "color-string": "^1.6.0" } }, "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "color-string": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", - "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", + "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", "requires": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, - "colorette": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.10.tgz", - "integrity": "sha512-tQ7Prvd+zU6EUsQVXJNqxJUZdZ4btI34jlp/W1N/bNyWsY55dxe2oSg+ss3COV4vKbWLVlwLJt5xMSCuZ3R4og==" + "colord": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.1.tgz", + "integrity": "sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw==" }, "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" }, "concat-map": { "version": "0.0.1", @@ -351,9 +352,9 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "cosmiconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", - "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", "requires": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -368,22 +369,22 @@ "integrity": "sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==" }, "css-declaration-sorter": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.0.0.tgz", - "integrity": "sha512-S0TE4E0ha5+tBHdLWPc5n+S8E4dFBS5xScPvgHkLNZwWvX4ISoFGhGeerLC9uS1cKA/sC+K2wHq6qEbcagT/fg==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.3.tgz", + "integrity": "sha512-SvjQjNRZgh4ULK1LDJ2AduPKUKxIqmtU7ZAyi47BTV+M90Qvxr9AB6lKlLbDUfXqI9IQeYA8LbAsCZPpJEV3aA==", "requires": { "timsort": "^0.3.0" } }, "css-select": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz", - "integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", "requires": { "boolbase": "^1.0.0", - "css-what": "^4.0.0", - "domhandler": "^4.0.0", - "domutils": "^2.4.3", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", "nth-check": "^2.0.0" } }, @@ -402,9 +403,9 @@ "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" }, "css-what": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz", - "integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==" + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", + "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==" }, "cssesc": { "version": "3.0.0", @@ -412,56 +413,56 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" }, "cssnano": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.0.tgz", - "integrity": "sha512-beaqJEU9aI0B2PpKWXy+UJdtw+Q2J2c2f2nHVphL/gb2wvkuQV+Zxf5Q5SsNXiPUb9Djo/+ja+UOelQWhHnVow==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.8.tgz", + "integrity": "sha512-Lda7geZU0Yu+RZi2SGpjYuQz4HI4/1Y+BhdD0jL7NXAQ5larCzVn+PUGuZbDMYz904AXXCOgO5L1teSvgu7aFg==", "requires": { - "cosmiconfig": "^7.0.0", - "cssnano-preset-default": "^5.0.0", + "cssnano-preset-default": "^5.1.4", "is-resolvable": "^1.1.0", - "opencollective-postinstall": "^2.0.2" + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" } }, "cssnano-preset-default": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.0.0.tgz", - "integrity": "sha512-zsLppqF7PxY6Tk+ghVx8djf4o1jIOu2GNufqy9lMxldt7gGpSy3FQ6jn7FCd5DZWCaBa7A/1/HVh8CK3BdFSJg==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.4.tgz", + "integrity": "sha512-sPpQNDQBI3R/QsYxQvfB4mXeEcWuw0wGtKtmS5eg8wudyStYMgKOQT39G07EbW1LB56AOYrinRS9f0ig4Y3MhQ==", "requires": { - "css-declaration-sorter": "6.0.0", - "cssnano-utils": "^2.0.0", + "css-declaration-sorter": "^6.0.3", + "cssnano-utils": "^2.0.1", "postcss-calc": "^8.0.0", - "postcss-colormin": "^5.0.0", - "postcss-convert-values": "^5.0.0", - "postcss-discard-comments": "^5.0.0", - "postcss-discard-duplicates": "^5.0.0", - "postcss-discard-empty": "^5.0.0", - "postcss-discard-overridden": "^5.0.0", - "postcss-merge-longhand": "^5.0.0", - "postcss-merge-rules": "^5.0.0", - "postcss-minify-font-values": "^5.0.0", - "postcss-minify-gradients": "^5.0.0", - "postcss-minify-params": "^5.0.0", - "postcss-minify-selectors": "^5.0.0", - "postcss-normalize-charset": "^5.0.0", - "postcss-normalize-display-values": "^5.0.0", - "postcss-normalize-positions": "^5.0.0", - "postcss-normalize-repeat-style": "^5.0.0", - "postcss-normalize-string": "^5.0.0", - "postcss-normalize-timing-functions": "^5.0.0", - "postcss-normalize-unicode": "^5.0.0", - "postcss-normalize-url": "^5.0.0", - "postcss-normalize-whitespace": "^5.0.0", - "postcss-ordered-values": "^5.0.0", - "postcss-reduce-initial": "^5.0.0", - "postcss-reduce-transforms": "^5.0.0", - "postcss-svgo": "^5.0.0", - "postcss-unique-selectors": "^5.0.0" + "postcss-colormin": "^5.2.0", + "postcss-convert-values": "^5.0.1", + "postcss-discard-comments": "^5.0.1", + "postcss-discard-duplicates": "^5.0.1", + "postcss-discard-empty": "^5.0.1", + "postcss-discard-overridden": "^5.0.1", + "postcss-merge-longhand": "^5.0.2", + "postcss-merge-rules": "^5.0.2", + "postcss-minify-font-values": "^5.0.1", + "postcss-minify-gradients": "^5.0.2", + "postcss-minify-params": "^5.0.1", + "postcss-minify-selectors": "^5.1.0", + "postcss-normalize-charset": "^5.0.1", + "postcss-normalize-display-values": "^5.0.1", + "postcss-normalize-positions": "^5.0.1", + "postcss-normalize-repeat-style": "^5.0.1", + "postcss-normalize-string": "^5.0.1", + "postcss-normalize-timing-functions": "^5.0.1", + "postcss-normalize-unicode": "^5.0.1", + "postcss-normalize-url": "^5.0.2", + "postcss-normalize-whitespace": "^5.0.1", + "postcss-ordered-values": "^5.0.2", + "postcss-reduce-initial": "^5.0.1", + "postcss-reduce-transforms": "^5.0.1", + "postcss-svgo": "^5.0.2", + "postcss-unique-selectors": "^5.0.1" } }, "cssnano-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.0.tgz", - "integrity": "sha512-xvxmTszdrvSyTACdPe8VU5J6p4sm3egpgw54dILvNqt5eBUv6TFjACLhSxtRuEsxYrgy8uDy269YjScO5aKbGA==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", + "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==" }, "csso": { "version": "4.2.0", @@ -510,12 +511,12 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, "dom-serializer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.1.tgz", - "integrity": "sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", "requires": { "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", + "domhandler": "^4.2.0", "entities": "^2.0.0" } }, @@ -525,30 +526,27 @@ "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" }, "domhandler": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.1.0.tgz", - "integrity": "sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", + "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", "requires": { "domelementtype": "^2.2.0" } }, "domutils": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.5.2.tgz", - "integrity": "sha512-MHTthCb1zj8f1GVfRpeZUbohQf/HdBos0oX5gZcQFepOZPLLRyj6Wn7XS7EMnY7CVpwv8863u2vyE83Hfu28HQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", "requires": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", - "domhandler": "^4.1.0" + "domhandler": "^4.2.0" } }, - "dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "requires": { - "is-obj": "^2.0.0" - } + "electron-to-chromium": { + "version": "1.3.878", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.878.tgz", + "integrity": "sha512-O6yxWCN9ph2AdspAIszBnd9v8s11hQx8ub9w4UGApzmNRnoKhbulOWqbO8THEQec/aEHtvy+donHZMlh6l1rbA==" }, "emoji-regex": { "version": "8.0.0", @@ -566,6 +564,13 @@ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "requires": { "is-arrayish": "^0.2.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + } } }, "escalade": { @@ -591,9 +596,9 @@ } }, "fastq": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.1.tgz", - "integrity": "sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", "requires": { "reusify": "^1.0.4" } @@ -612,9 +617,9 @@ "integrity": "sha512-pgJnJLrtb0tcDgU1fzGaQXmR8h++nXvILJ+r5SmOXaaL/2pocunQo2a8TAXhjQnBpRLPtZ1KCz/TYpqeNuE2ew==" }, "fraction.js": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.13.tgz", - "integrity": "sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", + "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==" }, "fs-extra": { "version": "10.0.0", @@ -632,9 +637,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", - "integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "optional": true }, "function-bind": { @@ -684,40 +689,12 @@ "ignore": "^5.1.8", "merge2": "^1.4.1", "slash": "^4.0.0" - }, - "dependencies": { - "fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" - } } }, "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "has": { "version": "1.0.3", @@ -728,9 +705,9 @@ } }, "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "hex-color-regex": { "version": "1.1.0", @@ -772,6 +749,13 @@ "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + } } }, "import-from": { @@ -780,20 +764,8 @@ "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", "requires": { "resolve-from": "^5.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" - } } }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -814,9 +786,9 @@ "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==" }, "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "is-binary-path": { "version": "2.1.0", @@ -847,9 +819,9 @@ } }, "is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "requires": { "has": "^1.0.3" } @@ -865,9 +837,9 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "requires": { "is-extglob": "^2.1.1" } @@ -877,11 +849,6 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" - }, "is-resolvable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", @@ -978,19 +945,12 @@ "requires": { "braces": "^3.0.1", "picomatch": "^2.2.3" - }, - "dependencies": { - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" - } } }, "mini-svg-data-uri": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.2.3.tgz", - "integrity": "sha512-zd6KCAyXgmq6FV1mR10oKXYtvmA9vRoB6xPSTUJTbFApCtkefDnYueVR1gkof3KcdLZo1Y8mjF2DFmQMIxsHNQ==" + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.3.tgz", + "integrity": "sha512-gSfqpMRC8IxghvMcxzzmMnWpXAChSA+vy4cia33RgerMS8Fex95akUyQZPbxJJmeBGiGmK7n/1OpUX8ksRjIdA==" }, "minimatch": { "version": "3.0.4", @@ -1010,10 +970,15 @@ "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==" }, + "nanocolors": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.1.12.tgz", + "integrity": "sha512-2nMHqg1x5PU+unxX7PGY7AuYxl2qDx7PSrTRjizr8sxdd3l/3hBuWWaki62qmtYm2U5i4Z5E7GbjlyDFhs9/EQ==" + }, "nanoid": { - "version": "3.1.22", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", - "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==" + "version": "3.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==" }, "node-emoji": { "version": "1.11.0", @@ -1023,6 +988,11 @@ "lodash": "^4.17.21" } }, + "node-releases": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==" + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1034,9 +1004,9 @@ "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" }, "normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==" + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" }, "nth-check": { "version": "2.0.1", @@ -1059,24 +1029,12 @@ "wrappy": "1" } }, - "opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==" - }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "requires": { "callsites": "^3.0.0" - }, - "dependencies": { - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - } } }, "parse-json": { @@ -1105,10 +1063,15 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + }, "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" }, "pify": { "version": "2.3.0", @@ -1116,19 +1079,19 @@ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" }, "postcss": { - "version": "8.2.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.10.tgz", - "integrity": "sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==", + "version": "8.3.11", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz", + "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==", "requires": { - "colorette": "^1.2.2", - "nanoid": "^3.1.22", - "source-map": "^0.6.1" + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^0.6.2" }, "dependencies": { - "colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" } } }, @@ -1142,16 +1105,16 @@ } }, "postcss-cli": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-9.0.0.tgz", - "integrity": "sha512-tg6MK/jYyO7Ye9PObPYkjCQa7Bh2K6dA3a3I0muczRuw4T4HAtOTpPR+nOCw+On+WDB2sdsbGOsjlwO8BNRbUw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-9.0.1.tgz", + "integrity": "sha512-zO160OBaAZBFUWO+QZIzEKMjnPIc5c61dMg1d7xafblh9cxbNb6s16ahJuP91PcVsu//gqr7BKllJxRiRDsSYw==", "requires": { "chokidar": "^3.3.0", - "colorette": "^2.0.0", "dependency-graph": "^0.11.0", "fs-extra": "^10.0.0", "get-stdin": "^9.0.0", "globby": "^12.0.0", + "nanocolors": "^0.2.11", "postcss-load-config": "^3.0.0", "postcss-reporter": "^7.0.0", "pretty-hrtime": "^1.0.3", @@ -1160,60 +1123,56 @@ "yargs": "^17.0.0" }, "dependencies": { - "fs-extra": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", - "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } + "nanocolors": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.13.tgz", + "integrity": "sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==" } } }, "postcss-colormin": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.0.0.tgz", - "integrity": "sha512-Yt84+5V6CgS/AhK7d7MA58vG8dSZ7+ytlRtWLaQhag3HXOncTfmYpuUOX4cDoXjvLfw1sHRCHMiBjYhc35CymQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.2.0.tgz", + "integrity": "sha512-+HC6GfWU3upe5/mqmxuqYZ9B2Wl4lcoUUNkoaX59nEWV4EtADCMiBqui111Bu8R8IvaZTmqmxrqOAqjbHIwXPw==", "requires": { - "browserslist": "^4.16.0", - "color": "^3.1.1", + "browserslist": "^4.16.6", + "caniuse-api": "^3.0.0", + "colord": "^2.0.1", "postcss-value-parser": "^4.1.0" } }, "postcss-convert-values": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.0.tgz", - "integrity": "sha512-V5kmYm4xoBAjNs+eHY/6XzXJkkGeg4kwNf2ocfqhLb1WBPEa4oaSmoi1fnVO7Dkblqvus9h+AenDvhCKUCK7uQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz", + "integrity": "sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg==", "requires": { "postcss-value-parser": "^4.1.0" } }, "postcss-discard-comments": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.0.tgz", - "integrity": "sha512-Umig6Gxs8m20RihiXY6QkePd6mp4FxkA1Dg+f/Kd6uw0gEMfKRjDeQOyFkLibexbJJGHpE3lrN/Q0R9SMrUMbQ==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", + "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==" }, "postcss-discard-duplicates": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.0.tgz", - "integrity": "sha512-vEJJ+Y3pFUnO1FyCBA6PSisGjHtnphL3V6GsNvkASq/VkP3OX5/No5RYXXLxHa2QegStNzg6HYrYdo71uR4caQ==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", + "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==" }, "postcss-discard-empty": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.0.tgz", - "integrity": "sha512-+wigy099Y1xZxG36WG5L1f2zeH1oicntkJEW4TDIqKKDO2g9XVB3OhoiHTu08rDEjLnbcab4rw0BAccwi2VjiQ==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", + "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==" }, "postcss-discard-overridden": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.0.tgz", - "integrity": "sha512-hybnScTaZM2iEA6kzVQ6Spozy7kVdLw+lGw8hftLlBEzt93uzXoltkYp9u0tI8xbfhxDLTOOzHsHQCkYdmzRUg==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", + "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==" }, "postcss-import": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.1.tgz", - "integrity": "sha512-Xn2+z++vWObbEPhiiKO1a78JiyhqipyrXHBb3AHpv0ks7Cdg+GxQQJ24ODNMTanldf7197gSP3axppO9yaG0lA==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.2.tgz", + "integrity": "sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g==", "requires": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -1237,86 +1196,67 @@ "import-cwd": "^3.0.0", "lilconfig": "^2.0.3", "yaml": "^1.10.2" - }, - "dependencies": { - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" - } } }, "postcss-merge-longhand": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.0.tgz", - "integrity": "sha512-VZNFA40K8BYHzJNA6jHPdg1Nofsz/nK5Dkszrcb5IgWcLroSBZOD6I/iNQzpejSU/3XwpOiZNaYAdBV4KcvxWA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz", + "integrity": "sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw==", "requires": { "css-color-names": "^1.0.1", "postcss-value-parser": "^4.1.0", - "stylehacks": "^5.0.0" + "stylehacks": "^5.0.1" } }, "postcss-merge-rules": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.0.tgz", - "integrity": "sha512-TfsXbKjNYCGfUPEXGIGPySnMiJbdS+3gcVeV8gwmJP4RajyKZHW8E0FYDL1WmggTj3hi+m+WUCAvqRpX2ut4Kg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz", + "integrity": "sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==", "requires": { - "browserslist": "^4.16.0", + "browserslist": "^4.16.6", "caniuse-api": "^3.0.0", - "cssnano-utils": "^2.0.0", - "postcss-selector-parser": "^6.0.4", + "cssnano-utils": "^2.0.1", + "postcss-selector-parser": "^6.0.5", "vendors": "^1.0.3" } }, "postcss-minify-font-values": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.0.tgz", - "integrity": "sha512-zi2JhFaMOcIaNxhndX5uhsqSY1rexKDp23wV8EOmC9XERqzLbHsoRye3aYF716Zm+hkcR4loqKDt8LZlmihwAg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz", + "integrity": "sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==", "requires": { "postcss-value-parser": "^4.1.0" } }, "postcss-minify-gradients": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.0.tgz", - "integrity": "sha512-/jPtNgs6JySMwgsE5dPOq8a2xEopWTW3RyqoB9fLqxgR+mDUNLSi7joKd+N1z7FXWgVkc4l/dEBMXHgNAaUbvg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.2.tgz", + "integrity": "sha512-7Do9JP+wqSD6Prittitt2zDLrfzP9pqKs2EcLX7HJYxsxCOwrrcLt4x/ctQTsiOw+/8HYotAoqNkrzItL19SdQ==", "requires": { - "cssnano-utils": "^2.0.0", - "is-color-stop": "^1.1.0", + "colord": "^2.6", + "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0" } }, "postcss-minify-params": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.0.tgz", - "integrity": "sha512-KvZYIxTPBVKjdd+XgObq9A+Sfv8lMkXTpbZTsjhr42XbfWIeLaTItMlygsDWfjArEc3muUfDaUFgNSeDiJ5jug==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz", + "integrity": "sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==", "requires": { "alphanum-sort": "^1.0.2", "browserslist": "^4.16.0", - "cssnano-utils": "^2.0.0", + "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0", "uniqs": "^2.0.0" } }, "postcss-minify-selectors": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.0.0.tgz", - "integrity": "sha512-cEM0O0eWwFIvmo6nfB0lH0vO/XFwgqIvymODbfPXZ1gTA3i76FKnb7TGUrEpiTxaXH6tgYQ6DcTHwRiRS+YQLQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz", + "integrity": "sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==", "requires": { "alphanum-sort": "^1.0.2", - "postcss-selector-parser": "^3.1.2" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } + "postcss-selector-parser": "^6.0.5" } }, "postcss-nested": { @@ -1325,168 +1265,155 @@ "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", "requires": { "postcss-selector-parser": "^6.0.6" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", - "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - } } }, "postcss-normalize-charset": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.0.tgz", - "integrity": "sha512-pqsCkgo9KmQP0ew6DqSA+uP9YN6EfsW20pQ3JU5JoQge09Z6Too4qU0TNDsTNWuEaP8SWsMp+19l15210MsDZQ==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", + "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==" }, "postcss-normalize-display-values": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.0.tgz", - "integrity": "sha512-t4f2d//gH1f7Ns0Jq3eNdnWuPT7TeLuISZ6RQx4j8gpl5XrhkdshdNcOnlrEK48YU6Tcb6jqK7dorME3N4oOGA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz", + "integrity": "sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==", "requires": { - "cssnano-utils": "^2.0.0", + "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0" } }, "postcss-normalize-positions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.0.tgz", - "integrity": "sha512-0o6/qU5ky74X/eWYj/tv4iiKCm3YqJnrhmVADpIMNXxzFZywsSQxl8F7cKs8jQEtF3VrJBgcDHTexZy1zgDoYg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz", + "integrity": "sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==", "requires": { "postcss-value-parser": "^4.1.0" } }, "postcss-normalize-repeat-style": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.0.tgz", - "integrity": "sha512-KRT14JbrXKcFMYuc4q7lh8lvv8u22wLyMrq+UpHKLtbx2H/LOjvWXYdoDxmNrrrJzomAWL+ViEXr48/IhSUJnQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz", + "integrity": "sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==", "requires": { - "cssnano-utils": "^2.0.0", + "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0" } }, "postcss-normalize-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.0.tgz", - "integrity": "sha512-wSO4pf7GNcDZpmelREWYADF1+XZWrAcbFLQCOqoE92ZwYgaP/RLumkUTaamEzdT2YKRZAH8eLLKGWotU/7FNPw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz", + "integrity": "sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==", "requires": { "postcss-value-parser": "^4.1.0" } }, "postcss-normalize-timing-functions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.0.tgz", - "integrity": "sha512-TwPaDX+wl9wO3MUm23lzGmOzGCGKnpk+rSDgzB2INpakD5dgWR3L6bJq1P1LQYzBAvz8fRIj2NWdnZdV4EV98Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz", + "integrity": "sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==", "requires": { - "cssnano-utils": "^2.0.0", + "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0" } }, "postcss-normalize-unicode": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.0.tgz", - "integrity": "sha512-2CpVoz/67rXU5s9tsPZDxG1YGS9OFHwoY9gsLAzrURrCxTAb0H7Vp87/62LvVPgRWTa5ZmvgmqTp2rL8tlm72A==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz", + "integrity": "sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==", "requires": { "browserslist": "^4.16.0", "postcss-value-parser": "^4.1.0" } }, "postcss-normalize-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.0.tgz", - "integrity": "sha512-ICDaGFBqLgA3dlrCIRuhblLl80D13YtgEV9NJPTYJtgR72vu61KgxAHv+z/lKMs1EbwfSQa3ALjOFLSmXiE34A==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz", + "integrity": "sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==", "requires": { "is-absolute-url": "^3.0.3", - "normalize-url": "^4.5.0", + "normalize-url": "^6.0.1", "postcss-value-parser": "^4.1.0" } }, "postcss-normalize-whitespace": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.0.tgz", - "integrity": "sha512-KRnxQvQAVkJfaeXSz7JlnD9nBN9sFZF9lrk9452Q2uRoqrRSkinqifF8Iex7wZGei2DZVG/qpmDFDmRvbNAOGA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz", + "integrity": "sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==", "requires": { "postcss-value-parser": "^4.1.0" } }, "postcss-ordered-values": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.0.tgz", - "integrity": "sha512-dPr+SRObiHueCIc4IUaG0aOGQmYkuNu50wQvdXTGKy+rzi2mjmPsbeDsheLk5WPb9Zyf2tp8E+I+h40cnivm6g==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz", + "integrity": "sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==", "requires": { - "cssnano-utils": "^2.0.0", + "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0" } }, "postcss-reduce-initial": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.0.tgz", - "integrity": "sha512-wR6pXUaFbSMG1oCKx8pKVA+rnSXCHlca5jMrlmkmif+uig0HNUTV9oGN5kjKsM3mATQAldv2PF9Tbl2vqLFjnA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz", + "integrity": "sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==", "requires": { "browserslist": "^4.16.0", "caniuse-api": "^3.0.0" } }, "postcss-reduce-transforms": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.0.tgz", - "integrity": "sha512-iHdGODW4YzM3WjVecBhPQt6fpJC4lGQZxJKjkBNHpp2b8dzmvj0ogKThqya+IRodQEFzjfXgYeESkf172FH5Lw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz", + "integrity": "sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==", "requires": { - "cssnano-utils": "^2.0.0", + "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0" } }, "postcss-reporter": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.2.tgz", - "integrity": "sha512-JyQ96NTQQsso42y6L1H1RqHfWH1C3Jr0pt91mVv5IdYddZAE9DUZxuferNgk6q0o6vBVOrfVJb10X1FgDzjmDw==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.4.tgz", + "integrity": "sha512-jY/fnpGSin7kwJeunXbY35STp5O3VIxSFdjee5JkoPQ+FfGH5JW3N+Xe9oAPcL9UkjWjkK+JC72o8XH4XXKdhw==", "requires": { - "colorette": "^1.2.1", "lodash.difference": "^4.5.0", "lodash.forown": "^4.4.0", "lodash.get": "^4.4.2", "lodash.groupby": "^4.6.0", - "lodash.sortby": "^4.7.0" + "lodash.sortby": "^4.7.0", + "picocolors": "^1.0.0" }, "dependencies": { - "colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" } } }, "postcss-selector-parser": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", - "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", "requires": { "cssesc": "^3.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1", "util-deprecate": "^1.0.2" } }, "postcss-svgo": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.0.tgz", - "integrity": "sha512-M3/VS4sFI1Yp9g0bPL+xzzCNz5iLdRUztoFaugMit5a8sMfkVzzhwqbsOlD8IFFymCdJDmXmh31waYHWw1K4BA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.2.tgz", + "integrity": "sha512-YzQuFLZu3U3aheizD+B1joQ94vzPfE6BNUcSYuceNxlVnKKsOtdo6hL9/zyC168Q8EwfLSgaDSalsUGa9f2C0A==", "requires": { "postcss-value-parser": "^4.1.0", "svgo": "^2.3.0" } }, "postcss-unique-selectors": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.0.tgz", - "integrity": "sha512-o9l4pF8SRn7aCMTmzb/kNv/kjV7wPZpZ8Nlb1Gq8v/Qvw969K1wanz1RVA0ehHzWe9+wHXaC2DvZlak/gdMJ5w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz", + "integrity": "sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==", "requires": { "alphanum-sort": "^1.0.2", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^6.0.5", "uniqs": "^2.0.0" } }, @@ -1509,8 +1436,20 @@ "glob": "^7.0.0", "postcss": "^8.2.1", "postcss-selector-parser": "^6.0.2" + }, + "dependencies": { + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + } } }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, "quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -1525,9 +1464,9 @@ } }, "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "requires": { "picomatch": "^2.2.1" } @@ -1563,9 +1502,9 @@ } }, "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" }, "reusify": { "version": "1.0.4", @@ -1591,9 +1530,12 @@ } }, "run-parallel": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", - "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "requires": { + "queue-microtask": "^1.2.2" + } }, "simple-swizzle": { "version": "0.2.2", @@ -1601,13 +1543,6 @@ "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", "requires": { "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } } }, "slash": { @@ -1620,6 +1555,11 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==" + }, "stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -1644,9 +1584,9 @@ } }, "stylehacks": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.0.tgz", - "integrity": "sha512-QOWm6XivDLb+fqffTZP8jrmPmPITVChl2KCY2R05nsCWwLi3VGhCdVc3IVGNwd1zzTt1jPd67zIKjpQfxzQZeA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz", + "integrity": "sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==", "requires": { "browserslist": "^4.16.0", "postcss-selector-parser": "^6.0.4" @@ -1658,70 +1598,26 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "requires": { "has-flag": "^4.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - } } }, "svgo": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.3.0.tgz", - "integrity": "sha512-fz4IKjNO6HDPgIQxu4IxwtubtbSfGEAJUq/IXyTPIkGhWck/faiiwfkvsB8LnBkKLvSoyNNIY6d13lZprJMc9Q==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.7.0.tgz", + "integrity": "sha512-aDLsGkre4fTDCWvolyW+fs8ZJFABpzLXbtdK1y71CKnHzAnpDxKXPj2mNKj+pyOXUCzFHzuxRJ94XOFygOWV3w==", "requires": { - "@trysound/sax": "0.1.1", - "chalk": "^4.1.0", - "commander": "^7.1.0", - "css-select": "^3.1.2", - "css-tree": "^1.1.2", + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", "csso": "^4.2.0", + "nanocolors": "^0.1.12", "stable": "^0.1.8" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" - } } }, "tailwindcss": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.16.tgz", - "integrity": "sha512-EireCtpQyyJ4Xz8NYzHafBoy4baCOO96flM0+HgtsFcIQ9KFy/YBK3GEtlnD+rXen0e4xm8t3WiUcKBJmN6yjg==", + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.17.tgz", + "integrity": "sha512-WgRpn+Pxn7eWqlruxnxEbL9ByVRWi3iC10z4b6dW0zSdnkPVC4hPMSWLQkkW8GCyBIv/vbJ0bxIi9dVrl4CfoA==", "requires": { "arg": "^5.0.1", "bytes": "^3.0.0", @@ -1757,139 +1653,12 @@ "tmp": "^0.2.1" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "color": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/color/-/color-4.0.1.tgz", - "integrity": "sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==", - "requires": { - "color-convert": "^2.0.1", - "color-string": "^1.6.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "color-string": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", - "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "requires": { "is-glob": "^4.0.3" - }, - "dependencies": { - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "requires": { - "is-extglob": "^2.1.1" - } - } - } - }, - "postcss-selector-parser": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", - "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "requires": { - "picomatch": "^2.2.1" } } } @@ -1915,11 +1684,6 @@ "is-number": "^7.0.0" } }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" - }, "uniqs": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", @@ -1948,29 +1712,6 @@ "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - } } }, "wrappy": { @@ -1989,14 +1730,14 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yaml": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", - "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, "yargs": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.0.tgz", - "integrity": "sha512-UPeZv4h9Xv510ibpt5rdsUNzgD78nMa1rhxxCgvkKiq06hlKCEHJLiJ6Ub8zDg/wR6hedEI6ovnd2vCvJ4nusA==", + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", + "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==", "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", diff --git a/modules/webapp/package.json b/modules/webapp/package.json index d5c85f4b..33eb7cd9 100644 --- a/modules/webapp/package.json +++ b/modules/webapp/package.json @@ -3,14 +3,14 @@ "version": "1.0.0", "private": true, "dependencies": { - "@fortawesome/fontawesome-free": "^5.15.3", - "@tailwindcss/forms": "^0.3.0", - "autoprefixer": "^10.2.5", - "cssnano": "^5.0.0", + "@fortawesome/fontawesome-free": "^5.15.4", + "@tailwindcss/forms": "^0.3.4", + "autoprefixer": "^10.3.7", + "cssnano": "^5.0.8", "flag-icon-css": "^3.5.0", - "postcss": "^8.2.9", - "postcss-cli": "^9.0.0", - "postcss-import": "^14.0.1", - "tailwindcss": "^2.2.16" + "postcss": "^8.3.11", + "postcss-cli": "^9.0.1", + "postcss-import": "^14.0.2", + "tailwindcss": "^2.2.17" } } From c2d54cebb563afd0fd7eeea6cb72b1ae0301c075 Mon Sep 17 00:00:00 2001 From: eikek Date: Sun, 24 Oct 2021 14:45:03 +0200 Subject: [PATCH 37/37] Fix postcss-purgecss dependency --- modules/webapp/package-lock.json | 606 ++++++++++++++++++++++++------- modules/webapp/package.json | 10 +- modules/webapp/postcss.config.js | 2 +- project/StylesPlugin.scala | 3 +- 4 files changed, 487 insertions(+), 134 deletions(-) diff --git a/modules/webapp/package-lock.json b/modules/webapp/package-lock.json index bcd14273..d9b88b4e 100644 --- a/modules/webapp/package-lock.json +++ b/modules/webapp/package-lock.json @@ -8,6 +8,7 @@ "version": "7.15.8", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, "requires": { "@babel/highlight": "^7.14.5" } @@ -15,12 +16,14 @@ "@babel/helper-validator-identifier": { "version": "7.15.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", - "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==" + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true }, "@babel/highlight": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.5", "chalk": "^2.0.0", @@ -31,6 +34,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -39,6 +43,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -49,6 +54,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -56,17 +62,20 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -76,12 +85,14 @@ "@fortawesome/fontawesome-free": { "version": "5.15.4", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", - "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==" + "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==", + "dev": true }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "requires": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -90,12 +101,14 @@ "@nodelib/fs.stat": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true }, "@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -105,6 +118,7 @@ "version": "0.3.4", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.4.tgz", "integrity": "sha512-vlAoBifNJUkagB+PAdW4aHMe4pKmSLroH398UPgIogBFc91D2VlHUxe4pjxQhiJl0Nfw53sHSJSQBSTQBZP3vA==", + "dev": true, "requires": { "mini-svg-data-uri": "^1.2.3" } @@ -112,22 +126,26 @@ "@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true }, "acorn-node": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, "requires": { "acorn": "^7.0.0", "acorn-walk": "^7.0.0", @@ -137,22 +155,26 @@ "acorn-walk": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true }, "alphanum-sort": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -161,6 +183,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -169,17 +192,20 @@ "arg": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", - "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==" + "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==", + "dev": true }, "array-union": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", - "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==" + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", + "dev": true }, "autoprefixer": { "version": "10.3.7", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.7.tgz", "integrity": "sha512-EmGpu0nnQVmMhX8ROoJ7Mx8mKYPlcUHuxkwrRYEYMz85lu7H09v8w6R1P0JPdn/hKU32GjpLBFEOuIlDWCRWvg==", + "dev": true, "requires": { "browserslist": "^4.17.3", "caniuse-lite": "^1.0.30001264", @@ -192,22 +218,26 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -217,6 +247,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -225,6 +256,7 @@ "version": "4.17.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.5.tgz", "integrity": "sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA==", + "dev": true, "requires": { "caniuse-lite": "^1.0.30001271", "electron-to-chromium": "^1.3.878", @@ -236,29 +268,34 @@ "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true } } }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true }, "camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true }, "caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, "requires": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", @@ -269,12 +306,14 @@ "caniuse-lite": { "version": "1.0.30001271", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz", - "integrity": "sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==" + "integrity": "sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==", + "dev": true }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -284,6 +323,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -299,6 +339,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -309,6 +350,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/color/-/color-4.0.1.tgz", "integrity": "sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==", + "dev": true, "requires": { "color-convert": "^2.0.1", "color-string": "^1.6.0" @@ -318,6 +360,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -325,12 +368,14 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "color-string": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", + "dev": true, "requires": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -339,22 +384,26 @@ "colord": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.1.tgz", - "integrity": "sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw==" + "integrity": "sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw==", + "dev": true }, "commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, "requires": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -366,12 +415,14 @@ "css-color-names": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-1.0.1.tgz", - "integrity": "sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==" + "integrity": "sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==", + "dev": true }, "css-declaration-sorter": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.3.tgz", "integrity": "sha512-SvjQjNRZgh4ULK1LDJ2AduPKUKxIqmtU7ZAyi47BTV+M90Qvxr9AB6lKlLbDUfXqI9IQeYA8LbAsCZPpJEV3aA==", + "dev": true, "requires": { "timsort": "^0.3.0" } @@ -380,6 +431,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "dev": true, "requires": { "boolbase": "^1.0.0", "css-what": "^5.0.0", @@ -392,6 +444,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, "requires": { "mdn-data": "2.0.14", "source-map": "^0.6.1" @@ -400,22 +453,26 @@ "css-unit-converter": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz", - "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==" + "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==", + "dev": true }, "css-what": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", - "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==" + "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", + "dev": true }, "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true }, "cssnano": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.8.tgz", "integrity": "sha512-Lda7geZU0Yu+RZi2SGpjYuQz4HI4/1Y+BhdD0jL7NXAQ5larCzVn+PUGuZbDMYz904AXXCOgO5L1teSvgu7aFg==", + "dev": true, "requires": { "cssnano-preset-default": "^5.1.4", "is-resolvable": "^1.1.0", @@ -427,6 +484,7 @@ "version": "5.1.4", "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.4.tgz", "integrity": "sha512-sPpQNDQBI3R/QsYxQvfB4mXeEcWuw0wGtKtmS5eg8wudyStYMgKOQT39G07EbW1LB56AOYrinRS9f0ig4Y3MhQ==", + "dev": true, "requires": { "css-declaration-sorter": "^6.0.3", "cssnano-utils": "^2.0.1", @@ -462,12 +520,14 @@ "cssnano-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", - "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==" + "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", + "dev": true }, "csso": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, "requires": { "css-tree": "^1.1.2" } @@ -475,17 +535,20 @@ "defined": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true }, "dependency-graph": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", - "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==" + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true }, "detective": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, "requires": { "acorn-node": "^1.6.1", "defined": "^1.0.0", @@ -495,12 +558,14 @@ "didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, "requires": { "path-type": "^4.0.0" } @@ -508,12 +573,14 @@ "dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true }, "dom-serializer": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, "requires": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -523,12 +590,14 @@ "domelementtype": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true }, "domhandler": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", + "dev": true, "requires": { "domelementtype": "^2.2.0" } @@ -537,6 +606,7 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, "requires": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -546,22 +616,26 @@ "electron-to-chromium": { "version": "1.3.878", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.878.tgz", - "integrity": "sha512-O6yxWCN9ph2AdspAIszBnd9v8s11hQx8ub9w4UGApzmNRnoKhbulOWqbO8THEQec/aEHtvy+donHZMlh6l1rbA==" + "integrity": "sha512-O6yxWCN9ph2AdspAIszBnd9v8s11hQx8ub9w4UGApzmNRnoKhbulOWqbO8THEQec/aEHtvy+donHZMlh6l1rbA==", + "dev": true }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, "requires": { "is-arrayish": "^0.2.1" }, @@ -569,24 +643,28 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true } } }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "fast-glob": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -599,6 +677,7 @@ "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, "requires": { "reusify": "^1.0.4" } @@ -607,6 +686,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -614,17 +694,20 @@ "flag-icon-css": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/flag-icon-css/-/flag-icon-css-3.5.0.tgz", - "integrity": "sha512-pgJnJLrtb0tcDgU1fzGaQXmR8h++nXvILJ+r5SmOXaaL/2pocunQo2a8TAXhjQnBpRLPtZ1KCz/TYpqeNuE2ew==" + "integrity": "sha512-pgJnJLrtb0tcDgU1fzGaQXmR8h++nXvILJ+r5SmOXaaL/2pocunQo2a8TAXhjQnBpRLPtZ1KCz/TYpqeNuE2ew==", + "dev": true }, "fraction.js": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", - "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==" + "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==", + "dev": true }, "fs-extra": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "dev": true, "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -634,33 +717,39 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "optional": true }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true }, "get-stdin": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", - "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==" + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -674,6 +763,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -682,6 +772,7 @@ "version": "12.0.2", "resolved": "https://registry.npmjs.org/globby/-/globby-12.0.2.tgz", "integrity": "sha512-lAsmb/5Lww4r7MM9nCCliDZVIKbZTavrsunAsHLr9oHthrZP1qi7/gAnHOsUs9bLvEt2vKVJhHmxuL7QbDuPdQ==", + "dev": true, "requires": { "array-union": "^3.0.1", "dir-glob": "^3.0.1", @@ -694,12 +785,14 @@ "graceful-fs": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -707,37 +800,44 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "hex-color-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true }, "hsl-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=" + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true }, "hsla-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=" + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true }, "html-tags": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", - "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==" + "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", + "dev": true }, "ignore": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true }, "import-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, "requires": { "import-from": "^3.0.0" } @@ -746,6 +846,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -754,7 +855,8 @@ "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true } } }, @@ -762,6 +864,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, "requires": { "resolve-from": "^5.0.0" } @@ -770,6 +873,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -778,22 +882,26 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "is-absolute-url": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==" + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "dev": true }, "is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "requires": { "binary-extensions": "^2.0.0" } @@ -802,6 +910,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, "requires": { "css-color-names": "^0.0.4", "hex-color-regex": "^1.1.0", @@ -814,7 +923,8 @@ "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=" + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true } } }, @@ -822,6 +932,7 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "dev": true, "requires": { "has": "^1.0.3" } @@ -829,17 +940,20 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -847,27 +961,32 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true }, "is-resolvable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "requires": { "graceful-fs": "^4.1.6", "universalify": "^2.0.0" @@ -876,72 +995,86 @@ "lilconfig": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", - "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==" + "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==", + "dev": true }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", + "dev": true }, "lodash.forown": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.forown/-/lodash.forown-4.4.0.tgz", - "integrity": "sha1-hRFc8E9z75ZuztUlEdOJPMRmg68=" + "integrity": "sha1-hRFc8E9z75ZuztUlEdOJPMRmg68=", + "dev": true }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true }, "lodash.groupby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", - "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=" + "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=", + "dev": true }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true }, "lodash.topath": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", - "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=" + "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=", + "dev": true }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true }, "mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true }, "micromatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, "requires": { "braces": "^3.0.1", "picomatch": "^2.2.3" @@ -950,12 +1083,14 @@ "mini-svg-data-uri": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.3.tgz", - "integrity": "sha512-gSfqpMRC8IxghvMcxzzmMnWpXAChSA+vy4cia33RgerMS8Fex95akUyQZPbxJJmeBGiGmK7n/1OpUX8ksRjIdA==" + "integrity": "sha512-gSfqpMRC8IxghvMcxzzmMnWpXAChSA+vy4cia33RgerMS8Fex95akUyQZPbxJJmeBGiGmK7n/1OpUX8ksRjIdA==", + "dev": true }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -963,27 +1098,32 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true }, "modern-normalize": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz", - "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==" + "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==", + "dev": true }, "nanocolors": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.1.12.tgz", - "integrity": "sha512-2nMHqg1x5PU+unxX7PGY7AuYxl2qDx7PSrTRjizr8sxdd3l/3hBuWWaki62qmtYm2U5i4Z5E7GbjlyDFhs9/EQ==" + "integrity": "sha512-2nMHqg1x5PU+unxX7PGY7AuYxl2qDx7PSrTRjizr8sxdd3l/3hBuWWaki62qmtYm2U5i4Z5E7GbjlyDFhs9/EQ==", + "dev": true }, "nanoid": { "version": "3.1.30", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", - "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==" + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "dev": true }, "node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, "requires": { "lodash": "^4.17.21" } @@ -991,27 +1131,32 @@ "node-releases": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==" + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", + "dev": true }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true }, "normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true }, "normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true }, "nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "dev": true, "requires": { "boolbase": "^1.0.0" } @@ -1019,12 +1164,14 @@ "object-hash": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "dev": true }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -1033,6 +1180,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "requires": { "callsites": "^3.0.0" } @@ -1041,6 +1189,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -1051,37 +1200,44 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true }, "picocolors": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true }, "picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true }, "postcss": { "version": "8.3.11", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz", "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==", + "dev": true, "requires": { "nanoid": "^3.1.30", "picocolors": "^1.0.0", @@ -1091,7 +1247,8 @@ "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true } } }, @@ -1099,6 +1256,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.0.0.tgz", "integrity": "sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==", + "dev": true, "requires": { "postcss-selector-parser": "^6.0.2", "postcss-value-parser": "^4.0.2" @@ -1108,6 +1266,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-9.0.1.tgz", "integrity": "sha512-zO160OBaAZBFUWO+QZIzEKMjnPIc5c61dMg1d7xafblh9cxbNb6s16ahJuP91PcVsu//gqr7BKllJxRiRDsSYw==", + "dev": true, "requires": { "chokidar": "^3.3.0", "dependency-graph": "^0.11.0", @@ -1126,7 +1285,8 @@ "nanocolors": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.13.tgz", - "integrity": "sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==" + "integrity": "sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==", + "dev": true } } }, @@ -1134,6 +1294,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.2.0.tgz", "integrity": "sha512-+HC6GfWU3upe5/mqmxuqYZ9B2Wl4lcoUUNkoaX59nEWV4EtADCMiBqui111Bu8R8IvaZTmqmxrqOAqjbHIwXPw==", + "dev": true, "requires": { "browserslist": "^4.16.6", "caniuse-api": "^3.0.0", @@ -1145,6 +1306,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz", "integrity": "sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg==", + "dev": true, "requires": { "postcss-value-parser": "^4.1.0" } @@ -1152,27 +1314,32 @@ "postcss-discard-comments": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", - "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==" + "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", + "dev": true }, "postcss-discard-duplicates": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", - "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==" + "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", + "dev": true }, "postcss-discard-empty": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", - "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==" + "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", + "dev": true }, "postcss-discard-overridden": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", - "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==" + "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", + "dev": true }, "postcss-import": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.2.tgz", "integrity": "sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g==", + "dev": true, "requires": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -1183,6 +1350,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz", "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==", + "dev": true, "requires": { "camelcase-css": "^2.0.1", "postcss": "^8.1.6" @@ -1192,6 +1360,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.0.tgz", "integrity": "sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==", + "dev": true, "requires": { "import-cwd": "^3.0.0", "lilconfig": "^2.0.3", @@ -1202,6 +1371,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz", "integrity": "sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw==", + "dev": true, "requires": { "css-color-names": "^1.0.1", "postcss-value-parser": "^4.1.0", @@ -1212,6 +1382,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz", "integrity": "sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==", + "dev": true, "requires": { "browserslist": "^4.16.6", "caniuse-api": "^3.0.0", @@ -1224,6 +1395,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz", "integrity": "sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==", + "dev": true, "requires": { "postcss-value-parser": "^4.1.0" } @@ -1232,6 +1404,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.2.tgz", "integrity": "sha512-7Do9JP+wqSD6Prittitt2zDLrfzP9pqKs2EcLX7HJYxsxCOwrrcLt4x/ctQTsiOw+/8HYotAoqNkrzItL19SdQ==", + "dev": true, "requires": { "colord": "^2.6", "cssnano-utils": "^2.0.1", @@ -1242,6 +1415,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz", "integrity": "sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==", + "dev": true, "requires": { "alphanum-sort": "^1.0.2", "browserslist": "^4.16.0", @@ -1254,6 +1428,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz", "integrity": "sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==", + "dev": true, "requires": { "alphanum-sort": "^1.0.2", "postcss-selector-parser": "^6.0.5" @@ -1263,6 +1438,7 @@ "version": "5.0.6", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, "requires": { "postcss-selector-parser": "^6.0.6" } @@ -1270,12 +1446,14 @@ "postcss-normalize-charset": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", - "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==" + "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", + "dev": true }, "postcss-normalize-display-values": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz", "integrity": "sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==", + "dev": true, "requires": { "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0" @@ -1285,6 +1463,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz", "integrity": "sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==", + "dev": true, "requires": { "postcss-value-parser": "^4.1.0" } @@ -1293,6 +1472,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz", "integrity": "sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==", + "dev": true, "requires": { "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0" @@ -1302,6 +1482,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz", "integrity": "sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==", + "dev": true, "requires": { "postcss-value-parser": "^4.1.0" } @@ -1310,6 +1491,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz", "integrity": "sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==", + "dev": true, "requires": { "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0" @@ -1319,6 +1501,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz", "integrity": "sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==", + "dev": true, "requires": { "browserslist": "^4.16.0", "postcss-value-parser": "^4.1.0" @@ -1328,6 +1511,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz", "integrity": "sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==", + "dev": true, "requires": { "is-absolute-url": "^3.0.3", "normalize-url": "^6.0.1", @@ -1338,6 +1522,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz", "integrity": "sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==", + "dev": true, "requires": { "postcss-value-parser": "^4.1.0" } @@ -1346,15 +1531,132 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz", "integrity": "sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==", + "dev": true, "requires": { "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0" } }, + "postcss-purgecss": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/postcss-purgecss/-/postcss-purgecss-2.0.3.tgz", + "integrity": "sha512-cuQin5PgZzvDe7EjW4S27iM6p4ZNz4iBEPmBrAykXm2WyaBtri1sA4ZVn/zECN7x3uxeADwDq1u4VDY5C9iusg==", + "dev": true, + "requires": { + "postcss": "7.0.26", + "purgecss": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "postcss": { + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.26.tgz", + "integrity": "sha512-IY4oRjpXWYshuTDFxMVkJDtWIk2LhsTlu8bZnbEJA4+bYT16Lvpo8Qv6EvDumhYRgzjZl489pmsY3qVgJQ08nA==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "purgecss": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-2.3.0.tgz", + "integrity": "sha512-BE5CROfVGsx2XIhxGuZAT7rTH9lLeQx/6M0P7DTXQH4IUc3BBzs9JUzt4yzGf3JrH9enkeq6YJBe9CTtkm1WmQ==", + "dev": true, + "requires": { + "commander": "^5.0.0", + "glob": "^7.0.0", + "postcss": "7.0.32", + "postcss-selector-parser": "^6.0.2" + }, + "dependencies": { + "postcss": { + "version": "7.0.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", + "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + } + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "postcss-reduce-initial": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz", "integrity": "sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==", + "dev": true, "requires": { "browserslist": "^4.16.0", "caniuse-api": "^3.0.0" @@ -1364,6 +1666,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz", "integrity": "sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==", + "dev": true, "requires": { "cssnano-utils": "^2.0.1", "postcss-value-parser": "^4.1.0" @@ -1373,6 +1676,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.4.tgz", "integrity": "sha512-jY/fnpGSin7kwJeunXbY35STp5O3VIxSFdjee5JkoPQ+FfGH5JW3N+Xe9oAPcL9UkjWjkK+JC72o8XH4XXKdhw==", + "dev": true, "requires": { "lodash.difference": "^4.5.0", "lodash.forown": "^4.4.0", @@ -1385,7 +1689,8 @@ "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true } } }, @@ -1393,6 +1698,7 @@ "version": "6.0.6", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "dev": true, "requires": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -1402,6 +1708,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.2.tgz", "integrity": "sha512-YzQuFLZu3U3aheizD+B1joQ94vzPfE6BNUcSYuceNxlVnKKsOtdo6hL9/zyC168Q8EwfLSgaDSalsUGa9f2C0A==", + "dev": true, "requires": { "postcss-value-parser": "^4.1.0", "svgo": "^2.3.0" @@ -1411,6 +1718,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz", "integrity": "sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==", + "dev": true, "requires": { "alphanum-sort": "^1.0.2", "postcss-selector-parser": "^6.0.5", @@ -1420,17 +1728,20 @@ "postcss-value-parser": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true }, "pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true }, "purgecss": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.0.3.tgz", "integrity": "sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==", + "dev": true, "requires": { "commander": "^6.0.0", "glob": "^7.0.0", @@ -1441,24 +1752,28 @@ "commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true } } }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true }, "quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, "requires": { "pify": "^2.3.0" } @@ -1467,6 +1782,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "requires": { "picomatch": "^2.2.1" } @@ -1475,6 +1791,7 @@ "version": "2.1.8", "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz", "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==", + "dev": true, "requires": { "css-unit-converter": "^1.1.1", "postcss-value-parser": "^3.3.0" @@ -1483,19 +1800,22 @@ "postcss-value-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true } } }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, "requires": { "is-core-module": "^2.2.0", "path-parse": "^1.0.6" @@ -1504,27 +1824,32 @@ "resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true }, "rgb-regex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=" + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true }, "rgba-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "requires": { "glob": "^7.1.3" } @@ -1533,6 +1858,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "requires": { "queue-microtask": "^1.2.2" } @@ -1541,6 +1867,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, "requires": { "is-arrayish": "^0.3.1" } @@ -1548,27 +1875,32 @@ "slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==" + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true }, "source-map-js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", - "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==" + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "dev": true }, "stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1579,6 +1911,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -1587,6 +1920,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz", "integrity": "sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==", + "dev": true, "requires": { "browserslist": "^4.16.0", "postcss-selector-parser": "^6.0.4" @@ -1596,6 +1930,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -1604,6 +1939,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.7.0.tgz", "integrity": "sha512-aDLsGkre4fTDCWvolyW+fs8ZJFABpzLXbtdK1y71CKnHzAnpDxKXPj2mNKj+pyOXUCzFHzuxRJ94XOFygOWV3w==", + "dev": true, "requires": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", @@ -1618,6 +1954,7 @@ "version": "2.2.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.17.tgz", "integrity": "sha512-WgRpn+Pxn7eWqlruxnxEbL9ByVRWi3iC10z4b6dW0zSdnkPVC4hPMSWLQkkW8GCyBIv/vbJ0bxIi9dVrl4CfoA==", + "dev": true, "requires": { "arg": "^5.0.1", "bytes": "^3.0.0", @@ -1657,6 +1994,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "requires": { "is-glob": "^4.0.3" } @@ -1666,12 +2004,14 @@ "timsort": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, "requires": { "rimraf": "^3.0.0" } @@ -1680,6 +2020,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "requires": { "is-number": "^7.0.0" } @@ -1687,27 +2028,32 @@ "uniqs": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=" + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true }, "vendors": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", - "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==" + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", + "dev": true }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -1717,27 +2063,32 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true }, "yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true }, "yargs": { "version": "17.2.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==", + "dev": true, "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -1751,7 +2102,8 @@ "yargs-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true } } } diff --git a/modules/webapp/package.json b/modules/webapp/package.json index 33eb7cd9..5b03270e 100644 --- a/modules/webapp/package.json +++ b/modules/webapp/package.json @@ -2,15 +2,17 @@ "name": "docspell-css", "version": "1.0.0", "private": true, - "dependencies": { + "dependencies": {}, + "devDependencies": { "@fortawesome/fontawesome-free": "^5.15.4", "@tailwindcss/forms": "^0.3.4", - "autoprefixer": "^10.3.7", - "cssnano": "^5.0.8", "flag-icon-css": "^3.5.0", - "postcss": "^8.3.11", "postcss-cli": "^9.0.1", "postcss-import": "^14.0.2", + "autoprefixer": "^10.3.7", + "cssnano": "^5.0.8", + "postcss": "^8.3.11", + "postcss-purgecss": "^2.0.3", "tailwindcss": "^2.2.17" } } diff --git a/modules/webapp/postcss.config.js b/modules/webapp/postcss.config.js index 049f289e..6976de4b 100644 --- a/modules/webapp/postcss.config.js +++ b/modules/webapp/postcss.config.js @@ -13,7 +13,7 @@ const prodPlugins = require('postcss-import'), tailwindcss("./tailwind.config.js"), require("autoprefixer"), - require("@fullhuman/postcss-purgecss")({ + require("postcss-purgecss")({ content: [ "./src/main/elm/**/*.elm", "./src/main/styles/keep.txt", diff --git a/project/StylesPlugin.scala b/project/StylesPlugin.scala index 5bb1b507..d83ff2e2 100644 --- a/project/StylesPlugin.scala +++ b/project/StylesPlugin.scala @@ -55,7 +55,7 @@ object StylesPlugin extends AutoPlugin { val files = postCss(npx, inDir, outDir, wd, mode, logger) ++ copyWebfonts(wd, outDir, logger) ++ copyFlags(wd, outDir, logger) - logger.info("Styles built") + logger.info(s"Styles built at $outDir") files }, stylesInstall := { @@ -63,7 +63,6 @@ object StylesPlugin extends AutoPlugin { val npm = stylesNpmCommand.value val wd = (LocalRootProject / baseDirectory).value npmInstall(npm, wd, logger) - } )