mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-06 15:15:58 +00:00
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.
This commit is contained in:
parent
9009ebcb39
commit
2ac0b84e52
@ -21,20 +21,24 @@ import docspell.query.ItemQuery.Expr
|
|||||||
import docspell.query.ItemQuery.Expr.AttachId
|
import docspell.query.ItemQuery.Expr.AttachId
|
||||||
import docspell.store.Store
|
import docspell.store.Store
|
||||||
import docspell.store.queries.SearchSummary
|
import docspell.store.queries.SearchSummary
|
||||||
import docspell.store.records.{RShare, RUserEmail}
|
import docspell.store.records._
|
||||||
|
|
||||||
import emil._
|
import emil._
|
||||||
import scodec.bits.ByteVector
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
trait OShare[F[_]] {
|
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 delete(id: Ident, collective: Ident): F[Boolean]
|
||||||
|
|
||||||
def addNew(share: OShare.NewShare): F[OShare.ChangeResult]
|
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(
|
def update(
|
||||||
id: Ident,
|
id: Ident,
|
||||||
@ -91,12 +95,7 @@ object OShare {
|
|||||||
case object NotFound extends SendResult
|
case object NotFound extends SendResult
|
||||||
}
|
}
|
||||||
|
|
||||||
final case class ShareQuery(id: Ident, cid: Ident, query: ItemQuery) {
|
final case class ShareQuery(id: Ident, account: AccountId, query: ItemQuery)
|
||||||
|
|
||||||
//TODO
|
|
||||||
def asAccount: AccountId =
|
|
||||||
AccountId(cid, Ident.unsafe(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed trait VerifyResult {
|
sealed trait VerifyResult {
|
||||||
def toEither: Either[String, ShareToken] =
|
def toEither: Either[String, ShareToken] =
|
||||||
@ -121,7 +120,7 @@ object OShare {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final case class NewShare(
|
final case class NewShare(
|
||||||
cid: Ident,
|
account: AccountId,
|
||||||
name: Option[String],
|
name: Option[String],
|
||||||
query: ItemQuery,
|
query: ItemQuery,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
@ -133,11 +132,15 @@ object OShare {
|
|||||||
object ChangeResult {
|
object ChangeResult {
|
||||||
final case class Success(id: Ident) extends ChangeResult
|
final case class Success(id: Ident) extends ChangeResult
|
||||||
case object PublishUntilInPast extends ChangeResult
|
case object PublishUntilInPast extends ChangeResult
|
||||||
|
case object NotFound extends ChangeResult
|
||||||
|
|
||||||
def success(id: Ident): ChangeResult = Success(id)
|
def success(id: Ident): ChangeResult = Success(id)
|
||||||
def publishUntilInPast: ChangeResult = PublishUntilInPast
|
def publishUntilInPast: ChangeResult = PublishUntilInPast
|
||||||
|
def notFound: ChangeResult = NotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final case class ShareData(share: RShare, user: RUser)
|
||||||
|
|
||||||
def apply[F[_]: Async](
|
def apply[F[_]: Async](
|
||||||
store: Store[F],
|
store: Store[F],
|
||||||
itemSearch: OItemSearch[F],
|
itemSearch: OItemSearch[F],
|
||||||
@ -147,8 +150,14 @@ object OShare {
|
|||||||
new OShare[F] {
|
new OShare[F] {
|
||||||
private[this] val logger = Logger.log4s[F](org.log4s.getLogger)
|
private[this] val logger = Logger.log4s[F](org.log4s.getLogger)
|
||||||
|
|
||||||
def findAll(collective: Ident): F[List[RShare]] =
|
def findAll(
|
||||||
store.transact(RShare.findAllByCollective(collective))
|
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] =
|
def delete(id: Ident, collective: Ident): F[Boolean] =
|
||||||
store.transact(RShare.deleteByIdAndCid(id, collective)).map(_ > 0)
|
store.transact(RShare.deleteByIdAndCid(id, collective)).map(_ > 0)
|
||||||
@ -157,10 +166,11 @@ object OShare {
|
|||||||
for {
|
for {
|
||||||
curTime <- Timestamp.current[F]
|
curTime <- Timestamp.current[F]
|
||||||
id <- Ident.randomId[F]
|
id <- Ident.randomId[F]
|
||||||
|
user <- store.transact(RUser.findByAccount(share.account))
|
||||||
pass = share.password.map(PasswordCrypt.crypt)
|
pass = share.password.map(PasswordCrypt.crypt)
|
||||||
record = RShare(
|
record = RShare(
|
||||||
id,
|
id,
|
||||||
share.cid,
|
user.map(_.uid).getOrElse(Ident.unsafe("-error-no-user-")),
|
||||||
share.name,
|
share.name,
|
||||||
share.query,
|
share.query,
|
||||||
share.enabled,
|
share.enabled,
|
||||||
@ -182,9 +192,10 @@ object OShare {
|
|||||||
): F[ChangeResult] =
|
): F[ChangeResult] =
|
||||||
for {
|
for {
|
||||||
curTime <- Timestamp.current[F]
|
curTime <- Timestamp.current[F]
|
||||||
|
user <- store.transact(RUser.findByAccount(share.account))
|
||||||
record = RShare(
|
record = RShare(
|
||||||
id,
|
id,
|
||||||
share.cid,
|
user.map(_.uid).getOrElse(Ident.unsafe("-error-no-user-")),
|
||||||
share.name,
|
share.name,
|
||||||
share.query,
|
share.query,
|
||||||
share.enabled,
|
share.enabled,
|
||||||
@ -199,11 +210,14 @@ object OShare {
|
|||||||
else
|
else
|
||||||
store
|
store
|
||||||
.transact(RShare.updateData(record, removePassword))
|
.transact(RShare.updateData(record, removePassword))
|
||||||
.map(_ => ChangeResult.success(id))
|
.map(n => if (n > 0) ChangeResult.success(id) else ChangeResult.notFound)
|
||||||
} yield res
|
} yield res
|
||||||
|
|
||||||
def findOne(id: Ident, collective: Ident): OptionT[F, RShare] =
|
def findOne(id: Ident, collective: Ident): OptionT[F, ShareData] =
|
||||||
RShare.findOne(id, collective).mapK(store.transform)
|
RShare
|
||||||
|
.findOne(id, collective)
|
||||||
|
.mapK(store.transform)
|
||||||
|
.map(ShareData.tupled)
|
||||||
|
|
||||||
def verify(
|
def verify(
|
||||||
key: ByteVector
|
key: ByteVector
|
||||||
@ -211,7 +225,7 @@ object OShare {
|
|||||||
RShare
|
RShare
|
||||||
.findCurrentActive(id)
|
.findCurrentActive(id)
|
||||||
.mapK(store.transform)
|
.mapK(store.transform)
|
||||||
.semiflatMap { share =>
|
.semiflatMap { case (share, _) =>
|
||||||
val pwCheck =
|
val pwCheck =
|
||||||
share.password.map(encPw => password.exists(PasswordCrypt.check(_, encPw)))
|
share.password.map(encPw => password.exists(PasswordCrypt.check(_, encPw)))
|
||||||
|
|
||||||
@ -257,7 +271,9 @@ object OShare {
|
|||||||
RShare
|
RShare
|
||||||
.findCurrentActive(id)
|
.findCurrentActive(id)
|
||||||
.mapK(store.transform)
|
.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(
|
def findAttachmentPreview(
|
||||||
attachId: Ident,
|
attachId: Ident,
|
||||||
@ -266,21 +282,23 @@ object OShare {
|
|||||||
for {
|
for {
|
||||||
sq <- findShareQuery(shareId)
|
sq <- findShareQuery(shareId)
|
||||||
_ <- checkAttachment(sq, AttachId(attachId.id))
|
_ <- checkAttachment(sq, AttachId(attachId.id))
|
||||||
res <- OptionT(itemSearch.findAttachmentPreview(attachId, sq.cid))
|
res <- OptionT(
|
||||||
|
itemSearch.findAttachmentPreview(attachId, sq.account.collective)
|
||||||
|
)
|
||||||
} yield res
|
} yield res
|
||||||
|
|
||||||
def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] =
|
def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] =
|
||||||
for {
|
for {
|
||||||
sq <- findShareQuery(shareId)
|
sq <- findShareQuery(shareId)
|
||||||
_ <- checkAttachment(sq, AttachId(attachId.id))
|
_ <- checkAttachment(sq, AttachId(attachId.id))
|
||||||
res <- OptionT(itemSearch.findAttachment(attachId, sq.cid))
|
res <- OptionT(itemSearch.findAttachment(attachId, sq.account.collective))
|
||||||
} yield res
|
} yield res
|
||||||
|
|
||||||
def findItem(itemId: Ident, shareId: Ident): OptionT[F, ItemData] =
|
def findItem(itemId: Ident, shareId: Ident): OptionT[F, ItemData] =
|
||||||
for {
|
for {
|
||||||
sq <- findShareQuery(shareId)
|
sq <- findShareQuery(shareId)
|
||||||
_ <- checkAttachment(sq, Expr.itemIdEq(itemId.id))
|
_ <- checkAttachment(sq, Expr.itemIdEq(itemId.id))
|
||||||
res <- OptionT(itemSearch.findItem(itemId, sq.cid))
|
res <- OptionT(itemSearch.findItem(itemId, sq.account.collective))
|
||||||
} yield res
|
} yield res
|
||||||
|
|
||||||
/** Check whether the attachment with the given id is in the results of the given
|
/** 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] = {
|
private def checkAttachment(sq: ShareQuery, idExpr: Expr): OptionT[F, Unit] = {
|
||||||
val checkQuery = Query(
|
val checkQuery = Query(
|
||||||
Query.Fix(sq.asAccount, Some(sq.query.expr), None),
|
Query.Fix(sq.account, Some(sq.query.expr), None),
|
||||||
Query.QueryExpr(idExpr)
|
Query.QueryExpr(idExpr)
|
||||||
)
|
)
|
||||||
OptionT(
|
OptionT(
|
||||||
@ -310,7 +328,7 @@ object OShare {
|
|||||||
): OptionT[F, StringSearchResult[SearchSummary]] =
|
): OptionT[F, StringSearchResult[SearchSummary]] =
|
||||||
findShareQuery(shareId)
|
findShareQuery(shareId)
|
||||||
.semiflatMap { share =>
|
.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
|
simpleSearch
|
||||||
.searchSummaryByString(settings)(fix, q)
|
.searchSummaryByString(settings)(fix, q)
|
||||||
.map {
|
.map {
|
||||||
@ -350,7 +368,7 @@ object OShare {
|
|||||||
(for {
|
(for {
|
||||||
_ <- RShare
|
_ <- RShare
|
||||||
.findCurrentActive(mail.shareId)
|
.findCurrentActive(mail.shareId)
|
||||||
.filter(_.cid == account.collective)
|
.filter(_._2.cid == account.collective)
|
||||||
.mapK(store.transform)
|
.mapK(store.transform)
|
||||||
mailCfg <- getSmtpSettings
|
mailCfg <- getSmtpSettings
|
||||||
mail <- createMail(mailCfg)
|
mail <- createMail(mailCfg)
|
||||||
|
@ -8,6 +8,7 @@ package docspell.common
|
|||||||
|
|
||||||
import io.circe._
|
import io.circe._
|
||||||
|
|
||||||
|
/** The collective and user name. */
|
||||||
case class AccountId(collective: Ident, user: Ident) {
|
case class AccountId(collective: Ident, user: Ident) {
|
||||||
def asString =
|
def asString =
|
||||||
if (collective == user) user.id
|
if (collective == user) user.id
|
||||||
|
@ -1932,6 +1932,9 @@ paths:
|
|||||||
Return a list of all shares for this collective.
|
Return a list of all shares for this collective.
|
||||||
security:
|
security:
|
||||||
- authTokenHeader: []
|
- authTokenHeader: []
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/q"
|
||||||
|
- $ref: "#/components/parameters/owningShare"
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Ok
|
description: Ok
|
||||||
@ -4496,6 +4499,7 @@ components:
|
|||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- query
|
- query
|
||||||
|
- owner
|
||||||
- enabled
|
- enabled
|
||||||
- publishAt
|
- publishAt
|
||||||
- publishUntil
|
- publishUntil
|
||||||
@ -4509,6 +4513,8 @@ components:
|
|||||||
query:
|
query:
|
||||||
type: string
|
type: string
|
||||||
format: itemquery
|
format: itemquery
|
||||||
|
owner:
|
||||||
|
$ref: "#/components/schemas/IdName"
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
enabled:
|
enabled:
|
||||||
@ -6805,6 +6811,13 @@ components:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
owningShare:
|
||||||
|
name: owning
|
||||||
|
in: query
|
||||||
|
description: Return my own shares only
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
checksum:
|
checksum:
|
||||||
name: checksum
|
name: checksum
|
||||||
in: path
|
in: path
|
||||||
|
@ -16,7 +16,7 @@ import docspell.common.SearchMode
|
|||||||
|
|
||||||
import org.http4s.ParseFailure
|
import org.http4s.ParseFailure
|
||||||
import org.http4s.QueryParamDecoder
|
import org.http4s.QueryParamDecoder
|
||||||
import org.http4s.dsl.impl.OptionalQueryParamDecoderMatcher
|
import org.http4s.dsl.impl.{FlagQueryParamMatcher, OptionalQueryParamDecoderMatcher}
|
||||||
|
|
||||||
object QueryParam {
|
object QueryParam {
|
||||||
case class QueryString(q: String)
|
case class QueryString(q: String)
|
||||||
@ -67,6 +67,7 @@ object QueryParam {
|
|||||||
object FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full")
|
object FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full")
|
||||||
|
|
||||||
object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning")
|
object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning")
|
||||||
|
object OwningFlag extends FlagQueryParamMatcher("owning")
|
||||||
|
|
||||||
object ContactKindOpt extends OptionalQueryParamDecoderMatcher[ContactKind]("kind")
|
object ContactKindOpt extends OptionalQueryParamDecoderMatcher[ContactKind]("kind")
|
||||||
|
|
||||||
|
@ -18,8 +18,7 @@ import docspell.common.{Ident, Timestamp}
|
|||||||
import docspell.restapi.model._
|
import docspell.restapi.model._
|
||||||
import docspell.restserver.Config
|
import docspell.restserver.Config
|
||||||
import docspell.restserver.auth.ShareCookieData
|
import docspell.restserver.auth.ShareCookieData
|
||||||
import docspell.restserver.http4s.{ClientRequestInfo, ResponseGenerator}
|
import docspell.restserver.http4s.{ClientRequestInfo, QueryParam => QP, ResponseGenerator}
|
||||||
import docspell.store.records.RShare
|
|
||||||
|
|
||||||
import emil.MailAddress
|
import emil.MailAddress
|
||||||
import emil.javamail.syntax._
|
import emil.javamail.syntax._
|
||||||
@ -35,9 +34,10 @@ object ShareRoutes {
|
|||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
HttpRoutes.of {
|
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 {
|
for {
|
||||||
all <- backend.share.findAll(user.account.collective)
|
all <- backend.share.findAll(user.account.collective, login, q)
|
||||||
now <- Timestamp.current[F]
|
now <- Timestamp.current[F]
|
||||||
res <- Ok(ShareList(all.map(mkShareDetail(now))))
|
res <- Ok(ShareList(all.map(mkShareDetail(now))))
|
||||||
} yield res
|
} yield res
|
||||||
@ -111,7 +111,7 @@ object ShareRoutes {
|
|||||||
|
|
||||||
def mkNewShare(data: ShareData, user: AuthToken): OShare.NewShare =
|
def mkNewShare(data: ShareData, user: AuthToken): OShare.NewShare =
|
||||||
OShare.NewShare(
|
OShare.NewShare(
|
||||||
user.account.collective,
|
user.account,
|
||||||
data.name,
|
data.name,
|
||||||
data.query,
|
data.query,
|
||||||
data.enabled,
|
data.enabled,
|
||||||
@ -124,6 +124,12 @@ object ShareRoutes {
|
|||||||
case OShare.ChangeResult.Success(id) => IdResult(true, msg, id)
|
case OShare.ChangeResult.Success(id) => IdResult(true, msg, id)
|
||||||
case OShare.ChangeResult.PublishUntilInPast =>
|
case OShare.ChangeResult.PublishUntilInPast =>
|
||||||
IdResult(false, "Until date must not be in the past", Ident.unsafe(""))
|
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 =
|
def mkBasicResult(r: OShare.ChangeResult, msg: => String): BasicResult =
|
||||||
@ -131,20 +137,26 @@ object ShareRoutes {
|
|||||||
case OShare.ChangeResult.Success(_) => BasicResult(true, msg)
|
case OShare.ChangeResult.Success(_) => BasicResult(true, msg)
|
||||||
case OShare.ChangeResult.PublishUntilInPast =>
|
case OShare.ChangeResult.PublishUntilInPast =>
|
||||||
BasicResult(false, "Until date must not be in the past")
|
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(
|
ShareDetail(
|
||||||
r.id,
|
r.share.id,
|
||||||
r.query,
|
r.share.query,
|
||||||
r.name,
|
IdName(r.user.uid, r.user.login.id),
|
||||||
r.enabled,
|
r.share.name,
|
||||||
r.publishAt,
|
r.share.enabled,
|
||||||
r.publishUntil,
|
r.share.publishAt,
|
||||||
now > r.publishUntil,
|
r.share.publishUntil,
|
||||||
r.password.isDefined,
|
now > r.share.publishUntil,
|
||||||
r.views,
|
r.share.password.isDefined,
|
||||||
r.lastAccess
|
r.share.views,
|
||||||
|
r.share.lastAccess
|
||||||
)
|
)
|
||||||
|
|
||||||
def convertIn(s: SimpleShareMail): Either[String, ShareMail] =
|
def convertIn(s: SimpleShareMail): Either[String, ShareMail] =
|
||||||
|
@ -59,7 +59,7 @@ object ShareSearchRoutes {
|
|||||||
cfg.maxNoteLength,
|
cfg.maxNoteLength,
|
||||||
searchMode = SearchMode.Normal
|
searchMode = SearchMode.Normal
|
||||||
)
|
)
|
||||||
account = share.asAccount
|
account = share.account
|
||||||
fixQuery = Query.Fix(account, Some(share.query.expr), None)
|
fixQuery = Query.Fix(account, Some(share.query.expr), None)
|
||||||
_ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}")
|
_ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}")
|
||||||
resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery)
|
resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
CREATE TABLE "item_share" (
|
CREATE TABLE "item_share" (
|
||||||
"id" varchar(254) not null primary key,
|
"id" varchar(254) not null primary key,
|
||||||
"cid" varchar(254) not null,
|
"user_id" varchar(254) not null,
|
||||||
"name" varchar(254),
|
"name" varchar(254),
|
||||||
"query" varchar(2000) not null,
|
"query" varchar(2000) not null,
|
||||||
"enabled" boolean not null,
|
"enabled" boolean not null,
|
||||||
@ -9,5 +9,5 @@ CREATE TABLE "item_share" (
|
|||||||
"publish_until" timestamp not null,
|
"publish_until" timestamp not null,
|
||||||
"views" int not null,
|
"views" int not null,
|
||||||
"last_access" timestamp,
|
"last_access" timestamp,
|
||||||
foreign key ("cid") references "collective"("cid") on delete cascade
|
foreign key ("user_id") references "user_"("uid") on delete cascade
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
CREATE TABLE `item_share` (
|
CREATE TABLE `item_share` (
|
||||||
`id` varchar(254) not null primary key,
|
`id` varchar(254) not null primary key,
|
||||||
`cid` varchar(254) not null,
|
`user_id` varchar(254) not null,
|
||||||
`name` varchar(254),
|
`name` varchar(254),
|
||||||
`query` varchar(2000) not null,
|
`query` varchar(2000) not null,
|
||||||
`enabled` boolean not null,
|
`enabled` boolean not null,
|
||||||
@ -9,5 +9,5 @@ CREATE TABLE `item_share` (
|
|||||||
`publish_until` timestamp not null,
|
`publish_until` timestamp not null,
|
||||||
`views` int not null,
|
`views` int not null,
|
||||||
`last_access` timestamp,
|
`last_access` timestamp,
|
||||||
foreign key (`cid`) references `collective`(`cid`) on delete cascade
|
foreign key (`user_id`) references `user_`(`uid`) on delete cascade
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
CREATE TABLE "item_share" (
|
CREATE TABLE "item_share" (
|
||||||
"id" varchar(254) not null primary key,
|
"id" varchar(254) not null primary key,
|
||||||
"cid" varchar(254) not null,
|
"user_id" varchar(254) not null,
|
||||||
"name" varchar(254),
|
"name" varchar(254),
|
||||||
"query" varchar(2000) not null,
|
"query" varchar(2000) not null,
|
||||||
"enabled" boolean not null,
|
"enabled" boolean not null,
|
||||||
@ -9,5 +9,5 @@ CREATE TABLE "item_share" (
|
|||||||
"publish_until" timestamp not null,
|
"publish_until" timestamp not null,
|
||||||
"views" int not null,
|
"views" int not null,
|
||||||
"last_access" timestamp,
|
"last_access" timestamp,
|
||||||
foreign key ("cid") references "collective"("cid") on delete cascade
|
foreign key ("user_id") references "user_"("uid") on delete cascade
|
||||||
)
|
)
|
||||||
|
@ -18,7 +18,7 @@ import doobie.implicits._
|
|||||||
|
|
||||||
final case class RShare(
|
final case class RShare(
|
||||||
id: Ident,
|
id: Ident,
|
||||||
cid: Ident,
|
userId: Ident,
|
||||||
name: Option[String],
|
name: Option[String],
|
||||||
query: ItemQuery,
|
query: ItemQuery,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
@ -35,7 +35,7 @@ object RShare {
|
|||||||
val tableName = "item_share";
|
val tableName = "item_share";
|
||||||
|
|
||||||
val id = Column[Ident]("id", this)
|
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 name = Column[String]("name", this)
|
||||||
val query = Column[ItemQuery]("query", this)
|
val query = Column[ItemQuery]("query", this)
|
||||||
val enabled = Column[Boolean]("enabled", this)
|
val enabled = Column[Boolean]("enabled", this)
|
||||||
@ -48,7 +48,7 @@ object RShare {
|
|||||||
val all: NonEmptyList[Column[_]] =
|
val all: NonEmptyList[Column[_]] =
|
||||||
NonEmptyList.of(
|
NonEmptyList.of(
|
||||||
id,
|
id,
|
||||||
cid,
|
userId,
|
||||||
name,
|
name,
|
||||||
query,
|
query,
|
||||||
enabled,
|
enabled,
|
||||||
@ -67,7 +67,7 @@ object RShare {
|
|||||||
DML.insert(
|
DML.insert(
|
||||||
T,
|
T,
|
||||||
T.all,
|
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] =
|
def incAccess(id: Ident): ConnectionIO[Int] =
|
||||||
@ -83,7 +83,7 @@ object RShare {
|
|||||||
def updateData(r: RShare, removePassword: Boolean): ConnectionIO[Int] =
|
def updateData(r: RShare, removePassword: Boolean): ConnectionIO[Int] =
|
||||||
DML.update(
|
DML.update(
|
||||||
T,
|
T,
|
||||||
T.id === r.id && T.cid === r.cid,
|
T.id === r.id && T.userId === r.userId,
|
||||||
DML.set(
|
DML.set(
|
||||||
T.name.setTo(r.name),
|
T.name.setTo(r.name),
|
||||||
T.query.setTo(r.query),
|
T.query.setTo(r.query),
|
||||||
@ -94,26 +94,41 @@ object RShare {
|
|||||||
else Nil)
|
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(
|
OptionT(
|
||||||
Select(select(T.all), from(T), T.id === id && T.cid === cid).build
|
Select(
|
||||||
.query[RShare]
|
select(s.all, u.all),
|
||||||
|
from(s).innerJoin(u, u.uid === s.userId),
|
||||||
|
s.id === id && u.cid === cid
|
||||||
|
).build
|
||||||
|
.query[(RShare, RUser)]
|
||||||
.option
|
.option
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private def activeCondition(t: Table, id: Ident, current: Timestamp): Condition =
|
private def activeCondition(t: Table, id: Ident, current: Timestamp): Condition =
|
||||||
t.id === id && t.enabled === true && t.publishedUntil > current
|
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(
|
OptionT(
|
||||||
Select(
|
Select(
|
||||||
select(T.all),
|
select(s.all, u.all),
|
||||||
from(T),
|
from(s).innerJoin(u, s.userId === u.uid),
|
||||||
activeCondition(T, id, current)
|
activeCondition(s, id, current)
|
||||||
).build.query[RShare].option
|
).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))
|
OptionT.liftF(Timestamp.current[ConnectionIO]).flatMap(now => findActive(id, now))
|
||||||
|
|
||||||
def findActivePassword(id: Ident): OptionT[ConnectionIO, Option[Password]] =
|
def findActivePassword(id: Ident): OptionT[ConnectionIO, Option[Password]] =
|
||||||
@ -123,13 +138,30 @@ object RShare {
|
|||||||
.option
|
.option
|
||||||
})
|
})
|
||||||
|
|
||||||
def findAllByCollective(cid: Ident): ConnectionIO[List[RShare]] =
|
def findAllByCollective(
|
||||||
Select(select(T.all), from(T), T.cid === cid)
|
cid: Ident,
|
||||||
.orderBy(T.publishedAt.desc)
|
ownerLogin: Option[Ident],
|
||||||
.build
|
q: Option[String]
|
||||||
.query[RShare]
|
): ConnectionIO[List[(RShare, RUser)]] = {
|
||||||
.to[List]
|
val s = RShare.as("s")
|
||||||
|
val u = RUser.as("u")
|
||||||
|
|
||||||
def deleteByIdAndCid(id: Ident, cid: Ident): ConnectionIO[Int] =
|
val ownerQ = ownerLogin.map(name => u.login === name)
|
||||||
DML.delete(T, T.id === id && T.cid === cid)
|
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)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,13 @@ case class RUser(
|
|||||||
loginCount: Int,
|
loginCount: Int,
|
||||||
lastLogin: Option[Timestamp],
|
lastLogin: Option[Timestamp],
|
||||||
created: Timestamp
|
created: Timestamp
|
||||||
) {}
|
) {
|
||||||
|
def accountId: AccountId =
|
||||||
|
AccountId(cid, login)
|
||||||
|
|
||||||
|
def idRef: IdRef =
|
||||||
|
IdRef(uid, login.id)
|
||||||
|
}
|
||||||
|
|
||||||
object RUser {
|
object RUser {
|
||||||
|
|
||||||
|
23
modules/webapp/package-lock.json
generated
23
modules/webapp/package-lock.json
generated
@ -153,20 +153,8 @@
|
|||||||
"electron-to-chromium": "^1.3.719",
|
"electron-to-chromium": "^1.3.719",
|
||||||
"escalade": "^3.1.1",
|
"escalade": "^3.1.1",
|
||||||
"node-releases": "^1.1.71"
|
"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": {
|
"colorette": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||||
@ -228,11 +216,6 @@
|
|||||||
"node-releases": "^1.1.71"
|
"node-releases": "^1.1.71"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"colorette": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||||
@ -272,9 +255,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001208",
|
"version": "1.0.30001271",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz",
|
||||||
"integrity": "sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA=="
|
"integrity": "sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA=="
|
||||||
},
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
|
@ -2228,10 +2228,19 @@ disableOtp flags otp receive =
|
|||||||
--- Share
|
--- Share
|
||||||
|
|
||||||
|
|
||||||
getShares : Flags -> (Result Http.Error ShareList -> msg) -> Cmd msg
|
getShares : Flags -> String -> Bool -> (Result Http.Error ShareList -> msg) -> Cmd msg
|
||||||
getShares flags receive =
|
getShares flags query owning receive =
|
||||||
Http2.authGet
|
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
|
, account = getAccount flags
|
||||||
, expect = Http.expectJson receive Api.Model.ShareList.decoder
|
, expect = Http.expectJson receive Api.Model.ShareList.decoder
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,8 @@ type alias Model =
|
|||||||
, loading : Bool
|
, loading : Bool
|
||||||
, formError : FormError
|
, formError : FormError
|
||||||
, deleteConfirm : DeleteConfirm
|
, deleteConfirm : DeleteConfirm
|
||||||
|
, query : String
|
||||||
|
, owningOnly : Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -75,6 +77,8 @@ init flags =
|
|||||||
, loading = False
|
, loading = False
|
||||||
, formError = FormErrorNone
|
, formError = FormErrorNone
|
||||||
, deleteConfirm = DeleteConfirmOff
|
, deleteConfirm = DeleteConfirmOff
|
||||||
|
, query = ""
|
||||||
|
, owningOnly = True
|
||||||
}
|
}
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ Cmd.map FormMsg fc
|
[ Cmd.map FormMsg fc
|
||||||
@ -90,6 +94,8 @@ type Msg
|
|||||||
| MailMsg Comp.ShareMail.Msg
|
| MailMsg Comp.ShareMail.Msg
|
||||||
| InitNewShare
|
| InitNewShare
|
||||||
| SetViewMode ViewMode
|
| SetViewMode ViewMode
|
||||||
|
| SetQuery String
|
||||||
|
| ToggleOwningOnly
|
||||||
| Submit
|
| Submit
|
||||||
| RequestDelete
|
| RequestDelete
|
||||||
| CancelDelete
|
| CancelDelete
|
||||||
@ -126,7 +132,7 @@ update texts flags msg model =
|
|||||||
SetViewMode vm ->
|
SetViewMode vm ->
|
||||||
( { model | viewMode = vm, formError = FormErrorNone }
|
( { model | viewMode = vm, formError = FormErrorNone }
|
||||||
, if vm == Table then
|
, if vm == Table then
|
||||||
Api.getShares flags LoadSharesResp
|
Api.getShares flags model.query model.owningOnly LoadSharesResp
|
||||||
|
|
||||||
else
|
else
|
||||||
Cmd.none
|
Cmd.none
|
||||||
@ -165,7 +171,10 @@ update texts flags msg model =
|
|||||||
)
|
)
|
||||||
|
|
||||||
LoadShares ->
|
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) ->
|
LoadSharesResp (Ok list) ->
|
||||||
( { model | loading = False, shares = list.items, formError = FormErrorNone }
|
( { model | loading = False, shares = list.items, formError = FormErrorNone }
|
||||||
@ -231,6 +240,26 @@ update texts flags msg model =
|
|||||||
in
|
in
|
||||||
( { model | mailModel = mm }, Cmd.map MailMsg mc, Sub.none )
|
( { 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 -> ShareDetail -> Flags -> Model -> ( Model, Cmd Msg, Sub Msg )
|
||||||
setShare texts share flags model =
|
setShare texts share flags model =
|
||||||
@ -271,7 +300,19 @@ viewTable texts model =
|
|||||||
div [ class "flex flex-col" ]
|
div [ class "flex flex-col" ]
|
||||||
[ MB.view
|
[ MB.view
|
||||||
{ start =
|
{ 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 =
|
, end =
|
||||||
[ MB.PrimaryButton
|
[ MB.PrimaryButton
|
||||||
{ tagger = InitNewShare
|
{ tagger = InitNewShare
|
||||||
@ -295,6 +336,11 @@ viewForm texts settings flags model =
|
|||||||
let
|
let
|
||||||
newShare =
|
newShare =
|
||||||
model.formModel.share.id == ""
|
model.formModel.share.id == ""
|
||||||
|
|
||||||
|
isOwner =
|
||||||
|
Maybe.map .user flags.account
|
||||||
|
|> Maybe.map ((==) model.formModel.share.owner.name)
|
||||||
|
|> Maybe.withDefault False
|
||||||
in
|
in
|
||||||
div []
|
div []
|
||||||
[ Html.form []
|
[ Html.form []
|
||||||
@ -305,20 +351,34 @@ viewForm texts settings flags model =
|
|||||||
|
|
||||||
else
|
else
|
||||||
h1 [ class S.header2 ]
|
h1 [ class S.header2 ]
|
||||||
[ text <| Maybe.withDefault texts.noName model.formModel.share.name
|
[ div [ class "flex flex-row items-center" ]
|
||||||
, div [ class "opacity-50 text-sm" ]
|
[ div
|
||||||
[ text "Id: "
|
[ class "flex text-sm opacity-75 label mr-3"
|
||||||
, text model.formModel.share.id
|
, 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
|
, MB.view
|
||||||
{ start =
|
{ start =
|
||||||
[ MB.PrimaryButton
|
[ MB.CustomElement <|
|
||||||
{ tagger = Submit
|
B.primaryButton
|
||||||
, title = "Submit this form"
|
{ handler = onClick Submit
|
||||||
, icon = Just "fa fa-save"
|
, title = "Submit this form"
|
||||||
, label = texts.basics.submit
|
, icon = "fa fa-save"
|
||||||
}
|
, label = texts.basics.submit
|
||||||
|
, disabled = not isOwner
|
||||||
|
, attrs = [ href "#" ]
|
||||||
|
}
|
||||||
, MB.SecondaryButton
|
, MB.SecondaryButton
|
||||||
{ tagger = SetViewMode Table
|
{ tagger = SetViewMode Table
|
||||||
, title = texts.basics.backToList
|
, title = texts.basics.backToList
|
||||||
@ -360,7 +420,15 @@ viewForm texts settings flags model =
|
|||||||
FormErrorSubmit m ->
|
FormErrorSubmit m ->
|
||||||
text 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
|
, B.loadingDimmer
|
||||||
{ active = model.loading
|
{ active = model.loading
|
||||||
, label = texts.basics.loading
|
, label = texts.basics.loading
|
||||||
|
@ -56,7 +56,10 @@ view texts shares =
|
|||||||
, th [ class "text-center" ]
|
, th [ class "text-center" ]
|
||||||
[ text texts.active
|
[ 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
|
[ text texts.publishUntil
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -88,6 +91,9 @@ renderShareLine texts share =
|
|||||||
else
|
else
|
||||||
i [ class "fa fa-check" ] []
|
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" ]
|
, td [ class "hidden sm:table-cell text-center" ]
|
||||||
[ texts.formatDateTime share.publishUntil |> text
|
[ texts.formatDateTime share.publishUntil |> text
|
||||||
]
|
]
|
||||||
|
@ -39,6 +39,8 @@ type alias Texts =
|
|||||||
, noName : String
|
, noName : String
|
||||||
, shareInformation : String
|
, shareInformation : String
|
||||||
, sendMail : String
|
, sendMail : String
|
||||||
|
, notOwnerInfo : String
|
||||||
|
, showOwningSharesOnly : String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -62,6 +64,8 @@ gb =
|
|||||||
, noName = "No Name"
|
, noName = "No Name"
|
||||||
, shareInformation = "Share Information"
|
, shareInformation = "Share Information"
|
||||||
, sendMail = "Send via E-Mail"
|
, 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"
|
, noName = "Ohne Name"
|
||||||
, shareInformation = "Informationen zur Freigabe"
|
, shareInformation = "Informationen zur Freigabe"
|
||||||
, sendMail = "Per E-Mail versenden"
|
, sendMail = "Per E-Mail versenden"
|
||||||
|
, notOwnerInfo = "Nur der Benutzer, der diese Freigabe erstellt hat, kann diese auch ändern."
|
||||||
|
, showOwningSharesOnly = "Nur meine Freigaben anzeigen"
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ type alias Texts =
|
|||||||
, formatDateTime : Int -> String
|
, formatDateTime : Int -> String
|
||||||
, active : String
|
, active : String
|
||||||
, publishUntil : String
|
, publishUntil : String
|
||||||
|
, user : String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ gb =
|
|||||||
, formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.English
|
, formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.English
|
||||||
, active = "Active"
|
, active = "Active"
|
||||||
, publishUntil = "Publish Until"
|
, publishUntil = "Publish Until"
|
||||||
|
, user = "User"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -39,4 +41,5 @@ de =
|
|||||||
, formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.German
|
, formatDateTime = DF.formatDateTimeLong Messages.UiLanguage.German
|
||||||
, active = "Aktiv"
|
, active = "Aktiv"
|
||||||
, publishUntil = "Publiziert bis"
|
, publishUntil = "Publiziert bis"
|
||||||
|
, user = "Benutzer"
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user