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" }