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:
eikek 2021-10-23 23:29:36 +02:00
parent 9009ebcb39
commit 2ac0b84e52
17 changed files with 268 additions and 110 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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] =

View File

@ -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)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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)))
}
}

View File

@ -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 {

View File

@ -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",

View File

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

View File

@ -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

View File

@ -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
]

View File

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

View File

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